Compare commits
7 Commits
folding-ra
...
feat/corpu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ec60f1c50 | ||
|
|
cc5b25d5c3 | ||
|
|
3305465d1f | ||
|
|
47ad905f5b | ||
|
|
75b9ea05b8 | ||
|
|
939ab6d0d4 | ||
|
|
e1202d2fa5 |
4
.github/workflows/main.yml
vendored
4
.github/workflows/main.yml
vendored
@@ -7,6 +7,10 @@ on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
|
||||
|
||||
28
.github/workflows/test-cmake.yml
vendored
28
.github/workflows/test-cmake.yml
vendored
@@ -96,9 +96,20 @@ jobs:
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
- name: Run tests
|
||||
- name: Unit tests
|
||||
if: ${{ !matrix.build_only }}
|
||||
run: pixi run test ${{ matrix.build_type }}
|
||||
timeout-minutes: 5
|
||||
run: pixi run unit-test ${{ matrix.build_type }}
|
||||
|
||||
- name: Integration tests
|
||||
if: ${{ !matrix.build_only }}
|
||||
timeout-minutes: 20
|
||||
run: pixi run integration-test ${{ matrix.build_type }}
|
||||
|
||||
- name: Smoke tests
|
||||
if: ${{ !matrix.build_only }}
|
||||
timeout-minutes: 15
|
||||
run: pixi run smoke-test ${{ matrix.build_type }}
|
||||
|
||||
- name: Print cache stats and stop server
|
||||
if: always()
|
||||
@@ -146,5 +157,14 @@ jobs:
|
||||
if: runner.os != 'Windows'
|
||||
run: chmod +x build/${{ matrix.build_type }}/bin/*
|
||||
|
||||
- name: Run tests
|
||||
run: pixi run -e test-run test ${{ matrix.build_type }}
|
||||
- name: Unit tests
|
||||
timeout-minutes: 5
|
||||
run: pixi run -e test-run unit-test ${{ matrix.build_type }}
|
||||
|
||||
- name: Integration tests
|
||||
timeout-minutes: 20
|
||||
run: pixi run -e test-run integration-test ${{ matrix.build_type }}
|
||||
|
||||
- name: Smoke tests
|
||||
timeout-minutes: 10
|
||||
run: pixi run -e test-run smoke-test ${{ matrix.build_type }}
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -72,3 +72,4 @@ tests/unit/Local/
|
||||
.claude/*
|
||||
!.claude/CLAUDE.md
|
||||
!.claude/commands/
|
||||
openspec/
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
#include "support/path_pool.h"
|
||||
#include "syntax/dependency_graph.h"
|
||||
|
||||
#include "kota/codec/json/serializer.h"
|
||||
#include "kota/codec/json/json.h"
|
||||
#include "kota/deco/deco.h"
|
||||
#include "llvm/Support/FileSystem.h"
|
||||
|
||||
|
||||
@@ -41,8 +41,7 @@ set(FLATBUFFERS_BUILD_FLATHASH OFF CACHE BOOL "" FORCE)
|
||||
FetchContent_Declare(
|
||||
kotatsu
|
||||
GIT_REPOSITORY https://github.com/clice-io/kotatsu
|
||||
GIT_TAG main
|
||||
GIT_SHALLOW TRUE
|
||||
GIT_TAG 73814044ce8142f4438a3028f44668675fc09fff
|
||||
)
|
||||
|
||||
set(KOTA_ENABLE_ZEST ON)
|
||||
|
||||
@@ -153,7 +153,7 @@ String values support `${workspace}` substitution.
|
||||
|
||||
## IPC Protocol
|
||||
|
||||
The master and workers communicate using custom RPC messages defined in `src/server/protocol.h`. Each message type has a `RequestTraits` or `NotificationTraits` specialization that defines the method name and result type.
|
||||
The master and workers communicate using custom RPC messages defined in `src/server/protocol/`. Each message type has a `RequestTraits` or `NotificationTraits` specialization that defines the method name and result type.
|
||||
|
||||
### Stateful Worker Messages
|
||||
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
schema: spec-driven
|
||||
created: 2026-04-22
|
||||
@@ -1,113 +0,0 @@
|
||||
## Downloaded Upstream Reference
|
||||
|
||||
Downloaded from GitHub tag `llvmorg-21.1.8` into `openspec/changes/explore-improve-folding-range-support/reference/clangd/llvmorg-21.1.8/` using `curl`.
|
||||
|
||||
Files downloaded:
|
||||
|
||||
- `clang-tools-extra/clangd/SemanticSelection.cpp`
|
||||
- `clang-tools-extra/clangd/SemanticSelection.h`
|
||||
- `clang-tools-extra/clangd/ClangdServer.cpp`
|
||||
- `clang-tools-extra/clangd/ClangdServer.h`
|
||||
- `clang-tools-extra/clangd/ClangdLSPServer.cpp`
|
||||
- `clang-tools-extra/clangd/Protocol.h`
|
||||
- `clang-tools-extra/clangd/Protocol.cpp`
|
||||
- `clang-tools-extra/clangd/test/folding-range.test`
|
||||
- `clang-tools-extra/clangd/unittests/SemanticSelectionTests.cpp`
|
||||
|
||||
Raw GitHub URLs used:
|
||||
|
||||
- `https://raw.githubusercontent.com/llvm/llvm-project/llvmorg-21.1.8/clang-tools-extra/clangd/SemanticSelection.cpp`
|
||||
- `https://raw.githubusercontent.com/llvm/llvm-project/llvmorg-21.1.8/clang-tools-extra/clangd/SemanticSelection.h`
|
||||
- `https://raw.githubusercontent.com/llvm/llvm-project/llvmorg-21.1.8/clang-tools-extra/clangd/ClangdServer.cpp`
|
||||
- `https://raw.githubusercontent.com/llvm/llvm-project/llvmorg-21.1.8/clang-tools-extra/clangd/ClangdServer.h`
|
||||
- `https://raw.githubusercontent.com/llvm/llvm-project/llvmorg-21.1.8/clang-tools-extra/clangd/ClangdLSPServer.cpp`
|
||||
- `https://raw.githubusercontent.com/llvm/llvm-project/llvmorg-21.1.8/clang-tools-extra/clangd/Protocol.h`
|
||||
- `https://raw.githubusercontent.com/llvm/llvm-project/llvmorg-21.1.8/clang-tools-extra/clangd/Protocol.cpp`
|
||||
- `https://raw.githubusercontent.com/llvm/llvm-project/llvmorg-21.1.8/clang-tools-extra/clangd/test/folding-range.test`
|
||||
- `https://raw.githubusercontent.com/llvm/llvm-project/llvmorg-21.1.8/clang-tools-extra/clangd/unittests/SemanticSelectionTests.cpp`
|
||||
|
||||
## Clice Reference Files
|
||||
|
||||
Current branch files compared against:
|
||||
|
||||
- `src/feature/folding_ranges.cpp`
|
||||
- `src/server/master_server.cpp`
|
||||
- `tests/unit/feature/folding_range_tests.cpp`
|
||||
|
||||
## Confirmed Comparison Findings
|
||||
|
||||
### 1. clangd already has dedicated comment folding and line-only rendering
|
||||
|
||||
clangd's pseudo-parser folding path in `SemanticSelection.cpp` explicitly handles:
|
||||
|
||||
- bracket folds with line-only adjustment at `SemanticSelection.cpp:223-235`
|
||||
- multiline block and contiguous comment-group folds at `SemanticSelection.cpp:238-269`
|
||||
|
||||
The request path wires `LineFoldingOnly` from client capabilities in:
|
||||
|
||||
- `ClangdLSPServer.cpp:545`
|
||||
- `ClangdServer.cpp:967-980`
|
||||
|
||||
Regression coverage exists in:
|
||||
|
||||
- `test/folding-range.test:6-20`
|
||||
- `unittests/SemanticSelectionTests.cpp:269-455`
|
||||
|
||||
Current clice does not have any comment collector in `src/feature/folding_ranges.cpp`, and the server request path in `src/server/master_server.cpp:517-525` forwards folding requests without any folding-specific options.
|
||||
|
||||
### 2. clice already folds more AST structure than clangd
|
||||
|
||||
clangd's AST-oriented `getFoldingRanges(ParsedAST &AST)` is intentionally narrow and only walks syntax-tree compound statements in `SemanticSelection.cpp:170-175`.
|
||||
|
||||
Current clice already folds:
|
||||
|
||||
- namespaces at `src/feature/folding_ranges.cpp:66-80`
|
||||
- records and access-specifier regions at `src/feature/folding_ranges.cpp:82-121`
|
||||
- function parameter lists and bodies at `src/feature/folding_ranges.cpp:123-144`, `246-269`
|
||||
- lambda captures at `src/feature/folding_ranges.cpp:134-143`
|
||||
- call argument lists at `src/feature/folding_ranges.cpp:146-179`
|
||||
- initializer lists at `src/feature/folding_ranges.cpp:181-185`
|
||||
- compound statements at `src/feature/folding_ranges.cpp:271-284`
|
||||
|
||||
That is materially broader than clangd's current AST folding baseline.
|
||||
|
||||
### 3. clice still exposes richer but less compatible output
|
||||
|
||||
Current clice maps many internal categories directly to custom kind strings in `src/feature/folding_ranges.cpp:35-54`, and carries `collapsed_text` through `src/feature/folding_ranges.cpp:56-60` and `src/feature/folding_ranges.cpp:363-376`.
|
||||
|
||||
clangd's downloaded protocol reference exposes only folding kinds in `Protocol.h:1970-1981` and serializes them in `Protocol.cpp:1680-1692`. The downloaded clangd protocol does not expose `collapsedText`, so `collapsedText` is a clice-specific protocol improvement rather than a clangd parity requirement.
|
||||
|
||||
### 4. clice still has an incomplete `#endif` branch closure bug
|
||||
|
||||
Current clice closes a prior conditional branch only when `#else` is seen at `src/feature/folding_ranges.cpp:302-311`. On `#endif`, it only pops the stack at `src/feature/folding_ranges.cpp:314-317` and emits no range for the final branch body.
|
||||
|
||||
clangd does not solve this either. Upstream `SemanticSelection.cpp:178-190` still leaves PP conditional regions and disabled regions as FIXME items. This means `#if` branch folding remains a clice extension opportunity, not a direct clangd parity target.
|
||||
|
||||
### 5. clice lacks client-capability plumbing for folding
|
||||
|
||||
Current clice only advertises `caps.folding_range_provider = true` in `src/server/master_server.cpp:244`, and the request handler in `src/server/master_server.cpp:517-525` forwards no `lineFoldingOnly`, `rangeLimit`, or `collapsedText` support signals into the feature layer.
|
||||
|
||||
clangd at least threads `LineFoldingOnly` from the client into folding generation via `ClangdLSPServer.cpp:545` and `ClangdServer.cpp:974-976`.
|
||||
|
||||
### 6. clice test coverage is still weaker in the most important gap areas
|
||||
|
||||
Current clice has structural tests, but the directive and pragma-region cases remain placeholder-only in `tests/unit/feature/folding_range_tests.cpp:398-430`. The tests also do not assert folding kinds.
|
||||
|
||||
clangd's downloaded tests cover:
|
||||
|
||||
- AST folding
|
||||
- comment folding
|
||||
- line-folding-only behavior
|
||||
- macro-related exclusion cases
|
||||
|
||||
Those are visible in `unittests/SemanticSelectionTests.cpp:269-455`.
|
||||
|
||||
## Planning Implications
|
||||
|
||||
The downloaded source narrows the real parity target:
|
||||
|
||||
- confirmed clangd parity gaps for clice: comment folding, `lineFoldingOnly`, standard public kind behavior, stronger tests
|
||||
- confirmed clice advantages over clangd: namespaces, access-specifier regions, lambda captures, function parameter folds, function-call folds, initializer folds, pragma regions, collapsed text
|
||||
- confirmed clice-specific extension space beyond clangd: inactive-branch folding, complete `#if/#elif/#else/#endif` folding, macro-definition folding, include/import grouping
|
||||
|
||||
The earlier `third_party` vendor plan was the wrong storage model for this branch. The correct model is a change-local downloaded reference under `openspec/changes/explore-improve-folding-range-support/reference/`.
|
||||
@@ -1,279 +0,0 @@
|
||||
## Context
|
||||
|
||||
`clice` currently implements folding ranges in `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.
|
||||
|
||||
The request path is currently split across:
|
||||
|
||||
- `src/feature/folding_ranges.cpp` for collection and rendering
|
||||
- `src/server/master_server.cpp` for request plumbing and capability advertisement
|
||||
- generated `kota` LSP protocol types for request/response shapes
|
||||
- `tests/unit/feature/folding_range_tests.cpp` for unit coverage
|
||||
|
||||
That split reveals three immediate shortcomings:
|
||||
|
||||
- the current collector has no comment path at all
|
||||
- folding-specific client capabilities such as `lineFoldingOnly`, `rangeLimit`, and `collapsedText` are not threaded through the request path
|
||||
- directive-related tests are mostly placeholders and do not assert important behavior
|
||||
|
||||
The comparison target for this exploration change should be fixed and versioned. At tag `llvmorg-21.1.8`, clangd's folding behavior is centered on `clang-tools-extra/clangd/SemanticSelection.cpp`, with request plumbing in `ClangdServer.cpp` and `ClangdLSPServer.cpp`, protocol types in `Protocol.h` and `Protocol.cpp`, and regression coverage in `test/folding-range.test` plus `unittests/SemanticSelectionTests.cpp`. Those files have been downloaded into `openspec/changes/explore-improve-folding-range-support/reference/clangd/llvmorg-21.1.8/`, and the side-by-side analysis lives in `comparison.md`.
|
||||
|
||||
Compared with that clangd baseline, the current gap is clear:
|
||||
|
||||
- clangd already has behavior that `clice` still lacks:
|
||||
- multiline comment folding
|
||||
- contiguous `//` comment-group folding
|
||||
- `lineFoldingOnly` rendering behavior wired from client capabilities into folding generation
|
||||
- consistent use of standard public folding kinds
|
||||
- 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 downloaded clangd source confirms that clangd still does not implement PP conditional regions, include grouping, or access-specifier folding in `SemanticSelection.cpp`; those are explicitly left as FIXME items upstream. The real parity target is therefore narrower than "match everything clangd does": comments, line-only rendering, standard kinds, and test discipline are the confirmed baseline gaps. Everything around directive groups, inactive branches, and richer structural categories remains a clice-specific extension opportunity.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- Download a focused clangd reference set from `llvmorg-21.1.8` into this change directory and use it as the explicit comparison baseline for this branch.
|
||||
- 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:**
|
||||
|
||||
- Import clangd implementation code directly into `clice` production paths or make the build depend on the downloaded reference files.
|
||||
- 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. Download a focused clangd reference set into the change directory before implementation work
|
||||
|
||||
The branch should first download a small, reviewable set of clangd's folding-related sources from tag `llvmorg-21.1.8` into `openspec/changes/explore-improve-folding-range-support/reference/clangd/llvmorg-21.1.8/`. The downloaded set should include the implementation, request plumbing, protocol types, and relevant tests that explain folding behavior, rather than the whole LLVM tree.
|
||||
|
||||
Why:
|
||||
|
||||
- it creates a stable review artifact for this exploration branch
|
||||
- later implementation work can point at local upstream code instead of external URLs
|
||||
- it keeps the eventual runtime change honest about what is parity work and what is a clice-specific extension
|
||||
- it avoids adding a repo-level vendor location for a one-branch study artifact
|
||||
|
||||
Alternative considered:
|
||||
|
||||
- Put the files under `third_party/`. Rejected because this is an exploration artifact, not a production dependency.
|
||||
|
||||
### 2. 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
|
||||
|
||||
Follow-up discussion narrows this design point: the existing `RawFoldingRange` model is finished for the current pipeline work and should not be redesigned here. The missing part is an explicit options object, passed as `Opts`/`FoldingRangeOptions`, that lets callers configure renderer behavior such as `line_folding_only`.
|
||||
|
||||
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.
|
||||
|
||||
### 3. 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.
|
||||
|
||||
### 4. Use the downloaded clangd files as a behavior reference, not as a direct implementation template
|
||||
|
||||
clangd's folding logic is text- and token-oriented rather than AST-oriented. `clice` should study the upstream behavior to match the useful parts, but it should not force its own collector architecture to look like clangd's when `CompilationUnitRef::directives()` and the existing AST visitor provide better raw data.
|
||||
|
||||
Why:
|
||||
|
||||
- parity should be measured at the behavior boundary, not by mirroring file structure
|
||||
- `clice` already has data sources that clangd does not, especially for directive metadata
|
||||
- this keeps the change focused on correctness and value, not on source-level imitation
|
||||
|
||||
Alternative considered:
|
||||
|
||||
- Rewrite `clice` folding collection to resemble clangd's text parser closely. Rejected because that would discard existing strengths without a clear benefit.
|
||||
|
||||
### 5. 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
|
||||
- preserve source spans that let the renderer adjust closing boundaries for `lineFoldingOnly` mode
|
||||
|
||||
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.
|
||||
|
||||
### 6. 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.
|
||||
|
||||
### 7. 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.
|
||||
|
||||
### 8. Separate clangd parity capabilities from clice-only protocol improvements
|
||||
|
||||
This change should treat comment folding, `lineFoldingOnly`, and standard public kinds as clangd parity work. `collapsedText` gating and deterministic `rangeLimit` trimming remain clice-side protocol improvements. The downloaded clangd `Protocol.h` / `Protocol.cpp` reference does not expose `collapsedText`, so the design and tests should not imply that clangd already provides that capability.
|
||||
|
||||
Why:
|
||||
|
||||
- it keeps the comparison honest
|
||||
- it allows reviewer discussion to separate "must match upstream baseline" from "valuable extra behavior"
|
||||
- it keeps spec language compatible with LSP without overstating clangd
|
||||
|
||||
Alternative considered:
|
||||
|
||||
- Treat all capability work as a clangd parity gap. Rejected because clangd's known folding path does not establish that broader claim.
|
||||
|
||||
### 9. 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`
|
||||
|
||||
Capability state should be translated into a feature-layer options object before rendering. The initial option needed by the current discussion is:
|
||||
|
||||
```cpp
|
||||
struct FoldingRangeOptions {
|
||||
bool line_folding_only = false;
|
||||
};
|
||||
```
|
||||
|
||||
The feature API should accept that options object separately from the source collector inputs, for example as `folding_ranges(unit, opts, encoding)`. Later protocol work can extend the same object for collapsed-text gating or range limiting without changing collectors.
|
||||
|
||||
Rendering rules:
|
||||
|
||||
- when `opts.line_folding_only = 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.
|
||||
- Thread capability state into collectors directly. Rejected because it would reopen the raw model and collection contract even though line-only behavior is a renderer policy.
|
||||
|
||||
### 10. 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` 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
|
||||
|
||||
- [The downloaded clangd reference set could sprawl or become noisy in review] -> Mitigation: keep only the small folding-related file set needed for comparison under the change directory and record the exact URLs in `comparison.md`.
|
||||
- [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. Download the focused clangd `llvmorg-21.1.8` folding reference files into `openspec/changes/explore-improve-folding-range-support/reference/clangd/llvmorg-21.1.8/`.
|
||||
2. Record the confirmed clangd-vs-clice comparison in this change, including exact URLs, which behaviors are parity gaps, and which are clice-specific extensions.
|
||||
3. Keep the existing `RawFoldingRange` data flow, add `FoldingRangeOptions` for `line_folding_only`, and add standard kind mapping.
|
||||
4. Add the comment collector and assertion-backed tests for multiline comment folding.
|
||||
5. Rewrite conditional-directive and `#pragma region` collection so `#if` branches close correctly through `#endif`.
|
||||
6. Add multiline macro folding and grouped include/import collectors.
|
||||
7. Wire folding client capabilities through initialize/request handling and add integration coverage.
|
||||
8. Add `rangeLimit` trimming and regression cleanup after the new collectors are in place.
|
||||
|
||||
Rollback strategy:
|
||||
|
||||
- If the downloaded reference set becomes more distracting than useful, keep only the documented comparison notes and delete the change-local downloads before merging.
|
||||
- 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
|
||||
|
||||
- Are `test/folding-range.test` and `unittests/SemanticSelectionTests.cpp` enough for ongoing comparison, or will later implementation work need more upstream folding-related tests?
|
||||
- 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?
|
||||
@@ -1,36 +0,0 @@
|
||||
## Why
|
||||
|
||||
`clice` already goes beyond clangd in several structural folding cases: it can fold namespaces, records, function parameter lists and bodies, lambda captures, call argument lists, access-specifier sections, and some preprocessor regions. However, the current implementation in `src/feature/folding_ranges.cpp` still misses several baseline behaviors that clangd already exposes well, especially multiline comment folding, line-only folding rendering, standard public folding kinds, and a stronger regression test matrix. Its preprocessor branch folding is also not yet fully closed.
|
||||
|
||||
This exploration branch needs a fixed upstream reference instead of relying on memory. At tag `llvmorg-21.1.8`, clangd's folding implementation is centered around `clang-tools-extra/clangd/SemanticSelection.cpp`, with request plumbing in `ClangdServer.cpp` and `ClangdLSPServer.cpp`, protocol types in `Protocol.h` and `Protocol.cpp`, and folding coverage in `test/folding-range.test` plus `unittests/SemanticSelectionTests.cpp`. Downloading those files into this change directory with `curl` gives the branch a stable, reviewable baseline for side-by-side comparison without introducing a repository-level vendor tree.
|
||||
|
||||
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: after filling the real parity gaps, folding ranges can become a more useful C/C++ feature by covering macro definitions, `#if` branches, and include/import groups that clangd does not currently handle well.
|
||||
|
||||
Follow-up discussion clarified the split with `split-folding-range-pipeline`: the existing `RawFoldingRange` model is finished for the current architecture work. The missing capability path is explicit folding options, passed as `Opts`/`FoldingRangeOptions`, so `line_folding_only` can be requested by server capability plumbing and consumed by the renderer.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Download the clangd folding-range reference files for tag `llvmorg-21.1.8` into `openspec/changes/explore-improve-folding-range-support/reference/clangd/llvmorg-21.1.8/` using `curl` from GitHub raw URLs.
|
||||
- Record a concrete clangd-vs-clice comparison in `comparison.md`, including the upstream files consulted, exact download URLs, confirmed parity gaps, clice-only capabilities, and known implementation bugs.
|
||||
- Fill the remaining folding-range baseline gaps between `clice` and clangd, especially multiline comment folding, line-only folding rendering, and standard public kind mapping.
|
||||
- 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` output so standard kinds remain compatible while clice-specific fold categories degrade predictably.
|
||||
- Make folding range responses honor client capabilities such as `lineFoldingOnly`, optional `collapsedText` support, and range limiting, using folding options rather than collector-specific state.
|
||||
- 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
|
||||
|
||||
- A change-local upstream reference set has been downloaded under `openspec/changes/explore-improve-folding-range-support/reference/clangd/llvmorg-21.1.8/`, limited to the folding-range implementation, protocol, and tests needed for analysis.
|
||||
- The side-by-side analysis is recorded in `openspec/changes/explore-improve-folding-range-support/comparison.md`.
|
||||
- Primary runtime impact is in `src/feature/folding_ranges.cpp`, the compile-unit/preprocessor metadata access paths, request handling in `src/server/master_server.cpp`, and the folding options object used to carry capability-derived rendering choices.
|
||||
- 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.
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,531 +0,0 @@
|
||||
//===--- ClangdServer.h - Main clangd server code ----------------*- C++-*-===//
|
||||
//
|
||||
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
|
||||
// See https://llvm.org/LICENSE.txt for license information.
|
||||
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANGD_CLANGDSERVER_H
|
||||
#define LLVM_CLANG_TOOLS_EXTRA_CLANGD_CLANGDSERVER_H
|
||||
|
||||
#include "CodeComplete.h"
|
||||
#include "ConfigProvider.h"
|
||||
#include "Diagnostics.h"
|
||||
#include "DraftStore.h"
|
||||
#include "FeatureModule.h"
|
||||
#include "GlobalCompilationDatabase.h"
|
||||
#include "Hover.h"
|
||||
#include "ModulesBuilder.h"
|
||||
#include "Protocol.h"
|
||||
#include "SemanticHighlighting.h"
|
||||
#include "TUScheduler.h"
|
||||
#include "XRefs.h"
|
||||
#include "index/Background.h"
|
||||
#include "index/FileIndex.h"
|
||||
#include "index/Index.h"
|
||||
#include "refactor/Rename.h"
|
||||
#include "refactor/Tweak.h"
|
||||
#include "support/Function.h"
|
||||
#include "support/MemoryTree.h"
|
||||
#include "support/Path.h"
|
||||
#include "support/ThreadsafeFS.h"
|
||||
#include "clang/Tooling/Core/Replacement.h"
|
||||
#include "llvm/ADT/ArrayRef.h"
|
||||
#include "llvm/ADT/FunctionExtras.h"
|
||||
#include "llvm/ADT/StringRef.h"
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
|
||||
namespace clang {
|
||||
namespace clangd {
|
||||
/// Manages a collection of source files and derived data (ASTs, indexes),
|
||||
/// and provides language-aware features such as code completion.
|
||||
///
|
||||
/// The primary client is ClangdLSPServer which exposes these features via
|
||||
/// the Language Server protocol. ClangdServer may also be embedded directly,
|
||||
/// though its API is not stable over time.
|
||||
///
|
||||
/// ClangdServer should be used from a single thread. Many potentially-slow
|
||||
/// operations have asynchronous APIs and deliver their results on another
|
||||
/// thread.
|
||||
/// Such operations support cancellation: if the caller sets up a cancelable
|
||||
/// context, many operations will notice cancellation and fail early.
|
||||
/// (ClangdLSPServer uses this to implement $/cancelRequest).
|
||||
class ClangdServer {
|
||||
public:
|
||||
/// Interface with hooks for users of ClangdServer to be notified of events.
|
||||
class Callbacks {
|
||||
public:
|
||||
virtual ~Callbacks() = default;
|
||||
|
||||
/// Called by ClangdServer when \p Diagnostics for \p File are ready.
|
||||
/// These pushed diagnostics might correspond to an older version of the
|
||||
/// file, they do not interfere with "pull-based" ClangdServer::diagnostics.
|
||||
/// May be called concurrently for separate files, not for a single file.
|
||||
virtual void onDiagnosticsReady(PathRef File, llvm::StringRef Version,
|
||||
llvm::ArrayRef<Diag> Diagnostics) {}
|
||||
/// Called whenever the file status is updated.
|
||||
/// May be called concurrently for separate files, not for a single file.
|
||||
virtual void onFileUpdated(PathRef File, const TUStatus &Status) {}
|
||||
|
||||
/// Called when background indexing tasks are enqueued/started/completed.
|
||||
/// Not called concurrently.
|
||||
virtual void
|
||||
onBackgroundIndexProgress(const BackgroundQueue::Stats &Stats) {}
|
||||
|
||||
/// Called when the meaning of a source code may have changed without an
|
||||
/// edit. Usually clients assume that responses to requests are valid until
|
||||
/// they next edit the file. If they're invalidated at other times, we
|
||||
/// should tell the client. In particular, when an asynchronous preamble
|
||||
/// build finishes, we can provide more accurate semantic tokens, so we
|
||||
/// should tell the client to refresh.
|
||||
virtual void onSemanticsMaybeChanged(PathRef File) {}
|
||||
|
||||
/// Called by ClangdServer when some \p InactiveRegions for \p File are
|
||||
/// ready.
|
||||
virtual void onInactiveRegionsReady(PathRef File,
|
||||
std::vector<Range> InactiveRegions) {}
|
||||
};
|
||||
/// Creates a context provider that loads and installs config.
|
||||
/// Errors in loading config are reported as diagnostics via Callbacks.
|
||||
/// (This is typically used as ClangdServer::Options::ContextProvider).
|
||||
static std::function<Context(PathRef)>
|
||||
createConfiguredContextProvider(const config::Provider *Provider,
|
||||
ClangdServer::Callbacks *);
|
||||
|
||||
struct Options {
|
||||
/// To process requests asynchronously, ClangdServer spawns worker threads.
|
||||
/// If this is zero, no threads are spawned. All work is done on the calling
|
||||
/// thread, and callbacks are invoked before "async" functions return.
|
||||
unsigned AsyncThreadsCount = getDefaultAsyncThreadsCount();
|
||||
|
||||
/// AST caching policy. The default is to keep up to 3 ASTs in memory.
|
||||
ASTRetentionPolicy RetentionPolicy;
|
||||
|
||||
/// Cached preambles are potentially large. If false, store them on disk.
|
||||
bool StorePreamblesInMemory = true;
|
||||
|
||||
/// Call hierarchy's outgoing calls feature requires additional index
|
||||
/// serving structures which increase memory usage. If false, these are
|
||||
/// not created and the feature is not enabled.
|
||||
bool EnableOutgoingCalls = true;
|
||||
|
||||
/// This throttler controls which preambles may be built at a given time.
|
||||
clangd::PreambleThrottler *PreambleThrottler = nullptr;
|
||||
|
||||
/// Manages to build module files.
|
||||
ModulesBuilder *ModulesManager = nullptr;
|
||||
|
||||
/// If true, ClangdServer builds a dynamic in-memory index for symbols in
|
||||
/// opened files and uses the index to augment code completion results.
|
||||
bool BuildDynamicSymbolIndex = false;
|
||||
/// If true, ClangdServer automatically indexes files in the current project
|
||||
/// on background threads. The index is stored in the project root.
|
||||
bool BackgroundIndex = false;
|
||||
llvm::ThreadPriority BackgroundIndexPriority = llvm::ThreadPriority::Low;
|
||||
|
||||
/// If set, use this index to augment code completion results.
|
||||
SymbolIndex *StaticIndex = nullptr;
|
||||
|
||||
/// If set, queried to derive a processing context for some work.
|
||||
/// Usually used to inject Config (see createConfiguredContextProvider).
|
||||
///
|
||||
/// When the provider is called, the active context will be that inherited
|
||||
/// from the request (e.g. addDocument()), or from the ClangdServer
|
||||
/// constructor if there is no such request (e.g. background indexing).
|
||||
///
|
||||
/// The path is an absolute path of the file being processed.
|
||||
/// If there is no particular file (e.g. project loading) then it is empty.
|
||||
std::function<Context(PathRef)> ContextProvider;
|
||||
|
||||
/// The Options provider to use when running clang-tidy. If null, clang-tidy
|
||||
/// checks will be disabled.
|
||||
TidyProviderRef ClangTidyProvider;
|
||||
|
||||
/// Clangd's workspace root. Relevant for "workspace" operations not bound
|
||||
/// to a particular file.
|
||||
/// FIXME: If not set, should use the current working directory.
|
||||
std::optional<std::string> WorkspaceRoot;
|
||||
|
||||
/// The resource directory is used to find internal headers, overriding
|
||||
/// defaults and -resource-dir compiler flag).
|
||||
/// If std::nullopt, ClangdServer calls
|
||||
/// CompilerInvocation::GetResourcePath() to obtain the standard resource
|
||||
/// directory.
|
||||
std::optional<std::string> ResourceDir;
|
||||
|
||||
/// Time to wait after a new file version before computing diagnostics.
|
||||
DebouncePolicy UpdateDebounce = DebouncePolicy{
|
||||
/*Min=*/std::chrono::milliseconds(50),
|
||||
/*Max=*/std::chrono::milliseconds(500),
|
||||
/*RebuildRatio=*/1,
|
||||
};
|
||||
|
||||
/// Cancel certain requests if the file changes before they begin running.
|
||||
/// This is useful for "transient" actions like enumerateTweaks that were
|
||||
/// likely implicitly generated, and avoids redundant work if clients forget
|
||||
/// to cancel. Clients that always cancel stale requests should clear this.
|
||||
bool ImplicitCancellation = true;
|
||||
|
||||
/// Clangd will execute compiler drivers matching one of these globs to
|
||||
/// fetch system include path.
|
||||
std::vector<std::string> QueryDriverGlobs;
|
||||
|
||||
// Whether the client supports folding only complete lines.
|
||||
bool LineFoldingOnly = false;
|
||||
|
||||
FeatureModuleSet *FeatureModules = nullptr;
|
||||
/// If true, use the dirty buffer contents when building Preambles.
|
||||
bool UseDirtyHeaders = false;
|
||||
|
||||
// If true, parse emplace-like functions in the preamble.
|
||||
bool PreambleParseForwardingFunctions = true;
|
||||
|
||||
/// Whether include fixer insertions for Objective-C code should use #import
|
||||
/// instead of #include.
|
||||
bool ImportInsertions = false;
|
||||
|
||||
/// Whether to collect and publish information about inactive preprocessor
|
||||
/// regions in the document.
|
||||
bool PublishInactiveRegions = false;
|
||||
|
||||
explicit operator TUScheduler::Options() const;
|
||||
};
|
||||
// Sensible default options for use in tests.
|
||||
// Features like indexing must be enabled if desired.
|
||||
static Options optsForTest();
|
||||
|
||||
/// Creates a new ClangdServer instance.
|
||||
///
|
||||
/// ClangdServer uses \p CDB to obtain compilation arguments for parsing. Note
|
||||
/// that ClangdServer only obtains compilation arguments once for each newly
|
||||
/// added file (i.e., when processing a first call to addDocument) and reuses
|
||||
/// those arguments for subsequent reparses. However, ClangdServer will check
|
||||
/// if compilation arguments changed on calls to forceReparse().
|
||||
ClangdServer(const GlobalCompilationDatabase &CDB, const ThreadsafeFS &TFS,
|
||||
const Options &Opts, Callbacks *Callbacks = nullptr);
|
||||
~ClangdServer();
|
||||
|
||||
/// Gets the installed feature module of a given type, if any.
|
||||
/// This exposes access the public interface of feature modules that have one.
|
||||
template <typename Mod> Mod *featureModule() {
|
||||
return FeatureModules ? FeatureModules->get<Mod>() : nullptr;
|
||||
}
|
||||
template <typename Mod> const Mod *featureModule() const {
|
||||
return FeatureModules ? FeatureModules->get<Mod>() : nullptr;
|
||||
}
|
||||
|
||||
/// Add a \p File to the list of tracked C++ files or update the contents if
|
||||
/// \p File is already tracked. Also schedules parsing of the AST for it on a
|
||||
/// separate thread. When the parsing is complete, DiagConsumer passed in
|
||||
/// constructor will receive onDiagnosticsReady callback.
|
||||
/// Version identifies this snapshot and is propagated to ASTs, preambles,
|
||||
/// diagnostics etc built from it. If empty, a version number is generated.
|
||||
void addDocument(PathRef File, StringRef Contents,
|
||||
llvm::StringRef Version = "null",
|
||||
WantDiagnostics WD = WantDiagnostics::Auto,
|
||||
bool ForceRebuild = false);
|
||||
|
||||
/// Remove \p File from list of tracked files, schedule a request to free
|
||||
/// resources associated with it. Pending diagnostics for closed files may not
|
||||
/// be delivered, even if requested with WantDiags::Auto or WantDiags::Yes.
|
||||
/// An empty set of diagnostics will be delivered, with Version = "".
|
||||
void removeDocument(PathRef File);
|
||||
|
||||
/// Requests a reparse of currently opened files using their latest source.
|
||||
/// This will typically only rebuild if something other than the source has
|
||||
/// changed (e.g. the CDB yields different flags, or files included in the
|
||||
/// preamble have been modified).
|
||||
void reparseOpenFilesIfNeeded(
|
||||
llvm::function_ref<bool(llvm::StringRef File)> Filter);
|
||||
|
||||
/// Run code completion for \p File at \p Pos.
|
||||
///
|
||||
/// This method should only be called for currently tracked files.
|
||||
void codeComplete(PathRef File, Position Pos,
|
||||
const clangd::CodeCompleteOptions &Opts,
|
||||
Callback<CodeCompleteResult> CB);
|
||||
|
||||
/// Provide signature help for \p File at \p Pos. This method should only be
|
||||
/// called for tracked files.
|
||||
void signatureHelp(PathRef File, Position Pos, MarkupKind DocumentationFormat,
|
||||
Callback<SignatureHelp> CB);
|
||||
|
||||
/// Find declaration/definition locations of symbol at a specified position.
|
||||
void locateSymbolAt(PathRef File, Position Pos,
|
||||
Callback<std::vector<LocatedSymbol>> CB);
|
||||
|
||||
/// Switch to a corresponding source file when given a header file, and vice
|
||||
/// versa.
|
||||
void switchSourceHeader(PathRef Path,
|
||||
Callback<std::optional<clangd::Path>> CB);
|
||||
|
||||
/// Get document highlights for a given position.
|
||||
void findDocumentHighlights(PathRef File, Position Pos,
|
||||
Callback<std::vector<DocumentHighlight>> CB);
|
||||
|
||||
/// Get code hover for a given position.
|
||||
void findHover(PathRef File, Position Pos,
|
||||
Callback<std::optional<HoverInfo>> CB);
|
||||
|
||||
/// Get information about type hierarchy for a given position.
|
||||
void typeHierarchy(PathRef File, Position Pos, int Resolve,
|
||||
TypeHierarchyDirection Direction,
|
||||
Callback<std::vector<TypeHierarchyItem>> CB);
|
||||
/// Get direct parents of a type hierarchy item.
|
||||
void superTypes(const TypeHierarchyItem &Item,
|
||||
Callback<std::optional<std::vector<TypeHierarchyItem>>> CB);
|
||||
/// Get direct children of a type hierarchy item.
|
||||
void subTypes(const TypeHierarchyItem &Item,
|
||||
Callback<std::vector<TypeHierarchyItem>> CB);
|
||||
|
||||
/// Resolve type hierarchy item in the given direction.
|
||||
void resolveTypeHierarchy(TypeHierarchyItem Item, int Resolve,
|
||||
TypeHierarchyDirection Direction,
|
||||
Callback<std::optional<TypeHierarchyItem>> CB);
|
||||
|
||||
/// Get information about call hierarchy for a given position.
|
||||
void prepareCallHierarchy(PathRef File, Position Pos,
|
||||
Callback<std::vector<CallHierarchyItem>> CB);
|
||||
|
||||
/// Resolve incoming calls for a given call hierarchy item.
|
||||
void incomingCalls(const CallHierarchyItem &Item,
|
||||
Callback<std::vector<CallHierarchyIncomingCall>>);
|
||||
|
||||
/// Resolve outgoing calls for a given call hierarchy item.
|
||||
void outgoingCalls(const CallHierarchyItem &Item,
|
||||
Callback<std::vector<CallHierarchyOutgoingCall>>);
|
||||
|
||||
/// Resolve inlay hints for a given document.
|
||||
void inlayHints(PathRef File, std::optional<Range> RestrictRange,
|
||||
Callback<std::vector<InlayHint>>);
|
||||
|
||||
/// Retrieve the top symbols from the workspace matching a query.
|
||||
void workspaceSymbols(StringRef Query, int Limit,
|
||||
Callback<std::vector<SymbolInformation>> CB);
|
||||
|
||||
/// Retrieve the symbols within the specified file.
|
||||
void documentSymbols(StringRef File,
|
||||
Callback<std::vector<DocumentSymbol>> CB);
|
||||
|
||||
/// Retrieve ranges that can be used to fold code within the specified file.
|
||||
void foldingRanges(StringRef File, Callback<std::vector<FoldingRange>> CB);
|
||||
|
||||
/// Retrieve implementations for virtual method.
|
||||
void findImplementations(PathRef File, Position Pos,
|
||||
Callback<std::vector<LocatedSymbol>> CB);
|
||||
|
||||
/// Retrieve symbols for types referenced at \p Pos.
|
||||
void findType(PathRef File, Position Pos,
|
||||
Callback<std::vector<LocatedSymbol>> CB);
|
||||
|
||||
/// Retrieve locations for symbol references.
|
||||
void findReferences(PathRef File, Position Pos, uint32_t Limit,
|
||||
bool AddContainer, Callback<ReferencesResult> CB);
|
||||
|
||||
/// Run formatting for the \p File with content \p Code.
|
||||
/// If \p Rng is non-empty, formats only those regions.
|
||||
void formatFile(PathRef File, const std::vector<Range> &Rngs,
|
||||
Callback<tooling::Replacements> CB);
|
||||
|
||||
/// Run formatting after \p TriggerText was typed at \p Pos in \p File with
|
||||
/// content \p Code.
|
||||
void formatOnType(PathRef File, Position Pos, StringRef TriggerText,
|
||||
Callback<std::vector<TextEdit>> CB);
|
||||
|
||||
/// Test the validity of a rename operation.
|
||||
///
|
||||
/// If NewName is provided, it performs a name validation.
|
||||
void prepareRename(PathRef File, Position Pos,
|
||||
std::optional<std::string> NewName,
|
||||
const RenameOptions &RenameOpts,
|
||||
Callback<RenameResult> CB);
|
||||
|
||||
/// Rename all occurrences of the symbol at the \p Pos in \p File to
|
||||
/// \p NewName.
|
||||
/// If WantFormat is false, the final TextEdit will be not formatted,
|
||||
/// embedders could use this method to get all occurrences of the symbol (e.g.
|
||||
/// highlighting them in prepare stage).
|
||||
void rename(PathRef File, Position Pos, llvm::StringRef NewName,
|
||||
const RenameOptions &Opts, Callback<RenameResult> CB);
|
||||
|
||||
struct TweakRef {
|
||||
std::string ID; /// ID to pass for applyTweak.
|
||||
std::string Title; /// A single-line message to show in the UI.
|
||||
llvm::StringLiteral Kind;
|
||||
};
|
||||
|
||||
// Ref to the clangd::Diag.
|
||||
struct DiagRef {
|
||||
clangd::Range Range;
|
||||
std::string Message;
|
||||
bool operator==(const DiagRef &Other) const {
|
||||
return std::tie(Range, Message) == std::tie(Other.Range, Other.Message);
|
||||
}
|
||||
bool operator<(const DiagRef &Other) const {
|
||||
return std::tie(Range, Message) < std::tie(Other.Range, Other.Message);
|
||||
}
|
||||
};
|
||||
|
||||
struct CodeActionInputs {
|
||||
std::string File;
|
||||
Range Selection;
|
||||
|
||||
/// Requested kind of actions to return.
|
||||
std::vector<std::string> RequestedActionKinds;
|
||||
|
||||
/// Diagnostics attached to the code action request.
|
||||
std::vector<DiagRef> Diagnostics;
|
||||
|
||||
/// Tweaks where Filter returns false will not be checked or included.
|
||||
std::function<bool(const Tweak &)> TweakFilter;
|
||||
};
|
||||
struct CodeActionResult {
|
||||
std::string Version;
|
||||
struct QuickFix {
|
||||
DiagRef Diag;
|
||||
Fix F;
|
||||
};
|
||||
std::vector<QuickFix> QuickFixes;
|
||||
std::vector<TweakRef> TweakRefs;
|
||||
struct Rename {
|
||||
DiagRef Diag;
|
||||
std::string FixMessage;
|
||||
std::string NewName;
|
||||
};
|
||||
std::vector<Rename> Renames;
|
||||
};
|
||||
/// Surface code actions (quick-fixes for diagnostics, or available code
|
||||
/// tweaks) for a given range in a file.
|
||||
void codeAction(const CodeActionInputs &Inputs,
|
||||
Callback<CodeActionResult> CB);
|
||||
|
||||
/// Apply the code tweak with a specified \p ID.
|
||||
void applyTweak(PathRef File, Range Sel, StringRef ID,
|
||||
Callback<Tweak::Effect> CB);
|
||||
|
||||
/// Called when an event occurs for a watched file in the workspace.
|
||||
void onFileEvent(const DidChangeWatchedFilesParams &Params);
|
||||
|
||||
/// Get symbol info for given position.
|
||||
/// Clangd extension - not part of official LSP.
|
||||
void symbolInfo(PathRef File, Position Pos,
|
||||
Callback<std::vector<SymbolDetails>> CB);
|
||||
|
||||
/// Get semantic ranges around a specified position in a file.
|
||||
void semanticRanges(PathRef File, const std::vector<Position> &Pos,
|
||||
Callback<std::vector<SelectionRange>> CB);
|
||||
|
||||
/// Get all document links in a file.
|
||||
void documentLinks(PathRef File, Callback<std::vector<DocumentLink>> CB);
|
||||
|
||||
void semanticHighlights(PathRef File,
|
||||
Callback<std::vector<HighlightingToken>>);
|
||||
|
||||
/// Describe the AST subtree for a piece of code.
|
||||
void getAST(PathRef File, std::optional<Range> R,
|
||||
Callback<std::optional<ASTNode>> CB);
|
||||
|
||||
/// Runs an arbitrary action that has access to the AST of the specified file.
|
||||
/// The action will execute on one of ClangdServer's internal threads.
|
||||
/// The AST is only valid for the duration of the callback.
|
||||
/// As with other actions, the file must have been opened.
|
||||
void customAction(PathRef File, llvm::StringRef Name,
|
||||
Callback<InputsAndAST> Action);
|
||||
|
||||
/// Fetches diagnostics for current version of the \p File. This might fail if
|
||||
/// server is busy (building a preamble) and would require a long time to
|
||||
/// prepare diagnostics. If it fails, clients should wait for
|
||||
/// onSemanticsMaybeChanged and then retry.
|
||||
/// These 'pulled' diagnostics do not interfere with the diagnostics 'pushed'
|
||||
/// to Callbacks::onDiagnosticsReady, and clients may use either or both.
|
||||
void diagnostics(PathRef File, Callback<std::vector<Diag>> CB);
|
||||
|
||||
/// Returns estimated memory usage and other statistics for each of the
|
||||
/// currently open files.
|
||||
/// Overall memory usage of clangd may be significantly more than reported
|
||||
/// here, as this metric does not account (at least) for:
|
||||
/// - memory occupied by static and dynamic index,
|
||||
/// - memory required for in-flight requests,
|
||||
/// FIXME: those metrics might be useful too, we should add them.
|
||||
llvm::StringMap<TUScheduler::FileStats> fileStats() const;
|
||||
|
||||
/// Gets the contents of a currently tracked file. Returns nullptr if the file
|
||||
/// isn't being tracked.
|
||||
std::shared_ptr<const std::string> getDraft(PathRef File) const;
|
||||
|
||||
// Blocks the main thread until the server is idle. Only for use in tests.
|
||||
// Returns false if the timeout expires.
|
||||
// FIXME: various subcomponents each get the full timeout, so it's more of
|
||||
// an order of magnitude than a hard deadline.
|
||||
[[nodiscard]] bool
|
||||
blockUntilIdleForTest(std::optional<double> TimeoutSeconds = 10);
|
||||
|
||||
/// Builds a nested representation of memory used by components.
|
||||
void profile(MemoryTree &MT) const;
|
||||
|
||||
private:
|
||||
FeatureModuleSet *FeatureModules;
|
||||
const GlobalCompilationDatabase &CDB;
|
||||
const ThreadsafeFS &getHeaderFS() const {
|
||||
return UseDirtyHeaders ? *DirtyFS : TFS;
|
||||
}
|
||||
const ThreadsafeFS &TFS;
|
||||
|
||||
Path ResourceDir;
|
||||
// The index used to look up symbols. This could be:
|
||||
// - null (all index functionality is optional)
|
||||
// - the dynamic index owned by ClangdServer (DynamicIdx)
|
||||
// - the static index passed to the constructor
|
||||
// - a merged view of a static and dynamic index (MergedIndex)
|
||||
const SymbolIndex *Index = nullptr;
|
||||
// If present, an index of symbols in open files. Read via *Index.
|
||||
std::unique_ptr<FileIndex> DynamicIdx;
|
||||
// If present, the new "auto-index" maintained in background threads.
|
||||
std::unique_ptr<BackgroundIndex> BackgroundIdx;
|
||||
// Storage for merged views of the various indexes.
|
||||
std::vector<std::unique_ptr<SymbolIndex>> MergedIdx;
|
||||
// Manage module files.
|
||||
ModulesBuilder *ModulesManager = nullptr;
|
||||
|
||||
// When set, provides clang-tidy options for a specific file.
|
||||
TidyProviderRef ClangTidyProvider;
|
||||
|
||||
bool UseDirtyHeaders = false;
|
||||
|
||||
// Whether the client supports folding only complete lines.
|
||||
bool LineFoldingOnly = false;
|
||||
|
||||
bool PreambleParseForwardingFunctions = true;
|
||||
|
||||
bool ImportInsertions = false;
|
||||
|
||||
bool PublishInactiveRegions = false;
|
||||
|
||||
// GUARDED_BY(CachedCompletionFuzzyFindRequestMutex)
|
||||
llvm::StringMap<std::optional<FuzzyFindRequest>>
|
||||
CachedCompletionFuzzyFindRequestByFile;
|
||||
mutable std::mutex CachedCompletionFuzzyFindRequestMutex;
|
||||
|
||||
std::optional<std::string> WorkspaceRoot;
|
||||
std::optional<AsyncTaskRunner> IndexTasks; // for stdlib indexing.
|
||||
std::optional<TUScheduler> WorkScheduler;
|
||||
// Invalidation policy used for actions that we assume are "transient".
|
||||
TUScheduler::ASTActionInvalidation Transient;
|
||||
|
||||
// Store of the current versions of the open documents.
|
||||
// Only written from the main thread (despite being threadsafe).
|
||||
DraftStore DraftMgr;
|
||||
|
||||
std::unique_ptr<ThreadsafeFS> DirtyFS;
|
||||
};
|
||||
|
||||
} // namespace clangd
|
||||
} // namespace clang
|
||||
|
||||
#endif
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,274 +0,0 @@
|
||||
//===--- SemanticSelection.cpp -----------------------------------*- C++-*-===//
|
||||
//
|
||||
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
|
||||
// See https://llvm.org/LICENSE.txt for license information.
|
||||
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
#include "SemanticSelection.h"
|
||||
#include "ParsedAST.h"
|
||||
#include "Protocol.h"
|
||||
#include "Selection.h"
|
||||
#include "SourceCode.h"
|
||||
#include "clang/AST/DeclBase.h"
|
||||
#include "clang/Basic/SourceLocation.h"
|
||||
#include "clang/Basic/SourceManager.h"
|
||||
#include "clang/Tooling/Syntax/BuildTree.h"
|
||||
#include "clang/Tooling/Syntax/Nodes.h"
|
||||
#include "clang/Tooling/Syntax/TokenBufferTokenManager.h"
|
||||
#include "clang/Tooling/Syntax/Tree.h"
|
||||
#include "llvm/ADT/ArrayRef.h"
|
||||
#include "llvm/ADT/StringRef.h"
|
||||
#include "llvm/Support/Casting.h"
|
||||
#include "llvm/Support/Error.h"
|
||||
#include "support/Bracket.h"
|
||||
#include "support/DirectiveTree.h"
|
||||
#include "support/Token.h"
|
||||
#include <optional>
|
||||
#include <queue>
|
||||
#include <vector>
|
||||
|
||||
namespace clang {
|
||||
namespace clangd {
|
||||
namespace {
|
||||
|
||||
// Adds Range \p R to the Result if it is distinct from the last added Range.
|
||||
// Assumes that only consecutive ranges can coincide.
|
||||
void addIfDistinct(const Range &R, std::vector<Range> &Result) {
|
||||
if (Result.empty() || Result.back() != R) {
|
||||
Result.push_back(R);
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<FoldingRange> toFoldingRange(SourceRange SR,
|
||||
const SourceManager &SM) {
|
||||
const auto Begin = SM.getDecomposedLoc(SR.getBegin()),
|
||||
End = SM.getDecomposedLoc(SR.getEnd());
|
||||
// Do not produce folding ranges if either range ends is not within the main
|
||||
// file. Macros have their own FileID so this also checks if locations are not
|
||||
// within the macros.
|
||||
if ((Begin.first != SM.getMainFileID()) || (End.first != SM.getMainFileID()))
|
||||
return std::nullopt;
|
||||
FoldingRange Range;
|
||||
Range.startCharacter = SM.getColumnNumber(Begin.first, Begin.second) - 1;
|
||||
Range.startLine = SM.getLineNumber(Begin.first, Begin.second) - 1;
|
||||
Range.endCharacter = SM.getColumnNumber(End.first, End.second) - 1;
|
||||
Range.endLine = SM.getLineNumber(End.first, End.second) - 1;
|
||||
return Range;
|
||||
}
|
||||
|
||||
std::optional<FoldingRange>
|
||||
extractFoldingRange(const syntax::Node *Node,
|
||||
const syntax::TokenBufferTokenManager &TM) {
|
||||
if (const auto *Stmt = dyn_cast<syntax::CompoundStatement>(Node)) {
|
||||
const auto *LBrace = cast_or_null<syntax::Leaf>(
|
||||
Stmt->findChild(syntax::NodeRole::OpenParen));
|
||||
// FIXME(kirillbobyrev): This should find the last child. Compound
|
||||
// statements have only one pair of braces so this is valid but for other
|
||||
// node kinds it might not be correct.
|
||||
const auto *RBrace = cast_or_null<syntax::Leaf>(
|
||||
Stmt->findChild(syntax::NodeRole::CloseParen));
|
||||
if (!LBrace || !RBrace)
|
||||
return std::nullopt;
|
||||
// Fold the entire range within braces, including whitespace.
|
||||
const SourceLocation LBraceLocInfo =
|
||||
TM.getToken(LBrace->getTokenKey())->endLocation(),
|
||||
RBraceLocInfo =
|
||||
TM.getToken(RBrace->getTokenKey())->location();
|
||||
auto Range = toFoldingRange(SourceRange(LBraceLocInfo, RBraceLocInfo),
|
||||
TM.sourceManager());
|
||||
// Do not generate folding range for compound statements without any
|
||||
// nodes and newlines.
|
||||
if (Range && Range->startLine != Range->endLine)
|
||||
return Range;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Traverse the tree and collect folding ranges along the way.
|
||||
std::vector<FoldingRange>
|
||||
collectFoldingRanges(const syntax::Node *Root,
|
||||
const syntax::TokenBufferTokenManager &TM) {
|
||||
std::queue<const syntax::Node *> Nodes;
|
||||
Nodes.push(Root);
|
||||
std::vector<FoldingRange> Result;
|
||||
while (!Nodes.empty()) {
|
||||
const syntax::Node *Node = Nodes.front();
|
||||
Nodes.pop();
|
||||
const auto Range = extractFoldingRange(Node, TM);
|
||||
if (Range)
|
||||
Result.push_back(*Range);
|
||||
if (const auto *T = dyn_cast<syntax::Tree>(Node))
|
||||
for (const auto *NextNode = T->getFirstChild(); NextNode;
|
||||
NextNode = NextNode->getNextSibling())
|
||||
Nodes.push(NextNode);
|
||||
}
|
||||
return Result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
llvm::Expected<SelectionRange> getSemanticRanges(ParsedAST &AST, Position Pos) {
|
||||
std::vector<Range> Ranges;
|
||||
const auto &SM = AST.getSourceManager();
|
||||
const auto &LangOpts = AST.getLangOpts();
|
||||
|
||||
auto FID = SM.getMainFileID();
|
||||
auto Offset = positionToOffset(SM.getBufferData(FID), Pos);
|
||||
if (!Offset) {
|
||||
return Offset.takeError();
|
||||
}
|
||||
|
||||
// Get node under the cursor.
|
||||
SelectionTree ST = SelectionTree::createRight(
|
||||
AST.getASTContext(), AST.getTokens(), *Offset, *Offset);
|
||||
for (const auto *Node = ST.commonAncestor(); Node != nullptr;
|
||||
Node = Node->Parent) {
|
||||
if (const Decl *D = Node->ASTNode.get<Decl>()) {
|
||||
if (llvm::isa<TranslationUnitDecl>(D)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
auto SR = toHalfOpenFileRange(SM, LangOpts, Node->ASTNode.getSourceRange());
|
||||
if (!SR || SM.getFileID(SR->getBegin()) != SM.getMainFileID()) {
|
||||
continue;
|
||||
}
|
||||
Range R;
|
||||
R.start = sourceLocToPosition(SM, SR->getBegin());
|
||||
R.end = sourceLocToPosition(SM, SR->getEnd());
|
||||
addIfDistinct(R, Ranges);
|
||||
}
|
||||
|
||||
if (Ranges.empty()) {
|
||||
// LSP provides no way to signal "the point is not within a semantic range".
|
||||
// Return an empty range at the point.
|
||||
SelectionRange Empty;
|
||||
Empty.range.start = Empty.range.end = Pos;
|
||||
return std::move(Empty);
|
||||
}
|
||||
|
||||
// Convert to the LSP linked-list representation.
|
||||
SelectionRange Head;
|
||||
Head.range = std::move(Ranges.front());
|
||||
SelectionRange *Tail = &Head;
|
||||
for (auto &Range :
|
||||
llvm::MutableArrayRef(Ranges.data(), Ranges.size()).drop_front()) {
|
||||
Tail->parent = std::make_unique<SelectionRange>();
|
||||
Tail = Tail->parent.get();
|
||||
Tail->range = std::move(Range);
|
||||
}
|
||||
|
||||
return std::move(Head);
|
||||
}
|
||||
|
||||
// FIXME(kirillbobyrev): Collect comments, PP conditional regions, includes and
|
||||
// other code regions (e.g. public/private/protected sections of classes,
|
||||
// control flow statement bodies).
|
||||
// Related issue: https://github.com/clangd/clangd/issues/310
|
||||
llvm::Expected<std::vector<FoldingRange>> getFoldingRanges(ParsedAST &AST) {
|
||||
syntax::Arena A;
|
||||
syntax::TokenBufferTokenManager TM(AST.getTokens(), AST.getLangOpts(),
|
||||
AST.getSourceManager());
|
||||
const auto *SyntaxTree = syntax::buildSyntaxTree(A, TM, AST.getASTContext());
|
||||
return collectFoldingRanges(SyntaxTree, TM);
|
||||
}
|
||||
|
||||
// FIXME( usaxena95): Collect PP conditional regions, includes and other code
|
||||
// regions (e.g. public/private/protected sections of classes, control flow
|
||||
// statement bodies).
|
||||
// Related issue: https://github.com/clangd/clangd/issues/310
|
||||
llvm::Expected<std::vector<FoldingRange>>
|
||||
getFoldingRanges(const std::string &Code, bool LineFoldingOnly) {
|
||||
auto OrigStream = lex(Code, genericLangOpts());
|
||||
|
||||
auto DirectiveStructure = DirectiveTree::parse(OrigStream);
|
||||
chooseConditionalBranches(DirectiveStructure, OrigStream);
|
||||
|
||||
// FIXME: Provide ranges in the disabled-PP regions as well.
|
||||
auto Preprocessed = DirectiveStructure.stripDirectives(OrigStream);
|
||||
|
||||
auto ParseableStream = cook(Preprocessed, genericLangOpts());
|
||||
pairBrackets(ParseableStream);
|
||||
|
||||
std::vector<FoldingRange> Result;
|
||||
auto AddFoldingRange = [&](Position Start, Position End,
|
||||
llvm::StringLiteral Kind) {
|
||||
if (Start.line >= End.line)
|
||||
return;
|
||||
FoldingRange FR;
|
||||
FR.startLine = Start.line;
|
||||
FR.startCharacter = Start.character;
|
||||
FR.endLine = End.line;
|
||||
FR.endCharacter = End.character;
|
||||
FR.kind = Kind.str();
|
||||
Result.push_back(FR);
|
||||
};
|
||||
auto OriginalToken = [&](const Token &T) {
|
||||
return OrigStream.tokens()[T.OriginalIndex];
|
||||
};
|
||||
auto StartOffset = [&](const Token &T) {
|
||||
return OriginalToken(T).text().data() - Code.data();
|
||||
};
|
||||
auto StartPosition = [&](const Token &T) {
|
||||
return offsetToPosition(Code, StartOffset(T));
|
||||
};
|
||||
auto EndOffset = [&](const Token &T) {
|
||||
return StartOffset(T) + OriginalToken(T).Length;
|
||||
};
|
||||
auto EndPosition = [&](const Token &T) {
|
||||
return offsetToPosition(Code, EndOffset(T));
|
||||
};
|
||||
auto Tokens = ParseableStream.tokens();
|
||||
// Brackets.
|
||||
for (const auto &Tok : Tokens) {
|
||||
if (auto *Paired = Tok.pair()) {
|
||||
// Process only token at the start of the range. Avoid ranges on a single
|
||||
// line.
|
||||
if (Tok.Line < Paired->Line) {
|
||||
Position Start = offsetToPosition(Code, 1 + StartOffset(Tok));
|
||||
Position End = StartPosition(*Paired);
|
||||
if (LineFoldingOnly)
|
||||
End.line--;
|
||||
AddFoldingRange(Start, End, FoldingRange::REGION_KIND);
|
||||
}
|
||||
}
|
||||
}
|
||||
auto IsBlockComment = [&](const Token &T) {
|
||||
assert(T.Kind == tok::comment);
|
||||
return OriginalToken(T).Length >= 2 &&
|
||||
Code.substr(StartOffset(T), 2) == "/*";
|
||||
};
|
||||
// Multi-line comments.
|
||||
for (auto *T = Tokens.begin(); T != Tokens.end();) {
|
||||
if (T->Kind != tok::comment) {
|
||||
T++;
|
||||
continue;
|
||||
}
|
||||
Token *FirstComment = T;
|
||||
// Show starting sentinals (// and /*) of the comment.
|
||||
Position Start = offsetToPosition(Code, 2 + StartOffset(*FirstComment));
|
||||
Token *LastComment = T;
|
||||
Position End = EndPosition(*T);
|
||||
while (T != Tokens.end() && T->Kind == tok::comment &&
|
||||
StartPosition(*T).line <= End.line + 1) {
|
||||
End = EndPosition(*T);
|
||||
LastComment = T;
|
||||
T++;
|
||||
}
|
||||
if (IsBlockComment(*FirstComment)) {
|
||||
if (LineFoldingOnly)
|
||||
// Show last line of a block comment.
|
||||
End.line--;
|
||||
if (IsBlockComment(*LastComment))
|
||||
// Show ending sentinal "*/" of the block comment.
|
||||
End.character -= 2;
|
||||
}
|
||||
AddFoldingRange(Start, End, FoldingRange::COMMENT_KIND);
|
||||
}
|
||||
return Result;
|
||||
}
|
||||
|
||||
} // namespace clangd
|
||||
} // namespace clang
|
||||
@@ -1,41 +0,0 @@
|
||||
//===--- SemanticSelection.h -------------------------------------*- C++-*-===//
|
||||
//
|
||||
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
|
||||
// See https://llvm.org/LICENSE.txt for license information.
|
||||
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// Features for giving interesting semantic ranges around the cursor.
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANGD_SEMANTICSELECTION_H
|
||||
#define LLVM_CLANG_TOOLS_EXTRA_CLANGD_SEMANTICSELECTION_H
|
||||
#include "ParsedAST.h"
|
||||
#include "Protocol.h"
|
||||
#include "llvm/Support/Error.h"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
namespace clang {
|
||||
namespace clangd {
|
||||
|
||||
/// Returns the list of all interesting ranges around the Position \p Pos.
|
||||
/// The interesting ranges corresponds to the AST nodes in the SelectionTree
|
||||
/// containing \p Pos.
|
||||
/// If pos is not in any interesting range, return [Pos, Pos).
|
||||
llvm::Expected<SelectionRange> getSemanticRanges(ParsedAST &AST, Position Pos);
|
||||
|
||||
/// Returns a list of ranges whose contents might be collapsible in an editor.
|
||||
/// This should include large scopes, preprocessor blocks etc.
|
||||
llvm::Expected<std::vector<FoldingRange>> getFoldingRanges(ParsedAST &AST);
|
||||
|
||||
/// Returns a list of ranges whose contents might be collapsible in an editor.
|
||||
/// This version uses the pseudoparser which does not require the AST.
|
||||
llvm::Expected<std::vector<FoldingRange>>
|
||||
getFoldingRanges(const std::string &Code, bool LineFoldingOnly);
|
||||
|
||||
} // namespace clangd
|
||||
} // namespace clang
|
||||
|
||||
#endif // LLVM_CLANG_TOOLS_EXTRA_CLANGD_SEMANTICSELECTION_H
|
||||
@@ -1,24 +0,0 @@
|
||||
# RUN: clangd -lit-test < %s | FileCheck -strict-whitespace %s
|
||||
void f() {
|
||||
|
||||
}
|
||||
---
|
||||
{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{"textDocument": {"foldingRange": {"lineFoldingOnly": true}}},"trace":"off"}}
|
||||
---
|
||||
{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"languageId":"cpp","text":"void f() {\n\n}\n","uri":"test:///foo.cpp","version":1}}}
|
||||
---
|
||||
{"id":1,"jsonrpc":"2.0","method":"textDocument/foldingRange","params":{"textDocument":{"uri":"test:///foo.cpp"}}}
|
||||
# CHECK: "id": 1,
|
||||
# CHECK-NEXT: "jsonrpc": "2.0",
|
||||
# CHECK-NEXT: "result": [
|
||||
# CHECK-NEXT: {
|
||||
# CHECK-NEXT: "endLine": 1,
|
||||
# CHECK-NEXT: "kind": "region",
|
||||
# CHECK-NEXT: "startCharacter": 10,
|
||||
# CHECK-NEXT: "startLine": 0
|
||||
# CHECK-NEXT: }
|
||||
# CHECK-NEXT: ]
|
||||
---
|
||||
{"jsonrpc":"2.0","id":5,"method":"shutdown"}
|
||||
---
|
||||
{"jsonrpc":"2.0","method":"exit"}
|
||||
@@ -1,459 +0,0 @@
|
||||
//===-- SemanticSelectionTests.cpp ----------------*- C++ -*--------------===//
|
||||
//
|
||||
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
|
||||
// See https://llvm.org/LICENSE.txt for license information.
|
||||
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
#include "Annotations.h"
|
||||
#include "ClangdServer.h"
|
||||
#include "Protocol.h"
|
||||
#include "SemanticSelection.h"
|
||||
#include "SyncAPI.h"
|
||||
#include "TestFS.h"
|
||||
#include "TestTU.h"
|
||||
#include "llvm/ADT/ArrayRef.h"
|
||||
#include "llvm/Support/Error.h"
|
||||
#include "gmock/gmock.h"
|
||||
#include "gtest/gtest.h"
|
||||
#include <vector>
|
||||
|
||||
namespace clang {
|
||||
namespace clangd {
|
||||
namespace {
|
||||
|
||||
using ::testing::ElementsAre;
|
||||
using ::testing::ElementsAreArray;
|
||||
using ::testing::UnorderedElementsAreArray;
|
||||
|
||||
// front() is SR.range, back() is outermost range.
|
||||
std::vector<Range> gatherRanges(const SelectionRange &SR) {
|
||||
std::vector<Range> Ranges;
|
||||
for (const SelectionRange *S = &SR; S; S = S->parent.get())
|
||||
Ranges.push_back(S->range);
|
||||
return Ranges;
|
||||
}
|
||||
|
||||
std::vector<Range>
|
||||
gatherFoldingRanges(llvm::ArrayRef<FoldingRange> FoldingRanges) {
|
||||
std::vector<Range> Ranges;
|
||||
Range NextRange;
|
||||
for (const auto &R : FoldingRanges) {
|
||||
NextRange.start.line = R.startLine;
|
||||
NextRange.start.character = R.startCharacter;
|
||||
NextRange.end.line = R.endLine;
|
||||
NextRange.end.character = R.endCharacter;
|
||||
Ranges.push_back(NextRange);
|
||||
}
|
||||
return Ranges;
|
||||
}
|
||||
|
||||
TEST(SemanticSelection, All) {
|
||||
const char *Tests[] = {
|
||||
R"cpp( // Single statement in a function body.
|
||||
[[void func() [[{
|
||||
[[[[int v = [[1^00]]]];]]
|
||||
}]]]]
|
||||
)cpp",
|
||||
R"cpp( // Expression
|
||||
[[void func() [[{
|
||||
int a = 1;
|
||||
// int v = (10 + 2) * (a + a);
|
||||
[[[[int v = [[[[([[[[10^]] + 2]])]] * (a + a)]]]];]]
|
||||
}]]]]
|
||||
)cpp",
|
||||
R"cpp( // Function call.
|
||||
int add(int x, int y) { return x + y; }
|
||||
[[void callee() [[{
|
||||
// int res = add(11, 22);
|
||||
[[[[int res = [[add([[1^1]], 22)]]]];]]
|
||||
}]]]]
|
||||
)cpp",
|
||||
R"cpp( // Tricky macros.
|
||||
#define MUL ) * (
|
||||
[[void func() [[{
|
||||
// int var = (4 + 15 MUL 6 + 10);
|
||||
[[[[int var = [[[[([[4 + [[1^5]]]] MUL]] 6 + 10)]]]];]]
|
||||
}]]]]
|
||||
)cpp",
|
||||
R"cpp( // Cursor inside a macro.
|
||||
#define HASH(x) ((x) % 10)
|
||||
[[void func() [[{
|
||||
[[[[int a = [[HASH([[[[2^3]] + 34]])]]]];]]
|
||||
}]]]]
|
||||
)cpp",
|
||||
R"cpp( // Cursor on a macro.
|
||||
#define HASH(x) ((x) % 10)
|
||||
[[void func() [[{
|
||||
[[[[int a = [[HA^SH(23)]]]];]]
|
||||
}]]]]
|
||||
)cpp",
|
||||
R"cpp( // Multiple declaration.
|
||||
[[void func() [[{
|
||||
[[[[int var1, var^2]], var3;]]
|
||||
}]]]]
|
||||
)cpp",
|
||||
R"cpp( // Before comment.
|
||||
[[void func() [[{
|
||||
int var1 = 1;
|
||||
[[[[int var2 = [[[[var1]]^ /*some comment*/ + 41]]]];]]
|
||||
}]]]]
|
||||
)cpp",
|
||||
// Empty file.
|
||||
"[[^]]",
|
||||
// FIXME: We should get the whole DeclStmt as a range.
|
||||
R"cpp( // Single statement in TU.
|
||||
[[int v = [[1^00]]]];
|
||||
)cpp",
|
||||
R"cpp( // Cursor at end of VarDecl.
|
||||
[[int v = [[100]]^]];
|
||||
)cpp",
|
||||
// FIXME: No node found associated to the position.
|
||||
R"cpp( // Cursor in between spaces.
|
||||
void func() {
|
||||
int v = 100 + [[^]] 100;
|
||||
}
|
||||
)cpp",
|
||||
// Structs.
|
||||
R"cpp(
|
||||
struct AAA { struct BBB { static int ccc(); };};
|
||||
[[void func() [[{
|
||||
// int x = AAA::BBB::ccc();
|
||||
[[[[int x = [[[[AAA::BBB::c^cc]]()]]]];]]
|
||||
}]]]]
|
||||
)cpp",
|
||||
R"cpp(
|
||||
struct AAA { struct BBB { static int ccc(); };};
|
||||
[[void func() [[{
|
||||
// int x = AAA::BBB::ccc();
|
||||
[[[[int x = [[[[[[[[[[AA^A]]::]]BBB::]]ccc]]()]]]];]]
|
||||
}]]]]
|
||||
)cpp",
|
||||
R"cpp( // Inside struct.
|
||||
struct A { static int a(); };
|
||||
[[struct B {
|
||||
[[static int b() [[{
|
||||
[[return [[[[1^1]] + 2]]]];
|
||||
}]]]]
|
||||
}]];
|
||||
)cpp",
|
||||
// Namespaces.
|
||||
R"cpp(
|
||||
[[namespace nsa {
|
||||
[[namespace nsb {
|
||||
static int ccc();
|
||||
[[void func() [[{
|
||||
// int x = nsa::nsb::ccc();
|
||||
[[[[int x = [[[[nsa::nsb::cc^c]]()]]]];]]
|
||||
}]]]]
|
||||
}]]
|
||||
}]]
|
||||
)cpp",
|
||||
|
||||
};
|
||||
|
||||
for (const char *Test : Tests) {
|
||||
auto T = Annotations(Test);
|
||||
auto AST = TestTU::withCode(T.code()).build();
|
||||
EXPECT_THAT(gatherRanges(llvm::cantFail(getSemanticRanges(AST, T.point()))),
|
||||
ElementsAreArray(T.ranges()))
|
||||
<< Test;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(SemanticSelection, RunViaClangdServer) {
|
||||
MockFS FS;
|
||||
MockCompilationDatabase CDB;
|
||||
ClangdServer Server(CDB, FS, ClangdServer::optsForTest());
|
||||
|
||||
auto FooH = testPath("foo.h");
|
||||
FS.Files[FooH] = R"cpp(
|
||||
int foo(int x);
|
||||
#define HASH(x) ((x) % 10)
|
||||
)cpp";
|
||||
|
||||
auto FooCpp = testPath("Foo.cpp");
|
||||
const char *SourceContents = R"cpp(
|
||||
#include "foo.h"
|
||||
[[void bar(int& inp) [[{
|
||||
// inp = HASH(foo(inp));
|
||||
[[inp = [[HASH([[foo([[in^p]])]])]]]];
|
||||
}]]]]
|
||||
$empty[[^]]
|
||||
)cpp";
|
||||
Annotations SourceAnnotations(SourceContents);
|
||||
FS.Files[FooCpp] = std::string(SourceAnnotations.code());
|
||||
Server.addDocument(FooCpp, SourceAnnotations.code());
|
||||
|
||||
auto Ranges = runSemanticRanges(Server, FooCpp, SourceAnnotations.points());
|
||||
ASSERT_TRUE(bool(Ranges))
|
||||
<< "getSemanticRange returned an error: " << Ranges.takeError();
|
||||
ASSERT_EQ(Ranges->size(), SourceAnnotations.points().size());
|
||||
EXPECT_THAT(gatherRanges(Ranges->front()),
|
||||
ElementsAreArray(SourceAnnotations.ranges()));
|
||||
EXPECT_THAT(gatherRanges(Ranges->back()),
|
||||
ElementsAre(SourceAnnotations.range("empty")));
|
||||
}
|
||||
|
||||
TEST(FoldingRanges, ASTAll) {
|
||||
const char *Tests[] = {
|
||||
R"cpp(
|
||||
#define FOO int foo() {\
|
||||
int Variable = 42; \
|
||||
return 0; \
|
||||
}
|
||||
|
||||
// Do not generate folding range for braces within macro expansion.
|
||||
FOO
|
||||
|
||||
// Do not generate folding range within macro arguments.
|
||||
#define FUNCTOR(functor) functor
|
||||
void func() {[[
|
||||
FUNCTOR([](){});
|
||||
]]}
|
||||
|
||||
// Do not generate folding range with a brace coming from macro.
|
||||
#define LBRACE {
|
||||
void bar() LBRACE
|
||||
int X = 42;
|
||||
}
|
||||
)cpp",
|
||||
R"cpp(
|
||||
void func() {[[
|
||||
int Variable = 100;
|
||||
|
||||
if (Variable > 5) {[[
|
||||
Variable += 42;
|
||||
]]} else if (Variable++)
|
||||
++Variable;
|
||||
else {[[
|
||||
Variable--;
|
||||
]]}
|
||||
|
||||
// Do not generate FoldingRange for empty CompoundStmts.
|
||||
for (;;) {}
|
||||
|
||||
// If there are newlines between {}, we should generate one.
|
||||
for (;;) {[[
|
||||
|
||||
]]}
|
||||
]]}
|
||||
)cpp",
|
||||
R"cpp(
|
||||
class Foo {
|
||||
public:
|
||||
Foo() {[[
|
||||
int X = 1;
|
||||
]]}
|
||||
|
||||
private:
|
||||
int getBar() {[[
|
||||
return 42;
|
||||
]]}
|
||||
|
||||
// Braces are located at the same line: no folding range here.
|
||||
void getFooBar() { }
|
||||
};
|
||||
)cpp",
|
||||
};
|
||||
for (const char *Test : Tests) {
|
||||
auto T = Annotations(Test);
|
||||
auto AST = TestTU::withCode(T.code()).build();
|
||||
EXPECT_THAT(gatherFoldingRanges(llvm::cantFail(getFoldingRanges(AST))),
|
||||
UnorderedElementsAreArray(T.ranges()))
|
||||
<< Test;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(FoldingRanges, PseudoParserWithoutLineFoldings) {
|
||||
const char *Tests[] = {
|
||||
R"cpp(
|
||||
#define FOO int foo() {\
|
||||
int Variable = 42; \
|
||||
}
|
||||
|
||||
// Do not generate folding range for braces within macro expansion.
|
||||
FOO
|
||||
|
||||
// Do not generate folding range within macro arguments.
|
||||
#define FUNCTOR(functor) functor
|
||||
void func() {[[
|
||||
FUNCTOR([](){});
|
||||
]]}
|
||||
|
||||
// Do not generate folding range with a brace coming from macro.
|
||||
#define LBRACE {
|
||||
void bar() LBRACE
|
||||
int X = 42;
|
||||
}
|
||||
)cpp",
|
||||
R"cpp(
|
||||
void func() {[[
|
||||
int Variable = 100;
|
||||
|
||||
if (Variable > 5) {[[
|
||||
Variable += 42;
|
||||
]]} else if (Variable++)
|
||||
++Variable;
|
||||
else {[[
|
||||
Variable--;
|
||||
]]}
|
||||
|
||||
// Do not generate FoldingRange for empty CompoundStmts.
|
||||
for (;;) {}
|
||||
|
||||
// If there are newlines between {}, we should generate one.
|
||||
for (;;) {[[
|
||||
|
||||
]]}
|
||||
]]}
|
||||
)cpp",
|
||||
R"cpp(
|
||||
class Foo {[[
|
||||
public:
|
||||
Foo() {[[
|
||||
int X = 1;
|
||||
]]}
|
||||
|
||||
private:
|
||||
int getBar() {[[
|
||||
return 42;
|
||||
]]}
|
||||
|
||||
// Braces are located at the same line: no folding range here.
|
||||
void getFooBar() { }
|
||||
]]};
|
||||
)cpp",
|
||||
R"cpp(
|
||||
// Range boundaries on escaped newlines.
|
||||
class Foo \
|
||||
\
|
||||
{[[ \
|
||||
public:
|
||||
Foo() {[[\
|
||||
int X = 1;
|
||||
]]} \
|
||||
]]};
|
||||
)cpp",
|
||||
R"cpp(
|
||||
/*[[ Multi
|
||||
* line
|
||||
* comment
|
||||
]]*/
|
||||
)cpp",
|
||||
R"cpp(
|
||||
//[[ Comment
|
||||
// 1]]
|
||||
|
||||
//[[ Comment
|
||||
// 2]]
|
||||
|
||||
// No folding for single line comment.
|
||||
|
||||
/*[[ comment 3
|
||||
]]*/
|
||||
|
||||
/*[[ comment 4
|
||||
]]*/
|
||||
|
||||
/*[[ foo */
|
||||
/* bar ]]*/
|
||||
|
||||
/*[[ foo */
|
||||
// baz
|
||||
/* bar ]]*/
|
||||
|
||||
/*[[ foo */
|
||||
/* bar*/
|
||||
// baz]]
|
||||
|
||||
//[[ foo
|
||||
/* bar */]]
|
||||
)cpp",
|
||||
};
|
||||
for (const char *Test : Tests) {
|
||||
auto T = Annotations(Test);
|
||||
EXPECT_THAT(gatherFoldingRanges(llvm::cantFail(getFoldingRanges(
|
||||
T.code().str(), /*LineFoldingsOnly=*/false))),
|
||||
UnorderedElementsAreArray(T.ranges()))
|
||||
<< Test;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(FoldingRanges, PseudoParserLineFoldingsOnly) {
|
||||
const char *Tests[] = {
|
||||
R"cpp(
|
||||
void func(int a) {[[
|
||||
a++;]]
|
||||
}
|
||||
)cpp",
|
||||
R"cpp(
|
||||
// Always exclude last line for brackets.
|
||||
void func(int a) {[[
|
||||
if(a == 1) {[[
|
||||
a++;]]
|
||||
} else if (a == 2){[[
|
||||
a--;]]
|
||||
} else { // No folding for 2 line bracketed ranges.
|
||||
}]]
|
||||
}
|
||||
)cpp",
|
||||
R"cpp(
|
||||
/*[[ comment
|
||||
* comment]]
|
||||
*/
|
||||
|
||||
/* No folding for this comment.
|
||||
*/
|
||||
|
||||
// No folding for this comment.
|
||||
|
||||
//[[ 2 single line comment.
|
||||
// 2 single line comment.]]
|
||||
|
||||
//[[ >=2 line comments.
|
||||
// >=2 line comments.
|
||||
// >=2 line comments.]]
|
||||
|
||||
//[[ foo\
|
||||
bar\
|
||||
baz]]
|
||||
|
||||
/*[[ foo */
|
||||
/* bar */]]
|
||||
/* baz */
|
||||
|
||||
/*[[ foo */
|
||||
/* bar]]
|
||||
* This does not fold me */
|
||||
|
||||
//[[ foo
|
||||
/* bar */]]
|
||||
)cpp",
|
||||
// FIXME: Support folding template arguments.
|
||||
// R"cpp(
|
||||
// template <[[typename foo, class bar]]> struct baz {};
|
||||
// )cpp",
|
||||
|
||||
};
|
||||
auto StripColumns = [](const std::vector<Range> &Ranges) {
|
||||
std::vector<Range> Res;
|
||||
for (Range R : Ranges) {
|
||||
R.start.character = R.end.character = 0;
|
||||
Res.push_back(R);
|
||||
}
|
||||
return Res;
|
||||
};
|
||||
for (const char *Test : Tests) {
|
||||
auto T = Annotations(Test);
|
||||
EXPECT_THAT(
|
||||
StripColumns(gatherFoldingRanges(llvm::cantFail(
|
||||
getFoldingRanges(T.code().str(), /*LineFoldingsOnly=*/true)))),
|
||||
UnorderedElementsAreArray(StripColumns(T.ranges())))
|
||||
<< Test;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
} // namespace clangd
|
||||
} // namespace clang
|
||||
@@ -1,70 +0,0 @@
|
||||
## 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: Client line-only support is propagated through folding options
|
||||
- **WHEN** the client declares `textDocument.foldingRange.lineFoldingOnly = true`
|
||||
- **THEN** the server MUST invoke folding rendering with options equivalent to `line_folding_only = true`
|
||||
- **AND** collectors MUST NOT need to inspect client capability state to produce different raw ranges
|
||||
|
||||
#### 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: Preferred range limits are applied deterministically
|
||||
- **WHEN** the client declares `textDocument.foldingRange.rangeLimit = N` and the server can produce more than `N` folding ranges for a document
|
||||
- **THEN** the server MUST return no more than `N` ranges and MUST choose them using a deterministic ordering rule
|
||||
|
||||
#### 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`
|
||||
@@ -1,39 +0,0 @@
|
||||
## 1. Reference Snapshot
|
||||
|
||||
- [x] 1.1 Download the clangd folding-range reference files for `llvmorg-21.1.8` into `openspec/changes/explore-improve-folding-range-support/reference/clangd/llvmorg-21.1.8/` using `curl` against GitHub raw URLs.
|
||||
- [x] 1.2 Include `SemanticSelection.{cpp,h}`, `ClangdServer.{cpp,h}`, `ClangdLSPServer.cpp`, `Protocol.{h,cpp}`, `test/folding-range.test`, and `unittests/SemanticSelectionTests.cpp`.
|
||||
- [x] 1.3 Record the exact raw GitHub URLs and downloaded file layout in a change-local comparison note.
|
||||
|
||||
## 2. Comparison and Pipeline
|
||||
|
||||
- [x] 2.1 Record a side-by-side comparison in the change artifacts between clangd's folding path and clice's current path, calling out confirmed parity gaps, clice-only capabilities, and known bugs.
|
||||
- [ ] 2.2 Keep the existing `RawFoldingRange` model as the settled collection contract while completing normalization and options-driven rendering.
|
||||
- [ ] 2.3 Replace direct exposure of clice-specific public folding kinds with a stable mapping to standard LSP `comment` / `imports` / `region` kinds.
|
||||
|
||||
## 3. Comment and Structural Baseline
|
||||
|
||||
- [ ] 3.1 Add a comment collector that folds multi-line block comments and contiguous multi-line `//` comment groups in the main file.
|
||||
- [ ] 3.2 Preserve existing AST structural folding behavior while routing it through the new normalization/rendering pipeline.
|
||||
- [ ] 3.3 Add focused unit tests for comment folding, single-line comment exclusion, and structural folding regressions.
|
||||
|
||||
## 4. Preprocessor Folding
|
||||
|
||||
- [ ] 4.1 Rework conditional-directive collection so each `#if/#elif/#else/#endif` branch body closes correctly, including the final branch ending at `#endif`.
|
||||
- [ ] 4.2 Add folding support for inactive conditional branches using the existing preprocessor condition metadata.
|
||||
- [ ] 4.3 Strengthen `#pragma region` handling and convert the current placeholder directive tests into assertion-backed coverage.
|
||||
|
||||
## 5. Protocol and Rendering
|
||||
|
||||
- [ ] 5.1 Capture client folding capabilities during initialize and translate them into `FoldingRangeOptions`/`Opts` when serving `textDocument/foldingRange`.
|
||||
- [ ] 5.2 Honor `lineFoldingOnly` through `opts.line_folding_only`, gate `collapsedText`, and apply deterministic `rangeLimit` trimming during folding-range rendering.
|
||||
- [ ] 5.3 Add integration coverage for line-only rendering, standard kind output, optional collapsed text, and range limiting.
|
||||
|
||||
## 6. Clice-Specific Folding Extensions
|
||||
|
||||
- [ ] 6.1 Add folding ranges for multi-line macro definitions using `directive.macros` and stable main-file source ranges.
|
||||
- [ ] 6.2 Add grouping folds for contiguous `#include` blocks and return them as `imports` ranges.
|
||||
- [ ] 6.3 Add grouping folds for contiguous C++ module `import` declarations and cover mixed include/import layouts with tests.
|
||||
|
||||
## 7. Verification
|
||||
|
||||
- [ ] 7.1 Run the relevant folding-range unit and integration tests, then fix any ordering, deduplication, or boundary regressions found during verification.
|
||||
@@ -1,2 +0,0 @@
|
||||
schema: spec-driven
|
||||
created: 2026-04-22
|
||||
@@ -1,192 +0,0 @@
|
||||
## Context
|
||||
|
||||
This change extracts decision `2` from `openspec/changes/explore-improve-folding-range-support/design.md` into a standalone proposal. The current folding implementation in `src/feature/folding_ranges.cpp` already has an internal `RawFoldingRange` handoff, but it still leaves two important concerns too implicit:
|
||||
|
||||
- deciding which ranges survive deduplication and validation
|
||||
- shaping the final LSP response, including client-specific output rules such as `line_folding_only`
|
||||
|
||||
Follow-up discussion clarified that the existing `RawFoldingRange` shape is finished for this extracted change. The remaining architectural gap is an explicit options path, passed as `Opts`/`FoldingRangeOptions`, so rendering can be configured without reworking collectors. This proposal therefore keeps scope narrow: it does not add new fold categories or redesign raw ranges, but it creates the normalization and options-driven rendering boundaries that later changes can build on without destabilizing existing structural folding.
|
||||
|
||||
The downloaded clangd reference confirms both the value and the limit of the upstream design. clangd has useful, tested folding behavior for brace bodies, comment blocks, contiguous `//` groups, and `lineFoldingOnly`, but its implementation largely emits protocol-shaped `FoldingRange` objects directly from collection code. In `SemanticSelection.cpp`, both the AST path and the pseudo-parser path build `FoldingRange` results directly, and the pseudo-parser applies rendering details such as delimiter trimming and `lineFoldingOnly` adjustments while collecting ranges. That is a good behavior reference, but it is not the architecture this extracted change should copy.
|
||||
|
||||
`clice` already has stronger ingredients for a real pipeline:
|
||||
|
||||
- the existing `RawFoldingRange` gives collectors a feature-local representation that is not a final LSP response
|
||||
- `LocalSourceRange` gives us a main-file, half-open offset representation that is independent of LSP position encoding
|
||||
- directive metadata already captures information clangd does not expose well, including conditional-branch state, pragma regions, includes, imports, and macro references
|
||||
- the current tests are boundary-oriented, which makes them a good fit for validating raw spans before protocol rendering
|
||||
|
||||
The design therefore separates "what fold exists in the source" from "how that fold should be emitted to this client". clangd's tested boundary rules are still relevant, but they should become renderer policy selected by options and normalization rules rather than collector output format.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- Keep the existing `RawFoldingRange` collection contract stable while completing normalization and rendering boundaries.
|
||||
- Preserve the existing AST structural folding categories already supported by `clice`.
|
||||
- Make ordering, deduplication, and boundary validation deterministic and testable.
|
||||
- Add an explicit folding options object so `line_folding_only` can be configured by callers and consumed only by rendering.
|
||||
- Give later changes a stable extension point for comments, directives, and client-driven rendering options.
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- Add comment folding in this change.
|
||||
- Fix preprocessor branch-closing behavior in this change.
|
||||
- Add new fold categories such as macro definitions or include/import grouping.
|
||||
- Redesign or replace the existing `RawFoldingRange` model.
|
||||
- Depend on initialize-time client capability plumbing being implemented first.
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. Use clangd as a behavior reference, not an architecture template
|
||||
|
||||
This change should borrow clangd's confirmed folding behavior where it is useful, especially around multiline comments, contiguous `//` comment groups, main-file-only filtering, and `lineFoldingOnly` boundary shaping. It should not copy clangd's habit of emitting protocol-shaped `FoldingRange` objects directly from collection logic.
|
||||
|
||||
Why:
|
||||
|
||||
- clangd's tests are valuable because they pin down tricky folding behavior around comments, macro boundaries, and line-only rendering
|
||||
- clangd's data flow is intentionally narrow and mixes collection with response shaping
|
||||
- `clice` already has richer file-local and directive metadata that supports a cleaner internal representation
|
||||
|
||||
Alternative considered:
|
||||
|
||||
- Treat clangd's direct `FoldingRange` construction as the architecture to reproduce. Rejected because it would preserve the same coupling this extracted change is meant to remove.
|
||||
|
||||
### 2. Treat the existing raw internal folding-range model as finished
|
||||
|
||||
Collectors should continue to emit the existing internal `RawFoldingRange` structure instead of final LSP protocol objects. The raw model is sufficient for this extracted change and should not be redesigned as part of adding `line_folding_only` support.
|
||||
|
||||
The raw model should remain shaped around file-local source structure, not client capability state. In the current implementation it carries:
|
||||
|
||||
- a main-file `LocalSourceRange` span using half-open byte offsets
|
||||
- an optional public folding kind to preserve existing behavior
|
||||
- an optional collapsed-text hint
|
||||
|
||||
```cpp
|
||||
struct RawFoldingRange {
|
||||
LocalSourceRange range;
|
||||
std::optional<protocol::FoldingRangeKind> kind;
|
||||
std::string collapsed_text;
|
||||
};
|
||||
```
|
||||
|
||||
The important design choice is that `range` represents the foldable source envelope in the main file while client-specific rendering state stays out of the raw model. For example:
|
||||
|
||||
- brace-based structural folds keep their source span and let the renderer decide line-only boundary shaping
|
||||
- future block comments can keep the full `/* ... */` span and let the renderer decide whether to hide the closing delimiter or final line
|
||||
- future contiguous `//` groups can keep the grouped span and let the renderer decide line-only output
|
||||
|
||||
Why:
|
||||
|
||||
- collectors should describe what was found, not how it will be serialized
|
||||
- `LocalSourceRange` is already the natural coordinate system for `clice`
|
||||
- future comment and directive collectors can share the same pipeline contract
|
||||
- tests can validate collection independently from rendering
|
||||
- the missing `line_folding_only` behavior belongs in options and rendering, not in the raw range shape
|
||||
|
||||
Alternatives considered:
|
||||
|
||||
- Continue emitting LSP ranges directly from collectors. Rejected because it keeps protocol concerns entangled with source discovery.
|
||||
- Expand `RawFoldingRange` now with render-hint fields for line-only behavior. Rejected because follow-up discussion established the raw model as finished for this slice, and line-only support can be configured through rendering options instead.
|
||||
|
||||
### 3. Normalize ranges before rendering
|
||||
|
||||
All collected ranges should pass through a normalization step before any response is emitted. Normalization is responsible for deterministic ordering, duplicate removal, and rejection of degenerate or unmappable ranges.
|
||||
|
||||
Normalization should operate on raw spans and raw metadata, not on already-rendered LSP line/character fields. Its responsibilities include:
|
||||
|
||||
- deterministic ordering independent of collector traversal order
|
||||
- duplicate collapse for collectors that discover the same fold
|
||||
- invalid-range filtering after raw spans are mapped and validated
|
||||
- stable tie-breaking for overlapping ranges from different origins
|
||||
|
||||
Collectors may still reject obviously invalid inputs, such as non-main-file locations that cannot be mapped to `LocalSourceRange`, but normalization remains the phase that decides which collected folds survive to rendering.
|
||||
|
||||
Why:
|
||||
|
||||
- duplicate or invalid ranges are easier to reason about in one place than across many collectors
|
||||
- stable ordering reduces regression noise and makes range limiting predictable later
|
||||
- metadata-aware normalization preserves fold meaning until the renderer maps it to public output
|
||||
- normalization lets new collectors plug in without each collector re-implementing cleanup logic
|
||||
|
||||
Alternative considered:
|
||||
|
||||
- Let each collector manage its own sorting and duplicate suppression. Rejected because cross-collector interactions would still remain undefined.
|
||||
|
||||
### 4. Keep the current AST visitor as the first collector boundary
|
||||
|
||||
The initial extraction should preserve the current AST visitor as one collector feeding the raw model. This reduces refactor risk while still creating the new phase boundaries.
|
||||
|
||||
Why:
|
||||
|
||||
- the existing structural fold coverage is valuable and should not be rewritten unnecessarily
|
||||
- an adapter-style refactor is easier to verify against current tests than a full collector redesign
|
||||
|
||||
Alternative considered:
|
||||
|
||||
- Rewrite collection around a brand-new multi-source manager immediately. Rejected because it adds scope before the phase split is proven.
|
||||
|
||||
### 5. Move output shaping into an options-driven renderer
|
||||
|
||||
The renderer should translate normalized ranges into LSP folding ranges. Boundary shaping, output kinds, and optional metadata emission should live there, even if some options still use default values until later protocol plumbing exists.
|
||||
|
||||
Renderer input should be the normalized raw model plus a separate `FoldingRangeOptions` structure. The public feature API should follow the existing feature-options style, for example:
|
||||
|
||||
```cpp
|
||||
struct FoldingRangeOptions {
|
||||
bool line_folding_only = false;
|
||||
};
|
||||
|
||||
auto folding_ranges(CompilationUnitRef unit,
|
||||
const FoldingRangeOptions& opts = {},
|
||||
PositionEncoding encoding = PositionEncoding::UTF16)
|
||||
-> std::vector<protocol::FoldingRange>;
|
||||
```
|
||||
|
||||
`line_folding_only` defaults to `false`, preserving the current behavior for existing callers. When server capability plumbing is added later, the server should translate `textDocument.foldingRange.lineFoldingOnly` into this option instead of exposing session state to collectors.
|
||||
|
||||
The renderer then becomes responsible for:
|
||||
|
||||
- converting `LocalSourceRange` into protocol positions for the requested encoding
|
||||
- applying line-only adjustments when `opts.line_folding_only = true`
|
||||
- mapping raw kind metadata to emitted LSP kinds
|
||||
- deciding whether collapsed text is emitted or suppressed
|
||||
- later applying deterministic `rangeLimit` trimming without changing collectors
|
||||
|
||||
This is the key point where `clice` should intentionally diverge from clangd. clangd threads `lineFoldingOnly` into collection and directly produces protocol objects. `clice` should keep those capability and transport decisions isolated in rendering so collectors remain stable as client support evolves.
|
||||
|
||||
Why:
|
||||
|
||||
- rendering rules are a separate concern from source discovery
|
||||
- later work on line-only output, metadata gating, or public kind mapping should not force collector rewrites
|
||||
- clangd-style line-only shaping is still supported, but as renderer policy rather than collector output
|
||||
- isolating rendering makes behavioral diffs easier to review
|
||||
- a small options object makes the missing `line_folding_only` support explicit without expanding `RawFoldingRange`
|
||||
|
||||
Alternative considered:
|
||||
|
||||
- Keep final boundary shaping next to the AST collector and only add a small helper for sorting. Rejected because it only moves a symptom, not the architectural problem.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [Refactoring the current path can accidentally change fold ordering] -> Mitigation: add deterministic-order assertions and compare outputs for existing structural fixtures.
|
||||
- [The raw model could become too abstract too early] -> Mitigation: do not redesign `RawFoldingRange` in this change; keep the existing fields unless implementation proves a concrete need.
|
||||
- [Line-only behavior can be accidentally encoded in collectors] -> Mitigation: expose `line_folding_only` only through `FoldingRangeOptions` and assert renderer-level behavior in tests.
|
||||
- [A renderer abstraction may appear premature before full capability plumbing exists] -> Mitigation: keep default render options aligned with current behavior and treat future options as extension points, not immediate scope.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. Keep the existing `RawFoldingRange` collection path stable behind the current entrypoint.
|
||||
2. Introduce `FoldingRangeOptions` with `line_folding_only = false` by default.
|
||||
3. Insert normalization between collection and response emission.
|
||||
4. Move LSP object construction into a dedicated renderer that consumes normalized ranges plus options.
|
||||
5. Add line-only renderer tests and verify that existing structural folding fixtures still produce the expected default ranges.
|
||||
|
||||
Rollback strategy:
|
||||
|
||||
- If the refactor destabilizes output, keep the new helper types but temporarily route the old direct-emission path until normalization and rendering regressions are resolved.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Whether public kind remapping should land in this extracted change or remain a follow-up proposal once the renderer boundary exists.
|
||||
- Whether `FoldingRangeOptions` should initially contain only `line_folding_only`, or also reserve fields for later collapsed-text and `rangeLimit` behavior.
|
||||
@@ -1,29 +0,0 @@
|
||||
## Why
|
||||
|
||||
`explore-improve-folding-range-support` combines several different concerns: upstream comparison work, baseline folding fixes, preprocessor extensions, and folding renderer behavior. The second design point in that change, splitting the folding-range pipeline into collection, normalization, and rendering, is the architectural slice that other work depends on and should be referenceable as its own proposal.
|
||||
|
||||
Follow-up discussion clarified that the existing internal `RawFoldingRange` shape is finished for this slice. The missing architectural part is not another raw-range redesign; it is an explicit folding options path so callers can request client-specific rendering behavior, starting with `line_folding_only`.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Extract the pipeline-splitting work from `explore-improve-folding-range-support` into a standalone change focused on folding-range architecture.
|
||||
- Treat the existing `RawFoldingRange` model as the settled internal collection contract for this change.
|
||||
- Define a normalization phase that performs deterministic sorting, duplicate removal, and boundary validation before response generation.
|
||||
- Define a folding options object, passed as `Opts`/`FoldingRangeOptions`, that configures rendering without changing collectors.
|
||||
- Define a rendering phase that owns line/column shaping, including `line_folding_only`, and optional metadata emission instead of mixing those concerns into collectors.
|
||||
- Preserve the current AST structural folding coverage while establishing extension points for future comment, directive, and capability-aware rendering work.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `folding-range-pipeline`: Provide a deterministic folding-range pipeline that separates collection, normalization, and rendering while preserving existing structural folds.
|
||||
|
||||
### Modified Capabilities
|
||||
- None.
|
||||
|
||||
## Impact
|
||||
|
||||
- `src/feature/folding_ranges.cpp` will keep raw-range collection but gain explicit normalization/rendering boundaries and options-driven rendering.
|
||||
- `src/feature/feature.h` will need a folding options type or equivalent public API extension so `line_folding_only` can be configured without changing collection.
|
||||
- `tests/unit/feature/folding_range_tests.cpp` will need regression coverage for structural folds, deterministic ordering, and `line_folding_only` boundary shaping.
|
||||
- `openspec/changes/explore-improve-folding-range-support/design.md` remains the source change from which this standalone proposal was extracted.
|
||||
@@ -1,50 +0,0 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Folding ranges are normalized before response emission
|
||||
The server SHALL convert collected folding candidates into a deterministic normalized set before emitting the folding range response.
|
||||
|
||||
#### Scenario: Duplicate candidates collapse to one emitted fold
|
||||
- **WHEN** multiple collectors produce the same folding candidate for the same source span and raw metadata
|
||||
- **THEN** the server MUST emit at most one folding range for that candidate
|
||||
|
||||
#### Scenario: Invalid candidates are dropped during normalization
|
||||
- **WHEN** a collected folding candidate does not span multiple lines or cannot be mapped back to the main file
|
||||
- **THEN** the server MUST omit that candidate from the emitted folding ranges
|
||||
|
||||
#### Scenario: Output ordering is deterministic
|
||||
- **WHEN** the same document is analyzed repeatedly without source changes
|
||||
- **THEN** the server MUST emit folding ranges in a deterministic order that does not depend on collector traversal order
|
||||
|
||||
### Requirement: Existing structural folding survives the pipeline split
|
||||
The server SHALL preserve the currently supported AST structural folding categories after collection, normalization, and rendering are separated.
|
||||
|
||||
#### Scenario: Supported structural regions remain foldable
|
||||
- **WHEN** a document contains a supported multi-line namespace, record, function body, parameter list, lambda body, initializer list, call argument list, or compound statement
|
||||
- **THEN** the server MUST still return a folding range for that region when its boundaries can be mapped to the main file
|
||||
|
||||
#### Scenario: Structural coverage is preserved through normalization
|
||||
- **WHEN** the document contains only currently supported AST-driven folding categories
|
||||
- **THEN** normalization and rendering MUST NOT remove a valid structural fold except when it is an exact duplicate or an invalid range
|
||||
|
||||
### Requirement: Rendering decisions are applied after normalization
|
||||
The server SHALL derive final LSP folding-range output from normalized internal ranges instead of requiring collectors to emit protocol-shaped results directly.
|
||||
|
||||
#### Scenario: Rendering options do not require collector changes
|
||||
- **WHEN** rendering rules change how line or metadata output is shaped for a normalized fold
|
||||
- **THEN** the server MUST apply that change in the rendering phase without requiring collector-specific logic changes
|
||||
|
||||
#### Scenario: Metadata hints remain optional until rendering
|
||||
- **WHEN** a collected or normalized fold carries optional kind or collapsed-text hints
|
||||
- **THEN** the renderer MUST decide whether to surface, transform, or suppress that metadata in the emitted LSP range
|
||||
|
||||
### Requirement: Folding rendering is configured through explicit options
|
||||
The server SHALL expose folding-specific rendering options so client capability behavior can be selected without changing collectors or raw ranges.
|
||||
|
||||
#### Scenario: Default options preserve existing output
|
||||
- **WHEN** folding ranges are requested without explicit folding options
|
||||
- **THEN** rendering MUST behave as if `line_folding_only = false`
|
||||
|
||||
#### Scenario: Line-only rendering is selected by options
|
||||
- **WHEN** folding ranges are rendered with `line_folding_only = true`
|
||||
- **THEN** the renderer MUST emit ranges that remain valid when interpreted as whole-line folds
|
||||
- **AND** collectors MUST NOT need to inspect client capability state or emit different raw ranges for line-only clients
|
||||
@@ -1,19 +0,0 @@
|
||||
## 1. Existing Raw Model and Collector Boundary
|
||||
|
||||
- [x] 1.1 Treat the existing `RawFoldingRange` as the finished internal collection model for this change.
|
||||
- [ ] 1.2 Keep the existing AST structural folding path routed through raw ranges instead of reintroducing direct collector-to-LSP emission.
|
||||
- [ ] 1.3 Add regression fixtures or assertions that cover the currently supported structural fold categories before further rendering changes.
|
||||
|
||||
## 2. Normalization, Opts, and Rendering
|
||||
|
||||
- [ ] 2.1 Implement normalization for deterministic sorting, duplicate removal, and invalid-range filtering.
|
||||
- [ ] 2.2 Introduce `FoldingRangeOptions`/`Opts` with `line_folding_only = false` as the default.
|
||||
- [ ] 2.3 Introduce a dedicated renderer that converts normalized ranges plus `Opts` into final LSP folding-range objects.
|
||||
- [ ] 2.4 Honor `line_folding_only` in rendering by shaping emitted boundaries for clients that only support whole-line folds.
|
||||
- [ ] 2.5 Keep default rendered output compatible with current structural behavior while exposing extension points for future collectors and render rules.
|
||||
|
||||
## 3. Verification
|
||||
|
||||
- [ ] 3.1 Compare pre-refactor and post-refactor outputs for the existing structural folding test cases.
|
||||
- [ ] 3.2 Add focused tests for `line_folding_only` output using the new folding options path.
|
||||
- [ ] 3.3 Run relevant folding-range unit tests and fix any ordering, deduplication, or boundary regressions introduced by the new pipeline.
|
||||
23
pixi.lock
generated
23
pixi.lock
generated
@@ -1078,6 +1078,7 @@ environments:
|
||||
- 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/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl
|
||||
linux-aarch64:
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/_openmp_mutex-4.5-20_gnu.conda
|
||||
@@ -1152,6 +1153,7 @@ environments:
|
||||
- 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/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl
|
||||
osx-64:
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-h500dc9f_9.conda
|
||||
@@ -1224,6 +1226,7 @@ environments:
|
||||
- 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/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.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:
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_8.conda
|
||||
@@ -1289,6 +1292,7 @@ environments:
|
||||
- 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/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.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:
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_8.conda
|
||||
@@ -1343,6 +1347,7 @@ environments:
|
||||
- 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/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl
|
||||
format:
|
||||
channels:
|
||||
@@ -1704,6 +1709,7 @@ environments:
|
||||
- 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/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl
|
||||
linux-aarch64:
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/_openmp_mutex-4.5-20_gnu.conda
|
||||
@@ -1782,6 +1788,7 @@ environments:
|
||||
- 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/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl
|
||||
osx-64:
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-h500dc9f_9.conda
|
||||
@@ -1858,6 +1865,7 @@ environments:
|
||||
- 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/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.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:
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_8.conda
|
||||
@@ -1926,6 +1934,7 @@ environments:
|
||||
- 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/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.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:
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_8.conda
|
||||
@@ -1982,6 +1991,7 @@ environments:
|
||||
- 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/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl
|
||||
test-run:
|
||||
channels:
|
||||
@@ -2025,6 +2035,7 @@ environments:
|
||||
- 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/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl
|
||||
linux-aarch64:
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/_openmp_mutex-4.5-20_gnu.conda
|
||||
@@ -2058,6 +2069,7 @@ environments:
|
||||
- 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/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl
|
||||
osx-64:
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-h500dc9f_9.conda
|
||||
@@ -2113,6 +2125,7 @@ environments:
|
||||
- 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/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.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:
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_9.conda
|
||||
@@ -2168,6 +2181,7 @@ environments:
|
||||
- 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/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.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:
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_9.conda
|
||||
@@ -2199,6 +2213,7 @@ environments:
|
||||
- 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/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl
|
||||
win-arm64:
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-arm64/bzip2-1.0.8-h50b96f5_9.conda
|
||||
@@ -2229,6 +2244,7 @@ environments:
|
||||
- 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/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl
|
||||
packages:
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2
|
||||
@@ -7795,6 +7811,13 @@ packages:
|
||||
- coverage>=6.2 ; extra == 'testing'
|
||||
- hypothesis>=5.7.1 ; extra == 'testing'
|
||||
requires_python: '>=3.10'
|
||||
- pypi: https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl
|
||||
name: pytest-timeout
|
||||
version: 2.4.0
|
||||
sha256: c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2
|
||||
requires_dist:
|
||||
- pytest>=7.0.0
|
||||
requires_python: '>=3.7'
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.14.2-h32b2ec7_100_cp314.conda
|
||||
build_number: 100
|
||||
sha256: a120fb2da4e4d51dd32918c149b04a08815fd2bd52099dad1334647984bb07f1
|
||||
|
||||
@@ -102,6 +102,7 @@ lld = "==20.1.8"
|
||||
[feature.test.pypi-dependencies]
|
||||
pytest = "*"
|
||||
pytest-asyncio = ">=1.1.0"
|
||||
pytest-timeout = "*"
|
||||
pygls = ">=2.0.0"
|
||||
lsprotocol = ">=2024.0.0"
|
||||
|
||||
@@ -160,13 +161,13 @@ depends-on = [{ task = "lint-cpp", args = ["{{ type }}"] }]
|
||||
|
||||
[feature.test.tasks.unit-test]
|
||||
args = [{ arg = "type", default = "RelWithDebInfo" }]
|
||||
cmd = './build/{{ type }}/bin/unit_tests --test-dir="./tests/data"'
|
||||
cmd = './build/{{ type }}/bin/unit_tests --test-dir="./tests/data" --snapshot-dir="./tests/snapshots" --corpus-dir="./tests/corpus" --verbose'
|
||||
|
||||
[feature.test.tasks.integration-test]
|
||||
args = [{ arg = "type", default = "RelWithDebInfo" }]
|
||||
cmd = """
|
||||
pytest -s --log-cli-level=INFO tests/integration \
|
||||
--executable=./build/{{ type }}/bin/clice
|
||||
pytest -s --log-cli-level=INFO --timeout=300 --timeout-method=thread \
|
||||
tests/integration --executable=./build/{{ type }}/bin/clice
|
||||
"""
|
||||
|
||||
[feature.test.tasks.smoke-test]
|
||||
@@ -256,7 +257,7 @@ format-markdown = "fd -H -e md -x prettier --write"
|
||||
format-json = "fd -H -e json -E package-lock.json -x prettier --write"
|
||||
format-toml = "fd -H -e toml -x tombi format"
|
||||
format-yaml = """
|
||||
fd -H -e yaml -e yml -E pnpm-lock.yaml -x prettier --write && \
|
||||
fd -H -e yaml -e yml -E pnpm-lock.yaml -E '*.snap.yml' -x prettier --write && \
|
||||
fd -H "^\\.clang-(format|tidy)$" -x prettier --write --parser yaml
|
||||
"""
|
||||
format = { depends-on = [
|
||||
|
||||
177
src/clice.cc
177
src/clice.cc
@@ -4,33 +4,33 @@
|
||||
#include <print>
|
||||
#include <string>
|
||||
|
||||
#include "server/master_server.h"
|
||||
#include "server/stateful_worker.h"
|
||||
#include "server/stateless_worker.h"
|
||||
#include "server/service/agentic.h"
|
||||
#include "server/service/master_server.h"
|
||||
#include "server/worker/stateful_worker.h"
|
||||
#include "server/worker/stateless_worker.h"
|
||||
#include "support/logging.h"
|
||||
|
||||
#include "kota/async/async.h"
|
||||
#include "kota/deco/deco.h"
|
||||
#include "kota/ipc/codec/json.h"
|
||||
#include "kota/ipc/peer.h"
|
||||
#include "kota/ipc/recording_transport.h"
|
||||
#include "kota/ipc/transport.h"
|
||||
|
||||
namespace clice {
|
||||
|
||||
using kota::deco::decl::KVStyle;
|
||||
|
||||
struct Options {
|
||||
DecoKV(style = KVStyle::JoinedOrSeparate,
|
||||
help = "Running mode: pipe, socket, stateless-worker, stateful-worker",
|
||||
required = false)
|
||||
DecoKV(
|
||||
style = KVStyle::JoinedOrSeparate,
|
||||
help =
|
||||
"Running mode: pipe, socket, daemon, relay, agentic, stateless-worker, stateful-worker",
|
||||
required = false)
|
||||
<std::string> mode;
|
||||
|
||||
DecoKV(style = KVStyle::JoinedOrSeparate, help = "Socket mode address", required = false)
|
||||
<std::string> host = "127.0.0.1";
|
||||
|
||||
DecoKV(style = KVStyle::JoinedOrSeparate, help = "Socket mode port", required = false)
|
||||
<int> port = 50051;
|
||||
DecoKV(style = KVStyle::JoinedOrSeparate,
|
||||
help = "Agentic TCP port (0 = disabled)",
|
||||
required = false)
|
||||
<int> port = 0;
|
||||
|
||||
DecoKV(style = KVStyle::JoinedOrSeparate,
|
||||
names = {"--log-level", "--log-level="},
|
||||
@@ -43,6 +43,50 @@ struct Options {
|
||||
required = false)
|
||||
<std::string> record;
|
||||
|
||||
DecoKV(style = KVStyle::JoinedOrSeparate,
|
||||
help = "File path for agentic queries",
|
||||
required = false)
|
||||
<std::string> path;
|
||||
|
||||
DecoKV(
|
||||
style = KVStyle::JoinedOrSeparate,
|
||||
help =
|
||||
"Agentic method (compileCommand, symbolSearch, definition, references, "
|
||||
"documentSymbols, readSymbol, callGraph, typeHierarchy, projectFiles, "
|
||||
"fileDeps, impactAnalysis, status, shutdown)",
|
||||
required = false)
|
||||
<std::string> method;
|
||||
|
||||
DecoKV(style = KVStyle::JoinedOrSeparate,
|
||||
help = "Symbol name for agentic queries",
|
||||
required = false)
|
||||
<std::string> name;
|
||||
|
||||
DecoKV(style = KVStyle::JoinedOrSeparate,
|
||||
help = "Search query for symbolSearch",
|
||||
required = false)
|
||||
<std::string> query;
|
||||
|
||||
DecoKV(style = KVStyle::JoinedOrSeparate,
|
||||
help = "Line number for position-based lookup",
|
||||
required = false)
|
||||
<int> line;
|
||||
|
||||
DecoKV(style = KVStyle::JoinedOrSeparate,
|
||||
help = "Direction: callers/callees or supertypes/subtypes",
|
||||
required = false)
|
||||
<std::string> direction;
|
||||
|
||||
DecoKV(style = KVStyle::JoinedOrSeparate,
|
||||
help = "Unix domain socket path for daemon mode",
|
||||
required = false)
|
||||
<std::string> socket;
|
||||
|
||||
DecoKV(style = KVStyle::JoinedOrSeparate,
|
||||
help = "Workspace root directory for daemon mode",
|
||||
required = false)
|
||||
<std::string> workspace;
|
||||
|
||||
// Internal options (passed from master to worker processes)
|
||||
DecoKV(style = KVStyle::JoinedOrSeparate,
|
||||
names = {"--worker-memory-limit", "--worker-memory-limit="},
|
||||
@@ -68,9 +112,6 @@ struct Options {
|
||||
|
||||
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
|
||||
|
||||
@@ -110,8 +151,6 @@ int main(int argc, const char** argv) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::string self_path = argv[0];
|
||||
|
||||
auto& mode = *opts.mode;
|
||||
|
||||
auto worker_name = opts.worker_name.value_or("");
|
||||
@@ -131,77 +170,51 @@ int main(int argc, const char** argv) {
|
||||
log_dir);
|
||||
}
|
||||
|
||||
if(mode == "pipe") {
|
||||
clice::logging::stderr_logger("master", clice::logging::options);
|
||||
|
||||
kota::event_loop loop;
|
||||
|
||||
auto transport = kota::ipc::StreamTransport::open_stdio(loop);
|
||||
if(!transport) {
|
||||
LOG_ERROR("failed to open stdio transport");
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::unique_ptr<kota::ipc::Transport> final_transport = std::move(*transport);
|
||||
if(opts.record.has_value()) {
|
||||
final_transport =
|
||||
std::make_unique<kota::ipc::RecordingTransport>(std::move(final_transport),
|
||||
*opts.record);
|
||||
}
|
||||
|
||||
kota::ipc::JsonPeer peer(loop, std::move(final_transport));
|
||||
clice::MasterServer server(loop, peer, std::move(self_path));
|
||||
server.register_handlers();
|
||||
|
||||
loop.schedule(peer.run());
|
||||
loop.run();
|
||||
return 0;
|
||||
if(mode == "pipe" || mode == "socket") {
|
||||
clice::ServerOptions server_opts;
|
||||
server_opts.mode = mode;
|
||||
server_opts.host = opts.host.value_or("127.0.0.1");
|
||||
server_opts.port = opts.port.value_or(0);
|
||||
server_opts.self_path = argv[0];
|
||||
server_opts.record = opts.record.value_or("");
|
||||
return clice::run_server_mode(server_opts);
|
||||
}
|
||||
|
||||
if(mode == "socket") {
|
||||
clice::logging::stderr_logger("master", clice::logging::options);
|
||||
|
||||
kota::event_loop loop;
|
||||
|
||||
auto host = opts.host.value_or("127.0.0.1");
|
||||
auto port = opts.port.value_or(50051);
|
||||
|
||||
auto acceptor = kota::tcp::listen(host, port, {}, loop);
|
||||
if(!acceptor) {
|
||||
LOG_ERROR("failed to listen on {}:{}", host, port);
|
||||
if(mode == "daemon") {
|
||||
auto workspace = opts.workspace.value_or("");
|
||||
if(workspace.empty()) {
|
||||
LOG_ERROR("--workspace is required for daemon mode");
|
||||
return 1;
|
||||
}
|
||||
|
||||
LOG_INFO("Listening on {}:{} ...", host, port);
|
||||
clice::DaemonOptions daemon_opts;
|
||||
daemon_opts.socket_path = opts.socket.value_or("");
|
||||
daemon_opts.workspace = std::move(workspace);
|
||||
daemon_opts.self_path = argv[0];
|
||||
return clice::run_daemon_mode(daemon_opts);
|
||||
}
|
||||
|
||||
auto task = [&]() -> kota::task<> {
|
||||
auto client = co_await acceptor->accept();
|
||||
if(!client.has_value()) {
|
||||
LOG_ERROR("failed to accept connection");
|
||||
loop.stop();
|
||||
co_return;
|
||||
}
|
||||
if(mode == "agentic") {
|
||||
auto port = opts.port.value_or(0);
|
||||
if(port <= 0) {
|
||||
LOG_ERROR("--port is required for agentic mode");
|
||||
return 1;
|
||||
}
|
||||
clice::AgenticQueryOptions aq;
|
||||
aq.host = opts.host.value_or("127.0.0.1");
|
||||
aq.port = port;
|
||||
aq.method = opts.method.value_or("compileCommand");
|
||||
aq.path = opts.path.value_or("");
|
||||
aq.name = opts.name.value_or("");
|
||||
aq.query = opts.query.value_or("");
|
||||
aq.line = opts.line.value_or(0);
|
||||
aq.direction = opts.direction.value_or("");
|
||||
return clice::run_agentic_mode(aq);
|
||||
}
|
||||
|
||||
LOG_INFO("Client connected");
|
||||
|
||||
std::unique_ptr<kota::ipc::Transport> transport =
|
||||
std::make_unique<kota::ipc::StreamTransport>(std::move(client.value()));
|
||||
if(opts.record.has_value()) {
|
||||
transport = std::make_unique<kota::ipc::RecordingTransport>(std::move(transport),
|
||||
*opts.record);
|
||||
}
|
||||
kota::ipc::JsonPeer peer(loop, std::move(transport));
|
||||
clice::MasterServer server(loop, peer, std::string(self_path));
|
||||
server.register_handlers();
|
||||
|
||||
co_await peer.run();
|
||||
peer.close();
|
||||
loop.stop();
|
||||
};
|
||||
|
||||
loop.schedule(task());
|
||||
loop.run();
|
||||
return 0;
|
||||
if(mode == "relay") {
|
||||
auto socket = opts.socket.value_or("");
|
||||
return clice::run_relay_mode(socket);
|
||||
}
|
||||
|
||||
LOG_ERROR("unknown mode '{}'", mode);
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "support/logging.h"
|
||||
|
||||
#include "kota/meta/enum.h"
|
||||
#include "llvm/ADT/DenseSet.h"
|
||||
#include "llvm/ADT/ScopeExit.h"
|
||||
#include "llvm/Support/CommandLine.h"
|
||||
#include "llvm/Support/FileSystem.h"
|
||||
@@ -17,6 +18,7 @@
|
||||
#include "llvm/TargetParser/Host.h"
|
||||
#include "clang/Driver/Compilation.h"
|
||||
#include "clang/Driver/Driver.h"
|
||||
#include "clang/Driver/Options.h"
|
||||
#include "clang/Driver/Tool.h"
|
||||
|
||||
#ifndef _WIN32
|
||||
@@ -470,11 +472,32 @@ std::vector<const char*> query_clang_toolchain(const QueryParams& params) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for(auto arg: args) {
|
||||
if(arg == "-###"sv) {
|
||||
// FIXME: the system compiler may be newer than our embedded LLVM,
|
||||
// producing cc1 flags we don't recognize. Filter them out here.
|
||||
// Long-term we should unify the command pipeline so the driver
|
||||
// version always matches the embedded LLVM.
|
||||
auto& table = clang::driver::getDriverOptTable();
|
||||
auto cc1_args = llvm::ArrayRef(args).drop_front(2);
|
||||
unsigned missing_index = 0, missing_count = 0;
|
||||
auto parsed = table.ParseArgs(cc1_args, missing_index, missing_count);
|
||||
|
||||
llvm::DenseSet<unsigned> unknown_indices;
|
||||
for(auto* a: parsed) {
|
||||
if(a->getOption().getKind() == llvm::opt::Option::UnknownClass) {
|
||||
unknown_indices.insert(a->getIndex());
|
||||
}
|
||||
}
|
||||
|
||||
result.emplace_back(params.callback(args[0]));
|
||||
result.emplace_back(params.callback(args[1]));
|
||||
for(unsigned i = 0; i < cc1_args.size(); ++i) {
|
||||
if(unknown_indices.contains(i)) {
|
||||
continue;
|
||||
}
|
||||
result.emplace_back(params.callback(arg));
|
||||
if(cc1_args[i] == "-###"sv) {
|
||||
continue;
|
||||
}
|
||||
result.emplace_back(params.callback(cc1_args[i]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,9 +219,10 @@ public:
|
||||
|
||||
auto CreateASTConsumer(clang::CompilerInstance& instance, llvm::StringRef file)
|
||||
-> std::unique_ptr<clang::ASTConsumer> final {
|
||||
return std::make_unique<ProxyASTConsumer>(
|
||||
WrapperFrontendAction::CreateASTConsumer(instance, file),
|
||||
unit);
|
||||
auto consumer = WrapperFrontendAction::CreateASTConsumer(instance, file);
|
||||
if(!consumer)
|
||||
return nullptr;
|
||||
return std::make_unique<ProxyASTConsumer>(std::move(consumer), unit);
|
||||
}
|
||||
|
||||
/// Make this public.
|
||||
|
||||
@@ -81,7 +81,8 @@ auto CompilationUnitRef::file_offset(clang::SourceLocation location) -> std::uin
|
||||
}
|
||||
|
||||
auto CompilationUnitRef::file_path(clang::FileID fid) -> llvm::StringRef {
|
||||
assert(fid.isValid() && "Invalid fid");
|
||||
if(!fid.isValid())
|
||||
return {};
|
||||
if(auto it = self->path_cache.find(fid); it != self->path_cache.end()) {
|
||||
return it->second;
|
||||
}
|
||||
|
||||
@@ -92,15 +92,11 @@ tidy::ClangTidyOptions create_options() {
|
||||
// include-cleaner is directly integrated in IncludeCleaner.cpp
|
||||
"-misc-include-cleaner",
|
||||
|
||||
// ----- False Positives -----
|
||||
|
||||
// Check relies on seeing ifndef/define/endif directives,
|
||||
// clangd doesn't replay those when using a preamble.
|
||||
"-llvm-header-guard",
|
||||
"-modernize-macro-to-enum",
|
||||
|
||||
// ----- Crashing Checks -----
|
||||
|
||||
// Check can choke on invalid (intermediate) c++
|
||||
// code, which is often the case when clangd
|
||||
// tries to build an AST.
|
||||
|
||||
@@ -93,18 +93,9 @@ auto symbol_detail(clang::ASTContext& context, const clang::NamedDecl& decl) ->
|
||||
return detail;
|
||||
}
|
||||
|
||||
struct InternalSymbol {
|
||||
std::string name;
|
||||
std::string detail;
|
||||
SymbolKind kind = SymbolKind::Invalid;
|
||||
LocalSourceRange range;
|
||||
LocalSourceRange selection_range;
|
||||
std::vector<InternalSymbol> children;
|
||||
};
|
||||
|
||||
struct SymbolFrame {
|
||||
std::vector<InternalSymbol> symbols;
|
||||
std::vector<InternalSymbol>* cursor = &symbols;
|
||||
std::vector<DocumentSymbol> symbols;
|
||||
std::vector<DocumentSymbol>* cursor = &symbols;
|
||||
};
|
||||
|
||||
class DocumentSymbolCollector : public FilteredASTVisitor<DocumentSymbolCollector> {
|
||||
@@ -143,7 +134,7 @@ public:
|
||||
return ok;
|
||||
}
|
||||
|
||||
auto collect() -> std::vector<InternalSymbol> {
|
||||
auto collect() -> std::vector<DocumentSymbol> {
|
||||
TraverseDecl(unit.tu());
|
||||
return std::move(result.symbols);
|
||||
}
|
||||
@@ -174,8 +165,8 @@ private:
|
||||
SymbolFrame result;
|
||||
};
|
||||
|
||||
void sort_symbols(std::vector<InternalSymbol>& symbols) {
|
||||
std::ranges::sort(symbols, [](const InternalSymbol& lhs, const InternalSymbol& rhs) {
|
||||
void sort_symbols(std::vector<DocumentSymbol>& symbols) {
|
||||
std::ranges::sort(symbols, [](const DocumentSymbol& lhs, const DocumentSymbol& rhs) {
|
||||
if(lhs.range.begin != rhs.range.begin) {
|
||||
return lhs.range.begin < rhs.range.begin;
|
||||
}
|
||||
@@ -187,7 +178,7 @@ void sort_symbols(std::vector<InternalSymbol>& symbols) {
|
||||
}
|
||||
}
|
||||
|
||||
auto to_protocol_symbol(const InternalSymbol& symbol, const PositionMapper& converter)
|
||||
auto to_protocol_symbol(const DocumentSymbol& symbol, const PositionMapper& converter)
|
||||
-> protocol::DocumentSymbol {
|
||||
protocol::DocumentSymbol result{
|
||||
.name = symbol.name,
|
||||
@@ -215,10 +206,15 @@ auto to_protocol_symbol(const InternalSymbol& symbol, const PositionMapper& conv
|
||||
|
||||
} // namespace
|
||||
|
||||
auto document_symbols(CompilationUnitRef unit) -> std::vector<DocumentSymbol> {
|
||||
auto result = DocumentSymbolCollector(unit).collect();
|
||||
sort_symbols(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
auto document_symbols(CompilationUnitRef unit, PositionEncoding encoding)
|
||||
-> std::vector<protocol::DocumentSymbol> {
|
||||
auto internal = DocumentSymbolCollector(unit).collect();
|
||||
sort_symbols(internal);
|
||||
auto internal = document_symbols(unit);
|
||||
|
||||
PositionMapper converter(unit.interested_content(), encoding);
|
||||
std::vector<protocol::DocumentSymbol> symbols;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#include "compile/compilation.h"
|
||||
#include "compile/compilation_unit.h"
|
||||
#include "semantic/symbol_kind.h"
|
||||
|
||||
#include "kota/ipc/lsp/position.h"
|
||||
#include "kota/ipc/lsp/protocol.h"
|
||||
@@ -59,18 +60,66 @@ struct InlayHintsOptions {
|
||||
|
||||
struct SignatureHelpOptions {};
|
||||
|
||||
auto semantic_tokens(CompilationUnitRef unit, PositionEncoding encoding = PositionEncoding::UTF16)
|
||||
struct SemanticToken {
|
||||
LocalSourceRange range;
|
||||
SymbolKind kind = SymbolKind::Invalid;
|
||||
std::uint32_t modifiers = 0;
|
||||
};
|
||||
|
||||
struct FoldingRange {
|
||||
LocalSourceRange range;
|
||||
std::optional<protocol::FoldingRangeKind> kind;
|
||||
std::string collapsed_text;
|
||||
};
|
||||
|
||||
struct DocumentSymbol {
|
||||
std::string name;
|
||||
std::string detail;
|
||||
SymbolKind kind = SymbolKind::Invalid;
|
||||
LocalSourceRange range;
|
||||
LocalSourceRange selection_range;
|
||||
std::vector<DocumentSymbol> children;
|
||||
};
|
||||
|
||||
enum class HintCategory : std::uint8_t {
|
||||
Parameter,
|
||||
DefaultArgument,
|
||||
Type,
|
||||
Designator,
|
||||
BlockEnd,
|
||||
};
|
||||
|
||||
struct InlayHint {
|
||||
std::uint32_t offset = 0;
|
||||
HintCategory kind = HintCategory::Type;
|
||||
std::string label;
|
||||
bool padding_left = false;
|
||||
bool padding_right = false;
|
||||
};
|
||||
|
||||
auto semantic_tokens(CompilationUnitRef unit) -> std::vector<SemanticToken>;
|
||||
auto semantic_tokens(CompilationUnitRef unit, PositionEncoding encoding)
|
||||
-> protocol::SemanticTokens;
|
||||
|
||||
auto folding_ranges(CompilationUnitRef unit) -> std::vector<FoldingRange>;
|
||||
auto folding_ranges(CompilationUnitRef unit, PositionEncoding encoding)
|
||||
-> std::vector<protocol::FoldingRange>;
|
||||
|
||||
auto document_symbols(CompilationUnitRef unit) -> std::vector<DocumentSymbol>;
|
||||
auto document_symbols(CompilationUnitRef unit, PositionEncoding encoding)
|
||||
-> std::vector<protocol::DocumentSymbol>;
|
||||
|
||||
auto inlay_hints(CompilationUnitRef unit,
|
||||
LocalSourceRange target,
|
||||
const InlayHintsOptions& options = {}) -> std::vector<InlayHint>;
|
||||
auto inlay_hints(CompilationUnitRef unit,
|
||||
LocalSourceRange target,
|
||||
const InlayHintsOptions& options,
|
||||
PositionEncoding encoding) -> std::vector<protocol::InlayHint>;
|
||||
|
||||
auto document_links(CompilationUnitRef unit, PositionEncoding encoding = PositionEncoding::UTF16)
|
||||
-> std::vector<protocol::DocumentLink>;
|
||||
|
||||
auto document_symbols(CompilationUnitRef unit, PositionEncoding encoding = PositionEncoding::UTF16)
|
||||
-> std::vector<protocol::DocumentSymbol>;
|
||||
|
||||
auto folding_ranges(CompilationUnitRef unit, PositionEncoding encoding = PositionEncoding::UTF16)
|
||||
-> std::vector<protocol::FoldingRange>;
|
||||
|
||||
auto diagnostics(CompilationUnitRef unit, PositionEncoding encoding = PositionEncoding::UTF16)
|
||||
-> std::vector<protocol::Diagnostic>;
|
||||
|
||||
@@ -89,12 +138,6 @@ auto hover(CompilationUnitRef unit,
|
||||
const HoverOptions& options = {},
|
||||
PositionEncoding encoding = PositionEncoding::UTF16) -> std::optional<protocol::Hover>;
|
||||
|
||||
auto inlay_hints(CompilationUnitRef unit,
|
||||
LocalSourceRange target,
|
||||
const InlayHintsOptions& options = {},
|
||||
PositionEncoding encoding = PositionEncoding::UTF16)
|
||||
-> std::vector<protocol::InlayHint>;
|
||||
|
||||
auto signature_help(CompilationParams& params, const SignatureHelpOptions& options = {})
|
||||
-> protocol::SignatureHelp;
|
||||
|
||||
|
||||
@@ -53,12 +53,6 @@ auto to_kind(FoldingKind kind) -> protocol::FoldingRangeKind {
|
||||
return protocol::FoldingRangeKind(protocol::FoldingRangeKind::region);
|
||||
}
|
||||
|
||||
struct RawFoldingRange {
|
||||
LocalSourceRange range;
|
||||
std::optional<protocol::FoldingRangeKind> kind;
|
||||
std::string collapsed_text;
|
||||
};
|
||||
|
||||
class FoldingRangeCollector : public FilteredASTVisitor<FoldingRangeCollector> {
|
||||
public:
|
||||
explicit FoldingRangeCollector(CompilationUnitRef unit) : FilteredASTVisitor(unit, true) {}
|
||||
@@ -185,7 +179,7 @@ public:
|
||||
return true;
|
||||
}
|
||||
|
||||
auto collect() -> std::vector<RawFoldingRange> {
|
||||
auto collect() -> std::vector<FoldingRange> {
|
||||
TraverseDecl(unit.tu());
|
||||
|
||||
auto directives_it = unit.directives().find(unit.interested_file());
|
||||
@@ -193,7 +187,7 @@ public:
|
||||
collect_directives(directives_it->second);
|
||||
}
|
||||
|
||||
std::ranges::sort(ranges, [](const RawFoldingRange& lhs, const RawFoldingRange& rhs) {
|
||||
std::ranges::sort(ranges, [](const FoldingRange& lhs, const FoldingRange& rhs) {
|
||||
if(lhs.range.begin != rhs.range.begin) {
|
||||
return lhs.range.begin < rhs.range.begin;
|
||||
}
|
||||
@@ -343,14 +337,18 @@ private:
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<RawFoldingRange> ranges;
|
||||
std::vector<FoldingRange> ranges;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
auto folding_ranges(CompilationUnitRef unit) -> std::vector<FoldingRange> {
|
||||
return FoldingRangeCollector(unit).collect();
|
||||
}
|
||||
|
||||
auto folding_ranges(CompilationUnitRef unit, PositionEncoding encoding)
|
||||
-> std::vector<protocol::FoldingRange> {
|
||||
auto collected = FoldingRangeCollector(unit).collect();
|
||||
auto collected = folding_ranges(unit);
|
||||
PositionMapper converter(unit.interested_content(), encoding);
|
||||
|
||||
std::vector<protocol::FoldingRange> result;
|
||||
|
||||
@@ -49,7 +49,7 @@ auto document_format(llvm::StringRef file,
|
||||
range ? tooling::Range(range->begin, range->length()) : tooling::Range(0, content.size());
|
||||
auto replacements = format_content(file, content, selection);
|
||||
if(!replacements) {
|
||||
LOG_INFO("Fail to format for {}\n{}", file, replacements.error());
|
||||
LOG_WARN("Failed to format {}: {}", file, replacements.error());
|
||||
return edits;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,22 +26,6 @@ using llvm::dyn_cast_or_null;
|
||||
// For now, inlay hints are always anchored at the left or right of their range.
|
||||
enum class HintSide { Left, Right };
|
||||
|
||||
enum class HintCategory : std::uint8_t {
|
||||
Parameter,
|
||||
DefaultArgument,
|
||||
Type,
|
||||
Designator,
|
||||
BlockEnd,
|
||||
};
|
||||
|
||||
struct RawInlayHint {
|
||||
std::uint32_t offset = 0;
|
||||
HintCategory kind = HintCategory::Type;
|
||||
std::string label;
|
||||
bool padding_left = false;
|
||||
bool padding_right = false;
|
||||
};
|
||||
|
||||
bool is_expanded_from_param_pack(const clang::ParmVarDecl* param) {
|
||||
return ast::underlying_pack_type(param) != nullptr;
|
||||
}
|
||||
@@ -123,7 +107,7 @@ struct Callee {
|
||||
|
||||
class Builder {
|
||||
public:
|
||||
Builder(std::vector<RawInlayHint>& result,
|
||||
Builder(std::vector<InlayHint>& result,
|
||||
CompilationUnitRef unit,
|
||||
LocalSourceRange restrict_range,
|
||||
const InlayHintsOptions& options) :
|
||||
@@ -499,7 +483,7 @@ public:
|
||||
bool pad_left = prefix.consume_front(" ");
|
||||
bool pad_right = suffix.consume_back(" ");
|
||||
|
||||
RawInlayHint hint{
|
||||
InlayHint hint{
|
||||
.offset = offset,
|
||||
.kind = kind,
|
||||
.label = (prefix + label + suffix).str(),
|
||||
@@ -554,7 +538,7 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<RawInlayHint>& result;
|
||||
std::vector<InlayHint>& result;
|
||||
CompilationUnitRef unit;
|
||||
LocalSourceRange restrict_range;
|
||||
const InlayHintsOptions& options;
|
||||
@@ -913,36 +897,43 @@ private:
|
||||
|
||||
} // namespace
|
||||
|
||||
auto inlay_hints(CompilationUnitRef unit,
|
||||
LocalSourceRange target,
|
||||
const InlayHintsOptions& options,
|
||||
PositionEncoding encoding) -> std::vector<protocol::InlayHint> {
|
||||
auto inlay_hints(CompilationUnitRef unit, LocalSourceRange target, const InlayHintsOptions& options)
|
||||
-> std::vector<InlayHint> {
|
||||
if(!options.enabled) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<RawInlayHint> raw_hints;
|
||||
std::vector<InlayHint> raw_hints;
|
||||
|
||||
Builder builder(raw_hints, unit, target, options);
|
||||
Visitor visitor(builder, unit, target, options);
|
||||
visitor.TraverseDecl(unit.tu());
|
||||
|
||||
std::ranges::sort(raw_hints, [](const RawInlayHint& lhs, const RawInlayHint& rhs) {
|
||||
std::ranges::sort(raw_hints, [](const InlayHint& lhs, const InlayHint& rhs) {
|
||||
return std::tie(lhs.offset, lhs.label, lhs.kind, lhs.padding_left, lhs.padding_right) <
|
||||
std::tie(rhs.offset, rhs.label, rhs.kind, rhs.padding_left, rhs.padding_right);
|
||||
});
|
||||
auto unique_begin =
|
||||
std::ranges::unique(raw_hints, [](const RawInlayHint& lhs, const RawInlayHint& rhs) {
|
||||
std::ranges::unique(raw_hints, [](const InlayHint& lhs, const InlayHint& rhs) {
|
||||
return lhs.offset == rhs.offset && lhs.kind == rhs.kind && lhs.label == rhs.label &&
|
||||
lhs.padding_left == rhs.padding_left && lhs.padding_right == rhs.padding_right;
|
||||
});
|
||||
raw_hints.erase(unique_begin.begin(), unique_begin.end());
|
||||
|
||||
return raw_hints;
|
||||
}
|
||||
|
||||
auto inlay_hints(CompilationUnitRef unit,
|
||||
LocalSourceRange target,
|
||||
const InlayHintsOptions& options,
|
||||
PositionEncoding encoding) -> std::vector<protocol::InlayHint> {
|
||||
auto collected = inlay_hints(unit, target, options);
|
||||
|
||||
PositionMapper converter(unit.interested_content(), encoding);
|
||||
std::vector<protocol::InlayHint> hints;
|
||||
hints.reserve(raw_hints.size());
|
||||
hints.reserve(collected.size());
|
||||
|
||||
for(const auto& hint: raw_hints) {
|
||||
for(const auto& hint: collected) {
|
||||
protocol::InlayHint out{
|
||||
.position = *converter.to_position(hint.offset),
|
||||
.label = hint.label,
|
||||
|
||||
@@ -18,12 +18,6 @@ namespace clice::feature {
|
||||
|
||||
namespace {
|
||||
|
||||
struct RawToken {
|
||||
LocalSourceRange range;
|
||||
SymbolKind kind = SymbolKind::Invalid;
|
||||
std::uint32_t modifiers = 0;
|
||||
};
|
||||
|
||||
void add_modifier(std::uint32_t& modifiers, SymbolModifiers::Kind kind) {
|
||||
modifiers |= SymbolModifiers::to_mask(kind);
|
||||
}
|
||||
@@ -166,7 +160,7 @@ class SemanticTokensCollector : public SemanticVisitor<SemanticTokensCollector>
|
||||
public:
|
||||
explicit SemanticTokensCollector(CompilationUnitRef unit) : SemanticVisitor(unit, true) {}
|
||||
|
||||
auto collect() -> std::vector<RawToken> {
|
||||
auto collect() -> std::vector<SemanticToken> {
|
||||
highlight_lexical(unit.interested_file());
|
||||
run();
|
||||
highlight_modules();
|
||||
@@ -398,7 +392,7 @@ private:
|
||||
}
|
||||
}
|
||||
|
||||
static void resolve_conflict(RawToken& last, const RawToken& current) {
|
||||
static void resolve_conflict(SemanticToken& last, const SemanticToken& current) {
|
||||
if(last.kind == SymbolKind::Conflict) {
|
||||
return;
|
||||
}
|
||||
@@ -414,14 +408,14 @@ private:
|
||||
}
|
||||
|
||||
void merge_tokens() {
|
||||
std::ranges::sort(tokens, [](const RawToken& lhs, const RawToken& rhs) {
|
||||
std::ranges::sort(tokens, [](const SemanticToken& lhs, const SemanticToken& rhs) {
|
||||
if(lhs.range.begin != rhs.range.begin) {
|
||||
return lhs.range.begin < rhs.range.begin;
|
||||
}
|
||||
return lhs.range.end < rhs.range.end;
|
||||
});
|
||||
|
||||
std::vector<RawToken> merged;
|
||||
std::vector<SemanticToken> merged;
|
||||
merged.reserve(tokens.size());
|
||||
|
||||
for(const auto& token: tokens) {
|
||||
@@ -448,7 +442,7 @@ private:
|
||||
}
|
||||
|
||||
public:
|
||||
std::vector<RawToken> tokens;
|
||||
std::vector<SemanticToken> tokens;
|
||||
};
|
||||
|
||||
class SemanticTokenEncoder {
|
||||
@@ -458,7 +452,7 @@ public:
|
||||
protocol::SemanticTokens& output) :
|
||||
content(content), converter(content, encoding), output(output) {}
|
||||
|
||||
void append(const RawToken& token) {
|
||||
void append(const SemanticToken& token) {
|
||||
if(!token.range.valid() || token.range.end <= token.range.begin ||
|
||||
token.range.end > content.size()) {
|
||||
return;
|
||||
@@ -542,10 +536,14 @@ private:
|
||||
|
||||
} // namespace
|
||||
|
||||
auto semantic_tokens(CompilationUnitRef unit) -> std::vector<SemanticToken> {
|
||||
SemanticTokensCollector collector(unit);
|
||||
return collector.collect();
|
||||
}
|
||||
|
||||
auto semantic_tokens(CompilationUnitRef unit, PositionEncoding encoding)
|
||||
-> protocol::SemanticTokens {
|
||||
SemanticTokensCollector collector(unit);
|
||||
auto tokens = collector.collect();
|
||||
auto tokens = semantic_tokens(unit);
|
||||
|
||||
protocol::SemanticTokens result;
|
||||
result.data.reserve(tokens.size() * 5);
|
||||
|
||||
@@ -308,6 +308,10 @@ const clang::NamedDecl* decl_of_impl(const void* T) {
|
||||
}
|
||||
|
||||
auto decl_of(clang::QualType type) -> const clang::NamedDecl* {
|
||||
if(type.isNull()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Strip type-sugar that wraps the underlying type without adding a decl
|
||||
// (e.g. ElaboratedType for "struct Foo" vs plain "Foo").
|
||||
if(auto ET = type->getAs<clang::ElaboratedType>()) {
|
||||
|
||||
@@ -1111,8 +1111,6 @@ public:
|
||||
return Base::TransformDecltypeType(TLB, TL);
|
||||
}
|
||||
|
||||
// --- State ---
|
||||
|
||||
private:
|
||||
clang::Sema& sema;
|
||||
clang::ASTContext& context;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "server/compile_graph.h"
|
||||
#include "server/compiler/compile_graph.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "server/compiler.h"
|
||||
#include "server/compiler/compiler.h"
|
||||
|
||||
#include <format>
|
||||
#include <ranges>
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
#include "command/search_config.h"
|
||||
#include "index/tu_index.h"
|
||||
#include "server/protocol.h"
|
||||
#include "server/protocol/worker.h"
|
||||
#include "support/filesystem.h"
|
||||
#include "support/logging.h"
|
||||
#include "syntax/include_resolver.h"
|
||||
@@ -28,16 +28,20 @@ using serde_raw = kota::codec::RawValue;
|
||||
/// Detect whether the cursor is inside a preamble directive (include/import).
|
||||
|
||||
Compiler::Compiler(kota::event_loop& loop,
|
||||
kota::ipc::JsonPeer& peer,
|
||||
Workspace& workspace,
|
||||
WorkerPool& pool,
|
||||
llvm::DenseMap<std::uint32_t, Session>& sessions) :
|
||||
loop(loop), peer(peer), workspace(workspace), pool(pool), sessions(sessions) {}
|
||||
loop(loop), workspace(workspace), pool(pool), sessions(sessions) {}
|
||||
|
||||
Compiler::~Compiler() {
|
||||
workspace.cancel_all();
|
||||
}
|
||||
|
||||
kota::task<> Compiler::stop() {
|
||||
compile_tasks.cancel();
|
||||
co_await compile_tasks.join();
|
||||
}
|
||||
|
||||
void Compiler::init_compile_graph() {
|
||||
if(workspace.path_to_module.empty()) {
|
||||
LOG_INFO("No C++20 modules detected, skipping CompileGraph");
|
||||
@@ -410,6 +414,8 @@ std::string uri_to_path(const std::string& uri) {
|
||||
void Compiler::publish_diagnostics(const std::string& uri,
|
||||
int version,
|
||||
const kota::codec::RawValue& diagnostics_json) {
|
||||
if(!peer)
|
||||
return;
|
||||
std::vector<protocol::Diagnostic> diagnostics;
|
||||
if(!diagnostics_json.empty()) {
|
||||
auto status = kota::codec::json::from_json(diagnostics_json.data, diagnostics);
|
||||
@@ -421,14 +427,16 @@ void Compiler::publish_diagnostics(const std::string& uri,
|
||||
params.uri = uri;
|
||||
params.version = version;
|
||||
params.diagnostics = std::move(diagnostics);
|
||||
peer.send_notification(params);
|
||||
peer->send_notification(params);
|
||||
}
|
||||
|
||||
void Compiler::clear_diagnostics(const std::string& uri) {
|
||||
if(!peer)
|
||||
return;
|
||||
protocol::PublishDiagnosticsParams params;
|
||||
params.uri = uri;
|
||||
params.diagnostics = {};
|
||||
peer.send_notification(params);
|
||||
peer->send_notification(params);
|
||||
}
|
||||
|
||||
kota::task<bool> Compiler::ensure_pch(Session& session,
|
||||
@@ -490,6 +498,22 @@ kota::task<bool> Compiler::ensure_pch(Session& session,
|
||||
auto completion = std::make_shared<kota::event>();
|
||||
workspace.pch_cache[path_id].building = completion;
|
||||
|
||||
if(workspace.config.project.cache_dir.empty()) {
|
||||
LOG_WARN("PCH build skipped: cache_dir is not configured");
|
||||
workspace.pch_cache[path_id].building.reset();
|
||||
completion->set();
|
||||
co_return false;
|
||||
}
|
||||
|
||||
// Ensure the PCH cache directory exists.
|
||||
auto pch_dir = path::join(workspace.config.project.cache_dir, "cache", "pch");
|
||||
if(auto ec = llvm::sys::fs::create_directories(pch_dir)) {
|
||||
LOG_WARN("Cannot create PCH cache dir {}: {}", pch_dir, ec.message());
|
||||
workspace.pch_cache[path_id].building.reset();
|
||||
completion->set();
|
||||
co_return false;
|
||||
}
|
||||
|
||||
// Build a new PCH via stateless worker.
|
||||
worker::BuildParams bp;
|
||||
bp.kind = worker::BuildKind::BuildPCH;
|
||||
@@ -613,6 +637,101 @@ void Compiler::record_deps(Session& session, llvm::ArrayRef<std::string> deps) {
|
||||
/// 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
|
||||
kota::task<> Compiler::run_compile(std::uint32_t pid, std::shared_ptr<Session::PendingCompile> pc) {
|
||||
auto find_session = [&]() -> Session* {
|
||||
auto it = sessions.find(pid);
|
||||
return it != 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 path_id={}", pid);
|
||||
pc->done.set();
|
||||
};
|
||||
|
||||
auto gen = sess->generation;
|
||||
LOG_INFO("ensure_compiled: starting compile path_id={} gen={}", pid, gen);
|
||||
|
||||
auto file_path = std::string(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(!fill_compile_args(file_path, params.directory, params.arguments, sess)) {
|
||||
finish_compile();
|
||||
co_return;
|
||||
}
|
||||
|
||||
if(!co_await 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;
|
||||
}
|
||||
|
||||
sess = find_session();
|
||||
if(!sess) {
|
||||
pc->done.set();
|
||||
co_return;
|
||||
}
|
||||
|
||||
auto result = co_await pool.send_stateful(pid, params);
|
||||
|
||||
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);
|
||||
clear_diagnostics(uri_str);
|
||||
finish_compile();
|
||||
co_return;
|
||||
}
|
||||
|
||||
sess->ast_dirty = false;
|
||||
pc->succeeded = true;
|
||||
record_deps(*sess, result.value().deps);
|
||||
|
||||
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(uri_str, version, result.value().diagnostics);
|
||||
if(on_indexing_needed)
|
||||
on_indexing_needed();
|
||||
}
|
||||
|
||||
/// AST and diagnostics have been published to the client.
|
||||
///
|
||||
/// Lifecycle overview (pull-based model):
|
||||
@@ -632,9 +751,9 @@ void Compiler::record_deps(Session& session, llvm::ArrayRef<std::string> deps) {
|
||||
/// 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
|
||||
/// each call ensure_compiled(). The first one spawns a compile task into the
|
||||
/// Compiler's task_group; subsequent ones wait on the shared event.
|
||||
/// The spawned task is not cancelled by LSP $/cancelRequest, preventing
|
||||
/// the race where cancellation wakes all waiters and they all start compiles.
|
||||
kota::task<bool> Compiler::ensure_compiled(Session& session) {
|
||||
auto path_id = session.path_id;
|
||||
@@ -663,124 +782,12 @@ kota::task<bool> Compiler::ensure_compiled(Session& session) {
|
||||
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);
|
||||
LOG_INFO("ensure_compiled: launching compile path_id={} gen={}", path_id, session.generation);
|
||||
|
||||
// Capture path_id by value so the detached lambda can re-lookup the session
|
||||
// from the sessions map after co_await (DenseMap may invalidate pointers).
|
||||
loop.schedule([](Compiler* self,
|
||||
std::uint32_t pid,
|
||||
std::shared_ptr<Session::PendingCompile> pc) -> kota::task<> {
|
||||
// Re-lookup session from the sessions map (pointer may have been
|
||||
// invalidated by DenseMap growth during co_await).
|
||||
auto find_session = [&]() -> Session* {
|
||||
auto it = self->sessions.find(pid);
|
||||
return it != self->sessions.end() ? &it->second : nullptr;
|
||||
};
|
||||
|
||||
auto* sess = find_session();
|
||||
if(!sess) {
|
||||
pc->done.set();
|
||||
co_return;
|
||||
}
|
||||
|
||||
auto finish_compile = [&]() {
|
||||
auto* s = find_session();
|
||||
if(s && s->compiling == pc) {
|
||||
s->compiling.reset();
|
||||
}
|
||||
LOG_INFO("ensure_compiled: finish_compile (detached) path_id={}", pid);
|
||||
pc->done.set();
|
||||
};
|
||||
|
||||
auto gen = sess->generation;
|
||||
LOG_INFO("ensure_compiled: starting compile (detached) path_id={} gen={}", pid, gen);
|
||||
|
||||
auto file_path = std::string(self->workspace.path_pool.resolve(pid));
|
||||
auto uri = lsp::URI::from_file_path(file_path);
|
||||
std::string uri_str = uri.has_value() ? uri->str() : file_path;
|
||||
|
||||
worker::CompileParams params;
|
||||
params.path = file_path;
|
||||
params.version = sess->version;
|
||||
params.text = sess->text;
|
||||
if(!self->fill_compile_args(file_path, params.directory, params.arguments, sess)) {
|
||||
finish_compile();
|
||||
co_return;
|
||||
}
|
||||
|
||||
if(!co_await self
|
||||
->ensure_deps(*sess, params.directory, params.arguments, params.pch, params.pcms)) {
|
||||
LOG_WARN("Dependency preparation failed for {}, skipping compile", uri_str);
|
||||
finish_compile();
|
||||
co_return;
|
||||
}
|
||||
|
||||
// Re-lookup after co_await (DenseMap may have grown).
|
||||
sess = find_session();
|
||||
if(!sess) {
|
||||
pc->done.set();
|
||||
co_return;
|
||||
}
|
||||
|
||||
auto result = co_await self->pool.send_stateful(pid, params);
|
||||
|
||||
// Re-lookup after co_await.
|
||||
sess = find_session();
|
||||
if(!sess) {
|
||||
pc->done.set();
|
||||
co_return;
|
||||
}
|
||||
|
||||
if(sess->generation != gen) {
|
||||
LOG_INFO("ensure_compiled: generation mismatch ({} vs {}) for {}",
|
||||
sess->generation,
|
||||
gen,
|
||||
uri_str);
|
||||
finish_compile();
|
||||
co_return;
|
||||
}
|
||||
|
||||
if(!result.has_value()) {
|
||||
LOG_WARN("Compile failed for {}: {}", uri_str, result.error().message);
|
||||
self->clear_diagnostics(uri_str);
|
||||
finish_compile();
|
||||
co_return;
|
||||
}
|
||||
|
||||
sess->ast_dirty = false;
|
||||
pc->succeeded = true;
|
||||
self->record_deps(*sess, result.value().deps);
|
||||
|
||||
// Store open file index from the stateful worker's TUIndex.
|
||||
if(!result.value().tu_index_data.empty()) {
|
||||
auto tu_index = index::TUIndex::from(result.value().tu_index_data.data());
|
||||
OpenFileIndex ofi;
|
||||
ofi.file_index = std::move(tu_index.main_file_index);
|
||||
ofi.symbols = std::move(tu_index.symbols);
|
||||
ofi.content = sess->text;
|
||||
ofi.mapper.emplace(ofi.content, lsp::PositionEncoding::UTF16);
|
||||
sess->file_index = std::move(ofi);
|
||||
}
|
||||
|
||||
auto version = sess->version;
|
||||
finish_compile();
|
||||
|
||||
// Publish diagnostics AFTER marking compile as done, so that concurrent
|
||||
// forward_query() calls can proceed immediately.
|
||||
self->publish_diagnostics(uri_str, version, result.value().diagnostics);
|
||||
if(self->on_indexing_needed)
|
||||
self->on_indexing_needed();
|
||||
}(this, path_id, pending_compile));
|
||||
compile_tasks.spawn(run_compile(path_id, pending_compile));
|
||||
|
||||
// Wait for the detached compile to finish. If this wait is cancelled
|
||||
// by LSP $/cancelRequest, the detached task continues unaffected.
|
||||
@@ -875,6 +882,32 @@ Compiler::RawResult Compiler::forward_build(worker::BuildKind kind,
|
||||
co_return std::move(result.value().result_json);
|
||||
}
|
||||
|
||||
Compiler::RawResult Compiler::forward_format(Session& session,
|
||||
std::optional<protocol::Range> range) {
|
||||
auto path_id = session.path_id;
|
||||
auto path = std::string(workspace.path_pool.resolve(path_id));
|
||||
|
||||
worker::BuildParams wp;
|
||||
wp.kind = worker::BuildKind::Format;
|
||||
wp.file = path;
|
||||
wp.text = session.text;
|
||||
|
||||
if(range) {
|
||||
lsp::PositionMapper mapper(wp.text, lsp::PositionEncoding::UTF16);
|
||||
auto begin = mapper.to_offset(range->start);
|
||||
auto end = mapper.to_offset(range->end);
|
||||
if(!begin || !end)
|
||||
co_return serde_raw{"null"};
|
||||
wp.format_range = {*begin, *end};
|
||||
}
|
||||
|
||||
auto result = co_await pool.send_stateless(wp);
|
||||
if(!result.has_value()) {
|
||||
co_return serde_raw{"null"};
|
||||
}
|
||||
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;
|
||||
@@ -8,13 +8,13 @@
|
||||
#include <vector>
|
||||
|
||||
#include "command/command.h"
|
||||
#include "server/session.h"
|
||||
#include "server/worker_pool.h"
|
||||
#include "server/workspace.h"
|
||||
#include "server/service/session.h"
|
||||
#include "server/worker/worker_pool.h"
|
||||
#include "server/workspace/workspace.h"
|
||||
#include "syntax/completion.h"
|
||||
|
||||
#include "kota/async/async.h"
|
||||
#include "kota/codec/raw_value.h"
|
||||
#include "kota/codec/json/json.h"
|
||||
#include "kota/ipc/codec/json.h"
|
||||
#include "kota/ipc/lsp/protocol.h"
|
||||
#include "kota/ipc/peer.h"
|
||||
@@ -50,10 +50,14 @@ std::string uri_to_path(const std::string& uri);
|
||||
class Compiler {
|
||||
public:
|
||||
Compiler(kota::event_loop& loop,
|
||||
kota::ipc::JsonPeer& peer,
|
||||
Workspace& workspace,
|
||||
WorkerPool& pool,
|
||||
llvm::DenseMap<std::uint32_t, Session>& sessions);
|
||||
|
||||
void set_peer(kota::ipc::JsonPeer* p) {
|
||||
peer = p;
|
||||
}
|
||||
|
||||
~Compiler();
|
||||
|
||||
void init_compile_graph();
|
||||
@@ -86,6 +90,9 @@ public:
|
||||
const protocol::Position& position,
|
||||
Session& session);
|
||||
|
||||
/// Forward a formatting request to a stateless worker.
|
||||
RawResult forward_format(Session& session, std::optional<protocol::Range> range = {});
|
||||
|
||||
/// 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);
|
||||
@@ -96,7 +103,12 @@ public:
|
||||
/// Callback invoked when indexing should be scheduled.
|
||||
std::function<void()> on_indexing_needed;
|
||||
|
||||
/// Cancel in-flight compile tasks and wait for them to finish.
|
||||
kota::task<> stop();
|
||||
|
||||
private:
|
||||
kota::task<> run_compile(std::uint32_t path_id, std::shared_ptr<Session::PendingCompile> pc);
|
||||
|
||||
kota::task<bool> ensure_deps(Session& session,
|
||||
const std::string& directory,
|
||||
const std::vector<std::string>& arguments,
|
||||
@@ -125,10 +137,11 @@ private:
|
||||
|
||||
private:
|
||||
kota::event_loop& loop;
|
||||
kota::ipc::JsonPeer& peer;
|
||||
kota::ipc::JsonPeer* peer = nullptr;
|
||||
Workspace& workspace;
|
||||
WorkerPool& pool;
|
||||
llvm::DenseMap<std::uint32_t, Session>& sessions;
|
||||
kota::task_group<> compile_tasks{loop};
|
||||
};
|
||||
|
||||
} // namespace clice
|
||||
@@ -1,14 +1,15 @@
|
||||
#include "server/indexer.h"
|
||||
#include "server/compiler/indexer.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
#include "index/tu_index.h"
|
||||
#include "server/compiler.h"
|
||||
#include "server/protocol.h"
|
||||
#include "server/session.h"
|
||||
#include "server/worker_pool.h"
|
||||
#include "server/compiler/compiler.h"
|
||||
#include "server/protocol/worker.h"
|
||||
#include "server/service/session.h"
|
||||
#include "server/worker/worker_pool.h"
|
||||
#include "support/filesystem.h"
|
||||
#include "support/logging.h"
|
||||
|
||||
@@ -446,6 +447,152 @@ std::optional<SymbolInfo> Indexer::resolve_symbol(index::SymbolHash hash) {
|
||||
return SymbolInfo{hash, std::move(name), kind, def_loc->uri, def_loc->range};
|
||||
}
|
||||
|
||||
static std::string extract_line(llvm::StringRef content, std::uint32_t offset) {
|
||||
if(content.empty() || offset >= content.size())
|
||||
return {};
|
||||
std::size_t line_start = 0;
|
||||
if(offset > 0) {
|
||||
auto pos = content.rfind('\n', offset - 1);
|
||||
if(pos != llvm::StringRef::npos)
|
||||
line_start = pos + 1;
|
||||
}
|
||||
auto line_end = content.find('\n', offset);
|
||||
if(line_end == llvm::StringRef::npos)
|
||||
line_end = content.size();
|
||||
return content.slice(line_start, line_end).str();
|
||||
}
|
||||
|
||||
std::optional<Indexer::DefinitionText> Indexer::get_definition_text(index::SymbolHash hash) {
|
||||
for(auto& [id, sess]: sessions) {
|
||||
if(!sess.file_index || !sess.file_index->mapper)
|
||||
continue;
|
||||
auto it = sess.file_index->file_index.relations.find(hash);
|
||||
if(it == sess.file_index->file_index.relations.end())
|
||||
continue;
|
||||
for(auto& rel: it->second) {
|
||||
if(rel.kind.value() != RelationKind::Definition)
|
||||
continue;
|
||||
auto def_range = std::bit_cast<LocalSourceRange>(rel.target_symbol);
|
||||
if(def_range.begin >= def_range.end)
|
||||
continue;
|
||||
llvm::StringRef content = sess.file_index->content;
|
||||
if(def_range.end > content.size())
|
||||
continue;
|
||||
auto start = sess.file_index->mapper->to_position(def_range.begin);
|
||||
auto end = sess.file_index->mapper->to_position(def_range.end);
|
||||
if(!start || !end)
|
||||
continue;
|
||||
return DefinitionText{
|
||||
.file = std::string(workspace.path_pool.resolve(id)),
|
||||
.start_line = static_cast<int>(start->line) + 1,
|
||||
.end_line = static_cast<int>(end->line) + 1,
|
||||
.text =
|
||||
std::string(content.substr(def_range.begin, def_range.end - def_range.begin)),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
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* m = shard_it->second.mapper();
|
||||
if(!m)
|
||||
continue;
|
||||
auto content = shard_it->second.index.content();
|
||||
|
||||
std::optional<DefinitionText> result;
|
||||
shard_it->second.index.lookup(
|
||||
hash,
|
||||
RelationKind::Definition,
|
||||
[&](const index::Relation& r) {
|
||||
auto def_range = std::bit_cast<LocalSourceRange>(r.target_symbol);
|
||||
if(def_range.begin >= def_range.end || def_range.end > content.size())
|
||||
return true;
|
||||
auto start = m->to_position(def_range.begin);
|
||||
auto end = m->to_position(def_range.end);
|
||||
if(!start || !end)
|
||||
return true;
|
||||
result = DefinitionText{
|
||||
.file = workspace.project_index.path_pool.path(file_id).str(),
|
||||
.start_line = static_cast<int>(start->line) + 1,
|
||||
.end_line = static_cast<int>(end->line) + 1,
|
||||
.text = std::string(
|
||||
content.substr(def_range.begin, def_range.end - def_range.begin)),
|
||||
};
|
||||
return false;
|
||||
});
|
||||
if(result)
|
||||
return result;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::vector<Indexer::ReferenceWithContext> Indexer::collect_references(index::SymbolHash hash,
|
||||
RelationKind kind) {
|
||||
std::vector<ReferenceWithContext> results;
|
||||
|
||||
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;
|
||||
auto* m = shard_it->second.mapper();
|
||||
if(!m)
|
||||
continue;
|
||||
auto content = shard_it->second.index.content();
|
||||
auto file_path = workspace.project_index.path_pool.path(file_id);
|
||||
|
||||
shard_it->second.index.lookup(hash, kind, [&](const index::Relation& r) {
|
||||
auto start = m->to_position(r.range.begin);
|
||||
if(!start)
|
||||
return true;
|
||||
results.push_back(ReferenceWithContext{
|
||||
.file = file_path.str(),
|
||||
.line = static_cast<int>(start->line) + 1,
|
||||
.context = extract_line(content, r.range.begin),
|
||||
});
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for(auto& [id, sess]: sessions) {
|
||||
if(!sess.file_index || !sess.file_index->mapper)
|
||||
continue;
|
||||
auto it = sess.file_index->file_index.relations.find(hash);
|
||||
if(it == sess.file_index->file_index.relations.end())
|
||||
continue;
|
||||
auto file_path = workspace.path_pool.resolve(id);
|
||||
llvm::StringRef content = sess.file_index->content;
|
||||
|
||||
for(auto& rel: it->second) {
|
||||
if(rel.kind != kind)
|
||||
continue;
|
||||
auto start = sess.file_index->mapper->to_position(rel.range.begin);
|
||||
if(!start)
|
||||
continue;
|
||||
results.push_back(ReferenceWithContext{
|
||||
.file = file_path.str(),
|
||||
.line = static_cast<int>(start->line) + 1,
|
||||
.context = extract_line(content, rel.range.begin),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
std::vector<protocol::CallHierarchyIncomingCall>
|
||||
Indexer::find_incoming_calls(index::SymbolHash hash) {
|
||||
llvm::DenseMap<index::SymbolHash, std::vector<protocol::Range>> caller_ranges;
|
||||
@@ -624,6 +771,28 @@ void Indexer::enqueue(std::uint32_t server_path_id) {
|
||||
index_queue.push_back(server_path_id);
|
||||
}
|
||||
|
||||
void Indexer::pause_indexing() {
|
||||
++pause_depth;
|
||||
if(pause_depth == 1) {
|
||||
resume_event.reset();
|
||||
LOG_DEBUG("Background indexing paused");
|
||||
}
|
||||
}
|
||||
|
||||
void Indexer::resume_indexing() {
|
||||
if(pause_depth > 0)
|
||||
--pause_depth;
|
||||
if(pause_depth == 0) {
|
||||
resume_event.set();
|
||||
LOG_DEBUG("Background indexing resumed");
|
||||
}
|
||||
}
|
||||
|
||||
kota::task<> Indexer::stop() {
|
||||
bg_tasks.cancel();
|
||||
co_await bg_tasks.join();
|
||||
}
|
||||
|
||||
void Indexer::schedule() {
|
||||
if(!*workspace.config.project.enable_indexing || indexing_active || indexing_scheduled)
|
||||
return;
|
||||
@@ -633,7 +802,77 @@ void Indexer::schedule() {
|
||||
index_idle_timer = std::make_shared<kota::timer>(kota::timer::create(loop));
|
||||
}
|
||||
index_idle_timer->start(std::chrono::milliseconds(*workspace.config.project.idle_timeout_ms));
|
||||
loop.schedule(run_background_indexing());
|
||||
|
||||
if(!bg_tasks.spawn(run_background_indexing())) {
|
||||
indexing_scheduled = false;
|
||||
LOG_WARN("Failed to spawn background indexing task (task group stopped)");
|
||||
}
|
||||
}
|
||||
|
||||
kota::task<> Indexer::index_one(std::uint32_t server_path_id) {
|
||||
auto file_path = std::string(workspace.path_pool.resolve(server_path_id));
|
||||
|
||||
if(sessions.contains(server_path_id))
|
||||
co_return;
|
||||
|
||||
if(!need_update(file_path))
|
||||
co_return;
|
||||
|
||||
// For module interface units, compile their PCM (and transitive deps)
|
||||
// first so the stateless worker has the artifacts it needs.
|
||||
if(workspace.compile_graph && workspace.path_to_module.contains(server_path_id)) {
|
||||
co_await workspace.compile_graph->compile(server_path_id);
|
||||
}
|
||||
|
||||
worker::BuildParams params;
|
||||
params.kind = worker::BuildKind::Index;
|
||||
params.file = file_path;
|
||||
if(!compiler.fill_compile_args(file_path, params.directory, params.arguments, nullptr))
|
||||
co_return;
|
||||
|
||||
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());
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
||||
kota::task<> Indexer::monitor_resources() {
|
||||
while(true) {
|
||||
co_await kota::sleep(std::chrono::milliseconds(3000));
|
||||
|
||||
auto mem = kota::sys::memory();
|
||||
if(mem.total == 0)
|
||||
continue;
|
||||
|
||||
auto effective_total =
|
||||
(mem.constrained > 0 && mem.constrained < mem.total) ? mem.constrained : mem.total;
|
||||
auto ratio = static_cast<double>(mem.available) / static_cast<double>(effective_total);
|
||||
|
||||
if(ratio < 0.15 && max_concurrent > 1) {
|
||||
--max_concurrent;
|
||||
LOG_INFO("Index concurrency -> {} (memory pressure: {:.0f}% available)",
|
||||
max_concurrent,
|
||||
ratio * 100);
|
||||
} else if(ratio > 0.30 && max_concurrent < baseline_concurrent) {
|
||||
++max_concurrent;
|
||||
LOG_DEBUG("Index concurrency -> {} (memory OK: {:.0f}% available)",
|
||||
max_concurrent,
|
||||
ratio * 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
kota::task<> Indexer::run_background_indexing() {
|
||||
@@ -648,48 +887,74 @@ kota::task<> Indexer::run_background_indexing() {
|
||||
}
|
||||
|
||||
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++;
|
||||
kota::cancellation_source monitor_cancel;
|
||||
bg_tasks.spawn(kota::with_token(monitor_resources(), monitor_cancel.token()));
|
||||
|
||||
auto file_path = std::string(workspace.path_pool.resolve(server_path_id));
|
||||
std::stable_partition(
|
||||
index_queue.begin() + index_queue_pos,
|
||||
index_queue.end(),
|
||||
[this](std::uint32_t id) { return workspace.path_to_module.contains(id); });
|
||||
|
||||
if(sessions.contains(server_path_id))
|
||||
continue;
|
||||
auto total = index_queue.size() - index_queue_pos;
|
||||
std::size_t dispatched = 0;
|
||||
std::size_t completed = 0;
|
||||
|
||||
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);
|
||||
std::optional<lsp::ProgressReporter<kota::ipc::JsonPeer>> progress;
|
||||
if(peer) {
|
||||
progress.emplace(*peer, protocol::ProgressToken(std::string("clice/backgroundIndex")));
|
||||
auto create_result = co_await progress->create();
|
||||
if(!create_result.has_error()) {
|
||||
progress->begin("Indexing", std::format("0/{} files", total), 0);
|
||||
} else {
|
||||
LOG_WARN("Background index IPC error for {}: {}", file_path, result.error().message);
|
||||
progress.reset();
|
||||
}
|
||||
}
|
||||
|
||||
kota::task_group<> workers(loop);
|
||||
std::size_t in_flight = 0;
|
||||
kota::event slot_available;
|
||||
|
||||
while(index_queue_pos < index_queue.size()) {
|
||||
if(pause_depth > 0)
|
||||
co_await resume_event.wait();
|
||||
|
||||
auto server_path_id = index_queue[index_queue_pos++];
|
||||
auto file_path = std::string(workspace.path_pool.resolve(server_path_id));
|
||||
if(sessions.contains(server_path_id) || !need_update(file_path)) {
|
||||
++completed;
|
||||
continue;
|
||||
}
|
||||
|
||||
while(in_flight >= max_concurrent) {
|
||||
slot_available.reset();
|
||||
co_await slot_available.wait();
|
||||
}
|
||||
|
||||
++in_flight;
|
||||
++dispatched;
|
||||
workers.spawn([&, server_path_id]() -> kota::task<> {
|
||||
co_await index_one(server_path_id);
|
||||
--in_flight;
|
||||
++completed;
|
||||
if(progress) {
|
||||
auto pct = total > 0 ? static_cast<std::uint32_t>(completed * 100 / total) : 100;
|
||||
progress->report(std::format("{}/{} files", completed, total), pct);
|
||||
}
|
||||
slot_available.set();
|
||||
}());
|
||||
}
|
||||
|
||||
co_await workers.join();
|
||||
|
||||
if(progress) {
|
||||
progress->end(std::format("Indexed {} files", dispatched));
|
||||
}
|
||||
|
||||
monitor_cancel.cancel();
|
||||
|
||||
indexing_active = false;
|
||||
LOG_INFO("Background indexing complete: {} files processed", processed);
|
||||
LOG_INFO("Background indexing complete: {} files dispatched", dispatched);
|
||||
save(workspace.config.project.index_dir);
|
||||
}
|
||||
|
||||
@@ -9,10 +9,12 @@
|
||||
|
||||
#include "semantic/relation_kind.h"
|
||||
#include "semantic/symbol_kind.h"
|
||||
#include "server/workspace.h"
|
||||
#include "server/workspace/workspace.h"
|
||||
|
||||
#include "kota/async/async.h"
|
||||
#include "kota/ipc/codec/json.h"
|
||||
#include "kota/ipc/lsp/position.h"
|
||||
#include "kota/ipc/lsp/progress.h"
|
||||
#include "kota/ipc/lsp/protocol.h"
|
||||
#include "llvm/ADT/DenseMap.h"
|
||||
#include "llvm/ADT/SmallVector.h"
|
||||
@@ -59,8 +61,49 @@ public:
|
||||
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)) {}
|
||||
loop(loop), bg_tasks(loop), workspace(workspace), sessions(sessions), pool(pool),
|
||||
compiler(compiler), is_file_open(std::move(is_file_open)) {}
|
||||
|
||||
/// Set the LSP peer for progress reporting. Must be called before
|
||||
/// schedule() if progress notifications are desired.
|
||||
void set_peer(kota::ipc::JsonPeer* p) {
|
||||
peer = p;
|
||||
}
|
||||
|
||||
/// Temporarily pause background indexing to give priority to user
|
||||
/// requests. Indexing tasks already dispatched to workers continue,
|
||||
/// but no new tasks will be sent until resume_indexing() is called.
|
||||
void pause_indexing();
|
||||
|
||||
/// Resume background indexing after a pause.
|
||||
void resume_indexing();
|
||||
|
||||
/// RAII guard that pauses indexing for its lifetime.
|
||||
struct [[nodiscard]] ScopedPause {
|
||||
Indexer& indexer;
|
||||
|
||||
explicit ScopedPause(Indexer& idx) : indexer(idx) {
|
||||
indexer.pause_indexing();
|
||||
}
|
||||
|
||||
~ScopedPause() {
|
||||
indexer.resume_indexing();
|
||||
}
|
||||
|
||||
ScopedPause(const ScopedPause&) = delete;
|
||||
ScopedPause& operator=(const ScopedPause&) = delete;
|
||||
};
|
||||
|
||||
ScopedPause scoped_pause() {
|
||||
return ScopedPause{*this};
|
||||
}
|
||||
|
||||
/// Set the maximum number of concurrent index tasks.
|
||||
/// Also sets the baseline that dynamic adjustment will restore to.
|
||||
void set_max_concurrency(std::size_t n) {
|
||||
max_concurrent = std::max<std::size_t>(n, 1);
|
||||
baseline_concurrent = max_concurrent;
|
||||
}
|
||||
|
||||
/// Add a file to the background indexing queue.
|
||||
void enqueue(std::uint32_t server_path_id);
|
||||
@@ -124,6 +167,43 @@ public:
|
||||
std::vector<protocol::SymbolInformation> search_symbols(llvm::StringRef query,
|
||||
std::size_t max_results = 100);
|
||||
|
||||
struct DefinitionText {
|
||||
std::string file;
|
||||
int start_line;
|
||||
int end_line;
|
||||
std::string text;
|
||||
};
|
||||
|
||||
/// Get full definition text for a symbol, using stored index ranges and content.
|
||||
std::optional<DefinitionText> get_definition_text(index::SymbolHash hash);
|
||||
|
||||
struct ReferenceWithContext {
|
||||
std::string file;
|
||||
int line;
|
||||
std::string context;
|
||||
};
|
||||
|
||||
/// Collect references (or definitions) with context lines from stored content.
|
||||
std::vector<ReferenceWithContext> collect_references(index::SymbolHash hash, RelationKind kind);
|
||||
|
||||
/// Cancel background indexing and wait for all tasks to settle.
|
||||
kota::task<> stop();
|
||||
|
||||
/// Whether background indexing is currently idle (no active or queued work).
|
||||
bool is_idle() const {
|
||||
return !indexing_active && index_queue_pos >= index_queue.size();
|
||||
}
|
||||
|
||||
/// Number of files remaining in the indexing queue.
|
||||
std::size_t pending_files() const {
|
||||
return index_queue_pos < index_queue.size() ? index_queue.size() - index_queue_pos : 0;
|
||||
}
|
||||
|
||||
/// Total files that were enqueued in the current (or last) indexing round.
|
||||
std::size_t total_queued() const {
|
||||
return index_queue.size();
|
||||
}
|
||||
|
||||
/// Convert internal SymbolKind to LSP SymbolKind.
|
||||
static protocol::SymbolKind to_lsp_symbol_kind(SymbolKind kind);
|
||||
|
||||
@@ -165,6 +245,7 @@ private:
|
||||
|
||||
private:
|
||||
kota::event_loop& loop;
|
||||
kota::task_group<> bg_tasks;
|
||||
Workspace& workspace;
|
||||
llvm::DenseMap<std::uint32_t, Session>& sessions;
|
||||
WorkerPool& pool;
|
||||
@@ -175,6 +256,9 @@ private:
|
||||
/// server-path-id-keyed sessions map to project-level path_ids.
|
||||
std::function<bool(std::uint32_t)> is_file_open;
|
||||
|
||||
/// LSP peer for progress reporting (optional, not owned).
|
||||
kota::ipc::JsonPeer* peer = nullptr;
|
||||
|
||||
/// Background indexing queue and scheduling state.
|
||||
std::vector<std::uint32_t> index_queue;
|
||||
std::size_t index_queue_pos = 0;
|
||||
@@ -182,7 +266,18 @@ private:
|
||||
bool indexing_scheduled = false;
|
||||
std::shared_ptr<kota::timer> index_idle_timer;
|
||||
|
||||
/// Concurrency control for background indexing.
|
||||
std::size_t max_concurrent = 2;
|
||||
std::size_t baseline_concurrent = 2;
|
||||
|
||||
/// Pause/resume: when paused, new index tasks wait on this event.
|
||||
/// Uses a counter so nested pause/resume pairs work correctly.
|
||||
std::size_t pause_depth = 0;
|
||||
kota::event resume_event{true};
|
||||
|
||||
kota::task<> run_background_indexing();
|
||||
kota::task<> index_one(std::uint32_t server_path_id);
|
||||
kota::task<> monitor_resources();
|
||||
};
|
||||
|
||||
} // namespace clice
|
||||
@@ -1,81 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "server/compiler.h"
|
||||
#include "server/indexer.h"
|
||||
#include "server/session.h"
|
||||
#include "server/worker_pool.h"
|
||||
#include "server/workspace.h"
|
||||
|
||||
#include "kota/async/async.h"
|
||||
#include "kota/codec/raw_value.h"
|
||||
#include "kota/ipc/peer.h"
|
||||
#include "llvm/ADT/DenseMap.h"
|
||||
|
||||
namespace clice {
|
||||
|
||||
enum class ServerLifecycle : std::uint8_t {
|
||||
Uninitialized,
|
||||
Initialized,
|
||||
Ready,
|
||||
ShuttingDown,
|
||||
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(kota::event_loop& loop, kota::ipc::JsonPeer& peer, std::string self_path);
|
||||
~MasterServer();
|
||||
|
||||
void register_handlers();
|
||||
|
||||
private:
|
||||
kota::event_loop& loop;
|
||||
kota::ipc::JsonPeer& peer;
|
||||
|
||||
/// Persistent project-wide state (config, CDB, path pool, dependency
|
||||
/// graphs, compilation caches, symbol index).
|
||||
Workspace workspace;
|
||||
|
||||
/// Per-file editing sessions, keyed by server-level path_id.
|
||||
llvm::DenseMap<std::uint32_t, Session> sessions;
|
||||
|
||||
/// Worker process pool for offloading compilation and queries.
|
||||
WorkerPool pool;
|
||||
|
||||
/// Compilation lifecycle manager (reads/writes workspace and sessions).
|
||||
Compiler compiler;
|
||||
|
||||
/// Index query and background scheduling (reads from workspace and sessions).
|
||||
Indexer indexer;
|
||||
|
||||
ServerLifecycle lifecycle = ServerLifecycle::Uninitialized;
|
||||
std::string self_path;
|
||||
std::string workspace_root;
|
||||
std::string session_log_dir;
|
||||
std::string init_options_json; ///< Raw JSON from initializationOptions, consumed once.
|
||||
|
||||
kota::task<> load_workspace();
|
||||
|
||||
using RawResult = kota::task<kota::codec::RawValue, kota::ipc::Error>;
|
||||
};
|
||||
|
||||
} // namespace clice
|
||||
297
src/server/protocol/agentic.h
Normal file
297
src/server/protocol/agentic.h
Normal file
@@ -0,0 +1,297 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "kota/ipc/protocol.h"
|
||||
|
||||
namespace clice::agentic {
|
||||
|
||||
struct CompileCommandParams {
|
||||
std::string path;
|
||||
};
|
||||
|
||||
struct CompileCommandResult {
|
||||
std::string file;
|
||||
std::string directory;
|
||||
std::vector<std::string> arguments;
|
||||
};
|
||||
|
||||
struct FileInfo {
|
||||
std::string path;
|
||||
std::string kind;
|
||||
std::optional<std::string> module_name;
|
||||
};
|
||||
|
||||
struct ProjectFilesParams {
|
||||
std::optional<std::string> filter;
|
||||
};
|
||||
|
||||
struct ProjectFilesResult {
|
||||
std::vector<FileInfo> files;
|
||||
int total = 0;
|
||||
};
|
||||
|
||||
struct DepEntry {
|
||||
std::string path;
|
||||
int depth = 0;
|
||||
};
|
||||
|
||||
struct FileDepsParams {
|
||||
std::string path;
|
||||
std::optional<std::string> direction;
|
||||
std::optional<int> depth;
|
||||
};
|
||||
|
||||
struct FileDepsResult {
|
||||
std::string file;
|
||||
std::vector<DepEntry> includes;
|
||||
std::vector<DepEntry> includers;
|
||||
};
|
||||
|
||||
struct ImpactAnalysisParams {
|
||||
std::string path;
|
||||
};
|
||||
|
||||
struct ImpactAnalysisResult {
|
||||
std::vector<std::string> direct_dependents;
|
||||
std::vector<std::string> transitive_dependents;
|
||||
std::vector<std::string> affected_modules;
|
||||
};
|
||||
|
||||
struct SymbolEntry {
|
||||
std::string name;
|
||||
std::string kind;
|
||||
std::string file;
|
||||
int line = 0;
|
||||
std::optional<std::string> container;
|
||||
std::uint64_t symbol_id = 0;
|
||||
};
|
||||
|
||||
struct SymbolSearchParams {
|
||||
std::string query;
|
||||
std::optional<std::vector<std::string>> kind_filter;
|
||||
std::optional<int> max_results;
|
||||
};
|
||||
|
||||
struct SymbolSearchResult {
|
||||
std::vector<SymbolEntry> symbols;
|
||||
};
|
||||
|
||||
struct ReadSymbolParams {
|
||||
std::optional<std::string> name;
|
||||
std::optional<std::string> path;
|
||||
std::optional<int> line;
|
||||
std::optional<std::uint64_t> symbol_id;
|
||||
};
|
||||
|
||||
struct ReadSymbolResult {
|
||||
std::string name;
|
||||
std::string kind;
|
||||
std::string file;
|
||||
int start_line = 0;
|
||||
int end_line = 0;
|
||||
std::string text;
|
||||
std::optional<std::string> signature;
|
||||
std::uint64_t symbol_id = 0;
|
||||
};
|
||||
|
||||
struct DocumentSymbolEntry {
|
||||
std::string name;
|
||||
std::string kind;
|
||||
int start_line = 0;
|
||||
int end_line = 0;
|
||||
std::uint64_t symbol_id = 0;
|
||||
};
|
||||
|
||||
struct DocumentSymbolsParams {
|
||||
std::string path;
|
||||
};
|
||||
|
||||
struct DocumentSymbolsResult {
|
||||
std::vector<DocumentSymbolEntry> symbols;
|
||||
};
|
||||
|
||||
struct DefinitionParams {
|
||||
std::optional<std::string> name;
|
||||
std::optional<std::string> path;
|
||||
std::optional<int> line;
|
||||
std::optional<std::uint64_t> symbol_id;
|
||||
};
|
||||
|
||||
struct LocationEntry {
|
||||
std::string file;
|
||||
int start_line = 0;
|
||||
int end_line = 0;
|
||||
std::string text;
|
||||
};
|
||||
|
||||
struct DefinitionResult {
|
||||
std::string name;
|
||||
std::string kind;
|
||||
std::uint64_t symbol_id = 0;
|
||||
std::optional<LocationEntry> definition;
|
||||
};
|
||||
|
||||
struct ReferenceEntry {
|
||||
std::string file;
|
||||
int line = 0;
|
||||
std::string context;
|
||||
};
|
||||
|
||||
struct ReferencesParams {
|
||||
std::optional<std::string> name;
|
||||
std::optional<std::string> path;
|
||||
std::optional<int> line;
|
||||
std::optional<std::uint64_t> symbol_id;
|
||||
std::optional<bool> include_declaration;
|
||||
};
|
||||
|
||||
struct ReferencesResult {
|
||||
std::string name;
|
||||
std::string kind;
|
||||
std::uint64_t symbol_id = 0;
|
||||
std::vector<ReferenceEntry> references;
|
||||
int total = 0;
|
||||
};
|
||||
|
||||
struct CallGraphEntry {
|
||||
std::string name;
|
||||
std::string kind;
|
||||
std::string file;
|
||||
int line = 0;
|
||||
std::uint64_t symbol_id = 0;
|
||||
};
|
||||
|
||||
struct CallGraphParams {
|
||||
std::optional<std::string> name;
|
||||
std::optional<std::string> path;
|
||||
std::optional<int> line;
|
||||
std::optional<std::uint64_t> symbol_id;
|
||||
std::optional<std::string> direction;
|
||||
std::optional<int> depth;
|
||||
};
|
||||
|
||||
struct CallGraphResult {
|
||||
CallGraphEntry root;
|
||||
std::vector<CallGraphEntry> callers;
|
||||
std::vector<CallGraphEntry> callees;
|
||||
};
|
||||
|
||||
struct TypeHierarchyEntry {
|
||||
std::string name;
|
||||
std::string kind;
|
||||
std::string file;
|
||||
int line = 0;
|
||||
std::uint64_t symbol_id = 0;
|
||||
};
|
||||
|
||||
struct TypeHierarchyParams {
|
||||
std::optional<std::string> name;
|
||||
std::optional<std::string> path;
|
||||
std::optional<int> line;
|
||||
std::optional<std::uint64_t> symbol_id;
|
||||
std::optional<std::string> direction;
|
||||
};
|
||||
|
||||
struct TypeHierarchyResult {
|
||||
TypeHierarchyEntry root;
|
||||
std::vector<TypeHierarchyEntry> supertypes;
|
||||
std::vector<TypeHierarchyEntry> subtypes;
|
||||
};
|
||||
|
||||
struct StatusParams {};
|
||||
|
||||
struct StatusResult {
|
||||
bool idle = true;
|
||||
int pending = 0;
|
||||
int total = 0;
|
||||
int indexed = 0;
|
||||
};
|
||||
|
||||
struct ShutdownParams {};
|
||||
|
||||
} // namespace clice::agentic
|
||||
|
||||
namespace kota::ipc::protocol {
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::agentic::CompileCommandParams> {
|
||||
using Result = clice::agentic::CompileCommandResult;
|
||||
constexpr inline static std::string_view method = "agentic/compileCommand";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::agentic::ProjectFilesParams> {
|
||||
using Result = clice::agentic::ProjectFilesResult;
|
||||
constexpr inline static std::string_view method = "agentic/projectFiles";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::agentic::FileDepsParams> {
|
||||
using Result = clice::agentic::FileDepsResult;
|
||||
constexpr inline static std::string_view method = "agentic/fileDeps";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::agentic::ImpactAnalysisParams> {
|
||||
using Result = clice::agentic::ImpactAnalysisResult;
|
||||
constexpr inline static std::string_view method = "agentic/impactAnalysis";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::agentic::SymbolSearchParams> {
|
||||
using Result = clice::agentic::SymbolSearchResult;
|
||||
constexpr inline static std::string_view method = "agentic/symbolSearch";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::agentic::ReadSymbolParams> {
|
||||
using Result = clice::agentic::ReadSymbolResult;
|
||||
constexpr inline static std::string_view method = "agentic/readSymbol";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::agentic::DocumentSymbolsParams> {
|
||||
using Result = clice::agentic::DocumentSymbolsResult;
|
||||
constexpr inline static std::string_view method = "agentic/documentSymbols";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::agentic::DefinitionParams> {
|
||||
using Result = clice::agentic::DefinitionResult;
|
||||
constexpr inline static std::string_view method = "agentic/definition";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::agentic::ReferencesParams> {
|
||||
using Result = clice::agentic::ReferencesResult;
|
||||
constexpr inline static std::string_view method = "agentic/references";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::agentic::CallGraphParams> {
|
||||
using Result = clice::agentic::CallGraphResult;
|
||||
constexpr inline static std::string_view method = "agentic/callGraph";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::agentic::TypeHierarchyParams> {
|
||||
using Result = clice::agentic::TypeHierarchyResult;
|
||||
constexpr inline static std::string_view method = "agentic/typeHierarchy";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::agentic::StatusParams> {
|
||||
using Result = clice::agentic::StatusResult;
|
||||
constexpr inline static std::string_view method = "agentic/status";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct NotificationTraits<clice::agentic::ShutdownParams> {
|
||||
constexpr inline static std::string_view method = "agentic/shutdown";
|
||||
};
|
||||
|
||||
} // namespace kota::ipc::protocol
|
||||
42
src/server/protocol/extension.h
Normal file
42
src/server/protocol/extension.h
Normal file
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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 = 0;
|
||||
};
|
||||
|
||||
struct CurrentContextParams {
|
||||
std::string uri;
|
||||
};
|
||||
|
||||
struct CurrentContextResult {
|
||||
std::optional<ContextItem> context;
|
||||
};
|
||||
|
||||
struct SwitchContextParams {
|
||||
std::string uri;
|
||||
std::string context_uri;
|
||||
};
|
||||
|
||||
struct SwitchContextResult {
|
||||
bool success = false;
|
||||
};
|
||||
|
||||
} // namespace clice::ext
|
||||
@@ -1,7 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
@@ -9,8 +8,7 @@
|
||||
|
||||
#include "syntax/token.h"
|
||||
|
||||
#include "kota/codec/raw_value.h"
|
||||
#include "kota/ipc/lsp/protocol.h"
|
||||
#include "kota/codec/json/json.h"
|
||||
#include "kota/ipc/protocol.h"
|
||||
|
||||
namespace clice::worker {
|
||||
@@ -66,6 +64,7 @@ enum class BuildKind : uint8_t {
|
||||
Index,
|
||||
Completion,
|
||||
SignatureHelp,
|
||||
Format,
|
||||
};
|
||||
|
||||
/// Unified parameters for all stateless build/compilation tasks.
|
||||
@@ -76,6 +75,7 @@ enum class BuildKind : uint8_t {
|
||||
/// - Index: + pcms
|
||||
/// - Completion: + text, version, offset, pch, pcms
|
||||
/// - SignatureHelp: + text, version, offset, pch, pcms
|
||||
/// - Format: + text, format_range (optional)
|
||||
struct BuildParams {
|
||||
BuildKind kind;
|
||||
std::string file;
|
||||
@@ -92,6 +92,7 @@ struct BuildParams {
|
||||
std::string output_path; ///< BuildPCH, BuildPCM
|
||||
std::string module_name; ///< BuildPCM
|
||||
uint32_t preamble_bound = UINT32_MAX; ///< BuildPCH
|
||||
LocalSourceRange format_range; ///< Format (default = full document)
|
||||
};
|
||||
|
||||
/// Unified result for stateless build tasks.
|
||||
@@ -122,43 +123,6 @@ 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 kota::ipc::protocol {
|
||||
|
||||
template <>
|
||||
787
src/server/service/agent_client.cpp
Normal file
787
src/server/service/agent_client.cpp
Normal file
@@ -0,0 +1,787 @@
|
||||
#include "server/service/agent_client.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <format>
|
||||
#include <ranges>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "server/protocol/agentic.h"
|
||||
#include "server/service/master_server.h"
|
||||
#include "support/filesystem.h"
|
||||
#include "support/logging.h"
|
||||
|
||||
#include "kota/ipc/lsp/uri.h"
|
||||
#include "kota/meta/enum.h"
|
||||
#include "llvm/ADT/DenseSet.h"
|
||||
#include "llvm/ADT/SmallVector.h"
|
||||
|
||||
namespace clice {
|
||||
|
||||
using kota::ipc::RequestResult;
|
||||
using RequestContext = kota::ipc::JsonPeer::RequestContext;
|
||||
namespace lsp = kota::ipc::lsp;
|
||||
namespace protocol = kota::ipc::protocol;
|
||||
|
||||
static std::string_view symbol_kind_name(SymbolKind kind) {
|
||||
constexpr auto names = kota::meta::reflection<SymbolKind::Kind>::member_names;
|
||||
auto idx = static_cast<std::size_t>(kind.value());
|
||||
if(idx < names.size())
|
||||
return names[idx];
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
struct ResolvedSymbol {
|
||||
index::SymbolHash hash = 0;
|
||||
std::string name;
|
||||
SymbolKind kind;
|
||||
std::string file;
|
||||
int line = 0;
|
||||
};
|
||||
|
||||
static std::vector<ResolvedSymbol> resolve_locator(const agentic::ReadSymbolParams& loc,
|
||||
Workspace& workspace,
|
||||
llvm::DenseMap<std::uint32_t, Session>& sessions,
|
||||
Indexer& indexer) {
|
||||
if(loc.symbol_id.has_value() && *loc.symbol_id != 0) {
|
||||
auto hash = static_cast<index::SymbolHash>(*loc.symbol_id);
|
||||
std::string name;
|
||||
SymbolKind kind;
|
||||
if(!indexer.find_symbol_info(hash, name, kind))
|
||||
return {};
|
||||
auto def_loc = indexer.find_definition_location(hash);
|
||||
if(!def_loc)
|
||||
return {};
|
||||
auto file = uri_to_path(def_loc->uri);
|
||||
int line_num = static_cast<int>(def_loc->range.start.line) + 1;
|
||||
return {
|
||||
{hash, std::move(name), kind, std::move(file), line_num}
|
||||
};
|
||||
}
|
||||
|
||||
if(loc.name.has_value() && !loc.name->empty()) {
|
||||
std::string query_lower = llvm::StringRef(*loc.name).lower();
|
||||
std::vector<ResolvedSymbol> candidates;
|
||||
std::vector<ResolvedSymbol> exact_matches;
|
||||
llvm::DenseSet<index::SymbolHash> seen;
|
||||
|
||||
auto try_symbol = [&](index::SymbolHash hash, const index::Symbol& symbol) {
|
||||
if(symbol.name.empty())
|
||||
return;
|
||||
if(llvm::StringRef(symbol.name).lower().find(query_lower) == std::string::npos)
|
||||
return;
|
||||
auto def_loc = indexer.find_definition_location(hash);
|
||||
if(!def_loc)
|
||||
return;
|
||||
if(!seen.insert(hash).second)
|
||||
return;
|
||||
|
||||
auto file = uri_to_path(def_loc->uri);
|
||||
int line_num = static_cast<int>(def_loc->range.start.line) + 1;
|
||||
|
||||
if(loc.path.has_value() && !loc.path->empty()) {
|
||||
llvm::StringRef wanted(*loc.path);
|
||||
bool basename_only = wanted.find_last_of("/\\") == llvm::StringRef::npos;
|
||||
if(basename_only) {
|
||||
if(llvm::sys::path::filename(file) != wanted)
|
||||
return;
|
||||
} else if(!llvm::StringRef(file).ends_with(wanted)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
bool is_exact = llvm::StringRef(symbol.name).lower() == query_lower ||
|
||||
llvm::StringRef(symbol.name).ends_with("::" + *loc.name);
|
||||
|
||||
ResolvedSymbol rs{hash, symbol.name, symbol.kind, std::move(file), line_num};
|
||||
if(is_exact)
|
||||
exact_matches.push_back(std::move(rs));
|
||||
else
|
||||
candidates.push_back(std::move(rs));
|
||||
};
|
||||
|
||||
for(auto& [hash, symbol]: workspace.project_index.symbols)
|
||||
try_symbol(hash, symbol);
|
||||
for(auto& [_, sess]: sessions) {
|
||||
if(!sess.file_index)
|
||||
continue;
|
||||
for(auto& [hash, symbol]: sess.file_index->symbols)
|
||||
try_symbol(hash, symbol);
|
||||
}
|
||||
|
||||
if(!exact_matches.empty())
|
||||
return exact_matches;
|
||||
return candidates;
|
||||
}
|
||||
|
||||
if(loc.path.has_value() && loc.line.has_value()) {
|
||||
auto path_str = *loc.path;
|
||||
auto target_line = static_cast<protocol::uinteger>(*loc.line - 1);
|
||||
|
||||
auto pool_it = workspace.path_pool.cache.find(path_str);
|
||||
auto server_id = pool_it != workspace.path_pool.cache.end() ? pool_it->second : ~0u;
|
||||
auto* sess =
|
||||
server_id != ~0u && sessions.contains(server_id) ? &sessions[server_id] : nullptr;
|
||||
if(sess && sess->file_index) {
|
||||
auto& fi = *sess->file_index;
|
||||
if(fi.mapper) {
|
||||
for(auto& [hash, rels]: fi.file_index.relations) {
|
||||
for(auto& rel: rels) {
|
||||
if(rel.kind.value() != RelationKind::Definition)
|
||||
continue;
|
||||
auto start = fi.mapper->to_position(rel.range.begin);
|
||||
if(start && start->line == target_line) {
|
||||
std::string name;
|
||||
SymbolKind kind;
|
||||
if(indexer.find_symbol_info(hash, name, kind))
|
||||
return {
|
||||
{hash, std::move(name), kind, path_str, *loc.line}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto it = workspace.project_index.path_pool.find(path_str);
|
||||
if(it == workspace.project_index.path_pool.cache.end())
|
||||
return {};
|
||||
|
||||
auto proj_id = it->second;
|
||||
auto shard_it = workspace.merged_indices.find(proj_id);
|
||||
if(shard_it == workspace.merged_indices.end())
|
||||
return {};
|
||||
|
||||
for(auto& [hash, symbol]: workspace.project_index.symbols) {
|
||||
if(!symbol.reference_files.contains(proj_id))
|
||||
continue;
|
||||
bool found = false;
|
||||
shard_it->second.find_relations(hash,
|
||||
RelationKind::Definition,
|
||||
[&](const index::Relation&, protocol::Range range) {
|
||||
if(range.start.line == target_line) {
|
||||
found = true;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if(found)
|
||||
return {
|
||||
{hash, symbol.name, symbol.kind, path_str, *loc.line}
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
static std::uint64_t extract_symbol_id(const std::optional<protocol::LSPAny>& data) {
|
||||
if(!data.has_value())
|
||||
return 0;
|
||||
if(auto* val = std::get_if<std::int64_t>(&static_cast<const protocol::LSPVariant&>(*data)))
|
||||
return static_cast<std::uint64_t>(*val);
|
||||
LOG_WARN("extract_symbol_id: unexpected LSPAny variant type");
|
||||
return 0;
|
||||
}
|
||||
|
||||
AgentClient::AgentClient(MasterServer& server, kota::ipc::JsonPeer& peer) :
|
||||
server(server), peer(peer) {
|
||||
using namespace agentic;
|
||||
|
||||
auto& srv = this->server;
|
||||
|
||||
peer.on_request(
|
||||
[&srv](RequestContext&,
|
||||
const CompileCommandParams& params) -> RequestResult<CompileCommandParams> {
|
||||
std::string directory;
|
||||
std::vector<std::string> arguments;
|
||||
if(!srv.compiler.fill_compile_args(params.path, directory, arguments)) {
|
||||
co_return kota::outcome_error(
|
||||
kota::ipc::Error{std::format("no compile command found for {}", params.path)});
|
||||
}
|
||||
|
||||
co_return CompileCommandResult{
|
||||
.file = params.path,
|
||||
.directory = std::move(directory),
|
||||
.arguments = std::move(arguments),
|
||||
};
|
||||
});
|
||||
|
||||
peer.on_request([&srv](RequestContext&,
|
||||
const ProjectFilesParams& params) -> RequestResult<ProjectFilesParams> {
|
||||
auto& ws = srv.workspace;
|
||||
auto filter = params.filter.value_or("all");
|
||||
|
||||
ProjectFilesResult result;
|
||||
llvm::DenseSet<std::uint32_t> seen;
|
||||
|
||||
for(auto& entry: ws.cdb.get_entries()) {
|
||||
auto file_path = ws.cdb.resolve_path(entry.file);
|
||||
if(file_path.empty())
|
||||
continue;
|
||||
|
||||
auto proj_it = ws.project_index.path_pool.find(file_path);
|
||||
if(proj_it != ws.project_index.path_pool.cache.end()) {
|
||||
if(!seen.insert(proj_it->second).second)
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string kind_str;
|
||||
auto mod_it = ws.path_to_module.find(ws.path_pool.intern(file_path));
|
||||
if(mod_it != ws.path_to_module.end()) {
|
||||
kind_str = "module";
|
||||
} else {
|
||||
auto ext = llvm::sys::path::extension(file_path);
|
||||
if(ext == ".h" || ext == ".hpp" || ext == ".hxx" || ext == ".hh")
|
||||
kind_str = "header";
|
||||
else
|
||||
kind_str = "source";
|
||||
}
|
||||
|
||||
if(filter != "all" && filter != kind_str)
|
||||
continue;
|
||||
|
||||
FileInfo fi;
|
||||
fi.path = file_path.str();
|
||||
fi.kind = std::move(kind_str);
|
||||
if(mod_it != ws.path_to_module.end())
|
||||
fi.module_name = mod_it->second;
|
||||
result.files.push_back(std::move(fi));
|
||||
}
|
||||
|
||||
if(filter == "all" || filter == "header") {
|
||||
for(auto& [path_id, shard]: ws.merged_indices) {
|
||||
if(seen.contains(path_id))
|
||||
continue;
|
||||
auto path_str = ws.project_index.path_pool.path(path_id);
|
||||
auto ext = llvm::sys::path::extension(path_str);
|
||||
if(ext == ".h" || ext == ".hpp" || ext == ".hxx" || ext == ".hh") {
|
||||
seen.insert(path_id);
|
||||
result.files.push_back(FileInfo{
|
||||
.path = path_str.str(),
|
||||
.kind = "header",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.total = static_cast<int>(result.files.size());
|
||||
co_return result;
|
||||
});
|
||||
|
||||
peer.on_request(
|
||||
[&srv](RequestContext&, const FileDepsParams& params) -> RequestResult<FileDepsParams> {
|
||||
auto& ws = srv.workspace;
|
||||
auto pool_it = ws.path_pool.cache.find(params.path);
|
||||
if(pool_it == ws.path_pool.cache.end())
|
||||
co_return FileDepsResult{.file = params.path};
|
||||
auto path_id = pool_it->second;
|
||||
auto direction = params.direction.value_or("both");
|
||||
auto max_depth = params.depth.value_or(1);
|
||||
|
||||
FileDepsResult result;
|
||||
result.file = params.path;
|
||||
|
||||
if(direction == "includes" || direction == "both") {
|
||||
auto includes = ws.dep_graph.get_all_includes(path_id);
|
||||
for(auto inc_id: includes) {
|
||||
auto real_id = inc_id & DependencyGraph::PATH_ID_MASK;
|
||||
auto inc_path = ws.path_pool.resolve(real_id);
|
||||
result.includes.push_back(DepEntry{.path = inc_path.str(), .depth = 1});
|
||||
}
|
||||
|
||||
if(max_depth == 0 || max_depth > 1) {
|
||||
llvm::DenseSet<std::uint32_t> visited;
|
||||
visited.insert(path_id);
|
||||
for(auto& dep: result.includes)
|
||||
visited.insert(ws.path_pool.intern(dep.path));
|
||||
|
||||
for(std::size_t i = 0; i < result.includes.size(); ++i) {
|
||||
if(max_depth > 0 && result.includes[i].depth >= max_depth)
|
||||
continue;
|
||||
auto dep_id = ws.path_pool.intern(result.includes[i].path);
|
||||
auto sub = ws.dep_graph.get_all_includes(dep_id);
|
||||
for(auto sub_id: sub) {
|
||||
auto real_id = sub_id & DependencyGraph::PATH_ID_MASK;
|
||||
if(!visited.insert(real_id).second)
|
||||
continue;
|
||||
auto sub_path = ws.path_pool.resolve(real_id);
|
||||
result.includes.push_back(DepEntry{
|
||||
.path = sub_path.str(),
|
||||
.depth = result.includes[i].depth + 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(direction == "includers" || direction == "both") {
|
||||
auto includers = ws.dep_graph.get_includers(path_id);
|
||||
for(auto inc_id: includers) {
|
||||
auto inc_path = ws.path_pool.resolve(inc_id);
|
||||
result.includers.push_back(DepEntry{.path = inc_path.str(), .depth = 1});
|
||||
}
|
||||
|
||||
if(max_depth == 0 || max_depth > 1) {
|
||||
llvm::DenseSet<std::uint32_t> visited;
|
||||
visited.insert(path_id);
|
||||
for(auto& dep: result.includers) {
|
||||
auto it = ws.path_pool.cache.find(dep.path);
|
||||
if(it != ws.path_pool.cache.end())
|
||||
visited.insert(it->second);
|
||||
}
|
||||
|
||||
for(std::size_t i = 0; i < result.includers.size(); ++i) {
|
||||
if(max_depth > 0 && result.includers[i].depth >= max_depth)
|
||||
continue;
|
||||
auto dep_it = ws.path_pool.cache.find(result.includers[i].path);
|
||||
if(dep_it == ws.path_pool.cache.end())
|
||||
continue;
|
||||
auto sub = ws.dep_graph.get_includers(dep_it->second);
|
||||
for(auto sub_id: sub) {
|
||||
if(!visited.insert(sub_id).second)
|
||||
continue;
|
||||
auto sub_path = ws.path_pool.resolve(sub_id);
|
||||
result.includers.push_back(DepEntry{
|
||||
.path = sub_path.str(),
|
||||
.depth = result.includers[i].depth + 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
co_return result;
|
||||
});
|
||||
|
||||
peer.on_request(
|
||||
[&srv](RequestContext&,
|
||||
const ImpactAnalysisParams& params) -> RequestResult<ImpactAnalysisParams> {
|
||||
auto& ws = srv.workspace;
|
||||
auto pool_it = ws.path_pool.cache.find(params.path);
|
||||
if(pool_it == ws.path_pool.cache.end())
|
||||
co_return ImpactAnalysisResult{};
|
||||
auto path_id = pool_it->second;
|
||||
|
||||
ImpactAnalysisResult result;
|
||||
|
||||
auto direct_includers = ws.dep_graph.get_includers(path_id);
|
||||
for(auto inc_id: direct_includers) {
|
||||
result.direct_dependents.push_back(ws.path_pool.resolve(inc_id).str());
|
||||
}
|
||||
|
||||
auto hosts = ws.dep_graph.find_host_sources(path_id);
|
||||
llvm::DenseSet<std::uint32_t> seen;
|
||||
seen.insert(path_id);
|
||||
for(auto inc_id: direct_includers)
|
||||
seen.insert(inc_id);
|
||||
for(auto host_id: hosts) {
|
||||
if(seen.insert(host_id).second)
|
||||
result.transitive_dependents.push_back(ws.path_pool.resolve(host_id).str());
|
||||
}
|
||||
|
||||
for(auto host_id: hosts) {
|
||||
auto it = ws.path_to_module.find(host_id);
|
||||
if(it != ws.path_to_module.end())
|
||||
result.affected_modules.push_back(it->second);
|
||||
}
|
||||
auto mod_it = ws.path_to_module.find(path_id);
|
||||
if(mod_it != ws.path_to_module.end())
|
||||
result.affected_modules.push_back(mod_it->second);
|
||||
|
||||
co_return result;
|
||||
});
|
||||
|
||||
peer.on_request([&srv](RequestContext&,
|
||||
const SymbolSearchParams& params) -> RequestResult<SymbolSearchParams> {
|
||||
auto max = params.max_results.value_or(100);
|
||||
std::string query_lower = llvm::StringRef(params.query).lower();
|
||||
|
||||
SymbolSearchResult result;
|
||||
llvm::DenseSet<index::SymbolHash> seen;
|
||||
|
||||
auto try_symbol = [&](index::SymbolHash hash, const index::Symbol& symbol) {
|
||||
if(static_cast<int>(result.symbols.size()) >= max)
|
||||
return;
|
||||
if(symbol.name.empty())
|
||||
return;
|
||||
if(!query_lower.empty() &&
|
||||
llvm::StringRef(symbol.name).lower().find(query_lower) == std::string::npos)
|
||||
return;
|
||||
if(params.kind_filter.has_value()) {
|
||||
auto kind_name = std::string(symbol_kind_name(symbol.kind));
|
||||
auto& filter = *params.kind_filter;
|
||||
if(std::ranges::find(filter, kind_name) == filter.end())
|
||||
return;
|
||||
}
|
||||
auto def_loc = srv.indexer.find_definition_location(hash);
|
||||
if(!def_loc)
|
||||
return;
|
||||
if(!seen.insert(hash).second)
|
||||
return;
|
||||
auto file = uri_to_path(def_loc->uri);
|
||||
result.symbols.push_back(SymbolEntry{
|
||||
.name = symbol.name,
|
||||
.kind = std::string(symbol_kind_name(symbol.kind)),
|
||||
.file = std::move(file),
|
||||
.line = static_cast<int>(def_loc->range.start.line) + 1,
|
||||
.symbol_id = hash,
|
||||
});
|
||||
};
|
||||
|
||||
for(auto& [hash, symbol]: srv.workspace.project_index.symbols)
|
||||
try_symbol(hash, symbol);
|
||||
for(auto& [_, sess]: srv.sessions) {
|
||||
if(!sess.file_index)
|
||||
continue;
|
||||
for(auto& [hash, symbol]: sess.file_index->symbols)
|
||||
try_symbol(hash, symbol);
|
||||
}
|
||||
|
||||
co_return result;
|
||||
});
|
||||
|
||||
peer.on_request(
|
||||
[&srv](RequestContext&, const ReadSymbolParams& params) -> RequestResult<ReadSymbolParams> {
|
||||
auto candidates = resolve_locator(params, srv.workspace, srv.sessions, srv.indexer);
|
||||
if(candidates.empty())
|
||||
co_return kota::outcome_error(kota::ipc::Error{"symbol not found"});
|
||||
if(candidates.size() > 1) {
|
||||
co_return kota::outcome_error(kota::ipc::Error{
|
||||
std::format("ambiguous: {} candidates, use symbolId to disambiguate",
|
||||
candidates.size())});
|
||||
}
|
||||
|
||||
auto& rs = candidates[0];
|
||||
auto def_text = srv.indexer.get_definition_text(rs.hash);
|
||||
if(!def_text)
|
||||
co_return kota::outcome_error(kota::ipc::Error{"definition not found"});
|
||||
|
||||
co_return ReadSymbolResult{
|
||||
.name = rs.name,
|
||||
.kind = std::string(symbol_kind_name(rs.kind)),
|
||||
.file = std::move(def_text->file),
|
||||
.start_line = def_text->start_line,
|
||||
.end_line = def_text->end_line,
|
||||
.text = std::move(def_text->text),
|
||||
.symbol_id = rs.hash,
|
||||
};
|
||||
});
|
||||
|
||||
peer.on_request(
|
||||
[&srv](RequestContext&,
|
||||
const DocumentSymbolsParams& params) -> RequestResult<DocumentSymbolsParams> {
|
||||
auto is_document_level = [](SymbolKind kind) {
|
||||
return kind == SymbolKind::Namespace || kind == SymbolKind::Class ||
|
||||
kind == SymbolKind::Struct || kind == SymbolKind::Union ||
|
||||
kind == SymbolKind::Enum || kind == SymbolKind::Type ||
|
||||
kind == SymbolKind::Field || kind == SymbolKind::EnumMember ||
|
||||
kind == SymbolKind::Function || kind == SymbolKind::Method ||
|
||||
kind == SymbolKind::Variable || kind == SymbolKind::Macro ||
|
||||
kind == SymbolKind::Concept || kind == SymbolKind::Module ||
|
||||
kind == SymbolKind::Operator || kind == SymbolKind::Attribute;
|
||||
};
|
||||
|
||||
DocumentSymbolsResult result;
|
||||
|
||||
auto pool_it = srv.workspace.path_pool.cache.find(params.path);
|
||||
if(pool_it == srv.workspace.path_pool.cache.end())
|
||||
co_return result;
|
||||
auto server_id = pool_it->second;
|
||||
auto sess_it = srv.sessions.find(server_id);
|
||||
if(sess_it != srv.sessions.end() && sess_it->second.file_index) {
|
||||
auto& fi = *sess_it->second.file_index;
|
||||
for(auto& [hash, rels]: fi.file_index.relations) {
|
||||
for(auto& rel: rels) {
|
||||
if(rel.kind.value() != RelationKind::Definition)
|
||||
continue;
|
||||
std::string name;
|
||||
SymbolKind kind;
|
||||
if(!srv.indexer.find_symbol_info(hash, name, kind))
|
||||
continue;
|
||||
if(!is_document_level(kind))
|
||||
continue;
|
||||
if(fi.mapper) {
|
||||
auto start = fi.mapper->to_position(rel.range.begin);
|
||||
auto end = fi.mapper->to_position(rel.range.end);
|
||||
if(start && end) {
|
||||
result.symbols.push_back(DocumentSymbolEntry{
|
||||
.name = std::move(name),
|
||||
.kind = std::string(symbol_kind_name(kind)),
|
||||
.start_line = static_cast<int>(start->line) + 1,
|
||||
.end_line = static_cast<int>(end->line) + 1,
|
||||
.symbol_id = hash,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
co_return result;
|
||||
}
|
||||
|
||||
auto it = srv.workspace.project_index.path_pool.find(params.path);
|
||||
if(it == srv.workspace.project_index.path_pool.cache.end())
|
||||
co_return result;
|
||||
|
||||
auto proj_id = it->second;
|
||||
auto shard_it = srv.workspace.merged_indices.find(proj_id);
|
||||
if(shard_it == srv.workspace.merged_indices.end())
|
||||
co_return result;
|
||||
|
||||
for(auto& [hash, symbol]: srv.workspace.project_index.symbols) {
|
||||
if(symbol.name.empty())
|
||||
continue;
|
||||
if(!is_document_level(symbol.kind))
|
||||
continue;
|
||||
if(!symbol.reference_files.contains(proj_id))
|
||||
continue;
|
||||
|
||||
shard_it->second.find_relations(
|
||||
hash,
|
||||
RelationKind::Definition,
|
||||
[&](const index::Relation&, protocol::Range range) {
|
||||
result.symbols.push_back(DocumentSymbolEntry{
|
||||
.name = symbol.name,
|
||||
.kind = std::string(symbol_kind_name(symbol.kind)),
|
||||
.start_line = static_cast<int>(range.start.line) + 1,
|
||||
.end_line = static_cast<int>(range.end.line) + 1,
|
||||
.symbol_id = hash,
|
||||
});
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
co_return result;
|
||||
});
|
||||
|
||||
peer.on_request(
|
||||
[&srv](RequestContext&, const DefinitionParams& params) -> RequestResult<DefinitionParams> {
|
||||
auto candidates = resolve_locator(
|
||||
ReadSymbolParams{params.name, params.path, params.line, params.symbol_id},
|
||||
srv.workspace,
|
||||
srv.sessions,
|
||||
srv.indexer);
|
||||
if(candidates.empty())
|
||||
co_return kota::outcome_error(kota::ipc::Error{"symbol not found"});
|
||||
if(candidates.size() > 1) {
|
||||
co_return kota::outcome_error(kota::ipc::Error{
|
||||
std::format("ambiguous: {} candidates, use symbolId to disambiguate",
|
||||
candidates.size())});
|
||||
}
|
||||
|
||||
auto& rs = candidates[0];
|
||||
|
||||
DefinitionResult result;
|
||||
result.name = rs.name;
|
||||
result.kind = std::string(symbol_kind_name(rs.kind));
|
||||
result.symbol_id = rs.hash;
|
||||
|
||||
if(auto def_text = srv.indexer.get_definition_text(rs.hash)) {
|
||||
result.definition = LocationEntry{
|
||||
.file = std::move(def_text->file),
|
||||
.start_line = def_text->start_line,
|
||||
.end_line = def_text->end_line,
|
||||
.text = std::move(def_text->text),
|
||||
};
|
||||
}
|
||||
|
||||
co_return result;
|
||||
});
|
||||
|
||||
peer.on_request(
|
||||
[&srv](RequestContext&, const ReferencesParams& params) -> RequestResult<ReferencesParams> {
|
||||
auto candidates = resolve_locator(
|
||||
ReadSymbolParams{params.name, params.path, params.line, params.symbol_id},
|
||||
srv.workspace,
|
||||
srv.sessions,
|
||||
srv.indexer);
|
||||
if(candidates.empty())
|
||||
co_return kota::outcome_error(kota::ipc::Error{"symbol not found"});
|
||||
if(candidates.size() > 1) {
|
||||
co_return kota::outcome_error(kota::ipc::Error{
|
||||
std::format("ambiguous: {} candidates, use symbolId to disambiguate",
|
||||
candidates.size())});
|
||||
}
|
||||
|
||||
auto& rs = candidates[0];
|
||||
|
||||
ReferencesResult result;
|
||||
result.name = rs.name;
|
||||
result.kind = std::string(symbol_kind_name(rs.kind));
|
||||
result.symbol_id = rs.hash;
|
||||
|
||||
for(auto& ref: srv.indexer.collect_references(rs.hash, RelationKind::Reference)) {
|
||||
result.references.push_back(ReferenceEntry{
|
||||
.file = std::move(ref.file),
|
||||
.line = ref.line,
|
||||
.context = std::move(ref.context),
|
||||
});
|
||||
}
|
||||
if(params.include_declaration.value_or(false)) {
|
||||
for(auto& ref: srv.indexer.collect_references(rs.hash, RelationKind::Definition)) {
|
||||
result.references.push_back(ReferenceEntry{
|
||||
.file = std::move(ref.file),
|
||||
.line = ref.line,
|
||||
.context = std::move(ref.context),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
result.total = static_cast<int>(result.references.size());
|
||||
co_return result;
|
||||
});
|
||||
|
||||
peer.on_request(
|
||||
[&srv](RequestContext&, const CallGraphParams& params) -> RequestResult<CallGraphParams> {
|
||||
auto candidates = resolve_locator(
|
||||
ReadSymbolParams{params.name, params.path, params.line, params.symbol_id},
|
||||
srv.workspace,
|
||||
srv.sessions,
|
||||
srv.indexer);
|
||||
if(candidates.empty())
|
||||
co_return kota::outcome_error(kota::ipc::Error{"symbol not found"});
|
||||
if(candidates.size() > 1) {
|
||||
co_return kota::outcome_error(kota::ipc::Error{
|
||||
std::format("ambiguous: {} candidates, use symbolId to disambiguate",
|
||||
candidates.size())});
|
||||
}
|
||||
|
||||
auto& rs = candidates[0];
|
||||
auto direction = params.direction.value_or("both");
|
||||
|
||||
CallGraphResult result;
|
||||
result.root = CallGraphEntry{
|
||||
.name = rs.name,
|
||||
.kind = std::string(symbol_kind_name(rs.kind)),
|
||||
.file = rs.file,
|
||||
.line = rs.line,
|
||||
.symbol_id = rs.hash,
|
||||
};
|
||||
|
||||
auto resolve_kind = [&](std::uint64_t sym_id) -> std::string {
|
||||
if(sym_id == 0)
|
||||
return "Function";
|
||||
std::string name;
|
||||
SymbolKind kind;
|
||||
if(srv.indexer.find_symbol_info(sym_id, name, kind))
|
||||
return std::string(symbol_kind_name(kind));
|
||||
return "Function";
|
||||
};
|
||||
|
||||
if(direction == "callers" || direction == "both") {
|
||||
auto incoming = srv.indexer.find_incoming_calls(rs.hash);
|
||||
for(auto& call: incoming) {
|
||||
auto sid = extract_symbol_id(call.from.data);
|
||||
result.callers.push_back(CallGraphEntry{
|
||||
.name = call.from.name,
|
||||
.kind = resolve_kind(sid),
|
||||
.file = uri_to_path(call.from.uri),
|
||||
.line = static_cast<int>(call.from.range.start.line) + 1,
|
||||
.symbol_id = sid,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if(direction == "callees" || direction == "both") {
|
||||
auto outgoing = srv.indexer.find_outgoing_calls(rs.hash);
|
||||
for(auto& call: outgoing) {
|
||||
auto sid = extract_symbol_id(call.to.data);
|
||||
result.callees.push_back(CallGraphEntry{
|
||||
.name = call.to.name,
|
||||
.kind = resolve_kind(sid),
|
||||
.file = uri_to_path(call.to.uri),
|
||||
.line = static_cast<int>(call.to.range.start.line) + 1,
|
||||
.symbol_id = sid,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
co_return result;
|
||||
});
|
||||
|
||||
peer.on_request(
|
||||
[&srv](RequestContext&,
|
||||
const TypeHierarchyParams& params) -> RequestResult<TypeHierarchyParams> {
|
||||
auto candidates = resolve_locator(
|
||||
ReadSymbolParams{params.name, params.path, params.line, params.symbol_id},
|
||||
srv.workspace,
|
||||
srv.sessions,
|
||||
srv.indexer);
|
||||
if(candidates.empty())
|
||||
co_return kota::outcome_error(kota::ipc::Error{"symbol not found"});
|
||||
if(candidates.size() > 1) {
|
||||
co_return kota::outcome_error(kota::ipc::Error{
|
||||
std::format("ambiguous: {} candidates, use symbolId to disambiguate",
|
||||
candidates.size())});
|
||||
}
|
||||
|
||||
auto& rs = candidates[0];
|
||||
auto direction = params.direction.value_or("both");
|
||||
|
||||
TypeHierarchyResult result;
|
||||
result.root = TypeHierarchyEntry{
|
||||
.name = rs.name,
|
||||
.kind = std::string(symbol_kind_name(rs.kind)),
|
||||
.file = rs.file,
|
||||
.line = rs.line,
|
||||
.symbol_id = rs.hash,
|
||||
};
|
||||
|
||||
auto resolve_kind = [&](std::uint64_t sym_id) -> std::string {
|
||||
if(sym_id == 0)
|
||||
return "Class";
|
||||
std::string name;
|
||||
SymbolKind kind;
|
||||
if(srv.indexer.find_symbol_info(sym_id, name, kind))
|
||||
return std::string(symbol_kind_name(kind));
|
||||
return "Class";
|
||||
};
|
||||
|
||||
if(direction == "supertypes" || direction == "both") {
|
||||
for(auto& item: srv.indexer.find_supertypes(rs.hash)) {
|
||||
auto sid = extract_symbol_id(item.data);
|
||||
result.supertypes.push_back(TypeHierarchyEntry{
|
||||
.name = item.name,
|
||||
.kind = resolve_kind(sid),
|
||||
.file = uri_to_path(item.uri),
|
||||
.line = static_cast<int>(item.range.start.line) + 1,
|
||||
.symbol_id = sid,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if(direction == "subtypes" || direction == "both") {
|
||||
for(auto& item: srv.indexer.find_subtypes(rs.hash)) {
|
||||
auto sid = extract_symbol_id(item.data);
|
||||
result.subtypes.push_back(TypeHierarchyEntry{
|
||||
.name = item.name,
|
||||
.kind = resolve_kind(sid),
|
||||
.file = uri_to_path(item.uri),
|
||||
.line = static_cast<int>(item.range.start.line) + 1,
|
||||
.symbol_id = sid,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
co_return result;
|
||||
});
|
||||
|
||||
peer.on_request([&srv](RequestContext&, const StatusParams&) -> RequestResult<StatusParams> {
|
||||
StatusResult result;
|
||||
result.idle = srv.indexer.is_idle();
|
||||
result.pending = static_cast<int>(srv.indexer.pending_files());
|
||||
result.total = static_cast<int>(srv.indexer.total_queued());
|
||||
result.indexed = std::max(0, result.total - result.pending);
|
||||
co_return result;
|
||||
});
|
||||
|
||||
peer.on_notification([&srv](const ShutdownParams&) {
|
||||
LOG_INFO("agentic/shutdown received, shutting down");
|
||||
srv.schedule_shutdown();
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace clice
|
||||
18
src/server/service/agent_client.h
Normal file
18
src/server/service/agent_client.h
Normal file
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include "kota/ipc/codec/json.h"
|
||||
|
||||
namespace clice {
|
||||
|
||||
class MasterServer;
|
||||
|
||||
class AgentClient {
|
||||
public:
|
||||
AgentClient(MasterServer& server, kota::ipc::JsonPeer& peer);
|
||||
|
||||
private:
|
||||
MasterServer& server;
|
||||
kota::ipc::JsonPeer& peer;
|
||||
};
|
||||
|
||||
} // namespace clice
|
||||
177
src/server/service/agentic.cpp
Normal file
177
src/server/service/agentic.cpp
Normal file
@@ -0,0 +1,177 @@
|
||||
#include "server/service/agentic.h"
|
||||
|
||||
#include <memory>
|
||||
#include <print>
|
||||
#include <string>
|
||||
|
||||
#include "server/protocol/agentic.h"
|
||||
#include "support/filesystem.h"
|
||||
#include "support/logging.h"
|
||||
|
||||
#include "kota/async/async.h"
|
||||
#include "kota/ipc/codec/json.h"
|
||||
#include "kota/ipc/transport.h"
|
||||
|
||||
namespace clice {
|
||||
|
||||
template <typename Params>
|
||||
static kota::task<bool> send_and_print(kota::ipc::JsonPeer& peer, Params params) {
|
||||
auto result = co_await peer.send_request(std::move(params));
|
||||
if(!result) {
|
||||
LOG_ERROR("request failed: {}", result.error().message);
|
||||
co_return false;
|
||||
}
|
||||
auto json = kota::codec::json::to_string<kota::ipc::lsp_config>(*result);
|
||||
std::println("{}", json ? *json : "null");
|
||||
co_return true;
|
||||
}
|
||||
|
||||
static kota::task<> agentic_request(kota::ipc::JsonPeer& peer,
|
||||
int& exit_code,
|
||||
const AgenticQueryOptions& opts) {
|
||||
bool ok = false;
|
||||
|
||||
if(opts.method == "compileCommand") {
|
||||
ok = co_await send_and_print(peer, agentic::CompileCommandParams{.path = opts.path});
|
||||
} else if(opts.method == "projectFiles") {
|
||||
auto filter = opts.query.empty() ? std::nullopt : std::optional(opts.query);
|
||||
ok = co_await send_and_print(peer, agentic::ProjectFilesParams{.filter = filter});
|
||||
} else if(opts.method == "symbolSearch") {
|
||||
ok = co_await send_and_print(peer, agentic::SymbolSearchParams{.query = opts.query});
|
||||
} else if(opts.method == "definition") {
|
||||
auto name = opts.name.empty() ? std::nullopt : std::optional(opts.name);
|
||||
auto path = opts.path.empty() ? std::nullopt : std::optional(opts.path);
|
||||
auto line = opts.line > 0 ? std::optional(opts.line) : std::nullopt;
|
||||
ok = co_await send_and_print(
|
||||
peer,
|
||||
agentic::DefinitionParams{.name = name, .path = path, .line = line});
|
||||
} else if(opts.method == "references") {
|
||||
auto name = opts.name.empty() ? std::nullopt : std::optional(opts.name);
|
||||
auto path = opts.path.empty() ? std::nullopt : std::optional(opts.path);
|
||||
auto line = opts.line > 0 ? std::optional(opts.line) : std::nullopt;
|
||||
ok = co_await send_and_print(
|
||||
peer,
|
||||
agentic::ReferencesParams{.name = name, .path = path, .line = line});
|
||||
} else if(opts.method == "readSymbol") {
|
||||
auto name = opts.name.empty() ? std::nullopt : std::optional(opts.name);
|
||||
auto path = opts.path.empty() ? std::nullopt : std::optional(opts.path);
|
||||
auto line = opts.line > 0 ? std::optional(opts.line) : std::nullopt;
|
||||
ok = co_await send_and_print(
|
||||
peer,
|
||||
agentic::ReadSymbolParams{.name = name, .path = path, .line = line});
|
||||
} else if(opts.method == "documentSymbols") {
|
||||
ok = co_await send_and_print(peer, agentic::DocumentSymbolsParams{.path = opts.path});
|
||||
} else if(opts.method == "callGraph") {
|
||||
auto name = opts.name.empty() ? std::nullopt : std::optional(opts.name);
|
||||
auto path = opts.path.empty() ? std::nullopt : std::optional(opts.path);
|
||||
auto line = opts.line > 0 ? std::optional(opts.line) : std::nullopt;
|
||||
auto dir = opts.direction.empty() ? std::nullopt : std::optional(opts.direction);
|
||||
ok = co_await send_and_print(peer,
|
||||
agentic::CallGraphParams{
|
||||
.name = name,
|
||||
.path = path,
|
||||
.line = line,
|
||||
.direction = dir,
|
||||
});
|
||||
} else if(opts.method == "typeHierarchy") {
|
||||
auto name = opts.name.empty() ? std::nullopt : std::optional(opts.name);
|
||||
auto path = opts.path.empty() ? std::nullopt : std::optional(opts.path);
|
||||
auto line = opts.line > 0 ? std::optional(opts.line) : std::nullopt;
|
||||
auto dir = opts.direction.empty() ? std::nullopt : std::optional(opts.direction);
|
||||
ok = co_await send_and_print(peer,
|
||||
agentic::TypeHierarchyParams{
|
||||
.name = name,
|
||||
.path = path,
|
||||
.line = line,
|
||||
.direction = dir,
|
||||
});
|
||||
} else if(opts.method == "fileDeps") {
|
||||
auto dir = opts.direction.empty() ? std::nullopt : std::optional(opts.direction);
|
||||
ok = co_await send_and_print(peer,
|
||||
agentic::FileDepsParams{.path = opts.path, .direction = dir});
|
||||
} else if(opts.method == "impactAnalysis") {
|
||||
ok = co_await send_and_print(peer, agentic::ImpactAnalysisParams{.path = opts.path});
|
||||
} else if(opts.method == "status") {
|
||||
ok = co_await send_and_print(peer, agentic::StatusParams{});
|
||||
} else if(opts.method == "shutdown") {
|
||||
peer.send_notification(agentic::ShutdownParams{});
|
||||
ok = true;
|
||||
} else {
|
||||
LOG_ERROR("unknown agentic method '{}'", opts.method);
|
||||
}
|
||||
|
||||
if(ok)
|
||||
exit_code = 0;
|
||||
peer.close();
|
||||
}
|
||||
|
||||
static kota::task<> agentic_client(int& exit_code,
|
||||
std::unique_ptr<kota::ipc::JsonPeer>& peer_out,
|
||||
const AgenticQueryOptions& opts) {
|
||||
auto& loop = kota::event_loop::current();
|
||||
auto transport = co_await kota::ipc::StreamTransport::connect_tcp(opts.host, opts.port, loop);
|
||||
if(!transport) {
|
||||
LOG_ERROR("failed to connect to {}:{}", opts.host, opts.port);
|
||||
co_return;
|
||||
}
|
||||
|
||||
peer_out = std::make_unique<kota::ipc::JsonPeer>(loop, std::move(*transport));
|
||||
co_await kota::when_all(peer_out->run(), agentic_request(*peer_out, exit_code, opts));
|
||||
}
|
||||
|
||||
int run_agentic_mode(const AgenticQueryOptions& opts) {
|
||||
logging::stderr_logger("agentic", logging::options);
|
||||
|
||||
kota::event_loop loop;
|
||||
int exit_code = 1;
|
||||
std::unique_ptr<kota::ipc::JsonPeer> peer;
|
||||
loop.schedule(agentic_client(exit_code, peer, opts));
|
||||
loop.run();
|
||||
return exit_code;
|
||||
}
|
||||
|
||||
static kota::task<> relay_forward(kota::ipc::Transport& from, kota::ipc::Transport& to) {
|
||||
while(true) {
|
||||
auto msg = co_await from.read_message();
|
||||
if(!msg)
|
||||
break;
|
||||
co_await to.write_message(*msg);
|
||||
}
|
||||
to.close();
|
||||
}
|
||||
|
||||
static kota::task<> relay_main(kota::event_loop& loop, int& exit_code, std::string socket_path) {
|
||||
auto stdio = kota::ipc::StreamTransport::open_stdio(loop);
|
||||
if(!stdio) {
|
||||
LOG_ERROR("failed to open stdio transport");
|
||||
loop.stop();
|
||||
co_return;
|
||||
}
|
||||
|
||||
auto conn = co_await kota::pipe::connect(socket_path, {}, loop);
|
||||
if(!conn) {
|
||||
LOG_ERROR("failed to connect to {}", socket_path);
|
||||
loop.stop();
|
||||
co_return;
|
||||
}
|
||||
|
||||
auto socket = std::make_unique<kota::ipc::StreamTransport>(std::move(*conn));
|
||||
|
||||
co_await kota::when_all(relay_forward(**stdio, *socket), relay_forward(*socket, **stdio));
|
||||
exit_code = 0;
|
||||
loop.stop();
|
||||
}
|
||||
|
||||
int run_relay_mode(llvm::StringRef socket_path) {
|
||||
logging::stderr_logger("relay", logging::options);
|
||||
|
||||
auto path = socket_path.empty() ? path::default_socket_path() : socket_path.str();
|
||||
|
||||
kota::event_loop loop;
|
||||
int exit_code = 1;
|
||||
loop.schedule(relay_main(loop, exit_code, std::move(path)));
|
||||
loop.run();
|
||||
return exit_code;
|
||||
}
|
||||
|
||||
} // namespace clice
|
||||
24
src/server/service/agentic.h
Normal file
24
src/server/service/agentic.h
Normal file
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "llvm/ADT/StringRef.h"
|
||||
|
||||
namespace clice {
|
||||
|
||||
struct AgenticQueryOptions {
|
||||
std::string host;
|
||||
int port = 0;
|
||||
std::string method;
|
||||
std::string path;
|
||||
std::string name;
|
||||
std::string query;
|
||||
int line = 0;
|
||||
std::string direction;
|
||||
};
|
||||
|
||||
int run_agentic_mode(const AgenticQueryOptions& opts);
|
||||
|
||||
int run_relay_mode(llvm::StringRef socket_path);
|
||||
|
||||
} // namespace clice
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "server/master_server.h"
|
||||
#include "server/service/lsp_client.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <format>
|
||||
@@ -7,7 +7,9 @@
|
||||
#include <variant>
|
||||
|
||||
#include "semantic/symbol_kind.h"
|
||||
#include "server/protocol.h"
|
||||
#include "server/protocol/extension.h"
|
||||
#include "server/protocol/worker.h"
|
||||
#include "server/service/master_server.h"
|
||||
#include "support/filesystem.h"
|
||||
#include "support/logging.h"
|
||||
|
||||
@@ -16,7 +18,6 @@
|
||||
#include "kota/ipc/lsp/protocol.h"
|
||||
#include "kota/ipc/lsp/uri.h"
|
||||
#include "kota/meta/enum.h"
|
||||
#include "llvm/Support/FileSystem.h"
|
||||
#include "llvm/Support/Path.h"
|
||||
#include "llvm/Support/Process.h"
|
||||
|
||||
@@ -29,177 +30,39 @@ using kota::ipc::RequestResult;
|
||||
using RequestContext = kota::ipc::JsonPeer::RequestContext;
|
||||
using serde_raw = kota::codec::RawValue;
|
||||
|
||||
/// Serialize a value to a JSON RawValue using LSP config.
|
||||
template <typename T>
|
||||
static serde_raw to_raw(const T& value) {
|
||||
auto json = kota::codec::json::to_json<kota::ipc::lsp_config>(value);
|
||||
return serde_raw{json ? std::move(*json) : "null"};
|
||||
}
|
||||
|
||||
MasterServer::MasterServer(kota::event_loop& loop,
|
||||
kota::ipc::JsonPeer& peer,
|
||||
std::string self_path) :
|
||||
loop(loop), peer(peer), pool(loop), compiler(loop, peer, workspace, pool, sessions),
|
||||
indexer(loop,
|
||||
workspace,
|
||||
sessions,
|
||||
pool,
|
||||
compiler,
|
||||
[this](uint32_t proj_path_id) {
|
||||
// Bridge project-level path_id to server-level path_id.
|
||||
// The two PathPools may assign different IDs to the same path.
|
||||
auto path = workspace.project_index.path_pool.path(proj_path_id);
|
||||
auto server_id = workspace.path_pool.intern(path);
|
||||
return sessions.contains(server_id);
|
||||
}),
|
||||
self_path(std::move(self_path)) {}
|
||||
LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(server), peer(peer) {
|
||||
server.compiler.set_peer(&peer);
|
||||
server.indexer.set_peer(&peer);
|
||||
|
||||
MasterServer::~MasterServer() = default;
|
||||
|
||||
kota::task<> MasterServer::load_workspace() {
|
||||
if(workspace_root.empty())
|
||||
co_return;
|
||||
|
||||
auto& cfg = workspace.config.project;
|
||||
|
||||
if(!cfg.cache_dir.empty()) {
|
||||
auto ec = llvm::sys::fs::create_directories(cfg.cache_dir);
|
||||
if(ec) {
|
||||
LOG_WARN("Failed to create cache directory {}: {}",
|
||||
std::string_view(cfg.cache_dir),
|
||||
ec.message());
|
||||
} else {
|
||||
LOG_INFO("Cache directory: {}", std::string_view(cfg.cache_dir));
|
||||
}
|
||||
|
||||
for(auto* subdir: {"cache/pch", "cache/pcm"}) {
|
||||
auto dir = path::join(cfg.cache_dir, subdir);
|
||||
if(auto ec2 = llvm::sys::fs::create_directories(dir))
|
||||
LOG_WARN("Failed to create {}: {}", dir, ec2.message());
|
||||
}
|
||||
|
||||
workspace.cleanup_cache();
|
||||
workspace.load_cache();
|
||||
}
|
||||
|
||||
// Discover compile_commands.json: configured paths first, then auto-scan.
|
||||
std::string cdb_path;
|
||||
for(auto& configured: cfg.compile_commands_paths) {
|
||||
// Each entry can be a file or a directory containing compile_commands.json.
|
||||
if(llvm::sys::fs::is_directory(configured)) {
|
||||
auto candidate = path::join(configured, "compile_commands.json");
|
||||
if(llvm::sys::fs::exists(candidate)) {
|
||||
cdb_path = std::move(candidate);
|
||||
break;
|
||||
}
|
||||
} else if(llvm::sys::fs::exists(configured)) {
|
||||
cdb_path = configured;
|
||||
break;
|
||||
} else {
|
||||
LOG_WARN("Configured compile_commands_path not found: {}", configured);
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-scan: workspace root + all immediate subdirectories.
|
||||
if(cdb_path.empty()) {
|
||||
auto try_candidate = [&](llvm::StringRef dir) -> bool {
|
||||
auto candidate = path::join(dir, "compile_commands.json");
|
||||
if(llvm::sys::fs::exists(candidate)) {
|
||||
cdb_path = std::move(candidate);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
if(!try_candidate(workspace_root)) {
|
||||
std::error_code ec;
|
||||
for(llvm::sys::fs::directory_iterator it(workspace_root, ec), end; it != end && !ec;
|
||||
it.increment(ec)) {
|
||||
if(it->type() == llvm::sys::fs::file_type::directory_file) {
|
||||
if(try_candidate(it->path()))
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(cdb_path.empty()) {
|
||||
LOG_WARN("No compile_commands.json found in workspace {}", workspace_root);
|
||||
co_return;
|
||||
}
|
||||
|
||||
auto count = workspace.cdb.load(cdb_path);
|
||||
LOG_INFO("Loaded CDB from {} with {} entries", cdb_path, count);
|
||||
|
||||
auto report = scan_dependency_graph(workspace.cdb,
|
||||
workspace.path_pool,
|
||||
workspace.dep_graph,
|
||||
/*cache=*/nullptr,
|
||||
[this](llvm::StringRef path,
|
||||
std::vector<std::string>& append,
|
||||
std::vector<std::string>& remove) {
|
||||
workspace.config.match_rules(path, append, remove);
|
||||
});
|
||||
workspace.dep_graph.build_reverse_map();
|
||||
|
||||
auto unresolved = report.includes_found - report.includes_resolved;
|
||||
double accuracy =
|
||||
report.includes_found > 0
|
||||
? 100.0 * static_cast<double>(report.includes_resolved) / report.includes_found
|
||||
: 100.0;
|
||||
LOG_INFO(
|
||||
"Dependency scan: {}ms, {} files ({} source + {} header), " "{} edges, {}/{} resolved ({:.1f}%), {} waves",
|
||||
report.elapsed_ms,
|
||||
report.total_files,
|
||||
report.source_files,
|
||||
report.header_files,
|
||||
report.total_edges,
|
||||
report.includes_resolved,
|
||||
report.includes_found,
|
||||
accuracy,
|
||||
report.waves);
|
||||
if(unresolved > 0)
|
||||
LOG_WARN("{} unresolved includes", unresolved);
|
||||
|
||||
workspace.build_module_map();
|
||||
indexer.load(cfg.index_dir);
|
||||
|
||||
if(*cfg.enable_indexing) {
|
||||
for(auto& entry: workspace.cdb.get_entries()) {
|
||||
auto file = workspace.cdb.resolve_path(entry.file);
|
||||
auto server_id = workspace.path_pool.intern(file);
|
||||
indexer.enqueue(server_id);
|
||||
}
|
||||
indexer.schedule();
|
||||
}
|
||||
|
||||
compiler.init_compile_graph();
|
||||
}
|
||||
|
||||
void MasterServer::register_handlers() {
|
||||
using StringVec = std::vector<std::string>;
|
||||
|
||||
peer.on_request([this](RequestContext& ctx, const protocol::InitializeParams& params)
|
||||
-> RequestResult<protocol::InitializeParams> {
|
||||
if(lifecycle != ServerLifecycle::Uninitialized) {
|
||||
auto& srv = this->server;
|
||||
if(srv.lifecycle != ServerLifecycle::Uninitialized) {
|
||||
co_return kota::outcome_error(protocol::Error{"Server already initialized"});
|
||||
}
|
||||
|
||||
auto& init = params.lsp__initialize_params;
|
||||
if(init.root_uri.has_value()) {
|
||||
workspace_root = uri_to_path(*init.root_uri);
|
||||
srv.workspace_root = uri_to_path(*init.root_uri);
|
||||
}
|
||||
|
||||
// Capture initializationOptions as raw JSON for config loading.
|
||||
if(init.initialization_options.has_value()) {
|
||||
auto json =
|
||||
kota::codec::json::to_json<kota::ipc::lsp_config>(*init.initialization_options);
|
||||
if(json)
|
||||
init_options_json = std::move(*json);
|
||||
srv.init_options_json = std::move(*json);
|
||||
}
|
||||
|
||||
lifecycle = ServerLifecycle::Initialized;
|
||||
LOG_INFO("Initialized with workspace: {}", workspace_root);
|
||||
srv.lifecycle = ServerLifecycle::Initialized;
|
||||
LOG_INFO("Initialized with workspace: {}", srv.workspace_root);
|
||||
|
||||
protocol::InitializeResult result;
|
||||
auto& caps = result.capabilities;
|
||||
@@ -222,7 +85,6 @@ void MasterServer::register_handlers() {
|
||||
caps.signature_help_provider = protocol::SignatureHelpOptions{
|
||||
.trigger_characters = StringVec{"(", ")", "{", "}", "<", ">", ","},
|
||||
};
|
||||
/// FIXME: In the future, we would support work done progress.
|
||||
caps.declaration_provider = protocol::DeclarationOptions{
|
||||
.work_done_progress = false,
|
||||
};
|
||||
@@ -246,6 +108,8 @@ void MasterServer::register_handlers() {
|
||||
caps.call_hierarchy_provider = true;
|
||||
caps.type_hierarchy_provider = true;
|
||||
caps.workspace_symbol_provider = true;
|
||||
caps.document_formatting_provider = true;
|
||||
caps.document_range_formatting_provider = true;
|
||||
|
||||
protocol::SemanticTokensOptions sem_opts;
|
||||
{
|
||||
@@ -277,100 +141,32 @@ void MasterServer::register_handlers() {
|
||||
co_return result;
|
||||
});
|
||||
|
||||
peer.on_notification([this](const protocol::InitializedParams& params) {
|
||||
// Config priority: initializationOptions > clice.toml > defaults.
|
||||
// Load the workspace config (with defaults applied) first, then overlay
|
||||
// any initializationOptions on top so fields not mentioned in the JSON
|
||||
// keep the values from clice.toml — kotatsu's deserializer only touches
|
||||
// fields that are present in the input.
|
||||
workspace.config = Config::load_from_workspace(workspace_root);
|
||||
if(!init_options_json.empty()) {
|
||||
if(auto ov = kota::codec::json::parse(init_options_json, workspace.config); !ov) {
|
||||
LOG_WARN("Failed to apply initializationOptions: {}", ov.error().to_string());
|
||||
} else {
|
||||
// Re-run apply_defaults so overridden strings get workspace
|
||||
// substitution and `compiled_rules` is rebuilt if `rules`
|
||||
// changed. Defaults are gated on zero/empty sentinels, so
|
||||
// existing values from the overlay are preserved.
|
||||
workspace.config.apply_defaults(workspace_root);
|
||||
LOG_INFO("Applied initializationOptions overlay");
|
||||
}
|
||||
init_options_json.clear();
|
||||
}
|
||||
|
||||
auto& cfg = workspace.config.project;
|
||||
|
||||
if(!cfg.logging_dir.empty()) {
|
||||
auto now = std::chrono::system_clock::now();
|
||||
auto pid = llvm::sys::Process::getProcessId();
|
||||
auto session_dir =
|
||||
path::join(cfg.logging_dir, std::format("{:%Y-%m-%d_%H-%M-%S}_{}", now, pid));
|
||||
logging::file_logger("master", session_dir, logging::options);
|
||||
session_log_dir = session_dir;
|
||||
}
|
||||
|
||||
LOG_INFO("Server ready (stateful={}, stateless={}, idle={}ms)",
|
||||
cfg.stateful_worker_count.value,
|
||||
cfg.stateless_worker_count.value,
|
||||
*cfg.idle_timeout_ms);
|
||||
|
||||
WorkerPoolOptions pool_opts;
|
||||
pool_opts.self_path = self_path;
|
||||
pool_opts.stateful_count = cfg.stateful_worker_count;
|
||||
pool_opts.stateless_count = cfg.stateless_worker_count;
|
||||
pool_opts.worker_memory_limit = cfg.worker_memory_limit;
|
||||
pool_opts.log_dir = session_log_dir;
|
||||
if(!pool.start(pool_opts)) {
|
||||
LOG_ERROR("Failed to start worker pool");
|
||||
return;
|
||||
}
|
||||
|
||||
lifecycle = ServerLifecycle::Ready;
|
||||
|
||||
compiler.on_indexing_needed = [this]() {
|
||||
indexer.schedule();
|
||||
};
|
||||
|
||||
loop.schedule(load_workspace());
|
||||
peer.on_notification([this]([[maybe_unused]] const protocol::InitializedParams& params) {
|
||||
this->server.initialize();
|
||||
});
|
||||
|
||||
peer.on_request(
|
||||
[this](RequestContext& ctx,
|
||||
const protocol::ShutdownParams& params) -> RequestResult<protocol::ShutdownParams> {
|
||||
lifecycle = ServerLifecycle::ShuttingDown;
|
||||
this->server.lifecycle = ServerLifecycle::ShuttingDown;
|
||||
LOG_INFO("Shutdown requested");
|
||||
co_return nullptr;
|
||||
});
|
||||
|
||||
peer.on_notification([this](const protocol::ExitParams& params) {
|
||||
lifecycle = ServerLifecycle::Exited;
|
||||
peer.on_notification([this]([[maybe_unused]] const protocol::ExitParams& params) {
|
||||
LOG_INFO("Exit notification received");
|
||||
|
||||
indexer.save(workspace.config.project.index_dir);
|
||||
workspace.save_cache();
|
||||
|
||||
loop.schedule([this]() -> kota::task<> {
|
||||
co_await pool.stop();
|
||||
loop.stop();
|
||||
}());
|
||||
this->server.schedule_shutdown();
|
||||
});
|
||||
|
||||
/// Document lifecycle — handled directly by MasterServer.
|
||||
|
||||
peer.on_notification([this](const protocol::DidOpenTextDocumentParams& params) {
|
||||
if(lifecycle != ServerLifecycle::Ready)
|
||||
auto& srv = this->server;
|
||||
if(srv.lifecycle != ServerLifecycle::Ready)
|
||||
return;
|
||||
|
||||
auto path = uri_to_path(params.text_document.uri);
|
||||
auto path_id = workspace.path_pool.intern(path);
|
||||
auto path_id = srv.workspace.path_pool.intern(path);
|
||||
|
||||
auto [it, inserted] = sessions.try_emplace(path_id);
|
||||
auto& session = it->second;
|
||||
if(!inserted) {
|
||||
// DenseMap tombstone may retain stale data — reset to a fresh Session.
|
||||
session = Session{};
|
||||
}
|
||||
session.path_id = path_id;
|
||||
auto& session = srv.open_session(path_id);
|
||||
session.version = params.text_document.version;
|
||||
session.text = params.text_document.text;
|
||||
session.generation++;
|
||||
@@ -379,18 +175,18 @@ void MasterServer::register_handlers() {
|
||||
});
|
||||
|
||||
peer.on_notification([this](const protocol::DidChangeTextDocumentParams& params) {
|
||||
if(lifecycle != ServerLifecycle::Ready)
|
||||
auto& srv = this->server;
|
||||
if(srv.lifecycle != ServerLifecycle::Ready)
|
||||
return;
|
||||
|
||||
auto path = uri_to_path(params.text_document.uri);
|
||||
auto path_id = workspace.path_pool.intern(path);
|
||||
auto path_id = srv.workspace.path_pool.intern(path);
|
||||
|
||||
auto it = sessions.find(path_id);
|
||||
if(it == sessions.end())
|
||||
auto* session = srv.find_session(path_id);
|
||||
if(!session)
|
||||
return;
|
||||
|
||||
auto& session = it->second;
|
||||
session.version = params.text_document.version;
|
||||
session->version = params.text_document.version;
|
||||
|
||||
for(auto& change: params.content_changes) {
|
||||
std::visit(
|
||||
@@ -398,186 +194,157 @@ void MasterServer::register_handlers() {
|
||||
using T = std::remove_cvref_t<decltype(c)>;
|
||||
if constexpr(std::is_same_v<T,
|
||||
protocol::TextDocumentContentChangeWholeDocument>) {
|
||||
session.text = c.text;
|
||||
session->text = c.text;
|
||||
} else {
|
||||
auto& range = c.range;
|
||||
lsp::PositionMapper mapper(session.text, lsp::PositionEncoding::UTF16);
|
||||
lsp::PositionMapper mapper(session->text, lsp::PositionEncoding::UTF16);
|
||||
auto start = mapper.to_offset(range.start);
|
||||
auto end = mapper.to_offset(range.end);
|
||||
if(start && end && *start <= *end) {
|
||||
session.text.replace(*start, *end - *start, c.text);
|
||||
session->text.replace(*start, *end - *start, c.text);
|
||||
}
|
||||
}
|
||||
},
|
||||
change);
|
||||
}
|
||||
|
||||
session.generation++;
|
||||
session.ast_dirty = true;
|
||||
session->generation++;
|
||||
session->ast_dirty = true;
|
||||
|
||||
LOG_DEBUG("didChange: path={} version={} gen={}",
|
||||
path,
|
||||
session.version,
|
||||
session.generation);
|
||||
session->version,
|
||||
session->generation);
|
||||
|
||||
worker::DocumentUpdateParams update;
|
||||
update.path = path;
|
||||
update.version = session.version;
|
||||
pool.notify_stateful(path_id, update);
|
||||
update.version = session->version;
|
||||
srv.pool.notify_stateful(path_id, update);
|
||||
});
|
||||
|
||||
peer.on_notification([this](const protocol::DidCloseTextDocumentParams& params) {
|
||||
if(lifecycle != ServerLifecycle::Ready)
|
||||
auto& srv = this->server;
|
||||
if(srv.lifecycle != ServerLifecycle::Ready)
|
||||
return;
|
||||
|
||||
auto path = uri_to_path(params.text_document.uri);
|
||||
auto path_id = workspace.path_pool.intern(path);
|
||||
|
||||
workspace.on_file_closed(path_id);
|
||||
pool.notify_stateful(path_id, worker::EvictParams{path});
|
||||
|
||||
// Clear diagnostics for the closed file.
|
||||
protocol::PublishDiagnosticsParams diag_params;
|
||||
diag_params.uri = params.text_document.uri;
|
||||
peer.send_notification(diag_params);
|
||||
|
||||
sessions.erase(path_id);
|
||||
|
||||
indexer.enqueue(path_id);
|
||||
indexer.schedule();
|
||||
|
||||
LOG_DEBUG("didClose: {}", path);
|
||||
auto path_id = srv.workspace.path_pool.intern(uri_to_path(params.text_document.uri));
|
||||
srv.close_session(path_id, this->peer);
|
||||
});
|
||||
|
||||
peer.on_notification([this](const protocol::DidSaveTextDocumentParams& params) {
|
||||
if(lifecycle != ServerLifecycle::Ready)
|
||||
auto& srv = this->server;
|
||||
if(srv.lifecycle != ServerLifecycle::Ready)
|
||||
return;
|
||||
|
||||
auto path = uri_to_path(params.text_document.uri);
|
||||
auto path_id = workspace.path_pool.intern(path);
|
||||
|
||||
auto dirtied = workspace.on_file_saved(path_id);
|
||||
for(auto dirty_id: dirtied) {
|
||||
if(auto sit = sessions.find(dirty_id); sit != sessions.end()) {
|
||||
sit->second.ast_dirty = true;
|
||||
} else {
|
||||
indexer.enqueue(dirty_id);
|
||||
}
|
||||
}
|
||||
|
||||
// Invalidate header contexts for sessions whose host is this file.
|
||||
for(auto& [hdr_id, session]: sessions) {
|
||||
if(session.header_context && session.header_context->host_path_id == path_id) {
|
||||
session.header_context.reset();
|
||||
session.ast_dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
indexer.schedule();
|
||||
auto path_id = srv.workspace.path_pool.intern(path);
|
||||
srv.on_file_saved(path_id);
|
||||
|
||||
LOG_DEBUG("didSave: {}", path);
|
||||
});
|
||||
|
||||
/// Feature requests — stateful forwarding.
|
||||
|
||||
peer.on_request([this](RequestContext& ctx, const protocol::HoverParams& params) -> RawResult {
|
||||
auto& srv = this->server;
|
||||
auto path = uri_to_path(params.text_document_position_params.text_document.uri);
|
||||
auto path_id = workspace.path_pool.intern(path);
|
||||
auto sit = sessions.find(path_id);
|
||||
if(sit == sessions.end())
|
||||
auto path_id = srv.workspace.path_pool.intern(path);
|
||||
auto* session = srv.find_session(path_id);
|
||||
if(!session)
|
||||
co_return serde_raw{"null"};
|
||||
co_return co_await compiler.forward_query(worker::QueryKind::Hover,
|
||||
sit->second,
|
||||
params.text_document_position_params.position);
|
||||
co_return co_await srv.compiler.forward_query(
|
||||
worker::QueryKind::Hover,
|
||||
*session,
|
||||
params.text_document_position_params.position);
|
||||
});
|
||||
|
||||
peer.on_request([this](RequestContext& ctx,
|
||||
const protocol::SemanticTokensParams& params) -> RawResult {
|
||||
auto& srv = this->server;
|
||||
auto path = uri_to_path(params.text_document.uri);
|
||||
auto path_id = workspace.path_pool.intern(path);
|
||||
auto sit = sessions.find(path_id);
|
||||
if(sit == sessions.end())
|
||||
auto path_id = srv.workspace.path_pool.intern(path);
|
||||
auto* session = srv.find_session(path_id);
|
||||
if(!session)
|
||||
co_return serde_raw{"null"};
|
||||
co_return co_await compiler.forward_query(worker::QueryKind::SemanticTokens, sit->second);
|
||||
co_return co_await srv.compiler.forward_query(worker::QueryKind::SemanticTokens, *session);
|
||||
});
|
||||
|
||||
peer.on_request(
|
||||
[this](RequestContext& ctx, const protocol::InlayHintParams& params) -> RawResult {
|
||||
auto& srv = this->server;
|
||||
auto path = uri_to_path(params.text_document.uri);
|
||||
auto path_id = workspace.path_pool.intern(path);
|
||||
auto sit = sessions.find(path_id);
|
||||
if(sit == sessions.end())
|
||||
auto path_id = srv.workspace.path_pool.intern(path);
|
||||
auto* session = srv.find_session(path_id);
|
||||
if(!session)
|
||||
co_return serde_raw{"null"};
|
||||
co_return co_await compiler.forward_query(worker::QueryKind::InlayHints,
|
||||
sit->second,
|
||||
{},
|
||||
params.range);
|
||||
co_return co_await srv.compiler.forward_query(worker::QueryKind::InlayHints,
|
||||
*session,
|
||||
{},
|
||||
params.range);
|
||||
});
|
||||
|
||||
peer.on_request(
|
||||
[this](RequestContext& ctx, const protocol::FoldingRangeParams& params) -> RawResult {
|
||||
auto path = uri_to_path(params.text_document.uri);
|
||||
auto path_id = workspace.path_pool.intern(path);
|
||||
auto sit = sessions.find(path_id);
|
||||
if(sit == sessions.end())
|
||||
co_return serde_raw{"null"};
|
||||
co_return co_await compiler.forward_query(worker::QueryKind::FoldingRange, sit->second);
|
||||
});
|
||||
peer.on_request([this](RequestContext& ctx,
|
||||
const protocol::FoldingRangeParams& params) -> RawResult {
|
||||
auto& srv = this->server;
|
||||
auto path = uri_to_path(params.text_document.uri);
|
||||
auto path_id = srv.workspace.path_pool.intern(path);
|
||||
auto* session = srv.find_session(path_id);
|
||||
if(!session)
|
||||
co_return serde_raw{"null"};
|
||||
co_return co_await srv.compiler.forward_query(worker::QueryKind::FoldingRange, *session);
|
||||
});
|
||||
|
||||
peer.on_request([this](RequestContext& ctx,
|
||||
const protocol::DocumentSymbolParams& params) -> RawResult {
|
||||
auto& srv = this->server;
|
||||
auto path = uri_to_path(params.text_document.uri);
|
||||
auto path_id = workspace.path_pool.intern(path);
|
||||
auto sit = sessions.find(path_id);
|
||||
if(sit == sessions.end())
|
||||
auto path_id = srv.workspace.path_pool.intern(path);
|
||||
auto* session = srv.find_session(path_id);
|
||||
if(!session)
|
||||
co_return serde_raw{"null"};
|
||||
co_return co_await compiler.forward_query(worker::QueryKind::DocumentSymbol, sit->second);
|
||||
});
|
||||
|
||||
peer.on_request([this](RequestContext& ctx,
|
||||
const protocol::DocumentLinkParams& params) -> RawResult {
|
||||
auto path = uri_to_path(params.text_document.uri);
|
||||
auto path_id = workspace.path_pool.intern(path);
|
||||
auto sit = sessions.find(path_id);
|
||||
if(sit == sessions.end())
|
||||
co_return serde_raw{"null"};
|
||||
auto& session = sit->second;
|
||||
auto result = co_await compiler.forward_query(worker::QueryKind::DocumentLink, session);
|
||||
if(!result.has_value())
|
||||
co_return serde_raw{"null"};
|
||||
// Merge document links from PCH if available.
|
||||
auto& links = result.value();
|
||||
// Re-lookup session after co_await since iterators may be invalidated.
|
||||
auto sit2 = sessions.find(path_id);
|
||||
if(sit2 != sessions.end() && sit2->second.pch_ref) {
|
||||
auto pch_it = workspace.pch_cache.find(sit2->second.pch_ref->path_id);
|
||||
if(pch_it != workspace.pch_cache.end() && !pch_it->second.document_links_json.empty()) {
|
||||
auto& pch_json = pch_it->second.document_links_json;
|
||||
// Merge two JSON arrays.
|
||||
if(!links.data.empty() && links.data != "null" && links.data.size() > 2) {
|
||||
// "[a,b]" + "[c,d]" -> "[a,b,c,d]"
|
||||
links.data.pop_back(); // remove trailing ']'
|
||||
links.data += ',';
|
||||
links.data.append(pch_json.begin() + 1, pch_json.end()); // skip '['
|
||||
} else {
|
||||
links.data = pch_json;
|
||||
}
|
||||
}
|
||||
}
|
||||
co_return std::move(links);
|
||||
co_return co_await srv.compiler.forward_query(worker::QueryKind::DocumentSymbol, *session);
|
||||
});
|
||||
|
||||
peer.on_request(
|
||||
[this](RequestContext& ctx, const protocol::CodeActionParams& params) -> RawResult {
|
||||
[this](RequestContext& ctx, const protocol::DocumentLinkParams& params) -> RawResult {
|
||||
auto& srv = this->server;
|
||||
auto path = uri_to_path(params.text_document.uri);
|
||||
auto path_id = workspace.path_pool.intern(path);
|
||||
auto sit = sessions.find(path_id);
|
||||
if(sit == sessions.end())
|
||||
auto path_id = srv.workspace.path_pool.intern(path);
|
||||
auto* session = srv.find_session(path_id);
|
||||
if(!session)
|
||||
co_return serde_raw{"null"};
|
||||
co_return co_await compiler.forward_query(worker::QueryKind::CodeAction, sit->second);
|
||||
auto result =
|
||||
co_await srv.compiler.forward_query(worker::QueryKind::DocumentLink, *session);
|
||||
if(!result.has_value())
|
||||
co_return serde_raw{"null"};
|
||||
auto& links = result.value();
|
||||
auto* session2 = srv.find_session(path_id);
|
||||
if(session2 && session2->pch_ref) {
|
||||
auto& pch_cache = srv.workspace.pch_cache;
|
||||
auto pch_it = pch_cache.find(session2->pch_ref->path_id);
|
||||
if(pch_it != pch_cache.end() && !pch_it->second.document_links_json.empty()) {
|
||||
auto& pch_json = pch_it->second.document_links_json;
|
||||
if(!links.data.empty() && links.data != "null" && links.data.size() > 2) {
|
||||
links.data.pop_back();
|
||||
links.data += ',';
|
||||
links.data.append(pch_json.begin() + 1, pch_json.end());
|
||||
} else {
|
||||
links.data = pch_json;
|
||||
}
|
||||
}
|
||||
}
|
||||
co_return std::move(links);
|
||||
});
|
||||
|
||||
peer.on_request(
|
||||
[this](RequestContext& ctx, const protocol::CodeActionParams& params) -> RawResult {
|
||||
auto& srv = this->server;
|
||||
auto path = uri_to_path(params.text_document.uri);
|
||||
auto path_id = srv.workspace.path_pool.intern(path);
|
||||
auto* session = srv.find_session(path_id);
|
||||
if(!session)
|
||||
co_return serde_raw{"null"};
|
||||
co_return co_await srv.compiler.forward_query(worker::QueryKind::CodeAction, *session);
|
||||
});
|
||||
|
||||
/// Helper: resolve URI to path, path_id, and Session pointer.
|
||||
auto resolve_uri = [this](const std::string& uri) {
|
||||
struct Result {
|
||||
std::string path;
|
||||
@@ -585,22 +352,21 @@ void MasterServer::register_handlers() {
|
||||
Session* session;
|
||||
};
|
||||
auto path = uri_to_path(uri);
|
||||
auto path_id = workspace.path_pool.intern(path);
|
||||
auto sit = sessions.find(path_id);
|
||||
Session* session = (sit != sessions.end()) ? &sit->second : nullptr;
|
||||
auto path_id = this->server.workspace.path_pool.intern(path);
|
||||
auto* session = this->server.find_session(path_id);
|
||||
return Result{std::move(path), path_id, session};
|
||||
};
|
||||
|
||||
auto lookup_at = [this, resolve_uri](const std::string& uri, const protocol::Position& pos) {
|
||||
auto [path, path_id, session] = resolve_uri(uri);
|
||||
return indexer.lookup_symbol(uri, path, pos, session);
|
||||
return this->server.indexer.lookup_symbol(uri, path, pos, session);
|
||||
};
|
||||
|
||||
auto query_at = [this, resolve_uri](const std::string& uri,
|
||||
const protocol::Position& pos,
|
||||
RelationKind kind) -> std::vector<protocol::Location> {
|
||||
auto [path, path_id, session] = resolve_uri(uri);
|
||||
return indexer.query_relations(path, pos, kind, session);
|
||||
return this->server.indexer.query_relations(path, pos, kind, session);
|
||||
};
|
||||
|
||||
auto resolve_item =
|
||||
@@ -609,11 +375,9 @@ void MasterServer::register_handlers() {
|
||||
const protocol::Range& range,
|
||||
const std::optional<protocol::LSPAny>& data) -> std::optional<SymbolInfo> {
|
||||
auto [path, path_id, session] = resolve_uri(uri);
|
||||
return indexer.resolve_hierarchy_item(uri, path, range, data, session);
|
||||
return this->server.indexer.resolve_hierarchy_item(uri, path, range, data, session);
|
||||
};
|
||||
|
||||
/// Feature requests — index-based with AST fallback.
|
||||
|
||||
peer.on_request([this, query_at](RequestContext& ctx,
|
||||
const protocol::DefinitionParams& params) -> RawResult {
|
||||
auto& uri = params.text_document_position_params.text_document.uri;
|
||||
@@ -624,14 +388,15 @@ void MasterServer::register_handlers() {
|
||||
co_return to_raw(result);
|
||||
}
|
||||
|
||||
auto& srv = this->server;
|
||||
auto path = uri_to_path(uri);
|
||||
auto path_id = workspace.path_pool.intern(path);
|
||||
auto sit = sessions.find(path_id);
|
||||
if(sit == sessions.end())
|
||||
auto path_id = srv.workspace.path_pool.intern(path);
|
||||
auto* session = srv.find_session(path_id);
|
||||
if(!session)
|
||||
co_return serde_raw{"null"};
|
||||
co_return co_await compiler.forward_query(worker::QueryKind::GoToDefinition,
|
||||
sit->second,
|
||||
pos);
|
||||
co_return co_await srv.compiler.forward_query(worker::QueryKind::GoToDefinition,
|
||||
*session,
|
||||
pos);
|
||||
});
|
||||
|
||||
peer.on_request([this, query_at](RequestContext& ctx,
|
||||
@@ -668,32 +433,60 @@ void MasterServer::register_handlers() {
|
||||
co_return serde_raw{"null"};
|
||||
});
|
||||
|
||||
/// Feature requests — stateless forwarding.
|
||||
|
||||
peer.on_request([this](RequestContext& ctx,
|
||||
const protocol::CompletionParams& params) -> RawResult {
|
||||
auto& srv = this->server;
|
||||
auto path = uri_to_path(params.text_document_position_params.text_document.uri);
|
||||
auto path_id = workspace.path_pool.intern(path);
|
||||
auto sit = sessions.find(path_id);
|
||||
if(sit == sessions.end())
|
||||
auto path_id = srv.workspace.path_pool.intern(path);
|
||||
auto* session = srv.find_session(path_id);
|
||||
if(!session)
|
||||
co_return serde_raw{"null"};
|
||||
co_return co_await compiler.handle_completion(params.text_document_position_params.position,
|
||||
sit->second);
|
||||
auto pause = srv.indexer.scoped_pause();
|
||||
auto result =
|
||||
co_await srv.compiler.handle_completion(params.text_document_position_params.position,
|
||||
*session);
|
||||
co_return std::move(result);
|
||||
});
|
||||
|
||||
peer.on_request(
|
||||
[this](RequestContext& ctx, const protocol::SignatureHelpParams& params) -> RawResult {
|
||||
auto& srv = this->server;
|
||||
auto path = uri_to_path(params.text_document_position_params.text_document.uri);
|
||||
auto path_id = workspace.path_pool.intern(path);
|
||||
auto sit = sessions.find(path_id);
|
||||
if(sit == sessions.end())
|
||||
auto path_id = srv.workspace.path_pool.intern(path);
|
||||
auto* session = srv.find_session(path_id);
|
||||
if(!session)
|
||||
co_return serde_raw{"null"};
|
||||
co_return co_await compiler.forward_build(worker::BuildKind::SignatureHelp,
|
||||
params.text_document_position_params.position,
|
||||
sit->second);
|
||||
auto pause = srv.indexer.scoped_pause();
|
||||
auto result =
|
||||
co_await srv.compiler.forward_build(worker::BuildKind::SignatureHelp,
|
||||
params.text_document_position_params.position,
|
||||
*session);
|
||||
co_return std::move(result);
|
||||
});
|
||||
|
||||
/// Hierarchy queries — index-based.
|
||||
peer.on_request(
|
||||
[this](RequestContext& ctx, const protocol::DocumentFormattingParams& params) -> RawResult {
|
||||
auto& srv = this->server;
|
||||
auto path = uri_to_path(params.text_document.uri);
|
||||
auto path_id = srv.workspace.path_pool.intern(path);
|
||||
auto* session = srv.find_session(path_id);
|
||||
if(!session)
|
||||
co_return serde_raw{"null"};
|
||||
auto pause = srv.indexer.scoped_pause();
|
||||
co_return co_await srv.compiler.forward_format(*session);
|
||||
});
|
||||
|
||||
peer.on_request([this](RequestContext& ctx,
|
||||
const protocol::DocumentRangeFormattingParams& params) -> RawResult {
|
||||
auto& srv = this->server;
|
||||
auto path = uri_to_path(params.text_document.uri);
|
||||
auto path_id = srv.workspace.path_pool.intern(path);
|
||||
auto* session = srv.find_session(path_id);
|
||||
if(!session)
|
||||
co_return serde_raw{"null"};
|
||||
auto pause = srv.indexer.scoped_pause();
|
||||
co_return co_await srv.compiler.forward_format(*session, params.range);
|
||||
});
|
||||
|
||||
peer.on_request(
|
||||
[this, lookup_at](RequestContext& ctx,
|
||||
@@ -718,7 +511,7 @@ void MasterServer::register_handlers() {
|
||||
auto info = resolve_item(params.item.uri, params.item.range, params.item.data);
|
||||
if(!info)
|
||||
co_return serde_raw{"null"};
|
||||
auto results = indexer.find_incoming_calls(info->hash);
|
||||
auto results = this->server.indexer.find_incoming_calls(info->hash);
|
||||
if(results.empty())
|
||||
co_return serde_raw{"null"};
|
||||
co_return to_raw(results);
|
||||
@@ -730,7 +523,7 @@ void MasterServer::register_handlers() {
|
||||
auto info = resolve_item(params.item.uri, params.item.range, params.item.data);
|
||||
if(!info)
|
||||
co_return serde_raw{"null"};
|
||||
auto results = indexer.find_outgoing_calls(info->hash);
|
||||
auto results = this->server.indexer.find_outgoing_calls(info->hash);
|
||||
if(results.empty())
|
||||
co_return serde_raw{"null"};
|
||||
co_return to_raw(results);
|
||||
@@ -760,7 +553,7 @@ void MasterServer::register_handlers() {
|
||||
auto info = resolve_item(params.item.uri, params.item.range, params.item.data);
|
||||
if(!info)
|
||||
co_return serde_raw{"null"};
|
||||
auto results = indexer.find_supertypes(info->hash);
|
||||
auto results = this->server.indexer.find_supertypes(info->hash);
|
||||
if(results.empty())
|
||||
co_return serde_raw{"null"};
|
||||
co_return to_raw(results);
|
||||
@@ -772,7 +565,7 @@ void MasterServer::register_handlers() {
|
||||
auto info = resolve_item(params.item.uri, params.item.range, params.item.data);
|
||||
if(!info)
|
||||
co_return serde_raw{"null"};
|
||||
auto results = indexer.find_subtypes(info->hash);
|
||||
auto results = this->server.indexer.find_subtypes(info->hash);
|
||||
if(results.empty())
|
||||
co_return serde_raw{"null"};
|
||||
co_return to_raw(results);
|
||||
@@ -780,29 +573,29 @@ void MasterServer::register_handlers() {
|
||||
|
||||
peer.on_request(
|
||||
[this](RequestContext& ctx, const protocol::WorkspaceSymbolParams& params) -> RawResult {
|
||||
auto results = indexer.search_symbols(params.query);
|
||||
auto results = this->server.indexer.search_symbols(params.query);
|
||||
if(results.empty())
|
||||
co_return serde_raw{"null"};
|
||||
co_return to_raw(results);
|
||||
});
|
||||
|
||||
/// clice/ extension commands.
|
||||
|
||||
peer.on_request(
|
||||
"clice/queryContext",
|
||||
[this](RequestContext& ctx, const ext::QueryContextParams& params) -> RawResult {
|
||||
auto& srv = this->server;
|
||||
auto path = uri_to_path(params.uri);
|
||||
auto path_id = workspace.path_pool.intern(path);
|
||||
auto path_id = srv.workspace.path_pool.intern(path);
|
||||
int offset_val = std::max(0, params.offset.value_or(0));
|
||||
constexpr int page_size = 10;
|
||||
|
||||
ext::QueryContextResult result;
|
||||
std::vector<ext::ContextItem> all_items;
|
||||
|
||||
auto hosts = workspace.dep_graph.find_host_sources(path_id);
|
||||
auto& ws = srv.workspace;
|
||||
auto hosts = ws.dep_graph.find_host_sources(path_id);
|
||||
for(auto host_id: hosts) {
|
||||
auto host_path = workspace.path_pool.resolve(host_id);
|
||||
auto host_cdb = workspace.cdb.lookup(host_path, {.suppress_logging = true});
|
||||
auto host_path = ws.path_pool.resolve(host_id);
|
||||
auto host_cdb = ws.cdb.lookup(host_path, {.suppress_logging = true});
|
||||
if(host_cdb.empty())
|
||||
continue;
|
||||
auto host_uri_opt = lsp::URI::from_file_path(std::string(host_path));
|
||||
@@ -816,7 +609,7 @@ void MasterServer::register_handlers() {
|
||||
}
|
||||
|
||||
if(hosts.empty()) {
|
||||
auto entries = workspace.cdb.lookup(path, {.suppress_logging = true});
|
||||
auto entries = ws.cdb.lookup(path, {.suppress_logging = true});
|
||||
for(std::size_t i = 0; i < entries.size(); ++i) {
|
||||
auto& cmd = entries[i];
|
||||
auto argv = cmd.to_argv();
|
||||
@@ -858,13 +651,14 @@ void MasterServer::register_handlers() {
|
||||
peer.on_request(
|
||||
"clice/currentContext",
|
||||
[this](RequestContext& ctx, const ext::CurrentContextParams& params) -> RawResult {
|
||||
auto& srv = this->server;
|
||||
auto path = uri_to_path(params.uri);
|
||||
auto path_id = workspace.path_pool.intern(path);
|
||||
auto path_id = srv.workspace.path_pool.intern(path);
|
||||
|
||||
ext::CurrentContextResult result;
|
||||
auto sit = sessions.find(path_id);
|
||||
if(sit != sessions.end() && sit->second.active_context) {
|
||||
auto ctx_path = workspace.path_pool.resolve(*sit->second.active_context);
|
||||
auto* session = srv.find_session(path_id);
|
||||
if(session && session->active_context) {
|
||||
auto ctx_path = srv.workspace.path_pool.resolve(*session->active_context);
|
||||
auto ctx_uri_opt = lsp::URI::from_file_path(std::string(ctx_path));
|
||||
if(ctx_uri_opt) {
|
||||
ext::ContextItem item;
|
||||
@@ -880,34 +674,41 @@ void MasterServer::register_handlers() {
|
||||
peer.on_request(
|
||||
"clice/switchContext",
|
||||
[this](RequestContext& ctx, const ext::SwitchContextParams& params) -> RawResult {
|
||||
auto& srv = this->server;
|
||||
auto path = uri_to_path(params.uri);
|
||||
auto path_id = workspace.path_pool.intern(path);
|
||||
auto path_id = srv.workspace.path_pool.intern(path);
|
||||
auto context_path = uri_to_path(params.context_uri);
|
||||
auto context_path_id = workspace.path_pool.intern(context_path);
|
||||
auto context_path_id = srv.workspace.path_pool.intern(context_path);
|
||||
|
||||
ext::SwitchContextResult result;
|
||||
|
||||
auto context_cdb = workspace.cdb.lookup(context_path, {.suppress_logging = true});
|
||||
auto& ws = srv.workspace;
|
||||
auto context_cdb = ws.cdb.lookup(context_path, {.suppress_logging = true});
|
||||
if(context_cdb.empty()) {
|
||||
result.success = false;
|
||||
co_return to_raw(result);
|
||||
}
|
||||
|
||||
auto sit = sessions.find(path_id);
|
||||
if(sit == sessions.end()) {
|
||||
auto* session = srv.find_session(path_id);
|
||||
if(!session) {
|
||||
result.success = false;
|
||||
co_return to_raw(result);
|
||||
}
|
||||
|
||||
sit->second.active_context = context_path_id;
|
||||
sit->second.header_context.reset();
|
||||
sit->second.pch_ref.reset();
|
||||
sit->second.ast_deps.reset();
|
||||
sit->second.ast_dirty = true;
|
||||
session->active_context = context_path_id;
|
||||
session->header_context.reset();
|
||||
session->pch_ref.reset();
|
||||
session->ast_deps.reset();
|
||||
session->ast_dirty = true;
|
||||
|
||||
result.success = true;
|
||||
co_return to_raw(result);
|
||||
});
|
||||
}
|
||||
|
||||
LSPClient::~LSPClient() {
|
||||
server.compiler.set_peer(nullptr);
|
||||
server.indexer.set_peer(nullptr);
|
||||
}
|
||||
|
||||
} // namespace clice
|
||||
23
src/server/service/lsp_client.h
Normal file
23
src/server/service/lsp_client.h
Normal file
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include "kota/async/async.h"
|
||||
#include "kota/codec/json/json.h"
|
||||
#include "kota/ipc/codec/json.h"
|
||||
|
||||
namespace clice {
|
||||
|
||||
class MasterServer;
|
||||
|
||||
class LSPClient {
|
||||
public:
|
||||
LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer);
|
||||
~LSPClient();
|
||||
|
||||
private:
|
||||
using RawResult = kota::task<kota::codec::RawValue, kota::ipc::Error>;
|
||||
|
||||
MasterServer& server;
|
||||
kota::ipc::JsonPeer& peer;
|
||||
};
|
||||
|
||||
} // namespace clice
|
||||
551
src/server/service/master_server.cpp
Normal file
551
src/server/service/master_server.cpp
Normal file
@@ -0,0 +1,551 @@
|
||||
#include "server/service/master_server.h"
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstring>
|
||||
#include <list>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#ifndef _WIN32
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#include "server/protocol/worker.h"
|
||||
#include "server/service/agent_client.h"
|
||||
#include "server/service/lsp_client.h"
|
||||
#include "support/filesystem.h"
|
||||
#include "support/logging.h"
|
||||
|
||||
#include "kota/async/async.h"
|
||||
#include "kota/async/io/fs_event.h"
|
||||
#include "kota/codec/json/json.h"
|
||||
#include "kota/ipc/codec/json.h"
|
||||
#include "kota/ipc/lsp/protocol.h"
|
||||
#include "kota/ipc/lsp/uri.h"
|
||||
#include "kota/ipc/recording_transport.h"
|
||||
#include "kota/ipc/transport.h"
|
||||
#include "llvm/Support/FileSystem.h"
|
||||
#include "llvm/Support/Path.h"
|
||||
#include "llvm/Support/Process.h"
|
||||
|
||||
namespace clice {
|
||||
|
||||
namespace lsp = kota::ipc::lsp;
|
||||
namespace protocol = kota::ipc::protocol;
|
||||
|
||||
MasterServer::MasterServer(kota::event_loop& loop, std::string self_path) :
|
||||
loop(loop), pool(loop), compiler(loop, workspace, pool, sessions),
|
||||
indexer(loop,
|
||||
workspace,
|
||||
sessions,
|
||||
pool,
|
||||
compiler,
|
||||
[this](uint32_t proj_path_id) {
|
||||
auto path = workspace.project_index.path_pool.path(proj_path_id);
|
||||
auto server_id = workspace.path_pool.intern(path);
|
||||
return sessions.contains(server_id);
|
||||
}),
|
||||
self_path(std::move(self_path)) {}
|
||||
|
||||
MasterServer::~MasterServer() = default;
|
||||
|
||||
void MasterServer::initialize() {
|
||||
workspace.config = Config::load_from_workspace(workspace_root);
|
||||
if(!init_options_json.empty()) {
|
||||
if(auto ov = kota::codec::json::parse(init_options_json, workspace.config); !ov) {
|
||||
LOG_WARN("Failed to apply initializationOptions: {}", ov.error().to_string());
|
||||
} else {
|
||||
workspace.config.apply_defaults(workspace_root);
|
||||
LOG_INFO("Applied initializationOptions overlay");
|
||||
}
|
||||
init_options_json.clear();
|
||||
}
|
||||
|
||||
auto& cfg = workspace.config.project;
|
||||
|
||||
if(!cfg.logging_dir.empty()) {
|
||||
auto now = std::chrono::system_clock::now();
|
||||
auto pid = llvm::sys::Process::getProcessId();
|
||||
session_log_dir =
|
||||
path::join(cfg.logging_dir, std::format("{:%Y-%m-%d_%H-%M-%S}_{}", now, pid));
|
||||
logging::file_logger("master", session_log_dir, logging::options);
|
||||
}
|
||||
|
||||
LOG_INFO("Server ready (stateful={}, stateless={}, idle={}ms)",
|
||||
cfg.stateful_worker_count.value,
|
||||
cfg.stateless_worker_count.value,
|
||||
*cfg.idle_timeout_ms);
|
||||
|
||||
WorkerPoolOptions pool_opts;
|
||||
pool_opts.self_path = self_path;
|
||||
pool_opts.stateful_count = cfg.stateful_worker_count;
|
||||
pool_opts.stateless_count = cfg.stateless_worker_count;
|
||||
pool_opts.worker_memory_limit = cfg.worker_memory_limit;
|
||||
pool_opts.log_dir = session_log_dir;
|
||||
if(!pool.start(pool_opts)) {
|
||||
LOG_ERROR("Failed to start worker pool");
|
||||
return;
|
||||
}
|
||||
|
||||
lifecycle = ServerLifecycle::Ready;
|
||||
|
||||
compiler.on_indexing_needed = [this]() {
|
||||
indexer.schedule();
|
||||
};
|
||||
|
||||
indexer.set_max_concurrency(cfg.stateless_worker_count.value);
|
||||
|
||||
load_workspace();
|
||||
}
|
||||
|
||||
void MasterServer::initialize(llvm::StringRef root) {
|
||||
workspace_root = root.str();
|
||||
initialize();
|
||||
}
|
||||
|
||||
void MasterServer::start_file_watcher() {
|
||||
if(workspace_root.empty())
|
||||
return;
|
||||
|
||||
loop.schedule([this]() -> kota::task<> {
|
||||
auto watcher = kota::fs_event::create(workspace_root, {}, loop);
|
||||
if(!watcher) {
|
||||
LOG_WARN("Failed to start file watcher for {}", workspace_root);
|
||||
co_return;
|
||||
}
|
||||
|
||||
LOG_INFO("File watcher started for {}", workspace_root);
|
||||
|
||||
while(true) {
|
||||
auto changes = co_await watcher->next();
|
||||
if(!changes)
|
||||
break;
|
||||
|
||||
for(auto& change: *changes) {
|
||||
if(change.type != kota::fs_event::effect::modify &&
|
||||
change.type != kota::fs_event::effect::create)
|
||||
continue;
|
||||
|
||||
llvm::StringRef file(change.path);
|
||||
if(file.ends_with("compile_commands.json")) {
|
||||
LOG_INFO("CDB changed, reloading workspace");
|
||||
load_workspace();
|
||||
continue;
|
||||
}
|
||||
|
||||
if(file.ends_with(".cpp") || file.ends_with(".cc") || file.ends_with(".cxx") ||
|
||||
file.ends_with(".c") || file.ends_with(".h") || file.ends_with(".hpp") ||
|
||||
file.ends_with(".hxx") || file.ends_with(".cppm") || file.ends_with(".ixx")) {
|
||||
auto path_id = workspace.path_pool.intern(file);
|
||||
on_file_saved(path_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}());
|
||||
}
|
||||
|
||||
Session* MasterServer::find_session(std::uint32_t path_id) {
|
||||
auto it = sessions.find(path_id);
|
||||
return it != sessions.end() ? &it->second : nullptr;
|
||||
}
|
||||
|
||||
Session& MasterServer::open_session(std::uint32_t path_id) {
|
||||
auto [it, inserted] = sessions.try_emplace(path_id);
|
||||
auto& session = it->second;
|
||||
if(!inserted)
|
||||
session = Session{};
|
||||
session.path_id = path_id;
|
||||
return session;
|
||||
}
|
||||
|
||||
void MasterServer::close_session(std::uint32_t path_id, kota::ipc::JsonPeer& peer) {
|
||||
namespace protocol = kota::ipc::protocol;
|
||||
|
||||
auto path = workspace.path_pool.resolve(path_id);
|
||||
workspace.on_file_closed(path_id);
|
||||
pool.notify_stateful(path_id, worker::EvictParams{std::string(path)});
|
||||
|
||||
protocol::PublishDiagnosticsParams diag_params;
|
||||
auto uri = lsp::URI::from_file_path(std::string(path));
|
||||
if(uri)
|
||||
diag_params.uri = uri->str();
|
||||
diag_params.diagnostics = {};
|
||||
peer.send_notification(diag_params);
|
||||
|
||||
sessions.erase(path_id);
|
||||
|
||||
indexer.enqueue(path_id);
|
||||
indexer.schedule();
|
||||
|
||||
LOG_DEBUG("didClose: {}", path);
|
||||
}
|
||||
|
||||
void MasterServer::on_file_saved(std::uint32_t path_id) {
|
||||
auto dirtied = workspace.on_file_saved(path_id);
|
||||
for(auto dirty_id: dirtied) {
|
||||
if(auto* session = find_session(dirty_id)) {
|
||||
session->ast_dirty = true;
|
||||
} else {
|
||||
indexer.enqueue(dirty_id);
|
||||
}
|
||||
}
|
||||
|
||||
for(auto& [hdr_id, session]: sessions) {
|
||||
if(session.header_context && session.header_context->host_path_id == path_id) {
|
||||
session.header_context.reset();
|
||||
session.ast_dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
indexer.schedule();
|
||||
}
|
||||
|
||||
void MasterServer::schedule_shutdown() {
|
||||
if(lifecycle == ServerLifecycle::Exited)
|
||||
return;
|
||||
lifecycle = ServerLifecycle::Exited;
|
||||
|
||||
indexer.save(workspace.config.project.index_dir);
|
||||
workspace.save_cache();
|
||||
shutdown_event.set();
|
||||
|
||||
loop.schedule([this]() -> kota::task<> {
|
||||
co_await kota::when_all(indexer.stop(), compiler.stop(), pool.stop());
|
||||
loop.stop();
|
||||
}());
|
||||
}
|
||||
|
||||
void MasterServer::load_workspace() {
|
||||
if(workspace_root.empty())
|
||||
return;
|
||||
|
||||
auto& cfg = workspace.config.project;
|
||||
|
||||
if(!cfg.cache_dir.empty()) {
|
||||
auto ec = llvm::sys::fs::create_directories(cfg.cache_dir);
|
||||
if(ec) {
|
||||
LOG_WARN("Failed to create cache directory {}: {}",
|
||||
std::string_view(cfg.cache_dir),
|
||||
ec.message());
|
||||
} else {
|
||||
LOG_INFO("Cache directory: {}", std::string_view(cfg.cache_dir));
|
||||
}
|
||||
|
||||
for(auto* subdir: {"cache/pch", "cache/pcm"}) {
|
||||
auto dir = path::join(cfg.cache_dir, subdir);
|
||||
if(auto ec2 = llvm::sys::fs::create_directories(dir))
|
||||
LOG_WARN("Failed to create {}: {}", dir, ec2.message());
|
||||
}
|
||||
|
||||
workspace.cleanup_cache();
|
||||
workspace.load_cache();
|
||||
}
|
||||
|
||||
std::string cdb_path;
|
||||
for(auto& configured: cfg.compile_commands_paths) {
|
||||
if(llvm::sys::fs::is_directory(configured)) {
|
||||
auto candidate = path::join(configured, "compile_commands.json");
|
||||
if(llvm::sys::fs::exists(candidate)) {
|
||||
cdb_path = std::move(candidate);
|
||||
break;
|
||||
}
|
||||
} else if(llvm::sys::fs::exists(configured)) {
|
||||
cdb_path = configured;
|
||||
break;
|
||||
} else {
|
||||
LOG_WARN("Configured compile_commands_path not found: {}", configured);
|
||||
}
|
||||
}
|
||||
|
||||
if(cdb_path.empty()) {
|
||||
auto try_candidate = [&](llvm::StringRef dir) -> bool {
|
||||
auto candidate = path::join(dir, "compile_commands.json");
|
||||
if(llvm::sys::fs::exists(candidate)) {
|
||||
cdb_path = std::move(candidate);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
if(!try_candidate(workspace_root)) {
|
||||
std::error_code ec;
|
||||
for(llvm::sys::fs::directory_iterator it(workspace_root, ec), end; it != end && !ec;
|
||||
it.increment(ec)) {
|
||||
if(it->type() == llvm::sys::fs::file_type::directory_file) {
|
||||
if(try_candidate(it->path()))
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(cdb_path.empty()) {
|
||||
LOG_WARN("No compile_commands.json found in workspace {}", workspace_root);
|
||||
return;
|
||||
}
|
||||
|
||||
auto count = workspace.cdb.load(cdb_path);
|
||||
LOG_INFO("Loaded CDB from {} with {} entries", cdb_path, count);
|
||||
|
||||
auto report = scan_dependency_graph(workspace.cdb,
|
||||
workspace.path_pool,
|
||||
workspace.dep_graph,
|
||||
/*cache=*/nullptr,
|
||||
[this](llvm::StringRef path,
|
||||
std::vector<std::string>& append,
|
||||
std::vector<std::string>& remove) {
|
||||
workspace.config.match_rules(path, append, remove);
|
||||
});
|
||||
workspace.dep_graph.build_reverse_map();
|
||||
|
||||
auto unresolved = report.includes_found - report.includes_resolved;
|
||||
double accuracy =
|
||||
report.includes_found > 0
|
||||
? 100.0 * static_cast<double>(report.includes_resolved) / report.includes_found
|
||||
: 100.0;
|
||||
LOG_INFO(
|
||||
"Dependency scan: {}ms, {} files ({} source + {} header), " "{} edges, {}/{} resolved ({:.1f}%), {} waves",
|
||||
report.elapsed_ms,
|
||||
report.total_files,
|
||||
report.source_files,
|
||||
report.header_files,
|
||||
report.total_edges,
|
||||
report.includes_resolved,
|
||||
report.includes_found,
|
||||
accuracy,
|
||||
report.waves);
|
||||
if(unresolved > 0)
|
||||
LOG_WARN("{} unresolved includes", unresolved);
|
||||
|
||||
workspace.build_module_map();
|
||||
indexer.load(cfg.index_dir);
|
||||
|
||||
if(*cfg.enable_indexing) {
|
||||
for(auto& entry: workspace.cdb.get_entries()) {
|
||||
auto file = workspace.cdb.resolve_path(entry.file);
|
||||
auto server_id = workspace.path_pool.intern(file);
|
||||
indexer.enqueue(server_id);
|
||||
}
|
||||
indexer.schedule();
|
||||
}
|
||||
|
||||
compiler.init_compile_graph();
|
||||
}
|
||||
|
||||
struct Connection {
|
||||
std::unique_ptr<kota::ipc::JsonPeer> peer;
|
||||
std::unique_ptr<LSPClient> lsp_client;
|
||||
std::unique_ptr<AgentClient> agent_client;
|
||||
};
|
||||
|
||||
static kota::task<> run_connection(kota::ipc::JsonPeer* peer,
|
||||
std::list<Connection>& connections,
|
||||
std::list<Connection>::iterator pos) {
|
||||
co_await peer->run();
|
||||
LOG_INFO("Client disconnected");
|
||||
connections.erase(pos);
|
||||
}
|
||||
|
||||
static kota::task<> accept_connections(MasterServer& server,
|
||||
kota::tcp::acceptor acceptor,
|
||||
bool register_lsp,
|
||||
std::list<Connection>& connections) {
|
||||
auto& loop = kota::event_loop::current();
|
||||
kota::task_group<> connection_group(loop);
|
||||
bool lsp_registered = false;
|
||||
|
||||
while(true) {
|
||||
auto conn = co_await acceptor.accept();
|
||||
if(!conn.has_value())
|
||||
break;
|
||||
|
||||
LOG_INFO("Client connected");
|
||||
|
||||
auto transport = std::make_unique<kota::ipc::StreamTransport>(std::move(*conn));
|
||||
auto peer = std::make_unique<kota::ipc::JsonPeer>(loop, std::move(transport));
|
||||
|
||||
std::unique_ptr<LSPClient> lsp;
|
||||
if(register_lsp && !lsp_registered) {
|
||||
lsp = std::make_unique<LSPClient>(server, *peer);
|
||||
lsp_registered = true;
|
||||
}
|
||||
auto agent = std::make_unique<AgentClient>(server, *peer);
|
||||
|
||||
auto* peer_ptr = peer.get();
|
||||
auto it = connections.emplace(connections.end(),
|
||||
Connection{
|
||||
.peer = std::move(peer),
|
||||
.lsp_client = std::move(lsp),
|
||||
.agent_client = std::move(agent),
|
||||
});
|
||||
|
||||
connection_group.spawn(run_connection(peer_ptr, connections, it));
|
||||
}
|
||||
|
||||
co_await connection_group.join();
|
||||
}
|
||||
|
||||
int run_server_mode(const ServerOptions& opts) {
|
||||
logging::stderr_logger("master", logging::options);
|
||||
|
||||
kota::event_loop loop;
|
||||
MasterServer server(loop, opts.self_path);
|
||||
std::list<Connection> connections;
|
||||
|
||||
if(opts.mode == "pipe") {
|
||||
auto transport = kota::ipc::StreamTransport::open_stdio(loop);
|
||||
if(!transport) {
|
||||
LOG_ERROR("failed to open stdio transport");
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::unique_ptr<kota::ipc::Transport> final_transport = std::move(*transport);
|
||||
if(!opts.record.empty()) {
|
||||
final_transport =
|
||||
std::make_unique<kota::ipc::RecordingTransport>(std::move(final_transport),
|
||||
opts.record);
|
||||
}
|
||||
|
||||
kota::ipc::JsonPeer lsp_peer(loop, std::move(final_transport));
|
||||
LSPClient lsp_client(server, lsp_peer);
|
||||
|
||||
if(opts.port > 0) {
|
||||
auto acceptor = kota::tcp::listen(opts.host, opts.port, {}, loop);
|
||||
if(acceptor) {
|
||||
LOG_INFO("Agentic protocol listening on {}:{}", opts.host, opts.port);
|
||||
loop.schedule(accept_connections(server, std::move(*acceptor), false, connections));
|
||||
} else {
|
||||
LOG_WARN("Failed to start agentic listener on {}:{}", opts.host, opts.port);
|
||||
}
|
||||
}
|
||||
|
||||
loop.schedule(lsp_peer.run());
|
||||
loop.run();
|
||||
return 0;
|
||||
}
|
||||
|
||||
if(opts.mode == "socket") {
|
||||
auto acceptor = kota::tcp::listen(opts.host, opts.port, {}, loop);
|
||||
if(!acceptor) {
|
||||
LOG_ERROR("failed to listen on {}:{}", opts.host, opts.port);
|
||||
return 1;
|
||||
}
|
||||
|
||||
LOG_INFO("Listening on {}:{} ...", opts.host, opts.port);
|
||||
loop.schedule(accept_connections(server, std::move(*acceptor), true, connections));
|
||||
loop.run();
|
||||
return 0;
|
||||
}
|
||||
|
||||
LOG_ERROR("unknown server mode '{}'", opts.mode);
|
||||
return 1;
|
||||
}
|
||||
|
||||
struct DaemonConnection {
|
||||
std::unique_ptr<kota::ipc::JsonPeer> peer;
|
||||
std::unique_ptr<AgentClient> agent_client;
|
||||
};
|
||||
|
||||
static kota::task<> run_daemon_connection(kota::ipc::JsonPeer* peer,
|
||||
std::list<DaemonConnection>& connections,
|
||||
std::list<DaemonConnection>::iterator pos) {
|
||||
co_await peer->run();
|
||||
LOG_INFO("Daemon client disconnected");
|
||||
connections.erase(pos);
|
||||
}
|
||||
|
||||
static kota::task<> daemon_main(MasterServer& server, kota::pipe::acceptor acceptor) {
|
||||
auto& loop = kota::event_loop::current();
|
||||
std::list<DaemonConnection> connections;
|
||||
kota::task_group<> connection_group(loop);
|
||||
|
||||
co_await kota::when_all(
|
||||
[&]() -> kota::task<> {
|
||||
while(true) {
|
||||
auto conn = co_await acceptor.accept();
|
||||
if(!conn.has_value())
|
||||
break;
|
||||
|
||||
LOG_INFO("Daemon client connected");
|
||||
|
||||
auto transport = std::make_unique<kota::ipc::StreamTransport>(std::move(*conn));
|
||||
auto peer = std::make_unique<kota::ipc::JsonPeer>(loop, std::move(transport));
|
||||
auto agent = std::make_unique<AgentClient>(server, *peer);
|
||||
|
||||
auto* peer_ptr = peer.get();
|
||||
auto it = connections.emplace(connections.end(),
|
||||
DaemonConnection{
|
||||
.peer = std::move(peer),
|
||||
.agent_client = std::move(agent),
|
||||
});
|
||||
|
||||
connection_group.spawn(run_daemon_connection(peer_ptr, connections, it));
|
||||
}
|
||||
}(),
|
||||
[&]() -> kota::task<> {
|
||||
co_await server.get_shutdown_event().wait();
|
||||
acceptor.stop();
|
||||
for(auto& conn: connections) {
|
||||
conn.peer->close();
|
||||
}
|
||||
}());
|
||||
|
||||
co_await connection_group.join();
|
||||
}
|
||||
|
||||
int run_daemon_mode(const DaemonOptions& opts) {
|
||||
logging::stderr_logger("daemon", logging::options);
|
||||
|
||||
auto socket_path = opts.socket_path.empty() ? path::default_socket_path() : opts.socket_path;
|
||||
|
||||
auto socket_dir = llvm::sys::path::parent_path(socket_path);
|
||||
if(auto ec = llvm::sys::fs::create_directories(socket_dir)) {
|
||||
LOG_ERROR("Failed to create socket directory {}: {}", socket_dir, ec.message());
|
||||
return 1;
|
||||
}
|
||||
|
||||
if(llvm::sys::fs::exists(socket_path)) {
|
||||
#ifndef _WIN32
|
||||
int fd = ::socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if(fd >= 0) {
|
||||
struct sockaddr_un addr{};
|
||||
addr.sun_family = AF_UNIX;
|
||||
auto len = std::min(socket_path.size(), sizeof(addr.sun_path) - 1);
|
||||
std::memcpy(addr.sun_path, socket_path.data(), len);
|
||||
bool live = ::connect(fd, reinterpret_cast<struct sockaddr*>(&addr), sizeof(addr)) == 0;
|
||||
::close(fd);
|
||||
if(live) {
|
||||
LOG_ERROR("Another daemon is already running on {}", socket_path);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
llvm::sys::fs::remove(socket_path);
|
||||
}
|
||||
|
||||
kota::event_loop loop;
|
||||
MasterServer server(loop, opts.self_path);
|
||||
|
||||
if(!opts.workspace.empty()) {
|
||||
server.initialize(opts.workspace);
|
||||
server.start_file_watcher();
|
||||
}
|
||||
|
||||
auto acceptor = kota::pipe::listen(socket_path, {}, loop);
|
||||
if(!acceptor) {
|
||||
LOG_ERROR("Failed to listen on {}", socket_path);
|
||||
return 1;
|
||||
}
|
||||
|
||||
LOG_INFO("Daemon listening on {}", socket_path);
|
||||
loop.schedule(daemon_main(server, std::move(*acceptor)));
|
||||
loop.run();
|
||||
|
||||
llvm::sys::fs::remove(socket_path);
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace clice
|
||||
93
src/server/service/master_server.h
Normal file
93
src/server/service/master_server.h
Normal file
@@ -0,0 +1,93 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
#include "server/compiler/compiler.h"
|
||||
#include "server/compiler/indexer.h"
|
||||
#include "server/service/session.h"
|
||||
#include "server/worker/worker_pool.h"
|
||||
#include "server/workspace/workspace.h"
|
||||
|
||||
#include "kota/async/async.h"
|
||||
#include "llvm/ADT/DenseMap.h"
|
||||
#include "llvm/ADT/StringRef.h"
|
||||
|
||||
namespace clice {
|
||||
|
||||
enum class ServerLifecycle : std::uint8_t {
|
||||
Uninitialized,
|
||||
Initialized,
|
||||
Ready,
|
||||
ShuttingDown,
|
||||
Exited,
|
||||
};
|
||||
|
||||
/// Core server state — owns the two-layer state model (Workspace + Sessions),
|
||||
/// the worker pool, compilation engine, and indexer.
|
||||
///
|
||||
/// Does NOT own any transport or peer. Protocol-specific handler registration
|
||||
/// is done by LSPClient and AgentClient, which access private members directly.
|
||||
class MasterServer {
|
||||
friend class LSPClient;
|
||||
friend class AgentClient;
|
||||
|
||||
public:
|
||||
MasterServer(kota::event_loop& loop, std::string self_path);
|
||||
~MasterServer();
|
||||
|
||||
void initialize();
|
||||
void initialize(llvm::StringRef root);
|
||||
|
||||
void start_file_watcher();
|
||||
|
||||
Session* find_session(std::uint32_t path_id);
|
||||
Session& open_session(std::uint32_t path_id);
|
||||
void close_session(std::uint32_t path_id, kota::ipc::JsonPeer& peer);
|
||||
|
||||
void on_file_saved(std::uint32_t path_id);
|
||||
|
||||
void schedule_shutdown();
|
||||
|
||||
kota::event& get_shutdown_event() {
|
||||
return shutdown_event;
|
||||
}
|
||||
|
||||
private:
|
||||
kota::event shutdown_event;
|
||||
void load_workspace();
|
||||
|
||||
kota::event_loop& loop;
|
||||
|
||||
Workspace workspace;
|
||||
llvm::DenseMap<std::uint32_t, Session> sessions;
|
||||
WorkerPool pool;
|
||||
Compiler compiler;
|
||||
Indexer indexer;
|
||||
|
||||
ServerLifecycle lifecycle = ServerLifecycle::Uninitialized;
|
||||
std::string self_path;
|
||||
std::string workspace_root;
|
||||
std::string session_log_dir;
|
||||
std::string init_options_json;
|
||||
};
|
||||
|
||||
struct ServerOptions {
|
||||
std::string mode;
|
||||
std::string host = "127.0.0.1";
|
||||
int port = 0;
|
||||
std::string self_path;
|
||||
std::string record;
|
||||
};
|
||||
|
||||
int run_server_mode(const ServerOptions& opts);
|
||||
|
||||
struct DaemonOptions {
|
||||
std::string socket_path;
|
||||
std::string workspace;
|
||||
std::string self_path;
|
||||
};
|
||||
|
||||
int run_daemon_mode(const DaemonOptions& opts);
|
||||
|
||||
} // namespace clice
|
||||
@@ -5,7 +5,7 @@
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#include "server/workspace.h"
|
||||
#include "server/workspace/workspace.h"
|
||||
|
||||
#include "kota/async/async.h"
|
||||
#include "llvm/ADT/SmallVector.h"
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "server/stateful_worker.h"
|
||||
#include "server/worker/stateful_worker.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
@@ -10,8 +10,8 @@
|
||||
#include "compile/compilation.h"
|
||||
#include "feature/feature.h"
|
||||
#include "index/tu_index.h"
|
||||
#include "server/protocol.h"
|
||||
#include "server/worker_common.h"
|
||||
#include "server/protocol/worker.h"
|
||||
#include "server/worker/worker_common.h"
|
||||
#include "support/logging.h"
|
||||
|
||||
#include "kota/async/async.h"
|
||||
@@ -245,26 +245,33 @@ void StatefulWorker::register_handlers() {
|
||||
co_return kota::codec::RawValue{"[]"};
|
||||
case K::SemanticTokens:
|
||||
co_return co_await with_ast(params.path, [&](DocumentEntry& doc) {
|
||||
return to_raw(feature::semantic_tokens(doc.unit));
|
||||
return to_raw(
|
||||
feature::semantic_tokens(doc.unit, feature::PositionEncoding::UTF16));
|
||||
});
|
||||
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));
|
||||
return to_raw(feature::inlay_hints(doc.unit,
|
||||
range,
|
||||
{},
|
||||
feature::PositionEncoding::UTF16));
|
||||
});
|
||||
case K::FoldingRange:
|
||||
co_return co_await with_ast(params.path, [&](DocumentEntry& doc) {
|
||||
return to_raw(feature::folding_ranges(doc.unit));
|
||||
return to_raw(
|
||||
feature::folding_ranges(doc.unit, feature::PositionEncoding::UTF16));
|
||||
});
|
||||
case K::DocumentSymbol:
|
||||
co_return co_await with_ast(params.path, [&](DocumentEntry& doc) {
|
||||
return to_raw(feature::document_symbols(doc.unit));
|
||||
return to_raw(
|
||||
feature::document_symbols(doc.unit, feature::PositionEncoding::UTF16));
|
||||
});
|
||||
case K::DocumentLink:
|
||||
co_return co_await with_ast(params.path, [&](DocumentEntry& doc) {
|
||||
return to_raw(feature::document_links(doc.unit));
|
||||
return to_raw(
|
||||
feature::document_links(doc.unit, feature::PositionEncoding::UTF16));
|
||||
});
|
||||
case K::CodeAction:
|
||||
// TODO: Implement code actions
|
||||
@@ -1,10 +1,10 @@
|
||||
#include "server/stateless_worker.h"
|
||||
#include "server/worker/stateless_worker.h"
|
||||
|
||||
#include "compile/compilation.h"
|
||||
#include "feature/feature.h"
|
||||
#include "index/tu_index.h"
|
||||
#include "server/protocol.h"
|
||||
#include "server/worker_common.h"
|
||||
#include "server/protocol/worker.h"
|
||||
#include "server/worker/worker_common.h"
|
||||
#include "support/logging.h"
|
||||
|
||||
#include "kota/async/async.h"
|
||||
@@ -15,6 +15,22 @@
|
||||
|
||||
namespace clice {
|
||||
|
||||
/// RAII guard that lowers the current process's scheduling priority and
|
||||
/// restores it on destruction.
|
||||
struct ScopedNice {
|
||||
int saved;
|
||||
|
||||
explicit ScopedNice(int increment = 10) {
|
||||
auto p = kota::sys::priority();
|
||||
saved = p ? *p : 0;
|
||||
kota::sys::set_priority(saved + increment);
|
||||
}
|
||||
|
||||
~ScopedNice() {
|
||||
kota::sys::set_priority(saved);
|
||||
}
|
||||
};
|
||||
|
||||
using kota::ipc::RequestResult;
|
||||
using RequestContext = kota::ipc::BincodePeer::RequestContext;
|
||||
|
||||
@@ -258,6 +274,22 @@ static worker::BuildResult handle_signature_help(const worker::BuildParams& para
|
||||
return result;
|
||||
}
|
||||
|
||||
static worker::BuildResult handle_format(const worker::BuildParams& params) {
|
||||
ScopedTimer timer;
|
||||
|
||||
std::optional<LocalSourceRange> range;
|
||||
if(params.format_range.valid()) {
|
||||
range = params.format_range;
|
||||
}
|
||||
|
||||
auto edits = feature::document_format(params.file, params.text, range);
|
||||
LOG_DEBUG("Format done: {} edits, {}ms", edits.size(), timer.ms());
|
||||
|
||||
worker::BuildResult result;
|
||||
result.result_json = to_raw(edits);
|
||||
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()) {
|
||||
@@ -283,9 +315,13 @@ int run_stateless_worker_mode(const std::string& worker_name, const std::string&
|
||||
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::Index: {
|
||||
ScopedNice guard;
|
||||
return handle_index(params);
|
||||
}
|
||||
case K::Completion: return handle_completion(params);
|
||||
case K::SignatureHelp: return handle_signature_help(params);
|
||||
case K::Format: return handle_format(params);
|
||||
}
|
||||
return {false, "Unknown build kind"};
|
||||
});
|
||||
@@ -8,8 +8,7 @@
|
||||
|
||||
#include "compile/compilation.h"
|
||||
|
||||
#include "kota/codec/json/serializer.h"
|
||||
#include "kota/codec/raw_value.h"
|
||||
#include "kota/codec/json/json.h"
|
||||
#include "kota/ipc/codec/json.h"
|
||||
|
||||
namespace clice {
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "server/worker_pool.h"
|
||||
#include "server/worker/worker_pool.h"
|
||||
|
||||
#include <csignal>
|
||||
#include <string>
|
||||
@@ -13,14 +13,13 @@ 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.
|
||||
/// (crash stacktraces, assertion failures, sanitizer reports, etc.).
|
||||
kota::task<> drain_stderr(kota::pipe stderr_pipe, std::string prefix) {
|
||||
std::string buffer;
|
||||
while(true) {
|
||||
auto result = co_await stderr_pipe.read();
|
||||
if(!result.has_value()) {
|
||||
if(!result.has_value())
|
||||
break;
|
||||
}
|
||||
auto& chunk = result.value();
|
||||
if(chunk.empty())
|
||||
break;
|
||||
@@ -34,7 +33,7 @@ kota::task<> drain_stderr(kota::pipe stderr_pipe, std::string prefix) {
|
||||
break;
|
||||
auto line = buffer.substr(pos, nl - pos);
|
||||
if(!line.empty()) {
|
||||
LOG_DEBUG("{} {}", prefix, line);
|
||||
LOG_WARN("{} {}", prefix, line);
|
||||
}
|
||||
pos = nl + 1;
|
||||
}
|
||||
@@ -42,7 +41,7 @@ kota::task<> drain_stderr(kota::pipe stderr_pipe, std::string prefix) {
|
||||
}
|
||||
|
||||
if(!buffer.empty()) {
|
||||
LOG_DEBUG("{} {}", prefix, buffer);
|
||||
LOG_WARN("{} {}", prefix, buffer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,9 +96,8 @@ bool WorkerPool::spawn_worker(const std::string& self_path,
|
||||
std::move(spawn.stdin_pipe));
|
||||
auto peer = std::make_unique<kota::ipc::BincodePeer>(loop, std::move(transport));
|
||||
|
||||
// Schedule stderr log collection
|
||||
std::string prefix = "[" + worker_name + "]";
|
||||
loop.schedule(drain_stderr(std::move(spawn.stderr_pipe), prefix));
|
||||
io_group.spawn(drain_stderr(std::move(spawn.stderr_pipe), prefix));
|
||||
|
||||
workers.push_back(WorkerProcess{
|
||||
.proc = std::move(spawn.proc),
|
||||
@@ -108,24 +106,28 @@ bool WorkerPool::spawn_worker(const std::string& self_path,
|
||||
});
|
||||
|
||||
auto& w = workers.back();
|
||||
loop.schedule(w.peer->run());
|
||||
w.alive = true;
|
||||
io_group.spawn(w.peer->run());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WorkerPool::start(const WorkerPoolOptions& options) {
|
||||
options_ = 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;
|
||||
}
|
||||
monitor_group.spawn(monitor_worker(stateless_workers.size() - 1, false));
|
||||
}
|
||||
|
||||
for(std::uint32_t i = 0; i < options.stateful_count; ++i) {
|
||||
if(!spawn_worker(options.self_path, true, options.worker_memory_limit)) {
|
||||
return false;
|
||||
}
|
||||
monitor_group.spawn(monitor_worker(stateful_workers.size() - 1, true));
|
||||
}
|
||||
|
||||
// Register evicted notification handler for each stateful worker
|
||||
@@ -145,30 +147,19 @@ bool WorkerPool::start(const WorkerPoolOptions& options) {
|
||||
|
||||
kota::task<> WorkerPool::stop() {
|
||||
LOG_INFO("WorkerPool stopping...");
|
||||
shutting_down_ = true;
|
||||
|
||||
// Close output pipes to signal workers to exit gracefully
|
||||
for(auto& w: stateless_workers) {
|
||||
for(auto& w: stateless_workers)
|
||||
w.peer->close_output();
|
||||
}
|
||||
for(auto& w: stateful_workers) {
|
||||
for(auto& w: stateful_workers)
|
||||
w.peer->close_output();
|
||||
}
|
||||
|
||||
// Send SIGTERM to all workers
|
||||
for(auto& w: stateless_workers) {
|
||||
for(auto& w: stateless_workers)
|
||||
w.proc.kill(SIGTERM);
|
||||
}
|
||||
for(auto& w: stateful_workers) {
|
||||
for(auto& w: stateful_workers)
|
||||
w.proc.kill(SIGTERM);
|
||||
}
|
||||
|
||||
// Wait for all worker processes to exit
|
||||
for(auto& w: stateless_workers) {
|
||||
co_await w.proc.wait();
|
||||
}
|
||||
for(auto& w: stateful_workers) {
|
||||
co_await w.proc.wait();
|
||||
}
|
||||
co_await kota::when_all(monitor_group.join(), io_group.join());
|
||||
|
||||
LOG_INFO("WorkerPool stopped");
|
||||
}
|
||||
@@ -198,7 +189,10 @@ std::size_t WorkerPool::assign_worker(std::uint32_t path_id) {
|
||||
std::size_t WorkerPool::pick_least_loaded() {
|
||||
std::size_t best = 0;
|
||||
for(std::size_t i = 1; i < stateful_workers.size(); ++i) {
|
||||
if(stateful_workers[i].owned_documents < stateful_workers[best].owned_documents) {
|
||||
if(!stateful_workers[i].alive)
|
||||
continue;
|
||||
if(!stateful_workers[best].alive ||
|
||||
stateful_workers[i].owned_documents < stateful_workers[best].owned_documents) {
|
||||
best = i;
|
||||
}
|
||||
}
|
||||
@@ -233,4 +227,122 @@ void WorkerPool::clear_owner(std::size_t worker_index) {
|
||||
}
|
||||
}
|
||||
|
||||
kota::task<> WorkerPool::monitor_worker(std::size_t index, bool stateful) {
|
||||
auto& workers = stateful ? stateful_workers : stateless_workers;
|
||||
auto& w = workers[index];
|
||||
auto name = std::string(stateful ? "SF-" : "SL-") + std::to_string(index);
|
||||
|
||||
auto result = co_await w.proc.wait();
|
||||
w.alive = false;
|
||||
|
||||
if(shutting_down_)
|
||||
co_return;
|
||||
|
||||
if(result.has_value()) {
|
||||
auto& exit = result.value();
|
||||
if(exit.term_signal != 0) {
|
||||
LOG_ERROR("Worker {} killed by signal {} (restarts: {})",
|
||||
name,
|
||||
exit.term_signal,
|
||||
w.restart_count);
|
||||
} else {
|
||||
LOG_ERROR("Worker {} exited with code {} (restarts: {})",
|
||||
name,
|
||||
exit.status,
|
||||
w.restart_count);
|
||||
}
|
||||
} else {
|
||||
LOG_ERROR("Worker {} lost: {} (restarts: {})",
|
||||
name,
|
||||
result.error().message(),
|
||||
w.restart_count);
|
||||
}
|
||||
|
||||
if(stateful)
|
||||
clear_owner(index);
|
||||
|
||||
constexpr unsigned max_restarts = 5;
|
||||
if(w.restart_count >= max_restarts) {
|
||||
LOG_ERROR("Worker {} exceeded max restarts ({}), giving up", name, max_restarts);
|
||||
co_return;
|
||||
}
|
||||
|
||||
if(!respawn_worker(index, stateful)) {
|
||||
LOG_ERROR("Worker {} respawn failed", name);
|
||||
}
|
||||
}
|
||||
|
||||
bool WorkerPool::respawn_worker(std::size_t index, bool stateful) {
|
||||
auto& workers = stateful ? stateful_workers : stateless_workers;
|
||||
auto old_restart_count = workers[index].restart_count + 1;
|
||||
auto worker_name = std::string(stateful ? "SF-" : "SL-") + std::to_string(index);
|
||||
|
||||
// Close the old peer and retire it so its coroutines (run/write_loop)
|
||||
// can finish naturally before the object is destroyed.
|
||||
if(workers[index].peer) {
|
||||
workers[index].peer->close();
|
||||
retired_peers.push_back(std::move(workers[index].peer));
|
||||
}
|
||||
|
||||
kota::process::options opts;
|
||||
opts.file = options_.self_path;
|
||||
if(stateful) {
|
||||
opts.args = {options_.self_path,
|
||||
"--mode",
|
||||
"stateful-worker",
|
||||
"--worker-memory-limit",
|
||||
std::to_string(options_.worker_memory_limit)};
|
||||
} else {
|
||||
opts.args = {options_.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 = {
|
||||
kota::process::stdio::pipe(true, false),
|
||||
kota::process::stdio::pipe(false, true),
|
||||
kota::process::stdio::pipe(false, true),
|
||||
};
|
||||
|
||||
auto result = kota::process::spawn(opts, loop);
|
||||
if(!result) {
|
||||
LOG_ERROR("Failed to respawn worker {}: {}", worker_name, result.error().message());
|
||||
return false;
|
||||
}
|
||||
|
||||
auto& spawn = *result;
|
||||
auto transport = std::make_unique<kota::ipc::StreamTransport>(std::move(spawn.stdout_pipe),
|
||||
std::move(spawn.stdin_pipe));
|
||||
auto peer = std::make_unique<kota::ipc::BincodePeer>(loop, std::move(transport));
|
||||
|
||||
std::string prefix = "[" + worker_name + "]";
|
||||
io_group.spawn(drain_stderr(std::move(spawn.stderr_pipe), prefix));
|
||||
|
||||
workers[index] = WorkerProcess{
|
||||
.proc = std::move(spawn.proc),
|
||||
.peer = std::move(peer),
|
||||
.owned_documents = 0,
|
||||
.alive = true,
|
||||
.restart_count = old_restart_count,
|
||||
};
|
||||
|
||||
auto& w = workers[index];
|
||||
io_group.spawn(w.peer->run());
|
||||
|
||||
if(stateful) {
|
||||
w.peer->on_notification([this](const worker::EvictedParams& params) {
|
||||
if(on_evicted)
|
||||
on_evicted(params.path);
|
||||
});
|
||||
}
|
||||
|
||||
monitor_group.spawn(monitor_worker(index, stateful));
|
||||
|
||||
LOG_INFO("Worker {} restarted (attempt {})", worker_name, old_restart_count);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace clice
|
||||
@@ -6,7 +6,7 @@
|
||||
#include <list>
|
||||
#include <memory>
|
||||
|
||||
#include "server/protocol.h"
|
||||
#include "server/protocol/worker.h"
|
||||
|
||||
#include "kota/async/async.h"
|
||||
#include "kota/ipc/codec/bincode.h"
|
||||
@@ -64,6 +64,8 @@ private:
|
||||
kota::process proc;
|
||||
std::unique_ptr<kota::ipc::BincodePeer> peer;
|
||||
std::size_t owned_documents = 0;
|
||||
bool alive = true;
|
||||
unsigned restart_count = 0;
|
||||
};
|
||||
|
||||
kota::event_loop& loop;
|
||||
@@ -80,8 +82,19 @@ private:
|
||||
void clear_owner(std::size_t worker_index);
|
||||
std::size_t pick_least_loaded();
|
||||
|
||||
bool shutting_down_ = false;
|
||||
kota::task_group<> monitor_group{loop};
|
||||
kota::task_group<> io_group{loop};
|
||||
WorkerPoolOptions options_;
|
||||
std::string log_dir_;
|
||||
|
||||
/// Peers moved here during respawn so their coroutines can finish
|
||||
/// before the object is destroyed.
|
||||
llvm::SmallVector<std::unique_ptr<kota::ipc::BincodePeer>> retired_peers;
|
||||
|
||||
bool spawn_worker(const std::string& self_path, bool stateful, std::uint64_t memory_limit);
|
||||
bool respawn_worker(std::size_t index, bool stateful);
|
||||
kota::task<> monitor_worker(std::size_t index, bool stateful);
|
||||
};
|
||||
|
||||
template <typename Params>
|
||||
@@ -91,11 +104,10 @@ RequestResult<Params> WorkerPool::send_stateful(std::uint32_t path_id,
|
||||
if(stateful_workers.empty()) {
|
||||
co_return kota::outcome_error(kota::ipc::Error{"No stateful workers available"});
|
||||
}
|
||||
// No timeout: compile tasks run as detached tasks (loop.schedule) that
|
||||
// are immune to LSP $/cancelRequest. Adding a timeout here would use
|
||||
// kotatsu's with_token/when_any which has a spurious-cancellation bug
|
||||
// that kills requests within milliseconds instead of the configured period.
|
||||
auto idx = assign_worker(path_id);
|
||||
if(!stateful_workers[idx].alive) {
|
||||
co_return kota::outcome_error(kota::ipc::Error{"Assigned stateful worker is down"});
|
||||
}
|
||||
co_return co_await stateful_workers[idx].peer->send_request(params, opts);
|
||||
}
|
||||
|
||||
@@ -105,9 +117,16 @@ RequestResult<Params> WorkerPool::send_stateless(const Params& params,
|
||||
if(stateless_workers.empty()) {
|
||||
co_return kota::outcome_error(kota::ipc::Error{"No stateless workers available"});
|
||||
}
|
||||
auto idx = next_stateless;
|
||||
next_stateless = (next_stateless + 1) % stateless_workers.size();
|
||||
co_return co_await stateless_workers[idx].peer->send_request(params, opts);
|
||||
// Round-robin, skipping dead workers.
|
||||
auto start = next_stateless;
|
||||
for(std::size_t i = 0; i < stateless_workers.size(); ++i) {
|
||||
auto idx = (start + i) % stateless_workers.size();
|
||||
if(stateless_workers[idx].alive) {
|
||||
next_stateless = (idx + 1) % stateless_workers.size();
|
||||
co_return co_await stateless_workers[idx].peer->send_request(params, opts);
|
||||
}
|
||||
}
|
||||
co_return kota::outcome_error(kota::ipc::Error{"All stateless workers are down"});
|
||||
}
|
||||
|
||||
template <typename Params>
|
||||
@@ -115,6 +134,8 @@ void WorkerPool::notify_stateful(std::uint32_t path_id, const Params& params) {
|
||||
auto it = owner.find(path_id);
|
||||
if(it == owner.end())
|
||||
return;
|
||||
if(!stateful_workers[it->second].alive)
|
||||
return;
|
||||
stateful_workers[it->second].peer->send_notification(params);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "server/config.h"
|
||||
#include "server/workspace/config.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
#include "support/glob_pattern.h"
|
||||
#include "support/logging.h"
|
||||
|
||||
#include "kota/async/io/system.h"
|
||||
#include "kota/codec/json/json.h"
|
||||
#include "kota/codec/toml.h"
|
||||
#include "kota/codec/toml/toml.h"
|
||||
#include "llvm/Support/FileSystem.h"
|
||||
#include "llvm/Support/Path.h"
|
||||
#include "llvm/Support/Process.h"
|
||||
@@ -65,8 +66,10 @@ void Config::apply_defaults(llvm::StringRef workspace_root) {
|
||||
|
||||
if(p.stateful_worker_count == 0)
|
||||
p.stateful_worker_count = 2;
|
||||
if(p.stateless_worker_count == 0)
|
||||
p.stateless_worker_count = 3;
|
||||
if(p.stateless_worker_count == 0) {
|
||||
auto cores = kota::sys::parallelism();
|
||||
p.stateless_worker_count = std::max(cores / 2, 2u);
|
||||
}
|
||||
if(p.worker_memory_limit == 0)
|
||||
p.worker_memory_limit = 4ULL * 1024 * 1024 * 1024; // 4GB
|
||||
|
||||
@@ -153,13 +156,13 @@ std::optional<Config> Config::load(llvm::StringRef path, llvm::StringRef workspa
|
||||
}
|
||||
|
||||
std::optional<Config> Config::load_from_json(llvm::StringRef json, llvm::StringRef workspace_root) {
|
||||
auto result = kota::codec::json::from_json<Config>(json);
|
||||
Config config{};
|
||||
auto result = kota::codec::json::from_json(json, config);
|
||||
if(!result) {
|
||||
LOG_WARN("Failed to parse initializationOptions JSON: {}", result.error().message());
|
||||
LOG_WARN("Failed to parse initializationOptions JSON: {}", result.error().message);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto config = std::move(*result);
|
||||
config.apply_defaults(workspace_root);
|
||||
LOG_INFO("Loaded config from initializationOptions");
|
||||
return config;
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "server/workspace.h"
|
||||
#include "server/workspace/workspace.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
@@ -11,8 +11,8 @@
|
||||
#include "index/merged_index.h"
|
||||
#include "index/project_index.h"
|
||||
#include "semantic/relation_kind.h"
|
||||
#include "server/compile_graph.h"
|
||||
#include "server/config.h"
|
||||
#include "server/compiler/compile_graph.h"
|
||||
#include "server/workspace/config.h"
|
||||
#include "support/path_pool.h"
|
||||
#include "syntax/dependency_graph.h"
|
||||
|
||||
@@ -37,6 +37,14 @@ inline std::string real_path(llvm::StringRef file) {
|
||||
return path.str().str();
|
||||
}
|
||||
|
||||
inline std::string default_socket_path() {
|
||||
llvm::SmallString<128> home;
|
||||
if(!llvm::sys::path::home_directory(home))
|
||||
return "/tmp/clice.sock";
|
||||
llvm::sys::path::append(home, ".clice", "clice.sock");
|
||||
return home.str().str();
|
||||
}
|
||||
|
||||
} // namespace path
|
||||
|
||||
namespace fs {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import asyncio
|
||||
import json
|
||||
import shutil
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
@@ -93,24 +94,27 @@ def workspace(request: pytest.FixtureRequest, test_data_dir: Path) -> Path | Non
|
||||
|
||||
@pytest.fixture
|
||||
async def client(
|
||||
request: pytest.FixtureRequest, executable: Path, workspace: Path | None
|
||||
request: pytest.FixtureRequest,
|
||||
executable: Path,
|
||||
workspace: Path | None,
|
||||
):
|
||||
"""Spawn clice server, auto-initialize if @pytest.mark.workspace is present."""
|
||||
config = request.config
|
||||
mode = config.getoption("--mode")
|
||||
host = config.getoption("--host")
|
||||
|
||||
cmd = [str(executable), "--mode", mode]
|
||||
if mode == "socket":
|
||||
host = config.getoption("--host")
|
||||
port = config.getoption("--port")
|
||||
cmd += ["--host", host, "--port", str(port)]
|
||||
cmd = [str(executable), "--mode", mode, "--host", host]
|
||||
|
||||
c = CliceClient()
|
||||
await c.start_io(*cmd)
|
||||
|
||||
if workspace is not None:
|
||||
init_options_marker = request.node.get_closest_marker("init_options")
|
||||
init_options = init_options_marker.args[0] if init_options_marker else None
|
||||
init_options = dict(init_options_marker.args[0]) if init_options_marker else {}
|
||||
# Force cache_dir into the workspace so .clice/ cleanup prevents stale PCH.
|
||||
project = dict(init_options.get("project", {}))
|
||||
project.setdefault("cache_dir", str(workspace / ".clice"))
|
||||
init_options["project"] = project
|
||||
await c.initialize(workspace, initialization_options=init_options)
|
||||
|
||||
yield c
|
||||
@@ -118,6 +122,39 @@ async def client(
|
||||
await _shutdown_client(c)
|
||||
|
||||
|
||||
def _find_free_port() -> int:
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
s.bind(("127.0.0.1", 0))
|
||||
return s.getsockname()[1]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def agentic(
|
||||
request: pytest.FixtureRequest,
|
||||
executable: Path,
|
||||
workspace: Path | None,
|
||||
):
|
||||
"""Start a server with agentic TCP port, yield (executable, host, port)."""
|
||||
host = "127.0.0.1"
|
||||
port = _find_free_port()
|
||||
cmd = [str(executable), "--mode", "pipe", "--host", host, "--port", str(port)]
|
||||
|
||||
c = CliceClient()
|
||||
await c.start_io(*cmd)
|
||||
|
||||
if workspace is not None:
|
||||
init_options_marker = request.node.get_closest_marker("init_options")
|
||||
init_options = dict(init_options_marker.args[0]) if init_options_marker else {}
|
||||
project = dict(init_options.get("project", {}))
|
||||
project.setdefault("cache_dir", str(workspace / ".clice"))
|
||||
init_options["project"] = project
|
||||
await c.initialize(workspace, initialization_options=init_options)
|
||||
|
||||
yield executable, host, port
|
||||
|
||||
await _shutdown_client(c)
|
||||
|
||||
|
||||
def generate_cdb(workspace: Path) -> None:
|
||||
"""Generate compile_commands.json using CMake with Ninja backend."""
|
||||
cmake = shutil.which("cmake")
|
||||
@@ -165,12 +202,17 @@ async def _shutdown_client(c: CliceClient) -> None:
|
||||
|
||||
try:
|
||||
server = getattr(c, "_server", None)
|
||||
if server and server.stderr:
|
||||
stderr_data = await asyncio.wait_for(server.stderr.read(), timeout=2.0)
|
||||
if stderr_data:
|
||||
for line in stderr_data.decode("utf-8", errors="replace").splitlines():
|
||||
if "[warn]" in line or "[error]" in line:
|
||||
print(f"[server] {line}", flush=True)
|
||||
if server:
|
||||
if server.returncode is not None:
|
||||
print(f"[server] exit code: {server.returncode}", flush=True)
|
||||
if server.stderr:
|
||||
stderr_data = await asyncio.wait_for(server.stderr.read(), timeout=2.0)
|
||||
if stderr_data:
|
||||
for line in stderr_data.decode(
|
||||
"utf-8", errors="replace"
|
||||
).splitlines():
|
||||
if "[warn]" in line or "[error]" in line or "Sanitizer" in line:
|
||||
print(f"[server] {line}", flush=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -250,6 +292,12 @@ def _generate_test_data_cdbs(data_dir: Path) -> None:
|
||||
if cr_main.exists():
|
||||
_write(cr_dir, [_entry(cr_dir, cr_main)])
|
||||
|
||||
# formatting
|
||||
fmt_dir = data_dir / "formatting"
|
||||
fmt_main = fmt_dir / "main.cpp"
|
||||
if fmt_main.exists():
|
||||
_write(fmt_dir, [_entry(fmt_dir, fmt_main)])
|
||||
|
||||
# pch_test
|
||||
pt_dir = data_dir / "pch_test"
|
||||
if pt_dir.exists():
|
||||
|
||||
36
tests/corpus/statements/if/basic_if.cpp
Normal file
36
tests/corpus/statements/if/basic_if.cpp
Normal file
@@ -0,0 +1,36 @@
|
||||
// basic if and if-else
|
||||
namespace basic_if {
|
||||
|
||||
int abs_val(int x) {
|
||||
if(x < 0)
|
||||
return -x;
|
||||
return x;
|
||||
}
|
||||
|
||||
const char* sign(int x) {
|
||||
if(x > 0) {
|
||||
return "positive";
|
||||
} else if(x < 0) {
|
||||
return "negative";
|
||||
} else {
|
||||
return "zero";
|
||||
}
|
||||
}
|
||||
|
||||
// dangling else: else binds to nearest if
|
||||
int nested_if(int a, int b) {
|
||||
if(a > 0)
|
||||
if(b > 0)
|
||||
return 1;
|
||||
else
|
||||
return 2;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void test() {
|
||||
[[maybe_unused]] int r1 = abs_val(-3);
|
||||
[[maybe_unused]] auto r2 = sign(5);
|
||||
[[maybe_unused]] int r3 = nested_if(1, -1);
|
||||
}
|
||||
|
||||
} // namespace basic_if
|
||||
49
tests/corpus/statements/if/if_chain_patterns.cpp
Normal file
49
tests/corpus/statements/if/if_chain_patterns.cpp
Normal file
@@ -0,0 +1,49 @@
|
||||
// if-else chain patterns and compound conditions
|
||||
namespace if_chain {
|
||||
|
||||
int classify_char(char c) {
|
||||
if(c >= 'a' && c <= 'z')
|
||||
return 1; // lowercase
|
||||
else if(c >= 'A' && c <= 'Z')
|
||||
return 2; // uppercase
|
||||
else if(c >= '0' && c <= '9')
|
||||
return 3; // digit
|
||||
else
|
||||
return 0; // other
|
||||
}
|
||||
|
||||
// early return pattern
|
||||
bool validate(int x, int y, int z) {
|
||||
if(x < 0)
|
||||
return false;
|
||||
if(y < 0)
|
||||
return false;
|
||||
if(z < 0)
|
||||
return false;
|
||||
if(x + y + z > 100)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// if with compound statement and multiple effects
|
||||
int process(int* data, int size) {
|
||||
int sum = 0;
|
||||
for(int i = 0; i < size; ++i) {
|
||||
if(data[i] > 0) {
|
||||
sum += data[i];
|
||||
data[i] = 0;
|
||||
} else if(data[i] < -10) {
|
||||
sum -= data[i];
|
||||
}
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
void test() {
|
||||
[[maybe_unused]] int r1 = classify_char('a');
|
||||
[[maybe_unused]] bool r2 = validate(1, 2, 3);
|
||||
int arr[] = {1, -20, 3, -5};
|
||||
[[maybe_unused]] int r3 = process(arr, 4);
|
||||
}
|
||||
|
||||
} // namespace if_chain
|
||||
46
tests/corpus/statements/if/if_consteval.cpp
Normal file
46
tests/corpus/statements/if/if_consteval.cpp
Normal file
@@ -0,0 +1,46 @@
|
||||
// C++23 if consteval
|
||||
namespace if_consteval {
|
||||
|
||||
constexpr int compute(int x) {
|
||||
if consteval {
|
||||
return x * x; // compile-time path
|
||||
} else {
|
||||
return x + x; // runtime path
|
||||
}
|
||||
}
|
||||
|
||||
// negated form: if !consteval
|
||||
constexpr int runtime_prefer(int x) {
|
||||
if !consteval {
|
||||
return x + 1; // runtime path
|
||||
} else {
|
||||
return x - 1; // compile-time path
|
||||
}
|
||||
}
|
||||
|
||||
// consteval function callable only at compile time
|
||||
consteval int ct_only(int x) {
|
||||
return x * 3;
|
||||
}
|
||||
|
||||
constexpr int dispatch(int x) {
|
||||
if consteval {
|
||||
return ct_only(x); // OK: in immediate context
|
||||
} else {
|
||||
return x;
|
||||
}
|
||||
}
|
||||
|
||||
// if consteval without else
|
||||
constexpr int maybe_optimize(int x) {
|
||||
if consteval {
|
||||
return x * x * x;
|
||||
}
|
||||
return x;
|
||||
}
|
||||
|
||||
static_assert(compute(5) == 25);
|
||||
static_assert(dispatch(3) == 9);
|
||||
static_assert(maybe_optimize(4) == 64);
|
||||
|
||||
} // namespace if_consteval
|
||||
38
tests/corpus/statements/if/if_constexpr_basic.cpp
Normal file
38
tests/corpus/statements/if/if_constexpr_basic.cpp
Normal file
@@ -0,0 +1,38 @@
|
||||
// if constexpr: basic compile-time branching
|
||||
namespace if_constexpr_basic {
|
||||
|
||||
template <typename T>
|
||||
constexpr int type_rank() {
|
||||
if constexpr(__is_same(T, char)) {
|
||||
return 1;
|
||||
} else if constexpr(__is_same(T, int)) {
|
||||
return 2;
|
||||
} else if constexpr(__is_same(T, long long)) {
|
||||
return 3;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static_assert(type_rank<char>() == 1);
|
||||
static_assert(type_rank<int>() == 2);
|
||||
static_assert(type_rank<long long>() == 3);
|
||||
static_assert(type_rank<float>() == 0);
|
||||
|
||||
// discarded branch not instantiated
|
||||
template <typename T>
|
||||
auto dereference(T val) {
|
||||
if constexpr(__is_pointer(T)) {
|
||||
return *val;
|
||||
} else {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
|
||||
void test() {
|
||||
int x = 42;
|
||||
[[maybe_unused]] int r1 = dereference(&x);
|
||||
[[maybe_unused]] int r2 = dereference(x);
|
||||
}
|
||||
|
||||
} // namespace if_constexpr_basic
|
||||
37
tests/corpus/statements/if/if_constexpr_requires.cpp
Normal file
37
tests/corpus/statements/if/if_constexpr_requires.cpp
Normal file
@@ -0,0 +1,37 @@
|
||||
// if constexpr with requires-expression
|
||||
namespace if_constexpr_requires {
|
||||
|
||||
template <typename T>
|
||||
int get_length(const T& x) {
|
||||
if constexpr(requires { x.size(); }) {
|
||||
return static_cast<int>(x.size());
|
||||
} else if constexpr(requires { x.length; }) {
|
||||
return x.length;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
struct WithLength {
|
||||
int length;
|
||||
};
|
||||
|
||||
struct Container {
|
||||
int data[4];
|
||||
|
||||
int size() const {
|
||||
return 4;
|
||||
}
|
||||
};
|
||||
|
||||
void test() {
|
||||
Container c{};
|
||||
[[maybe_unused]] int r1 = get_length(c);
|
||||
|
||||
WithLength wl{10};
|
||||
[[maybe_unused]] int r2 = get_length(wl);
|
||||
|
||||
[[maybe_unused]] int r3 = get_length(42);
|
||||
}
|
||||
|
||||
} // namespace if_constexpr_requires
|
||||
35
tests/corpus/statements/if/if_constexpr_variadic.cpp
Normal file
35
tests/corpus/statements/if/if_constexpr_variadic.cpp
Normal file
@@ -0,0 +1,35 @@
|
||||
// if constexpr with variadic templates
|
||||
namespace if_constexpr_variadic {
|
||||
|
||||
// recursive fold with if constexpr
|
||||
template <typename T, typename... Rest>
|
||||
constexpr T sum(T first, Rest... rest) {
|
||||
if constexpr(sizeof...(rest) == 0) {
|
||||
return first;
|
||||
} else {
|
||||
return first + sum(rest...);
|
||||
}
|
||||
}
|
||||
|
||||
static_assert(sum(1) == 1);
|
||||
static_assert(sum(1, 2, 3) == 6);
|
||||
static_assert(sum(1, 2, 3, 4, 5) == 15);
|
||||
|
||||
// process each element differently
|
||||
template <typename T>
|
||||
constexpr int count_one() {
|
||||
if constexpr(__is_integral(T)) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename... Ts>
|
||||
constexpr int count_integrals() {
|
||||
return (count_one<Ts>() + ...);
|
||||
}
|
||||
|
||||
static_assert(count_integrals<int, float, char, double>() == 2);
|
||||
|
||||
} // namespace if_constexpr_variadic
|
||||
47
tests/corpus/statements/if/if_declaration_condition.cpp
Normal file
47
tests/corpus/statements/if/if_declaration_condition.cpp
Normal file
@@ -0,0 +1,47 @@
|
||||
// if with declaration as condition
|
||||
namespace if_declaration {
|
||||
|
||||
struct Optional {
|
||||
int value;
|
||||
bool valid;
|
||||
|
||||
explicit operator bool() const {
|
||||
return valid;
|
||||
}
|
||||
};
|
||||
|
||||
Optional try_parse(int x) {
|
||||
if(x >= 0)
|
||||
return {x * 2, true};
|
||||
return {0, false};
|
||||
}
|
||||
|
||||
int use_declaration_condition(int x) {
|
||||
if(Optional result = try_parse(x)) {
|
||||
return result.value;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// pointer declaration as condition
|
||||
struct Node {
|
||||
int data;
|
||||
Node* next;
|
||||
};
|
||||
|
||||
int walk_list(Node* head) {
|
||||
int sum = 0;
|
||||
if(Node* p = head) {
|
||||
sum += p->data;
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
void test() {
|
||||
[[maybe_unused]] int r1 = use_declaration_condition(5);
|
||||
[[maybe_unused]] int r2 = use_declaration_condition(-1);
|
||||
Node n{42, nullptr};
|
||||
[[maybe_unused]] int r3 = walk_list(&n);
|
||||
}
|
||||
|
||||
} // namespace if_declaration
|
||||
49
tests/corpus/statements/if/if_init_statement.cpp
Normal file
49
tests/corpus/statements/if/if_init_statement.cpp
Normal file
@@ -0,0 +1,49 @@
|
||||
// C++17 if with init-statement
|
||||
namespace if_init {
|
||||
|
||||
struct Result {
|
||||
int value;
|
||||
bool ok;
|
||||
};
|
||||
|
||||
Result compute(int x) {
|
||||
if(x > 0)
|
||||
return {x * 10, true};
|
||||
return {0, false};
|
||||
}
|
||||
|
||||
int with_init(int x) {
|
||||
if(auto r = compute(x); r.ok) {
|
||||
return r.value;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// init-statement with array
|
||||
bool find_char(const char* str, char target) {
|
||||
if(int i = 0; str) {
|
||||
while(str[i]) {
|
||||
if(str[i] == target)
|
||||
return true;
|
||||
++i;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// init-statement scoping: variables not visible outside
|
||||
int scoping_demo(int x) {
|
||||
if(int doubled = x * 2; doubled > 10) {
|
||||
return doubled;
|
||||
} else {
|
||||
return doubled + 1; // doubled visible in else
|
||||
}
|
||||
}
|
||||
|
||||
void test() {
|
||||
[[maybe_unused]] int r1 = with_init(5);
|
||||
[[maybe_unused]] bool r2 = find_char("hello", 'l');
|
||||
[[maybe_unused]] int r3 = scoping_demo(3);
|
||||
}
|
||||
|
||||
} // namespace if_init
|
||||
46
tests/corpus/statements/if/if_init_structured_binding.cpp
Normal file
46
tests/corpus/statements/if/if_init_structured_binding.cpp
Normal file
@@ -0,0 +1,46 @@
|
||||
// if with init-statement using structured bindings
|
||||
namespace if_init_sb {
|
||||
|
||||
struct Pair {
|
||||
int first;
|
||||
int second;
|
||||
};
|
||||
|
||||
Pair divide(int a, int b) {
|
||||
if(b == 0)
|
||||
return {0, 0};
|
||||
return {a / b, a % b};
|
||||
}
|
||||
|
||||
int safe_divide(int a, int b) {
|
||||
if(auto [quot, rem] = divide(a, b); b != 0) {
|
||||
return quot;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
struct ParseResult {
|
||||
bool success;
|
||||
int value;
|
||||
const char* error;
|
||||
};
|
||||
|
||||
ParseResult parse(int x) {
|
||||
if(x < 0)
|
||||
return {false, 0, "negative"};
|
||||
return {true, x, nullptr};
|
||||
}
|
||||
|
||||
int try_parse(int x) {
|
||||
if(auto [ok, val, err] = parse(x); ok) {
|
||||
return val;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void test() {
|
||||
[[maybe_unused]] int r1 = safe_divide(10, 3);
|
||||
[[maybe_unused]] int r2 = try_parse(42);
|
||||
}
|
||||
|
||||
} // namespace if_init_sb
|
||||
40
tests/corpus/statements/if/if_null_statement.cpp
Normal file
40
tests/corpus/statements/if/if_null_statement.cpp
Normal file
@@ -0,0 +1,40 @@
|
||||
// if with null statement and single-statement bodies
|
||||
namespace if_null {
|
||||
|
||||
// null init-statement in C++17 if
|
||||
int with_null_init(int x) {
|
||||
if(; x > 0) {
|
||||
return x;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// single statement body (no braces)
|
||||
int single_stmt(int x) {
|
||||
if(x > 10)
|
||||
return x;
|
||||
if(x > 5)
|
||||
return x + 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// scope of variable declared in if body
|
||||
void scope_test() {
|
||||
if(true) {
|
||||
[[maybe_unused]] int x = 42;
|
||||
}
|
||||
// x is out of scope here
|
||||
|
||||
if(int y = 10; y > 0) {
|
||||
[[maybe_unused]] int z = y;
|
||||
}
|
||||
// y and z are out of scope here
|
||||
}
|
||||
|
||||
void test() {
|
||||
[[maybe_unused]] int r1 = with_null_init(5);
|
||||
[[maybe_unused]] int r2 = single_stmt(7);
|
||||
scope_test();
|
||||
}
|
||||
|
||||
} // namespace if_null
|
||||
3
tests/data/formatting/.clang-format
Normal file
3
tests/data/formatting/.clang-format
Normal file
@@ -0,0 +1,3 @@
|
||||
BasedOnStyle: LLVM
|
||||
IndentWidth: 4
|
||||
ColumnLimit: 80
|
||||
1
tests/data/formatting/main.cpp
Normal file
1
tests/data/formatting/main.cpp
Normal file
@@ -0,0 +1 @@
|
||||
int add(int a, int b) { return a + b; }
|
||||
0
tests/integration/agentic/__init__.py
Normal file
0
tests/integration/agentic/__init__.py
Normal file
592
tests/integration/agentic/test_agentic.py
Normal file
592
tests/integration/agentic/test_agentic.py
Normal file
@@ -0,0 +1,592 @@
|
||||
"""Tests for the agentic protocol handlers."""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import socket
|
||||
import subprocess
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.integration.utils.wait import wait_for_index
|
||||
|
||||
|
||||
class AgenticRpcClient:
|
||||
"""Minimal JSON-RPC client that speaks Content-Length framing over TCP."""
|
||||
|
||||
def __init__(self, host: str, port: int):
|
||||
self.sock = socket.create_connection((host, port), timeout=10)
|
||||
self.request_id = 0
|
||||
self.buffer = b""
|
||||
|
||||
def request(self, method: str, params: dict):
|
||||
self.request_id += 1
|
||||
body = json.dumps(
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": self.request_id,
|
||||
"method": method,
|
||||
"params": params,
|
||||
}
|
||||
)
|
||||
payload = f"Content-Length: {len(body)}\r\n\r\n{body}".encode("utf-8")
|
||||
self.sock.sendall(payload)
|
||||
return self._read_response()
|
||||
|
||||
def _read_response(self):
|
||||
while b"\r\n\r\n" not in self.buffer:
|
||||
data = self.sock.recv(4096)
|
||||
if not data:
|
||||
raise ConnectionError("connection closed")
|
||||
self.buffer += data
|
||||
|
||||
header_end = self.buffer.index(b"\r\n\r\n")
|
||||
headers = self.buffer[:header_end].decode("utf-8")
|
||||
self.buffer = self.buffer[header_end + 4 :]
|
||||
|
||||
content_length = 0
|
||||
for line in headers.split("\r\n"):
|
||||
if line.lower().startswith("content-length:"):
|
||||
content_length = int(line.split(":")[1].strip())
|
||||
|
||||
while len(self.buffer) < content_length:
|
||||
data = self.sock.recv(4096)
|
||||
if not data:
|
||||
raise ConnectionError("connection closed")
|
||||
self.buffer += data
|
||||
|
||||
body = self.buffer[:content_length].decode("utf-8")
|
||||
self.buffer = self.buffer[content_length:]
|
||||
return json.loads(body)
|
||||
|
||||
def close(self):
|
||||
self.sock.close()
|
||||
|
||||
|
||||
def run_agentic(executable, host, port, path, timeout=10):
|
||||
result = subprocess.run(
|
||||
[
|
||||
str(executable),
|
||||
"--mode",
|
||||
"agentic",
|
||||
"--host",
|
||||
host,
|
||||
"--port",
|
||||
str(port),
|
||||
"--path",
|
||||
path,
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout,
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@pytest.mark.workspace("hello_world")
|
||||
async def test_compile_command(agentic, workspace):
|
||||
executable, host, port = agentic
|
||||
main_cpp = (workspace / "main.cpp").as_posix()
|
||||
result = run_agentic(executable, host, port, main_cpp)
|
||||
assert result.returncode == 0, f"stderr: {result.stderr}"
|
||||
data = json.loads(result.stdout)
|
||||
assert data["file"] == main_cpp
|
||||
assert data["directory"] == workspace.as_posix()
|
||||
assert len(data["arguments"]) > 0
|
||||
|
||||
|
||||
@pytest.mark.workspace("hello_world")
|
||||
async def test_compile_command_fallback(agentic, workspace):
|
||||
executable, host, port = agentic
|
||||
result = run_agentic(executable, host, port, "/nonexistent/file.cpp")
|
||||
assert result.returncode == 0, f"stderr: {result.stderr}"
|
||||
data = json.loads(result.stdout)
|
||||
assert data["file"] == "/nonexistent/file.cpp"
|
||||
|
||||
|
||||
@pytest.mark.workspace("hello_world")
|
||||
async def test_multiple_requests(agentic, workspace):
|
||||
executable, host, port = agentic
|
||||
main_cpp = (workspace / "main.cpp").as_posix()
|
||||
for _ in range(3):
|
||||
result = run_agentic(executable, host, port, main_cpp)
|
||||
assert result.returncode == 0, f"stderr: {result.stderr}"
|
||||
data = json.loads(result.stdout)
|
||||
assert data["file"] == main_cpp
|
||||
|
||||
|
||||
async def test_connection_refused(executable):
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
s.bind(("127.0.0.1", 0))
|
||||
free_port = s.getsockname()[1]
|
||||
result = run_agentic(executable, "127.0.0.1", free_port, "/some/file.cpp")
|
||||
assert result.returncode != 0
|
||||
|
||||
|
||||
@pytest.mark.workspace("hello_world")
|
||||
async def test_concurrent_connections(agentic, workspace):
|
||||
executable, host, port = agentic
|
||||
main_cpp = (workspace / "main.cpp").as_posix()
|
||||
|
||||
def do_request(_):
|
||||
return run_agentic(executable, host, port, main_cpp)
|
||||
|
||||
with ThreadPoolExecutor(max_workers=4) as pool:
|
||||
results = list(pool.map(do_request, range(4)))
|
||||
|
||||
for r in results:
|
||||
assert r.returncode == 0, f"stderr: {r.stderr}"
|
||||
data = json.loads(r.stdout)
|
||||
assert data["file"] == main_cpp
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def indexed_agentic(request, executable, workspace):
|
||||
"""Start server with LSP+agentic, compile a file, wait for indexing."""
|
||||
from tests.integration.utils.client import CliceClient
|
||||
from tests.conftest import _shutdown_client, _find_free_port
|
||||
|
||||
host = "127.0.0.1"
|
||||
port = _find_free_port()
|
||||
cmd = [str(executable), "--mode", "pipe", "--host", host, "--port", str(port)]
|
||||
|
||||
c = CliceClient()
|
||||
await c.start_io(*cmd)
|
||||
|
||||
init_options = {"project": {"cache_dir": str(workspace / ".clice")}}
|
||||
await c.initialize(workspace, initialization_options=init_options)
|
||||
|
||||
uri, _ = await c.open_and_wait(workspace / "main.cpp")
|
||||
assert await wait_for_index(c, uri, "add"), "Index not ready"
|
||||
|
||||
rpc = AgenticRpcClient(host, port)
|
||||
|
||||
for _ in range(30):
|
||||
resp = rpc.request("agentic/symbolSearch", {"query": "add"})
|
||||
if "result" in resp and resp["result"]["symbols"]:
|
||||
break
|
||||
await asyncio.sleep(1)
|
||||
else:
|
||||
pytest.fail("agentic/symbolSearch never returned indexed symbols")
|
||||
|
||||
yield rpc, workspace
|
||||
|
||||
rpc.close()
|
||||
c.close(uri)
|
||||
await _shutdown_client(c)
|
||||
|
||||
|
||||
@pytest.mark.workspace("index_features")
|
||||
async def test_rpc_compile_command(indexed_agentic, workspace):
|
||||
rpc, _ = indexed_agentic
|
||||
path = (workspace / "main.cpp").as_posix()
|
||||
resp = rpc.request("agentic/compileCommand", {"path": path})
|
||||
assert "result" in resp, f"unexpected response: {resp}"
|
||||
result = resp["result"]
|
||||
assert result["file"] == path
|
||||
assert len(result["arguments"]) > 0
|
||||
|
||||
|
||||
@pytest.mark.workspace("index_features")
|
||||
async def test_rpc_project_files(indexed_agentic, workspace):
|
||||
rpc, _ = indexed_agentic
|
||||
resp = rpc.request("agentic/projectFiles", {})
|
||||
assert "result" in resp, f"unexpected response: {resp}"
|
||||
result = resp["result"]
|
||||
assert result["total"] > 0
|
||||
paths = [f["path"] for f in result["files"]]
|
||||
assert any("main.cpp" in p for p in paths)
|
||||
|
||||
|
||||
@pytest.mark.workspace("index_features")
|
||||
async def test_rpc_project_files_filter(indexed_agentic, workspace):
|
||||
rpc, _ = indexed_agentic
|
||||
resp = rpc.request("agentic/projectFiles", {"filter": "source"})
|
||||
assert "result" in resp
|
||||
for f in resp["result"]["files"]:
|
||||
assert f["kind"] == "source"
|
||||
|
||||
|
||||
@pytest.mark.workspace("index_features")
|
||||
async def test_rpc_symbol_search(indexed_agentic, workspace):
|
||||
rpc, _ = indexed_agentic
|
||||
resp = rpc.request("agentic/symbolSearch", {"query": "add"})
|
||||
assert "result" in resp, f"unexpected response: {resp}"
|
||||
symbols = resp["result"]["symbols"]
|
||||
add_sym = next((s for s in symbols if s["name"] == "add"), None)
|
||||
assert add_sym is not None, f"'add' not found in {[s['name'] for s in symbols]}"
|
||||
assert add_sym["kind"] == "Function"
|
||||
assert add_sym["line"] == 19
|
||||
assert add_sym["symbolId"] != 0
|
||||
assert "main.cpp" in add_sym["file"]
|
||||
|
||||
|
||||
@pytest.mark.workspace("index_features")
|
||||
async def test_rpc_symbol_search_kind(indexed_agentic, workspace):
|
||||
rpc, _ = indexed_agentic
|
||||
resp = rpc.request(
|
||||
"agentic/symbolSearch", {"query": "Animal", "kindFilter": ["Struct"]}
|
||||
)
|
||||
assert "result" in resp
|
||||
for s in resp["result"]["symbols"]:
|
||||
assert s["kind"] == "Struct"
|
||||
|
||||
|
||||
@pytest.mark.workspace("index_features")
|
||||
async def test_rpc_symbol_search_max(indexed_agentic, workspace):
|
||||
rpc, _ = indexed_agentic
|
||||
resp = rpc.request("agentic/symbolSearch", {"query": "", "maxResults": 3})
|
||||
assert "result" in resp
|
||||
assert len(resp["result"]["symbols"]) <= 3
|
||||
|
||||
|
||||
@pytest.mark.workspace("index_features")
|
||||
async def test_rpc_read_symbol(indexed_agentic, workspace):
|
||||
rpc, _ = indexed_agentic
|
||||
resp = rpc.request("agentic/readSymbol", {"name": "add"})
|
||||
assert "result" in resp, f"unexpected response: {resp}"
|
||||
result = resp["result"]
|
||||
assert result["name"] == "add"
|
||||
assert result["symbolId"] != 0
|
||||
assert result["startLine"] == 19
|
||||
assert result["endLine"] == 21
|
||||
assert "int add(int a, int b)" in result["text"]
|
||||
assert "return a + b;" in result["text"]
|
||||
|
||||
|
||||
@pytest.mark.workspace("index_features")
|
||||
async def test_rpc_read_symbol_by_id(indexed_agentic, workspace):
|
||||
rpc, _ = indexed_agentic
|
||||
resp1 = rpc.request("agentic/readSymbol", {"name": "add"})
|
||||
assert "result" in resp1
|
||||
sid = resp1["result"]["symbolId"]
|
||||
|
||||
resp2 = rpc.request("agentic/readSymbol", {"symbolId": sid})
|
||||
assert "result" in resp2
|
||||
assert resp2["result"]["name"] == "add"
|
||||
assert resp2["result"]["symbolId"] == sid
|
||||
|
||||
|
||||
@pytest.mark.workspace("index_features")
|
||||
async def test_rpc_document_symbols(indexed_agentic, workspace):
|
||||
rpc, _ = indexed_agentic
|
||||
path = (workspace / "main.cpp").as_posix()
|
||||
resp = rpc.request("agentic/documentSymbols", {"path": path})
|
||||
assert "result" in resp, f"unexpected response: {resp}"
|
||||
symbols = resp["result"]["symbols"]
|
||||
names = [s["name"] for s in symbols]
|
||||
kinds = [s["kind"] for s in symbols]
|
||||
assert "add" in names, f"expected 'add' in {names}"
|
||||
assert "main" in names, f"expected 'main' in {names}"
|
||||
assert "global_var" in names, f"expected 'global_var' in {names}"
|
||||
assert "Parameter" not in kinds, (
|
||||
f"Parameters should be filtered: {list(zip(names, kinds))}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.workspace("index_features")
|
||||
async def test_rpc_definition(indexed_agentic, workspace):
|
||||
rpc, _ = indexed_agentic
|
||||
resp = rpc.request("agentic/definition", {"name": "add"})
|
||||
assert "result" in resp, f"unexpected response: {resp}"
|
||||
result = resp["result"]
|
||||
assert result["name"] == "add"
|
||||
assert result["definition"] is not None
|
||||
defn = result["definition"]
|
||||
assert "main.cpp" in defn["file"]
|
||||
assert defn["startLine"] == 19
|
||||
assert defn["endLine"] == 21
|
||||
assert "int add(int a, int b)" in defn["text"]
|
||||
assert "return a + b;" in defn["text"]
|
||||
|
||||
|
||||
@pytest.mark.workspace("index_features")
|
||||
async def test_rpc_definition_by_position(indexed_agentic, workspace):
|
||||
rpc, _ = indexed_agentic
|
||||
path = (workspace / "main.cpp").as_posix()
|
||||
resp = rpc.request("agentic/definition", {"path": path, "line": 19})
|
||||
assert "result" in resp, f"unexpected response: {resp}"
|
||||
assert resp["result"]["name"] == "add"
|
||||
|
||||
|
||||
@pytest.mark.workspace("index_features")
|
||||
async def test_rpc_references(indexed_agentic, workspace):
|
||||
rpc, _ = indexed_agentic
|
||||
resp = rpc.request("agentic/references", {"name": "global_var"})
|
||||
assert "result" in resp, f"unexpected response: {resp}"
|
||||
result = resp["result"]
|
||||
assert result["name"] == "global_var"
|
||||
assert result["total"] == 2
|
||||
lines = sorted(r["line"] for r in result["references"])
|
||||
assert lines == [34, 38]
|
||||
contexts = [r["context"] for r in result["references"]]
|
||||
assert any("global_var + 1" in c for c in contexts)
|
||||
assert any("global_var * 2" in c for c in contexts)
|
||||
|
||||
|
||||
@pytest.mark.workspace("index_features")
|
||||
async def test_rpc_references_include_decl(indexed_agentic, workspace):
|
||||
rpc, _ = indexed_agentic
|
||||
resp = rpc.request(
|
||||
"agentic/references", {"name": "global_var", "includeDeclaration": True}
|
||||
)
|
||||
assert "result" in resp
|
||||
result = resp["result"]
|
||||
assert result["total"] == 3
|
||||
lines = sorted(r["line"] for r in result["references"])
|
||||
assert 31 in lines, f"expected declaration line 31 in {lines}"
|
||||
|
||||
|
||||
@pytest.mark.workspace("index_features")
|
||||
async def test_rpc_call_graph_incoming(indexed_agentic, workspace):
|
||||
rpc, _ = indexed_agentic
|
||||
resp = rpc.request("agentic/callGraph", {"name": "add", "direction": "callers"})
|
||||
assert "result" in resp, f"unexpected response: {resp}"
|
||||
result = resp["result"]
|
||||
assert result["root"]["name"] == "add"
|
||||
assert result["root"]["line"] == 19
|
||||
assert result["root"]["symbolId"] != 0
|
||||
callers = result["callers"]
|
||||
caller_names = [c["name"] for c in callers]
|
||||
assert "compute" in caller_names, f"expected 'compute' in {caller_names}"
|
||||
compute = next(c for c in callers if c["name"] == "compute")
|
||||
assert compute["line"] == 24
|
||||
assert compute["symbolId"] != 0
|
||||
assert result["callees"] == []
|
||||
|
||||
|
||||
@pytest.mark.workspace("index_features")
|
||||
async def test_rpc_call_graph_outgoing(indexed_agentic, workspace):
|
||||
rpc, _ = indexed_agentic
|
||||
resp = rpc.request("agentic/callGraph", {"name": "compute", "direction": "callees"})
|
||||
assert "result" in resp, f"unexpected response: {resp}"
|
||||
result = resp["result"]
|
||||
assert result["root"]["name"] == "compute"
|
||||
callees = result["callees"]
|
||||
callee_names = [c["name"] for c in callees]
|
||||
assert "add" in callee_names, f"expected 'add' in {callee_names}"
|
||||
add_entry = next(c for c in callees if c["name"] == "add")
|
||||
assert add_entry["line"] == 19
|
||||
assert result["callers"] == []
|
||||
|
||||
|
||||
@pytest.mark.workspace("index_features")
|
||||
async def test_rpc_type_hierarchy_supertypes(indexed_agentic, workspace):
|
||||
rpc, _ = indexed_agentic
|
||||
resp = rpc.request(
|
||||
"agentic/typeHierarchy", {"name": "Dog", "direction": "supertypes"}
|
||||
)
|
||||
assert "result" in resp, f"unexpected response: {resp}"
|
||||
result = resp["result"]
|
||||
assert result["root"]["name"] == "Dog"
|
||||
assert result["root"]["line"] == 9
|
||||
supertypes = result["supertypes"]
|
||||
supertype_names = [t["name"] for t in supertypes]
|
||||
assert "Animal" in supertype_names, f"expected 'Animal' in {supertype_names}"
|
||||
animal = next(t for t in supertypes if t["name"] == "Animal")
|
||||
assert animal["line"] == 2
|
||||
assert animal["symbolId"] != 0
|
||||
|
||||
|
||||
@pytest.mark.workspace("index_features")
|
||||
async def test_rpc_type_hierarchy_subtypes(indexed_agentic, workspace):
|
||||
rpc, _ = indexed_agentic
|
||||
resp = rpc.request(
|
||||
"agentic/typeHierarchy", {"name": "Animal", "direction": "subtypes"}
|
||||
)
|
||||
assert "result" in resp, f"unexpected response: {resp}"
|
||||
result = resp["result"]
|
||||
assert result["root"]["name"] == "Animal"
|
||||
assert result["root"]["line"] == 2
|
||||
subtypes = result["subtypes"]
|
||||
subtype_names = [t["name"] for t in subtypes]
|
||||
assert "Dog" in subtype_names, f"expected 'Dog' in {subtype_names}"
|
||||
assert "Cat" in subtype_names, f"expected 'Cat' in {subtype_names}"
|
||||
dog = next(t for t in subtypes if t["name"] == "Dog")
|
||||
assert dog["line"] == 9
|
||||
cat = next(t for t in subtypes if t["name"] == "Cat")
|
||||
assert cat["line"] == 14
|
||||
|
||||
|
||||
@pytest.mark.workspace("index_features")
|
||||
async def test_rpc_status(indexed_agentic, workspace):
|
||||
rpc, _ = indexed_agentic
|
||||
resp = rpc.request("agentic/status", {})
|
||||
assert "result" in resp, f"unexpected response: {resp}"
|
||||
result = resp["result"]
|
||||
assert isinstance(result["idle"], bool)
|
||||
assert result["total"] > 0
|
||||
assert isinstance(result["pending"], int)
|
||||
assert isinstance(result["indexed"], int)
|
||||
|
||||
|
||||
@pytest.mark.workspace("hello_world")
|
||||
async def test_rpc_shutdown(executable, workspace):
|
||||
"""Shutdown notification should cause the server to exit."""
|
||||
from tests.integration.utils.client import CliceClient
|
||||
from tests.conftest import _shutdown_client, _find_free_port
|
||||
|
||||
host = "127.0.0.1"
|
||||
port = _find_free_port()
|
||||
cmd = [str(executable), "--mode", "pipe", "--host", host, "--port", str(port)]
|
||||
|
||||
c = CliceClient()
|
||||
await c.start_io(*cmd)
|
||||
init_options = {"project": {"cache_dir": str(workspace / ".clice")}}
|
||||
await c.initialize(workspace, initialization_options=init_options)
|
||||
|
||||
rpc = AgenticRpcClient(host, port)
|
||||
body = json.dumps({"jsonrpc": "2.0", "method": "agentic/shutdown", "params": {}})
|
||||
rpc.sock.sendall(f"Content-Length: {len(body)}\r\n\r\n{body}".encode())
|
||||
rpc.sock.settimeout(5)
|
||||
try:
|
||||
rpc.sock.recv(4096)
|
||||
except (socket.timeout, OSError):
|
||||
pass
|
||||
rpc.sock.close()
|
||||
|
||||
import asyncio
|
||||
|
||||
for _ in range(20):
|
||||
if c._server.returncode is not None:
|
||||
break
|
||||
await asyncio.sleep(0.5)
|
||||
assert c._server.returncode is not None, "Server did not exit after shutdown"
|
||||
|
||||
|
||||
@pytest.mark.workspace("index_features")
|
||||
async def test_rpc_symbol_not_found(indexed_agentic, workspace):
|
||||
rpc, _ = indexed_agentic
|
||||
resp = rpc.request("agentic/definition", {"name": "nonexistent_symbol_xyz"})
|
||||
assert "error" in resp
|
||||
|
||||
|
||||
@pytest.mark.workspace("index_features")
|
||||
async def test_rpc_symbol_id_roundtrip(indexed_agentic, workspace):
|
||||
"""Search -> get symbolId -> definition -> verify consistency."""
|
||||
rpc, _ = indexed_agentic
|
||||
search = rpc.request("agentic/symbolSearch", {"query": "compute"})
|
||||
assert "result" in search
|
||||
symbols = search["result"]["symbols"]
|
||||
compute = next((s for s in symbols if s["name"] == "compute"), None)
|
||||
assert compute is not None, f"'compute' not found in {[s['name'] for s in symbols]}"
|
||||
|
||||
defn = rpc.request("agentic/definition", {"symbolId": compute["symbolId"]})
|
||||
assert "result" in defn
|
||||
assert defn["result"]["name"] == "compute"
|
||||
assert defn["result"]["symbolId"] == compute["symbolId"]
|
||||
|
||||
|
||||
@pytest.mark.workspace("index_features")
|
||||
async def test_rpc_file_deps(indexed_agentic, workspace):
|
||||
rpc, _ = indexed_agentic
|
||||
path = (workspace / "main.cpp").as_posix()
|
||||
resp = rpc.request("agentic/fileDeps", {"path": path})
|
||||
assert "result" in resp, f"unexpected response: {resp}"
|
||||
result = resp["result"]
|
||||
assert result["file"] == path
|
||||
assert isinstance(result["includes"], list)
|
||||
assert isinstance(result["includers"], list)
|
||||
|
||||
|
||||
@pytest.mark.workspace("index_features")
|
||||
async def test_rpc_file_deps_direction(indexed_agentic, workspace):
|
||||
rpc, _ = indexed_agentic
|
||||
path = (workspace / "main.cpp").as_posix()
|
||||
resp = rpc.request("agentic/fileDeps", {"path": path, "direction": "includes"})
|
||||
assert "result" in resp
|
||||
assert resp["result"]["includers"] == []
|
||||
|
||||
|
||||
@pytest.mark.workspace("index_features")
|
||||
async def test_rpc_file_deps_unknown(indexed_agentic, workspace):
|
||||
rpc, _ = indexed_agentic
|
||||
resp = rpc.request("agentic/fileDeps", {"path": "/nonexistent/file.cpp"})
|
||||
assert "result" in resp
|
||||
assert resp["result"]["includes"] == []
|
||||
assert resp["result"]["includers"] == []
|
||||
|
||||
|
||||
@pytest.mark.workspace("index_features")
|
||||
async def test_rpc_impact_analysis(indexed_agentic, workspace):
|
||||
rpc, _ = indexed_agentic
|
||||
path = (workspace / "main.cpp").as_posix()
|
||||
resp = rpc.request("agentic/impactAnalysis", {"path": path})
|
||||
assert "result" in resp, f"unexpected response: {resp}"
|
||||
result = resp["result"]
|
||||
assert isinstance(result["directDependents"], list)
|
||||
assert isinstance(result["transitiveDependents"], list)
|
||||
assert isinstance(result["affectedModules"], list)
|
||||
|
||||
|
||||
@pytest.mark.workspace("index_features")
|
||||
async def test_rpc_impact_analysis_unknown(indexed_agentic, workspace):
|
||||
rpc, _ = indexed_agentic
|
||||
resp = rpc.request("agentic/impactAnalysis", {"path": "/nonexistent/file.cpp"})
|
||||
assert "result" in resp
|
||||
assert resp["result"]["directDependents"] == []
|
||||
|
||||
|
||||
async def test_shutdown_during_indexing(executable, tmp_path):
|
||||
"""Shutdown during active background indexing must exit cleanly."""
|
||||
from tests.integration.utils.client import CliceClient
|
||||
from tests.conftest import _find_free_port
|
||||
|
||||
workspace = tmp_path / "ws"
|
||||
workspace.mkdir()
|
||||
|
||||
entries = []
|
||||
for i in range(20):
|
||||
src = workspace / f"file_{i}.cpp"
|
||||
src.write_text(
|
||||
f"struct Type_{i} {{ int v = {i}; void m() {{}} }};\n"
|
||||
f"int func_{i}(int x) {{ return x + {i}; }}\n"
|
||||
f"int caller_{i}() {{ return func_{i}({i}); }}\n"
|
||||
)
|
||||
entries.append(
|
||||
{
|
||||
"directory": workspace.as_posix(),
|
||||
"file": src.as_posix(),
|
||||
"arguments": ["clang++", "-std=c++17", "-fsyntax-only", src.as_posix()],
|
||||
}
|
||||
)
|
||||
|
||||
(workspace / "compile_commands.json").write_text(json.dumps(entries))
|
||||
|
||||
host = "127.0.0.1"
|
||||
port = _find_free_port()
|
||||
cmd = [str(executable), "--mode", "pipe", "--host", host, "--port", str(port)]
|
||||
|
||||
c = CliceClient()
|
||||
await c.start_io(*cmd)
|
||||
|
||||
init_options = {
|
||||
"project": {
|
||||
"cache_dir": str(workspace / ".clice"),
|
||||
"idle_timeout_ms": 0,
|
||||
}
|
||||
}
|
||||
await c.initialize(workspace, initialization_options=init_options)
|
||||
|
||||
# Give indexing a moment to start, then send shutdown
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
rpc = AgenticRpcClient(host, port)
|
||||
body = json.dumps({"jsonrpc": "2.0", "method": "agentic/shutdown", "params": {}})
|
||||
rpc.sock.sendall(f"Content-Length: {len(body)}\r\n\r\n{body}".encode())
|
||||
rpc.sock.settimeout(5)
|
||||
try:
|
||||
rpc.sock.recv(4096)
|
||||
except (socket.timeout, OSError):
|
||||
pass
|
||||
rpc.sock.close()
|
||||
|
||||
for _ in range(30):
|
||||
if c._server.returncode is not None:
|
||||
break
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
assert c._server.returncode is not None, "Server did not exit after shutdown"
|
||||
assert c._server.returncode >= 0, (
|
||||
f"Server crashed with signal {-c._server.returncode}"
|
||||
)
|
||||
189
tests/integration/agentic/test_cli.py
Normal file
189
tests/integration/agentic/test_cli.py
Normal file
@@ -0,0 +1,189 @@
|
||||
"""CLI-based tests for agentic mode — run clice --mode agentic as a subprocess."""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.integration.utils.wait import wait_for_index
|
||||
|
||||
|
||||
def run_cli(executable, host, port, method, **kwargs):
|
||||
cmd = [
|
||||
str(executable),
|
||||
"--mode",
|
||||
"agentic",
|
||||
"--host",
|
||||
host,
|
||||
"--port",
|
||||
str(port),
|
||||
"--method",
|
||||
method,
|
||||
]
|
||||
for k, v in kwargs.items():
|
||||
cmd.extend([f"--{k}", str(v)])
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
|
||||
return result
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def indexed_server(request, executable, workspace):
|
||||
"""Start server with LSP+agentic, compile a file, wait for indexing."""
|
||||
import asyncio
|
||||
from tests.integration.utils.client import CliceClient
|
||||
from tests.conftest import _shutdown_client, _find_free_port
|
||||
|
||||
host = "127.0.0.1"
|
||||
port = _find_free_port()
|
||||
cmd = [str(executable), "--mode", "pipe", "--host", host, "--port", str(port)]
|
||||
|
||||
c = CliceClient()
|
||||
await c.start_io(*cmd)
|
||||
|
||||
init_options = {"project": {"cache_dir": str(workspace / ".clice")}}
|
||||
await c.initialize(workspace, initialization_options=init_options)
|
||||
|
||||
uri, _ = await c.open_and_wait(workspace / "main.cpp")
|
||||
assert await wait_for_index(c, uri, "add"), "Index not ready"
|
||||
|
||||
from tests.integration.agentic.test_agentic import AgenticRpcClient
|
||||
|
||||
rpc = AgenticRpcClient(host, port)
|
||||
for _ in range(30):
|
||||
resp = rpc.request("agentic/symbolSearch", {"query": "add"})
|
||||
if "result" in resp and resp["result"]["symbols"]:
|
||||
break
|
||||
await asyncio.sleep(1)
|
||||
rpc.close()
|
||||
|
||||
yield executable, host, port, workspace
|
||||
|
||||
c.close(uri)
|
||||
await _shutdown_client(c)
|
||||
|
||||
|
||||
@pytest.mark.workspace("index_features")
|
||||
async def test_cli_compile_command(indexed_server, workspace):
|
||||
exe, host, port, _ = indexed_server
|
||||
path = (workspace / "main.cpp").as_posix()
|
||||
r = run_cli(exe, host, port, "compileCommand", path=path)
|
||||
assert r.returncode == 0, f"stderr: {r.stderr}"
|
||||
data = json.loads(r.stdout)
|
||||
assert data["file"] == path
|
||||
assert len(data["arguments"]) > 0
|
||||
|
||||
|
||||
@pytest.mark.workspace("index_features")
|
||||
async def test_cli_symbol_search(indexed_server, workspace):
|
||||
exe, host, port, _ = indexed_server
|
||||
r = run_cli(exe, host, port, "symbolSearch", query="add")
|
||||
assert r.returncode == 0, f"stderr: {r.stderr}"
|
||||
data = json.loads(r.stdout)
|
||||
names = [s["name"] for s in data["symbols"]]
|
||||
assert "add" in names
|
||||
add_sym = next(s for s in data["symbols"] if s["name"] == "add")
|
||||
assert add_sym["kind"] == "Function"
|
||||
assert add_sym["line"] == 19
|
||||
|
||||
|
||||
@pytest.mark.workspace("index_features")
|
||||
async def test_cli_definition(indexed_server, workspace):
|
||||
exe, host, port, _ = indexed_server
|
||||
r = run_cli(exe, host, port, "definition", name="add")
|
||||
assert r.returncode == 0, f"stderr: {r.stderr}"
|
||||
data = json.loads(r.stdout)
|
||||
assert data["name"] == "add"
|
||||
defn = data["definition"]
|
||||
assert defn["startLine"] == 19
|
||||
assert defn["endLine"] == 21
|
||||
assert "return a + b;" in defn["text"]
|
||||
|
||||
|
||||
@pytest.mark.workspace("index_features")
|
||||
async def test_cli_definition_by_position(indexed_server, workspace):
|
||||
exe, host, port, _ = indexed_server
|
||||
path = (workspace / "main.cpp").as_posix()
|
||||
r = run_cli(exe, host, port, "definition", path=path, line=19)
|
||||
assert r.returncode == 0, f"stderr: {r.stderr}"
|
||||
data = json.loads(r.stdout)
|
||||
assert data["name"] == "add"
|
||||
|
||||
|
||||
@pytest.mark.workspace("index_features")
|
||||
async def test_cli_references(indexed_server, workspace):
|
||||
exe, host, port, _ = indexed_server
|
||||
r = run_cli(exe, host, port, "references", name="global_var")
|
||||
assert r.returncode == 0, f"stderr: {r.stderr}"
|
||||
data = json.loads(r.stdout)
|
||||
assert data["name"] == "global_var"
|
||||
assert data["total"] == 2
|
||||
lines = sorted(ref["line"] for ref in data["references"])
|
||||
assert lines == [34, 38]
|
||||
|
||||
|
||||
@pytest.mark.workspace("index_features")
|
||||
async def test_cli_read_symbol(indexed_server, workspace):
|
||||
exe, host, port, _ = indexed_server
|
||||
r = run_cli(exe, host, port, "readSymbol", name="compute")
|
||||
assert r.returncode == 0, f"stderr: {r.stderr}"
|
||||
data = json.loads(r.stdout)
|
||||
assert data["name"] == "compute"
|
||||
assert "add(1, 2)" in data["text"]
|
||||
|
||||
|
||||
@pytest.mark.workspace("index_features")
|
||||
async def test_cli_document_symbols(indexed_server, workspace):
|
||||
exe, host, port, _ = indexed_server
|
||||
path = (workspace / "main.cpp").as_posix()
|
||||
r = run_cli(exe, host, port, "documentSymbols", path=path)
|
||||
assert r.returncode == 0, f"stderr: {r.stderr}"
|
||||
data = json.loads(r.stdout)
|
||||
names = [s["name"] for s in data["symbols"]]
|
||||
assert "add" in names
|
||||
assert "main" in names
|
||||
assert "global_var" in names
|
||||
|
||||
|
||||
@pytest.mark.workspace("index_features")
|
||||
async def test_cli_call_graph(indexed_server, workspace):
|
||||
exe, host, port, _ = indexed_server
|
||||
r = run_cli(exe, host, port, "callGraph", name="add", direction="callers")
|
||||
assert r.returncode == 0, f"stderr: {r.stderr}"
|
||||
data = json.loads(r.stdout)
|
||||
assert data["root"]["name"] == "add"
|
||||
caller_names = [c["name"] for c in data["callers"]]
|
||||
assert "compute" in caller_names
|
||||
|
||||
|
||||
@pytest.mark.workspace("index_features")
|
||||
async def test_cli_type_hierarchy(indexed_server, workspace):
|
||||
exe, host, port, _ = indexed_server
|
||||
r = run_cli(exe, host, port, "typeHierarchy", name="Dog", direction="supertypes")
|
||||
assert r.returncode == 0, f"stderr: {r.stderr}"
|
||||
data = json.loads(r.stdout)
|
||||
assert data["root"]["name"] == "Dog"
|
||||
supertype_names = [t["name"] for t in data["supertypes"]]
|
||||
assert "Animal" in supertype_names
|
||||
|
||||
|
||||
@pytest.mark.workspace("index_features")
|
||||
async def test_cli_project_files(indexed_server, workspace):
|
||||
exe, host, port, _ = indexed_server
|
||||
r = run_cli(exe, host, port, "projectFiles")
|
||||
assert r.returncode == 0, f"stderr: {r.stderr}"
|
||||
data = json.loads(r.stdout)
|
||||
assert data["total"] > 0
|
||||
paths = [f["path"] for f in data["files"]]
|
||||
assert any("main.cpp" in p for p in paths)
|
||||
|
||||
|
||||
@pytest.mark.workspace("index_features")
|
||||
async def test_cli_status(indexed_server, workspace):
|
||||
exe, host, port, _ = indexed_server
|
||||
r = run_cli(exe, host, port, "status")
|
||||
assert r.returncode == 0, f"stderr: {r.stderr}"
|
||||
data = json.loads(r.stdout)
|
||||
assert isinstance(data["idle"], bool)
|
||||
assert data["total"] > 0
|
||||
assert isinstance(data["pending"], int)
|
||||
assert isinstance(data["indexed"], int)
|
||||
74
tests/integration/features/test_formatting.py
Normal file
74
tests/integration/features/test_formatting.py
Normal file
@@ -0,0 +1,74 @@
|
||||
import pytest
|
||||
from lsprotocol.types import Position, Range
|
||||
|
||||
from tests.integration.utils.workspace import did_change
|
||||
|
||||
UNFORMATTED = "int add( int a , int b ) {\nreturn a+b ;\n}\n"
|
||||
FORMATTED = "int add(int a, int b) { return a + b; }\n"
|
||||
|
||||
|
||||
def apply_edits(text, edits):
|
||||
"""Apply LSP TextEdits to a string, processing from end to start."""
|
||||
lines = text.split("\n")
|
||||
for edit in sorted(
|
||||
edits, key=lambda e: (e.range.start.line, e.range.start.character), reverse=True
|
||||
):
|
||||
start = edit.range.start
|
||||
end = edit.range.end
|
||||
before = (
|
||||
"\n".join(lines[: start.line])
|
||||
+ ("\n" if start.line > 0 else "")
|
||||
+ lines[start.line][: start.character]
|
||||
)
|
||||
after = (
|
||||
lines[end.line][end.character :]
|
||||
+ ("\n" if end.line < len(lines) - 1 else "")
|
||||
+ "\n".join(lines[end.line + 1 :])
|
||||
)
|
||||
text = before + edit.new_text + after
|
||||
lines = text.split("\n")
|
||||
return text
|
||||
|
||||
|
||||
@pytest.mark.workspace("formatting")
|
||||
async def test_format_document(client, workspace):
|
||||
uri, _ = await client.open_and_wait(workspace / "main.cpp")
|
||||
|
||||
did_change(client, uri, 1, UNFORMATTED)
|
||||
edits = await client.format_document(uri)
|
||||
|
||||
assert edits is not None
|
||||
assert len(edits) > 0
|
||||
result = apply_edits(UNFORMATTED, edits)
|
||||
assert result == FORMATTED
|
||||
|
||||
client.close(uri)
|
||||
|
||||
|
||||
@pytest.mark.workspace("formatting")
|
||||
async def test_format_range(client, workspace):
|
||||
uri, _ = await client.open_and_wait(workspace / "main.cpp")
|
||||
|
||||
did_change(client, uri, 1, UNFORMATTED)
|
||||
edits = await client.format_range(
|
||||
uri,
|
||||
Range(start=Position(line=1, character=0), end=Position(line=2, character=0)),
|
||||
)
|
||||
|
||||
assert edits is not None
|
||||
assert len(edits) > 0
|
||||
|
||||
client.close(uri)
|
||||
|
||||
|
||||
@pytest.mark.workspace("formatting")
|
||||
async def test_format_already_formatted(client, workspace):
|
||||
uri, _ = await client.open_and_wait(workspace / "main.cpp")
|
||||
|
||||
did_change(client, uri, 1, FORMATTED)
|
||||
edits = await client.format_document(uri)
|
||||
|
||||
assert edits is not None
|
||||
assert len(edits) == 0
|
||||
|
||||
client.close(uri)
|
||||
@@ -34,6 +34,8 @@ async def test_capabilities(client, workspace):
|
||||
assert capability_enabled(caps.folding_range_provider)
|
||||
assert capability_enabled(caps.inlay_hint_provider)
|
||||
assert capability_enabled(caps.code_action_provider)
|
||||
assert caps.document_formatting_provider is True
|
||||
assert caps.document_range_formatting_provider is True
|
||||
assert caps.semantic_tokens_provider is not None
|
||||
|
||||
|
||||
|
||||
@@ -16,9 +16,12 @@ from lsprotocol.types import (
|
||||
Diagnostic,
|
||||
DidCloseTextDocumentParams,
|
||||
DidOpenTextDocumentParams,
|
||||
DocumentFormattingParams,
|
||||
DocumentLinkParams,
|
||||
DocumentRangeFormattingParams,
|
||||
DocumentSymbolParams,
|
||||
FoldingRangeParams,
|
||||
FormattingOptions,
|
||||
HoverParams,
|
||||
InlayHintParams,
|
||||
InitializeParams,
|
||||
@@ -92,13 +95,18 @@ class CliceClient(BaseLanguageClient):
|
||||
*,
|
||||
initialization_options: dict | None = None,
|
||||
) -> InitializeResult:
|
||||
if initialization_options is None:
|
||||
initialization_options = {}
|
||||
project = dict(initialization_options.get("project", {}))
|
||||
project.setdefault("cache_dir", str(workspace / ".clice"))
|
||||
initialization_options["project"] = project
|
||||
|
||||
params = InitializeParams(
|
||||
capabilities=ClientCapabilities(),
|
||||
root_uri=workspace.as_uri(),
|
||||
workspace_folders=[WorkspaceFolder(uri=workspace.as_uri(), name="test")],
|
||||
)
|
||||
if initialization_options is not None:
|
||||
params.initialization_options = initialization_options
|
||||
params.initialization_options = initialization_options
|
||||
result = await self.initialize_async(params)
|
||||
self.initialized(InitializedParams())
|
||||
self.init_result = result
|
||||
@@ -307,6 +315,29 @@ class CliceClient(BaseLanguageClient):
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
async def format_document(self, uri: str, *, timeout: float = 30.0):
|
||||
return await asyncio.wait_for(
|
||||
self.text_document_formatting_async(
|
||||
DocumentFormattingParams(
|
||||
text_document=TextDocumentIdentifier(uri=uri),
|
||||
options=FormattingOptions(tab_size=4, insert_spaces=True),
|
||||
)
|
||||
),
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
async def format_range(self, uri: str, range_: Range, *, timeout: float = 30.0):
|
||||
return await asyncio.wait_for(
|
||||
self.text_document_range_formatting_async(
|
||||
DocumentRangeFormattingParams(
|
||||
text_document=TextDocumentIdentifier(uri=uri),
|
||||
range=range_,
|
||||
options=FormattingOptions(tab_size=4, insert_spaces=True),
|
||||
)
|
||||
),
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
# ── Extension protocol ───────────────────────────────────────────
|
||||
|
||||
async def query_context(self, uri: str, *, timeout: float = 30.0):
|
||||
|
||||
@@ -13,6 +13,9 @@ import re
|
||||
import signal
|
||||
import sys
|
||||
import time
|
||||
|
||||
# Force line-buffered stdout so CI sees output immediately.
|
||||
sys.stdout.reconfigure(line_buffering=True)
|
||||
from pathlib import Path
|
||||
from urllib.parse import quote, unquote
|
||||
|
||||
@@ -109,7 +112,9 @@ async def write_lsp_message(writer: asyncio.StreamWriter, payload: str):
|
||||
await writer.drain()
|
||||
|
||||
|
||||
async def replay_one(trace_path: Path, clice_bin: Path, timeout: int) -> bool | None:
|
||||
async def replay_one(
|
||||
trace_path: Path, clice_bin: Path, timeout: int, wall_timeout: int = 300
|
||||
) -> bool | None:
|
||||
"""Replay a single trace. Returns True=PASS, False=FAIL, None=SKIP."""
|
||||
records = load_trace(trace_path)
|
||||
if not records:
|
||||
@@ -179,8 +184,21 @@ async def replay_one(trace_path: Path, clice_bin: Path, timeout: int) -> bool |
|
||||
last_method = None
|
||||
sent_count = 0
|
||||
|
||||
wall_deadline = wall_start + wall_timeout
|
||||
|
||||
def remaining_wall():
|
||||
return max(0, wall_deadline - time.monotonic())
|
||||
|
||||
try:
|
||||
for i, rec in enumerate(records):
|
||||
if remaining_wall() <= 0:
|
||||
elapsed = time.monotonic() - wall_start
|
||||
print(
|
||||
f" result: TIMEOUT (wall-clock {wall_timeout}s exceeded, {elapsed:.1f}s)"
|
||||
)
|
||||
success = False
|
||||
break
|
||||
|
||||
if i > 0:
|
||||
delay = rec["ts"] - records[i - 1]["ts"]
|
||||
if delay > 0:
|
||||
@@ -196,7 +214,7 @@ async def replay_one(trace_path: Path, clice_bin: Path, timeout: int) -> bool |
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
asyncio.gather(*pending.values(), return_exceptions=True),
|
||||
timeout=timeout,
|
||||
timeout=min(timeout, remaining_wall()),
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
elapsed = time.monotonic() - wall_start
|
||||
@@ -210,7 +228,19 @@ async def replay_one(trace_path: Path, clice_bin: Path, timeout: int) -> bool |
|
||||
if msg_id is not None and method is not None:
|
||||
pending[msg_id] = asyncio.get_event_loop().create_future()
|
||||
|
||||
await write_lsp_message(proc.stdin, rec["msg"])
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
write_lsp_message(proc.stdin, rec["msg"]),
|
||||
timeout=min(30, remaining_wall()),
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
elapsed = time.monotonic() - wall_start
|
||||
print(
|
||||
f" result: HANG (write blocked at {last_method},"
|
||||
f" sent={sent_count}/{len(records)}, {elapsed:.1f}s)"
|
||||
)
|
||||
success = False
|
||||
break
|
||||
sent_count = i + 1
|
||||
|
||||
except (ConnectionError, BrokenPipeError):
|
||||
@@ -231,7 +261,7 @@ async def replay_one(trace_path: Path, clice_bin: Path, timeout: int) -> bool |
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
asyncio.gather(*pending.values(), return_exceptions=True),
|
||||
timeout=timeout,
|
||||
timeout=min(timeout, remaining_wall()),
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
elapsed = time.monotonic() - wall_start
|
||||
@@ -294,7 +324,7 @@ async def async_main(args):
|
||||
print(f"SKIP: {trace} (not found)")
|
||||
skipped += 1
|
||||
continue
|
||||
result = await replay_one(trace, args.clice, args.timeout)
|
||||
result = await replay_one(trace, args.clice, args.timeout, args.wall_timeout)
|
||||
if result is None:
|
||||
skipped += 1
|
||||
elif result:
|
||||
@@ -317,7 +347,16 @@ def main():
|
||||
p.add_argument("traces", nargs="+", type=Path, help="JSONL trace files")
|
||||
p.add_argument("--clice", required=True, type=Path, help="Path to clice binary")
|
||||
p.add_argument(
|
||||
"--timeout", type=int, default=120, help="Timeout in seconds (default: 120)"
|
||||
"--timeout",
|
||||
type=int,
|
||||
default=120,
|
||||
help="Per-request timeout in seconds (default: 120)",
|
||||
)
|
||||
p.add_argument(
|
||||
"--wall-timeout",
|
||||
type=int,
|
||||
default=300,
|
||||
help="Max wall-clock time per trace in seconds (default: 300)",
|
||||
)
|
||||
args = p.parse_args()
|
||||
sys.exit(asyncio.run(async_main(args)))
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
source: document_symbol_tests.cpp
|
||||
created_at: 2026-05-20
|
||||
input_file: statements/if/basic_if.cpp
|
||||
---
|
||||
- { name: "basic_if", kind: Namespace, range: "1:0-35:1", selection_range: "1:10-1:18" }
|
||||
- { name: "abs_val", kind: Function, range: "3:0-7:1", selection_range: "3:4-3:11", detail: "int (int)" }
|
||||
- { name: "sign", kind: Function, range: "9:0-17:1", selection_range: "9:12-9:16", detail: "const char *(int)" }
|
||||
- { name: "nested_if", kind: Function, range: "20:0-27:1", selection_range: "20:4-20:13", detail: "int (int, int)" }
|
||||
- { name: "test", kind: Function, range: "29:0-33:1", selection_range: "29:5-29:9", detail: "void ()" }
|
||||
- { name: "r1", kind: Variable, range: "30:21-30:41", selection_range: "30:25-30:27", detail: "int" }
|
||||
- { name: "r2", kind: Variable, range: "31:21-31:38", selection_range: "31:26-31:28", detail: "const char *" }
|
||||
- { name: "r3", kind: Variable, range: "32:21-32:46", selection_range: "32:25-32:27", detail: "int" }
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
source: document_symbol_tests.cpp
|
||||
created_at: 2026-05-24
|
||||
input_file: statements/if/if_chain_patterns.cpp
|
||||
---
|
||||
- { name: "if_chain", kind: Namespace, range: "1:0-48:1", selection_range: "1:10-1:18" }
|
||||
- { name: "classify_char", kind: Function, range: "3:0-12:1", selection_range: "3:4-3:17", detail: "int (char)" }
|
||||
- { name: "validate", kind: Function, range: "15:0-25:1", selection_range: "15:5-15:13", detail: "bool (int, int, int)" }
|
||||
- { name: "process", kind: Function, range: "28:0-39:1", selection_range: "28:4-28:11", detail: "int (int *, int)" }
|
||||
- { name: "sum", kind: Variable, range: "29:4-29:15", selection_range: "29:8-29:11", detail: "int" }
|
||||
- { name: "i", kind: Variable, range: "30:8-30:17", selection_range: "30:12-30:13", detail: "int" }
|
||||
- { name: "test", kind: Function, range: "41:0-46:1", selection_range: "41:5-41:9", detail: "void ()" }
|
||||
- { name: "r1", kind: Variable, range: "42:21-42:48", selection_range: "42:25-42:27", detail: "int" }
|
||||
- { name: "r2", kind: Variable, range: "43:21-43:48", selection_range: "43:26-43:28", detail: "bool" }
|
||||
- { name: "arr", kind: Variable, range: "44:4-44:31", selection_range: "44:8-44:11", detail: "int[4]" }
|
||||
- { name: "r3", kind: Variable, range: "45:21-45:45", selection_range: "45:25-45:27", detail: "int" }
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
source: document_symbol_tests.cpp
|
||||
created_at: 2026-05-24
|
||||
input_file: statements/if/if_consteval.cpp
|
||||
---
|
||||
- { name: "if_consteval", kind: Namespace, range: "1:0-45:1", selection_range: "1:10-1:22" }
|
||||
- { name: "compute", kind: Function, range: "3:0-9:1", selection_range: "3:14-3:21", detail: "int (int)" }
|
||||
- { name: "runtime_prefer", kind: Function, range: "12:0-18:1", selection_range: "12:14-12:28", detail: "int (int)" }
|
||||
- { name: "ct_only", kind: Function, range: "21:0-23:1", selection_range: "21:14-21:21", detail: "int (int)" }
|
||||
- { name: "dispatch", kind: Function, range: "25:0-31:1", selection_range: "25:14-25:22", detail: "int (int)" }
|
||||
- { name: "maybe_optimize", kind: Function, range: "34:0-39:1", selection_range: "34:14-34:28", detail: "int (int)" }
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
source: document_symbol_tests.cpp
|
||||
created_at: 2026-05-24
|
||||
input_file: statements/if/if_constexpr_basic.cpp
|
||||
---
|
||||
- { name: "if_constexpr_basic", kind: Namespace, range: "1:0-37:1", selection_range: "1:10-1:28" }
|
||||
- { name: "type_rank", kind: Function, range: "4:0-14:1", selection_range: "4:14-4:23", detail: "template int ()" }
|
||||
- { name: "dereference", kind: Function, range: "23:0-29:1", selection_range: "23:5-23:16", detail: "template auto (T)" }
|
||||
- { name: "test", kind: Function, range: "31:0-35:1", selection_range: "31:5-31:9", detail: "void ()" }
|
||||
- { name: "x", kind: Variable, range: "32:4-32:14", selection_range: "32:8-32:9", detail: "int" }
|
||||
- { name: "r1", kind: Variable, range: "33:21-33:45", selection_range: "33:25-33:27", detail: "int" }
|
||||
- { name: "r2", kind: Variable, range: "34:21-34:44", selection_range: "34:25-34:27", detail: "int" }
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
source: document_symbol_tests.cpp
|
||||
created_at: 2026-05-24
|
||||
input_file: statements/if/if_constexpr_requires.cpp
|
||||
---
|
||||
- { name: "if_constexpr_requires", kind: Namespace, range: "1:0-36:1", selection_range: "1:10-1:31" }
|
||||
- { name: "get_length", kind: Function, range: "4:0-12:1", selection_range: "4:4-4:14", detail: "template int (const T &)" }
|
||||
- { name: "WithLength", kind: Struct, range: "14:0-16:1", selection_range: "14:7-14:17", detail: "struct" }
|
||||
- { name: "length", kind: Field, range: "15:4-15:14", selection_range: "15:8-15:14", detail: "int" }
|
||||
- { name: "Container", kind: Struct, range: "18:0-24:1", selection_range: "18:7-18:16", detail: "struct" }
|
||||
- { name: "data", kind: Field, range: "19:4-19:15", selection_range: "19:8-19:12", detail: "int[4]" }
|
||||
- { name: "size", kind: Method, range: "21:4-23:5", selection_range: "21:8-21:12", detail: "int () const" }
|
||||
- { name: "test", kind: Function, range: "26:0-34:1", selection_range: "26:5-26:9", detail: "void ()" }
|
||||
- { name: "c", kind: Variable, range: "27:4-27:17", selection_range: "27:14-27:15", detail: "Container" }
|
||||
- { name: "r1", kind: Variable, range: "28:21-28:43", selection_range: "28:25-28:27", detail: "int" }
|
||||
- { name: "wl", kind: Variable, range: "30:4-30:21", selection_range: "30:15-30:17", detail: "WithLength" }
|
||||
- { name: "r2", kind: Variable, range: "31:21-31:44", selection_range: "31:25-31:27", detail: "int" }
|
||||
- { name: "r3", kind: Variable, range: "33:21-33:44", selection_range: "33:25-33:27", detail: "int" }
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
source: document_symbol_tests.cpp
|
||||
created_at: 2026-05-24
|
||||
input_file: statements/if/if_constexpr_variadic.cpp
|
||||
---
|
||||
- { name: "if_constexpr_variadic", kind: Namespace, range: "1:0-34:1", selection_range: "1:10-1:31" }
|
||||
- { name: "sum", kind: Function, range: "5:0-11:1", selection_range: "5:12-5:15", detail: "template T (T, Rest...)" }
|
||||
- { name: "count_one", kind: Function, range: "19:0-25:1", selection_range: "19:14-19:23", detail: "template int ()" }
|
||||
- { name: "count_integrals", kind: Function, range: "28:0-30:1", selection_range: "28:14-28:29", detail: "template int ()" }
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
source: document_symbol_tests.cpp
|
||||
created_at: 2026-05-24
|
||||
input_file: statements/if/if_declaration_condition.cpp
|
||||
---
|
||||
- { name: "if_declaration", kind: Namespace, range: "1:0-46:1", selection_range: "1:10-1:24" }
|
||||
- { name: "Optional", kind: Struct, range: "3:0-10:1", selection_range: "3:7-3:15", detail: "struct" }
|
||||
- { name: "value", kind: Field, range: "4:4-4:13", selection_range: "4:8-4:13", detail: "int" }
|
||||
- { name: "valid", kind: Field, range: "5:4-5:14", selection_range: "5:9-5:14", detail: "bool" }
|
||||
- { name: "operator bool", kind: Method, range: "7:4-9:5", selection_range: "7:13-7:21", detail: "bool () const" }
|
||||
- { name: "try_parse", kind: Function, range: "12:0-16:1", selection_range: "12:9-12:18", detail: "Optional (int)" }
|
||||
- { name: "use_declaration_condition", kind: Function, range: "18:0-23:1", selection_range: "18:4-18:29", detail: "int (int)" }
|
||||
- { name: "result", kind: Variable, range: "19:7-19:37", selection_range: "19:16-19:22", detail: "Optional" }
|
||||
- { name: "Node", kind: Struct, range: "26:0-29:1", selection_range: "26:7-26:11", detail: "struct" }
|
||||
- { name: "data", kind: Field, range: "27:4-27:12", selection_range: "27:8-27:12", detail: "int" }
|
||||
- { name: "next", kind: Field, range: "28:4-28:14", selection_range: "28:10-28:14", detail: "Node *" }
|
||||
- { name: "walk_list", kind: Function, range: "31:0-37:1", selection_range: "31:4-31:13", detail: "int (Node *)" }
|
||||
- { name: "sum", kind: Variable, range: "32:4-32:15", selection_range: "32:8-32:11", detail: "int" }
|
||||
- { name: "p", kind: Variable, range: "33:7-33:21", selection_range: "33:13-33:14", detail: "Node *" }
|
||||
- { name: "test", kind: Function, range: "39:0-44:1", selection_range: "39:5-39:9", detail: "void ()" }
|
||||
- { name: "r1", kind: Variable, range: "40:21-40:58", selection_range: "40:25-40:27", detail: "int" }
|
||||
- { name: "r2", kind: Variable, range: "41:21-41:59", selection_range: "41:25-41:27", detail: "int" }
|
||||
- { name: "n", kind: Variable, range: "42:4-42:23", selection_range: "42:9-42:10", detail: "Node" }
|
||||
- { name: "r3", kind: Variable, range: "43:21-43:43", selection_range: "43:25-43:27", detail: "int" }
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
source: document_symbol_tests.cpp
|
||||
created_at: 2026-05-24
|
||||
input_file: statements/if/if_init_statement.cpp
|
||||
---
|
||||
- { name: "if_init", kind: Namespace, range: "1:0-48:1", selection_range: "1:10-1:17" }
|
||||
- { name: "Result", kind: Struct, range: "3:0-6:1", selection_range: "3:7-3:13", detail: "struct" }
|
||||
- { name: "value", kind: Field, range: "4:4-4:13", selection_range: "4:8-4:13", detail: "int" }
|
||||
- { name: "ok", kind: Field, range: "5:4-5:11", selection_range: "5:9-5:11", detail: "bool" }
|
||||
- { name: "compute", kind: Function, range: "8:0-12:1", selection_range: "8:7-8:14", detail: "Result (int)" }
|
||||
- { name: "with_init", kind: Function, range: "14:0-19:1", selection_range: "14:4-14:13", detail: "int (int)" }
|
||||
- { name: "r", kind: Variable, range: "15:7-15:26", selection_range: "15:12-15:13", detail: "Result" }
|
||||
- { name: "find_char", kind: Function, range: "22:0-31:1", selection_range: "22:5-22:14", detail: "bool (const char *, char)" }
|
||||
- { name: "i", kind: Variable, range: "23:7-23:16", selection_range: "23:11-23:12", detail: "int" }
|
||||
- { name: "scoping_demo", kind: Function, range: "34:0-40:1", selection_range: "34:4-34:16", detail: "int (int)" }
|
||||
- { name: "doubled", kind: Variable, range: "35:7-35:26", selection_range: "35:11-35:18", detail: "int" }
|
||||
- { name: "test", kind: Function, range: "42:0-46:1", selection_range: "42:5-42:9", detail: "void ()" }
|
||||
- { name: "r1", kind: Variable, range: "43:21-43:42", selection_range: "43:25-43:27", detail: "int" }
|
||||
- { name: "r2", kind: Variable, range: "44:21-44:54", selection_range: "44:26-44:28", detail: "bool" }
|
||||
- { name: "r3", kind: Variable, range: "45:21-45:45", selection_range: "45:25-45:27", detail: "int" }
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user