Compare commits
1 Commits
feat/docum
...
openspec-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eddb34e34e |
50
.clang-tidy
50
.clang-tidy
@@ -1,50 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,268 +0,0 @@
|
||||
# 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&`.
|
||||
@@ -1,15 +0,0 @@
|
||||
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
|
||||
@@ -1,5 +0,0 @@
|
||||
Format all project source files.
|
||||
|
||||
Run: `pixi run format`
|
||||
|
||||
Formats C++, Python, Lua, JS/TS, Markdown, JSON, TOML, and YAML files.
|
||||
@@ -1,19 +0,0 @@
|
||||
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,7 +1,2 @@
|
||||
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,7 +1,8 @@
|
||||
name: benchmark
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
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,31 +46,13 @@ jobs:
|
||||
- 'tests/**'
|
||||
- 'config/**'
|
||||
- '.github/workflows/test-cmake.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
|
||||
xmake:
|
||||
- 'xmake.lua'
|
||||
- 'src/**'
|
||||
- 'include/**'
|
||||
- 'tests/**'
|
||||
- 'config/**'
|
||||
- '.github/workflows/test-xmake.yml'
|
||||
|
||||
format:
|
||||
needs: changes
|
||||
@@ -100,6 +82,11 @@ 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
|
||||
@@ -117,17 +104,16 @@ 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: conventional-commit,format,deploy,clice,vscode,cmake
|
||||
allowed-skips: format,deploy,clice,vscode,cmake,xmake
|
||||
jobs: ${{ toJSON(needs) }}
|
||||
|
||||
26
.github/workflows/publish-clice.yml
vendored
26
.github/workflows/publish-clice.yml
vendored
@@ -29,18 +29,32 @@ 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
|
||||
|
||||
@@ -49,7 +63,7 @@ jobs:
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: build/RelWithDebInfo/${{ matrix.artifact_name }}
|
||||
file: build/xpack/clice/${{ matrix.artifact_name }}
|
||||
asset_name: ${{ matrix.asset_name }}
|
||||
tag: ${{ github.ref }}
|
||||
overwrite: true
|
||||
@@ -59,7 +73,7 @@ jobs:
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: build/RelWithDebInfo/${{ matrix.symbol_artifact_name }}
|
||||
file: build/xpack/clice/${{ matrix.symbol_artifact_name }}
|
||||
asset_name: ${{ matrix.symbol_asset_name }}
|
||||
tag: ${{ github.ref }}
|
||||
overwrite: true
|
||||
|
||||
42
.github/workflows/test-xmake.yml
vendored
Normal file
42
.github/workflows/test-xmake.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
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,11 +56,10 @@ __pycache__/
|
||||
tests/unit/Local/
|
||||
|
||||
# IDEs & Editors
|
||||
/.vscode/*
|
||||
!/.vscode/launch.json
|
||||
!/.vscode/tasks.json
|
||||
/.vscode/
|
||||
.vs/
|
||||
.idea/
|
||||
.claude
|
||||
.clangd
|
||||
|
||||
# pixi environments
|
||||
@@ -69,7 +68,5 @@ tests/unit/Local/
|
||||
!.pixi/config.toml
|
||||
|
||||
.codex/
|
||||
.claude/*
|
||||
!.claude/CLAUDE.md
|
||||
!.claude/commands/
|
||||
openspec/
|
||||
.claude/
|
||||
openspec/
|
||||
83
.vscode/launch.json
vendored
83
.vscode/launch.json
vendored
@@ -1,83 +0,0 @@
|
||||
{
|
||||
"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
42
.vscode/tasks.json
vendored
@@ -1,42 +0,0 @@
|
||||
{
|
||||
"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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -101,15 +101,8 @@ function(setup_llvm LLVM_VERSION)
|
||||
clangToolingSyntax
|
||||
)
|
||||
else()
|
||||
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})
|
||||
file(GLOB LLVM_LIBRARIES CONFIGURE_DEPENDS "${LLVM_INSTALL_PATH}/lib/*${CMAKE_STATIC_LIBRARY_SUFFIX}")
|
||||
target_link_libraries(llvm-libs INTERFACE ${LLVM_LIBRARIES})
|
||||
target_compile_definitions(llvm-libs INTERFACE CLANG_BUILD_STATIC=1)
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
@@ -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/logs"
|
||||
logging_dir = "${workspace}/.clice/logging"
|
||||
# Compile commands files or directories to search for compile_commands.json files.
|
||||
compile_commands_paths = ["${workspace}/build"]
|
||||
|
||||
|
||||
@@ -32,6 +32,18 @@ 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`.
|
||||
@@ -58,13 +70,30 @@ 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 downloads 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 and xmake download these LLVM libs by default.
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
|
||||
@@ -18,6 +18,13 @@ 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,73 +54,14 @@ bazel run @hedron_compile_commands//:refresh_all
|
||||
|
||||
### Visual Studio
|
||||
|
||||
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.
|
||||
TODO:
|
||||
|
||||
### Makefile
|
||||
|
||||
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.
|
||||
TODO:
|
||||
|
||||
### 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 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.
|
||||
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.
|
||||
|
||||
@@ -32,6 +32,18 @@ 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 中定义的版本要求。
|
||||
@@ -58,13 +70,30 @@ 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 默认会从此处下载 LLVM 库然后使用。
|
||||
1. 我们在 [clice-llvm](https://github.com/clice-io/clice-llvm/releases) 上会发布使用的 LLVM 版本的预编译二进制,用于 CI 或者 release 构建。在构建时 cmake 和 xmake 默认会从此处下载 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,6 +18,13 @@ $ ./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,73 +54,14 @@ bazel run @hedron_compile_commands//:refresh_all
|
||||
|
||||
### Visual Studio
|
||||
|
||||
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) 来生成编译数据库。
|
||||
TODO:
|
||||
|
||||
### Makefile
|
||||
|
||||
对于基于 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` 目录下。
|
||||
TODO:
|
||||
|
||||
### 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
|
||||
|
||||
对于任意其它的构建系统,可以使用 [catter](https://github.com/clice-io/catter) 来生成编译数据库。它通过伪装编译器的方式来捕获编译命令,能够可靠地与任何调用编译器可执行文件的构建系统配合工作。
|
||||
对于任意其它的构建系统,可以尝试使用 [bear](https://github.com/rizsotto/Bear) 或者 [scan-build](https://github.com/rizsotto/scan-build) 来拦截编译命令并获取到编译数据库(不保证成功)。我们计划在未来编写一个**新的工具**,通过假编译器的方式来实现编译命令的捕获。
|
||||
|
||||
9
editors/vscode/.vscode/extensions.json
vendored
Normal file
9
editors/vscode/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
// 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
Normal file
23
editors/vscode/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"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
Normal file
13
editors/vscode/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
// 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
Normal file
37
editors/vscode/.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
// 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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-04-03
|
||||
145
openspec/changes/assess-clice-production-readiness/design.md
Normal file
145
openspec/changes/assess-clice-production-readiness/design.md
Normal file
@@ -0,0 +1,145 @@
|
||||
## Context
|
||||
|
||||
`clice` already has the rough shape of a language server: a master process, stateful/stateless workers, compile scheduling, module/PCH support, an emerging index, and a set of AST-backed editor features. The current gap is not "missing framework", but a mismatch between the current implementation shape and the invariants required for real editor use.
|
||||
|
||||
The highest-risk issues are all cross-cutting:
|
||||
|
||||
- opened editor buffers are not yet the consistently authoritative source across compile, AST queries, and index-backed navigation
|
||||
- stateful feature reads can race with rebuilds and stale generations
|
||||
- header files and multi-context files do not yet have a sound compilation-context selection model
|
||||
- several features are either missing, stubbed, or substantially shallower than `clangd`
|
||||
- worker ownership, eviction, diagnostics, logging, and background work scheduling are not yet production-grade
|
||||
|
||||
This design therefore treats production readiness as four coordinated workstreams with explicit boundaries: document model, compilation context, AST-only feature baseline, and runtime hardening.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- make opened-buffer semantics explicit and enforceable across compile and query paths
|
||||
- define how `clice` selects and persists compilation contexts for sources and headers
|
||||
- set a concrete AST-only feature baseline that can be implemented independently from global index work
|
||||
- define runtime invariants for worker lifecycle, resource control, logging, and stale-result handling
|
||||
- produce a task breakdown that allows server/core work and feature work to proceed in parallel with minimal overlap
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- full global-index parity with `clangd`
|
||||
- final implementation details for every indexed feature such as workspace-wide rename or workspace symbols
|
||||
- performance tuning beyond the invariants needed to avoid obviously wrong scheduling and stale results
|
||||
- introducing compatibility layers or temporary abstractions whose only purpose is to avoid refactoring existing wrong-shape code
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. Treat the opened document as the primary source of truth
|
||||
|
||||
Open documents with unsaved changes must override disk content for compile and AST-backed queries. This applies to on-demand compiles, stateful feature reads, and the local side of navigation requests.
|
||||
|
||||
Why this over disk-first fallback:
|
||||
|
||||
- it matches editor expectations
|
||||
- it removes offset mismatches between the text used to compute positions and the AST used to answer them
|
||||
- it gives a single invariant that can be tested across hover, completion, semantic tokens, and local navigation
|
||||
|
||||
Alternative considered:
|
||||
|
||||
- continue using disk-backed index and best-effort stale tolerance
|
||||
Rejected because it produces silently wrong answers in the most common editing flow.
|
||||
|
||||
### 2. Gate AST-backed feature reads on a versioned build generation
|
||||
|
||||
Every opened document needs a version/generation model that ties together text, build scheduling, worker state, AST readiness, diagnostics publication, and feature requests. AST-backed reads must either use a generation known to match the current document or fail/retry cleanly; they must not run on a stale or fatal unit by default.
|
||||
|
||||
Why this over the current dirty-flag shape:
|
||||
|
||||
- `dirty` alone records staleness but does not protect the read path
|
||||
- version/generation checks let master and worker reject stale compile completions and stale feature answers deterministically
|
||||
|
||||
Alternative considered:
|
||||
|
||||
- optimistic reads while a rebuild is in flight
|
||||
Rejected because it preserves responsiveness at the cost of correctness and makes debugging behavior impossible.
|
||||
|
||||
### 3. Separate compilation-context resolution from feature implementation
|
||||
|
||||
Header context, compilation context, and multi-command source handling must be solved in the compile layer, not feature-by-feature. Feature handlers should receive a coherent AST/index view and not carry their own header fallback logic.
|
||||
|
||||
Why this over ad hoc per-feature fallback:
|
||||
|
||||
- it prevents each feature from inventing a different answer for the same file
|
||||
- it keeps AST-only feature work independently assignable
|
||||
- it matches the way `clangd` centralizes command transfer and TU scheduling
|
||||
|
||||
Alternative considered:
|
||||
|
||||
- keeping generic fallback compile commands for headers until later
|
||||
Rejected because it creates misleading "working" behavior with the wrong language mode and include environment.
|
||||
|
||||
### 4. Keep AST-only feature work independent from index work
|
||||
|
||||
The feature baseline for this change covers capabilities that can work from the current AST or local parse context. Missing index-backed features may be tracked, but they are not on the critical path for this change.
|
||||
|
||||
Why this split:
|
||||
|
||||
- it allows server/core work to proceed without blocking feature contributors
|
||||
- it matches the current repository structure, where most AST features already live under `src/feature`
|
||||
- it gives a realistic route to internal dogfooding before full index maturity
|
||||
|
||||
Alternative considered:
|
||||
|
||||
- waiting for index parity before improving features
|
||||
Rejected because it serializes unrelated work and delays useful editor quality improvements.
|
||||
|
||||
### 5. Remove or finish wrong-shape/stub behavior instead of hiding it
|
||||
|
||||
Capabilities that are currently advertised but stubbed, or data structures that exist without a correct runtime selection model, should either be finished or explicitly de-scoped. Production readiness must reduce misleading behavior, not preserve it.
|
||||
|
||||
Why this over compatibility shims:
|
||||
|
||||
- stubs make clients believe a feature exists when it does not
|
||||
- partial lifecycle plumbing causes leaks and stale ownership even if the server appears responsive
|
||||
- cleaning up wrong-shape code reduces the long-term cost of later index and context work
|
||||
|
||||
Alternative considered:
|
||||
|
||||
- preserve current surface and patch around it incrementally
|
||||
Rejected because it accumulates technical debt exactly in the areas that most need stable invariants.
|
||||
|
||||
### 6. Use one research document as the coordination artifact, then execute by small work packages
|
||||
|
||||
This change produces a detailed markdown report under `temp/` that groups findings into server/core and feature workstreams, then translates them into small, independently assignable tasks. The report is not the contract itself; the specs are. The report exists to preserve comparison context and justify prioritization.
|
||||
|
||||
Why this over burying all detail in tasks:
|
||||
|
||||
- the comparison against local `clangd` sources is too detailed to keep only in task bullets
|
||||
- the report gives future implementation agents a single place to understand gaps and evidence
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [Stricter generation gating may temporarily increase null/error responses] -> Mitigation: prefer explicit stale-result rejection over silently wrong data, and add targeted tests for retry behavior.
|
||||
- [Header-context design can expand into full index work] -> Mitigation: keep this change focused on command/context selection and defer cross-project index heuristics that are not needed for production dogfooding.
|
||||
- [Feature parity work may accidentally duplicate already-completed behavior] -> Mitigation: each feature task must confirm current implementation state before coding, using the gap-analysis report and local source references.
|
||||
- [Cleaning up wrong-shape code may require deleting partially integrated paths] -> Mitigation: treat deletion as part of completion criteria rather than as optional follow-up.
|
||||
- [Parallel execution can produce merge conflicts around `master_server.cpp` and protocol definitions] -> Mitigation: split tasks by ownership boundary, with server/core work owning protocol and lifecycle changes, and feature work owning only their feature modules plus the minimal routing needed for exposure.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. Land the planning artifacts and the gap-analysis document.
|
||||
2. Implement server/core invariants first where feature correctness depends on them:
|
||||
open-buffer source of truth, generation-safe AST reads, header/context selection, lifecycle cleanup.
|
||||
3. In parallel, implement AST-only feature tasks that do not depend on index maturity.
|
||||
4. Remove or hide stubbed feature advertisements that remain incomplete.
|
||||
5. Expand test coverage around document lifecycle, stale-result rejection, header handling, and feature-level golden cases.
|
||||
6. Start internal dogfooding only after the server/core acceptance criteria and the minimum AST-only baseline are both satisfied.
|
||||
|
||||
Rollback strategy:
|
||||
|
||||
- because this change is primarily a planning and refactoring contract, rollback means disabling newly advertised features or reverting individual implementation tasks rather than reverting the entire capability set at once
|
||||
|
||||
## Open Questions
|
||||
|
||||
- How should `clice` choose among multiple valid compilation contexts for the same source file before the full index is mature?
|
||||
- For headers without a direct compile command, what ranking rule should choose the preferred includer or proxy translation unit?
|
||||
- Should AST-backed requests block until a matching generation is ready, or return an explicit retriable error after a short timeout?
|
||||
- Which AST-only code actions are mandatory for first internal rollout, and which can remain behind capability flags?
|
||||
- How should dynamic, unsaved-document symbol data be combined with disk-backed merged index results without reintroducing offset drift?
|
||||
@@ -0,0 +1,32 @@
|
||||
## Why
|
||||
|
||||
`clice` has reached the stage where the core server loop starts, responds, and already exposes a meaningful set of AST-backed language features. However, it is not yet safe to treat the server as production-usable or even stable for internal dogfooding, because open-buffer semantics, compilation-context handling, stale AST/index reads, worker lifecycle, and several AST-only editor features still diverge from the behavior expected from a real C++ language server.
|
||||
|
||||
The project also lacks a single implementation contract that separates core server/compiler work from independently assignable feature work. A focused production-readiness change is needed now so server refactoring, feature parity work, and documentation updates can proceed in parallel without mixing concerns.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Define a production-readiness plan for `clice` that separates core server/compiler obligations from AST-only feature work.
|
||||
- Specify correct open-buffer versus on-disk behavior for compile, query, and navigation flows.
|
||||
- Specify compilation-context and header-context behavior required for headers and multi-context source files.
|
||||
- Specify the minimum AST-only feature baseline needed before internal rollout, including feature parity gaps against `clangd` where global index is not required.
|
||||
- Specify server hardening requirements for worker lifecycle, stale-result rejection, observability, resource control, and error handling.
|
||||
- Produce a detailed gap-analysis document under `./temp` that compares current `clice` behavior against local `clangd` sources and turns the findings into implementation workstreams.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `buffer-aware-document-model`: Define the source-of-truth rules for opened documents, unsaved edits, build generations, and request consistency.
|
||||
- `compilation-context-management`: Define how `clice` chooses and applies compilation contexts for source files and headers, including header context and compilation context behavior.
|
||||
- `ast-feature-baseline`: Define the AST-only feature set and quality baseline required before production dogfooding, including missing or incomplete `clangd`-style features.
|
||||
- `server-production-hardening`: Define the runtime, worker, indexing, logging, and lifecycle guarantees required for reliable day-to-day use.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
None.
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected code spans `src/server`, `src/command`, `src/compile`, `src/index`, `src/feature`, and related tests under `tests/unit` and `tests/integration`.
|
||||
- The change establishes the implementation contract for upcoming refactors to document state, compile scheduling, header/context selection, worker ownership/eviction, and AST-only LSP handlers.
|
||||
- Documentation output will include OpenSpec artifacts under `openspec/changes/assess-clice-production-readiness/` and a detailed analysis document under `temp/`.
|
||||
@@ -0,0 +1,44 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Production rollout includes a minimum AST-only feature baseline
|
||||
Before internal production dogfooding, `clice` SHALL provide a stable AST-only feature baseline consisting of hover, completion, signature help, document symbols, document links, folding ranges, semantic tokens, inlay hints, formatting, diagnostics, document highlight, selection range, and AST-backed code actions that do not require a global index.
|
||||
|
||||
#### Scenario: Missing AST-only feature is visible in capability surface
|
||||
- **WHEN** an AST-only feature is not implemented to the agreed baseline
|
||||
- **THEN** `clice` does not advertise it as available
|
||||
|
||||
#### Scenario: Implemented AST-only feature is routed end-to-end
|
||||
- **WHEN** an AST-only feature reaches the agreed baseline
|
||||
- **THEN** `clice` exposes it through LSP capability advertisement, request routing, and worker execution
|
||||
|
||||
### Requirement: Hover provides semantic detail beyond symbol name
|
||||
Hover responses SHALL include semantic information that is useful for real editing, such as declaration spelling, type information, or associated documentation when available.
|
||||
|
||||
#### Scenario: Hover on function shows semantic information
|
||||
- **WHEN** a user hovers a function or method symbol
|
||||
- **THEN** the hover response includes more than a bare symbol kind and name
|
||||
|
||||
### Requirement: Completion and signature help expose actionable assistance
|
||||
Completion and signature-help responses SHALL provide parameter-aware assistance that is suitable for day-to-day editing, including snippets or argument guidance when enabled by server options.
|
||||
|
||||
#### Scenario: Completion inserts parameter-aware text
|
||||
- **WHEN** snippet-style completion support is enabled and a callable symbol is completed
|
||||
- **THEN** the completion response includes parameter-aware insertion behavior
|
||||
|
||||
#### Scenario: Signature help identifies active parameter
|
||||
- **WHEN** a user requests signature help inside a callable argument list
|
||||
- **THEN** the response identifies the active parameter for the current cursor position
|
||||
|
||||
### Requirement: Inlay hint computation respects requested scope
|
||||
Inlay hints SHALL be computed for the requested document scope rather than always forcing a full-document computation.
|
||||
|
||||
#### Scenario: Range request limits inlay hint computation
|
||||
- **WHEN** a client requests inlay hints for a document range
|
||||
- **THEN** `clice` computes and returns hints for that range instead of scanning the entire file
|
||||
|
||||
### Requirement: AST-only code actions are either implemented or hidden
|
||||
If an AST-only code action is advertised by `clice`, it SHALL produce a meaningful result instead of an unconditional empty placeholder response.
|
||||
|
||||
#### Scenario: Advertised code action is not a stub
|
||||
- **WHEN** a client requests code actions for a context where an advertised AST-only code action applies
|
||||
- **THEN** `clice` returns a meaningful action or omits the capability from advertisement
|
||||
@@ -0,0 +1,41 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Opened buffer content is authoritative
|
||||
When a document is opened in the editor, `clice` SHALL treat the opened buffer content as the source of truth for compile inputs and AST-backed queries until the document is closed.
|
||||
|
||||
#### Scenario: Hover uses unsaved content
|
||||
- **WHEN** a user edits an opened file without saving and requests hover in that file
|
||||
- **THEN** `clice` uses the opened buffer content rather than the on-disk file content to answer the request
|
||||
|
||||
#### Scenario: Completion uses unsaved content
|
||||
- **WHEN** a user edits an opened file without saving and requests completion in that file
|
||||
- **THEN** `clice` builds completion context from the opened buffer content rather than the on-disk file content
|
||||
|
||||
### Requirement: AST-backed requests use a matching document generation
|
||||
`clice` SHALL answer AST-backed requests only from an AST generation that matches the current opened-document version, or otherwise reject/defer the request in a defined way.
|
||||
|
||||
#### Scenario: Stale AST result is discarded
|
||||
- **WHEN** a rebuild completes for an older document version after a newer edit has already been accepted
|
||||
- **THEN** `clice` discards the stale AST result and does not publish it as the current answer state
|
||||
|
||||
#### Scenario: Feature request does not read stale AST
|
||||
- **WHEN** a feature request arrives while the current document version has not yet produced a matching AST
|
||||
- **THEN** `clice` does not answer from an older AST generation
|
||||
|
||||
### Requirement: Active-file navigation starts from live document state
|
||||
For an opened file, `clice` SHALL resolve the source-side symbol and position mapping for navigation requests from the current document state before using disk-backed index data for remote results.
|
||||
|
||||
#### Scenario: Local definition lookup uses live offsets
|
||||
- **WHEN** a user makes unsaved edits that shift symbol offsets and requests definition from the opened file
|
||||
- **THEN** `clice` resolves the queried symbol from the current opened document state instead of interpreting the request against stale disk offsets
|
||||
|
||||
### Requirement: Closing a document ends opened-buffer authority
|
||||
When an opened document is closed, `clice` SHALL stop treating the editor buffer as authoritative and SHALL release associated opened-document runtime state.
|
||||
|
||||
#### Scenario: Closed document falls back to disk state
|
||||
- **WHEN** a document is closed and then queried again without reopening
|
||||
- **THEN** `clice` uses persisted project state rather than the former unsaved editor buffer
|
||||
|
||||
#### Scenario: Close releases owned runtime state
|
||||
- **WHEN** a document is closed
|
||||
- **THEN** `clice` releases worker ownership and opened-document runtime state associated with that document
|
||||
@@ -0,0 +1,41 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Header files use a real compilation context
|
||||
When a header file lacks a direct compile command, `clice` SHALL derive its compilation context from a valid project context rather than falling back to a generic language-default command.
|
||||
|
||||
#### Scenario: Opened header borrows project context
|
||||
- **WHEN** a user opens a header that is included by one or more project translation units but has no direct compile command entry
|
||||
- **THEN** `clice` selects a valid project-derived compilation context for that header
|
||||
|
||||
#### Scenario: No generic fallback for project header
|
||||
- **WHEN** a project header has at least one valid project-derived compilation context
|
||||
- **THEN** `clice` does not prefer a generic fallback command over the project-derived context
|
||||
|
||||
### Requirement: Header context and compilation context remain distinct runtime concepts
|
||||
`clice` SHALL distinguish header-context behavior from source-file compilation-context behavior in its runtime model and queries.
|
||||
|
||||
#### Scenario: Header query selects header context
|
||||
- **WHEN** a file is analyzed as a header included through a source file context
|
||||
- **THEN** `clice` uses header-context selection rules instead of treating the file as an ordinary standalone translation unit
|
||||
|
||||
#### Scenario: Source query selects compilation context
|
||||
- **WHEN** a file is analyzed as a directly compiled source file
|
||||
- **THEN** `clice` uses compilation-context selection rules appropriate to that source file
|
||||
|
||||
### Requirement: Context-sensitive data is queried with an explicit active context
|
||||
If multiple valid contexts exist for the same path, `clice` SHALL retain enough context identity to select the active one during query execution.
|
||||
|
||||
#### Scenario: Multi-context source file remains distinguishable
|
||||
- **WHEN** the same source file is compiled under multiple valid compile commands
|
||||
- **THEN** `clice` preserves distinguishable context identities instead of collapsing them into one ambiguous runtime state
|
||||
|
||||
#### Scenario: Context-sensitive lookup does not union incompatible states
|
||||
- **WHEN** a query targets a file whose symbols differ by context
|
||||
- **THEN** `clice` does not answer by blindly unioning incompatible context-specific results
|
||||
|
||||
### Requirement: Missing context is surfaced explicitly
|
||||
If `clice` cannot determine a valid compilation context for a file, it SHALL surface the degraded state explicitly through diagnostics or logs rather than silently pretending the file has a correct context.
|
||||
|
||||
#### Scenario: Missing header context is observable
|
||||
- **WHEN** `clice` cannot derive a usable context for an opened header
|
||||
- **THEN** the failure is visible through diagnostics, logs, or both
|
||||
@@ -0,0 +1,33 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Worker lifecycle changes are reflected in runtime ownership
|
||||
`clice` SHALL keep worker ownership, opened-document tracking, and eviction state consistent across open, change, save, close, and worker-failure events.
|
||||
|
||||
#### Scenario: Closing a document releases worker ownership
|
||||
- **WHEN** a document is closed
|
||||
- **THEN** `clice` removes the document from worker ownership tracking and allows its runtime state to be evicted
|
||||
|
||||
#### Scenario: Worker failure is surfaced
|
||||
- **WHEN** a worker crashes, times out, or fails IPC
|
||||
- **THEN** `clice` records the failure and surfaces a defined error outcome instead of silently pretending the request succeeded
|
||||
|
||||
### Requirement: Runtime resource controls are enforced rather than decorative
|
||||
Configured runtime limits for worker memory and background work SHALL affect actual scheduling or eviction behavior.
|
||||
|
||||
#### Scenario: Memory limit influences eviction
|
||||
- **WHEN** worker memory pressure exceeds the configured limit
|
||||
- **THEN** `clice` uses the configured limit to drive eviction or rejection behavior
|
||||
|
||||
### Requirement: Background work does not outrank active editing
|
||||
`clice` SHALL schedule compile and indexing work so that opened-document responsiveness takes priority over background throughput.
|
||||
|
||||
#### Scenario: Open document work outranks background indexing
|
||||
- **WHEN** a user edits an opened document while background indexing is pending
|
||||
- **THEN** `clice` prioritizes the opened-document work ahead of non-urgent background indexing
|
||||
|
||||
### Requirement: Production debugging information is observable
|
||||
`clice` SHALL emit enough logging and trace information to diagnose compile-context selection, worker failures, and request/response mismatches in production-style usage.
|
||||
|
||||
#### Scenario: Compile failure includes useful context
|
||||
- **WHEN** a compile, header-context selection, or worker request fails
|
||||
- **THEN** logs include enough context to identify the affected file, command/context, and failure site
|
||||
49
openspec/changes/assess-clice-production-readiness/tasks.md
Normal file
49
openspec/changes/assess-clice-production-readiness/tasks.md
Normal file
@@ -0,0 +1,49 @@
|
||||
## 1. Buffer-Aware Document Model
|
||||
|
||||
- [ ] 1.1 Introduce an explicit opened-document state model that tracks text, editor version, build generation, AST generation, and readiness state in the master/worker boundary
|
||||
- [ ] 1.2 Make `didOpen`, `didChange`, `didSave`, and `didClose` update one consistent source-of-truth flow for opened documents
|
||||
- [ ] 1.3 Replace the `ensure_compiled()` stub with generation-aware gating for AST-backed requests
|
||||
- [ ] 1.4 Reject or defer stale AST-backed feature requests instead of serving results from older generations
|
||||
- [ ] 1.5 Prevent AST-backed feature execution on fatal or otherwise unusable compilation units
|
||||
- [ ] 1.6 Add tests that cover unsaved edits, stale compile completion discard, and close-time state release
|
||||
|
||||
## 2. Compilation Context Management
|
||||
|
||||
- [ ] 2.1 Implement real project-derived compilation-context selection for headers without direct compile commands
|
||||
- [ ] 2.2 Define and persist distinct runtime identities for header contexts and source-file compilation contexts
|
||||
- [ ] 2.3 Make query paths accept an explicit active context instead of blindly unioning context-sensitive results
|
||||
- [ ] 2.4 Surface missing or degraded compilation-context selection through logs or diagnostics
|
||||
- [ ] 2.5 Add tests for header opening, project-context borrowing, and multi-context file handling
|
||||
|
||||
## 3. Server Runtime Hardening
|
||||
|
||||
- [ ] 3.1 Wire `didClose` to worker eviction and ownership cleanup so closed documents do not linger in worker memory
|
||||
- [ ] 3.2 Surface worker IPC failures, crashes, and timeouts as explicit server-side errors with useful logs
|
||||
- [ ] 3.3 Enforce configured resource controls such as worker memory limits instead of relying on hardcoded thresholds
|
||||
- [ ] 3.4 Prioritize opened-document compile work ahead of non-urgent background indexing
|
||||
- [ ] 3.5 Expand runtime logging for compile commands, context selection, worker failure causes, and request/result mismatches
|
||||
- [ ] 3.6 Add lifecycle and scheduling tests that cover worker cleanup, timeout handling, and active-edit priority
|
||||
|
||||
## 4. Live-State Navigation Correctness
|
||||
|
||||
- [ ] 4.1 Resolve source-side symbol lookup for opened files from the current AST before consulting disk-backed index data
|
||||
- [ ] 4.2 Define how active-document state is combined with index-backed remote results for definition and related navigation flows
|
||||
- [ ] 4.3 Remove or disable stubbed navigation fallback paths that cannot yet return correct results
|
||||
- [ ] 4.4 Add tests for navigation from unsaved buffers and mixed AST/index result paths
|
||||
|
||||
## 5. AST-Only Feature Baseline
|
||||
|
||||
- [ ] 5.1 Enrich hover so it returns semantic detail beyond bare symbol kind and name
|
||||
- [ ] 5.2 Add `documentHighlight` as an AST-backed feature with end-to-end capability advertisement and routing
|
||||
- [ ] 5.3 Expose `selectionRange` using the existing local selection machinery
|
||||
- [ ] 5.4 Upgrade completion and signature help with parameter-aware insertion and richer callable assistance
|
||||
- [ ] 5.5 Respect requested scope for inlay-hint computation instead of forcing full-document evaluation
|
||||
- [ ] 5.6 Implement the first production-worthy AST-only code actions or stop advertising stubbed code-action support
|
||||
- [ ] 5.7 Audit advertised AST-only capabilities and remove any remaining stub or placeholder surfaces
|
||||
- [ ] 5.8 Add or update feature-level tests and golden cases for every newly exposed AST-only feature
|
||||
|
||||
## 6. Documentation And Readiness Tracking
|
||||
|
||||
- [ ] 6.1 Write and maintain the detailed production-readiness gap analysis in `temp/`
|
||||
- [ ] 6.2 Update developer documentation for opened-buffer semantics, compilation-context handling, and feature ownership boundaries
|
||||
- [ ] 6.3 Define a dogfooding readiness checklist that combines server/core acceptance criteria with the minimum AST-only feature baseline
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-04-03
|
||||
250
openspec/changes/improve-code-completion-parity/design.md
Normal file
250
openspec/changes/improve-code-completion-parity/design.md
Normal file
@@ -0,0 +1,250 @@
|
||||
## Context
|
||||
|
||||
`clice` currently routes `textDocument/completion` through `MasterServer::forward_stateless()`, which prepares open-buffer text plus available PCH/PCM paths and sends a one-shot completion request to the stateless worker. The worker builds `CompilationParams`, invokes `feature::code_complete()`, and returns a raw `std::vector<CompletionItem>`.
|
||||
|
||||
The implementation in `src/feature/code_completion.cpp` is intentionally small:
|
||||
|
||||
- run Sema code completion at a single file/offset
|
||||
- compute the local identifier prefix
|
||||
- fuzzy-match candidate labels against the typed prefix
|
||||
- emit only basic LSP fields such as `label`, `kind`, `textEdit`, and `sortText`
|
||||
- bundle function overloads only by qualified name, with `detail = "(...)"` for grouped overloads
|
||||
|
||||
Compared with the local `clangd` implementation in `.llvm/clang-tools-extra/clangd`, the gap is large and structural rather than cosmetic. `clangd` has a full completion pipeline:
|
||||
|
||||
- request shaping from client capabilities and trigger context in `ClangdLSPServer`
|
||||
- `CodeCompleteFlow` as a staged completion engine rather than direct Sema rendering
|
||||
- candidate fusion from Sema, project index, and raw identifiers
|
||||
- context capture such as completion kind, query scopes, expected type, and following token
|
||||
- scoring that combines name match with semantic/contextual signals
|
||||
- richer LSP rendering including signatures, return types, docs, snippets, fix-its, additional edits, deprecation, `filterText`, `isIncomplete`, and include insertion
|
||||
|
||||
At the same time, `clice` has infrastructure that clangd either lacks or does not currently integrate into completion as deeply:
|
||||
|
||||
- explicit header-context modeling for headers shared by multiple includers
|
||||
- existing PCH/PCM request plumbing in the master/stateless worker path
|
||||
- merged per-file index shards and project-wide symbol/reference data
|
||||
- a project goal of instantiation-aware template processing
|
||||
|
||||
There is also a hard constraint: `clice`'s current index symbol payload only stores `name`, `kind`, and reference files. That is enough for navigation-oriented lookup, but not enough for clangd-style index completion, which needs scope, signature, return type, documentation, deprecation, canonical declaration/header ownership, and dependency insertion hints.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- close the highest-value completion gap with clangd in behavior, not in file-by-file implementation shape
|
||||
- replace the current direct Sema-to-LSP path with a staged completion pipeline
|
||||
- support rich, capability-aware LSP completion responses
|
||||
- augment Sema completion with identifier and project-index candidates
|
||||
- introduce heuristics that use semantic/contextual signals instead of raw fuzzy score alone
|
||||
- define how fix-its and dependency insertion edits attach to completion items
|
||||
- use `clice`'s header-context, module, and template machinery to define completion behaviors clangd does not currently provide
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- byte-for-byte parity with clangd internals or its exact scoring constants
|
||||
- introducing `completionItem/resolve` in this change
|
||||
- shipping a learned ranking model in the first implementation; heuristic ranking is sufficient
|
||||
- requiring the global index to become fully clangd-equivalent before completion quality improves
|
||||
- solving every completion-related editor quirk across all clients before the core pipeline exists
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. Split completion into request shaping, candidate collection, scoring, and rendering
|
||||
|
||||
`clice` should stop treating code completion as "run Sema and immediately serialize `CompletionItem`s". Instead, completion will become a staged pipeline:
|
||||
|
||||
1. request shaping at the LSP/server edge
|
||||
2. candidate collection in the worker
|
||||
3. merge/deduplicate/bundle
|
||||
4. score and truncate
|
||||
5. render to LSP according to client capabilities
|
||||
|
||||
Why this approach:
|
||||
|
||||
- it matches the actual gap with clangd, which is pipeline depth rather than one missing field
|
||||
- it keeps protocol negotiation in the server and semantic work in the worker
|
||||
- it allows `clice`-specific signals to affect ranking without polluting the LSP layer
|
||||
|
||||
Alternative considered:
|
||||
|
||||
- continue returning `std::vector<CompletionItem>` directly from `feature::code_complete()`
|
||||
Rejected because `isIncomplete`, score-aware truncation, richer origins, and delayed rendering all need an intermediate completion model.
|
||||
|
||||
### 2. Extend the completion request contract with client capabilities and trigger context
|
||||
|
||||
The current stateless completion request only carries file text, compile arguments, and offset. The request contract should be extended so the worker can render results correctly:
|
||||
|
||||
- completion trigger kind/character
|
||||
- client snippet support
|
||||
- documentation format preference
|
||||
- label-details support
|
||||
- supported completion item kinds
|
||||
- requested limit
|
||||
- overload-bundling preference
|
||||
|
||||
The master server should negotiate these from `initialize` capabilities and pass a normalized completion-options struct to the worker.
|
||||
|
||||
Why this approach:
|
||||
|
||||
- `clangd` already proves that completion quality depends on client capabilities
|
||||
- several existing `CodeCompletionOptions` fields in `clice` are never meaningfully driven by the LSP client today
|
||||
- it keeps editor-specific policy out of the core collector logic
|
||||
|
||||
Alternative considered:
|
||||
|
||||
- hardcode one rendering mode for all clients
|
||||
Rejected because it prevents snippet/doc behavior from ever becoming correct and makes the existing options struct mostly dead code.
|
||||
|
||||
### 3. Keep Sema as the primary source of truth, but augment it with identifiers and index symbols
|
||||
|
||||
Collection order should be:
|
||||
|
||||
- Sema candidates from the active compilation context
|
||||
- local/raw identifiers from the open buffer and active file context
|
||||
- project/index candidates when the completion context allows them
|
||||
|
||||
Sema remains primary because it understands visibility, module state, recovery fix-its, and the exact active compile command. Identifier and index sources fill gaps when Sema is incomplete or when project-wide results are useful.
|
||||
|
||||
Merge rules should deduplicate by insertion semantics, not just by display label. Function overload bundling should group only candidates that render identically and require the same dependency edits.
|
||||
|
||||
Why this approach:
|
||||
|
||||
- it preserves correctness for the current file while still expanding recall
|
||||
- it fits `clice`'s current stateless worker path, which already builds the correct compile inputs
|
||||
- it allows richer completion without waiting for index-only correctness
|
||||
|
||||
Alternative considered:
|
||||
|
||||
- move to index-first completion with Sema as fallback
|
||||
Rejected because `clice`'s strongest current signals for modules, fix-its, and header contexts come from active compilation, not from the persisted index.
|
||||
|
||||
### 4. Expand the index schema specifically for completion metadata
|
||||
|
||||
`clice`'s current `index::Symbol` payload is too small for rich completion. To support index-backed completion, the index data model should be extended with completion-oriented fields such as:
|
||||
|
||||
- qualified scope / shortest usable qualifier
|
||||
- signature and return type
|
||||
- deprecation flag
|
||||
- documentation summary or documentation lookup key
|
||||
- canonical declaration path
|
||||
- preferred include header or module/import source
|
||||
- symbol origin/category for ranking
|
||||
|
||||
The first implementation does not need every clangd field, but it does need enough metadata to render and rank project-index candidates meaningfully.
|
||||
|
||||
Why this approach:
|
||||
|
||||
- without extra index payload, project-index completion can only return weak name-only suggestions
|
||||
- dependency insertion cannot be justified without declaration ownership metadata
|
||||
- the serialization boundary already exists in `TUIndex`, `ProjectIndex`, and merged shards, so this is the right architectural layer
|
||||
|
||||
Alternative considered:
|
||||
|
||||
- use the current index unchanged and limit project-index completion to `name + kind`
|
||||
Rejected because it would add complexity without delivering clangd-level value, and would likely need to be rewritten immediately after.
|
||||
|
||||
### 5. Use heuristic ranking first, and encode the signals explicitly
|
||||
|
||||
The first ranking model should remain heuristic, but it must go beyond raw fuzzy score. Candidate scoring should combine:
|
||||
|
||||
- prefix/exact name match
|
||||
- semantic availability from Sema
|
||||
- in-scope vs required-qualifier insertion
|
||||
- expected type / preferred type compatibility when available
|
||||
- file/header-context proximity
|
||||
- deprecation penalty
|
||||
- module availability and dependency-edit cost
|
||||
- template-instantiation compatibility when `clice` can infer it
|
||||
|
||||
This keeps the design testable and explainable without blocking on a learned model.
|
||||
|
||||
Why this approach:
|
||||
|
||||
- it delivers most user-visible value quickly
|
||||
- it maps naturally onto `clice`'s existing semantics-heavy architecture
|
||||
- it leaves room for a later learned ranker without locking the system into clangd's exact model
|
||||
|
||||
Alternative considered:
|
||||
|
||||
- adopt clangd-style decision-forest ranking immediately
|
||||
Rejected because `clice` does not yet have the surrounding instrumentation and labeled data pipeline, and heuristic ranking is sufficient for this change.
|
||||
|
||||
### 6. Treat completion-related edits as first-class output, with staged support
|
||||
|
||||
Two classes of edits matter:
|
||||
|
||||
- Sema-provided fix-its near the completion site
|
||||
- dependency insertion edits such as `#include` or `import`
|
||||
|
||||
The completion model should carry both as structured edits and render them into LSP `additionalTextEdits` or merged `textEdit`s where appropriate.
|
||||
|
||||
Implementation should be staged:
|
||||
|
||||
- stage 1: preserve and emit Sema fix-its
|
||||
- stage 2: add dependency insertion for header-backed symbols once index metadata can justify it
|
||||
- stage 3: add module import insertion where the active TU and candidate source indicate a module-based dependency is the right form
|
||||
|
||||
Why this approach:
|
||||
|
||||
- it closes a large clangd parity gap without forcing all edit types at once
|
||||
- module-aware insertion is a place where `clice` can exceed clangd's current include-centric design
|
||||
|
||||
Alternative considered:
|
||||
|
||||
- postpone all completion edits until after ranking/index parity
|
||||
Rejected because Sema fix-its are already available and should not be discarded.
|
||||
|
||||
### 7. Make `clice`-specific header, module, and template context part of completion semantics
|
||||
|
||||
The main "beyond clangd" opportunities should not be separate side features. They should influence the same completion pipeline:
|
||||
|
||||
- **Header context**: completion in a header should use the active includer/source context for that header, instead of treating the header as a single global state.
|
||||
- **Modules**: available PCM/module dependencies should affect both candidate visibility and dependency insertion strategy.
|
||||
- **Template instantiation**: when `clice` can identify concrete instantiation or expected-type information, ranking should prefer candidates compatible with that instantiated context.
|
||||
|
||||
This means the primary completion request must always be evaluated in one active compilation context, not a union of all possible contexts. Alternate contexts can be explored later, but blindly merging them would produce unstable and misleading completions.
|
||||
|
||||
Why this approach:
|
||||
|
||||
- it uses the architecture `clice` already claims as a differentiator
|
||||
- it avoids the worst failure mode for shared headers: mixing incompatible contexts into one result set
|
||||
- it keeps "beyond clangd" work aligned with user-visible completion quality
|
||||
|
||||
Alternative considered:
|
||||
|
||||
- add cross-context completion by merging all known header contexts
|
||||
Rejected because it sacrifices correctness and predictable ranking for recall.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [Index payload expansion increases serialization and background-index cost] → Mitigation: add only completion-relevant fields first and measure shard-size growth before expanding documentation payloads further.
|
||||
- [Ranking heuristics become opaque or brittle] → Mitigation: keep signals explicit, loggable, and unit-tested rather than encoding hidden constants throughout the collector.
|
||||
- [Header-context-aware completion can produce confusing behavior if the active context is wrong] → Mitigation: bind completion to the same active header-context selection model used by navigation/AST features, not an ad hoc completion-only heuristic.
|
||||
- [Dependency insertion can apply the wrong form (`#include` vs `import`)] → Mitigation: gate edits on explicit symbol metadata and active compilation mode, and prefer no edit over a wrong edit.
|
||||
- [More completion metadata increases protocol churn between master and stateless workers] → Mitigation: add a dedicated options/result struct instead of spreading new fields loosely across existing request params.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. Extend the LSP/server edge to capture completion capabilities, trigger context, and limits.
|
||||
2. Introduce an internal completion result model and return a completion list shape with `isIncomplete`.
|
||||
3. Refactor the worker pipeline to separate candidate collection, merging, scoring, and rendering while preserving current Sema behavior.
|
||||
4. Add local-identifier completion and heuristic ranking improvements.
|
||||
5. Expand the index schema and integrate project-index candidates.
|
||||
6. Add completion-related edits, first for Sema fix-its and then for dependency insertion.
|
||||
7. Integrate header-context, module, and template-instantiation signals into ranking and edit policy.
|
||||
8. Expand unit and integration coverage around rendering, ranking, index augmentation, module-aware completion, and shared-header contexts.
|
||||
|
||||
Rollback strategy:
|
||||
|
||||
- keep Sema-only completion as a safe fallback path until the staged pipeline is stable
|
||||
- disable index augmentation or dependency insertion independently if they prove noisy
|
||||
- prefer full completion results without advanced edits over partially wrong completion behavior
|
||||
|
||||
## Open Questions
|
||||
|
||||
- How much documentation should be stored directly in `clice`'s index versus fetched/lazily summarized from declarations on demand?
|
||||
- Should project-index completion support all-scopes insertion from the first version, or require explicit qualifiers only after scope metadata is fully modeled?
|
||||
- What is the right UX for exposing the currently active header context to users when completion results differ across includers?
|
||||
- How aggressive should template-instantiation-aware ranking be before users perceive it as "hiding" generic but still legal completions?
|
||||
- Whether dependency insertion for modules should be represented as ordinary additional text edits or as a future code-action-style completion extension.
|
||||
36
openspec/changes/improve-code-completion-parity/proposal.md
Normal file
36
openspec/changes/improve-code-completion-parity/proposal.md
Normal file
@@ -0,0 +1,36 @@
|
||||
## Why
|
||||
|
||||
`clice` already answers `textDocument/completion`, but the current implementation is still a thin Sema wrapper: it gathers only compiler candidates, applies local fuzzy filtering, and returns minimal `CompletionItem` fields. Compared with the local `clangd` implementation under `.llvm/clang-tools-extra/clangd`, it is missing most of the completion pipeline that makes results accurate, stable, and editor-friendly: client capability negotiation, context-aware triggering, richer ranking, index-backed augmentation, richer rendering, and completion-related edits.
|
||||
|
||||
This is worth addressing now because `clice` already has the infrastructure that can support a stronger design: background project indexing, merged per-file index shards, header-context modeling, stateless completion workers, and PCH/PCM plumbing for module-aware compilation. Closing the clangd gap will materially improve day-to-day usability, while `clice`'s existing header-context, module, and instantiation-aware architecture creates opportunities to implement completion behaviors clangd does not currently provide.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Define a full code-completion pipeline for `clice` instead of the current direct Sema-to-LSP conversion path.
|
||||
- Add parity-oriented completion behavior for the highest-value clangd features:
|
||||
- client capability negotiation for snippets, documentation format, label details, completion item kinds, and overload bundling
|
||||
- trigger-character handling and suppression of obviously spurious auto-triggered completion
|
||||
- richer `CompletionItem` rendering including `filterText`, stable `sortText`, signatures/details, return types, documentation, deprecation, snippet suffixes, and completion-related edits
|
||||
- result limiting and `isIncomplete` signaling
|
||||
- completion candidates from project index and local identifiers in addition to Sema results
|
||||
- ranking that combines fuzzy match with semantic/contextual signals rather than raw name matching alone
|
||||
- Define how `clice` should attach edits associated with completion candidates, including fix-its and insertion of missing dependencies when the completion source can justify them.
|
||||
- Specify `clice`-specific completion extensions that go beyond clangd's current model:
|
||||
- header-context-aware completion for headers shared by multiple includers
|
||||
- module-aware completion that exploits existing PCM dependency tracking
|
||||
- instantiation-aware ranking and filtering for template-heavy code
|
||||
- Add targeted unit and integration coverage so completion behavior can evolve without regressions.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `code-completion`: Provide context-aware, multi-source C++ code completion with rich LSP rendering, ranking, dependency edits, and `clice`-specific support for header contexts, modules, and template instantiation.
|
||||
|
||||
### Modified Capabilities
|
||||
- None.
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected code: `src/feature/code_completion.cpp`, `src/feature/feature.h`, `src/server/master_server.cpp`, `src/server/stateless_worker.cpp`, `src/server/protocol.h`, and completion-related protocol wiring in the IPC/LSP layer.
|
||||
- Likely affected subsystems: project/merged index integration, document/client capability tracking, completion request routing, and any helper code used for include/import insertion or completion scoring.
|
||||
- Affected tests: completion unit tests, stateless worker tests, server integration tests, module fixtures, and new comparison-oriented cases for header-context and template-heavy completion.
|
||||
@@ -0,0 +1,77 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Capability-aware completion responses
|
||||
The server SHALL shape code-completion responses according to client capabilities, trigger context, and requested limits instead of returning one fixed completion-item format to every client.
|
||||
|
||||
#### Scenario: Snippet-capable client receives snippet completions
|
||||
- **WHEN** the client advertises snippet support and a completion candidate has callable arguments, template arguments, or other structured suffix text
|
||||
- **THEN** the completion response MUST encode that candidate using snippet-formatted insertion text rather than degrading it to plain text
|
||||
|
||||
#### Scenario: Plain-text client receives compatible completion items
|
||||
- **WHEN** the client does not advertise snippet support or label-details support
|
||||
- **THEN** the server MUST return completion items that remain correct for that client and MUST NOT require unsupported snippet or label-details fields to preserve meaning
|
||||
|
||||
#### Scenario: Completion result limit is enforced
|
||||
- **WHEN** the client or server applies a completion result limit and more matching candidates exist than can be returned
|
||||
- **THEN** the server MUST truncate the response to the chosen limit and MUST mark the completion list as incomplete
|
||||
|
||||
#### Scenario: Spurious auto-triggered completion is suppressed
|
||||
- **WHEN** completion is auto-triggered by a trigger character in a syntactic position that cannot produce meaningful completion candidates
|
||||
- **THEN** the server MUST return an empty completion list instead of forcing a normal completion run
|
||||
|
||||
### Requirement: Multi-source candidate collection and semantic ranking
|
||||
The server SHALL build completion results from multiple candidate sources and rank them using semantic/contextual signals rather than using only raw label matching.
|
||||
|
||||
#### Scenario: Sema and index candidates are merged by insertion semantics
|
||||
- **WHEN** Sema and project-index collection both produce candidates that insert the same symbol in the same way
|
||||
- **THEN** the server MUST return a single merged completion item rather than duplicate visible entries
|
||||
|
||||
#### Scenario: Local identifiers supplement Sema completion
|
||||
- **WHEN** the open document contains matching identifiers that are not surfaced by Sema at the completion point
|
||||
- **THEN** the server MUST be able to return those identifiers as completion candidates when they are relevant to the typed prefix
|
||||
|
||||
#### Scenario: Required qualifiers are preserved for out-of-scope symbols
|
||||
- **WHEN** a project-index completion candidate is not directly visible at the completion point but remains a valid suggestion through qualification
|
||||
- **THEN** the completion item MUST preserve the required qualifier in its rendered insertion text instead of pretending the symbol is already in scope
|
||||
|
||||
#### Scenario: Semantic relevance outranks weaker textual matches
|
||||
- **WHEN** two otherwise similar candidates match the typed prefix but only one is strongly favored by semantic/contextual signals such as active scope, expected type, or availability in the current compilation context
|
||||
- **THEN** the semantically favored candidate MUST rank above the weaker match
|
||||
|
||||
### Requirement: Rich completion item rendering and completion-related edits
|
||||
The server SHALL render completion items with enough metadata and edits to make them useful in modern editors, including signatures, detail fields, filtering/sorting metadata, documentation, deprecation, and completion-associated edits.
|
||||
|
||||
#### Scenario: Function completion includes signature and return-type detail
|
||||
- **WHEN** a callable declaration is returned as a completion candidate
|
||||
- **THEN** the completion item MUST expose its callable signature and MUST expose return-type or overload-summary detail in the rendered result
|
||||
|
||||
#### Scenario: Documentation and deprecation are preserved
|
||||
- **WHEN** a completion candidate has documentation text or is marked deprecated by Sema or index metadata
|
||||
- **THEN** the completion item MUST surface the documentation and deprecation state to the client
|
||||
|
||||
#### Scenario: Sema fix-its are attached to the completion item
|
||||
- **WHEN** Sema provides fix-it edits that are required or strongly suggested for a completion candidate
|
||||
- **THEN** the server MUST attach those edits to the completion item so the accepted completion can apply them
|
||||
|
||||
#### Scenario: Dependency insertion edits are attached when justified
|
||||
- **WHEN** a completion candidate refers to a symbol that requires adding a missing dependency and the completion source can identify the correct insertion form
|
||||
- **THEN** the completion item MUST attach the corresponding dependency insertion edits instead of returning only the symbol name
|
||||
|
||||
### Requirement: Context-sensitive completion for headers, modules, and templates
|
||||
The server SHALL use `clice`'s compilation-context machinery so completion reflects the active header context, module dependency state, and template-instantiation information instead of treating all files as context-free.
|
||||
|
||||
#### Scenario: Shared header completion follows the active header context
|
||||
- **WHEN** the same header is completed under different active includer/source contexts
|
||||
- **THEN** the completion results MUST reflect the active header context rather than a context-free union of all possible includers
|
||||
|
||||
#### Scenario: Module-aware completion uses available PCM dependencies
|
||||
- **WHEN** the active file depends on C++20 modules whose PCMs are available to the completion worker
|
||||
- **THEN** the completion pipeline MUST consider symbols made visible by those module dependencies when producing results
|
||||
|
||||
#### Scenario: Module dependency insertion prefers import-aware edits
|
||||
- **WHEN** the selected completion candidate is best satisfied by adding a module dependency rather than a textual include and the active translation unit supports that form
|
||||
- **THEN** the completion item MUST use a module-aware dependency insertion edit instead of forcing a header-style include edit
|
||||
|
||||
#### Scenario: Template-instantiation-aware ranking prefers concretely compatible candidates
|
||||
- **WHEN** the active completion site provides concrete template-instantiation or expected-type information that distinguishes candidate relevance
|
||||
- **THEN** the ranking logic MUST prefer candidates compatible with that instantiated context over otherwise similar generic alternatives
|
||||
40
openspec/changes/improve-code-completion-parity/tasks.md
Normal file
40
openspec/changes/improve-code-completion-parity/tasks.md
Normal file
@@ -0,0 +1,40 @@
|
||||
## 1. Completion Request Contract
|
||||
|
||||
- [ ] 1.1 Audit the current `textDocument/completion` request path and extend the master/worker protocol to carry trigger context, requested limit, and normalized client completion capabilities.
|
||||
- [ ] 1.2 Store completion-related client capabilities from `initialize` in the server and thread them into completion requests instead of relying on default `CodeCompletionOptions`.
|
||||
- [ ] 1.3 Change completion responses from a raw item vector to a completion-list shape that can represent `isIncomplete` and future completion metadata cleanly.
|
||||
|
||||
## 2. Internal Completion Pipeline
|
||||
|
||||
- [ ] 2.1 Introduce an internal completion-candidate/result model that separates collection, deduplication/bundling, scoring, truncation, and final LSP rendering.
|
||||
- [ ] 2.2 Refactor `src/feature/code_completion.cpp` so Sema collection populates the internal model instead of directly constructing final `CompletionItem`s.
|
||||
- [ ] 2.3 Implement capability-aware rendering for snippets, signatures/details, documentation, deprecation, `filterText`, and stable `sortText`.
|
||||
- [ ] 2.4 Add request-side suppression for obviously spurious auto-triggered completions and preserve correct fallback behavior for manual completion.
|
||||
|
||||
## 3. Candidate Sources And Ranking
|
||||
|
||||
- [ ] 3.1 Add local-identifier collection from the active buffer/file context and merge those candidates with Sema results.
|
||||
- [ ] 3.2 Implement semantic deduplication and overload bundling based on insertion semantics rather than label-only equality.
|
||||
- [ ] 3.3 Replace raw fuzzy-only ordering with explicit heuristic scoring that incorporates scope visibility, expected type, deprecation, and active compilation-context signals.
|
||||
- [ ] 3.4 Enforce result limits after scoring and verify that truncation drives `isIncomplete` correctly.
|
||||
|
||||
## 4. Index Completion And Completion Edits
|
||||
|
||||
- [ ] 4.1 Extend completion-relevant index data structures and serialization to store the metadata needed for index-backed completion rendering and ranking.
|
||||
- [ ] 4.2 Add project-index candidate lookup for completion and merge index candidates with Sema/identifier candidates without duplicating visible entries.
|
||||
- [ ] 4.3 Preserve and render Sema fix-it edits as completion-associated edits.
|
||||
- [ ] 4.4 Implement dependency insertion edits for header-backed symbols once index metadata can identify the correct declaration owner and insertion path.
|
||||
|
||||
## 5. `clice`-Specific Completion Extensions
|
||||
|
||||
- [ ] 5.1 Bind completion in headers to the active header-context selection model instead of treating shared headers as context-free.
|
||||
- [ ] 5.2 Integrate available PCM/module dependency information into candidate visibility and ranking for module-aware completion.
|
||||
- [ ] 5.3 Implement module-aware dependency insertion policy for completion items that should add `import`-style dependencies instead of header includes.
|
||||
- [ ] 5.4 Add template-instantiation-aware ranking signals where `clice` can derive concrete instantiation or expected-type compatibility.
|
||||
|
||||
## 6. Validation
|
||||
|
||||
- [ ] 6.1 Add focused unit tests for completion rendering, capability negotiation, overload bundling, sorting/filtering metadata, and `isIncomplete` behavior.
|
||||
- [ ] 6.2 Add unit or fixture-based tests for identifier augmentation, index-backed completion, fix-it edits, and dependency insertion edits.
|
||||
- [ ] 6.3 Add integration coverage for shared-header contexts, module-aware completion, and template-heavy completion cases.
|
||||
- [ ] 6.4 Run the relevant completion, worker, index, and integration test targets and fix regressions uncovered during verification.
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-04-03
|
||||
101
openspec/changes/improve-document-symbol-outline/design.md
Normal file
101
openspec/changes/improve-document-symbol-outline/design.md
Normal file
@@ -0,0 +1,101 @@
|
||||
## Context
|
||||
|
||||
The current `src/feature/document_symbols.cpp` implementation reuses the generic `FilteredASTVisitor`. Its advantage is brevity, but it does not model outline-specific semantics such as which declarations should become outline nodes, which should only forward traversal to children, and which subtrees should be skipped entirely.
|
||||
|
||||
Compared with `clangd`'s `DocumentOutline`, the current implementation has several structural shortcomings:
|
||||
|
||||
- Function nodes continue recursive traversal into function bodies, so local `VarDecl`, `RecordDecl`, `EnumDecl`, `BindingDecl`, and similar declarations may leak into the document outline.
|
||||
- Only implicit template instantiations are filtered today; explicit instantiations and explicit specializations do not have distinct visit policies.
|
||||
- Declarations produced by macro expansion are placed directly according to the expanded declaration instead of being grouped under main-file macro invocations as `clangd` does.
|
||||
- The implementation computes `range` / `selection_range` per declaration but does not repair parent-child range relationships after tree construction, and it does not provide fallback handling for macro locations or inconsistent written-name ranges.
|
||||
- Existing tests mostly assert total node counts, which is too weak to prevent hierarchy, naming, and range regressions.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- Align `clice` document outline behavior with `clangd` on the core semantic cases.
|
||||
- Show only document-level symbols users actually care about while preserving namespace, type, enum, and member hierarchy.
|
||||
- Define stable, testable behavior for templates and macros.
|
||||
- Replace count-only assertions with structural assertions that provide long-term regression protection.
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- This change does not introduce `#pragma mark`-style outline grouping.
|
||||
- This change does not modify `workspace/symbol` behavior or index-layer symbol handling.
|
||||
- This change does not refactor unrelated AST traversal infrastructure.
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. Use a dedicated outline traversal instead of piling more special cases onto `FilteredASTVisitor`
|
||||
|
||||
The core issue in document symbol collection is not "filter a few more decl kinds". It is the need to explicitly distinguish:
|
||||
|
||||
- do not visit
|
||||
- visit declaration only
|
||||
- visit children only
|
||||
- visit both declaration and children
|
||||
|
||||
This is exactly what `clangd`'s `VisitKind` model addresses. Keeping the generic visitor and layering more contextual flags on top would scatter function-body filtering, template behavior, and macro-container insertion across multiple callbacks and make the implementation harder to maintain.
|
||||
|
||||
The alternative is to keep the current visitor and add more contextual checks such as "do not collect local declarations inside function bodies". That approach is smaller, but it does not naturally express leaf-only explicit instantiations, wrapper decl pass-through, or macro container insertion, so it is not chosen.
|
||||
|
||||
### 2. Define template and wrapper-declaration behavior using explicit visit policies
|
||||
|
||||
The new traversal should cover at least these rules:
|
||||
|
||||
- `LinkageSpecDecl`, `ExportDecl`, and similar wrappers do not become outline nodes and only forward traversal to their children.
|
||||
- Functions and methods become nodes, but traversal must not descend into function-local declarations.
|
||||
- Implicit template instantiations are skipped entirely.
|
||||
- Explicit instantiations are shown as declaration-only leaf nodes and do not synthesize member children.
|
||||
- Explicit specializations show both the declaration itself and the children actually written in source.
|
||||
|
||||
This makes the outline reflect the structure users wrote in the file instead of all declarations that happen to be traversable in the AST.
|
||||
|
||||
### 3. Create container nodes for macro invocations written in the main file
|
||||
|
||||
`clangd` includes macro invocations written directly in the main file as outline hierarchy nodes. `clice` already has enough macro-location decomposition support to adopt a similar strategy:
|
||||
|
||||
- When a declaration originates from macro expansion, walk up the macro-caller chain until reaching a direct invocation written in the main file.
|
||||
- Create at most one container node per invocation site.
|
||||
- Attach all outline declarations produced by that invocation under the macro container.
|
||||
|
||||
This avoids having multiple macro-expanded declarations appear as if they were written directly in the enclosing namespace or class scope, and it keeps the outline aligned with the entry points users actually see in the source.
|
||||
|
||||
The alternative is to keep the current behavior and only repair ranges. That is simpler to implement, but it leaves macro-heavy outlines difficult to understand, so it is not chosen.
|
||||
|
||||
### 4. Repair ordering and ranges after building the tree
|
||||
|
||||
The protocol requires `selectionRange` to be contained within `range`, and editors generally assume parent ranges contain child ranges. The current implementation sorts declarations individually but does not apply a tree-level repair pass.
|
||||
|
||||
This change adds a post-processing step that:
|
||||
|
||||
- sorts siblings stably in source order
|
||||
- expands parent ranges when child ranges extend beyond the initially collected parent range
|
||||
- repairs inconsistent `selectionRange` values by falling back to a more reliable location, and collapses to a single range if necessary
|
||||
|
||||
This keeps editor-facing invariants centralized instead of scattering boundary-fix logic across declaration collection paths.
|
||||
|
||||
### 5. Test structure directly instead of asserting only node counts
|
||||
|
||||
New and updated tests should assert:
|
||||
|
||||
- top-level and child hierarchy
|
||||
- `name` / `kind` for key nodes
|
||||
- the absence of leaked local declarations
|
||||
- macro container presence and correct children
|
||||
- expected behavior for explicit instantiations and explicit specializations
|
||||
- `selectionRange` containment within `range`
|
||||
|
||||
This is the minimum needed to cover the semantics that actually matter in this change.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [Higher implementation complexity] -> The traversal logic will be longer than the current visitor-based version. Split visit-policy handling, macro-container logic, and range repair into separate helpers to keep it manageable.
|
||||
- [Macro container icon semantics may be imperfect] -> LSP `SymbolKind` does not offer an ideal macro-specific visual category. Prioritize correct hierarchy and ranges first.
|
||||
- [Template edge cases are numerous] -> Cover the highest-value cases first with tests for implicit instantiation, explicit instantiation, and explicit specialization behavior.
|
||||
- [Snapshot-style tests would be brittle] -> Prefer structural assertions over full JSON snapshots to reduce churn from unrelated field changes.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Should this change also add deprecated document symbol metadata, depending on whether the current protocol schema supports `DocumentSymbol.tags` or `deprecated` fields cleanly?
|
||||
28
openspec/changes/improve-document-symbol-outline/proposal.md
Normal file
28
openspec/changes/improve-document-symbol-outline/proposal.md
Normal file
@@ -0,0 +1,28 @@
|
||||
## Why
|
||||
|
||||
`clice`'s current `document_symbols` implementation collects `NamedDecl`s through the generic `FilteredASTVisitor`. Compared with `clangd`'s dedicated outline builder, it still lacks several important constraints: filtering function-local declarations, differentiating template specializations from instantiations, grouping macro-expanded declarations, and repairing `range` / `selectionRange` / parent-child nesting invariants.
|
||||
|
||||
These gaps directly affect outline stability and readability in editors, and they leave a visible behavior gap between `clice` and `clangd` on real C++ code. Closing them now should materially improve document outline quality without changing the LSP surface area.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Replace the generic "collect interesting decls during recursive traversal" approach with an outline-specific traversal model.
|
||||
- Exclude declarations that should not appear in document outline results, especially function-local declarations, implicit entities, and implicit template instantiations.
|
||||
- Define template behavior explicitly so specializations, explicit instantiations, and ordinary template declarations appear in the outline in a source-intuitive way.
|
||||
- Introduce macro container symbols for macro invocations written in the main file so expanded declarations appear under a stable hierarchy instead of leaking into the enclosing scope.
|
||||
- Normalize symbol ordering and range invariants so `selectionRange` is always contained in `range`, and parent ranges always contain child ranges.
|
||||
- Strengthen unit and integration coverage so regressions are caught by structural assertions instead of node counts alone.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `document-symbols`: Provide document outline output that matches source structure more closely and behaves robustly in template and macro-heavy code.
|
||||
|
||||
### Modified Capabilities
|
||||
- None.
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected code: `src/feature/document_symbols.cpp` and any reused helpers for macro locations or source range repair.
|
||||
- Affected tests: `tests/unit/feature/document_symbol_tests.cpp` and `tests/integration/test_server.py`.
|
||||
- User-visible behavior: editor Outline / Breadcrumb / Document Symbols views will behave more like `clangd`, especially for local declarations, templates, and macro-heavy code.
|
||||
@@ -0,0 +1,59 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Document outline excludes non-structural local declarations
|
||||
The document symbol response SHALL include user-written document-structure declarations from the main file, including namespaces, records, enums, fields, top-level variables, functions, methods, constructors, destructors, conversion functions, deduction guides, concepts, and structured bindings.
|
||||
|
||||
The document symbol response SHALL NOT include declarations that are local to a function or method body, and SHALL NOT include implicit declarations or implicit template instantiations.
|
||||
|
||||
#### Scenario: Local variable does not appear in outline
|
||||
- **WHEN** a function body contains a local variable declaration and a local class declaration
|
||||
- **THEN** the function itself appears in document symbols
|
||||
- **THEN** the local variable and local class do not appear as document symbol children
|
||||
|
||||
#### Scenario: Member hierarchy is preserved
|
||||
- **WHEN** a record contains nested records, fields, methods, and enum members
|
||||
- **THEN** the outer record appears in document symbols
|
||||
- **THEN** the nested declarations appear as children of that record in source order
|
||||
|
||||
### Requirement: Template specializations follow written-source structure
|
||||
The document symbol response SHALL ignore implicit template instantiations.
|
||||
|
||||
The document symbol response SHALL include user-written explicit specializations and explicit instantiations exactly once. Explicit specializations that define nested members SHALL expose those nested members as children. Explicit instantiations SHALL appear as leaf symbols unless the source itself contains nested declarations at that location.
|
||||
|
||||
#### Scenario: Explicit specialization exposes children
|
||||
- **WHEN** a class template has an explicit specialization with user-written members in the main file
|
||||
- **THEN** the explicit specialization appears in document symbols
|
||||
- **THEN** its user-written members appear as children of that specialization
|
||||
|
||||
#### Scenario: Explicit instantiation is a leaf
|
||||
- **WHEN** the main file contains an explicit template instantiation declaration or definition
|
||||
- **THEN** the instantiated symbol appears in document symbols exactly once
|
||||
- **THEN** it does not synthesize member children that are not written at the instantiation site
|
||||
|
||||
### Requirement: Macro-expanded declarations are grouped by macro invocation
|
||||
When a declaration included in the outline originates from a macro expansion whose invocation is written directly in the main file, the document symbol response SHALL group the expanded declarations under a macro container symbol associated with that invocation.
|
||||
|
||||
The macro container SHALL appear at the invocation site and SHALL contain all outline declarations produced by that invocation within the current scope.
|
||||
|
||||
#### Scenario: Function-like macro groups expanded declarations
|
||||
- **WHEN** a function-like macro invocation in the main file expands to a class declaration with members
|
||||
- **THEN** document symbols include a container node for that macro invocation
|
||||
- **THEN** the expanded class appears as a child of that container node
|
||||
|
||||
#### Scenario: One macro invocation creates one container
|
||||
- **WHEN** a single macro invocation expands to multiple top-level declarations
|
||||
- **THEN** document symbols include one container node for that invocation
|
||||
- **THEN** all declarations produced by that invocation appear beneath the same container node
|
||||
|
||||
### Requirement: Document symbol ranges are editor-safe
|
||||
Document symbols SHALL be returned in source order within each sibling list.
|
||||
|
||||
For every returned symbol, `selectionRange` SHALL be contained within `range`. For every parent symbol with children, the parent `range` SHALL contain each child `range`, including declarations originating from macro expansions.
|
||||
|
||||
#### Scenario: Selection range is repaired when written name range is inconsistent
|
||||
- **WHEN** a declaration's preferred name location would produce a `selectionRange` outside its declaration `range`
|
||||
- **THEN** the response repairs the symbol ranges so that `selectionRange` is contained within `range`
|
||||
|
||||
#### Scenario: Parent range contains nested child symbols
|
||||
- **WHEN** a symbol has nested child declarations whose ranges extend beyond the initially collected parent range
|
||||
- **THEN** the parent symbol range is expanded so that all child ranges are contained within it
|
||||
17
openspec/changes/improve-document-symbol-outline/tasks.md
Normal file
17
openspec/changes/improve-document-symbol-outline/tasks.md
Normal file
@@ -0,0 +1,17 @@
|
||||
## 1. Outline Traversal
|
||||
|
||||
- [ ] 1.1 Replace the generic document symbol collector with a dedicated outline traversal that can distinguish skip / only-decl / only-children / decl-and-children behavior.
|
||||
- [ ] 1.2 Filter out function-local declarations, implicit entities, and implicit template instantiations while preserving namespace, type, enum, field, and member hierarchy.
|
||||
- [ ] 1.3 Implement explicit specialization and explicit instantiation behavior so written specializations can expose children and explicit instantiations remain leaf symbols.
|
||||
|
||||
## 2. Macro And Range Handling
|
||||
|
||||
- [ ] 2.1 Build macro invocation container nodes for declarations expanded from main-file macro calls.
|
||||
- [ ] 2.2 Add post-processing to stabilize sibling ordering and enforce `selectionRange`-within-`range` plus parent-range-contains-child-range invariants.
|
||||
- [ ] 2.3 Confirm whether the current protocol layer can carry deprecated document symbol metadata and either implement it here or record a follow-up.
|
||||
|
||||
## 3. Validation
|
||||
|
||||
- [ ] 3.1 Rewrite unit tests to assert symbol names, kinds, hierarchy, and range invariants instead of only total node counts.
|
||||
- [ ] 3.2 Add regression cases covering local declarations, explicit specialization/instantiation, and macro-expanded declarations.
|
||||
- [ ] 3.3 Extend integration coverage so the server request path exercises representative document symbol responses.
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-04-03
|
||||
199
openspec/changes/improve-folding-range-support/design.md
Normal file
199
openspec/changes/improve-folding-range-support/design.md
Normal file
@@ -0,0 +1,199 @@
|
||||
## Context
|
||||
|
||||
`clice` currently implements folding ranges in [`src/feature/folding_ranges.cpp`](/home/ykiko/C++/clice/src/feature/folding_ranges.cpp). The implementation is primarily an AST visitor with extra handling for conditional compilation and `#pragma region` data from `CompilationUnitRef::directives()`. It already covers many structural folds that clangd does not currently expose, such as namespaces, records, function parameter lists, lambda captures, call argument lists, access-specifier sections, and initializer lists.
|
||||
|
||||
Compared with `.llvm/clang-tools-extra/clangd`, the current gap is clear:
|
||||
|
||||
- clangd already has behavior that `clice` still lacks:
|
||||
- multiline comment folding
|
||||
- `lineFoldingOnly` client-capability handling
|
||||
- a more complete and assertion-backed folding-range test matrix
|
||||
- `clice` already has behavior that clangd does not:
|
||||
- richer AST-structure folding
|
||||
- `#pragma region` and some conditional-compilation folding
|
||||
- `collapsedText`
|
||||
- `clice` still has obvious opportunities that are not fully implemented yet:
|
||||
- fully closing the last `#if/#elif/#else` branch at `#endif`
|
||||
- folding inactive branches
|
||||
- folding multiline macro definitions
|
||||
- grouping contiguous `#include` / `import` blocks
|
||||
- capability-aware `kind` and `collapsedText` rendering
|
||||
|
||||
In addition, the current public output exposes many internal categories directly through `FoldingRange.kind`, such as `"namespace"`, `"class"`, and `"functionBody"`. This is more aggressive than clangd, but standard LSP only defines `comment`, `imports`, and `region` as interoperable folding kinds. The design therefore needs to preserve richer internal classification while presenting a compatible external contract.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- Preserve `clice`'s current advantage in AST-structure folding instead of regressing to clangd's much narrower block-only baseline.
|
||||
- Fill the high-value baseline gaps that clangd already covers, especially multiline comments and `lineFoldingOnly`.
|
||||
- Turn preprocessor metadata into a differentiating `clice` capability covering conditional branches, macro definitions, and include/import grouping.
|
||||
- Make folding-range output respect client capabilities with predictable fallback behavior.
|
||||
- Lock behavior down with unit and integration tests across AST, comments, preprocessor handling, and protocol negotiation.
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- Achieve byte-for-byte or range-for-range parity with clangd in this change.
|
||||
- Add fine-grained folding for every C++ syntax detail such as template parameter lists, requires-clauses, or attribute arguments before their value is proven.
|
||||
- Introduce editor-specific behavior that only exists to satisfy one frontend.
|
||||
- Add cross-file or index-backed folding behavior.
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. Split the folding-range pipeline into collection, normalization, and rendering
|
||||
|
||||
The current implementation mixes "how a range is discovered" with "how it is emitted as LSP". The new design separates this into three layers:
|
||||
|
||||
- collection: produce internal `RawFoldingRange` entries from AST, comment scanning, and preprocessor metadata
|
||||
- normalization: sort, deduplicate, validate, and reconcile nested or overlapping ranges
|
||||
- rendering: decide line/column boundaries, `kind`, and `collapsedText` based on client capabilities
|
||||
|
||||
Why:
|
||||
|
||||
- `lineFoldingOnly`, `collapsedText`, and standards-compatible kind downgrading are rendering concerns and should not pollute collection logic
|
||||
- comments, macros, and include/import groups do not naturally belong inside the AST visitor
|
||||
- future range limiting or prioritization should also live in normalization/rendering instead of collector code
|
||||
|
||||
Alternative considered:
|
||||
|
||||
- Keep generating final LSP ranges directly inside the visitor. Rejected because capability negotiation and multi-source collection will keep making the function larger and harder to test.
|
||||
|
||||
### 2. Keep rich internal categories, but only promise standard-compatible public kinds
|
||||
|
||||
Internally, the implementation may still distinguish namespace, class, function body, macro definition, conditional branch, and similar categories so tests, prioritization, and `collapsedText` selection remain precise. However, public LSP output should default to standard kinds only:
|
||||
|
||||
- comment folds -> `comment`
|
||||
- contiguous include/import groups -> `imports`
|
||||
- all other structural and preprocessor folds -> `region`
|
||||
|
||||
If some client later proves it needs clice-specific kinds, that can be evaluated separately. This change does not make non-standard kind strings part of the compatibility contract.
|
||||
|
||||
Why:
|
||||
|
||||
- many current custom strings will not be understood by clients and do not produce stable UI semantics
|
||||
- the real differentiator is what `clice` can fold, not the literal `kind` label
|
||||
- once public kinds are standardized, `collapsedText` and range boundaries become the primary user-visible expression
|
||||
|
||||
Alternative considered:
|
||||
|
||||
- Continue exposing all custom kinds directly. Rejected because that leaves client compatibility up to luck rather than protocol design.
|
||||
|
||||
### 3. Implement comment folding through lexical/source scanning, not AST
|
||||
|
||||
Multiline comments are handled independently in clangd's pseudo-parser path, and `clice` should do the same. The design adds a comment collector that scans the main-file source or token stream directly:
|
||||
|
||||
- fold multiline `/* ... */` block comments
|
||||
- fold contiguous `//` comment groups
|
||||
- do not fold single-line comments
|
||||
- adjust closing boundaries for `lineFoldingOnly` mode so the final visible line is not swallowed incorrectly
|
||||
|
||||
Why:
|
||||
|
||||
- comments are not AST structure, so trying to derive them from AST produces fragile behavior
|
||||
- lexical scanning naturally handles adjacent-comment grouping and block-comment boundaries
|
||||
|
||||
Alternative considered:
|
||||
|
||||
- Only support block comments. Rejected because clangd already demonstrates that contiguous `//` comment groups are a useful folding case.
|
||||
|
||||
### 4. Rework preprocessor folding around complete branch blocks instead of the current half-open stack
|
||||
|
||||
Today `collect_condition_directives()` only closes the previous branch when it sees `#else`, but when it sees `#endif` it only pops the stack and does not emit a folding range for the final `#if/#elif/#else` branch. As a result, `#if` folding is incomplete.
|
||||
|
||||
The new design treats conditional compilation as an explicit branch-group model:
|
||||
|
||||
- maintain the ordered branch chain for each `#if` group
|
||||
- allow every branch to close at the next `#elif`, `#else`, or `#endif`
|
||||
- distinguish active and inactive branches
|
||||
- allow inactive branches to produce region folds, optionally with distinct `collapsedText`
|
||||
|
||||
Why:
|
||||
|
||||
- this is the minimum sound model needed to fix the current logical gap
|
||||
- `Condition::ConditionValue` already records true/false/skipped state and can drive inactive-branch folding directly
|
||||
|
||||
Alternative considered:
|
||||
|
||||
- Patch only the `#endif` closing case. Rejected because nested conditions, inactive branches, and range ordering would remain structurally weak.
|
||||
|
||||
### 5. Add dedicated directive-based collectors for macros and include/import groups
|
||||
|
||||
`clice` already collects:
|
||||
|
||||
- `directive.macros`
|
||||
- `directive.includes`
|
||||
- `directive.imports`
|
||||
|
||||
The new design therefore adds directive-based folding collectors for:
|
||||
|
||||
- multiline `#define` macro definitions, using continuation backslashes or stable definition ranges
|
||||
- contiguous `#include` blocks, merged into a single `imports` folding range
|
||||
- contiguous `import Foo;` / `import Foo:Bar;` module-import blocks, also emitted as `imports`
|
||||
|
||||
Why:
|
||||
|
||||
- the necessary data already exists in preprocessing metadata and does not require new AST modeling
|
||||
- this is one of the easiest places for `clice` to provide value beyond clangd
|
||||
|
||||
Alternative considered:
|
||||
|
||||
- Leave include/import grouping for a later change. Rejected because the metadata already exists, the implementation cost is relatively low, and the editor-facing value is immediate.
|
||||
|
||||
### 6. Folding-range output must be explicitly bound to client capabilities
|
||||
|
||||
The master server currently only advertises `foldingRangeProvider = true`, but it does not read or propagate folding-specific client capabilities. The new design requires the session to track at least:
|
||||
|
||||
- `lineFoldingOnly`
|
||||
- whether `collapsedText` is supported
|
||||
- optional `rangeLimit`
|
||||
|
||||
Rendering rules:
|
||||
|
||||
- when `lineFoldingOnly = true`, only emit ranges that remain meaningful as line-based folds, adjusting end lines where necessary
|
||||
- when the client does not support `collapsedText`, omit it
|
||||
- when a `rangeLimit` is declared, trim results deterministically rather than arbitrarily
|
||||
|
||||
Alternative considered:
|
||||
|
||||
- Continue always returning exact columns and `collapsedText`. Rejected because that relies on client tolerance instead of following the protocol contract.
|
||||
|
||||
### 7. Organize tests by source category and protocol behavior
|
||||
|
||||
Tests will be split into two dimensions:
|
||||
|
||||
- source-category unit tests: AST structure, comments, conditional compilation, multiline macros, `#pragma region`, and include/import groups
|
||||
- protocol-behavior tests: `lineFoldingOnly`, `collapsedText` support, public kind mapping, and range limiting
|
||||
|
||||
In particular, the current [`tests/unit/feature/folding_range_tests.cpp`](/home/ykiko/C++/clice/tests/unit/feature/folding_range_tests.cpp) contains `Directive` and `PragmaRegion` cases that do not actually assert results. This change upgrades them into strong assertion-based tests.
|
||||
|
||||
Alternative considered:
|
||||
|
||||
- Rely mostly on manual editor validation. Rejected because folding details regress easily, especially for preprocessor handling and line-only rendering.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [Client capabilities must flow from initialize state into request-time rendering] -> Mitigation: introduce a dedicated folding-options structure so session details do not leak broadly into the feature layer.
|
||||
- [Inactive-branch and macro-definition ranges can be unstable around expansion locations] -> Mitigation: prefer spelling/main-file ranges and explicitly filter or special-case macro-expansion ranges when necessary.
|
||||
- [Adding comments, macros, and include/import groups can increase the number of ranges quickly] -> Mitigation: implement stable sorting and `rangeLimit` trimming in the normalization layer.
|
||||
- [Mapping public kinds back to standard values changes current metadata output] -> Mitigation: the folds themselves remain; the user-visible change is mostly in optional metadata, and tests plus change notes will make that explicit.
|
||||
- [Multiple collectors may produce overlapping or duplicate ranges] -> Mitigation: normalize by source category and boundary rules so collectors do not amplify noise.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. Refactor the internal folding-range data flow and add render options plus standard kind mapping.
|
||||
2. Add the comment collector and assertion-backed tests for multiline comment folding.
|
||||
3. Rewrite conditional-directive and `#pragma region` collection so `#if` branches close correctly through `#endif`.
|
||||
4. Add multiline macro folding and grouped include/import collectors.
|
||||
5. Wire folding client capabilities through initialize/request handling and add integration coverage.
|
||||
6. Add `rangeLimit` trimming and regression cleanup after the new collectors are in place.
|
||||
|
||||
Rollback strategy:
|
||||
|
||||
- If protocol negotiation proves unstable, keep the new collectors but temporarily disable outward behavior changes tied to `collapsedText` or `rangeLimit`.
|
||||
- If a particular new fold category proves noisy, roll it back collector-by-collector instead of reverting the entire folding-range refactor.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Should multiline macro folding cover only the macro body, or the full `#define NAME(...)` line plus body as one fold region?
|
||||
- Should `rangeLimit` prioritize outer structure, top-of-file regions, or longer ranges when trimming results?
|
||||
- For structural AST folds originating from macro expansion, should `clice` preserve current behavior or restrict itself to cases with stable spelling ranges only?
|
||||
28
openspec/changes/improve-folding-range-support/proposal.md
Normal file
28
openspec/changes/improve-folding-range-support/proposal.md
Normal file
@@ -0,0 +1,28 @@
|
||||
## Why
|
||||
|
||||
`clice`'s folding range support is already more aggressive than clangd's: it can fold namespaces, records, function parameter lists and bodies, lambda captures, call argument lists, access-specifier sections, and some preprocessor regions. However, it still lacks several useful baseline capabilities that clangd already has, especially multiline comment folding, client capability negotiation, and a stronger regression test matrix. Its preprocessor branch folding is also not yet fully closed.
|
||||
|
||||
More importantly, `clice` already has preprocessor metadata that clangd does not fully exploit, such as `directive.macros`, `directive.includes`, `directive.imports`, and evaluated conditional-branch state. That means `clice` should not stop at matching clangd: folding ranges can become a feature that is more useful for real-world C/C++ editing by covering macro definitions, `#if` branches, and include/import groups that clangd does not currently handle well.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Fill the remaining folding-range baseline gaps between `clice` and clangd, especially multiline comment folding and protocol-level client capability handling.
|
||||
- Complete preprocessor-related folding so full `#if/#elif/#else/#endif` branch regions, nested `#pragma region` blocks, and inactive branches have well-defined behavior.
|
||||
- Add folding features that take advantage of `clice`'s existing preprocessor metadata, including multiline macro definitions and grouped `#include` / `import` blocks.
|
||||
- Normalize `FoldingRange.kind` and `collapsedText` output so standard kinds remain compatible while clice-specific fold categories degrade predictably.
|
||||
- Make folding range responses honor client capabilities such as `lineFoldingOnly`, `collapsedText` support, and range limiting.
|
||||
- Expand unit and integration coverage for AST folds, comments, preprocessor regions, macros, include/import groups, and protocol negotiation behavior.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `folding-ranges`: Provide LSP-compatible, C/C++-focused folding regions that cover AST structure, comments, preprocessor branches, macro definitions, and include/import groups.
|
||||
|
||||
### Modified Capabilities
|
||||
- None.
|
||||
|
||||
## Impact
|
||||
|
||||
- Primary impact is in `src/feature/folding_ranges.cpp`, the compile-unit/preprocessor metadata access paths, and folding range request handling plus capability negotiation around `src/server/master_server.cpp`.
|
||||
- Tests need expansion in `tests/unit/feature/folding_range_tests.cpp`, server/integration coverage, and any required fixtures for preprocessor and module scenarios.
|
||||
- User-visible behavior will be folding results that are closer to clangd where clangd already has coverage, while also adding high-value C/C++ folds that clangd does not currently provide well, especially macro-definition and conditional-compilation folding.
|
||||
@@ -0,0 +1,61 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Folding range responses honor client capabilities
|
||||
The server SHALL render folding ranges according to the client's declared folding capabilities instead of always returning the richest possible payload.
|
||||
|
||||
#### Scenario: Line-only folding is respected
|
||||
- **WHEN** the client declares `textDocument.foldingRange.lineFoldingOnly = true`
|
||||
- **THEN** the server MUST return folding ranges that remain valid when interpreted as whole-line folds, including adjusting end boundaries for bracketed or comment ranges whose closing delimiter is on the last line
|
||||
|
||||
#### Scenario: Collapsed text is gated by client support
|
||||
- **WHEN** the client does not declare support for `textDocument.foldingRange.foldingRange.collapsedText`
|
||||
- **THEN** the server MUST omit `collapsedText` from the folding range response
|
||||
|
||||
#### Scenario: Standard kinds are emitted compatibly
|
||||
- **WHEN** a folding range represents a comment block, an include/import block, or any other foldable region
|
||||
- **THEN** the server MUST emit `kind = comment`, `kind = imports`, or `kind = region` respectively, and MUST NOT require clients to understand clice-specific kind strings in order to fold correctly
|
||||
|
||||
### Requirement: Structural and comment folding baseline
|
||||
The server SHALL provide folding ranges for multi-line C/C++ structural regions and multi-line comments in the main file.
|
||||
|
||||
#### Scenario: Multi-line comment blocks can be folded
|
||||
- **WHEN** a document contains a multi-line `/* ... */` comment or a contiguous block of `//` comments spanning more than one line
|
||||
- **THEN** the server MUST return a folding range for that comment block with `kind = comment`
|
||||
|
||||
#### Scenario: Single-line comments are not folded
|
||||
- **WHEN** a document contains a single-line comment that does not extend across multiple lines and is not part of a larger contiguous comment block
|
||||
- **THEN** the server MUST NOT return a folding range for that comment
|
||||
|
||||
#### Scenario: Existing structural regions remain foldable
|
||||
- **WHEN** a document contains a multi-line namespace, record, function body, parameter list, lambda body, initializer list, or other supported structural region already collected by clice
|
||||
- **THEN** the server MUST continue to return a folding range for that region if its boundaries can be mapped back to the main file
|
||||
|
||||
### Requirement: Preprocessor regions fold as complete branch blocks
|
||||
The server SHALL provide complete and nested folding ranges for preprocessor branch structures instead of leaving the final branch in a conditional block unclosed.
|
||||
|
||||
#### Scenario: Final conditional branch closes at endif
|
||||
- **WHEN** a document contains a `#if/#elif/#else/#endif` chain
|
||||
- **THEN** the server MUST generate a folding range for each multi-line branch body, including the last branch body that ends at `#endif`
|
||||
|
||||
#### Scenario: Inactive conditional branches can be folded
|
||||
- **WHEN** a conditional branch is known to be inactive or skipped in the current preprocessing configuration
|
||||
- **THEN** the server MUST be able to return a folding range covering that inactive branch region using `kind = region`
|
||||
|
||||
#### Scenario: Nested pragma regions are folded
|
||||
- **WHEN** a document contains nested `#pragma region` / `#pragma endregion` pairs in the main file
|
||||
- **THEN** the server MUST return properly nested folding ranges for each matched region pair
|
||||
|
||||
### Requirement: C/C++ directive groups and multiline macros are foldable
|
||||
The server SHALL use clice's preprocessor metadata to expose foldable ranges that clangd does not currently provide.
|
||||
|
||||
#### Scenario: Multi-line macro definitions can be folded
|
||||
- **WHEN** a document contains a multi-line macro definition whose body spans more than one physical line
|
||||
- **THEN** the server MUST return a folding range for that macro definition using `kind = region`
|
||||
|
||||
#### Scenario: Consecutive include directives are grouped
|
||||
- **WHEN** a document contains a contiguous block of `#include` directives with no intervening non-trivia code lines
|
||||
- **THEN** the server MUST return a folding range covering that include block using `kind = imports`
|
||||
|
||||
#### Scenario: Consecutive module imports are grouped
|
||||
- **WHEN** a document contains a contiguous block of C++ module `import` declarations with no intervening non-trivia code lines
|
||||
- **THEN** the server MUST return a folding range covering that import block using `kind = imports`
|
||||
29
openspec/changes/improve-folding-range-support/tasks.md
Normal file
29
openspec/changes/improve-folding-range-support/tasks.md
Normal file
@@ -0,0 +1,29 @@
|
||||
## 1. Folding Range Pipeline
|
||||
|
||||
- [ ] 1.1 Refactor `src/feature/folding_ranges.cpp` so collection, normalization, and LSP rendering are separated by an internal raw-range model.
|
||||
- [ ] 1.2 Add folding render options for `lineFoldingOnly`, `collapsedText` support, and optional `rangeLimit`, and thread them through the folding request path.
|
||||
- [ ] 1.3 Replace direct exposure of clice-specific public folding kinds with a stable mapping to standard LSP `comment` / `imports` / `region` kinds.
|
||||
|
||||
## 2. Comment and Structural Baseline
|
||||
|
||||
- [ ] 2.1 Add a comment collector that folds multi-line block comments and contiguous multi-line `//` comment groups in the main file.
|
||||
- [ ] 2.2 Preserve existing AST structural folding behavior while routing it through the new normalization/rendering pipeline.
|
||||
- [ ] 2.3 Add focused unit tests for comment folding, single-line comment exclusion, and structural folding regressions.
|
||||
|
||||
## 3. Preprocessor Folding
|
||||
|
||||
- [ ] 3.1 Rework conditional-directive collection so each `#if/#elif/#else/#endif` branch body closes correctly, including the final branch ending at `#endif`.
|
||||
- [ ] 3.2 Add folding support for inactive conditional branches using the existing preprocessor condition metadata.
|
||||
- [ ] 3.3 Strengthen `#pragma region` handling and add assertion-based tests for nested region folding.
|
||||
|
||||
## 4. Clice-Specific Folding Extensions
|
||||
|
||||
- [ ] 4.1 Add folding ranges for multi-line macro definitions using `directive.macros` and stable main-file source ranges.
|
||||
- [ ] 4.2 Add grouping folds for contiguous `#include` blocks and return them as `imports` ranges.
|
||||
- [ ] 4.3 Add grouping folds for contiguous C++ module `import` declarations and cover mixed include/import layouts with tests.
|
||||
|
||||
## 5. Protocol and Validation
|
||||
|
||||
- [ ] 5.1 Capture client folding capabilities during initialize and use them when serving `textDocument/foldingRange`.
|
||||
- [ ] 5.2 Add integration coverage for `lineFoldingOnly`, `collapsedText` gating, and kind/output compatibility.
|
||||
- [ ] 5.3 Run the relevant folding-range unit and integration tests, then fix any ordering, deduplication, or boundary regressions found during verification.
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-04-03
|
||||
@@ -0,0 +1,141 @@
|
||||
## Context
|
||||
|
||||
`clice` already exposes `textDocument/semanticTokens/full` and advertises a legend derived from `SymbolKind` and `SymbolModifiers`, but the implementation in `src/feature/semantic_tokens.cpp` only emits:
|
||||
|
||||
- lexical tokens such as comments, strings, numbers, directives, headers, and keywords
|
||||
- declaration/definition markers for named declarations and macros
|
||||
- a template marker for templated declarations
|
||||
|
||||
This leaves most of the declared semantic surface unused. In particular, `SymbolKind::Module`, `Attribute`, `Operator`, `Paren`, `Bracket`, `Brace`, `Angle`, `Concept`, and several declaration kinds/modifiers are either never emitted or emitted only indirectly. The current merge logic also degrades overlapping classifications to `Conflict`, which throws away useful semantic information instead of preferring the more specific token.
|
||||
|
||||
The gap is visible when compared with clangd: clangd classifies more AST constructs, supports semantic token delta responses with `resultId`, and refreshes tokens when a more accurate AST becomes available. `clice` also already contains C++20 module infrastructure, `directive.imports`, `CompilationUnitRef::module_name()`, and a reserved `handleModuleOccurrence()` hook, so module-aware semantic tokens fit the current architecture rather than requiring a new subsystem.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- Emit materially richer semantic token kinds and modifiers from AST information instead of relying mostly on lexical fallback.
|
||||
- Highlight C++20 module declarations and imports, including named modules and partitions.
|
||||
- Replace `Conflict`-style overlap handling with deterministic precedence so semantic meaning is preserved.
|
||||
- Support semantic token `resultId` and `/full/delta` responses, with cache invalidation and refresh when rebuilds change highlighting.
|
||||
- Add tests that lock in token kinds, modifiers, and module-specific behavior.
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- Achieve byte-for-byte parity with clangd's full highlighting matrix in a single change.
|
||||
- Add `textDocument/semanticTokens/range`.
|
||||
- Add end-user configuration for disabling token kinds/modifiers in this change.
|
||||
- Redesign the broader `SymbolKind` taxonomy used by completion, hover, or indexing.
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. Keep the existing two-phase model, but turn semantic tokens into an overlay with precedence
|
||||
|
||||
The collector will continue to start with a lexical pass so comments, literals, directives, and headers remain available even before AST-specific enrichment. Semantic passes will then add higher-priority tokens for declarations, operators, attributes, concepts, brackets, and modules.
|
||||
|
||||
Instead of collapsing equal-range overlaps to `Conflict`, the merge step will apply explicit precedence:
|
||||
|
||||
- semantic classification beats lexical classification
|
||||
- more specific semantic kinds beat generic ones
|
||||
- modifiers are merged when the underlying symbol identity is compatible
|
||||
- `Conflict` remains only as a last-resort debugging state, not the normal output
|
||||
|
||||
Why this approach:
|
||||
|
||||
- it preserves the current fast lexical baseline
|
||||
- it minimizes churn in call sites
|
||||
- it matches how editors expect semantic tokens to augment syntax coloring rather than erase it
|
||||
|
||||
Alternative considered:
|
||||
|
||||
- emit semantic tokens from AST only and drop lexical fallback entirely. Rejected because it would lose highlighting for comments/directives/headers and make partially-built AST states visibly worse.
|
||||
|
||||
### 2. Use existing compile-layer module metadata for module token extraction
|
||||
|
||||
Module highlighting will use data already available in `CompilationUnitRef` and compile directives:
|
||||
|
||||
- `CompilationUnitRef::module_name()` and `is_module_interface_unit()` for the current unit
|
||||
- `directives().imports` for imported module names
|
||||
- `TokenBuffer` / spelled token APIs for mapping module names and separators to source ranges
|
||||
|
||||
This is preferred over trying to rely only on `VisitImportDecl` because the repository already captures import metadata during preprocessing, and the current `SemanticVisitor` module hooks are intentionally incomplete. The design can still route final token creation through `handleModuleOccurrence()`, but the source of truth for locating module-name tokens should be the compile-layer token data.
|
||||
|
||||
Alternative considered:
|
||||
|
||||
- finish the commented-out `SemanticVisitor::VisitImportDecl` path first and derive all module tokens from AST traversal. Rejected for now because preprocessing already records import structure reliably, while AST-only extraction is more fragile around partitions and special module forms.
|
||||
|
||||
### 3. Fill the most valuable semantic gaps first
|
||||
|
||||
The implementation should prioritize the categories that are already modeled in `SymbolKind`/`SymbolModifiers` and easy to validate in tests:
|
||||
|
||||
- declaration kinds already discoverable from `NamedDecl`
|
||||
- module names and partitions
|
||||
- attributes such as `final` / `override` and explicit attribute syntax
|
||||
- overloaded and built-in operators
|
||||
- bracket-like punctuation tied to templates and explicit operators/casts
|
||||
- modifiers such as `Const`, `Overloaded`, and `Typed` where the current AST plumbing can support them without ambiguity
|
||||
|
||||
Why this scope:
|
||||
|
||||
- it closes the largest visible gap quickly
|
||||
- it avoids inventing new token kinds before the existing taxonomy is exercised
|
||||
- it keeps the change aligned with the user's clangd comparison request
|
||||
|
||||
Alternative considered:
|
||||
|
||||
- introduce new token kinds/modifiers to mirror clangd more directly. Rejected because `clice` already has an internal taxonomy and the first priority is to use it consistently.
|
||||
|
||||
### 4. Add delta responses in the server layer, not inside feature collection
|
||||
|
||||
`feature::semantic_tokens()` should continue returning a full logical token stream for one snapshot of a document. Delta computation, `resultId` assignment, and cache management belong in the server/request layer because they depend on per-document history rather than AST semantics.
|
||||
|
||||
Expected server changes:
|
||||
|
||||
- advertise `semanticTokensProvider.full.delta = true`
|
||||
- accept `textDocument/semanticTokens/full/delta`
|
||||
- cache the last encoded token stream per document
|
||||
- invalidate the cache on close, rebuild, and version changes that replace the document snapshot
|
||||
- send `workspace/semanticTokens/refresh` when the server has moved from a stale/partial semantic view to a fresh compiled view and the client supports refresh
|
||||
|
||||
Alternative considered:
|
||||
|
||||
- compute deltas in the feature layer on raw tokens. Rejected because delta state must be keyed by document lifecycle and LSP request history, which the feature layer does not own.
|
||||
|
||||
### 5. Treat tests as the contract for token stability
|
||||
|
||||
This change will expand test coverage instead of relying on manual editor inspection alone:
|
||||
|
||||
- unit tests for token kinds/modifiers in small annotated snippets
|
||||
- unit tests for merge precedence and multiline encoding invariants
|
||||
- integration tests for full-token and delta requests
|
||||
- module fixture tests covering named modules, partitions, global module fragments, private fragments, and importers
|
||||
|
||||
Alternative considered:
|
||||
|
||||
- validate only through smoke tests in an editor. Rejected because token regressions are subtle and easy to reintroduce without precise assertions.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [Token overlap rules become too ad hoc] → Define a small explicit precedence table and test it directly instead of scattering special cases through the merge code.
|
||||
- [Template angle brackets and parser-token splitting differ from `TokenBuffer` behavior] → Scope bracket highlighting to ranges that can be proven from spelled tokens and add regression tests for nested templates.
|
||||
- [Module syntax has corner cases such as global module fragments, private fragments, and partitions] → Require stable highlighting only for named module/import identifiers and avoid inventing semantics for punctuation that cannot be located robustly.
|
||||
- [Delta cache becomes stale after recompilation or close/reopen cycles] → Tie cache lifetime to document URI/version and clear it on close, invalidation, and failed rebuilds.
|
||||
- [More semantic passes increase feature latency] → Keep lexical scanning unchanged, reuse existing AST traversal hooks, and benchmark only if tests show material slowdown.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. Extend token collection and merge rules while preserving the current full-token API.
|
||||
2. Add module-aware token extraction and expand unit tests.
|
||||
3. Wire server-side delta support and refresh/invalidation behavior.
|
||||
4. Update integration tests to cover both full and delta semantic token requests.
|
||||
|
||||
Rollback strategy:
|
||||
|
||||
- disable delta advertisement and continue serving full tokens if cache invalidation proves unstable
|
||||
- keep lexical fallback so the editor still receives usable highlighting even if some semantic categories are temporarily rolled back
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Whether `Const`, `Typed`, and `Overloaded` should be emitted only when derived with high confidence, or whether some heuristic cases are acceptable.
|
||||
- Whether refresh notifications should be sent only after successful recompilation or also after dependency-driven module invalidation.
|
||||
- Whether bracket/operator highlighting should be limited to the kinds already modeled in `SymbolKind`, or whether a follow-up change should align names more closely with standard LSP token vocabularies.
|
||||
@@ -0,0 +1,28 @@
|
||||
## Why
|
||||
|
||||
`clice` already advertises semantic tokens, but the current implementation only covers a narrow subset of the available `SymbolKind` and modifier space. Compared with clangd, it is missing several high-value semantic classifications, has no protocol support for semantic token deltas or refresh, and leaves C++20 module names unhighlighted even though the semantic visitor already reserves hooks for them.
|
||||
|
||||
Improving this now will make highlighting materially more accurate in real C++ code, reduce the visible gap with clangd, and let `clice`'s existing C++20 module pipeline surface richer editor feedback instead of treating modules as plain identifiers.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Expand semantic token collection beyond lexical tokens and basic declaration/reference handling so `clice` can emit more of its declared `SymbolKind` and modifier set.
|
||||
- Add AST-driven classification for operators, bracket-like punctuation, attributes, concepts, and other symbols that currently fall back to plain lexical highlighting or are omitted entirely.
|
||||
- Implement semantic highlighting for C++20 module declarations and imports, including named modules and partitions.
|
||||
- Improve token conflict resolution so overlapping lexical and semantic classifications prefer the most specific semantic meaning instead of collapsing to `Conflict`.
|
||||
- Add server-side support for semantic token result IDs and delta updates, with refresh support wired where document recompilation changes highlighting.
|
||||
- Strengthen unit and integration coverage for semantic tokens, especially module-specific fixtures and multi-token semantic cases.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `semantic-tokens`: Provide richer semantic token classification for C++ code, including AST-derived symbol kinds, modifiers, incremental token delivery, and C++20 module-aware highlighting.
|
||||
|
||||
### Modified Capabilities
|
||||
- None.
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected code: `src/feature/semantic_tokens.cpp`, `src/semantic/semantic_visitor.h`, semantic token request handling in the server, and semantic token protocol wiring.
|
||||
- Affected tests: `tests/unit/feature/semantic_tokens_tests.cpp`, server integration tests, and module fixtures under `tests/data/modules/`.
|
||||
- User-visible behavior: editors will receive more accurate semantic token kinds/modifiers and module-aware highlighting without requiring API-breaking changes.
|
||||
@@ -0,0 +1,50 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Rich semantic token classification
|
||||
The server SHALL classify semantic tokens using AST-derived symbol kinds and modifiers when that information is available, instead of limiting output to lexical token classes and basic declaration markers.
|
||||
|
||||
#### Scenario: Declaration kinds use semantic classification
|
||||
- **WHEN** a document contains declarations and references for namespaces, records, enums, functions, methods, fields, variables, parameters, labels, concepts, or macros
|
||||
- **THEN** the semantic token response MUST encode those occurrences with the corresponding `SymbolKind` rather than a generic lexical fallback
|
||||
|
||||
#### Scenario: Semantic modifiers are preserved
|
||||
- **WHEN** a token represents a declaration, definition, template-related symbol, const-qualified symbol, overloaded symbol, or typed punctuation that `clice` can determine from the AST
|
||||
- **THEN** the semantic token response MUST include the corresponding `SymbolModifiers` bits for that token
|
||||
|
||||
#### Scenario: Semantic meaning wins over lexical overlap
|
||||
- **WHEN** lexical and semantic passes produce overlapping classifications for the same source token
|
||||
- **THEN** the server MUST prefer the most specific semantic classification and MUST NOT degrade the final response to `Conflict` for normal supported cases
|
||||
|
||||
### Requirement: C++20 module-aware semantic highlighting
|
||||
The server SHALL emit semantic tokens for C++20 module declarations and imports so module names and partitions are distinguishable from ordinary identifiers.
|
||||
|
||||
#### Scenario: Named module declaration is highlighted
|
||||
- **WHEN** a module interface or implementation unit contains a named module declaration such as `export module Foo;` or `module Foo;`
|
||||
- **THEN** the `module` keyword MUST be highlighted as a keyword and the module-name tokens MUST be highlighted with `SymbolKind::Module`
|
||||
|
||||
#### Scenario: Module import is highlighted
|
||||
- **WHEN** a translation unit imports a named module or partition such as `import Foo;` or `import Foo:Bar;`
|
||||
- **THEN** the `import` keyword MUST be highlighted as a keyword and the imported module-name tokens MUST be highlighted with `SymbolKind::Module`
|
||||
|
||||
#### Scenario: Non-module fragments do not invent module-name tokens
|
||||
- **WHEN** a file contains module-related fragment syntax such as `module;` or `module :private;`
|
||||
- **THEN** the server MUST highlight the language keywords normally and MUST only emit `SymbolKind::Module` tokens for the actual named module identifiers that exist in source
|
||||
|
||||
### Requirement: Semantic token incremental delivery
|
||||
The server SHALL support stable semantic token result IDs and delta responses for documents whose previous semantic token result is known.
|
||||
|
||||
#### Scenario: Full request returns a result identifier
|
||||
- **WHEN** a client sends `textDocument/semanticTokens/full`
|
||||
- **THEN** the response MUST include the full semantic token data for the current document snapshot and a `resultId` that can be used for a later delta request
|
||||
|
||||
#### Scenario: Delta request returns edits when previous result matches
|
||||
- **WHEN** a client sends `textDocument/semanticTokens/full/delta` with the latest known `resultId` for a document
|
||||
- **THEN** the server MUST return semantic token edits relative to that previous result instead of forcing a full token payload
|
||||
|
||||
#### Scenario: Delta request falls back to full tokens when history is unavailable
|
||||
- **WHEN** a client sends `textDocument/semanticTokens/full/delta` with an unknown or stale `resultId`
|
||||
- **THEN** the server MUST return a full semantic token result for the current document snapshot
|
||||
|
||||
#### Scenario: Rebuild-driven semantic changes trigger refresh
|
||||
- **WHEN** recompilation, dependency invalidation, or module rebuild changes the semantic token output for an open document and the client supports semantic token refresh
|
||||
- **THEN** the server MUST request a semantic token refresh so the client can fetch updated tokens
|
||||
@@ -0,0 +1,23 @@
|
||||
## 1. Semantic Token Collection
|
||||
|
||||
- [ ] 1.1 Audit `src/feature/semantic_tokens.cpp` against `SymbolKind` and `SymbolModifiers`, and add collection paths for the high-value AST-derived kinds/modifiers covered by the spec.
|
||||
- [ ] 1.2 Replace equal-range `Conflict` collapsing with explicit token precedence and modifier-merging rules, and add focused unit tests for overlap behavior.
|
||||
- [ ] 1.3 Extend token encoding tests to cover newly emitted semantic kinds/modifiers without regressing multiline and UTF-8/UTF-16 behavior.
|
||||
|
||||
## 2. C++20 Module Highlighting
|
||||
|
||||
- [ ] 2.1 Implement module-name token extraction for named module declarations using existing `CompilationUnitRef`, directive, and token-buffer APIs.
|
||||
- [ ] 2.2 Implement module-name token extraction for `import` statements, including partition forms such as `Foo:Bar`.
|
||||
- [ ] 2.3 Add regression tests for named modules, imports, global module fragments, private fragments, and dotted/partitioned module fixtures.
|
||||
|
||||
## 3. LSP Semantic Token Delivery
|
||||
|
||||
- [ ] 3.1 Extend server capability advertisement and request routing to support `textDocument/semanticTokens/full/delta`.
|
||||
- [ ] 3.2 Add per-document semantic token caching with stable `resultId` handling and full-result fallback for stale delta requests.
|
||||
- [ ] 3.3 Invalidate semantic token caches on document lifecycle changes and trigger semantic token refresh when recompilation changes highlighting for open documents.
|
||||
|
||||
## 4. Validation
|
||||
|
||||
- [ ] 4.1 Add or expand unit tests for operators, attributes, concepts, bracket-like tokens, and semantic modifiers that are newly emitted.
|
||||
- [ ] 4.2 Add integration coverage for semantic token full and delta requests in the server test suite.
|
||||
- [ ] 4.3 Run the relevant semantic token and module test targets, then fix any fixture or behavior mismatches discovered during verification.
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-04-03
|
||||
@@ -0,0 +1,115 @@
|
||||
## Context
|
||||
|
||||
`clice` currently implements signature help in `src/feature/signature_help.cpp` by subclassing `clang::CodeCompleteConsumer`, iterating overload candidates, and manually rendering labels for a handful of candidate kinds. That gives a usable baseline, but it leaves several visible gaps compared with clangd:
|
||||
|
||||
- the collector always picks `active_signature = 0` and largely forwards `current_arg` directly, without clangd-style remapping for variadics or other candidate-specific parameter lists
|
||||
- signature labels are assembled manually rather than from Clang's `CodeCompletionString`, so optional parameters, documentation chunks, and rendered parameter metadata are less faithful
|
||||
- the server advertises a bare `SignatureHelpOptions{}` rather than clangd-like trigger and retrigger characters
|
||||
- the request path does not thread signature-help-specific client capabilities or context into feature generation
|
||||
- tests only verify a minimal overload case and non-crash request flow, while clangd covers overload ordering, function pointers, constructors, aggregates, nested expressions, variadics, templates, stale preambles, and prerequisite modules
|
||||
|
||||
clangd's implementation is a good reference because it solves these problems without inventing a separate subsystem: it still uses overload candidates, but builds signatures from `CodeCompletionString`, maps active arguments to rendered parameters, negotiates documentation format and offset support, and validates the behavior with broad regression coverage.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- Align `clice`'s user-visible signature help behavior with clangd for the major C++ call forms that users hit in practice.
|
||||
- Advertise and honor signature-help protocol details that affect editor behavior, especially trigger characters, documentation format, and parameter label offsets.
|
||||
- Make active-parameter selection robust for variadic, defaulted, templated, constructor, aggregate, and function-pointer signatures.
|
||||
- Add test coverage strong enough to prevent the current behavior gap from reopening.
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- Achieve byte-for-byte parity with every clangd-only extension or internal API.
|
||||
- Redesign completion, scheduling, or the broader server architecture outside what signature help needs.
|
||||
- Introduce unrelated LSP features such as snippet completion changes or semantic-token work in this change.
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. Keep the overload-candidate pipeline, but switch rendering to `CodeCompletionString`
|
||||
|
||||
`clice` should keep using `clang::CodeCompleteConsumer` and `ProcessOverloadCandidates()`, but the collector should stop hand-formatting signatures for each candidate kind wherever Clang already provides a `CodeCompletionString`. This matches clangd's approach and brings several benefits at once:
|
||||
|
||||
- rendered labels stay closer to Clang's own completion view
|
||||
- placeholder and optional chunks can drive parameter extraction directly
|
||||
- signature and parameter documentation become available without bespoke formatting logic
|
||||
|
||||
Alternative considered:
|
||||
|
||||
- keep the current manual `switch(candidate.getKind())` rendering and patch missing cases individually. Rejected because it duplicates logic clangd already solved and makes edge cases like optional parameters and documentation much harder to maintain.
|
||||
|
||||
### 2. Treat active parameter as a negotiated LSP field backed by candidate-aware remapping
|
||||
|
||||
The top-level `SignatureHelp.activeParameter` should remain the primary LSP signal, but `clice` should remap the current argument index against the rendered parameter list for the active candidate instead of forwarding `current_arg` blindly. In particular:
|
||||
|
||||
- variadic calls should clamp to the final variadic parameter
|
||||
- constructor and aggregate initializers should map to the field or constructor slot actually being edited
|
||||
- function-pointer and template cases should use the rendered parameter list rather than raw AST counts when they differ
|
||||
|
||||
Alternative considered:
|
||||
|
||||
- add per-signature active-parameter support first. Rejected for now because clangd does not rely on it either, and the highest-value fix is correct top-level `activeParameter` behavior.
|
||||
|
||||
### 3. Thread signature-help capabilities and context through the server and worker boundary
|
||||
|
||||
The feature layer needs more than just file path and offset. This change should carry enough request metadata to shape the output:
|
||||
|
||||
- requested documentation format
|
||||
- whether parameter label offsets are supported
|
||||
- any signature-help request context needed for retrigger behavior or follow-up refinements
|
||||
|
||||
This keeps protocol negotiation in the server layer while allowing feature code to decide which fields to populate.
|
||||
|
||||
Alternative considered:
|
||||
|
||||
- hardcode plain-text output and always return offset labels regardless of client support. Rejected because clangd already demonstrates that clients negotiate these details, and ignoring them makes interoperability less predictable.
|
||||
|
||||
### 4. Advertise clangd-like trigger coverage, but only promise behavior that `clice` can validate
|
||||
|
||||
The server should advertise trigger and retrigger characters for the call and initializer delimiters that matter in C++ editing: `(`, `)`, `{`, `}`, `<`, `>`, and `,`. This is close to clangd and matches the call forms already exercised by clangd's tests.
|
||||
|
||||
The implementation should still avoid overpromising beyond validated behavior. If a trigger reaches an unsupported syntactic corner case, the request may still return no signatures, but the advertised surface should cover the common forms users expect in editors.
|
||||
|
||||
Alternative considered:
|
||||
|
||||
- advertise only `(` and `,`. Rejected because it would keep `clice` visibly behind clangd for constructors, aggregates, and template-argument contexts.
|
||||
|
||||
### 5. Use clangd's existing tests as the baseline for `clice` regression coverage
|
||||
|
||||
This change should not invent a new test matrix from scratch. Instead, it should translate the highest-value clangd signature-help cases into `clice`'s unit, worker, integration, and module test suites:
|
||||
|
||||
- overloads and ordering
|
||||
- default and optional arguments
|
||||
- active argument detection inside nested expressions
|
||||
- constructors, aggregates, and function pointers
|
||||
- variadics and template arguments
|
||||
- imported declarations and prerequisite module builds
|
||||
|
||||
Alternative considered:
|
||||
|
||||
- rely on manual editor checks and the current smoke tests. Rejected because most of the gap is in edge cases that regress silently without precise assertions.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [Eventide LSP bindings do not expose all signature-help capability fields] -> Extend the local protocol surface or add thin adapter fields in `clice`'s request path, then lock that behavior down with serialization tests.
|
||||
- [Switching to `CodeCompletionString` changes overload ordering or rendered labels unexpectedly] -> Keep ordering rules explicit and update tests to assert the intended contract rather than incidental string formatting.
|
||||
- [Documentation lookup adds latency or becomes stale] -> Prefer AST docs when immediately available, use bounded index fallback where it materially improves output, and keep empty-documentation paths cheap.
|
||||
- [Module and imported-declaration cases depend on prerequisite build state] -> Reuse the existing module test infrastructure and make module-specific coverage part of the acceptance criteria instead of an optional follow-up.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. Extend protocol and options plumbing so signature-help requests can carry client capability details and the server can advertise trigger coverage.
|
||||
2. Refactor the collector to use richer completion-string data, fix active-parameter mapping, and add documentation support.
|
||||
3. Add clangd-inspired unit, worker, integration, and module tests until the intended behavior is covered end to end.
|
||||
|
||||
Rollback strategy:
|
||||
|
||||
- revert to the previous manual rendering path while keeping the safer request plumbing if the richer collector proves unstable
|
||||
- reduce advertised trigger coverage temporarily if a specific delimiter family produces repeated false positives
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Whether the current eventide protocol types already carry enough signature-help context, or whether `clice` needs to extend its local wrappers.
|
||||
- Whether parameter-level documentation is worth populating in this change, or whether signature-level documentation is sufficient for parity.
|
||||
- Whether `clice` should exactly mirror clangd's trigger set or trim it slightly if one delimiter family remains under-tested after implementation.
|
||||
@@ -0,0 +1,24 @@
|
||||
## Why
|
||||
|
||||
`clice` already returns basic signature help, but compared with clangd it mostly exposes raw overload candidates and misses several behaviors that make signature help reliable in real code: protocol trigger coverage, documentation, robust active-parameter mapping, and regression-tested handling for constructors, aggregates, variadics, templates, and imported declarations. Closing this gap now will make call-site assistance materially more useful and establish a spec-backed contract for one of `clice`'s core interactive features.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Audit `clice`'s current signature help implementation against clangd and close the highest-value gaps that affect LSP-visible behavior.
|
||||
- Improve signature construction so parameter labels, optional/defaulted arguments, variadics, templates, constructors, aggregates, and function-pointer calls are rendered consistently and the active parameter is chosen correctly.
|
||||
- Extend the server and request path to advertise and honor signature-help protocol details such as trigger/retrigger characters, documentation format, and parameter label offsets where supported by the client and protocol layer.
|
||||
- Add coverage for imported or module-provided declarations and other clangd-tested edge cases so signature help stays stable as the compiler pipeline evolves.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `signature-help`: Provide clangd-aligned signature help behavior for callable expressions, including robust active-parameter tracking, richer signature metadata, protocol negotiation, and regression-tested edge-case coverage.
|
||||
|
||||
### Modified Capabilities
|
||||
- None.
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected code: `src/feature/signature_help.cpp`, `src/feature/feature.h`, `src/server/master_server.cpp`, `src/server/stateless_worker.cpp`, `src/server/protocol.h`, and any eventide protocol bindings touched by signature-help capability negotiation.
|
||||
- Affected tests: `tests/unit/feature/signature_help_tests.cpp`, `tests/unit/server/stateless_worker_tests.cpp`, `tests/integration/test_server.py`, and new fixtures inspired by `.llvm/clang-tools-extra/clangd/unittests/CodeCompleteTests.cpp`.
|
||||
- User-visible behavior: editors should receive more complete and predictable signature help, especially while typing nested calls, constructor or aggregate initializers, variadic calls, and declarations coming from imported modules or prerequisite module builds.
|
||||
@@ -0,0 +1,52 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Server advertises signature help trigger coverage
|
||||
The server SHALL advertise a signature help provider that includes the trigger and retrigger characters needed to keep signature help updated while the user edits callable and initializer argument lists.
|
||||
|
||||
#### Scenario: Initialize returns signature help triggers
|
||||
- **WHEN** a client initializes against the server
|
||||
- **THEN** the advertised `signatureHelpProvider` includes trigger support for `(`, `)`, `{`, `}`, `<`, `>`, and `,`
|
||||
|
||||
### Requirement: Signature help returns overload sets and the active parameter
|
||||
The server SHALL return all applicable signature candidates for the callable at the cursor and SHALL report the active signature and active parameter for the call being edited.
|
||||
|
||||
#### Scenario: Overloaded function call
|
||||
- **WHEN** the cursor is inside a call to an overloaded function
|
||||
- **THEN** the result contains each viable overload signature
|
||||
- **THEN** the result identifies the active parameter for the preferred signature
|
||||
|
||||
### Requirement: Signature help maps arguments to rendered parameters across C++ call forms
|
||||
The server SHALL remap the current argument index to the correct rendered parameter for variadic, defaulted, templated, constructor, aggregate, and function-pointer signatures instead of exposing raw argument indices when they exceed or differ from the displayed parameter list.
|
||||
|
||||
#### Scenario: Variadic call stays on the variadic parameter
|
||||
- **WHEN** the user types beyond the fixed parameters of a variadic callable
|
||||
- **THEN** the reported active parameter remains the final variadic parameter instead of advancing past the signature
|
||||
|
||||
#### Scenario: Constructor or aggregate initializer
|
||||
- **WHEN** the cursor is inside a constructor call or aggregate braced initializer
|
||||
- **THEN** the result describes the constructor or aggregate fields being initialized
|
||||
- **THEN** the reported active parameter matches the current initializer slot
|
||||
|
||||
### Requirement: Signature help returns stable parameter metadata and documentation
|
||||
The server SHALL return parameter label metadata aligned with the rendered signature label, and SHALL include signature documentation in the format negotiated with the client when documentation is available from AST comments or indexed symbols.
|
||||
|
||||
#### Scenario: Client supports label offsets and markdown documentation
|
||||
- **WHEN** the client declares parameter label offset support and markdown signature documentation
|
||||
- **THEN** returned signatures include offset-based parameter labels
|
||||
- **THEN** returned signatures include markdown-formatted documentation when documentation is available
|
||||
|
||||
#### Scenario: Callable without documentation
|
||||
- **WHEN** a callable has no available documentation
|
||||
- **THEN** the server still returns the signature label and parameter metadata without failing the request
|
||||
|
||||
### Requirement: Signature help works for imported declarations and nested expressions
|
||||
The server SHALL provide signature help for call sites that resolve through imported or prerequisite-module declarations and for cursors nested inside expressions within argument lists.
|
||||
|
||||
#### Scenario: Imported module declaration
|
||||
- **WHEN** the user requests signature help for a function imported from a prerequisite C++20 module
|
||||
- **THEN** the result includes the imported declaration's signature
|
||||
|
||||
#### Scenario: Cursor inside nested argument expression
|
||||
- **WHEN** the cursor is inside an expression that itself appears in an argument position
|
||||
- **THEN** the server reports signature help for the innermost active call
|
||||
- **THEN** the reported active parameter matches that nested call site
|
||||
@@ -0,0 +1,21 @@
|
||||
## 1. Protocol And Request Plumbing
|
||||
|
||||
- [ ] 1.1 Extend signature-help capability advertisement and request plumbing to carry trigger or retrigger characters, documentation format, parameter label-offset support, and any required request context across the server and worker boundary.
|
||||
- [ ] 1.2 Expand `SignatureHelpOptions` and related protocol serialization tests so feature code can choose output shape based on negotiated client capabilities.
|
||||
|
||||
## 2. Signature Collection Parity
|
||||
|
||||
- [ ] 2.1 Refactor `src/feature/signature_help.cpp` to build labels and parameters from richer completion-string data instead of relying only on manual overload formatting.
|
||||
- [ ] 2.2 Add candidate-aware active-parameter mapping for variadic, defaulted, templated, constructor, aggregate, and function-pointer signatures, keeping overload ordering deterministic.
|
||||
- [ ] 2.3 Populate signature documentation and parameter metadata from AST comments and index or preamble lookups where available, without regressing empty-documentation cases.
|
||||
|
||||
## 3. Edge Cases And Module Coverage
|
||||
|
||||
- [ ] 3.1 Verify signature help for nested calls, opening delimiters, braced initializers, function pointers, and imported or module-provided declarations, fixing any parser or snapshot assumptions that block these cases.
|
||||
- [ ] 3.2 Add or update worker and server handling needed for stale compile state and prerequisite module builds so signature help remains available after document edits and module preparation.
|
||||
|
||||
## 4. Validation
|
||||
|
||||
- [ ] 4.1 Expand `tests/unit/feature/signature_help_tests.cpp` with clangd-inspired cases for overloads, active arguments, default arguments, variadics, templates, constructors, aggregates, and nested expressions.
|
||||
- [ ] 4.2 Add server and worker tests that assert capability advertisement and returned signature metadata instead of only checking for non-crash behavior.
|
||||
- [ ] 4.3 Run the relevant signature-help, stateless-worker, integration, and module test targets, then fix any regressions uncovered during verification.
|
||||
20
openspec/config.yaml
Normal file
20
openspec/config.yaml
Normal file
@@ -0,0 +1,20 @@
|
||||
schema: spec-driven
|
||||
|
||||
# Project context (optional)
|
||||
# This is shown to AI when creating artifacts.
|
||||
# Add your tech stack, conventions, style guides, domain knowledge, etc.
|
||||
# Example:
|
||||
# context: |
|
||||
# Tech stack: TypeScript, React, Node.js
|
||||
# We use conventional commits
|
||||
# Domain: e-commerce platform
|
||||
|
||||
# Per-artifact rules (optional)
|
||||
# Add custom rules for specific artifacts.
|
||||
# Example:
|
||||
# rules:
|
||||
# proposal:
|
||||
# - Keep proposals under 500 words
|
||||
# - Always include a "Non-goals" section
|
||||
# tasks:
|
||||
# - Break tasks into chunks of max 2 hours
|
||||
239
pixi.lock
generated
239
pixi.lock
generated
@@ -18,9 +18,6 @@ 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
|
||||
@@ -35,7 +32,6 @@ 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
|
||||
@@ -48,7 +44,6 @@ 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
|
||||
@@ -86,7 +81,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/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-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/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:
|
||||
@@ -96,9 +91,6 @@ 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
|
||||
@@ -106,7 +98,6 @@ 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
|
||||
@@ -118,7 +109,6 @@ 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
|
||||
@@ -150,7 +140,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/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-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/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:
|
||||
@@ -158,15 +148,12 @@ 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
|
||||
@@ -203,7 +190,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/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-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/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:
|
||||
@@ -383,9 +370,6 @@ 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
|
||||
@@ -399,7 +383,6 @@ 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
|
||||
@@ -412,7 +395,6 @@ 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
|
||||
@@ -454,7 +436,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/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-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/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:
|
||||
@@ -464,16 +446,12 @@ 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
|
||||
@@ -485,7 +463,6 @@ 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
|
||||
@@ -521,7 +498,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/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-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/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:
|
||||
@@ -529,14 +506,11 @@ 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
|
||||
@@ -576,7 +550,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/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-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/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:
|
||||
@@ -829,21 +803,6 @@ 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
|
||||
@@ -858,20 +817,6 @@ 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
|
||||
@@ -885,18 +830,6 @@ 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
|
||||
@@ -908,33 +841,6 @@ 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
|
||||
@@ -960,65 +866,6 @@ 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
|
||||
@@ -1539,57 +1386,6 @@ 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
|
||||
@@ -1965,25 +1761,8 @@ 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
|
||||
@@ -3058,10 +2837,10 @@ packages:
|
||||
requires_dist:
|
||||
- colorama>=0.4.6 ; extra == 'windows-terminal'
|
||||
requires_python: '>=3.9'
|
||||
- pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl
|
||||
name: pytest
|
||||
version: 9.0.3
|
||||
sha256: 2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9
|
||||
version: 9.0.2
|
||||
sha256: 711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b
|
||||
requires_dist:
|
||||
- colorama>=0.4 ; sys_platform == 'win32'
|
||||
- exceptiongroup>=1 ; python_full_version < '3.11'
|
||||
|
||||
64
pixi.toml
64
pixi.toml
@@ -33,7 +33,6 @@ 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]
|
||||
@@ -63,15 +62,6 @@ 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 #
|
||||
# ============================================================================== #
|
||||
@@ -104,10 +94,6 @@ 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"'
|
||||
@@ -133,6 +119,39 @@ 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 #
|
||||
# ============================================================================== #
|
||||
@@ -210,20 +229,3 @@ 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 }}"] },
|
||||
]
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
#!/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()
|
||||
69
src/clice.cc
69
src/clice.cc
@@ -1,4 +1,3 @@
|
||||
#include <csignal>
|
||||
#include <cstdint>
|
||||
#include <iostream>
|
||||
#include <print>
|
||||
@@ -16,62 +15,48 @@
|
||||
|
||||
namespace clice {
|
||||
|
||||
using deco::decl::KVStyle;
|
||||
|
||||
struct Options {
|
||||
DecoKV(style = KVStyle::JoinedOrSeparate,
|
||||
help = "Running mode: pipe, socket, stateless-worker, stateful-worker",
|
||||
required = false)
|
||||
DecoKV(names = {"--mode"};
|
||||
help = "Running mode: pipe, socket, stateless-worker, stateful-worker";
|
||||
required = false;)
|
||||
<std::string> mode;
|
||||
|
||||
DecoKV(style = KVStyle::JoinedOrSeparate, help = "Socket mode address", required = false)
|
||||
DecoKV(names = {"--host"}; help = "Socket mode address"; required = false;)
|
||||
<std::string> host = "127.0.0.1";
|
||||
|
||||
DecoKV(style = KVStyle::JoinedOrSeparate, help = "Socket mode port", required = false)
|
||||
DecoKV(names = {"--port"}; help = "Socket mode port"; required = false;)
|
||||
<int> port = 50051;
|
||||
|
||||
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 = {"--stateful-worker-count"}; help = "Number of stateful workers";
|
||||
required = false;)
|
||||
<std::uint32_t> stateful_worker_count;
|
||||
|
||||
DecoKV(style = KVStyle::JoinedOrSeparate,
|
||||
help = "Record LSP input to file for replay testing",
|
||||
required = false)
|
||||
<std::string> record;
|
||||
DecoKV(names = {"--stateless-worker-count"}; help = "Number of stateless workers";
|
||||
required = false;)
|
||||
<std::uint32_t> stateless_worker_count;
|
||||
|
||||
// Internal options (passed from master to worker processes)
|
||||
DecoKV(style = KVStyle::JoinedOrSeparate,
|
||||
names = {"--worker-memory-limit", "--worker-memory-limit="},
|
||||
required = false)
|
||||
DecoKV(names = {"--worker-memory-limit"}; help = "Memory limit per stateful worker (bytes)";
|
||||
required = false;)
|
||||
<std::uint64_t> worker_memory_limit;
|
||||
|
||||
DecoKV(style = KVStyle::JoinedOrSeparate,
|
||||
names = {"--worker-name", "--worker-name="},
|
||||
required = false)
|
||||
<std::string> worker_name;
|
||||
DecoKV(names = {"--log-level"}; help = "Log level: trace, debug, info, warn, error, off";
|
||||
required = false;)
|
||||
<std::string> log_level = "info";
|
||||
|
||||
DecoKV(style = KVStyle::JoinedOrSeparate, names = {"--log-dir", "--log-dir="}, required = false)
|
||||
<std::string> log_dir;
|
||||
DecoKV(names = {"--record"}; help = "Record LSP input to file for replay testing";
|
||||
required = false;)
|
||||
<std::string> record;
|
||||
|
||||
DecoFlag(names = {"-h", "--help"}, help = "Show help message", required = false)
|
||||
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) {
|
||||
#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 = deco::util::argvify(argc, argv);
|
||||
auto result = deco::cli::parse<clice::Options>(args);
|
||||
|
||||
@@ -112,21 +97,13 @@ 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(worker_name.empty() ? "stateless-worker"
|
||||
: worker_name,
|
||||
log_dir);
|
||||
return clice::run_stateless_worker_mode();
|
||||
}
|
||||
|
||||
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,
|
||||
worker_name.empty() ? "stateful-worker"
|
||||
: worker_name,
|
||||
log_dir);
|
||||
return clice::run_stateful_worker_mode(mem_limit);
|
||||
}
|
||||
|
||||
if(mode == "pipe") {
|
||||
|
||||
@@ -23,44 +23,6 @@ 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;
|
||||
@@ -367,8 +329,8 @@ std::size_t CompilationDatabase::load(llvm::StringRef path) {
|
||||
return entries.size();
|
||||
}
|
||||
|
||||
llvm::SmallVector<CompileCommand> CompilationDatabase::lookup(llvm::StringRef file,
|
||||
const CommandOptions& options) {
|
||||
llvm::SmallVector<CompilationContext> CompilationDatabase::lookup(llvm::StringRef file,
|
||||
const CommandOptions& options) {
|
||||
auto path_id = paths.intern(file);
|
||||
auto matched = find_entries(path_id);
|
||||
|
||||
@@ -376,18 +338,17 @@ llvm::SmallVector<CompileCommand> CompilationDatabase::lookup(llvm::StringRef fi
|
||||
render_arg_to([&](llvm::StringRef s) { out.push_back(strings.save(s).data()); }, arg);
|
||||
};
|
||||
|
||||
/// Build one CompileCommand from a single CompilationInfo.
|
||||
auto build_command = [&](object_ptr<CompilationInfo> info) -> CompileCommand {
|
||||
/// Build one CompilationContext from a single CompilationInfo.
|
||||
auto build_context = [&](object_ptr<CompilationInfo> info) -> CompilationContext {
|
||||
llvm::StringRef directory = info->directory;
|
||||
std::vector<const char*> flags;
|
||||
bool is_cc1 = false;
|
||||
std::vector<const char*> arguments;
|
||||
|
||||
auto append_arg = [&](llvm::StringRef s) {
|
||||
flags.emplace_back(strings.save(s).data());
|
||||
arguments.emplace_back(strings.save(s).data());
|
||||
};
|
||||
|
||||
auto append_args = [&](llvm::ArrayRef<const char*> args) {
|
||||
flags.insert(flags.end(), args.begin(), args.end());
|
||||
arguments.insert(arguments.end(), args.begin(), args.end());
|
||||
};
|
||||
|
||||
if(options.query_toolchain) {
|
||||
@@ -400,20 +361,23 @@ llvm::SmallVector<CompileCommand> CompilationDatabase::lookup(llvm::StringRef fi
|
||||
append_args(info->canonical->arguments);
|
||||
append_args(info->patch);
|
||||
} else {
|
||||
flags.assign(cached.begin(), cached.end());
|
||||
flags.pop_back(); // remove temp source file
|
||||
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
|
||||
|
||||
// Replace resource dir if needed.
|
||||
if(!resource_dir().empty()) {
|
||||
llvm::StringRef old_resource_dir;
|
||||
for(std::size_t i = 0; i + 1 < flags.size(); ++i) {
|
||||
if(flags[i] == llvm::StringRef("-resource-dir")) {
|
||||
old_resource_dir = flags[i + 1];
|
||||
for(std::size_t i = 0; i + 1 < arguments.size(); ++i) {
|
||||
if(arguments[i] == llvm::StringRef("-resource-dir")) {
|
||||
old_resource_dir = arguments[i + 1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!old_resource_dir.empty() && old_resource_dir != resource_dir()) {
|
||||
for(auto& arg: flags) {
|
||||
for(auto& arg: arguments) {
|
||||
llvm::StringRef s(arg);
|
||||
if(s.starts_with(old_resource_dir)) {
|
||||
auto replaced =
|
||||
@@ -426,42 +390,39 @@ llvm::SmallVector<CompileCommand> CompilationDatabase::lookup(llvm::StringRef fi
|
||||
|
||||
append_args(info->patch);
|
||||
|
||||
// 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
|
||||
// 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;
|
||||
continue;
|
||||
}
|
||||
cleaned.push_back(flags[i]);
|
||||
if(next_main_file) {
|
||||
arg = strings.save(path::filename(file)).data();
|
||||
next_main_file = false;
|
||||
}
|
||||
}
|
||||
flags = std::move(cleaned);
|
||||
}
|
||||
|
||||
// Detect cc1 mode (search rather than assuming index).
|
||||
is_cc1 = ranges::contains(flags, llvm::StringRef("-cc1"));
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
} 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>;
|
||||
@@ -479,12 +440,12 @@ llvm::SmallVector<CompileCommand> CompilationDatabase::lookup(llvm::StringRef fi
|
||||
};
|
||||
std::ranges::sort(remove_args, {}, get_id);
|
||||
|
||||
auto saved_flags = std::move(flags);
|
||||
flags.clear();
|
||||
flags.push_back(saved_flags.front());
|
||||
auto saved_args = std::move(arguments);
|
||||
arguments.clear();
|
||||
arguments.push_back(saved_args.front());
|
||||
|
||||
parser->parse(
|
||||
llvm::ArrayRef(saved_flags).drop_front(),
|
||||
llvm::ArrayRef(saved_args).drop_front(),
|
||||
[&](Arg arg) {
|
||||
auto id = arg->getOption().getID();
|
||||
auto range = std::ranges::equal_range(remove_args, id, {}, get_id);
|
||||
@@ -500,7 +461,7 @@ llvm::SmallVector<CompileCommand> CompilationDatabase::lookup(llvm::StringRef fi
|
||||
return;
|
||||
}
|
||||
}
|
||||
render_arg(flags, *arg);
|
||||
render_arg(arguments, *arg);
|
||||
},
|
||||
[](int, int) {});
|
||||
}
|
||||
@@ -509,34 +470,26 @@ llvm::SmallVector<CompileCommand> CompilationDatabase::lookup(llvm::StringRef fi
|
||||
append_arg(arg);
|
||||
}
|
||||
|
||||
return CompileCommand{
|
||||
ResolvedFlags{directory, std::move(flags), is_cc1},
|
||||
paths.resolve(path_id).data()
|
||||
};
|
||||
arguments.emplace_back(paths.resolve(path_id).data());
|
||||
return CompilationContext(directory, std::move(arguments));
|
||||
};
|
||||
|
||||
llvm::SmallVector<CompileCommand> results;
|
||||
llvm::SmallVector<CompilationContext> results;
|
||||
|
||||
if(!matched.empty()) {
|
||||
for(auto& entry: matched) {
|
||||
results.push_back(build_command(entry.info));
|
||||
results.push_back(build_context(entry.info));
|
||||
}
|
||||
} else {
|
||||
// No matching entry — synthesize a default command.
|
||||
std::vector<const char*> flags;
|
||||
std::vector<const char*> arguments;
|
||||
if(file.ends_with(".cpp") || file.ends_with(".hpp") || file.ends_with(".cc")) {
|
||||
flags = {"clang++", "-std=c++20"};
|
||||
arguments = {"clang++", "-std=c++20"};
|
||||
} else {
|
||||
flags = {"clang"};
|
||||
arguments = {"clang"};
|
||||
}
|
||||
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()
|
||||
});
|
||||
arguments.emplace_back(paths.resolve(path_id).data());
|
||||
results.push_back(CompilationContext({}, std::move(arguments)));
|
||||
}
|
||||
|
||||
return results;
|
||||
@@ -560,8 +513,8 @@ SearchConfig CompilationDatabase::lookup_search_config(llvm::StringRef file,
|
||||
}
|
||||
|
||||
auto results = lookup(file, options);
|
||||
auto& cmd = results.front();
|
||||
auto config = extract_search_config(cmd.to_argv(), cmd.resolved.directory);
|
||||
auto& ctx = results.front();
|
||||
auto config = extract_search_config(ctx.arguments, ctx.directory);
|
||||
|
||||
if(cacheable) {
|
||||
auto key = ConfigCacheKey{matched.front().info.ptr, options_bits(options)};
|
||||
@@ -697,11 +650,6 @@ 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,11 +29,6 @@ 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;
|
||||
|
||||
@@ -41,35 +36,12 @@ struct CommandOptions {
|
||||
llvm::ArrayRef<std::string> append;
|
||||
};
|
||||
|
||||
/// File-independent compilation flags (shareable, suitable as cache key input).
|
||||
/// Does NOT contain source file path or -main-file-name.
|
||||
struct ResolvedFlags {
|
||||
struct CompilationContext {
|
||||
/// The working directory of compilation.
|
||||
llvm::StringRef directory;
|
||||
|
||||
/// 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;
|
||||
/// The compilation arguments.
|
||||
std::vector<const char*> arguments;
|
||||
};
|
||||
|
||||
/// Shared compiler identity — driver + all semantics-affecting flags.
|
||||
@@ -202,10 +174,10 @@ public:
|
||||
/// but toolchain cache survives. Returns the number of entries loaded.
|
||||
std::size_t load(llvm::StringRef path);
|
||||
|
||||
/// Lookup the compile commands for a file. A file may have multiple
|
||||
/// Lookup the compilation contexts for a file. A file may have multiple
|
||||
/// compilation commands (e.g. different build configurations); all are returned.
|
||||
llvm::SmallVector<CompileCommand> lookup(llvm::StringRef file,
|
||||
const CommandOptions& options = {});
|
||||
llvm::SmallVector<CompilationContext> 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 = {});
|
||||
@@ -219,10 +191,6 @@ 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;
|
||||
|
||||
|
||||
@@ -87,9 +87,7 @@ auto CompilationUnitRef::file_path(clang::FileID fid) -> llvm::StringRef {
|
||||
}
|
||||
|
||||
auto entry = self->SM().getFileEntryRefForID(fid);
|
||||
if(!entry) {
|
||||
return {};
|
||||
}
|
||||
assert(entry && "Invalid file entry");
|
||||
|
||||
llvm::SmallString<128> path;
|
||||
|
||||
@@ -244,19 +242,13 @@ std::vector<std::string> CompilationUnitRef::deps() {
|
||||
for(auto& [fid, directive]: directives()) {
|
||||
for(auto& include: directive.includes) {
|
||||
if(!include.skipped) {
|
||||
auto path = file_path(include.fid);
|
||||
if(!path.empty()) {
|
||||
deps.try_emplace(path);
|
||||
}
|
||||
deps.try_emplace(file_path(include.fid));
|
||||
}
|
||||
}
|
||||
|
||||
for(auto& has_include: directive.has_includes) {
|
||||
if(has_include.fid.isValid()) {
|
||||
auto path = file_path(has_include.fid);
|
||||
if(!path.empty()) {
|
||||
deps.try_emplace(path);
|
||||
}
|
||||
deps.try_emplace(file_path(has_include.fid));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,36 +65,11 @@ 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,
|
||||
clang::CharSourceRange filename_range,
|
||||
clang::OptionalFileEntryRef,
|
||||
llvm::StringRef,
|
||||
llvm::StringRef,
|
||||
@@ -108,6 +83,7 @@ public:
|
||||
unit->directives[prev_fid].includes.emplace_back(Include{
|
||||
.fid = {},
|
||||
.location = include_tok.getLocation(),
|
||||
.filename_range = filename_range.getAsRange(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -20,8 +20,11 @@ struct Include {
|
||||
/// The file id of included file.
|
||||
clang::FileID fid;
|
||||
|
||||
/// Location of the `include` keyword.
|
||||
/// Location of the `include`.
|
||||
clang::SourceLocation location;
|
||||
|
||||
/// The range of filename(includes `""` or `<>`).
|
||||
clang::SourceRange filename_range;
|
||||
};
|
||||
|
||||
/// Information about `__has_include` directive.
|
||||
@@ -129,39 +132,6 @@ 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;
|
||||
@@ -169,8 +139,6 @@ 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::CXXConstructorDecl>(decl)) {
|
||||
return protocol::CompletionItemKind::Constructor;
|
||||
if(llvm::isa<clang::FunctionDecl, clang::FunctionTemplateDecl>(decl)) {
|
||||
return protocol::CompletionItemKind::Function;
|
||||
}
|
||||
|
||||
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::FunctionDecl, clang::FunctionTemplateDecl>(decl)) {
|
||||
return protocol::CompletionItemKind::Function;
|
||||
if(llvm::isa<clang::CXXConstructorDecl>(decl)) {
|
||||
return protocol::CompletionItemKind::Constructor;
|
||||
}
|
||||
|
||||
if(llvm::isa<clang::FieldDecl, clang::IndirectFieldDecl>(decl)) {
|
||||
@@ -109,115 +109,6 @@ 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;
|
||||
@@ -268,44 +159,29 @@ public:
|
||||
overloads.reserve(candidate_count);
|
||||
std::unordered_map<std::string, std::size_t> overload_index;
|
||||
|
||||
bool prefix_starts_with_underscore = prefix.spelling.starts_with("_");
|
||||
auto build_item =
|
||||
[&](llvm::StringRef label, protocol::CompletionItemKind kind, llvm::StringRef insert) {
|
||||
protocol::CompletionItem item{
|
||||
.label = label.str(),
|
||||
};
|
||||
item.kind = kind;
|
||||
|
||||
auto build_item = [&](llvm::StringRef label,
|
||||
protocol::CompletionItemKind kind,
|
||||
llvm::StringRef insert,
|
||||
bool is_snippet = false) {
|
||||
protocol::CompletionItem item{
|
||||
.label = label.str(),
|
||||
protocol::TextEdit edit{
|
||||
.range = replace_range,
|
||||
.new_text = insert.empty() ? label.str() : insert.str(),
|
||||
};
|
||||
item.text_edit = std::move(edit);
|
||||
return item;
|
||||
};
|
||||
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 signature = {},
|
||||
llvm::StringRef return_type = {},
|
||||
bool is_snippet = false) {
|
||||
llvm::StringRef overload_key) {
|
||||
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;
|
||||
@@ -315,18 +191,8 @@ public:
|
||||
auto [it, inserted] =
|
||||
overload_index.try_emplace(overload_key.str(), overloads.size());
|
||||
if(inserted) {
|
||||
auto item = build_item(label, kind, insert_text, is_snippet);
|
||||
auto item = build_item(label, kind, insert_text);
|
||||
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,
|
||||
@@ -343,18 +209,8 @@ public:
|
||||
return;
|
||||
}
|
||||
|
||||
auto item = build_item(label, kind, insert_text, is_snippet);
|
||||
auto item = build_item(label, kind, insert_text);
|
||||
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));
|
||||
};
|
||||
|
||||
@@ -386,58 +242,16 @@ 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;
|
||||
bool is_callable = kind == protocol::CompletionItemKind::Function ||
|
||||
kind == protocol::CompletionItemKind::Method ||
|
||||
kind == protocol::CompletionItemKind::Constructor;
|
||||
if(options.bundle_overloads && is_callable) {
|
||||
if(options.bundle_overloads && kind == protocol::CompletionItemKind::Function) {
|
||||
llvm::raw_svector_ostream stream(qualified_name);
|
||||
declaration->printQualifiedName(stream);
|
||||
}
|
||||
|
||||
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);
|
||||
try_add(label, kind, label, qualified_name.str());
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -445,48 +259,11 @@ public:
|
||||
|
||||
for(auto& entry: overloads) {
|
||||
if(entry.count > 1) {
|
||||
protocol::CompletionItemLabelDetails details;
|
||||
details.detail = std::format("(…) +{} overloads", entry.count);
|
||||
entry.item.label_details = std::move(details);
|
||||
entry.item.detail = "(...)";
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1,20 +1,13 @@
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "feature/feature.h"
|
||||
#include "syntax/lexer.h"
|
||||
|
||||
namespace clice::feature {
|
||||
|
||||
namespace {
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
namespace {} // namespace
|
||||
|
||||
auto document_links(CompilationUnitRef unit, PositionEncoding encoding)
|
||||
-> std::vector<protocol::DocumentLink> {
|
||||
@@ -30,92 +23,49 @@ auto document_links(CompilationUnitRef unit, PositionEncoding encoding)
|
||||
PositionMapper converter(content, encoding);
|
||||
auto& directives = directives_it->second;
|
||||
|
||||
// Find the filename argument of a preprocessor directive starting from `offset`.
|
||||
// Creates a Lexer from the line start so that # at start-of-line is detected,
|
||||
// which enables header_name mode for #include and #embed automatically.
|
||||
// For __has_include/__has_embed, manually enables header_name mode after (.
|
||||
auto find_argument_range = [&](std::uint32_t offset) -> std::optional<LocalSourceRange> {
|
||||
std::uint32_t line_start = 0;
|
||||
if(offset > 0) {
|
||||
if(auto nl = content.rfind('\n', offset - 1); nl != llvm::StringRef::npos)
|
||||
line_start = static_cast<std::uint32_t>(nl + 1);
|
||||
}
|
||||
|
||||
auto line = content.substr(line_start);
|
||||
Lexer lexer(line);
|
||||
bool after_has_keyword = 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;
|
||||
|
||||
// Detect __has_include/__has_embed to enable header_name mode after (.
|
||||
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(tok.kind == clang::tok::l_paren && after_has_keyword) {
|
||||
after_has_keyword = false;
|
||||
lexer.set_header_name_mode();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Only return tokens at or after the directive's starting offset.
|
||||
if(abs_begin < offset)
|
||||
continue;
|
||||
|
||||
if(tok.is_header_name() || tok.kind == clang::tok::string_literal)
|
||||
return LocalSourceRange(abs_begin, abs_end);
|
||||
|
||||
if(tok.is_identifier() && !is_directive_keyword(tok.text(line)))
|
||||
return LocalSourceRange(abs_begin, abs_end);
|
||||
}
|
||||
return std::nullopt;
|
||||
};
|
||||
|
||||
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_argument_range(offset);
|
||||
if(!range)
|
||||
return;
|
||||
protocol::DocumentLink link{.range = to_range(converter, *range)};
|
||||
link.target = target.str();
|
||||
links.push_back(std::move(link));
|
||||
};
|
||||
links.reserve(directives.includes.size() + directives.has_includes.size());
|
||||
|
||||
for(const auto& include: directives.includes) {
|
||||
if(include.fid.isValid()) {
|
||||
add_link(include.location, unit.file_path(include.fid));
|
||||
auto [fid, range] = unit.decompose_range(include.filename_range);
|
||||
if(fid != interested || !range.valid()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
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.isValid()) {
|
||||
add_link(has_include.location, unit.file_path(has_include.fid));
|
||||
if(has_include.fid.isInvalid()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
for(const auto& embed: directives.embeds) {
|
||||
if(embed.file) {
|
||||
add_link(embed.loc, embed.file->getName());
|
||||
auto [fid, offset] = unit.decompose_location(has_include.location);
|
||||
if(fid != interested || offset >= content.size()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
for(const auto& has_embed: directives.has_embeds) {
|
||||
if(has_embed.file) {
|
||||
add_link(has_embed.loc, has_embed.file->getName());
|
||||
auto tail = content.substr(offset);
|
||||
char open = tail.front();
|
||||
if(open != '<' && open != '"') {
|
||||
continue;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -23,8 +23,12 @@ 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 |= SymbolModifiers::to_mask(kind);
|
||||
modifiers |= bit(kind);
|
||||
}
|
||||
|
||||
auto type_index(SymbolKind kind) -> std::uint32_t {
|
||||
@@ -35,132 +39,6 @@ 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) {}
|
||||
@@ -177,8 +55,6 @@ 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);
|
||||
@@ -187,42 +63,6 @@ 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);
|
||||
}
|
||||
@@ -240,10 +80,6 @@ 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)) {
|
||||
|
||||
@@ -14,8 +14,8 @@ namespace {
|
||||
|
||||
class Builder : public SemanticVisitor<Builder> {
|
||||
public:
|
||||
Builder(TUIndex& result, CompilationUnitRef unit, bool interested_only) :
|
||||
SemanticVisitor<Builder>(unit, interested_only), result(result) {
|
||||
Builder(TUIndex& result, CompilationUnitRef unit) :
|
||||
SemanticVisitor<Builder>(unit, false), result(result) {
|
||||
result.graph = IncludeGraph::from(unit);
|
||||
}
|
||||
|
||||
@@ -188,11 +188,11 @@ std::array<std::uint8_t, 32> FileIndex::hash() {
|
||||
return hasher.final();
|
||||
}
|
||||
|
||||
TUIndex TUIndex::build(CompilationUnitRef unit, bool interested_only) {
|
||||
TUIndex TUIndex::build(CompilationUnitRef unit) {
|
||||
TUIndex index;
|
||||
index.built_at = unit.build_at();
|
||||
|
||||
Builder builder(index, unit, interested_only);
|
||||
Builder builder(index, unit);
|
||||
builder.build();
|
||||
|
||||
return index;
|
||||
|
||||
@@ -85,7 +85,7 @@ struct TUIndex {
|
||||
|
||||
FileIndex main_file_index;
|
||||
|
||||
static TUIndex build(CompilationUnitRef unit, bool interested_only = false);
|
||||
static TUIndex build(CompilationUnitRef unit);
|
||||
|
||||
void serialize(llvm::raw_ostream& os) const;
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,12 +17,9 @@ 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:
|
||||
explicit TemplateResolver(clang::Sema& sema) : sema(sema) {}
|
||||
TemplateResolver(clang::Sema& sema) : sema(sema) {}
|
||||
|
||||
clang::QualType resolve(clang::QualType type);
|
||||
|
||||
@@ -30,7 +27,7 @@ public:
|
||||
|
||||
void resolve(clang::UnresolvedLookupExpr* expr);
|
||||
|
||||
// TODO: Use a clearer approach for resolving UnresolvedLookupExpr.
|
||||
// TODO: use a relative clear way to resolve `UnresolvedLookupExpr`.
|
||||
|
||||
void resolve(clang::UnresolvedUsingType* type);
|
||||
|
||||
@@ -53,7 +50,7 @@ public:
|
||||
if(identifier) {
|
||||
return lookup(template_name.getQualifier(), identifier);
|
||||
} else {
|
||||
/// TODO: Operators don't have an IdentifierInfo; need DeclarationName-based lookup.
|
||||
/// FIXME: Operators does't have a name.
|
||||
return {};
|
||||
}
|
||||
}
|
||||
@@ -63,7 +60,7 @@ public:
|
||||
}
|
||||
|
||||
lookup_result lookup(const clang::UnresolvedLookupExpr* expr) {
|
||||
/// TODO: Only returns the first TemplateDecl; should handle overloaded lookups.
|
||||
/// FIXME:
|
||||
for(auto decl: expr->decls()) {
|
||||
if(auto TD = llvm::dyn_cast<clang::TemplateDecl>(decl)) {
|
||||
return lookup_result(TD);
|
||||
@@ -77,8 +74,8 @@ public:
|
||||
return {};
|
||||
}
|
||||
|
||||
/// TODO: Implement dependent member expression lookup (e.g. `x.template foo<T>()`).
|
||||
lookup_result lookup(const clang::CXXDependentScopeMemberExpr* expr) {
|
||||
/// TODO:
|
||||
lookup_result lookup(clang::CXXDependentScopeMemberExpr* expr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -86,19 +83,16 @@ public:
|
||||
return lookup(decl->getQualifier(), decl->getDeclName());
|
||||
}
|
||||
|
||||
lookup_result lookup(const clang::UnresolvedUsingTypenameDecl* decl) {
|
||||
lookup_result resolve(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;
|
||||
};
|
||||
|
||||
|
||||
@@ -80,79 +80,27 @@ private:
|
||||
struct SymbolModifiers {
|
||||
enum Kind : std::uint32_t {
|
||||
/// Represents that the symbol is a declaration(e.g. function declaration).
|
||||
Declaration = 0,
|
||||
Declaration = 1u << 0,
|
||||
|
||||
/// Represents that the symbol is a definition(e.g. function definition).
|
||||
Definition = 1,
|
||||
Definition = 1u << 1,
|
||||
|
||||
/// Represents that the symbol is const modified(e.g. `const` variable).
|
||||
Const = 2,
|
||||
Const = 1u << 2,
|
||||
|
||||
/// Represents that the symbol is overloaded(e.g. overloaded functions and operators).
|
||||
Overloaded = 3,
|
||||
Overloaded = 1u << 3,
|
||||
|
||||
/// Represents that the symbol is a part of type(e.g. `*` in `int*`).
|
||||
Typed = 4,
|
||||
Typed = 1u << 4,
|
||||
|
||||
/// Represents that the symbol is a template(e.g. class template or function template).
|
||||
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,
|
||||
Templated = 1u << 5,
|
||||
};
|
||||
|
||||
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(to_mask(kind)) {}
|
||||
constexpr SymbolModifiers(Kind kind) : value(static_cast<std::uint32_t>(kind)) {}
|
||||
|
||||
constexpr explicit SymbolModifiers(std::uint32_t bits) : value(bits) {}
|
||||
|
||||
@@ -161,7 +109,7 @@ struct SymbolModifiers {
|
||||
}
|
||||
|
||||
constexpr bool contains(Kind kind) const {
|
||||
return (value & to_mask(kind)) != 0;
|
||||
return (value & static_cast<std::uint32_t>(kind)) != 0;
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
|
||||
namespace clice {
|
||||
|
||||
namespace ranges = std::ranges;
|
||||
|
||||
CompileGraph::CompileGraph(dispatch_fn dispatch, resolve_fn resolve) :
|
||||
dispatch(std::move(dispatch)), resolve(std::move(resolve)) {}
|
||||
|
||||
@@ -33,19 +31,13 @@ void CompileGraph::ensure_resolved(std::uint32_t path_id) {
|
||||
}
|
||||
}
|
||||
|
||||
et::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);
|
||||
}
|
||||
|
||||
et::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,
|
||||
bool dispatch_self) {
|
||||
llvm::DenseSet<std::uint32_t> ancestors) {
|
||||
ensure_resolved(path_id);
|
||||
|
||||
// Cycle detection: if this unit is already in the compile chain, bail out.
|
||||
@@ -56,27 +48,6 @@ 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<et::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 et::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;
|
||||
@@ -93,17 +64,10 @@ et::task<bool> CompileGraph::compile_impl(std::uint32_t path_id,
|
||||
co_return !units.find(path_id)->second.dirty;
|
||||
}
|
||||
|
||||
// Begin compilation. The finish lambda ensures compiling/completion state
|
||||
// is always cleaned up, regardless of how the function exits.
|
||||
// Begin compilation.
|
||||
it->second.compiling = true;
|
||||
it->second.completion = std::make_unique<et::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;
|
||||
auto gen = it->second.generation;
|
||||
@@ -121,41 +85,52 @@ et::task<bool> CompileGraph::compile_impl(std::uint32_t path_id,
|
||||
|
||||
auto results = co_await et::when_all(std::move(dep_tasks));
|
||||
|
||||
auto& u = units.find(path_id)->second;
|
||||
if(results.is_cancelled()) {
|
||||
finish();
|
||||
u.compiling = false;
|
||||
u.completion->set();
|
||||
co_await et::cancel();
|
||||
}
|
||||
|
||||
for(auto ok: *results) {
|
||||
if(!ok) {
|
||||
finish();
|
||||
u.compiling = false;
|
||||
u.completion->set();
|
||||
co_return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dispatch the actual compilation, cancellable via the pre-captured token.
|
||||
auto result = co_await et::with_token(dispatch(path_id), 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);
|
||||
|
||||
if(!result.has_value()) {
|
||||
finish();
|
||||
co_await et::cancel();
|
||||
}
|
||||
|
||||
if(!*result) {
|
||||
finish();
|
||||
co_return false;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Success — only clear dirty if update() hasn't bumped the generation.
|
||||
auto& final_unit = units.find(path_id)->second;
|
||||
if(final_unit.generation != gen) {
|
||||
finish();
|
||||
// update() was called while dispatch was in flight.
|
||||
final_unit.compiling = false;
|
||||
final_unit.completion->set();
|
||||
co_return false;
|
||||
}
|
||||
|
||||
final_unit.dirty = false;
|
||||
finish();
|
||||
final_unit.compiling = false;
|
||||
final_unit.completion->set();
|
||||
co_return true;
|
||||
}
|
||||
|
||||
@@ -190,7 +165,8 @@ 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(ranges::remove(dependents, path_id).begin(), dependents.end());
|
||||
dependents.erase(std::remove(dependents.begin(), dependents.end(), path_id),
|
||||
dependents.end());
|
||||
}
|
||||
}
|
||||
unit.dependencies.clear();
|
||||
|
||||
@@ -50,10 +50,6 @@ public:
|
||||
/// Compile a unit and all its transitive dependencies.
|
||||
et::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.
|
||||
et::task<bool> compile_deps(std::uint32_t path_id);
|
||||
|
||||
/// Mark path_id and all transitive dependents as dirty,
|
||||
/// cancelling any in-progress compilations.
|
||||
/// Returns the set of all path_ids that were marked dirty.
|
||||
@@ -70,9 +66,7 @@ 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,
|
||||
bool dispatch_self = true);
|
||||
et::task<bool> compile_impl(std::uint32_t path_id, llvm::DenseSet<std::uint32_t> ancestors);
|
||||
|
||||
/// Check if waiting on `target` would deadlock given our `ancestors` chain.
|
||||
/// Walks the dependency graph through compiling units to see if any dep
|
||||
|
||||
@@ -1,920 +0,0 @@
|
||||
#include "server/compiler.h"
|
||||
|
||||
#include <format>
|
||||
#include <ranges>
|
||||
#include <string>
|
||||
|
||||
#include "command/search_config.h"
|
||||
#include "eventide/ipc/lsp/position.h"
|
||||
#include "eventide/ipc/lsp/uri.h"
|
||||
#include "eventide/serde/json/json.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 "llvm/Support/FileSystem.h"
|
||||
#include "llvm/Support/MemoryBuffer.h"
|
||||
#include "llvm/Support/Path.h"
|
||||
#include "llvm/Support/xxhash.h"
|
||||
|
||||
namespace clice {
|
||||
|
||||
namespace lsp = eventide::ipc::lsp;
|
||||
using serde_raw = et::serde::RawValue;
|
||||
|
||||
/// Detect whether the cursor is inside a preamble directive (include/import).
|
||||
|
||||
Compiler::Compiler(et::event_loop& loop,
|
||||
et::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) -> et::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 et::serde::RawValue& diagnostics_json) {
|
||||
std::vector<protocol::Diagnostic> diagnostics;
|
||||
if(!diagnostics_json.empty()) {
|
||||
auto status = et::serde::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);
|
||||
}
|
||||
|
||||
et::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<et::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).
|
||||
et::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.
|
||||
et::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) -> et::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 = et::serde::json::to_json<et::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 = et::serde::json::to_json<et::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
|
||||
@@ -1,132 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#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 "server/session.h"
|
||||
#include "server/worker_pool.h"
|
||||
#include "server/workspace.h"
|
||||
#include "syntax/completion.h"
|
||||
|
||||
#include "llvm/ADT/ArrayRef.h"
|
||||
#include "llvm/ADT/DenseMap.h"
|
||||
#include "llvm/ADT/SmallVector.h"
|
||||
#include "llvm/ADT/StringRef.h"
|
||||
|
||||
namespace clice {
|
||||
|
||||
namespace et = eventide;
|
||||
namespace protocol = et::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(et::event_loop& loop,
|
||||
et::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.
|
||||
et::task<bool> ensure_compiled(Session& session);
|
||||
|
||||
using RawResult = et::task<et::serde::RawValue, et::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:
|
||||
et::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);
|
||||
|
||||
et::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 et::serde::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:
|
||||
et::event_loop& loop;
|
||||
et::ipc::JsonPeer& peer;
|
||||
Workspace& workspace;
|
||||
WorkerPool& pool;
|
||||
llvm::DenseMap<std::uint32_t, Session>& sessions;
|
||||
};
|
||||
|
||||
} // namespace clice
|
||||
@@ -25,10 +25,10 @@ void CliceConfig::apply_defaults(const std::string& workspace_root) {
|
||||
cpu_count = 4;
|
||||
|
||||
if(stateful_worker_count == 0) {
|
||||
stateful_worker_count = 2;
|
||||
stateful_worker_count = std::max(1u, cpu_count / 4);
|
||||
}
|
||||
if(stateless_worker_count == 0) {
|
||||
stateless_worker_count = 3;
|
||||
stateless_worker_count = std::max(1u, cpu_count / 4);
|
||||
}
|
||||
if(worker_memory_limit == 0) {
|
||||
worker_memory_limit = 4ULL * 1024 * 1024 * 1024; // 4GB default
|
||||
@@ -41,15 +41,10 @@ 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,
|
||||
|
||||
@@ -22,8 +22,8 @@ struct CliceConfig {
|
||||
// Index storage directory (default: <cache_dir>/index/)
|
||||
std::string index_dir;
|
||||
|
||||
// Logging directory (default: <cache_dir>/logs/)
|
||||
std::string logging_dir;
|
||||
// Debounce interval for re-compilation after edits (milliseconds)
|
||||
int debounce_ms = 200;
|
||||
|
||||
// Background indexing
|
||||
bool enable_indexing = true;
|
||||
|
||||
@@ -1,696 +0,0 @@
|
||||
#include "server/indexer.h"
|
||||
|
||||
#include <string>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
#include "eventide/ipc/lsp/position.h"
|
||||
#include "eventide/ipc/lsp/protocol.h"
|
||||
#include "eventide/ipc/lsp/uri.h"
|
||||
#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 "llvm/Support/FileSystem.h"
|
||||
#include "llvm/Support/MemoryBuffer.h"
|
||||
#include "llvm/Support/Path.h"
|
||||
#include "llvm/Support/raw_ostream.h"
|
||||
|
||||
namespace clice {
|
||||
|
||||
namespace lsp = eventide::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<et::timer>(et::timer::create(loop));
|
||||
}
|
||||
index_idle_timer->start(std::chrono::milliseconds(workspace.config.idle_timeout_ms));
|
||||
loop.schedule(run_background_indexing());
|
||||
}
|
||||
|
||||
et::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
|
||||
@@ -1,189 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "eventide/async/async.h"
|
||||
#include "eventide/ipc/lsp/position.h"
|
||||
#include "eventide/ipc/lsp/protocol.h"
|
||||
#include "semantic/relation_kind.h"
|
||||
#include "semantic/symbol_kind.h"
|
||||
#include "server/workspace.h"
|
||||
|
||||
#include "llvm/ADT/DenseMap.h"
|
||||
#include "llvm/ADT/SmallVector.h"
|
||||
#include "llvm/ADT/StringRef.h"
|
||||
|
||||
namespace clice {
|
||||
|
||||
namespace et = eventide;
|
||||
namespace protocol = et::ipc::protocol;
|
||||
namespace lsp = et::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(et::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:
|
||||
et::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<et::timer> index_idle_timer;
|
||||
|
||||
et::task<> run_background_indexing();
|
||||
};
|
||||
|
||||
} // namespace clice
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,22 +3,39 @@
|
||||
#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 "server/compiler.h"
|
||||
#include "server/indexer.h"
|
||||
#include "server/session.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/worker_pool.h"
|
||||
#include "server/workspace.h"
|
||||
#include "support/path_pool.h"
|
||||
#include "syntax/dependency_graph.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,
|
||||
@@ -28,20 +45,6 @@ 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);
|
||||
@@ -52,31 +55,167 @@ public:
|
||||
private:
|
||||
et::event_loop& loop;
|
||||
et::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;
|
||||
|
||||
PathPool path_pool;
|
||||
ServerLifecycle lifecycle = ServerLifecycle::Uninitialized;
|
||||
|
||||
std::string self_path;
|
||||
std::string workspace_root;
|
||||
std::string session_log_dir;
|
||||
CliceConfig config;
|
||||
|
||||
CompilationDatabase cdb;
|
||||
DependencyGraph dependency_graph;
|
||||
|
||||
// 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);
|
||||
};
|
||||
|
||||
} // namespace clice
|
||||
|
||||
@@ -10,34 +10,13 @@
|
||||
#include "eventide/ipc/lsp/protocol.h"
|
||||
#include "eventide/ipc/protocol.h"
|
||||
#include "eventide/serde/serde/raw_value.h"
|
||||
#include "syntax/token.h"
|
||||
|
||||
namespace clice::worker {
|
||||
|
||||
namespace protocol = eventide::ipc::protocol;
|
||||
|
||||
/// Kind of AST query dispatched to a stateful worker.
|
||||
enum class QueryKind : uint8_t {
|
||||
Hover,
|
||||
GoToDefinition,
|
||||
SemanticTokens,
|
||||
InlayHints,
|
||||
FoldingRange,
|
||||
DocumentSymbol,
|
||||
DocumentLink,
|
||||
CodeAction,
|
||||
};
|
||||
// === StatefulWorker Requests ===
|
||||
|
||||
/// 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;
|
||||
@@ -53,62 +32,113 @@ struct CompileResult {
|
||||
/// Diagnostics serialized as JSON (RawValue) to avoid bincode/serde annotation conflicts.
|
||||
eventide::serde::RawValue diagnostics;
|
||||
std::size_t memory_usage;
|
||||
std::vector<std::string> deps;
|
||||
/// Serialized TUIndex for the main file (interested_only=true).
|
||||
std::string tu_index_data;
|
||||
};
|
||||
|
||||
/// Kind of build task dispatched to a stateless worker.
|
||||
enum class BuildKind : uint8_t {
|
||||
BuildPCH,
|
||||
BuildPCM,
|
||||
Index,
|
||||
Completion,
|
||||
SignatureHelp,
|
||||
struct HoverParams {
|
||||
std::string path;
|
||||
uint32_t offset;
|
||||
};
|
||||
|
||||
/// 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;
|
||||
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;
|
||||
|
||||
/// 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
|
||||
std::string content;
|
||||
std::uint32_t preamble_bound = UINT32_MAX;
|
||||
};
|
||||
|
||||
/// 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;
|
||||
struct BuildPCHResult {
|
||||
bool success;
|
||||
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
|
||||
eventide::serde::RawValue result_json; ///< Completion/SignatureHelp result
|
||||
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::string tu_index_data;
|
||||
};
|
||||
|
||||
// === Notifications ===
|
||||
|
||||
struct DocumentUpdateParams {
|
||||
std::string path;
|
||||
int version;
|
||||
std::string text;
|
||||
};
|
||||
|
||||
struct EvictParams {
|
||||
@@ -121,45 +151,10 @@ struct EvictedParams {
|
||||
|
||||
} // namespace clice::worker
|
||||
|
||||
namespace clice::ext {
|
||||
|
||||
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 eventide::ipc::protocol {
|
||||
|
||||
// === Stateful Requests ===
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::worker::CompileParams> {
|
||||
using Result = clice::worker::CompileResult;
|
||||
@@ -167,17 +162,87 @@ struct RequestTraits<clice::worker::CompileParams> {
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::worker::QueryParams> {
|
||||
struct RequestTraits<clice::worker::HoverParams> {
|
||||
using Result = eventide::serde::RawValue;
|
||||
constexpr inline static std::string_view method = "clice/worker/query";
|
||||
constexpr inline static std::string_view method = "clice/worker/hover";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::worker::BuildParams> {
|
||||
using Result = clice::worker::BuildResult;
|
||||
constexpr inline static std::string_view method = "clice/worker/build";
|
||||
struct RequestTraits<clice::worker::SemanticTokensParams> {
|
||||
using Result = eventide::serde::RawValue;
|
||||
constexpr inline static std::string_view method = "clice/worker/semanticTokens";
|
||||
};
|
||||
|
||||
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";
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#include "eventide/async/async.h"
|
||||
#include "server/workspace.h"
|
||||
|
||||
#include "llvm/ADT/SmallVector.h"
|
||||
|
||||
namespace clice {
|
||||
|
||||
namespace et = eventide;
|
||||
|
||||
/// 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 {
|
||||
et::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,6 +1,7 @@
|
||||
#include "server/stateful_worker.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <list>
|
||||
#include <memory>
|
||||
@@ -9,16 +10,16 @@
|
||||
|
||||
#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 "llvm/ADT/StringMap.h"
|
||||
#include "llvm/Support/raw_ostream.h"
|
||||
|
||||
namespace clice {
|
||||
|
||||
@@ -47,11 +48,37 @@ struct DocumentEntry {
|
||||
et::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;
|
||||
std::uint64_t memory_limit;
|
||||
|
||||
llvm::StringMap<std::shared_ptr<DocumentEntry>> documents;
|
||||
llvm::StringMap<std::unique_ptr<DocumentEntry>> documents;
|
||||
|
||||
// LRU tracking — owns keys so they don't dangle after request handler returns
|
||||
std::list<std::string> lru;
|
||||
@@ -79,13 +106,13 @@ class StatefulWorker {
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<DocumentEntry> get_or_create(llvm::StringRef path) {
|
||||
DocumentEntry& get_or_create(llvm::StringRef path) {
|
||||
auto [it, inserted] = documents.try_emplace(path, nullptr);
|
||||
if(inserted) {
|
||||
it->second = std::make_shared<DocumentEntry>();
|
||||
it->second = std::make_unique<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.
|
||||
@@ -93,24 +120,22 @@ class StatefulWorker {
|
||||
template <typename F>
|
||||
et::task<et::serde::RawValue> with_ast(llvm::StringRef path, F&& fn) {
|
||||
auto it = documents.find(path);
|
||||
if(it == documents.end()) {
|
||||
if(it == documents.end())
|
||||
co_return et::serde::RawValue{"null"};
|
||||
}
|
||||
|
||||
// Hold shared_ptr so Evict can't destroy the entry mid-request.
|
||||
auto doc = it->second;
|
||||
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()))
|
||||
if(!doc.has_ast || (!doc.unit.completed() && !doc.unit.fatal_error()))
|
||||
return et::serde::RawValue{"null"};
|
||||
return fn(*doc);
|
||||
return fn(doc);
|
||||
});
|
||||
|
||||
doc->strand.unlock();
|
||||
doc.strand.unlock();
|
||||
co_return result.value();
|
||||
}
|
||||
|
||||
@@ -128,83 +153,68 @@ void StatefulWorker::register_handlers() {
|
||||
const worker::CompileParams& params) -> RequestResult<worker::CompileParams> {
|
||||
LOG_INFO("Compile request: path={}, version={}", params.path, params.version);
|
||||
|
||||
// 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();
|
||||
|
||||
// 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();
|
||||
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);
|
||||
doc.pcms.try_emplace(name, pcm_path);
|
||||
}
|
||||
|
||||
touch_lru(params.path);
|
||||
|
||||
co_await doc.strand.lock();
|
||||
|
||||
auto compile_result = co_await et::queue([&]() -> worker::CompileResult {
|
||||
LOG_DEBUG("Compiling: path={}, {} args", params.path, doc.arguments.size());
|
||||
|
||||
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);
|
||||
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) : "[]"};
|
||||
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{"[]"};
|
||||
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 et::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);
|
||||
|
||||
@@ -214,7 +224,10 @@ void StatefulWorker::register_handlers() {
|
||||
return;
|
||||
}
|
||||
|
||||
it->second->dirty.store(true, std::memory_order_release);
|
||||
auto& doc = *it->second;
|
||||
doc.version = params.version;
|
||||
doc.text = params.text;
|
||||
doc.dirty.store(true, std::memory_order_release);
|
||||
});
|
||||
|
||||
// === Evict ===
|
||||
@@ -229,58 +242,78 @@ void StatefulWorker::register_handlers() {
|
||||
documents.erase(params.path);
|
||||
});
|
||||
|
||||
// === Query (hover, definition, semantic tokens, etc.) ===
|
||||
// === Hover ===
|
||||
peer.on_request(
|
||||
[this](RequestContext& ctx,
|
||||
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) : et::serde::RawValue{"null"};
|
||||
});
|
||||
case K::GoToDefinition:
|
||||
// TODO: Implement go-to-definition
|
||||
co_return et::serde::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 et::serde::RawValue{"[]"};
|
||||
}
|
||||
co_return et::serde::RawValue{"null"};
|
||||
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"};
|
||||
});
|
||||
});
|
||||
|
||||
// === 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,
|
||||
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);
|
||||
}
|
||||
int run_stateful_worker_mode(std::uint64_t memory_limit) {
|
||||
logging::stderr_logger("stateful-worker", logging::options);
|
||||
|
||||
LOG_INFO("Starting stateful worker, memory_limit={}MB", memory_limit / (1024 * 1024));
|
||||
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
#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,
|
||||
const std::string& worker_name,
|
||||
const std::string& log_dir);
|
||||
int run_stateful_worker_mode(std::uint64_t memory_limit);
|
||||
|
||||
} // namespace clice
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
#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 "llvm/Support/raw_ostream.h"
|
||||
@@ -18,251 +22,33 @@ namespace et = eventide;
|
||||
using et::ipc::RequestResult;
|
||||
using RequestContext = et::ipc::BincodePeer::RequestContext;
|
||||
|
||||
/// 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;
|
||||
}
|
||||
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();
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
};
|
||||
|
||||
/// 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};
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
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};
|
||||
}
|
||||
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_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);
|
||||
}
|
||||
int run_stateless_worker_mode() {
|
||||
logging::stderr_logger("stateless-worker", logging::options);
|
||||
|
||||
LOG_INFO("Starting stateless worker");
|
||||
|
||||
@@ -276,18 +62,170 @@ int run_stateless_worker_mode(const std::string& worker_name, const std::string&
|
||||
|
||||
et::ipc::BincodePeer peer(loop, std::move(*transport_result));
|
||||
|
||||
peer.on_request([&](RequestContext& ctx,
|
||||
const worker::BuildParams& params) -> RequestResult<worker::BuildParams> {
|
||||
using K = worker::BuildKind;
|
||||
auto result = co_await et::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);
|
||||
// === 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;
|
||||
}
|
||||
return {false, "Unknown build kind"};
|
||||
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);
|
||||
}
|
||||
|
||||
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)};
|
||||
});
|
||||
co_return result.value();
|
||||
});
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
#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(const std::string& worker_name, const std::string& log_dir);
|
||||
int run_stateless_worker_mode();
|
||||
|
||||
} // namespace clice
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
/// Shared utilities for stateful and stateless worker processes.
|
||||
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "compile/compilation.h"
|
||||
#include "eventide/ipc/json_codec.h"
|
||||
#include "eventide/serde/json/serializer.h"
|
||||
#include "eventide/serde/serde/raw_value.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 eventide::serde::RawValue to_raw(const T& value) {
|
||||
auto json = eventide::serde::json::to_json<eventide::ipc::lsp_config>(value);
|
||||
return eventide::serde::RawValue{json ? std::move(*json) : "null"};
|
||||
}
|
||||
|
||||
} // namespace clice
|
||||
@@ -10,14 +10,14 @@ namespace clice {
|
||||
|
||||
namespace {
|
||||
|
||||
/// 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.
|
||||
/// 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) {
|
||||
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,6 +26,7 @@ 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);
|
||||
@@ -33,15 +34,16 @@ et::task<> drain_stderr(et::pipe stderr_pipe, std::string prefix) {
|
||||
break;
|
||||
auto line = buffer.substr(pos, nl - pos);
|
||||
if(!line.empty()) {
|
||||
LOG_DEBUG("{} {}", prefix, line);
|
||||
LOG_INFO("{} {}", prefix, line);
|
||||
}
|
||||
pos = nl + 1;
|
||||
}
|
||||
buffer.erase(0, pos);
|
||||
}
|
||||
|
||||
// Flush any remaining partial line
|
||||
if(!buffer.empty()) {
|
||||
LOG_DEBUG("{} {}", prefix, buffer);
|
||||
LOG_INFO("{} {}", prefix, buffer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,10 +52,6 @@ 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) {
|
||||
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);
|
||||
|
||||
et::process::options opts;
|
||||
opts.file = self_path;
|
||||
if(stateful) {
|
||||
@@ -65,15 +63,6 @@ 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
|
||||
@@ -96,8 +85,14 @@ bool WorkerPool::spawn_worker(const std::string& self_path,
|
||||
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) + "]";
|
||||
|
||||
// Schedule stderr log collection
|
||||
std::string prefix = "[" + worker_name + "]";
|
||||
loop.schedule(drain_stderr(std::move(spawn.stderr_pipe), prefix));
|
||||
|
||||
workers.push_back(WorkerProcess{
|
||||
@@ -113,8 +108,6 @@ 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;
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
|
||||
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;
|
||||
|
||||
@@ -23,7 +26,6 @@ struct WorkerPoolOptions {
|
||||
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 {
|
||||
@@ -79,10 +81,11 @@ 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,
|
||||
@@ -90,10 +93,9 @@ RequestResult<Params> WorkerPool::send_stateful(std::uint32_t path_id,
|
||||
if(stateful_workers.empty()) {
|
||||
co_return et::outcome_error(et::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
|
||||
// eventide's with_token/when_any which has a spurious-cancellation bug
|
||||
// that kills requests within milliseconds instead of the configured period.
|
||||
if(!opts.timeout.has_value()) {
|
||||
opts.timeout = kWorkerRequestTimeout;
|
||||
}
|
||||
auto idx = assign_worker(path_id);
|
||||
co_return co_await stateful_workers[idx].peer->send_request(params, opts);
|
||||
}
|
||||
@@ -104,6 +106,9 @@ RequestResult<Params> WorkerPool::send_stateless(const Params& params,
|
||||
if(stateless_workers.empty()) {
|
||||
co_return et::outcome_error(et::ipc::Error{"No stateless workers available"});
|
||||
}
|
||||
if(!opts.timeout.has_value()) {
|
||||
opts.timeout = kWorkerRequestTimeout;
|
||||
}
|
||||
auto idx = next_stateless;
|
||||
next_stateless = (next_stateless + 1) % stateless_workers.size();
|
||||
co_return co_await stateless_workers[idx].peer->send_request(params, opts);
|
||||
|
||||
@@ -1,376 +0,0 @@
|
||||
#include "server/workspace.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
|
||||
#include "eventide/ipc/lsp/position.h"
|
||||
#include "eventide/ipc/lsp/protocol.h"
|
||||
#include "eventide/serde/json/json.h"
|
||||
#include "support/filesystem.h"
|
||||
#include "support/logging.h"
|
||||
#include "syntax/scan.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 = eventide::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 = eventide::serde::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 = eventide::serde::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
|
||||
@@ -1,248 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
|
||||
#include "command/command.h"
|
||||
#include "eventide/ipc/lsp/position.h"
|
||||
#include "eventide/ipc/lsp/protocol.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 "llvm/ADT/ArrayRef.h"
|
||||
#include "llvm/ADT/DenseMap.h"
|
||||
#include "llvm/ADT/SmallVector.h"
|
||||
#include "llvm/ADT/StringRef.h"
|
||||
|
||||
namespace clice {
|
||||
|
||||
namespace et = eventide;
|
||||
namespace protocol = et::ipc::protocol;
|
||||
namespace lsp = et::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<eventide::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
|
||||
@@ -74,14 +74,6 @@ 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;
|
||||
@@ -132,7 +124,7 @@ public:
|
||||
}
|
||||
|
||||
llvm::StringRef filename = path::filename(Path);
|
||||
if(filename.ends_with(".pch")) {
|
||||
if(filename.starts_with("preamble-") && filename.ends_with(".pch")) {
|
||||
return file;
|
||||
}
|
||||
return std::make_unique<VolatileFile>(std::move(*file));
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
#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 {
|
||||
|
||||
@@ -39,73 +38,27 @@ void stderr_logger(std::string_view name, const Options& options) {
|
||||
spdlog::set_default_logger(std::move(logger));
|
||||
}
|
||||
|
||||
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);
|
||||
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));
|
||||
|
||||
auto replay_buffer = ringbuffer_sink;
|
||||
if(options.replay_console && ringbuffer_sink) {
|
||||
sink->set_level(options.level);
|
||||
sink->set_pattern(pattern);
|
||||
|
||||
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);
|
||||
for(auto& log: ringbuffer_sink->last_raw()) {
|
||||
sink->log(log);
|
||||
}
|
||||
|
||||
ringbuffer_sink.reset();
|
||||
}
|
||||
|
||||
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");
|
||||
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));
|
||||
}
|
||||
|
||||
} // namespace clice::logging
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user