Compare commits
34 Commits
watch-sock
...
feat/symbo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1dde9ee92 | ||
|
|
005fc9ac21 | ||
|
|
418e190fa0 | ||
|
|
d42d9d5b29 | ||
|
|
9c89d20e76 | ||
|
|
8bafaa8171 | ||
|
|
92dae18fd4 | ||
|
|
e554660c06 | ||
|
|
342d82a7aa | ||
|
|
3dab2ead93 | ||
|
|
bd238fe59c | ||
|
|
8b3e3a9595 | ||
|
|
2bbdf6c02b | ||
|
|
9c9e6b0bcb | ||
|
|
bb0b160a28 | ||
|
|
ada202e489 | ||
|
|
836f415e50 | ||
|
|
1627b96d2b | ||
|
|
a40c0b3bf8 | ||
|
|
d253c1f099 | ||
|
|
0c107fc2c5 | ||
|
|
018bad4ea8 | ||
|
|
e239b0d32c | ||
|
|
aae246e465 | ||
|
|
d04bc6f774 | ||
|
|
8d4ad26834 | ||
|
|
b6886d222b | ||
|
|
c14b8de18f | ||
|
|
3838bedcbf | ||
|
|
31d9c609b6 | ||
|
|
a1b6c0632d | ||
|
|
1dd94e54c0 | ||
|
|
e24eff6c16 | ||
|
|
c697ffcf91 |
@@ -100,7 +100,7 @@ SortIncludes: true
|
||||
SortUsingDeclarations: Never
|
||||
IncludeBlocks: Regroup
|
||||
IncludeCategories:
|
||||
- Regex: '^["<](spdlog|toml\+\+|coraing|cpptrace|flatbuffers)/'
|
||||
- Regex: '^["<](spdlog|toml\+\+|coraing|cpptrace|flatbuffers|kota)/'
|
||||
Priority: 30
|
||||
SortPriority: 31
|
||||
|
||||
|
||||
50
.clang-tidy
Normal file
50
.clang-tidy
Normal file
@@ -0,0 +1,50 @@
|
||||
---
|
||||
Checks: >
|
||||
-*,
|
||||
bugprone-*,
|
||||
modernize-*,
|
||||
performance-*,
|
||||
readability-*,
|
||||
-modernize-use-trailing-return-type,
|
||||
-readability-magic-numbers,
|
||||
-readability-else-after-return,
|
||||
-readability-braces-around-statements,
|
||||
-readability-avoid-const-params-in-decls,
|
||||
-readability-named-parameter,
|
||||
-readability-implicit-bool-conversion,
|
||||
-readability-use-anyofallof,
|
||||
-bugprone-easily-swappable-parameters,
|
||||
-bugprone-exception-escape,
|
||||
-bugprone-narrowing-conversions,
|
||||
-modernize-use-nodiscard,
|
||||
|
||||
WarningsAsErrors: ""
|
||||
|
||||
HeaderFilterRegex: "(src|tests)/.*"
|
||||
|
||||
CheckOptions:
|
||||
# Naming conventions matching project style
|
||||
- key: readability-identifier-naming.ClassCase
|
||||
value: CamelCase
|
||||
- key: readability-identifier-naming.StructCase
|
||||
value: CamelCase
|
||||
- key: readability-identifier-naming.EnumCase
|
||||
value: CamelCase
|
||||
- key: readability-identifier-naming.EnumConstantCase
|
||||
value: CamelCase
|
||||
- key: readability-identifier-naming.TemplateParameterCase
|
||||
value: CamelCase
|
||||
- key: readability-identifier-naming.TypeAliasCase
|
||||
value: CamelCase
|
||||
- key: readability-identifier-naming.FunctionCase
|
||||
value: lower_case
|
||||
- key: readability-identifier-naming.MethodCase
|
||||
value: lower_case
|
||||
- key: readability-identifier-naming.VariableCase
|
||||
value: lower_case
|
||||
- key: readability-identifier-naming.ParameterCase
|
||||
value: lower_case
|
||||
- key: readability-identifier-naming.MemberCase
|
||||
value: lower_case
|
||||
- key: readability-identifier-naming.NamespaceCase
|
||||
value: lower_case
|
||||
268
.claude/CLAUDE.md
Normal file
268
.claude/CLAUDE.md
Normal file
@@ -0,0 +1,268 @@
|
||||
# clice — Project Guide
|
||||
|
||||
## Project Overview
|
||||
|
||||
clice is a next-generation C++ language server (LSP) built on LLVM/Clang, targeting modern C++ (C++20/23). It uses a multi-process architecture with a master server coordinating stateless and stateful workers.
|
||||
|
||||
## Core Correction Patterns — Lessons from Past Interactions
|
||||
|
||||
The following patterns were extracted from extensive real-world collaboration. These are recurring mistakes that MUST be avoided. Read them carefully — they represent hard-won lessons, not hypothetical concerns.
|
||||
|
||||
### Pattern 1: Misjudging Real-World Priorities
|
||||
|
||||
AI tends to optimize whatever metric looks most impressive, rather than what actually matters in the user's real scenario.
|
||||
|
||||
**Example**: During performance optimization, the AI proudly reported "hot cache is 4-7x faster!" — but the function in question runs at LSP server startup, which is ALWAYS a cold start. Optimizing hot cache was completely meaningless.
|
||||
|
||||
**Rule**: Before optimizing or analyzing anything, first understand the REAL usage scenario. Ask yourself: "When does this code actually run? What does the user actually experience?" Do not chase metrics that look good on paper but are irrelevant in practice.
|
||||
|
||||
### Pattern 2: Pushing Without Local Verification
|
||||
|
||||
The most common and most damaging pattern. AI proposes a fix, pushes it immediately, CI fails, then another fix, push, fail again — wasting CI cycles and the user's time.
|
||||
|
||||
**Rule**: NEVER push code that you haven't verified locally. Before every push:
|
||||
|
||||
- Build locally with the same configuration CI uses.
|
||||
- Run the relevant tests locally and confirm they pass.
|
||||
- If you cannot reproduce the CI environment locally, say so — do not just "try and see."
|
||||
- "It compiles" is NOT sufficient. Tests must pass.
|
||||
|
||||
### Pattern 3: Superficial Refactoring
|
||||
|
||||
When asked to refactor, AI tends to do mechanical code movement (copy functions from A to B) without understanding the deeper design intent (ownership, responsibility boundaries, API cleanliness).
|
||||
|
||||
**Example**: When splitting `MasterServer` into `Workspace` and `Session`, the AI moved functions but kept ugly APIs like `f(path_id, sessions_map)` instead of the clean `f(Session&)` that the refactoring was meant to achieve.
|
||||
|
||||
**Rule**: When refactoring, understand the WHY. Ask: "What design problem is this refactoring solving?" If you're just moving code around without improving the abstractions, you're not refactoring — you're rearranging deck chairs.
|
||||
|
||||
### Pattern 4: Fixing Only the Immediate Instance, Not the Pattern
|
||||
|
||||
When given a cleanup instruction, AI applies it to the single file or function currently being discussed, ignoring all other occurrences in the project.
|
||||
|
||||
**Example**: User says "remove decorative `===` comment separators." AI removes them from `workspace.cpp` only. User has to say: "The other files too!"
|
||||
|
||||
**Rule**: When given a cleanup or style instruction, apply it project-wide. Use `Grep` to find ALL occurrences and fix them all in one pass. Think: "Where else does this pattern appear?"
|
||||
|
||||
### Pattern 5: Never Skip, Disable, or Work Around Failing Tests
|
||||
|
||||
When stuck on a difficult bug (especially flaky CI, race conditions, platform-specific issues), AI may propose marking tests as `continue-on-error`, skipping them, or adding `expected-failure` annotations to make CI green.
|
||||
|
||||
**Rule**: This is ABSOLUTELY FORBIDDEN. If a test fails, fix the root cause. There are ZERO exceptions. Skipping a test to make CI green is not "fixing" — it is hiding a bug. If you ever find yourself thinking "maybe we should just skip this test," stop and reconsider your approach entirely.
|
||||
|
||||
### Pattern 6: Excessive Confirmation Seeking vs. Premature Execution
|
||||
|
||||
AI oscillates between two extremes: asking "should I do X?" for every trivial decision, or silently executing major changes without confirmation.
|
||||
|
||||
**Rule**: Calibrate based on reversibility and impact:
|
||||
|
||||
- **Small, reversible changes** (formatting, renaming a local variable, adding a test): just do it.
|
||||
- **Architecture decisions, API changes, large refactors**: propose the plan first, wait for confirmation.
|
||||
- **Pushing to remote, creating PRs, modifying CI**: always confirm.
|
||||
- When the user says "go ahead" or "do it," execute fully without asking again mid-way.
|
||||
|
||||
## Code Reuse & Understanding Before Implementation
|
||||
|
||||
**This is the single most important rule in this project.** Before writing ANY new code, you MUST thoroughly read and understand the existing codebase first. This project has a rich set of utilities, abstractions, and patterns already in place — duplicating them wastes effort and creates maintenance burden.
|
||||
|
||||
Concrete requirements:
|
||||
|
||||
1. **Read before you write.** Before implementing a feature or fix, explore the relevant modules in `src/`. Search for existing helpers, utilities, and patterns that solve the same or similar problems. Use `Grep`, `Glob`, and `Agent` tools to investigate thoroughly — do not assume something doesn't exist just because you haven't seen it yet.
|
||||
|
||||
2. **Reuse existing infrastructure.** This project already has:
|
||||
- A `Lexer` class (`src/syntax/lexer.h`) — do not hand-write token scanning logic.
|
||||
- A `PositionMapper` for source location conversion — do not reimplement offset-to-line/column math.
|
||||
- `CompilationUnitRef` methods (`decompose_location`, `decompose_range`, `file_path`, `directives`, etc.) — use them instead of raw Clang APIs.
|
||||
- `SemanticVisitor` for AST traversal — extend it, do not write custom recursive AST walkers.
|
||||
- `Tester` framework for unit tests with VFS, annotation support, and multi-phase compilation — use it, do not create ad-hoc test setups.
|
||||
- Utility functions in `src/support/` — check there before writing new helpers.
|
||||
|
||||
3. **Follow established patterns.** When adding a new feature (e.g., a new LSP request handler), look at how 2-3 existing features of the same kind are implemented. Match their structure: same file organization, same function signatures, same error handling patterns. If every other feature in `src/feature/` follows a certain pattern, yours should too.
|
||||
|
||||
4. **Do not reinvent what the project already has.** If you find yourself writing a helper function that feels generic (string manipulation, path handling, JSON serialization, source range conversion), STOP and search the codebase first. There is a high probability it already exists. Creating duplicates leads to inconsistencies and bugs when one copy gets updated but the other doesn't.
|
||||
|
||||
5. **When in doubt, ask.** If you're unsure whether an existing utility covers your use case or whether to extend an existing abstraction vs. create a new one, ask the user rather than guessing.
|
||||
|
||||
## Source Layout
|
||||
|
||||
- `src/server/` — LSP server core: master server, compiler, indexer, stateful/stateless workers
|
||||
- `src/feature/` — LSP feature implementations: hover, completion, document links, semantic tokens, etc.
|
||||
- `src/compile/` — Compilation orchestration: compilation unit, directives, diagnostics
|
||||
- `src/index/` — Symbol indexing: TUIndex, ProjectIndex, MergedIndex, include graph
|
||||
- `src/semantic/` — Semantic analysis: symbol kinds, relations, AST visitor, template resolver
|
||||
- `src/syntax/` — Lexer, scanner, token types, dependency graph
|
||||
- `src/command/` — CLI parsing, compilation database, toolchain detection
|
||||
- `src/support/` — Utilities: logging, filesystem, JSON, string helpers
|
||||
|
||||
## Build System
|
||||
|
||||
- Uses **pixi** for environment management and **CMake + Ninja** for building.
|
||||
- Two build types: `Debug` and `RelWithDebInfo` (default).
|
||||
- Build output goes to `build/[type]/`.
|
||||
- See `/build`, `/test`, `/format` commands for common operations.
|
||||
|
||||
## Commit Message Format
|
||||
|
||||
Use **conventional commits** — enforced by CI:
|
||||
|
||||
```
|
||||
<type>(<scope>): <short description>
|
||||
```
|
||||
|
||||
- **Types**: `feat`, `fix`, `refactor`, `chore`, `docs`, `ci`, `test`
|
||||
- **Scopes**: match `src/` subdirectories or feature names, e.g. `completion`, `server`, `index`, `tests`, `document links`
|
||||
- Keep the subject line under 70 characters.
|
||||
|
||||
## Tests
|
||||
|
||||
Three types of tests, all must pass before committing:
|
||||
|
||||
- **Unit tests** (`tests/unit/`): C++ tests using the project's own test framework. Test names should be at most 4 words.
|
||||
- **Integration tests** (`tests/integration/`): Python pytest tests that start a real clice server and communicate via LSP.
|
||||
- **Smoke tests** (`tests/smoke/`): Replay recorded LSP sessions via `tests/replay.py`.
|
||||
|
||||
### Integration Test Style
|
||||
|
||||
- Keep tests concise. Do NOT write large comment blocks explaining the test layout or expected behavior.
|
||||
- Use descriptive test function names and short inline comments only where logic is non-obvious.
|
||||
|
||||
## Pre-PR Review
|
||||
|
||||
Before opening a PR, launch **3 parallel subagents** to review the diff independently:
|
||||
|
||||
1. **Correctness reviewer**: Check for logic errors, edge cases, undefined behavior, and off-by-one mistakes.
|
||||
2. **Style reviewer**: Verify the code follows this project's naming conventions, coding style, and CLAUDE.md rules.
|
||||
3. **Test reviewer**: Confirm test coverage is adequate — new functionality has tests, edge cases are covered, and no existing tests were broken or weakened.
|
||||
|
||||
Each agent should read the full diff (`git diff main...HEAD`) and report issues. Fix all reported issues before opening the PR.
|
||||
|
||||
## Pre-commit Checklist
|
||||
|
||||
Before committing code, you MUST:
|
||||
|
||||
1. **Run `pixi run format`** to format all source files.
|
||||
2. **Pass all three types of tests:**
|
||||
- Unit tests: `pixi run unit-test [type]`
|
||||
- Integration tests: `pixi run integration-test [type]`
|
||||
- Smoke tests: `pixi run smoke-test [type]`
|
||||
3. **All test failures must be fixed before committing.** This is a HARD REQUIREMENT with NO exceptions:
|
||||
- If a test fails, it MUST be fixed before you commit. Do NOT commit with known failures.
|
||||
- Do NOT skip, disable, or mark tests as expected-failure to work around breakage.
|
||||
- Do NOT argue "this test was already broken before my changes" — if it fails on your branch, it is YOUR responsibility to fix it before committing. The main branch CI is green; any failure on your branch is caused by your changes, period.
|
||||
- Do NOT defer fixing to a follow-up PR. Fix it NOW, in this branch, before committing.
|
||||
|
||||
---
|
||||
|
||||
## C++ Coding Style
|
||||
|
||||
### Template & Type Traits
|
||||
|
||||
- Do NOT blindly add `std::remove_cvref_t` on every template parameter. Understand C++ template argument deduction rules:
|
||||
- `template<typename T> void f(T x)` — `T` is always deduced as a non-reference, non-cv-qualified type. No need for `remove_cvref_t`.
|
||||
- `template<typename T> void f(T& x)` — `T` is deduced as the referred-to type (possibly cv-qualified, but never a reference). No need for `remove_cvref_t` to strip references.
|
||||
- `template<typename T> void f(const T& x)` — `T` is deduced as a non-const, non-reference type. No need for `remove_cvref_t`.
|
||||
- `template<typename T> void f(T&& x)` — **forwarding reference**: `T` CAN be deduced as an lvalue reference (e.g., `int&`). This is the ONLY case where `std::remove_cvref_t<T>` is needed to get the bare type.
|
||||
- Class template parameters and return types are also never deduced as references; don't add `remove_cvref_t` on them either.
|
||||
|
||||
### Type Traits & Concepts (C++20/23)
|
||||
|
||||
- This project targets C++20/23. Use variable templates directly for type traits — do NOT use the old pattern of wrapping a class template static member in a variable template. Prefer:
|
||||
|
||||
```cpp
|
||||
// Good: directly specialize a variable template
|
||||
template<typename T>
|
||||
inline constexpr bool is_my_type_v = false;
|
||||
|
||||
template<>
|
||||
inline constexpr bool is_my_type_v<MyType> = true;
|
||||
```
|
||||
|
||||
```cpp
|
||||
// Bad: unnecessary class template wrapper
|
||||
template<typename T>
|
||||
struct is_my_type : std::false_type {};
|
||||
|
||||
template<>
|
||||
struct is_my_type<MyType> : std::true_type {};
|
||||
|
||||
template<typename T>
|
||||
inline constexpr bool is_my_type_v = is_my_type<T>::value;
|
||||
```
|
||||
|
||||
- When defining a concept that checks a type trait, do NOT add `std::remove_cvref_t` unless you specifically intend the concept to see through references/cv-qualifiers. If the concept is meant for a bare type, just use `T` directly — the caller is responsible for passing the right type.
|
||||
|
||||
```cpp
|
||||
// Good
|
||||
template<typename T>
|
||||
concept MyTrait = is_my_type_v<T>;
|
||||
|
||||
// Bad: unnecessary remove_cvref_t
|
||||
template<typename T>
|
||||
concept MyTrait = is_my_type_v<std::remove_cvref_t<T>>;
|
||||
```
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
- **Variables, member fields, function names**: `snake_case`. Class member fields do NOT use any special suffix/prefix (no trailing `_`, no `m_` prefix).
|
||||
- **Class names, template parameter names, enum names**: `PascalCase`. Exception: some class names also use `snake_case` — follow the existing style in the project.
|
||||
- **Enum values**: `PascalCase`.
|
||||
|
||||
### String Literals
|
||||
|
||||
- Prefer C++11 raw string literals `R"(...)"` over escaped strings. Avoid `\"`, `\\`, `\n` in string literals when a raw literal is cleaner.
|
||||
|
||||
### Error Handling
|
||||
|
||||
- **Prefer `if` with init-statements to tightly scope error variables**, but avoid them when they compromise code readability or flatten control flow.
|
||||
- **Omit redundant conditions:** If the error type provides an `operator bool` or evaluates implicitly (e.g., standard error codes, custom error wrappers), omit the redundant condition check.
|
||||
- **Avoid forced `else` branches:** If scoping the variable inside the `if` requires you to introduce an `else` block for the success path (especially when returning early on error), declare the variable in the local scope instead to keep the control flow flat.
|
||||
|
||||
```cpp
|
||||
// Good: Omit redundant condition when the type has operator bool
|
||||
if (auto err = foo()) {
|
||||
/* handle error */
|
||||
}
|
||||
|
||||
// Bad: Redundant condition check
|
||||
if (auto err = foo(); err) {
|
||||
/* handle error */
|
||||
}
|
||||
|
||||
// Good: Use init-statement when a custom condition is required,
|
||||
// AND the variable isn't needed outside the if-statement
|
||||
if (auto result = foo(); !result.has_value()) {
|
||||
/* handle error */
|
||||
}
|
||||
|
||||
// --- Scope and Control Flow Considerations ---
|
||||
|
||||
// Bad: Using init-statement forces an 'else' block because 'result'
|
||||
// goes out of scope, leading to nested/redundant code.
|
||||
if (auto result = get_data(); !result.has_value()) {
|
||||
return result.error();
|
||||
} else {
|
||||
process(result.value()); // Success path is forced into a nested block
|
||||
}
|
||||
|
||||
// Good: Declare as a regular local variable to allow early exit
|
||||
// and keep the success path un-nested (flat control flow).
|
||||
auto result = get_data();
|
||||
if (!result.has_value()) {
|
||||
return result.error();
|
||||
}
|
||||
process(result.value());
|
||||
```
|
||||
|
||||
### Style
|
||||
|
||||
- Prefer `[[maybe_unused]]` over `(void)` for intentionally unused variables or parameters.
|
||||
|
||||
### Modern C++ Usage
|
||||
|
||||
- Use C++20/23 APIs whenever possible. Do NOT use `<iostream>` facilities (`std::cout`, `std::cin`, `std::cerr`, etc.). Also do NOT use C-style I/O (`printf`, `fprintf`, etc.).
|
||||
- Prefer `std::ranges` / `std::views` APIs over raw loops and traditional `<algorithm>` calls.
|
||||
- If the project depends on LLVM, prefer LLVM's efficient data structures (e.g., `llvm::SmallVector`, `llvm::DenseMap`, `llvm::StringMap`, `llvm::StringRef`) over their `std` counterparts when appropriate.
|
||||
|
||||
### Parameter Passing Preferences
|
||||
|
||||
- For string parameters, prefer `llvm::StringRef` > `std::string_view` > `const std::string&`.
|
||||
- For array/span parameters, prefer `llvm::ArrayRef` > `std::span` > `const std::vector&`.
|
||||
15
.claude/commands/build.md
Normal file
15
.claude/commands/build.md
Normal file
@@ -0,0 +1,15 @@
|
||||
Build the project. Accepts an optional argument for build type: `Debug` or `RelWithDebInfo` (default).
|
||||
|
||||
Available build commands:
|
||||
|
||||
- CMake configure only: `pixi run cmake-config [type]`
|
||||
- CMake build only (skip configure): `pixi run cmake-build [type]`
|
||||
- Full build (configure + build): `pixi run build [type]`
|
||||
- Build a specific target: `pixi run cmake-build [type]` then `cmake --build build/[type] --target [target]`
|
||||
|
||||
Common targets: `clice`, `unit_tests`
|
||||
|
||||
Example usage:
|
||||
|
||||
- `/build` — full build RelWithDebInfo
|
||||
- `/build Debug` — full build Debug
|
||||
5
.claude/commands/format.md
Normal file
5
.claude/commands/format.md
Normal file
@@ -0,0 +1,5 @@
|
||||
Format all project source files.
|
||||
|
||||
Run: `pixi run format`
|
||||
|
||||
Formats C++, Python, Lua, JS/TS, Markdown, JSON, TOML, and YAML files.
|
||||
19
.claude/commands/test.md
Normal file
19
.claude/commands/test.md
Normal file
@@ -0,0 +1,19 @@
|
||||
Run tests. Accepts an optional argument for build type: `Debug` or `RelWithDebInfo` (default).
|
||||
|
||||
Available test commands:
|
||||
|
||||
- Unit tests: `pixi run unit-test [type]`
|
||||
- Integration tests: `pixi run integration-test [type]`
|
||||
- Smoke tests: `pixi run smoke-test [type]`
|
||||
- All tests (unit + integration): `pixi run test [type]`
|
||||
|
||||
Filtering specific tests:
|
||||
|
||||
- Unit tests: `pixi run unit-test [type] --test-filter=SuiteName.CaseName`
|
||||
- Integration tests: `pixi run pytest tests/integration -k "test_name" --executable=./build/[type]/bin/clice`
|
||||
- Smoke tests: `pixi run python tests/replay.py tests/smoke/specific.jsonl --clice=./build/[type]/bin/clice`
|
||||
|
||||
Example usage:
|
||||
|
||||
- `/test` — run all tests (RelWithDebInfo)
|
||||
- `/test Debug` — run all tests (Debug)
|
||||
@@ -1,2 +1,7 @@
|
||||
chat:
|
||||
auto_reply: false
|
||||
reviews:
|
||||
auto_review:
|
||||
enabled: true
|
||||
summary:
|
||||
enabled: false
|
||||
|
||||
3
.github/workflows/benchmark.yml
vendored
3
.github/workflows/benchmark.yml
vendored
@@ -1,8 +1,7 @@
|
||||
name: benchmark
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
benchmark:
|
||||
|
||||
44
.github/workflows/main.yml
vendored
44
.github/workflows/main.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
clice: ${{ steps.filter.outputs.clice }}
|
||||
vscode: ${{ steps.filter.outputs.vscode }}
|
||||
cmake: ${{ steps.filter.outputs.cmake }}
|
||||
xmake: ${{ steps.filter.outputs.xmake }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dorny/paths-filter@v3
|
||||
@@ -46,13 +46,31 @@ jobs:
|
||||
- 'tests/**'
|
||||
- 'config/**'
|
||||
- '.github/workflows/test-cmake.yml'
|
||||
xmake:
|
||||
- 'xmake.lua'
|
||||
- 'src/**'
|
||||
- 'include/**'
|
||||
- 'tests/**'
|
||||
- 'config/**'
|
||||
- '.github/workflows/test-xmake.yml'
|
||||
|
||||
conventional-commit:
|
||||
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check conventional commit format
|
||||
env:
|
||||
IS_PR: ${{ github.event_name == 'pull_request' }}
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
COMMIT_MSG: ${{ github.event.head_commit.message }}
|
||||
run: |
|
||||
pattern='^(feat|fix|refactor|chore|build|ci|docs|test|perf|style|revert)(\(.+\))?: .+'
|
||||
if [[ "$IS_PR" == "true" ]]; then
|
||||
subject="$PR_TITLE"
|
||||
label="PR title"
|
||||
else
|
||||
subject=$(echo "$COMMIT_MSG" | head -n1)
|
||||
label="Commit message"
|
||||
fi
|
||||
if [[ ! "$subject" =~ $pattern ]]; then
|
||||
echo "::error::$label must follow conventional commit format: type(scope)?: description"
|
||||
echo " Valid types: feat, fix, refactor, chore, build, ci, docs, test, perf, style, revert"
|
||||
echo " Got: '$subject'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
format:
|
||||
needs: changes
|
||||
@@ -82,11 +100,6 @@ jobs:
|
||||
if: ${{ needs.changes.outputs.cmake == 'true' }}
|
||||
uses: ./.github/workflows/test-cmake.yml
|
||||
|
||||
# xmake:
|
||||
# needs: changes
|
||||
# if: ${{ needs.changes.outputs.xmake == 'true' }}
|
||||
# uses: ./.github/workflows/test-xmake.yml
|
||||
|
||||
release-clice:
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -104,16 +117,17 @@ jobs:
|
||||
checks-passed:
|
||||
if: ${{ always() && !startsWith(github.ref, 'refs/tags/') }}
|
||||
needs:
|
||||
- conventional-commit
|
||||
- format
|
||||
- deploy
|
||||
# - clice
|
||||
- vscode
|
||||
- cmake
|
||||
# - xmake
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check results
|
||||
uses: re-actors/alls-green@release/v1
|
||||
with:
|
||||
allowed-skips: format,deploy,clice,vscode,cmake,xmake
|
||||
allowed-skips: conventional-commit,format,deploy,clice,vscode,cmake
|
||||
jobs: ${{ toJSON(needs) }}
|
||||
|
||||
26
.github/workflows/publish-clice.yml
vendored
26
.github/workflows/publish-clice.yml
vendored
@@ -29,32 +29,18 @@ jobs:
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup xmake
|
||||
uses: xmake-io/github-action-setup-xmake@v1
|
||||
with:
|
||||
xmake-version: 3.0.5
|
||||
actions-cache-folder: ".xmake-cache"
|
||||
actions-cache-key: ${{ matrix.os }}
|
||||
package-cache: true
|
||||
package-cache-key: ${{ matrix.os }}-pkg-release-v1
|
||||
build-cache: true
|
||||
build-cache-key: ${{ matrix.os }}-build-release-v1
|
||||
|
||||
- uses: ./.github/actions/setup-pixi
|
||||
with:
|
||||
environments: package
|
||||
|
||||
- name: Remove ci llvm toolchain on Windows
|
||||
if: runner.os == 'Windows'
|
||||
run: |
|
||||
# @see https://github.com/xmake-io/xmake/issues/7158
|
||||
xmake lua os.rmdir "C:/Program Files/Microsoft Visual Studio/2022/Enterprise/VC/Tools/Llvm"
|
||||
xmake lua os.rmdir "C:/Program Files/LLVM"
|
||||
|
||||
- name: Package
|
||||
run: pixi run package
|
||||
|
||||
@@ -63,7 +49,7 @@ jobs:
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: build/xpack/clice/${{ matrix.artifact_name }}
|
||||
file: build/RelWithDebInfo/${{ matrix.artifact_name }}
|
||||
asset_name: ${{ matrix.asset_name }}
|
||||
tag: ${{ github.ref }}
|
||||
overwrite: true
|
||||
@@ -73,7 +59,7 @@ jobs:
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: build/xpack/clice/${{ matrix.symbol_artifact_name }}
|
||||
file: build/RelWithDebInfo/${{ matrix.symbol_artifact_name }}
|
||||
asset_name: ${{ matrix.symbol_asset_name }}
|
||||
tag: ${{ github.ref }}
|
||||
overwrite: true
|
||||
|
||||
42
.github/workflows/test-xmake.yml
vendored
42
.github/workflows/test-xmake.yml
vendored
@@ -1,42 +0,0 @@
|
||||
name: xmake
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [windows-2025, ubuntu-24.04, macos-15]
|
||||
build_type: [debug, releasedbg]
|
||||
exclude:
|
||||
- os: windows-2025
|
||||
build_type: debug
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup xmake
|
||||
uses: xmake-io/github-action-setup-xmake@v1
|
||||
with:
|
||||
xmake-version: 3.0.5
|
||||
actions-cache-folder: ".xmake-cache"
|
||||
actions-cache-key: ${{ matrix.os }}
|
||||
package-cache: true
|
||||
package-cache-key: ${{ matrix.os }}-pixi
|
||||
build-cache: true
|
||||
build-cache-key: ${{ matrix.os }}-${{ matrix.build_type }}
|
||||
|
||||
- uses: ./.github/actions/setup-pixi
|
||||
|
||||
- name: Build
|
||||
run: pixi run xmake ${{ matrix.build_type }}
|
||||
|
||||
- name: Test
|
||||
run: pixi run xmake-test
|
||||
|
||||
- name: Remove llvm package (Linux)
|
||||
if: runner.os == 'Linux'
|
||||
run: xmake require --uninstall clice-llvm
|
||||
13
.gitignore
vendored
13
.gitignore
vendored
@@ -35,7 +35,7 @@
|
||||
*build*/
|
||||
temp/
|
||||
.cache/
|
||||
.xmake/
|
||||
|
||||
.llvm*/
|
||||
.clice/
|
||||
compile_commands.json
|
||||
@@ -56,10 +56,11 @@ __pycache__/
|
||||
tests/unit/Local/
|
||||
|
||||
# IDEs & Editors
|
||||
/.vscode/
|
||||
/.vscode/*
|
||||
!/.vscode/launch.json
|
||||
!/.vscode/tasks.json
|
||||
.vs/
|
||||
.idea/
|
||||
.claude
|
||||
.clangd
|
||||
|
||||
# pixi environments
|
||||
@@ -68,5 +69,7 @@ tests/unit/Local/
|
||||
!.pixi/config.toml
|
||||
|
||||
.codex/
|
||||
.claude/
|
||||
openspec/
|
||||
.claude/*
|
||||
!.claude/CLAUDE.md
|
||||
!.claude/commands/
|
||||
openspec/
|
||||
|
||||
83
.vscode/launch.json
vendored
Normal file
83
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug clice",
|
||||
"program": "${workspaceFolder}/build/Debug/bin/clice",
|
||||
"args": ["--mode=socket", "--port=50051"],
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug clice (socket, RelWithDebInfo)",
|
||||
"program": "${workspaceFolder}/build/RelWithDebInfo/bin/clice",
|
||||
"args": ["--mode=socket", "--port=50051"],
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"name": "VSCode Extension (pipe)",
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"args": [
|
||||
"--disable-extension=llvm-vs-code-extensions.vscode-clangd",
|
||||
"--disable-extension=ms-vscode.cpptools",
|
||||
"--disable-extension=ms-vscode.cpptools-extension-pack",
|
||||
"--extensionDevelopmentPath=${workspaceFolder}/editors/vscode"
|
||||
],
|
||||
"env": {
|
||||
"CLICE_MODE": "pipe"
|
||||
},
|
||||
"outFiles": ["${workspaceFolder}/editors/vscode/dist/**/*.js"],
|
||||
"preLaunchTask": "npm: watch vscode ext"
|
||||
},
|
||||
{
|
||||
"name": "VSCode Extension (socket)",
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"args": [
|
||||
"--disable-extension=llvm-vs-code-extensions.vscode-clangd",
|
||||
"--disable-extension=ms-vscode.cpptools",
|
||||
"--disable-extension=ms-vscode.cpptools-extension-pack",
|
||||
"--extensionDevelopmentPath=${workspaceFolder}/editors/vscode"
|
||||
],
|
||||
"env": {
|
||||
"CLICE_MODE": "socket"
|
||||
},
|
||||
"outFiles": ["${workspaceFolder}/editors/vscode/dist/**/*.js"],
|
||||
"preLaunchTask": "npm: watch vscode ext"
|
||||
},
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Unit Test",
|
||||
"program": "${workspaceFolder}/build/Debug/bin/unit_tests",
|
||||
"args": ["--test-dir=./tests/data", "--test-filter=${input:filter}"],
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Release Unit Test",
|
||||
"program": "${workspaceFolder}/build/RelWithDebInfo/bin/unit_tests",
|
||||
"args": ["--test-dir=./tests/data", "--test-filter=${input:filter}"],
|
||||
"cwd": "${workspaceFolder}"
|
||||
}
|
||||
],
|
||||
"compounds": [
|
||||
{
|
||||
"name": "clice + VSCode Extension (socket)",
|
||||
"configurations": ["Debug clice", "VSCode Extension (socket)"],
|
||||
"stopAll": true
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"id": "filter",
|
||||
"type": "promptString",
|
||||
"description": "Unit Test Filter"
|
||||
}
|
||||
]
|
||||
}
|
||||
42
.vscode/tasks.json
vendored
Normal file
42
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "npm: install vscode deps",
|
||||
"type": "shell",
|
||||
"command": "pnpm",
|
||||
"args": ["install"],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/editors/vscode"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "npm: watch vscode ext",
|
||||
"type": "shell",
|
||||
"command": "pnpm",
|
||||
"args": ["run", "watch"],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/editors/vscode"
|
||||
},
|
||||
"dependsOn": "npm: install vscode deps",
|
||||
"problemMatcher": "$ts-webpack-watch",
|
||||
"isBackground": true,
|
||||
"presentation": {
|
||||
"reveal": "never",
|
||||
"group": "watchers"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "npm: package vscode ext",
|
||||
"type": "shell",
|
||||
"command": "pnpm",
|
||||
"args": ["run", "package"],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/editors/vscode"
|
||||
},
|
||||
"dependsOn": "npm: install vscode deps",
|
||||
"problemMatcher": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -151,13 +151,13 @@ target_link_libraries(clice-core PUBLIC
|
||||
spdlog::spdlog
|
||||
roaring::roaring
|
||||
flatbuffers
|
||||
eventide::ipc::lsp
|
||||
eventide::serde::toml
|
||||
kota::ipc::lsp
|
||||
kota::codec::toml
|
||||
simdjson::simdjson
|
||||
)
|
||||
|
||||
add_executable(clice "${PROJECT_SOURCE_DIR}/src/clice.cc")
|
||||
target_link_libraries(clice PRIVATE clice::core eventide::deco)
|
||||
target_link_libraries(clice PRIVATE clice::core kota::deco)
|
||||
install(TARGETS clice RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
|
||||
|
||||
add_custom_target(copy_clang_resource ALL
|
||||
@@ -189,7 +189,7 @@ if(CLICE_ENABLE_TEST)
|
||||
"${PROJECT_SOURCE_DIR}/src"
|
||||
"${PROJECT_SOURCE_DIR}/tests/unit"
|
||||
)
|
||||
target_link_libraries(unit_tests PRIVATE clice::core eventide::zest eventide::deco)
|
||||
target_link_libraries(unit_tests PRIVATE clice::core kota::zest kota::deco)
|
||||
endif()
|
||||
|
||||
if(CLICE_ENABLE_BENCHMARK)
|
||||
@@ -199,7 +199,15 @@ if(CLICE_ENABLE_BENCHMARK)
|
||||
target_include_directories(scan_benchmark PRIVATE
|
||||
"${PROJECT_SOURCE_DIR}/src"
|
||||
)
|
||||
target_link_libraries(scan_benchmark PRIVATE clice::core eventide::deco)
|
||||
target_link_libraries(scan_benchmark PRIVATE clice::core kota::deco)
|
||||
|
||||
add_executable(index_benchmark
|
||||
"${PROJECT_SOURCE_DIR}/benchmarks/index_benchmark.cpp"
|
||||
)
|
||||
target_include_directories(index_benchmark PRIVATE
|
||||
"${PROJECT_SOURCE_DIR}/src"
|
||||
)
|
||||
target_link_libraries(index_benchmark PRIVATE clice::core kota::deco)
|
||||
endif()
|
||||
|
||||
if(CLICE_RELEASE)
|
||||
|
||||
664
benchmarks/index_benchmark.cpp
Normal file
664
benchmarks/index_benchmark.cpp
Normal file
@@ -0,0 +1,664 @@
|
||||
/// Benchmark for TUIndex build, MergedIndex merge, and query performance.
|
||||
///
|
||||
/// Usage:
|
||||
/// index_benchmark [OPTIONS] <compile_commands.json>
|
||||
///
|
||||
/// Example:
|
||||
/// ./build/RelWithDebInfo/bin/index_benchmark \
|
||||
/// /home/ykiko/C++/clice/.llvm/build-debug/compile_commands.json
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#include <mutex>
|
||||
#include <numeric>
|
||||
#include <print>
|
||||
#include <sstream>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "command/command.h"
|
||||
#include "compile/compilation.h"
|
||||
#include "index/merged_index.h"
|
||||
#include "index/project_index.h"
|
||||
#include "index/tu_index.h"
|
||||
#include "support/logging.h"
|
||||
|
||||
#include "kota/deco/deco.h"
|
||||
#include "kota/ipc/lsp/position.h"
|
||||
#include "llvm/ADT/StringMap.h"
|
||||
#include "llvm/Support/raw_ostream.h"
|
||||
|
||||
using namespace clice;
|
||||
|
||||
struct Options {
|
||||
DecoKV(names = {"--log-level"}; help = "Log level"; required = false;)
|
||||
<std::string> log_level = "off";
|
||||
|
||||
DecoKV(names = {"--limit"}; help = "Max files to index (0 = all)"; required = false;)
|
||||
<int> limit = 100;
|
||||
|
||||
DecoKV(names = {"--jobs", "-j"}; help = "Parallel compile jobs (0 = nproc)"; required = false;)
|
||||
<int> jobs = 0;
|
||||
|
||||
DecoFlag(names = {"-h", "--help"}; help = "Show help"; required = false;)
|
||||
help;
|
||||
|
||||
DecoInput(meta_var = "CDB"; help = "Path to compile_commands.json"; required = false;)
|
||||
<std::string> cdb_path;
|
||||
};
|
||||
|
||||
struct Timer {
|
||||
std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();
|
||||
|
||||
double ms() const {
|
||||
auto end = std::chrono::steady_clock::now();
|
||||
return std::chrono::duration<double, std::milli>(end - start).count();
|
||||
}
|
||||
|
||||
void reset() {
|
||||
start = std::chrono::steady_clock::now();
|
||||
}
|
||||
};
|
||||
|
||||
struct IndexStats {
|
||||
std::string file;
|
||||
double compile_ms = 0;
|
||||
double build_index_ms = 0;
|
||||
double serialize_ms = 0;
|
||||
std::size_t symbols = 0;
|
||||
std::size_t occurrences = 0;
|
||||
std::size_t relations = 0;
|
||||
std::size_t serialized_bytes = 0;
|
||||
std::size_t file_count = 0;
|
||||
};
|
||||
|
||||
struct IndexResult {
|
||||
IndexStats stats;
|
||||
index::TUIndex tu_index;
|
||||
};
|
||||
|
||||
struct MergeStats {
|
||||
std::size_t total_files = 0;
|
||||
std::size_t total_occurrences = 0;
|
||||
std::size_t total_relations = 0;
|
||||
double merge_ms = 0;
|
||||
double serialize_ms = 0;
|
||||
std::size_t total_serialized_bytes = 0;
|
||||
std::size_t shard_count = 0;
|
||||
};
|
||||
|
||||
void print_size(std::size_t bytes) {
|
||||
if(bytes < 1024)
|
||||
std::print("{}B", bytes);
|
||||
else if(bytes < 1024 * 1024)
|
||||
std::print("{:.1f}KB", bytes / 1024.0);
|
||||
else
|
||||
std::print("{:.1f}MB", bytes / (1024.0 * 1024.0));
|
||||
}
|
||||
|
||||
/// Per-task: compile one file, build TUIndex, serialize, collect stats.
|
||||
/// Everything here is thread-local, no shared state.
|
||||
static std::optional<IndexResult> index_one_file(CompilationDatabase& cdb,
|
||||
const CompilationEntry& entry) {
|
||||
auto file = cdb.resolve_path(entry.file);
|
||||
auto cmds = cdb.lookup(file);
|
||||
if(cmds.empty())
|
||||
return std::nullopt;
|
||||
|
||||
auto& cmd = cmds[0];
|
||||
auto argv_vec = cmd.to_argv();
|
||||
|
||||
CompilationParams cp;
|
||||
cp.kind = CompilationKind::Indexing;
|
||||
cp.directory = cmd.resolved.directory;
|
||||
for(auto* arg: argv_vec)
|
||||
cp.arguments.push_back(arg);
|
||||
|
||||
IndexResult result;
|
||||
result.stats.file = std::string(file);
|
||||
|
||||
Timer t;
|
||||
auto unit = compile(cp);
|
||||
result.stats.compile_ms = t.ms();
|
||||
|
||||
if(!unit.completed())
|
||||
return std::nullopt;
|
||||
|
||||
t.reset();
|
||||
result.tu_index = index::TUIndex::build(unit);
|
||||
result.stats.build_index_ms = t.ms();
|
||||
|
||||
result.stats.symbols = result.tu_index.symbols.size();
|
||||
result.stats.file_count = result.tu_index.file_indices.size() + 1;
|
||||
for(auto& [fid, fi]: result.tu_index.file_indices) {
|
||||
result.stats.occurrences += fi.occurrences.size();
|
||||
for(auto& [_, rels]: fi.relations)
|
||||
result.stats.relations += rels.size();
|
||||
}
|
||||
result.stats.occurrences += result.tu_index.main_file_index.occurrences.size();
|
||||
for(auto& [_, rels]: result.tu_index.main_file_index.relations)
|
||||
result.stats.relations += rels.size();
|
||||
|
||||
t.reset();
|
||||
std::string serialized;
|
||||
llvm::raw_string_ostream os(serialized);
|
||||
result.tu_index.serialize(os);
|
||||
result.stats.serialize_ms = t.ms();
|
||||
result.stats.serialized_bytes = serialized.size();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
int main(int argc, const char** argv) {
|
||||
auto args = deco::util::argvify(argc, argv);
|
||||
auto parsed = deco::cli::parse<Options>(args);
|
||||
if(!parsed.has_value()) {
|
||||
std::println(stderr, "Error: {}", parsed.error().message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto& opts = parsed->options;
|
||||
if(opts.help.value_or(false) || !opts.cdb_path.has_value()) {
|
||||
std::ostringstream oss;
|
||||
deco::cli::write_usage_for<Options>(oss, "index_benchmark [OPTIONS] <cdb>");
|
||||
std::print("{}", oss.str());
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto level = spdlog::level::from_str(*opts.log_level);
|
||||
clice::logging::options.level = level;
|
||||
clice::logging::stderr_logger("index_benchmark", clice::logging::options);
|
||||
|
||||
auto num_jobs = static_cast<unsigned>(*opts.jobs);
|
||||
if(num_jobs == 0)
|
||||
num_jobs = std::thread::hardware_concurrency();
|
||||
if(num_jobs == 0)
|
||||
num_jobs = 4;
|
||||
|
||||
// Load CDB.
|
||||
CompilationDatabase cdb;
|
||||
auto count = cdb.load(*opts.cdb_path);
|
||||
std::println("CDB: {} entries from {}", count, *opts.cdb_path);
|
||||
|
||||
auto entries = cdb.get_entries();
|
||||
auto limit = static_cast<std::size_t>(*opts.limit);
|
||||
if(limit > 0 && limit < entries.size())
|
||||
entries = entries.take_front(limit);
|
||||
std::println("Indexing {} files with {} threads...\n", entries.size(), num_jobs);
|
||||
|
||||
// Phase 1: Parallel compile & build TUIndex.
|
||||
// Each thread picks tasks from a shared atomic counter.
|
||||
// Results are written to a pre-sized vector (one slot per entry).
|
||||
std::vector<std::optional<IndexResult>> results(entries.size());
|
||||
std::atomic<std::size_t> next_task{0};
|
||||
std::atomic<std::size_t> completed{0};
|
||||
|
||||
Timer phase1_wall;
|
||||
|
||||
{
|
||||
std::vector<std::jthread> workers;
|
||||
workers.reserve(num_jobs);
|
||||
for(unsigned i = 0; i < num_jobs; i++) {
|
||||
workers.emplace_back([&] {
|
||||
while(true) {
|
||||
auto idx = next_task.fetch_add(1);
|
||||
if(idx >= entries.size())
|
||||
break;
|
||||
results[idx] = index_one_file(cdb, entries[idx]);
|
||||
auto done = completed.fetch_add(1) + 1;
|
||||
if(done % 100 == 0)
|
||||
std::println(" ... {}/{} done", done, entries.size());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
double phase1_wall_ms = phase1_wall.ms();
|
||||
|
||||
// Collect results on main thread.
|
||||
std::vector<IndexStats> all_stats;
|
||||
std::vector<std::pair<std::string, index::TUIndex>> tu_indices;
|
||||
all_stats.reserve(entries.size());
|
||||
tu_indices.reserve(entries.size());
|
||||
|
||||
double total_compile_ms = 0;
|
||||
double total_build_ms = 0;
|
||||
double total_serialize_ms = 0;
|
||||
std::size_t total_serialized = 0;
|
||||
|
||||
for(auto& r: results) {
|
||||
if(!r)
|
||||
continue;
|
||||
total_compile_ms += r->stats.compile_ms;
|
||||
total_build_ms += r->stats.build_index_ms;
|
||||
total_serialize_ms += r->stats.serialize_ms;
|
||||
total_serialized += r->stats.serialized_bytes;
|
||||
all_stats.push_back(r->stats);
|
||||
tu_indices.emplace_back(std::move(r->stats.file), std::move(r->tu_index));
|
||||
}
|
||||
|
||||
std::println("=== Phase 1: Compile & Build TUIndex ({} threads) ===", num_jobs);
|
||||
std::println(" Files indexed: {}", all_stats.size());
|
||||
std::println(" Wall time: {:.0f}ms", phase1_wall_ms);
|
||||
std::println(" Total compile: {:.0f}ms (sum of per-thread)", total_compile_ms);
|
||||
std::println(" Total build idx: {:.0f}ms (sum of per-thread)", total_build_ms);
|
||||
std::println(" Total serialize: {:.0f}ms (sum of per-thread)", total_serialize_ms);
|
||||
std::print(" Total TUIndex size: ");
|
||||
print_size(total_serialized);
|
||||
std::println("");
|
||||
|
||||
// Size distribution.
|
||||
if(!all_stats.empty()) {
|
||||
std::vector<std::size_t> sizes;
|
||||
std::vector<double> compile_times;
|
||||
std::vector<double> build_times;
|
||||
for(auto& s: all_stats) {
|
||||
sizes.push_back(s.serialized_bytes);
|
||||
compile_times.push_back(s.compile_ms);
|
||||
build_times.push_back(s.build_index_ms);
|
||||
}
|
||||
std::ranges::sort(sizes);
|
||||
std::ranges::sort(compile_times);
|
||||
std::ranges::sort(build_times);
|
||||
auto p = [](auto& v, double pct) {
|
||||
return v[static_cast<std::size_t>(v.size() * pct)];
|
||||
};
|
||||
|
||||
std::println("\n TUIndex size distribution:");
|
||||
std::print(" p50=");
|
||||
print_size(p(sizes, 0.5));
|
||||
std::print(" p90=");
|
||||
print_size(p(sizes, 0.9));
|
||||
std::print(" p99=");
|
||||
print_size(p(sizes, 0.99));
|
||||
std::print(" max=");
|
||||
print_size(sizes.back());
|
||||
std::println("");
|
||||
|
||||
std::println(" Compile time distribution:");
|
||||
std::println(" p50={:.0f}ms p90={:.0f}ms p99={:.0f}ms max={:.0f}ms",
|
||||
p(compile_times, 0.5),
|
||||
p(compile_times, 0.9),
|
||||
p(compile_times, 0.99),
|
||||
compile_times.back());
|
||||
|
||||
std::println(" Build index time distribution:");
|
||||
std::println(" p50={:.1f}ms p90={:.1f}ms p99={:.1f}ms max={:.1f}ms",
|
||||
p(build_times, 0.5),
|
||||
p(build_times, 0.9),
|
||||
p(build_times, 0.99),
|
||||
build_times.back());
|
||||
}
|
||||
|
||||
// Phase 2: Merge into MergedIndex shards (main thread).
|
||||
std::println("\n=== Phase 2: Merge into MergedIndex ===");
|
||||
|
||||
index::ProjectIndex project_index;
|
||||
llvm::DenseMap<std::uint32_t, index::MergedIndex> merged;
|
||||
MergeStats merge_stats;
|
||||
Timer merge_timer;
|
||||
|
||||
llvm::DenseMap<std::uint32_t, std::string> file_contents;
|
||||
|
||||
auto read_content = [&](std::uint32_t path_id, llvm::StringRef path) -> llvm::StringRef {
|
||||
auto it = file_contents.find(path_id);
|
||||
if(it != file_contents.end())
|
||||
return it->second;
|
||||
auto buf = llvm::MemoryBuffer::getFile(path);
|
||||
if(!buf)
|
||||
return {};
|
||||
auto [inserted, _] = file_contents.try_emplace(path_id, (*buf)->getBuffer().str());
|
||||
return inserted->second;
|
||||
};
|
||||
|
||||
// Pre-compute SHA256 hashes (simulates offloading to worker threads).
|
||||
// With cached hash, this warms the cache so MergedIndex::merge won't recompute.
|
||||
Timer hash_timer;
|
||||
std::size_t hash_count = 0;
|
||||
for(auto& [file, tu_index]: tu_indices) {
|
||||
for(auto& [fid, fi]: tu_index.file_indices) {
|
||||
fi.hash();
|
||||
hash_count++;
|
||||
}
|
||||
tu_index.main_file_index.hash();
|
||||
hash_count++;
|
||||
}
|
||||
double precompute_hash_ms = hash_timer.ms();
|
||||
|
||||
// Merge with both optimizations:
|
||||
// 1. SHA256 already cached from above (MergedIndex::merge uses cached values)
|
||||
// 2. Selective ProjectIndex update (skip symbols only in cache-hit files)
|
||||
double project_merge_full_ms = 0;
|
||||
double project_merge_selective_ms = 0;
|
||||
double merged_index_merge_ms = 0;
|
||||
std::size_t cache_hits = 0;
|
||||
std::size_t cache_misses = 0;
|
||||
|
||||
for(auto& [file, tu_index]: tu_indices) {
|
||||
auto& graph = tu_index.graph;
|
||||
auto main_tu_path_id = static_cast<std::uint32_t>(graph.paths.size() - 1);
|
||||
|
||||
// First: MergedIndex merges to find out which files are new.
|
||||
Bitmap new_file_ids;
|
||||
Timer mt;
|
||||
for(auto& [fid, fi]: tu_index.file_indices) {
|
||||
auto tu_path_id = graph.path_id(fid);
|
||||
auto global_path_id = project_index.path_pool.path_id(graph.paths[tu_path_id]);
|
||||
auto content = read_content(global_path_id, graph.paths[tu_path_id]);
|
||||
auto include_id = graph.include_location_id(fid);
|
||||
bool hit = merged[global_path_id].merge(global_path_id, include_id, fi, content);
|
||||
if(hit) {
|
||||
cache_hits++;
|
||||
} else {
|
||||
cache_misses++;
|
||||
new_file_ids.add(tu_path_id);
|
||||
}
|
||||
merge_stats.total_files++;
|
||||
}
|
||||
|
||||
// Main file is always new.
|
||||
new_file_ids.add(main_tu_path_id);
|
||||
auto global_main_id = project_index.path_pool.path_id(graph.paths[main_tu_path_id]);
|
||||
auto content = read_content(global_main_id, graph.paths[main_tu_path_id]);
|
||||
auto now = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch());
|
||||
|
||||
std::vector<index::IncludeLocation> remapped_locs;
|
||||
for(auto& loc: graph.locations) {
|
||||
auto remapped = loc;
|
||||
remapped.path_id = project_index.path_pool.path_id(graph.paths[loc.path_id]);
|
||||
remapped_locs.push_back(remapped);
|
||||
}
|
||||
merged[global_main_id].merge(global_main_id,
|
||||
now,
|
||||
std::move(remapped_locs),
|
||||
tu_index.main_file_index,
|
||||
content);
|
||||
cache_misses++;
|
||||
merged_index_merge_ms += mt.ms();
|
||||
|
||||
// Then: selective ProjectIndex merge (only symbols in new files).
|
||||
Timer pt;
|
||||
project_index.merge(tu_index, new_file_ids);
|
||||
project_merge_selective_ms += pt.ms();
|
||||
}
|
||||
|
||||
merge_stats.merge_ms = merge_timer.ms();
|
||||
merge_stats.shard_count = merged.size();
|
||||
|
||||
// Serialize all shards to measure size.
|
||||
merge_timer.reset();
|
||||
for(auto& [path, shard]: merged) {
|
||||
std::string buf;
|
||||
llvm::raw_string_ostream os(buf);
|
||||
shard.serialize(os);
|
||||
merge_stats.total_serialized_bytes += buf.size();
|
||||
}
|
||||
merge_stats.serialize_ms = merge_timer.ms();
|
||||
|
||||
std::println(" Shards: {}", merge_stats.shard_count);
|
||||
std::println(" SHA256 precompute (offloadable): {:.0f}ms ({} hashes)",
|
||||
precompute_hash_ms,
|
||||
hash_count);
|
||||
std::println(" MergedIndex merge (cached hash): {:.0f}ms (hit: {}, miss: {})",
|
||||
merged_index_merge_ms,
|
||||
cache_hits,
|
||||
cache_misses);
|
||||
std::println(" ProjectIndex merge (selective): {:.0f}ms", project_merge_selective_ms);
|
||||
if(!tu_indices.empty()) {
|
||||
std::println(" Main-thread total: {:.0f}ms ({:.1f}ms/TU)",
|
||||
merged_index_merge_ms + project_merge_selective_ms,
|
||||
(merged_index_merge_ms + project_merge_selective_ms) / tu_indices.size());
|
||||
}
|
||||
std::println(" Serialize time: {:.0f}ms", merge_stats.serialize_ms);
|
||||
std::print(" Total size: ");
|
||||
print_size(merge_stats.total_serialized_bytes);
|
||||
std::println("");
|
||||
|
||||
// Shard size distribution.
|
||||
{
|
||||
std::vector<std::size_t> shard_sizes;
|
||||
shard_sizes.reserve(merged.size());
|
||||
for(auto& [path, shard]: merged) {
|
||||
std::string buf;
|
||||
llvm::raw_string_ostream os(buf);
|
||||
shard.serialize(os);
|
||||
shard_sizes.push_back(buf.size());
|
||||
}
|
||||
std::ranges::sort(shard_sizes);
|
||||
if(!shard_sizes.empty()) {
|
||||
auto p = [&](double pct) {
|
||||
return shard_sizes[static_cast<std::size_t>(shard_sizes.size() * pct)];
|
||||
};
|
||||
std::println("\n Shard size distribution:");
|
||||
std::print(" p50=");
|
||||
print_size(p(0.5));
|
||||
std::print(" p90=");
|
||||
print_size(p(0.9));
|
||||
std::print(" p99=");
|
||||
print_size(p(0.99));
|
||||
std::print(" max=");
|
||||
print_size(shard_sizes.back());
|
||||
std::println("");
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 3: Query benchmark.
|
||||
std::println("\n=== Phase 3: Query Benchmark ===");
|
||||
|
||||
// Collect some symbol hashes to query.
|
||||
llvm::SmallVector<index::SymbolHash> sample_hashes;
|
||||
for(auto& [file, tu_index]: tu_indices) {
|
||||
for(auto& [hash, sym]: tu_index.symbols) {
|
||||
sample_hashes.push_back(hash);
|
||||
if(sample_hashes.size() >= 1000)
|
||||
break;
|
||||
}
|
||||
if(sample_hashes.size() >= 1000)
|
||||
break;
|
||||
}
|
||||
|
||||
// Occurrence lookup by offset (raw, no position conversion).
|
||||
{
|
||||
std::size_t hit_count = 0;
|
||||
Timer t;
|
||||
constexpr int rounds = 10;
|
||||
for(int r = 0; r < rounds; r++) {
|
||||
for(auto& [path, shard]: merged) {
|
||||
for(std::uint32_t offset = 0; offset < 1000; offset += 50) {
|
||||
shard.lookup(offset, [&](const index::Occurrence&) {
|
||||
hit_count++;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
auto elapsed = t.ms();
|
||||
auto queries = merged.size() * 20 * rounds;
|
||||
std::println(" Occurrence lookup (raw): {} queries in {:.1f}ms ({:.0f} q/ms, {} hits)",
|
||||
queries,
|
||||
elapsed,
|
||||
queries / elapsed,
|
||||
hit_count);
|
||||
}
|
||||
|
||||
// Relation lookup by symbol hash (raw, no position conversion).
|
||||
{
|
||||
std::size_t hit_count = 0;
|
||||
Timer t;
|
||||
constexpr int rounds = 10;
|
||||
for(int r = 0; r < rounds; r++) {
|
||||
for(auto& [path, shard]: merged) {
|
||||
for(auto hash: sample_hashes) {
|
||||
shard.lookup(hash, RelationKind::Definition, [&](const index::Relation&) {
|
||||
hit_count++;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
auto elapsed = t.ms();
|
||||
auto queries = merged.size() * sample_hashes.size() * rounds;
|
||||
std::println(" Relation lookup (raw): {} queries in {:.1f}ms ({:.0f} q/ms, {} hits)",
|
||||
queries,
|
||||
elapsed,
|
||||
queries / elapsed,
|
||||
hit_count);
|
||||
}
|
||||
|
||||
// PositionMapper construction benchmark.
|
||||
{
|
||||
using kota::ipc::lsp::PositionMapper;
|
||||
using kota::ipc::lsp::PositionEncoding;
|
||||
|
||||
std::size_t mapper_count = 0;
|
||||
std::vector<double> construct_times;
|
||||
|
||||
for(auto& [path, shard]: merged) {
|
||||
auto content = shard.content();
|
||||
if(content.empty())
|
||||
continue;
|
||||
Timer t;
|
||||
PositionMapper mapper(content, PositionEncoding::UTF16);
|
||||
construct_times.push_back(t.ms());
|
||||
mapper_count++;
|
||||
}
|
||||
|
||||
std::ranges::sort(construct_times);
|
||||
if(!construct_times.empty()) {
|
||||
auto sum = std::accumulate(construct_times.begin(), construct_times.end(), 0.0);
|
||||
auto p = [&](double pct) {
|
||||
return construct_times[static_cast<std::size_t>(construct_times.size() * pct)];
|
||||
};
|
||||
std::println("\n PositionMapper construction ({} shards):", mapper_count);
|
||||
std::println(" Total: {:.1f}ms", sum);
|
||||
std::println(" p50={:.3f}ms p90={:.3f}ms p99={:.3f}ms max={:.3f}ms",
|
||||
p(0.5),
|
||||
p(0.9),
|
||||
p(0.99),
|
||||
construct_times.back());
|
||||
}
|
||||
}
|
||||
|
||||
// Relation lookup with full position conversion (simulates real LSP query).
|
||||
{
|
||||
using kota::ipc::lsp::PositionMapper;
|
||||
using kota::ipc::lsp::PositionEncoding;
|
||||
|
||||
// Pre-build mappers for all shards.
|
||||
llvm::DenseMap<std::uint32_t, PositionMapper> mappers;
|
||||
for(auto& [path_id, shard]: merged) {
|
||||
auto content = shard.content();
|
||||
if(!content.empty())
|
||||
mappers.try_emplace(path_id, content, PositionEncoding::UTF16);
|
||||
}
|
||||
|
||||
std::size_t hit_count = 0;
|
||||
std::size_t convert_count = 0;
|
||||
Timer t;
|
||||
constexpr int rounds = 10;
|
||||
for(int r = 0; r < rounds; r++) {
|
||||
for(auto& [path_id, shard]: merged) {
|
||||
auto mapper_it = mappers.find(path_id);
|
||||
if(mapper_it == mappers.end())
|
||||
continue;
|
||||
auto& mapper = mapper_it->second;
|
||||
for(auto hash: sample_hashes) {
|
||||
shard.lookup(hash, RelationKind::Definition, [&](const index::Relation& rel) {
|
||||
auto start = mapper.to_position(rel.range.begin);
|
||||
auto end = mapper.to_position(rel.range.end);
|
||||
if(start && end)
|
||||
convert_count++;
|
||||
hit_count++;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
auto elapsed = t.ms();
|
||||
auto queries = merged.size() * sample_hashes.size() * rounds;
|
||||
std::println("\n Relation lookup + to_position:");
|
||||
std::println(" {} queries in {:.1f}ms ({:.0f} q/ms, {} hits, {} converted)",
|
||||
queries,
|
||||
elapsed,
|
||||
queries / elapsed,
|
||||
hit_count,
|
||||
convert_count);
|
||||
}
|
||||
|
||||
// Phase 4: Realistic "find references" for specific high-frequency symbols.
|
||||
std::println("=== Phase 4: Realistic Find-References ===");
|
||||
{
|
||||
using kota::ipc::lsp::PositionMapper;
|
||||
using kota::ipc::lsp::PositionEncoding;
|
||||
|
||||
// Use the properly merged ProjectIndex symbol table.
|
||||
auto& symbol_table = project_index.symbols;
|
||||
|
||||
// Find symbols by name to query. Collect ALL hashes whose name contains the target.
|
||||
llvm::StringRef targets[] = {"vector",
|
||||
"StringRef",
|
||||
"SourceLocation",
|
||||
"DenseMap",
|
||||
"SmallVector"};
|
||||
for(auto target_name: targets) {
|
||||
llvm::SmallVector<index::SymbolHash> matching_hashes;
|
||||
std::size_t ref_file_count = 0;
|
||||
for(auto& [hash, sym]: symbol_table) {
|
||||
if(sym.name == target_name) {
|
||||
matching_hashes.push_back(hash);
|
||||
ref_file_count += sym.reference_files.cardinality();
|
||||
}
|
||||
}
|
||||
|
||||
if(matching_hashes.empty()) {
|
||||
std::println(" {}: not found in symbol table", target_name);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Query all shards for this symbol's references (simulating real find-references).
|
||||
std::size_t total_refs = 0;
|
||||
std::size_t shards_queried = 0;
|
||||
RelationKind kinds[] = {
|
||||
RelationKind::Reference,
|
||||
RelationKind::Definition,
|
||||
RelationKind::Declaration,
|
||||
};
|
||||
Timer t;
|
||||
for(auto& [path, shard]: merged) {
|
||||
bool found = false;
|
||||
for(auto target_hash: matching_hashes) {
|
||||
for(auto kind: kinds) {
|
||||
shard.lookup(target_hash, kind, [&](const index::Relation&) {
|
||||
total_refs++;
|
||||
found = true;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
if(found)
|
||||
shards_queried++;
|
||||
}
|
||||
auto elapsed = t.ms();
|
||||
|
||||
std::println(
|
||||
" {} ({} hashes, {} ref_files): {} refs across {} shards, " "scanned {} shards in {:.2f}ms",
|
||||
target_name,
|
||||
matching_hashes.size(),
|
||||
ref_file_count,
|
||||
total_refs,
|
||||
shards_queried,
|
||||
merged.size(),
|
||||
elapsed);
|
||||
|
||||
// Now with reference_files bitmap to skip irrelevant shards.
|
||||
// We don't have path_id mapping here, so just measure the full scan above
|
||||
// vs a targeted scan using reference_files count as an estimate.
|
||||
}
|
||||
}
|
||||
|
||||
std::println("");
|
||||
return 0;
|
||||
}
|
||||
@@ -21,17 +21,15 @@
|
||||
#include <thread>
|
||||
|
||||
#include "command/command.h"
|
||||
#include "eventide/deco/deco.h"
|
||||
#include "eventide/serde/json/serializer.h"
|
||||
#include "support/filesystem.h"
|
||||
#include "support/logging.h"
|
||||
#include "support/path_pool.h"
|
||||
#include "syntax/dependency_graph.h"
|
||||
|
||||
#include "kota/codec/json/serializer.h"
|
||||
#include "kota/deco/deco.h"
|
||||
#include "llvm/Support/FileSystem.h"
|
||||
|
||||
namespace et = eventide;
|
||||
|
||||
using namespace clice;
|
||||
|
||||
struct BenchmarkOptions {
|
||||
@@ -97,7 +95,7 @@ void export_graph_json(const PathPool& path_pool,
|
||||
export_data.files.push_back(std::move(node));
|
||||
}
|
||||
|
||||
auto json = et::serde::json::to_json(export_data);
|
||||
auto json = kota::codec::json::to_json(export_data);
|
||||
if(!json) {
|
||||
std::println(stderr, "Failed to serialize dependency graph");
|
||||
return;
|
||||
@@ -221,8 +219,8 @@ void print_report(const ScanReport& report) {
|
||||
}
|
||||
|
||||
int main(int argc, const char** argv) {
|
||||
auto args = deco::util::argvify(argc, argv);
|
||||
auto result = deco::cli::parse<BenchmarkOptions>(args);
|
||||
auto args = kota::deco::util::argvify(argc, argv);
|
||||
auto result = kota::deco::cli::parse<BenchmarkOptions>(args);
|
||||
|
||||
if(!result.has_value()) {
|
||||
std::println(stderr, "Error: {}", result.error().message);
|
||||
@@ -233,7 +231,7 @@ int main(int argc, const char** argv) {
|
||||
|
||||
if(opts.help.value_or(false) || !opts.cdb_path.has_value()) {
|
||||
std::ostringstream oss;
|
||||
deco::cli::write_usage_for<BenchmarkOptions>(oss, "scan_benchmark [OPTIONS] <cdb>");
|
||||
kota::deco::cli::write_usage_for<BenchmarkOptions>(oss, "scan_benchmark [OPTIONS] <cdb>");
|
||||
std::print("{}", oss.str());
|
||||
return opts.help.value_or(false) ? 0 : 1;
|
||||
}
|
||||
|
||||
@@ -101,8 +101,15 @@ function(setup_llvm LLVM_VERSION)
|
||||
clangToolingSyntax
|
||||
)
|
||||
else()
|
||||
file(GLOB LLVM_LIBRARIES CONFIGURE_DEPENDS "${LLVM_INSTALL_PATH}/lib/*${CMAKE_STATIC_LIBRARY_SUFFIX}")
|
||||
target_link_libraries(llvm-libs INTERFACE ${LLVM_LIBRARIES})
|
||||
file(GLOB LLVM_LIBRARIES CONFIGURE_DEPENDS "${LLVM_INSTALL_PATH}/lib/${CMAKE_STATIC_LIBRARY_PREFIX}LLVM[a-zA-Z]*${CMAKE_STATIC_LIBRARY_SUFFIX}")
|
||||
file(GLOB CLANG_LIBRARIES CONFIGURE_DEPENDS "${LLVM_INSTALL_PATH}/lib/${CMAKE_STATIC_LIBRARY_PREFIX}clang[a-zA-Z]*${CMAKE_STATIC_LIBRARY_SUFFIX}")
|
||||
# TODO: find a better way to find out whether zlib and zstd are needed
|
||||
# Currently link if present in the LLVM lib directory
|
||||
file(GLOB OTHER_REQUIRED_LIBS CONFIGURE_DEPENDS
|
||||
"${LLVM_INSTALL_PATH}/lib/${CMAKE_STATIC_LIBRARY_PREFIX}z${CMAKE_STATIC_LIBRARY_SUFFIX}"
|
||||
"${LLVM_INSTALL_PATH}/lib/${CMAKE_STATIC_LIBRARY_PREFIX}zstd${CMAKE_STATIC_LIBRARY_SUFFIX}"
|
||||
)
|
||||
target_link_libraries(llvm-libs INTERFACE ${LLVM_LIBRARIES} ${CLANG_LIBRARIES} ${OTHER_REQUIRED_LIBS})
|
||||
target_compile_definitions(llvm-libs INTERFACE CLANG_BUILD_STATIC=1)
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
@@ -39,18 +39,18 @@ set(FLATBUFFERS_BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
||||
set(FLATBUFFERS_BUILD_FLATHASH OFF CACHE BOOL "" FORCE)
|
||||
|
||||
FetchContent_Declare(
|
||||
eventide
|
||||
GIT_REPOSITORY https://github.com/clice-io/eventide
|
||||
kotatsu
|
||||
GIT_REPOSITORY https://github.com/clice-io/kotatsu
|
||||
GIT_TAG main
|
||||
GIT_SHALLOW TRUE
|
||||
)
|
||||
|
||||
set(ETD_ENABLE_ZEST ON)
|
||||
set(ETD_ENABLE_TEST OFF)
|
||||
set(ETD_SERDE_ENABLE_SIMDJSON ON)
|
||||
set(ETD_SERDE_ENABLE_YYJSON ON)
|
||||
set(ETD_SERDE_ENABLE_TOML ON)
|
||||
set(ETD_ENABLE_EXCEPTIONS OFF)
|
||||
set(ETD_ENABLE_RTTI OFF)
|
||||
set(KOTA_ENABLE_ZEST ON)
|
||||
set(KOTA_ENABLE_TEST OFF)
|
||||
set(KOTA_CODEC_ENABLE_SIMDJSON ON)
|
||||
set(KOTA_CODEC_ENABLE_YYJSON ON)
|
||||
set(KOTA_CODEC_ENABLE_TOML ON)
|
||||
set(KOTA_ENABLE_EXCEPTIONS OFF)
|
||||
set(KOTA_ENABLE_RTTI OFF)
|
||||
|
||||
FetchContent_MakeAvailable(eventide spdlog croaring flatbuffers)
|
||||
FetchContent_MakeAvailable(kotatsu spdlog croaring flatbuffers)
|
||||
|
||||
@@ -20,7 +20,7 @@ max_active_file = 8
|
||||
cache_dir = "${workspace}/.clice/cache"
|
||||
# Directory for storing index files.
|
||||
index_dir = "${workspace}/.clice/index"
|
||||
logging_dir = "${workspace}/.clice/logging"
|
||||
logging_dir = "${workspace}/.clice/logs"
|
||||
# Compile commands files or directories to search for compile_commands.json files.
|
||||
compile_commands_paths = ["${workspace}/build"]
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ The worker pool (`src/server/worker_pool.cpp`) manages spawning and communicatin
|
||||
|
||||
### Communication
|
||||
|
||||
Workers communicate with the master via **stdio pipes** using a **bincode** serialization format (via `eventide::ipc::BincodePeer`). This is more compact and faster than JSON for internal IPC, while the master handles JSON for the external LSP protocol.
|
||||
Workers communicate with the master via **stdio pipes** using a **bincode** serialization format (via `kota::ipc::BincodePeer`). This is more compact and faster than JSON for internal IPC, while the master handles JSON for the external LSP protocol.
|
||||
|
||||
### Stateful Worker Routing
|
||||
|
||||
@@ -111,7 +111,7 @@ The stateful worker (`src/server/stateful_worker.cpp`) caches compiled ASTs in m
|
||||
- **Feature queries**: Look up the cached AST and invoke the corresponding `feature::*` function (hover, semantic tokens, etc.), serializing the result to JSON
|
||||
- **Document updates**: Received as notifications — the worker updates the stored text and marks the document as `dirty`, causing feature queries to return `null` until recompilation
|
||||
- **Eviction**: LRU-based; evicts the oldest document when capacity is exceeded, notifying the master
|
||||
- **Concurrency**: Each document has a per-document `et::mutex` (strand) to serialize compilation and feature queries. Heavy work (compilation, feature extraction) runs on a thread pool via `et::queue`.
|
||||
- **Concurrency**: Each document has a per-document `kota::mutex` (strand) to serialize compilation and feature queries. Heavy work (compilation, feature extraction) runs on a thread pool via `kota::queue`.
|
||||
|
||||
## Stateless Worker
|
||||
|
||||
@@ -123,7 +123,7 @@ The stateless worker (`src/server/stateless_worker.cpp`) handles one-shot reques
|
||||
- **Build PCM**: Compiles a C++20 module interface to a temporary file
|
||||
- **Index**: Compiles a file for indexing (TUIndex generation — currently a stub)
|
||||
|
||||
All requests are dispatched to a thread pool via `et::queue`.
|
||||
All requests are dispatched to a thread pool via `kota::queue`.
|
||||
|
||||
## Compile Graph
|
||||
|
||||
@@ -132,7 +132,7 @@ The compile graph (`src/server/compile_graph.cpp`) tracks compilation unit depen
|
||||
- **Registration**: Each file registers its included dependencies
|
||||
- **Cascade invalidation**: When a file changes, all transitive dependents are marked dirty and their ongoing compilations are cancelled
|
||||
- **Dependency compilation**: Before compiling a file, `compile_deps` ensures all dependencies (PCH, PCMs) are built first
|
||||
- **Cancellation**: Uses `et::cancellation_source` to abort in-flight compilations when files are invalidated
|
||||
- **Cancellation**: Uses `kota::cancellation_source` to abort in-flight compilations when files are invalidated
|
||||
|
||||
## Configuration
|
||||
|
||||
|
||||
@@ -32,18 +32,6 @@ pixi run integration-test Debug
|
||||
> [!TIP]
|
||||
> If you want to develop directly with `cmake`, `ninja`, `clang++`, etc., run `pixi shell` to enter a shell with all env vars configured.
|
||||
|
||||
### XMake
|
||||
|
||||
We also support building with XMake:
|
||||
|
||||
```shell
|
||||
# config & build (default releasedbg)
|
||||
pixi run xmake
|
||||
|
||||
# unit & integration
|
||||
pixi run xmake-test
|
||||
```
|
||||
|
||||
## Manual Build
|
||||
|
||||
If you plan to build manually, first ensure your toolchain matches the versions defined in `pixi.toml`.
|
||||
@@ -70,30 +58,13 @@ Optional build options:
|
||||
| CLICE_USE_LIBCXX | OFF | Build clice with libc++ (adds `-std=libc++`); if enabled, ensure the LLVM libs are also built with libc++ |
|
||||
| CLICE_CI_ENVIRONMENT | OFF | Enable the `CLICE_CI_ENVIRONMENT` macro; some tests only run in CI |
|
||||
|
||||
### XMake
|
||||
|
||||
Build clice with:
|
||||
|
||||
```bash
|
||||
xmake f -c --mode=releasedbg --toolchain=clang
|
||||
xmake build --all
|
||||
```
|
||||
|
||||
Optional build options:
|
||||
|
||||
| Option | Default | Effect |
|
||||
| ------------- | ------- | ---------------------------------------- |
|
||||
| --llvm | "" | Build clice with LLVM from a custom path |
|
||||
| --enable_test | false | Build clice unit tests |
|
||||
| --ci | false | Enable `CLICE_CI_ENVIRONMENT` |
|
||||
|
||||
## About LLVM
|
||||
|
||||
clice calls Clang APIs to parse C++ code, so it must link against LLVM/Clang. Because clice uses Clang's private headers (usually absent from distro packages), the system LLVM package cannot be used directly.
|
||||
|
||||
Two ways to satisfy this dependency:
|
||||
|
||||
1. We publish prebuilt binaries of the LLVM version we use at [clice-llvm](https://github.com/clice-io/clice-llvm/releases) for CI and release builds. During builds, cmake and xmake download these LLVM libs by default.
|
||||
1. We publish prebuilt binaries of the LLVM version we use at [clice-llvm](https://github.com/clice-io/clice-llvm/releases) for CI and release builds. During builds, cmake downloads these LLVM libs by default.
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
|
||||
@@ -18,13 +18,6 @@ We use pytest to run integration tests. Please refer to `pyproject.toml` to inst
|
||||
$ pytest -s --log-cli-level=INFO tests/integration --executable=./build/bin/clice
|
||||
```
|
||||
|
||||
If you use xmake as your build system, you can run the tests directly with xmake:
|
||||
|
||||
```shell
|
||||
$ xmake run --verbose unit_tests
|
||||
$ xmake test --verbose integration_tests/default
|
||||
```
|
||||
|
||||
## Debug
|
||||
|
||||
If you want to attach a debugger to clice for debugging, it is recommended to first start clice in socket mode independently, and then connect the client to it.
|
||||
|
||||
@@ -54,14 +54,73 @@ bazel run @hedron_compile_commands//:refresh_all
|
||||
|
||||
### Visual Studio
|
||||
|
||||
TODO:
|
||||
Visual Studio (2019 16.1+) can generate a compilation database via CMake integration. Open your project as a CMake project, then configure the generation in `CMakeSettings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"configurations": [
|
||||
{
|
||||
"name": "x64-Debug",
|
||||
"generator": "Ninja",
|
||||
"buildRoot": "${projectDir}\\build",
|
||||
"cmakeCommandArgs": "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Alternatively, for MSBuild-based projects (`.vcxproj`), you can use [compiledb-vs](https://github.com/pjbroad/compiledb-vs) or [catter](https://github.com/clice-io/catter) to generate the compilation database.
|
||||
|
||||
### Makefile
|
||||
|
||||
TODO:
|
||||
For Makefile-based projects, use [bear](https://github.com/rizsotto/Bear) to intercept compilation commands:
|
||||
|
||||
```bash
|
||||
bear -- make
|
||||
```
|
||||
|
||||
This will generate a `compile_commands.json` in the current directory. Note that `bear` requires a clean build to capture all commands — run `make clean` before `bear -- make` if needed.
|
||||
|
||||
Alternatively, if you use GNU Make, you can use [compiledb](https://github.com/nicktimko/compiledb):
|
||||
|
||||
```bash
|
||||
compiledb make
|
||||
```
|
||||
|
||||
### Meson
|
||||
|
||||
Meson generates a compilation database automatically during setup:
|
||||
|
||||
```bash
|
||||
meson setup build
|
||||
```
|
||||
|
||||
The `compile_commands.json` will be in the `build` directory.
|
||||
|
||||
### Xmake
|
||||
|
||||
Use one of the following approaches to generate a compilation database.
|
||||
|
||||
#### Command Line
|
||||
|
||||
Run the following command to manually generate a compilation database:
|
||||
|
||||
```bash
|
||||
xmake project -k compile_commands --lsp=clangd build
|
||||
```
|
||||
|
||||
> Compilation database generated manually doesn't automatically update itself. Re-generate if changes are made to the project.
|
||||
|
||||
#### VSCode Extension
|
||||
|
||||
The Xmake official VSCode extension automatically generates the compilation database when `xmake.lua` is updated. However, it generates the database to the `.vscode` directory by default. Add this setting in `settings.json`:
|
||||
|
||||
```json
|
||||
"xmake.compileCommandsDirectory": "build"
|
||||
```
|
||||
|
||||
to explicitly ask the extension to generate the compilation database in `build`.
|
||||
|
||||
### Others
|
||||
|
||||
For any other build system, you can try using [bear](https://github.com/rizsotto/Bear) or [scan-build](https://github.com/rizsotto/scan-build) to intercept compilation commands and obtain the compilation database (no guarantee of success). We plan to write a **new tool** in the future that captures compilation commands through a fake compiler approach.
|
||||
For any other build system, you can use [catter](https://github.com/clice-io/catter) to generate a compilation database. It captures compilation commands through a fake compiler approach and is designed to work reliably with any build system that invokes a compiler executable.
|
||||
|
||||
@@ -32,18 +32,6 @@ pixi run integration-test Debug
|
||||
> [!TIP]
|
||||
> 如果你想直接使用 `cmake`, `ninja`, `clang++` 等命令进行开发,请运行 `pixi shell` 进入已配置好环境变量的终端
|
||||
|
||||
### XMake
|
||||
|
||||
我们同样支持使用 XMake 构建:
|
||||
|
||||
```shell
|
||||
# config & build (default releasedbg)
|
||||
pixi run xmake
|
||||
|
||||
# unit & integration
|
||||
pixi run xmake-test
|
||||
```
|
||||
|
||||
## Manual Build
|
||||
|
||||
如果你打算手动构建,请务必先确认你的工具链满足 pixi.toml 中定义的版本要求。
|
||||
@@ -70,30 +58,13 @@ cmake -B build -G Ninja \
|
||||
| CLICE_USE_LIBCXX | OFF | 是否使用 libc++ 来构建 clice(添加 `-std=libc++`),如果开启,请确保 LLVM 库也是使用 libc++ 编译的 |
|
||||
| CLICE_CI_ENVIRONMENT | OFF | 是否打开 `CLICE_CI_ENVIRONMENT` 这个宏,有些测试在 CI 环境才会执行 |
|
||||
|
||||
### XMake
|
||||
|
||||
使用如下命令即可构建 clice:
|
||||
|
||||
```bash
|
||||
xmake f -c --mode=releasedbg --toolchain=clang
|
||||
xmake build --all
|
||||
```
|
||||
|
||||
可选的构建选项:
|
||||
|
||||
| 选项 | 默认值 | 效果 |
|
||||
| ------------- | ------ | ------------------------------------ |
|
||||
| --llvm | "" | 使用自定义路径的 LLVM 库来构建 clice |
|
||||
| --enable_test | false | 是否构建 clice 的单元测试 |
|
||||
| --ci | false | 是否打开 `CLICE_CI_ENVIRONMENT` |
|
||||
|
||||
## About LLVM
|
||||
|
||||
clice 调用 Clang API 来解析 C++ 代码,因此必须链接 LLVM/Clang 库。由于 clice 使用了 Clang 的私有头文件(这些文件通常不包含在发行版中),不能直接使用系统安装的 LLVM 包。
|
||||
|
||||
主要有两种方式解决这个依赖问题:
|
||||
|
||||
1. 我们在 [clice-llvm](https://github.com/clice-io/clice-llvm/releases) 上会发布使用的 LLVM 版本的预编译二进制,用于 CI 或者 release 构建。在构建时 cmake 和 xmake 默认会从此处下载 LLVM 库然后使用。
|
||||
1. 我们在 [clice-llvm](https://github.com/clice-io/clice-llvm/releases) 上会发布使用的 LLVM 版本的预编译二进制,用于 CI 或者 release 构建。在构建时 cmake 默认会从此处下载 LLVM 库然后使用。
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
|
||||
@@ -30,7 +30,7 @@ pixi run publish-vscode
|
||||
|
||||
1. `pixi shell -e node`
|
||||
2. 在 `editors/vscode` 下运行 `pnpm run watch`(增量构建)
|
||||
3. VSCode 中使用 “Run Extension/Launch Extension” 调试配置,或执行 `code --extensionDevelopmentPath=$(pwd)/editors/vscode`
|
||||
3. VSCode 中使用”Run Extension/Launch Extension”调试配置,或执行 `code --extensionDevelopmentPath=$(pwd)/editors/vscode`
|
||||
|
||||
常用脚本(在 `pixi shell -e node` 下):
|
||||
|
||||
|
||||
@@ -18,13 +18,6 @@ $ ./build/bin/unit_tests --test-dir="./tests/data"
|
||||
$ pytest -s --log-cli-level=INFO tests/integration --executable=./build/bin/clice
|
||||
```
|
||||
|
||||
如果你使用 xmake 作为构建系统,可以直接通过 xmake 运行测试:
|
||||
|
||||
```shell
|
||||
$ xmake run --verbose unit_tests
|
||||
$ xmake test --verbose integration_tests/default
|
||||
```
|
||||
|
||||
## Debug
|
||||
|
||||
如果想在 clice 上附加调试器并进行调试,推荐先单独以 socket 模式启动 clice,然后再将客户端连接到 clice 上
|
||||
|
||||
@@ -54,14 +54,73 @@ bazel run @hedron_compile_commands//:refresh_all
|
||||
|
||||
### Visual Studio
|
||||
|
||||
TODO:
|
||||
Visual Studio(2019 16.1+)可以通过 CMake 集成来生成编译数据库。将项目作为 CMake 项目打开,然后在 `CMakeSettings.json` 中配置:
|
||||
|
||||
```json
|
||||
{
|
||||
"configurations": [
|
||||
{
|
||||
"name": "x64-Debug",
|
||||
"generator": "Ninja",
|
||||
"buildRoot": "${projectDir}\\build",
|
||||
"cmakeCommandArgs": "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
对于基于 MSBuild 的项目(`.vcxproj`),可以使用 [compiledb-vs](https://github.com/pjbroad/compiledb-vs) 或 [catter](https://github.com/clice-io/catter) 来生成编译数据库。
|
||||
|
||||
### Makefile
|
||||
|
||||
TODO:
|
||||
对于基于 Makefile 的项目,使用 [bear](https://github.com/rizsotto/Bear) 来拦截编译命令:
|
||||
|
||||
```bash
|
||||
bear -- make
|
||||
```
|
||||
|
||||
这会在当前目录生成 `compile_commands.json`。注意 `bear` 需要干净的构建来捕获所有命令——如果需要的话,在运行 `bear -- make` 之前先执行 `make clean`。
|
||||
|
||||
另外,如果使用 GNU Make,也可以使用 [compiledb](https://github.com/nicktimko/compiledb):
|
||||
|
||||
```bash
|
||||
compiledb make
|
||||
```
|
||||
|
||||
### Meson
|
||||
|
||||
Meson 在配置阶段会自动生成编译数据库:
|
||||
|
||||
```bash
|
||||
meson setup build
|
||||
```
|
||||
|
||||
`compile_commands.json` 会生成在 `build` 目录下。
|
||||
|
||||
### Xmake
|
||||
|
||||
用下列任意方法生成编译数据库。
|
||||
|
||||
#### 命令行手动生成
|
||||
|
||||
在命令行中执行以下命令:
|
||||
|
||||
```bash
|
||||
xmake project -k compile_commands --lsp=clangd build
|
||||
```
|
||||
|
||||
> 通过这种方法生成的编译数据库无法自动更新,需要在项目编译配置更改时手动重新生成。
|
||||
|
||||
#### VSCode 扩展
|
||||
|
||||
Xmake 提供的官方 VSCode 扩展会在 `xmake.lua` 更新时自动生成编译数据库。然而默认情况下,它将编译数据库生成到了 `.vscode` 文件夹。在 `settings.json` 中添加以下配置:
|
||||
|
||||
```json
|
||||
"xmake.compileCommandsDirectory": "build"
|
||||
```
|
||||
|
||||
以将编译数据库的生成目录调整到 `build`,供 clice 使用。
|
||||
|
||||
### Others
|
||||
|
||||
对于任意其它的构建系统,可以尝试使用 [bear](https://github.com/rizsotto/Bear) 或者 [scan-build](https://github.com/rizsotto/scan-build) 来拦截编译命令并获取到编译数据库(不保证成功)。我们计划在未来编写一个**新的工具**,通过假编译器的方式来实现编译命令的捕获。
|
||||
对于任意其它的构建系统,可以使用 [catter](https://github.com/clice-io/catter) 来生成编译数据库。它通过伪装编译器的方式来捕获编译命令,能够可靠地与任何调用编译器可执行文件的构建系统配合工作。
|
||||
|
||||
9
editors/vscode/.vscode/extensions.json
vendored
9
editors/vscode/.vscode/extensions.json
vendored
@@ -1,9 +0,0 @@
|
||||
{
|
||||
// See http://go.microsoft.com/fwlink/?LinkId=827846
|
||||
// for the documentation about the extensions.json format
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"amodio.tsl-problem-matcher",
|
||||
"ms-vscode.extension-test-runner"
|
||||
]
|
||||
}
|
||||
23
editors/vscode/.vscode/launch.json
vendored
23
editors/vscode/.vscode/launch.json
vendored
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Run Extension (socket)",
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"args": ["--disable-extensions", "--extensionDevelopmentPath=${workspaceFolder}"],
|
||||
"env": { "CLICE_MODE": "socket" },
|
||||
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
|
||||
"preLaunchTask": "${defaultBuildTask}"
|
||||
},
|
||||
{
|
||||
"name": "Run Extension (pipe)",
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"args": ["--disable-extensions", "--extensionDevelopmentPath=${workspaceFolder}"],
|
||||
"env": { "CLICE_MODE": "pipe" },
|
||||
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
|
||||
"preLaunchTask": "${defaultBuildTask}"
|
||||
}
|
||||
]
|
||||
}
|
||||
13
editors/vscode/.vscode/settings.json
vendored
13
editors/vscode/.vscode/settings.json
vendored
@@ -1,13 +0,0 @@
|
||||
// Place your settings in this file to overwrite default and user settings.
|
||||
{
|
||||
"files.exclude": {
|
||||
"out": false, // set this to true to hide the "out" folder with the compiled JS files
|
||||
"dist": false // set this to true to hide the "dist" folder with the compiled JS files
|
||||
},
|
||||
"search.exclude": {
|
||||
"out": true, // set this to false to include "out" folder in search results
|
||||
"dist": true // set this to false to include "dist" folder in search results
|
||||
},
|
||||
// Turn off tsc task auto detection since we have the necessary tasks as npm scripts
|
||||
"typescript.tsc.autoDetect": "off"
|
||||
}
|
||||
37
editors/vscode/.vscode/tasks.json
vendored
37
editors/vscode/.vscode/tasks.json
vendored
@@ -1,37 +0,0 @@
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
// for the documentation about the tasks.json format
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "watch",
|
||||
"problemMatcher": "$ts-webpack-watch",
|
||||
"isBackground": true,
|
||||
"presentation": {
|
||||
"reveal": "never",
|
||||
"group": "watchers"
|
||||
},
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "watch-tests",
|
||||
"problemMatcher": "$tsc-watch",
|
||||
"isBackground": true,
|
||||
"presentation": {
|
||||
"reveal": "never",
|
||||
"group": "watchers"
|
||||
},
|
||||
"group": "build"
|
||||
},
|
||||
{
|
||||
"label": "tasks: watch-tests",
|
||||
"dependsOn": ["npm: watch", "npm: watch-tests"],
|
||||
"problemMatcher": []
|
||||
}
|
||||
]
|
||||
}
|
||||
239
pixi.lock
generated
239
pixi.lock
generated
@@ -18,6 +18,9 @@ environments:
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/ccache-4.13.2-hedf47ba_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/clang-20-20.1.8-default_h99862b1_5.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/clang-20.1.8-default_h36abe19_5.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/clang-format-20-20.1.8-default_h99862b1_14.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/clang-format-20.1.8-default_h99862b1_14.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/clang-tools-20.1.8-default_h57a47db_14.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/clangxx-20.1.8-default_h363a0c9_5.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/cmake-4.2.1-hc85cc9f_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/compiler-rt-20.1.8-hb700be7_1.conda
|
||||
@@ -32,6 +35,7 @@ environments:
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45-default_hbd61a6d_105.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/libclang-cpp20.1-20.1.8-default_h99862b1_5.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/libclang13-22.1.0-default_h746c552_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.17.0-h4e3cde8_1.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda
|
||||
@@ -44,6 +48,7 @@ environments:
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/libhiredis-1.3.0-h5888daf_1.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h3b78370_2.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/libllvm20-20.1.8-hf7376ad_1.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/libllvm22-22.1.1-hf7376ad_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.67.0-had1ee68_0.conda
|
||||
@@ -81,7 +86,7 @@ environments:
|
||||
- pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/fd/1a/208293b6c350f5abea6941d5606080d4a492644052504f5312e5de30a902/pygls-2.1.1-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl
|
||||
osx-arm64:
|
||||
@@ -91,6 +96,9 @@ environments:
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ccache-4.13.2-h414bf82_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-20-20.1.8-default_h73dfc95_5.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-20.1.8-default_hf9bcbb7_5.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-format-20-20.1.8-default_hf3020a7_14.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-format-20.1.8-default_hf3020a7_14.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-tools-20.1.8-default_h1589341_14.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/clangxx-20.1.8-default_h36137df_5.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/cmake-4.2.1-h54ad630_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/compiler-rt-20.1.8-h855ad52_1.conda
|
||||
@@ -98,6 +106,7 @@ environments:
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-78.1-h38cb7af_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.21.3-h237132a_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libclang-cpp20.1-20.1.8-default_h73dfc95_5.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libclang13-21.1.8-default_h13b06bd_3.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcurl-8.17.0-hdece5d2_1.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-21.1.8-hf598326_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-devel-20.1.8-h6dc3340_3.conda
|
||||
@@ -109,6 +118,7 @@ environments:
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libhiredis-1.3.0-h286801f_1.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libiconv-1.18-h23cfdf5_2.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libllvm20-20.1.8-h8e0c9ce_1.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libllvm21-21.1.8-h8e0c9ce_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.1-h39f12f2_2.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h5505292_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.67.0-hc438710_0.conda
|
||||
@@ -140,7 +150,7 @@ environments:
|
||||
- pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/fd/1a/208293b6c350f5abea6941d5606080d4a492644052504f5312e5de30a902/pygls-2.1.1-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl
|
||||
win-64:
|
||||
@@ -148,12 +158,15 @@ environments:
|
||||
- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.11.12-h4c7d964_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/clang-20-20.1.8-default_hac490eb_5.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/clang-20.1.8-default_hac490eb_5.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/clang-format-20.1.8-default_hac490eb_14.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/clang-tools-20.1.8-default_hac490eb_14.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/clangxx-20.1.8-default_hac490eb_5.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/cmake-4.2.1-hdcbee5b_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/compiler-rt-20.1.8-h49e36cd_1.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/noarch/compiler-rt_win-64-20.1.8-h49e36cd_1.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/icu-78.1-h637d24d_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.21.3-hdf4eb48_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/libclang13-22.1.0-default_ha2db4b5_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/libcurl-8.17.0-h43ecb02_1.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.7.3-hac47afa_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.5.2-h52bdfb6_0.conda
|
||||
@@ -190,7 +203,7 @@ environments:
|
||||
- pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/fd/1a/208293b6c350f5abea6941d5606080d4a492644052504f5312e5de30a902/pygls-2.1.1-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl
|
||||
format:
|
||||
@@ -370,6 +383,9 @@ environments:
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/ccache-4.13.2-hedf47ba_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/clang-20-20.1.8-default_h99862b1_5.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/clang-20.1.8-default_h36abe19_5.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/clang-format-20-20.1.8-default_h99862b1_14.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/clang-format-20.1.8-default_h99862b1_14.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/clang-tools-20.1.8-default_h57a47db_14.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/clangxx-20.1.8-default_h363a0c9_5.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/cmake-4.2.1-hc85cc9f_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/compiler-rt-20.1.8-hb700be7_1.conda
|
||||
@@ -383,6 +399,7 @@ environments:
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45-default_hbd61a6d_104.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/libclang-cpp20.1-20.1.8-default_h99862b1_5.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/libclang13-22.1.0-default_h746c552_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.17.0-h4e3cde8_1.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda
|
||||
@@ -395,6 +412,7 @@ environments:
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/libhiredis-1.3.0-h5888daf_1.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h3b78370_2.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/libllvm20-20.1.8-hf7376ad_1.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/libllvm22-22.1.1-hf7376ad_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-devel-5.8.1-hb9d3cd8_2.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda
|
||||
@@ -436,7 +454,7 @@ environments:
|
||||
- pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/fd/1a/208293b6c350f5abea6941d5606080d4a492644052504f5312e5de30a902/pygls-2.1.1-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl
|
||||
osx-arm64:
|
||||
@@ -446,12 +464,16 @@ environments:
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ccache-4.13.2-h414bf82_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-20-20.1.8-default_h73dfc95_5.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-20.1.8-default_hf9bcbb7_5.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-format-20-20.1.8-default_hf3020a7_14.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-format-20.1.8-default_hf3020a7_14.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-tools-20.1.8-default_h1589341_14.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/clangxx-20.1.8-default_h36137df_5.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/cmake-4.2.1-h54ad630_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/compiler-rt-20.1.8-h855ad52_1.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/noarch/compiler-rt_osx-arm64-20.1.8-he32a8d3_1.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.21.3-h237132a_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libclang-cpp20.1-20.1.8-default_h73dfc95_5.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libclang13-21.1.7-default_h13b06bd_4.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcurl-8.17.0-hdece5d2_1.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-21.1.7-hf598326_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-devel-20.1.8-h6dc3340_3.conda
|
||||
@@ -463,6 +485,7 @@ environments:
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libhiredis-1.3.0-h286801f_1.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libiconv-1.18-h23cfdf5_2.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libllvm20-20.1.8-h8e0c9ce_1.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libllvm21-21.1.8-h8e0c9ce_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.1-h39f12f2_2.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-devel-5.8.1-h39f12f2_2.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h5505292_0.conda
|
||||
@@ -498,7 +521,7 @@ environments:
|
||||
- pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/fd/1a/208293b6c350f5abea6941d5606080d4a492644052504f5312e5de30a902/pygls-2.1.1-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl
|
||||
win-64:
|
||||
@@ -506,11 +529,14 @@ environments:
|
||||
- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.11.12-h4c7d964_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/clang-20-20.1.8-default_hac490eb_5.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/clang-20.1.8-default_hac490eb_5.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/clang-format-20.1.8-default_hac490eb_14.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/clang-tools-20.1.8-default_hac490eb_14.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/clangxx-20.1.8-default_hac490eb_5.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/cmake-4.2.1-hdcbee5b_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/compiler-rt-20.1.8-h49e36cd_1.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/noarch/compiler-rt_win-64-20.1.8-h49e36cd_1.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.21.3-hdf4eb48_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/libclang13-22.1.0-default_ha2db4b5_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/libcurl-8.17.0-h43ecb02_1.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.7.3-hac47afa_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.5.2-h52bdfb6_0.conda
|
||||
@@ -550,7 +576,7 @@ environments:
|
||||
- pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/fd/1a/208293b6c350f5abea6941d5606080d4a492644052504f5312e5de30a902/pygls-2.1.1-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl
|
||||
packages:
|
||||
@@ -803,6 +829,21 @@ packages:
|
||||
purls: []
|
||||
size: 73427981
|
||||
timestamp: 1764398987103
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/clang-format-20.1.8-default_h99862b1_14.conda
|
||||
sha256: b58148bb200231f21a0a78e90456414be7de8dd4fc115ebdf6e9d808d151e9e6
|
||||
md5: de9baf19d75c9ca7b4e88f5609db0347
|
||||
depends:
|
||||
- __glibc >=2.17,<3.0.a0
|
||||
- clang-format-20 20.1.8 default_h99862b1_14
|
||||
- libclang-cpp20.1 >=20.1.8,<20.2.0a0
|
||||
- libgcc >=14
|
||||
- libllvm20 >=20.1.8,<20.2.0a0
|
||||
- libstdcxx >=14
|
||||
license: Apache-2.0 WITH LLVM-exception
|
||||
license_family: Apache
|
||||
purls: []
|
||||
size: 71009
|
||||
timestamp: 1773111214926
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/clang-format-21.1.7-default_h99862b1_2.conda
|
||||
sha256: 8a9c2e0df0fd55da3ea9352fc356d1ca9d9312bbf275e1a5e22552bfb63affb9
|
||||
md5: 8e0b6ced8948217e4748ad0c26f8df4d
|
||||
@@ -817,6 +858,20 @@ packages:
|
||||
license_family: Apache
|
||||
size: 27733
|
||||
timestamp: 1766016586518
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-format-20.1.8-default_hf3020a7_14.conda
|
||||
sha256: 0967d59e8a89c5f9c1cfa3579403ee3295f5770c01b308c9abb434df47acc5db
|
||||
md5: 86639340249e4d8fa1b37ab92e581d26
|
||||
depends:
|
||||
- __osx >=11.0
|
||||
- clang-format-20 20.1.8 default_hf3020a7_14
|
||||
- libclang-cpp20.1 >=20.1.8,<20.2.0a0
|
||||
- libcxx >=20.1.8
|
||||
- libllvm20 >=20.1.8,<20.2.0a0
|
||||
license: Apache-2.0 WITH LLVM-exception
|
||||
license_family: Apache
|
||||
purls: []
|
||||
size: 71355
|
||||
timestamp: 1773110589745
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-format-21.1.7-default_hf3020a7_2.conda
|
||||
sha256: 2b91ed7e567a1618fd342100ba43bcd670bff8a1183a5c65032cefc0a436d3d6
|
||||
md5: 37585890edaa4842a62037a7fa6e1e64
|
||||
@@ -830,6 +885,18 @@ packages:
|
||||
license_family: Apache
|
||||
size: 28328
|
||||
timestamp: 1766167306042
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/clang-format-20.1.8-default_hac490eb_14.conda
|
||||
sha256: 7c657222c2b310387e11a316bf835910181c04ed54f2e35e28885cc5423c8496
|
||||
md5: 188355e2c07643a0232b9b817c230a36
|
||||
depends:
|
||||
- ucrt >=10.0.20348.0
|
||||
- vc >=14.3,<15
|
||||
- vc14_runtime >=14.44.35208
|
||||
license: Apache-2.0 WITH LLVM-exception
|
||||
license_family: Apache
|
||||
purls: []
|
||||
size: 1265666
|
||||
timestamp: 1773142030144
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/clang-format-21.1.7-default_hac490eb_2.conda
|
||||
sha256: afa22671525e890a98445d1c2abc35e8b87c4c208d053e6527c5d2cbf6f7c938
|
||||
md5: 8afc1288250ef6c77e8f7aa0df683a65
|
||||
@@ -841,6 +908,33 @@ packages:
|
||||
license_family: Apache
|
||||
size: 1233929
|
||||
timestamp: 1766022144370
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/clang-format-20-20.1.8-default_h99862b1_14.conda
|
||||
sha256: 17b84830ca43b9db271b50d8474a4231204321016f9ff5c2c690dddeb0d66f28
|
||||
md5: 55910d2902458004d9c4e443648c5841
|
||||
depends:
|
||||
- __glibc >=2.17,<3.0.a0
|
||||
- libclang-cpp20.1 >=20.1.8,<20.2.0a0
|
||||
- libgcc >=14
|
||||
- libllvm20 >=20.1.8,<20.2.0a0
|
||||
- libstdcxx >=14
|
||||
license: Apache-2.0 WITH LLVM-exception
|
||||
license_family: Apache
|
||||
purls: []
|
||||
size: 114295
|
||||
timestamp: 1773111143555
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-format-20-20.1.8-default_hf3020a7_14.conda
|
||||
sha256: 52f77009e2f69db348c10cb20371e0b1bf6f49e2e91e026d5fb93addc6526ef6
|
||||
md5: 2250c216f6da8f1f875f2b6a794d5e67
|
||||
depends:
|
||||
- __osx >=11.0
|
||||
- libclang-cpp20.1 >=20.1.8,<20.2.0a0
|
||||
- libcxx >=20.1.8
|
||||
- libllvm20 >=20.1.8,<20.2.0a0
|
||||
license: Apache-2.0 WITH LLVM-exception
|
||||
license_family: Apache
|
||||
purls: []
|
||||
size: 108365
|
||||
timestamp: 1773110420001
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/clang-format-21-21.1.7-default_h99862b1_2.conda
|
||||
sha256: 4d360c464ddab6c47c8fceca87b625b58d6a3b3cd523958a314e8db7d5f4eb94
|
||||
md5: 5589c06b39ae5be8aca4d16e28a43518
|
||||
@@ -866,6 +960,65 @@ packages:
|
||||
license_family: Apache
|
||||
size: 65217
|
||||
timestamp: 1766167190049
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/clang-tools-20.1.8-default_h57a47db_14.conda
|
||||
sha256: 565bbd489160501c5374fe346dca83a6ec4dac23aecc5bc48c276fe47ca2c263
|
||||
md5: 897e3de8e6310eb23d01004907520513
|
||||
depends:
|
||||
- __glibc >=2.17,<3.0.a0
|
||||
- clang-format 20.1.8 default_h99862b1_14
|
||||
- libclang-cpp20.1 >=20.1.8,<20.2.0a0
|
||||
- libclang13 >=20.1.8
|
||||
- libgcc >=14
|
||||
- libllvm20 >=20.1.8,<20.2.0a0
|
||||
- libstdcxx >=14
|
||||
- libxml2
|
||||
- libxml2-16 >=2.14.6
|
||||
constrains:
|
||||
- clangdev 20.1.8
|
||||
license: Apache-2.0 WITH LLVM-exception
|
||||
license_family: Apache
|
||||
purls: []
|
||||
size: 21897132
|
||||
timestamp: 1773111341498
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-tools-20.1.8-default_h1589341_14.conda
|
||||
sha256: 372c9e722fd99e42838528d33148ca1fa7dd84a02cf06ac7d291657f657e7026
|
||||
md5: 8a25b0558245abf052a89a38eb8dbb58
|
||||
depends:
|
||||
- __osx >=11.0
|
||||
- clang-format 20.1.8 default_hf3020a7_14
|
||||
- libclang-cpp20.1 >=20.1.8,<20.2.0a0
|
||||
- libclang13 >=20.1.8
|
||||
- libcxx >=20.1.8
|
||||
- libllvm20 >=20.1.8,<20.2.0a0
|
||||
- libxml2
|
||||
- libxml2-16 >=2.14.6
|
||||
constrains:
|
||||
- clangdev 20.1.8
|
||||
license: Apache-2.0 WITH LLVM-exception
|
||||
license_family: Apache
|
||||
purls: []
|
||||
size: 14941399
|
||||
timestamp: 1773110809290
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/clang-tools-20.1.8-default_hac490eb_14.conda
|
||||
sha256: 25b5962d5e46c01edd9e91b0092a1cbe840276cfce6e0ccd8752ff9d21deb0fb
|
||||
md5: 7f081578424e00ff4724ded6228f1be8
|
||||
depends:
|
||||
- clang-format 20.1.8 default_hac490eb_14
|
||||
- libclang13 >=20.1.8
|
||||
- libxml2
|
||||
- libxml2-16 >=2.14.6
|
||||
- libzlib >=1.3.1,<2.0a0
|
||||
- ucrt >=10.0.20348.0
|
||||
- vc >=14.3,<15
|
||||
- vc14_runtime >=14.44.35208
|
||||
- zstd >=1.5.7,<1.6.0a0
|
||||
constrains:
|
||||
- clangdev 20.1.8
|
||||
license: Apache-2.0 WITH LLVM-exception
|
||||
license_family: Apache
|
||||
purls: []
|
||||
size: 319341053
|
||||
timestamp: 1773142869937
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/clangxx-20.1.8-default_h363a0c9_5.conda
|
||||
sha256: d30843fc1012ff5fb6c95e07c15182d22f03a37c380ae350ba069adf75092828
|
||||
md5: 692c84fedd1a3302ff26dbc9a211f626
|
||||
@@ -1386,6 +1539,57 @@ packages:
|
||||
license_family: Apache
|
||||
size: 13672685
|
||||
timestamp: 1766166376555
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/libclang13-22.1.0-default_h746c552_0.conda
|
||||
sha256: 4a9dd814492a129f2ff40cd4ab0b942232c9e3c6dbc0d0aaf861f1f65e99cc7d
|
||||
md5: 140459a7413d8f6884eb68205ce39a0d
|
||||
depends:
|
||||
- __glibc >=2.17,<3.0.a0
|
||||
- libgcc >=14
|
||||
- libllvm22 >=22.1.0,<22.2.0a0
|
||||
- libstdcxx >=14
|
||||
license: Apache-2.0 WITH LLVM-exception
|
||||
license_family: Apache
|
||||
purls: []
|
||||
size: 12817500
|
||||
timestamp: 1772101411287
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libclang13-21.1.7-default_h13b06bd_4.conda
|
||||
sha256: ed55559f8af599838de775c099fbf644c19d1720b9f57064a2abb0ab2ff99765
|
||||
md5: e886d09eb4ae02f446edd4c32c351ca6
|
||||
depends:
|
||||
- __osx >=11.0
|
||||
- libcxx >=21.1.7
|
||||
- libllvm21 >=21.1.7,<21.2.0a0
|
||||
license: Apache-2.0 WITH LLVM-exception
|
||||
license_family: Apache
|
||||
purls: []
|
||||
size: 8517139
|
||||
timestamp: 1767751023683
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libclang13-21.1.8-default_h13b06bd_3.conda
|
||||
sha256: dc803069f4001aae4bdd6069af20140785a9c1a0c9abe6a3704e0ef87ad0f4c2
|
||||
md5: d111e982033ec02a55845e994a6db09a
|
||||
depends:
|
||||
- __osx >=11.0
|
||||
- libcxx >=21.1.8
|
||||
- libllvm21 >=21.1.8,<21.2.0a0
|
||||
license: Apache-2.0 WITH LLVM-exception
|
||||
license_family: Apache
|
||||
purls: []
|
||||
size: 8516482
|
||||
timestamp: 1771032974531
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/libclang13-22.1.0-default_ha2db4b5_0.conda
|
||||
sha256: c8e34362c6bf7305ef50f0de4e16292cd97e31650ab6465282eeeac62f0a05c4
|
||||
md5: 7ad437870ea7d487e1b0e663503b6b1d
|
||||
depends:
|
||||
- libzlib >=1.3.1,<2.0a0
|
||||
- ucrt >=10.0.20348.0
|
||||
- vc >=14.3,<15
|
||||
- vc14_runtime >=14.44.35208
|
||||
- zstd >=1.5.7,<1.6.0a0
|
||||
license: Apache-2.0 WITH LLVM-exception
|
||||
license_family: Apache
|
||||
purls: []
|
||||
size: 30584641
|
||||
timestamp: 1772353741135
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.17.0-h4e3cde8_1.conda
|
||||
sha256: 2d7be2fe0f58a0945692abee7bb909f8b19284b518d958747e5ff51d0655c303
|
||||
md5: 117499f93e892ea1e57fdca16c2e8351
|
||||
@@ -1761,8 +1965,25 @@ packages:
|
||||
- zstd >=1.5.7,<1.6.0a0
|
||||
license: Apache-2.0 WITH LLVM-exception
|
||||
license_family: Apache
|
||||
purls: []
|
||||
size: 29398498
|
||||
timestamp: 1765924904821
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/libllvm22-22.1.1-hf7376ad_0.conda
|
||||
sha256: 1145f9e85f0fbbdba88f1da5c8c48672bee7702e2f40c563b2dd48350ab4d413
|
||||
md5: 97cc6dad22677304846a798c8a65064d
|
||||
depends:
|
||||
- __glibc >=2.17,<3.0.a0
|
||||
- libgcc >=14
|
||||
- libstdcxx >=14
|
||||
- libxml2
|
||||
- libxml2-16 >=2.14.6
|
||||
- libzlib >=1.3.1,<2.0a0
|
||||
- zstd >=1.5.7,<1.6.0a0
|
||||
license: Apache-2.0 WITH LLVM-exception
|
||||
license_family: Apache
|
||||
purls: []
|
||||
size: 44256563
|
||||
timestamp: 1773371774629
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda
|
||||
sha256: f2591c0069447bbe28d4d696b7fcb0c5bd0b4ac582769b89addbcf26fb3430d8
|
||||
md5: 1a580f7796c7bf6393fddb8bbbde58dc
|
||||
@@ -2837,10 +3058,10 @@ packages:
|
||||
requires_dist:
|
||||
- colorama>=0.4.6 ; extra == 'windows-terminal'
|
||||
requires_python: '>=3.9'
|
||||
- pypi: https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl
|
||||
name: pytest
|
||||
version: 9.0.2
|
||||
sha256: 711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b
|
||||
version: 9.0.3
|
||||
sha256: 2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9
|
||||
requires_dist:
|
||||
- colorama>=0.4 ; sys_platform == 'win32'
|
||||
- exceptiongroup>=1 ; python_full_version < '3.11'
|
||||
|
||||
64
pixi.toml
64
pixi.toml
@@ -33,6 +33,7 @@ clang = "==20.1.8"
|
||||
clangxx = "==20.1.8"
|
||||
lld = "==20.1.8"
|
||||
llvm-tools = "==20.1.8"
|
||||
clang-tools = "==20.1.8"
|
||||
compiler-rt = "==20.1.8"
|
||||
|
||||
[feature.build.target.win-64.dependencies]
|
||||
@@ -62,6 +63,15 @@ lsprotocol = ">=2024.0.0"
|
||||
[feature.package.dependencies]
|
||||
xz = ">=5.8.1,<6"
|
||||
|
||||
[feature.package.tasks.package]
|
||||
cmd = """
|
||||
cmake -B build/RelWithDebInfo -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
|
||||
-DCMAKE_TOOLCHAIN_FILE=cmake/toolchain.cmake \
|
||||
-DCLICE_RELEASE=ON && \
|
||||
cmake --build build/RelWithDebInfo
|
||||
"""
|
||||
|
||||
# ============================================================================== #
|
||||
# CMAKE #
|
||||
# ============================================================================== #
|
||||
@@ -94,6 +104,10 @@ depends-on = [
|
||||
{ task = "cmake-build", args = ["{{ type }}"] },
|
||||
]
|
||||
|
||||
[feature.build.tasks.clang-tidy]
|
||||
args = [{ arg = "type", default = "RelWithDebInfo" }]
|
||||
depends-on = [{ task = "lint-cpp", args = ["{{ type }}"] }]
|
||||
|
||||
[feature.build.tasks.unit-test]
|
||||
args = [{ arg = "type", default = "RelWithDebInfo" }]
|
||||
cmd = './build/{{ type }}/bin/unit_tests --test-dir="./tests/data"'
|
||||
@@ -119,39 +133,6 @@ depends-on = [
|
||||
{ task = "integration-test", args = ["{{ type }}"] },
|
||||
]
|
||||
|
||||
# ============================================================================== #
|
||||
# XMAKE #
|
||||
# ============================================================================== #
|
||||
[feature.build.tasks.xmake-config]
|
||||
args = [
|
||||
{ arg = "type", default = "releasedbg" },
|
||||
{ arg = "ci", default = "n" },
|
||||
]
|
||||
cmd = "xmake config --yes --mode={{ type }} --toolchain=clang --ci={{ ci }}"
|
||||
|
||||
[feature.build.tasks.xmake-build]
|
||||
cmd = "xmake build --verbose --diagnosis --all"
|
||||
|
||||
[feature.build.tasks.xmake]
|
||||
args = [
|
||||
{ arg = "type", default = "releasedbg" },
|
||||
{ arg = "ci", default = "n" },
|
||||
]
|
||||
depends-on = [
|
||||
{ task = "xmake-config", args = ["{{ type }}", "{{ ci }}"] },
|
||||
{ task = "xmake-build" },
|
||||
]
|
||||
|
||||
[feature.test.tasks.xmake-test]
|
||||
cmd = "xmake test --verbose"
|
||||
|
||||
[feature.package.tasks.package]
|
||||
cmd = """
|
||||
xmake config --yes --toolchain=clang --mode=releasedbg \
|
||||
--enable_test=n --dev=n --release=y && \
|
||||
xmake pack --verbose
|
||||
"""
|
||||
|
||||
# ============================================================================== #
|
||||
# HELPER TASKS #
|
||||
# ============================================================================== #
|
||||
@@ -229,3 +210,20 @@ format = { depends-on = [
|
||||
"format-toml",
|
||||
"format-yaml"
|
||||
] }
|
||||
|
||||
# ============================================================================== #
|
||||
# LINT #
|
||||
# ============================================================================== #
|
||||
[feature.format.tasks.lint-python]
|
||||
cmd = "ruff check ."
|
||||
|
||||
[feature.build.tasks.lint-cpp]
|
||||
args = [{ arg = "type", default = "RelWithDebInfo" }]
|
||||
cmd = "python scripts/run_clang_tidy.py build/{{ type }}"
|
||||
|
||||
[feature.build.tasks.lint]
|
||||
args = [{ arg = "type", default = "RelWithDebInfo" }]
|
||||
depends-on = [
|
||||
"lint-python",
|
||||
{ task = "lint-cpp", args = ["{{ type }}"] },
|
||||
]
|
||||
|
||||
66
scripts/run_clang_tidy.py
Normal file
66
scripts/run_clang_tidy.py
Normal file
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run clang-tidy in parallel on all files in compile_commands.json."""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def main():
|
||||
build_dir = sys.argv[1] if len(sys.argv) > 1 else "build/RelWithDebInfo"
|
||||
cdb_path = Path(build_dir) / "compile_commands.json"
|
||||
|
||||
if not cdb_path.exists():
|
||||
print(f"Error: {cdb_path} not found. Run cmake-config first.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
project_root = Path(__file__).resolve().parent.parent
|
||||
src_dirs = (project_root / "src", project_root / "tests")
|
||||
|
||||
cdb = json.loads(cdb_path.read_text())
|
||||
files = [
|
||||
entry["file"]
|
||||
for entry in cdb
|
||||
if any(Path(entry["file"]).resolve().is_relative_to(d) for d in src_dirs)
|
||||
]
|
||||
total = len(files)
|
||||
|
||||
lock = threading.Lock()
|
||||
done = 0
|
||||
failed = []
|
||||
|
||||
def run(file: str) -> tuple[str, int, str]:
|
||||
result = subprocess.run(
|
||||
["clang-tidy", "-p", build_dir, "--quiet", file],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
return file, result.returncode, result.stdout + result.stderr
|
||||
|
||||
with ThreadPoolExecutor() as pool:
|
||||
futures = {pool.submit(run, f): f for f in files}
|
||||
for future in as_completed(futures):
|
||||
file, code, output = future.result()
|
||||
with lock:
|
||||
done += 1
|
||||
name = Path(file).name
|
||||
if code != 0:
|
||||
failed.append(file)
|
||||
print(f"[{done}/{total}] FAIL {name}")
|
||||
if output.strip():
|
||||
print(output, end="")
|
||||
else:
|
||||
print(f"[{done}/{total}] OK {name}")
|
||||
|
||||
if failed:
|
||||
print(f"\nclang-tidy failed on {len(failed)}/{total} files.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print(f"\nclang-tidy passed on {total} files.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
121
src/clice.cc
121
src/clice.cc
@@ -1,64 +1,81 @@
|
||||
#include <csignal>
|
||||
#include <cstdint>
|
||||
#include <iostream>
|
||||
#include <print>
|
||||
#include <string>
|
||||
|
||||
#include "eventide/async/async.h"
|
||||
#include "eventide/deco/deco.h"
|
||||
#include "eventide/ipc/peer.h"
|
||||
#include "eventide/ipc/recording_transport.h"
|
||||
#include "eventide/ipc/transport.h"
|
||||
#include "server/master_server.h"
|
||||
#include "server/stateful_worker.h"
|
||||
#include "server/stateless_worker.h"
|
||||
#include "support/logging.h"
|
||||
|
||||
#include "kota/async/async.h"
|
||||
#include "kota/deco/deco.h"
|
||||
#include "kota/ipc/codec/json.h"
|
||||
#include "kota/ipc/peer.h"
|
||||
#include "kota/ipc/recording_transport.h"
|
||||
#include "kota/ipc/transport.h"
|
||||
|
||||
namespace clice {
|
||||
|
||||
using kota::deco::decl::KVStyle;
|
||||
|
||||
struct Options {
|
||||
DecoKV(names = {"--mode"};
|
||||
help = "Running mode: pipe, socket, stateless-worker, stateful-worker";
|
||||
required = false;)
|
||||
DecoKV(style = KVStyle::JoinedOrSeparate,
|
||||
help = "Running mode: pipe, socket, stateless-worker, stateful-worker",
|
||||
required = false)
|
||||
<std::string> mode;
|
||||
|
||||
DecoKV(names = {"--host"}; help = "Socket mode address"; required = false;)
|
||||
DecoKV(style = KVStyle::JoinedOrSeparate, help = "Socket mode address", required = false)
|
||||
<std::string> host = "127.0.0.1";
|
||||
|
||||
DecoKV(names = {"--port"}; help = "Socket mode port"; required = false;)
|
||||
DecoKV(style = KVStyle::JoinedOrSeparate, help = "Socket mode port", required = false)
|
||||
<int> port = 50051;
|
||||
|
||||
DecoKV(names = {"--stateful-worker-count"}; help = "Number of stateful workers";
|
||||
required = false;)
|
||||
<std::uint32_t> stateful_worker_count;
|
||||
|
||||
DecoKV(names = {"--stateless-worker-count"}; help = "Number of stateless workers";
|
||||
required = false;)
|
||||
<std::uint32_t> stateless_worker_count;
|
||||
|
||||
DecoKV(names = {"--worker-memory-limit"}; help = "Memory limit per stateful worker (bytes)";
|
||||
required = false;)
|
||||
<std::uint64_t> worker_memory_limit;
|
||||
|
||||
DecoKV(names = {"--log-level"}; help = "Log level: trace, debug, info, warn, error, off";
|
||||
required = false;)
|
||||
DecoKV(style = KVStyle::JoinedOrSeparate,
|
||||
names = {"--log-level", "--log-level="},
|
||||
help = "Log level: trace, debug, info, warn, error, off",
|
||||
required = false)
|
||||
<std::string> log_level = "info";
|
||||
|
||||
DecoKV(names = {"--record"}; help = "Record LSP input to file for replay testing";
|
||||
required = false;)
|
||||
DecoKV(style = KVStyle::JoinedOrSeparate,
|
||||
help = "Record LSP input to file for replay testing",
|
||||
required = false)
|
||||
<std::string> record;
|
||||
|
||||
DecoFlag(names = {"-h", "--help"}; help = "Show help message"; required = false;)
|
||||
// Internal options (passed from master to worker processes)
|
||||
DecoKV(style = KVStyle::JoinedOrSeparate,
|
||||
names = {"--worker-memory-limit", "--worker-memory-limit="},
|
||||
required = false)
|
||||
<std::uint64_t> worker_memory_limit;
|
||||
|
||||
DecoKV(style = KVStyle::JoinedOrSeparate,
|
||||
names = {"--worker-name", "--worker-name="},
|
||||
required = false)
|
||||
<std::string> worker_name;
|
||||
|
||||
DecoKV(style = KVStyle::JoinedOrSeparate, names = {"--log-dir", "--log-dir="}, required = false)
|
||||
<std::string> log_dir;
|
||||
|
||||
DecoFlag(names = {"-h", "--help"}, help = "Show help message", required = false)
|
||||
help;
|
||||
|
||||
DecoFlag(names = {"-v", "--version"}; help = "Show version"; required = false;)
|
||||
DecoFlag(names = {"-v", "--version"}, help = "Show version", required = false)
|
||||
version;
|
||||
};
|
||||
|
||||
} // namespace clice
|
||||
|
||||
int main(int argc, const char** argv) {
|
||||
auto args = deco::util::argvify(argc, argv);
|
||||
auto result = deco::cli::parse<clice::Options>(args);
|
||||
#ifndef _WIN32
|
||||
// On POSIX systems, ignore SIGPIPE so that writing to a closed pipe
|
||||
// (e.g. when the LSP client disconnects) returns EPIPE instead of
|
||||
// killing the process. This is standard practice for pipe-based servers.
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
#endif
|
||||
|
||||
auto args = kota::deco::util::argvify(argc, argv);
|
||||
auto result = kota::deco::cli::parse<clice::Options>(args);
|
||||
|
||||
if(!result.has_value()) {
|
||||
LOG_ERROR("{}", result.error().message);
|
||||
@@ -68,7 +85,7 @@ int main(int argc, const char** argv) {
|
||||
auto& opts = result->options;
|
||||
|
||||
if(opts.help.value_or(false)) {
|
||||
deco::cli::write_usage_for<clice::Options>(std::cout, "clice [OPTIONS]");
|
||||
kota::deco::cli::write_usage_for<clice::Options>(std::cout, "clice [OPTIONS]");
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -97,35 +114,42 @@ int main(int argc, const char** argv) {
|
||||
|
||||
auto& mode = *opts.mode;
|
||||
|
||||
auto worker_name = opts.worker_name.value_or("");
|
||||
auto log_dir = opts.log_dir.value_or("");
|
||||
|
||||
if(mode == "stateless-worker") {
|
||||
return clice::run_stateless_worker_mode();
|
||||
return clice::run_stateless_worker_mode(worker_name.empty() ? "stateless-worker"
|
||||
: worker_name,
|
||||
log_dir);
|
||||
}
|
||||
|
||||
if(mode == "stateful-worker") {
|
||||
auto mem_limit = opts.worker_memory_limit.value_or(4ULL * 1024 * 1024 * 1024);
|
||||
return clice::run_stateful_worker_mode(mem_limit);
|
||||
return clice::run_stateful_worker_mode(mem_limit,
|
||||
worker_name.empty() ? "stateful-worker"
|
||||
: worker_name,
|
||||
log_dir);
|
||||
}
|
||||
|
||||
if(mode == "pipe") {
|
||||
clice::logging::stderr_logger("master", clice::logging::options);
|
||||
|
||||
namespace et = eventide;
|
||||
et::event_loop loop;
|
||||
kota::event_loop loop;
|
||||
|
||||
auto transport = et::ipc::StreamTransport::open_stdio(loop);
|
||||
auto transport = kota::ipc::StreamTransport::open_stdio(loop);
|
||||
if(!transport) {
|
||||
LOG_ERROR("failed to open stdio transport");
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::unique_ptr<et::ipc::Transport> final_transport = std::move(*transport);
|
||||
std::unique_ptr<kota::ipc::Transport> final_transport = std::move(*transport);
|
||||
if(opts.record.has_value()) {
|
||||
final_transport =
|
||||
std::make_unique<et::ipc::RecordingTransport>(std::move(final_transport),
|
||||
*opts.record);
|
||||
std::make_unique<kota::ipc::RecordingTransport>(std::move(final_transport),
|
||||
*opts.record);
|
||||
}
|
||||
|
||||
et::ipc::JsonPeer peer(loop, std::move(final_transport));
|
||||
kota::ipc::JsonPeer peer(loop, std::move(final_transport));
|
||||
clice::MasterServer server(loop, peer, std::move(self_path));
|
||||
server.register_handlers();
|
||||
|
||||
@@ -137,13 +161,12 @@ int main(int argc, const char** argv) {
|
||||
if(mode == "socket") {
|
||||
clice::logging::stderr_logger("master", clice::logging::options);
|
||||
|
||||
namespace et = eventide;
|
||||
et::event_loop loop;
|
||||
kota::event_loop loop;
|
||||
|
||||
auto host = opts.host.value_or("127.0.0.1");
|
||||
auto port = opts.port.value_or(50051);
|
||||
|
||||
auto acceptor = et::tcp::listen(host, port, {}, loop);
|
||||
auto acceptor = kota::tcp::listen(host, port, {}, loop);
|
||||
if(!acceptor) {
|
||||
LOG_ERROR("failed to listen on {}:{}", host, port);
|
||||
return 1;
|
||||
@@ -151,7 +174,7 @@ int main(int argc, const char** argv) {
|
||||
|
||||
LOG_INFO("Listening on {}:{} ...", host, port);
|
||||
|
||||
auto task = [&]() -> et::task<> {
|
||||
auto task = [&]() -> kota::task<> {
|
||||
auto client = co_await acceptor->accept();
|
||||
if(!client.has_value()) {
|
||||
LOG_ERROR("failed to accept connection");
|
||||
@@ -161,13 +184,13 @@ int main(int argc, const char** argv) {
|
||||
|
||||
LOG_INFO("Client connected");
|
||||
|
||||
std::unique_ptr<et::ipc::Transport> transport =
|
||||
std::make_unique<et::ipc::StreamTransport>(std::move(client.value()));
|
||||
std::unique_ptr<kota::ipc::Transport> transport =
|
||||
std::make_unique<kota::ipc::StreamTransport>(std::move(client.value()));
|
||||
if(opts.record.has_value()) {
|
||||
transport = std::make_unique<et::ipc::RecordingTransport>(std::move(transport),
|
||||
*opts.record);
|
||||
transport = std::make_unique<kota::ipc::RecordingTransport>(std::move(transport),
|
||||
*opts.record);
|
||||
}
|
||||
et::ipc::JsonPeer peer(loop, std::move(transport));
|
||||
kota::ipc::JsonPeer peer(loop, std::move(transport));
|
||||
clice::MasterServer server(loop, peer, std::string(self_path));
|
||||
server.register_handlers();
|
||||
|
||||
|
||||
@@ -23,6 +23,44 @@ namespace ranges = std::ranges;
|
||||
|
||||
} // namespace
|
||||
|
||||
std::vector<const char*> CompileCommand::to_argv() const {
|
||||
std::vector<const char*> argv;
|
||||
argv.reserve(resolved.flags.size() + 4);
|
||||
|
||||
if(resolved.is_cc1 && source_file) {
|
||||
// cc1 mode requires TWO file-related arguments (both are needed):
|
||||
// 1. -main-file-name <basename> — used by clang for diagnostics/debug info
|
||||
// 2. <source_file> at the end — the actual input file path
|
||||
// These are NOT duplicates: (1) is just the basename, (2) is the full path.
|
||||
for(std::size_t i = 0; i < resolved.flags.size(); ++i) {
|
||||
argv.push_back(resolved.flags[i]);
|
||||
if(resolved.flags[i] == llvm::StringRef("-cc1")) {
|
||||
argv.push_back("-main-file-name");
|
||||
// path::filename returns a suffix of source_file (a pointer into
|
||||
// the same buffer), so .data() is null-terminated because source_file is.
|
||||
argv.push_back(path::filename(source_file).data());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
argv.insert(argv.end(), resolved.flags.begin(), resolved.flags.end());
|
||||
}
|
||||
|
||||
if(source_file) {
|
||||
argv.push_back(source_file);
|
||||
}
|
||||
return argv;
|
||||
}
|
||||
|
||||
std::vector<std::string> CompileCommand::to_string_argv() const {
|
||||
auto argv = to_argv();
|
||||
std::vector<std::string> result;
|
||||
result.reserve(argv.size());
|
||||
for(auto* arg: argv) {
|
||||
result.emplace_back(arg);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
CompilationDatabase::CompilationDatabase() = default;
|
||||
|
||||
CompilationDatabase::~CompilationDatabase() = default;
|
||||
@@ -329,8 +367,8 @@ std::size_t CompilationDatabase::load(llvm::StringRef path) {
|
||||
return entries.size();
|
||||
}
|
||||
|
||||
llvm::SmallVector<CompilationContext> CompilationDatabase::lookup(llvm::StringRef file,
|
||||
const CommandOptions& options) {
|
||||
llvm::SmallVector<CompileCommand> CompilationDatabase::lookup(llvm::StringRef file,
|
||||
const CommandOptions& options) {
|
||||
auto path_id = paths.intern(file);
|
||||
auto matched = find_entries(path_id);
|
||||
|
||||
@@ -338,17 +376,18 @@ llvm::SmallVector<CompilationContext> CompilationDatabase::lookup(llvm::StringRe
|
||||
render_arg_to([&](llvm::StringRef s) { out.push_back(strings.save(s).data()); }, arg);
|
||||
};
|
||||
|
||||
/// Build one CompilationContext from a single CompilationInfo.
|
||||
auto build_context = [&](object_ptr<CompilationInfo> info) -> CompilationContext {
|
||||
/// Build one CompileCommand from a single CompilationInfo.
|
||||
auto build_command = [&](object_ptr<CompilationInfo> info) -> CompileCommand {
|
||||
llvm::StringRef directory = info->directory;
|
||||
std::vector<const char*> arguments;
|
||||
std::vector<const char*> flags;
|
||||
bool is_cc1 = false;
|
||||
|
||||
auto append_arg = [&](llvm::StringRef s) {
|
||||
arguments.emplace_back(strings.save(s).data());
|
||||
flags.emplace_back(strings.save(s).data());
|
||||
};
|
||||
|
||||
auto append_args = [&](llvm::ArrayRef<const char*> args) {
|
||||
arguments.insert(arguments.end(), args.begin(), args.end());
|
||||
flags.insert(flags.end(), args.begin(), args.end());
|
||||
};
|
||||
|
||||
if(options.query_toolchain) {
|
||||
@@ -361,23 +400,20 @@ llvm::SmallVector<CompilationContext> CompilationDatabase::lookup(llvm::StringRe
|
||||
append_args(info->canonical->arguments);
|
||||
append_args(info->patch);
|
||||
} else {
|
||||
arguments.assign(cached.begin(), cached.end());
|
||||
// TODO: add an assertion that the last arg is the temp source
|
||||
// file (e.g., contains "query-toolchain") to guard against
|
||||
// future changes in clang cc1 argument ordering.
|
||||
arguments.pop_back(); // remove temp source file
|
||||
flags.assign(cached.begin(), cached.end());
|
||||
flags.pop_back(); // remove temp source file
|
||||
|
||||
// Replace resource dir if needed.
|
||||
if(!resource_dir().empty()) {
|
||||
llvm::StringRef old_resource_dir;
|
||||
for(std::size_t i = 0; i + 1 < arguments.size(); ++i) {
|
||||
if(arguments[i] == llvm::StringRef("-resource-dir")) {
|
||||
old_resource_dir = arguments[i + 1];
|
||||
for(std::size_t i = 0; i + 1 < flags.size(); ++i) {
|
||||
if(flags[i] == llvm::StringRef("-resource-dir")) {
|
||||
old_resource_dir = flags[i + 1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!old_resource_dir.empty() && old_resource_dir != resource_dir()) {
|
||||
for(auto& arg: arguments) {
|
||||
for(auto& arg: flags) {
|
||||
llvm::StringRef s(arg);
|
||||
if(s.starts_with(old_resource_dir)) {
|
||||
auto replaced =
|
||||
@@ -390,39 +426,42 @@ llvm::SmallVector<CompilationContext> CompilationDatabase::lookup(llvm::StringRe
|
||||
|
||||
append_args(info->patch);
|
||||
|
||||
// Fix -main-file-name to match the actual file.
|
||||
bool next_main_file = false;
|
||||
for(auto& arg: arguments) {
|
||||
if(arg == llvm::StringRef("-main-file-name")) {
|
||||
next_main_file = true;
|
||||
// Strip -main-file-name and its value from flags (to_argv() will
|
||||
// re-inject it with the correct basename when is_cc1 is set).
|
||||
std::vector<const char*> cleaned;
|
||||
cleaned.reserve(flags.size());
|
||||
for(std::size_t i = 0; i < flags.size(); ++i) {
|
||||
if(flags[i] == llvm::StringRef("-main-file-name") && i + 1 < flags.size()) {
|
||||
++i; // skip the value
|
||||
continue;
|
||||
}
|
||||
if(next_main_file) {
|
||||
arg = strings.save(path::filename(file)).data();
|
||||
next_main_file = false;
|
||||
}
|
||||
cleaned.push_back(flags[i]);
|
||||
}
|
||||
}
|
||||
flags = std::move(cleaned);
|
||||
|
||||
// Inject our resource dir if not already present.
|
||||
if(!resource_dir().empty()) {
|
||||
bool has_resource_dir = false;
|
||||
for(auto& arg: arguments) {
|
||||
if(arg == llvm::StringRef("-resource-dir")) {
|
||||
has_resource_dir = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!has_resource_dir) {
|
||||
append_arg("-resource-dir");
|
||||
append_arg(resource_dir());
|
||||
}
|
||||
// Detect cc1 mode (search rather than assuming index).
|
||||
is_cc1 = ranges::contains(flags, llvm::StringRef("-cc1"));
|
||||
}
|
||||
} else {
|
||||
append_args(info->canonical->arguments);
|
||||
append_args(info->patch);
|
||||
}
|
||||
|
||||
// Inject our resource dir if not already present.
|
||||
if(options.inject_resource_dir && !resource_dir().empty()) {
|
||||
bool has_resource_dir = false;
|
||||
for(auto& arg: flags) {
|
||||
if(arg == llvm::StringRef("-resource-dir")) {
|
||||
has_resource_dir = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!has_resource_dir) {
|
||||
append_arg("-resource-dir");
|
||||
append_arg(resource_dir());
|
||||
}
|
||||
}
|
||||
|
||||
// Apply remove filter.
|
||||
if(!options.remove.empty()) {
|
||||
using Arg = std::unique_ptr<llvm::opt::Arg>;
|
||||
@@ -440,12 +479,12 @@ llvm::SmallVector<CompilationContext> CompilationDatabase::lookup(llvm::StringRe
|
||||
};
|
||||
std::ranges::sort(remove_args, {}, get_id);
|
||||
|
||||
auto saved_args = std::move(arguments);
|
||||
arguments.clear();
|
||||
arguments.push_back(saved_args.front());
|
||||
auto saved_flags = std::move(flags);
|
||||
flags.clear();
|
||||
flags.push_back(saved_flags.front());
|
||||
|
||||
parser->parse(
|
||||
llvm::ArrayRef(saved_args).drop_front(),
|
||||
llvm::ArrayRef(saved_flags).drop_front(),
|
||||
[&](Arg arg) {
|
||||
auto id = arg->getOption().getID();
|
||||
auto range = std::ranges::equal_range(remove_args, id, {}, get_id);
|
||||
@@ -461,7 +500,7 @@ llvm::SmallVector<CompilationContext> CompilationDatabase::lookup(llvm::StringRe
|
||||
return;
|
||||
}
|
||||
}
|
||||
render_arg(arguments, *arg);
|
||||
render_arg(flags, *arg);
|
||||
},
|
||||
[](int, int) {});
|
||||
}
|
||||
@@ -470,26 +509,34 @@ llvm::SmallVector<CompilationContext> CompilationDatabase::lookup(llvm::StringRe
|
||||
append_arg(arg);
|
||||
}
|
||||
|
||||
arguments.emplace_back(paths.resolve(path_id).data());
|
||||
return CompilationContext(directory, std::move(arguments));
|
||||
return CompileCommand{
|
||||
ResolvedFlags{directory, std::move(flags), is_cc1},
|
||||
paths.resolve(path_id).data()
|
||||
};
|
||||
};
|
||||
|
||||
llvm::SmallVector<CompilationContext> results;
|
||||
llvm::SmallVector<CompileCommand> results;
|
||||
|
||||
if(!matched.empty()) {
|
||||
for(auto& entry: matched) {
|
||||
results.push_back(build_context(entry.info));
|
||||
results.push_back(build_command(entry.info));
|
||||
}
|
||||
} else {
|
||||
// No matching entry — synthesize a default command.
|
||||
std::vector<const char*> arguments;
|
||||
std::vector<const char*> flags;
|
||||
if(file.ends_with(".cpp") || file.ends_with(".hpp") || file.ends_with(".cc")) {
|
||||
arguments = {"clang++", "-std=c++20"};
|
||||
flags = {"clang++", "-std=c++20"};
|
||||
} else {
|
||||
arguments = {"clang"};
|
||||
flags = {"clang"};
|
||||
}
|
||||
arguments.emplace_back(paths.resolve(path_id).data());
|
||||
results.push_back(CompilationContext({}, std::move(arguments)));
|
||||
if(options.inject_resource_dir && !resource_dir().empty()) {
|
||||
flags.push_back(strings.save("-resource-dir").data());
|
||||
flags.push_back(strings.save(resource_dir()).data());
|
||||
}
|
||||
results.push_back(CompileCommand{
|
||||
ResolvedFlags{{}, std::move(flags), false},
|
||||
paths.resolve(path_id).data()
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
@@ -513,8 +560,8 @@ SearchConfig CompilationDatabase::lookup_search_config(llvm::StringRef file,
|
||||
}
|
||||
|
||||
auto results = lookup(file, options);
|
||||
auto& ctx = results.front();
|
||||
auto config = extract_search_config(ctx.arguments, ctx.directory);
|
||||
auto& cmd = results.front();
|
||||
auto config = extract_search_config(cmd.to_argv(), cmd.resolved.directory);
|
||||
|
||||
if(cacheable) {
|
||||
auto key = ConfigCacheKey{matched.front().info.ptr, options_bits(options)};
|
||||
@@ -650,6 +697,11 @@ std::uint32_t CompilationDatabase::intern_path(llvm::StringRef path) {
|
||||
return paths.intern(path);
|
||||
}
|
||||
|
||||
bool CompilationDatabase::has_entry(llvm::StringRef file) {
|
||||
auto path_id = paths.intern(file);
|
||||
return !find_entries(path_id).empty();
|
||||
}
|
||||
|
||||
llvm::ArrayRef<CompilationEntry> CompilationDatabase::get_entries() const {
|
||||
return entries;
|
||||
}
|
||||
|
||||
@@ -29,6 +29,11 @@ struct CommandOptions {
|
||||
/// Set true in unittests to avoid cluttering test output.
|
||||
bool suppress_logging = false;
|
||||
|
||||
/// Inject our resource dir into the flags if not already present.
|
||||
/// Enabled by default so clang tools always use matching builtin headers.
|
||||
/// Disable in unit tests that assert exact argument counts.
|
||||
bool inject_resource_dir = true;
|
||||
|
||||
/// Extra arguments to remove from the original command line.
|
||||
llvm::ArrayRef<std::string> remove;
|
||||
|
||||
@@ -36,12 +41,35 @@ struct CommandOptions {
|
||||
llvm::ArrayRef<std::string> append;
|
||||
};
|
||||
|
||||
struct CompilationContext {
|
||||
/// File-independent compilation flags (shareable, suitable as cache key input).
|
||||
/// Does NOT contain source file path or -main-file-name.
|
||||
struct ResolvedFlags {
|
||||
/// The working directory of compilation.
|
||||
llvm::StringRef directory;
|
||||
|
||||
/// The compilation arguments.
|
||||
std::vector<const char*> arguments;
|
||||
/// All flags excluding source file path and -main-file-name.
|
||||
std::vector<const char*> flags;
|
||||
|
||||
/// Whether flags come from toolchain query (cc1 mode).
|
||||
/// When true, flags are cc1 frontend args (resolved clang binary + "-cc1" + ...),
|
||||
/// NOT the original driver command. to_argv() scans for "-cc1" in flags and
|
||||
/// inserts -main-file-name immediately after it.
|
||||
bool is_cc1 = false;
|
||||
};
|
||||
|
||||
/// Compilation command = resolved flags + source file identity.
|
||||
struct CompileCommand {
|
||||
ResolvedFlags resolved;
|
||||
|
||||
/// Interned, pointer-stable. Must be null-terminated (required by to_argv()
|
||||
/// and path::filename().data() which relies on the suffix being null-terminated).
|
||||
const char* source_file = nullptr;
|
||||
|
||||
/// Produce full argv: flags + [-main-file-name <basename> if cc1] + source_file.
|
||||
std::vector<const char*> to_argv() const;
|
||||
|
||||
/// Convenience: to_argv() converted to vector<string>.
|
||||
std::vector<std::string> to_string_argv() const;
|
||||
};
|
||||
|
||||
/// Shared compiler identity — driver + all semantics-affecting flags.
|
||||
@@ -174,10 +202,10 @@ public:
|
||||
/// but toolchain cache survives. Returns the number of entries loaded.
|
||||
std::size_t load(llvm::StringRef path);
|
||||
|
||||
/// Lookup the compilation contexts for a file. A file may have multiple
|
||||
/// Lookup the compile commands for a file. A file may have multiple
|
||||
/// compilation commands (e.g. different build configurations); all are returned.
|
||||
llvm::SmallVector<CompilationContext> lookup(llvm::StringRef file,
|
||||
const CommandOptions& options = {});
|
||||
llvm::SmallVector<CompileCommand> lookup(llvm::StringRef file,
|
||||
const CommandOptions& options = {});
|
||||
|
||||
/// Combined lookup + extract_search_config with internal caching.
|
||||
SearchConfig lookup_search_config(llvm::StringRef file, const CommandOptions& options = {});
|
||||
@@ -191,6 +219,10 @@ public:
|
||||
/// Intern a file path and return its path_id.
|
||||
std::uint32_t intern_path(llvm::StringRef path);
|
||||
|
||||
/// Check if a file has an explicit entry in the compilation database
|
||||
/// (as opposed to a synthesized default).
|
||||
bool has_entry(llvm::StringRef file);
|
||||
|
||||
/// All compilation entries (sorted by path_id).
|
||||
llvm::ArrayRef<CompilationEntry> get_entries() const;
|
||||
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
#include <vector>
|
||||
|
||||
#include "command/argument_parser.h"
|
||||
#include "eventide/reflection/enum.h"
|
||||
#include "support/filesystem.h"
|
||||
#include "support/logging.h"
|
||||
|
||||
#include "kota/meta/enum.h"
|
||||
#include "llvm/ADT/ScopeExit.h"
|
||||
#include "llvm/Support/CommandLine.h"
|
||||
#include "llvm/Support/FileSystem.h"
|
||||
@@ -363,7 +363,7 @@ std::vector<const char*> query_toolchain(const QueryParams& params) {
|
||||
case CompilerFamily::Unknown: {
|
||||
/// TODO: nvcc and intel compilers need further exploration.
|
||||
LOG_ERROR("Fail to query driver, unknown supported driver kind: {}, driver is {}",
|
||||
eventide::refl::enum_name(family),
|
||||
kota::meta::enum_name(family),
|
||||
driver);
|
||||
|
||||
std::vector<const char*> result;
|
||||
|
||||
@@ -87,7 +87,9 @@ auto CompilationUnitRef::file_path(clang::FileID fid) -> llvm::StringRef {
|
||||
}
|
||||
|
||||
auto entry = self->SM().getFileEntryRefForID(fid);
|
||||
assert(entry && "Invalid file entry");
|
||||
if(!entry) {
|
||||
return {};
|
||||
}
|
||||
|
||||
llvm::SmallString<128> path;
|
||||
|
||||
@@ -242,13 +244,19 @@ std::vector<std::string> CompilationUnitRef::deps() {
|
||||
for(auto& [fid, directive]: directives()) {
|
||||
for(auto& include: directive.includes) {
|
||||
if(!include.skipped) {
|
||||
deps.try_emplace(file_path(include.fid));
|
||||
auto path = file_path(include.fid);
|
||||
if(!path.empty()) {
|
||||
deps.try_emplace(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(auto& has_include: directive.has_includes) {
|
||||
if(has_include.fid.isValid()) {
|
||||
deps.try_emplace(file_path(has_include.fid));
|
||||
auto path = file_path(has_include.fid);
|
||||
if(!path.empty()) {
|
||||
deps.try_emplace(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,11 +65,36 @@ public:
|
||||
/// Rewritten Preprocessor Callbacks
|
||||
/// ============================================================================
|
||||
|
||||
void HasEmbed(clang::SourceLocation location,
|
||||
llvm::StringRef filename,
|
||||
bool is_angled,
|
||||
clang::OptionalFileEntryRef file) override {
|
||||
unit->directives[unit.file_id(location)].has_embeds.emplace_back(clice::HasEmbed{
|
||||
.file_name = filename,
|
||||
.file = file,
|
||||
.is_angled = is_angled,
|
||||
.loc = location,
|
||||
});
|
||||
}
|
||||
|
||||
void EmbedDirective(clang::SourceLocation location,
|
||||
clang::StringRef filename,
|
||||
bool is_angled,
|
||||
clang::OptionalFileEntryRef file,
|
||||
const clang::LexEmbedParametersResult&) override {
|
||||
unit->directives[unit.file_id(location)].embeds.emplace_back(Embed{
|
||||
.file_name = filename,
|
||||
.file = file,
|
||||
.is_angled = is_angled,
|
||||
.loc = location,
|
||||
});
|
||||
}
|
||||
|
||||
void InclusionDirective(clang::SourceLocation hash_loc,
|
||||
const clang::Token& include_tok,
|
||||
llvm::StringRef,
|
||||
bool,
|
||||
clang::CharSourceRange filename_range,
|
||||
clang::CharSourceRange,
|
||||
clang::OptionalFileEntryRef,
|
||||
llvm::StringRef,
|
||||
llvm::StringRef,
|
||||
@@ -83,7 +108,6 @@ public:
|
||||
unit->directives[prev_fid].includes.emplace_back(Include{
|
||||
.fid = {},
|
||||
.location = include_tok.getLocation(),
|
||||
.filename_range = filename_range.getAsRange(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -20,11 +20,8 @@ struct Include {
|
||||
/// The file id of included file.
|
||||
clang::FileID fid;
|
||||
|
||||
/// Location of the `include`.
|
||||
/// Location of the `include` keyword.
|
||||
clang::SourceLocation location;
|
||||
|
||||
/// The range of filename(includes `""` or `<>`).
|
||||
clang::SourceRange filename_range;
|
||||
};
|
||||
|
||||
/// Information about `__has_include` directive.
|
||||
@@ -132,6 +129,39 @@ struct Import {
|
||||
std::vector<clang::SourceLocation> name_locations;
|
||||
};
|
||||
|
||||
/// Information about `#embed` directive.
|
||||
struct Embed {
|
||||
/// The file name in the embed directive, not including quotes or angle brackets.
|
||||
llvm::StringRef file_name;
|
||||
|
||||
/// The actual file that may be embedded by this embed directive.
|
||||
clang::OptionalFileEntryRef file;
|
||||
|
||||
/// Whether the file name is angled.
|
||||
bool is_angled;
|
||||
|
||||
/// Location of the `#` token.
|
||||
clang::SourceLocation loc;
|
||||
|
||||
/// TODO: Currently we do not store parameters of the embed directive.
|
||||
/// See clang::LexEmbedParametersResult for details.
|
||||
};
|
||||
|
||||
/// Information about `__has_embed` directive.
|
||||
struct HasEmbed {
|
||||
/// The file name in the embed directive, not including quotes or angle brackets.
|
||||
llvm::StringRef file_name;
|
||||
|
||||
/// The actual file that may be embedded by this embed directive.
|
||||
clang::OptionalFileEntryRef file;
|
||||
|
||||
/// Whether the file name is angled.
|
||||
bool is_angled;
|
||||
|
||||
/// Location of the `__has_embed` token.
|
||||
clang::SourceLocation loc;
|
||||
};
|
||||
|
||||
struct Directive {
|
||||
std::vector<Include> includes;
|
||||
std::vector<HasInclude> has_includes;
|
||||
@@ -139,6 +169,8 @@ struct Directive {
|
||||
std::vector<MacroRef> macros;
|
||||
std::vector<Pragma> pragmas;
|
||||
std::vector<Import> imports;
|
||||
std::vector<Embed> embeds;
|
||||
std::vector<HasEmbed> has_embeds;
|
||||
};
|
||||
|
||||
} // namespace clice
|
||||
|
||||
@@ -53,8 +53,8 @@ auto completion_kind(const clang::NamedDecl* decl) -> protocol::CompletionItemKi
|
||||
return protocol::CompletionItemKind::Module;
|
||||
}
|
||||
|
||||
if(llvm::isa<clang::FunctionDecl, clang::FunctionTemplateDecl>(decl)) {
|
||||
return protocol::CompletionItemKind::Function;
|
||||
if(llvm::isa<clang::CXXConstructorDecl>(decl)) {
|
||||
return protocol::CompletionItemKind::Constructor;
|
||||
}
|
||||
|
||||
if(llvm::isa<clang::CXXMethodDecl,
|
||||
@@ -64,8 +64,8 @@ auto completion_kind(const clang::NamedDecl* decl) -> protocol::CompletionItemKi
|
||||
return protocol::CompletionItemKind::Method;
|
||||
}
|
||||
|
||||
if(llvm::isa<clang::CXXConstructorDecl>(decl)) {
|
||||
return protocol::CompletionItemKind::Constructor;
|
||||
if(llvm::isa<clang::FunctionDecl, clang::FunctionTemplateDecl>(decl)) {
|
||||
return protocol::CompletionItemKind::Function;
|
||||
}
|
||||
|
||||
if(llvm::isa<clang::FieldDecl, clang::IndirectFieldDecl>(decl)) {
|
||||
@@ -109,6 +109,115 @@ auto completion_kind(const clang::NamedDecl* decl) -> protocol::CompletionItemKi
|
||||
return protocol::CompletionItemKind::Text;
|
||||
}
|
||||
|
||||
/// Extract the function signature (parameter list) from a CodeCompletionString.
|
||||
/// Returns something like "(int x, float y)" for display in labelDetails.detail.
|
||||
auto extract_signature(const clang::CodeCompletionString& ccs) -> std::string {
|
||||
std::string signature;
|
||||
bool in_parens = false;
|
||||
|
||||
for(const auto& chunk: ccs) {
|
||||
using CK = clang::CodeCompletionString::ChunkKind;
|
||||
switch(chunk.Kind) {
|
||||
case CK::CK_LeftParen:
|
||||
in_parens = true;
|
||||
signature += '(';
|
||||
break;
|
||||
case CK::CK_RightParen:
|
||||
signature += ')';
|
||||
in_parens = false;
|
||||
break;
|
||||
case CK::CK_Placeholder:
|
||||
case CK::CK_CurrentParameter:
|
||||
if(in_parens && chunk.Text) {
|
||||
signature += chunk.Text;
|
||||
}
|
||||
break;
|
||||
case CK::CK_Text:
|
||||
case CK::CK_Informative:
|
||||
if(in_parens && chunk.Text) {
|
||||
signature += chunk.Text;
|
||||
}
|
||||
break;
|
||||
case CK::CK_LeftAngle:
|
||||
signature += '<';
|
||||
in_parens = true;
|
||||
break;
|
||||
case CK::CK_RightAngle:
|
||||
signature += '>';
|
||||
in_parens = false;
|
||||
break;
|
||||
case CK::CK_Comma:
|
||||
if(in_parens) {
|
||||
signature += ", ";
|
||||
}
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
return signature;
|
||||
}
|
||||
|
||||
/// Build a snippet string from a CodeCompletionString.
|
||||
/// Produces e.g. "funcName(${1:int x}, ${2:float y})" for functions,
|
||||
/// or "ClassName<${1:T}>" for class templates.
|
||||
auto build_snippet(const clang::CodeCompletionString& ccs) -> std::string {
|
||||
std::string snippet;
|
||||
unsigned placeholder_index = 0;
|
||||
|
||||
for(const auto& chunk: ccs) {
|
||||
using CK = clang::CodeCompletionString::ChunkKind;
|
||||
switch(chunk.Kind) {
|
||||
case CK::CK_TypedText:
|
||||
if(chunk.Text) {
|
||||
snippet += chunk.Text;
|
||||
}
|
||||
break;
|
||||
case CK::CK_Placeholder:
|
||||
if(chunk.Text) {
|
||||
snippet += std::format("${{{0}:{1}}}", ++placeholder_index, chunk.Text);
|
||||
}
|
||||
break;
|
||||
case CK::CK_LeftParen: snippet += '('; break;
|
||||
case CK::CK_RightParen: snippet += ')'; break;
|
||||
case CK::CK_LeftAngle: snippet += '<'; break;
|
||||
case CK::CK_RightAngle: snippet += '>'; break;
|
||||
case CK::CK_Comma: snippet += ", "; break;
|
||||
case CK::CK_Text:
|
||||
if(chunk.Text) {
|
||||
snippet += chunk.Text;
|
||||
}
|
||||
break;
|
||||
case CK::CK_Optional:
|
||||
// Optional chunks contain default arguments — skip for snippet.
|
||||
break;
|
||||
case CK::CK_Informative:
|
||||
case CK::CK_ResultType:
|
||||
case CK::CK_CurrentParameter:
|
||||
// Display-only chunks, not part of insertion.
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
// If no placeholders were generated, return empty to signal plain text.
|
||||
if(placeholder_index == 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return snippet;
|
||||
}
|
||||
|
||||
/// Extract the return type from a CodeCompletionString.
|
||||
auto extract_return_type(const clang::CodeCompletionString& ccs) -> std::string {
|
||||
for(const auto& chunk: ccs) {
|
||||
if(chunk.Kind == clang::CodeCompletionString::CK_ResultType && chunk.Text) {
|
||||
return chunk.Text;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
struct OverloadItem {
|
||||
protocol::CompletionItem item;
|
||||
float score = 0.0F;
|
||||
@@ -159,29 +268,44 @@ public:
|
||||
overloads.reserve(candidate_count);
|
||||
std::unordered_map<std::string, std::size_t> overload_index;
|
||||
|
||||
auto build_item =
|
||||
[&](llvm::StringRef label, protocol::CompletionItemKind kind, llvm::StringRef insert) {
|
||||
protocol::CompletionItem item{
|
||||
.label = label.str(),
|
||||
};
|
||||
item.kind = kind;
|
||||
bool prefix_starts_with_underscore = prefix.spelling.starts_with("_");
|
||||
|
||||
protocol::TextEdit edit{
|
||||
.range = replace_range,
|
||||
.new_text = insert.empty() ? label.str() : insert.str(),
|
||||
};
|
||||
item.text_edit = std::move(edit);
|
||||
return item;
|
||||
auto build_item = [&](llvm::StringRef label,
|
||||
protocol::CompletionItemKind kind,
|
||||
llvm::StringRef insert,
|
||||
bool is_snippet = false) {
|
||||
protocol::CompletionItem item{
|
||||
.label = label.str(),
|
||||
};
|
||||
item.kind = kind;
|
||||
|
||||
protocol::TextEdit edit{
|
||||
.range = replace_range,
|
||||
.new_text = insert.empty() ? label.str() : insert.str(),
|
||||
};
|
||||
item.text_edit = std::move(edit);
|
||||
if(is_snippet) {
|
||||
item.insert_text_format = protocol::InsertTextFormat::Snippet;
|
||||
}
|
||||
return item;
|
||||
};
|
||||
|
||||
auto try_add = [&](llvm::StringRef label,
|
||||
protocol::CompletionItemKind kind,
|
||||
llvm::StringRef insert_text,
|
||||
llvm::StringRef overload_key) {
|
||||
llvm::StringRef overload_key,
|
||||
llvm::StringRef signature = {},
|
||||
llvm::StringRef return_type = {},
|
||||
bool is_snippet = false) {
|
||||
if(label.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter out _/__ prefixed internal symbols unless user typed _.
|
||||
if(!prefix_starts_with_underscore && label.starts_with("_")) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto score = matcher.match(label);
|
||||
if(!score.has_value()) {
|
||||
return;
|
||||
@@ -191,8 +315,18 @@ public:
|
||||
auto [it, inserted] =
|
||||
overload_index.try_emplace(overload_key.str(), overloads.size());
|
||||
if(inserted) {
|
||||
auto item = build_item(label, kind, insert_text);
|
||||
auto item = build_item(label, kind, insert_text, is_snippet);
|
||||
item.sort_text = std::format("{}", *score);
|
||||
if(!signature.empty() || !return_type.empty()) {
|
||||
protocol::CompletionItemLabelDetails details;
|
||||
if(!signature.empty()) {
|
||||
details.detail = signature.str();
|
||||
}
|
||||
if(!return_type.empty()) {
|
||||
details.description = return_type.str();
|
||||
}
|
||||
item.label_details = std::move(details);
|
||||
}
|
||||
overloads.push_back({
|
||||
.item = std::move(item),
|
||||
.score = *score,
|
||||
@@ -209,8 +343,18 @@ public:
|
||||
return;
|
||||
}
|
||||
|
||||
auto item = build_item(label, kind, insert_text);
|
||||
auto item = build_item(label, kind, insert_text, is_snippet);
|
||||
item.sort_text = std::format("{}", *score);
|
||||
if(!signature.empty() || !return_type.empty()) {
|
||||
protocol::CompletionItemLabelDetails details;
|
||||
if(!signature.empty()) {
|
||||
details.detail = signature.str();
|
||||
}
|
||||
if(!return_type.empty()) {
|
||||
details.description = return_type.str();
|
||||
}
|
||||
item.label_details = std::move(details);
|
||||
}
|
||||
collected.push_back(std::move(item));
|
||||
};
|
||||
|
||||
@@ -242,16 +386,58 @@ public:
|
||||
break;
|
||||
}
|
||||
|
||||
auto label = ast::name_of(declaration);
|
||||
auto kind = completion_kind(declaration);
|
||||
|
||||
// For constructors and deduction guides, use the class name
|
||||
// (without template args) instead of the full type name.
|
||||
// e.g. "vector" instead of "vector<_Tp, _Alloc>".
|
||||
std::string label;
|
||||
if(auto* ctor = llvm::dyn_cast<clang::CXXConstructorDecl>(declaration)) {
|
||||
label = ctor->getParent()->getName().str();
|
||||
} else if(auto* guide =
|
||||
llvm::dyn_cast<clang::CXXDeductionGuideDecl>(declaration)) {
|
||||
label = guide->getDeducedTemplate()->getName().str();
|
||||
} else {
|
||||
label = ast::name_of(declaration);
|
||||
}
|
||||
|
||||
llvm::SmallString<256> qualified_name;
|
||||
if(options.bundle_overloads && kind == protocol::CompletionItemKind::Function) {
|
||||
bool is_callable = kind == protocol::CompletionItemKind::Function ||
|
||||
kind == protocol::CompletionItemKind::Method ||
|
||||
kind == protocol::CompletionItemKind::Constructor;
|
||||
if(options.bundle_overloads && is_callable) {
|
||||
llvm::raw_svector_ostream stream(qualified_name);
|
||||
declaration->printQualifiedName(stream);
|
||||
}
|
||||
|
||||
try_add(label, kind, label, qualified_name.str());
|
||||
std::string signature;
|
||||
std::string return_type;
|
||||
std::string snippet;
|
||||
auto* ccs =
|
||||
candidate.CreateCodeCompletionString(sema,
|
||||
context,
|
||||
getAllocator(),
|
||||
getCodeCompletionTUInfo(),
|
||||
/*IncludeBriefComments=*/false);
|
||||
if(ccs) {
|
||||
signature = extract_signature(*ccs);
|
||||
return_type = extract_return_type(*ccs);
|
||||
// Generate snippet for non-bundled callables.
|
||||
if(is_callable && !options.bundle_overloads &&
|
||||
options.enable_function_arguments_snippet) {
|
||||
snippet = build_snippet(*ccs);
|
||||
}
|
||||
}
|
||||
|
||||
bool has_snippet = !snippet.empty();
|
||||
auto insert = has_snippet ? llvm::StringRef(snippet) : llvm::StringRef(label);
|
||||
try_add(label,
|
||||
kind,
|
||||
insert,
|
||||
qualified_name.str(),
|
||||
signature,
|
||||
return_type,
|
||||
has_snippet);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -259,11 +445,48 @@ public:
|
||||
|
||||
for(auto& entry: overloads) {
|
||||
if(entry.count > 1) {
|
||||
entry.item.detail = "(...)";
|
||||
protocol::CompletionItemLabelDetails details;
|
||||
details.detail = std::format("(…) +{} overloads", entry.count);
|
||||
entry.item.label_details = std::move(details);
|
||||
}
|
||||
collected.push_back(std::move(entry.item));
|
||||
}
|
||||
|
||||
// In bundle mode, deduplicate by label: when the same name appears as
|
||||
// both a class and its constructors/deduction guides, keep only the
|
||||
// highest-priority kind (Class > Function/Method > others).
|
||||
if(options.bundle_overloads) {
|
||||
auto kind_priority = [](protocol::CompletionItemKind k) -> int {
|
||||
switch(k) {
|
||||
case protocol::CompletionItemKind::Class:
|
||||
case protocol::CompletionItemKind::Struct: return 3;
|
||||
case protocol::CompletionItemKind::Function:
|
||||
case protocol::CompletionItemKind::Method: return 2;
|
||||
case protocol::CompletionItemKind::Constructor: return 1;
|
||||
default: return 0;
|
||||
}
|
||||
};
|
||||
|
||||
std::unordered_map<std::string, std::size_t> label_index;
|
||||
std::vector<protocol::CompletionItem> deduped;
|
||||
deduped.reserve(collected.size());
|
||||
|
||||
for(auto& item: collected) {
|
||||
auto [it, inserted] = label_index.try_emplace(item.label, deduped.size());
|
||||
if(inserted) {
|
||||
deduped.push_back(std::move(item));
|
||||
} else {
|
||||
auto& existing = deduped[it->second];
|
||||
int old_prio = existing.kind.has_value() ? kind_priority(*existing.kind) : 0;
|
||||
int new_prio = item.kind.has_value() ? kind_priority(*item.kind) : 0;
|
||||
if(new_prio > old_prio) {
|
||||
existing = std::move(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
collected.swap(deduped);
|
||||
}
|
||||
|
||||
output.clear();
|
||||
output.swap(collected);
|
||||
}
|
||||
|
||||
@@ -2,14 +2,15 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "eventide/ipc/lsp/uri.h"
|
||||
#include "feature/feature.h"
|
||||
|
||||
#include "kota/ipc/lsp/uri.h"
|
||||
|
||||
namespace clice::feature {
|
||||
|
||||
namespace {
|
||||
|
||||
namespace lsp = eventide::ipc::lsp;
|
||||
namespace lsp = kota::ipc::lsp;
|
||||
|
||||
auto to_uri(llvm::StringRef file) -> std::string {
|
||||
const auto file_view = std::string_view(file.data(), file.size());
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "feature/feature.h"
|
||||
#include "syntax/lexer.h"
|
||||
|
||||
namespace clice::feature {
|
||||
|
||||
namespace {} // namespace
|
||||
|
||||
auto document_links(CompilationUnitRef unit, PositionEncoding encoding)
|
||||
-> std::vector<protocol::DocumentLink> {
|
||||
std::vector<protocol::DocumentLink> links;
|
||||
@@ -22,50 +20,42 @@ auto document_links(CompilationUnitRef unit, PositionEncoding encoding)
|
||||
auto content = unit.interested_content();
|
||||
PositionMapper converter(content, encoding);
|
||||
auto& directives = directives_it->second;
|
||||
auto* lang_opts = &unit.lang_options();
|
||||
|
||||
links.reserve(directives.includes.size() + directives.has_includes.size());
|
||||
auto add_link = [&](clang::SourceLocation loc, llvm::StringRef target) {
|
||||
auto [fid, offset] = unit.decompose_location(loc);
|
||||
if(fid != interested || offset >= content.size())
|
||||
return;
|
||||
auto range = find_directive_argument(content, offset, lang_opts);
|
||||
if(!range)
|
||||
return;
|
||||
protocol::DocumentLink link{.range = to_range(converter, *range)};
|
||||
link.target = target.str();
|
||||
links.push_back(std::move(link));
|
||||
};
|
||||
|
||||
for(const auto& include: directives.includes) {
|
||||
auto [fid, range] = unit.decompose_range(include.filename_range);
|
||||
if(fid != interested || !range.valid()) {
|
||||
continue;
|
||||
if(include.fid.isValid()) {
|
||||
add_link(include.location, unit.file_path(include.fid));
|
||||
}
|
||||
|
||||
protocol::DocumentLink link{
|
||||
.range = to_range(converter, range),
|
||||
};
|
||||
link.target = std::string(unit.file_path(include.fid));
|
||||
links.push_back(std::move(link));
|
||||
}
|
||||
|
||||
for(const auto& has_include: directives.has_includes) {
|
||||
if(has_include.fid.isInvalid()) {
|
||||
continue;
|
||||
if(has_include.fid.isValid()) {
|
||||
add_link(has_include.location, unit.file_path(has_include.fid));
|
||||
}
|
||||
}
|
||||
|
||||
auto [fid, offset] = unit.decompose_location(has_include.location);
|
||||
if(fid != interested || offset >= content.size()) {
|
||||
continue;
|
||||
for(const auto& embed: directives.embeds) {
|
||||
if(embed.file) {
|
||||
add_link(embed.loc, embed.file->getName());
|
||||
}
|
||||
}
|
||||
|
||||
auto tail = content.substr(offset);
|
||||
char open = tail.front();
|
||||
if(open != '<' && open != '"') {
|
||||
continue;
|
||||
for(const auto& has_embed: directives.has_embeds) {
|
||||
if(has_embed.file) {
|
||||
add_link(has_embed.loc, has_embed.file->getName());
|
||||
}
|
||||
|
||||
char close = open == '<' ? '>' : '"';
|
||||
auto close_index = tail.find(close, 1);
|
||||
if(close_index == llvm::StringRef::npos) {
|
||||
continue;
|
||||
}
|
||||
|
||||
LocalSourceRange range(offset, offset + static_cast<std::uint32_t>(close_index + 1));
|
||||
protocol::DocumentLink link{
|
||||
.range = to_range(converter, range),
|
||||
};
|
||||
link.target = std::string(unit.file_path(has_include.fid));
|
||||
links.push_back(std::move(link));
|
||||
}
|
||||
|
||||
return links;
|
||||
|
||||
@@ -7,8 +7,9 @@
|
||||
|
||||
#include "compile/compilation.h"
|
||||
#include "compile/compilation_unit.h"
|
||||
#include "eventide/ipc/lsp/position.h"
|
||||
#include "eventide/ipc/lsp/protocol.h"
|
||||
|
||||
#include "kota/ipc/lsp/position.h"
|
||||
#include "kota/ipc/lsp/protocol.h"
|
||||
|
||||
namespace clang {
|
||||
|
||||
@@ -18,11 +19,11 @@ class NamedDecl;
|
||||
|
||||
namespace clice::feature {
|
||||
|
||||
namespace protocol = eventide::ipc::protocol;
|
||||
namespace protocol = kota::ipc::protocol;
|
||||
|
||||
using eventide::ipc::lsp::PositionEncoding;
|
||||
using eventide::ipc::lsp::PositionMapper;
|
||||
using eventide::ipc::lsp::parse_position_encoding;
|
||||
using kota::ipc::lsp::PositionEncoding;
|
||||
using kota::ipc::lsp::PositionMapper;
|
||||
using kota::ipc::lsp::parse_position_encoding;
|
||||
|
||||
inline auto to_range(const PositionMapper& converter, LocalSourceRange range) -> protocol::Range {
|
||||
return protocol::Range{
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
#include "clang/AST/Attr.h"
|
||||
#include "clang/Basic/IdentifierTable.h"
|
||||
#include "clang/Basic/Module.h"
|
||||
|
||||
namespace clice::feature {
|
||||
|
||||
@@ -23,12 +24,8 @@ struct RawToken {
|
||||
std::uint32_t modifiers = 0;
|
||||
};
|
||||
|
||||
constexpr std::uint32_t bit(SymbolModifiers::Kind kind) {
|
||||
return static_cast<std::uint32_t>(kind);
|
||||
}
|
||||
|
||||
void add_modifier(std::uint32_t& modifiers, SymbolModifiers::Kind kind) {
|
||||
modifiers |= bit(kind);
|
||||
modifiers |= SymbolModifiers::to_mask(kind);
|
||||
}
|
||||
|
||||
auto type_index(SymbolKind kind) -> std::uint32_t {
|
||||
@@ -39,6 +36,132 @@ auto encode_modifiers(std::uint32_t modifiers) -> std::uint32_t {
|
||||
return modifiers;
|
||||
}
|
||||
|
||||
bool is_dependent(const clang::Decl* D) {
|
||||
return isa<clang::UnresolvedUsingValueDecl>(D);
|
||||
}
|
||||
|
||||
/// Returns true if `decl` is considered to be from a default/system library.
|
||||
/// This currently checks the systemness of the file by include type, although
|
||||
/// different heuristics may be used in the future (e.g. sysroot paths).
|
||||
bool is_default_library(const clang::Decl* decl) {
|
||||
clang::SourceLocation location = decl->getLocation();
|
||||
if(!location.isValid()) {
|
||||
return false;
|
||||
}
|
||||
return decl->getASTContext().getSourceManager().isInSystemHeader(location);
|
||||
}
|
||||
|
||||
// "Static" means many things in C++, only some get the "static" modifier.
|
||||
//
|
||||
// Meanings that do:
|
||||
// - Members associated with the class rather than the instance.
|
||||
// This is what 'static' most often means across languages.
|
||||
// - static local variables
|
||||
// These are similarly "detached from their context" by the static keyword.
|
||||
// In practice, these are rarely used inside classes, reducing confusion.
|
||||
//
|
||||
// Meanings that don't:
|
||||
// - Namespace-scoped variables, which have static storage class.
|
||||
// This is implicit, so the keyword "static" isn't so strongly associated.
|
||||
// If we want a modifier for these, "global scope" is probably the concept.
|
||||
// - Namespace-scoped variables/functions explicitly marked "static".
|
||||
// There the keyword changes *linkage* , which is a totally different concept.
|
||||
// If we want to model this, "file scope" would be a nice modifier.
|
||||
//
|
||||
// This is confusing, and maybe we should use another name, but because "static"
|
||||
// is a standard LSP modifier, having one with that name has advantages.
|
||||
bool is_static(const clang::Decl* decl) {
|
||||
if(const auto* method = llvm::dyn_cast<clang::CXXMethodDecl>(decl)) {
|
||||
return method->isStatic();
|
||||
}
|
||||
if(const auto* var_decl = llvm::dyn_cast<clang::VarDecl>(decl)) {
|
||||
return var_decl->isStaticDataMember() || var_decl->isStaticLocal();
|
||||
}
|
||||
if(const auto* objc_property = llvm::dyn_cast<clang::ObjCPropertyDecl>(decl)) {
|
||||
return objc_property->isClassProperty();
|
||||
}
|
||||
if(const auto* objc_method = llvm::dyn_cast<clang::ObjCMethodDecl>(decl)) {
|
||||
return objc_method->isClassMethod();
|
||||
}
|
||||
if(const auto* function = llvm::dyn_cast<clang::FunctionDecl>(decl)) {
|
||||
return function->isStatic();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Whether `type` is const in a loose sense: would a value of this type be readonly?
|
||||
bool is_const(clang::QualType type) {
|
||||
if(type.isNull()) {
|
||||
return false;
|
||||
}
|
||||
type = type.getNonReferenceType();
|
||||
if(type.isConstQualified()) {
|
||||
return true;
|
||||
}
|
||||
if(const auto* array_type = type->getAsArrayTypeUnsafe()) {
|
||||
return is_const(array_type->getElementType());
|
||||
}
|
||||
if(is_const(type->getPointeeType())) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Whether `decl` is const in a loose sense (should it be highlighted as such?)
|
||||
// FIXME: This is separate from whether a particular usage can mutate `decl`.
|
||||
// We may want a receiver in `value.size()` to be readonly even if `value` is mutable.
|
||||
bool is_const(const clang::Decl* decl) {
|
||||
if(llvm::isa<clang::EnumConstantDecl>(decl) ||
|
||||
llvm::isa<clang::NonTypeTemplateParmDecl>(decl)) {
|
||||
return true;
|
||||
}
|
||||
if(llvm::isa<clang::FieldDecl>(decl) || llvm::isa<clang::VarDecl>(decl) ||
|
||||
llvm::isa<clang::MSPropertyDecl>(decl) || llvm::isa<clang::BindingDecl>(decl)) {
|
||||
if(is_const(llvm::cast<clang::ValueDecl>(decl)->getType())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if(const auto* objc_property = llvm::dyn_cast<clang::ObjCPropertyDecl>(decl)) {
|
||||
if(objc_property->isReadOnly()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if(const auto* ms_property = llvm::dyn_cast<clang::MSPropertyDecl>(decl)) {
|
||||
if(!ms_property->hasSetter()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if(const auto* method = llvm::dyn_cast<clang::CXXMethodDecl>(decl)) {
|
||||
if(method->isConst()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if(const auto* function = llvm::dyn_cast<clang::FunctionDecl>(decl)) {
|
||||
return is_const(function->getReturnType());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Indicates whether declaration `decl` is abstract in cases where it is a struct or a
|
||||
// class.
|
||||
bool is_abstract(const clang::Decl* decl) {
|
||||
if(const auto* method = llvm::dyn_cast<clang::CXXMethodDecl>(decl)) {
|
||||
return method->isPureVirtual();
|
||||
}
|
||||
if(const auto* record = llvm::dyn_cast<clang::CXXRecordDecl>(decl)) {
|
||||
return record->hasDefinition() && record->isAbstract();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Indicates whether declaration `decl` is virtual in cases where it is a method.
|
||||
bool is_virtual(const clang::Decl* decl) {
|
||||
if(const auto* method = llvm::dyn_cast<clang::CXXMethodDecl>(decl)) {
|
||||
return method->isVirtual();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
class SemanticTokensCollector : public SemanticVisitor<SemanticTokensCollector> {
|
||||
public:
|
||||
explicit SemanticTokensCollector(CompilationUnitRef unit) : SemanticVisitor(unit, true) {}
|
||||
@@ -46,6 +169,7 @@ public:
|
||||
auto collect() -> std::vector<RawToken> {
|
||||
highlight_lexical(unit.interested_file());
|
||||
run();
|
||||
highlight_modules();
|
||||
merge_tokens();
|
||||
return std::move(tokens);
|
||||
}
|
||||
@@ -55,6 +179,8 @@ public:
|
||||
clang::SourceLocation location) {
|
||||
std::uint32_t modifiers = 0;
|
||||
if(relation.is_one_of(RelationKind::Definition)) {
|
||||
// todo: clangd add both Declaration and Definition modifiers for definitions.
|
||||
// add_modifier(modifiers, SymbolModifiers::Declaration);
|
||||
add_modifier(modifiers, SymbolModifiers::Definition);
|
||||
} else if(relation.is_one_of(RelationKind::Declaration)) {
|
||||
add_modifier(modifiers, SymbolModifiers::Declaration);
|
||||
@@ -63,6 +189,42 @@ public:
|
||||
if(ast::is_templated(decl)) {
|
||||
add_modifier(modifiers, SymbolModifiers::Templated);
|
||||
}
|
||||
// Apply attribute-style modifiers to the underlying declaration.
|
||||
// The attribute tests don't want to look at the template.
|
||||
if(const auto* template_decl = llvm::dyn_cast<clang::TemplateDecl>(decl)) {
|
||||
if(const auto* templated_decl = template_decl->getTemplatedDecl())
|
||||
decl = templated_decl;
|
||||
}
|
||||
|
||||
// TODO: add scope-based modifiers once the local model supports them.
|
||||
// if (auto Mod = scopeModifier(Decl))
|
||||
// Tok.addModifier(*Mod);
|
||||
|
||||
if(is_const(decl)) {
|
||||
add_modifier(modifiers, SymbolModifiers::Readonly);
|
||||
}
|
||||
if(is_static(decl)) {
|
||||
add_modifier(modifiers, SymbolModifiers::Static);
|
||||
}
|
||||
if(is_abstract(decl)) {
|
||||
add_modifier(modifiers, SymbolModifiers::Abstract);
|
||||
}
|
||||
if(is_virtual(decl)) {
|
||||
add_modifier(modifiers, SymbolModifiers::Virtual);
|
||||
}
|
||||
if(is_default_library(decl)) {
|
||||
add_modifier(modifiers, SymbolModifiers::DefaultLibrary);
|
||||
}
|
||||
if(decl->isDeprecated()) {
|
||||
add_modifier(modifiers, SymbolModifiers::Deprecated);
|
||||
}
|
||||
if(is_dependent(decl)) {
|
||||
add_modifier(modifiers, SymbolModifiers::DependentName);
|
||||
}
|
||||
if(llvm::isa<clang::CXXConstructorDecl>(decl) ||
|
||||
llvm::isa<clang::CXXDestructorDecl>(decl)) {
|
||||
add_modifier(modifiers, SymbolModifiers::ConstructorOrDestructor);
|
||||
}
|
||||
|
||||
add_token(location, SymbolKind::from(decl), modifiers);
|
||||
}
|
||||
@@ -80,6 +242,10 @@ public:
|
||||
add_token(location, SymbolKind::Macro, modifiers);
|
||||
}
|
||||
|
||||
// handleModuleOccurrence
|
||||
|
||||
// handleRelation
|
||||
|
||||
void handleAttrOccurrence(const clang::Attr* attr, clang::SourceRange range) {
|
||||
auto [begin, end] = range;
|
||||
if(llvm::isa<clang::FinalAttr, clang::OverrideAttr>(attr)) {
|
||||
@@ -127,6 +293,58 @@ private:
|
||||
});
|
||||
}
|
||||
|
||||
void highlight_modules() {
|
||||
auto interested = unit.interested_file();
|
||||
|
||||
auto directives_it = unit.directives().find(interested);
|
||||
if(directives_it != unit.directives().end()) {
|
||||
for(const auto& import: directives_it->second.imports) {
|
||||
add_token(import.location, SymbolKind::Keyword, 0);
|
||||
for(auto loc: import.name_locations) {
|
||||
add_token(loc, SymbolKind::Module, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto* mod = unit.context().getCurrentNamedModule();
|
||||
if(!mod) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto def_loc = mod->DefinitionLoc;
|
||||
if(!def_loc.isValid() || !def_loc.isFileID()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto [fid, offset] = unit.decompose_location(def_loc);
|
||||
if(fid != interested) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto content = unit.file_content(fid);
|
||||
auto& lang_opts = unit.lang_options();
|
||||
Lexer lexer(content.substr(offset), false, &lang_opts);
|
||||
|
||||
auto module_token = lexer.advance();
|
||||
if(module_token.is_identifier()) {
|
||||
auto range = LocalSourceRange(offset + module_token.range.begin,
|
||||
offset + module_token.range.end);
|
||||
tokens.push_back({.range = range, .kind = SymbolKind::Keyword, .modifiers = 0});
|
||||
}
|
||||
|
||||
// Scan for identifiers (module name parts) until semicolon/eof.
|
||||
while(true) {
|
||||
auto token = lexer.advance();
|
||||
if(token.is_eof() || token.kind == clang::tok::semi) {
|
||||
break;
|
||||
}
|
||||
if(token.is_identifier()) {
|
||||
auto range = LocalSourceRange(offset + token.range.begin, offset + token.range.end);
|
||||
tokens.push_back({.range = range, .kind = SymbolKind::Module, .modifiers = 0});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void highlight_lexical(clang::FileID fid) {
|
||||
auto content = unit.file_content(fid);
|
||||
auto& lang_opts = unit.lang_options();
|
||||
@@ -181,10 +399,17 @@ private:
|
||||
}
|
||||
|
||||
static void resolve_conflict(RawToken& last, const RawToken& current) {
|
||||
(void)current;
|
||||
if(last.kind == SymbolKind::Conflict) {
|
||||
return;
|
||||
}
|
||||
// Directive is a low-priority lexical kind; semantic tokens override it.
|
||||
if(last.kind == SymbolKind::Directive) {
|
||||
last = current;
|
||||
return;
|
||||
}
|
||||
if(current.kind == SymbolKind::Directive) {
|
||||
return;
|
||||
}
|
||||
last.kind = SymbolKind::Conflict;
|
||||
}
|
||||
|
||||
|
||||
@@ -129,18 +129,19 @@ struct MergedIndex::Impl {
|
||||
std::vector<std::uint32_t> canonical_ref_counts;
|
||||
|
||||
/// The canonical id set of removed index.
|
||||
roaring::Roaring removed;
|
||||
ContextBitmap removed;
|
||||
|
||||
/// All merged symbol occurrences.
|
||||
llvm::DenseMap<Occurrence, roaring::Roaring> occurrences;
|
||||
llvm::DenseMap<Occurrence, ContextBitmap> occurrences;
|
||||
|
||||
/// All merged symbol relations.
|
||||
llvm::DenseMap<SymbolHash, llvm::DenseMap<Relation, roaring::Roaring>> relations;
|
||||
llvm::DenseMap<SymbolHash, llvm::DenseMap<Relation, ContextBitmap>> relations;
|
||||
|
||||
/// Sorted occurrences cache for fast lookup.
|
||||
std::vector<Occurrence> occurrences_cache;
|
||||
|
||||
void merge(this Impl& self, std::uint32_t path_id, FileIndex& index, auto&& add_context) {
|
||||
/// Returns true if this was a cache hit (no new data inserted).
|
||||
bool merge(this Impl& self, std::uint32_t path_id, FileIndex& index, auto&& add_context) {
|
||||
auto hash = index.hash();
|
||||
auto hash_key = llvm::StringRef(reinterpret_cast<char*>(hash.data()), hash.size());
|
||||
auto [it, success] = self.canonical_cache.try_emplace(hash_key, self.max_canonical_id);
|
||||
@@ -151,7 +152,7 @@ struct MergedIndex::Impl {
|
||||
if(!success) {
|
||||
self.canonical_ref_counts[canonical_id] += 1;
|
||||
self.removed.remove(canonical_id);
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
for(auto& occurrence: index.occurrences) {
|
||||
@@ -167,6 +168,7 @@ struct MergedIndex::Impl {
|
||||
|
||||
self.canonical_ref_counts.emplace_back(1);
|
||||
self.max_canonical_id += 1;
|
||||
return false;
|
||||
}
|
||||
|
||||
friend bool operator==(const Impl&, const Impl&) = default;
|
||||
@@ -237,19 +239,20 @@ void MergedIndex::load_in_memory(this Self& self) {
|
||||
|
||||
// Deserialize removed bitmap.
|
||||
if(root->removed() && root->removed()->size() > 0) {
|
||||
index.removed = read_bitmap(root->removed());
|
||||
index.removed = ContextBitmap::from_roaring(read_bitmap(root->removed()));
|
||||
}
|
||||
|
||||
for(auto entry: *root->occurrences()) {
|
||||
index.occurrences.try_emplace(*safe_cast<Occurrence>(entry->occurrence()),
|
||||
read_bitmap(entry->context()));
|
||||
ContextBitmap::from_roaring(read_bitmap(entry->context())));
|
||||
}
|
||||
|
||||
for(auto entry: *root->relations()) {
|
||||
auto& relations = index.relations[entry->symbol()];
|
||||
for(auto relation_entry: *entry->relations()) {
|
||||
relations.try_emplace(*safe_cast<Relation>(relation_entry->relation()),
|
||||
read_bitmap(relation_entry->context()));
|
||||
relations.try_emplace(
|
||||
*safe_cast<Relation>(relation_entry->relation()),
|
||||
ContextBitmap::from_roaring(read_bitmap(relation_entry->context())));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,17 +313,22 @@ void MergedIndex::serialize(this const Self& self, llvm::raw_ostream& out) {
|
||||
CreateStructVector<binary::IncludeLocation>(builder, context.include_locations));
|
||||
});
|
||||
|
||||
auto serialize_ctx_bitmap = [&](const ContextBitmap& ctx) {
|
||||
auto r = ctx.to_roaring();
|
||||
buffer.clear();
|
||||
buffer.resize_for_overwrite(r.getSizeInBytes(false));
|
||||
r.write(buffer.data(), false);
|
||||
return CreateVector(builder, buffer);
|
||||
};
|
||||
|
||||
llvm::SmallVector<const Occurrence*> occurrence_keys;
|
||||
occurrence_keys.reserve(index->occurrences.size());
|
||||
auto occurrences = transform(index->occurrences, [&](auto&& value) {
|
||||
auto&& [occurrence, bitmap] = value;
|
||||
buffer.clear();
|
||||
buffer.resize_for_overwrite(bitmap.getSizeInBytes(false));
|
||||
bitmap.write(buffer.data(), false);
|
||||
occurrence_keys.emplace_back(&occurrence);
|
||||
return binary::CreateOccurrenceEntry(builder,
|
||||
safe_cast<binary::Occurrence>(&occurrence),
|
||||
CreateVector(builder, buffer));
|
||||
serialize_ctx_bitmap(bitmap));
|
||||
});
|
||||
std::ranges::sort(std::views::zip(occurrence_keys, occurrences), [](auto lhs, auto rhs) {
|
||||
const auto& lo = *std::get<0>(lhs);
|
||||
@@ -335,12 +343,9 @@ void MergedIndex::serialize(this const Self& self, llvm::raw_ostream& out) {
|
||||
auto&& [symbol_id, symbol_relations] = value;
|
||||
auto relations = transform(symbol_relations, [&](auto&& value) {
|
||||
auto&& [relation, bitmap] = value;
|
||||
buffer.clear();
|
||||
buffer.resize_for_overwrite(bitmap.getSizeInBytes(false));
|
||||
bitmap.write(buffer.data(), false);
|
||||
return binary::CreateRelationEntry(builder,
|
||||
safe_cast<binary::Relation>(&relation),
|
||||
CreateVector(builder, buffer));
|
||||
serialize_ctx_bitmap(bitmap));
|
||||
});
|
||||
relation_keys.emplace_back(symbol_id);
|
||||
return binary::CreateSymbolRelationsEntry(builder,
|
||||
@@ -353,9 +358,10 @@ void MergedIndex::serialize(this const Self& self, llvm::raw_ostream& out) {
|
||||
|
||||
// Serialize removed bitmap.
|
||||
buffer.clear();
|
||||
if(!index->removed.isEmpty()) {
|
||||
buffer.resize_for_overwrite(index->removed.getSizeInBytes(false));
|
||||
index->removed.write(buffer.data(), false);
|
||||
if(!index->removed.is_empty()) {
|
||||
auto r = index->removed.to_roaring();
|
||||
buffer.resize_for_overwrite(r.getSizeInBytes(false));
|
||||
r.write(buffer.data(), false);
|
||||
}
|
||||
auto removed = CreateVector(builder, buffer);
|
||||
|
||||
@@ -398,14 +404,12 @@ void MergedIndex::lookup(this const Self& self,
|
||||
while(it != occurrences.end()) {
|
||||
if(it->range.contains(offset)) {
|
||||
// Skip occurrences whose canonical_ids are all removed.
|
||||
if(!index.removed.isEmpty()) {
|
||||
if(!index.removed.is_empty()) {
|
||||
auto bitmap_it = index.occurrences.find(*it);
|
||||
if(bitmap_it != index.occurrences.end()) {
|
||||
auto remaining = bitmap_it->second - index.removed;
|
||||
if(remaining.isEmpty()) {
|
||||
it++;
|
||||
continue;
|
||||
}
|
||||
if(bitmap_it != index.occurrences.end() &&
|
||||
!bitmap_it->second.any_not_in(index.removed)) {
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -413,7 +417,7 @@ void MergedIndex::lookup(this const Self& self,
|
||||
break;
|
||||
}
|
||||
|
||||
it++;
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -434,7 +438,7 @@ void MergedIndex::lookup(this const Self& self,
|
||||
break;
|
||||
}
|
||||
|
||||
it++;
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -457,11 +461,8 @@ void MergedIndex::lookup(this const Self& self,
|
||||
for(auto& [relation, bitmap]: relations) {
|
||||
if(relation.kind & kind) {
|
||||
// Skip relations whose canonical_ids are all removed.
|
||||
if(!self.impl->removed.isEmpty()) {
|
||||
auto remaining = bitmap - self.impl->removed;
|
||||
if(remaining.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
if(!self.impl->removed.is_empty() && !bitmap.any_not_in(self.impl->removed)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if(!callback(relation)) {
|
||||
@@ -579,7 +580,7 @@ void MergedIndex::remove(this Self& self, std::uint32_t path_id) {
|
||||
index.occurrences_cache.clear();
|
||||
}
|
||||
|
||||
void MergedIndex::merge(this Self& self,
|
||||
bool MergedIndex::merge(this Self& self,
|
||||
std::uint32_t path_id,
|
||||
std::chrono::milliseconds build_at,
|
||||
std::vector<IncludeLocation> include_locations,
|
||||
@@ -587,16 +588,17 @@ void MergedIndex::merge(this Self& self,
|
||||
llvm::StringRef content) {
|
||||
self.load_in_memory();
|
||||
self.impl->content = content.str();
|
||||
self.impl->merge(path_id, index, [&](Impl& self, std::uint32_t canonical_id) {
|
||||
bool hit = self.impl->merge(path_id, index, [&](Impl& self, std::uint32_t canonical_id) {
|
||||
auto& context = self.compilation_contexts[path_id];
|
||||
context.canonical_id = canonical_id;
|
||||
context.build_at = build_at.count();
|
||||
context.include_locations = std::move(include_locations);
|
||||
});
|
||||
self.impl->occurrences_cache.clear();
|
||||
return hit;
|
||||
}
|
||||
|
||||
void MergedIndex::merge(this Self& self,
|
||||
bool MergedIndex::merge(this Self& self,
|
||||
std::uint32_t path_id,
|
||||
std::uint32_t include_id,
|
||||
FileIndex& index,
|
||||
@@ -605,11 +607,12 @@ void MergedIndex::merge(this Self& self,
|
||||
if(self.impl->content.empty() && !content.empty()) {
|
||||
self.impl->content = content.str();
|
||||
}
|
||||
self.impl->merge(path_id, index, [&](Impl& self, std::uint32_t canonical_id) {
|
||||
bool hit = self.impl->merge(path_id, index, [&](Impl& self, std::uint32_t canonical_id) {
|
||||
auto& context = self.header_contexts[path_id];
|
||||
context.includes.emplace_back(include_id, canonical_id);
|
||||
});
|
||||
self.impl->occurrences_cache.clear();
|
||||
return hit;
|
||||
}
|
||||
|
||||
llvm::StringRef MergedIndex::content(this const Self& self) {
|
||||
|
||||
@@ -68,7 +68,8 @@ public:
|
||||
llvm::StringRef content(this const Self& self);
|
||||
|
||||
/// Merge the index with given compilation context.
|
||||
void merge(this Self& self,
|
||||
/// Returns true if this was a cache hit (no new data inserted).
|
||||
bool merge(this Self& self,
|
||||
std::uint32_t path_id,
|
||||
std::chrono::milliseconds build_at,
|
||||
std::vector<IncludeLocation> include_locations,
|
||||
@@ -76,7 +77,8 @@ public:
|
||||
llvm::StringRef content);
|
||||
|
||||
/// Merge the index with given header context.
|
||||
void merge(this Self& self,
|
||||
/// Returns true if this was a cache hit (no new data inserted).
|
||||
bool merge(this Self& self,
|
||||
std::uint32_t path_id,
|
||||
std::uint32_t include_id,
|
||||
FileIndex& index,
|
||||
|
||||
@@ -27,6 +27,42 @@ llvm::SmallVector<std::uint32_t> ProjectIndex::merge(this ProjectIndex& self, TU
|
||||
return file_ids_map;
|
||||
}
|
||||
|
||||
llvm::SmallVector<std::uint32_t> ProjectIndex::merge(this ProjectIndex& self,
|
||||
TUIndex& index,
|
||||
const Bitmap& new_file_ids) {
|
||||
auto& paths = index.graph.paths;
|
||||
llvm::SmallVector<std::uint32_t> file_ids_map;
|
||||
file_ids_map.resize_for_overwrite(paths.size());
|
||||
|
||||
for(std::uint32_t i = 0; i < paths.size(); i++) {
|
||||
file_ids_map[i] = self.path_pool.path_id(paths[i]);
|
||||
}
|
||||
|
||||
for(auto& [symbol_id, symbol]: index.symbols) {
|
||||
// Skip symbols that don't reference any new file.
|
||||
bool references_new = false;
|
||||
for(auto ref: symbol.reference_files) {
|
||||
if(new_file_ids.contains(ref)) {
|
||||
references_new = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!references_new)
|
||||
continue;
|
||||
|
||||
auto& target_symbol = self.symbols[symbol_id];
|
||||
if(target_symbol.name.empty()) {
|
||||
target_symbol.name = symbol.name;
|
||||
target_symbol.kind = symbol.kind;
|
||||
}
|
||||
for(auto ref: symbol.reference_files) {
|
||||
target_symbol.reference_files.add(file_ids_map[ref]);
|
||||
}
|
||||
}
|
||||
|
||||
return file_ids_map;
|
||||
}
|
||||
|
||||
void ProjectIndex::serialize(this ProjectIndex& self, llvm::raw_ostream& os) {
|
||||
fbs::FlatBufferBuilder builder(1024);
|
||||
|
||||
|
||||
@@ -82,6 +82,12 @@ struct ProjectIndex {
|
||||
|
||||
llvm::SmallVector<std::uint32_t> merge(this ProjectIndex& self, TUIndex& index);
|
||||
|
||||
/// Selective merge: only update symbols referenced by files in new_file_ids.
|
||||
/// new_file_ids contains TU-local path indices for cache-miss FileIndices.
|
||||
llvm::SmallVector<std::uint32_t> merge(this ProjectIndex& self,
|
||||
TUIndex& index,
|
||||
const Bitmap& new_file_ids);
|
||||
|
||||
void serialize(this ProjectIndex& self, llvm::raw_ostream& os);
|
||||
|
||||
static ProjectIndex from(const void* data);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "index/tu_index.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <tuple>
|
||||
|
||||
#include "index/serialization.h"
|
||||
@@ -14,8 +15,8 @@ namespace {
|
||||
|
||||
class Builder : public SemanticVisitor<Builder> {
|
||||
public:
|
||||
Builder(TUIndex& result, CompilationUnitRef unit) :
|
||||
SemanticVisitor<Builder>(unit, false), result(result) {
|
||||
Builder(TUIndex& result, CompilationUnitRef unit, bool interested_only) :
|
||||
SemanticVisitor<Builder>(unit, interested_only), result(result) {
|
||||
result.graph = IncludeGraph::from(unit);
|
||||
}
|
||||
|
||||
@@ -160,6 +161,9 @@ private:
|
||||
} // namespace
|
||||
|
||||
std::array<std::uint8_t, 32> FileIndex::hash() {
|
||||
if(cached_hash)
|
||||
return *cached_hash;
|
||||
|
||||
llvm::SHA256 hasher;
|
||||
|
||||
using u8 = std::uint8_t;
|
||||
@@ -172,27 +176,43 @@ std::array<std::uint8_t, 32> FileIndex::hash() {
|
||||
hasher.update(llvm::ArrayRef(data, size));
|
||||
}
|
||||
|
||||
for(auto& [symbol_id, relations]: relations) {
|
||||
// Sort keys and relations to ensure deterministic hashing regardless of
|
||||
// DenseMap iteration order. This mutates the relations in-place intentionally.
|
||||
llvm::SmallVector<SymbolHash> sorted_keys;
|
||||
sorted_keys.reserve(relations.size());
|
||||
for(auto& [symbol_id, _]: relations) {
|
||||
sorted_keys.push_back(symbol_id);
|
||||
}
|
||||
std::ranges::sort(sorted_keys);
|
||||
|
||||
for(auto symbol_id: sorted_keys) {
|
||||
hasher.update(std::bit_cast<std::array<u8, sizeof(symbol_id)>>(symbol_id));
|
||||
static_assert(sizeof(Relation) ==
|
||||
sizeof(RelationKind) + 4 + sizeof(Range) + sizeof(SymbolHash));
|
||||
static_assert(sizeof(Relation) % 8 == 0);
|
||||
|
||||
if(!relations.empty()) {
|
||||
auto data = reinterpret_cast<u8*>(relations.data());
|
||||
auto size = relations.size() * sizeof(Relation);
|
||||
auto& rels = relations[symbol_id];
|
||||
std::ranges::sort(rels, [](const Relation& lhs, const Relation& rhs) {
|
||||
return std::tuple(lhs.kind.value(), lhs.range.begin, lhs.range.end, lhs.target_symbol) <
|
||||
std::tuple(rhs.kind.value(), rhs.range.begin, rhs.range.end, rhs.target_symbol);
|
||||
});
|
||||
|
||||
if(!rels.empty()) {
|
||||
auto data = reinterpret_cast<u8*>(rels.data());
|
||||
auto size = rels.size() * sizeof(Relation);
|
||||
hasher.update(llvm::ArrayRef(data, size));
|
||||
}
|
||||
}
|
||||
|
||||
return hasher.final();
|
||||
cached_hash = hasher.final();
|
||||
return *cached_hash;
|
||||
}
|
||||
|
||||
TUIndex TUIndex::build(CompilationUnitRef unit) {
|
||||
TUIndex TUIndex::build(CompilationUnitRef unit, bool interested_only) {
|
||||
TUIndex index;
|
||||
index.built_at = unit.build_at();
|
||||
|
||||
Builder builder(index, unit);
|
||||
Builder builder(index, unit, interested_only);
|
||||
builder.build();
|
||||
|
||||
return index;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <bit>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -52,7 +53,12 @@ struct FileIndex {
|
||||
|
||||
std::vector<Occurrence> occurrences;
|
||||
|
||||
/// Compute (and cache) SHA256 hash of this index's content.
|
||||
/// Each FileIndex instance must only be hashed by one thread at a time.
|
||||
std::array<std::uint8_t, 32> hash();
|
||||
|
||||
private:
|
||||
std::optional<std::array<std::uint8_t, 32>> cached_hash;
|
||||
};
|
||||
|
||||
struct Symbol {
|
||||
@@ -85,7 +91,7 @@ struct TUIndex {
|
||||
|
||||
FileIndex main_file_index;
|
||||
|
||||
static TUIndex build(CompilationUnitRef unit);
|
||||
static TUIndex build(CompilationUnitRef unit, bool interested_only = false);
|
||||
|
||||
void serialize(llvm::raw_ostream& os) const;
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,9 +17,12 @@ namespace clice {
|
||||
/// completion, you cannot get go-to-definition, etc. To avoid this, we just use
|
||||
/// some heuristics to simplify the dependent names as normal type/expression.
|
||||
/// For example, `std::vector<T>::value_type` can be simplified as `T`.
|
||||
///
|
||||
/// Thread safety: NOT thread-safe. Each compilation unit should have its own resolver.
|
||||
/// The `resolved` cache persists across multiple resolve() calls on the same unit.
|
||||
class TemplateResolver {
|
||||
public:
|
||||
TemplateResolver(clang::Sema& sema) : sema(sema) {}
|
||||
explicit TemplateResolver(clang::Sema& sema) : sema(sema) {}
|
||||
|
||||
clang::QualType resolve(clang::QualType type);
|
||||
|
||||
@@ -27,7 +30,7 @@ public:
|
||||
|
||||
void resolve(clang::UnresolvedLookupExpr* expr);
|
||||
|
||||
// TODO: use a relative clear way to resolve `UnresolvedLookupExpr`.
|
||||
// TODO: Use a clearer approach for resolving UnresolvedLookupExpr.
|
||||
|
||||
void resolve(clang::UnresolvedUsingType* type);
|
||||
|
||||
@@ -50,7 +53,7 @@ public:
|
||||
if(identifier) {
|
||||
return lookup(template_name.getQualifier(), identifier);
|
||||
} else {
|
||||
/// FIXME: Operators does't have a name.
|
||||
/// TODO: Operators don't have an IdentifierInfo; need DeclarationName-based lookup.
|
||||
return {};
|
||||
}
|
||||
}
|
||||
@@ -60,7 +63,7 @@ public:
|
||||
}
|
||||
|
||||
lookup_result lookup(const clang::UnresolvedLookupExpr* expr) {
|
||||
/// FIXME:
|
||||
/// TODO: Only returns the first TemplateDecl; should handle overloaded lookups.
|
||||
for(auto decl: expr->decls()) {
|
||||
if(auto TD = llvm::dyn_cast<clang::TemplateDecl>(decl)) {
|
||||
return lookup_result(TD);
|
||||
@@ -74,8 +77,8 @@ public:
|
||||
return {};
|
||||
}
|
||||
|
||||
/// TODO:
|
||||
lookup_result lookup(clang::CXXDependentScopeMemberExpr* expr) {
|
||||
/// TODO: Implement dependent member expression lookup (e.g. `x.template foo<T>()`).
|
||||
lookup_result lookup(const clang::CXXDependentScopeMemberExpr* expr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -83,16 +86,19 @@ public:
|
||||
return lookup(decl->getQualifier(), decl->getDeclName());
|
||||
}
|
||||
|
||||
lookup_result resolve(const clang::UnresolvedUsingTypenameDecl* decl) {
|
||||
lookup_result lookup(const clang::UnresolvedUsingTypenameDecl* decl) {
|
||||
return lookup(decl->getQualifier(), decl->getDeclName());
|
||||
}
|
||||
|
||||
#ifndef NDEBUG
|
||||
static inline bool debug = false;
|
||||
#endif
|
||||
|
||||
private:
|
||||
clang::Sema& sema;
|
||||
|
||||
/// Cache of resolved dependent types, keyed by AST node pointer.
|
||||
/// Shared across resolve() calls within the same TU for performance.
|
||||
/// This is safe because a given AST node (DependentNameType*, etc.) has a
|
||||
/// unique identity within the TU — the same pointer always refers to the same
|
||||
/// syntactic occurrence. Different syntactic occurrences of the "same" type
|
||||
/// have different AST node pointers.
|
||||
llvm::DenseMap<const void*, clang::QualType> resolved;
|
||||
};
|
||||
|
||||
|
||||
@@ -131,33 +131,6 @@ public:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if(auto module = unit.context().getCurrentNamedModule()) {
|
||||
// auto keyword = module->DefinitionLoc;
|
||||
// auto begin = TB.spelledTokenContaining(keyword);
|
||||
// // assert(begin->kind() == clang::tok::identifier && begin->text(SM) == "module" &&
|
||||
// // "Invalid module declaration");
|
||||
//
|
||||
// begin += 1;
|
||||
// auto end = TB.spelledTokens(unit.file_id(keyword)).end();
|
||||
//
|
||||
// for(auto iter = begin; iter != end; ++iter) {
|
||||
// if(iter->kind() == clang::tok::identifier) {
|
||||
// if(auto next = iter + 1; next != end && (next->kind() == clang::tok::period ||
|
||||
// next->kind() == clang::tok::colon)) {
|
||||
// iter += 1;
|
||||
// continue;
|
||||
// }
|
||||
//
|
||||
// end = iter + 1;
|
||||
// break;
|
||||
// }
|
||||
//
|
||||
// std::unreachable();
|
||||
// }
|
||||
//
|
||||
// handleModuleOccurrence(keyword, llvm::ArrayRef<clang::syntax::Token>(begin, end));
|
||||
//}
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
@@ -80,27 +80,79 @@ private:
|
||||
struct SymbolModifiers {
|
||||
enum Kind : std::uint32_t {
|
||||
/// Represents that the symbol is a declaration(e.g. function declaration).
|
||||
Declaration = 1u << 0,
|
||||
Declaration = 0,
|
||||
|
||||
/// Represents that the symbol is a definition(e.g. function definition).
|
||||
Definition = 1u << 1,
|
||||
Definition = 1,
|
||||
|
||||
/// Represents that the symbol is const modified(e.g. `const` variable).
|
||||
Const = 1u << 2,
|
||||
Const = 2,
|
||||
|
||||
/// Represents that the symbol is overloaded(e.g. overloaded functions and operators).
|
||||
Overloaded = 1u << 3,
|
||||
Overloaded = 3,
|
||||
|
||||
/// Represents that the symbol is a part of type(e.g. `*` in `int*`).
|
||||
Typed = 1u << 4,
|
||||
Typed = 4,
|
||||
|
||||
/// Represents that the symbol is a template(e.g. class template or function template).
|
||||
Templated = 1u << 5,
|
||||
Templated = 5,
|
||||
|
||||
/// Represents that the symbol is deprecated.
|
||||
Deprecated = 6,
|
||||
|
||||
/// Represents that the symbol is deduced.
|
||||
Deduced = 7,
|
||||
|
||||
/// Represents that the symbol is readonly.
|
||||
Readonly = 8,
|
||||
|
||||
/// Represents that the symbol is static.
|
||||
Static = 9,
|
||||
|
||||
/// Represents that the symbol is abstract.
|
||||
Abstract = 10,
|
||||
|
||||
/// Represents that the symbol is virtual.
|
||||
Virtual = 11,
|
||||
|
||||
/// Represents that the symbol is a dependent name.
|
||||
DependentName = 12,
|
||||
|
||||
/// Represents that the symbol comes from the default library.
|
||||
DefaultLibrary = 13,
|
||||
|
||||
/// Represents that the symbol is used through a mutable reference.
|
||||
UsedAsMutableReference = 14,
|
||||
|
||||
/// Represents that the symbol is used through a mutable pointer.
|
||||
UsedAsMutablePointer = 15,
|
||||
|
||||
/// Represents that the symbol is a constructor or destructor.
|
||||
ConstructorOrDestructor = 16,
|
||||
|
||||
/// Represents that the symbol is user-defined.
|
||||
UserDefined = 17,
|
||||
|
||||
/// Represents that the symbol is function-scoped.
|
||||
FunctionScope = 18,
|
||||
|
||||
/// Represents that the symbol is class-scoped.
|
||||
ClassScope = 19,
|
||||
|
||||
/// Represents that the symbol is file-scoped.
|
||||
FileScope = 20,
|
||||
|
||||
/// Represents that the symbol is global-scoped.
|
||||
GlobalScope = 21,
|
||||
};
|
||||
|
||||
constexpr static std::uint32_t to_mask(Kind kind) {
|
||||
return std::uint32_t(1) << static_cast<std::uint32_t>(kind);
|
||||
}
|
||||
|
||||
constexpr SymbolModifiers() = default;
|
||||
|
||||
constexpr SymbolModifiers(Kind kind) : value(static_cast<std::uint32_t>(kind)) {}
|
||||
constexpr SymbolModifiers(Kind kind) : value(to_mask(kind)) {}
|
||||
|
||||
constexpr explicit SymbolModifiers(std::uint32_t bits) : value(bits) {}
|
||||
|
||||
@@ -109,7 +161,7 @@ struct SymbolModifiers {
|
||||
}
|
||||
|
||||
constexpr bool contains(Kind kind) const {
|
||||
return (value & static_cast<std::uint32_t>(kind)) != 0;
|
||||
return (value & to_mask(kind)) != 0;
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
|
||||
namespace clice {
|
||||
|
||||
namespace ranges = std::ranges;
|
||||
|
||||
CompileGraph::CompileGraph(dispatch_fn dispatch, resolve_fn resolve) :
|
||||
dispatch(std::move(dispatch)), resolve(std::move(resolve)) {}
|
||||
|
||||
@@ -31,13 +33,19 @@ void CompileGraph::ensure_resolved(std::uint32_t path_id) {
|
||||
}
|
||||
}
|
||||
|
||||
et::task<bool> CompileGraph::compile(std::uint32_t path_id) {
|
||||
kota::task<bool> CompileGraph::compile_deps(std::uint32_t path_id) {
|
||||
llvm::DenseSet<std::uint32_t> ancestors;
|
||||
co_return co_await compile_impl(path_id, ancestors, false);
|
||||
}
|
||||
|
||||
kota::task<bool> CompileGraph::compile(std::uint32_t path_id) {
|
||||
llvm::DenseSet<std::uint32_t> ancestors;
|
||||
co_return co_await compile_impl(path_id, ancestors);
|
||||
}
|
||||
|
||||
et::task<bool> CompileGraph::compile_impl(std::uint32_t path_id,
|
||||
llvm::DenseSet<std::uint32_t> ancestors) {
|
||||
kota::task<bool> CompileGraph::compile_impl(std::uint32_t path_id,
|
||||
llvm::DenseSet<std::uint32_t> ancestors,
|
||||
bool dispatch_self) {
|
||||
ensure_resolved(path_id);
|
||||
|
||||
// Cycle detection: if this unit is already in the compile chain, bail out.
|
||||
@@ -48,6 +56,27 @@ et::task<bool> CompileGraph::compile_impl(std::uint32_t path_id,
|
||||
// Re-lookup after ensure_resolved may have mutated the map.
|
||||
auto it = units.find(path_id);
|
||||
|
||||
// For deps-only mode, compile dependencies concurrently and return.
|
||||
if(!dispatch_self) {
|
||||
auto deps = it->second.dependencies;
|
||||
if(deps.empty()) {
|
||||
co_return true;
|
||||
}
|
||||
|
||||
std::vector<kota::task<bool>> dep_tasks;
|
||||
dep_tasks.reserve(deps.size());
|
||||
for(auto dep_id: deps) {
|
||||
dep_tasks.push_back(compile_impl(dep_id, ancestors));
|
||||
}
|
||||
auto results = co_await kota::when_all(std::move(dep_tasks));
|
||||
for(auto ok: results) {
|
||||
if(!ok) {
|
||||
co_return false;
|
||||
}
|
||||
}
|
||||
co_return true;
|
||||
}
|
||||
|
||||
// Already clean.
|
||||
if(!it->second.dirty) {
|
||||
co_return true;
|
||||
@@ -64,9 +93,16 @@ et::task<bool> CompileGraph::compile_impl(std::uint32_t path_id,
|
||||
co_return !units.find(path_id)->second.dirty;
|
||||
}
|
||||
|
||||
// Begin compilation.
|
||||
// Begin compilation. The finish lambda ensures compiling/completion state
|
||||
// is always cleaned up, regardless of how the function exits.
|
||||
it->second.compiling = true;
|
||||
it->second.completion = std::make_unique<et::event>();
|
||||
it->second.completion = std::make_unique<kota::event>();
|
||||
|
||||
auto finish = [&, path_id] {
|
||||
auto& u = units.find(path_id)->second;
|
||||
u.compiling = false;
|
||||
u.completion->set();
|
||||
};
|
||||
|
||||
// Copy deps and capture generation before co_await (DenseMap iterator safety).
|
||||
auto deps = it->second.dependencies;
|
||||
@@ -77,60 +113,49 @@ et::task<bool> CompileGraph::compile_impl(std::uint32_t path_id,
|
||||
// Deadlocks from cross-branch cycles (e.g. 1->{2,3}, 2->3, 3->2) are
|
||||
// prevented by has_wait_cycle() checking before completion.wait().
|
||||
if(!deps.empty()) {
|
||||
std::vector<et::task<bool, void, et::cancellation>> dep_tasks;
|
||||
std::vector<kota::task<bool, void, kota::cancellation>> dep_tasks;
|
||||
dep_tasks.reserve(deps.size());
|
||||
for(auto dep_id: deps) {
|
||||
dep_tasks.push_back(et::with_token(compile_impl(dep_id, ancestors), token));
|
||||
dep_tasks.push_back(kota::with_token(compile_impl(dep_id, ancestors), token));
|
||||
}
|
||||
|
||||
auto results = co_await et::when_all(std::move(dep_tasks));
|
||||
auto results = co_await kota::when_all(std::move(dep_tasks));
|
||||
|
||||
auto& u = units.find(path_id)->second;
|
||||
if(results.is_cancelled()) {
|
||||
u.compiling = false;
|
||||
u.completion->set();
|
||||
co_await et::cancel();
|
||||
finish();
|
||||
co_await kota::cancel();
|
||||
}
|
||||
|
||||
for(auto ok: *results) {
|
||||
if(!ok) {
|
||||
u.compiling = false;
|
||||
u.completion->set();
|
||||
finish();
|
||||
co_return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dispatch the actual compilation, cancellable via the pre-captured token.
|
||||
// Using the token captured before co_await ensures cancellation propagates
|
||||
// correctly even if update() replaces the source during dependency compilation.
|
||||
{
|
||||
auto result = co_await et::with_token(dispatch(path_id), token);
|
||||
auto result = co_await kota::with_token(dispatch(path_id), token);
|
||||
|
||||
auto& u = units.find(path_id)->second;
|
||||
if(!result.has_value()) {
|
||||
u.compiling = false;
|
||||
u.completion->set();
|
||||
co_await et::cancel();
|
||||
}
|
||||
if(!*result) {
|
||||
u.compiling = false;
|
||||
u.completion->set();
|
||||
co_return false;
|
||||
}
|
||||
if(!result.has_value()) {
|
||||
finish();
|
||||
co_await kota::cancel();
|
||||
}
|
||||
|
||||
if(!*result) {
|
||||
finish();
|
||||
co_return false;
|
||||
}
|
||||
|
||||
// Success — only clear dirty if update() hasn't bumped the generation.
|
||||
auto& final_unit = units.find(path_id)->second;
|
||||
if(final_unit.generation != gen) {
|
||||
// update() was called while dispatch was in flight.
|
||||
final_unit.compiling = false;
|
||||
final_unit.completion->set();
|
||||
finish();
|
||||
co_return false;
|
||||
}
|
||||
|
||||
final_unit.dirty = false;
|
||||
final_unit.compiling = false;
|
||||
final_unit.completion->set();
|
||||
finish();
|
||||
co_return true;
|
||||
}
|
||||
|
||||
@@ -165,8 +190,7 @@ llvm::SmallVector<std::uint32_t> CompileGraph::update(std::uint32_t path_id) {
|
||||
auto dep_it = units.find(dep_id);
|
||||
if(dep_it != units.end()) {
|
||||
auto& dependents = dep_it->second.dependents;
|
||||
dependents.erase(std::remove(dependents.begin(), dependents.end(), path_id),
|
||||
dependents.end());
|
||||
dependents.erase(ranges::remove(dependents, path_id).begin(), dependents.end());
|
||||
}
|
||||
}
|
||||
unit.dependencies.clear();
|
||||
@@ -175,7 +199,7 @@ llvm::SmallVector<std::uint32_t> CompileGraph::update(std::uint32_t path_id) {
|
||||
// Cancel in-flight compilation if running.
|
||||
if(unit.compiling) {
|
||||
unit.source->cancel();
|
||||
unit.source = std::make_unique<et::cancellation_source>();
|
||||
unit.source = std::make_unique<kota::cancellation_source>();
|
||||
}
|
||||
unit.dirty = true;
|
||||
unit.generation++;
|
||||
@@ -223,7 +247,7 @@ bool CompileGraph::has_wait_cycle(std::uint32_t target,
|
||||
void CompileGraph::cancel_all() {
|
||||
for(auto& [_, unit]: units) {
|
||||
unit.source->cancel();
|
||||
unit.source = std::make_unique<et::cancellation_source>();
|
||||
unit.source = std::make_unique<kota::cancellation_source>();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,16 +4,13 @@
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
#include "eventide/async/async.h"
|
||||
|
||||
#include "kota/async/async.h"
|
||||
#include "llvm/ADT/DenseMap.h"
|
||||
#include "llvm/ADT/DenseSet.h"
|
||||
#include "llvm/ADT/SmallVector.h"
|
||||
|
||||
namespace clice {
|
||||
|
||||
namespace et = eventide;
|
||||
|
||||
struct CompileUnit {
|
||||
std::uint32_t path_id = 0;
|
||||
|
||||
@@ -33,14 +30,15 @@ struct CompileUnit {
|
||||
/// stale completions without ABA risk from raw-pointer comparison.
|
||||
std::uint64_t generation = 0;
|
||||
|
||||
std::unique_ptr<et::cancellation_source> source = std::make_unique<et::cancellation_source>();
|
||||
std::unique_ptr<et::event> completion;
|
||||
std::unique_ptr<kota::cancellation_source> source =
|
||||
std::make_unique<kota::cancellation_source>();
|
||||
std::unique_ptr<kota::event> completion;
|
||||
};
|
||||
|
||||
class CompileGraph {
|
||||
public:
|
||||
/// Performs the actual compilation (e.g. produce PCM file).
|
||||
using dispatch_fn = std::function<et::task<bool>(std::uint32_t path_id)>;
|
||||
using dispatch_fn = std::function<kota::task<bool>(std::uint32_t path_id)>;
|
||||
|
||||
/// Returns the dependency path_ids for a given path_id (called lazily on first compile).
|
||||
using resolve_fn = std::function<llvm::SmallVector<std::uint32_t>(std::uint32_t path_id)>;
|
||||
@@ -48,7 +46,11 @@ public:
|
||||
CompileGraph(dispatch_fn dispatch, resolve_fn resolve);
|
||||
|
||||
/// Compile a unit and all its transitive dependencies.
|
||||
et::task<bool> compile(std::uint32_t path_id);
|
||||
kota::task<bool> compile(std::uint32_t path_id);
|
||||
|
||||
/// Compile all transitive module dependencies of path_id, but NOT path_id itself.
|
||||
/// Used for non-module files (plain .cpp) that import modules.
|
||||
kota::task<bool> compile_deps(std::uint32_t path_id);
|
||||
|
||||
/// Mark path_id and all transitive dependents as dirty,
|
||||
/// cancelling any in-progress compilations.
|
||||
@@ -66,7 +68,9 @@ private:
|
||||
void ensure_resolved(std::uint32_t path_id);
|
||||
|
||||
/// Internal compile with ancestor tracking for cycle detection.
|
||||
et::task<bool> compile_impl(std::uint32_t path_id, llvm::DenseSet<std::uint32_t> ancestors);
|
||||
kota::task<bool> compile_impl(std::uint32_t path_id,
|
||||
llvm::DenseSet<std::uint32_t> ancestors,
|
||||
bool dispatch_self = true);
|
||||
|
||||
/// Check if waiting on `target` would deadlock given our `ancestors` chain.
|
||||
/// Walks the dependency graph through compiling units to see if any dep
|
||||
|
||||
920
src/server/compiler.cpp
Normal file
920
src/server/compiler.cpp
Normal file
@@ -0,0 +1,920 @@
|
||||
#include "server/compiler.h"
|
||||
|
||||
#include <format>
|
||||
#include <ranges>
|
||||
#include <string>
|
||||
|
||||
#include "command/search_config.h"
|
||||
#include "index/tu_index.h"
|
||||
#include "server/protocol.h"
|
||||
#include "support/filesystem.h"
|
||||
#include "support/logging.h"
|
||||
#include "syntax/include_resolver.h"
|
||||
#include "syntax/scan.h"
|
||||
|
||||
#include "kota/codec/json/json.h"
|
||||
#include "kota/ipc/lsp/position.h"
|
||||
#include "kota/ipc/lsp/uri.h"
|
||||
#include "llvm/Support/FileSystem.h"
|
||||
#include "llvm/Support/MemoryBuffer.h"
|
||||
#include "llvm/Support/Path.h"
|
||||
#include "llvm/Support/xxhash.h"
|
||||
|
||||
namespace clice {
|
||||
|
||||
namespace lsp = kota::ipc::lsp;
|
||||
using serde_raw = kota::codec::RawValue;
|
||||
|
||||
/// Detect whether the cursor is inside a preamble directive (include/import).
|
||||
|
||||
Compiler::Compiler(kota::event_loop& loop,
|
||||
kota::ipc::JsonPeer& peer,
|
||||
Workspace& workspace,
|
||||
WorkerPool& pool,
|
||||
llvm::DenseMap<std::uint32_t, Session>& sessions) :
|
||||
loop(loop), peer(peer), workspace(workspace), pool(pool), sessions(sessions) {}
|
||||
|
||||
Compiler::~Compiler() {
|
||||
workspace.cancel_all();
|
||||
}
|
||||
|
||||
void Compiler::init_compile_graph() {
|
||||
if(workspace.path_to_module.empty()) {
|
||||
LOG_INFO("No C++20 modules detected, skipping CompileGraph");
|
||||
return;
|
||||
}
|
||||
|
||||
// Lazy dependency resolver: scans a module file on demand to discover imports.
|
||||
auto resolve = [this](std::uint32_t path_id) -> llvm::SmallVector<std::uint32_t> {
|
||||
auto file_path = workspace.path_pool.resolve(path_id);
|
||||
auto results =
|
||||
workspace.cdb.lookup(file_path, {.query_toolchain = true, .suppress_logging = true});
|
||||
if(results.empty())
|
||||
return {};
|
||||
|
||||
auto& cmd = results[0];
|
||||
auto scan_result = scan_precise(cmd.to_argv(), cmd.resolved.directory);
|
||||
|
||||
llvm::SmallVector<std::uint32_t> deps;
|
||||
for(auto& mod_name: scan_result.modules) {
|
||||
auto mod_ids = workspace.dep_graph.lookup_module(mod_name);
|
||||
if(!mod_ids.empty()) {
|
||||
deps.push_back(mod_ids[0]);
|
||||
}
|
||||
}
|
||||
|
||||
// Module implementation units implicitly depend on their interface unit.
|
||||
if(!scan_result.module_name.empty() && !scan_result.is_interface_unit) {
|
||||
auto mod_ids = workspace.dep_graph.lookup_module(scan_result.module_name);
|
||||
if(!mod_ids.empty()) {
|
||||
deps.push_back(mod_ids[0]);
|
||||
}
|
||||
}
|
||||
|
||||
return deps;
|
||||
};
|
||||
|
||||
// Dispatch: sends BuildPCM request to a stateless worker.
|
||||
auto dispatch = [this](std::uint32_t path_id) -> kota::task<bool> {
|
||||
auto mod_it = workspace.path_to_module.find(path_id);
|
||||
if(mod_it == workspace.path_to_module.end())
|
||||
co_return false;
|
||||
|
||||
auto file_path = std::string(workspace.path_pool.resolve(path_id));
|
||||
|
||||
worker::BuildParams bp;
|
||||
bp.kind = worker::BuildKind::BuildPCM;
|
||||
bp.file = file_path;
|
||||
if(!fill_compile_args(file_path, bp.directory, bp.arguments))
|
||||
co_return false;
|
||||
|
||||
// Compute deterministic content-addressed PCM path.
|
||||
auto safe_module_name = mod_it->second;
|
||||
std::ranges::replace(safe_module_name, ':', '-');
|
||||
std::string hash_input = file_path;
|
||||
for(auto& arg: bp.arguments) {
|
||||
hash_input += arg;
|
||||
}
|
||||
auto args_hash = llvm::xxh3_64bits(llvm::StringRef(hash_input));
|
||||
auto pcm_filename = std::format("{}-{:016x}.pcm", safe_module_name, args_hash);
|
||||
auto pcm_path = path::join(workspace.config.cache_dir, "cache", "pcm", pcm_filename);
|
||||
|
||||
// Check if cached PCM is still valid.
|
||||
if(auto pcm_it = workspace.pcm_cache.find(path_id); pcm_it != workspace.pcm_cache.end()) {
|
||||
if(!pcm_it->second.path.empty() && llvm::sys::fs::exists(pcm_it->second.path) &&
|
||||
!deps_changed(workspace.path_pool, pcm_it->second.deps)) {
|
||||
workspace.pcm_paths[path_id] = pcm_it->second.path;
|
||||
co_return true;
|
||||
}
|
||||
}
|
||||
|
||||
bp.module_name = mod_it->second;
|
||||
bp.output_path = pcm_path;
|
||||
|
||||
// Clang needs ALL transitive PCM deps, not just direct imports.
|
||||
workspace.fill_pcm_deps(bp.pcms);
|
||||
|
||||
auto result = co_await pool.send_stateless(bp);
|
||||
if(!result.has_value() || !result.value().success) {
|
||||
LOG_WARN("BuildPCM failed for module {}: {}",
|
||||
mod_it->second,
|
||||
result.has_value() ? result.value().error : result.error().message);
|
||||
co_return false;
|
||||
}
|
||||
|
||||
workspace.pcm_paths[path_id] = result.value().output_path;
|
||||
workspace.pcm_cache[path_id] = {
|
||||
result.value().output_path,
|
||||
capture_deps_snapshot(workspace.path_pool, result.value().deps)};
|
||||
LOG_INFO("Built PCM for module {}: {}", mod_it->second, result.value().output_path);
|
||||
|
||||
// Persist cache metadata after successful build.
|
||||
workspace.save_cache();
|
||||
|
||||
// Signal that new index data is available for background merge.
|
||||
if(on_indexing_needed)
|
||||
on_indexing_needed();
|
||||
|
||||
co_return true;
|
||||
};
|
||||
|
||||
workspace.compile_graph =
|
||||
std::make_unique<CompileGraph>(std::move(dispatch), std::move(resolve));
|
||||
LOG_INFO("CompileGraph initialized with {} module(s)", workspace.path_to_module.size());
|
||||
}
|
||||
|
||||
bool Compiler::fill_compile_args(llvm::StringRef path,
|
||||
std::string& directory,
|
||||
std::vector<std::string>& arguments,
|
||||
Session* session) {
|
||||
auto path_id = workspace.path_pool.intern(path);
|
||||
|
||||
// 1. If the session has an active header context via switchContext,
|
||||
// use the host source's CDB entry with file path replaced and preamble injected.
|
||||
if(session && session->active_context.has_value()) {
|
||||
return fill_header_context_args(path, path_id, directory, arguments, session);
|
||||
}
|
||||
|
||||
// 2. Normal CDB lookup for the file itself.
|
||||
auto results = workspace.cdb.lookup(path, {.query_toolchain = true});
|
||||
if(!results.empty()) {
|
||||
auto& cmd = results.front();
|
||||
directory = cmd.resolved.directory.str();
|
||||
arguments = cmd.to_string_argv();
|
||||
return true;
|
||||
}
|
||||
|
||||
// 3. No CDB entry — try automatic header context resolution.
|
||||
return fill_header_context_args(path, path_id, directory, arguments, session);
|
||||
}
|
||||
|
||||
bool Compiler::fill_header_context_args(llvm::StringRef path,
|
||||
std::uint32_t path_id,
|
||||
std::string& directory,
|
||||
std::vector<std::string>& arguments,
|
||||
Session* session) {
|
||||
// Use cached context if available; otherwise resolve.
|
||||
// If an active context override exists, invalidate cache if it points to
|
||||
// a different host so we re-resolve with the correct one.
|
||||
const HeaderFileContext* ctx_ptr = nullptr;
|
||||
if(session && session->header_context.has_value()) {
|
||||
if(session->active_context.has_value() &&
|
||||
session->header_context->host_path_id != *session->active_context) {
|
||||
session->header_context.reset();
|
||||
} else {
|
||||
ctx_ptr = &*session->header_context;
|
||||
}
|
||||
}
|
||||
if(!ctx_ptr) {
|
||||
auto resolved = resolve_header_context(path_id, session);
|
||||
if(!resolved) {
|
||||
LOG_WARN("No CDB entry and no header context for {}", path);
|
||||
return false;
|
||||
}
|
||||
if(session) {
|
||||
session->header_context = std::move(*resolved);
|
||||
ctx_ptr = &*session->header_context;
|
||||
} else {
|
||||
// Background indexing path — no session to store on.
|
||||
// Use a temporary (caller will use it immediately).
|
||||
// Store in a local and return.
|
||||
static thread_local std::optional<HeaderFileContext> tl_ctx;
|
||||
tl_ctx = std::move(*resolved);
|
||||
ctx_ptr = &*tl_ctx;
|
||||
}
|
||||
}
|
||||
|
||||
auto host_path = workspace.path_pool.resolve(ctx_ptr->host_path_id);
|
||||
auto host_results = workspace.cdb.lookup(host_path, {.query_toolchain = true});
|
||||
if(host_results.empty()) {
|
||||
LOG_WARN("fill_header_context_args: host {} has no CDB entry", host_path);
|
||||
return false;
|
||||
}
|
||||
|
||||
auto& host_cmd = host_results.front();
|
||||
directory = host_cmd.resolved.directory.str();
|
||||
|
||||
// Replace source_file and inject -include preamble into flags directly.
|
||||
CompileCommand header_cmd = host_cmd;
|
||||
header_cmd.source_file = workspace.path_pool.resolve(path_id).data();
|
||||
|
||||
// Inject -include <preamble> into flags: after "-cc1" for cc1, after driver otherwise.
|
||||
std::size_t inject_pos = header_cmd.resolved.is_cc1 ? 2 : 1;
|
||||
header_cmd.resolved.flags.insert(header_cmd.resolved.flags.begin() + inject_pos,
|
||||
ctx_ptr->preamble_path.c_str());
|
||||
header_cmd.resolved.flags.insert(header_cmd.resolved.flags.begin() + inject_pos, "-include");
|
||||
|
||||
arguments = header_cmd.to_string_argv();
|
||||
|
||||
LOG_INFO("fill_compile_args: header context for {} (host={}, preamble={})",
|
||||
path,
|
||||
host_path,
|
||||
ctx_ptr->preamble_path);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::optional<HeaderFileContext> Compiler::resolve_header_context(std::uint32_t header_path_id,
|
||||
Session* session) {
|
||||
// Find source files that transitively include this header.
|
||||
auto hosts = workspace.dep_graph.find_host_sources(header_path_id);
|
||||
if(hosts.empty()) {
|
||||
LOG_DEBUG("resolve_header_context: no host sources for path_id={}", header_path_id);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// If there's an active context override, prefer that host.
|
||||
std::uint32_t host_path_id = 0;
|
||||
std::vector<std::uint32_t> chain;
|
||||
if(session && session->active_context.has_value()) {
|
||||
auto preferred = *session->active_context;
|
||||
auto preferred_path = workspace.path_pool.resolve(preferred);
|
||||
auto results = workspace.cdb.lookup(preferred_path, {.suppress_logging = true});
|
||||
if(!results.empty()) {
|
||||
auto c = workspace.dep_graph.find_include_chain(preferred, header_path_id);
|
||||
if(!c.empty()) {
|
||||
host_path_id = preferred;
|
||||
chain = std::move(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to the first available host that has a CDB entry.
|
||||
if(chain.empty()) {
|
||||
for(auto candidate: hosts) {
|
||||
auto candidate_path = workspace.path_pool.resolve(candidate);
|
||||
auto results = workspace.cdb.lookup(candidate_path, {.suppress_logging = true});
|
||||
if(results.empty())
|
||||
continue;
|
||||
auto c = workspace.dep_graph.find_include_chain(candidate, header_path_id);
|
||||
if(c.empty())
|
||||
continue;
|
||||
host_path_id = candidate;
|
||||
chain = std::move(c);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(chain.empty()) {
|
||||
LOG_DEBUG("resolve_header_context: no usable host with include chain for path_id={}",
|
||||
header_path_id);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Build preamble text: for each file in the chain except the last (target),
|
||||
// append all content up to (but not including) the line that includes the
|
||||
// next file in the chain.
|
||||
std::string preamble;
|
||||
for(std::size_t i = 0; i + 1 < chain.size(); ++i) {
|
||||
auto cur_id = chain[i];
|
||||
auto next_id = chain[i + 1];
|
||||
|
||||
auto cur_path = workspace.path_pool.resolve(cur_id);
|
||||
auto next_path = workspace.path_pool.resolve(next_id);
|
||||
auto next_filename = llvm::sys::path::filename(next_path);
|
||||
|
||||
// Prefer in-memory document text over disk content.
|
||||
// Use the session if this file matches the session's path, otherwise
|
||||
// fall back to disk.
|
||||
std::string content;
|
||||
// Note: we don't have the sessions map here, so we always read from disk
|
||||
// for intermediate chain files. The session parameter only covers the
|
||||
// header file itself (the target), not intermediate files in the chain.
|
||||
auto buf = llvm::MemoryBuffer::getFile(cur_path);
|
||||
if(!buf) {
|
||||
LOG_WARN("resolve_header_context: cannot read {}", cur_path);
|
||||
return std::nullopt;
|
||||
}
|
||||
content = (*buf)->getBuffer().str();
|
||||
|
||||
// Scan line by line for the #include that brings in next_filename.
|
||||
llvm::StringRef content_ref(content);
|
||||
std::size_t line_start = 0;
|
||||
std::size_t include_line_start = std::string::npos;
|
||||
while(line_start <= content_ref.size()) {
|
||||
auto newline_pos = content_ref.find('\n', line_start);
|
||||
auto line_end =
|
||||
(newline_pos == llvm::StringRef::npos) ? content_ref.size() : newline_pos;
|
||||
auto line = content_ref.slice(line_start, line_end).trim();
|
||||
|
||||
if(line.starts_with("#include") || line.starts_with("# include")) {
|
||||
// Extract the filename from the #include directive.
|
||||
// Handles: #include "foo.h", #include <foo.h>, # include "foo.h"
|
||||
auto quote_start = line.find_first_of("\"<");
|
||||
auto quote_end = llvm::StringRef::npos;
|
||||
if(quote_start != llvm::StringRef::npos) {
|
||||
char close = (line[quote_start] == '"') ? '"' : '>';
|
||||
quote_end = line.find(close, quote_start + 1);
|
||||
}
|
||||
if(quote_start != llvm::StringRef::npos && quote_end != llvm::StringRef::npos) {
|
||||
auto included = line.slice(quote_start + 1, quote_end);
|
||||
auto included_filename = llvm::sys::path::filename(included);
|
||||
if(included_filename == next_filename) {
|
||||
include_line_start = line_start;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
line_start =
|
||||
(newline_pos == llvm::StringRef::npos) ? content_ref.size() + 1 : newline_pos + 1;
|
||||
}
|
||||
|
||||
// Emit a #line marker then all content before the include line.
|
||||
preamble += std::format("#line 1 \"{}\"\n", cur_path.str());
|
||||
if(include_line_start != std::string::npos) {
|
||||
preamble += content_ref.substr(0, include_line_start).str();
|
||||
} else {
|
||||
// No matching include line found — emit the whole file to be safe.
|
||||
LOG_DEBUG("resolve_header_context: include line for {} not found in {}, emitting full",
|
||||
next_filename,
|
||||
cur_path);
|
||||
preamble += content;
|
||||
}
|
||||
}
|
||||
|
||||
// Hash the preamble and write to cache directory.
|
||||
auto preamble_hash = llvm::xxh3_64bits(llvm::StringRef(preamble));
|
||||
auto preamble_filename = std::format("{:016x}.h", preamble_hash);
|
||||
auto preamble_dir = path::join(workspace.config.cache_dir, "header_context");
|
||||
auto preamble_path = path::join(preamble_dir, preamble_filename);
|
||||
|
||||
if(!llvm::sys::fs::exists(preamble_path)) {
|
||||
auto ec = llvm::sys::fs::create_directories(preamble_dir);
|
||||
if(ec) {
|
||||
LOG_WARN("resolve_header_context: cannot create dir {}: {}",
|
||||
preamble_dir,
|
||||
ec.message());
|
||||
return std::nullopt;
|
||||
}
|
||||
if(auto result = fs::write(preamble_path, preamble); !result) {
|
||||
LOG_WARN("resolve_header_context: cannot write preamble {}: {}",
|
||||
preamble_path,
|
||||
result.error().message());
|
||||
return std::nullopt;
|
||||
}
|
||||
LOG_INFO("resolve_header_context: wrote preamble {} for header path_id={}",
|
||||
preamble_path,
|
||||
header_path_id);
|
||||
}
|
||||
|
||||
return HeaderFileContext{host_path_id, preamble_path, preamble_hash};
|
||||
}
|
||||
|
||||
std::string uri_to_path(const std::string& uri) {
|
||||
auto parsed = lsp::URI::parse(uri);
|
||||
if(parsed.has_value()) {
|
||||
auto path = parsed->file_path();
|
||||
if(path.has_value()) {
|
||||
return std::move(*path);
|
||||
}
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
|
||||
void Compiler::publish_diagnostics(const std::string& uri,
|
||||
int version,
|
||||
const kota::codec::RawValue& diagnostics_json) {
|
||||
std::vector<protocol::Diagnostic> diagnostics;
|
||||
if(!diagnostics_json.empty()) {
|
||||
auto status = kota::codec::json::from_json(diagnostics_json.data, diagnostics);
|
||||
if(!status) {
|
||||
LOG_WARN("Failed to deserialize diagnostics JSON for {}", uri);
|
||||
}
|
||||
}
|
||||
protocol::PublishDiagnosticsParams params;
|
||||
params.uri = uri;
|
||||
params.version = version;
|
||||
params.diagnostics = std::move(diagnostics);
|
||||
peer.send_notification(params);
|
||||
}
|
||||
|
||||
void Compiler::clear_diagnostics(const std::string& uri) {
|
||||
protocol::PublishDiagnosticsParams params;
|
||||
params.uri = uri;
|
||||
params.diagnostics = {};
|
||||
peer.send_notification(params);
|
||||
}
|
||||
|
||||
kota::task<bool> Compiler::ensure_pch(Session& session,
|
||||
const std::string& directory,
|
||||
const std::vector<std::string>& arguments) {
|
||||
auto path_id = session.path_id;
|
||||
auto path = workspace.path_pool.resolve(path_id);
|
||||
auto& text = session.text;
|
||||
auto bound = compute_preamble_bound(text);
|
||||
if(bound == 0) {
|
||||
// No preamble directives — PCH would be empty. Clear any stale entry.
|
||||
workspace.pch_cache.erase(path_id);
|
||||
session.pch_ref.reset();
|
||||
co_return true;
|
||||
}
|
||||
|
||||
// FIXME: hash should also include compile flags that affect preprocessing
|
||||
// (e.g. -D, -I, -isystem, -std) so that files with the same preamble text
|
||||
// but different flags produce separate PCHs. Currently only the preamble
|
||||
// text is hashed — the source file path must be excluded from the hash
|
||||
// to allow sharing across files with identical preambles.
|
||||
auto preamble_text = llvm::StringRef(text).substr(0, bound);
|
||||
auto preamble_hash = llvm::xxh3_64bits(preamble_text);
|
||||
|
||||
// Deterministic content-addressed PCH path.
|
||||
auto pch_path = path::join(workspace.config.cache_dir,
|
||||
"cache",
|
||||
"pch",
|
||||
std::format("{:016x}.pch", preamble_hash));
|
||||
|
||||
// Reuse existing PCH if preamble content and deps haven't changed.
|
||||
if(auto it = workspace.pch_cache.find(path_id); it != workspace.pch_cache.end()) {
|
||||
auto& st = it->second;
|
||||
if(st.hash == preamble_hash && !st.path.empty() &&
|
||||
!deps_changed(workspace.path_pool, st.deps)) {
|
||||
st.bound = bound;
|
||||
session.pch_ref = Session::PCHRef{path_id, preamble_hash, bound};
|
||||
co_return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Preamble incomplete (user still typing) — defer rebuild, reuse old PCH if available.
|
||||
if(!is_preamble_complete(text, bound)) {
|
||||
LOG_DEBUG("Preamble incomplete for {}, deferring PCH rebuild", path);
|
||||
co_return workspace.pch_cache.count(path_id) && !workspace.pch_cache[path_id].path.empty();
|
||||
}
|
||||
|
||||
// If another coroutine is already building PCH for this file, wait for it.
|
||||
if(auto it = workspace.pch_cache.find(path_id);
|
||||
it != workspace.pch_cache.end() && it->second.building) {
|
||||
co_await it->second.building->wait();
|
||||
if(auto it2 = workspace.pch_cache.find(path_id); it2 != workspace.pch_cache.end()) {
|
||||
session.pch_ref = Session::PCHRef{path_id, it2->second.hash, it2->second.bound};
|
||||
}
|
||||
co_return workspace.pch_cache.count(path_id) && !workspace.pch_cache[path_id].path.empty();
|
||||
}
|
||||
|
||||
// Register in-flight build so concurrent requests wait on us.
|
||||
auto completion = std::make_shared<kota::event>();
|
||||
workspace.pch_cache[path_id].building = completion;
|
||||
|
||||
// Build a new PCH via stateless worker.
|
||||
worker::BuildParams bp;
|
||||
bp.kind = worker::BuildKind::BuildPCH;
|
||||
bp.file = std::string(path);
|
||||
bp.directory = directory;
|
||||
bp.arguments = arguments;
|
||||
bp.text = text;
|
||||
bp.preamble_bound = bound;
|
||||
bp.output_path = pch_path;
|
||||
|
||||
LOG_DEBUG("Building PCH for {}, bound={}, output={}", path, bound, pch_path);
|
||||
|
||||
auto result = co_await pool.send_stateless(bp);
|
||||
|
||||
if(!result.has_value() || !result.value().success) {
|
||||
LOG_WARN("PCH build failed for {}: {}",
|
||||
path,
|
||||
result.has_value() ? result.value().error : result.error().message);
|
||||
workspace.pch_cache[path_id].building.reset();
|
||||
completion->set();
|
||||
co_return false;
|
||||
}
|
||||
|
||||
auto& st = workspace.pch_cache[path_id];
|
||||
st.path = result.value().output_path;
|
||||
st.bound = bound;
|
||||
st.hash = preamble_hash;
|
||||
st.deps = capture_deps_snapshot(workspace.path_pool, result.value().deps);
|
||||
st.document_links_json = std::move(result.value().pch_links_json);
|
||||
st.building.reset();
|
||||
|
||||
session.pch_ref = Session::PCHRef{path_id, preamble_hash, bound};
|
||||
|
||||
LOG_INFO("PCH built for {}: {}", path, result.value().output_path);
|
||||
|
||||
// Persist cache metadata after successful build.
|
||||
workspace.save_cache();
|
||||
|
||||
completion->set();
|
||||
co_return true;
|
||||
}
|
||||
|
||||
/// Compile module dependencies, build/reuse PCH, and fill PCM paths.
|
||||
/// Shared preparation step used by both ensure_compiled() (stateful path)
|
||||
/// and forward_stateless() (completion/signatureHelp path).
|
||||
kota::task<bool> Compiler::ensure_deps(Session& session,
|
||||
const std::string& directory,
|
||||
const std::vector<std::string>& arguments,
|
||||
std::pair<std::string, uint32_t>& pch,
|
||||
std::unordered_map<std::string, std::string>& pcms) {
|
||||
auto path_id = session.path_id;
|
||||
|
||||
// Compile C++20 module dependencies (PCMs).
|
||||
if(workspace.compile_graph && !co_await workspace.compile_graph->compile_deps(path_id)) {
|
||||
co_return false;
|
||||
}
|
||||
|
||||
// Scan buffer text for module imports that might not be in compile_graph yet.
|
||||
// When a user adds `import std;` without saving, the compile_graph (disk-based)
|
||||
// doesn't know about the new dependency. Scan the in-memory text to find them.
|
||||
{
|
||||
auto scan_result = scan(session.text);
|
||||
for(auto& mod_name: scan_result.modules) {
|
||||
if(mod_name.empty())
|
||||
continue;
|
||||
bool found = false;
|
||||
for(auto& [pid, name]: workspace.path_to_module) {
|
||||
if(name == mod_name) {
|
||||
// If PCM not already built, try to build it.
|
||||
if(workspace.pcm_paths.find(pid) == workspace.pcm_paths.end()) {
|
||||
if(workspace.compile_graph && workspace.compile_graph->has_unit(pid)) {
|
||||
co_await workspace.compile_graph->compile_deps(pid);
|
||||
}
|
||||
}
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!found) {
|
||||
LOG_DEBUG("Buffer imports unknown module '{}', skipping", mod_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build or reuse PCH.
|
||||
auto pch_ok = co_await ensure_pch(session, directory, arguments);
|
||||
if(pch_ok) {
|
||||
if(auto pch_it = workspace.pch_cache.find(path_id); pch_it != workspace.pch_cache.end()) {
|
||||
pch = {pch_it->second.path, pch_it->second.bound};
|
||||
}
|
||||
}
|
||||
|
||||
// Fill all available PCM paths, excluding the file's own PCM
|
||||
// to avoid "multiple module declarations".
|
||||
workspace.fill_pcm_deps(pcms, path_id);
|
||||
|
||||
co_return true;
|
||||
}
|
||||
|
||||
bool Compiler::is_stale(const Session& session) {
|
||||
if(session.ast_deps.has_value() && deps_changed(workspace.path_pool, *session.ast_deps))
|
||||
return true;
|
||||
|
||||
// Check PCH staleness via the session's pch_ref.
|
||||
if(session.pch_ref.has_value()) {
|
||||
auto pch_it = workspace.pch_cache.find(session.pch_ref->path_id);
|
||||
if(pch_it != workspace.pch_cache.end() &&
|
||||
deps_changed(workspace.path_pool, pch_it->second.deps))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void Compiler::record_deps(Session& session, llvm::ArrayRef<std::string> deps) {
|
||||
session.ast_deps = capture_deps_snapshot(workspace.path_pool, deps);
|
||||
}
|
||||
|
||||
/// Pull-based compilation entry point for user-opened files.
|
||||
///
|
||||
/// Called lazily by forward_query() / forward_build() before every
|
||||
/// feature request (hover, semantic tokens, etc.). Guarantees that when it
|
||||
/// returns true the stateful worker assigned to `path_id` holds an up-to-date
|
||||
/// AST and diagnostics have been published to the client.
|
||||
///
|
||||
/// Lifecycle overview (pull-based model):
|
||||
///
|
||||
/// didOpen / didChange – only update Session, mark ast_dirty
|
||||
/// didSave – mark dependents dirty, queue indexing
|
||||
/// feature request arrives – calls ensure_compiled() first
|
||||
/// 1. Fast-path exit if AST is already clean (!ast_dirty).
|
||||
/// 2. Compile any C++20 module dependencies (PCMs) via CompileGraph.
|
||||
/// 3. Build / reuse the precompiled header (PCH) via ensure_pch().
|
||||
/// 4. Send CompileParams to the stateful worker, which builds the AST.
|
||||
/// 5. On success: publish diagnostics, clear ast_dirty, schedule indexing.
|
||||
/// 6. On generation mismatch (user edited during compile): keep dirty,
|
||||
/// the next feature request will trigger another compile cycle.
|
||||
///
|
||||
/// Only the opened file itself is remapped (its in-memory text is sent to the
|
||||
/// worker); every other file is read from disk by the compiler.
|
||||
///
|
||||
/// Concurrency: multiple concurrent feature requests for the same file will
|
||||
/// each call ensure_compiled(). The first one launches a detached compile
|
||||
/// task via loop.schedule(); subsequent ones wait on the shared event.
|
||||
/// The detached task cannot be cancelled by LSP $/cancelRequest, preventing
|
||||
/// the race where cancellation wakes all waiters and they all start compiles.
|
||||
kota::task<bool> Compiler::ensure_compiled(Session& session) {
|
||||
auto path_id = session.path_id;
|
||||
|
||||
LOG_DEBUG("ensure_compiled: path_id={} version={} gen={} ast_dirty={}",
|
||||
path_id,
|
||||
session.version,
|
||||
session.generation,
|
||||
session.ast_dirty);
|
||||
|
||||
if(!session.ast_dirty) {
|
||||
if(!is_stale(session)) {
|
||||
co_return true;
|
||||
}
|
||||
session.ast_dirty = true;
|
||||
}
|
||||
|
||||
// If another compile is already in flight, wait for it.
|
||||
// This co_await may be cancelled by LSP $/cancelRequest — that's fine,
|
||||
// it just means this particular feature request is abandoned. The
|
||||
// detached compile task keeps running independently.
|
||||
while(session.compiling) {
|
||||
auto pending = session.compiling;
|
||||
co_await pending->done.wait();
|
||||
if(!session.ast_dirty)
|
||||
co_return true;
|
||||
}
|
||||
|
||||
// No compile in flight and AST is dirty — launch a detached compile task.
|
||||
// The detached task is scheduled via loop.schedule() so it is NOT subject
|
||||
// to LSP $/cancelRequest cancellation. This eliminates the race where
|
||||
// cancellation fires the RAII guard, waking all waiters simultaneously
|
||||
// and causing them all to start new compiles.
|
||||
auto pending_compile = std::make_shared<Session::PendingCompile>();
|
||||
session.compiling = pending_compile;
|
||||
|
||||
LOG_INFO("ensure_compiled: launching detached compile path_id={} gen={}",
|
||||
path_id,
|
||||
session.generation);
|
||||
|
||||
// Capture path_id by value so the detached lambda can re-lookup the session
|
||||
// from the sessions map after co_await (DenseMap may invalidate pointers).
|
||||
loop.schedule([](Compiler* self,
|
||||
std::uint32_t pid,
|
||||
std::shared_ptr<Session::PendingCompile> pc) -> kota::task<> {
|
||||
// Re-lookup session from the sessions map (pointer may have been
|
||||
// invalidated by DenseMap growth during co_await).
|
||||
auto find_session = [&]() -> Session* {
|
||||
auto it = self->sessions.find(pid);
|
||||
return it != self->sessions.end() ? &it->second : nullptr;
|
||||
};
|
||||
|
||||
auto* sess = find_session();
|
||||
if(!sess) {
|
||||
pc->done.set();
|
||||
co_return;
|
||||
}
|
||||
|
||||
auto finish_compile = [&]() {
|
||||
auto* s = find_session();
|
||||
if(s && s->compiling == pc) {
|
||||
s->compiling.reset();
|
||||
}
|
||||
LOG_INFO("ensure_compiled: finish_compile (detached) path_id={}", pid);
|
||||
pc->done.set();
|
||||
};
|
||||
|
||||
auto gen = sess->generation;
|
||||
LOG_INFO("ensure_compiled: starting compile (detached) path_id={} gen={}", pid, gen);
|
||||
|
||||
auto file_path = std::string(self->workspace.path_pool.resolve(pid));
|
||||
auto uri = lsp::URI::from_file_path(file_path);
|
||||
std::string uri_str = uri.has_value() ? uri->str() : file_path;
|
||||
|
||||
worker::CompileParams params;
|
||||
params.path = file_path;
|
||||
params.version = sess->version;
|
||||
params.text = sess->text;
|
||||
if(!self->fill_compile_args(file_path, params.directory, params.arguments, sess)) {
|
||||
finish_compile();
|
||||
co_return;
|
||||
}
|
||||
|
||||
if(!co_await self
|
||||
->ensure_deps(*sess, params.directory, params.arguments, params.pch, params.pcms)) {
|
||||
LOG_WARN("Dependency preparation failed for {}, skipping compile", uri_str);
|
||||
finish_compile();
|
||||
co_return;
|
||||
}
|
||||
|
||||
// Re-lookup after co_await (DenseMap may have grown).
|
||||
sess = find_session();
|
||||
if(!sess) {
|
||||
pc->done.set();
|
||||
co_return;
|
||||
}
|
||||
|
||||
auto result = co_await self->pool.send_stateful(pid, params);
|
||||
|
||||
// Re-lookup after co_await.
|
||||
sess = find_session();
|
||||
if(!sess) {
|
||||
pc->done.set();
|
||||
co_return;
|
||||
}
|
||||
|
||||
if(sess->generation != gen) {
|
||||
LOG_INFO("ensure_compiled: generation mismatch ({} vs {}) for {}",
|
||||
sess->generation,
|
||||
gen,
|
||||
uri_str);
|
||||
finish_compile();
|
||||
co_return;
|
||||
}
|
||||
|
||||
if(!result.has_value()) {
|
||||
LOG_WARN("Compile failed for {}: {}", uri_str, result.error().message);
|
||||
self->clear_diagnostics(uri_str);
|
||||
finish_compile();
|
||||
co_return;
|
||||
}
|
||||
|
||||
sess->ast_dirty = false;
|
||||
pc->succeeded = true;
|
||||
self->record_deps(*sess, result.value().deps);
|
||||
|
||||
// Store open file index from the stateful worker's TUIndex.
|
||||
if(!result.value().tu_index_data.empty()) {
|
||||
auto tu_index = index::TUIndex::from(result.value().tu_index_data.data());
|
||||
OpenFileIndex ofi;
|
||||
ofi.file_index = std::move(tu_index.main_file_index);
|
||||
ofi.symbols = std::move(tu_index.symbols);
|
||||
ofi.content = sess->text;
|
||||
ofi.mapper.emplace(ofi.content, lsp::PositionEncoding::UTF16);
|
||||
sess->file_index = std::move(ofi);
|
||||
}
|
||||
|
||||
auto version = sess->version;
|
||||
finish_compile();
|
||||
|
||||
// Publish diagnostics AFTER marking compile as done, so that concurrent
|
||||
// forward_query() calls can proceed immediately.
|
||||
self->publish_diagnostics(uri_str, version, result.value().diagnostics);
|
||||
if(self->on_indexing_needed)
|
||||
self->on_indexing_needed();
|
||||
}(this, path_id, pending_compile));
|
||||
|
||||
// Wait for the detached compile to finish. If this wait is cancelled
|
||||
// by LSP $/cancelRequest, the detached task continues unaffected.
|
||||
co_await pending_compile->done.wait();
|
||||
|
||||
co_return !session.ast_dirty;
|
||||
}
|
||||
|
||||
Compiler::RawResult Compiler::forward_query(worker::QueryKind kind,
|
||||
Session& session,
|
||||
std::optional<protocol::Position> position,
|
||||
std::optional<protocol::Range> range) {
|
||||
auto path_id = session.path_id;
|
||||
auto path = std::string(workspace.path_pool.resolve(path_id));
|
||||
// Cache text before co_await — session reference may dangle if didClose
|
||||
// erases the entry from the sessions map during suspension.
|
||||
auto text = session.text;
|
||||
|
||||
if(!co_await ensure_compiled(session)) {
|
||||
co_return serde_raw{"null"};
|
||||
}
|
||||
|
||||
auto sit = sessions.find(path_id);
|
||||
if(sit == sessions.end() || sit->second.ast_dirty) {
|
||||
co_return serde_raw{"null"};
|
||||
}
|
||||
|
||||
worker::QueryParams wp;
|
||||
wp.kind = kind;
|
||||
wp.path = path;
|
||||
|
||||
lsp::PositionMapper mapper(text, lsp::PositionEncoding::UTF16);
|
||||
|
||||
if(position) {
|
||||
auto offset = mapper.to_offset(*position);
|
||||
if(!offset)
|
||||
co_return serde_raw{"null"};
|
||||
wp.offset = *offset;
|
||||
}
|
||||
|
||||
if(range) {
|
||||
auto start = mapper.to_offset(range->start);
|
||||
auto end = mapper.to_offset(range->end);
|
||||
if(start && end) {
|
||||
wp.range = {*start, *end};
|
||||
}
|
||||
}
|
||||
|
||||
auto result = co_await pool.send_stateful(path_id, wp);
|
||||
if(!result.has_value()) {
|
||||
co_return serde_raw{};
|
||||
}
|
||||
co_return std::move(result.value());
|
||||
}
|
||||
|
||||
Compiler::RawResult Compiler::forward_build(worker::BuildKind kind,
|
||||
const protocol::Position& position,
|
||||
Session& session) {
|
||||
auto path_id = session.path_id;
|
||||
auto path = std::string(workspace.path_pool.resolve(path_id));
|
||||
|
||||
worker::BuildParams wp;
|
||||
wp.kind = kind;
|
||||
wp.file = path;
|
||||
// Cache session fields before co_await — session reference may dangle
|
||||
// if didClose erases the entry from the sessions map during suspension.
|
||||
wp.version = session.version;
|
||||
wp.text = session.text;
|
||||
if(!fill_compile_args(path, wp.directory, wp.arguments, &session)) {
|
||||
co_return serde_raw{};
|
||||
}
|
||||
|
||||
if(!co_await ensure_deps(session, wp.directory, wp.arguments, wp.pch, wp.pcms)) {
|
||||
co_return serde_raw{};
|
||||
}
|
||||
|
||||
// After co_await, verify session still exists.
|
||||
if(sessions.find(path_id) == sessions.end()) {
|
||||
co_return serde_raw{};
|
||||
}
|
||||
|
||||
lsp::PositionMapper mapper(wp.text, lsp::PositionEncoding::UTF16);
|
||||
auto offset = mapper.to_offset(position);
|
||||
if(!offset)
|
||||
co_return serde_raw{"null"};
|
||||
wp.offset = *offset;
|
||||
|
||||
auto result = co_await pool.send_stateless(wp);
|
||||
if(!result.has_value()) {
|
||||
co_return serde_raw{};
|
||||
}
|
||||
co_return std::move(result.value().result_json);
|
||||
}
|
||||
|
||||
Compiler::RawResult Compiler::handle_completion(const protocol::Position& position,
|
||||
Session& session) {
|
||||
auto path_id = session.path_id;
|
||||
auto path = std::string(workspace.path_pool.resolve(path_id));
|
||||
|
||||
lsp::PositionMapper mapper(session.text, lsp::PositionEncoding::UTF16);
|
||||
auto offset = mapper.to_offset(position);
|
||||
if(offset) {
|
||||
auto pctx = detect_completion_context(session.text, *offset);
|
||||
if(pctx.kind == CompletionContext::IncludeQuoted ||
|
||||
pctx.kind == CompletionContext::IncludeAngled) {
|
||||
std::string directory;
|
||||
std::vector<std::string> arguments;
|
||||
if(!fill_compile_args(path, directory, arguments))
|
||||
co_return serde_raw{"[]"};
|
||||
|
||||
std::vector<const char*> args_ptrs;
|
||||
args_ptrs.reserve(arguments.size());
|
||||
for(auto& arg: arguments)
|
||||
args_ptrs.push_back(arg.c_str());
|
||||
|
||||
auto search_config = extract_search_config(args_ptrs, directory);
|
||||
DirListingCache dir_cache;
|
||||
auto resolved = resolve_search_config(search_config, dir_cache);
|
||||
bool angled = (pctx.kind == CompletionContext::IncludeAngled);
|
||||
auto candidates = complete_include_path(resolved, pctx.prefix, angled, dir_cache);
|
||||
|
||||
std::vector<protocol::CompletionItem> items;
|
||||
items.reserve(candidates.size());
|
||||
for(auto& c: candidates) {
|
||||
protocol::CompletionItem item;
|
||||
item.label = c.is_directory ? c.name + "/" : c.name;
|
||||
item.kind = protocol::CompletionItemKind::File;
|
||||
items.push_back(std::move(item));
|
||||
}
|
||||
auto json = kota::codec::json::to_json<kota::ipc::lsp_config>(items);
|
||||
co_return serde_raw{json ? std::move(*json) : "[]"};
|
||||
}
|
||||
if(pctx.kind == CompletionContext::Import) {
|
||||
auto module_names = complete_module_import(workspace.path_to_module, pctx.prefix);
|
||||
|
||||
std::vector<protocol::CompletionItem> items;
|
||||
items.reserve(module_names.size());
|
||||
for(auto& name: module_names) {
|
||||
protocol::CompletionItem item;
|
||||
item.label = name;
|
||||
item.kind = protocol::CompletionItemKind::Module;
|
||||
item.insert_text = name + ";";
|
||||
items.push_back(std::move(item));
|
||||
}
|
||||
auto json = kota::codec::json::to_json<kota::ipc::lsp_config>(items);
|
||||
co_return serde_raw{json ? std::move(*json) : "[]"};
|
||||
}
|
||||
}
|
||||
|
||||
co_return co_await forward_build(worker::BuildKind::Completion, position, session);
|
||||
}
|
||||
|
||||
} // namespace clice
|
||||
134
src/server/compiler.h
Normal file
134
src/server/compiler.h
Normal file
@@ -0,0 +1,134 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "command/command.h"
|
||||
#include "server/session.h"
|
||||
#include "server/worker_pool.h"
|
||||
#include "server/workspace.h"
|
||||
#include "syntax/completion.h"
|
||||
|
||||
#include "kota/async/async.h"
|
||||
#include "kota/codec/raw_value.h"
|
||||
#include "kota/ipc/codec/json.h"
|
||||
#include "kota/ipc/lsp/protocol.h"
|
||||
#include "kota/ipc/peer.h"
|
||||
#include "llvm/ADT/ArrayRef.h"
|
||||
#include "llvm/ADT/DenseMap.h"
|
||||
#include "llvm/ADT/SmallVector.h"
|
||||
#include "llvm/ADT/StringRef.h"
|
||||
|
||||
namespace clice {
|
||||
|
||||
namespace protocol = kota::ipc::protocol;
|
||||
|
||||
/// Convert a file:// URI to a local file path.
|
||||
std::string uri_to_path(const std::string& uri);
|
||||
|
||||
/// Compilation service — drives worker processes to build ASTs, PCHs, and PCMs.
|
||||
///
|
||||
/// Compiler holds no persistent state of its own. All project-wide data
|
||||
/// lives in Workspace; per-file data lives in Session. Compiler reads from
|
||||
/// both and writes compilation results back to Session (file_index, pch_ref,
|
||||
/// ast_deps, diagnostics).
|
||||
///
|
||||
/// Responsibilities:
|
||||
/// - AST compilation lifecycle (ensure_compiled → ensure_pch → ensure_deps)
|
||||
/// - Feature request forwarding to stateful/stateless workers
|
||||
/// - Compile argument resolution (CDB lookup + header context fallback)
|
||||
/// - Compile graph initialization (module DAG setup)
|
||||
///
|
||||
/// NOT responsible for:
|
||||
/// - Document lifecycle (didOpen/didChange/didClose) — handled by MasterServer
|
||||
/// - Index queries — handled by Indexer
|
||||
/// - Background indexing scheduling — handled by Indexer
|
||||
class Compiler {
|
||||
public:
|
||||
Compiler(kota::event_loop& loop,
|
||||
kota::ipc::JsonPeer& peer,
|
||||
Workspace& workspace,
|
||||
WorkerPool& pool,
|
||||
llvm::DenseMap<std::uint32_t, Session>& sessions);
|
||||
~Compiler();
|
||||
|
||||
void init_compile_graph();
|
||||
|
||||
/// Fill compile arguments for a file (CDB lookup + header context fallback).
|
||||
/// @param session If non-null, used for header context resolution on open files.
|
||||
bool fill_compile_args(llvm::StringRef path,
|
||||
std::string& directory,
|
||||
std::vector<std::string>& arguments,
|
||||
Session* session = nullptr);
|
||||
|
||||
/// Compile an open file's AST if dirty. On success, updates session's
|
||||
/// file_index, pch_ref, ast_deps, and publishes diagnostics.
|
||||
kota::task<bool> ensure_compiled(Session& session);
|
||||
|
||||
using RawResult = kota::task<kota::codec::RawValue, kota::ipc::Error>;
|
||||
|
||||
/// Forward a query to the stateful worker that holds this file's AST.
|
||||
/// Ensures compilation first. For position-sensitive queries (hover,
|
||||
/// goto-definition), pass a Position. For range-sensitive queries
|
||||
/// (inlay hints), pass a Range.
|
||||
RawResult forward_query(worker::QueryKind kind,
|
||||
Session& session,
|
||||
std::optional<protocol::Position> position = {},
|
||||
std::optional<protocol::Range> range = {});
|
||||
|
||||
/// Forward a build request (signature help, etc.) to a stateless worker.
|
||||
/// Sends the full buffer content and compile arguments.
|
||||
RawResult forward_build(worker::BuildKind kind,
|
||||
const protocol::Position& position,
|
||||
Session& session);
|
||||
|
||||
/// Handle completion requests. Detects preamble context (include/import)
|
||||
/// and serves those locally; delegates code completion to a stateless worker.
|
||||
RawResult handle_completion(const protocol::Position& position, Session& session);
|
||||
|
||||
/// Send an empty diagnostics notification to clear stale markers in the editor.
|
||||
void clear_diagnostics(const std::string& uri);
|
||||
|
||||
/// Callback invoked when indexing should be scheduled.
|
||||
std::function<void()> on_indexing_needed;
|
||||
|
||||
private:
|
||||
kota::task<bool> ensure_deps(Session& session,
|
||||
const std::string& directory,
|
||||
const std::vector<std::string>& arguments,
|
||||
std::pair<std::string, uint32_t>& pch,
|
||||
std::unordered_map<std::string, std::string>& pcms);
|
||||
|
||||
kota::task<bool> ensure_pch(Session& session,
|
||||
const std::string& directory,
|
||||
const std::vector<std::string>& arguments);
|
||||
|
||||
bool is_stale(const Session& session);
|
||||
void record_deps(Session& session, llvm::ArrayRef<std::string> deps);
|
||||
|
||||
void publish_diagnostics(const std::string& uri,
|
||||
int version,
|
||||
const kota::codec::RawValue& diags);
|
||||
|
||||
std::optional<HeaderFileContext> resolve_header_context(std::uint32_t header_path_id,
|
||||
Session* session);
|
||||
|
||||
bool fill_header_context_args(llvm::StringRef path,
|
||||
std::uint32_t path_id,
|
||||
std::string& directory,
|
||||
std::vector<std::string>& arguments,
|
||||
Session* session);
|
||||
|
||||
private:
|
||||
kota::event_loop& loop;
|
||||
kota::ipc::JsonPeer& peer;
|
||||
Workspace& workspace;
|
||||
WorkerPool& pool;
|
||||
llvm::DenseMap<std::uint32_t, Session>& sessions;
|
||||
};
|
||||
|
||||
} // namespace clice
|
||||
@@ -3,10 +3,11 @@
|
||||
#include <algorithm>
|
||||
#include <thread>
|
||||
|
||||
#include "eventide/serde/toml.h"
|
||||
#include "support/filesystem.h"
|
||||
#include "support/logging.h"
|
||||
|
||||
#include "kota/codec/toml.h"
|
||||
|
||||
namespace clice {
|
||||
|
||||
/// Replace all occurrences of ${workspace} with the workspace root.
|
||||
@@ -25,10 +26,10 @@ void CliceConfig::apply_defaults(const std::string& workspace_root) {
|
||||
cpu_count = 4;
|
||||
|
||||
if(stateful_worker_count == 0) {
|
||||
stateful_worker_count = std::max(1u, cpu_count / 4);
|
||||
stateful_worker_count = 2;
|
||||
}
|
||||
if(stateless_worker_count == 0) {
|
||||
stateless_worker_count = std::max(1u, cpu_count / 4);
|
||||
stateless_worker_count = 3;
|
||||
}
|
||||
if(worker_memory_limit == 0) {
|
||||
worker_memory_limit = 4ULL * 1024 * 1024 * 1024; // 4GB default
|
||||
@@ -41,10 +42,15 @@ void CliceConfig::apply_defaults(const std::string& workspace_root) {
|
||||
index_dir = path::join(cache_dir, "index");
|
||||
}
|
||||
|
||||
if(logging_dir.empty() && !cache_dir.empty()) {
|
||||
logging_dir = path::join(cache_dir, "logs");
|
||||
}
|
||||
|
||||
// Apply variable substitution to string fields
|
||||
substitute_workspace(compile_commands_path, workspace_root);
|
||||
substitute_workspace(cache_dir, workspace_root);
|
||||
substitute_workspace(index_dir, workspace_root);
|
||||
substitute_workspace(logging_dir, workspace_root);
|
||||
}
|
||||
|
||||
std::optional<CliceConfig> CliceConfig::load(const std::string& path,
|
||||
@@ -54,7 +60,7 @@ std::optional<CliceConfig> CliceConfig::load(const std::string& path,
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto result = eventide::serde::toml::parse<CliceConfig>(*content);
|
||||
auto result = kota::codec::toml::parse<CliceConfig>(*content);
|
||||
if(!result) {
|
||||
LOG_WARN("Failed to parse config file {}", path);
|
||||
return std::nullopt;
|
||||
|
||||
@@ -22,8 +22,8 @@ struct CliceConfig {
|
||||
// Index storage directory (default: <cache_dir>/index/)
|
||||
std::string index_dir;
|
||||
|
||||
// Debounce interval for re-compilation after edits (milliseconds)
|
||||
int debounce_ms = 200;
|
||||
// Logging directory (default: <cache_dir>/logs/)
|
||||
std::string logging_dir;
|
||||
|
||||
// Background indexing
|
||||
bool enable_indexing = true;
|
||||
|
||||
696
src/server/indexer.cpp
Normal file
696
src/server/indexer.cpp
Normal file
@@ -0,0 +1,696 @@
|
||||
#include "server/indexer.h"
|
||||
|
||||
#include <string>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
#include "index/tu_index.h"
|
||||
#include "server/compiler.h"
|
||||
#include "server/protocol.h"
|
||||
#include "server/session.h"
|
||||
#include "server/worker_pool.h"
|
||||
#include "support/filesystem.h"
|
||||
#include "support/logging.h"
|
||||
|
||||
#include "kota/ipc/lsp/position.h"
|
||||
#include "kota/ipc/lsp/protocol.h"
|
||||
#include "kota/ipc/lsp/uri.h"
|
||||
#include "llvm/Support/FileSystem.h"
|
||||
#include "llvm/Support/MemoryBuffer.h"
|
||||
#include "llvm/Support/Path.h"
|
||||
#include "llvm/Support/raw_ostream.h"
|
||||
|
||||
namespace clice {
|
||||
|
||||
namespace lsp = kota::ipc::lsp;
|
||||
|
||||
void Indexer::merge(const void* tu_index_data, std::size_t size) {
|
||||
auto tu_index = index::TUIndex::from(tu_index_data);
|
||||
if(tu_index.graph.paths.empty()) {
|
||||
LOG_WARN("Ignoring TUIndex with empty path graph");
|
||||
return;
|
||||
}
|
||||
auto file_ids_map = workspace.project_index.merge(tu_index);
|
||||
auto main_tu_path_id = static_cast<std::uint32_t>(tu_index.graph.paths.size() - 1);
|
||||
|
||||
auto merge_file_index = [&](std::uint32_t tu_path_id, index::FileIndex& file_idx) {
|
||||
auto global_path_id = file_ids_map[tu_path_id];
|
||||
auto& shard = workspace.merged_indices[global_path_id];
|
||||
|
||||
if(tu_path_id == main_tu_path_id) {
|
||||
std::vector<index::IncludeLocation> include_locs;
|
||||
for(auto& loc: tu_index.graph.locations) {
|
||||
index::IncludeLocation remapped = loc;
|
||||
remapped.path_id = file_ids_map[loc.path_id];
|
||||
include_locs.push_back(remapped);
|
||||
}
|
||||
auto file_path = workspace.project_index.path_pool.path(global_path_id);
|
||||
llvm::StringRef file_content;
|
||||
std::string file_content_storage;
|
||||
auto buf = llvm::MemoryBuffer::getFile(file_path);
|
||||
if(buf) {
|
||||
file_content_storage = (*buf)->getBuffer().str();
|
||||
file_content = file_content_storage;
|
||||
}
|
||||
shard.index.merge(global_path_id,
|
||||
tu_index.built_at,
|
||||
std::move(include_locs),
|
||||
file_idx,
|
||||
file_content);
|
||||
} else {
|
||||
std::optional<std::uint32_t> include_id;
|
||||
for(std::uint32_t i = 0; i < tu_index.graph.locations.size(); ++i) {
|
||||
if(tu_index.graph.locations[i].path_id == tu_path_id) {
|
||||
include_id = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!include_id) {
|
||||
LOG_WARN("Skip merge for path {}: include location not found", global_path_id);
|
||||
return;
|
||||
}
|
||||
auto header_path = workspace.project_index.path_pool.path(global_path_id);
|
||||
llvm::StringRef header_content;
|
||||
std::string header_content_storage;
|
||||
auto header_buf = llvm::MemoryBuffer::getFile(header_path);
|
||||
if(header_buf) {
|
||||
header_content_storage = (*header_buf)->getBuffer().str();
|
||||
header_content = header_content_storage;
|
||||
}
|
||||
shard.index.merge(global_path_id, *include_id, file_idx, header_content);
|
||||
}
|
||||
shard.invalidate_mapper();
|
||||
};
|
||||
|
||||
for(auto& [tu_path_id, file_idx]: tu_index.path_file_indices) {
|
||||
merge_file_index(tu_path_id, file_idx);
|
||||
}
|
||||
merge_file_index(main_tu_path_id, tu_index.main_file_index);
|
||||
|
||||
LOG_INFO("Merged TUIndex: {} paths, {} symbols, {} merged_shards",
|
||||
tu_index.graph.paths.size(),
|
||||
tu_index.symbols.size(),
|
||||
workspace.merged_indices.size());
|
||||
}
|
||||
|
||||
void Indexer::save(llvm::StringRef index_dir) {
|
||||
if(index_dir.empty())
|
||||
return;
|
||||
|
||||
auto ec = llvm::sys::fs::create_directories(index_dir);
|
||||
if(ec) {
|
||||
LOG_WARN("Failed to create index directory {}: {}", std::string(index_dir), ec.message());
|
||||
return;
|
||||
}
|
||||
|
||||
auto project_path = path::join(index_dir, "project.idx");
|
||||
{
|
||||
std::error_code write_ec;
|
||||
llvm::raw_fd_ostream os(project_path, write_ec);
|
||||
if(!write_ec) {
|
||||
workspace.project_index.serialize(os);
|
||||
LOG_INFO("Saved ProjectIndex to {}", project_path);
|
||||
} else {
|
||||
LOG_WARN("Failed to save ProjectIndex: {}", write_ec.message());
|
||||
}
|
||||
}
|
||||
|
||||
auto shards_dir = path::join(index_dir, "shards");
|
||||
ec = llvm::sys::fs::create_directories(shards_dir);
|
||||
if(ec) {
|
||||
LOG_WARN("Failed to create shards directory: {}", ec.message());
|
||||
return;
|
||||
}
|
||||
|
||||
std::size_t saved = 0;
|
||||
for(auto& [path_id, shard]: workspace.merged_indices) {
|
||||
if(!shard.index.need_rewrite())
|
||||
continue;
|
||||
auto shard_path = path::join(shards_dir, std::to_string(path_id) + ".idx");
|
||||
std::error_code write_ec;
|
||||
llvm::raw_fd_ostream os(shard_path, write_ec);
|
||||
if(!write_ec) {
|
||||
shard.index.serialize(os);
|
||||
++saved;
|
||||
}
|
||||
}
|
||||
LOG_INFO("Saved {} MergedIndex shards (of {} total)", saved, workspace.merged_indices.size());
|
||||
}
|
||||
|
||||
void Indexer::load(llvm::StringRef index_dir) {
|
||||
if(index_dir.empty())
|
||||
return;
|
||||
|
||||
auto project_path = path::join(index_dir, "project.idx");
|
||||
auto buf = llvm::MemoryBuffer::getFile(project_path);
|
||||
if(buf) {
|
||||
workspace.project_index = index::ProjectIndex::from((*buf)->getBufferStart());
|
||||
LOG_INFO("Loaded ProjectIndex: {} symbols", workspace.project_index.symbols.size());
|
||||
}
|
||||
|
||||
auto shards_dir = path::join(index_dir, "shards");
|
||||
std::error_code ec;
|
||||
for(auto it = llvm::sys::fs::directory_iterator(shards_dir, ec);
|
||||
!ec && it != llvm::sys::fs::directory_iterator();
|
||||
it.increment(ec)) {
|
||||
auto filename = llvm::sys::path::filename(it->path());
|
||||
if(!filename.ends_with(".idx"))
|
||||
continue;
|
||||
auto stem = filename.drop_back(4);
|
||||
std::uint32_t path_id = 0;
|
||||
if(stem.getAsInteger(10, path_id))
|
||||
continue;
|
||||
workspace.merged_indices[path_id] = MergedIndexShard{index::MergedIndex::load(it->path())};
|
||||
}
|
||||
|
||||
if(!workspace.merged_indices.empty()) {
|
||||
LOG_INFO("Loaded {} MergedIndex shards", workspace.merged_indices.size());
|
||||
}
|
||||
}
|
||||
|
||||
bool Indexer::need_update(llvm::StringRef file_path) {
|
||||
auto cache_it = workspace.project_index.path_pool.find(file_path);
|
||||
if(cache_it == workspace.project_index.path_pool.cache.end())
|
||||
return true;
|
||||
|
||||
auto merged_it = workspace.merged_indices.find(cache_it->second);
|
||||
if(merged_it == workspace.merged_indices.end())
|
||||
return true;
|
||||
|
||||
llvm::SmallVector<llvm::StringRef> path_mapping;
|
||||
for(auto& p: workspace.project_index.path_pool.paths) {
|
||||
path_mapping.push_back(p);
|
||||
}
|
||||
return merged_it->second.index.need_update(path_mapping);
|
||||
}
|
||||
|
||||
bool Indexer::find_symbol_info(index::SymbolHash hash, std::string& name, SymbolKind& kind) const {
|
||||
for(auto& [_, session]: sessions) {
|
||||
if(!session.file_index)
|
||||
continue;
|
||||
auto it = session.file_index->symbols.find(hash);
|
||||
if(it != session.file_index->symbols.end()) {
|
||||
name = it->second.name;
|
||||
kind = it->second.kind;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
auto it = workspace.project_index.symbols.find(hash);
|
||||
if(it != workspace.project_index.symbols.end()) {
|
||||
name = it->second.name;
|
||||
kind = it->second.kind;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Indexer::CursorHit Indexer::resolve_cursor(llvm::StringRef path,
|
||||
const protocol::Position& position,
|
||||
Session* session) {
|
||||
// Try the session's open file index first.
|
||||
if(session && session->file_index) {
|
||||
auto& index = *session->file_index;
|
||||
if(!index.mapper)
|
||||
return {};
|
||||
auto offset = index.mapper->to_offset(position);
|
||||
if(!offset)
|
||||
return {};
|
||||
if(auto found = index.find_occurrence(*offset))
|
||||
return {found->first, found->second};
|
||||
return {};
|
||||
}
|
||||
|
||||
// Fallback to MergedIndex, using session text (or reading from disk) for position -> offset.
|
||||
const std::string* doc_text = session ? &session->text : nullptr;
|
||||
if(!doc_text)
|
||||
return {};
|
||||
lsp::PositionMapper doc_mapper(*doc_text, lsp::PositionEncoding::UTF16);
|
||||
auto offset = doc_mapper.to_offset(position);
|
||||
if(!offset)
|
||||
return {};
|
||||
|
||||
auto proj_it = workspace.project_index.path_pool.find(path);
|
||||
if(proj_it == workspace.project_index.path_pool.cache.end())
|
||||
return {};
|
||||
auto shard_it = workspace.merged_indices.find(proj_it->second);
|
||||
if(shard_it == workspace.merged_indices.end())
|
||||
return {};
|
||||
|
||||
if(auto found = shard_it->second.find_occurrence(*offset))
|
||||
return {found->first, found->second};
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<protocol::Location> Indexer::query_relations(llvm::StringRef path,
|
||||
const protocol::Position& position,
|
||||
RelationKind kind,
|
||||
Session* session) {
|
||||
auto hit = resolve_cursor(path, position, session);
|
||||
if(hit.hash == 0)
|
||||
return {};
|
||||
|
||||
std::vector<protocol::Location> locations;
|
||||
|
||||
auto sym_it = workspace.project_index.symbols.find(hit.hash);
|
||||
if(sym_it != workspace.project_index.symbols.end()) {
|
||||
for(auto file_id: sym_it->second.reference_files) {
|
||||
if(is_proj_path_open(file_id))
|
||||
continue;
|
||||
auto shard_it = workspace.merged_indices.find(file_id);
|
||||
if(shard_it == workspace.merged_indices.end())
|
||||
continue;
|
||||
auto uri = lsp::URI::from_file_path(workspace.project_index.path_pool.path(file_id));
|
||||
if(!uri)
|
||||
continue;
|
||||
shard_it->second.find_relations(hit.hash,
|
||||
kind,
|
||||
[&](const auto&, protocol::Range range) {
|
||||
locations.push_back({uri->str(), range});
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for(auto& [id, sess]: sessions) {
|
||||
if(!sess.file_index)
|
||||
continue;
|
||||
auto uri = lsp::URI::from_file_path(std::string(workspace.path_pool.resolve(id)));
|
||||
if(!uri)
|
||||
continue;
|
||||
sess.file_index->find_relations(hit.hash, kind, [&](const auto&, protocol::Range range) {
|
||||
locations.push_back({uri->str(), range});
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
return locations;
|
||||
}
|
||||
|
||||
std::optional<SymbolInfo> Indexer::lookup_symbol(const std::string& uri,
|
||||
llvm::StringRef path,
|
||||
const protocol::Position& position,
|
||||
Session* session) {
|
||||
auto hit = resolve_cursor(path, position, session);
|
||||
if(hit.hash == 0)
|
||||
return std::nullopt;
|
||||
|
||||
std::string name;
|
||||
SymbolKind sym_kind;
|
||||
if(!find_symbol_info(hit.hash, name, sym_kind))
|
||||
return std::nullopt;
|
||||
|
||||
return SymbolInfo{hit.hash, std::move(name), sym_kind, uri, hit.range};
|
||||
}
|
||||
|
||||
std::optional<protocol::Location> Indexer::find_definition_location(index::SymbolHash hash) {
|
||||
// Open file indices first (fresher data for actively-edited files).
|
||||
for(auto& [id, sess]: sessions) {
|
||||
if(!sess.file_index)
|
||||
continue;
|
||||
auto uri = lsp::URI::from_file_path(std::string(workspace.path_pool.resolve(id)));
|
||||
if(!uri)
|
||||
continue;
|
||||
std::optional<protocol::Location> result;
|
||||
sess.file_index->find_relations(hash,
|
||||
RelationKind::Definition,
|
||||
[&](const auto&, protocol::Range range) {
|
||||
result = protocol::Location{uri->str(), range};
|
||||
return false;
|
||||
});
|
||||
if(result)
|
||||
return result;
|
||||
}
|
||||
|
||||
// Fall back to ProjectIndex reference files.
|
||||
auto sym_it = workspace.project_index.symbols.find(hash);
|
||||
if(sym_it == workspace.project_index.symbols.end())
|
||||
return std::nullopt;
|
||||
|
||||
for(auto file_id: sym_it->second.reference_files) {
|
||||
if(is_proj_path_open(file_id))
|
||||
continue;
|
||||
auto shard_it = workspace.merged_indices.find(file_id);
|
||||
if(shard_it == workspace.merged_indices.end())
|
||||
continue;
|
||||
auto uri = lsp::URI::from_file_path(workspace.project_index.path_pool.path(file_id));
|
||||
if(!uri)
|
||||
continue;
|
||||
std::optional<protocol::Location> result;
|
||||
shard_it->second.find_relations(hash,
|
||||
RelationKind::Definition,
|
||||
[&](const auto&, protocol::Range range) {
|
||||
result = protocol::Location{uri->str(), range};
|
||||
return false;
|
||||
});
|
||||
if(result)
|
||||
return result;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<SymbolInfo>
|
||||
Indexer::resolve_hierarchy_item(const std::string& uri,
|
||||
llvm::StringRef path,
|
||||
const protocol::Range& range,
|
||||
const std::optional<protocol::LSPAny>& data,
|
||||
Session* session) {
|
||||
if(data) {
|
||||
if(auto* int_val = std::get_if<std::int64_t>(&*data)) {
|
||||
auto hash = static_cast<index::SymbolHash>(*int_val);
|
||||
std::string name;
|
||||
SymbolKind kind;
|
||||
if(find_symbol_info(hash, name, kind)) {
|
||||
return SymbolInfo{hash, std::move(name), kind, uri, range};
|
||||
}
|
||||
}
|
||||
}
|
||||
return lookup_symbol(uri, path, range.start, session);
|
||||
}
|
||||
|
||||
void Indexer::collect_grouped_relations(
|
||||
index::SymbolHash hash,
|
||||
RelationKind kind,
|
||||
llvm::DenseMap<index::SymbolHash, std::vector<protocol::Range>>& target_ranges) {
|
||||
auto sym_it = workspace.project_index.symbols.find(hash);
|
||||
if(sym_it != workspace.project_index.symbols.end()) {
|
||||
for(auto file_id: sym_it->second.reference_files) {
|
||||
if(is_proj_path_open(file_id))
|
||||
continue;
|
||||
auto shard_it = workspace.merged_indices.find(file_id);
|
||||
if(shard_it == workspace.merged_indices.end())
|
||||
continue;
|
||||
shard_it->second.find_relations(hash, kind, [&](const auto& r, protocol::Range range) {
|
||||
target_ranges[r.target_symbol].push_back(range);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
for(auto& [_, sess]: sessions) {
|
||||
if(!sess.file_index)
|
||||
continue;
|
||||
sess.file_index->find_relations(hash, kind, [&](const auto& r, protocol::Range range) {
|
||||
target_ranges[r.target_symbol].push_back(range);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void Indexer::collect_unique_targets(index::SymbolHash hash,
|
||||
RelationKind kind,
|
||||
llvm::SmallVectorImpl<index::SymbolHash>& targets) {
|
||||
llvm::DenseSet<index::SymbolHash> seen;
|
||||
auto sym_it = workspace.project_index.symbols.find(hash);
|
||||
if(sym_it != workspace.project_index.symbols.end()) {
|
||||
for(auto file_id: sym_it->second.reference_files) {
|
||||
if(is_proj_path_open(file_id))
|
||||
continue;
|
||||
auto shard_it = workspace.merged_indices.find(file_id);
|
||||
if(shard_it == workspace.merged_indices.end())
|
||||
continue;
|
||||
/// No position conversion needed -- just collect target symbol hashes.
|
||||
shard_it->second.index.lookup(hash, kind, [&](const index::Relation& r) {
|
||||
if(seen.insert(r.target_symbol).second) {
|
||||
targets.push_back(r.target_symbol);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
for(auto& [_, sess]: sessions) {
|
||||
if(!sess.file_index)
|
||||
continue;
|
||||
auto rel_it = sess.file_index->file_index.relations.find(hash);
|
||||
if(rel_it == sess.file_index->file_index.relations.end())
|
||||
continue;
|
||||
for(auto& r: rel_it->second) {
|
||||
if(r.kind & kind) {
|
||||
if(seen.insert(r.target_symbol).second) {
|
||||
targets.push_back(r.target_symbol);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve a symbol hash into a SymbolInfo with definition location.
|
||||
/// Returns nullopt if the symbol or its definition cannot be found.
|
||||
std::optional<SymbolInfo> Indexer::resolve_symbol(index::SymbolHash hash) {
|
||||
std::string name;
|
||||
SymbolKind kind;
|
||||
if(!find_symbol_info(hash, name, kind))
|
||||
return std::nullopt;
|
||||
auto def_loc = find_definition_location(hash);
|
||||
if(!def_loc)
|
||||
return std::nullopt;
|
||||
return SymbolInfo{hash, std::move(name), kind, def_loc->uri, def_loc->range};
|
||||
}
|
||||
|
||||
std::vector<protocol::CallHierarchyIncomingCall>
|
||||
Indexer::find_incoming_calls(index::SymbolHash hash) {
|
||||
llvm::DenseMap<index::SymbolHash, std::vector<protocol::Range>> caller_ranges;
|
||||
collect_grouped_relations(hash, RelationKind::Caller, caller_ranges);
|
||||
|
||||
std::vector<protocol::CallHierarchyIncomingCall> results;
|
||||
for(auto& [caller_hash, ranges]: caller_ranges) {
|
||||
auto info = resolve_symbol(caller_hash);
|
||||
if(!info)
|
||||
continue;
|
||||
results.push_back({build_call_hierarchy_item(*info), std::move(ranges)});
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
std::vector<protocol::CallHierarchyOutgoingCall>
|
||||
Indexer::find_outgoing_calls(index::SymbolHash hash) {
|
||||
llvm::DenseMap<index::SymbolHash, std::vector<protocol::Range>> callee_ranges;
|
||||
collect_grouped_relations(hash, RelationKind::Callee, callee_ranges);
|
||||
|
||||
std::vector<protocol::CallHierarchyOutgoingCall> results;
|
||||
for(auto& [callee_hash, ranges]: callee_ranges) {
|
||||
auto info = resolve_symbol(callee_hash);
|
||||
if(!info)
|
||||
continue;
|
||||
results.push_back({build_call_hierarchy_item(*info), std::move(ranges)});
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
std::vector<protocol::TypeHierarchyItem> Indexer::find_supertypes(index::SymbolHash hash) {
|
||||
llvm::SmallVector<index::SymbolHash> base_hashes;
|
||||
collect_unique_targets(hash, RelationKind::Base, base_hashes);
|
||||
|
||||
std::vector<protocol::TypeHierarchyItem> results;
|
||||
for(auto target_hash: base_hashes) {
|
||||
auto info = resolve_symbol(target_hash);
|
||||
if(!info)
|
||||
continue;
|
||||
results.push_back(build_type_hierarchy_item(*info));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
std::vector<protocol::TypeHierarchyItem> Indexer::find_subtypes(index::SymbolHash hash) {
|
||||
llvm::SmallVector<index::SymbolHash> derived_hashes;
|
||||
collect_unique_targets(hash, RelationKind::Derived, derived_hashes);
|
||||
|
||||
std::vector<protocol::TypeHierarchyItem> results;
|
||||
for(auto target_hash: derived_hashes) {
|
||||
auto info = resolve_symbol(target_hash);
|
||||
if(!info)
|
||||
continue;
|
||||
results.push_back(build_type_hierarchy_item(*info));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
std::vector<protocol::SymbolInformation> Indexer::search_symbols(llvm::StringRef query,
|
||||
std::size_t max_results) {
|
||||
std::string query_lower = query.lower();
|
||||
|
||||
auto is_indexable_kind = [](SymbolKind sk) {
|
||||
return sk == SymbolKind::Namespace || sk == SymbolKind::Class || sk == SymbolKind::Struct ||
|
||||
sk == SymbolKind::Union || sk == SymbolKind::Enum || sk == SymbolKind::Type ||
|
||||
sk == SymbolKind::Field || sk == SymbolKind::EnumMember ||
|
||||
sk == SymbolKind::Function || sk == SymbolKind::Method ||
|
||||
sk == SymbolKind::Variable || sk == SymbolKind::Parameter ||
|
||||
sk == SymbolKind::Macro || sk == SymbolKind::Concept || sk == SymbolKind::Module ||
|
||||
sk == SymbolKind::Operator || sk == SymbolKind::MacroParameter ||
|
||||
sk == SymbolKind::Label || sk == SymbolKind::Attribute;
|
||||
};
|
||||
|
||||
auto matches_query = [&](llvm::StringRef name) {
|
||||
if(query_lower.empty())
|
||||
return true;
|
||||
return llvm::StringRef(name).lower().find(query_lower) != std::string::npos;
|
||||
};
|
||||
|
||||
std::vector<protocol::SymbolInformation> results;
|
||||
llvm::DenseSet<index::SymbolHash> seen;
|
||||
|
||||
for(auto& [hash, symbol]: workspace.project_index.symbols) {
|
||||
if(results.size() >= max_results)
|
||||
break;
|
||||
if(!is_indexable_kind(symbol.kind) || symbol.name.empty())
|
||||
continue;
|
||||
if(!matches_query(symbol.name))
|
||||
continue;
|
||||
auto def_loc = find_definition_location(hash);
|
||||
if(!def_loc)
|
||||
continue;
|
||||
|
||||
protocol::SymbolInformation info;
|
||||
info.name = symbol.name;
|
||||
info.kind = to_lsp_symbol_kind(symbol.kind);
|
||||
info.location = std::move(*def_loc);
|
||||
results.push_back(std::move(info));
|
||||
seen.insert(hash);
|
||||
}
|
||||
|
||||
for(auto& [_, sess]: sessions) {
|
||||
if(results.size() >= max_results)
|
||||
break;
|
||||
if(!sess.file_index)
|
||||
continue;
|
||||
for(auto& [hash, symbol]: sess.file_index->symbols) {
|
||||
if(results.size() >= max_results)
|
||||
break;
|
||||
if(seen.contains(hash))
|
||||
continue;
|
||||
if(!is_indexable_kind(symbol.kind) || symbol.name.empty())
|
||||
continue;
|
||||
if(!matches_query(symbol.name))
|
||||
continue;
|
||||
auto def_loc = find_definition_location(hash);
|
||||
if(!def_loc)
|
||||
continue;
|
||||
|
||||
protocol::SymbolInformation info;
|
||||
info.name = symbol.name;
|
||||
info.kind = to_lsp_symbol_kind(symbol.kind);
|
||||
info.location = std::move(*def_loc);
|
||||
results.push_back(std::move(info));
|
||||
seen.insert(hash);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
protocol::SymbolKind Indexer::to_lsp_symbol_kind(SymbolKind kind) {
|
||||
switch(kind) {
|
||||
case SymbolKind::Namespace: return protocol::SymbolKind::Namespace;
|
||||
case SymbolKind::Class: return protocol::SymbolKind::Class;
|
||||
case SymbolKind::Struct: return protocol::SymbolKind::Struct;
|
||||
case SymbolKind::Union: return protocol::SymbolKind::Class;
|
||||
case SymbolKind::Enum: return protocol::SymbolKind::Enum;
|
||||
case SymbolKind::Type: return protocol::SymbolKind::TypeParameter;
|
||||
case SymbolKind::Field: return protocol::SymbolKind::Field;
|
||||
case SymbolKind::EnumMember: return protocol::SymbolKind::EnumMember;
|
||||
case SymbolKind::Function: return protocol::SymbolKind::Function;
|
||||
case SymbolKind::Method: return protocol::SymbolKind::Method;
|
||||
case SymbolKind::Variable: return protocol::SymbolKind::Variable;
|
||||
case SymbolKind::Parameter: return protocol::SymbolKind::Variable;
|
||||
case SymbolKind::Macro: return protocol::SymbolKind::Function;
|
||||
case SymbolKind::Concept: return protocol::SymbolKind::Interface;
|
||||
case SymbolKind::Module: return protocol::SymbolKind::Module;
|
||||
case SymbolKind::Operator: return protocol::SymbolKind::Operator;
|
||||
default: return protocol::SymbolKind::Variable;
|
||||
}
|
||||
}
|
||||
|
||||
protocol::CallHierarchyItem Indexer::build_call_hierarchy_item(const SymbolInfo& info) {
|
||||
protocol::CallHierarchyItem item;
|
||||
item.name = info.name;
|
||||
item.kind = to_lsp_symbol_kind(info.kind);
|
||||
item.uri = info.uri;
|
||||
item.range = info.range;
|
||||
item.selection_range = info.range;
|
||||
item.data = protocol::LSPAny(static_cast<std::int64_t>(info.hash));
|
||||
return item;
|
||||
}
|
||||
|
||||
protocol::TypeHierarchyItem Indexer::build_type_hierarchy_item(const SymbolInfo& info) {
|
||||
protocol::TypeHierarchyItem item;
|
||||
item.name = info.name;
|
||||
item.kind = to_lsp_symbol_kind(info.kind);
|
||||
item.uri = info.uri;
|
||||
item.range = info.range;
|
||||
item.selection_range = info.range;
|
||||
item.data = protocol::LSPAny(static_cast<std::int64_t>(info.hash));
|
||||
return item;
|
||||
}
|
||||
|
||||
void Indexer::enqueue(std::uint32_t server_path_id) {
|
||||
index_queue.push_back(server_path_id);
|
||||
}
|
||||
|
||||
void Indexer::schedule() {
|
||||
if(!workspace.config.enable_indexing || indexing_active || indexing_scheduled)
|
||||
return;
|
||||
indexing_scheduled = true;
|
||||
|
||||
if(!index_idle_timer) {
|
||||
index_idle_timer = std::make_shared<kota::timer>(kota::timer::create(loop));
|
||||
}
|
||||
index_idle_timer->start(std::chrono::milliseconds(workspace.config.idle_timeout_ms));
|
||||
loop.schedule(run_background_indexing());
|
||||
}
|
||||
|
||||
kota::task<> Indexer::run_background_indexing() {
|
||||
if(index_idle_timer) {
|
||||
co_await index_idle_timer->wait();
|
||||
}
|
||||
indexing_scheduled = false;
|
||||
|
||||
if(index_queue_pos >= index_queue.size()) {
|
||||
LOG_DEBUG("Background indexing: queue exhausted");
|
||||
co_return;
|
||||
}
|
||||
|
||||
indexing_active = true;
|
||||
std::size_t processed = 0;
|
||||
|
||||
while(index_queue_pos < index_queue.size()) {
|
||||
auto server_path_id = index_queue[index_queue_pos];
|
||||
index_queue_pos++;
|
||||
|
||||
auto file_path = std::string(workspace.path_pool.resolve(server_path_id));
|
||||
|
||||
if(sessions.contains(server_path_id))
|
||||
continue;
|
||||
|
||||
if(!need_update(file_path))
|
||||
continue;
|
||||
|
||||
worker::BuildParams params;
|
||||
params.kind = worker::BuildKind::Index;
|
||||
params.file = file_path;
|
||||
if(!compiler.fill_compile_args(file_path, params.directory, params.arguments, nullptr))
|
||||
continue;
|
||||
|
||||
workspace.fill_pcm_deps(params.pcms);
|
||||
|
||||
LOG_INFO("Background indexing: {}", file_path);
|
||||
|
||||
auto result = co_await pool.send_stateless(params);
|
||||
if(result.has_value() && result.value().success && !result.value().tu_index_data.empty()) {
|
||||
LOG_INFO("Background indexing got TUIndex for {}: {} bytes",
|
||||
file_path,
|
||||
result.value().tu_index_data.size());
|
||||
merge(result.value().tu_index_data.data(), result.value().tu_index_data.size());
|
||||
++processed;
|
||||
} else if(result.has_value() && !result.value().success) {
|
||||
LOG_WARN("Background index failed for {}: {}", file_path, result.value().error);
|
||||
} else if(result.has_value() && result.value().tu_index_data.empty()) {
|
||||
LOG_WARN("Background index returned empty TUIndex for {}", file_path);
|
||||
} else {
|
||||
LOG_WARN("Background index IPC error for {}: {}", file_path, result.error().message);
|
||||
}
|
||||
}
|
||||
|
||||
indexing_active = false;
|
||||
LOG_INFO("Background indexing complete: {} files processed", processed);
|
||||
save(workspace.config.index_dir);
|
||||
}
|
||||
|
||||
} // namespace clice
|
||||
188
src/server/indexer.h
Normal file
188
src/server/indexer.h
Normal file
@@ -0,0 +1,188 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "semantic/relation_kind.h"
|
||||
#include "semantic/symbol_kind.h"
|
||||
#include "server/workspace.h"
|
||||
|
||||
#include "kota/async/async.h"
|
||||
#include "kota/ipc/lsp/position.h"
|
||||
#include "kota/ipc/lsp/protocol.h"
|
||||
#include "llvm/ADT/DenseMap.h"
|
||||
#include "llvm/ADT/SmallVector.h"
|
||||
#include "llvm/ADT/StringRef.h"
|
||||
|
||||
namespace clice {
|
||||
|
||||
namespace protocol = kota::ipc::protocol;
|
||||
namespace lsp = kota::ipc::lsp;
|
||||
|
||||
struct Session;
|
||||
class Compiler;
|
||||
class WorkerPool;
|
||||
|
||||
/// Information about a symbol at a given position.
|
||||
struct SymbolInfo {
|
||||
index::SymbolHash hash = 0;
|
||||
std::string name;
|
||||
SymbolKind kind;
|
||||
std::string uri;
|
||||
protocol::Range range;
|
||||
};
|
||||
|
||||
/// Index query layer and background indexing scheduler.
|
||||
///
|
||||
/// Indexer holds no index data of its own. All persistent data lives in
|
||||
/// Workspace (disk-derived ProjectIndex + MergedIndex shards) and per-file
|
||||
/// data lives in Session (OpenFileIndex from unsaved buffers).
|
||||
///
|
||||
/// Responsibilities:
|
||||
/// - Cross-file navigation queries (definition, references, hierarchy)
|
||||
/// - Symbol search (workspace/symbol)
|
||||
/// - Background indexing scheduling (enqueue → idle timer → worker dispatch)
|
||||
/// - Merging TUIndex results into Workspace's ProjectIndex
|
||||
///
|
||||
/// NOT responsible for:
|
||||
/// - Compilation — handled by Compiler
|
||||
/// - Document lifecycle — handled by MasterServer
|
||||
class Indexer {
|
||||
public:
|
||||
Indexer(kota::event_loop& loop,
|
||||
Workspace& workspace,
|
||||
llvm::DenseMap<std::uint32_t, Session>& sessions,
|
||||
WorkerPool& pool,
|
||||
Compiler& compiler,
|
||||
std::function<bool(std::uint32_t)> is_file_open = {}) :
|
||||
loop(loop), workspace(workspace), sessions(sessions), pool(pool), compiler(compiler),
|
||||
is_file_open(std::move(is_file_open)) {}
|
||||
|
||||
/// Add a file to the background indexing queue.
|
||||
void enqueue(std::uint32_t server_path_id);
|
||||
|
||||
/// Schedule background indexing (respects idle timeout and dedup).
|
||||
void schedule();
|
||||
|
||||
/// Merge a TUIndex result into Workspace's ProjectIndex and MergedIndex shards.
|
||||
void merge(const void* tu_index_data, std::size_t size);
|
||||
|
||||
/// Save Workspace's ProjectIndex and MergedIndex shards to disk.
|
||||
void save(llvm::StringRef index_dir);
|
||||
|
||||
/// Load Workspace's ProjectIndex and MergedIndex shards from disk.
|
||||
void load(llvm::StringRef index_dir);
|
||||
|
||||
/// Check whether a file needs re-indexing (stale or missing shard).
|
||||
bool need_update(llvm::StringRef file_path);
|
||||
|
||||
/// Query relations (Definition, Reference, etc.) for a symbol at cursor.
|
||||
/// @param session Active Session for this file, or nullptr to use MergedIndex only.
|
||||
std::vector<protocol::Location> query_relations(llvm::StringRef path,
|
||||
const protocol::Position& position,
|
||||
RelationKind kind,
|
||||
Session* session);
|
||||
|
||||
/// Look up symbol info (hash, name, kind, range) at a cursor position.
|
||||
/// @param session Active Session for this file, or nullptr.
|
||||
std::optional<SymbolInfo> lookup_symbol(const std::string& uri,
|
||||
llvm::StringRef path,
|
||||
const protocol::Position& position,
|
||||
Session* session);
|
||||
|
||||
/// Find the definition location of a symbol by hash.
|
||||
std::optional<protocol::Location> find_definition_location(index::SymbolHash hash);
|
||||
|
||||
/// Find a symbol's name and kind by hash.
|
||||
bool find_symbol_info(index::SymbolHash hash, std::string& name, SymbolKind& kind) const;
|
||||
|
||||
/// Resolve a hierarchy item (from stored data or by position lookup).
|
||||
/// @param session Active Session for this file, or nullptr.
|
||||
std::optional<SymbolInfo> resolve_hierarchy_item(const std::string& uri,
|
||||
llvm::StringRef path,
|
||||
const protocol::Range& range,
|
||||
const std::optional<protocol::LSPAny>& data,
|
||||
Session* session);
|
||||
|
||||
/// Find incoming calls to a function.
|
||||
std::vector<protocol::CallHierarchyIncomingCall> find_incoming_calls(index::SymbolHash hash);
|
||||
|
||||
/// Find outgoing calls from a function.
|
||||
std::vector<protocol::CallHierarchyOutgoingCall> find_outgoing_calls(index::SymbolHash hash);
|
||||
|
||||
/// Find supertypes (base classes) of a type.
|
||||
std::vector<protocol::TypeHierarchyItem> find_supertypes(index::SymbolHash hash);
|
||||
|
||||
/// Find subtypes (derived classes) of a type.
|
||||
std::vector<protocol::TypeHierarchyItem> find_subtypes(index::SymbolHash hash);
|
||||
|
||||
/// Search symbols by name substring.
|
||||
std::vector<protocol::SymbolInformation> search_symbols(llvm::StringRef query,
|
||||
std::size_t max_results = 100);
|
||||
|
||||
/// Convert internal SymbolKind to LSP SymbolKind.
|
||||
static protocol::SymbolKind to_lsp_symbol_kind(SymbolKind kind);
|
||||
|
||||
/// Build hierarchy items from SymbolInfo.
|
||||
static protocol::CallHierarchyItem build_call_hierarchy_item(const SymbolInfo& info);
|
||||
static protocol::TypeHierarchyItem build_type_hierarchy_item(const SymbolInfo& info);
|
||||
|
||||
private:
|
||||
/// Result of resolving a symbol at a cursor position.
|
||||
struct CursorHit {
|
||||
index::SymbolHash hash = 0;
|
||||
protocol::Range range{};
|
||||
};
|
||||
|
||||
/// Resolve the symbol at (position), checking Session's file_index first
|
||||
/// then falling back to Workspace's MergedIndex.
|
||||
CursorHit resolve_cursor(llvm::StringRef path,
|
||||
const protocol::Position& position,
|
||||
Session* session);
|
||||
|
||||
/// Collect relations grouped by target symbol, across all index sources.
|
||||
void collect_grouped_relations(
|
||||
index::SymbolHash hash,
|
||||
RelationKind kind,
|
||||
llvm::DenseMap<index::SymbolHash, std::vector<protocol::Range>>& target_ranges);
|
||||
|
||||
/// Collect unique target symbol hashes for a relation kind.
|
||||
void collect_unique_targets(index::SymbolHash hash,
|
||||
RelationKind kind,
|
||||
llvm::SmallVectorImpl<index::SymbolHash>& targets);
|
||||
|
||||
/// Resolve a symbol hash into a SymbolInfo with definition location.
|
||||
std::optional<SymbolInfo> resolve_symbol(index::SymbolHash hash);
|
||||
|
||||
/// Check whether a project-level path_id has an active Session.
|
||||
bool is_proj_path_open(std::uint32_t proj_path_id) const {
|
||||
return is_file_open && is_file_open(proj_path_id);
|
||||
}
|
||||
|
||||
private:
|
||||
kota::event_loop& loop;
|
||||
Workspace& workspace;
|
||||
llvm::DenseMap<std::uint32_t, Session>& sessions;
|
||||
WorkerPool& pool;
|
||||
Compiler& compiler;
|
||||
|
||||
/// Callback that checks if a *project-level* path_id has an active
|
||||
/// Session. Set by the owner (e.g. MasterServer) to bridge the
|
||||
/// server-path-id-keyed sessions map to project-level path_ids.
|
||||
std::function<bool(std::uint32_t)> is_file_open;
|
||||
|
||||
/// Background indexing queue and scheduling state.
|
||||
std::vector<std::uint32_t> index_queue;
|
||||
std::size_t index_queue_pos = 0;
|
||||
bool indexing_active = false;
|
||||
bool indexing_scheduled = false;
|
||||
std::shared_ptr<kota::timer> index_idle_timer;
|
||||
|
||||
kota::task<> run_background_indexing();
|
||||
};
|
||||
|
||||
} // namespace clice
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,40 +3,21 @@
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "command/command.h"
|
||||
#include "eventide/async/async.h"
|
||||
#include "eventide/ipc/lsp/protocol.h"
|
||||
#include "eventide/ipc/peer.h"
|
||||
#include "eventide/serde/serde/raw_value.h"
|
||||
#include "index/merged_index.h"
|
||||
#include "index/project_index.h"
|
||||
#include "semantic/relation_kind.h"
|
||||
#include "server/compile_graph.h"
|
||||
#include "server/config.h"
|
||||
#include "server/compiler.h"
|
||||
#include "server/indexer.h"
|
||||
#include "server/session.h"
|
||||
#include "server/worker_pool.h"
|
||||
#include "support/path_pool.h"
|
||||
#include "syntax/dependency_graph.h"
|
||||
#include "server/workspace.h"
|
||||
|
||||
#include "kota/async/async.h"
|
||||
#include "kota/codec/raw_value.h"
|
||||
#include "kota/ipc/peer.h"
|
||||
#include "llvm/ADT/DenseMap.h"
|
||||
#include "llvm/ADT/SmallVector.h"
|
||||
#include "llvm/ADT/StringMap.h"
|
||||
#include "llvm/ADT/StringRef.h"
|
||||
|
||||
namespace clice {
|
||||
|
||||
namespace et = eventide;
|
||||
namespace protocol = et::ipc::protocol;
|
||||
|
||||
struct DocumentState {
|
||||
int version = 0;
|
||||
std::string text;
|
||||
std::uint64_t generation = 0;
|
||||
bool build_running = false;
|
||||
bool build_requested = false;
|
||||
bool drain_scheduled = false;
|
||||
};
|
||||
|
||||
enum class ServerLifecycle : std::uint8_t {
|
||||
Uninitialized,
|
||||
Initialized,
|
||||
@@ -45,177 +26,55 @@ enum class ServerLifecycle : std::uint8_t {
|
||||
Exited,
|
||||
};
|
||||
|
||||
/// Top-level LSP server — the single orchestration point for the language
|
||||
/// server process.
|
||||
///
|
||||
/// Responsibilities:
|
||||
/// - Owns the two-layer state model: Workspace (disk truth) and Sessions
|
||||
/// (per-open-file volatile state).
|
||||
/// - Manages Session lifecycle directly: didOpen creates, didChange mutates,
|
||||
/// didSave syncs to Workspace, didClose destroys.
|
||||
/// - Dispatches compilation and feature queries to Compiler.
|
||||
/// - Dispatches index lookups and background indexing to Indexer.
|
||||
///
|
||||
/// Design principle:
|
||||
/// Open files are never depended upon by other files. Dependencies always
|
||||
/// point to disk files. The only path from Session to Workspace is didSave.
|
||||
class MasterServer {
|
||||
public:
|
||||
MasterServer(et::event_loop& loop, et::ipc::JsonPeer& peer, std::string self_path);
|
||||
MasterServer(kota::event_loop& loop, kota::ipc::JsonPeer& peer, std::string self_path);
|
||||
~MasterServer();
|
||||
|
||||
void register_handlers();
|
||||
|
||||
private:
|
||||
et::event_loop& loop;
|
||||
et::ipc::JsonPeer& peer;
|
||||
WorkerPool pool;
|
||||
PathPool path_pool;
|
||||
ServerLifecycle lifecycle = ServerLifecycle::Uninitialized;
|
||||
kota::event_loop& loop;
|
||||
kota::ipc::JsonPeer& peer;
|
||||
|
||||
/// Persistent project-wide state (config, CDB, path pool, dependency
|
||||
/// graphs, compilation caches, symbol index).
|
||||
Workspace workspace;
|
||||
|
||||
/// Per-file editing sessions, keyed by server-level path_id.
|
||||
llvm::DenseMap<std::uint32_t, Session> sessions;
|
||||
|
||||
/// Worker process pool for offloading compilation and queries.
|
||||
WorkerPool pool;
|
||||
|
||||
/// Compilation lifecycle manager (reads/writes workspace and sessions).
|
||||
Compiler compiler;
|
||||
|
||||
/// Index query and background scheduling (reads from workspace and sessions).
|
||||
Indexer indexer;
|
||||
|
||||
ServerLifecycle lifecycle = ServerLifecycle::Uninitialized;
|
||||
std::string self_path;
|
||||
std::string workspace_root;
|
||||
CliceConfig config;
|
||||
std::string session_log_dir;
|
||||
|
||||
CompilationDatabase cdb;
|
||||
DependencyGraph dependency_graph;
|
||||
kota::task<> load_workspace();
|
||||
|
||||
// Module compilation graph (lazy dependency resolution).
|
||||
std::unique_ptr<CompileGraph> compile_graph;
|
||||
|
||||
// path_id -> built PCM output path (set after successful module build).
|
||||
llvm::DenseMap<std::uint32_t, std::string> pcm_paths;
|
||||
|
||||
// path_id -> module name (for files that provide a module interface).
|
||||
llvm::DenseMap<std::uint32_t, std::string> path_to_module;
|
||||
|
||||
// path_id -> built PCH file path.
|
||||
llvm::DenseMap<std::uint32_t, std::string> pch_paths;
|
||||
|
||||
// path_id -> preamble bound (byte offset) used when building the PCH.
|
||||
llvm::DenseMap<std::uint32_t, std::uint32_t> pch_bounds;
|
||||
|
||||
// path_id -> hash of preamble content at PCH build time (for staleness detection).
|
||||
llvm::DenseMap<std::uint32_t, std::uint64_t> pch_hashes;
|
||||
|
||||
// path_id -> in-flight PCH build event (later arrivals co_await the same build).
|
||||
llvm::DenseMap<std::uint32_t, std::shared_ptr<et::event>> pch_building;
|
||||
|
||||
// === Index state ===
|
||||
|
||||
// Global symbol table and path mapping for the project.
|
||||
index::ProjectIndex project_index;
|
||||
|
||||
// Per-file merged index shards (keyed by project-level path_id).
|
||||
llvm::DenseMap<std::uint32_t, index::MergedIndex> merged_indices;
|
||||
|
||||
// Files queued for background indexing (server-level path_ids from CDB).
|
||||
std::vector<std::uint32_t> index_queue;
|
||||
|
||||
// Index of next file to process in index_queue.
|
||||
std::size_t index_queue_pos = 0;
|
||||
|
||||
// Whether background indexing is currently in progress.
|
||||
bool indexing_active = false;
|
||||
|
||||
// Whether a background indexing coroutine has been scheduled (waiting on timer).
|
||||
bool indexing_scheduled = false;
|
||||
|
||||
// Timer for idle-triggered background indexing.
|
||||
std::shared_ptr<et::timer> index_idle_timer;
|
||||
|
||||
// Document state: path_id -> DocumentState
|
||||
llvm::DenseMap<std::uint32_t, DocumentState> documents;
|
||||
|
||||
// Per-document debounce timers (shared_ptr so drain coroutines survive didClose)
|
||||
llvm::DenseMap<std::uint32_t, std::shared_ptr<et::timer>> debounce_timers;
|
||||
|
||||
// Helper: convert URI to file path
|
||||
std::string uri_to_path(const std::string& uri);
|
||||
|
||||
// Publish diagnostics to client
|
||||
void publish_diagnostics(const std::string& uri,
|
||||
int version,
|
||||
const eventide::serde::RawValue& diagnostics_json);
|
||||
void clear_diagnostics(const std::string& uri);
|
||||
|
||||
// Schedule a build after debounce
|
||||
void schedule_build(std::uint32_t path_id, const std::string& uri);
|
||||
|
||||
// Build drain coroutine: waits for debounce, then runs compile loop
|
||||
et::task<> run_build_drain(std::uint32_t path_id, std::string uri);
|
||||
|
||||
// Ensure a file has been compiled before servicing feature requests
|
||||
et::task<bool> ensure_compiled(std::uint32_t path_id, const std::string& uri);
|
||||
|
||||
// Load CDB and build initial include graph
|
||||
et::task<> load_workspace();
|
||||
|
||||
// Helper: fill compile arguments from CDB into worker params
|
||||
bool fill_compile_args(llvm::StringRef path,
|
||||
std::string& directory,
|
||||
std::vector<std::string>& arguments);
|
||||
|
||||
// Build or reuse PCH for a source file. Returns true if PCH is available.
|
||||
et::task<bool> ensure_pch(std::uint32_t path_id,
|
||||
llvm::StringRef path,
|
||||
const std::string& text,
|
||||
const std::string& directory,
|
||||
const std::vector<std::string>& arguments);
|
||||
|
||||
// Schedule background indexing when idle.
|
||||
void schedule_indexing();
|
||||
|
||||
// Background indexing coroutine: picks files from queue and dispatches to workers.
|
||||
et::task<> run_background_indexing();
|
||||
|
||||
// Merge a TUIndex result into ProjectIndex and MergedIndex shards.
|
||||
void merge_index_result(const void* tu_index_data, std::size_t size);
|
||||
|
||||
// Persist index state to disk.
|
||||
void save_index();
|
||||
|
||||
// Load index state from disk.
|
||||
void load_index();
|
||||
|
||||
// Forwarding helpers for feature requests (RawValue passthrough)
|
||||
using RawResult = et::task<et::serde::RawValue, et::ipc::Error>;
|
||||
|
||||
/// Forward a simple stateful request (path-only worker params).
|
||||
template <typename WorkerParams>
|
||||
RawResult forward_stateful(const std::string& uri);
|
||||
|
||||
/// Forward a stateful request with position-to-offset conversion.
|
||||
template <typename WorkerParams>
|
||||
RawResult forward_stateful(const std::string& uri, const protocol::Position& position);
|
||||
|
||||
/// Forward a stateless request with document content and compile args.
|
||||
template <typename WorkerParams>
|
||||
RawResult forward_stateless(const std::string& uri, const protocol::Position& position);
|
||||
|
||||
/// Query index for symbol relations (GoToDefinition, FindReferences, etc.).
|
||||
/// Returns LSP Location array as RawValue.
|
||||
RawResult query_index_relations(const std::string& uri,
|
||||
const protocol::Position& position,
|
||||
RelationKind kind);
|
||||
|
||||
/// Information about a symbol at a given position.
|
||||
struct SymbolInfo {
|
||||
index::SymbolHash hash = 0;
|
||||
std::string name;
|
||||
SymbolKind kind;
|
||||
std::string uri;
|
||||
protocol::Range range;
|
||||
};
|
||||
|
||||
/// Look up a symbol at a position, returning its hash, name, kind, and range.
|
||||
et::task<std::optional<SymbolInfo>>
|
||||
lookup_symbol_at_position(const std::string& uri, const protocol::Position& position);
|
||||
|
||||
/// Find the definition location (uri + range) of a symbol by its hash.
|
||||
std::optional<protocol::Location> find_symbol_definition_location(index::SymbolHash hash);
|
||||
|
||||
/// Convert clice::SymbolKind to LSP protocol::SymbolKind.
|
||||
static protocol::SymbolKind to_lsp_symbol_kind(SymbolKind kind);
|
||||
|
||||
/// Build a CallHierarchyItem from a SymbolInfo.
|
||||
protocol::CallHierarchyItem build_call_hierarchy_item(const SymbolInfo& info);
|
||||
|
||||
/// Build a TypeHierarchyItem from a SymbolInfo.
|
||||
protocol::TypeHierarchyItem build_type_hierarchy_item(const SymbolInfo& info);
|
||||
|
||||
/// Resolve SymbolInfo from a hierarchy item's stored data (symbol hash).
|
||||
/// Falls back to position-based lookup if data is missing.
|
||||
et::task<std::optional<SymbolInfo>>
|
||||
resolve_hierarchy_item(const std::string& uri,
|
||||
const protocol::Range& range,
|
||||
const std::optional<protocol::LSPAny>& data);
|
||||
using RawResult = kota::task<kota::codec::RawValue, kota::ipc::Error>;
|
||||
};
|
||||
|
||||
} // namespace clice
|
||||
|
||||
@@ -7,16 +7,38 @@
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "eventide/ipc/lsp/protocol.h"
|
||||
#include "eventide/ipc/protocol.h"
|
||||
#include "eventide/serde/serde/raw_value.h"
|
||||
#include "syntax/token.h"
|
||||
|
||||
#include "kota/codec/raw_value.h"
|
||||
#include "kota/ipc/lsp/protocol.h"
|
||||
#include "kota/ipc/protocol.h"
|
||||
|
||||
namespace clice::worker {
|
||||
|
||||
namespace protocol = eventide::ipc::protocol;
|
||||
namespace protocol = kota::ipc::protocol;
|
||||
|
||||
// === StatefulWorker Requests ===
|
||||
/// Kind of AST query dispatched to a stateful worker.
|
||||
enum class QueryKind : uint8_t {
|
||||
Hover,
|
||||
GoToDefinition,
|
||||
SemanticTokens,
|
||||
InlayHints,
|
||||
FoldingRange,
|
||||
DocumentSymbol,
|
||||
DocumentLink,
|
||||
CodeAction,
|
||||
};
|
||||
|
||||
/// Unified parameters for all stateful AST queries.
|
||||
/// The worker dispatches to the appropriate feature handler based on `kind`.
|
||||
struct QueryParams {
|
||||
QueryKind kind;
|
||||
std::string path;
|
||||
uint32_t offset = 0; ///< Byte offset for position-sensitive queries (Hover, GoToDefinition).
|
||||
LocalSourceRange range; ///< Byte range for range-sensitive queries (InlayHints).
|
||||
};
|
||||
|
||||
/// Parameters for stateful compilation (builds AST, publishes diagnostics).
|
||||
struct CompileParams {
|
||||
std::string path;
|
||||
int version;
|
||||
@@ -30,115 +52,64 @@ struct CompileParams {
|
||||
struct CompileResult {
|
||||
int version;
|
||||
/// Diagnostics serialized as JSON (RawValue) to avoid bincode/serde annotation conflicts.
|
||||
eventide::serde::RawValue diagnostics;
|
||||
kota::codec::RawValue diagnostics;
|
||||
std::size_t memory_usage;
|
||||
};
|
||||
|
||||
struct HoverParams {
|
||||
std::string path;
|
||||
uint32_t offset;
|
||||
};
|
||||
|
||||
struct SemanticTokensParams {
|
||||
std::string path;
|
||||
};
|
||||
|
||||
struct InlayHintsParams {
|
||||
std::string path;
|
||||
};
|
||||
|
||||
struct FoldingRangeParams {
|
||||
std::string path;
|
||||
};
|
||||
|
||||
struct DocumentSymbolParams {
|
||||
std::string path;
|
||||
};
|
||||
|
||||
struct DocumentLinkParams {
|
||||
std::string path;
|
||||
};
|
||||
|
||||
struct CodeActionParams {
|
||||
std::string path;
|
||||
};
|
||||
|
||||
struct GoToDefinitionParams {
|
||||
std::string path;
|
||||
uint32_t offset;
|
||||
};
|
||||
|
||||
// === StatelessWorker Requests ===
|
||||
|
||||
struct CompletionParams {
|
||||
std::string path;
|
||||
int version;
|
||||
std::string text;
|
||||
std::string directory;
|
||||
std::vector<std::string> arguments;
|
||||
std::pair<std::string, uint32_t> pch;
|
||||
std::unordered_map<std::string, std::string> pcms;
|
||||
uint32_t offset;
|
||||
};
|
||||
|
||||
struct SignatureHelpParams {
|
||||
std::string path;
|
||||
int version;
|
||||
std::string text;
|
||||
std::string directory;
|
||||
std::vector<std::string> arguments;
|
||||
std::pair<std::string, uint32_t> pch;
|
||||
std::unordered_map<std::string, std::string> pcms;
|
||||
uint32_t offset;
|
||||
};
|
||||
|
||||
struct BuildPCHParams {
|
||||
std::string file;
|
||||
std::string directory;
|
||||
std::vector<std::string> arguments;
|
||||
std::string content;
|
||||
std::uint32_t preamble_bound = UINT32_MAX;
|
||||
};
|
||||
|
||||
struct BuildPCHResult {
|
||||
bool success;
|
||||
std::string error;
|
||||
std::string pch_path;
|
||||
};
|
||||
|
||||
struct BuildPCMParams {
|
||||
std::string file;
|
||||
std::string directory;
|
||||
std::vector<std::string> arguments;
|
||||
std::string module_name;
|
||||
std::unordered_map<std::string, std::string> pcms;
|
||||
};
|
||||
|
||||
struct BuildPCMResult {
|
||||
bool success;
|
||||
std::string error;
|
||||
std::string pcm_path;
|
||||
};
|
||||
|
||||
struct IndexParams {
|
||||
std::string file;
|
||||
std::string directory;
|
||||
std::vector<std::string> arguments;
|
||||
std::unordered_map<std::string, std::string> pcms;
|
||||
};
|
||||
|
||||
struct IndexResult {
|
||||
bool success;
|
||||
std::string error;
|
||||
std::vector<std::string> deps;
|
||||
/// Serialized TUIndex for the main file (interested_only=true).
|
||||
std::string tu_index_data;
|
||||
};
|
||||
|
||||
// === Notifications ===
|
||||
/// Kind of build task dispatched to a stateless worker.
|
||||
enum class BuildKind : uint8_t {
|
||||
BuildPCH,
|
||||
BuildPCM,
|
||||
Index,
|
||||
Completion,
|
||||
SignatureHelp,
|
||||
};
|
||||
|
||||
/// Unified parameters for all stateless build/compilation tasks.
|
||||
/// Fields are used selectively based on `kind`:
|
||||
/// - All: file, directory, arguments
|
||||
/// - BuildPCH: + content, preamble_bound, output_path
|
||||
/// - BuildPCM: + module_name, pcms, output_path
|
||||
/// - Index: + pcms
|
||||
/// - Completion: + text, version, offset, pch, pcms
|
||||
/// - SignatureHelp: + text, version, offset, pch, pcms
|
||||
struct BuildParams {
|
||||
BuildKind kind;
|
||||
std::string file;
|
||||
std::string directory;
|
||||
std::vector<std::string> arguments;
|
||||
|
||||
/// Source text for Completion/SignatureHelp, preamble content for BuildPCH.
|
||||
std::string text;
|
||||
int version = 0;
|
||||
uint32_t offset = 0;
|
||||
std::pair<std::string, uint32_t> pch;
|
||||
std::unordered_map<std::string, std::string> pcms;
|
||||
|
||||
std::string output_path; ///< BuildPCH, BuildPCM
|
||||
std::string module_name; ///< BuildPCM
|
||||
uint32_t preamble_bound = UINT32_MAX; ///< BuildPCH
|
||||
};
|
||||
|
||||
/// Unified result for stateless build tasks.
|
||||
/// For Completion/SignatureHelp, the result JSON is in `result_json`.
|
||||
/// For BuildPCH/BuildPCM/Index, structured fields are used.
|
||||
struct BuildResult {
|
||||
bool success = true;
|
||||
std::string error;
|
||||
std::string output_path; ///< PCH or PCM path
|
||||
std::vector<std::string> deps;
|
||||
std::string tu_index_data;
|
||||
std::string pch_links_json; ///< Pre-serialized DocumentLink[] from PCH
|
||||
kota::codec::RawValue result_json; ///< Completion/SignatureHelp result
|
||||
};
|
||||
|
||||
struct DocumentUpdateParams {
|
||||
std::string path;
|
||||
int version;
|
||||
std::string text;
|
||||
};
|
||||
|
||||
struct EvictParams {
|
||||
@@ -151,9 +122,44 @@ struct EvictedParams {
|
||||
|
||||
} // namespace clice::worker
|
||||
|
||||
namespace eventide::ipc::protocol {
|
||||
namespace clice::ext {
|
||||
|
||||
// === Stateful Requests ===
|
||||
struct ContextItem {
|
||||
std::string label;
|
||||
std::string description;
|
||||
std::string uri;
|
||||
};
|
||||
|
||||
struct QueryContextParams {
|
||||
std::string uri;
|
||||
std::optional<int> offset;
|
||||
};
|
||||
|
||||
struct QueryContextResult {
|
||||
std::vector<ContextItem> contexts;
|
||||
int total;
|
||||
};
|
||||
|
||||
struct CurrentContextParams {
|
||||
std::string uri;
|
||||
};
|
||||
|
||||
struct CurrentContextResult {
|
||||
std::optional<ContextItem> context;
|
||||
};
|
||||
|
||||
struct SwitchContextParams {
|
||||
std::string uri;
|
||||
std::string context_uri;
|
||||
};
|
||||
|
||||
struct SwitchContextResult {
|
||||
bool success;
|
||||
};
|
||||
|
||||
} // namespace clice::ext
|
||||
|
||||
namespace kota::ipc::protocol {
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::worker::CompileParams> {
|
||||
@@ -162,87 +168,17 @@ struct RequestTraits<clice::worker::CompileParams> {
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::worker::HoverParams> {
|
||||
using Result = eventide::serde::RawValue;
|
||||
constexpr inline static std::string_view method = "clice/worker/hover";
|
||||
struct RequestTraits<clice::worker::QueryParams> {
|
||||
using Result = kota::codec::RawValue;
|
||||
constexpr inline static std::string_view method = "clice/worker/query";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::worker::SemanticTokensParams> {
|
||||
using Result = eventide::serde::RawValue;
|
||||
constexpr inline static std::string_view method = "clice/worker/semanticTokens";
|
||||
struct RequestTraits<clice::worker::BuildParams> {
|
||||
using Result = clice::worker::BuildResult;
|
||||
constexpr inline static std::string_view method = "clice/worker/build";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::worker::InlayHintsParams> {
|
||||
using Result = eventide::serde::RawValue;
|
||||
constexpr inline static std::string_view method = "clice/worker/inlayHints";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::worker::FoldingRangeParams> {
|
||||
using Result = eventide::serde::RawValue;
|
||||
constexpr inline static std::string_view method = "clice/worker/foldingRange";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::worker::DocumentSymbolParams> {
|
||||
using Result = eventide::serde::RawValue;
|
||||
constexpr inline static std::string_view method = "clice/worker/documentSymbol";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::worker::DocumentLinkParams> {
|
||||
using Result = eventide::serde::RawValue;
|
||||
constexpr inline static std::string_view method = "clice/worker/documentLink";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::worker::CodeActionParams> {
|
||||
using Result = eventide::serde::RawValue;
|
||||
constexpr inline static std::string_view method = "clice/worker/codeAction";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::worker::GoToDefinitionParams> {
|
||||
using Result = eventide::serde::RawValue;
|
||||
constexpr inline static std::string_view method = "clice/worker/goToDefinition";
|
||||
};
|
||||
|
||||
// === Stateless Requests ===
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::worker::CompletionParams> {
|
||||
using Result = eventide::serde::RawValue;
|
||||
constexpr inline static std::string_view method = "clice/worker/completion";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::worker::SignatureHelpParams> {
|
||||
using Result = eventide::serde::RawValue;
|
||||
constexpr inline static std::string_view method = "clice/worker/signatureHelp";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::worker::BuildPCHParams> {
|
||||
using Result = clice::worker::BuildPCHResult;
|
||||
constexpr inline static std::string_view method = "clice/worker/buildPCH";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::worker::BuildPCMParams> {
|
||||
using Result = clice::worker::BuildPCMResult;
|
||||
constexpr inline static std::string_view method = "clice/worker/buildPCM";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::worker::IndexParams> {
|
||||
using Result = clice::worker::IndexResult;
|
||||
constexpr inline static std::string_view method = "clice/worker/index";
|
||||
};
|
||||
|
||||
// === Notifications ===
|
||||
|
||||
template <>
|
||||
struct NotificationTraits<clice::worker::DocumentUpdateParams> {
|
||||
constexpr inline static std::string_view method = "clice/worker/documentUpdate";
|
||||
@@ -258,4 +194,4 @@ struct NotificationTraits<clice::worker::EvictedParams> {
|
||||
constexpr inline static std::string_view method = "clice/worker/evicted";
|
||||
};
|
||||
|
||||
} // namespace eventide::ipc::protocol
|
||||
} // namespace kota::ipc::protocol
|
||||
|
||||
82
src/server/session.h
Normal file
82
src/server/session.h
Normal file
@@ -0,0 +1,82 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#include "server/workspace.h"
|
||||
|
||||
#include "kota/async/async.h"
|
||||
#include "llvm/ADT/SmallVector.h"
|
||||
|
||||
namespace clice {
|
||||
|
||||
/// An editing session for a single file opened in the editor.
|
||||
///
|
||||
/// Design principle: open files are never depended upon by other files.
|
||||
/// Dependencies always point to disk files. The only path from Session
|
||||
/// to Workspace is didSave, which tells Workspace to rescan the disk file.
|
||||
///
|
||||
/// Created on didOpen, destroyed on didClose. All fields are local to this
|
||||
/// file's translation unit and NEVER leak to Workspace or other Sessions.
|
||||
/// Sessions may READ from Workspace (e.g. to obtain PCH/PCM paths, module
|
||||
/// mappings, include graph) but all compilation results stay here.
|
||||
struct Session {
|
||||
/// Path ID of this file in PathPool. Set on creation, never changes.
|
||||
std::uint32_t path_id = 0;
|
||||
|
||||
/// LSP document version, incremented by the client on each edit.
|
||||
int version = 0;
|
||||
|
||||
/// Current buffer content (may differ from disk until saved).
|
||||
std::string text;
|
||||
|
||||
/// Monotonic generation counter, incremented on every didChange.
|
||||
/// Used to detect stale compilation results (ABA prevention).
|
||||
std::uint64_t generation = 0;
|
||||
|
||||
/// Whether the AST needs to be rebuilt before serving queries.
|
||||
bool ast_dirty = true;
|
||||
|
||||
/// Non-null while a compilation is in flight for this file.
|
||||
/// Other queries wait on the event; the compilation task itself
|
||||
/// runs independently and cannot be cancelled by LSP $/cancelRequest.
|
||||
struct PendingCompile {
|
||||
kota::event done;
|
||||
bool succeeded = false;
|
||||
};
|
||||
|
||||
std::shared_ptr<PendingCompile> compiling;
|
||||
|
||||
/// Reference to the PCH entry in Workspace.pch_cache, if any.
|
||||
/// The PCH itself is owned by Workspace (shared, content-addressed);
|
||||
/// Session only stores enough to locate and validate it.
|
||||
struct PCHRef {
|
||||
std::uint32_t path_id = 0; ///< Key into Workspace.pch_cache.
|
||||
std::uint64_t hash = 0; ///< Preamble hash at build time.
|
||||
std::uint32_t bound = 0; ///< Preamble byte boundary.
|
||||
};
|
||||
|
||||
std::optional<PCHRef> pch_ref;
|
||||
|
||||
/// Dependency snapshot from the last successful AST compilation.
|
||||
/// Used for two-layer staleness detection (mtime + content hash).
|
||||
std::optional<DepsSnapshot> ast_deps;
|
||||
|
||||
/// Compilation context for header files that lack their own CDB entry.
|
||||
/// Stores the host source file and synthesized preamble for this header.
|
||||
std::optional<HeaderFileContext> header_context;
|
||||
|
||||
/// User-selected compilation context override (via clice/switchContext).
|
||||
/// When set, overrides automatic header context resolution.
|
||||
std::optional<std::uint32_t> active_context;
|
||||
|
||||
/// Symbol index built from the latest compilation of this file's buffer.
|
||||
/// Used for queries (hover, goto, references) on this file.
|
||||
/// NOT merged into Workspace.project_index — that only gets disk-derived
|
||||
/// data from background indexing.
|
||||
std::optional<OpenFileIndex> file_index;
|
||||
};
|
||||
|
||||
} // namespace clice
|
||||
@@ -1,7 +1,6 @@
|
||||
#include "server/stateful_worker.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <list>
|
||||
#include <memory>
|
||||
@@ -9,23 +8,23 @@
|
||||
#include <vector>
|
||||
|
||||
#include "compile/compilation.h"
|
||||
#include "eventide/async/async.h"
|
||||
#include "eventide/ipc/json_codec.h"
|
||||
#include "eventide/ipc/peer.h"
|
||||
#include "eventide/ipc/transport.h"
|
||||
#include "eventide/serde/json/serializer.h"
|
||||
#include "eventide/serde/serde/raw_value.h"
|
||||
#include "feature/feature.h"
|
||||
#include "index/tu_index.h"
|
||||
#include "server/protocol.h"
|
||||
#include "server/worker_common.h"
|
||||
#include "support/logging.h"
|
||||
|
||||
#include "kota/async/async.h"
|
||||
#include "kota/ipc/codec/bincode.h"
|
||||
#include "kota/ipc/peer.h"
|
||||
#include "kota/ipc/transport.h"
|
||||
#include "llvm/ADT/StringMap.h"
|
||||
#include "llvm/Support/raw_ostream.h"
|
||||
|
||||
namespace clice {
|
||||
|
||||
namespace et = eventide;
|
||||
using et::ipc::RequestResult;
|
||||
using RequestContext = et::ipc::BincodePeer::RequestContext;
|
||||
using kota::ipc::RequestResult;
|
||||
using RequestContext = kota::ipc::BincodePeer::RequestContext;
|
||||
|
||||
struct DocumentEntry {
|
||||
int version = 0;
|
||||
@@ -36,7 +35,7 @@ struct DocumentEntry {
|
||||
|
||||
// Signaled when the first compilation completes (has_ast becomes true).
|
||||
// Feature handlers co_await this before accessing the AST.
|
||||
et::event ast_ready{false};
|
||||
kota::event ast_ready{false};
|
||||
|
||||
// Compilation context (from CompileParams)
|
||||
std::string directory;
|
||||
@@ -45,40 +44,14 @@ struct DocumentEntry {
|
||||
llvm::StringMap<std::string> pcms;
|
||||
|
||||
// Per-document serialization mutex
|
||||
et::mutex strand;
|
||||
kota::mutex strand;
|
||||
};
|
||||
|
||||
struct ScopedTimer {
|
||||
std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();
|
||||
|
||||
long long ms() const {
|
||||
return std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - start)
|
||||
.count();
|
||||
}
|
||||
};
|
||||
|
||||
static void fill_args(CompilationParams& cp,
|
||||
const std::string& directory,
|
||||
const std::vector<std::string>& arguments) {
|
||||
cp.directory = directory;
|
||||
for(auto& arg: arguments) {
|
||||
cp.arguments.push_back(arg.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
/// Serialize any value to LSP JSON RawValue.
|
||||
template <typename T>
|
||||
static et::serde::RawValue to_raw(const T& value) {
|
||||
auto json = et::serde::json::to_json<et::ipc::lsp_config>(value);
|
||||
return et::serde::RawValue{json ? std::move(*json) : "null"};
|
||||
}
|
||||
|
||||
class StatefulWorker {
|
||||
et::ipc::BincodePeer& peer;
|
||||
kota::ipc::BincodePeer& peer;
|
||||
std::uint64_t memory_limit;
|
||||
|
||||
llvm::StringMap<std::unique_ptr<DocumentEntry>> documents;
|
||||
llvm::StringMap<std::shared_ptr<DocumentEntry>> documents;
|
||||
|
||||
// LRU tracking — owns keys so they don't dangle after request handler returns
|
||||
std::list<std::string> lru;
|
||||
@@ -106,41 +79,43 @@ class StatefulWorker {
|
||||
}
|
||||
}
|
||||
|
||||
DocumentEntry& get_or_create(llvm::StringRef path) {
|
||||
std::shared_ptr<DocumentEntry> get_or_create(llvm::StringRef path) {
|
||||
auto [it, inserted] = documents.try_emplace(path, nullptr);
|
||||
if(inserted) {
|
||||
it->second = std::make_unique<DocumentEntry>();
|
||||
it->second = std::make_shared<DocumentEntry>();
|
||||
LOG_DEBUG("Created new document entry: {}", path.str());
|
||||
}
|
||||
return *it->second;
|
||||
return it->second;
|
||||
}
|
||||
|
||||
/// Look up document, wait for AST, lock strand, run fn(doc) on thread pool, unlock.
|
||||
/// Returns "null" if document not found or AST not usable.
|
||||
template <typename F>
|
||||
et::task<et::serde::RawValue> with_ast(llvm::StringRef path, F&& fn) {
|
||||
kota::task<kota::codec::RawValue> with_ast(llvm::StringRef path, F&& fn) {
|
||||
auto it = documents.find(path);
|
||||
if(it == documents.end())
|
||||
co_return et::serde::RawValue{"null"};
|
||||
if(it == documents.end()) {
|
||||
co_return kota::codec::RawValue{"null"};
|
||||
}
|
||||
|
||||
auto& doc = *it->second;
|
||||
// Hold shared_ptr so Evict can't destroy the entry mid-request.
|
||||
auto doc = it->second;
|
||||
touch_lru(path);
|
||||
|
||||
co_await doc.ast_ready.wait();
|
||||
co_await doc.strand.lock();
|
||||
co_await doc->ast_ready.wait();
|
||||
co_await doc->strand.lock();
|
||||
|
||||
auto result = co_await et::queue([&]() -> et::serde::RawValue {
|
||||
if(!doc.has_ast || (!doc.unit.completed() && !doc.unit.fatal_error()))
|
||||
return et::serde::RawValue{"null"};
|
||||
return fn(doc);
|
||||
auto result = co_await kota::queue([&]() -> kota::codec::RawValue {
|
||||
if(!doc->has_ast || (!doc->unit.completed() && !doc->unit.fatal_error()))
|
||||
return kota::codec::RawValue{"null"};
|
||||
return fn(*doc);
|
||||
});
|
||||
|
||||
doc.strand.unlock();
|
||||
doc->strand.unlock();
|
||||
co_return result.value();
|
||||
}
|
||||
|
||||
public:
|
||||
StatefulWorker(et::ipc::BincodePeer& peer, std::uint64_t memory_limit) :
|
||||
StatefulWorker(kota::ipc::BincodePeer& peer, std::uint64_t memory_limit) :
|
||||
peer(peer), memory_limit(memory_limit) {}
|
||||
|
||||
void register_handlers();
|
||||
@@ -153,68 +128,83 @@ void StatefulWorker::register_handlers() {
|
||||
const worker::CompileParams& params) -> RequestResult<worker::CompileParams> {
|
||||
LOG_INFO("Compile request: path={}, version={}", params.path, params.version);
|
||||
|
||||
auto& doc = get_or_create(params.path);
|
||||
doc.version = params.version;
|
||||
doc.text = params.text;
|
||||
doc.directory = params.directory;
|
||||
doc.arguments = params.arguments;
|
||||
doc.pch = params.pch;
|
||||
doc.pcms.clear();
|
||||
for(auto& [name, pcm_path]: params.pcms) {
|
||||
doc.pcms.try_emplace(name, pcm_path);
|
||||
}
|
||||
|
||||
// Hold shared_ptr so Evict can't destroy the entry mid-compile.
|
||||
auto doc = get_or_create(params.path);
|
||||
touch_lru(params.path);
|
||||
|
||||
co_await doc.strand.lock();
|
||||
co_await doc->strand.lock();
|
||||
|
||||
auto compile_result = co_await et::queue([&]() -> worker::CompileResult {
|
||||
LOG_DEBUG("Compiling: path={}, {} args", params.path, doc.arguments.size());
|
||||
// Copy params to doc AFTER acquiring the strand lock, so that
|
||||
// concurrent Compile requests waiting on the strand don't
|
||||
// overwrite our fields before we use them.
|
||||
doc->version = params.version;
|
||||
doc->text = params.text;
|
||||
doc->directory = params.directory;
|
||||
doc->arguments = params.arguments;
|
||||
doc->pch = params.pch;
|
||||
doc->pcms.clear();
|
||||
for(auto& [name, pcm_path]: params.pcms) {
|
||||
doc->pcms.try_emplace(name, pcm_path);
|
||||
}
|
||||
|
||||
auto compile_result = co_await kota::queue([&]() -> worker::CompileResult {
|
||||
ScopedTimer timer;
|
||||
|
||||
CompilationParams cp;
|
||||
cp.kind = CompilationKind::Content;
|
||||
fill_args(cp, doc.directory, doc.arguments);
|
||||
if(!doc.pch.first.empty()) {
|
||||
cp.pch = doc.pch;
|
||||
fill_args(cp, doc->directory, doc->arguments);
|
||||
if(!doc->pch.first.empty()) {
|
||||
cp.pch = doc->pch;
|
||||
}
|
||||
cp.add_remapped_file(params.path, doc.text);
|
||||
for(auto& entry: doc.pcms) {
|
||||
cp.add_remapped_file(params.path, doc->text);
|
||||
for(auto& entry: doc->pcms) {
|
||||
cp.pcms.try_emplace(entry.getKey(), entry.getValue());
|
||||
}
|
||||
|
||||
doc.unit = compile(cp);
|
||||
doc.has_ast = true;
|
||||
doc.dirty.store(false, std::memory_order_release);
|
||||
doc->unit = compile(cp);
|
||||
doc->has_ast = true;
|
||||
doc->dirty.store(false, std::memory_order_release);
|
||||
|
||||
worker::CompileResult result;
|
||||
result.version = doc.version;
|
||||
if(doc.unit.completed() || doc.unit.fatal_error()) {
|
||||
auto diags = feature::diagnostics(doc.unit);
|
||||
auto json = et::serde::json::to_json<et::ipc::lsp_config>(diags);
|
||||
result.diagnostics = et::serde::RawValue{json ? std::move(*json) : "[]"};
|
||||
result.version = doc->version;
|
||||
if(doc->unit.completed() || doc->unit.fatal_error()) {
|
||||
auto diags = feature::diagnostics(doc->unit);
|
||||
auto json = kota::codec::json::to_json<kota::ipc::lsp_config>(diags);
|
||||
result.diagnostics = kota::codec::RawValue{json ? std::move(*json) : "[]"};
|
||||
LOG_INFO("Compile done: path={}, {}ms, {} diags, fatal={}",
|
||||
params.path,
|
||||
timer.ms(),
|
||||
diags.size(),
|
||||
doc.unit.fatal_error());
|
||||
doc->unit.fatal_error());
|
||||
} else {
|
||||
result.diagnostics = et::serde::RawValue{"[]"};
|
||||
result.diagnostics = kota::codec::RawValue{"[]"};
|
||||
LOG_WARN("Compile incomplete: path={}, {}ms", params.path, timer.ms());
|
||||
}
|
||||
result.memory_usage = 0; // TODO: query actual memory
|
||||
if(doc->unit.completed()) {
|
||||
result.deps = doc->unit.deps();
|
||||
|
||||
// Build index for main file only (interested_only=true).
|
||||
auto tu_index = index::TUIndex::build(doc->unit, true);
|
||||
llvm::raw_string_ostream os(result.tu_index_data);
|
||||
tu_index.serialize(os);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
doc.strand.unlock();
|
||||
doc.ast_ready.set();
|
||||
doc->strand.unlock();
|
||||
doc->ast_ready.set();
|
||||
shrink_if_over_limit();
|
||||
|
||||
co_return compile_result.value();
|
||||
});
|
||||
|
||||
// === DocumentUpdate ===
|
||||
// Only mark the document dirty — do NOT update doc.text or doc.version
|
||||
// here. The kota::queue compilation work may be reading doc.text on the
|
||||
// thread pool concurrently, so writing it from the event loop would be
|
||||
// a data race. The next Compile request will bring the correct text
|
||||
// and update it inside the strand lock.
|
||||
peer.on_notification([this](const worker::DocumentUpdateParams& params) {
|
||||
LOG_TRACE("DocumentUpdate: path={}, version={}", params.path, params.version);
|
||||
|
||||
@@ -224,10 +214,7 @@ void StatefulWorker::register_handlers() {
|
||||
return;
|
||||
}
|
||||
|
||||
auto& doc = *it->second;
|
||||
doc.version = params.version;
|
||||
doc.text = params.text;
|
||||
doc.dirty.store(true, std::memory_order_release);
|
||||
it->second->dirty.store(true, std::memory_order_release);
|
||||
});
|
||||
|
||||
// === Evict ===
|
||||
@@ -242,90 +229,70 @@ void StatefulWorker::register_handlers() {
|
||||
documents.erase(params.path);
|
||||
});
|
||||
|
||||
// === Hover ===
|
||||
// === Query (hover, definition, semantic tokens, etc.) ===
|
||||
peer.on_request(
|
||||
[this](RequestContext& ctx,
|
||||
const worker::HoverParams& params) -> RequestResult<worker::HoverParams> {
|
||||
co_return co_await with_ast(params.path, [&](DocumentEntry& doc) {
|
||||
auto result = feature::hover(doc.unit, params.offset);
|
||||
return result ? to_raw(*result) : et::serde::RawValue{"null"};
|
||||
});
|
||||
const worker::QueryParams& params) -> RequestResult<worker::QueryParams> {
|
||||
using K = worker::QueryKind;
|
||||
switch(params.kind) {
|
||||
case K::Hover:
|
||||
co_return co_await with_ast(params.path, [&](DocumentEntry& doc) {
|
||||
auto result = feature::hover(doc.unit, params.offset);
|
||||
return result ? to_raw(*result) : kota::codec::RawValue{"null"};
|
||||
});
|
||||
case K::GoToDefinition:
|
||||
// TODO: Implement go-to-definition
|
||||
co_return kota::codec::RawValue{"[]"};
|
||||
case K::SemanticTokens:
|
||||
co_return co_await with_ast(params.path, [&](DocumentEntry& doc) {
|
||||
return to_raw(feature::semantic_tokens(doc.unit));
|
||||
});
|
||||
case K::InlayHints:
|
||||
co_return co_await with_ast(params.path, [&](DocumentEntry& doc) {
|
||||
auto range = params.range;
|
||||
if(range.begin == static_cast<uint32_t>(-1))
|
||||
range = LocalSourceRange{0, static_cast<uint32_t>(doc.text.size())};
|
||||
return to_raw(feature::inlay_hints(doc.unit, range));
|
||||
});
|
||||
case K::FoldingRange:
|
||||
co_return co_await with_ast(params.path, [&](DocumentEntry& doc) {
|
||||
return to_raw(feature::folding_ranges(doc.unit));
|
||||
});
|
||||
case K::DocumentSymbol:
|
||||
co_return co_await with_ast(params.path, [&](DocumentEntry& doc) {
|
||||
return to_raw(feature::document_symbols(doc.unit));
|
||||
});
|
||||
case K::DocumentLink:
|
||||
co_return co_await with_ast(params.path, [&](DocumentEntry& doc) {
|
||||
return to_raw(feature::document_links(doc.unit));
|
||||
});
|
||||
case K::CodeAction:
|
||||
// TODO: Implement code actions
|
||||
co_return kota::codec::RawValue{"[]"};
|
||||
}
|
||||
co_return kota::codec::RawValue{"null"};
|
||||
});
|
||||
|
||||
// === SemanticTokens ===
|
||||
peer.on_request([this](RequestContext& ctx, const worker::SemanticTokensParams& params)
|
||||
-> RequestResult<worker::SemanticTokensParams> {
|
||||
co_return co_await with_ast(params.path, [&](DocumentEntry& doc) {
|
||||
return to_raw(feature::semantic_tokens(doc.unit));
|
||||
});
|
||||
});
|
||||
|
||||
// === InlayHints ===
|
||||
peer.on_request(
|
||||
[this](RequestContext& ctx,
|
||||
const worker::InlayHintsParams& params) -> RequestResult<worker::InlayHintsParams> {
|
||||
co_return co_await with_ast(params.path, [&](DocumentEntry& doc) {
|
||||
LocalSourceRange range{0, static_cast<uint32_t>(doc.text.size())};
|
||||
return to_raw(feature::inlay_hints(doc.unit, range));
|
||||
});
|
||||
});
|
||||
|
||||
// === FoldingRange ===
|
||||
peer.on_request([this](RequestContext& ctx, const worker::FoldingRangeParams& params)
|
||||
-> RequestResult<worker::FoldingRangeParams> {
|
||||
co_return co_await with_ast(params.path, [&](DocumentEntry& doc) {
|
||||
return to_raw(feature::folding_ranges(doc.unit));
|
||||
});
|
||||
});
|
||||
|
||||
// === DocumentSymbol ===
|
||||
peer.on_request([this](RequestContext& ctx, const worker::DocumentSymbolParams& params)
|
||||
-> RequestResult<worker::DocumentSymbolParams> {
|
||||
co_return co_await with_ast(params.path, [&](DocumentEntry& doc) {
|
||||
return to_raw(feature::document_symbols(doc.unit));
|
||||
});
|
||||
});
|
||||
|
||||
// === DocumentLink ===
|
||||
peer.on_request([this](RequestContext& ctx, const worker::DocumentLinkParams& params)
|
||||
-> RequestResult<worker::DocumentLinkParams> {
|
||||
co_return co_await with_ast(params.path, [&](DocumentEntry& doc) {
|
||||
return to_raw(feature::document_links(doc.unit));
|
||||
});
|
||||
});
|
||||
|
||||
// === CodeAction ===
|
||||
peer.on_request(
|
||||
[this](RequestContext& ctx,
|
||||
const worker::CodeActionParams& params) -> RequestResult<worker::CodeActionParams> {
|
||||
LOG_TRACE("CodeAction request: path={}", params.path);
|
||||
// TODO: Implement code actions
|
||||
co_return et::serde::RawValue{"[]"};
|
||||
});
|
||||
|
||||
// === GoToDefinition ===
|
||||
peer.on_request([this](RequestContext& ctx, const worker::GoToDefinitionParams& params)
|
||||
-> RequestResult<worker::GoToDefinitionParams> {
|
||||
LOG_TRACE("GoToDefinition request: path={}, offset={}", params.path, params.offset);
|
||||
// TODO: Implement go-to-definition
|
||||
co_return et::serde::RawValue{"[]"};
|
||||
});
|
||||
}
|
||||
|
||||
int run_stateful_worker_mode(std::uint64_t memory_limit) {
|
||||
logging::stderr_logger("stateful-worker", logging::options);
|
||||
int run_stateful_worker_mode(std::uint64_t memory_limit,
|
||||
const std::string& worker_name,
|
||||
const std::string& log_dir) {
|
||||
logging::stderr_logger(worker_name, logging::options);
|
||||
if(!log_dir.empty()) {
|
||||
logging::file_logger(worker_name, log_dir, logging::options);
|
||||
}
|
||||
|
||||
LOG_INFO("Starting stateful worker, memory_limit={}MB", memory_limit / (1024 * 1024));
|
||||
|
||||
et::event_loop loop;
|
||||
kota::event_loop loop;
|
||||
|
||||
auto transport_result = et::ipc::StreamTransport::open_stdio(loop);
|
||||
auto transport_result = kota::ipc::StreamTransport::open_stdio(loop);
|
||||
if(!transport_result) {
|
||||
LOG_ERROR("Failed to open stdio transport");
|
||||
return 1;
|
||||
}
|
||||
|
||||
et::ipc::BincodePeer peer(loop, std::move(*transport_result));
|
||||
kota::ipc::BincodePeer peer(loop, std::move(*transport_result));
|
||||
|
||||
StatefulWorker worker(peer, memory_limit);
|
||||
worker.register_handlers();
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace clice {
|
||||
|
||||
/// Run the stateful worker process mode.
|
||||
/// The worker holds compiled ASTs and handles feature requests
|
||||
/// (hover, semantic tokens, etc.) alongside compile requests.
|
||||
int run_stateful_worker_mode(std::uint64_t memory_limit);
|
||||
int run_stateful_worker_mode(std::uint64_t memory_limit,
|
||||
const std::string& worker_name,
|
||||
const std::string& log_dir);
|
||||
|
||||
} // namespace clice
|
||||
|
||||
@@ -1,231 +1,293 @@
|
||||
#include "server/stateless_worker.h"
|
||||
|
||||
#include <chrono>
|
||||
|
||||
#include "compile/compilation.h"
|
||||
#include "eventide/async/async.h"
|
||||
#include "eventide/ipc/json_codec.h"
|
||||
#include "eventide/ipc/peer.h"
|
||||
#include "eventide/ipc/transport.h"
|
||||
#include "eventide/serde/json/serializer.h"
|
||||
#include "eventide/serde/serde/raw_value.h"
|
||||
#include "feature/feature.h"
|
||||
#include "index/tu_index.h"
|
||||
#include "server/protocol.h"
|
||||
#include "server/worker_common.h"
|
||||
#include "support/logging.h"
|
||||
|
||||
#include "kota/async/async.h"
|
||||
#include "kota/ipc/codec/bincode.h"
|
||||
#include "kota/ipc/peer.h"
|
||||
#include "kota/ipc/transport.h"
|
||||
#include "llvm/Support/raw_ostream.h"
|
||||
|
||||
namespace clice {
|
||||
|
||||
namespace et = eventide;
|
||||
using et::ipc::RequestResult;
|
||||
using RequestContext = et::ipc::BincodePeer::RequestContext;
|
||||
using kota::ipc::RequestResult;
|
||||
using RequestContext = kota::ipc::BincodePeer::RequestContext;
|
||||
|
||||
struct ScopedTimer {
|
||||
std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();
|
||||
|
||||
long long ms() const {
|
||||
return std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - start)
|
||||
.count();
|
||||
/// Extract error messages from compilation diagnostics.
|
||||
static std::string collect_errors(CompilationUnit& unit) {
|
||||
std::string errors;
|
||||
for(auto& diag: unit.diagnostics()) {
|
||||
if(diag.id.level >= DiagnosticLevel::Error) {
|
||||
if(!errors.empty())
|
||||
errors += "; ";
|
||||
errors += diag.message;
|
||||
}
|
||||
}
|
||||
};
|
||||
return errors;
|
||||
}
|
||||
|
||||
static void fill_args(CompilationParams& cp,
|
||||
const std::string& directory,
|
||||
const std::vector<std::string>& arguments) {
|
||||
cp.directory = directory;
|
||||
for(auto& arg: arguments) {
|
||||
cp.arguments.push_back(arg.c_str());
|
||||
/// Build a TUIndex, serialize it, and return as a string.
|
||||
static std::string serialize_tu_index(CompilationUnit& unit, bool interested_only = false) {
|
||||
auto tu_index = index::TUIndex::build(unit, interested_only);
|
||||
if(!interested_only) {
|
||||
tu_index.main_file_index = index::FileIndex();
|
||||
}
|
||||
std::string serialized;
|
||||
llvm::raw_string_ostream os(serialized);
|
||||
tu_index.serialize(os);
|
||||
return serialized;
|
||||
}
|
||||
|
||||
/// Write compilation output to disk, handling tmp+rename pattern.
|
||||
/// Returns the final path on success, or empty string on failure.
|
||||
static std::string finalize_output(const std::string& output_path,
|
||||
const std::string& tmp_path,
|
||||
const std::string& file,
|
||||
const char* label) {
|
||||
if(!output_path.empty()) {
|
||||
auto ec = fs::rename(tmp_path, output_path);
|
||||
if(ec) {
|
||||
return output_path;
|
||||
} else {
|
||||
LOG_WARN("{}: rename {} -> {} failed: {}",
|
||||
label,
|
||||
tmp_path,
|
||||
output_path,
|
||||
ec.error().message());
|
||||
return tmp_path;
|
||||
}
|
||||
}
|
||||
return tmp_path;
|
||||
}
|
||||
|
||||
static worker::BuildResult handle_build_pch(const worker::BuildParams& params) {
|
||||
ScopedTimer timer;
|
||||
|
||||
CompilationParams cp;
|
||||
cp.kind = CompilationKind::Preamble;
|
||||
fill_args(cp, params.directory, params.arguments);
|
||||
cp.add_remapped_file(params.file, params.text, params.preamble_bound);
|
||||
|
||||
std::string tmp_path;
|
||||
bool has_output = !params.output_path.empty();
|
||||
if(has_output) {
|
||||
tmp_path = params.output_path + ".tmp";
|
||||
} else {
|
||||
auto tmp = fs::createTemporaryFile("clice-pch", "pch");
|
||||
if(!tmp) {
|
||||
LOG_ERROR("BuildPCH: failed to create temp file");
|
||||
return {false, "Failed to create temporary PCH file"};
|
||||
}
|
||||
tmp_path = *tmp;
|
||||
}
|
||||
cp.output_file = tmp_path;
|
||||
|
||||
PCHInfo pch_info;
|
||||
auto unit = compile(cp, pch_info);
|
||||
bool success = unit.completed();
|
||||
|
||||
std::string errors;
|
||||
if(!success)
|
||||
errors = collect_errors(unit);
|
||||
|
||||
std::string tu_index_data;
|
||||
std::string pch_links_json;
|
||||
if(success) {
|
||||
tu_index_data = serialize_tu_index(unit);
|
||||
auto links = feature::document_links(unit);
|
||||
auto raw = to_raw(links);
|
||||
pch_links_json = std::move(raw.data);
|
||||
}
|
||||
|
||||
// Destroy CompilationUnit to flush PCH to disk.
|
||||
unit = CompilationUnit(nullptr);
|
||||
|
||||
if(success) {
|
||||
auto final_path = finalize_output(params.output_path, tmp_path, params.file, "BuildPCH");
|
||||
LOG_INFO("BuildPCH done: file={}, output={}, {}ms", params.file, final_path, timer.ms());
|
||||
worker::BuildResult result;
|
||||
result.success = true;
|
||||
result.output_path = std::move(final_path);
|
||||
result.deps = pch_info.deps;
|
||||
result.tu_index_data = std::move(tu_index_data);
|
||||
result.pch_links_json = std::move(pch_links_json);
|
||||
return result;
|
||||
} else {
|
||||
LOG_WARN("BuildPCH failed: file={}, {}ms, errors=[{}]", params.file, timer.ms(), errors);
|
||||
fs::remove(tmp_path);
|
||||
return {false, errors.empty() ? "PCH compilation failed" : errors};
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
static et::serde::RawValue to_raw(const T& value) {
|
||||
auto json = et::serde::json::to_json<et::ipc::lsp_config>(value);
|
||||
return et::serde::RawValue{json ? std::move(*json) : "null"};
|
||||
static worker::BuildResult handle_build_pcm(const worker::BuildParams& params) {
|
||||
ScopedTimer timer;
|
||||
|
||||
CompilationParams cp;
|
||||
cp.kind = CompilationKind::ModuleInterface;
|
||||
fill_args(cp, params.directory, params.arguments);
|
||||
for(auto& [name, path]: params.pcms) {
|
||||
cp.pcms.try_emplace(name, path);
|
||||
}
|
||||
|
||||
std::string tmp_path;
|
||||
bool has_output = !params.output_path.empty();
|
||||
if(has_output) {
|
||||
tmp_path = params.output_path + ".tmp";
|
||||
} else {
|
||||
auto tmp = fs::createTemporaryFile("clice-pcm", "pcm");
|
||||
if(!tmp) {
|
||||
LOG_ERROR("BuildPCM: failed to create temp file");
|
||||
return {false, "Failed to create temporary PCM file"};
|
||||
}
|
||||
tmp_path = *tmp;
|
||||
}
|
||||
cp.output_file = tmp_path;
|
||||
|
||||
PCMInfo pcm_info;
|
||||
auto unit = compile(cp, pcm_info);
|
||||
bool success = unit.completed();
|
||||
|
||||
std::string errors;
|
||||
if(!success)
|
||||
errors = collect_errors(unit);
|
||||
|
||||
std::string tu_index_data;
|
||||
if(success)
|
||||
tu_index_data = serialize_tu_index(unit, true);
|
||||
|
||||
unit = CompilationUnit(nullptr);
|
||||
|
||||
if(success) {
|
||||
auto final_path = finalize_output(params.output_path, tmp_path, params.file, "BuildPCM");
|
||||
LOG_INFO("BuildPCM done: module={}, {}ms", params.module_name, timer.ms());
|
||||
worker::BuildResult result;
|
||||
result.success = true;
|
||||
result.output_path = std::move(final_path);
|
||||
result.deps = pcm_info.deps;
|
||||
result.tu_index_data = std::move(tu_index_data);
|
||||
return result;
|
||||
} else {
|
||||
LOG_WARN("BuildPCM failed: module={}, {}ms, errors=[{}]",
|
||||
params.module_name,
|
||||
timer.ms(),
|
||||
errors);
|
||||
fs::remove(tmp_path);
|
||||
return {false, errors.empty() ? "PCM compilation failed" : errors};
|
||||
}
|
||||
}
|
||||
|
||||
int run_stateless_worker_mode() {
|
||||
logging::stderr_logger("stateless-worker", logging::options);
|
||||
static worker::BuildResult handle_index(const worker::BuildParams& params) {
|
||||
ScopedTimer timer;
|
||||
|
||||
CompilationParams cp;
|
||||
cp.kind = CompilationKind::Indexing;
|
||||
fill_args(cp, params.directory, params.arguments);
|
||||
for(auto& [name, path]: params.pcms) {
|
||||
cp.pcms.try_emplace(name, path);
|
||||
}
|
||||
|
||||
auto unit = compile(cp);
|
||||
if(!unit.completed()) {
|
||||
LOG_WARN("Index failed: file={}, {}ms", params.file, timer.ms());
|
||||
return {false, "Index compilation failed"};
|
||||
}
|
||||
|
||||
auto tu_index = index::TUIndex::build(unit);
|
||||
std::string serialized;
|
||||
llvm::raw_string_ostream os(serialized);
|
||||
tu_index.serialize(os);
|
||||
|
||||
LOG_INFO("Index done: file={}, {} symbols, {}ms",
|
||||
params.file,
|
||||
tu_index.symbols.size(),
|
||||
timer.ms());
|
||||
worker::BuildResult result;
|
||||
result.success = true;
|
||||
result.tu_index_data = std::move(serialized);
|
||||
return result;
|
||||
}
|
||||
|
||||
static worker::BuildResult handle_completion(const worker::BuildParams& params) {
|
||||
ScopedTimer timer;
|
||||
|
||||
CompilationParams cp;
|
||||
cp.kind = CompilationKind::Completion;
|
||||
fill_args(cp, params.directory, params.arguments);
|
||||
if(!params.pch.first.empty()) {
|
||||
cp.pch = params.pch;
|
||||
}
|
||||
for(auto& [name, path]: params.pcms) {
|
||||
cp.pcms.try_emplace(name, path);
|
||||
}
|
||||
cp.add_remapped_file(params.file, params.text);
|
||||
cp.completion = {params.file, params.offset};
|
||||
|
||||
auto items = feature::code_complete(cp);
|
||||
LOG_DEBUG("Completion done: {} items, {}ms", items.size(), timer.ms());
|
||||
|
||||
worker::BuildResult result;
|
||||
result.result_json = to_raw(items);
|
||||
return result;
|
||||
}
|
||||
|
||||
static worker::BuildResult handle_signature_help(const worker::BuildParams& params) {
|
||||
ScopedTimer timer;
|
||||
|
||||
CompilationParams cp;
|
||||
cp.kind = CompilationKind::Completion;
|
||||
fill_args(cp, params.directory, params.arguments);
|
||||
if(!params.pch.first.empty()) {
|
||||
cp.pch = params.pch;
|
||||
}
|
||||
for(auto& [name, path]: params.pcms) {
|
||||
cp.pcms.try_emplace(name, path);
|
||||
}
|
||||
cp.add_remapped_file(params.file, params.text);
|
||||
cp.completion = {params.file, params.offset};
|
||||
|
||||
auto help = feature::signature_help(cp);
|
||||
LOG_DEBUG("SignatureHelp done: {}ms", timer.ms());
|
||||
|
||||
worker::BuildResult result;
|
||||
result.result_json = to_raw(help);
|
||||
return result;
|
||||
}
|
||||
|
||||
int run_stateless_worker_mode(const std::string& worker_name, const std::string& log_dir) {
|
||||
logging::stderr_logger(worker_name, logging::options);
|
||||
if(!log_dir.empty()) {
|
||||
logging::file_logger(worker_name, log_dir, logging::options);
|
||||
}
|
||||
|
||||
LOG_INFO("Starting stateless worker");
|
||||
|
||||
et::event_loop loop;
|
||||
kota::event_loop loop;
|
||||
|
||||
auto transport_result = et::ipc::StreamTransport::open_stdio(loop);
|
||||
auto transport_result = kota::ipc::StreamTransport::open_stdio(loop);
|
||||
if(!transport_result) {
|
||||
LOG_ERROR("Failed to open stdio transport");
|
||||
return 1;
|
||||
}
|
||||
|
||||
et::ipc::BincodePeer peer(loop, std::move(*transport_result));
|
||||
kota::ipc::BincodePeer peer(loop, std::move(*transport_result));
|
||||
|
||||
// === BuildPCH ===
|
||||
peer.on_request(
|
||||
[&](RequestContext& ctx,
|
||||
const worker::BuildPCHParams& params) -> RequestResult<worker::BuildPCHParams> {
|
||||
LOG_INFO("BuildPCH request: file={}", params.file);
|
||||
|
||||
auto result = co_await et::queue([&]() -> worker::BuildPCHResult {
|
||||
ScopedTimer timer;
|
||||
|
||||
CompilationParams cp;
|
||||
cp.kind = CompilationKind::Preamble;
|
||||
fill_args(cp, params.directory, params.arguments);
|
||||
cp.add_remapped_file(params.file, params.content, params.preamble_bound);
|
||||
|
||||
auto tmp = fs::createTemporaryFile("clice-pch", "pch");
|
||||
if(!tmp) {
|
||||
LOG_ERROR("BuildPCH: failed to create temp file");
|
||||
return {false, "Failed to create temporary PCH file", ""};
|
||||
}
|
||||
cp.output_file = *tmp;
|
||||
|
||||
PCHInfo pch_info;
|
||||
auto unit = compile(cp, pch_info);
|
||||
|
||||
if(unit.completed()) {
|
||||
LOG_INFO("BuildPCH done: file={}, output={}, {}ms",
|
||||
params.file,
|
||||
cp.output_file,
|
||||
timer.ms());
|
||||
return {true, "", std::string(cp.output_file)};
|
||||
} else {
|
||||
LOG_WARN("BuildPCH failed: file={}, {}ms", params.file, timer.ms());
|
||||
fs::remove(cp.output_file);
|
||||
return {false, "PCH compilation failed", ""};
|
||||
}
|
||||
});
|
||||
co_return result.value();
|
||||
});
|
||||
|
||||
// === BuildPCM ===
|
||||
peer.on_request(
|
||||
[&](RequestContext& ctx,
|
||||
const worker::BuildPCMParams& params) -> RequestResult<worker::BuildPCMParams> {
|
||||
LOG_INFO("BuildPCM request: file={}, module={}", params.file, params.module_name);
|
||||
|
||||
auto result = co_await et::queue([&]() -> worker::BuildPCMResult {
|
||||
ScopedTimer timer;
|
||||
|
||||
CompilationParams cp;
|
||||
cp.kind = CompilationKind::ModuleInterface;
|
||||
fill_args(cp, params.directory, params.arguments);
|
||||
for(auto& [name, path]: params.pcms) {
|
||||
cp.pcms.try_emplace(name, path);
|
||||
}
|
||||
|
||||
auto tmp = fs::createTemporaryFile("clice-pcm", "pcm");
|
||||
if(!tmp) {
|
||||
LOG_ERROR("BuildPCM: failed to create temp file");
|
||||
return {false, "Failed to create temporary PCM file"};
|
||||
}
|
||||
cp.output_file = *tmp;
|
||||
|
||||
PCMInfo pcm_info;
|
||||
auto unit = compile(cp, pcm_info);
|
||||
|
||||
if(unit.completed()) {
|
||||
LOG_INFO("BuildPCM done: module={}, {}ms", params.module_name, timer.ms());
|
||||
return {true, "", std::string(cp.output_file)};
|
||||
} else {
|
||||
LOG_WARN("BuildPCM failed: module={}, {}ms", params.module_name, timer.ms());
|
||||
return {false, "PCM compilation failed", ""};
|
||||
}
|
||||
});
|
||||
co_return result.value();
|
||||
});
|
||||
|
||||
// === Completion ===
|
||||
peer.on_request(
|
||||
[&](RequestContext& ctx,
|
||||
const worker::CompletionParams& params) -> RequestResult<worker::CompletionParams> {
|
||||
LOG_DEBUG("Completion request: path={}, offset={}", params.path, params.offset);
|
||||
|
||||
auto result = co_await et::queue([&]() -> et::serde::RawValue {
|
||||
ScopedTimer timer;
|
||||
|
||||
CompilationParams cp;
|
||||
cp.kind = CompilationKind::Completion;
|
||||
fill_args(cp, params.directory, params.arguments);
|
||||
if(!params.pch.first.empty()) {
|
||||
cp.pch = params.pch;
|
||||
}
|
||||
for(auto& [name, path]: params.pcms) {
|
||||
cp.pcms.try_emplace(name, path);
|
||||
}
|
||||
cp.add_remapped_file(params.path, params.text);
|
||||
cp.completion = {params.path, params.offset};
|
||||
|
||||
auto items = feature::code_complete(cp);
|
||||
LOG_DEBUG("Completion done: {} items, {}ms", items.size(), timer.ms());
|
||||
return to_raw(items);
|
||||
});
|
||||
co_return result.value();
|
||||
});
|
||||
|
||||
// === SignatureHelp ===
|
||||
peer.on_request([&](RequestContext& ctx, const worker::SignatureHelpParams& params)
|
||||
-> RequestResult<worker::SignatureHelpParams> {
|
||||
LOG_DEBUG("SignatureHelp request: path={}, offset={}", params.path, params.offset);
|
||||
|
||||
auto result = co_await et::queue([&]() -> et::serde::RawValue {
|
||||
ScopedTimer timer;
|
||||
|
||||
CompilationParams cp;
|
||||
cp.kind = CompilationKind::Completion;
|
||||
fill_args(cp, params.directory, params.arguments);
|
||||
if(!params.pch.first.empty()) {
|
||||
cp.pch = params.pch;
|
||||
}
|
||||
for(auto& [name, path]: params.pcms) {
|
||||
cp.pcms.try_emplace(name, path);
|
||||
}
|
||||
cp.add_remapped_file(params.path, params.text);
|
||||
cp.completion = {params.path, params.offset};
|
||||
|
||||
auto help = feature::signature_help(cp);
|
||||
LOG_DEBUG("SignatureHelp done: {}ms", timer.ms());
|
||||
return to_raw(help);
|
||||
});
|
||||
co_return result.value();
|
||||
});
|
||||
|
||||
// === Index ===
|
||||
peer.on_request([&](RequestContext& ctx,
|
||||
const worker::IndexParams& params) -> RequestResult<worker::IndexParams> {
|
||||
LOG_INFO("Index request: file={}", params.file);
|
||||
|
||||
auto result = co_await et::queue([&]() -> worker::IndexResult {
|
||||
ScopedTimer timer;
|
||||
|
||||
CompilationParams cp;
|
||||
cp.kind = CompilationKind::Indexing;
|
||||
fill_args(cp, params.directory, params.arguments);
|
||||
for(auto& [name, path]: params.pcms) {
|
||||
cp.pcms.try_emplace(name, path);
|
||||
const worker::BuildParams& params) -> RequestResult<worker::BuildParams> {
|
||||
using K = worker::BuildKind;
|
||||
auto result = co_await kota::queue([&]() -> worker::BuildResult {
|
||||
switch(params.kind) {
|
||||
case K::BuildPCH: return handle_build_pch(params);
|
||||
case K::BuildPCM: return handle_build_pcm(params);
|
||||
case K::Index: return handle_index(params);
|
||||
case K::Completion: return handle_completion(params);
|
||||
case K::SignatureHelp: return handle_signature_help(params);
|
||||
}
|
||||
|
||||
auto unit = compile(cp);
|
||||
|
||||
if(!unit.completed()) {
|
||||
LOG_WARN("Index failed: file={}, {}ms", params.file, timer.ms());
|
||||
return {false, "Index compilation failed", ""};
|
||||
}
|
||||
|
||||
auto tu_index = index::TUIndex::build(unit);
|
||||
|
||||
std::string serialized;
|
||||
llvm::raw_string_ostream os(serialized);
|
||||
tu_index.serialize(os);
|
||||
|
||||
LOG_INFO("Index done: file={}, {} symbols, {}ms",
|
||||
params.file,
|
||||
tu_index.symbols.size(),
|
||||
timer.ms());
|
||||
return {true, "", std::move(serialized)};
|
||||
return {false, "Unknown build kind"};
|
||||
});
|
||||
co_return result.value();
|
||||
});
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace clice {
|
||||
|
||||
/// Run the stateless worker process mode.
|
||||
/// The worker receives one-shot compilation tasks (BuildPCH, BuildPCM,
|
||||
/// Completion, SignatureHelp, Index) via stdin/stdout bincode IPC,
|
||||
/// executes them on a thread pool, and returns results.
|
||||
int run_stateless_worker_mode();
|
||||
int run_stateless_worker_mode(const std::string& worker_name, const std::string& log_dir);
|
||||
|
||||
} // namespace clice
|
||||
|
||||
45
src/server/worker_common.h
Normal file
45
src/server/worker_common.h
Normal file
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
/// Shared utilities for stateful and stateless worker processes.
|
||||
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "compile/compilation.h"
|
||||
|
||||
#include "kota/codec/json/serializer.h"
|
||||
#include "kota/codec/raw_value.h"
|
||||
#include "kota/ipc/codec/json.h"
|
||||
|
||||
namespace clice {
|
||||
|
||||
/// RAII timer for measuring elapsed milliseconds.
|
||||
struct ScopedTimer {
|
||||
std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();
|
||||
|
||||
long long ms() const {
|
||||
return std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - start)
|
||||
.count();
|
||||
}
|
||||
};
|
||||
|
||||
/// Fill CompilationParams directory and arguments from worker request fields.
|
||||
inline void fill_args(CompilationParams& cp,
|
||||
const std::string& directory,
|
||||
const std::vector<std::string>& arguments) {
|
||||
cp.directory = directory;
|
||||
for(auto& arg: arguments) {
|
||||
cp.arguments.push_back(arg.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
/// Serialize a value to JSON RawValue using LSP config.
|
||||
template <typename T>
|
||||
inline kota::codec::RawValue to_raw(const T& value) {
|
||||
auto json = kota::codec::json::to_json<kota::ipc::lsp_config>(value);
|
||||
return kota::codec::RawValue{json ? std::move(*json) : "null"};
|
||||
}
|
||||
|
||||
} // namespace clice
|
||||
@@ -3,21 +3,22 @@
|
||||
#include <csignal>
|
||||
#include <string>
|
||||
|
||||
#include "eventide/ipc/transport.h"
|
||||
#include "support/logging.h"
|
||||
|
||||
#include "kota/ipc/transport.h"
|
||||
|
||||
namespace clice {
|
||||
|
||||
namespace {
|
||||
|
||||
/// Coroutine that reads lines from a worker's stderr pipe and logs them
|
||||
/// with a prefix like [SL-0] or [SF-1].
|
||||
et::task<> drain_stderr(et::pipe stderr_pipe, std::string prefix) {
|
||||
/// Coroutine that drains a worker's stderr pipe.
|
||||
/// Workers write their own log files, so this only captures unexpected output
|
||||
/// (crash stacktraces, assertion failures, etc.) that bypasses spdlog.
|
||||
kota::task<> drain_stderr(kota::pipe stderr_pipe, std::string prefix) {
|
||||
std::string buffer;
|
||||
while(true) {
|
||||
auto result = co_await stderr_pipe.read();
|
||||
if(!result.has_value()) {
|
||||
// EOF or error — worker has exited
|
||||
break;
|
||||
}
|
||||
auto& chunk = result.value();
|
||||
@@ -26,7 +27,6 @@ et::task<> drain_stderr(et::pipe stderr_pipe, std::string prefix) {
|
||||
|
||||
buffer += chunk;
|
||||
|
||||
// Log complete lines
|
||||
std::size_t pos = 0;
|
||||
while(true) {
|
||||
auto nl = buffer.find('\n', pos);
|
||||
@@ -34,16 +34,15 @@ et::task<> drain_stderr(et::pipe stderr_pipe, std::string prefix) {
|
||||
break;
|
||||
auto line = buffer.substr(pos, nl - pos);
|
||||
if(!line.empty()) {
|
||||
LOG_INFO("{} {}", prefix, line);
|
||||
LOG_DEBUG("{} {}", prefix, line);
|
||||
}
|
||||
pos = nl + 1;
|
||||
}
|
||||
buffer.erase(0, pos);
|
||||
}
|
||||
|
||||
// Flush any remaining partial line
|
||||
if(!buffer.empty()) {
|
||||
LOG_INFO("{} {}", prefix, buffer);
|
||||
LOG_DEBUG("{} {}", prefix, buffer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +51,11 @@ et::task<> drain_stderr(et::pipe stderr_pipe, std::string prefix) {
|
||||
bool WorkerPool::spawn_worker(const std::string& self_path,
|
||||
bool stateful,
|
||||
std::uint64_t memory_limit) {
|
||||
et::process::options opts;
|
||||
auto& workers = stateful ? stateful_workers : stateless_workers;
|
||||
auto worker_index = workers.size();
|
||||
std::string worker_name = std::string(stateful ? "SF-" : "SL-") + std::to_string(worker_index);
|
||||
|
||||
kota::process::options opts;
|
||||
opts.file = self_path;
|
||||
if(stateful) {
|
||||
opts.args = {self_path,
|
||||
@@ -63,13 +66,22 @@ bool WorkerPool::spawn_worker(const std::string& self_path,
|
||||
} else {
|
||||
opts.args = {self_path, "--mode", "stateless-worker"};
|
||||
}
|
||||
|
||||
opts.args.push_back("--worker-name");
|
||||
opts.args.push_back(worker_name);
|
||||
|
||||
if(!log_dir_.empty()) {
|
||||
opts.args.push_back("--log-dir");
|
||||
opts.args.push_back(log_dir_);
|
||||
}
|
||||
|
||||
opts.streams = {
|
||||
et::process::stdio::pipe(true, false), // stdin: child reads
|
||||
et::process::stdio::pipe(false, true), // stdout: child writes
|
||||
et::process::stdio::pipe(false, true), // stderr: child writes
|
||||
kota::process::stdio::pipe(true, false), // stdin: child reads
|
||||
kota::process::stdio::pipe(false, true), // stdout: child writes
|
||||
kota::process::stdio::pipe(false, true), // stderr: child writes
|
||||
};
|
||||
|
||||
auto result = et::process::spawn(opts, loop);
|
||||
auto result = kota::process::spawn(opts, loop);
|
||||
if(!result) {
|
||||
LOG_ERROR("Failed to spawn {} worker: {}",
|
||||
stateful ? "stateful" : "stateless",
|
||||
@@ -81,18 +93,12 @@ bool WorkerPool::spawn_worker(const std::string& self_path,
|
||||
|
||||
// StreamTransport: input = child's stdout (parent reads), output = child's stdin (parent
|
||||
// writes)
|
||||
auto transport = std::make_unique<et::ipc::StreamTransport>(std::move(spawn.stdout_pipe),
|
||||
std::move(spawn.stdin_pipe));
|
||||
auto peer = std::make_unique<et::ipc::BincodePeer>(loop, std::move(transport));
|
||||
|
||||
auto& workers = stateful ? stateful_workers : stateless_workers;
|
||||
auto worker_index = workers.size();
|
||||
|
||||
// Build log prefix: [SF-0] for stateful, [SL-0] for stateless
|
||||
std::string prefix =
|
||||
std::string("[") + (stateful ? "SF-" : "SL-") + std::to_string(worker_index) + "]";
|
||||
auto transport = std::make_unique<kota::ipc::StreamTransport>(std::move(spawn.stdout_pipe),
|
||||
std::move(spawn.stdin_pipe));
|
||||
auto peer = std::make_unique<kota::ipc::BincodePeer>(loop, std::move(transport));
|
||||
|
||||
// Schedule stderr log collection
|
||||
std::string prefix = "[" + worker_name + "]";
|
||||
loop.schedule(drain_stderr(std::move(spawn.stderr_pipe), prefix));
|
||||
|
||||
workers.push_back(WorkerProcess{
|
||||
@@ -108,6 +114,8 @@ bool WorkerPool::spawn_worker(const std::string& self_path,
|
||||
}
|
||||
|
||||
bool WorkerPool::start(const WorkerPoolOptions& options) {
|
||||
log_dir_ = options.log_dir;
|
||||
|
||||
for(std::uint32_t i = 0; i < options.stateless_count; ++i) {
|
||||
if(!spawn_worker(options.self_path, false, 0)) {
|
||||
return false;
|
||||
@@ -135,7 +143,7 @@ bool WorkerPool::start(const WorkerPoolOptions& options) {
|
||||
return true;
|
||||
}
|
||||
|
||||
et::task<> WorkerPool::stop() {
|
||||
kota::task<> WorkerPool::stop() {
|
||||
LOG_INFO("WorkerPool stopping...");
|
||||
|
||||
// Close output pipes to signal workers to exit gracefully
|
||||
|
||||
@@ -6,47 +6,46 @@
|
||||
#include <list>
|
||||
#include <memory>
|
||||
|
||||
#include "eventide/async/async.h"
|
||||
#include "eventide/ipc/peer.h"
|
||||
#include "server/protocol.h"
|
||||
|
||||
#include "kota/async/async.h"
|
||||
#include "kota/ipc/codec/bincode.h"
|
||||
#include "kota/ipc/peer.h"
|
||||
#include "llvm/ADT/DenseMap.h"
|
||||
#include "llvm/ADT/SmallVector.h"
|
||||
|
||||
namespace clice {
|
||||
|
||||
/// Default timeout for IPC requests to worker processes.
|
||||
constexpr inline auto kWorkerRequestTimeout = std::chrono::milliseconds(30000);
|
||||
|
||||
namespace et = eventide;
|
||||
using et::ipc::RequestResult;
|
||||
using kota::ipc::RequestResult;
|
||||
|
||||
struct WorkerPoolOptions {
|
||||
std::string self_path;
|
||||
std::uint32_t stateless_count = 2;
|
||||
std::uint32_t stateful_count = 2;
|
||||
std::uint64_t worker_memory_limit = 4ULL * 1024 * 1024 * 1024; // 4GB default
|
||||
std::string log_dir;
|
||||
};
|
||||
|
||||
class WorkerPool {
|
||||
public:
|
||||
WorkerPool(et::event_loop& loop) : loop(loop) {}
|
||||
WorkerPool(kota::event_loop& loop) : loop(loop) {}
|
||||
|
||||
/// Spawn all worker processes. Returns false on failure.
|
||||
bool start(const WorkerPoolOptions& options);
|
||||
|
||||
/// Gracefully stop all workers.
|
||||
et::task<> stop();
|
||||
kota::task<> stop();
|
||||
|
||||
/// Send a request to a stateful worker with path_id affinity routing.
|
||||
template <typename Params>
|
||||
RequestResult<Params> send_stateful(std::uint32_t path_id,
|
||||
const Params& params,
|
||||
et::ipc::request_options opts = {});
|
||||
kota::ipc::request_options opts = {});
|
||||
|
||||
/// Send a request to a stateless worker with round-robin dispatch.
|
||||
template <typename Params>
|
||||
RequestResult<Params> send_stateless(const Params& params, et::ipc::request_options opts = {});
|
||||
RequestResult<Params> send_stateless(const Params& params,
|
||||
kota::ipc::request_options opts = {});
|
||||
|
||||
/// Send a notification to the stateful worker owning path_id (if any).
|
||||
template <typename Params>
|
||||
@@ -62,12 +61,12 @@ public:
|
||||
|
||||
private:
|
||||
struct WorkerProcess {
|
||||
et::process proc;
|
||||
std::unique_ptr<et::ipc::BincodePeer> peer;
|
||||
kota::process proc;
|
||||
std::unique_ptr<kota::ipc::BincodePeer> peer;
|
||||
std::size_t owned_documents = 0;
|
||||
};
|
||||
|
||||
et::event_loop& loop;
|
||||
kota::event_loop& loop;
|
||||
llvm::SmallVector<WorkerProcess> stateless_workers;
|
||||
llvm::SmallVector<WorkerProcess> stateful_workers;
|
||||
std::size_t next_stateless = 0;
|
||||
@@ -81,33 +80,30 @@ private:
|
||||
void clear_owner(std::size_t worker_index);
|
||||
std::size_t pick_least_loaded();
|
||||
|
||||
std::string log_dir_;
|
||||
bool spawn_worker(const std::string& self_path, bool stateful, std::uint64_t memory_limit);
|
||||
};
|
||||
|
||||
// --- Template implementations ---------------------------------------------------
|
||||
|
||||
template <typename Params>
|
||||
RequestResult<Params> WorkerPool::send_stateful(std::uint32_t path_id,
|
||||
const Params& params,
|
||||
et::ipc::request_options opts) {
|
||||
kota::ipc::request_options opts) {
|
||||
if(stateful_workers.empty()) {
|
||||
co_return et::outcome_error(et::ipc::Error{"No stateful workers available"});
|
||||
}
|
||||
if(!opts.timeout.has_value()) {
|
||||
opts.timeout = kWorkerRequestTimeout;
|
||||
co_return kota::outcome_error(kota::ipc::Error{"No stateful workers available"});
|
||||
}
|
||||
// No timeout: compile tasks run as detached tasks (loop.schedule) that
|
||||
// are immune to LSP $/cancelRequest. Adding a timeout here would use
|
||||
// kotatsu's with_token/when_any which has a spurious-cancellation bug
|
||||
// that kills requests within milliseconds instead of the configured period.
|
||||
auto idx = assign_worker(path_id);
|
||||
co_return co_await stateful_workers[idx].peer->send_request(params, opts);
|
||||
}
|
||||
|
||||
template <typename Params>
|
||||
RequestResult<Params> WorkerPool::send_stateless(const Params& params,
|
||||
et::ipc::request_options opts) {
|
||||
kota::ipc::request_options opts) {
|
||||
if(stateless_workers.empty()) {
|
||||
co_return et::outcome_error(et::ipc::Error{"No stateless workers available"});
|
||||
}
|
||||
if(!opts.timeout.has_value()) {
|
||||
opts.timeout = kWorkerRequestTimeout;
|
||||
co_return kota::outcome_error(kota::ipc::Error{"No stateless workers available"});
|
||||
}
|
||||
auto idx = next_stateless;
|
||||
next_stateless = (next_stateless + 1) % stateless_workers.size();
|
||||
|
||||
376
src/server/workspace.cpp
Normal file
376
src/server/workspace.cpp
Normal file
@@ -0,0 +1,376 @@
|
||||
#include "server/workspace.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
|
||||
#include "support/filesystem.h"
|
||||
#include "support/logging.h"
|
||||
#include "syntax/scan.h"
|
||||
|
||||
#include "kota/codec/json/json.h"
|
||||
#include "kota/ipc/lsp/position.h"
|
||||
#include "kota/ipc/lsp/protocol.h"
|
||||
#include "llvm/Support/Chrono.h"
|
||||
#include "llvm/Support/FileSystem.h"
|
||||
#include "llvm/Support/MemoryBuffer.h"
|
||||
#include "llvm/Support/Path.h"
|
||||
#include "llvm/Support/xxhash.h"
|
||||
|
||||
namespace clice {
|
||||
|
||||
namespace lsp = kota::ipc::lsp;
|
||||
|
||||
/// Find the tightest (innermost) occurrence containing `offset` via binary search.
|
||||
const static index::Occurrence* lookup_occurrence(const std::vector<index::Occurrence>& occs,
|
||||
std::uint32_t offset) {
|
||||
auto it = std::ranges::lower_bound(occs, offset, {}, [](const index::Occurrence& o) {
|
||||
return o.range.end;
|
||||
});
|
||||
const index::Occurrence* best = nullptr;
|
||||
while(it != occs.end() && it->range.contains(offset)) {
|
||||
if(!best || (it->range.end - it->range.begin) < (best->range.end - best->range.begin)) {
|
||||
best = &*it;
|
||||
}
|
||||
++it;
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
std::optional<std::pair<index::SymbolHash, protocol::Range>>
|
||||
OpenFileIndex::find_occurrence(std::uint32_t offset) const {
|
||||
if(!mapper)
|
||||
return std::nullopt;
|
||||
auto* occ = lookup_occurrence(file_index.occurrences, offset);
|
||||
if(!occ)
|
||||
return std::nullopt;
|
||||
auto start = mapper->to_position(occ->range.begin);
|
||||
auto end = mapper->to_position(occ->range.end);
|
||||
if(!start || !end)
|
||||
return std::nullopt;
|
||||
return std::pair{
|
||||
occ->target,
|
||||
protocol::Range{*start, *end}
|
||||
};
|
||||
}
|
||||
|
||||
std::optional<std::pair<index::SymbolHash, protocol::Range>>
|
||||
MergedIndexShard::find_occurrence(std::uint32_t offset) const {
|
||||
auto* m = mapper();
|
||||
if(!m)
|
||||
return std::nullopt;
|
||||
std::optional<std::pair<index::SymbolHash, protocol::Range>> result;
|
||||
index.lookup(offset, [&](const index::Occurrence& o) {
|
||||
auto start = m->to_position(o.range.begin);
|
||||
auto end = m->to_position(o.range.end);
|
||||
if(start && end) {
|
||||
result = {
|
||||
o.target,
|
||||
protocol::Range{*start, *end}
|
||||
};
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
llvm::SmallVector<std::uint32_t> Workspace::on_file_saved(std::uint32_t path_id) {
|
||||
llvm::SmallVector<std::uint32_t> dirtied;
|
||||
|
||||
// Re-scan the saved file for module declarations and update path_to_module.
|
||||
auto file_path = path_pool.resolve(path_id);
|
||||
if(auto buf = llvm::MemoryBuffer::getFile(file_path)) {
|
||||
auto result = scan((*buf)->getBuffer());
|
||||
if(!result.module_name.empty()) {
|
||||
path_to_module[path_id] = std::move(result.module_name);
|
||||
} else {
|
||||
path_to_module.erase(path_id);
|
||||
}
|
||||
}
|
||||
|
||||
if(compile_graph) {
|
||||
auto result = compile_graph->update(path_id);
|
||||
for(auto id: result) {
|
||||
dirtied.push_back(id);
|
||||
pcm_paths.erase(id);
|
||||
pcm_cache.erase(id);
|
||||
}
|
||||
}
|
||||
return dirtied;
|
||||
}
|
||||
|
||||
void Workspace::on_file_closed(std::uint32_t path_id) {
|
||||
if(compile_graph && compile_graph->has_unit(path_id)) {
|
||||
compile_graph->update(path_id);
|
||||
}
|
||||
pch_cache.erase(path_id);
|
||||
}
|
||||
|
||||
std::uint64_t hash_file(llvm::StringRef path) {
|
||||
auto buf = llvm::MemoryBuffer::getFile(path);
|
||||
if(!buf)
|
||||
return 0;
|
||||
return llvm::xxh3_64bits((*buf)->getBuffer());
|
||||
}
|
||||
|
||||
DepsSnapshot capture_deps_snapshot(PathPool& pool, llvm::ArrayRef<std::string> deps) {
|
||||
DepsSnapshot snap;
|
||||
// Capture timestamp BEFORE hashing to avoid TOCTOU: if a file is modified
|
||||
// during hashing, its mtime will be > build_at, triggering Layer 2 re-hash.
|
||||
snap.build_at = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
|
||||
snap.path_ids.reserve(deps.size());
|
||||
snap.hashes.reserve(deps.size());
|
||||
for(const auto& file: deps) {
|
||||
snap.path_ids.push_back(pool.intern(file));
|
||||
snap.hashes.push_back(hash_file(file));
|
||||
}
|
||||
return snap;
|
||||
}
|
||||
|
||||
bool deps_changed(const PathPool& pool, const DepsSnapshot& snap) {
|
||||
for(std::size_t i = 0; i < snap.path_ids.size(); ++i) {
|
||||
auto path = pool.resolve(snap.path_ids[i]);
|
||||
llvm::sys::fs::file_status status;
|
||||
if(auto ec = llvm::sys::fs::status(path, status)) {
|
||||
// File disappeared — definitely changed.
|
||||
if(snap.hashes[i] != 0)
|
||||
return true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Layer 1: mtime check (cheap, stat only).
|
||||
auto current_mtime = llvm::sys::toTimeT(status.getLastModificationTime());
|
||||
if(current_mtime <= snap.build_at)
|
||||
continue;
|
||||
|
||||
// Layer 2: mtime is newer — re-hash content to confirm actual change.
|
||||
auto current_hash = hash_file(path);
|
||||
if(current_hash != snap.hashes[i])
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
struct CacheDepEntry {
|
||||
std::uint32_t path; // index into CacheData::paths
|
||||
std::uint64_t hash;
|
||||
};
|
||||
|
||||
struct CachePCHEntry {
|
||||
std::string filename;
|
||||
std::uint32_t source_file; // index into CacheData::paths
|
||||
std::uint64_t hash;
|
||||
std::uint32_t bound;
|
||||
std::int64_t build_at;
|
||||
std::vector<CacheDepEntry> deps;
|
||||
};
|
||||
|
||||
struct CachePCMEntry {
|
||||
std::string filename;
|
||||
std::uint32_t source_file;
|
||||
std::string module_name;
|
||||
std::int64_t build_at;
|
||||
std::vector<CacheDepEntry> deps;
|
||||
};
|
||||
|
||||
struct CacheData {
|
||||
std::vector<std::string> paths;
|
||||
std::vector<CachePCHEntry> pch;
|
||||
std::vector<CachePCMEntry> pcm;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
void Workspace::load_cache() {
|
||||
if(config.cache_dir.empty())
|
||||
return;
|
||||
|
||||
auto cache_path = path::join(config.cache_dir, "cache", "cache.json");
|
||||
auto content = fs::read(cache_path);
|
||||
if(!content) {
|
||||
LOG_DEBUG("No cache.json found at {}", cache_path);
|
||||
return;
|
||||
}
|
||||
|
||||
CacheData data;
|
||||
auto status = kota::codec::json::from_json(*content, data);
|
||||
if(!status) {
|
||||
LOG_WARN("Failed to parse cache.json");
|
||||
return;
|
||||
}
|
||||
|
||||
auto resolve = [&](std::uint32_t idx) -> llvm::StringRef {
|
||||
return idx < data.paths.size() ? llvm::StringRef(data.paths[idx]) : "";
|
||||
};
|
||||
|
||||
auto load_deps = [&](std::int64_t build_at, const auto& dep_entries) -> DepsSnapshot {
|
||||
DepsSnapshot deps;
|
||||
deps.build_at = build_at;
|
||||
for(auto& dep: dep_entries) {
|
||||
auto dep_path = resolve(dep.path);
|
||||
if(dep_path.empty())
|
||||
continue;
|
||||
deps.path_ids.push_back(path_pool.intern(dep_path));
|
||||
deps.hashes.push_back(dep.hash);
|
||||
}
|
||||
return deps;
|
||||
};
|
||||
|
||||
for(auto& entry: data.pch) {
|
||||
auto pch_path = path::join(config.cache_dir, "cache", "pch", entry.filename);
|
||||
auto source = resolve(entry.source_file);
|
||||
if(!llvm::sys::fs::exists(pch_path) || source.empty())
|
||||
continue;
|
||||
|
||||
auto path_id = path_pool.intern(source);
|
||||
auto& st = pch_cache[path_id];
|
||||
st.path = pch_path;
|
||||
st.hash = entry.hash;
|
||||
st.bound = entry.bound;
|
||||
st.deps = load_deps(entry.build_at, entry.deps);
|
||||
|
||||
LOG_DEBUG("Loaded cached PCH: {} -> {}", source, pch_path);
|
||||
}
|
||||
|
||||
for(auto& entry: data.pcm) {
|
||||
auto pcm_path = path::join(config.cache_dir, "cache", "pcm", entry.filename);
|
||||
auto source = resolve(entry.source_file);
|
||||
if(!llvm::sys::fs::exists(pcm_path) || source.empty())
|
||||
continue;
|
||||
|
||||
auto path_id = path_pool.intern(source);
|
||||
pcm_cache[path_id] = {pcm_path, load_deps(entry.build_at, entry.deps)};
|
||||
pcm_paths[path_id] = pcm_path;
|
||||
|
||||
LOG_DEBUG("Loaded cached PCM: {} (module {}) -> {}", source, entry.module_name, pcm_path);
|
||||
}
|
||||
|
||||
LOG_INFO("Loaded cache.json: {} PCH entries, {} PCM entries",
|
||||
pch_cache.size(),
|
||||
pcm_cache.size());
|
||||
}
|
||||
|
||||
void Workspace::save_cache() {
|
||||
if(config.cache_dir.empty())
|
||||
return;
|
||||
|
||||
CacheData data;
|
||||
std::unordered_map<std::string, std::uint32_t> index_map;
|
||||
|
||||
auto intern = [&](std::uint32_t runtime_path_id) -> std::uint32_t {
|
||||
auto path = std::string(path_pool.resolve(runtime_path_id));
|
||||
auto [it, inserted] =
|
||||
index_map.try_emplace(path, static_cast<std::uint32_t>(data.paths.size()));
|
||||
if(inserted) {
|
||||
data.paths.push_back(path);
|
||||
}
|
||||
return it->second;
|
||||
};
|
||||
|
||||
for(auto& [path_id, st]: pch_cache) {
|
||||
if(st.path.empty())
|
||||
continue;
|
||||
|
||||
CachePCHEntry entry;
|
||||
entry.filename = std::string(path::filename(st.path));
|
||||
entry.source_file = intern(path_id);
|
||||
entry.hash = st.hash;
|
||||
entry.bound = st.bound;
|
||||
entry.build_at = st.deps.build_at;
|
||||
for(std::size_t i = 0; i < st.deps.path_ids.size(); ++i) {
|
||||
entry.deps.push_back({intern(st.deps.path_ids[i]), st.deps.hashes[i]});
|
||||
}
|
||||
data.pch.push_back(std::move(entry));
|
||||
}
|
||||
|
||||
for(auto& [path_id, st]: pcm_cache) {
|
||||
if(st.path.empty())
|
||||
continue;
|
||||
|
||||
CachePCMEntry entry;
|
||||
entry.filename = std::string(path::filename(st.path));
|
||||
entry.source_file = intern(path_id);
|
||||
auto mod_it = path_to_module.find(path_id);
|
||||
entry.module_name = mod_it != path_to_module.end() ? mod_it->second : "";
|
||||
entry.build_at = st.deps.build_at;
|
||||
for(std::size_t i = 0; i < st.deps.path_ids.size(); ++i) {
|
||||
entry.deps.push_back({intern(st.deps.path_ids[i]), st.deps.hashes[i]});
|
||||
}
|
||||
data.pcm.push_back(std::move(entry));
|
||||
}
|
||||
|
||||
auto json_str = kota::codec::json::to_json(data);
|
||||
if(!json_str) {
|
||||
LOG_WARN("Failed to serialize cache.json");
|
||||
return;
|
||||
}
|
||||
|
||||
auto cache_path = path::join(config.cache_dir, "cache", "cache.json");
|
||||
auto tmp_path = cache_path + ".tmp";
|
||||
auto write_result = fs::write(tmp_path, *json_str);
|
||||
if(!write_result) {
|
||||
LOG_WARN("Failed to write cache.json.tmp: {}", write_result.error().message());
|
||||
return;
|
||||
}
|
||||
auto rename_result = fs::rename(tmp_path, cache_path);
|
||||
if(!rename_result) {
|
||||
LOG_WARN("Failed to rename cache.json.tmp to cache.json: {}",
|
||||
rename_result.error().message());
|
||||
}
|
||||
}
|
||||
|
||||
void Workspace::cleanup_cache(int max_age_days) {
|
||||
if(config.cache_dir.empty())
|
||||
return;
|
||||
|
||||
auto now = std::chrono::system_clock::now();
|
||||
auto max_age = std::chrono::hours(max_age_days * 24);
|
||||
|
||||
for(auto* subdir: {"cache/pch", "cache/pcm"}) {
|
||||
auto dir = path::join(config.cache_dir, subdir);
|
||||
std::error_code ec;
|
||||
for(auto it = llvm::sys::fs::directory_iterator(dir, ec);
|
||||
!ec && it != llvm::sys::fs::directory_iterator();
|
||||
it.increment(ec)) {
|
||||
llvm::sys::fs::file_status status;
|
||||
if(auto stat_ec = llvm::sys::fs::status(it->path(), status))
|
||||
continue;
|
||||
|
||||
auto mtime = status.getLastModificationTime();
|
||||
auto age = now - mtime;
|
||||
if(age > max_age) {
|
||||
llvm::sys::fs::remove(it->path());
|
||||
LOG_DEBUG("Cleaned up stale cache file: {}", it->path());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Workspace::build_module_map() {
|
||||
for(auto& [module_name, path_ids]: dep_graph.modules()) {
|
||||
for(auto path_id: path_ids) {
|
||||
path_to_module[path_id] = module_name.str();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Workspace::fill_pcm_deps(std::unordered_map<std::string, std::string>& pcms,
|
||||
std::uint32_t exclude_path_id) const {
|
||||
for(auto& [pid, pcm_path]: pcm_paths) {
|
||||
if(pid == exclude_path_id)
|
||||
continue;
|
||||
auto mod_it = path_to_module.find(pid);
|
||||
if(mod_it != path_to_module.end()) {
|
||||
pcms[mod_it->second] = pcm_path;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Workspace::cancel_all() {
|
||||
if(compile_graph) {
|
||||
compile_graph->cancel_all();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace clice
|
||||
247
src/server/workspace.h
Normal file
247
src/server/workspace.h
Normal file
@@ -0,0 +1,247 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
|
||||
#include "command/command.h"
|
||||
#include "index/merged_index.h"
|
||||
#include "index/project_index.h"
|
||||
#include "semantic/relation_kind.h"
|
||||
#include "server/compile_graph.h"
|
||||
#include "server/config.h"
|
||||
#include "support/path_pool.h"
|
||||
#include "syntax/dependency_graph.h"
|
||||
|
||||
#include "kota/ipc/lsp/position.h"
|
||||
#include "kota/ipc/lsp/protocol.h"
|
||||
#include "llvm/ADT/ArrayRef.h"
|
||||
#include "llvm/ADT/DenseMap.h"
|
||||
#include "llvm/ADT/SmallVector.h"
|
||||
#include "llvm/ADT/StringRef.h"
|
||||
|
||||
namespace clice {
|
||||
|
||||
namespace protocol = kota::ipc::protocol;
|
||||
namespace lsp = kota::ipc::lsp;
|
||||
|
||||
/// Two-layer staleness snapshot for compilation artifacts (PCH, AST, etc.).
|
||||
///
|
||||
/// Layer 1 (fast): compare each file's current mtime against build_at.
|
||||
/// If all mtimes <= build_at, the artifact is fresh (zero I/O beyond stat).
|
||||
///
|
||||
/// Layer 2 (precise): for files whose mtime changed, re-hash their content
|
||||
/// and compare against the stored hash. If the hash matches, the file was
|
||||
/// "touched" but not actually modified — skip the rebuild.
|
||||
struct DepsSnapshot {
|
||||
llvm::SmallVector<std::uint32_t> path_ids;
|
||||
llvm::SmallVector<std::uint64_t> hashes;
|
||||
std::int64_t build_at = 0;
|
||||
};
|
||||
|
||||
/// Context for compiling a header file that lacks its own CDB entry.
|
||||
struct HeaderFileContext {
|
||||
std::uint32_t host_path_id; ///< Source file acting as host.
|
||||
std::string preamble_path; ///< Path to generated preamble file on disk.
|
||||
std::uint64_t preamble_hash; ///< Hash of preamble content for staleness.
|
||||
};
|
||||
|
||||
/// In-memory index for an open file. Kept separate from MergedIndex because
|
||||
/// open files change frequently, are based on unsaved buffer content, and only
|
||||
/// need to track the main file (headers are covered by PCH/PCM indexing).
|
||||
struct OpenFileIndex {
|
||||
index::FileIndex file_index;
|
||||
index::SymbolTable symbols;
|
||||
std::string content; ///< Buffer text at index time (for position mapping).
|
||||
|
||||
/// Cached PositionMapper built from `content`. Avoids re-scanning line
|
||||
/// offsets on every query. Initialized by Indexer::set_open_file().
|
||||
std::optional<lsp::PositionMapper> mapper;
|
||||
|
||||
/// Find the tightest occurrence containing `offset`.
|
||||
/// Returns (symbol_hash, LSP range) with positions already converted.
|
||||
std::optional<std::pair<index::SymbolHash, protocol::Range>>
|
||||
find_occurrence(std::uint32_t offset) const;
|
||||
|
||||
/// Iterate relations matching `kind`, calling back with pre-converted ranges.
|
||||
/// Callback: (const index::Relation&, protocol::Range) -> bool (true = continue).
|
||||
template <typename Fn>
|
||||
void find_relations(index::SymbolHash hash, RelationKind kind, Fn&& fn) const {
|
||||
if(!mapper)
|
||||
return;
|
||||
auto it = file_index.relations.find(hash);
|
||||
if(it == file_index.relations.end())
|
||||
return;
|
||||
for(auto& r: it->second) {
|
||||
if(r.kind & kind) {
|
||||
auto start = mapper->to_position(r.range.begin);
|
||||
auto end = mapper->to_position(r.range.end);
|
||||
if(start && end) {
|
||||
if(!fn(r, protocol::Range{*start, *end}))
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// Wraps index::MergedIndex with a lazily-cached PositionMapper.
|
||||
struct MergedIndexShard {
|
||||
index::MergedIndex index;
|
||||
mutable std::optional<lsp::PositionMapper> cached_mapper;
|
||||
|
||||
/// Get or lazily build a PositionMapper from the index's stored content.
|
||||
const lsp::PositionMapper* mapper() const {
|
||||
if(!cached_mapper) {
|
||||
auto c = index.content();
|
||||
if(!c.empty()) {
|
||||
cached_mapper.emplace(c, lsp::PositionEncoding::UTF16);
|
||||
}
|
||||
}
|
||||
return cached_mapper ? &*cached_mapper : nullptr;
|
||||
}
|
||||
|
||||
/// Invalidate the cached mapper (call after merge changes content).
|
||||
void invalidate_mapper() {
|
||||
cached_mapper.reset();
|
||||
}
|
||||
|
||||
/// Find occurrence at byte offset.
|
||||
/// Returns (symbol_hash, LSP range) with positions already converted.
|
||||
std::optional<std::pair<index::SymbolHash, protocol::Range>>
|
||||
find_occurrence(std::uint32_t offset) const;
|
||||
|
||||
/// Iterate relations matching `kind`, calling back with pre-converted ranges.
|
||||
/// Callback: (const index::Relation&, protocol::Range) -> bool (true = continue).
|
||||
template <typename Fn>
|
||||
void find_relations(index::SymbolHash hash, RelationKind kind, Fn&& fn) const {
|
||||
auto* m = mapper();
|
||||
if(!m)
|
||||
return;
|
||||
index.lookup(hash, kind, [&](const index::Relation& r) {
|
||||
auto start = m->to_position(r.range.begin);
|
||||
auto end = m->to_position(r.range.end);
|
||||
if(start && end) {
|
||||
return fn(r, protocol::Range{*start, *end});
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/// Cached PCH state. Content-addressed by preamble hash — shared across all
|
||||
/// files (open or on-disk) that have the same preamble content.
|
||||
struct PCHState {
|
||||
std::string path;
|
||||
std::uint32_t bound = 0;
|
||||
std::uint64_t hash = 0;
|
||||
DepsSnapshot deps;
|
||||
std::string document_links_json; ///< Pre-serialized DocumentLink[] from PCH build
|
||||
std::shared_ptr<kota::event> building;
|
||||
};
|
||||
|
||||
/// Cached PCM state for a single C++20 module. Shared across all files that
|
||||
/// import the same module.
|
||||
struct PCMState {
|
||||
std::string path;
|
||||
DepsSnapshot deps;
|
||||
};
|
||||
|
||||
/// All persistent, project-wide state derived from files on disk.
|
||||
///
|
||||
/// Design principle: open files are never depended upon by other files.
|
||||
/// Dependencies always point to disk files. This enforces a clean two-layer
|
||||
/// architecture:
|
||||
/// - Global layer (Workspace): tracks disk truth, shared by all files
|
||||
/// - Per-file layer (Session): tracks buffer truth, isolated per TU
|
||||
///
|
||||
/// Workspace is the single source of truth for:
|
||||
/// - dependency relationships (include graph, module DAG)
|
||||
/// - compilation artifacts shared across files (PCH/PCM caches)
|
||||
/// - symbol index (ProjectIndex + per-file MergedIndex shards)
|
||||
/// - compilation database and configuration
|
||||
///
|
||||
/// Workspace is NEVER modified by unsaved buffer content. The only mutation
|
||||
/// paths are:
|
||||
/// - Initialization (load_workspace at startup)
|
||||
/// - didSave (on_file_saved: rescan disk, cascade invalidation)
|
||||
/// - Background index (merge TUIndex results from stateless workers)
|
||||
struct Workspace {
|
||||
CliceConfig config;
|
||||
CompilationDatabase cdb;
|
||||
|
||||
PathPool path_pool;
|
||||
|
||||
/// Include relationships between files on disk (#include edges).
|
||||
/// Built once at startup from CDB scan; updated incrementally on didSave.
|
||||
DependencyGraph dep_graph;
|
||||
|
||||
/// C++20 module compilation ordering DAG.
|
||||
/// Lazily resolves module dependencies; updated on didSave via cascade.
|
||||
std::unique_ptr<CompileGraph> compile_graph;
|
||||
|
||||
/// Reverse mapping: file path_id → module name (e.g. "std", "foo.bar").
|
||||
/// Built from dep_graph at startup; updated on didSave when module
|
||||
/// declarations change.
|
||||
llvm::DenseMap<std::uint32_t, std::string> path_to_module;
|
||||
|
||||
/// PCH cache, keyed by file path_id.
|
||||
/// TODO: re-key by preamble content hash to enable cross-file sharing and
|
||||
/// add LRU eviction. Compile flags should also be part of the key.
|
||||
llvm::DenseMap<std::uint32_t, PCHState> pch_cache;
|
||||
|
||||
/// PCM cache, keyed by module source path_id.
|
||||
llvm::DenseMap<std::uint32_t, PCMState> pcm_cache;
|
||||
|
||||
/// PCM output paths, keyed by module source path_id.
|
||||
/// Maps to the .pcm file on disk used as -fmodule-file argument.
|
||||
llvm::DenseMap<std::uint32_t, std::string> pcm_paths;
|
||||
|
||||
/// Global symbol table across all indexed translation units.
|
||||
index::ProjectIndex project_index;
|
||||
|
||||
/// Per-file index shards from background indexing, keyed by project-level
|
||||
/// path_id. Contains symbol occurrences, relations, and stored content
|
||||
/// for position mapping.
|
||||
llvm::DenseMap<std::uint32_t, MergedIndexShard> merged_indices;
|
||||
|
||||
/// Called when a file is saved to disk. Cascades invalidation through
|
||||
/// compile_graph and clears affected PCM caches.
|
||||
/// Returns path_ids of all files dirtied by the cascade.
|
||||
llvm::SmallVector<std::uint32_t> on_file_saved(std::uint32_t path_id);
|
||||
|
||||
/// Called when a file is closed. Notifies compile_graph if this file
|
||||
/// is a module unit so dependents can be re-evaluated on next compile.
|
||||
void on_file_closed(std::uint32_t path_id);
|
||||
|
||||
/// Load PCH/PCM cache from cache.json on disk.
|
||||
void load_cache();
|
||||
/// Save PCH/PCM cache to cache.json on disk.
|
||||
void save_cache();
|
||||
/// Remove stale PCH/PCM files older than max_age_days.
|
||||
void cleanup_cache(int max_age_days = 7);
|
||||
/// Build path_to_module reverse mapping from dep_graph.
|
||||
void build_module_map();
|
||||
/// Fill PCM paths for all built modules, excluding exclude_path_id.
|
||||
void fill_pcm_deps(std::unordered_map<std::string, std::string>& pcms,
|
||||
std::uint32_t exclude_path_id = UINT32_MAX) const;
|
||||
/// Cancel all in-flight compilations.
|
||||
void cancel_all();
|
||||
};
|
||||
|
||||
/// Hash a file's content using xxh3_64bits. Returns 0 on read failure.
|
||||
std::uint64_t hash_file(llvm::StringRef path);
|
||||
|
||||
/// Capture a two-layer staleness snapshot after a successful compilation.
|
||||
/// Interns dependency paths into the PathPool and hashes each file's content.
|
||||
DepsSnapshot capture_deps_snapshot(PathPool& pool, llvm::ArrayRef<std::string> deps);
|
||||
|
||||
/// Two-layer staleness check.
|
||||
/// Layer 1 (fast): stat each dep file, compare mtime against build_at.
|
||||
/// Layer 2 (precise): for files with mtime > build_at, re-hash content.
|
||||
bool deps_changed(const PathPool& pool, const DepsSnapshot& snap);
|
||||
|
||||
} // namespace clice
|
||||
@@ -1,6 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include <bit>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <utility>
|
||||
|
||||
#define ROARING_EXCEPTIONS 0
|
||||
#define ROARING_TERMINATE(message) std::abort()
|
||||
@@ -10,4 +13,162 @@ namespace clice {
|
||||
|
||||
using Bitmap = roaring::Roaring;
|
||||
|
||||
/// Compact bitmap optimized for small canonical-id sets.
|
||||
///
|
||||
/// Uses a tagged pointer scheme in a single uint64_t:
|
||||
/// - Low bit 1: inline mode, bits 1-63 store the bitmap (ids 0-62)
|
||||
/// - Low bit 0: heap mode, the value is a roaring::Roaring* (aligned, so low bit is 0)
|
||||
///
|
||||
/// Default state is inline-empty (data == 1). Upgrades to heap when an id >= 63
|
||||
/// is added. The common case (< 63 canonical ids per MergedIndex) stays entirely
|
||||
/// inline with zero heap allocation and single-instruction bitwise operations.
|
||||
class ContextBitmap {
|
||||
constexpr static std::uint32_t inline_capacity = 63;
|
||||
constexpr static std::uint64_t inline_tag = 1;
|
||||
|
||||
std::uint64_t data = inline_tag;
|
||||
|
||||
bool is_inline() const {
|
||||
return data & inline_tag;
|
||||
}
|
||||
|
||||
roaring::Roaring* as_heap() const {
|
||||
return reinterpret_cast<roaring::Roaring*>(data);
|
||||
}
|
||||
|
||||
std::uint64_t bits() const {
|
||||
return data >> 1;
|
||||
}
|
||||
|
||||
void free_heap() {
|
||||
if(!is_inline())
|
||||
delete as_heap();
|
||||
}
|
||||
|
||||
void upgrade(std::uint32_t new_id) {
|
||||
auto* r = new roaring::Roaring();
|
||||
auto b = bits();
|
||||
while(b) {
|
||||
r->add(static_cast<std::uint32_t>(std::countr_zero(b)));
|
||||
b &= b - 1;
|
||||
}
|
||||
r->add(new_id);
|
||||
data = reinterpret_cast<std::uint64_t>(r);
|
||||
}
|
||||
|
||||
public:
|
||||
ContextBitmap() = default;
|
||||
|
||||
~ContextBitmap() {
|
||||
free_heap();
|
||||
}
|
||||
|
||||
ContextBitmap(ContextBitmap&& other) noexcept : data(std::exchange(other.data, inline_tag)) {}
|
||||
|
||||
ContextBitmap& operator=(ContextBitmap&& other) noexcept {
|
||||
if(this != &other) {
|
||||
free_heap();
|
||||
data = std::exchange(other.data, inline_tag);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
ContextBitmap(const ContextBitmap&) = delete;
|
||||
ContextBitmap& operator=(const ContextBitmap&) = delete;
|
||||
|
||||
void add(std::uint32_t id) {
|
||||
if(is_inline()) {
|
||||
if(id < inline_capacity) {
|
||||
data |= (1ULL << (id + 1));
|
||||
} else {
|
||||
upgrade(id);
|
||||
}
|
||||
} else {
|
||||
as_heap()->add(id);
|
||||
}
|
||||
}
|
||||
|
||||
void remove(std::uint32_t id) {
|
||||
if(is_inline()) {
|
||||
if(id < inline_capacity)
|
||||
data &= ~(1ULL << (id + 1));
|
||||
} else {
|
||||
as_heap()->remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
bool is_empty() const {
|
||||
if(is_inline())
|
||||
return bits() == 0;
|
||||
return as_heap()->isEmpty();
|
||||
}
|
||||
|
||||
/// Check if (this - other) has any bits set, without allocating in the common inline case.
|
||||
bool any_not_in(const ContextBitmap& other) const {
|
||||
if(is_inline() && other.is_inline())
|
||||
return (bits() & ~other.bits()) != 0;
|
||||
|
||||
// Rare: at least one side is heap. Use roaring operations.
|
||||
if(is_inline()) {
|
||||
auto b = bits();
|
||||
while(b) {
|
||||
auto i = static_cast<std::uint32_t>(std::countr_zero(b));
|
||||
if(!other.as_heap()->contains(i))
|
||||
return true;
|
||||
b &= b - 1;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if(other.is_inline()) {
|
||||
for(auto v: *as_heap()) {
|
||||
if(v >= inline_capacity || !(other.bits() & (1ULL << v)))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return !(*as_heap() - *other.as_heap()).isEmpty();
|
||||
}
|
||||
|
||||
friend bool operator==(const ContextBitmap& lhs, const ContextBitmap& rhs) {
|
||||
if(lhs.is_inline() && rhs.is_inline())
|
||||
return lhs.data == rhs.data;
|
||||
// Different modes implies different sets: a heap bitmap always contains
|
||||
// at least one id >= inline_capacity that an inline bitmap cannot hold.
|
||||
if(lhs.is_inline() != rhs.is_inline())
|
||||
return false;
|
||||
return *lhs.as_heap() == *rhs.as_heap();
|
||||
}
|
||||
|
||||
/// Convert to roaring::Roaring (for serialization).
|
||||
roaring::Roaring to_roaring() const {
|
||||
roaring::Roaring r;
|
||||
if(is_inline()) {
|
||||
auto b = bits();
|
||||
while(b) {
|
||||
r.add(static_cast<std::uint32_t>(std::countr_zero(b)));
|
||||
b &= b - 1;
|
||||
}
|
||||
} else {
|
||||
r = *as_heap();
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
/// Construct from roaring::Roaring (for deserialization).
|
||||
static ContextBitmap from_roaring(const roaring::Roaring& r) {
|
||||
ContextBitmap result;
|
||||
if(r.isEmpty())
|
||||
return result;
|
||||
if(r.maximum() < inline_capacity) {
|
||||
for(auto v: r)
|
||||
result.data |= (1ULL << (v + 1));
|
||||
} else {
|
||||
result.data = reinterpret_cast<std::uint64_t>(new roaring::Roaring(r));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace clice
|
||||
|
||||
@@ -74,6 +74,14 @@ inline std::expected<std::string, std::error_code> read(llvm::StringRef path) {
|
||||
return buffer.get()->getBuffer().str();
|
||||
}
|
||||
|
||||
inline std::expected<void, std::error_code> rename(llvm::StringRef from, llvm::StringRef to) {
|
||||
auto error = llvm::sys::fs::rename(from, to);
|
||||
if(error) {
|
||||
return std::unexpected(error);
|
||||
}
|
||||
return std::expected<void, std::error_code>();
|
||||
}
|
||||
|
||||
} // namespace fs
|
||||
|
||||
namespace vfs = llvm::vfs;
|
||||
@@ -124,7 +132,7 @@ public:
|
||||
}
|
||||
|
||||
llvm::StringRef filename = path::filename(Path);
|
||||
if(filename.starts_with("preamble-") && filename.ends_with(".pch")) {
|
||||
if(filename.ends_with(".pch")) {
|
||||
return file;
|
||||
}
|
||||
return std::make_unique<VolatileFile>(std::move(*file));
|
||||
|
||||
@@ -6,11 +6,10 @@
|
||||
#include <system_error>
|
||||
#include <type_traits>
|
||||
|
||||
#include "eventide/common/meta.h"
|
||||
#include "eventide/common/ranges.h"
|
||||
#include "eventide/reflection/enum.h"
|
||||
#include "eventide/reflection/struct.h"
|
||||
|
||||
#include "kota/meta/enum.h"
|
||||
#include "kota/meta/struct.h"
|
||||
#include "kota/support/ranges.h"
|
||||
#include "kota/support/type_traits.h"
|
||||
#include "llvm/ADT/SmallString.h"
|
||||
#include "llvm/ADT/StringRef.h"
|
||||
#include "llvm/Support/Error.h"
|
||||
@@ -86,7 +85,7 @@ struct std::formatter<std::error_code> : std::formatter<std::string_view> {
|
||||
}
|
||||
};
|
||||
|
||||
template <eventide::refl::enum_type E>
|
||||
template <kota::meta::enum_type E>
|
||||
struct std::formatter<E> : std::formatter<std::string> {
|
||||
using Base = std::formatter<std::string>;
|
||||
|
||||
@@ -97,7 +96,7 @@ struct std::formatter<E> : std::formatter<std::string> {
|
||||
|
||||
template <typename FormatContext>
|
||||
auto format(const E& value, FormatContext& ctx) const {
|
||||
auto name = eventide::refl::enum_name(value);
|
||||
auto name = kota::meta::enum_name(value);
|
||||
if(name.empty()) {
|
||||
using U = std::underlying_type_t<E>;
|
||||
return Base::format(std::format("{}", static_cast<U>(value)), ctx);
|
||||
@@ -107,9 +106,8 @@ struct std::formatter<E> : std::formatter<std::string> {
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
concept clice_reflectable_class =
|
||||
eventide::refl::reflectable_class<T> && !eventide::sequence_range<T> &&
|
||||
!eventide::set_range<T> && !eventide::map_range<T>;
|
||||
concept clice_reflectable_class = kota::meta::reflectable_class<T> && !kota::sequence_range<T> &&
|
||||
!kota::set_range<T> && !kota::map_range<T>;
|
||||
|
||||
template <clice_reflectable_class T>
|
||||
struct std::formatter<T> : std::formatter<std::string> {
|
||||
@@ -138,7 +136,7 @@ std::string dump(const Object& object) {
|
||||
return std::format("\"{}\"", object);
|
||||
} else if constexpr(std::is_same_v<T, llvm::StringRef>) {
|
||||
return std::format("\"{}\"", object);
|
||||
} else if constexpr(eventide::map_range<T>) {
|
||||
} else if constexpr(kota::map_range<T>) {
|
||||
std::string result = "{";
|
||||
bool first = true;
|
||||
for(auto&& [key, value]: object) {
|
||||
@@ -150,8 +148,8 @@ std::string dump(const Object& object) {
|
||||
}
|
||||
result += "}";
|
||||
return result;
|
||||
} else if constexpr(eventide::set_range<T> || eventide::sequence_range<T>) {
|
||||
std::string result = eventide::set_range<T> ? "{" : "[";
|
||||
} else if constexpr(kota::set_range<T> || kota::sequence_range<T>) {
|
||||
std::string result = kota::set_range<T> ? "{" : "[";
|
||||
bool first = true;
|
||||
for(auto&& value: object) {
|
||||
if(!first) {
|
||||
@@ -160,10 +158,10 @@ std::string dump(const Object& object) {
|
||||
first = false;
|
||||
result += dump(value);
|
||||
}
|
||||
result += eventide::set_range<T> ? "}" : "]";
|
||||
result += kota::set_range<T> ? "}" : "]";
|
||||
return result;
|
||||
} else if constexpr(eventide::refl::enum_type<T>) {
|
||||
auto name = eventide::refl::enum_name(object);
|
||||
} else if constexpr(kota::meta::enum_type<T>) {
|
||||
auto name = kota::meta::enum_name(object);
|
||||
if(!name.empty()) {
|
||||
return std::format("\"{}\"", name);
|
||||
}
|
||||
@@ -172,7 +170,7 @@ std::string dump(const Object& object) {
|
||||
} else if constexpr(clice_reflectable_class<T>) {
|
||||
std::string result = "{";
|
||||
bool first = true;
|
||||
eventide::refl::for_each(object, [&](auto field) {
|
||||
kota::meta::for_each(object, [&](auto field) {
|
||||
if(!first) {
|
||||
result += ", ";
|
||||
}
|
||||
@@ -181,7 +179,7 @@ std::string dump(const Object& object) {
|
||||
});
|
||||
result += "}";
|
||||
return result;
|
||||
} else if constexpr(eventide::Formattable<T>) {
|
||||
} else if constexpr(kota::Formattable<T>) {
|
||||
return std::format("{}", object);
|
||||
} else {
|
||||
return "<unformattable>";
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "spdlog/sinks/ringbuffer_sink.h"
|
||||
#include "spdlog/sinks/stdout_color_sinks.h"
|
||||
#include "spdlog/sinks/stdout_sinks.h"
|
||||
#include "llvm/Support/Signals.h"
|
||||
|
||||
namespace clice::logging {
|
||||
|
||||
@@ -38,27 +39,73 @@ void stderr_logger(std::string_view name, const Options& options) {
|
||||
spdlog::set_default_logger(std::move(logger));
|
||||
}
|
||||
|
||||
void file_loggger(std::string_view name, std::string_view dir, const Options& options) {
|
||||
auto now = std::chrono::system_clock::now();
|
||||
auto filename = std::format("{:%Y-%m-%d_%H-%M-%S}.log", now);
|
||||
auto sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>(path::join(dir, filename));
|
||||
void file_logger(std::string_view name, std::string_view dir, const Options& options) {
|
||||
if(auto ec = llvm::sys::fs::create_directories(dir)) {
|
||||
spdlog::error("Failed to create log directory {}: {}", std::string(dir), ec.message());
|
||||
return;
|
||||
}
|
||||
auto filepath = path::join(dir, std::format("{}.log", name));
|
||||
// Verify we can write to the file before constructing the sink.
|
||||
// (spdlog would throw on failure, but exceptions are disabled in this project.)
|
||||
{
|
||||
std::error_code ec;
|
||||
llvm::raw_fd_ostream test(filepath, ec, llvm::sys::fs::OF_Append);
|
||||
if(ec) {
|
||||
spdlog::error("Failed to open log file {}: {}", filepath, ec.message());
|
||||
return;
|
||||
}
|
||||
}
|
||||
auto file_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>(filepath);
|
||||
|
||||
if(options.replay_console && ringbuffer_sink) {
|
||||
sink->set_level(options.level);
|
||||
sink->set_pattern(pattern);
|
||||
auto replay_buffer = ringbuffer_sink;
|
||||
|
||||
for(auto& log: ringbuffer_sink->last_raw()) {
|
||||
sink->log(log);
|
||||
auto console_sink = std::make_shared<spdlog::sinks::stderr_color_sink_mt>(options.color);
|
||||
std::array<spdlog::sink_ptr, 2> sinks = {file_sink, console_sink};
|
||||
auto logger = std::make_shared<spdlog::logger>(std::string(name), sinks.begin(), sinks.end());
|
||||
logger->set_level(options.level);
|
||||
logger->set_pattern(pattern);
|
||||
logger->flush_on(Level::trace);
|
||||
spdlog::set_default_logger(std::move(logger));
|
||||
|
||||
// Replay buffered logs after swapping the default logger, so no messages
|
||||
// emitted between the snapshot and the swap are lost.
|
||||
if(options.replay_console && replay_buffer) {
|
||||
file_sink->set_level(options.level);
|
||||
file_sink->set_pattern(pattern);
|
||||
|
||||
for(auto& log: replay_buffer->last_raw()) {
|
||||
file_sink->log(log);
|
||||
}
|
||||
|
||||
ringbuffer_sink.reset();
|
||||
}
|
||||
|
||||
auto logger = std::make_shared<spdlog::logger>(std::string(name), std::move(sink));
|
||||
logger->set_level(options.level);
|
||||
logger->set_pattern(pattern);
|
||||
logger->flush_on(Level::trace);
|
||||
spdlog::set_default_logger(std::move(logger));
|
||||
install_crash_handler(filepath);
|
||||
}
|
||||
|
||||
static std::unique_ptr<llvm::raw_fd_ostream> crash_log_stream;
|
||||
|
||||
static void crash_handler(void*) {
|
||||
if(crash_log_stream) {
|
||||
*crash_log_stream << "\n=== CRASH STACK TRACE ===\n";
|
||||
llvm::sys::PrintStackTrace(*crash_log_stream);
|
||||
crash_log_stream->flush();
|
||||
}
|
||||
}
|
||||
|
||||
void install_crash_handler(std::string_view log_path) {
|
||||
std::error_code ec;
|
||||
crash_log_stream =
|
||||
std::make_unique<llvm::raw_fd_ostream>(llvm::StringRef(log_path.data(), log_path.size()),
|
||||
ec,
|
||||
llvm::sys::fs::OF_Append);
|
||||
if(ec) {
|
||||
LOG_WARN("Failed to install crash handler for {}: {}", log_path, ec.message());
|
||||
crash_log_stream.reset();
|
||||
return;
|
||||
}
|
||||
llvm::sys::AddSignalHandler(crash_handler, nullptr);
|
||||
llvm::sys::PrintStackTraceOnErrorSignal("clice");
|
||||
}
|
||||
|
||||
} // namespace clice::logging
|
||||
|
||||
@@ -26,7 +26,12 @@ extern Options options;
|
||||
|
||||
void stderr_logger(std::string_view name, const Options& options);
|
||||
|
||||
void file_loggger(std::string_view name, std::string_view dir, const Options& options);
|
||||
void file_logger(std::string_view name, std::string_view dir, const Options& options);
|
||||
|
||||
/// Install a signal handler that writes crash stacktraces to the given log file.
|
||||
/// Also enables LLVM's default stderr stacktrace output.
|
||||
/// Must be called after file_logger so the log file path is known.
|
||||
void install_crash_handler(std::string_view log_path);
|
||||
|
||||
template <typename... Args>
|
||||
struct logging_rformat {
|
||||
@@ -95,9 +100,11 @@ void critical [[noreturn]] (logging_format<Args...> fmt, Args&&... args) {
|
||||
} // namespace clice::logging
|
||||
|
||||
#define LOG_MESSAGE(name, fmt, ...) \
|
||||
if(clice::logging::options.level <= clice::logging::Level::name) { \
|
||||
clice::logging::name(fmt __VA_OPT__(, ) __VA_ARGS__); \
|
||||
}
|
||||
do { \
|
||||
if(clice::logging::options.level <= clice::logging::Level::name) { \
|
||||
clice::logging::name(fmt __VA_OPT__(, ) __VA_ARGS__); \
|
||||
} \
|
||||
} while(0)
|
||||
|
||||
#define LOG_TRACE(fmt, ...) LOG_MESSAGE(trace, fmt, __VA_ARGS__)
|
||||
#define LOG_DEBUG(fmt, ...) LOG_MESSAGE(debug, fmt, __VA_ARGS__)
|
||||
|
||||
121
src/syntax/completion.cpp
Normal file
121
src/syntax/completion.cpp
Normal file
@@ -0,0 +1,121 @@
|
||||
#include "syntax/completion.h"
|
||||
|
||||
#include "syntax/include_resolver.h"
|
||||
|
||||
#include "llvm/ADT/SmallString.h"
|
||||
#include "llvm/ADT/StringSet.h"
|
||||
#include "llvm/Support/FileSystem.h"
|
||||
#include "llvm/Support/Path.h"
|
||||
|
||||
namespace clice {
|
||||
|
||||
PreambleCompletionContext detect_completion_context(llvm::StringRef text, std::uint32_t offset) {
|
||||
auto line_start = text.rfind('\n', offset > 0 ? offset - 1 : 0);
|
||||
line_start = (line_start == llvm::StringRef::npos) ? 0 : line_start + 1;
|
||||
|
||||
auto line = text.slice(line_start, offset);
|
||||
auto trimmed = line.ltrim();
|
||||
|
||||
if(trimmed.starts_with("#")) {
|
||||
auto directive = trimmed.drop_front(1).ltrim();
|
||||
if(directive.consume_front("include")) {
|
||||
directive = directive.ltrim();
|
||||
if(directive.consume_front("\"")) {
|
||||
return {CompletionContext::IncludeQuoted, directive.str()};
|
||||
}
|
||||
if(directive.consume_front("<")) {
|
||||
return {CompletionContext::IncludeAngled, directive.str()};
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
auto import_check = trimmed;
|
||||
if(import_check.consume_front("export") && !import_check.empty() &&
|
||||
!std::isalnum(import_check[0])) {
|
||||
import_check = import_check.ltrim();
|
||||
}
|
||||
if(import_check.consume_front("import") &&
|
||||
(import_check.empty() || !std::isalnum(import_check[0]))) {
|
||||
import_check = import_check.ltrim();
|
||||
|
||||
auto line_end = text.find('\n', offset);
|
||||
if(line_end == llvm::StringRef::npos)
|
||||
line_end = text.size();
|
||||
auto rest_of_line = text.slice(line_start, line_end);
|
||||
if(!rest_of_line.contains(';')) {
|
||||
return {CompletionContext::Import, import_check.str()};
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<std::string>
|
||||
complete_module_import(const llvm::DenseMap<std::uint32_t, std::string>& modules,
|
||||
llvm::StringRef prefix) {
|
||||
std::vector<std::string> results;
|
||||
for(auto& [path_id, module_name]: modules) {
|
||||
if(llvm::StringRef(module_name).starts_with(prefix)) {
|
||||
results.push_back(module_name);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
std::vector<IncludeCandidate> complete_include_path(const ResolvedSearchConfig& resolved,
|
||||
llvm::StringRef prefix,
|
||||
bool angled,
|
||||
DirListingCache& dir_cache) {
|
||||
llvm::StringRef dir_prefix;
|
||||
llvm::StringRef file_prefix = prefix;
|
||||
auto slash_pos = prefix.rfind('/');
|
||||
if(slash_pos != llvm::StringRef::npos) {
|
||||
dir_prefix = prefix.slice(0, slash_pos);
|
||||
file_prefix = prefix.slice(slash_pos + 1, llvm::StringRef::npos);
|
||||
}
|
||||
|
||||
unsigned start_idx = angled ? resolved.angled_start_idx : 0;
|
||||
|
||||
std::vector<IncludeCandidate> results;
|
||||
llvm::StringSet<> seen;
|
||||
|
||||
for(unsigned i = start_idx; i < resolved.dirs.size(); ++i) {
|
||||
auto& search_dir = resolved.dirs[i];
|
||||
|
||||
const llvm::StringSet<>* entries = nullptr;
|
||||
if(!dir_prefix.empty()) {
|
||||
llvm::SmallString<256> sub_path(search_dir.path);
|
||||
llvm::sys::path::append(sub_path, dir_prefix);
|
||||
entries = resolve_dir(sub_path, dir_cache);
|
||||
} else {
|
||||
entries = search_dir.entries;
|
||||
}
|
||||
|
||||
if(!entries)
|
||||
continue;
|
||||
|
||||
for(auto& entry: *entries) {
|
||||
auto name = entry.getKey();
|
||||
if(!name.starts_with(file_prefix))
|
||||
continue;
|
||||
if(!seen.insert(name).second)
|
||||
continue;
|
||||
|
||||
llvm::SmallString<256> full_path(search_dir.path);
|
||||
if(!dir_prefix.empty()) {
|
||||
llvm::sys::path::append(full_path, dir_prefix);
|
||||
}
|
||||
llvm::sys::path::append(full_path, name);
|
||||
|
||||
bool is_dir = false;
|
||||
llvm::sys::fs::is_directory(llvm::Twine(full_path), is_dir);
|
||||
|
||||
results.push_back({name.str(), is_dir});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
} // namespace clice
|
||||
57
src/syntax/completion.h
Normal file
57
src/syntax/completion.h
Normal file
@@ -0,0 +1,57 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "llvm/ADT/DenseMap.h"
|
||||
#include "llvm/ADT/StringRef.h"
|
||||
|
||||
namespace clice {
|
||||
|
||||
struct ResolvedSearchConfig;
|
||||
struct DirListingCache;
|
||||
|
||||
/// What kind of preamble-level completion is being requested.
|
||||
enum class CompletionContext : std::uint8_t {
|
||||
None,
|
||||
IncludeQuoted,
|
||||
IncludeAngled,
|
||||
Import,
|
||||
};
|
||||
|
||||
/// Result of detecting the completion context from source text.
|
||||
struct PreambleCompletionContext {
|
||||
CompletionContext kind = CompletionContext::None;
|
||||
std::string prefix;
|
||||
};
|
||||
|
||||
/// Detect whether the cursor is inside a #include or import directive.
|
||||
/// Pure text parsing — no compiler state needed.
|
||||
PreambleCompletionContext detect_completion_context(llvm::StringRef text, std::uint32_t offset);
|
||||
|
||||
/// Return module names matching a prefix, suitable for `import` completion.
|
||||
/// @param modules Module name map (path_id → module name).
|
||||
/// @param prefix Partially-typed module name to match against.
|
||||
std::vector<std::string>
|
||||
complete_module_import(const llvm::DenseMap<std::uint32_t, std::string>& modules,
|
||||
llvm::StringRef prefix);
|
||||
|
||||
/// Entry in the include path completion result.
|
||||
struct IncludeCandidate {
|
||||
std::string name;
|
||||
bool is_directory = false;
|
||||
};
|
||||
|
||||
/// Return file/directory names matching a prefix in the given search paths.
|
||||
/// @param resolved Pre-resolved search directories with cached directory listings.
|
||||
/// @param angled_start Index where angled (<>) search dirs begin.
|
||||
/// @param prefix Partially-typed include path (e.g. "vec" or "sys/").
|
||||
/// @param angled True for <> includes, false for "" includes.
|
||||
/// @param dir_cache Shared directory listing cache (for subdirectory lookups).
|
||||
std::vector<IncludeCandidate> complete_include_path(const ResolvedSearchConfig& resolved,
|
||||
llvm::StringRef prefix,
|
||||
bool angled,
|
||||
DirListingCache& dir_cache);
|
||||
|
||||
} // namespace clice
|
||||
@@ -1,13 +1,14 @@
|
||||
#include "syntax/dependency_graph.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
|
||||
#include "command/toolchain.h"
|
||||
#include "eventide/async/async.h"
|
||||
#include "support/logging.h"
|
||||
#include "syntax/include_resolver.h"
|
||||
#include "syntax/scan.h"
|
||||
|
||||
#include "kota/async/async.h"
|
||||
#include "llvm/ADT/DenseSet.h"
|
||||
#include "llvm/ADT/StringSet.h"
|
||||
#include "llvm/Support/FileSystem.h"
|
||||
@@ -17,11 +18,7 @@
|
||||
|
||||
namespace clice {
|
||||
|
||||
namespace et = eventide;
|
||||
|
||||
// ============================================================================
|
||||
// DependencyGraph implementation
|
||||
// ============================================================================
|
||||
|
||||
void DependencyGraph::add_module(llvm::StringRef module_name, std::uint32_t path_id) {
|
||||
auto& ids = module_to_path[module_name];
|
||||
@@ -101,9 +98,110 @@ std::size_t DependencyGraph::edge_count() const {
|
||||
return count;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
void DependencyGraph::build_reverse_map() {
|
||||
reverse_includes_.clear();
|
||||
for(auto& [key, ids]: includes) {
|
||||
for(auto flagged_id: ids) {
|
||||
auto included_id = flagged_id & PATH_ID_MASK;
|
||||
auto& vec = reverse_includes_[included_id];
|
||||
if(llvm::find(vec, key.path_id) == vec.end()) {
|
||||
vec.push_back(key.path_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
llvm::ArrayRef<std::uint32_t> DependencyGraph::get_includers(std::uint32_t path_id) const {
|
||||
auto it = reverse_includes_.find(path_id);
|
||||
if(it != reverse_includes_.end()) {
|
||||
return it->second;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
llvm::SmallVector<std::uint32_t, 4>
|
||||
DependencyGraph::find_host_sources(std::uint32_t header_path_id) const {
|
||||
llvm::SmallVector<std::uint32_t, 4> result;
|
||||
llvm::DenseSet<std::uint32_t> visited;
|
||||
llvm::SmallVector<std::uint32_t, 16> queue;
|
||||
|
||||
queue.push_back(header_path_id);
|
||||
visited.insert(header_path_id);
|
||||
|
||||
while(!queue.empty()) {
|
||||
auto current = queue.pop_back_val();
|
||||
auto includers = get_includers(current);
|
||||
if(includers.empty()) {
|
||||
// No includers: this is a root (source file).
|
||||
// Exclude the starting header itself.
|
||||
if(current != header_path_id) {
|
||||
result.push_back(current);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
for(auto includer: includers) {
|
||||
if(visited.insert(includer).second) {
|
||||
queue.push_back(includer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<std::uint32_t> DependencyGraph::find_include_chain(std::uint32_t host_path_id,
|
||||
std::uint32_t target_path_id) const {
|
||||
if(host_path_id == target_path_id) {
|
||||
return {host_path_id};
|
||||
}
|
||||
|
||||
// BFS: predecessor map for path reconstruction.
|
||||
llvm::DenseMap<std::uint32_t, std::uint32_t> prev;
|
||||
llvm::SmallVector<std::uint32_t, 16> queue;
|
||||
|
||||
prev[host_path_id] = host_path_id;
|
||||
queue.push_back(host_path_id);
|
||||
|
||||
bool found = false;
|
||||
while(!queue.empty() && !found) {
|
||||
llvm::SmallVector<std::uint32_t, 16> next_queue;
|
||||
for(auto current: queue) {
|
||||
auto includes_union = get_all_includes(current);
|
||||
for(auto flagged_id: includes_union) {
|
||||
auto child = flagged_id & PATH_ID_MASK;
|
||||
if(prev.find(child) == prev.end()) {
|
||||
prev[child] = current;
|
||||
if(child == target_path_id) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
next_queue.push_back(child);
|
||||
}
|
||||
}
|
||||
if(found) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
queue = std::move(next_queue);
|
||||
}
|
||||
|
||||
if(!found) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Reconstruct path from target back to host.
|
||||
std::vector<std::uint32_t> chain;
|
||||
auto node = target_path_id;
|
||||
while(node != host_path_id) {
|
||||
chain.push_back(node);
|
||||
node = prev[node];
|
||||
}
|
||||
chain.push_back(host_path_id);
|
||||
std::reverse(chain.begin(), chain.end());
|
||||
return chain;
|
||||
}
|
||||
|
||||
// Wavefront BFS scanner — async implementation
|
||||
// ============================================================================
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -153,12 +251,12 @@ FileScanResult scan_file_worker(const char* path, std::uint32_t path_id, std::ui
|
||||
}
|
||||
|
||||
/// The async scan implementation that runs on a local event loop.
|
||||
et::task<> scan_impl(CompilationDatabase& cdb,
|
||||
PathPool& path_pool,
|
||||
DependencyGraph& graph,
|
||||
ScanReport& report,
|
||||
ScanCache* ext_cache,
|
||||
et::event_loop& loop) {
|
||||
kota::task<> scan_impl(CompilationDatabase& cdb,
|
||||
PathPool& path_pool,
|
||||
DependencyGraph& graph,
|
||||
ScanReport& report,
|
||||
ScanCache* ext_cache,
|
||||
kota::event_loop& loop) {
|
||||
auto start_time = std::chrono::steady_clock::now();
|
||||
|
||||
// Reuse context groups and configs from cache when available (warm runs).
|
||||
@@ -216,10 +314,10 @@ et::task<> scan_impl(CompilationDatabase& cdb,
|
||||
if(!pending.empty()) {
|
||||
LOG_INFO("Warming toolchain cache: {} unique queries", pending.size());
|
||||
|
||||
std::vector<et::task<ToolchainResult, et::error>> tasks;
|
||||
std::vector<kota::task<ToolchainResult, kota::error>> tasks;
|
||||
tasks.reserve(pending.size());
|
||||
for(auto& query: pending) {
|
||||
tasks.push_back(et::queue(
|
||||
tasks.push_back(kota::queue(
|
||||
[q = std::move(query)]() -> ToolchainResult {
|
||||
ToolchainResult result;
|
||||
result.key = q.key;
|
||||
@@ -237,7 +335,7 @@ et::task<> scan_impl(CompilationDatabase& cdb,
|
||||
loop));
|
||||
}
|
||||
|
||||
auto outcome = co_await et::when_all(std::move(tasks));
|
||||
auto outcome = co_await kota::when_all(std::move(tasks));
|
||||
if(outcome.has_value()) {
|
||||
cdb.inject_results(*outcome);
|
||||
} else {
|
||||
@@ -290,7 +388,7 @@ et::task<> scan_impl(CompilationDatabase& cdb,
|
||||
llvm::StringSet<> entries;
|
||||
};
|
||||
|
||||
std::vector<et::task<DirEntry, et::error>> pending_dir_tasks;
|
||||
std::vector<kota::task<DirEntry, kota::error>> pending_dir_tasks;
|
||||
|
||||
if(dir_cache.dirs.empty()) {
|
||||
llvm::StringSet<> unique_dirs;
|
||||
@@ -312,7 +410,7 @@ et::task<> scan_impl(CompilationDatabase& cdb,
|
||||
pending_dir_tasks.reserve(unique_dirs.size());
|
||||
for(auto& entry: unique_dirs) {
|
||||
auto dir_path = entry.getKey().str();
|
||||
pending_dir_tasks.push_back(et::queue(
|
||||
pending_dir_tasks.push_back(kota::queue(
|
||||
[dir_path = std::move(dir_path)]() -> DirEntry {
|
||||
DirEntry result;
|
||||
result.dir_path = dir_path;
|
||||
@@ -363,7 +461,7 @@ et::task<> scan_impl(CompilationDatabase& cdb,
|
||||
// queued for scanning on the thread pool. When wave N+1 starts,
|
||||
// these tasks are already running (or finished), eliminating most
|
||||
// of the Phase 1 wait time for subsequent waves.
|
||||
std::vector<et::task<FileScanResult, et::error>> prefetch_tasks;
|
||||
std::vector<kota::task<FileScanResult, kota::error>> prefetch_tasks;
|
||||
|
||||
// Pre-resolved search configs: built once after dir cache is populated,
|
||||
// then reused for all waves. Eliminates StringMap lookups in Phase 2.
|
||||
@@ -400,7 +498,7 @@ et::task<> scan_impl(CompilationDatabase& cdb,
|
||||
|
||||
if(!prefetch_tasks.empty()) {
|
||||
// Waves 1+: await prefetched scan tasks from previous Phase 2.
|
||||
auto scan_outcome = co_await et::when_all(std::move(prefetch_tasks));
|
||||
auto scan_outcome = co_await kota::when_all(std::move(prefetch_tasks));
|
||||
prefetch_tasks.clear();
|
||||
if(scan_outcome.has_error()) {
|
||||
LOG_ERROR("Prefetch scan failed: {}", scan_outcome.error().message());
|
||||
@@ -414,7 +512,7 @@ et::task<> scan_impl(CompilationDatabase& cdb,
|
||||
}
|
||||
} else {
|
||||
// Wave 0 (or warm run with all cache hits): create scan tasks now.
|
||||
std::vector<et::task<FileScanResult, et::error>> scan_tasks;
|
||||
std::vector<kota::task<FileScanResult, kota::error>> scan_tasks;
|
||||
scan_tasks.reserve(current_wave.size());
|
||||
for(auto& entry: current_wave) {
|
||||
auto pid = entry.path_id;
|
||||
@@ -425,8 +523,8 @@ et::task<> scan_impl(CompilationDatabase& cdb,
|
||||
}
|
||||
auto path = path_pool.resolve(pid).data();
|
||||
scan_tasks.push_back(
|
||||
et::queue([path, pid, cid]() { return scan_file_worker(path, pid, cid); },
|
||||
loop));
|
||||
kota::queue([path, pid, cid]() { return scan_file_worker(path, pid, cid); },
|
||||
loop));
|
||||
}
|
||||
|
||||
// Optimization 1: await dir cache tasks concurrently with scan tasks.
|
||||
@@ -435,7 +533,7 @@ et::task<> scan_impl(CompilationDatabase& cdb,
|
||||
// max(dir_time, scan_time) instead of dir_time + scan_time.
|
||||
if(!pending_dir_tasks.empty()) {
|
||||
auto dir_t0 = std::chrono::steady_clock::now();
|
||||
auto dir_outcome = co_await et::when_all(std::move(pending_dir_tasks));
|
||||
auto dir_outcome = co_await kota::when_all(std::move(pending_dir_tasks));
|
||||
pending_dir_tasks.clear();
|
||||
if(dir_outcome.has_value()) {
|
||||
for(auto& entry: *dir_outcome) {
|
||||
@@ -449,7 +547,7 @@ et::task<> scan_impl(CompilationDatabase& cdb,
|
||||
}
|
||||
|
||||
if(!scan_tasks.empty()) {
|
||||
auto scan_outcome = co_await et::when_all(std::move(scan_tasks));
|
||||
auto scan_outcome = co_await kota::when_all(std::move(scan_tasks));
|
||||
if(scan_outcome.has_error()) {
|
||||
LOG_ERROR("Parallel scan failed: {}", scan_outcome.error().message());
|
||||
break;
|
||||
@@ -523,8 +621,9 @@ et::task<> scan_impl(CompilationDatabase& cdb,
|
||||
auto contexts =
|
||||
cdb.lookup(file_path, {.query_toolchain = true, .suppress_logging = true});
|
||||
if(!contexts.empty()) {
|
||||
auto& ctx = contexts[0];
|
||||
auto fallback = scan_module_decl(ctx.arguments, ctx.directory, /*content=*/{});
|
||||
auto& cmd = contexts[0];
|
||||
auto fallback =
|
||||
scan_module_decl(cmd.to_argv(), cmd.resolved.directory, /*content=*/{});
|
||||
if(!fallback.module_name.empty()) {
|
||||
scan_result.scan_result.module_name = std::move(fallback.module_name);
|
||||
scan_result.scan_result.is_interface_unit = fallback.is_interface_unit;
|
||||
@@ -648,7 +747,7 @@ et::task<> scan_impl(CompilationDatabase& cdb,
|
||||
if(!ext_cache ||
|
||||
ext_cache->scan_results.find(inc_path_id) == ext_cache->scan_results.end()) {
|
||||
auto inc_path = path_pool.resolve(inc_path_id).data();
|
||||
prefetch_tasks.push_back(et::queue(
|
||||
prefetch_tasks.push_back(kota::queue(
|
||||
[inc_path, inc_path_id, cid = scan_result.config_id]() {
|
||||
return scan_file_worker(inc_path, inc_path_id, cid);
|
||||
},
|
||||
@@ -715,9 +814,7 @@ et::task<> scan_impl(CompilationDatabase& cdb,
|
||||
|
||||
} // namespace
|
||||
|
||||
// ============================================================================
|
||||
// Public sync entry point
|
||||
// ============================================================================
|
||||
|
||||
ScanReport scan_dependency_graph(CompilationDatabase& cdb,
|
||||
PathPool& path_pool,
|
||||
@@ -728,7 +825,7 @@ ScanReport scan_dependency_graph(CompilationDatabase& cdb,
|
||||
return report;
|
||||
}
|
||||
|
||||
et::event_loop loop;
|
||||
kota::event_loop loop;
|
||||
loop.schedule(scan_impl(cdb, path_pool, graph, report, cache, loop));
|
||||
loop.run();
|
||||
return report;
|
||||
|
||||
@@ -70,6 +70,24 @@ public:
|
||||
/// Get the union of includes across all configs for a file.
|
||||
llvm::SmallVector<std::uint32_t> get_all_includes(std::uint32_t path_id) const;
|
||||
|
||||
/// Build the reverse include map from the forward includes.
|
||||
/// Must be called after all set_includes() calls are complete.
|
||||
void build_reverse_map();
|
||||
|
||||
/// Get the direct includers of a file (files that directly include path_id).
|
||||
llvm::ArrayRef<std::uint32_t> get_includers(std::uint32_t path_id) const;
|
||||
|
||||
/// BFS upward through reverse edges to find all source files (roots)
|
||||
/// that transitively include header_path_id.
|
||||
/// Source files are those that have no includers (i.e. they are roots in the graph).
|
||||
llvm::SmallVector<std::uint32_t, 4> find_host_sources(std::uint32_t header_path_id) const;
|
||||
|
||||
/// BFS forward through include edges to find the shortest include chain
|
||||
/// from host_path_id to target_path_id.
|
||||
/// Returns [host, intermediate1, ..., target], or empty if no path exists.
|
||||
std::vector<std::uint32_t> find_include_chain(std::uint32_t host_path_id,
|
||||
std::uint32_t target_path_id) const;
|
||||
|
||||
/// Number of files with include entries.
|
||||
std::size_t file_count() const;
|
||||
|
||||
@@ -94,6 +112,10 @@ private:
|
||||
|
||||
/// Track which files have any include entries (for file_count).
|
||||
llvm::DenseMap<std::uint32_t, llvm::SmallVector<std::uint32_t>> file_configs;
|
||||
|
||||
/// Reverse include map: PathID -> list of PathIDs that directly include it.
|
||||
/// Populated by build_reverse_map().
|
||||
llvm::DenseMap<std::uint32_t, llvm::SmallVector<std::uint32_t, 4>> reverse_includes_;
|
||||
};
|
||||
|
||||
/// A (file, search-config) pair used to track per-wave work items.
|
||||
|
||||
@@ -53,7 +53,8 @@ void Lexer::lex(Token& token) {
|
||||
}
|
||||
} else if(parse_pp_keyword) {
|
||||
parse_pp_keyword = false;
|
||||
parse_header_name = token.text(content) == "include";
|
||||
auto kw = token.text(content);
|
||||
parse_header_name = kw == "include" || kw == "include_next" || kw == "embed";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,4 +106,60 @@ Token Lexer::advance_until(TokenKind kind) {
|
||||
}
|
||||
}
|
||||
|
||||
static bool is_directive_keyword(llvm::StringRef word) {
|
||||
return word == "include" || word == "include_next" || word == "import" || word == "embed" ||
|
||||
word == "__has_include" || word == "__has_include_next" || word == "__has_embed";
|
||||
}
|
||||
|
||||
std::optional<LocalSourceRange> find_directive_argument(llvm::StringRef content,
|
||||
std::uint32_t offset,
|
||||
const clang::LangOptions* lang_opts) {
|
||||
std::uint32_t line_start = 0;
|
||||
if(auto nl = content.rfind('\n', offset); nl != llvm::StringRef::npos)
|
||||
line_start = static_cast<std::uint32_t>(nl + 1);
|
||||
|
||||
auto line = content.substr(line_start);
|
||||
Lexer lexer(line, true, lang_opts);
|
||||
bool after_has_keyword = false;
|
||||
bool ready = false;
|
||||
|
||||
while(true) {
|
||||
auto tok = lexer.advance();
|
||||
if(tok.is_eof() || tok.is_eod())
|
||||
break;
|
||||
|
||||
auto abs_begin = line_start + tok.range.begin;
|
||||
auto abs_end = line_start + tok.range.end;
|
||||
|
||||
if(tok.is_identifier()) {
|
||||
auto text = tok.text(line);
|
||||
if(text == "__has_include" || text == "__has_include_next" || text == "__has_embed") {
|
||||
after_has_keyword = true;
|
||||
continue;
|
||||
}
|
||||
if(text == "include" || text == "include_next" || text == "embed") {
|
||||
ready = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if(tok.kind == clang::tok::l_paren && after_has_keyword) {
|
||||
after_has_keyword = false;
|
||||
ready = true;
|
||||
lexer.set_header_name_mode();
|
||||
continue;
|
||||
}
|
||||
|
||||
if(abs_begin < offset || !ready)
|
||||
continue;
|
||||
|
||||
if(tok.is_header_name() || tok.kind == clang::tok::string_literal)
|
||||
return LocalSourceRange(abs_begin, abs_end);
|
||||
|
||||
if(tok.is_identifier())
|
||||
return LocalSourceRange(abs_begin, abs_end);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
} // namespace clice
|
||||
|
||||
@@ -51,6 +51,15 @@ public:
|
||||
|
||||
Token advance_until(TokenKind kind);
|
||||
|
||||
/// Force the lexer into header-name mode so the next token is lexed
|
||||
/// via LexIncludeFilename (correctly handling both "..." and <...>).
|
||||
/// Use this before lexing filename arguments in contexts like
|
||||
/// __has_include() or __has_embed() where the lexer cannot detect
|
||||
/// the mode automatically.
|
||||
void set_header_name_mode() {
|
||||
parse_header_name = true;
|
||||
}
|
||||
|
||||
private:
|
||||
bool ignore_end_of_directive = true;
|
||||
bool parse_pp_keyword = false;
|
||||
@@ -64,4 +73,13 @@ private:
|
||||
std::unique_ptr<clang::Lexer> lexer;
|
||||
};
|
||||
|
||||
/// Find the range of the filename argument in a preprocessor directive line.
|
||||
/// `content` is the full source text, `offset` points at or before the directive keyword.
|
||||
/// Returns the range of the first filename-like token (header name, string literal,
|
||||
/// or macro identifier) found on the same line, or nullopt if none.
|
||||
std::optional<LocalSourceRange>
|
||||
find_directive_argument(llvm::StringRef content,
|
||||
std::uint32_t offset,
|
||||
const clang::LangOptions* lang_opts = nullptr);
|
||||
|
||||
} // namespace clice
|
||||
|
||||
@@ -476,4 +476,60 @@ std::vector<std::uint32_t> compute_preamble_bounds(llvm::StringRef content) {
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Check if a preprocessor #include/#import directive line is complete.
|
||||
static bool is_include_directive_complete(llvm::StringRef directive) {
|
||||
if(directive.contains('"')) {
|
||||
auto after_keyword = directive.drop_front(directive.starts_with("import") ? 6 : 7);
|
||||
return after_keyword.count('"') >= 2;
|
||||
}
|
||||
if(directive.contains('<')) {
|
||||
return directive.contains('>');
|
||||
}
|
||||
// No " or < — might be a macro (#include FOO) or just incomplete (#include ).
|
||||
auto after_keyword = directive.drop_front(directive.starts_with("import") ? 6 : 7).ltrim();
|
||||
return !after_keyword.empty();
|
||||
}
|
||||
|
||||
/// Check if a C++20 module statement line (import/export module) is complete.
|
||||
/// A complete statement must end with ';'.
|
||||
static bool is_module_statement_complete(llvm::StringRef trimmed) {
|
||||
return trimmed.rtrim().ends_with(";");
|
||||
}
|
||||
|
||||
bool is_preamble_complete(llvm::StringRef content, std::uint32_t bound) {
|
||||
auto preamble = content.substr(0, bound);
|
||||
|
||||
while(!preamble.empty()) {
|
||||
auto [line, rest] = preamble.split('\n');
|
||||
preamble = rest;
|
||||
|
||||
auto trimmed = line.ltrim();
|
||||
|
||||
// Preprocessor directive: #include or #import
|
||||
if(trimmed.starts_with("#")) {
|
||||
auto directive = trimmed.drop_front(1).ltrim();
|
||||
if(directive.starts_with("include") || directive.starts_with("import")) {
|
||||
if(!is_include_directive_complete(directive)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// C++20 module statements: import, export module, export import
|
||||
// Check word boundary to avoid matching identifiers like "important".
|
||||
auto is_keyword = [](llvm::StringRef s, llvm::StringRef keyword) {
|
||||
return s.starts_with(keyword) &&
|
||||
(s.size() == keyword.size() || !llvm::isAlnum(s[keyword.size()]));
|
||||
};
|
||||
if(is_keyword(trimmed, "import") || is_keyword(trimmed, "export")) {
|
||||
if(!is_module_statement_complete(trimmed)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace clice
|
||||
|
||||
@@ -100,4 +100,10 @@ std::uint32_t compute_preamble_bound(llvm::StringRef content);
|
||||
|
||||
std::vector<std::uint32_t> compute_preamble_bounds(llvm::StringRef content);
|
||||
|
||||
/// Check if the preamble region contains only syntactically complete directives.
|
||||
/// Returns false if any #include/#import has an unclosed "" or <>, or any
|
||||
/// C++20 module statement (import/export) is missing a trailing ';',
|
||||
/// indicating the user is still typing and PCH/PCM rebuild should be deferred.
|
||||
bool is_preamble_complete(llvm::StringRef content, std::uint32_t bound);
|
||||
|
||||
} // namespace clice
|
||||
|
||||
@@ -44,10 +44,6 @@ struct LocalSourceRange {
|
||||
std::uint32_t begin = static_cast<std::uint32_t>(-1);
|
||||
std::uint32_t end = static_cast<std::uint32_t>(-1);
|
||||
|
||||
constexpr LocalSourceRange() = default;
|
||||
|
||||
constexpr LocalSourceRange(std::uint32_t begin, std::uint32_t end) : begin(begin), end(end) {}
|
||||
|
||||
constexpr bool operator==(const LocalSourceRange& other) const = default;
|
||||
|
||||
constexpr std::uint32_t length() const {
|
||||
|
||||
@@ -1,31 +1,13 @@
|
||||
"""Fixtures and shared helpers for clice LSP integration tests using pygls LanguageClient."""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from collections.abc import AsyncGenerator
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from lsprotocol.types import (
|
||||
PROGRESS,
|
||||
TEXT_DOCUMENT_PUBLISH_DIAGNOSTICS,
|
||||
WINDOW_WORK_DONE_PROGRESS_CREATE,
|
||||
ClientCapabilities,
|
||||
Diagnostic,
|
||||
DidOpenTextDocumentParams,
|
||||
InitializeParams,
|
||||
InitializeResult,
|
||||
InitializedParams,
|
||||
ProgressParams,
|
||||
PublishDiagnosticsParams,
|
||||
TextDocumentItem,
|
||||
WorkDoneProgressCreateParams,
|
||||
WorkspaceFolder,
|
||||
)
|
||||
from pygls.lsp.client import BaseLanguageClient
|
||||
|
||||
from tests.integration.utils.client import CliceClient
|
||||
|
||||
|
||||
def pytest_addoption(parser: pytest.Parser) -> None:
|
||||
@@ -55,91 +37,6 @@ def pytest_addoption(parser: pytest.Parser) -> None:
|
||||
)
|
||||
|
||||
|
||||
class CliceClient(BaseLanguageClient):
|
||||
"""Language client that tracks server-sent notifications."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__("clice-test-client", "0.1.0")
|
||||
self.diagnostics: dict[str, list[Diagnostic]] = {}
|
||||
self.diagnostics_events: dict[str, asyncio.Event] = {}
|
||||
self.progress_tokens: list[str] = []
|
||||
self.progress_events: list[dict] = []
|
||||
self.init_result: InitializeResult | None = None
|
||||
|
||||
@self.feature(TEXT_DOCUMENT_PUBLISH_DIAGNOSTICS)
|
||||
def on_diagnostics(params: PublishDiagnosticsParams) -> None:
|
||||
self.diagnostics[params.uri] = list(params.diagnostics)
|
||||
if params.uri in self.diagnostics_events:
|
||||
self.diagnostics_events[params.uri].set()
|
||||
|
||||
@self.feature(WINDOW_WORK_DONE_PROGRESS_CREATE)
|
||||
def on_create_progress(params: WorkDoneProgressCreateParams) -> None:
|
||||
token = str(params.token) if isinstance(params.token, int) else params.token
|
||||
self.progress_tokens.append(token)
|
||||
return None
|
||||
|
||||
@self.feature(PROGRESS)
|
||||
def on_progress(params: ProgressParams) -> None:
|
||||
token = str(params.token) if isinstance(params.token, int) else params.token
|
||||
self.progress_events.append({"token": token, "value": params.value})
|
||||
|
||||
def wait_for_diagnostics(self, uri: str) -> asyncio.Event:
|
||||
"""Get or create an event that fires when diagnostics arrive for uri."""
|
||||
if uri not in self.diagnostics_events:
|
||||
self.diagnostics_events[uri] = asyncio.Event()
|
||||
else:
|
||||
self.diagnostics_events[uri].clear()
|
||||
return self.diagnostics_events[uri]
|
||||
|
||||
async def initialize(self, workspace: Path) -> InitializeResult:
|
||||
"""Initialize the LSP server with a workspace folder and return the result."""
|
||||
result = await self.initialize_async(
|
||||
InitializeParams(
|
||||
capabilities=ClientCapabilities(),
|
||||
root_uri=workspace.as_uri(),
|
||||
workspace_folders=[
|
||||
WorkspaceFolder(uri=workspace.as_uri(), name="test")
|
||||
],
|
||||
)
|
||||
)
|
||||
self.initialized(InitializedParams())
|
||||
self.init_result = result
|
||||
return result
|
||||
|
||||
def open(self, filepath: Path, version: int = 0) -> tuple[str, str]:
|
||||
"""Open a text document and return (uri, content)."""
|
||||
# Read in binary mode to preserve CRLF on Windows, matching real LSP clients.
|
||||
content = filepath.read_bytes().decode("utf-8")
|
||||
uri = filepath.as_uri()
|
||||
self.text_document_did_open(
|
||||
DidOpenTextDocumentParams(
|
||||
text_document=TextDocumentItem(
|
||||
uri=uri, language_id="cpp", version=version, text=content
|
||||
)
|
||||
)
|
||||
)
|
||||
return uri, content
|
||||
|
||||
async def wait_diagnostics(self, uri: str, timeout: float = 30.0) -> None:
|
||||
"""Wait for diagnostics on the given URI."""
|
||||
if uri in self.diagnostics:
|
||||
return
|
||||
event = self.wait_for_diagnostics(uri)
|
||||
if uri in self.diagnostics:
|
||||
return
|
||||
await asyncio.wait_for(event.wait(), timeout=timeout)
|
||||
|
||||
async def open_and_wait(
|
||||
self, filepath: Path, timeout: float = 60.0
|
||||
) -> tuple[str, str]:
|
||||
"""Open a file and wait for compilation diagnostics."""
|
||||
uri = filepath.as_uri()
|
||||
event = self.wait_for_diagnostics(uri)
|
||||
_, content = self.open(filepath)
|
||||
await asyncio.wait_for(event.wait(), timeout=timeout)
|
||||
return uri, content
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def executable(request: pytest.FixtureRequest) -> Path:
|
||||
exe = request.config.getoption("--executable")
|
||||
@@ -165,51 +62,10 @@ def executable(request: pytest.FixtureRequest) -> Path:
|
||||
def test_data_dir() -> Path:
|
||||
path = Path(__file__).parent / "data"
|
||||
data_dir = path.resolve()
|
||||
|
||||
# Generate compile_commands.json for hello_world
|
||||
hw_dir = data_dir / "hello_world"
|
||||
main_cpp = hw_dir / "main.cpp"
|
||||
cdb_path = hw_dir / "compile_commands.json"
|
||||
if main_cpp.exists() and not cdb_path.exists():
|
||||
cdb = [
|
||||
{
|
||||
"directory": hw_dir.as_posix(),
|
||||
"file": main_cpp.as_posix(),
|
||||
"arguments": [
|
||||
"clang++",
|
||||
"-std=c++17",
|
||||
"-fsyntax-only",
|
||||
main_cpp.as_posix(),
|
||||
],
|
||||
}
|
||||
]
|
||||
cdb_path.write_text(json.dumps(cdb, indent=2))
|
||||
|
||||
_generate_test_data_cdbs(data_dir)
|
||||
return data_dir
|
||||
|
||||
|
||||
def generate_cdb(workspace: Path) -> None:
|
||||
"""Generate compile_commands.json using CMake with Ninja backend."""
|
||||
cmake = shutil.which("cmake")
|
||||
if cmake is None:
|
||||
raise RuntimeError("cmake executable not found in PATH")
|
||||
toolchain = Path(__file__).resolve().parent.parent / "cmake" / "toolchain.cmake"
|
||||
cmd = [
|
||||
cmake,
|
||||
"-G",
|
||||
"Ninja",
|
||||
"-DCMAKE_EXPORT_COMPILE_COMMANDS=ON",
|
||||
f"-DCMAKE_TOOLCHAIN_FILE={toolchain}",
|
||||
"-S",
|
||||
str(workspace),
|
||||
"-B",
|
||||
str(workspace / "build"),
|
||||
]
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"cmake failed:\n{result.stderr}")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def workspace(request: pytest.FixtureRequest, test_data_dir: Path) -> Path | None:
|
||||
"""Resolve workspace path from @pytest.mark.workspace("subdir") marker.
|
||||
@@ -257,7 +113,41 @@ async def client(
|
||||
|
||||
yield c
|
||||
|
||||
# Graceful shutdown
|
||||
await _shutdown_client(c)
|
||||
|
||||
|
||||
def generate_cdb(workspace: Path) -> None:
|
||||
"""Generate compile_commands.json using CMake with Ninja backend."""
|
||||
cmake = shutil.which("cmake")
|
||||
if cmake is None:
|
||||
raise RuntimeError("cmake executable not found in PATH")
|
||||
toolchain = Path(__file__).resolve().parent.parent / "cmake" / "toolchain.cmake"
|
||||
cmd = [
|
||||
cmake,
|
||||
"-G",
|
||||
"Ninja",
|
||||
"-DCMAKE_EXPORT_COMPILE_COMMANDS=ON",
|
||||
f"-DCMAKE_TOOLCHAIN_FILE={toolchain}",
|
||||
"-S",
|
||||
str(workspace),
|
||||
"-B",
|
||||
str(workspace / "build"),
|
||||
]
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"cmake failed:\n{result.stderr}")
|
||||
|
||||
|
||||
async def make_client(executable: Path, workspace: Path) -> CliceClient:
|
||||
"""Spawn a fresh clice server and initialize it. For multi-session tests."""
|
||||
c = CliceClient()
|
||||
await c.start_io(str(executable), "--mode", "pipe")
|
||||
await c.initialize(workspace)
|
||||
return c
|
||||
|
||||
|
||||
async def _shutdown_client(c: CliceClient) -> None:
|
||||
"""Gracefully shut down a client, force-kill if needed."""
|
||||
try:
|
||||
await asyncio.wait_for(c.shutdown_async(None), timeout=3.0)
|
||||
except Exception:
|
||||
@@ -267,12 +157,10 @@ async def client(
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Wait briefly, then force-kill if still running
|
||||
await asyncio.sleep(0.3)
|
||||
if hasattr(c, "_server") and c._server is not None and c._server.returncode is None:
|
||||
c._server.kill()
|
||||
|
||||
# Dump server stderr warnings for diagnostics.
|
||||
try:
|
||||
server = getattr(c, "_server", None)
|
||||
if server and server.stderr:
|
||||
@@ -284,7 +172,6 @@ async def client(
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Stop pygls client (with timeout to avoid hanging)
|
||||
try:
|
||||
c._stop_event.set()
|
||||
for task in c._async_tasks:
|
||||
@@ -292,3 +179,73 @@ async def client(
|
||||
await asyncio.sleep(0.1)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
shutdown_client = _shutdown_client # Public alias for multi-session tests
|
||||
|
||||
|
||||
def _generate_test_data_cdbs(data_dir: Path) -> None:
|
||||
"""Generate compile_commands.json for all static test data directories."""
|
||||
|
||||
def _write(directory: Path, entries: list[dict]) -> None:
|
||||
(directory / "compile_commands.json").write_text(json.dumps(entries, indent=2))
|
||||
|
||||
def _entry(directory: Path, source: Path, extra_args: list[str] | None = None):
|
||||
args = ["clang++", "-std=c++17", "-fsyntax-only"]
|
||||
if extra_args:
|
||||
args.extend(extra_args)
|
||||
args.append(source.as_posix())
|
||||
return {
|
||||
"directory": directory.as_posix(),
|
||||
"file": source.as_posix(),
|
||||
"arguments": args,
|
||||
}
|
||||
|
||||
# hello_world
|
||||
hw_dir = data_dir / "hello_world"
|
||||
hw_main = hw_dir / "main.cpp"
|
||||
if hw_main.exists():
|
||||
_write(hw_dir, [_entry(hw_dir, hw_main)])
|
||||
|
||||
# header_context (always regenerate — absolute paths)
|
||||
hc_dir = data_dir / "header_context"
|
||||
hc_main = hc_dir / "main.cpp"
|
||||
if hc_main.exists():
|
||||
_write(hc_dir, [_entry(hc_dir, hc_main, [f"-I{hc_dir.as_posix()}"])])
|
||||
|
||||
# multi_context (same file, two configs)
|
||||
mc_dir = data_dir / "multi_context"
|
||||
mc_main = mc_dir / "main.cpp"
|
||||
if mc_main.exists():
|
||||
_write(
|
||||
mc_dir,
|
||||
[
|
||||
_entry(mc_dir, mc_main, ["-DCONFIG_A"]),
|
||||
_entry(mc_dir, mc_main, ["-DCONFIG_B"]),
|
||||
],
|
||||
)
|
||||
|
||||
# include_completion
|
||||
ic_dir = data_dir / "include_completion"
|
||||
ic_main = ic_dir / "main.cpp"
|
||||
if ic_main.exists():
|
||||
_write(ic_dir, [_entry(ic_dir, ic_main, ["-I."])])
|
||||
|
||||
# document_links
|
||||
dl_dir = data_dir / "document_links"
|
||||
dl_main = dl_dir / "main.cpp"
|
||||
if dl_main.exists():
|
||||
_write(
|
||||
dl_dir, [_entry(dl_dir, dl_main, [f"-I{dl_dir.as_posix()}", "-std=c++23"])]
|
||||
)
|
||||
|
||||
# pch_test
|
||||
pt_dir = data_dir / "pch_test"
|
||||
if pt_dir.exists():
|
||||
entries = []
|
||||
for src_name in ["main.cpp", "no_includes.cpp"]:
|
||||
src = pt_dir / src_name
|
||||
if src.exists():
|
||||
entries.append(_entry(pt_dir, src))
|
||||
if entries:
|
||||
_write(pt_dir, entries)
|
||||
|
||||
1
tests/data/document_links/data.bin
Normal file
1
tests/data/document_links/data.bin
Normal file
@@ -0,0 +1 @@
|
||||
0123456789
|
||||
3
tests/data/document_links/header_a.h
Normal file
3
tests/data/document_links/header_a.h
Normal file
@@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
int a = 1;
|
||||
3
tests/data/document_links/header_b.h
Normal file
3
tests/data/document_links/header_b.h
Normal file
@@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
int b = 2;
|
||||
3
tests/data/document_links/header_c.h
Normal file
3
tests/data/document_links/header_c.h
Normal file
@@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
int c = 3;
|
||||
20
tests/data/document_links/main.cpp
Normal file
20
tests/data/document_links/main.cpp
Normal file
@@ -0,0 +1,20 @@
|
||||
#include "header_a.h"
|
||||
#include "header_b.h"
|
||||
int x = 1;
|
||||
#include "header_c.h"
|
||||
|
||||
const char data[] = {
|
||||
#embed "data.bin"
|
||||
};
|
||||
|
||||
#if __has_embed("data.bin")
|
||||
int has_embed_found = 1;
|
||||
#endif
|
||||
|
||||
#if __has_embed("no_such_file.bin")
|
||||
int has_embed_not_found = 1;
|
||||
#endif
|
||||
|
||||
int main() {
|
||||
return a + b + c;
|
||||
}
|
||||
6
tests/data/header_context/inner.h
Normal file
6
tests/data/header_context/inner.h
Normal file
@@ -0,0 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
// Non self-contained: uses Point from the include chain.
|
||||
inline Point inner_origin() {
|
||||
return Point{0, 0};
|
||||
}
|
||||
8
tests/data/header_context/main.cpp
Normal file
8
tests/data/header_context/main.cpp
Normal file
@@ -0,0 +1,8 @@
|
||||
#include "types.h"
|
||||
#include "utils.h"
|
||||
|
||||
int main() {
|
||||
Point p{3, 4};
|
||||
int d = calc(p);
|
||||
return d;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user