Compare commits
1 Commits
fix/server
...
raw-foldin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
183b90d572 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -72,4 +72,3 @@ tests/unit/Local/
|
||||
.claude/*
|
||||
!.claude/CLAUDE.md
|
||||
!.claude/commands/
|
||||
openspec/
|
||||
|
||||
@@ -41,7 +41,8 @@ set(FLATBUFFERS_BUILD_FLATHASH OFF CACHE BOOL "" FORCE)
|
||||
FetchContent_Declare(
|
||||
kotatsu
|
||||
GIT_REPOSITORY https://github.com/clice-io/kotatsu
|
||||
GIT_TAG 73814044ce8142f4438a3028f44668675fc09fff
|
||||
GIT_TAG main
|
||||
GIT_SHALLOW TRUE
|
||||
)
|
||||
|
||||
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/`. 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.h`. Each message type has a `RequestTraits` or `NotificationTraits` specialization that defines the method name and result type.
|
||||
|
||||
### Stateful Worker Messages
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-04-22
|
||||
185
openspec/changes/split-folding-range-pipeline/design.md
Normal file
185
openspec/changes/split-folding-range-pipeline/design.md
Normal file
@@ -0,0 +1,185 @@
|
||||
## 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` mixes three responsibilities in one path:
|
||||
|
||||
- discovering foldable structure from AST data
|
||||
- deciding which ranges survive deduplication and validation
|
||||
- shaping the final LSP response, including output metadata
|
||||
|
||||
That coupling makes the code harder to extend safely. Comment folding, directive-based collectors, capability-aware rendering, and range limiting all become riskier when collection and rendering rules share the same code path. The extracted proposal keeps scope narrower: it does not add new fold categories by itself, but it creates the architecture 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:
|
||||
|
||||
- `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 and normalization rules rather than collector output format.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- Separate folding processing into collection, normalization, and rendering phases.
|
||||
- Preserve the existing AST structural folding categories already supported by `clice`.
|
||||
- Make ordering, deduplication, and boundary validation deterministic and testable.
|
||||
- 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.
|
||||
- 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. Introduce a raw internal folding-range model
|
||||
|
||||
Collectors should emit an internal `RawFoldingRange`-style structure instead of final LSP protocol objects. The raw model should preserve source locations, an internal category, and optional metadata hints that later phases may use.
|
||||
|
||||
The raw model should be shaped around file-local source structure, not LSP transport fields. At minimum it should carry:
|
||||
|
||||
- a main-file `LocalSourceRange` span using half-open byte offsets
|
||||
- an internal fold category such as namespace, record, access section, function body, comment block, comment group, conditional branch, pragma region, include group, or import group
|
||||
- the collector origin, such as AST, comment scanning, or directive metadata, so normalization has a stable tie-break and debugging surface
|
||||
- render hints for syntax-specific shaping, such as delimiter trimming, whether line-only folding should hide the final line, and an optional collapsed-text hint
|
||||
|
||||
In other words, the raw model should look closer to:
|
||||
|
||||
```cpp
|
||||
struct RawFoldRenderHint {
|
||||
std::uint8_t trim_start_bytes = 0;
|
||||
std::uint8_t trim_end_bytes = 0;
|
||||
bool hide_last_line_when_line_only = false;
|
||||
std::string collapsed_text_hint;
|
||||
};
|
||||
|
||||
struct RawFoldingRange {
|
||||
LocalSourceRange span;
|
||||
RawFoldCategory category;
|
||||
RawFoldOrigin origin;
|
||||
RawFoldRenderHint render;
|
||||
};
|
||||
```
|
||||
|
||||
The important design choice is that `span` represents the foldable source envelope in the main file, while renderer-specific trimming stays in `render` hints. For example:
|
||||
|
||||
- brace-based structural folds keep the full braced span and let the renderer trim interior boundaries
|
||||
- block comments keep the full `/* ... */` span and let the renderer decide whether to hide the closing delimiter or final line
|
||||
- contiguous `//` groups keep the full grouped span and let the renderer decide how much of the opening sentinel remains visible
|
||||
|
||||
Why:
|
||||
|
||||
- collectors should describe what was found, not how it will be serialized
|
||||
- `LocalSourceRange` is already the natural coordinate system for `clice`
|
||||
- public LSP kinds such as `comment`, `imports`, and `region` are too lossy to use as the internal category model
|
||||
- future comment and directive collectors can share the same pipeline contract
|
||||
- tests can validate collection independently from rendering
|
||||
|
||||
Alternatives considered:
|
||||
|
||||
- Continue emitting LSP ranges directly from collectors. Rejected because it keeps protocol concerns entangled with source discovery.
|
||||
- Make the raw model store already-trimmed visible interior spans instead of the full source envelope. Rejected because line-only rendering, collapsed text, and comment delimiter rules would still leak back into every collector.
|
||||
|
||||
### 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 internal categories, not on final LSP 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 and render hints are reconciled
|
||||
- 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
|
||||
- category-aware normalization preserves internal meaning until the renderer maps it to public kinds
|
||||
- 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 a dedicated 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 `FoldingRenderOptions` structure. The renderer then becomes responsible for:
|
||||
|
||||
- converting `LocalSourceRange` into protocol positions for the requested encoding
|
||||
- applying delimiter trimming and line-only adjustments
|
||||
- mapping internal categories to public 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
|
||||
|
||||
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: keep the initial fields minimal and only include data already needed by current structural folds.
|
||||
- [Full-envelope raw spans plus render hints may feel less direct than storing already-trimmed ranges] -> Mitigation: use a small, explicit render-hint structure and validate brace/comment shaping with focused renderer 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. Introduce raw folding-range and render-option types behind the existing entrypoint.
|
||||
2. Convert the current AST-based collectors to emit raw ranges.
|
||||
3. Insert normalization between collection and response emission.
|
||||
4. Move LSP object construction into a dedicated renderer.
|
||||
5. Verify that existing structural folding fixtures still produce the expected 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 collector origin should remain part of the long-term raw model after normalization policy stabilizes, or only exist temporarily as a debugging and tie-break aid.
|
||||
26
openspec/changes/split-folding-range-pipeline/proposal.md
Normal file
26
openspec/changes/split-folding-range-pipeline/proposal.md
Normal file
@@ -0,0 +1,26 @@
|
||||
## Why
|
||||
|
||||
`explore-improve-folding-range-support` combines several different concerns: upstream comparison work, baseline folding fixes, preprocessor extensions, and an internal refactor. 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.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Extract the pipeline-splitting work from `explore-improve-folding-range-support` into a standalone change focused on folding-range architecture.
|
||||
- Introduce an internal raw folding-range model so collectors no longer emit final LSP objects directly.
|
||||
- Define a normalization phase that performs deterministic sorting, duplicate removal, and boundary validation before response generation.
|
||||
- Define a rendering phase that owns line/column shaping 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 be refactored around raw-range collection, normalization, and rendering boundaries.
|
||||
- Folding-related helper types may be introduced near the folding feature implementation.
|
||||
- `tests/unit/feature/folding_range_tests.cpp` will need regression coverage for structural folds and deterministic ordering.
|
||||
- `openspec/changes/explore-improve-folding-range-support/design.md` remains the source change from which this standalone proposal was extracted.
|
||||
@@ -0,0 +1,38 @@
|
||||
## 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 internal category
|
||||
- **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
|
||||
16
openspec/changes/split-folding-range-pipeline/tasks.md
Normal file
16
openspec/changes/split-folding-range-pipeline/tasks.md
Normal file
@@ -0,0 +1,16 @@
|
||||
## 1. Raw Model and Collector Boundary
|
||||
|
||||
- [ ] 1.1 Introduce internal raw folding-range and render-option types while keeping the current folding entrypoint stable.
|
||||
- [ ] 1.2 Convert the existing AST structural folding path in `src/feature/folding_ranges.cpp` to emit raw ranges instead of final LSP ranges.
|
||||
- [ ] 1.3 Add regression fixtures or assertions that cover the currently supported structural fold categories before further refactoring.
|
||||
|
||||
## 2. Normalization and Rendering
|
||||
|
||||
- [ ] 2.1 Implement normalization for deterministic sorting, duplicate removal, and invalid-range filtering.
|
||||
- [ ] 2.2 Introduce a dedicated renderer that converts normalized ranges into final LSP folding-range objects.
|
||||
- [ ] 2.3 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 Run relevant folding-range unit tests and fix any ordering, deduplication, or boundary regressions introduced by the new pipeline.
|
||||
@@ -161,7 +161,7 @@ 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" --snapshot-dir="./tests/snapshots" --corpus-dir="./tests/corpus" --verbose'
|
||||
cmd = './build/{{ type }}/bin/unit_tests --test-dir="./tests/data"'
|
||||
|
||||
[feature.test.tasks.integration-test]
|
||||
args = [{ arg = "type", default = "RelWithDebInfo" }]
|
||||
@@ -257,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 -E '*.snap.yml' -x prettier --write && \
|
||||
fd -H -e yaml -e yml -E pnpm-lock.yaml -x prettier --write && \
|
||||
fd -H "^\\.clang-(format|tidy)$" -x prettier --write --parser yaml
|
||||
"""
|
||||
format = { depends-on = [
|
||||
|
||||
173
src/clice.cc
173
src/clice.cc
@@ -4,33 +4,33 @@
|
||||
#include <print>
|
||||
#include <string>
|
||||
|
||||
#include "server/service/agentic.h"
|
||||
#include "server/service/master_server.h"
|
||||
#include "server/worker/stateful_worker.h"
|
||||
#include "server/worker/stateless_worker.h"
|
||||
#include "server/master_server.h"
|
||||
#include "server/stateful_worker.h"
|
||||
#include "server/stateless_worker.h"
|
||||
#include "support/logging.h"
|
||||
|
||||
#include "kota/async/async.h"
|
||||
#include "kota/deco/deco.h"
|
||||
#include "kota/ipc/codec/json.h"
|
||||
#include "kota/ipc/peer.h"
|
||||
#include "kota/ipc/recording_transport.h"
|
||||
#include "kota/ipc/transport.h"
|
||||
|
||||
namespace clice {
|
||||
|
||||
using kota::deco::decl::KVStyle;
|
||||
|
||||
struct Options {
|
||||
DecoKV(
|
||||
style = KVStyle::JoinedOrSeparate,
|
||||
help =
|
||||
"Running mode: pipe, socket, daemon, relay, agentic, stateless-worker, stateful-worker",
|
||||
required = false)
|
||||
DecoKV(style = KVStyle::JoinedOrSeparate,
|
||||
help = "Running mode: pipe, socket, stateless-worker, stateful-worker",
|
||||
required = false)
|
||||
<std::string> mode;
|
||||
|
||||
DecoKV(style = KVStyle::JoinedOrSeparate, help = "Socket mode address", required = false)
|
||||
<std::string> host = "127.0.0.1";
|
||||
|
||||
DecoKV(style = KVStyle::JoinedOrSeparate,
|
||||
help = "Agentic TCP port (0 = disabled)",
|
||||
required = false)
|
||||
<int> port = 0;
|
||||
DecoKV(style = KVStyle::JoinedOrSeparate, help = "Socket mode port", required = false)
|
||||
<int> port = 50051;
|
||||
|
||||
DecoKV(style = KVStyle::JoinedOrSeparate,
|
||||
names = {"--log-level", "--log-level="},
|
||||
@@ -43,50 +43,6 @@ 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="},
|
||||
@@ -112,6 +68,9 @@ 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
|
||||
|
||||
@@ -151,6 +110,8 @@ 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("");
|
||||
@@ -170,51 +131,77 @@ int main(int argc, const char** argv) {
|
||||
log_dir);
|
||||
}
|
||||
|
||||
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 == "pipe") {
|
||||
clice::logging::stderr_logger("master", clice::logging::options);
|
||||
|
||||
if(mode == "daemon") {
|
||||
auto workspace = opts.workspace.value_or("");
|
||||
if(workspace.empty()) {
|
||||
LOG_ERROR("--workspace is required for daemon mode");
|
||||
kota::event_loop loop;
|
||||
|
||||
auto transport = kota::ipc::StreamTransport::open_stdio(loop);
|
||||
if(!transport) {
|
||||
LOG_ERROR("failed to open stdio transport");
|
||||
return 1;
|
||||
}
|
||||
|
||||
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);
|
||||
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 == "agentic") {
|
||||
auto port = opts.port.value_or(0);
|
||||
if(port <= 0) {
|
||||
LOG_ERROR("--port is required for agentic mode");
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
if(mode == "relay") {
|
||||
auto socket = opts.socket.value_or("");
|
||||
return clice::run_relay_mode(socket);
|
||||
LOG_INFO("Listening on {}:{} ...", host, port);
|
||||
|
||||
auto task = [&]() -> kota::task<> {
|
||||
auto client = co_await acceptor->accept();
|
||||
if(!client.has_value()) {
|
||||
LOG_ERROR("failed to accept connection");
|
||||
loop.stop();
|
||||
co_return;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
LOG_ERROR("unknown mode '{}'", mode);
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
#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"
|
||||
@@ -18,7 +17,6 @@
|
||||
#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
|
||||
@@ -472,32 +470,11 @@ std::vector<const char*> query_clang_toolchain(const QueryParams& params) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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)) {
|
||||
for(auto arg: args) {
|
||||
if(arg == "-###"sv) {
|
||||
continue;
|
||||
}
|
||||
if(cc1_args[i] == "-###"sv) {
|
||||
continue;
|
||||
}
|
||||
result.emplace_back(params.callback(cc1_args[i]));
|
||||
result.emplace_back(params.callback(arg));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,11 +92,15 @@ 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,9 +93,18 @@ 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<DocumentSymbol> symbols;
|
||||
std::vector<DocumentSymbol>* cursor = &symbols;
|
||||
std::vector<InternalSymbol> symbols;
|
||||
std::vector<InternalSymbol>* cursor = &symbols;
|
||||
};
|
||||
|
||||
class DocumentSymbolCollector : public FilteredASTVisitor<DocumentSymbolCollector> {
|
||||
@@ -134,7 +143,7 @@ public:
|
||||
return ok;
|
||||
}
|
||||
|
||||
auto collect() -> std::vector<DocumentSymbol> {
|
||||
auto collect() -> std::vector<InternalSymbol> {
|
||||
TraverseDecl(unit.tu());
|
||||
return std::move(result.symbols);
|
||||
}
|
||||
@@ -165,8 +174,8 @@ private:
|
||||
SymbolFrame result;
|
||||
};
|
||||
|
||||
void sort_symbols(std::vector<DocumentSymbol>& symbols) {
|
||||
std::ranges::sort(symbols, [](const DocumentSymbol& lhs, const DocumentSymbol& rhs) {
|
||||
void sort_symbols(std::vector<InternalSymbol>& symbols) {
|
||||
std::ranges::sort(symbols, [](const InternalSymbol& lhs, const InternalSymbol& rhs) {
|
||||
if(lhs.range.begin != rhs.range.begin) {
|
||||
return lhs.range.begin < rhs.range.begin;
|
||||
}
|
||||
@@ -178,7 +187,7 @@ void sort_symbols(std::vector<DocumentSymbol>& symbols) {
|
||||
}
|
||||
}
|
||||
|
||||
auto to_protocol_symbol(const DocumentSymbol& symbol, const PositionMapper& converter)
|
||||
auto to_protocol_symbol(const InternalSymbol& symbol, const PositionMapper& converter)
|
||||
-> protocol::DocumentSymbol {
|
||||
protocol::DocumentSymbol result{
|
||||
.name = symbol.name,
|
||||
@@ -206,15 +215,10 @@ auto to_protocol_symbol(const DocumentSymbol& 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 = document_symbols(unit);
|
||||
auto internal = DocumentSymbolCollector(unit).collect();
|
||||
sort_symbols(internal);
|
||||
|
||||
PositionMapper converter(unit.interested_content(), encoding);
|
||||
std::vector<protocol::DocumentSymbol> symbols;
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
#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"
|
||||
@@ -60,66 +59,18 @@ struct InlayHintsOptions {
|
||||
|
||||
struct SignatureHelpOptions {};
|
||||
|
||||
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)
|
||||
auto semantic_tokens(CompilationUnitRef unit, PositionEncoding encoding = PositionEncoding::UTF16)
|
||||
-> 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>;
|
||||
|
||||
@@ -138,6 +89,12 @@ 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,6 +53,12 @@ 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) {}
|
||||
@@ -179,7 +185,7 @@ public:
|
||||
return true;
|
||||
}
|
||||
|
||||
auto collect() -> std::vector<FoldingRange> {
|
||||
auto collect() -> std::vector<RawFoldingRange> {
|
||||
TraverseDecl(unit.tu());
|
||||
|
||||
auto directives_it = unit.directives().find(unit.interested_file());
|
||||
@@ -187,7 +193,7 @@ public:
|
||||
collect_directives(directives_it->second);
|
||||
}
|
||||
|
||||
std::ranges::sort(ranges, [](const FoldingRange& lhs, const FoldingRange& rhs) {
|
||||
std::ranges::sort(ranges, [](const RawFoldingRange& lhs, const RawFoldingRange& rhs) {
|
||||
if(lhs.range.begin != rhs.range.begin) {
|
||||
return lhs.range.begin < rhs.range.begin;
|
||||
}
|
||||
@@ -337,18 +343,14 @@ private:
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<FoldingRange> ranges;
|
||||
std::vector<RawFoldingRange> 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 = folding_ranges(unit);
|
||||
auto collected = FoldingRangeCollector(unit).collect();
|
||||
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_WARN("Failed to format {}: {}", file, replacements.error());
|
||||
LOG_INFO("Fail to format for {}\n{}", file, replacements.error());
|
||||
return edits;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,22 @@ 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;
|
||||
}
|
||||
@@ -107,7 +123,7 @@ struct Callee {
|
||||
|
||||
class Builder {
|
||||
public:
|
||||
Builder(std::vector<InlayHint>& result,
|
||||
Builder(std::vector<RawInlayHint>& result,
|
||||
CompilationUnitRef unit,
|
||||
LocalSourceRange restrict_range,
|
||||
const InlayHintsOptions& options) :
|
||||
@@ -483,7 +499,7 @@ public:
|
||||
bool pad_left = prefix.consume_front(" ");
|
||||
bool pad_right = suffix.consume_back(" ");
|
||||
|
||||
InlayHint hint{
|
||||
RawInlayHint hint{
|
||||
.offset = offset,
|
||||
.kind = kind,
|
||||
.label = (prefix + label + suffix).str(),
|
||||
@@ -538,7 +554,7 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<InlayHint>& result;
|
||||
std::vector<RawInlayHint>& result;
|
||||
CompilationUnitRef unit;
|
||||
LocalSourceRange restrict_range;
|
||||
const InlayHintsOptions& options;
|
||||
@@ -897,43 +913,36 @@ private:
|
||||
|
||||
} // namespace
|
||||
|
||||
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> {
|
||||
if(!options.enabled) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<InlayHint> raw_hints;
|
||||
std::vector<RawInlayHint> 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 InlayHint& lhs, const InlayHint& rhs) {
|
||||
std::ranges::sort(raw_hints, [](const RawInlayHint& lhs, const RawInlayHint& 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 InlayHint& lhs, const InlayHint& rhs) {
|
||||
std::ranges::unique(raw_hints, [](const RawInlayHint& lhs, const RawInlayHint& 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(collected.size());
|
||||
hints.reserve(raw_hints.size());
|
||||
|
||||
for(const auto& hint: collected) {
|
||||
for(const auto& hint: raw_hints) {
|
||||
protocol::InlayHint out{
|
||||
.position = *converter.to_position(hint.offset),
|
||||
.label = hint.label,
|
||||
|
||||
@@ -18,6 +18,12 @@ 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);
|
||||
}
|
||||
@@ -160,7 +166,7 @@ class SemanticTokensCollector : public SemanticVisitor<SemanticTokensCollector>
|
||||
public:
|
||||
explicit SemanticTokensCollector(CompilationUnitRef unit) : SemanticVisitor(unit, true) {}
|
||||
|
||||
auto collect() -> std::vector<SemanticToken> {
|
||||
auto collect() -> std::vector<RawToken> {
|
||||
highlight_lexical(unit.interested_file());
|
||||
run();
|
||||
highlight_modules();
|
||||
@@ -392,7 +398,7 @@ private:
|
||||
}
|
||||
}
|
||||
|
||||
static void resolve_conflict(SemanticToken& last, const SemanticToken& current) {
|
||||
static void resolve_conflict(RawToken& last, const RawToken& current) {
|
||||
if(last.kind == SymbolKind::Conflict) {
|
||||
return;
|
||||
}
|
||||
@@ -408,14 +414,14 @@ private:
|
||||
}
|
||||
|
||||
void merge_tokens() {
|
||||
std::ranges::sort(tokens, [](const SemanticToken& lhs, const SemanticToken& rhs) {
|
||||
std::ranges::sort(tokens, [](const RawToken& lhs, const RawToken& rhs) {
|
||||
if(lhs.range.begin != rhs.range.begin) {
|
||||
return lhs.range.begin < rhs.range.begin;
|
||||
}
|
||||
return lhs.range.end < rhs.range.end;
|
||||
});
|
||||
|
||||
std::vector<SemanticToken> merged;
|
||||
std::vector<RawToken> merged;
|
||||
merged.reserve(tokens.size());
|
||||
|
||||
for(const auto& token: tokens) {
|
||||
@@ -442,7 +448,7 @@ private:
|
||||
}
|
||||
|
||||
public:
|
||||
std::vector<SemanticToken> tokens;
|
||||
std::vector<RawToken> tokens;
|
||||
};
|
||||
|
||||
class SemanticTokenEncoder {
|
||||
@@ -452,7 +458,7 @@ public:
|
||||
protocol::SemanticTokens& output) :
|
||||
content(content), converter(content, encoding), output(output) {}
|
||||
|
||||
void append(const SemanticToken& token) {
|
||||
void append(const RawToken& token) {
|
||||
if(!token.range.valid() || token.range.end <= token.range.begin ||
|
||||
token.range.end > content.size()) {
|
||||
return;
|
||||
@@ -536,14 +542,10 @@ 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 {
|
||||
auto tokens = semantic_tokens(unit);
|
||||
SemanticTokensCollector collector(unit);
|
||||
auto tokens = collector.collect();
|
||||
|
||||
protocol::SemanticTokens result;
|
||||
result.data.reserve(tokens.size() * 5);
|
||||
|
||||
@@ -1111,6 +1111,8 @@ public:
|
||||
return Base::TransformDecltypeType(TLB, TL);
|
||||
}
|
||||
|
||||
// --- State ---
|
||||
|
||||
private:
|
||||
clang::Sema& sema;
|
||||
clang::ASTContext& context;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "server/compiler/compile_graph.h"
|
||||
#include "server/compile_graph.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "server/compiler/compiler.h"
|
||||
#include "server/compiler.h"
|
||||
|
||||
#include <format>
|
||||
#include <ranges>
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
#include "command/search_config.h"
|
||||
#include "index/tu_index.h"
|
||||
#include "server/protocol/worker.h"
|
||||
#include "server/protocol.h"
|
||||
#include "support/filesystem.h"
|
||||
#include "support/logging.h"
|
||||
#include "syntax/include_resolver.h"
|
||||
@@ -28,20 +28,16 @@ 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), workspace(workspace), pool(pool), sessions(sessions) {}
|
||||
loop(loop), peer(peer), 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");
|
||||
@@ -414,8 +410,6 @@ 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);
|
||||
@@ -427,16 +421,14 @@ 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,
|
||||
@@ -637,101 +629,6 @@ 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):
|
||||
@@ -751,9 +648,9 @@ kota::task<> Compiler::run_compile(std::uint32_t pid, std::shared_ptr<Session::P
|
||||
/// 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 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
|
||||
/// each call ensure_compiled(). The first one launches a detached compile
|
||||
/// task via loop.schedule(); subsequent ones wait on the shared event.
|
||||
/// The detached task cannot be cancelled by LSP $/cancelRequest, preventing
|
||||
/// the race where cancellation wakes all waiters and they all start compiles.
|
||||
kota::task<bool> Compiler::ensure_compiled(Session& session) {
|
||||
auto path_id = session.path_id;
|
||||
@@ -782,12 +679,124 @@ 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 compile path_id={} gen={}", path_id, session.generation);
|
||||
LOG_INFO("ensure_compiled: launching detached compile path_id={} gen={}",
|
||||
path_id,
|
||||
session.generation);
|
||||
|
||||
compile_tasks.spawn(run_compile(path_id, pending_compile));
|
||||
// Capture path_id by value so the detached lambda can re-lookup the session
|
||||
// from the sessions map after co_await (DenseMap may invalidate pointers).
|
||||
loop.schedule([](Compiler* self,
|
||||
std::uint32_t pid,
|
||||
std::shared_ptr<Session::PendingCompile> pc) -> kota::task<> {
|
||||
// Re-lookup session from the sessions map (pointer may have been
|
||||
// invalidated by DenseMap growth during co_await).
|
||||
auto find_session = [&]() -> Session* {
|
||||
auto it = self->sessions.find(pid);
|
||||
return it != self->sessions.end() ? &it->second : nullptr;
|
||||
};
|
||||
|
||||
auto* sess = find_session();
|
||||
if(!sess) {
|
||||
pc->done.set();
|
||||
co_return;
|
||||
}
|
||||
|
||||
auto finish_compile = [&]() {
|
||||
auto* s = find_session();
|
||||
if(s && s->compiling == pc) {
|
||||
s->compiling.reset();
|
||||
}
|
||||
LOG_INFO("ensure_compiled: finish_compile (detached) path_id={}", pid);
|
||||
pc->done.set();
|
||||
};
|
||||
|
||||
auto gen = sess->generation;
|
||||
LOG_INFO("ensure_compiled: starting compile (detached) path_id={} gen={}", pid, gen);
|
||||
|
||||
auto file_path = std::string(self->workspace.path_pool.resolve(pid));
|
||||
auto uri = lsp::URI::from_file_path(file_path);
|
||||
std::string uri_str = uri.has_value() ? uri->str() : file_path;
|
||||
|
||||
worker::CompileParams params;
|
||||
params.path = file_path;
|
||||
params.version = sess->version;
|
||||
params.text = sess->text;
|
||||
if(!self->fill_compile_args(file_path, params.directory, params.arguments, sess)) {
|
||||
finish_compile();
|
||||
co_return;
|
||||
}
|
||||
|
||||
if(!co_await self
|
||||
->ensure_deps(*sess, params.directory, params.arguments, params.pch, params.pcms)) {
|
||||
LOG_WARN("Dependency preparation failed for {}, skipping compile", uri_str);
|
||||
finish_compile();
|
||||
co_return;
|
||||
}
|
||||
|
||||
// Re-lookup after co_await (DenseMap may have grown).
|
||||
sess = find_session();
|
||||
if(!sess) {
|
||||
pc->done.set();
|
||||
co_return;
|
||||
}
|
||||
|
||||
auto result = co_await self->pool.send_stateful(pid, params);
|
||||
|
||||
// Re-lookup after co_await.
|
||||
sess = find_session();
|
||||
if(!sess) {
|
||||
pc->done.set();
|
||||
co_return;
|
||||
}
|
||||
|
||||
if(sess->generation != gen) {
|
||||
LOG_INFO("ensure_compiled: generation mismatch ({} vs {}) for {}",
|
||||
sess->generation,
|
||||
gen,
|
||||
uri_str);
|
||||
finish_compile();
|
||||
co_return;
|
||||
}
|
||||
|
||||
if(!result.has_value()) {
|
||||
LOG_WARN("Compile failed for {}: {}", uri_str, result.error().message);
|
||||
self->clear_diagnostics(uri_str);
|
||||
finish_compile();
|
||||
co_return;
|
||||
}
|
||||
|
||||
sess->ast_dirty = false;
|
||||
pc->succeeded = true;
|
||||
self->record_deps(*sess, result.value().deps);
|
||||
|
||||
// Store open file index from the stateful worker's TUIndex.
|
||||
if(!result.value().tu_index_data.empty()) {
|
||||
auto tu_index = index::TUIndex::from(result.value().tu_index_data.data());
|
||||
OpenFileIndex ofi;
|
||||
ofi.file_index = std::move(tu_index.main_file_index);
|
||||
ofi.symbols = std::move(tu_index.symbols);
|
||||
ofi.content = sess->text;
|
||||
ofi.mapper.emplace(ofi.content, lsp::PositionEncoding::UTF16);
|
||||
sess->file_index = std::move(ofi);
|
||||
}
|
||||
|
||||
auto version = sess->version;
|
||||
finish_compile();
|
||||
|
||||
// Publish diagnostics AFTER marking compile as done, so that concurrent
|
||||
// forward_query() calls can proceed immediately.
|
||||
self->publish_diagnostics(uri_str, version, result.value().diagnostics);
|
||||
if(self->on_indexing_needed)
|
||||
self->on_indexing_needed();
|
||||
}(this, path_id, pending_compile));
|
||||
|
||||
// Wait for the detached compile to finish. If this wait is cancelled
|
||||
// by LSP $/cancelRequest, the detached task continues unaffected.
|
||||
@@ -882,32 +891,6 @@ 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,9 +8,9 @@
|
||||
#include <vector>
|
||||
|
||||
#include "command/command.h"
|
||||
#include "server/service/session.h"
|
||||
#include "server/worker/worker_pool.h"
|
||||
#include "server/workspace/workspace.h"
|
||||
#include "server/session.h"
|
||||
#include "server/worker_pool.h"
|
||||
#include "server/workspace.h"
|
||||
#include "syntax/completion.h"
|
||||
|
||||
#include "kota/async/async.h"
|
||||
@@ -50,14 +50,10 @@ 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();
|
||||
@@ -90,9 +86,6 @@ 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);
|
||||
@@ -103,12 +96,7 @@ 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,
|
||||
@@ -137,11 +125,10 @@ private:
|
||||
|
||||
private:
|
||||
kota::event_loop& loop;
|
||||
kota::ipc::JsonPeer* peer = nullptr;
|
||||
kota::ipc::JsonPeer& peer;
|
||||
Workspace& workspace;
|
||||
WorkerPool& pool;
|
||||
llvm::DenseMap<std::uint32_t, Session>& sessions;
|
||||
kota::task_group<> compile_tasks{loop};
|
||||
};
|
||||
|
||||
} // namespace clice
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "server/workspace/config.h"
|
||||
#include "server/config.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
@@ -156,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) {
|
||||
Config config{};
|
||||
auto result = kota::codec::json::from_json(json, config);
|
||||
auto result = kota::codec::json::from_json<Config>(json);
|
||||
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/compiler/indexer.h"
|
||||
#include "server/indexer.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
@@ -6,10 +6,10 @@
|
||||
#include <vector>
|
||||
|
||||
#include "index/tu_index.h"
|
||||
#include "server/compiler/compiler.h"
|
||||
#include "server/protocol/worker.h"
|
||||
#include "server/service/session.h"
|
||||
#include "server/worker/worker_pool.h"
|
||||
#include "server/compiler.h"
|
||||
#include "server/protocol.h"
|
||||
#include "server/session.h"
|
||||
#include "server/worker_pool.h"
|
||||
#include "support/filesystem.h"
|
||||
#include "support/logging.h"
|
||||
|
||||
@@ -447,152 +447,6 @@ 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;
|
||||
@@ -788,11 +642,6 @@ void Indexer::resume_indexing() {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -802,11 +651,7 @@ 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));
|
||||
|
||||
if(!bg_tasks.spawn(run_background_indexing())) {
|
||||
indexing_scheduled = false;
|
||||
LOG_WARN("Failed to spawn background indexing task (task group stopped)");
|
||||
}
|
||||
loop.schedule(run_background_indexing());
|
||||
}
|
||||
|
||||
kota::task<> Indexer::index_one(std::uint32_t server_path_id) {
|
||||
@@ -849,14 +694,18 @@ kota::task<> Indexer::index_one(std::uint32_t server_path_id) {
|
||||
}
|
||||
}
|
||||
|
||||
kota::task<> Indexer::monitor_resources() {
|
||||
while(true) {
|
||||
co_await kota::sleep(std::chrono::milliseconds(3000));
|
||||
kota::task<> Indexer::monitor_resources(std::uint32_t generation) {
|
||||
while(generation == monitor_generation) {
|
||||
co_await kota::sleep(std::chrono::milliseconds(3000), loop);
|
||||
|
||||
if(generation != monitor_generation)
|
||||
break;
|
||||
|
||||
auto mem = kota::sys::memory();
|
||||
if(mem.total == 0)
|
||||
continue;
|
||||
|
||||
// Respect cgroup/container limits when present.
|
||||
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);
|
||||
@@ -887,73 +736,87 @@ kota::task<> Indexer::run_background_indexing() {
|
||||
}
|
||||
|
||||
indexing_active = true;
|
||||
++monitor_generation;
|
||||
loop.schedule(monitor_resources(monitor_generation));
|
||||
|
||||
kota::cancellation_source monitor_cancel;
|
||||
bg_tasks.spawn(kota::with_token(monitor_resources(), monitor_cancel.token()));
|
||||
|
||||
// Put module interface units first so their PCMs are built before
|
||||
// non-module files that might import them.
|
||||
std::stable_partition(
|
||||
index_queue.begin() + index_queue_pos,
|
||||
index_queue.end(),
|
||||
[this](std::uint32_t id) { return workspace.path_to_module.contains(id); });
|
||||
|
||||
auto total = index_queue.size() - index_queue_pos;
|
||||
auto batch = index_queue.size() - index_queue_pos;
|
||||
std::size_t dispatched = 0;
|
||||
std::size_t completed = 0;
|
||||
finished = 0;
|
||||
|
||||
// Progress reporting via LSP $/progress.
|
||||
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);
|
||||
progress->begin("Indexing", std::format("0/{} files", batch), 0);
|
||||
} else {
|
||||
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);
|
||||
while(index_queue_pos < index_queue.size() || inflight > 0) {
|
||||
// Dispatch new tasks up to max_concurrent.
|
||||
while(index_queue_pos < index_queue.size() && inflight < max_concurrent) {
|
||||
// Wait if paused by a user request.
|
||||
if(pause_depth > 0) {
|
||||
co_await resume_event.wait();
|
||||
}
|
||||
slot_available.set();
|
||||
}());
|
||||
}
|
||||
|
||||
co_await workers.join();
|
||||
auto server_path_id = index_queue[index_queue_pos++];
|
||||
|
||||
// Quick pre-filter: skip open files and fresh files without
|
||||
// consuming a concurrency slot.
|
||||
auto file_path = std::string(workspace.path_pool.resolve(server_path_id));
|
||||
if(sessions.contains(server_path_id) || !need_update(file_path)) {
|
||||
++completed;
|
||||
continue;
|
||||
}
|
||||
|
||||
++inflight;
|
||||
++dispatched;
|
||||
|
||||
// Launch the index task. On completion it decrements
|
||||
// inflight, bumps finished, and signals the event.
|
||||
loop.schedule([](Indexer* self, std::uint32_t id, kota::event& done) -> kota::task<> {
|
||||
co_await self->index_one(id);
|
||||
--self->inflight;
|
||||
++self->finished;
|
||||
done.set();
|
||||
}(this, server_path_id, completion_event));
|
||||
}
|
||||
|
||||
if(inflight == 0)
|
||||
break;
|
||||
|
||||
// Wait for at least one task to finish.
|
||||
co_await completion_event.wait();
|
||||
completion_event.reset();
|
||||
|
||||
// Drain all completions that occurred since last wake.
|
||||
completed += std::exchange(finished, 0);
|
||||
|
||||
// Report progress.
|
||||
if(progress) {
|
||||
auto pct = batch > 0 ? static_cast<std::uint32_t>(completed * 100 / batch) : 100;
|
||||
progress->report(std::format("{}/{} files", completed, batch), pct);
|
||||
}
|
||||
}
|
||||
|
||||
if(progress) {
|
||||
progress->end(std::format("Indexed {} files", dispatched));
|
||||
}
|
||||
|
||||
monitor_cancel.cancel();
|
||||
|
||||
indexing_active = false;
|
||||
++monitor_generation; // Stop the monitor coroutine.
|
||||
LOG_INFO("Background indexing complete: {} files dispatched", dispatched);
|
||||
save(workspace.config.project.index_dir);
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
#include "semantic/relation_kind.h"
|
||||
#include "semantic/symbol_kind.h"
|
||||
#include "server/workspace/workspace.h"
|
||||
#include "server/workspace.h"
|
||||
|
||||
#include "kota/async/async.h"
|
||||
#include "kota/ipc/codec/json.h"
|
||||
@@ -61,8 +61,8 @@ public:
|
||||
WorkerPool& pool,
|
||||
Compiler& compiler,
|
||||
std::function<bool(std::uint32_t)> is_file_open = {}) :
|
||||
loop(loop), bg_tasks(loop), workspace(workspace), sessions(sessions), pool(pool),
|
||||
compiler(compiler), is_file_open(std::move(is_file_open)) {}
|
||||
loop(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.
|
||||
@@ -167,43 +167,6 @@ 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);
|
||||
|
||||
@@ -245,7 +208,6 @@ private:
|
||||
|
||||
private:
|
||||
kota::event_loop& loop;
|
||||
kota::task_group<> bg_tasks;
|
||||
Workspace& workspace;
|
||||
llvm::DenseMap<std::uint32_t, Session>& sessions;
|
||||
WorkerPool& pool;
|
||||
@@ -269,15 +231,27 @@ private:
|
||||
/// Concurrency control for background indexing.
|
||||
std::size_t max_concurrent = 2;
|
||||
std::size_t baseline_concurrent = 2;
|
||||
std::size_t inflight = 0;
|
||||
std::size_t finished = 0; ///< Incremented by each completed dispatch task.
|
||||
|
||||
/// 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};
|
||||
|
||||
/// Completion event — signalled by each finished dispatch task so the
|
||||
/// main loop can wake up. Must be a member (not local to the coroutine)
|
||||
/// because inflight tasks capture it by reference and may outlive the
|
||||
/// coroutine frame during server shutdown.
|
||||
kota::event completion_event;
|
||||
|
||||
/// Generation counter — incremented each run so a stale monitor_resources
|
||||
/// coroutine can detect that its owning run has ended.
|
||||
std::uint32_t monitor_generation = 0;
|
||||
|
||||
kota::task<> run_background_indexing();
|
||||
kota::task<> index_one(std::uint32_t server_path_id);
|
||||
kota::task<> monitor_resources();
|
||||
kota::task<> monitor_resources(std::uint32_t generation);
|
||||
};
|
||||
|
||||
} // namespace clice
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "server/service/lsp_client.h"
|
||||
#include "server/master_server.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <format>
|
||||
@@ -7,9 +7,7 @@
|
||||
#include <variant>
|
||||
|
||||
#include "semantic/symbol_kind.h"
|
||||
#include "server/protocol/extension.h"
|
||||
#include "server/protocol/worker.h"
|
||||
#include "server/service/master_server.h"
|
||||
#include "server/protocol.h"
|
||||
#include "support/filesystem.h"
|
||||
#include "support/logging.h"
|
||||
|
||||
@@ -18,6 +16,7 @@
|
||||
#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"
|
||||
|
||||
@@ -30,39 +29,177 @@ 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"};
|
||||
}
|
||||
|
||||
LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(server), peer(peer) {
|
||||
server.compiler.set_peer(&peer);
|
||||
server.indexer.set_peer(&peer);
|
||||
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)) {}
|
||||
|
||||
MasterServer::~MasterServer() = default;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
// 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);
|
||||
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> {
|
||||
auto& srv = this->server;
|
||||
if(srv.lifecycle != ServerLifecycle::Uninitialized) {
|
||||
if(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()) {
|
||||
srv.workspace_root = uri_to_path(*init.root_uri);
|
||||
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)
|
||||
srv.init_options_json = std::move(*json);
|
||||
init_options_json = std::move(*json);
|
||||
}
|
||||
|
||||
srv.lifecycle = ServerLifecycle::Initialized;
|
||||
LOG_INFO("Initialized with workspace: {}", srv.workspace_root);
|
||||
lifecycle = ServerLifecycle::Initialized;
|
||||
LOG_INFO("Initialized with workspace: {}", workspace_root);
|
||||
|
||||
protocol::InitializeResult result;
|
||||
auto& caps = result.capabilities;
|
||||
@@ -85,6 +222,7 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s
|
||||
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,
|
||||
};
|
||||
@@ -108,8 +246,6 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s
|
||||
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;
|
||||
{
|
||||
@@ -141,33 +277,103 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s
|
||||
co_return result;
|
||||
});
|
||||
|
||||
peer.on_notification([this]([[maybe_unused]] const protocol::InitializedParams& params) {
|
||||
this->server.initialize();
|
||||
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();
|
||||
};
|
||||
|
||||
indexer.set_peer(&peer);
|
||||
indexer.set_max_concurrency(cfg.stateless_worker_count.value);
|
||||
|
||||
load_workspace();
|
||||
});
|
||||
|
||||
peer.on_request(
|
||||
[this](RequestContext& ctx,
|
||||
const protocol::ShutdownParams& params) -> RequestResult<protocol::ShutdownParams> {
|
||||
this->server.lifecycle = ServerLifecycle::ShuttingDown;
|
||||
lifecycle = ServerLifecycle::ShuttingDown;
|
||||
LOG_INFO("Shutdown requested");
|
||||
co_return nullptr;
|
||||
});
|
||||
|
||||
peer.on_notification([this]([[maybe_unused]] const protocol::ExitParams& params) {
|
||||
peer.on_notification([this](const protocol::ExitParams& params) {
|
||||
lifecycle = ServerLifecycle::Exited;
|
||||
LOG_INFO("Exit notification received");
|
||||
this->server.schedule_shutdown();
|
||||
this->peer.close();
|
||||
|
||||
indexer.save(workspace.config.project.index_dir);
|
||||
workspace.save_cache();
|
||||
|
||||
loop.schedule([this]() -> kota::task<> {
|
||||
co_await pool.stop();
|
||||
loop.stop();
|
||||
}());
|
||||
});
|
||||
|
||||
/// Document lifecycle — handled directly by MasterServer.
|
||||
|
||||
peer.on_notification([this](const protocol::DidOpenTextDocumentParams& params) {
|
||||
auto& srv = this->server;
|
||||
if(srv.lifecycle != ServerLifecycle::Ready)
|
||||
if(lifecycle != ServerLifecycle::Ready)
|
||||
return;
|
||||
|
||||
auto path = uri_to_path(params.text_document.uri);
|
||||
auto path_id = srv.workspace.path_pool.intern(path);
|
||||
auto path_id = workspace.path_pool.intern(path);
|
||||
|
||||
auto& session = srv.open_session(path_id);
|
||||
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;
|
||||
session.version = params.text_document.version;
|
||||
session.text = params.text_document.text;
|
||||
session.generation++;
|
||||
@@ -176,18 +382,18 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s
|
||||
});
|
||||
|
||||
peer.on_notification([this](const protocol::DidChangeTextDocumentParams& params) {
|
||||
auto& srv = this->server;
|
||||
if(srv.lifecycle != ServerLifecycle::Ready)
|
||||
if(lifecycle != ServerLifecycle::Ready)
|
||||
return;
|
||||
|
||||
auto path = uri_to_path(params.text_document.uri);
|
||||
auto path_id = srv.workspace.path_pool.intern(path);
|
||||
auto path_id = workspace.path_pool.intern(path);
|
||||
|
||||
auto* session = srv.find_session(path_id);
|
||||
if(!session)
|
||||
auto it = sessions.find(path_id);
|
||||
if(it == sessions.end())
|
||||
return;
|
||||
|
||||
session->version = params.text_document.version;
|
||||
auto& session = it->second;
|
||||
session.version = params.text_document.version;
|
||||
|
||||
for(auto& change: params.content_changes) {
|
||||
std::visit(
|
||||
@@ -195,157 +401,186 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s
|
||||
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;
|
||||
srv.pool.notify_stateful(path_id, update);
|
||||
update.version = session.version;
|
||||
pool.notify_stateful(path_id, update);
|
||||
});
|
||||
|
||||
peer.on_notification([this](const protocol::DidCloseTextDocumentParams& params) {
|
||||
auto& srv = this->server;
|
||||
if(srv.lifecycle != ServerLifecycle::Ready)
|
||||
return;
|
||||
|
||||
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) {
|
||||
auto& srv = this->server;
|
||||
if(srv.lifecycle != ServerLifecycle::Ready)
|
||||
if(lifecycle != ServerLifecycle::Ready)
|
||||
return;
|
||||
|
||||
auto path = uri_to_path(params.text_document.uri);
|
||||
auto path_id = srv.workspace.path_pool.intern(path);
|
||||
srv.on_file_saved(path_id);
|
||||
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);
|
||||
});
|
||||
|
||||
peer.on_notification([this](const protocol::DidSaveTextDocumentParams& params) {
|
||||
if(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();
|
||||
|
||||
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 = srv.workspace.path_pool.intern(path);
|
||||
auto* session = srv.find_session(path_id);
|
||||
if(!session)
|
||||
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 srv.compiler.forward_query(
|
||||
worker::QueryKind::Hover,
|
||||
*session,
|
||||
params.text_document_position_params.position);
|
||||
co_return co_await compiler.forward_query(worker::QueryKind::Hover,
|
||||
sit->second,
|
||||
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 = srv.workspace.path_pool.intern(path);
|
||||
auto* session = srv.find_session(path_id);
|
||||
if(!session)
|
||||
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 srv.compiler.forward_query(worker::QueryKind::SemanticTokens, *session);
|
||||
co_return co_await compiler.forward_query(worker::QueryKind::SemanticTokens, sit->second);
|
||||
});
|
||||
|
||||
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 = srv.workspace.path_pool.intern(path);
|
||||
auto* session = srv.find_session(path_id);
|
||||
if(!session)
|
||||
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 srv.compiler.forward_query(worker::QueryKind::InlayHints,
|
||||
*session,
|
||||
{},
|
||||
params.range);
|
||||
co_return co_await compiler.forward_query(worker::QueryKind::InlayHints,
|
||||
sit->second,
|
||||
{},
|
||||
params.range);
|
||||
});
|
||||
|
||||
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::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::DocumentSymbolParams& 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)
|
||||
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 srv.compiler.forward_query(worker::QueryKind::DocumentSymbol, *session);
|
||||
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);
|
||||
});
|
||||
|
||||
peer.on_request(
|
||||
[this](RequestContext& ctx, const protocol::DocumentLinkParams& 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 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)
|
||||
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 srv.compiler.forward_query(worker::QueryKind::CodeAction, *session);
|
||||
co_return co_await compiler.forward_query(worker::QueryKind::CodeAction, sit->second);
|
||||
});
|
||||
|
||||
/// Helper: resolve URI to path, path_id, and Session pointer.
|
||||
auto resolve_uri = [this](const std::string& uri) {
|
||||
struct Result {
|
||||
std::string path;
|
||||
@@ -353,21 +588,22 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s
|
||||
Session* session;
|
||||
};
|
||||
auto path = uri_to_path(uri);
|
||||
auto path_id = this->server.workspace.path_pool.intern(path);
|
||||
auto* session = this->server.find_session(path_id);
|
||||
auto path_id = workspace.path_pool.intern(path);
|
||||
auto sit = sessions.find(path_id);
|
||||
Session* session = (sit != sessions.end()) ? &sit->second : nullptr;
|
||||
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 this->server.indexer.lookup_symbol(uri, path, pos, session);
|
||||
return 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 this->server.indexer.query_relations(path, pos, kind, session);
|
||||
return indexer.query_relations(path, pos, kind, session);
|
||||
};
|
||||
|
||||
auto resolve_item =
|
||||
@@ -376,9 +612,11 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s
|
||||
const protocol::Range& range,
|
||||
const std::optional<protocol::LSPAny>& data) -> std::optional<SymbolInfo> {
|
||||
auto [path, path_id, session] = resolve_uri(uri);
|
||||
return this->server.indexer.resolve_hierarchy_item(uri, path, range, data, session);
|
||||
return 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;
|
||||
@@ -389,15 +627,14 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s
|
||||
co_return to_raw(result);
|
||||
}
|
||||
|
||||
auto& srv = this->server;
|
||||
auto path = uri_to_path(uri);
|
||||
auto path_id = srv.workspace.path_pool.intern(path);
|
||||
auto* session = srv.find_session(path_id);
|
||||
if(!session)
|
||||
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 srv.compiler.forward_query(worker::QueryKind::GoToDefinition,
|
||||
*session,
|
||||
pos);
|
||||
co_return co_await compiler.forward_query(worker::QueryKind::GoToDefinition,
|
||||
sit->second,
|
||||
pos);
|
||||
});
|
||||
|
||||
peer.on_request([this, query_at](RequestContext& ctx,
|
||||
@@ -434,61 +671,38 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s
|
||||
co_return serde_raw{"null"};
|
||||
});
|
||||
|
||||
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 = 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();
|
||||
auto result =
|
||||
co_await srv.compiler.handle_completion(params.text_document_position_params.position,
|
||||
*session);
|
||||
co_return std::move(result);
|
||||
});
|
||||
/// Feature requests — stateless forwarding.
|
||||
|
||||
peer.on_request(
|
||||
[this](RequestContext& ctx, const protocol::SignatureHelpParams& params) -> RawResult {
|
||||
auto& srv = this->server;
|
||||
[this](RequestContext& ctx, const protocol::CompletionParams& params) -> RawResult {
|
||||
auto path = uri_to_path(params.text_document_position_params.text_document.uri);
|
||||
auto path_id = srv.workspace.path_pool.intern(path);
|
||||
auto* session = srv.find_session(path_id);
|
||||
if(!session)
|
||||
auto path_id = workspace.path_pool.intern(path);
|
||||
auto sit = sessions.find(path_id);
|
||||
if(sit == sessions.end())
|
||||
co_return serde_raw{"null"};
|
||||
auto pause = srv.indexer.scoped_pause();
|
||||
auto pause = indexer.scoped_pause();
|
||||
auto result =
|
||||
co_await srv.compiler.forward_build(worker::BuildKind::SignatureHelp,
|
||||
params.text_document_position_params.position,
|
||||
*session);
|
||||
co_await compiler.handle_completion(params.text_document_position_params.position,
|
||||
sit->second);
|
||||
co_return std::move(result);
|
||||
});
|
||||
|
||||
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)
|
||||
const protocol::SignatureHelpParams& params) -> RawResult {
|
||||
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())
|
||||
co_return serde_raw{"null"};
|
||||
auto pause = srv.indexer.scoped_pause();
|
||||
co_return co_await srv.compiler.forward_format(*session, params.range);
|
||||
auto pause = indexer.scoped_pause();
|
||||
auto result = co_await compiler.forward_build(worker::BuildKind::SignatureHelp,
|
||||
params.text_document_position_params.position,
|
||||
sit->second);
|
||||
co_return std::move(result);
|
||||
});
|
||||
|
||||
/// Hierarchy queries — index-based.
|
||||
|
||||
peer.on_request(
|
||||
[this, lookup_at](RequestContext& ctx,
|
||||
const protocol::CallHierarchyPrepareParams& params) -> RawResult {
|
||||
@@ -512,7 +726,7 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s
|
||||
auto info = resolve_item(params.item.uri, params.item.range, params.item.data);
|
||||
if(!info)
|
||||
co_return serde_raw{"null"};
|
||||
auto results = this->server.indexer.find_incoming_calls(info->hash);
|
||||
auto results = indexer.find_incoming_calls(info->hash);
|
||||
if(results.empty())
|
||||
co_return serde_raw{"null"};
|
||||
co_return to_raw(results);
|
||||
@@ -524,7 +738,7 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s
|
||||
auto info = resolve_item(params.item.uri, params.item.range, params.item.data);
|
||||
if(!info)
|
||||
co_return serde_raw{"null"};
|
||||
auto results = this->server.indexer.find_outgoing_calls(info->hash);
|
||||
auto results = indexer.find_outgoing_calls(info->hash);
|
||||
if(results.empty())
|
||||
co_return serde_raw{"null"};
|
||||
co_return to_raw(results);
|
||||
@@ -554,7 +768,7 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s
|
||||
auto info = resolve_item(params.item.uri, params.item.range, params.item.data);
|
||||
if(!info)
|
||||
co_return serde_raw{"null"};
|
||||
auto results = this->server.indexer.find_supertypes(info->hash);
|
||||
auto results = indexer.find_supertypes(info->hash);
|
||||
if(results.empty())
|
||||
co_return serde_raw{"null"};
|
||||
co_return to_raw(results);
|
||||
@@ -566,7 +780,7 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s
|
||||
auto info = resolve_item(params.item.uri, params.item.range, params.item.data);
|
||||
if(!info)
|
||||
co_return serde_raw{"null"};
|
||||
auto results = this->server.indexer.find_subtypes(info->hash);
|
||||
auto results = indexer.find_subtypes(info->hash);
|
||||
if(results.empty())
|
||||
co_return serde_raw{"null"};
|
||||
co_return to_raw(results);
|
||||
@@ -574,29 +788,29 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s
|
||||
|
||||
peer.on_request(
|
||||
[this](RequestContext& ctx, const protocol::WorkspaceSymbolParams& params) -> RawResult {
|
||||
auto results = this->server.indexer.search_symbols(params.query);
|
||||
auto results = 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 = srv.workspace.path_pool.intern(path);
|
||||
auto path_id = 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& ws = srv.workspace;
|
||||
auto hosts = ws.dep_graph.find_host_sources(path_id);
|
||||
auto hosts = workspace.dep_graph.find_host_sources(path_id);
|
||||
for(auto host_id: hosts) {
|
||||
auto host_path = ws.path_pool.resolve(host_id);
|
||||
auto host_cdb = ws.cdb.lookup(host_path, {.suppress_logging = true});
|
||||
auto host_path = workspace.path_pool.resolve(host_id);
|
||||
auto host_cdb = workspace.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));
|
||||
@@ -610,7 +824,7 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s
|
||||
}
|
||||
|
||||
if(hosts.empty()) {
|
||||
auto entries = ws.cdb.lookup(path, {.suppress_logging = true});
|
||||
auto entries = workspace.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();
|
||||
@@ -652,14 +866,13 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s
|
||||
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 = srv.workspace.path_pool.intern(path);
|
||||
auto path_id = workspace.path_pool.intern(path);
|
||||
|
||||
ext::CurrentContextResult result;
|
||||
auto* session = srv.find_session(path_id);
|
||||
if(session && session->active_context) {
|
||||
auto ctx_path = srv.workspace.path_pool.resolve(*session->active_context);
|
||||
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 ctx_uri_opt = lsp::URI::from_file_path(std::string(ctx_path));
|
||||
if(ctx_uri_opt) {
|
||||
ext::ContextItem item;
|
||||
@@ -675,41 +888,34 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s
|
||||
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 = srv.workspace.path_pool.intern(path);
|
||||
auto path_id = workspace.path_pool.intern(path);
|
||||
auto context_path = uri_to_path(params.context_uri);
|
||||
auto context_path_id = srv.workspace.path_pool.intern(context_path);
|
||||
auto context_path_id = workspace.path_pool.intern(context_path);
|
||||
|
||||
ext::SwitchContextResult result;
|
||||
|
||||
auto& ws = srv.workspace;
|
||||
auto context_cdb = ws.cdb.lookup(context_path, {.suppress_logging = true});
|
||||
auto context_cdb = workspace.cdb.lookup(context_path, {.suppress_logging = true});
|
||||
if(context_cdb.empty()) {
|
||||
result.success = false;
|
||||
co_return to_raw(result);
|
||||
}
|
||||
|
||||
auto* session = srv.find_session(path_id);
|
||||
if(!session) {
|
||||
auto sit = sessions.find(path_id);
|
||||
if(sit == sessions.end()) {
|
||||
result.success = false;
|
||||
co_return to_raw(result);
|
||||
}
|
||||
|
||||
session->active_context = context_path_id;
|
||||
session->header_context.reset();
|
||||
session->pch_ref.reset();
|
||||
session->ast_deps.reset();
|
||||
session->ast_dirty = true;
|
||||
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;
|
||||
|
||||
result.success = true;
|
||||
co_return to_raw(result);
|
||||
});
|
||||
}
|
||||
|
||||
LSPClient::~LSPClient() {
|
||||
server.compiler.set_peer(nullptr);
|
||||
server.indexer.set_peer(nullptr);
|
||||
}
|
||||
|
||||
} // namespace clice
|
||||
81
src/server/master_server.h
Normal file
81
src/server/master_server.h
Normal file
@@ -0,0 +1,81 @@
|
||||
#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/json/json.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.
|
||||
|
||||
void load_workspace();
|
||||
|
||||
using RawResult = kota::task<kota::codec::RawValue, kota::ipc::Error>;
|
||||
};
|
||||
|
||||
} // namespace clice
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
@@ -9,6 +10,7 @@
|
||||
#include "syntax/token.h"
|
||||
|
||||
#include "kota/codec/json/json.h"
|
||||
#include "kota/ipc/lsp/protocol.h"
|
||||
#include "kota/ipc/protocol.h"
|
||||
|
||||
namespace clice::worker {
|
||||
@@ -64,7 +66,6 @@ enum class BuildKind : uint8_t {
|
||||
Index,
|
||||
Completion,
|
||||
SignatureHelp,
|
||||
Format,
|
||||
};
|
||||
|
||||
/// Unified parameters for all stateless build/compilation tasks.
|
||||
@@ -75,7 +76,6 @@ 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,7 +92,6 @@ 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.
|
||||
@@ -123,6 +122,43 @@ 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 <>
|
||||
@@ -1,297 +0,0 @@
|
||||
#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
|
||||
@@ -1,42 +0,0 @@
|
||||
#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,788 +0,0 @@
|
||||
#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([this, &srv](const ShutdownParams&) {
|
||||
LOG_INFO("agentic/shutdown received, shutting down");
|
||||
srv.schedule_shutdown();
|
||||
this->peer.close();
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace clice
|
||||
@@ -1,18 +0,0 @@
|
||||
#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
|
||||
@@ -1,177 +0,0 @@
|
||||
#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
|
||||
@@ -1,24 +0,0 @@
|
||||
#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,23 +0,0 @@
|
||||
#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
|
||||
@@ -1,608 +0,0 @@
|
||||
#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([](MasterServer& server) -> kota::task<> {
|
||||
auto watcher = kota::fs_event::create(server.workspace_root, {}, server.loop);
|
||||
if(!watcher) {
|
||||
LOG_WARN("Failed to start file watcher for {}", server.workspace_root);
|
||||
co_return;
|
||||
}
|
||||
|
||||
LOG_INFO("File watcher started for {}", server.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");
|
||||
server.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 = server.workspace.path_pool.intern(file);
|
||||
server.on_file_saved(path_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}(*this));
|
||||
}
|
||||
|
||||
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([](MasterServer& server) -> kota::task<> {
|
||||
co_await kota::when_all(server.indexer.stop(), server.compiler.stop(), server.pool.stop());
|
||||
server.loop.stop();
|
||||
}(*this));
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
co_await kota::when_all(
|
||||
[](MasterServer& server,
|
||||
kota::tcp::acceptor& acceptor,
|
||||
bool register_lsp,
|
||||
std::list<Connection>& connections,
|
||||
kota::task_group<>& connection_group) -> kota::task<> {
|
||||
auto& loop = kota::event_loop::current();
|
||||
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));
|
||||
}
|
||||
}(server, acceptor, register_lsp, connections, connection_group),
|
||||
[](MasterServer& server,
|
||||
kota::tcp::acceptor& acceptor,
|
||||
std::list<Connection>& connections) -> kota::task<> {
|
||||
co_await server.get_shutdown_event().wait();
|
||||
acceptor.stop();
|
||||
for(auto& conn: connections) {
|
||||
conn.peer->close();
|
||||
}
|
||||
}(server, acceptor, connections));
|
||||
|
||||
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);
|
||||
loop.schedule([](MasterServer& server, kota::ipc::JsonPeer& peer) -> kota::task<> {
|
||||
co_await server.get_shutdown_event().wait();
|
||||
peer.close();
|
||||
}(server, lsp_peer));
|
||||
|
||||
kota::tcp::acceptor agent_acceptor;
|
||||
bool has_agent_acceptor = false;
|
||||
|
||||
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);
|
||||
agent_acceptor = std::move(*acceptor);
|
||||
has_agent_acceptor = true;
|
||||
} else {
|
||||
LOG_WARN("Failed to start agentic listener on {}:{}", opts.host, opts.port);
|
||||
}
|
||||
}
|
||||
|
||||
loop.schedule([](MasterServer& server,
|
||||
kota::ipc::JsonPeer& peer,
|
||||
std::list<Connection>& connections,
|
||||
kota::tcp::acceptor acceptor,
|
||||
bool has_acceptor) -> kota::task<> {
|
||||
auto run_peer = [](MasterServer& server, kota::ipc::JsonPeer& peer) -> kota::task<> {
|
||||
co_await peer.run();
|
||||
server.schedule_shutdown();
|
||||
};
|
||||
auto close_peer_on_shutdown = [](MasterServer& server,
|
||||
kota::ipc::JsonPeer& peer) -> kota::task<> {
|
||||
co_await server.get_shutdown_event().wait();
|
||||
peer.close();
|
||||
};
|
||||
|
||||
if(has_acceptor) {
|
||||
co_await kota::when_all(
|
||||
run_peer(server, peer),
|
||||
close_peer_on_shutdown(server, peer),
|
||||
accept_connections(server, std::move(acceptor), false, connections));
|
||||
} else {
|
||||
co_await kota::when_all(run_peer(server, peer),
|
||||
close_peer_on_shutdown(server, peer));
|
||||
}
|
||||
}(server, lsp_peer, connections, std::move(agent_acceptor), has_agent_acceptor));
|
||||
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(
|
||||
[](MasterServer& server,
|
||||
kota::pipe::acceptor& acceptor,
|
||||
std::list<DaemonConnection>& connections,
|
||||
kota::task_group<>& connection_group) -> kota::task<> {
|
||||
auto& loop = kota::event_loop::current();
|
||||
|
||||
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));
|
||||
}
|
||||
}(server, acceptor, connections, connection_group),
|
||||
[](MasterServer& server,
|
||||
kota::pipe::acceptor& acceptor,
|
||||
std::list<DaemonConnection>& connections) -> kota::task<> {
|
||||
co_await server.get_shutdown_event().wait();
|
||||
acceptor.stop();
|
||||
for(auto& conn: connections) {
|
||||
conn.peer->close();
|
||||
}
|
||||
}(server, acceptor, connections));
|
||||
|
||||
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
|
||||
@@ -1,93 +0,0 @@
|
||||
#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/workspace.h"
|
||||
#include "server/workspace.h"
|
||||
|
||||
#include "kota/async/async.h"
|
||||
#include "llvm/ADT/SmallVector.h"
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "server/worker/stateful_worker.h"
|
||||
#include "server/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/worker.h"
|
||||
#include "server/worker/worker_common.h"
|
||||
#include "server/protocol.h"
|
||||
#include "server/worker_common.h"
|
||||
#include "support/logging.h"
|
||||
|
||||
#include "kota/async/async.h"
|
||||
@@ -245,33 +245,26 @@ 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, feature::PositionEncoding::UTF16));
|
||||
return to_raw(feature::semantic_tokens(doc.unit));
|
||||
});
|
||||
case K::InlayHints:
|
||||
co_return co_await with_ast(params.path, [&](DocumentEntry& doc) {
|
||||
auto range = params.range;
|
||||
if(range.begin == static_cast<uint32_t>(-1))
|
||||
range = LocalSourceRange{0, static_cast<uint32_t>(doc.text.size())};
|
||||
return to_raw(feature::inlay_hints(doc.unit,
|
||||
range,
|
||||
{},
|
||||
feature::PositionEncoding::UTF16));
|
||||
return to_raw(feature::inlay_hints(doc.unit, range));
|
||||
});
|
||||
case K::FoldingRange:
|
||||
co_return co_await with_ast(params.path, [&](DocumentEntry& doc) {
|
||||
return to_raw(
|
||||
feature::folding_ranges(doc.unit, feature::PositionEncoding::UTF16));
|
||||
return to_raw(feature::folding_ranges(doc.unit));
|
||||
});
|
||||
case K::DocumentSymbol:
|
||||
co_return co_await with_ast(params.path, [&](DocumentEntry& doc) {
|
||||
return to_raw(
|
||||
feature::document_symbols(doc.unit, feature::PositionEncoding::UTF16));
|
||||
return to_raw(feature::document_symbols(doc.unit));
|
||||
});
|
||||
case K::DocumentLink:
|
||||
co_return co_await with_ast(params.path, [&](DocumentEntry& doc) {
|
||||
return to_raw(
|
||||
feature::document_links(doc.unit, feature::PositionEncoding::UTF16));
|
||||
return to_raw(feature::document_links(doc.unit));
|
||||
});
|
||||
case K::CodeAction:
|
||||
// TODO: Implement code actions
|
||||
@@ -1,10 +1,10 @@
|
||||
#include "server/worker/stateless_worker.h"
|
||||
#include "server/stateless_worker.h"
|
||||
|
||||
#include "compile/compilation.h"
|
||||
#include "feature/feature.h"
|
||||
#include "index/tu_index.h"
|
||||
#include "server/protocol/worker.h"
|
||||
#include "server/worker/worker_common.h"
|
||||
#include "server/protocol.h"
|
||||
#include "server/worker_common.h"
|
||||
#include "support/logging.h"
|
||||
|
||||
#include "kota/async/async.h"
|
||||
@@ -274,22 +274,6 @@ 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()) {
|
||||
@@ -321,7 +305,6 @@ int run_stateless_worker_mode(const std::string& worker_name, const std::string&
|
||||
}
|
||||
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"};
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "server/worker/worker_pool.h"
|
||||
#include "server/worker_pool.h"
|
||||
|
||||
#include <csignal>
|
||||
#include <string>
|
||||
@@ -96,8 +96,9 @@ 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 + "]";
|
||||
io_group.spawn(drain_stderr(std::move(spawn.stderr_pipe), prefix));
|
||||
loop.schedule(drain_stderr(std::move(spawn.stderr_pipe), prefix));
|
||||
|
||||
workers.push_back(WorkerProcess{
|
||||
.proc = std::move(spawn.proc),
|
||||
@@ -107,7 +108,8 @@ bool WorkerPool::spawn_worker(const std::string& self_path,
|
||||
|
||||
auto& w = workers.back();
|
||||
w.alive = true;
|
||||
io_group.spawn(w.peer->run());
|
||||
++alive_count_;
|
||||
loop.schedule(w.peer->run());
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -116,21 +118,18 @@ bool WorkerPool::start(const WorkerPoolOptions& options) {
|
||||
options_ = options;
|
||||
log_dir_ = options.log_dir;
|
||||
|
||||
stateless_workers.reserve(options.stateless_count);
|
||||
stateful_workers.reserve(options.stateful_count);
|
||||
|
||||
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));
|
||||
loop.schedule(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));
|
||||
loop.schedule(monitor_worker(stateful_workers.size() - 1, true));
|
||||
}
|
||||
|
||||
// Register evicted notification handler for each stateful worker
|
||||
@@ -152,17 +151,23 @@ 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)
|
||||
w.peer->close_output();
|
||||
for(auto& w: stateful_workers)
|
||||
w.peer->close_output();
|
||||
|
||||
// Send SIGTERM. monitor_worker coroutines handle the wait.
|
||||
for(auto& w: stateless_workers)
|
||||
w.proc.kill(SIGTERM);
|
||||
for(auto& w: stateful_workers)
|
||||
w.proc.kill(SIGTERM);
|
||||
|
||||
co_await kota::when_all(monitor_group.join(), io_group.join());
|
||||
// Wait until all monitor_worker coroutines have finished.
|
||||
if(alive_count_ > 0) {
|
||||
all_exited_.reset();
|
||||
co_await all_exited_.wait();
|
||||
}
|
||||
|
||||
LOG_INFO("WorkerPool stopped");
|
||||
}
|
||||
@@ -232,14 +237,18 @@ 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 workers[index].proc.wait();
|
||||
auto& w = workers[index];
|
||||
auto result = co_await w.proc.wait();
|
||||
w.alive = false;
|
||||
--alive_count_;
|
||||
|
||||
if(shutting_down_)
|
||||
if(shutting_down_) {
|
||||
if(alive_count_ == 0)
|
||||
all_exited_.set();
|
||||
co_return;
|
||||
}
|
||||
|
||||
if(result.has_value()) {
|
||||
auto& exit = result.value();
|
||||
@@ -322,7 +331,7 @@ bool WorkerPool::respawn_worker(std::size_t index, bool stateful) {
|
||||
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));
|
||||
loop.schedule(drain_stderr(std::move(spawn.stderr_pipe), prefix));
|
||||
|
||||
workers[index] = WorkerProcess{
|
||||
.proc = std::move(spawn.proc),
|
||||
@@ -333,7 +342,8 @@ bool WorkerPool::respawn_worker(std::size_t index, bool stateful) {
|
||||
};
|
||||
|
||||
auto& w = workers[index];
|
||||
io_group.spawn(w.peer->run());
|
||||
++alive_count_;
|
||||
loop.schedule(w.peer->run());
|
||||
|
||||
if(stateful) {
|
||||
w.peer->on_notification([this](const worker::EvictedParams& params) {
|
||||
@@ -342,7 +352,7 @@ bool WorkerPool::respawn_worker(std::size_t index, bool stateful) {
|
||||
});
|
||||
}
|
||||
|
||||
monitor_group.spawn(monitor_worker(index, stateful));
|
||||
loop.schedule(monitor_worker(index, stateful));
|
||||
|
||||
LOG_INFO("Worker {} restarted (attempt {})", worker_name, old_restart_count);
|
||||
return true;
|
||||
@@ -6,7 +6,7 @@
|
||||
#include <list>
|
||||
#include <memory>
|
||||
|
||||
#include "server/protocol/worker.h"
|
||||
#include "server/protocol.h"
|
||||
|
||||
#include "kota/async/async.h"
|
||||
#include "kota/ipc/codec/bincode.h"
|
||||
@@ -83,8 +83,8 @@ private:
|
||||
std::size_t pick_least_loaded();
|
||||
|
||||
bool shutting_down_ = false;
|
||||
kota::task_group<> monitor_group{loop};
|
||||
kota::task_group<> io_group{loop};
|
||||
std::size_t alive_count_ = 0;
|
||||
kota::event all_exited_{true}; // Signalled when alive_count_ reaches 0.
|
||||
WorkerPoolOptions options_;
|
||||
std::string log_dir_;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "server/workspace/workspace.h"
|
||||
#include "server/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/compiler/compile_graph.h"
|
||||
#include "server/workspace/config.h"
|
||||
#include "server/compile_graph.h"
|
||||
#include "server/config.h"
|
||||
#include "support/path_pool.h"
|
||||
#include "syntax/dependency_graph.h"
|
||||
|
||||
@@ -37,14 +37,6 @@ 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,7 +1,6 @@
|
||||
import asyncio
|
||||
import json
|
||||
import shutil
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
@@ -94,16 +93,17 @@ 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, "--host", host]
|
||||
cmd = [str(executable), "--mode", mode]
|
||||
if mode == "socket":
|
||||
host = config.getoption("--host")
|
||||
port = config.getoption("--port")
|
||||
cmd += ["--host", host, "--port", str(port)]
|
||||
|
||||
c = CliceClient()
|
||||
await c.start_io(*cmd)
|
||||
@@ -122,39 +122,6 @@ 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")
|
||||
@@ -185,94 +152,44 @@ async def make_client(executable: Path, workspace: Path) -> CliceClient:
|
||||
return c
|
||||
|
||||
|
||||
SANITIZER_MARKERS = (
|
||||
"AddressSanitizer",
|
||||
"LeakSanitizer",
|
||||
"MemorySanitizer",
|
||||
"ThreadSanitizer",
|
||||
"UndefinedBehaviorSanitizer",
|
||||
"==ERROR:",
|
||||
"runtime error:",
|
||||
)
|
||||
|
||||
|
||||
def _server_stderr_excerpt(stderr_text: str) -> str:
|
||||
interesting = [
|
||||
line
|
||||
for line in stderr_text.splitlines()
|
||||
if "[warn]" in line
|
||||
or "[error]" in line
|
||||
or "Sanitizer" in line
|
||||
or "==ERROR:" in line
|
||||
or "runtime error:" in line
|
||||
]
|
||||
return "\n".join(interesting[-80:])
|
||||
|
||||
|
||||
async def assert_server_exited_cleanly(server, timeout: float = 3.0) -> None:
|
||||
failures: list[str] = []
|
||||
|
||||
if server is None:
|
||||
return
|
||||
|
||||
if server.returncode is None:
|
||||
try:
|
||||
await asyncio.wait_for(server.wait(), timeout=timeout)
|
||||
except asyncio.TimeoutError:
|
||||
server.kill()
|
||||
await server.wait()
|
||||
failures.append(f"server did not exit within {timeout:g}s after shutdown")
|
||||
|
||||
print(f"[server] exit code: {server.returncode}", flush=True)
|
||||
|
||||
stderr_text = ""
|
||||
if server.stderr:
|
||||
try:
|
||||
stderr_data = await asyncio.wait_for(server.stderr.read(), timeout=2.0)
|
||||
stderr_text = stderr_data.decode("utf-8", errors="replace")
|
||||
except Exception as exc:
|
||||
failures.append(f"failed to collect server stderr: {exc!r}")
|
||||
|
||||
for line in _server_stderr_excerpt(stderr_text).splitlines():
|
||||
print(f"[server] {line}", flush=True)
|
||||
|
||||
if server.returncode != 0:
|
||||
failures.append(f"server exited with code {server.returncode}")
|
||||
|
||||
if any(marker in stderr_text for marker in SANITIZER_MARKERS):
|
||||
failures.append("server stderr contains sanitizer/runtime error output")
|
||||
|
||||
if failures:
|
||||
excerpt = _server_stderr_excerpt(stderr_text)
|
||||
if excerpt:
|
||||
failures.append("server stderr excerpt:\n" + excerpt)
|
||||
pytest.fail("\n".join(failures))
|
||||
|
||||
|
||||
async def _shutdown_client(c: CliceClient) -> None:
|
||||
"""Gracefully shut down a client, force-kill if needed."""
|
||||
server = getattr(c, "_server", None)
|
||||
|
||||
try:
|
||||
await asyncio.wait_for(c.shutdown_async(None), timeout=3.0)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
c.exit(None)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
await asyncio.sleep(0.3)
|
||||
if hasattr(c, "_server") and c._server is not None and c._server.returncode is None:
|
||||
c._server.kill()
|
||||
|
||||
try:
|
||||
await assert_server_exited_cleanly(server)
|
||||
finally:
|
||||
try:
|
||||
c._stop_event.set()
|
||||
for task in c._async_tasks:
|
||||
task.cancel()
|
||||
await asyncio.sleep(0.1)
|
||||
except Exception:
|
||||
pass
|
||||
server = getattr(c, "_server", None)
|
||||
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
|
||||
|
||||
try:
|
||||
c._stop_event.set()
|
||||
for task in c._async_tasks:
|
||||
task.cancel()
|
||||
await asyncio.sleep(0.1)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
shutdown_client = _shutdown_client # Public alias for multi-session tests
|
||||
@@ -342,12 +259,6 @@ 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():
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
// 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
|
||||
@@ -1,3 +0,0 @@
|
||||
BasedOnStyle: LLVM
|
||||
IndentWidth: 4
|
||||
ColumnLimit: 80
|
||||
@@ -1 +0,0 @@
|
||||
int add(int a, int b) { return a + b; }
|
||||
@@ -1,597 +0,0 @@
|
||||
"""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, assert_server_exited_cleanly
|
||||
|
||||
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)
|
||||
|
||||
try:
|
||||
init_options = {
|
||||
"project": {
|
||||
"cache_dir": str(workspace / ".clice"),
|
||||
"idle_timeout_ms": 0,
|
||||
}
|
||||
}
|
||||
try:
|
||||
await c.initialize(workspace, initialization_options=init_options)
|
||||
except Exception:
|
||||
if c._server.returncode is not None:
|
||||
await assert_server_exited_cleanly(c._server, timeout=15.0)
|
||||
raise
|
||||
|
||||
# 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()
|
||||
|
||||
await assert_server_exited_cleanly(c._server, timeout=15.0)
|
||||
finally:
|
||||
c._stop_event.set()
|
||||
for task in c._async_tasks:
|
||||
task.cancel()
|
||||
await asyncio.sleep(0.1)
|
||||
@@ -1,189 +0,0 @@
|
||||
"""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)
|
||||
@@ -1,74 +0,0 @@
|
||||
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,8 +34,6 @@ 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,12 +16,9 @@ from lsprotocol.types import (
|
||||
Diagnostic,
|
||||
DidCloseTextDocumentParams,
|
||||
DidOpenTextDocumentParams,
|
||||
DocumentFormattingParams,
|
||||
DocumentLinkParams,
|
||||
DocumentRangeFormattingParams,
|
||||
DocumentSymbolParams,
|
||||
FoldingRangeParams,
|
||||
FormattingOptions,
|
||||
HoverParams,
|
||||
InlayHintParams,
|
||||
InitializeParams,
|
||||
@@ -95,18 +92,13 @@ 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")],
|
||||
)
|
||||
params.initialization_options = initialization_options
|
||||
if initialization_options is not None:
|
||||
params.initialization_options = initialization_options
|
||||
result = await self.initialize_async(params)
|
||||
self.initialized(InitializedParams())
|
||||
self.init_result = result
|
||||
@@ -315,29 +307,6 @@ 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):
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
---
|
||||
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" }
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
---
|
||||
source: folding_range_tests.cpp
|
||||
created_at: 2026-05-20
|
||||
input_file: statements/if/basic_if.cpp
|
||||
---
|
||||
- { range: "1:19-35:1", kind: namespace, collapsed_text: "{...}" }
|
||||
- { range: "3:19-7:1", kind: functionBody, collapsed_text: "{...}" }
|
||||
- { range: "9:24-17:1", kind: functionBody, collapsed_text: "{...}" }
|
||||
- { range: "20:28-27:1", kind: functionBody, collapsed_text: "{...}" }
|
||||
- { range: "29:12-33:1", kind: functionBody, collapsed_text: "{...}" }
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
---
|
||||
source: inlay_hint_tests.cpp
|
||||
created_at: 2026-05-20
|
||||
input_file: statements/if/basic_if.cpp
|
||||
---
|
||||
- { pos: "30:38", kind: Parameter, label: "x:", padding_right: true }
|
||||
- { pos: "31:28", kind: Type, label: ": const char *" }
|
||||
- { pos: "31:36", kind: Parameter, label: "x:", padding_right: true }
|
||||
- { pos: "32:40", kind: Parameter, label: "a:", padding_right: true }
|
||||
- { pos: "32:43", kind: Parameter, label: "b:", padding_right: true }
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
---
|
||||
source: semantic_tokens_tests.cpp
|
||||
created_at: 2026-05-20
|
||||
input_file: statements/if/basic_if.cpp
|
||||
---
|
||||
- { loc: "0:0", text: "// basic if and if-else", kind: Comment }
|
||||
- { loc: "1:0", text: "namespace", kind: Keyword }
|
||||
- { loc: "1:10", text: "basic_if", kind: Namespace, modifiers: [Definition] }
|
||||
- { loc: "3:0", text: "int", kind: Keyword }
|
||||
- { loc: "3:4", text: "abs_val", kind: Function, modifiers: [Definition] }
|
||||
- { loc: "3:12", text: "int", kind: Keyword }
|
||||
- { loc: "3:16", text: "x", kind: Parameter, modifiers: [Definition] }
|
||||
- { loc: "4:4", text: "if", kind: Keyword }
|
||||
- { loc: "4:7", text: "x", kind: Parameter }
|
||||
- { loc: "4:11", text: "0", kind: Number }
|
||||
- { loc: "5:8", text: "return", kind: Keyword }
|
||||
- { loc: "5:16", text: "x", kind: Parameter }
|
||||
- { loc: "6:4", text: "return", kind: Keyword }
|
||||
- { loc: "6:11", text: "x", kind: Parameter }
|
||||
- { loc: "9:0", text: "const", kind: Keyword }
|
||||
- { loc: "9:6", text: "char", kind: Keyword }
|
||||
- { loc: "9:12", text: "sign", kind: Function, modifiers: [Definition, Readonly] }
|
||||
- { loc: "9:17", text: "int", kind: Keyword }
|
||||
- { loc: "9:21", text: "x", kind: Parameter, modifiers: [Definition] }
|
||||
- { loc: "10:4", text: "if", kind: Keyword }
|
||||
- { loc: "10:7", text: "x", kind: Parameter }
|
||||
- { loc: "10:11", text: "0", kind: Number }
|
||||
- { loc: "11:8", text: "return", kind: Keyword }
|
||||
- { loc: "11:15", text: "\"positive\"", kind: String }
|
||||
- { loc: "12:6", text: "else", kind: Keyword }
|
||||
- { loc: "12:11", text: "if", kind: Keyword }
|
||||
- { loc: "12:14", text: "x", kind: Parameter }
|
||||
- { loc: "12:18", text: "0", kind: Number }
|
||||
- { loc: "13:8", text: "return", kind: Keyword }
|
||||
- { loc: "13:15", text: "\"negative\"", kind: String }
|
||||
- { loc: "14:6", text: "else", kind: Keyword }
|
||||
- { loc: "15:8", text: "return", kind: Keyword }
|
||||
- { loc: "15:15", text: "\"zero\"", kind: String }
|
||||
- { loc: "19:0", text: "// dangling else: else binds to nearest if", kind: Comment }
|
||||
- { loc: "20:0", text: "int", kind: Keyword }
|
||||
- { loc: "20:4", text: "nested_if", kind: Function, modifiers: [Definition] }
|
||||
- { loc: "20:14", text: "int", kind: Keyword }
|
||||
- { loc: "20:18", text: "a", kind: Parameter, modifiers: [Definition] }
|
||||
- { loc: "20:21", text: "int", kind: Keyword }
|
||||
- { loc: "20:25", text: "b", kind: Parameter, modifiers: [Definition] }
|
||||
- { loc: "21:4", text: "if", kind: Keyword }
|
||||
- { loc: "21:7", text: "a", kind: Parameter }
|
||||
- { loc: "21:11", text: "0", kind: Number }
|
||||
- { loc: "22:8", text: "if", kind: Keyword }
|
||||
- { loc: "22:11", text: "b", kind: Parameter }
|
||||
- { loc: "22:15", text: "0", kind: Number }
|
||||
- { loc: "23:12", text: "return", kind: Keyword }
|
||||
- { loc: "23:19", text: "1", kind: Number }
|
||||
- { loc: "24:8", text: "else", kind: Keyword }
|
||||
- { loc: "25:12", text: "return", kind: Keyword }
|
||||
- { loc: "25:19", text: "2", kind: Number }
|
||||
- { loc: "26:4", text: "return", kind: Keyword }
|
||||
- { loc: "26:11", text: "0", kind: Number }
|
||||
- { loc: "29:0", text: "void", kind: Keyword }
|
||||
- { loc: "29:5", text: "test", kind: Function, modifiers: [Definition] }
|
||||
- { loc: "30:21", text: "int", kind: Keyword }
|
||||
- { loc: "30:25", text: "r1", kind: Variable, modifiers: [Definition] }
|
||||
- { loc: "30:30", text: "abs_val", kind: Function }
|
||||
- { loc: "30:39", text: "3", kind: Number }
|
||||
- { loc: "31:21", text: "auto", kind: Keyword }
|
||||
- { loc: "31:26", text: "r2", kind: Variable, modifiers: [Definition, Readonly] }
|
||||
- { loc: "31:31", text: "sign", kind: Function, modifiers: [Readonly] }
|
||||
- { loc: "31:36", text: "5", kind: Number }
|
||||
- { loc: "32:21", text: "int", kind: Keyword }
|
||||
- { loc: "32:25", text: "r3", kind: Variable, modifiers: [Definition] }
|
||||
- { loc: "32:30", text: "nested_if", kind: Function }
|
||||
- { loc: "32:40", text: "1", kind: Number }
|
||||
- { loc: "32:44", text: "1", kind: Number }
|
||||
- { loc: "35:3", text: "// namespace basic_if", kind: Comment }
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
---
|
||||
source: tu_index_tests.cpp
|
||||
created_at: 2026-05-20
|
||||
input_file: statements/if/basic_if.cpp
|
||||
---
|
||||
- { loc: "1:10", kind: Namespace, text: "basic_if", relations: [Definition] }
|
||||
- { loc: "3:4", kind: Function, text: "abs_val", relations: [Definition] }
|
||||
- { loc: "3:16", kind: Parameter, text: "x", relations: [Definition] }
|
||||
- { loc: "4:7", kind: Parameter, text: "x", relations: [Reference] }
|
||||
- { loc: "5:16", kind: Parameter, text: "x", relations: [Reference] }
|
||||
- { loc: "6:11", kind: Parameter, text: "x", relations: [Reference] }
|
||||
- { loc: "9:12", kind: Function, text: "sign", relations: [Definition] }
|
||||
- { loc: "9:21", kind: Parameter, text: "x", relations: [Definition] }
|
||||
- { loc: "10:7", kind: Parameter, text: "x", relations: [Reference] }
|
||||
- { loc: "12:14", kind: Parameter, text: "x", relations: [Reference] }
|
||||
- { loc: "20:4", kind: Function, text: "nested_if", relations: [Definition] }
|
||||
- { loc: "20:18", kind: Parameter, text: "a", relations: [Definition] }
|
||||
- { loc: "20:25", kind: Parameter, text: "b", relations: [Definition] }
|
||||
- { loc: "21:7", kind: Parameter, text: "a", relations: [Reference] }
|
||||
- { loc: "22:11", kind: Parameter, text: "b", relations: [Reference] }
|
||||
- { loc: "29:5", kind: Function, text: "test", relations: [Definition] }
|
||||
- { loc: "30:25", kind: Variable, text: "r1", relations: [Definition] }
|
||||
- { loc: "30:30", kind: Function, text: "abs_val", relations: [Reference] }
|
||||
- { loc: "31:26", kind: Variable, text: "r2", relations: [Definition] }
|
||||
- { loc: "31:31", kind: Function, text: "sign", relations: [Reference] }
|
||||
- { loc: "32:25", kind: Variable, text: "r3", relations: [Definition] }
|
||||
- { loc: "32:30", kind: Function, text: "nested_if", relations: [Reference] }
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace {
|
||||
|
||||
namespace protocol = kota::ipc::protocol;
|
||||
|
||||
TEST_SUITE(document_link, Tester) {
|
||||
TEST_SUITE(DocumentLink, Tester) {
|
||||
|
||||
std::vector<protocol::DocumentLink> links;
|
||||
|
||||
@@ -136,7 +136,7 @@ ABCDE
|
||||
EXPECT_LINK(0, "0", TestVFS::path("data.bin"));
|
||||
}
|
||||
|
||||
}; // TEST_SUITE(document_link)
|
||||
}; // TEST_SUITE(DocumentLink)
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include <cstddef>
|
||||
#include <format>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
@@ -8,15 +7,13 @@
|
||||
#include "test/tester.h"
|
||||
#include "feature/feature.h"
|
||||
|
||||
#include "kota/meta/enum.h"
|
||||
|
||||
namespace clice::testing {
|
||||
|
||||
namespace {
|
||||
|
||||
namespace protocol = kota::ipc::protocol;
|
||||
|
||||
TEST_SUITE(document_symbol, Tester) {
|
||||
TEST_SUITE(DocumentSymbol, Tester) {
|
||||
|
||||
std::vector<protocol::DocumentSymbol> symbols;
|
||||
|
||||
@@ -183,57 +180,7 @@ VAR(test)
|
||||
ASSERT_EQ(total_size(symbols), 3U);
|
||||
}
|
||||
|
||||
void format_document_symbols(std::string& out,
|
||||
const feature::PositionMapper& mapper,
|
||||
llvm::ArrayRef<feature::DocumentSymbol> nodes,
|
||||
int depth) {
|
||||
auto pad = std::string(depth * 2, ' ');
|
||||
for(auto& node: nodes) {
|
||||
auto kind = kota::meta::enum_name(static_cast<SymbolKind::Kind>(node.kind), "Unknown");
|
||||
auto start = mapper.to_position(node.range.begin);
|
||||
auto end = mapper.to_position(node.range.end);
|
||||
if(!start || !end)
|
||||
continue;
|
||||
auto sel_start = mapper.to_position(node.selection_range.begin);
|
||||
auto sel_end = mapper.to_position(node.selection_range.end);
|
||||
out += std::format("- {}{{ name: {}, kind: {}, range: \"{}:{}-{}:{}\"",
|
||||
pad,
|
||||
yaml_str(node.name),
|
||||
kind,
|
||||
start->line,
|
||||
start->character,
|
||||
end->line,
|
||||
end->character);
|
||||
if(sel_start && sel_end) {
|
||||
out += std::format(", selection_range: \"{}:{}-{}:{}\"",
|
||||
sel_start->line,
|
||||
sel_start->character,
|
||||
sel_end->line,
|
||||
sel_end->character);
|
||||
}
|
||||
if(!node.detail.empty()) {
|
||||
out += std::format(", detail: {}", yaml_str(node.detail));
|
||||
}
|
||||
out += " }\n";
|
||||
if(!node.children.empty()) {
|
||||
format_document_symbols(out, mapper, node.children, depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE(snapshot) {
|
||||
ASSERT_SNAPSHOT_GLOB(corpus_dir, "**/*.cpp", [&](std::string_view path) -> std::string {
|
||||
if(!compile_file(path))
|
||||
return "COMPILE_ERROR";
|
||||
auto content = unit->interested_content();
|
||||
feature::PositionMapper mapper(content, feature::PositionEncoding::UTF8);
|
||||
std::string result;
|
||||
format_document_symbols(result, mapper, feature::document_symbols(*unit), 0);
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
}; // TEST_SUITE(document_symbol)
|
||||
}; // TEST_SUITE(DocumentSymbol)
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace {
|
||||
|
||||
namespace protocol = kota::ipc::protocol;
|
||||
|
||||
TEST_SUITE(folding_range, Tester) {
|
||||
TEST_SUITE(FoldingRange, Tester) {
|
||||
|
||||
std::vector<protocol::FoldingRange> ranges;
|
||||
|
||||
@@ -429,36 +429,7 @@ $(1)#pragma region level1
|
||||
)cpp");
|
||||
}
|
||||
|
||||
TEST_CASE(snapshot) {
|
||||
ASSERT_SNAPSHOT_GLOB(corpus_dir, "**/*.cpp", [&](std::string_view path) -> std::string {
|
||||
if(!compile_file(path))
|
||||
return "COMPILE_ERROR";
|
||||
auto ranges = feature::folding_ranges(*unit);
|
||||
feature::PositionMapper mapper(unit->interested_content(), feature::PositionEncoding::UTF8);
|
||||
std::string result;
|
||||
for(auto& r: ranges) {
|
||||
auto start = mapper.to_position(r.range.begin);
|
||||
auto end = mapper.to_position(r.range.end);
|
||||
if(!start || !end)
|
||||
continue;
|
||||
result += std::format("- {{ range: \"{}:{}-{}:{}\"",
|
||||
start->line,
|
||||
start->character,
|
||||
end->line,
|
||||
end->character);
|
||||
if(r.kind.has_value()) {
|
||||
result += std::format(", kind: {}", static_cast<const std::string&>(*r.kind));
|
||||
}
|
||||
if(!r.collapsed_text.empty()) {
|
||||
result += std::format(", collapsed_text: {}", yaml_str(r.collapsed_text));
|
||||
}
|
||||
result += " }\n";
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
}; // TEST_SUITE(folding_range)
|
||||
}; // TEST_SUITE(FoldingRange)
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
@@ -12,29 +12,6 @@ TEST_CASE(Simple) {
|
||||
ASSERT_NE(edits.size(), 0U);
|
||||
}
|
||||
|
||||
TEST_CASE(RangeFormat) {
|
||||
llvm::StringRef code = "int x=1;\nint y = 2 ;\nint z=3;\n";
|
||||
LocalSourceRange range;
|
||||
range.begin = static_cast<std::uint32_t>(code.find("int y"));
|
||||
range.end = static_cast<std::uint32_t>(code.find("\nint z") + 1);
|
||||
auto range_edits = feature::document_format("main.cpp", code, range);
|
||||
auto full_edits = feature::document_format("main.cpp", code, std::nullopt);
|
||||
ASSERT_NE(range_edits.size(), 0U);
|
||||
EXPECT_LE(range_edits.size(), full_edits.size());
|
||||
}
|
||||
|
||||
TEST_CASE(Idempotent) {
|
||||
llvm::StringRef code = "int main() {\n return 0;\n}\n";
|
||||
auto edits = feature::document_format("main.cpp", code, std::nullopt);
|
||||
EXPECT_EQ(edits.size(), 0U);
|
||||
}
|
||||
|
||||
TEST_CASE(IncludeSort) {
|
||||
llvm::StringRef code = "#include <vector>\n#include <algorithm>\n\nint main() {}\n";
|
||||
auto edits = feature::document_format("main.cpp", code, std::nullopt);
|
||||
ASSERT_NE(edits.size(), 0U);
|
||||
}
|
||||
|
||||
}; // TEST_SUITE(Formatting)
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
#include <format>
|
||||
#include <string>
|
||||
|
||||
#include "test/test.h"
|
||||
#include "test/tester.h"
|
||||
#include "feature/feature.h"
|
||||
|
||||
#include "kota/meta/enum.h"
|
||||
|
||||
namespace clice::testing {
|
||||
|
||||
namespace {
|
||||
|
||||
namespace protocol = kota::ipc::protocol;
|
||||
|
||||
TEST_SUITE(inlay_hint, Tester) {
|
||||
TEST_SUITE(InlayHint, Tester) {
|
||||
|
||||
std::vector<protocol::InlayHint> hints;
|
||||
llvm::DenseMap<std::uint32_t, protocol::InlayHint> hints_map;
|
||||
@@ -1532,38 +1529,6 @@ TEST_CASE(Dependent, skip = true) {
|
||||
EXPECT_HINT("2", "par3:");
|
||||
}
|
||||
|
||||
TEST_CASE(snapshot) {
|
||||
ASSERT_SNAPSHOT_GLOB(corpus_dir, "**/*.cpp", [&](std::string_view path) -> std::string {
|
||||
if(!compile_file(path))
|
||||
return "COMPILE_ERROR";
|
||||
auto content = unit->interested_content();
|
||||
LocalSourceRange range(0, content.size());
|
||||
auto hints = feature::inlay_hints(*unit, range);
|
||||
feature::PositionMapper mapper(content, feature::PositionEncoding::UTF8);
|
||||
std::string result;
|
||||
for(auto& hint: hints) {
|
||||
auto pos = mapper.to_position(hint.offset);
|
||||
if(!pos)
|
||||
continue;
|
||||
auto kind = kota::meta::enum_name(hint.kind, "Unknown");
|
||||
result += std::format("- {{ pos: \"{}:{}\", kind: {}, label: {}",
|
||||
pos->line,
|
||||
pos->character,
|
||||
kind,
|
||||
yaml_str(hint.label));
|
||||
if(hint.padding_left) {
|
||||
result += ", padding_left: true";
|
||||
}
|
||||
if(hint.padding_right) {
|
||||
result += ", padding_right: true";
|
||||
}
|
||||
result += " }\n";
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
}; // TEST_SUITE(inlay_hint)
|
||||
|
||||
}; // TEST_SUITE(InlayHint)
|
||||
} // namespace
|
||||
} // namespace clice::testing
|
||||
|
||||
@@ -9,8 +9,6 @@
|
||||
#include "feature/feature.h"
|
||||
#include "semantic/symbol_kind.h"
|
||||
|
||||
#include "kota/meta/enum.h"
|
||||
|
||||
namespace clice::testing {
|
||||
|
||||
namespace {
|
||||
@@ -101,7 +99,7 @@ auto decode_relative_tokens(const protocol::SemanticTokens& tokens) -> std::vect
|
||||
return result;
|
||||
}
|
||||
|
||||
TEST_SUITE(semantic_tokens, Tester) {
|
||||
TEST_SUITE(SemanticTokens, Tester) {
|
||||
|
||||
protocol::SemanticTokens tokens;
|
||||
std::vector<DecodedToken> decoded;
|
||||
@@ -541,53 +539,7 @@ void f() {
|
||||
EXPECT_TOKEN("v2", SymbolKind::Variable, definition);
|
||||
}
|
||||
|
||||
TEST_CASE(snapshot) {
|
||||
ASSERT_SNAPSHOT_GLOB(corpus_dir, "**/*.cpp", [&](std::string_view path) -> std::string {
|
||||
if(!compile_file(path))
|
||||
return "COMPILE_ERROR";
|
||||
auto content = unit->interested_content();
|
||||
auto tokens = feature::semantic_tokens(*unit);
|
||||
feature::PositionMapper mapper(content, feature::PositionEncoding::UTF8);
|
||||
std::string result;
|
||||
for(auto& token: tokens) {
|
||||
if(!token.range.valid() || token.range.end <= token.range.begin ||
|
||||
token.range.end > content.size())
|
||||
continue;
|
||||
|
||||
auto pos = mapper.to_position(token.range.begin);
|
||||
if(!pos)
|
||||
continue;
|
||||
|
||||
auto text = content.substr(token.range.begin, token.range.length());
|
||||
auto kind = kota::meta::enum_name(static_cast<SymbolKind::Kind>(token.kind), "Unknown");
|
||||
|
||||
result += std::format("- {{ loc: \"{}:{}\", text: {}, kind: {}",
|
||||
pos->line,
|
||||
pos->character,
|
||||
yaml_str(text),
|
||||
kind);
|
||||
|
||||
std::string mods;
|
||||
for(std::uint32_t i = 0; i < 32; ++i) {
|
||||
if(token.modifiers & (1u << i)) {
|
||||
auto name = kota::meta::enum_name(static_cast<SymbolModifiers::Kind>(i));
|
||||
if(!name.empty()) {
|
||||
if(!mods.empty())
|
||||
mods += ", ";
|
||||
mods += name;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(!mods.empty()) {
|
||||
result += std::format(", modifiers: [{}]", mods);
|
||||
}
|
||||
result += " }\n";
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
}; // TEST_SUITE(semantic_tokens)
|
||||
}; // TEST_SUITE(SemanticTokens)
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
#include <algorithm>
|
||||
#include <format>
|
||||
#include <set>
|
||||
|
||||
#include "test/test.h"
|
||||
#include "test/tester.h"
|
||||
#include "feature/feature.h"
|
||||
#include "index/tu_index.h"
|
||||
|
||||
#include "kota/meta/enum.h"
|
||||
|
||||
namespace clice::testing {
|
||||
namespace {
|
||||
|
||||
TEST_SUITE(tu_index, Tester) {
|
||||
TEST_SUITE(TUIndex, Tester) {
|
||||
|
||||
index::TUIndex tu_index;
|
||||
|
||||
@@ -505,64 +500,6 @@ TEST_CASE(SymbolKinds) {
|
||||
check_kind("ns", SymbolKind::Namespace);
|
||||
}
|
||||
|
||||
TEST_CASE(snapshot) {
|
||||
ASSERT_SNAPSHOT_GLOB(corpus_dir, "**/*.cpp", [&](std::string_view path) -> std::string {
|
||||
if(!compile_file(path))
|
||||
return "COMPILE_ERROR";
|
||||
auto idx = index::TUIndex::build(*unit);
|
||||
auto content = unit->interested_content();
|
||||
feature::PositionMapper mapper(content, feature::PositionEncoding::UTF8);
|
||||
std::string result;
|
||||
|
||||
auto sorted = idx.main_file_index.occurrences;
|
||||
std::ranges::sort(sorted, [](auto& lhs, auto& rhs) {
|
||||
return std::tuple(lhs.range.begin, lhs.range.end, lhs.target) <
|
||||
std::tuple(rhs.range.begin, rhs.range.end, rhs.target);
|
||||
});
|
||||
|
||||
for(auto& occ: sorted) {
|
||||
auto text = content.substr(occ.range.begin, occ.range.end - occ.range.begin);
|
||||
auto pos = mapper.to_position(occ.range.begin);
|
||||
if(!pos)
|
||||
continue;
|
||||
|
||||
auto sym_it = idx.symbols.find(occ.target);
|
||||
std::string_view kind_name = "?";
|
||||
if(sym_it != idx.symbols.end()) {
|
||||
kind_name =
|
||||
kota::meta::enum_name(static_cast<SymbolKind::Kind>(sym_it->second.kind),
|
||||
"Unknown");
|
||||
}
|
||||
|
||||
result += std::format("- {{ loc: \"{}:{}\", kind: {}, text: {}",
|
||||
pos->line,
|
||||
pos->character,
|
||||
kind_name,
|
||||
yaml_str(text));
|
||||
|
||||
auto rel_it = idx.main_file_index.relations.find(occ.target);
|
||||
if(rel_it != idx.main_file_index.relations.end()) {
|
||||
std::string rels;
|
||||
for(auto& rel: rel_it->second) {
|
||||
if(rel.range != occ.range)
|
||||
continue;
|
||||
if(!rels.empty())
|
||||
rels += ", ";
|
||||
rels += kota::meta::enum_name(static_cast<RelationKind::Kind>(rel.kind), "?");
|
||||
}
|
||||
if(!rels.empty()) {
|
||||
result += std::format(", relations: [{}]", rels);
|
||||
}
|
||||
}
|
||||
|
||||
result += " }\n";
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
}; // TEST_SUITE(tu_index)
|
||||
|
||||
}; // TEST_SUITE(TUIndex)
|
||||
} // namespace
|
||||
} // namespace clice::testing
|
||||
|
||||
@@ -456,6 +456,8 @@ TEST_CASE(BasePackExpansion) {
|
||||
)code");
|
||||
}
|
||||
|
||||
// --- Robustness tests for edge cases found during stress testing ---
|
||||
|
||||
TEST_CASE(RecursiveBaseClass) {
|
||||
// Regression test: callback_traits<F> inherits callback_traits<decltype(&F::operator())>,
|
||||
// creating infinite recursion through lookupInBases. CTD cycle detection must bail out.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#include "test/test.h"
|
||||
#include "command/command.h"
|
||||
#include "compile/compilation.h"
|
||||
#include "server/compiler/compile_graph.h"
|
||||
#include "server/compile_graph.h"
|
||||
#include "support/path_pool.h"
|
||||
#include "syntax/dependency_graph.h"
|
||||
#include "syntax/scan.h"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#include <optional>
|
||||
|
||||
#include "test/test.h"
|
||||
#include "server/compiler/compile_graph.h"
|
||||
#include "server/compile_graph.h"
|
||||
|
||||
namespace clice::testing {
|
||||
namespace {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
#include "test/temp_dir.h"
|
||||
#include "test/test.h"
|
||||
#include "server/workspace/config.h"
|
||||
#include "server/config.h"
|
||||
#include "support/filesystem.h"
|
||||
|
||||
#include "kota/codec/json/json.h"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include "test/test.h"
|
||||
#include "server/protocol/worker.h"
|
||||
#include "server/protocol.h"
|
||||
#include "server/worker_test_helpers.h"
|
||||
|
||||
namespace clice::testing {
|
||||
@@ -29,6 +29,7 @@ TEST_CASE(BuildPCMThenCompileWithImport) {
|
||||
tmp.touch("consumer.cpp", "import Hello;\n" "int main() { return hello()[0]; }\n");
|
||||
auto consumer = tmp.path("consumer.cpp");
|
||||
|
||||
// --- Phase 1: Build PCM via stateless worker ---
|
||||
WorkerHandle sl;
|
||||
ASSERT_TRUE(sl.spawn("stateless-worker"));
|
||||
|
||||
@@ -62,6 +63,7 @@ TEST_CASE(BuildPCMThenCompileWithImport) {
|
||||
ASSERT_TRUE(phase1_done);
|
||||
ASSERT_FALSE(pcm_path.empty());
|
||||
|
||||
// --- Phase 2: Compile consumer with the PCM via stateful worker ---
|
||||
WorkerHandle sf;
|
||||
ASSERT_TRUE(sf.spawn("stateful-worker"));
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include "test/test.h"
|
||||
#include "server/protocol/worker.h"
|
||||
#include "server/protocol.h"
|
||||
#include "server/worker_test_helpers.h"
|
||||
#include "syntax/scan.h"
|
||||
|
||||
@@ -30,6 +30,7 @@ TEST_CASE(BuildPCHThenCompile) {
|
||||
|
||||
auto dir = std::string(tmp.root);
|
||||
|
||||
// --- Phase 1: Build PCH via stateless worker ---
|
||||
WorkerHandle sl;
|
||||
ASSERT_TRUE(sl.spawn("stateless-worker"));
|
||||
|
||||
@@ -68,6 +69,7 @@ TEST_CASE(BuildPCHThenCompile) {
|
||||
// Verify the PCH file exists on disk.
|
||||
ASSERT_TRUE(llvm::sys::fs::exists(pch_path));
|
||||
|
||||
// --- Phase 2: Compile with PCH via stateful worker ---
|
||||
WorkerHandle sf;
|
||||
ASSERT_TRUE(sf.spawn("stateful-worker"));
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include "test/test.h"
|
||||
#include "server/protocol/worker.h"
|
||||
#include "server/protocol.h"
|
||||
#include "server/worker_test_helpers.h"
|
||||
|
||||
#include "kota/codec/json/json.h"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include "test/test.h"
|
||||
#include "server/protocol/worker.h"
|
||||
#include "server/protocol.h"
|
||||
#include "server/worker_test_helpers.h"
|
||||
|
||||
#include "kota/codec/bincode/bincode.h"
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
#include "test/temp_dir.h"
|
||||
#include "command/argument_parser.h"
|
||||
#include "command/command.h"
|
||||
#include "server/protocol/worker.h"
|
||||
#include "server/protocol.h"
|
||||
#include "support/filesystem.h"
|
||||
|
||||
#include "kota/async/async.h"
|
||||
|
||||
@@ -291,6 +291,8 @@ int x;
|
||||
|
||||
TEST_SUITE(PreambleComplete) {
|
||||
|
||||
// --- #include completeness ---
|
||||
|
||||
TEST_CASE(CompleteQuotedInclude) {
|
||||
llvm::StringRef content = "#include \"foo.h\"\nint x;";
|
||||
auto bound = compute_preamble_bound(content);
|
||||
@@ -339,7 +341,8 @@ TEST_CASE(MultipleIncludesLastIncomplete) {
|
||||
EXPECT_FALSE(is_preamble_complete(content, bound));
|
||||
}
|
||||
|
||||
// compute_preamble_bound does not include import/export lines in its
|
||||
// --- C++20 module statements ---
|
||||
// Note: compute_preamble_bound does not include import/export lines in its
|
||||
// bound, so we pass manual bounds covering the relevant lines.
|
||||
|
||||
TEST_CASE(CompleteImport) {
|
||||
@@ -378,6 +381,8 @@ TEST_CASE(CompleteExportImport) {
|
||||
EXPECT_TRUE(is_preamble_complete(content, 19));
|
||||
}
|
||||
|
||||
// --- Edge cases ---
|
||||
|
||||
TEST_CASE(EmptyPreamble) {
|
||||
llvm::StringRef content = "int x;";
|
||||
EXPECT_TRUE(is_preamble_complete(content, 0));
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#include <string>
|
||||
|
||||
#include "llvm/ADT/SmallString.h"
|
||||
#include "llvm/Support/MemoryBuffer.h"
|
||||
#include "llvm/Support/Path.h"
|
||||
@@ -7,12 +5,6 @@
|
||||
|
||||
namespace clice::testing {
|
||||
|
||||
/// Set by --test-dir from the command line; empty if not specified.
|
||||
inline std::string test_dir;
|
||||
|
||||
/// Set by --corpus-dir from the command line; empty if not specified.
|
||||
inline std::string corpus_dir;
|
||||
|
||||
#ifdef _WIN32
|
||||
constexpr inline bool Windows = true;
|
||||
#else
|
||||
|
||||
@@ -230,17 +230,6 @@ bool Tester::compile_with_modules(llvm::StringRef standard) {
|
||||
return try_compile();
|
||||
}
|
||||
|
||||
bool Tester::compile_file(llvm::StringRef path, llvm::StringRef standard) {
|
||||
auto buffer = llvm::MemoryBuffer::getFile(path);
|
||||
if(!buffer) {
|
||||
LOG_ERROR("Failed to read file: {}", path);
|
||||
return false;
|
||||
}
|
||||
auto filename = llvm::sys::path::filename(path);
|
||||
add_main(filename, (*buffer)->getBuffer());
|
||||
return compile(standard);
|
||||
}
|
||||
|
||||
std::uint32_t Tester::point(llvm::StringRef name, llvm::StringRef file) {
|
||||
if(file.empty()) {
|
||||
file = src_path;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <format>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -64,9 +63,6 @@ struct Tester {
|
||||
|
||||
bool compile_with_modules(llvm::StringRef standard = "-std=c++20");
|
||||
|
||||
/// Read a file from disk and compile it directly (no VFS content needed).
|
||||
bool compile_file(llvm::StringRef path, llvm::StringRef standard = "-std=c++20");
|
||||
|
||||
/// Driver path: uses CompilationDatabase + toolchain cache, has system headers.
|
||||
void prepare_driver(llvm::StringRef standard = "-std=c++20");
|
||||
|
||||
@@ -89,28 +85,4 @@ struct Tester {
|
||||
void clear();
|
||||
};
|
||||
|
||||
inline std::string yaml_str(llvm::StringRef s) {
|
||||
std::string result;
|
||||
result.reserve(s.size() + 2);
|
||||
result += '"';
|
||||
for(char c: s) {
|
||||
switch(c) {
|
||||
case '"': result += "\\\""; break;
|
||||
case '\\': result += "\\\\"; break;
|
||||
case '\n': result += "\\n"; break;
|
||||
case '\r': result += "\\r"; break;
|
||||
case '\t': result += "\\t"; break;
|
||||
default:
|
||||
if(static_cast<unsigned char>(c) < 0x20) {
|
||||
result += std::format("\\x{:02x}", static_cast<unsigned char>(c));
|
||||
} else {
|
||||
result += c;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
result += '"';
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace clice::testing
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include "test/platform.h"
|
||||
#include "support/logging.h"
|
||||
|
||||
#include "kota/deco/deco.h"
|
||||
@@ -12,20 +11,23 @@ namespace {
|
||||
using kota::deco::decl::KVStyle;
|
||||
|
||||
struct TestOptions {
|
||||
kota::zest::Options zest;
|
||||
DecoKV(style = KVStyle::JoinedOrSeparate,
|
||||
names = {"--test-filter", "--test-filter="},
|
||||
help = "Filter tests by name",
|
||||
required = false)
|
||||
<std::string> test_filter;
|
||||
|
||||
DecoKVStyled(KVStyle::JoinedOrSeparate, help = "log level: trace/debug/info/warn/err";
|
||||
required = false)
|
||||
DecoKV(style = KVStyle::JoinedOrSeparate,
|
||||
names = {"--log-level", "--log-level="},
|
||||
help = "Log level: trace/debug/info/warn/err",
|
||||
required = false)
|
||||
<std::string> log_level;
|
||||
|
||||
DecoKVStyled(KVStyle::JoinedOrSeparate, meta_var = "<DIR>"; help = "test data directory";
|
||||
required = false)
|
||||
DecoKV(style = KVStyle::JoinedOrSeparate,
|
||||
names = {"--test-dir", "--test-dir="},
|
||||
help = "Test data directory",
|
||||
required = false)
|
||||
<std::string> test_dir;
|
||||
|
||||
DecoKVStyled(KVStyle::JoinedOrSeparate, meta_var = "<DIR>";
|
||||
help = "corpus directory for snapshot glob tests";
|
||||
required = false)
|
||||
<std::string> corpus_dir;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
@@ -34,20 +36,13 @@ int main(int argc, const char** argv) {
|
||||
auto args = kota::deco::util::argvify(argc, argv);
|
||||
auto parsed = kota::deco::cli::parse<TestOptions>(args);
|
||||
|
||||
if(!parsed.has_value()) {
|
||||
return 1;
|
||||
std::string_view filter = {};
|
||||
if(parsed.has_value() && parsed->options.test_filter.has_value()) {
|
||||
filter = *parsed->options.test_filter;
|
||||
}
|
||||
|
||||
auto& opts = parsed->options;
|
||||
|
||||
if(opts.test_dir.has_value())
|
||||
clice::testing::test_dir = *opts.test_dir;
|
||||
|
||||
if(opts.corpus_dir.has_value())
|
||||
clice::testing::corpus_dir = *opts.corpus_dir;
|
||||
|
||||
if(opts.log_level.has_value()) {
|
||||
auto level = *opts.log_level;
|
||||
if(parsed.has_value() && parsed->options.log_level.has_value()) {
|
||||
auto level = *parsed->options.log_level;
|
||||
if(level == "trace") {
|
||||
clice::logging::options.level = clice::logging::Level::trace;
|
||||
} else if(level == "debug") {
|
||||
@@ -63,5 +58,5 @@ int main(int argc, const char** argv) {
|
||||
|
||||
clice::logging::stderr_logger("test", clice::logging::options);
|
||||
|
||||
return kota::zest::run_tests(std::move(opts.zest));
|
||||
return kota::zest::Runner::instance().run_tests(filter);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user