Compare commits
36 Commits
openspec-p
...
feat/docum
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f714c3b4a | ||
|
|
ccc805d0c3 | ||
|
|
e554660c06 | ||
|
|
342d82a7aa | ||
|
|
d48236de9c | ||
|
|
b691ed1d06 | ||
|
|
02e4f74347 | ||
|
|
8af2704723 | ||
|
|
4d8c335c0d | ||
|
|
3dab2ead93 | ||
|
|
bd238fe59c | ||
|
|
4926b4ac32 | ||
|
|
13527b7084 | ||
|
|
8b3e3a9595 | ||
|
|
2bbdf6c02b | ||
|
|
9c9e6b0bcb | ||
|
|
bb0b160a28 | ||
|
|
ada202e489 | ||
|
|
836f415e50 | ||
|
|
1627b96d2b | ||
|
|
a40c0b3bf8 | ||
|
|
d253c1f099 | ||
|
|
0c107fc2c5 | ||
|
|
018bad4ea8 | ||
|
|
e239b0d32c | ||
|
|
aae246e465 | ||
|
|
d04bc6f774 | ||
|
|
8d4ad26834 | ||
|
|
b6886d222b | ||
|
|
c14b8de18f | ||
|
|
3838bedcbf | ||
|
|
31d9c609b6 | ||
|
|
a1b6c0632d | ||
|
|
1dd94e54c0 | ||
|
|
e24eff6c16 | ||
|
|
c697ffcf91 |
50
.clang-tidy
Normal file
50
.clang-tidy
Normal file
@@ -0,0 +1,50 @@
|
||||
---
|
||||
Checks: >
|
||||
-*,
|
||||
bugprone-*,
|
||||
modernize-*,
|
||||
performance-*,
|
||||
readability-*,
|
||||
-modernize-use-trailing-return-type,
|
||||
-readability-magic-numbers,
|
||||
-readability-else-after-return,
|
||||
-readability-braces-around-statements,
|
||||
-readability-avoid-const-params-in-decls,
|
||||
-readability-named-parameter,
|
||||
-readability-implicit-bool-conversion,
|
||||
-readability-use-anyofallof,
|
||||
-bugprone-easily-swappable-parameters,
|
||||
-bugprone-exception-escape,
|
||||
-bugprone-narrowing-conversions,
|
||||
-modernize-use-nodiscard,
|
||||
|
||||
WarningsAsErrors: ""
|
||||
|
||||
HeaderFilterRegex: "(src|tests)/.*"
|
||||
|
||||
CheckOptions:
|
||||
# Naming conventions matching project style
|
||||
- key: readability-identifier-naming.ClassCase
|
||||
value: CamelCase
|
||||
- key: readability-identifier-naming.StructCase
|
||||
value: CamelCase
|
||||
- key: readability-identifier-naming.EnumCase
|
||||
value: CamelCase
|
||||
- key: readability-identifier-naming.EnumConstantCase
|
||||
value: CamelCase
|
||||
- key: readability-identifier-naming.TemplateParameterCase
|
||||
value: CamelCase
|
||||
- key: readability-identifier-naming.TypeAliasCase
|
||||
value: CamelCase
|
||||
- key: readability-identifier-naming.FunctionCase
|
||||
value: lower_case
|
||||
- key: readability-identifier-naming.MethodCase
|
||||
value: lower_case
|
||||
- key: readability-identifier-naming.VariableCase
|
||||
value: lower_case
|
||||
- key: readability-identifier-naming.ParameterCase
|
||||
value: lower_case
|
||||
- key: readability-identifier-naming.MemberCase
|
||||
value: lower_case
|
||||
- key: readability-identifier-naming.NamespaceCase
|
||||
value: lower_case
|
||||
268
.claude/CLAUDE.md
Normal file
268
.claude/CLAUDE.md
Normal file
@@ -0,0 +1,268 @@
|
||||
# clice — Project Guide
|
||||
|
||||
## Project Overview
|
||||
|
||||
clice is a next-generation C++ language server (LSP) built on LLVM/Clang, targeting modern C++ (C++20/23). It uses a multi-process architecture with a master server coordinating stateless and stateful workers.
|
||||
|
||||
## Core Correction Patterns — Lessons from Past Interactions
|
||||
|
||||
The following patterns were extracted from extensive real-world collaboration. These are recurring mistakes that MUST be avoided. Read them carefully — they represent hard-won lessons, not hypothetical concerns.
|
||||
|
||||
### Pattern 1: Misjudging Real-World Priorities
|
||||
|
||||
AI tends to optimize whatever metric looks most impressive, rather than what actually matters in the user's real scenario.
|
||||
|
||||
**Example**: During performance optimization, the AI proudly reported "hot cache is 4-7x faster!" — but the function in question runs at LSP server startup, which is ALWAYS a cold start. Optimizing hot cache was completely meaningless.
|
||||
|
||||
**Rule**: Before optimizing or analyzing anything, first understand the REAL usage scenario. Ask yourself: "When does this code actually run? What does the user actually experience?" Do not chase metrics that look good on paper but are irrelevant in practice.
|
||||
|
||||
### Pattern 2: Pushing Without Local Verification
|
||||
|
||||
The most common and most damaging pattern. AI proposes a fix, pushes it immediately, CI fails, then another fix, push, fail again — wasting CI cycles and the user's time.
|
||||
|
||||
**Rule**: NEVER push code that you haven't verified locally. Before every push:
|
||||
|
||||
- Build locally with the same configuration CI uses.
|
||||
- Run the relevant tests locally and confirm they pass.
|
||||
- If you cannot reproduce the CI environment locally, say so — do not just "try and see."
|
||||
- "It compiles" is NOT sufficient. Tests must pass.
|
||||
|
||||
### Pattern 3: Superficial Refactoring
|
||||
|
||||
When asked to refactor, AI tends to do mechanical code movement (copy functions from A to B) without understanding the deeper design intent (ownership, responsibility boundaries, API cleanliness).
|
||||
|
||||
**Example**: When splitting `MasterServer` into `Workspace` and `Session`, the AI moved functions but kept ugly APIs like `f(path_id, sessions_map)` instead of the clean `f(Session&)` that the refactoring was meant to achieve.
|
||||
|
||||
**Rule**: When refactoring, understand the WHY. Ask: "What design problem is this refactoring solving?" If you're just moving code around without improving the abstractions, you're not refactoring — you're rearranging deck chairs.
|
||||
|
||||
### Pattern 4: Fixing Only the Immediate Instance, Not the Pattern
|
||||
|
||||
When given a cleanup instruction, AI applies it to the single file or function currently being discussed, ignoring all other occurrences in the project.
|
||||
|
||||
**Example**: User says "remove decorative `===` comment separators." AI removes them from `workspace.cpp` only. User has to say: "The other files too!"
|
||||
|
||||
**Rule**: When given a cleanup or style instruction, apply it project-wide. Use `Grep` to find ALL occurrences and fix them all in one pass. Think: "Where else does this pattern appear?"
|
||||
|
||||
### Pattern 5: Never Skip, Disable, or Work Around Failing Tests
|
||||
|
||||
When stuck on a difficult bug (especially flaky CI, race conditions, platform-specific issues), AI may propose marking tests as `continue-on-error`, skipping them, or adding `expected-failure` annotations to make CI green.
|
||||
|
||||
**Rule**: This is ABSOLUTELY FORBIDDEN. If a test fails, fix the root cause. There are ZERO exceptions. Skipping a test to make CI green is not "fixing" — it is hiding a bug. If you ever find yourself thinking "maybe we should just skip this test," stop and reconsider your approach entirely.
|
||||
|
||||
### Pattern 6: Excessive Confirmation Seeking vs. Premature Execution
|
||||
|
||||
AI oscillates between two extremes: asking "should I do X?" for every trivial decision, or silently executing major changes without confirmation.
|
||||
|
||||
**Rule**: Calibrate based on reversibility and impact:
|
||||
|
||||
- **Small, reversible changes** (formatting, renaming a local variable, adding a test): just do it.
|
||||
- **Architecture decisions, API changes, large refactors**: propose the plan first, wait for confirmation.
|
||||
- **Pushing to remote, creating PRs, modifying CI**: always confirm.
|
||||
- When the user says "go ahead" or "do it," execute fully without asking again mid-way.
|
||||
|
||||
## Code Reuse & Understanding Before Implementation
|
||||
|
||||
**This is the single most important rule in this project.** Before writing ANY new code, you MUST thoroughly read and understand the existing codebase first. This project has a rich set of utilities, abstractions, and patterns already in place — duplicating them wastes effort and creates maintenance burden.
|
||||
|
||||
Concrete requirements:
|
||||
|
||||
1. **Read before you write.** Before implementing a feature or fix, explore the relevant modules in `src/`. Search for existing helpers, utilities, and patterns that solve the same or similar problems. Use `Grep`, `Glob`, and `Agent` tools to investigate thoroughly — do not assume something doesn't exist just because you haven't seen it yet.
|
||||
|
||||
2. **Reuse existing infrastructure.** This project already has:
|
||||
- A `Lexer` class (`src/syntax/lexer.h`) — do not hand-write token scanning logic.
|
||||
- A `PositionMapper` for source location conversion — do not reimplement offset-to-line/column math.
|
||||
- `CompilationUnitRef` methods (`decompose_location`, `decompose_range`, `file_path`, `directives`, etc.) — use them instead of raw Clang APIs.
|
||||
- `SemanticVisitor` for AST traversal — extend it, do not write custom recursive AST walkers.
|
||||
- `Tester` framework for unit tests with VFS, annotation support, and multi-phase compilation — use it, do not create ad-hoc test setups.
|
||||
- Utility functions in `src/support/` — check there before writing new helpers.
|
||||
|
||||
3. **Follow established patterns.** When adding a new feature (e.g., a new LSP request handler), look at how 2-3 existing features of the same kind are implemented. Match their structure: same file organization, same function signatures, same error handling patterns. If every other feature in `src/feature/` follows a certain pattern, yours should too.
|
||||
|
||||
4. **Do not reinvent what the project already has.** If you find yourself writing a helper function that feels generic (string manipulation, path handling, JSON serialization, source range conversion), STOP and search the codebase first. There is a high probability it already exists. Creating duplicates leads to inconsistencies and bugs when one copy gets updated but the other doesn't.
|
||||
|
||||
5. **When in doubt, ask.** If you're unsure whether an existing utility covers your use case or whether to extend an existing abstraction vs. create a new one, ask the user rather than guessing.
|
||||
|
||||
## Source Layout
|
||||
|
||||
- `src/server/` — LSP server core: master server, compiler, indexer, stateful/stateless workers
|
||||
- `src/feature/` — LSP feature implementations: hover, completion, document links, semantic tokens, etc.
|
||||
- `src/compile/` — Compilation orchestration: compilation unit, directives, diagnostics
|
||||
- `src/index/` — Symbol indexing: TUIndex, ProjectIndex, MergedIndex, include graph
|
||||
- `src/semantic/` — Semantic analysis: symbol kinds, relations, AST visitor, template resolver
|
||||
- `src/syntax/` — Lexer, scanner, token types, dependency graph
|
||||
- `src/command/` — CLI parsing, compilation database, toolchain detection
|
||||
- `src/support/` — Utilities: logging, filesystem, JSON, string helpers
|
||||
|
||||
## Build System
|
||||
|
||||
- Uses **pixi** for environment management and **CMake + Ninja** for building.
|
||||
- Two build types: `Debug` and `RelWithDebInfo` (default).
|
||||
- Build output goes to `build/[type]/`.
|
||||
- See `/build`, `/test`, `/format` commands for common operations.
|
||||
|
||||
## Commit Message Format
|
||||
|
||||
Use **conventional commits** — enforced by CI:
|
||||
|
||||
```
|
||||
<type>(<scope>): <short description>
|
||||
```
|
||||
|
||||
- **Types**: `feat`, `fix`, `refactor`, `chore`, `docs`, `ci`, `test`
|
||||
- **Scopes**: match `src/` subdirectories or feature names, e.g. `completion`, `server`, `index`, `tests`, `document links`
|
||||
- Keep the subject line under 70 characters.
|
||||
|
||||
## Tests
|
||||
|
||||
Three types of tests, all must pass before committing:
|
||||
|
||||
- **Unit tests** (`tests/unit/`): C++ tests using the project's own test framework. Test names should be at most 4 words.
|
||||
- **Integration tests** (`tests/integration/`): Python pytest tests that start a real clice server and communicate via LSP.
|
||||
- **Smoke tests** (`tests/smoke/`): Replay recorded LSP sessions via `tests/replay.py`.
|
||||
|
||||
### Integration Test Style
|
||||
|
||||
- Keep tests concise. Do NOT write large comment blocks explaining the test layout or expected behavior.
|
||||
- Use descriptive test function names and short inline comments only where logic is non-obvious.
|
||||
|
||||
## Pre-PR Review
|
||||
|
||||
Before opening a PR, launch **3 parallel subagents** to review the diff independently:
|
||||
|
||||
1. **Correctness reviewer**: Check for logic errors, edge cases, undefined behavior, and off-by-one mistakes.
|
||||
2. **Style reviewer**: Verify the code follows this project's naming conventions, coding style, and CLAUDE.md rules.
|
||||
3. **Test reviewer**: Confirm test coverage is adequate — new functionality has tests, edge cases are covered, and no existing tests were broken or weakened.
|
||||
|
||||
Each agent should read the full diff (`git diff main...HEAD`) and report issues. Fix all reported issues before opening the PR.
|
||||
|
||||
## Pre-commit Checklist
|
||||
|
||||
Before committing code, you MUST:
|
||||
|
||||
1. **Run `pixi run format`** to format all source files.
|
||||
2. **Pass all three types of tests:**
|
||||
- Unit tests: `pixi run unit-test [type]`
|
||||
- Integration tests: `pixi run integration-test [type]`
|
||||
- Smoke tests: `pixi run smoke-test [type]`
|
||||
3. **All test failures must be fixed before committing.** This is a HARD REQUIREMENT with NO exceptions:
|
||||
- If a test fails, it MUST be fixed before you commit. Do NOT commit with known failures.
|
||||
- Do NOT skip, disable, or mark tests as expected-failure to work around breakage.
|
||||
- Do NOT argue "this test was already broken before my changes" — if it fails on your branch, it is YOUR responsibility to fix it before committing. The main branch CI is green; any failure on your branch is caused by your changes, period.
|
||||
- Do NOT defer fixing to a follow-up PR. Fix it NOW, in this branch, before committing.
|
||||
|
||||
---
|
||||
|
||||
## C++ Coding Style
|
||||
|
||||
### Template & Type Traits
|
||||
|
||||
- Do NOT blindly add `std::remove_cvref_t` on every template parameter. Understand C++ template argument deduction rules:
|
||||
- `template<typename T> void f(T x)` — `T` is always deduced as a non-reference, non-cv-qualified type. No need for `remove_cvref_t`.
|
||||
- `template<typename T> void f(T& x)` — `T` is deduced as the referred-to type (possibly cv-qualified, but never a reference). No need for `remove_cvref_t` to strip references.
|
||||
- `template<typename T> void f(const T& x)` — `T` is deduced as a non-const, non-reference type. No need for `remove_cvref_t`.
|
||||
- `template<typename T> void f(T&& x)` — **forwarding reference**: `T` CAN be deduced as an lvalue reference (e.g., `int&`). This is the ONLY case where `std::remove_cvref_t<T>` is needed to get the bare type.
|
||||
- Class template parameters and return types are also never deduced as references; don't add `remove_cvref_t` on them either.
|
||||
|
||||
### Type Traits & Concepts (C++20/23)
|
||||
|
||||
- This project targets C++20/23. Use variable templates directly for type traits — do NOT use the old pattern of wrapping a class template static member in a variable template. Prefer:
|
||||
|
||||
```cpp
|
||||
// Good: directly specialize a variable template
|
||||
template<typename T>
|
||||
inline constexpr bool is_my_type_v = false;
|
||||
|
||||
template<>
|
||||
inline constexpr bool is_my_type_v<MyType> = true;
|
||||
```
|
||||
|
||||
```cpp
|
||||
// Bad: unnecessary class template wrapper
|
||||
template<typename T>
|
||||
struct is_my_type : std::false_type {};
|
||||
|
||||
template<>
|
||||
struct is_my_type<MyType> : std::true_type {};
|
||||
|
||||
template<typename T>
|
||||
inline constexpr bool is_my_type_v = is_my_type<T>::value;
|
||||
```
|
||||
|
||||
- When defining a concept that checks a type trait, do NOT add `std::remove_cvref_t` unless you specifically intend the concept to see through references/cv-qualifiers. If the concept is meant for a bare type, just use `T` directly — the caller is responsible for passing the right type.
|
||||
|
||||
```cpp
|
||||
// Good
|
||||
template<typename T>
|
||||
concept MyTrait = is_my_type_v<T>;
|
||||
|
||||
// Bad: unnecessary remove_cvref_t
|
||||
template<typename T>
|
||||
concept MyTrait = is_my_type_v<std::remove_cvref_t<T>>;
|
||||
```
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
- **Variables, member fields, function names**: `snake_case`. Class member fields do NOT use any special suffix/prefix (no trailing `_`, no `m_` prefix).
|
||||
- **Class names, template parameter names, enum names**: `PascalCase`. Exception: some class names also use `snake_case` — follow the existing style in the project.
|
||||
- **Enum values**: `PascalCase`.
|
||||
|
||||
### String Literals
|
||||
|
||||
- Prefer C++11 raw string literals `R"(...)"` over escaped strings. Avoid `\"`, `\\`, `\n` in string literals when a raw literal is cleaner.
|
||||
|
||||
### Error Handling
|
||||
|
||||
- **Prefer `if` with init-statements to tightly scope error variables**, but avoid them when they compromise code readability or flatten control flow.
|
||||
- **Omit redundant conditions:** If the error type provides an `operator bool` or evaluates implicitly (e.g., standard error codes, custom error wrappers), omit the redundant condition check.
|
||||
- **Avoid forced `else` branches:** If scoping the variable inside the `if` requires you to introduce an `else` block for the success path (especially when returning early on error), declare the variable in the local scope instead to keep the control flow flat.
|
||||
|
||||
```cpp
|
||||
// Good: Omit redundant condition when the type has operator bool
|
||||
if (auto err = foo()) {
|
||||
/* handle error */
|
||||
}
|
||||
|
||||
// Bad: Redundant condition check
|
||||
if (auto err = foo(); err) {
|
||||
/* handle error */
|
||||
}
|
||||
|
||||
// Good: Use init-statement when a custom condition is required,
|
||||
// AND the variable isn't needed outside the if-statement
|
||||
if (auto result = foo(); !result.has_value()) {
|
||||
/* handle error */
|
||||
}
|
||||
|
||||
// --- Scope and Control Flow Considerations ---
|
||||
|
||||
// Bad: Using init-statement forces an 'else' block because 'result'
|
||||
// goes out of scope, leading to nested/redundant code.
|
||||
if (auto result = get_data(); !result.has_value()) {
|
||||
return result.error();
|
||||
} else {
|
||||
process(result.value()); // Success path is forced into a nested block
|
||||
}
|
||||
|
||||
// Good: Declare as a regular local variable to allow early exit
|
||||
// and keep the success path un-nested (flat control flow).
|
||||
auto result = get_data();
|
||||
if (!result.has_value()) {
|
||||
return result.error();
|
||||
}
|
||||
process(result.value());
|
||||
```
|
||||
|
||||
### Style
|
||||
|
||||
- Prefer `[[maybe_unused]]` over `(void)` for intentionally unused variables or parameters.
|
||||
|
||||
### Modern C++ Usage
|
||||
|
||||
- Use C++20/23 APIs whenever possible. Do NOT use `<iostream>` facilities (`std::cout`, `std::cin`, `std::cerr`, etc.). Also do NOT use C-style I/O (`printf`, `fprintf`, etc.).
|
||||
- Prefer `std::ranges` / `std::views` APIs over raw loops and traditional `<algorithm>` calls.
|
||||
- If the project depends on LLVM, prefer LLVM's efficient data structures (e.g., `llvm::SmallVector`, `llvm::DenseMap`, `llvm::StringMap`, `llvm::StringRef`) over their `std` counterparts when appropriate.
|
||||
|
||||
### Parameter Passing Preferences
|
||||
|
||||
- For string parameters, prefer `llvm::StringRef` > `std::string_view` > `const std::string&`.
|
||||
- For array/span parameters, prefer `llvm::ArrayRef` > `std::span` > `const std::vector&`.
|
||||
15
.claude/commands/build.md
Normal file
15
.claude/commands/build.md
Normal file
@@ -0,0 +1,15 @@
|
||||
Build the project. Accepts an optional argument for build type: `Debug` or `RelWithDebInfo` (default).
|
||||
|
||||
Available build commands:
|
||||
|
||||
- CMake configure only: `pixi run cmake-config [type]`
|
||||
- CMake build only (skip configure): `pixi run cmake-build [type]`
|
||||
- Full build (configure + build): `pixi run build [type]`
|
||||
- Build a specific target: `pixi run cmake-build [type]` then `cmake --build build/[type] --target [target]`
|
||||
|
||||
Common targets: `clice`, `unit_tests`
|
||||
|
||||
Example usage:
|
||||
|
||||
- `/build` — full build RelWithDebInfo
|
||||
- `/build Debug` — full build Debug
|
||||
5
.claude/commands/format.md
Normal file
5
.claude/commands/format.md
Normal file
@@ -0,0 +1,5 @@
|
||||
Format all project source files.
|
||||
|
||||
Run: `pixi run format`
|
||||
|
||||
Formats C++, Python, Lua, JS/TS, Markdown, JSON, TOML, and YAML files.
|
||||
19
.claude/commands/test.md
Normal file
19
.claude/commands/test.md
Normal file
@@ -0,0 +1,19 @@
|
||||
Run tests. Accepts an optional argument for build type: `Debug` or `RelWithDebInfo` (default).
|
||||
|
||||
Available test commands:
|
||||
|
||||
- Unit tests: `pixi run unit-test [type]`
|
||||
- Integration tests: `pixi run integration-test [type]`
|
||||
- Smoke tests: `pixi run smoke-test [type]`
|
||||
- All tests (unit + integration): `pixi run test [type]`
|
||||
|
||||
Filtering specific tests:
|
||||
|
||||
- Unit tests: `pixi run unit-test [type] --test-filter=SuiteName.CaseName`
|
||||
- Integration tests: `pixi run pytest tests/integration -k "test_name" --executable=./build/[type]/bin/clice`
|
||||
- Smoke tests: `pixi run python tests/replay.py tests/smoke/specific.jsonl --clice=./build/[type]/bin/clice`
|
||||
|
||||
Example usage:
|
||||
|
||||
- `/test` — run all tests (RelWithDebInfo)
|
||||
- `/test Debug` — run all tests (Debug)
|
||||
@@ -1,2 +1,7 @@
|
||||
chat:
|
||||
auto_reply: false
|
||||
reviews:
|
||||
auto_review:
|
||||
enabled: true
|
||||
summary:
|
||||
enabled: false
|
||||
|
||||
3
.github/workflows/benchmark.yml
vendored
3
.github/workflows/benchmark.yml
vendored
@@ -1,8 +1,7 @@
|
||||
name: benchmark
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
benchmark:
|
||||
|
||||
44
.github/workflows/main.yml
vendored
44
.github/workflows/main.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
clice: ${{ steps.filter.outputs.clice }}
|
||||
vscode: ${{ steps.filter.outputs.vscode }}
|
||||
cmake: ${{ steps.filter.outputs.cmake }}
|
||||
xmake: ${{ steps.filter.outputs.xmake }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dorny/paths-filter@v3
|
||||
@@ -46,13 +46,31 @@ jobs:
|
||||
- 'tests/**'
|
||||
- 'config/**'
|
||||
- '.github/workflows/test-cmake.yml'
|
||||
xmake:
|
||||
- 'xmake.lua'
|
||||
- 'src/**'
|
||||
- 'include/**'
|
||||
- 'tests/**'
|
||||
- 'config/**'
|
||||
- '.github/workflows/test-xmake.yml'
|
||||
|
||||
conventional-commit:
|
||||
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check conventional commit format
|
||||
env:
|
||||
IS_PR: ${{ github.event_name == 'pull_request' }}
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
COMMIT_MSG: ${{ github.event.head_commit.message }}
|
||||
run: |
|
||||
pattern='^(feat|fix|refactor|chore|build|ci|docs|test|perf|style|revert)(\(.+\))?: .+'
|
||||
if [[ "$IS_PR" == "true" ]]; then
|
||||
subject="$PR_TITLE"
|
||||
label="PR title"
|
||||
else
|
||||
subject=$(echo "$COMMIT_MSG" | head -n1)
|
||||
label="Commit message"
|
||||
fi
|
||||
if [[ ! "$subject" =~ $pattern ]]; then
|
||||
echo "::error::$label must follow conventional commit format: type(scope)?: description"
|
||||
echo " Valid types: feat, fix, refactor, chore, build, ci, docs, test, perf, style, revert"
|
||||
echo " Got: '$subject'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
format:
|
||||
needs: changes
|
||||
@@ -82,11 +100,6 @@ jobs:
|
||||
if: ${{ needs.changes.outputs.cmake == 'true' }}
|
||||
uses: ./.github/workflows/test-cmake.yml
|
||||
|
||||
# xmake:
|
||||
# needs: changes
|
||||
# if: ${{ needs.changes.outputs.xmake == 'true' }}
|
||||
# uses: ./.github/workflows/test-xmake.yml
|
||||
|
||||
release-clice:
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -104,16 +117,17 @@ jobs:
|
||||
checks-passed:
|
||||
if: ${{ always() && !startsWith(github.ref, 'refs/tags/') }}
|
||||
needs:
|
||||
- conventional-commit
|
||||
- format
|
||||
- deploy
|
||||
# - clice
|
||||
- vscode
|
||||
- cmake
|
||||
# - xmake
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check results
|
||||
uses: re-actors/alls-green@release/v1
|
||||
with:
|
||||
allowed-skips: format,deploy,clice,vscode,cmake,xmake
|
||||
allowed-skips: conventional-commit,format,deploy,clice,vscode,cmake
|
||||
jobs: ${{ toJSON(needs) }}
|
||||
|
||||
26
.github/workflows/publish-clice.yml
vendored
26
.github/workflows/publish-clice.yml
vendored
@@ -29,32 +29,18 @@ jobs:
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup xmake
|
||||
uses: xmake-io/github-action-setup-xmake@v1
|
||||
with:
|
||||
xmake-version: 3.0.5
|
||||
actions-cache-folder: ".xmake-cache"
|
||||
actions-cache-key: ${{ matrix.os }}
|
||||
package-cache: true
|
||||
package-cache-key: ${{ matrix.os }}-pkg-release-v1
|
||||
build-cache: true
|
||||
build-cache-key: ${{ matrix.os }}-build-release-v1
|
||||
|
||||
- uses: ./.github/actions/setup-pixi
|
||||
with:
|
||||
environments: package
|
||||
|
||||
- name: Remove ci llvm toolchain on Windows
|
||||
if: runner.os == 'Windows'
|
||||
run: |
|
||||
# @see https://github.com/xmake-io/xmake/issues/7158
|
||||
xmake lua os.rmdir "C:/Program Files/Microsoft Visual Studio/2022/Enterprise/VC/Tools/Llvm"
|
||||
xmake lua os.rmdir "C:/Program Files/LLVM"
|
||||
|
||||
- name: Package
|
||||
run: pixi run package
|
||||
|
||||
@@ -63,7 +49,7 @@ jobs:
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: build/xpack/clice/${{ matrix.artifact_name }}
|
||||
file: build/RelWithDebInfo/${{ matrix.artifact_name }}
|
||||
asset_name: ${{ matrix.asset_name }}
|
||||
tag: ${{ github.ref }}
|
||||
overwrite: true
|
||||
@@ -73,7 +59,7 @@ jobs:
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: build/xpack/clice/${{ matrix.symbol_artifact_name }}
|
||||
file: build/RelWithDebInfo/${{ matrix.symbol_artifact_name }}
|
||||
asset_name: ${{ matrix.symbol_asset_name }}
|
||||
tag: ${{ github.ref }}
|
||||
overwrite: true
|
||||
|
||||
42
.github/workflows/test-xmake.yml
vendored
42
.github/workflows/test-xmake.yml
vendored
@@ -1,42 +0,0 @@
|
||||
name: xmake
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [windows-2025, ubuntu-24.04, macos-15]
|
||||
build_type: [debug, releasedbg]
|
||||
exclude:
|
||||
- os: windows-2025
|
||||
build_type: debug
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup xmake
|
||||
uses: xmake-io/github-action-setup-xmake@v1
|
||||
with:
|
||||
xmake-version: 3.0.5
|
||||
actions-cache-folder: ".xmake-cache"
|
||||
actions-cache-key: ${{ matrix.os }}
|
||||
package-cache: true
|
||||
package-cache-key: ${{ matrix.os }}-pixi
|
||||
build-cache: true
|
||||
build-cache-key: ${{ matrix.os }}-${{ matrix.build_type }}
|
||||
|
||||
- uses: ./.github/actions/setup-pixi
|
||||
|
||||
- name: Build
|
||||
run: pixi run xmake ${{ matrix.build_type }}
|
||||
|
||||
- name: Test
|
||||
run: pixi run xmake-test
|
||||
|
||||
- name: Remove llvm package (Linux)
|
||||
if: runner.os == 'Linux'
|
||||
run: xmake require --uninstall clice-llvm
|
||||
13
.gitignore
vendored
13
.gitignore
vendored
@@ -35,7 +35,7 @@
|
||||
*build*/
|
||||
temp/
|
||||
.cache/
|
||||
.xmake/
|
||||
|
||||
.llvm*/
|
||||
.clice/
|
||||
compile_commands.json
|
||||
@@ -56,10 +56,11 @@ __pycache__/
|
||||
tests/unit/Local/
|
||||
|
||||
# IDEs & Editors
|
||||
/.vscode/
|
||||
/.vscode/*
|
||||
!/.vscode/launch.json
|
||||
!/.vscode/tasks.json
|
||||
.vs/
|
||||
.idea/
|
||||
.claude
|
||||
.clangd
|
||||
|
||||
# pixi environments
|
||||
@@ -68,5 +69,7 @@ tests/unit/Local/
|
||||
!.pixi/config.toml
|
||||
|
||||
.codex/
|
||||
.claude/
|
||||
openspec/
|
||||
.claude/*
|
||||
!.claude/CLAUDE.md
|
||||
!.claude/commands/
|
||||
openspec/
|
||||
|
||||
83
.vscode/launch.json
vendored
Normal file
83
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug clice",
|
||||
"program": "${workspaceFolder}/build/Debug/bin/clice",
|
||||
"args": ["--mode=socket", "--port=50051"],
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug clice (socket, RelWithDebInfo)",
|
||||
"program": "${workspaceFolder}/build/RelWithDebInfo/bin/clice",
|
||||
"args": ["--mode=socket", "--port=50051"],
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"name": "VSCode Extension (pipe)",
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"args": [
|
||||
"--disable-extension=llvm-vs-code-extensions.vscode-clangd",
|
||||
"--disable-extension=ms-vscode.cpptools",
|
||||
"--disable-extension=ms-vscode.cpptools-extension-pack",
|
||||
"--extensionDevelopmentPath=${workspaceFolder}/editors/vscode"
|
||||
],
|
||||
"env": {
|
||||
"CLICE_MODE": "pipe"
|
||||
},
|
||||
"outFiles": ["${workspaceFolder}/editors/vscode/dist/**/*.js"],
|
||||
"preLaunchTask": "npm: watch vscode ext"
|
||||
},
|
||||
{
|
||||
"name": "VSCode Extension (socket)",
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"args": [
|
||||
"--disable-extension=llvm-vs-code-extensions.vscode-clangd",
|
||||
"--disable-extension=ms-vscode.cpptools",
|
||||
"--disable-extension=ms-vscode.cpptools-extension-pack",
|
||||
"--extensionDevelopmentPath=${workspaceFolder}/editors/vscode"
|
||||
],
|
||||
"env": {
|
||||
"CLICE_MODE": "socket"
|
||||
},
|
||||
"outFiles": ["${workspaceFolder}/editors/vscode/dist/**/*.js"],
|
||||
"preLaunchTask": "npm: watch vscode ext"
|
||||
},
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Unit Test",
|
||||
"program": "${workspaceFolder}/build/Debug/bin/unit_tests",
|
||||
"args": ["--test-dir=./tests/data", "--test-filter=${input:filter}"],
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Release Unit Test",
|
||||
"program": "${workspaceFolder}/build/RelWithDebInfo/bin/unit_tests",
|
||||
"args": ["--test-dir=./tests/data", "--test-filter=${input:filter}"],
|
||||
"cwd": "${workspaceFolder}"
|
||||
}
|
||||
],
|
||||
"compounds": [
|
||||
{
|
||||
"name": "clice + VSCode Extension (socket)",
|
||||
"configurations": ["Debug clice", "VSCode Extension (socket)"],
|
||||
"stopAll": true
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"id": "filter",
|
||||
"type": "promptString",
|
||||
"description": "Unit Test Filter"
|
||||
}
|
||||
]
|
||||
}
|
||||
42
.vscode/tasks.json
vendored
Normal file
42
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "npm: install vscode deps",
|
||||
"type": "shell",
|
||||
"command": "pnpm",
|
||||
"args": ["install"],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/editors/vscode"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "npm: watch vscode ext",
|
||||
"type": "shell",
|
||||
"command": "pnpm",
|
||||
"args": ["run", "watch"],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/editors/vscode"
|
||||
},
|
||||
"dependsOn": "npm: install vscode deps",
|
||||
"problemMatcher": "$ts-webpack-watch",
|
||||
"isBackground": true,
|
||||
"presentation": {
|
||||
"reveal": "never",
|
||||
"group": "watchers"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "npm: package vscode ext",
|
||||
"type": "shell",
|
||||
"command": "pnpm",
|
||||
"args": ["run", "package"],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/editors/vscode"
|
||||
},
|
||||
"dependsOn": "npm: install vscode deps",
|
||||
"problemMatcher": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -101,8 +101,15 @@ function(setup_llvm LLVM_VERSION)
|
||||
clangToolingSyntax
|
||||
)
|
||||
else()
|
||||
file(GLOB LLVM_LIBRARIES CONFIGURE_DEPENDS "${LLVM_INSTALL_PATH}/lib/*${CMAKE_STATIC_LIBRARY_SUFFIX}")
|
||||
target_link_libraries(llvm-libs INTERFACE ${LLVM_LIBRARIES})
|
||||
file(GLOB LLVM_LIBRARIES CONFIGURE_DEPENDS "${LLVM_INSTALL_PATH}/lib/${CMAKE_STATIC_LIBRARY_PREFIX}LLVM[a-zA-Z]*${CMAKE_STATIC_LIBRARY_SUFFIX}")
|
||||
file(GLOB CLANG_LIBRARIES CONFIGURE_DEPENDS "${LLVM_INSTALL_PATH}/lib/${CMAKE_STATIC_LIBRARY_PREFIX}clang[a-zA-Z]*${CMAKE_STATIC_LIBRARY_SUFFIX}")
|
||||
# TODO: find a better way to find out whether zlib and zstd are needed
|
||||
# Currently link if present in the LLVM lib directory
|
||||
file(GLOB OTHER_REQUIRED_LIBS CONFIGURE_DEPENDS
|
||||
"${LLVM_INSTALL_PATH}/lib/${CMAKE_STATIC_LIBRARY_PREFIX}z${CMAKE_STATIC_LIBRARY_SUFFIX}"
|
||||
"${LLVM_INSTALL_PATH}/lib/${CMAKE_STATIC_LIBRARY_PREFIX}zstd${CMAKE_STATIC_LIBRARY_SUFFIX}"
|
||||
)
|
||||
target_link_libraries(llvm-libs INTERFACE ${LLVM_LIBRARIES} ${CLANG_LIBRARIES} ${OTHER_REQUIRED_LIBS})
|
||||
target_compile_definitions(llvm-libs INTERFACE CLANG_BUILD_STATIC=1)
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
@@ -20,7 +20,7 @@ max_active_file = 8
|
||||
cache_dir = "${workspace}/.clice/cache"
|
||||
# Directory for storing index files.
|
||||
index_dir = "${workspace}/.clice/index"
|
||||
logging_dir = "${workspace}/.clice/logging"
|
||||
logging_dir = "${workspace}/.clice/logs"
|
||||
# Compile commands files or directories to search for compile_commands.json files.
|
||||
compile_commands_paths = ["${workspace}/build"]
|
||||
|
||||
|
||||
@@ -32,18 +32,6 @@ pixi run integration-test Debug
|
||||
> [!TIP]
|
||||
> If you want to develop directly with `cmake`, `ninja`, `clang++`, etc., run `pixi shell` to enter a shell with all env vars configured.
|
||||
|
||||
### XMake
|
||||
|
||||
We also support building with XMake:
|
||||
|
||||
```shell
|
||||
# config & build (default releasedbg)
|
||||
pixi run xmake
|
||||
|
||||
# unit & integration
|
||||
pixi run xmake-test
|
||||
```
|
||||
|
||||
## Manual Build
|
||||
|
||||
If you plan to build manually, first ensure your toolchain matches the versions defined in `pixi.toml`.
|
||||
@@ -70,30 +58,13 @@ Optional build options:
|
||||
| CLICE_USE_LIBCXX | OFF | Build clice with libc++ (adds `-std=libc++`); if enabled, ensure the LLVM libs are also built with libc++ |
|
||||
| CLICE_CI_ENVIRONMENT | OFF | Enable the `CLICE_CI_ENVIRONMENT` macro; some tests only run in CI |
|
||||
|
||||
### XMake
|
||||
|
||||
Build clice with:
|
||||
|
||||
```bash
|
||||
xmake f -c --mode=releasedbg --toolchain=clang
|
||||
xmake build --all
|
||||
```
|
||||
|
||||
Optional build options:
|
||||
|
||||
| Option | Default | Effect |
|
||||
| ------------- | ------- | ---------------------------------------- |
|
||||
| --llvm | "" | Build clice with LLVM from a custom path |
|
||||
| --enable_test | false | Build clice unit tests |
|
||||
| --ci | false | Enable `CLICE_CI_ENVIRONMENT` |
|
||||
|
||||
## About LLVM
|
||||
|
||||
clice calls Clang APIs to parse C++ code, so it must link against LLVM/Clang. Because clice uses Clang's private headers (usually absent from distro packages), the system LLVM package cannot be used directly.
|
||||
|
||||
Two ways to satisfy this dependency:
|
||||
|
||||
1. We publish prebuilt binaries of the LLVM version we use at [clice-llvm](https://github.com/clice-io/clice-llvm/releases) for CI and release builds. During builds, cmake and xmake download these LLVM libs by default.
|
||||
1. We publish prebuilt binaries of the LLVM version we use at [clice-llvm](https://github.com/clice-io/clice-llvm/releases) for CI and release builds. During builds, cmake downloads these LLVM libs by default.
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
|
||||
@@ -18,13 +18,6 @@ We use pytest to run integration tests. Please refer to `pyproject.toml` to inst
|
||||
$ pytest -s --log-cli-level=INFO tests/integration --executable=./build/bin/clice
|
||||
```
|
||||
|
||||
If you use xmake as your build system, you can run the tests directly with xmake:
|
||||
|
||||
```shell
|
||||
$ xmake run --verbose unit_tests
|
||||
$ xmake test --verbose integration_tests/default
|
||||
```
|
||||
|
||||
## Debug
|
||||
|
||||
If you want to attach a debugger to clice for debugging, it is recommended to first start clice in socket mode independently, and then connect the client to it.
|
||||
|
||||
@@ -54,14 +54,73 @@ bazel run @hedron_compile_commands//:refresh_all
|
||||
|
||||
### Visual Studio
|
||||
|
||||
TODO:
|
||||
Visual Studio (2019 16.1+) can generate a compilation database via CMake integration. Open your project as a CMake project, then configure the generation in `CMakeSettings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"configurations": [
|
||||
{
|
||||
"name": "x64-Debug",
|
||||
"generator": "Ninja",
|
||||
"buildRoot": "${projectDir}\\build",
|
||||
"cmakeCommandArgs": "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Alternatively, for MSBuild-based projects (`.vcxproj`), you can use [compiledb-vs](https://github.com/pjbroad/compiledb-vs) or [catter](https://github.com/clice-io/catter) to generate the compilation database.
|
||||
|
||||
### Makefile
|
||||
|
||||
TODO:
|
||||
For Makefile-based projects, use [bear](https://github.com/rizsotto/Bear) to intercept compilation commands:
|
||||
|
||||
```bash
|
||||
bear -- make
|
||||
```
|
||||
|
||||
This will generate a `compile_commands.json` in the current directory. Note that `bear` requires a clean build to capture all commands — run `make clean` before `bear -- make` if needed.
|
||||
|
||||
Alternatively, if you use GNU Make, you can use [compiledb](https://github.com/nicktimko/compiledb):
|
||||
|
||||
```bash
|
||||
compiledb make
|
||||
```
|
||||
|
||||
### Meson
|
||||
|
||||
Meson generates a compilation database automatically during setup:
|
||||
|
||||
```bash
|
||||
meson setup build
|
||||
```
|
||||
|
||||
The `compile_commands.json` will be in the `build` directory.
|
||||
|
||||
### Xmake
|
||||
|
||||
Use one of the following approaches to generate a compilation database.
|
||||
|
||||
#### Command Line
|
||||
|
||||
Run the following command to manually generate a compilation database:
|
||||
|
||||
```bash
|
||||
xmake project -k compile_commands --lsp=clangd build
|
||||
```
|
||||
|
||||
> Compilation database generated manually doesn't automatically update itself. Re-generate if changes are made to the project.
|
||||
|
||||
#### VSCode Extension
|
||||
|
||||
The Xmake official VSCode extension automatically generates the compilation database when `xmake.lua` is updated. However, it generates the database to the `.vscode` directory by default. Add this setting in `settings.json`:
|
||||
|
||||
```json
|
||||
"xmake.compileCommandsDirectory": "build"
|
||||
```
|
||||
|
||||
to explicitly ask the extension to generate the compilation database in `build`.
|
||||
|
||||
### Others
|
||||
|
||||
For any other build system, you can try using [bear](https://github.com/rizsotto/Bear) or [scan-build](https://github.com/rizsotto/scan-build) to intercept compilation commands and obtain the compilation database (no guarantee of success). We plan to write a **new tool** in the future that captures compilation commands through a fake compiler approach.
|
||||
For any other build system, you can use [catter](https://github.com/clice-io/catter) to generate a compilation database. It captures compilation commands through a fake compiler approach and is designed to work reliably with any build system that invokes a compiler executable.
|
||||
|
||||
@@ -32,18 +32,6 @@ pixi run integration-test Debug
|
||||
> [!TIP]
|
||||
> 如果你想直接使用 `cmake`, `ninja`, `clang++` 等命令进行开发,请运行 `pixi shell` 进入已配置好环境变量的终端
|
||||
|
||||
### XMake
|
||||
|
||||
我们同样支持使用 XMake 构建:
|
||||
|
||||
```shell
|
||||
# config & build (default releasedbg)
|
||||
pixi run xmake
|
||||
|
||||
# unit & integration
|
||||
pixi run xmake-test
|
||||
```
|
||||
|
||||
## Manual Build
|
||||
|
||||
如果你打算手动构建,请务必先确认你的工具链满足 pixi.toml 中定义的版本要求。
|
||||
@@ -70,30 +58,13 @@ cmake -B build -G Ninja \
|
||||
| CLICE_USE_LIBCXX | OFF | 是否使用 libc++ 来构建 clice(添加 `-std=libc++`),如果开启,请确保 LLVM 库也是使用 libc++ 编译的 |
|
||||
| CLICE_CI_ENVIRONMENT | OFF | 是否打开 `CLICE_CI_ENVIRONMENT` 这个宏,有些测试在 CI 环境才会执行 |
|
||||
|
||||
### XMake
|
||||
|
||||
使用如下命令即可构建 clice:
|
||||
|
||||
```bash
|
||||
xmake f -c --mode=releasedbg --toolchain=clang
|
||||
xmake build --all
|
||||
```
|
||||
|
||||
可选的构建选项:
|
||||
|
||||
| 选项 | 默认值 | 效果 |
|
||||
| ------------- | ------ | ------------------------------------ |
|
||||
| --llvm | "" | 使用自定义路径的 LLVM 库来构建 clice |
|
||||
| --enable_test | false | 是否构建 clice 的单元测试 |
|
||||
| --ci | false | 是否打开 `CLICE_CI_ENVIRONMENT` |
|
||||
|
||||
## About LLVM
|
||||
|
||||
clice 调用 Clang API 来解析 C++ 代码,因此必须链接 LLVM/Clang 库。由于 clice 使用了 Clang 的私有头文件(这些文件通常不包含在发行版中),不能直接使用系统安装的 LLVM 包。
|
||||
|
||||
主要有两种方式解决这个依赖问题:
|
||||
|
||||
1. 我们在 [clice-llvm](https://github.com/clice-io/clice-llvm/releases) 上会发布使用的 LLVM 版本的预编译二进制,用于 CI 或者 release 构建。在构建时 cmake 和 xmake 默认会从此处下载 LLVM 库然后使用。
|
||||
1. 我们在 [clice-llvm](https://github.com/clice-io/clice-llvm/releases) 上会发布使用的 LLVM 版本的预编译二进制,用于 CI 或者 release 构建。在构建时 cmake 默认会从此处下载 LLVM 库然后使用。
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
|
||||
@@ -30,7 +30,7 @@ pixi run publish-vscode
|
||||
|
||||
1. `pixi shell -e node`
|
||||
2. 在 `editors/vscode` 下运行 `pnpm run watch`(增量构建)
|
||||
3. VSCode 中使用 “Run Extension/Launch Extension” 调试配置,或执行 `code --extensionDevelopmentPath=$(pwd)/editors/vscode`
|
||||
3. VSCode 中使用”Run Extension/Launch Extension”调试配置,或执行 `code --extensionDevelopmentPath=$(pwd)/editors/vscode`
|
||||
|
||||
常用脚本(在 `pixi shell -e node` 下):
|
||||
|
||||
|
||||
@@ -18,13 +18,6 @@ $ ./build/bin/unit_tests --test-dir="./tests/data"
|
||||
$ pytest -s --log-cli-level=INFO tests/integration --executable=./build/bin/clice
|
||||
```
|
||||
|
||||
如果你使用 xmake 作为构建系统,可以直接通过 xmake 运行测试:
|
||||
|
||||
```shell
|
||||
$ xmake run --verbose unit_tests
|
||||
$ xmake test --verbose integration_tests/default
|
||||
```
|
||||
|
||||
## Debug
|
||||
|
||||
如果想在 clice 上附加调试器并进行调试,推荐先单独以 socket 模式启动 clice,然后再将客户端连接到 clice 上
|
||||
|
||||
@@ -54,14 +54,73 @@ bazel run @hedron_compile_commands//:refresh_all
|
||||
|
||||
### Visual Studio
|
||||
|
||||
TODO:
|
||||
Visual Studio(2019 16.1+)可以通过 CMake 集成来生成编译数据库。将项目作为 CMake 项目打开,然后在 `CMakeSettings.json` 中配置:
|
||||
|
||||
```json
|
||||
{
|
||||
"configurations": [
|
||||
{
|
||||
"name": "x64-Debug",
|
||||
"generator": "Ninja",
|
||||
"buildRoot": "${projectDir}\\build",
|
||||
"cmakeCommandArgs": "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
对于基于 MSBuild 的项目(`.vcxproj`),可以使用 [compiledb-vs](https://github.com/pjbroad/compiledb-vs) 或 [catter](https://github.com/clice-io/catter) 来生成编译数据库。
|
||||
|
||||
### Makefile
|
||||
|
||||
TODO:
|
||||
对于基于 Makefile 的项目,使用 [bear](https://github.com/rizsotto/Bear) 来拦截编译命令:
|
||||
|
||||
```bash
|
||||
bear -- make
|
||||
```
|
||||
|
||||
这会在当前目录生成 `compile_commands.json`。注意 `bear` 需要干净的构建来捕获所有命令——如果需要的话,在运行 `bear -- make` 之前先执行 `make clean`。
|
||||
|
||||
另外,如果使用 GNU Make,也可以使用 [compiledb](https://github.com/nicktimko/compiledb):
|
||||
|
||||
```bash
|
||||
compiledb make
|
||||
```
|
||||
|
||||
### Meson
|
||||
|
||||
Meson 在配置阶段会自动生成编译数据库:
|
||||
|
||||
```bash
|
||||
meson setup build
|
||||
```
|
||||
|
||||
`compile_commands.json` 会生成在 `build` 目录下。
|
||||
|
||||
### Xmake
|
||||
|
||||
用下列任意方法生成编译数据库。
|
||||
|
||||
#### 命令行手动生成
|
||||
|
||||
在命令行中执行以下命令:
|
||||
|
||||
```bash
|
||||
xmake project -k compile_commands --lsp=clangd build
|
||||
```
|
||||
|
||||
> 通过这种方法生成的编译数据库无法自动更新,需要在项目编译配置更改时手动重新生成。
|
||||
|
||||
#### VSCode 扩展
|
||||
|
||||
Xmake 提供的官方 VSCode 扩展会在 `xmake.lua` 更新时自动生成编译数据库。然而默认情况下,它将编译数据库生成到了 `.vscode` 文件夹。在 `settings.json` 中添加以下配置:
|
||||
|
||||
```json
|
||||
"xmake.compileCommandsDirectory": "build"
|
||||
```
|
||||
|
||||
以将编译数据库的生成目录调整到 `build`,供 clice 使用。
|
||||
|
||||
### Others
|
||||
|
||||
对于任意其它的构建系统,可以尝试使用 [bear](https://github.com/rizsotto/Bear) 或者 [scan-build](https://github.com/rizsotto/scan-build) 来拦截编译命令并获取到编译数据库(不保证成功)。我们计划在未来编写一个**新的工具**,通过假编译器的方式来实现编译命令的捕获。
|
||||
对于任意其它的构建系统,可以使用 [catter](https://github.com/clice-io/catter) 来生成编译数据库。它通过伪装编译器的方式来捕获编译命令,能够可靠地与任何调用编译器可执行文件的构建系统配合工作。
|
||||
|
||||
9
editors/vscode/.vscode/extensions.json
vendored
9
editors/vscode/.vscode/extensions.json
vendored
@@ -1,9 +0,0 @@
|
||||
{
|
||||
// See http://go.microsoft.com/fwlink/?LinkId=827846
|
||||
// for the documentation about the extensions.json format
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"amodio.tsl-problem-matcher",
|
||||
"ms-vscode.extension-test-runner"
|
||||
]
|
||||
}
|
||||
23
editors/vscode/.vscode/launch.json
vendored
23
editors/vscode/.vscode/launch.json
vendored
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Run Extension (socket)",
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"args": ["--disable-extensions", "--extensionDevelopmentPath=${workspaceFolder}"],
|
||||
"env": { "CLICE_MODE": "socket" },
|
||||
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
|
||||
"preLaunchTask": "${defaultBuildTask}"
|
||||
},
|
||||
{
|
||||
"name": "Run Extension (pipe)",
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"args": ["--disable-extensions", "--extensionDevelopmentPath=${workspaceFolder}"],
|
||||
"env": { "CLICE_MODE": "pipe" },
|
||||
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
|
||||
"preLaunchTask": "${defaultBuildTask}"
|
||||
}
|
||||
]
|
||||
}
|
||||
13
editors/vscode/.vscode/settings.json
vendored
13
editors/vscode/.vscode/settings.json
vendored
@@ -1,13 +0,0 @@
|
||||
// Place your settings in this file to overwrite default and user settings.
|
||||
{
|
||||
"files.exclude": {
|
||||
"out": false, // set this to true to hide the "out" folder with the compiled JS files
|
||||
"dist": false // set this to true to hide the "dist" folder with the compiled JS files
|
||||
},
|
||||
"search.exclude": {
|
||||
"out": true, // set this to false to include "out" folder in search results
|
||||
"dist": true // set this to false to include "dist" folder in search results
|
||||
},
|
||||
// Turn off tsc task auto detection since we have the necessary tasks as npm scripts
|
||||
"typescript.tsc.autoDetect": "off"
|
||||
}
|
||||
37
editors/vscode/.vscode/tasks.json
vendored
37
editors/vscode/.vscode/tasks.json
vendored
@@ -1,37 +0,0 @@
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
// for the documentation about the tasks.json format
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "watch",
|
||||
"problemMatcher": "$ts-webpack-watch",
|
||||
"isBackground": true,
|
||||
"presentation": {
|
||||
"reveal": "never",
|
||||
"group": "watchers"
|
||||
},
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "watch-tests",
|
||||
"problemMatcher": "$tsc-watch",
|
||||
"isBackground": true,
|
||||
"presentation": {
|
||||
"reveal": "never",
|
||||
"group": "watchers"
|
||||
},
|
||||
"group": "build"
|
||||
},
|
||||
{
|
||||
"label": "tasks: watch-tests",
|
||||
"dependsOn": ["npm: watch", "npm: watch-tests"],
|
||||
"problemMatcher": []
|
||||
}
|
||||
]
|
||||
}
|
||||
239
pixi.lock
generated
239
pixi.lock
generated
@@ -18,6 +18,9 @@ environments:
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/ccache-4.13.2-hedf47ba_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/clang-20-20.1.8-default_h99862b1_5.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/clang-20.1.8-default_h36abe19_5.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/clang-format-20-20.1.8-default_h99862b1_14.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/clang-format-20.1.8-default_h99862b1_14.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/clang-tools-20.1.8-default_h57a47db_14.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/clangxx-20.1.8-default_h363a0c9_5.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/cmake-4.2.1-hc85cc9f_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/compiler-rt-20.1.8-hb700be7_1.conda
|
||||
@@ -32,6 +35,7 @@ environments:
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45-default_hbd61a6d_105.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/libclang-cpp20.1-20.1.8-default_h99862b1_5.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/libclang13-22.1.0-default_h746c552_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.17.0-h4e3cde8_1.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda
|
||||
@@ -44,6 +48,7 @@ environments:
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/libhiredis-1.3.0-h5888daf_1.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h3b78370_2.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/libllvm20-20.1.8-hf7376ad_1.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/libllvm22-22.1.1-hf7376ad_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.67.0-had1ee68_0.conda
|
||||
@@ -81,7 +86,7 @@ environments:
|
||||
- pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/fd/1a/208293b6c350f5abea6941d5606080d4a492644052504f5312e5de30a902/pygls-2.1.1-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl
|
||||
osx-arm64:
|
||||
@@ -91,6 +96,9 @@ environments:
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ccache-4.13.2-h414bf82_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-20-20.1.8-default_h73dfc95_5.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-20.1.8-default_hf9bcbb7_5.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-format-20-20.1.8-default_hf3020a7_14.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-format-20.1.8-default_hf3020a7_14.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-tools-20.1.8-default_h1589341_14.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/clangxx-20.1.8-default_h36137df_5.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/cmake-4.2.1-h54ad630_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/compiler-rt-20.1.8-h855ad52_1.conda
|
||||
@@ -98,6 +106,7 @@ environments:
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-78.1-h38cb7af_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.21.3-h237132a_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libclang-cpp20.1-20.1.8-default_h73dfc95_5.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libclang13-21.1.8-default_h13b06bd_3.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcurl-8.17.0-hdece5d2_1.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-21.1.8-hf598326_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-devel-20.1.8-h6dc3340_3.conda
|
||||
@@ -109,6 +118,7 @@ environments:
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libhiredis-1.3.0-h286801f_1.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libiconv-1.18-h23cfdf5_2.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libllvm20-20.1.8-h8e0c9ce_1.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libllvm21-21.1.8-h8e0c9ce_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.1-h39f12f2_2.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h5505292_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.67.0-hc438710_0.conda
|
||||
@@ -140,7 +150,7 @@ environments:
|
||||
- pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/fd/1a/208293b6c350f5abea6941d5606080d4a492644052504f5312e5de30a902/pygls-2.1.1-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl
|
||||
win-64:
|
||||
@@ -148,12 +158,15 @@ environments:
|
||||
- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.11.12-h4c7d964_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/clang-20-20.1.8-default_hac490eb_5.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/clang-20.1.8-default_hac490eb_5.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/clang-format-20.1.8-default_hac490eb_14.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/clang-tools-20.1.8-default_hac490eb_14.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/clangxx-20.1.8-default_hac490eb_5.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/cmake-4.2.1-hdcbee5b_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/compiler-rt-20.1.8-h49e36cd_1.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/noarch/compiler-rt_win-64-20.1.8-h49e36cd_1.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/icu-78.1-h637d24d_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.21.3-hdf4eb48_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/libclang13-22.1.0-default_ha2db4b5_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/libcurl-8.17.0-h43ecb02_1.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.7.3-hac47afa_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.5.2-h52bdfb6_0.conda
|
||||
@@ -190,7 +203,7 @@ environments:
|
||||
- pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/fd/1a/208293b6c350f5abea6941d5606080d4a492644052504f5312e5de30a902/pygls-2.1.1-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl
|
||||
format:
|
||||
@@ -370,6 +383,9 @@ environments:
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/ccache-4.13.2-hedf47ba_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/clang-20-20.1.8-default_h99862b1_5.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/clang-20.1.8-default_h36abe19_5.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/clang-format-20-20.1.8-default_h99862b1_14.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/clang-format-20.1.8-default_h99862b1_14.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/clang-tools-20.1.8-default_h57a47db_14.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/clangxx-20.1.8-default_h363a0c9_5.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/cmake-4.2.1-hc85cc9f_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/compiler-rt-20.1.8-hb700be7_1.conda
|
||||
@@ -383,6 +399,7 @@ environments:
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45-default_hbd61a6d_104.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/libclang-cpp20.1-20.1.8-default_h99862b1_5.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/libclang13-22.1.0-default_h746c552_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.17.0-h4e3cde8_1.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda
|
||||
@@ -395,6 +412,7 @@ environments:
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/libhiredis-1.3.0-h5888daf_1.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h3b78370_2.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/libllvm20-20.1.8-hf7376ad_1.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/libllvm22-22.1.1-hf7376ad_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-devel-5.8.1-hb9d3cd8_2.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda
|
||||
@@ -436,7 +454,7 @@ environments:
|
||||
- pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/fd/1a/208293b6c350f5abea6941d5606080d4a492644052504f5312e5de30a902/pygls-2.1.1-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl
|
||||
osx-arm64:
|
||||
@@ -446,12 +464,16 @@ environments:
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ccache-4.13.2-h414bf82_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-20-20.1.8-default_h73dfc95_5.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-20.1.8-default_hf9bcbb7_5.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-format-20-20.1.8-default_hf3020a7_14.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-format-20.1.8-default_hf3020a7_14.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-tools-20.1.8-default_h1589341_14.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/clangxx-20.1.8-default_h36137df_5.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/cmake-4.2.1-h54ad630_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/compiler-rt-20.1.8-h855ad52_1.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/noarch/compiler-rt_osx-arm64-20.1.8-he32a8d3_1.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.21.3-h237132a_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libclang-cpp20.1-20.1.8-default_h73dfc95_5.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libclang13-21.1.7-default_h13b06bd_4.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcurl-8.17.0-hdece5d2_1.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-21.1.7-hf598326_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-devel-20.1.8-h6dc3340_3.conda
|
||||
@@ -463,6 +485,7 @@ environments:
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libhiredis-1.3.0-h286801f_1.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libiconv-1.18-h23cfdf5_2.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libllvm20-20.1.8-h8e0c9ce_1.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libllvm21-21.1.8-h8e0c9ce_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.1-h39f12f2_2.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-devel-5.8.1-h39f12f2_2.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h5505292_0.conda
|
||||
@@ -498,7 +521,7 @@ environments:
|
||||
- pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/fd/1a/208293b6c350f5abea6941d5606080d4a492644052504f5312e5de30a902/pygls-2.1.1-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl
|
||||
win-64:
|
||||
@@ -506,11 +529,14 @@ environments:
|
||||
- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.11.12-h4c7d964_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/clang-20-20.1.8-default_hac490eb_5.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/clang-20.1.8-default_hac490eb_5.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/clang-format-20.1.8-default_hac490eb_14.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/clang-tools-20.1.8-default_hac490eb_14.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/clangxx-20.1.8-default_hac490eb_5.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/cmake-4.2.1-hdcbee5b_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/compiler-rt-20.1.8-h49e36cd_1.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/noarch/compiler-rt_win-64-20.1.8-h49e36cd_1.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.21.3-hdf4eb48_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/libclang13-22.1.0-default_ha2db4b5_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/libcurl-8.17.0-h43ecb02_1.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.7.3-hac47afa_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.5.2-h52bdfb6_0.conda
|
||||
@@ -550,7 +576,7 @@ environments:
|
||||
- pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/fd/1a/208293b6c350f5abea6941d5606080d4a492644052504f5312e5de30a902/pygls-2.1.1-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl
|
||||
packages:
|
||||
@@ -803,6 +829,21 @@ packages:
|
||||
purls: []
|
||||
size: 73427981
|
||||
timestamp: 1764398987103
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/clang-format-20.1.8-default_h99862b1_14.conda
|
||||
sha256: b58148bb200231f21a0a78e90456414be7de8dd4fc115ebdf6e9d808d151e9e6
|
||||
md5: de9baf19d75c9ca7b4e88f5609db0347
|
||||
depends:
|
||||
- __glibc >=2.17,<3.0.a0
|
||||
- clang-format-20 20.1.8 default_h99862b1_14
|
||||
- libclang-cpp20.1 >=20.1.8,<20.2.0a0
|
||||
- libgcc >=14
|
||||
- libllvm20 >=20.1.8,<20.2.0a0
|
||||
- libstdcxx >=14
|
||||
license: Apache-2.0 WITH LLVM-exception
|
||||
license_family: Apache
|
||||
purls: []
|
||||
size: 71009
|
||||
timestamp: 1773111214926
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/clang-format-21.1.7-default_h99862b1_2.conda
|
||||
sha256: 8a9c2e0df0fd55da3ea9352fc356d1ca9d9312bbf275e1a5e22552bfb63affb9
|
||||
md5: 8e0b6ced8948217e4748ad0c26f8df4d
|
||||
@@ -817,6 +858,20 @@ packages:
|
||||
license_family: Apache
|
||||
size: 27733
|
||||
timestamp: 1766016586518
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-format-20.1.8-default_hf3020a7_14.conda
|
||||
sha256: 0967d59e8a89c5f9c1cfa3579403ee3295f5770c01b308c9abb434df47acc5db
|
||||
md5: 86639340249e4d8fa1b37ab92e581d26
|
||||
depends:
|
||||
- __osx >=11.0
|
||||
- clang-format-20 20.1.8 default_hf3020a7_14
|
||||
- libclang-cpp20.1 >=20.1.8,<20.2.0a0
|
||||
- libcxx >=20.1.8
|
||||
- libllvm20 >=20.1.8,<20.2.0a0
|
||||
license: Apache-2.0 WITH LLVM-exception
|
||||
license_family: Apache
|
||||
purls: []
|
||||
size: 71355
|
||||
timestamp: 1773110589745
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-format-21.1.7-default_hf3020a7_2.conda
|
||||
sha256: 2b91ed7e567a1618fd342100ba43bcd670bff8a1183a5c65032cefc0a436d3d6
|
||||
md5: 37585890edaa4842a62037a7fa6e1e64
|
||||
@@ -830,6 +885,18 @@ packages:
|
||||
license_family: Apache
|
||||
size: 28328
|
||||
timestamp: 1766167306042
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/clang-format-20.1.8-default_hac490eb_14.conda
|
||||
sha256: 7c657222c2b310387e11a316bf835910181c04ed54f2e35e28885cc5423c8496
|
||||
md5: 188355e2c07643a0232b9b817c230a36
|
||||
depends:
|
||||
- ucrt >=10.0.20348.0
|
||||
- vc >=14.3,<15
|
||||
- vc14_runtime >=14.44.35208
|
||||
license: Apache-2.0 WITH LLVM-exception
|
||||
license_family: Apache
|
||||
purls: []
|
||||
size: 1265666
|
||||
timestamp: 1773142030144
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/clang-format-21.1.7-default_hac490eb_2.conda
|
||||
sha256: afa22671525e890a98445d1c2abc35e8b87c4c208d053e6527c5d2cbf6f7c938
|
||||
md5: 8afc1288250ef6c77e8f7aa0df683a65
|
||||
@@ -841,6 +908,33 @@ packages:
|
||||
license_family: Apache
|
||||
size: 1233929
|
||||
timestamp: 1766022144370
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/clang-format-20-20.1.8-default_h99862b1_14.conda
|
||||
sha256: 17b84830ca43b9db271b50d8474a4231204321016f9ff5c2c690dddeb0d66f28
|
||||
md5: 55910d2902458004d9c4e443648c5841
|
||||
depends:
|
||||
- __glibc >=2.17,<3.0.a0
|
||||
- libclang-cpp20.1 >=20.1.8,<20.2.0a0
|
||||
- libgcc >=14
|
||||
- libllvm20 >=20.1.8,<20.2.0a0
|
||||
- libstdcxx >=14
|
||||
license: Apache-2.0 WITH LLVM-exception
|
||||
license_family: Apache
|
||||
purls: []
|
||||
size: 114295
|
||||
timestamp: 1773111143555
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-format-20-20.1.8-default_hf3020a7_14.conda
|
||||
sha256: 52f77009e2f69db348c10cb20371e0b1bf6f49e2e91e026d5fb93addc6526ef6
|
||||
md5: 2250c216f6da8f1f875f2b6a794d5e67
|
||||
depends:
|
||||
- __osx >=11.0
|
||||
- libclang-cpp20.1 >=20.1.8,<20.2.0a0
|
||||
- libcxx >=20.1.8
|
||||
- libllvm20 >=20.1.8,<20.2.0a0
|
||||
license: Apache-2.0 WITH LLVM-exception
|
||||
license_family: Apache
|
||||
purls: []
|
||||
size: 108365
|
||||
timestamp: 1773110420001
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/clang-format-21-21.1.7-default_h99862b1_2.conda
|
||||
sha256: 4d360c464ddab6c47c8fceca87b625b58d6a3b3cd523958a314e8db7d5f4eb94
|
||||
md5: 5589c06b39ae5be8aca4d16e28a43518
|
||||
@@ -866,6 +960,65 @@ packages:
|
||||
license_family: Apache
|
||||
size: 65217
|
||||
timestamp: 1766167190049
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/clang-tools-20.1.8-default_h57a47db_14.conda
|
||||
sha256: 565bbd489160501c5374fe346dca83a6ec4dac23aecc5bc48c276fe47ca2c263
|
||||
md5: 897e3de8e6310eb23d01004907520513
|
||||
depends:
|
||||
- __glibc >=2.17,<3.0.a0
|
||||
- clang-format 20.1.8 default_h99862b1_14
|
||||
- libclang-cpp20.1 >=20.1.8,<20.2.0a0
|
||||
- libclang13 >=20.1.8
|
||||
- libgcc >=14
|
||||
- libllvm20 >=20.1.8,<20.2.0a0
|
||||
- libstdcxx >=14
|
||||
- libxml2
|
||||
- libxml2-16 >=2.14.6
|
||||
constrains:
|
||||
- clangdev 20.1.8
|
||||
license: Apache-2.0 WITH LLVM-exception
|
||||
license_family: Apache
|
||||
purls: []
|
||||
size: 21897132
|
||||
timestamp: 1773111341498
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-tools-20.1.8-default_h1589341_14.conda
|
||||
sha256: 372c9e722fd99e42838528d33148ca1fa7dd84a02cf06ac7d291657f657e7026
|
||||
md5: 8a25b0558245abf052a89a38eb8dbb58
|
||||
depends:
|
||||
- __osx >=11.0
|
||||
- clang-format 20.1.8 default_hf3020a7_14
|
||||
- libclang-cpp20.1 >=20.1.8,<20.2.0a0
|
||||
- libclang13 >=20.1.8
|
||||
- libcxx >=20.1.8
|
||||
- libllvm20 >=20.1.8,<20.2.0a0
|
||||
- libxml2
|
||||
- libxml2-16 >=2.14.6
|
||||
constrains:
|
||||
- clangdev 20.1.8
|
||||
license: Apache-2.0 WITH LLVM-exception
|
||||
license_family: Apache
|
||||
purls: []
|
||||
size: 14941399
|
||||
timestamp: 1773110809290
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/clang-tools-20.1.8-default_hac490eb_14.conda
|
||||
sha256: 25b5962d5e46c01edd9e91b0092a1cbe840276cfce6e0ccd8752ff9d21deb0fb
|
||||
md5: 7f081578424e00ff4724ded6228f1be8
|
||||
depends:
|
||||
- clang-format 20.1.8 default_hac490eb_14
|
||||
- libclang13 >=20.1.8
|
||||
- libxml2
|
||||
- libxml2-16 >=2.14.6
|
||||
- libzlib >=1.3.1,<2.0a0
|
||||
- ucrt >=10.0.20348.0
|
||||
- vc >=14.3,<15
|
||||
- vc14_runtime >=14.44.35208
|
||||
- zstd >=1.5.7,<1.6.0a0
|
||||
constrains:
|
||||
- clangdev 20.1.8
|
||||
license: Apache-2.0 WITH LLVM-exception
|
||||
license_family: Apache
|
||||
purls: []
|
||||
size: 319341053
|
||||
timestamp: 1773142869937
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/clangxx-20.1.8-default_h363a0c9_5.conda
|
||||
sha256: d30843fc1012ff5fb6c95e07c15182d22f03a37c380ae350ba069adf75092828
|
||||
md5: 692c84fedd1a3302ff26dbc9a211f626
|
||||
@@ -1386,6 +1539,57 @@ packages:
|
||||
license_family: Apache
|
||||
size: 13672685
|
||||
timestamp: 1766166376555
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/libclang13-22.1.0-default_h746c552_0.conda
|
||||
sha256: 4a9dd814492a129f2ff40cd4ab0b942232c9e3c6dbc0d0aaf861f1f65e99cc7d
|
||||
md5: 140459a7413d8f6884eb68205ce39a0d
|
||||
depends:
|
||||
- __glibc >=2.17,<3.0.a0
|
||||
- libgcc >=14
|
||||
- libllvm22 >=22.1.0,<22.2.0a0
|
||||
- libstdcxx >=14
|
||||
license: Apache-2.0 WITH LLVM-exception
|
||||
license_family: Apache
|
||||
purls: []
|
||||
size: 12817500
|
||||
timestamp: 1772101411287
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libclang13-21.1.7-default_h13b06bd_4.conda
|
||||
sha256: ed55559f8af599838de775c099fbf644c19d1720b9f57064a2abb0ab2ff99765
|
||||
md5: e886d09eb4ae02f446edd4c32c351ca6
|
||||
depends:
|
||||
- __osx >=11.0
|
||||
- libcxx >=21.1.7
|
||||
- libllvm21 >=21.1.7,<21.2.0a0
|
||||
license: Apache-2.0 WITH LLVM-exception
|
||||
license_family: Apache
|
||||
purls: []
|
||||
size: 8517139
|
||||
timestamp: 1767751023683
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libclang13-21.1.8-default_h13b06bd_3.conda
|
||||
sha256: dc803069f4001aae4bdd6069af20140785a9c1a0c9abe6a3704e0ef87ad0f4c2
|
||||
md5: d111e982033ec02a55845e994a6db09a
|
||||
depends:
|
||||
- __osx >=11.0
|
||||
- libcxx >=21.1.8
|
||||
- libllvm21 >=21.1.8,<21.2.0a0
|
||||
license: Apache-2.0 WITH LLVM-exception
|
||||
license_family: Apache
|
||||
purls: []
|
||||
size: 8516482
|
||||
timestamp: 1771032974531
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/libclang13-22.1.0-default_ha2db4b5_0.conda
|
||||
sha256: c8e34362c6bf7305ef50f0de4e16292cd97e31650ab6465282eeeac62f0a05c4
|
||||
md5: 7ad437870ea7d487e1b0e663503b6b1d
|
||||
depends:
|
||||
- libzlib >=1.3.1,<2.0a0
|
||||
- ucrt >=10.0.20348.0
|
||||
- vc >=14.3,<15
|
||||
- vc14_runtime >=14.44.35208
|
||||
- zstd >=1.5.7,<1.6.0a0
|
||||
license: Apache-2.0 WITH LLVM-exception
|
||||
license_family: Apache
|
||||
purls: []
|
||||
size: 30584641
|
||||
timestamp: 1772353741135
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.17.0-h4e3cde8_1.conda
|
||||
sha256: 2d7be2fe0f58a0945692abee7bb909f8b19284b518d958747e5ff51d0655c303
|
||||
md5: 117499f93e892ea1e57fdca16c2e8351
|
||||
@@ -1761,8 +1965,25 @@ packages:
|
||||
- zstd >=1.5.7,<1.6.0a0
|
||||
license: Apache-2.0 WITH LLVM-exception
|
||||
license_family: Apache
|
||||
purls: []
|
||||
size: 29398498
|
||||
timestamp: 1765924904821
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/libllvm22-22.1.1-hf7376ad_0.conda
|
||||
sha256: 1145f9e85f0fbbdba88f1da5c8c48672bee7702e2f40c563b2dd48350ab4d413
|
||||
md5: 97cc6dad22677304846a798c8a65064d
|
||||
depends:
|
||||
- __glibc >=2.17,<3.0.a0
|
||||
- libgcc >=14
|
||||
- libstdcxx >=14
|
||||
- libxml2
|
||||
- libxml2-16 >=2.14.6
|
||||
- libzlib >=1.3.1,<2.0a0
|
||||
- zstd >=1.5.7,<1.6.0a0
|
||||
license: Apache-2.0 WITH LLVM-exception
|
||||
license_family: Apache
|
||||
purls: []
|
||||
size: 44256563
|
||||
timestamp: 1773371774629
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda
|
||||
sha256: f2591c0069447bbe28d4d696b7fcb0c5bd0b4ac582769b89addbcf26fb3430d8
|
||||
md5: 1a580f7796c7bf6393fddb8bbbde58dc
|
||||
@@ -2837,10 +3058,10 @@ packages:
|
||||
requires_dist:
|
||||
- colorama>=0.4.6 ; extra == 'windows-terminal'
|
||||
requires_python: '>=3.9'
|
||||
- pypi: https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl
|
||||
name: pytest
|
||||
version: 9.0.2
|
||||
sha256: 711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b
|
||||
version: 9.0.3
|
||||
sha256: 2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9
|
||||
requires_dist:
|
||||
- colorama>=0.4 ; sys_platform == 'win32'
|
||||
- exceptiongroup>=1 ; python_full_version < '3.11'
|
||||
|
||||
64
pixi.toml
64
pixi.toml
@@ -33,6 +33,7 @@ clang = "==20.1.8"
|
||||
clangxx = "==20.1.8"
|
||||
lld = "==20.1.8"
|
||||
llvm-tools = "==20.1.8"
|
||||
clang-tools = "==20.1.8"
|
||||
compiler-rt = "==20.1.8"
|
||||
|
||||
[feature.build.target.win-64.dependencies]
|
||||
@@ -62,6 +63,15 @@ lsprotocol = ">=2024.0.0"
|
||||
[feature.package.dependencies]
|
||||
xz = ">=5.8.1,<6"
|
||||
|
||||
[feature.package.tasks.package]
|
||||
cmd = """
|
||||
cmake -B build/RelWithDebInfo -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
|
||||
-DCMAKE_TOOLCHAIN_FILE=cmake/toolchain.cmake \
|
||||
-DCLICE_RELEASE=ON && \
|
||||
cmake --build build/RelWithDebInfo
|
||||
"""
|
||||
|
||||
# ============================================================================== #
|
||||
# CMAKE #
|
||||
# ============================================================================== #
|
||||
@@ -94,6 +104,10 @@ depends-on = [
|
||||
{ task = "cmake-build", args = ["{{ type }}"] },
|
||||
]
|
||||
|
||||
[feature.build.tasks.clang-tidy]
|
||||
args = [{ arg = "type", default = "RelWithDebInfo" }]
|
||||
depends-on = [{ task = "lint-cpp", args = ["{{ type }}"] }]
|
||||
|
||||
[feature.build.tasks.unit-test]
|
||||
args = [{ arg = "type", default = "RelWithDebInfo" }]
|
||||
cmd = './build/{{ type }}/bin/unit_tests --test-dir="./tests/data"'
|
||||
@@ -119,39 +133,6 @@ depends-on = [
|
||||
{ task = "integration-test", args = ["{{ type }}"] },
|
||||
]
|
||||
|
||||
# ============================================================================== #
|
||||
# XMAKE #
|
||||
# ============================================================================== #
|
||||
[feature.build.tasks.xmake-config]
|
||||
args = [
|
||||
{ arg = "type", default = "releasedbg" },
|
||||
{ arg = "ci", default = "n" },
|
||||
]
|
||||
cmd = "xmake config --yes --mode={{ type }} --toolchain=clang --ci={{ ci }}"
|
||||
|
||||
[feature.build.tasks.xmake-build]
|
||||
cmd = "xmake build --verbose --diagnosis --all"
|
||||
|
||||
[feature.build.tasks.xmake]
|
||||
args = [
|
||||
{ arg = "type", default = "releasedbg" },
|
||||
{ arg = "ci", default = "n" },
|
||||
]
|
||||
depends-on = [
|
||||
{ task = "xmake-config", args = ["{{ type }}", "{{ ci }}"] },
|
||||
{ task = "xmake-build" },
|
||||
]
|
||||
|
||||
[feature.test.tasks.xmake-test]
|
||||
cmd = "xmake test --verbose"
|
||||
|
||||
[feature.package.tasks.package]
|
||||
cmd = """
|
||||
xmake config --yes --toolchain=clang --mode=releasedbg \
|
||||
--enable_test=n --dev=n --release=y && \
|
||||
xmake pack --verbose
|
||||
"""
|
||||
|
||||
# ============================================================================== #
|
||||
# HELPER TASKS #
|
||||
# ============================================================================== #
|
||||
@@ -229,3 +210,20 @@ format = { depends-on = [
|
||||
"format-toml",
|
||||
"format-yaml"
|
||||
] }
|
||||
|
||||
# ============================================================================== #
|
||||
# LINT #
|
||||
# ============================================================================== #
|
||||
[feature.format.tasks.lint-python]
|
||||
cmd = "ruff check ."
|
||||
|
||||
[feature.build.tasks.lint-cpp]
|
||||
args = [{ arg = "type", default = "RelWithDebInfo" }]
|
||||
cmd = "python scripts/run_clang_tidy.py build/{{ type }}"
|
||||
|
||||
[feature.build.tasks.lint]
|
||||
args = [{ arg = "type", default = "RelWithDebInfo" }]
|
||||
depends-on = [
|
||||
"lint-python",
|
||||
{ task = "lint-cpp", args = ["{{ type }}"] },
|
||||
]
|
||||
|
||||
66
scripts/run_clang_tidy.py
Normal file
66
scripts/run_clang_tidy.py
Normal file
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run clang-tidy in parallel on all files in compile_commands.json."""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def main():
|
||||
build_dir = sys.argv[1] if len(sys.argv) > 1 else "build/RelWithDebInfo"
|
||||
cdb_path = Path(build_dir) / "compile_commands.json"
|
||||
|
||||
if not cdb_path.exists():
|
||||
print(f"Error: {cdb_path} not found. Run cmake-config first.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
project_root = Path(__file__).resolve().parent.parent
|
||||
src_dirs = (project_root / "src", project_root / "tests")
|
||||
|
||||
cdb = json.loads(cdb_path.read_text())
|
||||
files = [
|
||||
entry["file"]
|
||||
for entry in cdb
|
||||
if any(Path(entry["file"]).resolve().is_relative_to(d) for d in src_dirs)
|
||||
]
|
||||
total = len(files)
|
||||
|
||||
lock = threading.Lock()
|
||||
done = 0
|
||||
failed = []
|
||||
|
||||
def run(file: str) -> tuple[str, int, str]:
|
||||
result = subprocess.run(
|
||||
["clang-tidy", "-p", build_dir, "--quiet", file],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
return file, result.returncode, result.stdout + result.stderr
|
||||
|
||||
with ThreadPoolExecutor() as pool:
|
||||
futures = {pool.submit(run, f): f for f in files}
|
||||
for future in as_completed(futures):
|
||||
file, code, output = future.result()
|
||||
with lock:
|
||||
done += 1
|
||||
name = Path(file).name
|
||||
if code != 0:
|
||||
failed.append(file)
|
||||
print(f"[{done}/{total}] FAIL {name}")
|
||||
if output.strip():
|
||||
print(output, end="")
|
||||
else:
|
||||
print(f"[{done}/{total}] OK {name}")
|
||||
|
||||
if failed:
|
||||
print(f"\nclang-tidy failed on {len(failed)}/{total} files.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print(f"\nclang-tidy passed on {total} files.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
73
src/clice.cc
73
src/clice.cc
@@ -1,3 +1,4 @@
|
||||
#include <csignal>
|
||||
#include <cstdint>
|
||||
#include <iostream>
|
||||
#include <print>
|
||||
@@ -15,48 +16,62 @@
|
||||
|
||||
namespace clice {
|
||||
|
||||
using deco::decl::KVStyle;
|
||||
|
||||
struct Options {
|
||||
DecoKV(names = {"--mode"};
|
||||
help = "Running mode: pipe, socket, stateless-worker, stateful-worker";
|
||||
required = false;)
|
||||
DecoKV(style = KVStyle::JoinedOrSeparate,
|
||||
help = "Running mode: pipe, socket, stateless-worker, stateful-worker",
|
||||
required = false)
|
||||
<std::string> mode;
|
||||
|
||||
DecoKV(names = {"--host"}; help = "Socket mode address"; required = false;)
|
||||
DecoKV(style = KVStyle::JoinedOrSeparate, help = "Socket mode address", required = false)
|
||||
<std::string> host = "127.0.0.1";
|
||||
|
||||
DecoKV(names = {"--port"}; help = "Socket mode port"; required = false;)
|
||||
DecoKV(style = KVStyle::JoinedOrSeparate, help = "Socket mode port", required = false)
|
||||
<int> port = 50051;
|
||||
|
||||
DecoKV(names = {"--stateful-worker-count"}; help = "Number of stateful workers";
|
||||
required = false;)
|
||||
<std::uint32_t> stateful_worker_count;
|
||||
|
||||
DecoKV(names = {"--stateless-worker-count"}; help = "Number of stateless workers";
|
||||
required = false;)
|
||||
<std::uint32_t> stateless_worker_count;
|
||||
|
||||
DecoKV(names = {"--worker-memory-limit"}; help = "Memory limit per stateful worker (bytes)";
|
||||
required = false;)
|
||||
<std::uint64_t> worker_memory_limit;
|
||||
|
||||
DecoKV(names = {"--log-level"}; help = "Log level: trace, debug, info, warn, error, off";
|
||||
required = false;)
|
||||
DecoKV(style = KVStyle::JoinedOrSeparate,
|
||||
names = {"--log-level", "--log-level="},
|
||||
help = "Log level: trace, debug, info, warn, error, off",
|
||||
required = false)
|
||||
<std::string> log_level = "info";
|
||||
|
||||
DecoKV(names = {"--record"}; help = "Record LSP input to file for replay testing";
|
||||
required = false;)
|
||||
DecoKV(style = KVStyle::JoinedOrSeparate,
|
||||
help = "Record LSP input to file for replay testing",
|
||||
required = false)
|
||||
<std::string> record;
|
||||
|
||||
DecoFlag(names = {"-h", "--help"}; help = "Show help message"; required = false;)
|
||||
// Internal options (passed from master to worker processes)
|
||||
DecoKV(style = KVStyle::JoinedOrSeparate,
|
||||
names = {"--worker-memory-limit", "--worker-memory-limit="},
|
||||
required = false)
|
||||
<std::uint64_t> worker_memory_limit;
|
||||
|
||||
DecoKV(style = KVStyle::JoinedOrSeparate,
|
||||
names = {"--worker-name", "--worker-name="},
|
||||
required = false)
|
||||
<std::string> worker_name;
|
||||
|
||||
DecoKV(style = KVStyle::JoinedOrSeparate, names = {"--log-dir", "--log-dir="}, required = false)
|
||||
<std::string> log_dir;
|
||||
|
||||
DecoFlag(names = {"-h", "--help"}, help = "Show help message", required = false)
|
||||
help;
|
||||
|
||||
DecoFlag(names = {"-v", "--version"}; help = "Show version"; required = false;)
|
||||
DecoFlag(names = {"-v", "--version"}, help = "Show version", required = false)
|
||||
version;
|
||||
};
|
||||
|
||||
} // namespace clice
|
||||
|
||||
int main(int argc, const char** argv) {
|
||||
#ifndef _WIN32
|
||||
// On POSIX systems, ignore SIGPIPE so that writing to a closed pipe
|
||||
// (e.g. when the LSP client disconnects) returns EPIPE instead of
|
||||
// killing the process. This is standard practice for pipe-based servers.
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
#endif
|
||||
|
||||
auto args = deco::util::argvify(argc, argv);
|
||||
auto result = deco::cli::parse<clice::Options>(args);
|
||||
|
||||
@@ -97,13 +112,21 @@ int main(int argc, const char** argv) {
|
||||
|
||||
auto& mode = *opts.mode;
|
||||
|
||||
auto worker_name = opts.worker_name.value_or("");
|
||||
auto log_dir = opts.log_dir.value_or("");
|
||||
|
||||
if(mode == "stateless-worker") {
|
||||
return clice::run_stateless_worker_mode();
|
||||
return clice::run_stateless_worker_mode(worker_name.empty() ? "stateless-worker"
|
||||
: worker_name,
|
||||
log_dir);
|
||||
}
|
||||
|
||||
if(mode == "stateful-worker") {
|
||||
auto mem_limit = opts.worker_memory_limit.value_or(4ULL * 1024 * 1024 * 1024);
|
||||
return clice::run_stateful_worker_mode(mem_limit);
|
||||
return clice::run_stateful_worker_mode(mem_limit,
|
||||
worker_name.empty() ? "stateful-worker"
|
||||
: worker_name,
|
||||
log_dir);
|
||||
}
|
||||
|
||||
if(mode == "pipe") {
|
||||
|
||||
@@ -23,6 +23,44 @@ namespace ranges = std::ranges;
|
||||
|
||||
} // namespace
|
||||
|
||||
std::vector<const char*> CompileCommand::to_argv() const {
|
||||
std::vector<const char*> argv;
|
||||
argv.reserve(resolved.flags.size() + 4);
|
||||
|
||||
if(resolved.is_cc1 && source_file) {
|
||||
// cc1 mode requires TWO file-related arguments (both are needed):
|
||||
// 1. -main-file-name <basename> — used by clang for diagnostics/debug info
|
||||
// 2. <source_file> at the end — the actual input file path
|
||||
// These are NOT duplicates: (1) is just the basename, (2) is the full path.
|
||||
for(std::size_t i = 0; i < resolved.flags.size(); ++i) {
|
||||
argv.push_back(resolved.flags[i]);
|
||||
if(resolved.flags[i] == llvm::StringRef("-cc1")) {
|
||||
argv.push_back("-main-file-name");
|
||||
// path::filename returns a suffix of source_file (a pointer into
|
||||
// the same buffer), so .data() is null-terminated because source_file is.
|
||||
argv.push_back(path::filename(source_file).data());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
argv.insert(argv.end(), resolved.flags.begin(), resolved.flags.end());
|
||||
}
|
||||
|
||||
if(source_file) {
|
||||
argv.push_back(source_file);
|
||||
}
|
||||
return argv;
|
||||
}
|
||||
|
||||
std::vector<std::string> CompileCommand::to_string_argv() const {
|
||||
auto argv = to_argv();
|
||||
std::vector<std::string> result;
|
||||
result.reserve(argv.size());
|
||||
for(auto* arg: argv) {
|
||||
result.emplace_back(arg);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
CompilationDatabase::CompilationDatabase() = default;
|
||||
|
||||
CompilationDatabase::~CompilationDatabase() = default;
|
||||
@@ -329,8 +367,8 @@ std::size_t CompilationDatabase::load(llvm::StringRef path) {
|
||||
return entries.size();
|
||||
}
|
||||
|
||||
llvm::SmallVector<CompilationContext> CompilationDatabase::lookup(llvm::StringRef file,
|
||||
const CommandOptions& options) {
|
||||
llvm::SmallVector<CompileCommand> CompilationDatabase::lookup(llvm::StringRef file,
|
||||
const CommandOptions& options) {
|
||||
auto path_id = paths.intern(file);
|
||||
auto matched = find_entries(path_id);
|
||||
|
||||
@@ -338,17 +376,18 @@ llvm::SmallVector<CompilationContext> CompilationDatabase::lookup(llvm::StringRe
|
||||
render_arg_to([&](llvm::StringRef s) { out.push_back(strings.save(s).data()); }, arg);
|
||||
};
|
||||
|
||||
/// Build one CompilationContext from a single CompilationInfo.
|
||||
auto build_context = [&](object_ptr<CompilationInfo> info) -> CompilationContext {
|
||||
/// Build one CompileCommand from a single CompilationInfo.
|
||||
auto build_command = [&](object_ptr<CompilationInfo> info) -> CompileCommand {
|
||||
llvm::StringRef directory = info->directory;
|
||||
std::vector<const char*> arguments;
|
||||
std::vector<const char*> flags;
|
||||
bool is_cc1 = false;
|
||||
|
||||
auto append_arg = [&](llvm::StringRef s) {
|
||||
arguments.emplace_back(strings.save(s).data());
|
||||
flags.emplace_back(strings.save(s).data());
|
||||
};
|
||||
|
||||
auto append_args = [&](llvm::ArrayRef<const char*> args) {
|
||||
arguments.insert(arguments.end(), args.begin(), args.end());
|
||||
flags.insert(flags.end(), args.begin(), args.end());
|
||||
};
|
||||
|
||||
if(options.query_toolchain) {
|
||||
@@ -361,23 +400,20 @@ llvm::SmallVector<CompilationContext> CompilationDatabase::lookup(llvm::StringRe
|
||||
append_args(info->canonical->arguments);
|
||||
append_args(info->patch);
|
||||
} else {
|
||||
arguments.assign(cached.begin(), cached.end());
|
||||
// TODO: add an assertion that the last arg is the temp source
|
||||
// file (e.g., contains "query-toolchain") to guard against
|
||||
// future changes in clang cc1 argument ordering.
|
||||
arguments.pop_back(); // remove temp source file
|
||||
flags.assign(cached.begin(), cached.end());
|
||||
flags.pop_back(); // remove temp source file
|
||||
|
||||
// Replace resource dir if needed.
|
||||
if(!resource_dir().empty()) {
|
||||
llvm::StringRef old_resource_dir;
|
||||
for(std::size_t i = 0; i + 1 < arguments.size(); ++i) {
|
||||
if(arguments[i] == llvm::StringRef("-resource-dir")) {
|
||||
old_resource_dir = arguments[i + 1];
|
||||
for(std::size_t i = 0; i + 1 < flags.size(); ++i) {
|
||||
if(flags[i] == llvm::StringRef("-resource-dir")) {
|
||||
old_resource_dir = flags[i + 1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!old_resource_dir.empty() && old_resource_dir != resource_dir()) {
|
||||
for(auto& arg: arguments) {
|
||||
for(auto& arg: flags) {
|
||||
llvm::StringRef s(arg);
|
||||
if(s.starts_with(old_resource_dir)) {
|
||||
auto replaced =
|
||||
@@ -390,39 +426,42 @@ llvm::SmallVector<CompilationContext> CompilationDatabase::lookup(llvm::StringRe
|
||||
|
||||
append_args(info->patch);
|
||||
|
||||
// Fix -main-file-name to match the actual file.
|
||||
bool next_main_file = false;
|
||||
for(auto& arg: arguments) {
|
||||
if(arg == llvm::StringRef("-main-file-name")) {
|
||||
next_main_file = true;
|
||||
// Strip -main-file-name and its value from flags (to_argv() will
|
||||
// re-inject it with the correct basename when is_cc1 is set).
|
||||
std::vector<const char*> cleaned;
|
||||
cleaned.reserve(flags.size());
|
||||
for(std::size_t i = 0; i < flags.size(); ++i) {
|
||||
if(flags[i] == llvm::StringRef("-main-file-name") && i + 1 < flags.size()) {
|
||||
++i; // skip the value
|
||||
continue;
|
||||
}
|
||||
if(next_main_file) {
|
||||
arg = strings.save(path::filename(file)).data();
|
||||
next_main_file = false;
|
||||
}
|
||||
cleaned.push_back(flags[i]);
|
||||
}
|
||||
}
|
||||
flags = std::move(cleaned);
|
||||
|
||||
// Inject our resource dir if not already present.
|
||||
if(!resource_dir().empty()) {
|
||||
bool has_resource_dir = false;
|
||||
for(auto& arg: arguments) {
|
||||
if(arg == llvm::StringRef("-resource-dir")) {
|
||||
has_resource_dir = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!has_resource_dir) {
|
||||
append_arg("-resource-dir");
|
||||
append_arg(resource_dir());
|
||||
}
|
||||
// Detect cc1 mode (search rather than assuming index).
|
||||
is_cc1 = ranges::contains(flags, llvm::StringRef("-cc1"));
|
||||
}
|
||||
} else {
|
||||
append_args(info->canonical->arguments);
|
||||
append_args(info->patch);
|
||||
}
|
||||
|
||||
// Inject our resource dir if not already present.
|
||||
if(options.inject_resource_dir && !resource_dir().empty()) {
|
||||
bool has_resource_dir = false;
|
||||
for(auto& arg: flags) {
|
||||
if(arg == llvm::StringRef("-resource-dir")) {
|
||||
has_resource_dir = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!has_resource_dir) {
|
||||
append_arg("-resource-dir");
|
||||
append_arg(resource_dir());
|
||||
}
|
||||
}
|
||||
|
||||
// Apply remove filter.
|
||||
if(!options.remove.empty()) {
|
||||
using Arg = std::unique_ptr<llvm::opt::Arg>;
|
||||
@@ -440,12 +479,12 @@ llvm::SmallVector<CompilationContext> CompilationDatabase::lookup(llvm::StringRe
|
||||
};
|
||||
std::ranges::sort(remove_args, {}, get_id);
|
||||
|
||||
auto saved_args = std::move(arguments);
|
||||
arguments.clear();
|
||||
arguments.push_back(saved_args.front());
|
||||
auto saved_flags = std::move(flags);
|
||||
flags.clear();
|
||||
flags.push_back(saved_flags.front());
|
||||
|
||||
parser->parse(
|
||||
llvm::ArrayRef(saved_args).drop_front(),
|
||||
llvm::ArrayRef(saved_flags).drop_front(),
|
||||
[&](Arg arg) {
|
||||
auto id = arg->getOption().getID();
|
||||
auto range = std::ranges::equal_range(remove_args, id, {}, get_id);
|
||||
@@ -461,7 +500,7 @@ llvm::SmallVector<CompilationContext> CompilationDatabase::lookup(llvm::StringRe
|
||||
return;
|
||||
}
|
||||
}
|
||||
render_arg(arguments, *arg);
|
||||
render_arg(flags, *arg);
|
||||
},
|
||||
[](int, int) {});
|
||||
}
|
||||
@@ -470,26 +509,34 @@ llvm::SmallVector<CompilationContext> CompilationDatabase::lookup(llvm::StringRe
|
||||
append_arg(arg);
|
||||
}
|
||||
|
||||
arguments.emplace_back(paths.resolve(path_id).data());
|
||||
return CompilationContext(directory, std::move(arguments));
|
||||
return CompileCommand{
|
||||
ResolvedFlags{directory, std::move(flags), is_cc1},
|
||||
paths.resolve(path_id).data()
|
||||
};
|
||||
};
|
||||
|
||||
llvm::SmallVector<CompilationContext> results;
|
||||
llvm::SmallVector<CompileCommand> results;
|
||||
|
||||
if(!matched.empty()) {
|
||||
for(auto& entry: matched) {
|
||||
results.push_back(build_context(entry.info));
|
||||
results.push_back(build_command(entry.info));
|
||||
}
|
||||
} else {
|
||||
// No matching entry — synthesize a default command.
|
||||
std::vector<const char*> arguments;
|
||||
std::vector<const char*> flags;
|
||||
if(file.ends_with(".cpp") || file.ends_with(".hpp") || file.ends_with(".cc")) {
|
||||
arguments = {"clang++", "-std=c++20"};
|
||||
flags = {"clang++", "-std=c++20"};
|
||||
} else {
|
||||
arguments = {"clang"};
|
||||
flags = {"clang"};
|
||||
}
|
||||
arguments.emplace_back(paths.resolve(path_id).data());
|
||||
results.push_back(CompilationContext({}, std::move(arguments)));
|
||||
if(options.inject_resource_dir && !resource_dir().empty()) {
|
||||
flags.push_back(strings.save("-resource-dir").data());
|
||||
flags.push_back(strings.save(resource_dir()).data());
|
||||
}
|
||||
results.push_back(CompileCommand{
|
||||
ResolvedFlags{{}, std::move(flags), false},
|
||||
paths.resolve(path_id).data()
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
@@ -513,8 +560,8 @@ SearchConfig CompilationDatabase::lookup_search_config(llvm::StringRef file,
|
||||
}
|
||||
|
||||
auto results = lookup(file, options);
|
||||
auto& ctx = results.front();
|
||||
auto config = extract_search_config(ctx.arguments, ctx.directory);
|
||||
auto& cmd = results.front();
|
||||
auto config = extract_search_config(cmd.to_argv(), cmd.resolved.directory);
|
||||
|
||||
if(cacheable) {
|
||||
auto key = ConfigCacheKey{matched.front().info.ptr, options_bits(options)};
|
||||
@@ -650,6 +697,11 @@ std::uint32_t CompilationDatabase::intern_path(llvm::StringRef path) {
|
||||
return paths.intern(path);
|
||||
}
|
||||
|
||||
bool CompilationDatabase::has_entry(llvm::StringRef file) {
|
||||
auto path_id = paths.intern(file);
|
||||
return !find_entries(path_id).empty();
|
||||
}
|
||||
|
||||
llvm::ArrayRef<CompilationEntry> CompilationDatabase::get_entries() const {
|
||||
return entries;
|
||||
}
|
||||
|
||||
@@ -29,6 +29,11 @@ struct CommandOptions {
|
||||
/// Set true in unittests to avoid cluttering test output.
|
||||
bool suppress_logging = false;
|
||||
|
||||
/// Inject our resource dir into the flags if not already present.
|
||||
/// Enabled by default so clang tools always use matching builtin headers.
|
||||
/// Disable in unit tests that assert exact argument counts.
|
||||
bool inject_resource_dir = true;
|
||||
|
||||
/// Extra arguments to remove from the original command line.
|
||||
llvm::ArrayRef<std::string> remove;
|
||||
|
||||
@@ -36,12 +41,35 @@ struct CommandOptions {
|
||||
llvm::ArrayRef<std::string> append;
|
||||
};
|
||||
|
||||
struct CompilationContext {
|
||||
/// File-independent compilation flags (shareable, suitable as cache key input).
|
||||
/// Does NOT contain source file path or -main-file-name.
|
||||
struct ResolvedFlags {
|
||||
/// The working directory of compilation.
|
||||
llvm::StringRef directory;
|
||||
|
||||
/// The compilation arguments.
|
||||
std::vector<const char*> arguments;
|
||||
/// All flags excluding source file path and -main-file-name.
|
||||
std::vector<const char*> flags;
|
||||
|
||||
/// Whether flags come from toolchain query (cc1 mode).
|
||||
/// When true, flags are cc1 frontend args (resolved clang binary + "-cc1" + ...),
|
||||
/// NOT the original driver command. to_argv() scans for "-cc1" in flags and
|
||||
/// inserts -main-file-name immediately after it.
|
||||
bool is_cc1 = false;
|
||||
};
|
||||
|
||||
/// Compilation command = resolved flags + source file identity.
|
||||
struct CompileCommand {
|
||||
ResolvedFlags resolved;
|
||||
|
||||
/// Interned, pointer-stable. Must be null-terminated (required by to_argv()
|
||||
/// and path::filename().data() which relies on the suffix being null-terminated).
|
||||
const char* source_file = nullptr;
|
||||
|
||||
/// Produce full argv: flags + [-main-file-name <basename> if cc1] + source_file.
|
||||
std::vector<const char*> to_argv() const;
|
||||
|
||||
/// Convenience: to_argv() converted to vector<string>.
|
||||
std::vector<std::string> to_string_argv() const;
|
||||
};
|
||||
|
||||
/// Shared compiler identity — driver + all semantics-affecting flags.
|
||||
@@ -174,10 +202,10 @@ public:
|
||||
/// but toolchain cache survives. Returns the number of entries loaded.
|
||||
std::size_t load(llvm::StringRef path);
|
||||
|
||||
/// Lookup the compilation contexts for a file. A file may have multiple
|
||||
/// Lookup the compile commands for a file. A file may have multiple
|
||||
/// compilation commands (e.g. different build configurations); all are returned.
|
||||
llvm::SmallVector<CompilationContext> lookup(llvm::StringRef file,
|
||||
const CommandOptions& options = {});
|
||||
llvm::SmallVector<CompileCommand> lookup(llvm::StringRef file,
|
||||
const CommandOptions& options = {});
|
||||
|
||||
/// Combined lookup + extract_search_config with internal caching.
|
||||
SearchConfig lookup_search_config(llvm::StringRef file, const CommandOptions& options = {});
|
||||
@@ -191,6 +219,10 @@ public:
|
||||
/// Intern a file path and return its path_id.
|
||||
std::uint32_t intern_path(llvm::StringRef path);
|
||||
|
||||
/// Check if a file has an explicit entry in the compilation database
|
||||
/// (as opposed to a synthesized default).
|
||||
bool has_entry(llvm::StringRef file);
|
||||
|
||||
/// All compilation entries (sorted by path_id).
|
||||
llvm::ArrayRef<CompilationEntry> get_entries() const;
|
||||
|
||||
|
||||
@@ -87,7 +87,9 @@ auto CompilationUnitRef::file_path(clang::FileID fid) -> llvm::StringRef {
|
||||
}
|
||||
|
||||
auto entry = self->SM().getFileEntryRefForID(fid);
|
||||
assert(entry && "Invalid file entry");
|
||||
if(!entry) {
|
||||
return {};
|
||||
}
|
||||
|
||||
llvm::SmallString<128> path;
|
||||
|
||||
@@ -242,13 +244,19 @@ std::vector<std::string> CompilationUnitRef::deps() {
|
||||
for(auto& [fid, directive]: directives()) {
|
||||
for(auto& include: directive.includes) {
|
||||
if(!include.skipped) {
|
||||
deps.try_emplace(file_path(include.fid));
|
||||
auto path = file_path(include.fid);
|
||||
if(!path.empty()) {
|
||||
deps.try_emplace(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(auto& has_include: directive.has_includes) {
|
||||
if(has_include.fid.isValid()) {
|
||||
deps.try_emplace(file_path(has_include.fid));
|
||||
auto path = file_path(has_include.fid);
|
||||
if(!path.empty()) {
|
||||
deps.try_emplace(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,11 +65,36 @@ public:
|
||||
/// Rewritten Preprocessor Callbacks
|
||||
/// ============================================================================
|
||||
|
||||
void HasEmbed(clang::SourceLocation location,
|
||||
llvm::StringRef filename,
|
||||
bool is_angled,
|
||||
clang::OptionalFileEntryRef file) override {
|
||||
unit->directives[unit.file_id(location)].has_embeds.emplace_back(clice::HasEmbed{
|
||||
.file_name = filename,
|
||||
.file = file,
|
||||
.is_angled = is_angled,
|
||||
.loc = location,
|
||||
});
|
||||
}
|
||||
|
||||
void EmbedDirective(clang::SourceLocation location,
|
||||
clang::StringRef filename,
|
||||
bool is_angled,
|
||||
clang::OptionalFileEntryRef file,
|
||||
const clang::LexEmbedParametersResult&) override {
|
||||
unit->directives[unit.file_id(location)].embeds.emplace_back(Embed{
|
||||
.file_name = filename,
|
||||
.file = file,
|
||||
.is_angled = is_angled,
|
||||
.loc = location,
|
||||
});
|
||||
}
|
||||
|
||||
void InclusionDirective(clang::SourceLocation hash_loc,
|
||||
const clang::Token& include_tok,
|
||||
llvm::StringRef,
|
||||
bool,
|
||||
clang::CharSourceRange filename_range,
|
||||
clang::CharSourceRange,
|
||||
clang::OptionalFileEntryRef,
|
||||
llvm::StringRef,
|
||||
llvm::StringRef,
|
||||
@@ -83,7 +108,6 @@ public:
|
||||
unit->directives[prev_fid].includes.emplace_back(Include{
|
||||
.fid = {},
|
||||
.location = include_tok.getLocation(),
|
||||
.filename_range = filename_range.getAsRange(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -20,11 +20,8 @@ struct Include {
|
||||
/// The file id of included file.
|
||||
clang::FileID fid;
|
||||
|
||||
/// Location of the `include`.
|
||||
/// Location of the `include` keyword.
|
||||
clang::SourceLocation location;
|
||||
|
||||
/// The range of filename(includes `""` or `<>`).
|
||||
clang::SourceRange filename_range;
|
||||
};
|
||||
|
||||
/// Information about `__has_include` directive.
|
||||
@@ -132,6 +129,39 @@ struct Import {
|
||||
std::vector<clang::SourceLocation> name_locations;
|
||||
};
|
||||
|
||||
/// Information about `#embed` directive.
|
||||
struct Embed {
|
||||
/// The file name in the embed directive, not including quotes or angle brackets.
|
||||
llvm::StringRef file_name;
|
||||
|
||||
/// The actual file that may be embedded by this embed directive.
|
||||
clang::OptionalFileEntryRef file;
|
||||
|
||||
/// Whether the file name is angled.
|
||||
bool is_angled;
|
||||
|
||||
/// Location of the `#` token.
|
||||
clang::SourceLocation loc;
|
||||
|
||||
/// TODO: Currently we do not store parameters of the embed directive.
|
||||
/// See clang::LexEmbedParametersResult for details.
|
||||
};
|
||||
|
||||
/// Information about `__has_embed` directive.
|
||||
struct HasEmbed {
|
||||
/// The file name in the embed directive, not including quotes or angle brackets.
|
||||
llvm::StringRef file_name;
|
||||
|
||||
/// The actual file that may be embedded by this embed directive.
|
||||
clang::OptionalFileEntryRef file;
|
||||
|
||||
/// Whether the file name is angled.
|
||||
bool is_angled;
|
||||
|
||||
/// Location of the `__has_embed` token.
|
||||
clang::SourceLocation loc;
|
||||
};
|
||||
|
||||
struct Directive {
|
||||
std::vector<Include> includes;
|
||||
std::vector<HasInclude> has_includes;
|
||||
@@ -139,6 +169,8 @@ struct Directive {
|
||||
std::vector<MacroRef> macros;
|
||||
std::vector<Pragma> pragmas;
|
||||
std::vector<Import> imports;
|
||||
std::vector<Embed> embeds;
|
||||
std::vector<HasEmbed> has_embeds;
|
||||
};
|
||||
|
||||
} // namespace clice
|
||||
|
||||
@@ -53,8 +53,8 @@ auto completion_kind(const clang::NamedDecl* decl) -> protocol::CompletionItemKi
|
||||
return protocol::CompletionItemKind::Module;
|
||||
}
|
||||
|
||||
if(llvm::isa<clang::FunctionDecl, clang::FunctionTemplateDecl>(decl)) {
|
||||
return protocol::CompletionItemKind::Function;
|
||||
if(llvm::isa<clang::CXXConstructorDecl>(decl)) {
|
||||
return protocol::CompletionItemKind::Constructor;
|
||||
}
|
||||
|
||||
if(llvm::isa<clang::CXXMethodDecl,
|
||||
@@ -64,8 +64,8 @@ auto completion_kind(const clang::NamedDecl* decl) -> protocol::CompletionItemKi
|
||||
return protocol::CompletionItemKind::Method;
|
||||
}
|
||||
|
||||
if(llvm::isa<clang::CXXConstructorDecl>(decl)) {
|
||||
return protocol::CompletionItemKind::Constructor;
|
||||
if(llvm::isa<clang::FunctionDecl, clang::FunctionTemplateDecl>(decl)) {
|
||||
return protocol::CompletionItemKind::Function;
|
||||
}
|
||||
|
||||
if(llvm::isa<clang::FieldDecl, clang::IndirectFieldDecl>(decl)) {
|
||||
@@ -109,6 +109,115 @@ auto completion_kind(const clang::NamedDecl* decl) -> protocol::CompletionItemKi
|
||||
return protocol::CompletionItemKind::Text;
|
||||
}
|
||||
|
||||
/// Extract the function signature (parameter list) from a CodeCompletionString.
|
||||
/// Returns something like "(int x, float y)" for display in labelDetails.detail.
|
||||
auto extract_signature(const clang::CodeCompletionString& ccs) -> std::string {
|
||||
std::string signature;
|
||||
bool in_parens = false;
|
||||
|
||||
for(const auto& chunk: ccs) {
|
||||
using CK = clang::CodeCompletionString::ChunkKind;
|
||||
switch(chunk.Kind) {
|
||||
case CK::CK_LeftParen:
|
||||
in_parens = true;
|
||||
signature += '(';
|
||||
break;
|
||||
case CK::CK_RightParen:
|
||||
signature += ')';
|
||||
in_parens = false;
|
||||
break;
|
||||
case CK::CK_Placeholder:
|
||||
case CK::CK_CurrentParameter:
|
||||
if(in_parens && chunk.Text) {
|
||||
signature += chunk.Text;
|
||||
}
|
||||
break;
|
||||
case CK::CK_Text:
|
||||
case CK::CK_Informative:
|
||||
if(in_parens && chunk.Text) {
|
||||
signature += chunk.Text;
|
||||
}
|
||||
break;
|
||||
case CK::CK_LeftAngle:
|
||||
signature += '<';
|
||||
in_parens = true;
|
||||
break;
|
||||
case CK::CK_RightAngle:
|
||||
signature += '>';
|
||||
in_parens = false;
|
||||
break;
|
||||
case CK::CK_Comma:
|
||||
if(in_parens) {
|
||||
signature += ", ";
|
||||
}
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
return signature;
|
||||
}
|
||||
|
||||
/// Build a snippet string from a CodeCompletionString.
|
||||
/// Produces e.g. "funcName(${1:int x}, ${2:float y})" for functions,
|
||||
/// or "ClassName<${1:T}>" for class templates.
|
||||
auto build_snippet(const clang::CodeCompletionString& ccs) -> std::string {
|
||||
std::string snippet;
|
||||
unsigned placeholder_index = 0;
|
||||
|
||||
for(const auto& chunk: ccs) {
|
||||
using CK = clang::CodeCompletionString::ChunkKind;
|
||||
switch(chunk.Kind) {
|
||||
case CK::CK_TypedText:
|
||||
if(chunk.Text) {
|
||||
snippet += chunk.Text;
|
||||
}
|
||||
break;
|
||||
case CK::CK_Placeholder:
|
||||
if(chunk.Text) {
|
||||
snippet += std::format("${{{0}:{1}}}", ++placeholder_index, chunk.Text);
|
||||
}
|
||||
break;
|
||||
case CK::CK_LeftParen: snippet += '('; break;
|
||||
case CK::CK_RightParen: snippet += ')'; break;
|
||||
case CK::CK_LeftAngle: snippet += '<'; break;
|
||||
case CK::CK_RightAngle: snippet += '>'; break;
|
||||
case CK::CK_Comma: snippet += ", "; break;
|
||||
case CK::CK_Text:
|
||||
if(chunk.Text) {
|
||||
snippet += chunk.Text;
|
||||
}
|
||||
break;
|
||||
case CK::CK_Optional:
|
||||
// Optional chunks contain default arguments — skip for snippet.
|
||||
break;
|
||||
case CK::CK_Informative:
|
||||
case CK::CK_ResultType:
|
||||
case CK::CK_CurrentParameter:
|
||||
// Display-only chunks, not part of insertion.
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
// If no placeholders were generated, return empty to signal plain text.
|
||||
if(placeholder_index == 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return snippet;
|
||||
}
|
||||
|
||||
/// Extract the return type from a CodeCompletionString.
|
||||
auto extract_return_type(const clang::CodeCompletionString& ccs) -> std::string {
|
||||
for(const auto& chunk: ccs) {
|
||||
if(chunk.Kind == clang::CodeCompletionString::CK_ResultType && chunk.Text) {
|
||||
return chunk.Text;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
struct OverloadItem {
|
||||
protocol::CompletionItem item;
|
||||
float score = 0.0F;
|
||||
@@ -159,29 +268,44 @@ public:
|
||||
overloads.reserve(candidate_count);
|
||||
std::unordered_map<std::string, std::size_t> overload_index;
|
||||
|
||||
auto build_item =
|
||||
[&](llvm::StringRef label, protocol::CompletionItemKind kind, llvm::StringRef insert) {
|
||||
protocol::CompletionItem item{
|
||||
.label = label.str(),
|
||||
};
|
||||
item.kind = kind;
|
||||
bool prefix_starts_with_underscore = prefix.spelling.starts_with("_");
|
||||
|
||||
protocol::TextEdit edit{
|
||||
.range = replace_range,
|
||||
.new_text = insert.empty() ? label.str() : insert.str(),
|
||||
};
|
||||
item.text_edit = std::move(edit);
|
||||
return item;
|
||||
auto build_item = [&](llvm::StringRef label,
|
||||
protocol::CompletionItemKind kind,
|
||||
llvm::StringRef insert,
|
||||
bool is_snippet = false) {
|
||||
protocol::CompletionItem item{
|
||||
.label = label.str(),
|
||||
};
|
||||
item.kind = kind;
|
||||
|
||||
protocol::TextEdit edit{
|
||||
.range = replace_range,
|
||||
.new_text = insert.empty() ? label.str() : insert.str(),
|
||||
};
|
||||
item.text_edit = std::move(edit);
|
||||
if(is_snippet) {
|
||||
item.insert_text_format = protocol::InsertTextFormat::Snippet;
|
||||
}
|
||||
return item;
|
||||
};
|
||||
|
||||
auto try_add = [&](llvm::StringRef label,
|
||||
protocol::CompletionItemKind kind,
|
||||
llvm::StringRef insert_text,
|
||||
llvm::StringRef overload_key) {
|
||||
llvm::StringRef overload_key,
|
||||
llvm::StringRef signature = {},
|
||||
llvm::StringRef return_type = {},
|
||||
bool is_snippet = false) {
|
||||
if(label.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter out _/__ prefixed internal symbols unless user typed _.
|
||||
if(!prefix_starts_with_underscore && label.starts_with("_")) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto score = matcher.match(label);
|
||||
if(!score.has_value()) {
|
||||
return;
|
||||
@@ -191,8 +315,18 @@ public:
|
||||
auto [it, inserted] =
|
||||
overload_index.try_emplace(overload_key.str(), overloads.size());
|
||||
if(inserted) {
|
||||
auto item = build_item(label, kind, insert_text);
|
||||
auto item = build_item(label, kind, insert_text, is_snippet);
|
||||
item.sort_text = std::format("{}", *score);
|
||||
if(!signature.empty() || !return_type.empty()) {
|
||||
protocol::CompletionItemLabelDetails details;
|
||||
if(!signature.empty()) {
|
||||
details.detail = signature.str();
|
||||
}
|
||||
if(!return_type.empty()) {
|
||||
details.description = return_type.str();
|
||||
}
|
||||
item.label_details = std::move(details);
|
||||
}
|
||||
overloads.push_back({
|
||||
.item = std::move(item),
|
||||
.score = *score,
|
||||
@@ -209,8 +343,18 @@ public:
|
||||
return;
|
||||
}
|
||||
|
||||
auto item = build_item(label, kind, insert_text);
|
||||
auto item = build_item(label, kind, insert_text, is_snippet);
|
||||
item.sort_text = std::format("{}", *score);
|
||||
if(!signature.empty() || !return_type.empty()) {
|
||||
protocol::CompletionItemLabelDetails details;
|
||||
if(!signature.empty()) {
|
||||
details.detail = signature.str();
|
||||
}
|
||||
if(!return_type.empty()) {
|
||||
details.description = return_type.str();
|
||||
}
|
||||
item.label_details = std::move(details);
|
||||
}
|
||||
collected.push_back(std::move(item));
|
||||
};
|
||||
|
||||
@@ -242,16 +386,58 @@ public:
|
||||
break;
|
||||
}
|
||||
|
||||
auto label = ast::name_of(declaration);
|
||||
auto kind = completion_kind(declaration);
|
||||
|
||||
// For constructors and deduction guides, use the class name
|
||||
// (without template args) instead of the full type name.
|
||||
// e.g. "vector" instead of "vector<_Tp, _Alloc>".
|
||||
std::string label;
|
||||
if(auto* ctor = llvm::dyn_cast<clang::CXXConstructorDecl>(declaration)) {
|
||||
label = ctor->getParent()->getName().str();
|
||||
} else if(auto* guide =
|
||||
llvm::dyn_cast<clang::CXXDeductionGuideDecl>(declaration)) {
|
||||
label = guide->getDeducedTemplate()->getName().str();
|
||||
} else {
|
||||
label = ast::name_of(declaration);
|
||||
}
|
||||
|
||||
llvm::SmallString<256> qualified_name;
|
||||
if(options.bundle_overloads && kind == protocol::CompletionItemKind::Function) {
|
||||
bool is_callable = kind == protocol::CompletionItemKind::Function ||
|
||||
kind == protocol::CompletionItemKind::Method ||
|
||||
kind == protocol::CompletionItemKind::Constructor;
|
||||
if(options.bundle_overloads && is_callable) {
|
||||
llvm::raw_svector_ostream stream(qualified_name);
|
||||
declaration->printQualifiedName(stream);
|
||||
}
|
||||
|
||||
try_add(label, kind, label, qualified_name.str());
|
||||
std::string signature;
|
||||
std::string return_type;
|
||||
std::string snippet;
|
||||
auto* ccs =
|
||||
candidate.CreateCodeCompletionString(sema,
|
||||
context,
|
||||
getAllocator(),
|
||||
getCodeCompletionTUInfo(),
|
||||
/*IncludeBriefComments=*/false);
|
||||
if(ccs) {
|
||||
signature = extract_signature(*ccs);
|
||||
return_type = extract_return_type(*ccs);
|
||||
// Generate snippet for non-bundled callables.
|
||||
if(is_callable && !options.bundle_overloads &&
|
||||
options.enable_function_arguments_snippet) {
|
||||
snippet = build_snippet(*ccs);
|
||||
}
|
||||
}
|
||||
|
||||
bool has_snippet = !snippet.empty();
|
||||
auto insert = has_snippet ? llvm::StringRef(snippet) : llvm::StringRef(label);
|
||||
try_add(label,
|
||||
kind,
|
||||
insert,
|
||||
qualified_name.str(),
|
||||
signature,
|
||||
return_type,
|
||||
has_snippet);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -259,11 +445,48 @@ public:
|
||||
|
||||
for(auto& entry: overloads) {
|
||||
if(entry.count > 1) {
|
||||
entry.item.detail = "(...)";
|
||||
protocol::CompletionItemLabelDetails details;
|
||||
details.detail = std::format("(…) +{} overloads", entry.count);
|
||||
entry.item.label_details = std::move(details);
|
||||
}
|
||||
collected.push_back(std::move(entry.item));
|
||||
}
|
||||
|
||||
// In bundle mode, deduplicate by label: when the same name appears as
|
||||
// both a class and its constructors/deduction guides, keep only the
|
||||
// highest-priority kind (Class > Function/Method > others).
|
||||
if(options.bundle_overloads) {
|
||||
auto kind_priority = [](protocol::CompletionItemKind k) -> int {
|
||||
switch(k) {
|
||||
case protocol::CompletionItemKind::Class:
|
||||
case protocol::CompletionItemKind::Struct: return 3;
|
||||
case protocol::CompletionItemKind::Function:
|
||||
case protocol::CompletionItemKind::Method: return 2;
|
||||
case protocol::CompletionItemKind::Constructor: return 1;
|
||||
default: return 0;
|
||||
}
|
||||
};
|
||||
|
||||
std::unordered_map<std::string, std::size_t> label_index;
|
||||
std::vector<protocol::CompletionItem> deduped;
|
||||
deduped.reserve(collected.size());
|
||||
|
||||
for(auto& item: collected) {
|
||||
auto [it, inserted] = label_index.try_emplace(item.label, deduped.size());
|
||||
if(inserted) {
|
||||
deduped.push_back(std::move(item));
|
||||
} else {
|
||||
auto& existing = deduped[it->second];
|
||||
int old_prio = existing.kind.has_value() ? kind_priority(*existing.kind) : 0;
|
||||
int new_prio = item.kind.has_value() ? kind_priority(*item.kind) : 0;
|
||||
if(new_prio > old_prio) {
|
||||
existing = std::move(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
collected.swap(deduped);
|
||||
}
|
||||
|
||||
output.clear();
|
||||
output.swap(collected);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "feature/feature.h"
|
||||
#include "syntax/lexer.h"
|
||||
|
||||
namespace clice::feature {
|
||||
|
||||
namespace {} // namespace
|
||||
namespace {
|
||||
|
||||
bool is_directive_keyword(llvm::StringRef word) {
|
||||
return word == "include" || word == "include_next" || word == "import" || word == "embed" ||
|
||||
word == "__has_include" || word == "__has_include_next" || word == "__has_embed";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
auto document_links(CompilationUnitRef unit, PositionEncoding encoding)
|
||||
-> std::vector<protocol::DocumentLink> {
|
||||
@@ -23,49 +30,92 @@ auto document_links(CompilationUnitRef unit, PositionEncoding encoding)
|
||||
PositionMapper converter(content, encoding);
|
||||
auto& directives = directives_it->second;
|
||||
|
||||
links.reserve(directives.includes.size() + directives.has_includes.size());
|
||||
|
||||
for(const auto& include: directives.includes) {
|
||||
auto [fid, range] = unit.decompose_range(include.filename_range);
|
||||
if(fid != interested || !range.valid()) {
|
||||
continue;
|
||||
// Find the filename argument of a preprocessor directive starting from `offset`.
|
||||
// Creates a Lexer from the line start so that # at start-of-line is detected,
|
||||
// which enables header_name mode for #include and #embed automatically.
|
||||
// For __has_include/__has_embed, manually enables header_name mode after (.
|
||||
auto find_argument_range = [&](std::uint32_t offset) -> std::optional<LocalSourceRange> {
|
||||
std::uint32_t line_start = 0;
|
||||
if(offset > 0) {
|
||||
if(auto nl = content.rfind('\n', offset - 1); nl != llvm::StringRef::npos)
|
||||
line_start = static_cast<std::uint32_t>(nl + 1);
|
||||
}
|
||||
|
||||
protocol::DocumentLink link{
|
||||
.range = to_range(converter, range),
|
||||
};
|
||||
link.target = std::string(unit.file_path(include.fid));
|
||||
auto line = content.substr(line_start);
|
||||
Lexer lexer(line);
|
||||
bool after_has_keyword = false;
|
||||
|
||||
while(true) {
|
||||
auto tok = lexer.advance();
|
||||
if(tok.is_eof() || tok.is_eod())
|
||||
break;
|
||||
|
||||
auto abs_begin = line_start + tok.range.begin;
|
||||
auto abs_end = line_start + tok.range.end;
|
||||
|
||||
// Detect __has_include/__has_embed to enable header_name mode after (.
|
||||
if(tok.is_identifier()) {
|
||||
auto text = tok.text(line);
|
||||
if(text == "__has_include" || text == "__has_include_next" ||
|
||||
text == "__has_embed") {
|
||||
after_has_keyword = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if(tok.kind == clang::tok::l_paren && after_has_keyword) {
|
||||
after_has_keyword = false;
|
||||
lexer.set_header_name_mode();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Only return tokens at or after the directive's starting offset.
|
||||
if(abs_begin < offset)
|
||||
continue;
|
||||
|
||||
if(tok.is_header_name() || tok.kind == clang::tok::string_literal)
|
||||
return LocalSourceRange(abs_begin, abs_end);
|
||||
|
||||
if(tok.is_identifier() && !is_directive_keyword(tok.text(line)))
|
||||
return LocalSourceRange(abs_begin, abs_end);
|
||||
}
|
||||
return std::nullopt;
|
||||
};
|
||||
|
||||
auto add_link = [&](clang::SourceLocation loc, llvm::StringRef target) {
|
||||
auto [fid, offset] = unit.decompose_location(loc);
|
||||
if(fid != interested || offset >= content.size())
|
||||
return;
|
||||
auto range = find_argument_range(offset);
|
||||
if(!range)
|
||||
return;
|
||||
protocol::DocumentLink link{.range = to_range(converter, *range)};
|
||||
link.target = target.str();
|
||||
links.push_back(std::move(link));
|
||||
};
|
||||
|
||||
for(const auto& include: directives.includes) {
|
||||
if(include.fid.isValid()) {
|
||||
add_link(include.location, unit.file_path(include.fid));
|
||||
}
|
||||
}
|
||||
|
||||
for(const auto& has_include: directives.has_includes) {
|
||||
if(has_include.fid.isInvalid()) {
|
||||
continue;
|
||||
if(has_include.fid.isValid()) {
|
||||
add_link(has_include.location, unit.file_path(has_include.fid));
|
||||
}
|
||||
}
|
||||
|
||||
auto [fid, offset] = unit.decompose_location(has_include.location);
|
||||
if(fid != interested || offset >= content.size()) {
|
||||
continue;
|
||||
for(const auto& embed: directives.embeds) {
|
||||
if(embed.file) {
|
||||
add_link(embed.loc, embed.file->getName());
|
||||
}
|
||||
}
|
||||
|
||||
auto tail = content.substr(offset);
|
||||
char open = tail.front();
|
||||
if(open != '<' && open != '"') {
|
||||
continue;
|
||||
for(const auto& has_embed: directives.has_embeds) {
|
||||
if(has_embed.file) {
|
||||
add_link(has_embed.loc, has_embed.file->getName());
|
||||
}
|
||||
|
||||
char close = open == '<' ? '>' : '"';
|
||||
auto close_index = tail.find(close, 1);
|
||||
if(close_index == llvm::StringRef::npos) {
|
||||
continue;
|
||||
}
|
||||
|
||||
LocalSourceRange range(offset, offset + static_cast<std::uint32_t>(close_index + 1));
|
||||
protocol::DocumentLink link{
|
||||
.range = to_range(converter, range),
|
||||
};
|
||||
link.target = std::string(unit.file_path(has_include.fid));
|
||||
links.push_back(std::move(link));
|
||||
}
|
||||
|
||||
return links;
|
||||
|
||||
@@ -23,12 +23,8 @@ struct RawToken {
|
||||
std::uint32_t modifiers = 0;
|
||||
};
|
||||
|
||||
constexpr std::uint32_t bit(SymbolModifiers::Kind kind) {
|
||||
return static_cast<std::uint32_t>(kind);
|
||||
}
|
||||
|
||||
void add_modifier(std::uint32_t& modifiers, SymbolModifiers::Kind kind) {
|
||||
modifiers |= bit(kind);
|
||||
modifiers |= SymbolModifiers::to_mask(kind);
|
||||
}
|
||||
|
||||
auto type_index(SymbolKind kind) -> std::uint32_t {
|
||||
@@ -39,6 +35,132 @@ auto encode_modifiers(std::uint32_t modifiers) -> std::uint32_t {
|
||||
return modifiers;
|
||||
}
|
||||
|
||||
bool is_dependent(const clang::Decl* D) {
|
||||
return isa<clang::UnresolvedUsingValueDecl>(D);
|
||||
}
|
||||
|
||||
/// Returns true if `decl` is considered to be from a default/system library.
|
||||
/// This currently checks the systemness of the file by include type, although
|
||||
/// different heuristics may be used in the future (e.g. sysroot paths).
|
||||
bool is_default_library(const clang::Decl* decl) {
|
||||
clang::SourceLocation location = decl->getLocation();
|
||||
if(!location.isValid()) {
|
||||
return false;
|
||||
}
|
||||
return decl->getASTContext().getSourceManager().isInSystemHeader(location);
|
||||
}
|
||||
|
||||
// "Static" means many things in C++, only some get the "static" modifier.
|
||||
//
|
||||
// Meanings that do:
|
||||
// - Members associated with the class rather than the instance.
|
||||
// This is what 'static' most often means across languages.
|
||||
// - static local variables
|
||||
// These are similarly "detached from their context" by the static keyword.
|
||||
// In practice, these are rarely used inside classes, reducing confusion.
|
||||
//
|
||||
// Meanings that don't:
|
||||
// - Namespace-scoped variables, which have static storage class.
|
||||
// This is implicit, so the keyword "static" isn't so strongly associated.
|
||||
// If we want a modifier for these, "global scope" is probably the concept.
|
||||
// - Namespace-scoped variables/functions explicitly marked "static".
|
||||
// There the keyword changes *linkage* , which is a totally different concept.
|
||||
// If we want to model this, "file scope" would be a nice modifier.
|
||||
//
|
||||
// This is confusing, and maybe we should use another name, but because "static"
|
||||
// is a standard LSP modifier, having one with that name has advantages.
|
||||
bool is_static(const clang::Decl* decl) {
|
||||
if(const auto* method = llvm::dyn_cast<clang::CXXMethodDecl>(decl)) {
|
||||
return method->isStatic();
|
||||
}
|
||||
if(const auto* var_decl = llvm::dyn_cast<clang::VarDecl>(decl)) {
|
||||
return var_decl->isStaticDataMember() || var_decl->isStaticLocal();
|
||||
}
|
||||
if(const auto* objc_property = llvm::dyn_cast<clang::ObjCPropertyDecl>(decl)) {
|
||||
return objc_property->isClassProperty();
|
||||
}
|
||||
if(const auto* objc_method = llvm::dyn_cast<clang::ObjCMethodDecl>(decl)) {
|
||||
return objc_method->isClassMethod();
|
||||
}
|
||||
if(const auto* function = llvm::dyn_cast<clang::FunctionDecl>(decl)) {
|
||||
return function->isStatic();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Whether `type` is const in a loose sense: would a value of this type be readonly?
|
||||
bool is_const(clang::QualType type) {
|
||||
if(type.isNull()) {
|
||||
return false;
|
||||
}
|
||||
type = type.getNonReferenceType();
|
||||
if(type.isConstQualified()) {
|
||||
return true;
|
||||
}
|
||||
if(const auto* array_type = type->getAsArrayTypeUnsafe()) {
|
||||
return is_const(array_type->getElementType());
|
||||
}
|
||||
if(is_const(type->getPointeeType())) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Whether `decl` is const in a loose sense (should it be highlighted as such?)
|
||||
// FIXME: This is separate from whether a particular usage can mutate `decl`.
|
||||
// We may want a receiver in `value.size()` to be readonly even if `value` is mutable.
|
||||
bool is_const(const clang::Decl* decl) {
|
||||
if(llvm::isa<clang::EnumConstantDecl>(decl) ||
|
||||
llvm::isa<clang::NonTypeTemplateParmDecl>(decl)) {
|
||||
return true;
|
||||
}
|
||||
if(llvm::isa<clang::FieldDecl>(decl) || llvm::isa<clang::VarDecl>(decl) ||
|
||||
llvm::isa<clang::MSPropertyDecl>(decl) || llvm::isa<clang::BindingDecl>(decl)) {
|
||||
if(is_const(llvm::cast<clang::ValueDecl>(decl)->getType())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if(const auto* objc_property = llvm::dyn_cast<clang::ObjCPropertyDecl>(decl)) {
|
||||
if(objc_property->isReadOnly()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if(const auto* ms_property = llvm::dyn_cast<clang::MSPropertyDecl>(decl)) {
|
||||
if(!ms_property->hasSetter()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if(const auto* method = llvm::dyn_cast<clang::CXXMethodDecl>(decl)) {
|
||||
if(method->isConst()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if(const auto* function = llvm::dyn_cast<clang::FunctionDecl>(decl)) {
|
||||
return is_const(function->getReturnType());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Indicates whether declaration `decl` is abstract in cases where it is a struct or a
|
||||
// class.
|
||||
bool is_abstract(const clang::Decl* decl) {
|
||||
if(const auto* method = llvm::dyn_cast<clang::CXXMethodDecl>(decl)) {
|
||||
return method->isPureVirtual();
|
||||
}
|
||||
if(const auto* record = llvm::dyn_cast<clang::CXXRecordDecl>(decl)) {
|
||||
return record->hasDefinition() && record->isAbstract();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Indicates whether declaration `decl` is virtual in cases where it is a method.
|
||||
bool is_virtual(const clang::Decl* decl) {
|
||||
if(const auto* method = llvm::dyn_cast<clang::CXXMethodDecl>(decl)) {
|
||||
return method->isVirtual();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
class SemanticTokensCollector : public SemanticVisitor<SemanticTokensCollector> {
|
||||
public:
|
||||
explicit SemanticTokensCollector(CompilationUnitRef unit) : SemanticVisitor(unit, true) {}
|
||||
@@ -55,6 +177,8 @@ public:
|
||||
clang::SourceLocation location) {
|
||||
std::uint32_t modifiers = 0;
|
||||
if(relation.is_one_of(RelationKind::Definition)) {
|
||||
// todo: clangd add both Declaration and Definition modifiers for definitions.
|
||||
// add_modifier(modifiers, SymbolModifiers::Declaration);
|
||||
add_modifier(modifiers, SymbolModifiers::Definition);
|
||||
} else if(relation.is_one_of(RelationKind::Declaration)) {
|
||||
add_modifier(modifiers, SymbolModifiers::Declaration);
|
||||
@@ -63,6 +187,42 @@ public:
|
||||
if(ast::is_templated(decl)) {
|
||||
add_modifier(modifiers, SymbolModifiers::Templated);
|
||||
}
|
||||
// Apply attribute-style modifiers to the underlying declaration.
|
||||
// The attribute tests don't want to look at the template.
|
||||
if(const auto* template_decl = llvm::dyn_cast<clang::TemplateDecl>(decl)) {
|
||||
if(const auto* templated_decl = template_decl->getTemplatedDecl())
|
||||
decl = templated_decl;
|
||||
}
|
||||
|
||||
// TODO: add scope-based modifiers once the local model supports them.
|
||||
// if (auto Mod = scopeModifier(Decl))
|
||||
// Tok.addModifier(*Mod);
|
||||
|
||||
if(is_const(decl)) {
|
||||
add_modifier(modifiers, SymbolModifiers::Readonly);
|
||||
}
|
||||
if(is_static(decl)) {
|
||||
add_modifier(modifiers, SymbolModifiers::Static);
|
||||
}
|
||||
if(is_abstract(decl)) {
|
||||
add_modifier(modifiers, SymbolModifiers::Abstract);
|
||||
}
|
||||
if(is_virtual(decl)) {
|
||||
add_modifier(modifiers, SymbolModifiers::Virtual);
|
||||
}
|
||||
if(is_default_library(decl)) {
|
||||
add_modifier(modifiers, SymbolModifiers::DefaultLibrary);
|
||||
}
|
||||
if(decl->isDeprecated()) {
|
||||
add_modifier(modifiers, SymbolModifiers::Deprecated);
|
||||
}
|
||||
if(is_dependent(decl)) {
|
||||
add_modifier(modifiers, SymbolModifiers::DependentName);
|
||||
}
|
||||
if(llvm::isa<clang::CXXConstructorDecl>(decl) ||
|
||||
llvm::isa<clang::CXXDestructorDecl>(decl)) {
|
||||
add_modifier(modifiers, SymbolModifiers::ConstructorOrDestructor);
|
||||
}
|
||||
|
||||
add_token(location, SymbolKind::from(decl), modifiers);
|
||||
}
|
||||
@@ -80,6 +240,10 @@ public:
|
||||
add_token(location, SymbolKind::Macro, modifiers);
|
||||
}
|
||||
|
||||
// handleModuleOccurrence
|
||||
|
||||
// handleRelation
|
||||
|
||||
void handleAttrOccurrence(const clang::Attr* attr, clang::SourceRange range) {
|
||||
auto [begin, end] = range;
|
||||
if(llvm::isa<clang::FinalAttr, clang::OverrideAttr>(attr)) {
|
||||
|
||||
@@ -14,8 +14,8 @@ namespace {
|
||||
|
||||
class Builder : public SemanticVisitor<Builder> {
|
||||
public:
|
||||
Builder(TUIndex& result, CompilationUnitRef unit) :
|
||||
SemanticVisitor<Builder>(unit, false), result(result) {
|
||||
Builder(TUIndex& result, CompilationUnitRef unit, bool interested_only) :
|
||||
SemanticVisitor<Builder>(unit, interested_only), result(result) {
|
||||
result.graph = IncludeGraph::from(unit);
|
||||
}
|
||||
|
||||
@@ -188,11 +188,11 @@ std::array<std::uint8_t, 32> FileIndex::hash() {
|
||||
return hasher.final();
|
||||
}
|
||||
|
||||
TUIndex TUIndex::build(CompilationUnitRef unit) {
|
||||
TUIndex TUIndex::build(CompilationUnitRef unit, bool interested_only) {
|
||||
TUIndex index;
|
||||
index.built_at = unit.build_at();
|
||||
|
||||
Builder builder(index, unit);
|
||||
Builder builder(index, unit, interested_only);
|
||||
builder.build();
|
||||
|
||||
return index;
|
||||
|
||||
@@ -85,7 +85,7 @@ struct TUIndex {
|
||||
|
||||
FileIndex main_file_index;
|
||||
|
||||
static TUIndex build(CompilationUnitRef unit);
|
||||
static TUIndex build(CompilationUnitRef unit, bool interested_only = false);
|
||||
|
||||
void serialize(llvm::raw_ostream& os) const;
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,9 +17,12 @@ namespace clice {
|
||||
/// completion, you cannot get go-to-definition, etc. To avoid this, we just use
|
||||
/// some heuristics to simplify the dependent names as normal type/expression.
|
||||
/// For example, `std::vector<T>::value_type` can be simplified as `T`.
|
||||
///
|
||||
/// Thread safety: NOT thread-safe. Each compilation unit should have its own resolver.
|
||||
/// The `resolved` cache persists across multiple resolve() calls on the same unit.
|
||||
class TemplateResolver {
|
||||
public:
|
||||
TemplateResolver(clang::Sema& sema) : sema(sema) {}
|
||||
explicit TemplateResolver(clang::Sema& sema) : sema(sema) {}
|
||||
|
||||
clang::QualType resolve(clang::QualType type);
|
||||
|
||||
@@ -27,7 +30,7 @@ public:
|
||||
|
||||
void resolve(clang::UnresolvedLookupExpr* expr);
|
||||
|
||||
// TODO: use a relative clear way to resolve `UnresolvedLookupExpr`.
|
||||
// TODO: Use a clearer approach for resolving UnresolvedLookupExpr.
|
||||
|
||||
void resolve(clang::UnresolvedUsingType* type);
|
||||
|
||||
@@ -50,7 +53,7 @@ public:
|
||||
if(identifier) {
|
||||
return lookup(template_name.getQualifier(), identifier);
|
||||
} else {
|
||||
/// FIXME: Operators does't have a name.
|
||||
/// TODO: Operators don't have an IdentifierInfo; need DeclarationName-based lookup.
|
||||
return {};
|
||||
}
|
||||
}
|
||||
@@ -60,7 +63,7 @@ public:
|
||||
}
|
||||
|
||||
lookup_result lookup(const clang::UnresolvedLookupExpr* expr) {
|
||||
/// FIXME:
|
||||
/// TODO: Only returns the first TemplateDecl; should handle overloaded lookups.
|
||||
for(auto decl: expr->decls()) {
|
||||
if(auto TD = llvm::dyn_cast<clang::TemplateDecl>(decl)) {
|
||||
return lookup_result(TD);
|
||||
@@ -74,8 +77,8 @@ public:
|
||||
return {};
|
||||
}
|
||||
|
||||
/// TODO:
|
||||
lookup_result lookup(clang::CXXDependentScopeMemberExpr* expr) {
|
||||
/// TODO: Implement dependent member expression lookup (e.g. `x.template foo<T>()`).
|
||||
lookup_result lookup(const clang::CXXDependentScopeMemberExpr* expr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -83,16 +86,19 @@ public:
|
||||
return lookup(decl->getQualifier(), decl->getDeclName());
|
||||
}
|
||||
|
||||
lookup_result resolve(const clang::UnresolvedUsingTypenameDecl* decl) {
|
||||
lookup_result lookup(const clang::UnresolvedUsingTypenameDecl* decl) {
|
||||
return lookup(decl->getQualifier(), decl->getDeclName());
|
||||
}
|
||||
|
||||
#ifndef NDEBUG
|
||||
static inline bool debug = false;
|
||||
#endif
|
||||
|
||||
private:
|
||||
clang::Sema& sema;
|
||||
|
||||
/// Cache of resolved dependent types, keyed by AST node pointer.
|
||||
/// Shared across resolve() calls within the same TU for performance.
|
||||
/// This is safe because a given AST node (DependentNameType*, etc.) has a
|
||||
/// unique identity within the TU — the same pointer always refers to the same
|
||||
/// syntactic occurrence. Different syntactic occurrences of the "same" type
|
||||
/// have different AST node pointers.
|
||||
llvm::DenseMap<const void*, clang::QualType> resolved;
|
||||
};
|
||||
|
||||
|
||||
@@ -80,27 +80,79 @@ private:
|
||||
struct SymbolModifiers {
|
||||
enum Kind : std::uint32_t {
|
||||
/// Represents that the symbol is a declaration(e.g. function declaration).
|
||||
Declaration = 1u << 0,
|
||||
Declaration = 0,
|
||||
|
||||
/// Represents that the symbol is a definition(e.g. function definition).
|
||||
Definition = 1u << 1,
|
||||
Definition = 1,
|
||||
|
||||
/// Represents that the symbol is const modified(e.g. `const` variable).
|
||||
Const = 1u << 2,
|
||||
Const = 2,
|
||||
|
||||
/// Represents that the symbol is overloaded(e.g. overloaded functions and operators).
|
||||
Overloaded = 1u << 3,
|
||||
Overloaded = 3,
|
||||
|
||||
/// Represents that the symbol is a part of type(e.g. `*` in `int*`).
|
||||
Typed = 1u << 4,
|
||||
Typed = 4,
|
||||
|
||||
/// Represents that the symbol is a template(e.g. class template or function template).
|
||||
Templated = 1u << 5,
|
||||
Templated = 5,
|
||||
|
||||
/// Represents that the symbol is deprecated.
|
||||
Deprecated = 6,
|
||||
|
||||
/// Represents that the symbol is deduced.
|
||||
Deduced = 7,
|
||||
|
||||
/// Represents that the symbol is readonly.
|
||||
Readonly = 8,
|
||||
|
||||
/// Represents that the symbol is static.
|
||||
Static = 9,
|
||||
|
||||
/// Represents that the symbol is abstract.
|
||||
Abstract = 10,
|
||||
|
||||
/// Represents that the symbol is virtual.
|
||||
Virtual = 11,
|
||||
|
||||
/// Represents that the symbol is a dependent name.
|
||||
DependentName = 12,
|
||||
|
||||
/// Represents that the symbol comes from the default library.
|
||||
DefaultLibrary = 13,
|
||||
|
||||
/// Represents that the symbol is used through a mutable reference.
|
||||
UsedAsMutableReference = 14,
|
||||
|
||||
/// Represents that the symbol is used through a mutable pointer.
|
||||
UsedAsMutablePointer = 15,
|
||||
|
||||
/// Represents that the symbol is a constructor or destructor.
|
||||
ConstructorOrDestructor = 16,
|
||||
|
||||
/// Represents that the symbol is user-defined.
|
||||
UserDefined = 17,
|
||||
|
||||
/// Represents that the symbol is function-scoped.
|
||||
FunctionScope = 18,
|
||||
|
||||
/// Represents that the symbol is class-scoped.
|
||||
ClassScope = 19,
|
||||
|
||||
/// Represents that the symbol is file-scoped.
|
||||
FileScope = 20,
|
||||
|
||||
/// Represents that the symbol is global-scoped.
|
||||
GlobalScope = 21,
|
||||
};
|
||||
|
||||
constexpr static std::uint32_t to_mask(Kind kind) {
|
||||
return std::uint32_t(1) << static_cast<std::uint32_t>(kind);
|
||||
}
|
||||
|
||||
constexpr SymbolModifiers() = default;
|
||||
|
||||
constexpr SymbolModifiers(Kind kind) : value(static_cast<std::uint32_t>(kind)) {}
|
||||
constexpr SymbolModifiers(Kind kind) : value(to_mask(kind)) {}
|
||||
|
||||
constexpr explicit SymbolModifiers(std::uint32_t bits) : value(bits) {}
|
||||
|
||||
@@ -109,7 +161,7 @@ struct SymbolModifiers {
|
||||
}
|
||||
|
||||
constexpr bool contains(Kind kind) const {
|
||||
return (value & static_cast<std::uint32_t>(kind)) != 0;
|
||||
return (value & to_mask(kind)) != 0;
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
|
||||
namespace clice {
|
||||
|
||||
namespace ranges = std::ranges;
|
||||
|
||||
CompileGraph::CompileGraph(dispatch_fn dispatch, resolve_fn resolve) :
|
||||
dispatch(std::move(dispatch)), resolve(std::move(resolve)) {}
|
||||
|
||||
@@ -31,13 +33,19 @@ void CompileGraph::ensure_resolved(std::uint32_t path_id) {
|
||||
}
|
||||
}
|
||||
|
||||
et::task<bool> CompileGraph::compile_deps(std::uint32_t path_id) {
|
||||
llvm::DenseSet<std::uint32_t> ancestors;
|
||||
co_return co_await compile_impl(path_id, ancestors, false);
|
||||
}
|
||||
|
||||
et::task<bool> CompileGraph::compile(std::uint32_t path_id) {
|
||||
llvm::DenseSet<std::uint32_t> ancestors;
|
||||
co_return co_await compile_impl(path_id, ancestors);
|
||||
}
|
||||
|
||||
et::task<bool> CompileGraph::compile_impl(std::uint32_t path_id,
|
||||
llvm::DenseSet<std::uint32_t> ancestors) {
|
||||
llvm::DenseSet<std::uint32_t> ancestors,
|
||||
bool dispatch_self) {
|
||||
ensure_resolved(path_id);
|
||||
|
||||
// Cycle detection: if this unit is already in the compile chain, bail out.
|
||||
@@ -48,6 +56,27 @@ et::task<bool> CompileGraph::compile_impl(std::uint32_t path_id,
|
||||
// Re-lookup after ensure_resolved may have mutated the map.
|
||||
auto it = units.find(path_id);
|
||||
|
||||
// For deps-only mode, compile dependencies concurrently and return.
|
||||
if(!dispatch_self) {
|
||||
auto deps = it->second.dependencies;
|
||||
if(deps.empty()) {
|
||||
co_return true;
|
||||
}
|
||||
|
||||
std::vector<et::task<bool>> dep_tasks;
|
||||
dep_tasks.reserve(deps.size());
|
||||
for(auto dep_id: deps) {
|
||||
dep_tasks.push_back(compile_impl(dep_id, ancestors));
|
||||
}
|
||||
auto results = co_await et::when_all(std::move(dep_tasks));
|
||||
for(auto ok: results) {
|
||||
if(!ok) {
|
||||
co_return false;
|
||||
}
|
||||
}
|
||||
co_return true;
|
||||
}
|
||||
|
||||
// Already clean.
|
||||
if(!it->second.dirty) {
|
||||
co_return true;
|
||||
@@ -64,10 +93,17 @@ et::task<bool> CompileGraph::compile_impl(std::uint32_t path_id,
|
||||
co_return !units.find(path_id)->second.dirty;
|
||||
}
|
||||
|
||||
// Begin compilation.
|
||||
// Begin compilation. The finish lambda ensures compiling/completion state
|
||||
// is always cleaned up, regardless of how the function exits.
|
||||
it->second.compiling = true;
|
||||
it->second.completion = std::make_unique<et::event>();
|
||||
|
||||
auto finish = [&, path_id] {
|
||||
auto& u = units.find(path_id)->second;
|
||||
u.compiling = false;
|
||||
u.completion->set();
|
||||
};
|
||||
|
||||
// Copy deps and capture generation before co_await (DenseMap iterator safety).
|
||||
auto deps = it->second.dependencies;
|
||||
auto gen = it->second.generation;
|
||||
@@ -85,52 +121,41 @@ et::task<bool> CompileGraph::compile_impl(std::uint32_t path_id,
|
||||
|
||||
auto results = co_await et::when_all(std::move(dep_tasks));
|
||||
|
||||
auto& u = units.find(path_id)->second;
|
||||
if(results.is_cancelled()) {
|
||||
u.compiling = false;
|
||||
u.completion->set();
|
||||
finish();
|
||||
co_await et::cancel();
|
||||
}
|
||||
|
||||
for(auto ok: *results) {
|
||||
if(!ok) {
|
||||
u.compiling = false;
|
||||
u.completion->set();
|
||||
finish();
|
||||
co_return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dispatch the actual compilation, cancellable via the pre-captured token.
|
||||
// Using the token captured before co_await ensures cancellation propagates
|
||||
// correctly even if update() replaces the source during dependency compilation.
|
||||
{
|
||||
auto result = co_await et::with_token(dispatch(path_id), token);
|
||||
auto result = co_await et::with_token(dispatch(path_id), token);
|
||||
|
||||
auto& u = units.find(path_id)->second;
|
||||
if(!result.has_value()) {
|
||||
u.compiling = false;
|
||||
u.completion->set();
|
||||
co_await et::cancel();
|
||||
}
|
||||
if(!*result) {
|
||||
u.compiling = false;
|
||||
u.completion->set();
|
||||
co_return false;
|
||||
}
|
||||
if(!result.has_value()) {
|
||||
finish();
|
||||
co_await et::cancel();
|
||||
}
|
||||
|
||||
if(!*result) {
|
||||
finish();
|
||||
co_return false;
|
||||
}
|
||||
|
||||
// Success — only clear dirty if update() hasn't bumped the generation.
|
||||
auto& final_unit = units.find(path_id)->second;
|
||||
if(final_unit.generation != gen) {
|
||||
// update() was called while dispatch was in flight.
|
||||
final_unit.compiling = false;
|
||||
final_unit.completion->set();
|
||||
finish();
|
||||
co_return false;
|
||||
}
|
||||
|
||||
final_unit.dirty = false;
|
||||
final_unit.compiling = false;
|
||||
final_unit.completion->set();
|
||||
finish();
|
||||
co_return true;
|
||||
}
|
||||
|
||||
@@ -165,8 +190,7 @@ llvm::SmallVector<std::uint32_t> CompileGraph::update(std::uint32_t path_id) {
|
||||
auto dep_it = units.find(dep_id);
|
||||
if(dep_it != units.end()) {
|
||||
auto& dependents = dep_it->second.dependents;
|
||||
dependents.erase(std::remove(dependents.begin(), dependents.end(), path_id),
|
||||
dependents.end());
|
||||
dependents.erase(ranges::remove(dependents, path_id).begin(), dependents.end());
|
||||
}
|
||||
}
|
||||
unit.dependencies.clear();
|
||||
|
||||
@@ -50,6 +50,10 @@ public:
|
||||
/// Compile a unit and all its transitive dependencies.
|
||||
et::task<bool> compile(std::uint32_t path_id);
|
||||
|
||||
/// Compile all transitive module dependencies of path_id, but NOT path_id itself.
|
||||
/// Used for non-module files (plain .cpp) that import modules.
|
||||
et::task<bool> compile_deps(std::uint32_t path_id);
|
||||
|
||||
/// Mark path_id and all transitive dependents as dirty,
|
||||
/// cancelling any in-progress compilations.
|
||||
/// Returns the set of all path_ids that were marked dirty.
|
||||
@@ -66,7 +70,9 @@ private:
|
||||
void ensure_resolved(std::uint32_t path_id);
|
||||
|
||||
/// Internal compile with ancestor tracking for cycle detection.
|
||||
et::task<bool> compile_impl(std::uint32_t path_id, llvm::DenseSet<std::uint32_t> ancestors);
|
||||
et::task<bool> compile_impl(std::uint32_t path_id,
|
||||
llvm::DenseSet<std::uint32_t> ancestors,
|
||||
bool dispatch_self = true);
|
||||
|
||||
/// Check if waiting on `target` would deadlock given our `ancestors` chain.
|
||||
/// Walks the dependency graph through compiling units to see if any dep
|
||||
|
||||
920
src/server/compiler.cpp
Normal file
920
src/server/compiler.cpp
Normal file
@@ -0,0 +1,920 @@
|
||||
#include "server/compiler.h"
|
||||
|
||||
#include <format>
|
||||
#include <ranges>
|
||||
#include <string>
|
||||
|
||||
#include "command/search_config.h"
|
||||
#include "eventide/ipc/lsp/position.h"
|
||||
#include "eventide/ipc/lsp/uri.h"
|
||||
#include "eventide/serde/json/json.h"
|
||||
#include "index/tu_index.h"
|
||||
#include "server/protocol.h"
|
||||
#include "support/filesystem.h"
|
||||
#include "support/logging.h"
|
||||
#include "syntax/include_resolver.h"
|
||||
#include "syntax/scan.h"
|
||||
|
||||
#include "llvm/Support/FileSystem.h"
|
||||
#include "llvm/Support/MemoryBuffer.h"
|
||||
#include "llvm/Support/Path.h"
|
||||
#include "llvm/Support/xxhash.h"
|
||||
|
||||
namespace clice {
|
||||
|
||||
namespace lsp = eventide::ipc::lsp;
|
||||
using serde_raw = et::serde::RawValue;
|
||||
|
||||
/// Detect whether the cursor is inside a preamble directive (include/import).
|
||||
|
||||
Compiler::Compiler(et::event_loop& loop,
|
||||
et::ipc::JsonPeer& peer,
|
||||
Workspace& workspace,
|
||||
WorkerPool& pool,
|
||||
llvm::DenseMap<std::uint32_t, Session>& sessions) :
|
||||
loop(loop), peer(peer), workspace(workspace), pool(pool), sessions(sessions) {}
|
||||
|
||||
Compiler::~Compiler() {
|
||||
workspace.cancel_all();
|
||||
}
|
||||
|
||||
void Compiler::init_compile_graph() {
|
||||
if(workspace.path_to_module.empty()) {
|
||||
LOG_INFO("No C++20 modules detected, skipping CompileGraph");
|
||||
return;
|
||||
}
|
||||
|
||||
// Lazy dependency resolver: scans a module file on demand to discover imports.
|
||||
auto resolve = [this](std::uint32_t path_id) -> llvm::SmallVector<std::uint32_t> {
|
||||
auto file_path = workspace.path_pool.resolve(path_id);
|
||||
auto results =
|
||||
workspace.cdb.lookup(file_path, {.query_toolchain = true, .suppress_logging = true});
|
||||
if(results.empty())
|
||||
return {};
|
||||
|
||||
auto& cmd = results[0];
|
||||
auto scan_result = scan_precise(cmd.to_argv(), cmd.resolved.directory);
|
||||
|
||||
llvm::SmallVector<std::uint32_t> deps;
|
||||
for(auto& mod_name: scan_result.modules) {
|
||||
auto mod_ids = workspace.dep_graph.lookup_module(mod_name);
|
||||
if(!mod_ids.empty()) {
|
||||
deps.push_back(mod_ids[0]);
|
||||
}
|
||||
}
|
||||
|
||||
// Module implementation units implicitly depend on their interface unit.
|
||||
if(!scan_result.module_name.empty() && !scan_result.is_interface_unit) {
|
||||
auto mod_ids = workspace.dep_graph.lookup_module(scan_result.module_name);
|
||||
if(!mod_ids.empty()) {
|
||||
deps.push_back(mod_ids[0]);
|
||||
}
|
||||
}
|
||||
|
||||
return deps;
|
||||
};
|
||||
|
||||
// Dispatch: sends BuildPCM request to a stateless worker.
|
||||
auto dispatch = [this](std::uint32_t path_id) -> et::task<bool> {
|
||||
auto mod_it = workspace.path_to_module.find(path_id);
|
||||
if(mod_it == workspace.path_to_module.end())
|
||||
co_return false;
|
||||
|
||||
auto file_path = std::string(workspace.path_pool.resolve(path_id));
|
||||
|
||||
worker::BuildParams bp;
|
||||
bp.kind = worker::BuildKind::BuildPCM;
|
||||
bp.file = file_path;
|
||||
if(!fill_compile_args(file_path, bp.directory, bp.arguments))
|
||||
co_return false;
|
||||
|
||||
// Compute deterministic content-addressed PCM path.
|
||||
auto safe_module_name = mod_it->second;
|
||||
std::ranges::replace(safe_module_name, ':', '-');
|
||||
std::string hash_input = file_path;
|
||||
for(auto& arg: bp.arguments) {
|
||||
hash_input += arg;
|
||||
}
|
||||
auto args_hash = llvm::xxh3_64bits(llvm::StringRef(hash_input));
|
||||
auto pcm_filename = std::format("{}-{:016x}.pcm", safe_module_name, args_hash);
|
||||
auto pcm_path = path::join(workspace.config.cache_dir, "cache", "pcm", pcm_filename);
|
||||
|
||||
// Check if cached PCM is still valid.
|
||||
if(auto pcm_it = workspace.pcm_cache.find(path_id); pcm_it != workspace.pcm_cache.end()) {
|
||||
if(!pcm_it->second.path.empty() && llvm::sys::fs::exists(pcm_it->second.path) &&
|
||||
!deps_changed(workspace.path_pool, pcm_it->second.deps)) {
|
||||
workspace.pcm_paths[path_id] = pcm_it->second.path;
|
||||
co_return true;
|
||||
}
|
||||
}
|
||||
|
||||
bp.module_name = mod_it->second;
|
||||
bp.output_path = pcm_path;
|
||||
|
||||
// Clang needs ALL transitive PCM deps, not just direct imports.
|
||||
workspace.fill_pcm_deps(bp.pcms);
|
||||
|
||||
auto result = co_await pool.send_stateless(bp);
|
||||
if(!result.has_value() || !result.value().success) {
|
||||
LOG_WARN("BuildPCM failed for module {}: {}",
|
||||
mod_it->second,
|
||||
result.has_value() ? result.value().error : result.error().message);
|
||||
co_return false;
|
||||
}
|
||||
|
||||
workspace.pcm_paths[path_id] = result.value().output_path;
|
||||
workspace.pcm_cache[path_id] = {
|
||||
result.value().output_path,
|
||||
capture_deps_snapshot(workspace.path_pool, result.value().deps)};
|
||||
LOG_INFO("Built PCM for module {}: {}", mod_it->second, result.value().output_path);
|
||||
|
||||
// Persist cache metadata after successful build.
|
||||
workspace.save_cache();
|
||||
|
||||
// Signal that new index data is available for background merge.
|
||||
if(on_indexing_needed)
|
||||
on_indexing_needed();
|
||||
|
||||
co_return true;
|
||||
};
|
||||
|
||||
workspace.compile_graph =
|
||||
std::make_unique<CompileGraph>(std::move(dispatch), std::move(resolve));
|
||||
LOG_INFO("CompileGraph initialized with {} module(s)", workspace.path_to_module.size());
|
||||
}
|
||||
|
||||
bool Compiler::fill_compile_args(llvm::StringRef path,
|
||||
std::string& directory,
|
||||
std::vector<std::string>& arguments,
|
||||
Session* session) {
|
||||
auto path_id = workspace.path_pool.intern(path);
|
||||
|
||||
// 1. If the session has an active header context via switchContext,
|
||||
// use the host source's CDB entry with file path replaced and preamble injected.
|
||||
if(session && session->active_context.has_value()) {
|
||||
return fill_header_context_args(path, path_id, directory, arguments, session);
|
||||
}
|
||||
|
||||
// 2. Normal CDB lookup for the file itself.
|
||||
auto results = workspace.cdb.lookup(path, {.query_toolchain = true});
|
||||
if(!results.empty()) {
|
||||
auto& cmd = results.front();
|
||||
directory = cmd.resolved.directory.str();
|
||||
arguments = cmd.to_string_argv();
|
||||
return true;
|
||||
}
|
||||
|
||||
// 3. No CDB entry — try automatic header context resolution.
|
||||
return fill_header_context_args(path, path_id, directory, arguments, session);
|
||||
}
|
||||
|
||||
bool Compiler::fill_header_context_args(llvm::StringRef path,
|
||||
std::uint32_t path_id,
|
||||
std::string& directory,
|
||||
std::vector<std::string>& arguments,
|
||||
Session* session) {
|
||||
// Use cached context if available; otherwise resolve.
|
||||
// If an active context override exists, invalidate cache if it points to
|
||||
// a different host so we re-resolve with the correct one.
|
||||
const HeaderFileContext* ctx_ptr = nullptr;
|
||||
if(session && session->header_context.has_value()) {
|
||||
if(session->active_context.has_value() &&
|
||||
session->header_context->host_path_id != *session->active_context) {
|
||||
session->header_context.reset();
|
||||
} else {
|
||||
ctx_ptr = &*session->header_context;
|
||||
}
|
||||
}
|
||||
if(!ctx_ptr) {
|
||||
auto resolved = resolve_header_context(path_id, session);
|
||||
if(!resolved) {
|
||||
LOG_WARN("No CDB entry and no header context for {}", path);
|
||||
return false;
|
||||
}
|
||||
if(session) {
|
||||
session->header_context = std::move(*resolved);
|
||||
ctx_ptr = &*session->header_context;
|
||||
} else {
|
||||
// Background indexing path — no session to store on.
|
||||
// Use a temporary (caller will use it immediately).
|
||||
// Store in a local and return.
|
||||
static thread_local std::optional<HeaderFileContext> tl_ctx;
|
||||
tl_ctx = std::move(*resolved);
|
||||
ctx_ptr = &*tl_ctx;
|
||||
}
|
||||
}
|
||||
|
||||
auto host_path = workspace.path_pool.resolve(ctx_ptr->host_path_id);
|
||||
auto host_results = workspace.cdb.lookup(host_path, {.query_toolchain = true});
|
||||
if(host_results.empty()) {
|
||||
LOG_WARN("fill_header_context_args: host {} has no CDB entry", host_path);
|
||||
return false;
|
||||
}
|
||||
|
||||
auto& host_cmd = host_results.front();
|
||||
directory = host_cmd.resolved.directory.str();
|
||||
|
||||
// Replace source_file and inject -include preamble into flags directly.
|
||||
CompileCommand header_cmd = host_cmd;
|
||||
header_cmd.source_file = workspace.path_pool.resolve(path_id).data();
|
||||
|
||||
// Inject -include <preamble> into flags: after "-cc1" for cc1, after driver otherwise.
|
||||
std::size_t inject_pos = header_cmd.resolved.is_cc1 ? 2 : 1;
|
||||
header_cmd.resolved.flags.insert(header_cmd.resolved.flags.begin() + inject_pos,
|
||||
ctx_ptr->preamble_path.c_str());
|
||||
header_cmd.resolved.flags.insert(header_cmd.resolved.flags.begin() + inject_pos, "-include");
|
||||
|
||||
arguments = header_cmd.to_string_argv();
|
||||
|
||||
LOG_INFO("fill_compile_args: header context for {} (host={}, preamble={})",
|
||||
path,
|
||||
host_path,
|
||||
ctx_ptr->preamble_path);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::optional<HeaderFileContext> Compiler::resolve_header_context(std::uint32_t header_path_id,
|
||||
Session* session) {
|
||||
// Find source files that transitively include this header.
|
||||
auto hosts = workspace.dep_graph.find_host_sources(header_path_id);
|
||||
if(hosts.empty()) {
|
||||
LOG_DEBUG("resolve_header_context: no host sources for path_id={}", header_path_id);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// If there's an active context override, prefer that host.
|
||||
std::uint32_t host_path_id = 0;
|
||||
std::vector<std::uint32_t> chain;
|
||||
if(session && session->active_context.has_value()) {
|
||||
auto preferred = *session->active_context;
|
||||
auto preferred_path = workspace.path_pool.resolve(preferred);
|
||||
auto results = workspace.cdb.lookup(preferred_path, {.suppress_logging = true});
|
||||
if(!results.empty()) {
|
||||
auto c = workspace.dep_graph.find_include_chain(preferred, header_path_id);
|
||||
if(!c.empty()) {
|
||||
host_path_id = preferred;
|
||||
chain = std::move(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to the first available host that has a CDB entry.
|
||||
if(chain.empty()) {
|
||||
for(auto candidate: hosts) {
|
||||
auto candidate_path = workspace.path_pool.resolve(candidate);
|
||||
auto results = workspace.cdb.lookup(candidate_path, {.suppress_logging = true});
|
||||
if(results.empty())
|
||||
continue;
|
||||
auto c = workspace.dep_graph.find_include_chain(candidate, header_path_id);
|
||||
if(c.empty())
|
||||
continue;
|
||||
host_path_id = candidate;
|
||||
chain = std::move(c);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(chain.empty()) {
|
||||
LOG_DEBUG("resolve_header_context: no usable host with include chain for path_id={}",
|
||||
header_path_id);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Build preamble text: for each file in the chain except the last (target),
|
||||
// append all content up to (but not including) the line that includes the
|
||||
// next file in the chain.
|
||||
std::string preamble;
|
||||
for(std::size_t i = 0; i + 1 < chain.size(); ++i) {
|
||||
auto cur_id = chain[i];
|
||||
auto next_id = chain[i + 1];
|
||||
|
||||
auto cur_path = workspace.path_pool.resolve(cur_id);
|
||||
auto next_path = workspace.path_pool.resolve(next_id);
|
||||
auto next_filename = llvm::sys::path::filename(next_path);
|
||||
|
||||
// Prefer in-memory document text over disk content.
|
||||
// Use the session if this file matches the session's path, otherwise
|
||||
// fall back to disk.
|
||||
std::string content;
|
||||
// Note: we don't have the sessions map here, so we always read from disk
|
||||
// for intermediate chain files. The session parameter only covers the
|
||||
// header file itself (the target), not intermediate files in the chain.
|
||||
auto buf = llvm::MemoryBuffer::getFile(cur_path);
|
||||
if(!buf) {
|
||||
LOG_WARN("resolve_header_context: cannot read {}", cur_path);
|
||||
return std::nullopt;
|
||||
}
|
||||
content = (*buf)->getBuffer().str();
|
||||
|
||||
// Scan line by line for the #include that brings in next_filename.
|
||||
llvm::StringRef content_ref(content);
|
||||
std::size_t line_start = 0;
|
||||
std::size_t include_line_start = std::string::npos;
|
||||
while(line_start <= content_ref.size()) {
|
||||
auto newline_pos = content_ref.find('\n', line_start);
|
||||
auto line_end =
|
||||
(newline_pos == llvm::StringRef::npos) ? content_ref.size() : newline_pos;
|
||||
auto line = content_ref.slice(line_start, line_end).trim();
|
||||
|
||||
if(line.starts_with("#include") || line.starts_with("# include")) {
|
||||
// Extract the filename from the #include directive.
|
||||
// Handles: #include "foo.h", #include <foo.h>, # include "foo.h"
|
||||
auto quote_start = line.find_first_of("\"<");
|
||||
auto quote_end = llvm::StringRef::npos;
|
||||
if(quote_start != llvm::StringRef::npos) {
|
||||
char close = (line[quote_start] == '"') ? '"' : '>';
|
||||
quote_end = line.find(close, quote_start + 1);
|
||||
}
|
||||
if(quote_start != llvm::StringRef::npos && quote_end != llvm::StringRef::npos) {
|
||||
auto included = line.slice(quote_start + 1, quote_end);
|
||||
auto included_filename = llvm::sys::path::filename(included);
|
||||
if(included_filename == next_filename) {
|
||||
include_line_start = line_start;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
line_start =
|
||||
(newline_pos == llvm::StringRef::npos) ? content_ref.size() + 1 : newline_pos + 1;
|
||||
}
|
||||
|
||||
// Emit a #line marker then all content before the include line.
|
||||
preamble += std::format("#line 1 \"{}\"\n", cur_path.str());
|
||||
if(include_line_start != std::string::npos) {
|
||||
preamble += content_ref.substr(0, include_line_start).str();
|
||||
} else {
|
||||
// No matching include line found — emit the whole file to be safe.
|
||||
LOG_DEBUG("resolve_header_context: include line for {} not found in {}, emitting full",
|
||||
next_filename,
|
||||
cur_path);
|
||||
preamble += content;
|
||||
}
|
||||
}
|
||||
|
||||
// Hash the preamble and write to cache directory.
|
||||
auto preamble_hash = llvm::xxh3_64bits(llvm::StringRef(preamble));
|
||||
auto preamble_filename = std::format("{:016x}.h", preamble_hash);
|
||||
auto preamble_dir = path::join(workspace.config.cache_dir, "header_context");
|
||||
auto preamble_path = path::join(preamble_dir, preamble_filename);
|
||||
|
||||
if(!llvm::sys::fs::exists(preamble_path)) {
|
||||
auto ec = llvm::sys::fs::create_directories(preamble_dir);
|
||||
if(ec) {
|
||||
LOG_WARN("resolve_header_context: cannot create dir {}: {}",
|
||||
preamble_dir,
|
||||
ec.message());
|
||||
return std::nullopt;
|
||||
}
|
||||
if(auto result = fs::write(preamble_path, preamble); !result) {
|
||||
LOG_WARN("resolve_header_context: cannot write preamble {}: {}",
|
||||
preamble_path,
|
||||
result.error().message());
|
||||
return std::nullopt;
|
||||
}
|
||||
LOG_INFO("resolve_header_context: wrote preamble {} for header path_id={}",
|
||||
preamble_path,
|
||||
header_path_id);
|
||||
}
|
||||
|
||||
return HeaderFileContext{host_path_id, preamble_path, preamble_hash};
|
||||
}
|
||||
|
||||
std::string uri_to_path(const std::string& uri) {
|
||||
auto parsed = lsp::URI::parse(uri);
|
||||
if(parsed.has_value()) {
|
||||
auto path = parsed->file_path();
|
||||
if(path.has_value()) {
|
||||
return std::move(*path);
|
||||
}
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
|
||||
void Compiler::publish_diagnostics(const std::string& uri,
|
||||
int version,
|
||||
const et::serde::RawValue& diagnostics_json) {
|
||||
std::vector<protocol::Diagnostic> diagnostics;
|
||||
if(!diagnostics_json.empty()) {
|
||||
auto status = et::serde::json::from_json(diagnostics_json.data, diagnostics);
|
||||
if(!status) {
|
||||
LOG_WARN("Failed to deserialize diagnostics JSON for {}", uri);
|
||||
}
|
||||
}
|
||||
protocol::PublishDiagnosticsParams params;
|
||||
params.uri = uri;
|
||||
params.version = version;
|
||||
params.diagnostics = std::move(diagnostics);
|
||||
peer.send_notification(params);
|
||||
}
|
||||
|
||||
void Compiler::clear_diagnostics(const std::string& uri) {
|
||||
protocol::PublishDiagnosticsParams params;
|
||||
params.uri = uri;
|
||||
params.diagnostics = {};
|
||||
peer.send_notification(params);
|
||||
}
|
||||
|
||||
et::task<bool> Compiler::ensure_pch(Session& session,
|
||||
const std::string& directory,
|
||||
const std::vector<std::string>& arguments) {
|
||||
auto path_id = session.path_id;
|
||||
auto path = workspace.path_pool.resolve(path_id);
|
||||
auto& text = session.text;
|
||||
auto bound = compute_preamble_bound(text);
|
||||
if(bound == 0) {
|
||||
// No preamble directives — PCH would be empty. Clear any stale entry.
|
||||
workspace.pch_cache.erase(path_id);
|
||||
session.pch_ref.reset();
|
||||
co_return true;
|
||||
}
|
||||
|
||||
// FIXME: hash should also include compile flags that affect preprocessing
|
||||
// (e.g. -D, -I, -isystem, -std) so that files with the same preamble text
|
||||
// but different flags produce separate PCHs. Currently only the preamble
|
||||
// text is hashed — the source file path must be excluded from the hash
|
||||
// to allow sharing across files with identical preambles.
|
||||
auto preamble_text = llvm::StringRef(text).substr(0, bound);
|
||||
auto preamble_hash = llvm::xxh3_64bits(preamble_text);
|
||||
|
||||
// Deterministic content-addressed PCH path.
|
||||
auto pch_path = path::join(workspace.config.cache_dir,
|
||||
"cache",
|
||||
"pch",
|
||||
std::format("{:016x}.pch", preamble_hash));
|
||||
|
||||
// Reuse existing PCH if preamble content and deps haven't changed.
|
||||
if(auto it = workspace.pch_cache.find(path_id); it != workspace.pch_cache.end()) {
|
||||
auto& st = it->second;
|
||||
if(st.hash == preamble_hash && !st.path.empty() &&
|
||||
!deps_changed(workspace.path_pool, st.deps)) {
|
||||
st.bound = bound;
|
||||
session.pch_ref = Session::PCHRef{path_id, preamble_hash, bound};
|
||||
co_return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Preamble incomplete (user still typing) — defer rebuild, reuse old PCH if available.
|
||||
if(!is_preamble_complete(text, bound)) {
|
||||
LOG_DEBUG("Preamble incomplete for {}, deferring PCH rebuild", path);
|
||||
co_return workspace.pch_cache.count(path_id) && !workspace.pch_cache[path_id].path.empty();
|
||||
}
|
||||
|
||||
// If another coroutine is already building PCH for this file, wait for it.
|
||||
if(auto it = workspace.pch_cache.find(path_id);
|
||||
it != workspace.pch_cache.end() && it->second.building) {
|
||||
co_await it->second.building->wait();
|
||||
if(auto it2 = workspace.pch_cache.find(path_id); it2 != workspace.pch_cache.end()) {
|
||||
session.pch_ref = Session::PCHRef{path_id, it2->second.hash, it2->second.bound};
|
||||
}
|
||||
co_return workspace.pch_cache.count(path_id) && !workspace.pch_cache[path_id].path.empty();
|
||||
}
|
||||
|
||||
// Register in-flight build so concurrent requests wait on us.
|
||||
auto completion = std::make_shared<et::event>();
|
||||
workspace.pch_cache[path_id].building = completion;
|
||||
|
||||
// Build a new PCH via stateless worker.
|
||||
worker::BuildParams bp;
|
||||
bp.kind = worker::BuildKind::BuildPCH;
|
||||
bp.file = std::string(path);
|
||||
bp.directory = directory;
|
||||
bp.arguments = arguments;
|
||||
bp.text = text;
|
||||
bp.preamble_bound = bound;
|
||||
bp.output_path = pch_path;
|
||||
|
||||
LOG_DEBUG("Building PCH for {}, bound={}, output={}", path, bound, pch_path);
|
||||
|
||||
auto result = co_await pool.send_stateless(bp);
|
||||
|
||||
if(!result.has_value() || !result.value().success) {
|
||||
LOG_WARN("PCH build failed for {}: {}",
|
||||
path,
|
||||
result.has_value() ? result.value().error : result.error().message);
|
||||
workspace.pch_cache[path_id].building.reset();
|
||||
completion->set();
|
||||
co_return false;
|
||||
}
|
||||
|
||||
auto& st = workspace.pch_cache[path_id];
|
||||
st.path = result.value().output_path;
|
||||
st.bound = bound;
|
||||
st.hash = preamble_hash;
|
||||
st.deps = capture_deps_snapshot(workspace.path_pool, result.value().deps);
|
||||
st.document_links_json = std::move(result.value().pch_links_json);
|
||||
st.building.reset();
|
||||
|
||||
session.pch_ref = Session::PCHRef{path_id, preamble_hash, bound};
|
||||
|
||||
LOG_INFO("PCH built for {}: {}", path, result.value().output_path);
|
||||
|
||||
// Persist cache metadata after successful build.
|
||||
workspace.save_cache();
|
||||
|
||||
completion->set();
|
||||
co_return true;
|
||||
}
|
||||
|
||||
/// Compile module dependencies, build/reuse PCH, and fill PCM paths.
|
||||
/// Shared preparation step used by both ensure_compiled() (stateful path)
|
||||
/// and forward_stateless() (completion/signatureHelp path).
|
||||
et::task<bool> Compiler::ensure_deps(Session& session,
|
||||
const std::string& directory,
|
||||
const std::vector<std::string>& arguments,
|
||||
std::pair<std::string, uint32_t>& pch,
|
||||
std::unordered_map<std::string, std::string>& pcms) {
|
||||
auto path_id = session.path_id;
|
||||
|
||||
// Compile C++20 module dependencies (PCMs).
|
||||
if(workspace.compile_graph && !co_await workspace.compile_graph->compile_deps(path_id)) {
|
||||
co_return false;
|
||||
}
|
||||
|
||||
// Scan buffer text for module imports that might not be in compile_graph yet.
|
||||
// When a user adds `import std;` without saving, the compile_graph (disk-based)
|
||||
// doesn't know about the new dependency. Scan the in-memory text to find them.
|
||||
{
|
||||
auto scan_result = scan(session.text);
|
||||
for(auto& mod_name: scan_result.modules) {
|
||||
if(mod_name.empty())
|
||||
continue;
|
||||
bool found = false;
|
||||
for(auto& [pid, name]: workspace.path_to_module) {
|
||||
if(name == mod_name) {
|
||||
// If PCM not already built, try to build it.
|
||||
if(workspace.pcm_paths.find(pid) == workspace.pcm_paths.end()) {
|
||||
if(workspace.compile_graph && workspace.compile_graph->has_unit(pid)) {
|
||||
co_await workspace.compile_graph->compile_deps(pid);
|
||||
}
|
||||
}
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!found) {
|
||||
LOG_DEBUG("Buffer imports unknown module '{}', skipping", mod_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build or reuse PCH.
|
||||
auto pch_ok = co_await ensure_pch(session, directory, arguments);
|
||||
if(pch_ok) {
|
||||
if(auto pch_it = workspace.pch_cache.find(path_id); pch_it != workspace.pch_cache.end()) {
|
||||
pch = {pch_it->second.path, pch_it->second.bound};
|
||||
}
|
||||
}
|
||||
|
||||
// Fill all available PCM paths, excluding the file's own PCM
|
||||
// to avoid "multiple module declarations".
|
||||
workspace.fill_pcm_deps(pcms, path_id);
|
||||
|
||||
co_return true;
|
||||
}
|
||||
|
||||
bool Compiler::is_stale(const Session& session) {
|
||||
if(session.ast_deps.has_value() && deps_changed(workspace.path_pool, *session.ast_deps))
|
||||
return true;
|
||||
|
||||
// Check PCH staleness via the session's pch_ref.
|
||||
if(session.pch_ref.has_value()) {
|
||||
auto pch_it = workspace.pch_cache.find(session.pch_ref->path_id);
|
||||
if(pch_it != workspace.pch_cache.end() &&
|
||||
deps_changed(workspace.path_pool, pch_it->second.deps))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void Compiler::record_deps(Session& session, llvm::ArrayRef<std::string> deps) {
|
||||
session.ast_deps = capture_deps_snapshot(workspace.path_pool, deps);
|
||||
}
|
||||
|
||||
/// Pull-based compilation entry point for user-opened files.
|
||||
///
|
||||
/// Called lazily by forward_query() / forward_build() before every
|
||||
/// feature request (hover, semantic tokens, etc.). Guarantees that when it
|
||||
/// returns true the stateful worker assigned to `path_id` holds an up-to-date
|
||||
/// AST and diagnostics have been published to the client.
|
||||
///
|
||||
/// Lifecycle overview (pull-based model):
|
||||
///
|
||||
/// didOpen / didChange – only update Session, mark ast_dirty
|
||||
/// didSave – mark dependents dirty, queue indexing
|
||||
/// feature request arrives – calls ensure_compiled() first
|
||||
/// 1. Fast-path exit if AST is already clean (!ast_dirty).
|
||||
/// 2. Compile any C++20 module dependencies (PCMs) via CompileGraph.
|
||||
/// 3. Build / reuse the precompiled header (PCH) via ensure_pch().
|
||||
/// 4. Send CompileParams to the stateful worker, which builds the AST.
|
||||
/// 5. On success: publish diagnostics, clear ast_dirty, schedule indexing.
|
||||
/// 6. On generation mismatch (user edited during compile): keep dirty,
|
||||
/// the next feature request will trigger another compile cycle.
|
||||
///
|
||||
/// Only the opened file itself is remapped (its in-memory text is sent to the
|
||||
/// worker); every other file is read from disk by the compiler.
|
||||
///
|
||||
/// Concurrency: multiple concurrent feature requests for the same file will
|
||||
/// each call ensure_compiled(). The first one launches a detached compile
|
||||
/// task via loop.schedule(); subsequent ones wait on the shared event.
|
||||
/// The detached task cannot be cancelled by LSP $/cancelRequest, preventing
|
||||
/// the race where cancellation wakes all waiters and they all start compiles.
|
||||
et::task<bool> Compiler::ensure_compiled(Session& session) {
|
||||
auto path_id = session.path_id;
|
||||
|
||||
LOG_DEBUG("ensure_compiled: path_id={} version={} gen={} ast_dirty={}",
|
||||
path_id,
|
||||
session.version,
|
||||
session.generation,
|
||||
session.ast_dirty);
|
||||
|
||||
if(!session.ast_dirty) {
|
||||
if(!is_stale(session)) {
|
||||
co_return true;
|
||||
}
|
||||
session.ast_dirty = true;
|
||||
}
|
||||
|
||||
// If another compile is already in flight, wait for it.
|
||||
// This co_await may be cancelled by LSP $/cancelRequest — that's fine,
|
||||
// it just means this particular feature request is abandoned. The
|
||||
// detached compile task keeps running independently.
|
||||
while(session.compiling) {
|
||||
auto pending = session.compiling;
|
||||
co_await pending->done.wait();
|
||||
if(!session.ast_dirty)
|
||||
co_return true;
|
||||
}
|
||||
|
||||
// No compile in flight and AST is dirty — launch a detached compile task.
|
||||
// The detached task is scheduled via loop.schedule() so it is NOT subject
|
||||
// to LSP $/cancelRequest cancellation. This eliminates the race where
|
||||
// cancellation fires the RAII guard, waking all waiters simultaneously
|
||||
// and causing them all to start new compiles.
|
||||
auto pending_compile = std::make_shared<Session::PendingCompile>();
|
||||
session.compiling = pending_compile;
|
||||
|
||||
LOG_INFO("ensure_compiled: launching detached compile path_id={} gen={}",
|
||||
path_id,
|
||||
session.generation);
|
||||
|
||||
// Capture path_id by value so the detached lambda can re-lookup the session
|
||||
// from the sessions map after co_await (DenseMap may invalidate pointers).
|
||||
loop.schedule([](Compiler* self,
|
||||
std::uint32_t pid,
|
||||
std::shared_ptr<Session::PendingCompile> pc) -> et::task<> {
|
||||
// Re-lookup session from the sessions map (pointer may have been
|
||||
// invalidated by DenseMap growth during co_await).
|
||||
auto find_session = [&]() -> Session* {
|
||||
auto it = self->sessions.find(pid);
|
||||
return it != self->sessions.end() ? &it->second : nullptr;
|
||||
};
|
||||
|
||||
auto* sess = find_session();
|
||||
if(!sess) {
|
||||
pc->done.set();
|
||||
co_return;
|
||||
}
|
||||
|
||||
auto finish_compile = [&]() {
|
||||
auto* s = find_session();
|
||||
if(s && s->compiling == pc) {
|
||||
s->compiling.reset();
|
||||
}
|
||||
LOG_INFO("ensure_compiled: finish_compile (detached) path_id={}", pid);
|
||||
pc->done.set();
|
||||
};
|
||||
|
||||
auto gen = sess->generation;
|
||||
LOG_INFO("ensure_compiled: starting compile (detached) path_id={} gen={}", pid, gen);
|
||||
|
||||
auto file_path = std::string(self->workspace.path_pool.resolve(pid));
|
||||
auto uri = lsp::URI::from_file_path(file_path);
|
||||
std::string uri_str = uri.has_value() ? uri->str() : file_path;
|
||||
|
||||
worker::CompileParams params;
|
||||
params.path = file_path;
|
||||
params.version = sess->version;
|
||||
params.text = sess->text;
|
||||
if(!self->fill_compile_args(file_path, params.directory, params.arguments, sess)) {
|
||||
finish_compile();
|
||||
co_return;
|
||||
}
|
||||
|
||||
if(!co_await self
|
||||
->ensure_deps(*sess, params.directory, params.arguments, params.pch, params.pcms)) {
|
||||
LOG_WARN("Dependency preparation failed for {}, skipping compile", uri_str);
|
||||
finish_compile();
|
||||
co_return;
|
||||
}
|
||||
|
||||
// Re-lookup after co_await (DenseMap may have grown).
|
||||
sess = find_session();
|
||||
if(!sess) {
|
||||
pc->done.set();
|
||||
co_return;
|
||||
}
|
||||
|
||||
auto result = co_await self->pool.send_stateful(pid, params);
|
||||
|
||||
// Re-lookup after co_await.
|
||||
sess = find_session();
|
||||
if(!sess) {
|
||||
pc->done.set();
|
||||
co_return;
|
||||
}
|
||||
|
||||
if(sess->generation != gen) {
|
||||
LOG_INFO("ensure_compiled: generation mismatch ({} vs {}) for {}",
|
||||
sess->generation,
|
||||
gen,
|
||||
uri_str);
|
||||
finish_compile();
|
||||
co_return;
|
||||
}
|
||||
|
||||
if(!result.has_value()) {
|
||||
LOG_WARN("Compile failed for {}: {}", uri_str, result.error().message);
|
||||
self->clear_diagnostics(uri_str);
|
||||
finish_compile();
|
||||
co_return;
|
||||
}
|
||||
|
||||
sess->ast_dirty = false;
|
||||
pc->succeeded = true;
|
||||
self->record_deps(*sess, result.value().deps);
|
||||
|
||||
// Store open file index from the stateful worker's TUIndex.
|
||||
if(!result.value().tu_index_data.empty()) {
|
||||
auto tu_index = index::TUIndex::from(result.value().tu_index_data.data());
|
||||
OpenFileIndex ofi;
|
||||
ofi.file_index = std::move(tu_index.main_file_index);
|
||||
ofi.symbols = std::move(tu_index.symbols);
|
||||
ofi.content = sess->text;
|
||||
ofi.mapper.emplace(ofi.content, lsp::PositionEncoding::UTF16);
|
||||
sess->file_index = std::move(ofi);
|
||||
}
|
||||
|
||||
auto version = sess->version;
|
||||
finish_compile();
|
||||
|
||||
// Publish diagnostics AFTER marking compile as done, so that concurrent
|
||||
// forward_query() calls can proceed immediately.
|
||||
self->publish_diagnostics(uri_str, version, result.value().diagnostics);
|
||||
if(self->on_indexing_needed)
|
||||
self->on_indexing_needed();
|
||||
}(this, path_id, pending_compile));
|
||||
|
||||
// Wait for the detached compile to finish. If this wait is cancelled
|
||||
// by LSP $/cancelRequest, the detached task continues unaffected.
|
||||
co_await pending_compile->done.wait();
|
||||
|
||||
co_return !session.ast_dirty;
|
||||
}
|
||||
|
||||
Compiler::RawResult Compiler::forward_query(worker::QueryKind kind,
|
||||
Session& session,
|
||||
std::optional<protocol::Position> position,
|
||||
std::optional<protocol::Range> range) {
|
||||
auto path_id = session.path_id;
|
||||
auto path = std::string(workspace.path_pool.resolve(path_id));
|
||||
// Cache text before co_await — session reference may dangle if didClose
|
||||
// erases the entry from the sessions map during suspension.
|
||||
auto text = session.text;
|
||||
|
||||
if(!co_await ensure_compiled(session)) {
|
||||
co_return serde_raw{"null"};
|
||||
}
|
||||
|
||||
auto sit = sessions.find(path_id);
|
||||
if(sit == sessions.end() || sit->second.ast_dirty) {
|
||||
co_return serde_raw{"null"};
|
||||
}
|
||||
|
||||
worker::QueryParams wp;
|
||||
wp.kind = kind;
|
||||
wp.path = path;
|
||||
|
||||
lsp::PositionMapper mapper(text, lsp::PositionEncoding::UTF16);
|
||||
|
||||
if(position) {
|
||||
auto offset = mapper.to_offset(*position);
|
||||
if(!offset)
|
||||
co_return serde_raw{"null"};
|
||||
wp.offset = *offset;
|
||||
}
|
||||
|
||||
if(range) {
|
||||
auto start = mapper.to_offset(range->start);
|
||||
auto end = mapper.to_offset(range->end);
|
||||
if(start && end) {
|
||||
wp.range = {*start, *end};
|
||||
}
|
||||
}
|
||||
|
||||
auto result = co_await pool.send_stateful(path_id, wp);
|
||||
if(!result.has_value()) {
|
||||
co_return serde_raw{};
|
||||
}
|
||||
co_return std::move(result.value());
|
||||
}
|
||||
|
||||
Compiler::RawResult Compiler::forward_build(worker::BuildKind kind,
|
||||
const protocol::Position& position,
|
||||
Session& session) {
|
||||
auto path_id = session.path_id;
|
||||
auto path = std::string(workspace.path_pool.resolve(path_id));
|
||||
|
||||
worker::BuildParams wp;
|
||||
wp.kind = kind;
|
||||
wp.file = path;
|
||||
// Cache session fields before co_await — session reference may dangle
|
||||
// if didClose erases the entry from the sessions map during suspension.
|
||||
wp.version = session.version;
|
||||
wp.text = session.text;
|
||||
if(!fill_compile_args(path, wp.directory, wp.arguments, &session)) {
|
||||
co_return serde_raw{};
|
||||
}
|
||||
|
||||
if(!co_await ensure_deps(session, wp.directory, wp.arguments, wp.pch, wp.pcms)) {
|
||||
co_return serde_raw{};
|
||||
}
|
||||
|
||||
// After co_await, verify session still exists.
|
||||
if(sessions.find(path_id) == sessions.end()) {
|
||||
co_return serde_raw{};
|
||||
}
|
||||
|
||||
lsp::PositionMapper mapper(wp.text, lsp::PositionEncoding::UTF16);
|
||||
auto offset = mapper.to_offset(position);
|
||||
if(!offset)
|
||||
co_return serde_raw{"null"};
|
||||
wp.offset = *offset;
|
||||
|
||||
auto result = co_await pool.send_stateless(wp);
|
||||
if(!result.has_value()) {
|
||||
co_return serde_raw{};
|
||||
}
|
||||
co_return std::move(result.value().result_json);
|
||||
}
|
||||
|
||||
Compiler::RawResult Compiler::handle_completion(const protocol::Position& position,
|
||||
Session& session) {
|
||||
auto path_id = session.path_id;
|
||||
auto path = std::string(workspace.path_pool.resolve(path_id));
|
||||
|
||||
lsp::PositionMapper mapper(session.text, lsp::PositionEncoding::UTF16);
|
||||
auto offset = mapper.to_offset(position);
|
||||
if(offset) {
|
||||
auto pctx = detect_completion_context(session.text, *offset);
|
||||
if(pctx.kind == CompletionContext::IncludeQuoted ||
|
||||
pctx.kind == CompletionContext::IncludeAngled) {
|
||||
std::string directory;
|
||||
std::vector<std::string> arguments;
|
||||
if(!fill_compile_args(path, directory, arguments))
|
||||
co_return serde_raw{"[]"};
|
||||
|
||||
std::vector<const char*> args_ptrs;
|
||||
args_ptrs.reserve(arguments.size());
|
||||
for(auto& arg: arguments)
|
||||
args_ptrs.push_back(arg.c_str());
|
||||
|
||||
auto search_config = extract_search_config(args_ptrs, directory);
|
||||
DirListingCache dir_cache;
|
||||
auto resolved = resolve_search_config(search_config, dir_cache);
|
||||
bool angled = (pctx.kind == CompletionContext::IncludeAngled);
|
||||
auto candidates = complete_include_path(resolved, pctx.prefix, angled, dir_cache);
|
||||
|
||||
std::vector<protocol::CompletionItem> items;
|
||||
items.reserve(candidates.size());
|
||||
for(auto& c: candidates) {
|
||||
protocol::CompletionItem item;
|
||||
item.label = c.is_directory ? c.name + "/" : c.name;
|
||||
item.kind = protocol::CompletionItemKind::File;
|
||||
items.push_back(std::move(item));
|
||||
}
|
||||
auto json = et::serde::json::to_json<et::ipc::lsp_config>(items);
|
||||
co_return serde_raw{json ? std::move(*json) : "[]"};
|
||||
}
|
||||
if(pctx.kind == CompletionContext::Import) {
|
||||
auto module_names = complete_module_import(workspace.path_to_module, pctx.prefix);
|
||||
|
||||
std::vector<protocol::CompletionItem> items;
|
||||
items.reserve(module_names.size());
|
||||
for(auto& name: module_names) {
|
||||
protocol::CompletionItem item;
|
||||
item.label = name;
|
||||
item.kind = protocol::CompletionItemKind::Module;
|
||||
item.insert_text = name + ";";
|
||||
items.push_back(std::move(item));
|
||||
}
|
||||
auto json = et::serde::json::to_json<et::ipc::lsp_config>(items);
|
||||
co_return serde_raw{json ? std::move(*json) : "[]"};
|
||||
}
|
||||
}
|
||||
|
||||
co_return co_await forward_build(worker::BuildKind::Completion, position, session);
|
||||
}
|
||||
|
||||
} // namespace clice
|
||||
132
src/server/compiler.h
Normal file
132
src/server/compiler.h
Normal file
@@ -0,0 +1,132 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "command/command.h"
|
||||
#include "eventide/async/async.h"
|
||||
#include "eventide/ipc/lsp/protocol.h"
|
||||
#include "eventide/ipc/peer.h"
|
||||
#include "eventide/serde/serde/raw_value.h"
|
||||
#include "server/session.h"
|
||||
#include "server/worker_pool.h"
|
||||
#include "server/workspace.h"
|
||||
#include "syntax/completion.h"
|
||||
|
||||
#include "llvm/ADT/ArrayRef.h"
|
||||
#include "llvm/ADT/DenseMap.h"
|
||||
#include "llvm/ADT/SmallVector.h"
|
||||
#include "llvm/ADT/StringRef.h"
|
||||
|
||||
namespace clice {
|
||||
|
||||
namespace et = eventide;
|
||||
namespace protocol = et::ipc::protocol;
|
||||
|
||||
/// Convert a file:// URI to a local file path.
|
||||
std::string uri_to_path(const std::string& uri);
|
||||
|
||||
/// Compilation service — drives worker processes to build ASTs, PCHs, and PCMs.
|
||||
///
|
||||
/// Compiler holds no persistent state of its own. All project-wide data
|
||||
/// lives in Workspace; per-file data lives in Session. Compiler reads from
|
||||
/// both and writes compilation results back to Session (file_index, pch_ref,
|
||||
/// ast_deps, diagnostics).
|
||||
///
|
||||
/// Responsibilities:
|
||||
/// - AST compilation lifecycle (ensure_compiled → ensure_pch → ensure_deps)
|
||||
/// - Feature request forwarding to stateful/stateless workers
|
||||
/// - Compile argument resolution (CDB lookup + header context fallback)
|
||||
/// - Compile graph initialization (module DAG setup)
|
||||
///
|
||||
/// NOT responsible for:
|
||||
/// - Document lifecycle (didOpen/didChange/didClose) — handled by MasterServer
|
||||
/// - Index queries — handled by Indexer
|
||||
/// - Background indexing scheduling — handled by Indexer
|
||||
class Compiler {
|
||||
public:
|
||||
Compiler(et::event_loop& loop,
|
||||
et::ipc::JsonPeer& peer,
|
||||
Workspace& workspace,
|
||||
WorkerPool& pool,
|
||||
llvm::DenseMap<std::uint32_t, Session>& sessions);
|
||||
~Compiler();
|
||||
|
||||
void init_compile_graph();
|
||||
|
||||
/// Fill compile arguments for a file (CDB lookup + header context fallback).
|
||||
/// @param session If non-null, used for header context resolution on open files.
|
||||
bool fill_compile_args(llvm::StringRef path,
|
||||
std::string& directory,
|
||||
std::vector<std::string>& arguments,
|
||||
Session* session = nullptr);
|
||||
|
||||
/// Compile an open file's AST if dirty. On success, updates session's
|
||||
/// file_index, pch_ref, ast_deps, and publishes diagnostics.
|
||||
et::task<bool> ensure_compiled(Session& session);
|
||||
|
||||
using RawResult = et::task<et::serde::RawValue, et::ipc::Error>;
|
||||
|
||||
/// Forward a query to the stateful worker that holds this file's AST.
|
||||
/// Ensures compilation first. For position-sensitive queries (hover,
|
||||
/// goto-definition), pass a Position. For range-sensitive queries
|
||||
/// (inlay hints), pass a Range.
|
||||
RawResult forward_query(worker::QueryKind kind,
|
||||
Session& session,
|
||||
std::optional<protocol::Position> position = {},
|
||||
std::optional<protocol::Range> range = {});
|
||||
|
||||
/// Forward a build request (signature help, etc.) to a stateless worker.
|
||||
/// Sends the full buffer content and compile arguments.
|
||||
RawResult forward_build(worker::BuildKind kind,
|
||||
const protocol::Position& position,
|
||||
Session& session);
|
||||
|
||||
/// Handle completion requests. Detects preamble context (include/import)
|
||||
/// and serves those locally; delegates code completion to a stateless worker.
|
||||
RawResult handle_completion(const protocol::Position& position, Session& session);
|
||||
|
||||
/// Send an empty diagnostics notification to clear stale markers in the editor.
|
||||
void clear_diagnostics(const std::string& uri);
|
||||
|
||||
/// Callback invoked when indexing should be scheduled.
|
||||
std::function<void()> on_indexing_needed;
|
||||
|
||||
private:
|
||||
et::task<bool> ensure_deps(Session& session,
|
||||
const std::string& directory,
|
||||
const std::vector<std::string>& arguments,
|
||||
std::pair<std::string, uint32_t>& pch,
|
||||
std::unordered_map<std::string, std::string>& pcms);
|
||||
|
||||
et::task<bool> ensure_pch(Session& session,
|
||||
const std::string& directory,
|
||||
const std::vector<std::string>& arguments);
|
||||
|
||||
bool is_stale(const Session& session);
|
||||
void record_deps(Session& session, llvm::ArrayRef<std::string> deps);
|
||||
|
||||
void publish_diagnostics(const std::string& uri, int version, const et::serde::RawValue& diags);
|
||||
|
||||
std::optional<HeaderFileContext> resolve_header_context(std::uint32_t header_path_id,
|
||||
Session* session);
|
||||
|
||||
bool fill_header_context_args(llvm::StringRef path,
|
||||
std::uint32_t path_id,
|
||||
std::string& directory,
|
||||
std::vector<std::string>& arguments,
|
||||
Session* session);
|
||||
|
||||
private:
|
||||
et::event_loop& loop;
|
||||
et::ipc::JsonPeer& peer;
|
||||
Workspace& workspace;
|
||||
WorkerPool& pool;
|
||||
llvm::DenseMap<std::uint32_t, Session>& sessions;
|
||||
};
|
||||
|
||||
} // namespace clice
|
||||
@@ -25,10 +25,10 @@ void CliceConfig::apply_defaults(const std::string& workspace_root) {
|
||||
cpu_count = 4;
|
||||
|
||||
if(stateful_worker_count == 0) {
|
||||
stateful_worker_count = std::max(1u, cpu_count / 4);
|
||||
stateful_worker_count = 2;
|
||||
}
|
||||
if(stateless_worker_count == 0) {
|
||||
stateless_worker_count = std::max(1u, cpu_count / 4);
|
||||
stateless_worker_count = 3;
|
||||
}
|
||||
if(worker_memory_limit == 0) {
|
||||
worker_memory_limit = 4ULL * 1024 * 1024 * 1024; // 4GB default
|
||||
@@ -41,10 +41,15 @@ void CliceConfig::apply_defaults(const std::string& workspace_root) {
|
||||
index_dir = path::join(cache_dir, "index");
|
||||
}
|
||||
|
||||
if(logging_dir.empty() && !cache_dir.empty()) {
|
||||
logging_dir = path::join(cache_dir, "logs");
|
||||
}
|
||||
|
||||
// Apply variable substitution to string fields
|
||||
substitute_workspace(compile_commands_path, workspace_root);
|
||||
substitute_workspace(cache_dir, workspace_root);
|
||||
substitute_workspace(index_dir, workspace_root);
|
||||
substitute_workspace(logging_dir, workspace_root);
|
||||
}
|
||||
|
||||
std::optional<CliceConfig> CliceConfig::load(const std::string& path,
|
||||
|
||||
@@ -22,8 +22,8 @@ struct CliceConfig {
|
||||
// Index storage directory (default: <cache_dir>/index/)
|
||||
std::string index_dir;
|
||||
|
||||
// Debounce interval for re-compilation after edits (milliseconds)
|
||||
int debounce_ms = 200;
|
||||
// Logging directory (default: <cache_dir>/logs/)
|
||||
std::string logging_dir;
|
||||
|
||||
// Background indexing
|
||||
bool enable_indexing = true;
|
||||
|
||||
696
src/server/indexer.cpp
Normal file
696
src/server/indexer.cpp
Normal file
@@ -0,0 +1,696 @@
|
||||
#include "server/indexer.h"
|
||||
|
||||
#include <string>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
#include "eventide/ipc/lsp/position.h"
|
||||
#include "eventide/ipc/lsp/protocol.h"
|
||||
#include "eventide/ipc/lsp/uri.h"
|
||||
#include "index/tu_index.h"
|
||||
#include "server/compiler.h"
|
||||
#include "server/protocol.h"
|
||||
#include "server/session.h"
|
||||
#include "server/worker_pool.h"
|
||||
#include "support/filesystem.h"
|
||||
#include "support/logging.h"
|
||||
|
||||
#include "llvm/Support/FileSystem.h"
|
||||
#include "llvm/Support/MemoryBuffer.h"
|
||||
#include "llvm/Support/Path.h"
|
||||
#include "llvm/Support/raw_ostream.h"
|
||||
|
||||
namespace clice {
|
||||
|
||||
namespace lsp = eventide::ipc::lsp;
|
||||
|
||||
void Indexer::merge(const void* tu_index_data, std::size_t size) {
|
||||
auto tu_index = index::TUIndex::from(tu_index_data);
|
||||
if(tu_index.graph.paths.empty()) {
|
||||
LOG_WARN("Ignoring TUIndex with empty path graph");
|
||||
return;
|
||||
}
|
||||
auto file_ids_map = workspace.project_index.merge(tu_index);
|
||||
auto main_tu_path_id = static_cast<std::uint32_t>(tu_index.graph.paths.size() - 1);
|
||||
|
||||
auto merge_file_index = [&](std::uint32_t tu_path_id, index::FileIndex& file_idx) {
|
||||
auto global_path_id = file_ids_map[tu_path_id];
|
||||
auto& shard = workspace.merged_indices[global_path_id];
|
||||
|
||||
if(tu_path_id == main_tu_path_id) {
|
||||
std::vector<index::IncludeLocation> include_locs;
|
||||
for(auto& loc: tu_index.graph.locations) {
|
||||
index::IncludeLocation remapped = loc;
|
||||
remapped.path_id = file_ids_map[loc.path_id];
|
||||
include_locs.push_back(remapped);
|
||||
}
|
||||
auto file_path = workspace.project_index.path_pool.path(global_path_id);
|
||||
llvm::StringRef file_content;
|
||||
std::string file_content_storage;
|
||||
auto buf = llvm::MemoryBuffer::getFile(file_path);
|
||||
if(buf) {
|
||||
file_content_storage = (*buf)->getBuffer().str();
|
||||
file_content = file_content_storage;
|
||||
}
|
||||
shard.index.merge(global_path_id,
|
||||
tu_index.built_at,
|
||||
std::move(include_locs),
|
||||
file_idx,
|
||||
file_content);
|
||||
} else {
|
||||
std::optional<std::uint32_t> include_id;
|
||||
for(std::uint32_t i = 0; i < tu_index.graph.locations.size(); ++i) {
|
||||
if(tu_index.graph.locations[i].path_id == tu_path_id) {
|
||||
include_id = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!include_id) {
|
||||
LOG_WARN("Skip merge for path {}: include location not found", global_path_id);
|
||||
return;
|
||||
}
|
||||
auto header_path = workspace.project_index.path_pool.path(global_path_id);
|
||||
llvm::StringRef header_content;
|
||||
std::string header_content_storage;
|
||||
auto header_buf = llvm::MemoryBuffer::getFile(header_path);
|
||||
if(header_buf) {
|
||||
header_content_storage = (*header_buf)->getBuffer().str();
|
||||
header_content = header_content_storage;
|
||||
}
|
||||
shard.index.merge(global_path_id, *include_id, file_idx, header_content);
|
||||
}
|
||||
shard.invalidate_mapper();
|
||||
};
|
||||
|
||||
for(auto& [tu_path_id, file_idx]: tu_index.path_file_indices) {
|
||||
merge_file_index(tu_path_id, file_idx);
|
||||
}
|
||||
merge_file_index(main_tu_path_id, tu_index.main_file_index);
|
||||
|
||||
LOG_INFO("Merged TUIndex: {} paths, {} symbols, {} merged_shards",
|
||||
tu_index.graph.paths.size(),
|
||||
tu_index.symbols.size(),
|
||||
workspace.merged_indices.size());
|
||||
}
|
||||
|
||||
void Indexer::save(llvm::StringRef index_dir) {
|
||||
if(index_dir.empty())
|
||||
return;
|
||||
|
||||
auto ec = llvm::sys::fs::create_directories(index_dir);
|
||||
if(ec) {
|
||||
LOG_WARN("Failed to create index directory {}: {}", std::string(index_dir), ec.message());
|
||||
return;
|
||||
}
|
||||
|
||||
auto project_path = path::join(index_dir, "project.idx");
|
||||
{
|
||||
std::error_code write_ec;
|
||||
llvm::raw_fd_ostream os(project_path, write_ec);
|
||||
if(!write_ec) {
|
||||
workspace.project_index.serialize(os);
|
||||
LOG_INFO("Saved ProjectIndex to {}", project_path);
|
||||
} else {
|
||||
LOG_WARN("Failed to save ProjectIndex: {}", write_ec.message());
|
||||
}
|
||||
}
|
||||
|
||||
auto shards_dir = path::join(index_dir, "shards");
|
||||
ec = llvm::sys::fs::create_directories(shards_dir);
|
||||
if(ec) {
|
||||
LOG_WARN("Failed to create shards directory: {}", ec.message());
|
||||
return;
|
||||
}
|
||||
|
||||
std::size_t saved = 0;
|
||||
for(auto& [path_id, shard]: workspace.merged_indices) {
|
||||
if(!shard.index.need_rewrite())
|
||||
continue;
|
||||
auto shard_path = path::join(shards_dir, std::to_string(path_id) + ".idx");
|
||||
std::error_code write_ec;
|
||||
llvm::raw_fd_ostream os(shard_path, write_ec);
|
||||
if(!write_ec) {
|
||||
shard.index.serialize(os);
|
||||
++saved;
|
||||
}
|
||||
}
|
||||
LOG_INFO("Saved {} MergedIndex shards (of {} total)", saved, workspace.merged_indices.size());
|
||||
}
|
||||
|
||||
void Indexer::load(llvm::StringRef index_dir) {
|
||||
if(index_dir.empty())
|
||||
return;
|
||||
|
||||
auto project_path = path::join(index_dir, "project.idx");
|
||||
auto buf = llvm::MemoryBuffer::getFile(project_path);
|
||||
if(buf) {
|
||||
workspace.project_index = index::ProjectIndex::from((*buf)->getBufferStart());
|
||||
LOG_INFO("Loaded ProjectIndex: {} symbols", workspace.project_index.symbols.size());
|
||||
}
|
||||
|
||||
auto shards_dir = path::join(index_dir, "shards");
|
||||
std::error_code ec;
|
||||
for(auto it = llvm::sys::fs::directory_iterator(shards_dir, ec);
|
||||
!ec && it != llvm::sys::fs::directory_iterator();
|
||||
it.increment(ec)) {
|
||||
auto filename = llvm::sys::path::filename(it->path());
|
||||
if(!filename.ends_with(".idx"))
|
||||
continue;
|
||||
auto stem = filename.drop_back(4);
|
||||
std::uint32_t path_id = 0;
|
||||
if(stem.getAsInteger(10, path_id))
|
||||
continue;
|
||||
workspace.merged_indices[path_id] = MergedIndexShard{index::MergedIndex::load(it->path())};
|
||||
}
|
||||
|
||||
if(!workspace.merged_indices.empty()) {
|
||||
LOG_INFO("Loaded {} MergedIndex shards", workspace.merged_indices.size());
|
||||
}
|
||||
}
|
||||
|
||||
bool Indexer::need_update(llvm::StringRef file_path) {
|
||||
auto cache_it = workspace.project_index.path_pool.find(file_path);
|
||||
if(cache_it == workspace.project_index.path_pool.cache.end())
|
||||
return true;
|
||||
|
||||
auto merged_it = workspace.merged_indices.find(cache_it->second);
|
||||
if(merged_it == workspace.merged_indices.end())
|
||||
return true;
|
||||
|
||||
llvm::SmallVector<llvm::StringRef> path_mapping;
|
||||
for(auto& p: workspace.project_index.path_pool.paths) {
|
||||
path_mapping.push_back(p);
|
||||
}
|
||||
return merged_it->second.index.need_update(path_mapping);
|
||||
}
|
||||
|
||||
bool Indexer::find_symbol_info(index::SymbolHash hash, std::string& name, SymbolKind& kind) const {
|
||||
for(auto& [_, session]: sessions) {
|
||||
if(!session.file_index)
|
||||
continue;
|
||||
auto it = session.file_index->symbols.find(hash);
|
||||
if(it != session.file_index->symbols.end()) {
|
||||
name = it->second.name;
|
||||
kind = it->second.kind;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
auto it = workspace.project_index.symbols.find(hash);
|
||||
if(it != workspace.project_index.symbols.end()) {
|
||||
name = it->second.name;
|
||||
kind = it->second.kind;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Indexer::CursorHit Indexer::resolve_cursor(llvm::StringRef path,
|
||||
const protocol::Position& position,
|
||||
Session* session) {
|
||||
// Try the session's open file index first.
|
||||
if(session && session->file_index) {
|
||||
auto& index = *session->file_index;
|
||||
if(!index.mapper)
|
||||
return {};
|
||||
auto offset = index.mapper->to_offset(position);
|
||||
if(!offset)
|
||||
return {};
|
||||
if(auto found = index.find_occurrence(*offset))
|
||||
return {found->first, found->second};
|
||||
return {};
|
||||
}
|
||||
|
||||
// Fallback to MergedIndex, using session text (or reading from disk) for position -> offset.
|
||||
const std::string* doc_text = session ? &session->text : nullptr;
|
||||
if(!doc_text)
|
||||
return {};
|
||||
lsp::PositionMapper doc_mapper(*doc_text, lsp::PositionEncoding::UTF16);
|
||||
auto offset = doc_mapper.to_offset(position);
|
||||
if(!offset)
|
||||
return {};
|
||||
|
||||
auto proj_it = workspace.project_index.path_pool.find(path);
|
||||
if(proj_it == workspace.project_index.path_pool.cache.end())
|
||||
return {};
|
||||
auto shard_it = workspace.merged_indices.find(proj_it->second);
|
||||
if(shard_it == workspace.merged_indices.end())
|
||||
return {};
|
||||
|
||||
if(auto found = shard_it->second.find_occurrence(*offset))
|
||||
return {found->first, found->second};
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<protocol::Location> Indexer::query_relations(llvm::StringRef path,
|
||||
const protocol::Position& position,
|
||||
RelationKind kind,
|
||||
Session* session) {
|
||||
auto hit = resolve_cursor(path, position, session);
|
||||
if(hit.hash == 0)
|
||||
return {};
|
||||
|
||||
std::vector<protocol::Location> locations;
|
||||
|
||||
auto sym_it = workspace.project_index.symbols.find(hit.hash);
|
||||
if(sym_it != workspace.project_index.symbols.end()) {
|
||||
for(auto file_id: sym_it->second.reference_files) {
|
||||
if(is_proj_path_open(file_id))
|
||||
continue;
|
||||
auto shard_it = workspace.merged_indices.find(file_id);
|
||||
if(shard_it == workspace.merged_indices.end())
|
||||
continue;
|
||||
auto uri = lsp::URI::from_file_path(workspace.project_index.path_pool.path(file_id));
|
||||
if(!uri)
|
||||
continue;
|
||||
shard_it->second.find_relations(hit.hash,
|
||||
kind,
|
||||
[&](const auto&, protocol::Range range) {
|
||||
locations.push_back({uri->str(), range});
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for(auto& [id, sess]: sessions) {
|
||||
if(!sess.file_index)
|
||||
continue;
|
||||
auto uri = lsp::URI::from_file_path(std::string(workspace.path_pool.resolve(id)));
|
||||
if(!uri)
|
||||
continue;
|
||||
sess.file_index->find_relations(hit.hash, kind, [&](const auto&, protocol::Range range) {
|
||||
locations.push_back({uri->str(), range});
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
return locations;
|
||||
}
|
||||
|
||||
std::optional<SymbolInfo> Indexer::lookup_symbol(const std::string& uri,
|
||||
llvm::StringRef path,
|
||||
const protocol::Position& position,
|
||||
Session* session) {
|
||||
auto hit = resolve_cursor(path, position, session);
|
||||
if(hit.hash == 0)
|
||||
return std::nullopt;
|
||||
|
||||
std::string name;
|
||||
SymbolKind sym_kind;
|
||||
if(!find_symbol_info(hit.hash, name, sym_kind))
|
||||
return std::nullopt;
|
||||
|
||||
return SymbolInfo{hit.hash, std::move(name), sym_kind, uri, hit.range};
|
||||
}
|
||||
|
||||
std::optional<protocol::Location> Indexer::find_definition_location(index::SymbolHash hash) {
|
||||
// Open file indices first (fresher data for actively-edited files).
|
||||
for(auto& [id, sess]: sessions) {
|
||||
if(!sess.file_index)
|
||||
continue;
|
||||
auto uri = lsp::URI::from_file_path(std::string(workspace.path_pool.resolve(id)));
|
||||
if(!uri)
|
||||
continue;
|
||||
std::optional<protocol::Location> result;
|
||||
sess.file_index->find_relations(hash,
|
||||
RelationKind::Definition,
|
||||
[&](const auto&, protocol::Range range) {
|
||||
result = protocol::Location{uri->str(), range};
|
||||
return false;
|
||||
});
|
||||
if(result)
|
||||
return result;
|
||||
}
|
||||
|
||||
// Fall back to ProjectIndex reference files.
|
||||
auto sym_it = workspace.project_index.symbols.find(hash);
|
||||
if(sym_it == workspace.project_index.symbols.end())
|
||||
return std::nullopt;
|
||||
|
||||
for(auto file_id: sym_it->second.reference_files) {
|
||||
if(is_proj_path_open(file_id))
|
||||
continue;
|
||||
auto shard_it = workspace.merged_indices.find(file_id);
|
||||
if(shard_it == workspace.merged_indices.end())
|
||||
continue;
|
||||
auto uri = lsp::URI::from_file_path(workspace.project_index.path_pool.path(file_id));
|
||||
if(!uri)
|
||||
continue;
|
||||
std::optional<protocol::Location> result;
|
||||
shard_it->second.find_relations(hash,
|
||||
RelationKind::Definition,
|
||||
[&](const auto&, protocol::Range range) {
|
||||
result = protocol::Location{uri->str(), range};
|
||||
return false;
|
||||
});
|
||||
if(result)
|
||||
return result;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<SymbolInfo>
|
||||
Indexer::resolve_hierarchy_item(const std::string& uri,
|
||||
llvm::StringRef path,
|
||||
const protocol::Range& range,
|
||||
const std::optional<protocol::LSPAny>& data,
|
||||
Session* session) {
|
||||
if(data) {
|
||||
if(auto* int_val = std::get_if<std::int64_t>(&*data)) {
|
||||
auto hash = static_cast<index::SymbolHash>(*int_val);
|
||||
std::string name;
|
||||
SymbolKind kind;
|
||||
if(find_symbol_info(hash, name, kind)) {
|
||||
return SymbolInfo{hash, std::move(name), kind, uri, range};
|
||||
}
|
||||
}
|
||||
}
|
||||
return lookup_symbol(uri, path, range.start, session);
|
||||
}
|
||||
|
||||
void Indexer::collect_grouped_relations(
|
||||
index::SymbolHash hash,
|
||||
RelationKind kind,
|
||||
llvm::DenseMap<index::SymbolHash, std::vector<protocol::Range>>& target_ranges) {
|
||||
auto sym_it = workspace.project_index.symbols.find(hash);
|
||||
if(sym_it != workspace.project_index.symbols.end()) {
|
||||
for(auto file_id: sym_it->second.reference_files) {
|
||||
if(is_proj_path_open(file_id))
|
||||
continue;
|
||||
auto shard_it = workspace.merged_indices.find(file_id);
|
||||
if(shard_it == workspace.merged_indices.end())
|
||||
continue;
|
||||
shard_it->second.find_relations(hash, kind, [&](const auto& r, protocol::Range range) {
|
||||
target_ranges[r.target_symbol].push_back(range);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
for(auto& [_, sess]: sessions) {
|
||||
if(!sess.file_index)
|
||||
continue;
|
||||
sess.file_index->find_relations(hash, kind, [&](const auto& r, protocol::Range range) {
|
||||
target_ranges[r.target_symbol].push_back(range);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void Indexer::collect_unique_targets(index::SymbolHash hash,
|
||||
RelationKind kind,
|
||||
llvm::SmallVectorImpl<index::SymbolHash>& targets) {
|
||||
llvm::DenseSet<index::SymbolHash> seen;
|
||||
auto sym_it = workspace.project_index.symbols.find(hash);
|
||||
if(sym_it != workspace.project_index.symbols.end()) {
|
||||
for(auto file_id: sym_it->second.reference_files) {
|
||||
if(is_proj_path_open(file_id))
|
||||
continue;
|
||||
auto shard_it = workspace.merged_indices.find(file_id);
|
||||
if(shard_it == workspace.merged_indices.end())
|
||||
continue;
|
||||
/// No position conversion needed -- just collect target symbol hashes.
|
||||
shard_it->second.index.lookup(hash, kind, [&](const index::Relation& r) {
|
||||
if(seen.insert(r.target_symbol).second) {
|
||||
targets.push_back(r.target_symbol);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
for(auto& [_, sess]: sessions) {
|
||||
if(!sess.file_index)
|
||||
continue;
|
||||
auto rel_it = sess.file_index->file_index.relations.find(hash);
|
||||
if(rel_it == sess.file_index->file_index.relations.end())
|
||||
continue;
|
||||
for(auto& r: rel_it->second) {
|
||||
if(r.kind & kind) {
|
||||
if(seen.insert(r.target_symbol).second) {
|
||||
targets.push_back(r.target_symbol);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve a symbol hash into a SymbolInfo with definition location.
|
||||
/// Returns nullopt if the symbol or its definition cannot be found.
|
||||
std::optional<SymbolInfo> Indexer::resolve_symbol(index::SymbolHash hash) {
|
||||
std::string name;
|
||||
SymbolKind kind;
|
||||
if(!find_symbol_info(hash, name, kind))
|
||||
return std::nullopt;
|
||||
auto def_loc = find_definition_location(hash);
|
||||
if(!def_loc)
|
||||
return std::nullopt;
|
||||
return SymbolInfo{hash, std::move(name), kind, def_loc->uri, def_loc->range};
|
||||
}
|
||||
|
||||
std::vector<protocol::CallHierarchyIncomingCall>
|
||||
Indexer::find_incoming_calls(index::SymbolHash hash) {
|
||||
llvm::DenseMap<index::SymbolHash, std::vector<protocol::Range>> caller_ranges;
|
||||
collect_grouped_relations(hash, RelationKind::Caller, caller_ranges);
|
||||
|
||||
std::vector<protocol::CallHierarchyIncomingCall> results;
|
||||
for(auto& [caller_hash, ranges]: caller_ranges) {
|
||||
auto info = resolve_symbol(caller_hash);
|
||||
if(!info)
|
||||
continue;
|
||||
results.push_back({build_call_hierarchy_item(*info), std::move(ranges)});
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
std::vector<protocol::CallHierarchyOutgoingCall>
|
||||
Indexer::find_outgoing_calls(index::SymbolHash hash) {
|
||||
llvm::DenseMap<index::SymbolHash, std::vector<protocol::Range>> callee_ranges;
|
||||
collect_grouped_relations(hash, RelationKind::Callee, callee_ranges);
|
||||
|
||||
std::vector<protocol::CallHierarchyOutgoingCall> results;
|
||||
for(auto& [callee_hash, ranges]: callee_ranges) {
|
||||
auto info = resolve_symbol(callee_hash);
|
||||
if(!info)
|
||||
continue;
|
||||
results.push_back({build_call_hierarchy_item(*info), std::move(ranges)});
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
std::vector<protocol::TypeHierarchyItem> Indexer::find_supertypes(index::SymbolHash hash) {
|
||||
llvm::SmallVector<index::SymbolHash> base_hashes;
|
||||
collect_unique_targets(hash, RelationKind::Base, base_hashes);
|
||||
|
||||
std::vector<protocol::TypeHierarchyItem> results;
|
||||
for(auto target_hash: base_hashes) {
|
||||
auto info = resolve_symbol(target_hash);
|
||||
if(!info)
|
||||
continue;
|
||||
results.push_back(build_type_hierarchy_item(*info));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
std::vector<protocol::TypeHierarchyItem> Indexer::find_subtypes(index::SymbolHash hash) {
|
||||
llvm::SmallVector<index::SymbolHash> derived_hashes;
|
||||
collect_unique_targets(hash, RelationKind::Derived, derived_hashes);
|
||||
|
||||
std::vector<protocol::TypeHierarchyItem> results;
|
||||
for(auto target_hash: derived_hashes) {
|
||||
auto info = resolve_symbol(target_hash);
|
||||
if(!info)
|
||||
continue;
|
||||
results.push_back(build_type_hierarchy_item(*info));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
std::vector<protocol::SymbolInformation> Indexer::search_symbols(llvm::StringRef query,
|
||||
std::size_t max_results) {
|
||||
std::string query_lower = query.lower();
|
||||
|
||||
auto is_indexable_kind = [](SymbolKind sk) {
|
||||
return sk == SymbolKind::Namespace || sk == SymbolKind::Class || sk == SymbolKind::Struct ||
|
||||
sk == SymbolKind::Union || sk == SymbolKind::Enum || sk == SymbolKind::Type ||
|
||||
sk == SymbolKind::Field || sk == SymbolKind::EnumMember ||
|
||||
sk == SymbolKind::Function || sk == SymbolKind::Method ||
|
||||
sk == SymbolKind::Variable || sk == SymbolKind::Parameter ||
|
||||
sk == SymbolKind::Macro || sk == SymbolKind::Concept || sk == SymbolKind::Module ||
|
||||
sk == SymbolKind::Operator || sk == SymbolKind::MacroParameter ||
|
||||
sk == SymbolKind::Label || sk == SymbolKind::Attribute;
|
||||
};
|
||||
|
||||
auto matches_query = [&](llvm::StringRef name) {
|
||||
if(query_lower.empty())
|
||||
return true;
|
||||
return llvm::StringRef(name).lower().find(query_lower) != std::string::npos;
|
||||
};
|
||||
|
||||
std::vector<protocol::SymbolInformation> results;
|
||||
llvm::DenseSet<index::SymbolHash> seen;
|
||||
|
||||
for(auto& [hash, symbol]: workspace.project_index.symbols) {
|
||||
if(results.size() >= max_results)
|
||||
break;
|
||||
if(!is_indexable_kind(symbol.kind) || symbol.name.empty())
|
||||
continue;
|
||||
if(!matches_query(symbol.name))
|
||||
continue;
|
||||
auto def_loc = find_definition_location(hash);
|
||||
if(!def_loc)
|
||||
continue;
|
||||
|
||||
protocol::SymbolInformation info;
|
||||
info.name = symbol.name;
|
||||
info.kind = to_lsp_symbol_kind(symbol.kind);
|
||||
info.location = std::move(*def_loc);
|
||||
results.push_back(std::move(info));
|
||||
seen.insert(hash);
|
||||
}
|
||||
|
||||
for(auto& [_, sess]: sessions) {
|
||||
if(results.size() >= max_results)
|
||||
break;
|
||||
if(!sess.file_index)
|
||||
continue;
|
||||
for(auto& [hash, symbol]: sess.file_index->symbols) {
|
||||
if(results.size() >= max_results)
|
||||
break;
|
||||
if(seen.contains(hash))
|
||||
continue;
|
||||
if(!is_indexable_kind(symbol.kind) || symbol.name.empty())
|
||||
continue;
|
||||
if(!matches_query(symbol.name))
|
||||
continue;
|
||||
auto def_loc = find_definition_location(hash);
|
||||
if(!def_loc)
|
||||
continue;
|
||||
|
||||
protocol::SymbolInformation info;
|
||||
info.name = symbol.name;
|
||||
info.kind = to_lsp_symbol_kind(symbol.kind);
|
||||
info.location = std::move(*def_loc);
|
||||
results.push_back(std::move(info));
|
||||
seen.insert(hash);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
protocol::SymbolKind Indexer::to_lsp_symbol_kind(SymbolKind kind) {
|
||||
switch(kind) {
|
||||
case SymbolKind::Namespace: return protocol::SymbolKind::Namespace;
|
||||
case SymbolKind::Class: return protocol::SymbolKind::Class;
|
||||
case SymbolKind::Struct: return protocol::SymbolKind::Struct;
|
||||
case SymbolKind::Union: return protocol::SymbolKind::Class;
|
||||
case SymbolKind::Enum: return protocol::SymbolKind::Enum;
|
||||
case SymbolKind::Type: return protocol::SymbolKind::TypeParameter;
|
||||
case SymbolKind::Field: return protocol::SymbolKind::Field;
|
||||
case SymbolKind::EnumMember: return protocol::SymbolKind::EnumMember;
|
||||
case SymbolKind::Function: return protocol::SymbolKind::Function;
|
||||
case SymbolKind::Method: return protocol::SymbolKind::Method;
|
||||
case SymbolKind::Variable: return protocol::SymbolKind::Variable;
|
||||
case SymbolKind::Parameter: return protocol::SymbolKind::Variable;
|
||||
case SymbolKind::Macro: return protocol::SymbolKind::Function;
|
||||
case SymbolKind::Concept: return protocol::SymbolKind::Interface;
|
||||
case SymbolKind::Module: return protocol::SymbolKind::Module;
|
||||
case SymbolKind::Operator: return protocol::SymbolKind::Operator;
|
||||
default: return protocol::SymbolKind::Variable;
|
||||
}
|
||||
}
|
||||
|
||||
protocol::CallHierarchyItem Indexer::build_call_hierarchy_item(const SymbolInfo& info) {
|
||||
protocol::CallHierarchyItem item;
|
||||
item.name = info.name;
|
||||
item.kind = to_lsp_symbol_kind(info.kind);
|
||||
item.uri = info.uri;
|
||||
item.range = info.range;
|
||||
item.selection_range = info.range;
|
||||
item.data = protocol::LSPAny(static_cast<std::int64_t>(info.hash));
|
||||
return item;
|
||||
}
|
||||
|
||||
protocol::TypeHierarchyItem Indexer::build_type_hierarchy_item(const SymbolInfo& info) {
|
||||
protocol::TypeHierarchyItem item;
|
||||
item.name = info.name;
|
||||
item.kind = to_lsp_symbol_kind(info.kind);
|
||||
item.uri = info.uri;
|
||||
item.range = info.range;
|
||||
item.selection_range = info.range;
|
||||
item.data = protocol::LSPAny(static_cast<std::int64_t>(info.hash));
|
||||
return item;
|
||||
}
|
||||
|
||||
void Indexer::enqueue(std::uint32_t server_path_id) {
|
||||
index_queue.push_back(server_path_id);
|
||||
}
|
||||
|
||||
void Indexer::schedule() {
|
||||
if(!workspace.config.enable_indexing || indexing_active || indexing_scheduled)
|
||||
return;
|
||||
indexing_scheduled = true;
|
||||
|
||||
if(!index_idle_timer) {
|
||||
index_idle_timer = std::make_shared<et::timer>(et::timer::create(loop));
|
||||
}
|
||||
index_idle_timer->start(std::chrono::milliseconds(workspace.config.idle_timeout_ms));
|
||||
loop.schedule(run_background_indexing());
|
||||
}
|
||||
|
||||
et::task<> Indexer::run_background_indexing() {
|
||||
if(index_idle_timer) {
|
||||
co_await index_idle_timer->wait();
|
||||
}
|
||||
indexing_scheduled = false;
|
||||
|
||||
if(index_queue_pos >= index_queue.size()) {
|
||||
LOG_DEBUG("Background indexing: queue exhausted");
|
||||
co_return;
|
||||
}
|
||||
|
||||
indexing_active = true;
|
||||
std::size_t processed = 0;
|
||||
|
||||
while(index_queue_pos < index_queue.size()) {
|
||||
auto server_path_id = index_queue[index_queue_pos];
|
||||
index_queue_pos++;
|
||||
|
||||
auto file_path = std::string(workspace.path_pool.resolve(server_path_id));
|
||||
|
||||
if(sessions.contains(server_path_id))
|
||||
continue;
|
||||
|
||||
if(!need_update(file_path))
|
||||
continue;
|
||||
|
||||
worker::BuildParams params;
|
||||
params.kind = worker::BuildKind::Index;
|
||||
params.file = file_path;
|
||||
if(!compiler.fill_compile_args(file_path, params.directory, params.arguments, nullptr))
|
||||
continue;
|
||||
|
||||
workspace.fill_pcm_deps(params.pcms);
|
||||
|
||||
LOG_INFO("Background indexing: {}", file_path);
|
||||
|
||||
auto result = co_await pool.send_stateless(params);
|
||||
if(result.has_value() && result.value().success && !result.value().tu_index_data.empty()) {
|
||||
LOG_INFO("Background indexing got TUIndex for {}: {} bytes",
|
||||
file_path,
|
||||
result.value().tu_index_data.size());
|
||||
merge(result.value().tu_index_data.data(), result.value().tu_index_data.size());
|
||||
++processed;
|
||||
} else if(result.has_value() && !result.value().success) {
|
||||
LOG_WARN("Background index failed for {}: {}", file_path, result.value().error);
|
||||
} else if(result.has_value() && result.value().tu_index_data.empty()) {
|
||||
LOG_WARN("Background index returned empty TUIndex for {}", file_path);
|
||||
} else {
|
||||
LOG_WARN("Background index IPC error for {}: {}", file_path, result.error().message);
|
||||
}
|
||||
}
|
||||
|
||||
indexing_active = false;
|
||||
LOG_INFO("Background indexing complete: {} files processed", processed);
|
||||
save(workspace.config.index_dir);
|
||||
}
|
||||
|
||||
} // namespace clice
|
||||
189
src/server/indexer.h
Normal file
189
src/server/indexer.h
Normal file
@@ -0,0 +1,189 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "eventide/async/async.h"
|
||||
#include "eventide/ipc/lsp/position.h"
|
||||
#include "eventide/ipc/lsp/protocol.h"
|
||||
#include "semantic/relation_kind.h"
|
||||
#include "semantic/symbol_kind.h"
|
||||
#include "server/workspace.h"
|
||||
|
||||
#include "llvm/ADT/DenseMap.h"
|
||||
#include "llvm/ADT/SmallVector.h"
|
||||
#include "llvm/ADT/StringRef.h"
|
||||
|
||||
namespace clice {
|
||||
|
||||
namespace et = eventide;
|
||||
namespace protocol = et::ipc::protocol;
|
||||
namespace lsp = et::ipc::lsp;
|
||||
|
||||
struct Session;
|
||||
class Compiler;
|
||||
class WorkerPool;
|
||||
|
||||
/// Information about a symbol at a given position.
|
||||
struct SymbolInfo {
|
||||
index::SymbolHash hash = 0;
|
||||
std::string name;
|
||||
SymbolKind kind;
|
||||
std::string uri;
|
||||
protocol::Range range;
|
||||
};
|
||||
|
||||
/// Index query layer and background indexing scheduler.
|
||||
///
|
||||
/// Indexer holds no index data of its own. All persistent data lives in
|
||||
/// Workspace (disk-derived ProjectIndex + MergedIndex shards) and per-file
|
||||
/// data lives in Session (OpenFileIndex from unsaved buffers).
|
||||
///
|
||||
/// Responsibilities:
|
||||
/// - Cross-file navigation queries (definition, references, hierarchy)
|
||||
/// - Symbol search (workspace/symbol)
|
||||
/// - Background indexing scheduling (enqueue → idle timer → worker dispatch)
|
||||
/// - Merging TUIndex results into Workspace's ProjectIndex
|
||||
///
|
||||
/// NOT responsible for:
|
||||
/// - Compilation — handled by Compiler
|
||||
/// - Document lifecycle — handled by MasterServer
|
||||
class Indexer {
|
||||
public:
|
||||
Indexer(et::event_loop& loop,
|
||||
Workspace& workspace,
|
||||
llvm::DenseMap<std::uint32_t, Session>& sessions,
|
||||
WorkerPool& pool,
|
||||
Compiler& compiler,
|
||||
std::function<bool(std::uint32_t)> is_file_open = {}) :
|
||||
loop(loop), workspace(workspace), sessions(sessions), pool(pool), compiler(compiler),
|
||||
is_file_open(std::move(is_file_open)) {}
|
||||
|
||||
/// Add a file to the background indexing queue.
|
||||
void enqueue(std::uint32_t server_path_id);
|
||||
|
||||
/// Schedule background indexing (respects idle timeout and dedup).
|
||||
void schedule();
|
||||
|
||||
/// Merge a TUIndex result into Workspace's ProjectIndex and MergedIndex shards.
|
||||
void merge(const void* tu_index_data, std::size_t size);
|
||||
|
||||
/// Save Workspace's ProjectIndex and MergedIndex shards to disk.
|
||||
void save(llvm::StringRef index_dir);
|
||||
|
||||
/// Load Workspace's ProjectIndex and MergedIndex shards from disk.
|
||||
void load(llvm::StringRef index_dir);
|
||||
|
||||
/// Check whether a file needs re-indexing (stale or missing shard).
|
||||
bool need_update(llvm::StringRef file_path);
|
||||
|
||||
/// Query relations (Definition, Reference, etc.) for a symbol at cursor.
|
||||
/// @param session Active Session for this file, or nullptr to use MergedIndex only.
|
||||
std::vector<protocol::Location> query_relations(llvm::StringRef path,
|
||||
const protocol::Position& position,
|
||||
RelationKind kind,
|
||||
Session* session);
|
||||
|
||||
/// Look up symbol info (hash, name, kind, range) at a cursor position.
|
||||
/// @param session Active Session for this file, or nullptr.
|
||||
std::optional<SymbolInfo> lookup_symbol(const std::string& uri,
|
||||
llvm::StringRef path,
|
||||
const protocol::Position& position,
|
||||
Session* session);
|
||||
|
||||
/// Find the definition location of a symbol by hash.
|
||||
std::optional<protocol::Location> find_definition_location(index::SymbolHash hash);
|
||||
|
||||
/// Find a symbol's name and kind by hash.
|
||||
bool find_symbol_info(index::SymbolHash hash, std::string& name, SymbolKind& kind) const;
|
||||
|
||||
/// Resolve a hierarchy item (from stored data or by position lookup).
|
||||
/// @param session Active Session for this file, or nullptr.
|
||||
std::optional<SymbolInfo> resolve_hierarchy_item(const std::string& uri,
|
||||
llvm::StringRef path,
|
||||
const protocol::Range& range,
|
||||
const std::optional<protocol::LSPAny>& data,
|
||||
Session* session);
|
||||
|
||||
/// Find incoming calls to a function.
|
||||
std::vector<protocol::CallHierarchyIncomingCall> find_incoming_calls(index::SymbolHash hash);
|
||||
|
||||
/// Find outgoing calls from a function.
|
||||
std::vector<protocol::CallHierarchyOutgoingCall> find_outgoing_calls(index::SymbolHash hash);
|
||||
|
||||
/// Find supertypes (base classes) of a type.
|
||||
std::vector<protocol::TypeHierarchyItem> find_supertypes(index::SymbolHash hash);
|
||||
|
||||
/// Find subtypes (derived classes) of a type.
|
||||
std::vector<protocol::TypeHierarchyItem> find_subtypes(index::SymbolHash hash);
|
||||
|
||||
/// Search symbols by name substring.
|
||||
std::vector<protocol::SymbolInformation> search_symbols(llvm::StringRef query,
|
||||
std::size_t max_results = 100);
|
||||
|
||||
/// Convert internal SymbolKind to LSP SymbolKind.
|
||||
static protocol::SymbolKind to_lsp_symbol_kind(SymbolKind kind);
|
||||
|
||||
/// Build hierarchy items from SymbolInfo.
|
||||
static protocol::CallHierarchyItem build_call_hierarchy_item(const SymbolInfo& info);
|
||||
static protocol::TypeHierarchyItem build_type_hierarchy_item(const SymbolInfo& info);
|
||||
|
||||
private:
|
||||
/// Result of resolving a symbol at a cursor position.
|
||||
struct CursorHit {
|
||||
index::SymbolHash hash = 0;
|
||||
protocol::Range range{};
|
||||
};
|
||||
|
||||
/// Resolve the symbol at (position), checking Session's file_index first
|
||||
/// then falling back to Workspace's MergedIndex.
|
||||
CursorHit resolve_cursor(llvm::StringRef path,
|
||||
const protocol::Position& position,
|
||||
Session* session);
|
||||
|
||||
/// Collect relations grouped by target symbol, across all index sources.
|
||||
void collect_grouped_relations(
|
||||
index::SymbolHash hash,
|
||||
RelationKind kind,
|
||||
llvm::DenseMap<index::SymbolHash, std::vector<protocol::Range>>& target_ranges);
|
||||
|
||||
/// Collect unique target symbol hashes for a relation kind.
|
||||
void collect_unique_targets(index::SymbolHash hash,
|
||||
RelationKind kind,
|
||||
llvm::SmallVectorImpl<index::SymbolHash>& targets);
|
||||
|
||||
/// Resolve a symbol hash into a SymbolInfo with definition location.
|
||||
std::optional<SymbolInfo> resolve_symbol(index::SymbolHash hash);
|
||||
|
||||
/// Check whether a project-level path_id has an active Session.
|
||||
bool is_proj_path_open(std::uint32_t proj_path_id) const {
|
||||
return is_file_open && is_file_open(proj_path_id);
|
||||
}
|
||||
|
||||
private:
|
||||
et::event_loop& loop;
|
||||
Workspace& workspace;
|
||||
llvm::DenseMap<std::uint32_t, Session>& sessions;
|
||||
WorkerPool& pool;
|
||||
Compiler& compiler;
|
||||
|
||||
/// Callback that checks if a *project-level* path_id has an active
|
||||
/// Session. Set by the owner (e.g. MasterServer) to bridge the
|
||||
/// server-path-id-keyed sessions map to project-level path_ids.
|
||||
std::function<bool(std::uint32_t)> is_file_open;
|
||||
|
||||
/// Background indexing queue and scheduling state.
|
||||
std::vector<std::uint32_t> index_queue;
|
||||
std::size_t index_queue_pos = 0;
|
||||
bool indexing_active = false;
|
||||
bool indexing_scheduled = false;
|
||||
std::shared_ptr<et::timer> index_idle_timer;
|
||||
|
||||
et::task<> run_background_indexing();
|
||||
};
|
||||
|
||||
} // namespace clice
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,39 +3,22 @@
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "command/command.h"
|
||||
#include "eventide/async/async.h"
|
||||
#include "eventide/ipc/lsp/protocol.h"
|
||||
#include "eventide/ipc/peer.h"
|
||||
#include "eventide/serde/serde/raw_value.h"
|
||||
#include "index/merged_index.h"
|
||||
#include "index/project_index.h"
|
||||
#include "semantic/relation_kind.h"
|
||||
#include "server/compile_graph.h"
|
||||
#include "server/config.h"
|
||||
#include "server/compiler.h"
|
||||
#include "server/indexer.h"
|
||||
#include "server/session.h"
|
||||
#include "server/worker_pool.h"
|
||||
#include "support/path_pool.h"
|
||||
#include "syntax/dependency_graph.h"
|
||||
#include "server/workspace.h"
|
||||
|
||||
#include "llvm/ADT/DenseMap.h"
|
||||
#include "llvm/ADT/SmallVector.h"
|
||||
#include "llvm/ADT/StringMap.h"
|
||||
#include "llvm/ADT/StringRef.h"
|
||||
|
||||
namespace clice {
|
||||
|
||||
namespace et = eventide;
|
||||
namespace protocol = et::ipc::protocol;
|
||||
|
||||
struct DocumentState {
|
||||
int version = 0;
|
||||
std::string text;
|
||||
std::uint64_t generation = 0;
|
||||
bool build_running = false;
|
||||
bool build_requested = false;
|
||||
bool drain_scheduled = false;
|
||||
};
|
||||
|
||||
enum class ServerLifecycle : std::uint8_t {
|
||||
Uninitialized,
|
||||
@@ -45,6 +28,20 @@ enum class ServerLifecycle : std::uint8_t {
|
||||
Exited,
|
||||
};
|
||||
|
||||
/// Top-level LSP server — the single orchestration point for the language
|
||||
/// server process.
|
||||
///
|
||||
/// Responsibilities:
|
||||
/// - Owns the two-layer state model: Workspace (disk truth) and Sessions
|
||||
/// (per-open-file volatile state).
|
||||
/// - Manages Session lifecycle directly: didOpen creates, didChange mutates,
|
||||
/// didSave syncs to Workspace, didClose destroys.
|
||||
/// - Dispatches compilation and feature queries to Compiler.
|
||||
/// - Dispatches index lookups and background indexing to Indexer.
|
||||
///
|
||||
/// Design principle:
|
||||
/// Open files are never depended upon by other files. Dependencies always
|
||||
/// point to disk files. The only path from Session to Workspace is didSave.
|
||||
class MasterServer {
|
||||
public:
|
||||
MasterServer(et::event_loop& loop, et::ipc::JsonPeer& peer, std::string self_path);
|
||||
@@ -55,167 +52,31 @@ public:
|
||||
private:
|
||||
et::event_loop& loop;
|
||||
et::ipc::JsonPeer& peer;
|
||||
WorkerPool pool;
|
||||
PathPool path_pool;
|
||||
ServerLifecycle lifecycle = ServerLifecycle::Uninitialized;
|
||||
|
||||
/// Persistent project-wide state (config, CDB, path pool, dependency
|
||||
/// graphs, compilation caches, symbol index).
|
||||
Workspace workspace;
|
||||
|
||||
/// Per-file editing sessions, keyed by server-level path_id.
|
||||
llvm::DenseMap<std::uint32_t, Session> sessions;
|
||||
|
||||
/// Worker process pool for offloading compilation and queries.
|
||||
WorkerPool pool;
|
||||
|
||||
/// Compilation lifecycle manager (reads/writes workspace and sessions).
|
||||
Compiler compiler;
|
||||
|
||||
/// Index query and background scheduling (reads from workspace and sessions).
|
||||
Indexer indexer;
|
||||
|
||||
ServerLifecycle lifecycle = ServerLifecycle::Uninitialized;
|
||||
std::string self_path;
|
||||
std::string workspace_root;
|
||||
CliceConfig config;
|
||||
std::string session_log_dir;
|
||||
|
||||
CompilationDatabase cdb;
|
||||
DependencyGraph dependency_graph;
|
||||
|
||||
// Module compilation graph (lazy dependency resolution).
|
||||
std::unique_ptr<CompileGraph> compile_graph;
|
||||
|
||||
// path_id -> built PCM output path (set after successful module build).
|
||||
llvm::DenseMap<std::uint32_t, std::string> pcm_paths;
|
||||
|
||||
// path_id -> module name (for files that provide a module interface).
|
||||
llvm::DenseMap<std::uint32_t, std::string> path_to_module;
|
||||
|
||||
// path_id -> built PCH file path.
|
||||
llvm::DenseMap<std::uint32_t, std::string> pch_paths;
|
||||
|
||||
// path_id -> preamble bound (byte offset) used when building the PCH.
|
||||
llvm::DenseMap<std::uint32_t, std::uint32_t> pch_bounds;
|
||||
|
||||
// path_id -> hash of preamble content at PCH build time (for staleness detection).
|
||||
llvm::DenseMap<std::uint32_t, std::uint64_t> pch_hashes;
|
||||
|
||||
// path_id -> in-flight PCH build event (later arrivals co_await the same build).
|
||||
llvm::DenseMap<std::uint32_t, std::shared_ptr<et::event>> pch_building;
|
||||
|
||||
// === Index state ===
|
||||
|
||||
// Global symbol table and path mapping for the project.
|
||||
index::ProjectIndex project_index;
|
||||
|
||||
// Per-file merged index shards (keyed by project-level path_id).
|
||||
llvm::DenseMap<std::uint32_t, index::MergedIndex> merged_indices;
|
||||
|
||||
// Files queued for background indexing (server-level path_ids from CDB).
|
||||
std::vector<std::uint32_t> index_queue;
|
||||
|
||||
// Index of next file to process in index_queue.
|
||||
std::size_t index_queue_pos = 0;
|
||||
|
||||
// Whether background indexing is currently in progress.
|
||||
bool indexing_active = false;
|
||||
|
||||
// Whether a background indexing coroutine has been scheduled (waiting on timer).
|
||||
bool indexing_scheduled = false;
|
||||
|
||||
// Timer for idle-triggered background indexing.
|
||||
std::shared_ptr<et::timer> index_idle_timer;
|
||||
|
||||
// Document state: path_id -> DocumentState
|
||||
llvm::DenseMap<std::uint32_t, DocumentState> documents;
|
||||
|
||||
// Per-document debounce timers (shared_ptr so drain coroutines survive didClose)
|
||||
llvm::DenseMap<std::uint32_t, std::shared_ptr<et::timer>> debounce_timers;
|
||||
|
||||
// Helper: convert URI to file path
|
||||
std::string uri_to_path(const std::string& uri);
|
||||
|
||||
// Publish diagnostics to client
|
||||
void publish_diagnostics(const std::string& uri,
|
||||
int version,
|
||||
const eventide::serde::RawValue& diagnostics_json);
|
||||
void clear_diagnostics(const std::string& uri);
|
||||
|
||||
// Schedule a build after debounce
|
||||
void schedule_build(std::uint32_t path_id, const std::string& uri);
|
||||
|
||||
// Build drain coroutine: waits for debounce, then runs compile loop
|
||||
et::task<> run_build_drain(std::uint32_t path_id, std::string uri);
|
||||
|
||||
// Ensure a file has been compiled before servicing feature requests
|
||||
et::task<bool> ensure_compiled(std::uint32_t path_id, const std::string& uri);
|
||||
|
||||
// Load CDB and build initial include graph
|
||||
et::task<> load_workspace();
|
||||
|
||||
// Helper: fill compile arguments from CDB into worker params
|
||||
bool fill_compile_args(llvm::StringRef path,
|
||||
std::string& directory,
|
||||
std::vector<std::string>& arguments);
|
||||
|
||||
// Build or reuse PCH for a source file. Returns true if PCH is available.
|
||||
et::task<bool> ensure_pch(std::uint32_t path_id,
|
||||
llvm::StringRef path,
|
||||
const std::string& text,
|
||||
const std::string& directory,
|
||||
const std::vector<std::string>& arguments);
|
||||
|
||||
// Schedule background indexing when idle.
|
||||
void schedule_indexing();
|
||||
|
||||
// Background indexing coroutine: picks files from queue and dispatches to workers.
|
||||
et::task<> run_background_indexing();
|
||||
|
||||
// Merge a TUIndex result into ProjectIndex and MergedIndex shards.
|
||||
void merge_index_result(const void* tu_index_data, std::size_t size);
|
||||
|
||||
// Persist index state to disk.
|
||||
void save_index();
|
||||
|
||||
// Load index state from disk.
|
||||
void load_index();
|
||||
|
||||
// Forwarding helpers for feature requests (RawValue passthrough)
|
||||
using RawResult = et::task<et::serde::RawValue, et::ipc::Error>;
|
||||
|
||||
/// Forward a simple stateful request (path-only worker params).
|
||||
template <typename WorkerParams>
|
||||
RawResult forward_stateful(const std::string& uri);
|
||||
|
||||
/// Forward a stateful request with position-to-offset conversion.
|
||||
template <typename WorkerParams>
|
||||
RawResult forward_stateful(const std::string& uri, const protocol::Position& position);
|
||||
|
||||
/// Forward a stateless request with document content and compile args.
|
||||
template <typename WorkerParams>
|
||||
RawResult forward_stateless(const std::string& uri, const protocol::Position& position);
|
||||
|
||||
/// Query index for symbol relations (GoToDefinition, FindReferences, etc.).
|
||||
/// Returns LSP Location array as RawValue.
|
||||
RawResult query_index_relations(const std::string& uri,
|
||||
const protocol::Position& position,
|
||||
RelationKind kind);
|
||||
|
||||
/// Information about a symbol at a given position.
|
||||
struct SymbolInfo {
|
||||
index::SymbolHash hash = 0;
|
||||
std::string name;
|
||||
SymbolKind kind;
|
||||
std::string uri;
|
||||
protocol::Range range;
|
||||
};
|
||||
|
||||
/// Look up a symbol at a position, returning its hash, name, kind, and range.
|
||||
et::task<std::optional<SymbolInfo>>
|
||||
lookup_symbol_at_position(const std::string& uri, const protocol::Position& position);
|
||||
|
||||
/// Find the definition location (uri + range) of a symbol by its hash.
|
||||
std::optional<protocol::Location> find_symbol_definition_location(index::SymbolHash hash);
|
||||
|
||||
/// Convert clice::SymbolKind to LSP protocol::SymbolKind.
|
||||
static protocol::SymbolKind to_lsp_symbol_kind(SymbolKind kind);
|
||||
|
||||
/// Build a CallHierarchyItem from a SymbolInfo.
|
||||
protocol::CallHierarchyItem build_call_hierarchy_item(const SymbolInfo& info);
|
||||
|
||||
/// Build a TypeHierarchyItem from a SymbolInfo.
|
||||
protocol::TypeHierarchyItem build_type_hierarchy_item(const SymbolInfo& info);
|
||||
|
||||
/// Resolve SymbolInfo from a hierarchy item's stored data (symbol hash).
|
||||
/// Falls back to position-based lookup if data is missing.
|
||||
et::task<std::optional<SymbolInfo>>
|
||||
resolve_hierarchy_item(const std::string& uri,
|
||||
const protocol::Range& range,
|
||||
const std::optional<protocol::LSPAny>& data);
|
||||
};
|
||||
|
||||
} // namespace clice
|
||||
|
||||
@@ -10,13 +10,34 @@
|
||||
#include "eventide/ipc/lsp/protocol.h"
|
||||
#include "eventide/ipc/protocol.h"
|
||||
#include "eventide/serde/serde/raw_value.h"
|
||||
#include "syntax/token.h"
|
||||
|
||||
namespace clice::worker {
|
||||
|
||||
namespace protocol = eventide::ipc::protocol;
|
||||
|
||||
// === StatefulWorker Requests ===
|
||||
/// Kind of AST query dispatched to a stateful worker.
|
||||
enum class QueryKind : uint8_t {
|
||||
Hover,
|
||||
GoToDefinition,
|
||||
SemanticTokens,
|
||||
InlayHints,
|
||||
FoldingRange,
|
||||
DocumentSymbol,
|
||||
DocumentLink,
|
||||
CodeAction,
|
||||
};
|
||||
|
||||
/// Unified parameters for all stateful AST queries.
|
||||
/// The worker dispatches to the appropriate feature handler based on `kind`.
|
||||
struct QueryParams {
|
||||
QueryKind kind;
|
||||
std::string path;
|
||||
uint32_t offset = 0; ///< Byte offset for position-sensitive queries (Hover, GoToDefinition).
|
||||
LocalSourceRange range; ///< Byte range for range-sensitive queries (InlayHints).
|
||||
};
|
||||
|
||||
/// Parameters for stateful compilation (builds AST, publishes diagnostics).
|
||||
struct CompileParams {
|
||||
std::string path;
|
||||
int version;
|
||||
@@ -32,113 +53,62 @@ struct CompileResult {
|
||||
/// Diagnostics serialized as JSON (RawValue) to avoid bincode/serde annotation conflicts.
|
||||
eventide::serde::RawValue diagnostics;
|
||||
std::size_t memory_usage;
|
||||
};
|
||||
|
||||
struct HoverParams {
|
||||
std::string path;
|
||||
uint32_t offset;
|
||||
};
|
||||
|
||||
struct SemanticTokensParams {
|
||||
std::string path;
|
||||
};
|
||||
|
||||
struct InlayHintsParams {
|
||||
std::string path;
|
||||
};
|
||||
|
||||
struct FoldingRangeParams {
|
||||
std::string path;
|
||||
};
|
||||
|
||||
struct DocumentSymbolParams {
|
||||
std::string path;
|
||||
};
|
||||
|
||||
struct DocumentLinkParams {
|
||||
std::string path;
|
||||
};
|
||||
|
||||
struct CodeActionParams {
|
||||
std::string path;
|
||||
};
|
||||
|
||||
struct GoToDefinitionParams {
|
||||
std::string path;
|
||||
uint32_t offset;
|
||||
};
|
||||
|
||||
// === StatelessWorker Requests ===
|
||||
|
||||
struct CompletionParams {
|
||||
std::string path;
|
||||
int version;
|
||||
std::string text;
|
||||
std::string directory;
|
||||
std::vector<std::string> arguments;
|
||||
std::pair<std::string, uint32_t> pch;
|
||||
std::unordered_map<std::string, std::string> pcms;
|
||||
uint32_t offset;
|
||||
};
|
||||
|
||||
struct SignatureHelpParams {
|
||||
std::string path;
|
||||
int version;
|
||||
std::string text;
|
||||
std::string directory;
|
||||
std::vector<std::string> arguments;
|
||||
std::pair<std::string, uint32_t> pch;
|
||||
std::unordered_map<std::string, std::string> pcms;
|
||||
uint32_t offset;
|
||||
};
|
||||
|
||||
struct BuildPCHParams {
|
||||
std::string file;
|
||||
std::string directory;
|
||||
std::vector<std::string> arguments;
|
||||
std::string content;
|
||||
std::uint32_t preamble_bound = UINT32_MAX;
|
||||
};
|
||||
|
||||
struct BuildPCHResult {
|
||||
bool success;
|
||||
std::string error;
|
||||
std::string pch_path;
|
||||
};
|
||||
|
||||
struct BuildPCMParams {
|
||||
std::string file;
|
||||
std::string directory;
|
||||
std::vector<std::string> arguments;
|
||||
std::string module_name;
|
||||
std::unordered_map<std::string, std::string> pcms;
|
||||
};
|
||||
|
||||
struct BuildPCMResult {
|
||||
bool success;
|
||||
std::string error;
|
||||
std::string pcm_path;
|
||||
};
|
||||
|
||||
struct IndexParams {
|
||||
std::string file;
|
||||
std::string directory;
|
||||
std::vector<std::string> arguments;
|
||||
std::unordered_map<std::string, std::string> pcms;
|
||||
};
|
||||
|
||||
struct IndexResult {
|
||||
bool success;
|
||||
std::string error;
|
||||
std::vector<std::string> deps;
|
||||
/// Serialized TUIndex for the main file (interested_only=true).
|
||||
std::string tu_index_data;
|
||||
};
|
||||
|
||||
// === Notifications ===
|
||||
/// Kind of build task dispatched to a stateless worker.
|
||||
enum class BuildKind : uint8_t {
|
||||
BuildPCH,
|
||||
BuildPCM,
|
||||
Index,
|
||||
Completion,
|
||||
SignatureHelp,
|
||||
};
|
||||
|
||||
/// Unified parameters for all stateless build/compilation tasks.
|
||||
/// Fields are used selectively based on `kind`:
|
||||
/// - All: file, directory, arguments
|
||||
/// - BuildPCH: + content, preamble_bound, output_path
|
||||
/// - BuildPCM: + module_name, pcms, output_path
|
||||
/// - Index: + pcms
|
||||
/// - Completion: + text, version, offset, pch, pcms
|
||||
/// - SignatureHelp: + text, version, offset, pch, pcms
|
||||
struct BuildParams {
|
||||
BuildKind kind;
|
||||
std::string file;
|
||||
std::string directory;
|
||||
std::vector<std::string> arguments;
|
||||
|
||||
/// Source text for Completion/SignatureHelp, preamble content for BuildPCH.
|
||||
std::string text;
|
||||
int version = 0;
|
||||
uint32_t offset = 0;
|
||||
std::pair<std::string, uint32_t> pch;
|
||||
std::unordered_map<std::string, std::string> pcms;
|
||||
|
||||
std::string output_path; ///< BuildPCH, BuildPCM
|
||||
std::string module_name; ///< BuildPCM
|
||||
uint32_t preamble_bound = UINT32_MAX; ///< BuildPCH
|
||||
};
|
||||
|
||||
/// Unified result for stateless build tasks.
|
||||
/// For Completion/SignatureHelp, the result JSON is in `result_json`.
|
||||
/// For BuildPCH/BuildPCM/Index, structured fields are used.
|
||||
struct BuildResult {
|
||||
bool success = true;
|
||||
std::string error;
|
||||
std::string output_path; ///< PCH or PCM path
|
||||
std::vector<std::string> deps;
|
||||
std::string tu_index_data;
|
||||
std::string pch_links_json; ///< Pre-serialized DocumentLink[] from PCH
|
||||
eventide::serde::RawValue result_json; ///< Completion/SignatureHelp result
|
||||
};
|
||||
|
||||
struct DocumentUpdateParams {
|
||||
std::string path;
|
||||
int version;
|
||||
std::string text;
|
||||
};
|
||||
|
||||
struct EvictParams {
|
||||
@@ -151,9 +121,44 @@ struct EvictedParams {
|
||||
|
||||
} // namespace clice::worker
|
||||
|
||||
namespace eventide::ipc::protocol {
|
||||
namespace clice::ext {
|
||||
|
||||
// === Stateful Requests ===
|
||||
struct ContextItem {
|
||||
std::string label;
|
||||
std::string description;
|
||||
std::string uri;
|
||||
};
|
||||
|
||||
struct QueryContextParams {
|
||||
std::string uri;
|
||||
std::optional<int> offset;
|
||||
};
|
||||
|
||||
struct QueryContextResult {
|
||||
std::vector<ContextItem> contexts;
|
||||
int total;
|
||||
};
|
||||
|
||||
struct CurrentContextParams {
|
||||
std::string uri;
|
||||
};
|
||||
|
||||
struct CurrentContextResult {
|
||||
std::optional<ContextItem> context;
|
||||
};
|
||||
|
||||
struct SwitchContextParams {
|
||||
std::string uri;
|
||||
std::string context_uri;
|
||||
};
|
||||
|
||||
struct SwitchContextResult {
|
||||
bool success;
|
||||
};
|
||||
|
||||
} // namespace clice::ext
|
||||
|
||||
namespace eventide::ipc::protocol {
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::worker::CompileParams> {
|
||||
@@ -162,87 +167,17 @@ struct RequestTraits<clice::worker::CompileParams> {
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::worker::HoverParams> {
|
||||
struct RequestTraits<clice::worker::QueryParams> {
|
||||
using Result = eventide::serde::RawValue;
|
||||
constexpr inline static std::string_view method = "clice/worker/hover";
|
||||
constexpr inline static std::string_view method = "clice/worker/query";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::worker::SemanticTokensParams> {
|
||||
using Result = eventide::serde::RawValue;
|
||||
constexpr inline static std::string_view method = "clice/worker/semanticTokens";
|
||||
struct RequestTraits<clice::worker::BuildParams> {
|
||||
using Result = clice::worker::BuildResult;
|
||||
constexpr inline static std::string_view method = "clice/worker/build";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::worker::InlayHintsParams> {
|
||||
using Result = eventide::serde::RawValue;
|
||||
constexpr inline static std::string_view method = "clice/worker/inlayHints";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::worker::FoldingRangeParams> {
|
||||
using Result = eventide::serde::RawValue;
|
||||
constexpr inline static std::string_view method = "clice/worker/foldingRange";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::worker::DocumentSymbolParams> {
|
||||
using Result = eventide::serde::RawValue;
|
||||
constexpr inline static std::string_view method = "clice/worker/documentSymbol";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::worker::DocumentLinkParams> {
|
||||
using Result = eventide::serde::RawValue;
|
||||
constexpr inline static std::string_view method = "clice/worker/documentLink";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::worker::CodeActionParams> {
|
||||
using Result = eventide::serde::RawValue;
|
||||
constexpr inline static std::string_view method = "clice/worker/codeAction";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::worker::GoToDefinitionParams> {
|
||||
using Result = eventide::serde::RawValue;
|
||||
constexpr inline static std::string_view method = "clice/worker/goToDefinition";
|
||||
};
|
||||
|
||||
// === Stateless Requests ===
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::worker::CompletionParams> {
|
||||
using Result = eventide::serde::RawValue;
|
||||
constexpr inline static std::string_view method = "clice/worker/completion";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::worker::SignatureHelpParams> {
|
||||
using Result = eventide::serde::RawValue;
|
||||
constexpr inline static std::string_view method = "clice/worker/signatureHelp";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::worker::BuildPCHParams> {
|
||||
using Result = clice::worker::BuildPCHResult;
|
||||
constexpr inline static std::string_view method = "clice/worker/buildPCH";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::worker::BuildPCMParams> {
|
||||
using Result = clice::worker::BuildPCMResult;
|
||||
constexpr inline static std::string_view method = "clice/worker/buildPCM";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::worker::IndexParams> {
|
||||
using Result = clice::worker::IndexResult;
|
||||
constexpr inline static std::string_view method = "clice/worker/index";
|
||||
};
|
||||
|
||||
// === Notifications ===
|
||||
|
||||
template <>
|
||||
struct NotificationTraits<clice::worker::DocumentUpdateParams> {
|
||||
constexpr inline static std::string_view method = "clice/worker/documentUpdate";
|
||||
|
||||
84
src/server/session.h
Normal file
84
src/server/session.h
Normal file
@@ -0,0 +1,84 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#include "eventide/async/async.h"
|
||||
#include "server/workspace.h"
|
||||
|
||||
#include "llvm/ADT/SmallVector.h"
|
||||
|
||||
namespace clice {
|
||||
|
||||
namespace et = eventide;
|
||||
|
||||
/// An editing session for a single file opened in the editor.
|
||||
///
|
||||
/// Design principle: open files are never depended upon by other files.
|
||||
/// Dependencies always point to disk files. The only path from Session
|
||||
/// to Workspace is didSave, which tells Workspace to rescan the disk file.
|
||||
///
|
||||
/// Created on didOpen, destroyed on didClose. All fields are local to this
|
||||
/// file's translation unit and NEVER leak to Workspace or other Sessions.
|
||||
/// Sessions may READ from Workspace (e.g. to obtain PCH/PCM paths, module
|
||||
/// mappings, include graph) but all compilation results stay here.
|
||||
struct Session {
|
||||
/// Path ID of this file in PathPool. Set on creation, never changes.
|
||||
std::uint32_t path_id = 0;
|
||||
|
||||
/// LSP document version, incremented by the client on each edit.
|
||||
int version = 0;
|
||||
|
||||
/// Current buffer content (may differ from disk until saved).
|
||||
std::string text;
|
||||
|
||||
/// Monotonic generation counter, incremented on every didChange.
|
||||
/// Used to detect stale compilation results (ABA prevention).
|
||||
std::uint64_t generation = 0;
|
||||
|
||||
/// Whether the AST needs to be rebuilt before serving queries.
|
||||
bool ast_dirty = true;
|
||||
|
||||
/// Non-null while a compilation is in flight for this file.
|
||||
/// Other queries wait on the event; the compilation task itself
|
||||
/// runs independently and cannot be cancelled by LSP $/cancelRequest.
|
||||
struct PendingCompile {
|
||||
et::event done;
|
||||
bool succeeded = false;
|
||||
};
|
||||
|
||||
std::shared_ptr<PendingCompile> compiling;
|
||||
|
||||
/// Reference to the PCH entry in Workspace.pch_cache, if any.
|
||||
/// The PCH itself is owned by Workspace (shared, content-addressed);
|
||||
/// Session only stores enough to locate and validate it.
|
||||
struct PCHRef {
|
||||
std::uint32_t path_id = 0; ///< Key into Workspace.pch_cache.
|
||||
std::uint64_t hash = 0; ///< Preamble hash at build time.
|
||||
std::uint32_t bound = 0; ///< Preamble byte boundary.
|
||||
};
|
||||
|
||||
std::optional<PCHRef> pch_ref;
|
||||
|
||||
/// Dependency snapshot from the last successful AST compilation.
|
||||
/// Used for two-layer staleness detection (mtime + content hash).
|
||||
std::optional<DepsSnapshot> ast_deps;
|
||||
|
||||
/// Compilation context for header files that lack their own CDB entry.
|
||||
/// Stores the host source file and synthesized preamble for this header.
|
||||
std::optional<HeaderFileContext> header_context;
|
||||
|
||||
/// User-selected compilation context override (via clice/switchContext).
|
||||
/// When set, overrides automatic header context resolution.
|
||||
std::optional<std::uint32_t> active_context;
|
||||
|
||||
/// Symbol index built from the latest compilation of this file's buffer.
|
||||
/// Used for queries (hover, goto, references) on this file.
|
||||
/// NOT merged into Workspace.project_index — that only gets disk-derived
|
||||
/// data from background indexing.
|
||||
std::optional<OpenFileIndex> file_index;
|
||||
};
|
||||
|
||||
} // namespace clice
|
||||
@@ -1,7 +1,6 @@
|
||||
#include "server/stateful_worker.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <list>
|
||||
#include <memory>
|
||||
@@ -10,16 +9,16 @@
|
||||
|
||||
#include "compile/compilation.h"
|
||||
#include "eventide/async/async.h"
|
||||
#include "eventide/ipc/json_codec.h"
|
||||
#include "eventide/ipc/peer.h"
|
||||
#include "eventide/ipc/transport.h"
|
||||
#include "eventide/serde/json/serializer.h"
|
||||
#include "eventide/serde/serde/raw_value.h"
|
||||
#include "feature/feature.h"
|
||||
#include "index/tu_index.h"
|
||||
#include "server/protocol.h"
|
||||
#include "server/worker_common.h"
|
||||
#include "support/logging.h"
|
||||
|
||||
#include "llvm/ADT/StringMap.h"
|
||||
#include "llvm/Support/raw_ostream.h"
|
||||
|
||||
namespace clice {
|
||||
|
||||
@@ -48,37 +47,11 @@ struct DocumentEntry {
|
||||
et::mutex strand;
|
||||
};
|
||||
|
||||
struct ScopedTimer {
|
||||
std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();
|
||||
|
||||
long long ms() const {
|
||||
return std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - start)
|
||||
.count();
|
||||
}
|
||||
};
|
||||
|
||||
static void fill_args(CompilationParams& cp,
|
||||
const std::string& directory,
|
||||
const std::vector<std::string>& arguments) {
|
||||
cp.directory = directory;
|
||||
for(auto& arg: arguments) {
|
||||
cp.arguments.push_back(arg.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
/// Serialize any value to LSP JSON RawValue.
|
||||
template <typename T>
|
||||
static et::serde::RawValue to_raw(const T& value) {
|
||||
auto json = et::serde::json::to_json<et::ipc::lsp_config>(value);
|
||||
return et::serde::RawValue{json ? std::move(*json) : "null"};
|
||||
}
|
||||
|
||||
class StatefulWorker {
|
||||
et::ipc::BincodePeer& peer;
|
||||
std::uint64_t memory_limit;
|
||||
|
||||
llvm::StringMap<std::unique_ptr<DocumentEntry>> documents;
|
||||
llvm::StringMap<std::shared_ptr<DocumentEntry>> documents;
|
||||
|
||||
// LRU tracking — owns keys so they don't dangle after request handler returns
|
||||
std::list<std::string> lru;
|
||||
@@ -106,13 +79,13 @@ class StatefulWorker {
|
||||
}
|
||||
}
|
||||
|
||||
DocumentEntry& get_or_create(llvm::StringRef path) {
|
||||
std::shared_ptr<DocumentEntry> get_or_create(llvm::StringRef path) {
|
||||
auto [it, inserted] = documents.try_emplace(path, nullptr);
|
||||
if(inserted) {
|
||||
it->second = std::make_unique<DocumentEntry>();
|
||||
it->second = std::make_shared<DocumentEntry>();
|
||||
LOG_DEBUG("Created new document entry: {}", path.str());
|
||||
}
|
||||
return *it->second;
|
||||
return it->second;
|
||||
}
|
||||
|
||||
/// Look up document, wait for AST, lock strand, run fn(doc) on thread pool, unlock.
|
||||
@@ -120,22 +93,24 @@ class StatefulWorker {
|
||||
template <typename F>
|
||||
et::task<et::serde::RawValue> with_ast(llvm::StringRef path, F&& fn) {
|
||||
auto it = documents.find(path);
|
||||
if(it == documents.end())
|
||||
if(it == documents.end()) {
|
||||
co_return et::serde::RawValue{"null"};
|
||||
}
|
||||
|
||||
auto& doc = *it->second;
|
||||
// Hold shared_ptr so Evict can't destroy the entry mid-request.
|
||||
auto doc = it->second;
|
||||
touch_lru(path);
|
||||
|
||||
co_await doc.ast_ready.wait();
|
||||
co_await doc.strand.lock();
|
||||
co_await doc->ast_ready.wait();
|
||||
co_await doc->strand.lock();
|
||||
|
||||
auto result = co_await et::queue([&]() -> et::serde::RawValue {
|
||||
if(!doc.has_ast || (!doc.unit.completed() && !doc.unit.fatal_error()))
|
||||
if(!doc->has_ast || (!doc->unit.completed() && !doc->unit.fatal_error()))
|
||||
return et::serde::RawValue{"null"};
|
||||
return fn(doc);
|
||||
return fn(*doc);
|
||||
});
|
||||
|
||||
doc.strand.unlock();
|
||||
doc->strand.unlock();
|
||||
co_return result.value();
|
||||
}
|
||||
|
||||
@@ -153,68 +128,83 @@ void StatefulWorker::register_handlers() {
|
||||
const worker::CompileParams& params) -> RequestResult<worker::CompileParams> {
|
||||
LOG_INFO("Compile request: path={}, version={}", params.path, params.version);
|
||||
|
||||
auto& doc = get_or_create(params.path);
|
||||
doc.version = params.version;
|
||||
doc.text = params.text;
|
||||
doc.directory = params.directory;
|
||||
doc.arguments = params.arguments;
|
||||
doc.pch = params.pch;
|
||||
doc.pcms.clear();
|
||||
for(auto& [name, pcm_path]: params.pcms) {
|
||||
doc.pcms.try_emplace(name, pcm_path);
|
||||
}
|
||||
|
||||
// Hold shared_ptr so Evict can't destroy the entry mid-compile.
|
||||
auto doc = get_or_create(params.path);
|
||||
touch_lru(params.path);
|
||||
|
||||
co_await doc.strand.lock();
|
||||
co_await doc->strand.lock();
|
||||
|
||||
// Copy params to doc AFTER acquiring the strand lock, so that
|
||||
// concurrent Compile requests waiting on the strand don't
|
||||
// overwrite our fields before we use them.
|
||||
doc->version = params.version;
|
||||
doc->text = params.text;
|
||||
doc->directory = params.directory;
|
||||
doc->arguments = params.arguments;
|
||||
doc->pch = params.pch;
|
||||
doc->pcms.clear();
|
||||
for(auto& [name, pcm_path]: params.pcms) {
|
||||
doc->pcms.try_emplace(name, pcm_path);
|
||||
}
|
||||
|
||||
auto compile_result = co_await et::queue([&]() -> worker::CompileResult {
|
||||
LOG_DEBUG("Compiling: path={}, {} args", params.path, doc.arguments.size());
|
||||
|
||||
ScopedTimer timer;
|
||||
|
||||
CompilationParams cp;
|
||||
cp.kind = CompilationKind::Content;
|
||||
fill_args(cp, doc.directory, doc.arguments);
|
||||
if(!doc.pch.first.empty()) {
|
||||
cp.pch = doc.pch;
|
||||
fill_args(cp, doc->directory, doc->arguments);
|
||||
if(!doc->pch.first.empty()) {
|
||||
cp.pch = doc->pch;
|
||||
}
|
||||
cp.add_remapped_file(params.path, doc.text);
|
||||
for(auto& entry: doc.pcms) {
|
||||
cp.add_remapped_file(params.path, doc->text);
|
||||
for(auto& entry: doc->pcms) {
|
||||
cp.pcms.try_emplace(entry.getKey(), entry.getValue());
|
||||
}
|
||||
|
||||
doc.unit = compile(cp);
|
||||
doc.has_ast = true;
|
||||
doc.dirty.store(false, std::memory_order_release);
|
||||
doc->unit = compile(cp);
|
||||
doc->has_ast = true;
|
||||
doc->dirty.store(false, std::memory_order_release);
|
||||
|
||||
worker::CompileResult result;
|
||||
result.version = doc.version;
|
||||
if(doc.unit.completed() || doc.unit.fatal_error()) {
|
||||
auto diags = feature::diagnostics(doc.unit);
|
||||
result.version = doc->version;
|
||||
if(doc->unit.completed() || doc->unit.fatal_error()) {
|
||||
auto diags = feature::diagnostics(doc->unit);
|
||||
auto json = et::serde::json::to_json<et::ipc::lsp_config>(diags);
|
||||
result.diagnostics = et::serde::RawValue{json ? std::move(*json) : "[]"};
|
||||
LOG_INFO("Compile done: path={}, {}ms, {} diags, fatal={}",
|
||||
params.path,
|
||||
timer.ms(),
|
||||
diags.size(),
|
||||
doc.unit.fatal_error());
|
||||
doc->unit.fatal_error());
|
||||
} else {
|
||||
result.diagnostics = et::serde::RawValue{"[]"};
|
||||
LOG_WARN("Compile incomplete: path={}, {}ms", params.path, timer.ms());
|
||||
}
|
||||
result.memory_usage = 0; // TODO: query actual memory
|
||||
if(doc->unit.completed()) {
|
||||
result.deps = doc->unit.deps();
|
||||
|
||||
// Build index for main file only (interested_only=true).
|
||||
auto tu_index = index::TUIndex::build(doc->unit, true);
|
||||
llvm::raw_string_ostream os(result.tu_index_data);
|
||||
tu_index.serialize(os);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
doc.strand.unlock();
|
||||
doc.ast_ready.set();
|
||||
doc->strand.unlock();
|
||||
doc->ast_ready.set();
|
||||
shrink_if_over_limit();
|
||||
|
||||
co_return compile_result.value();
|
||||
});
|
||||
|
||||
// === DocumentUpdate ===
|
||||
// Only mark the document dirty — do NOT update doc.text or doc.version
|
||||
// here. The et::queue compilation work may be reading doc.text on the
|
||||
// thread pool concurrently, so writing it from the event loop would be
|
||||
// a data race. The next Compile request will bring the correct text
|
||||
// and update it inside the strand lock.
|
||||
peer.on_notification([this](const worker::DocumentUpdateParams& params) {
|
||||
LOG_TRACE("DocumentUpdate: path={}, version={}", params.path, params.version);
|
||||
|
||||
@@ -224,10 +214,7 @@ void StatefulWorker::register_handlers() {
|
||||
return;
|
||||
}
|
||||
|
||||
auto& doc = *it->second;
|
||||
doc.version = params.version;
|
||||
doc.text = params.text;
|
||||
doc.dirty.store(true, std::memory_order_release);
|
||||
it->second->dirty.store(true, std::memory_order_release);
|
||||
});
|
||||
|
||||
// === Evict ===
|
||||
@@ -242,78 +229,58 @@ void StatefulWorker::register_handlers() {
|
||||
documents.erase(params.path);
|
||||
});
|
||||
|
||||
// === Hover ===
|
||||
// === Query (hover, definition, semantic tokens, etc.) ===
|
||||
peer.on_request(
|
||||
[this](RequestContext& ctx,
|
||||
const worker::HoverParams& params) -> RequestResult<worker::HoverParams> {
|
||||
co_return co_await with_ast(params.path, [&](DocumentEntry& doc) {
|
||||
auto result = feature::hover(doc.unit, params.offset);
|
||||
return result ? to_raw(*result) : et::serde::RawValue{"null"};
|
||||
});
|
||||
const worker::QueryParams& params) -> RequestResult<worker::QueryParams> {
|
||||
using K = worker::QueryKind;
|
||||
switch(params.kind) {
|
||||
case K::Hover:
|
||||
co_return co_await with_ast(params.path, [&](DocumentEntry& doc) {
|
||||
auto result = feature::hover(doc.unit, params.offset);
|
||||
return result ? to_raw(*result) : et::serde::RawValue{"null"};
|
||||
});
|
||||
case K::GoToDefinition:
|
||||
// TODO: Implement go-to-definition
|
||||
co_return et::serde::RawValue{"[]"};
|
||||
case K::SemanticTokens:
|
||||
co_return co_await with_ast(params.path, [&](DocumentEntry& doc) {
|
||||
return to_raw(feature::semantic_tokens(doc.unit));
|
||||
});
|
||||
case K::InlayHints:
|
||||
co_return co_await with_ast(params.path, [&](DocumentEntry& doc) {
|
||||
auto range = params.range;
|
||||
if(range.begin == static_cast<uint32_t>(-1))
|
||||
range = LocalSourceRange{0, static_cast<uint32_t>(doc.text.size())};
|
||||
return to_raw(feature::inlay_hints(doc.unit, range));
|
||||
});
|
||||
case K::FoldingRange:
|
||||
co_return co_await with_ast(params.path, [&](DocumentEntry& doc) {
|
||||
return to_raw(feature::folding_ranges(doc.unit));
|
||||
});
|
||||
case K::DocumentSymbol:
|
||||
co_return co_await with_ast(params.path, [&](DocumentEntry& doc) {
|
||||
return to_raw(feature::document_symbols(doc.unit));
|
||||
});
|
||||
case K::DocumentLink:
|
||||
co_return co_await with_ast(params.path, [&](DocumentEntry& doc) {
|
||||
return to_raw(feature::document_links(doc.unit));
|
||||
});
|
||||
case K::CodeAction:
|
||||
// TODO: Implement code actions
|
||||
co_return et::serde::RawValue{"[]"};
|
||||
}
|
||||
co_return et::serde::RawValue{"null"};
|
||||
});
|
||||
|
||||
// === SemanticTokens ===
|
||||
peer.on_request([this](RequestContext& ctx, const worker::SemanticTokensParams& params)
|
||||
-> RequestResult<worker::SemanticTokensParams> {
|
||||
co_return co_await with_ast(params.path, [&](DocumentEntry& doc) {
|
||||
return to_raw(feature::semantic_tokens(doc.unit));
|
||||
});
|
||||
});
|
||||
|
||||
// === InlayHints ===
|
||||
peer.on_request(
|
||||
[this](RequestContext& ctx,
|
||||
const worker::InlayHintsParams& params) -> RequestResult<worker::InlayHintsParams> {
|
||||
co_return co_await with_ast(params.path, [&](DocumentEntry& doc) {
|
||||
LocalSourceRange range{0, static_cast<uint32_t>(doc.text.size())};
|
||||
return to_raw(feature::inlay_hints(doc.unit, range));
|
||||
});
|
||||
});
|
||||
|
||||
// === FoldingRange ===
|
||||
peer.on_request([this](RequestContext& ctx, const worker::FoldingRangeParams& params)
|
||||
-> RequestResult<worker::FoldingRangeParams> {
|
||||
co_return co_await with_ast(params.path, [&](DocumentEntry& doc) {
|
||||
return to_raw(feature::folding_ranges(doc.unit));
|
||||
});
|
||||
});
|
||||
|
||||
// === DocumentSymbol ===
|
||||
peer.on_request([this](RequestContext& ctx, const worker::DocumentSymbolParams& params)
|
||||
-> RequestResult<worker::DocumentSymbolParams> {
|
||||
co_return co_await with_ast(params.path, [&](DocumentEntry& doc) {
|
||||
return to_raw(feature::document_symbols(doc.unit));
|
||||
});
|
||||
});
|
||||
|
||||
// === DocumentLink ===
|
||||
peer.on_request([this](RequestContext& ctx, const worker::DocumentLinkParams& params)
|
||||
-> RequestResult<worker::DocumentLinkParams> {
|
||||
co_return co_await with_ast(params.path, [&](DocumentEntry& doc) {
|
||||
return to_raw(feature::document_links(doc.unit));
|
||||
});
|
||||
});
|
||||
|
||||
// === CodeAction ===
|
||||
peer.on_request(
|
||||
[this](RequestContext& ctx,
|
||||
const worker::CodeActionParams& params) -> RequestResult<worker::CodeActionParams> {
|
||||
LOG_TRACE("CodeAction request: path={}", params.path);
|
||||
// TODO: Implement code actions
|
||||
co_return et::serde::RawValue{"[]"};
|
||||
});
|
||||
|
||||
// === GoToDefinition ===
|
||||
peer.on_request([this](RequestContext& ctx, const worker::GoToDefinitionParams& params)
|
||||
-> RequestResult<worker::GoToDefinitionParams> {
|
||||
LOG_TRACE("GoToDefinition request: path={}, offset={}", params.path, params.offset);
|
||||
// TODO: Implement go-to-definition
|
||||
co_return et::serde::RawValue{"[]"};
|
||||
});
|
||||
}
|
||||
|
||||
int run_stateful_worker_mode(std::uint64_t memory_limit) {
|
||||
logging::stderr_logger("stateful-worker", logging::options);
|
||||
int run_stateful_worker_mode(std::uint64_t memory_limit,
|
||||
const std::string& worker_name,
|
||||
const std::string& log_dir) {
|
||||
logging::stderr_logger(worker_name, logging::options);
|
||||
if(!log_dir.empty()) {
|
||||
logging::file_logger(worker_name, log_dir, logging::options);
|
||||
}
|
||||
|
||||
LOG_INFO("Starting stateful worker, memory_limit={}MB", memory_limit / (1024 * 1024));
|
||||
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace clice {
|
||||
|
||||
/// Run the stateful worker process mode.
|
||||
/// The worker holds compiled ASTs and handles feature requests
|
||||
/// (hover, semantic tokens, etc.) alongside compile requests.
|
||||
int run_stateful_worker_mode(std::uint64_t memory_limit);
|
||||
int run_stateful_worker_mode(std::uint64_t memory_limit,
|
||||
const std::string& worker_name,
|
||||
const std::string& log_dir);
|
||||
|
||||
} // namespace clice
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
#include "server/stateless_worker.h"
|
||||
|
||||
#include <chrono>
|
||||
|
||||
#include "compile/compilation.h"
|
||||
#include "eventide/async/async.h"
|
||||
#include "eventide/ipc/json_codec.h"
|
||||
#include "eventide/ipc/peer.h"
|
||||
#include "eventide/ipc/transport.h"
|
||||
#include "eventide/serde/json/serializer.h"
|
||||
#include "eventide/serde/serde/raw_value.h"
|
||||
#include "feature/feature.h"
|
||||
#include "index/tu_index.h"
|
||||
#include "server/protocol.h"
|
||||
#include "server/worker_common.h"
|
||||
#include "support/logging.h"
|
||||
|
||||
#include "llvm/Support/raw_ostream.h"
|
||||
@@ -22,33 +18,251 @@ namespace et = eventide;
|
||||
using et::ipc::RequestResult;
|
||||
using RequestContext = et::ipc::BincodePeer::RequestContext;
|
||||
|
||||
struct ScopedTimer {
|
||||
std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();
|
||||
|
||||
long long ms() const {
|
||||
return std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - start)
|
||||
.count();
|
||||
/// Extract error messages from compilation diagnostics.
|
||||
static std::string collect_errors(CompilationUnit& unit) {
|
||||
std::string errors;
|
||||
for(auto& diag: unit.diagnostics()) {
|
||||
if(diag.id.level >= DiagnosticLevel::Error) {
|
||||
if(!errors.empty())
|
||||
errors += "; ";
|
||||
errors += diag.message;
|
||||
}
|
||||
}
|
||||
};
|
||||
return errors;
|
||||
}
|
||||
|
||||
static void fill_args(CompilationParams& cp,
|
||||
const std::string& directory,
|
||||
const std::vector<std::string>& arguments) {
|
||||
cp.directory = directory;
|
||||
for(auto& arg: arguments) {
|
||||
cp.arguments.push_back(arg.c_str());
|
||||
/// Build a TUIndex, serialize it, and return as a string.
|
||||
static std::string serialize_tu_index(CompilationUnit& unit, bool interested_only = false) {
|
||||
auto tu_index = index::TUIndex::build(unit, interested_only);
|
||||
if(!interested_only) {
|
||||
tu_index.main_file_index = index::FileIndex();
|
||||
}
|
||||
std::string serialized;
|
||||
llvm::raw_string_ostream os(serialized);
|
||||
tu_index.serialize(os);
|
||||
return serialized;
|
||||
}
|
||||
|
||||
/// Write compilation output to disk, handling tmp+rename pattern.
|
||||
/// Returns the final path on success, or empty string on failure.
|
||||
static std::string finalize_output(const std::string& output_path,
|
||||
const std::string& tmp_path,
|
||||
const std::string& file,
|
||||
const char* label) {
|
||||
if(!output_path.empty()) {
|
||||
auto ec = fs::rename(tmp_path, output_path);
|
||||
if(ec) {
|
||||
return output_path;
|
||||
} else {
|
||||
LOG_WARN("{}: rename {} -> {} failed: {}",
|
||||
label,
|
||||
tmp_path,
|
||||
output_path,
|
||||
ec.error().message());
|
||||
return tmp_path;
|
||||
}
|
||||
}
|
||||
return tmp_path;
|
||||
}
|
||||
|
||||
static worker::BuildResult handle_build_pch(const worker::BuildParams& params) {
|
||||
ScopedTimer timer;
|
||||
|
||||
CompilationParams cp;
|
||||
cp.kind = CompilationKind::Preamble;
|
||||
fill_args(cp, params.directory, params.arguments);
|
||||
cp.add_remapped_file(params.file, params.text, params.preamble_bound);
|
||||
|
||||
std::string tmp_path;
|
||||
bool has_output = !params.output_path.empty();
|
||||
if(has_output) {
|
||||
tmp_path = params.output_path + ".tmp";
|
||||
} else {
|
||||
auto tmp = fs::createTemporaryFile("clice-pch", "pch");
|
||||
if(!tmp) {
|
||||
LOG_ERROR("BuildPCH: failed to create temp file");
|
||||
return {false, "Failed to create temporary PCH file"};
|
||||
}
|
||||
tmp_path = *tmp;
|
||||
}
|
||||
cp.output_file = tmp_path;
|
||||
|
||||
PCHInfo pch_info;
|
||||
auto unit = compile(cp, pch_info);
|
||||
bool success = unit.completed();
|
||||
|
||||
std::string errors;
|
||||
if(!success)
|
||||
errors = collect_errors(unit);
|
||||
|
||||
std::string tu_index_data;
|
||||
std::string pch_links_json;
|
||||
if(success) {
|
||||
tu_index_data = serialize_tu_index(unit);
|
||||
auto links = feature::document_links(unit);
|
||||
auto raw = to_raw(links);
|
||||
pch_links_json = std::move(raw.data);
|
||||
}
|
||||
|
||||
// Destroy CompilationUnit to flush PCH to disk.
|
||||
unit = CompilationUnit(nullptr);
|
||||
|
||||
if(success) {
|
||||
auto final_path = finalize_output(params.output_path, tmp_path, params.file, "BuildPCH");
|
||||
LOG_INFO("BuildPCH done: file={}, output={}, {}ms", params.file, final_path, timer.ms());
|
||||
worker::BuildResult result;
|
||||
result.success = true;
|
||||
result.output_path = std::move(final_path);
|
||||
result.deps = pch_info.deps;
|
||||
result.tu_index_data = std::move(tu_index_data);
|
||||
result.pch_links_json = std::move(pch_links_json);
|
||||
return result;
|
||||
} else {
|
||||
LOG_WARN("BuildPCH failed: file={}, {}ms, errors=[{}]", params.file, timer.ms(), errors);
|
||||
fs::remove(tmp_path);
|
||||
return {false, errors.empty() ? "PCH compilation failed" : errors};
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
static et::serde::RawValue to_raw(const T& value) {
|
||||
auto json = et::serde::json::to_json<et::ipc::lsp_config>(value);
|
||||
return et::serde::RawValue{json ? std::move(*json) : "null"};
|
||||
static worker::BuildResult handle_build_pcm(const worker::BuildParams& params) {
|
||||
ScopedTimer timer;
|
||||
|
||||
CompilationParams cp;
|
||||
cp.kind = CompilationKind::ModuleInterface;
|
||||
fill_args(cp, params.directory, params.arguments);
|
||||
for(auto& [name, path]: params.pcms) {
|
||||
cp.pcms.try_emplace(name, path);
|
||||
}
|
||||
|
||||
std::string tmp_path;
|
||||
bool has_output = !params.output_path.empty();
|
||||
if(has_output) {
|
||||
tmp_path = params.output_path + ".tmp";
|
||||
} else {
|
||||
auto tmp = fs::createTemporaryFile("clice-pcm", "pcm");
|
||||
if(!tmp) {
|
||||
LOG_ERROR("BuildPCM: failed to create temp file");
|
||||
return {false, "Failed to create temporary PCM file"};
|
||||
}
|
||||
tmp_path = *tmp;
|
||||
}
|
||||
cp.output_file = tmp_path;
|
||||
|
||||
PCMInfo pcm_info;
|
||||
auto unit = compile(cp, pcm_info);
|
||||
bool success = unit.completed();
|
||||
|
||||
std::string errors;
|
||||
if(!success)
|
||||
errors = collect_errors(unit);
|
||||
|
||||
std::string tu_index_data;
|
||||
if(success)
|
||||
tu_index_data = serialize_tu_index(unit, true);
|
||||
|
||||
unit = CompilationUnit(nullptr);
|
||||
|
||||
if(success) {
|
||||
auto final_path = finalize_output(params.output_path, tmp_path, params.file, "BuildPCM");
|
||||
LOG_INFO("BuildPCM done: module={}, {}ms", params.module_name, timer.ms());
|
||||
worker::BuildResult result;
|
||||
result.success = true;
|
||||
result.output_path = std::move(final_path);
|
||||
result.deps = pcm_info.deps;
|
||||
result.tu_index_data = std::move(tu_index_data);
|
||||
return result;
|
||||
} else {
|
||||
LOG_WARN("BuildPCM failed: module={}, {}ms, errors=[{}]",
|
||||
params.module_name,
|
||||
timer.ms(),
|
||||
errors);
|
||||
fs::remove(tmp_path);
|
||||
return {false, errors.empty() ? "PCM compilation failed" : errors};
|
||||
}
|
||||
}
|
||||
|
||||
int run_stateless_worker_mode() {
|
||||
logging::stderr_logger("stateless-worker", logging::options);
|
||||
static worker::BuildResult handle_index(const worker::BuildParams& params) {
|
||||
ScopedTimer timer;
|
||||
|
||||
CompilationParams cp;
|
||||
cp.kind = CompilationKind::Indexing;
|
||||
fill_args(cp, params.directory, params.arguments);
|
||||
for(auto& [name, path]: params.pcms) {
|
||||
cp.pcms.try_emplace(name, path);
|
||||
}
|
||||
|
||||
auto unit = compile(cp);
|
||||
if(!unit.completed()) {
|
||||
LOG_WARN("Index failed: file={}, {}ms", params.file, timer.ms());
|
||||
return {false, "Index compilation failed"};
|
||||
}
|
||||
|
||||
auto tu_index = index::TUIndex::build(unit);
|
||||
std::string serialized;
|
||||
llvm::raw_string_ostream os(serialized);
|
||||
tu_index.serialize(os);
|
||||
|
||||
LOG_INFO("Index done: file={}, {} symbols, {}ms",
|
||||
params.file,
|
||||
tu_index.symbols.size(),
|
||||
timer.ms());
|
||||
worker::BuildResult result;
|
||||
result.success = true;
|
||||
result.tu_index_data = std::move(serialized);
|
||||
return result;
|
||||
}
|
||||
|
||||
static worker::BuildResult handle_completion(const worker::BuildParams& params) {
|
||||
ScopedTimer timer;
|
||||
|
||||
CompilationParams cp;
|
||||
cp.kind = CompilationKind::Completion;
|
||||
fill_args(cp, params.directory, params.arguments);
|
||||
if(!params.pch.first.empty()) {
|
||||
cp.pch = params.pch;
|
||||
}
|
||||
for(auto& [name, path]: params.pcms) {
|
||||
cp.pcms.try_emplace(name, path);
|
||||
}
|
||||
cp.add_remapped_file(params.file, params.text);
|
||||
cp.completion = {params.file, params.offset};
|
||||
|
||||
auto items = feature::code_complete(cp);
|
||||
LOG_DEBUG("Completion done: {} items, {}ms", items.size(), timer.ms());
|
||||
|
||||
worker::BuildResult result;
|
||||
result.result_json = to_raw(items);
|
||||
return result;
|
||||
}
|
||||
|
||||
static worker::BuildResult handle_signature_help(const worker::BuildParams& params) {
|
||||
ScopedTimer timer;
|
||||
|
||||
CompilationParams cp;
|
||||
cp.kind = CompilationKind::Completion;
|
||||
fill_args(cp, params.directory, params.arguments);
|
||||
if(!params.pch.first.empty()) {
|
||||
cp.pch = params.pch;
|
||||
}
|
||||
for(auto& [name, path]: params.pcms) {
|
||||
cp.pcms.try_emplace(name, path);
|
||||
}
|
||||
cp.add_remapped_file(params.file, params.text);
|
||||
cp.completion = {params.file, params.offset};
|
||||
|
||||
auto help = feature::signature_help(cp);
|
||||
LOG_DEBUG("SignatureHelp done: {}ms", timer.ms());
|
||||
|
||||
worker::BuildResult result;
|
||||
result.result_json = to_raw(help);
|
||||
return result;
|
||||
}
|
||||
|
||||
int run_stateless_worker_mode(const std::string& worker_name, const std::string& log_dir) {
|
||||
logging::stderr_logger(worker_name, logging::options);
|
||||
if(!log_dir.empty()) {
|
||||
logging::file_logger(worker_name, log_dir, logging::options);
|
||||
}
|
||||
|
||||
LOG_INFO("Starting stateless worker");
|
||||
|
||||
@@ -62,170 +276,18 @@ int run_stateless_worker_mode() {
|
||||
|
||||
et::ipc::BincodePeer peer(loop, std::move(*transport_result));
|
||||
|
||||
// === BuildPCH ===
|
||||
peer.on_request(
|
||||
[&](RequestContext& ctx,
|
||||
const worker::BuildPCHParams& params) -> RequestResult<worker::BuildPCHParams> {
|
||||
LOG_INFO("BuildPCH request: file={}", params.file);
|
||||
|
||||
auto result = co_await et::queue([&]() -> worker::BuildPCHResult {
|
||||
ScopedTimer timer;
|
||||
|
||||
CompilationParams cp;
|
||||
cp.kind = CompilationKind::Preamble;
|
||||
fill_args(cp, params.directory, params.arguments);
|
||||
cp.add_remapped_file(params.file, params.content, params.preamble_bound);
|
||||
|
||||
auto tmp = fs::createTemporaryFile("clice-pch", "pch");
|
||||
if(!tmp) {
|
||||
LOG_ERROR("BuildPCH: failed to create temp file");
|
||||
return {false, "Failed to create temporary PCH file", ""};
|
||||
}
|
||||
cp.output_file = *tmp;
|
||||
|
||||
PCHInfo pch_info;
|
||||
auto unit = compile(cp, pch_info);
|
||||
|
||||
if(unit.completed()) {
|
||||
LOG_INFO("BuildPCH done: file={}, output={}, {}ms",
|
||||
params.file,
|
||||
cp.output_file,
|
||||
timer.ms());
|
||||
return {true, "", std::string(cp.output_file)};
|
||||
} else {
|
||||
LOG_WARN("BuildPCH failed: file={}, {}ms", params.file, timer.ms());
|
||||
fs::remove(cp.output_file);
|
||||
return {false, "PCH compilation failed", ""};
|
||||
}
|
||||
});
|
||||
co_return result.value();
|
||||
});
|
||||
|
||||
// === BuildPCM ===
|
||||
peer.on_request(
|
||||
[&](RequestContext& ctx,
|
||||
const worker::BuildPCMParams& params) -> RequestResult<worker::BuildPCMParams> {
|
||||
LOG_INFO("BuildPCM request: file={}, module={}", params.file, params.module_name);
|
||||
|
||||
auto result = co_await et::queue([&]() -> worker::BuildPCMResult {
|
||||
ScopedTimer timer;
|
||||
|
||||
CompilationParams cp;
|
||||
cp.kind = CompilationKind::ModuleInterface;
|
||||
fill_args(cp, params.directory, params.arguments);
|
||||
for(auto& [name, path]: params.pcms) {
|
||||
cp.pcms.try_emplace(name, path);
|
||||
}
|
||||
|
||||
auto tmp = fs::createTemporaryFile("clice-pcm", "pcm");
|
||||
if(!tmp) {
|
||||
LOG_ERROR("BuildPCM: failed to create temp file");
|
||||
return {false, "Failed to create temporary PCM file"};
|
||||
}
|
||||
cp.output_file = *tmp;
|
||||
|
||||
PCMInfo pcm_info;
|
||||
auto unit = compile(cp, pcm_info);
|
||||
|
||||
if(unit.completed()) {
|
||||
LOG_INFO("BuildPCM done: module={}, {}ms", params.module_name, timer.ms());
|
||||
return {true, "", std::string(cp.output_file)};
|
||||
} else {
|
||||
LOG_WARN("BuildPCM failed: module={}, {}ms", params.module_name, timer.ms());
|
||||
return {false, "PCM compilation failed", ""};
|
||||
}
|
||||
});
|
||||
co_return result.value();
|
||||
});
|
||||
|
||||
// === Completion ===
|
||||
peer.on_request(
|
||||
[&](RequestContext& ctx,
|
||||
const worker::CompletionParams& params) -> RequestResult<worker::CompletionParams> {
|
||||
LOG_DEBUG("Completion request: path={}, offset={}", params.path, params.offset);
|
||||
|
||||
auto result = co_await et::queue([&]() -> et::serde::RawValue {
|
||||
ScopedTimer timer;
|
||||
|
||||
CompilationParams cp;
|
||||
cp.kind = CompilationKind::Completion;
|
||||
fill_args(cp, params.directory, params.arguments);
|
||||
if(!params.pch.first.empty()) {
|
||||
cp.pch = params.pch;
|
||||
}
|
||||
for(auto& [name, path]: params.pcms) {
|
||||
cp.pcms.try_emplace(name, path);
|
||||
}
|
||||
cp.add_remapped_file(params.path, params.text);
|
||||
cp.completion = {params.path, params.offset};
|
||||
|
||||
auto items = feature::code_complete(cp);
|
||||
LOG_DEBUG("Completion done: {} items, {}ms", items.size(), timer.ms());
|
||||
return to_raw(items);
|
||||
});
|
||||
co_return result.value();
|
||||
});
|
||||
|
||||
// === SignatureHelp ===
|
||||
peer.on_request([&](RequestContext& ctx, const worker::SignatureHelpParams& params)
|
||||
-> RequestResult<worker::SignatureHelpParams> {
|
||||
LOG_DEBUG("SignatureHelp request: path={}, offset={}", params.path, params.offset);
|
||||
|
||||
auto result = co_await et::queue([&]() -> et::serde::RawValue {
|
||||
ScopedTimer timer;
|
||||
|
||||
CompilationParams cp;
|
||||
cp.kind = CompilationKind::Completion;
|
||||
fill_args(cp, params.directory, params.arguments);
|
||||
if(!params.pch.first.empty()) {
|
||||
cp.pch = params.pch;
|
||||
}
|
||||
for(auto& [name, path]: params.pcms) {
|
||||
cp.pcms.try_emplace(name, path);
|
||||
}
|
||||
cp.add_remapped_file(params.path, params.text);
|
||||
cp.completion = {params.path, params.offset};
|
||||
|
||||
auto help = feature::signature_help(cp);
|
||||
LOG_DEBUG("SignatureHelp done: {}ms", timer.ms());
|
||||
return to_raw(help);
|
||||
});
|
||||
co_return result.value();
|
||||
});
|
||||
|
||||
// === Index ===
|
||||
peer.on_request([&](RequestContext& ctx,
|
||||
const worker::IndexParams& params) -> RequestResult<worker::IndexParams> {
|
||||
LOG_INFO("Index request: file={}", params.file);
|
||||
|
||||
auto result = co_await et::queue([&]() -> worker::IndexResult {
|
||||
ScopedTimer timer;
|
||||
|
||||
CompilationParams cp;
|
||||
cp.kind = CompilationKind::Indexing;
|
||||
fill_args(cp, params.directory, params.arguments);
|
||||
for(auto& [name, path]: params.pcms) {
|
||||
cp.pcms.try_emplace(name, path);
|
||||
const worker::BuildParams& params) -> RequestResult<worker::BuildParams> {
|
||||
using K = worker::BuildKind;
|
||||
auto result = co_await et::queue([&]() -> worker::BuildResult {
|
||||
switch(params.kind) {
|
||||
case K::BuildPCH: return handle_build_pch(params);
|
||||
case K::BuildPCM: return handle_build_pcm(params);
|
||||
case K::Index: return handle_index(params);
|
||||
case K::Completion: return handle_completion(params);
|
||||
case K::SignatureHelp: return handle_signature_help(params);
|
||||
}
|
||||
|
||||
auto unit = compile(cp);
|
||||
|
||||
if(!unit.completed()) {
|
||||
LOG_WARN("Index failed: file={}, {}ms", params.file, timer.ms());
|
||||
return {false, "Index compilation failed", ""};
|
||||
}
|
||||
|
||||
auto tu_index = index::TUIndex::build(unit);
|
||||
|
||||
std::string serialized;
|
||||
llvm::raw_string_ostream os(serialized);
|
||||
tu_index.serialize(os);
|
||||
|
||||
LOG_INFO("Index done: file={}, {} symbols, {}ms",
|
||||
params.file,
|
||||
tu_index.symbols.size(),
|
||||
timer.ms());
|
||||
return {true, "", std::move(serialized)};
|
||||
return {false, "Unknown build kind"};
|
||||
});
|
||||
co_return result.value();
|
||||
});
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace clice {
|
||||
|
||||
/// Run the stateless worker process mode.
|
||||
/// The worker receives one-shot compilation tasks (BuildPCH, BuildPCM,
|
||||
/// Completion, SignatureHelp, Index) via stdin/stdout bincode IPC,
|
||||
/// executes them on a thread pool, and returns results.
|
||||
int run_stateless_worker_mode();
|
||||
int run_stateless_worker_mode(const std::string& worker_name, const std::string& log_dir);
|
||||
|
||||
} // namespace clice
|
||||
|
||||
44
src/server/worker_common.h
Normal file
44
src/server/worker_common.h
Normal file
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
|
||||
/// Shared utilities for stateful and stateless worker processes.
|
||||
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "compile/compilation.h"
|
||||
#include "eventide/ipc/json_codec.h"
|
||||
#include "eventide/serde/json/serializer.h"
|
||||
#include "eventide/serde/serde/raw_value.h"
|
||||
|
||||
namespace clice {
|
||||
|
||||
/// RAII timer for measuring elapsed milliseconds.
|
||||
struct ScopedTimer {
|
||||
std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();
|
||||
|
||||
long long ms() const {
|
||||
return std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - start)
|
||||
.count();
|
||||
}
|
||||
};
|
||||
|
||||
/// Fill CompilationParams directory and arguments from worker request fields.
|
||||
inline void fill_args(CompilationParams& cp,
|
||||
const std::string& directory,
|
||||
const std::vector<std::string>& arguments) {
|
||||
cp.directory = directory;
|
||||
for(auto& arg: arguments) {
|
||||
cp.arguments.push_back(arg.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
/// Serialize a value to JSON RawValue using LSP config.
|
||||
template <typename T>
|
||||
inline eventide::serde::RawValue to_raw(const T& value) {
|
||||
auto json = eventide::serde::json::to_json<eventide::ipc::lsp_config>(value);
|
||||
return eventide::serde::RawValue{json ? std::move(*json) : "null"};
|
||||
}
|
||||
|
||||
} // namespace clice
|
||||
@@ -10,14 +10,14 @@ namespace clice {
|
||||
|
||||
namespace {
|
||||
|
||||
/// Coroutine that reads lines from a worker's stderr pipe and logs them
|
||||
/// with a prefix like [SL-0] or [SF-1].
|
||||
/// Coroutine that drains a worker's stderr pipe.
|
||||
/// Workers write their own log files, so this only captures unexpected output
|
||||
/// (crash stacktraces, assertion failures, etc.) that bypasses spdlog.
|
||||
et::task<> drain_stderr(et::pipe stderr_pipe, std::string prefix) {
|
||||
std::string buffer;
|
||||
while(true) {
|
||||
auto result = co_await stderr_pipe.read();
|
||||
if(!result.has_value()) {
|
||||
// EOF or error — worker has exited
|
||||
break;
|
||||
}
|
||||
auto& chunk = result.value();
|
||||
@@ -26,7 +26,6 @@ et::task<> drain_stderr(et::pipe stderr_pipe, std::string prefix) {
|
||||
|
||||
buffer += chunk;
|
||||
|
||||
// Log complete lines
|
||||
std::size_t pos = 0;
|
||||
while(true) {
|
||||
auto nl = buffer.find('\n', pos);
|
||||
@@ -34,16 +33,15 @@ et::task<> drain_stderr(et::pipe stderr_pipe, std::string prefix) {
|
||||
break;
|
||||
auto line = buffer.substr(pos, nl - pos);
|
||||
if(!line.empty()) {
|
||||
LOG_INFO("{} {}", prefix, line);
|
||||
LOG_DEBUG("{} {}", prefix, line);
|
||||
}
|
||||
pos = nl + 1;
|
||||
}
|
||||
buffer.erase(0, pos);
|
||||
}
|
||||
|
||||
// Flush any remaining partial line
|
||||
if(!buffer.empty()) {
|
||||
LOG_INFO("{} {}", prefix, buffer);
|
||||
LOG_DEBUG("{} {}", prefix, buffer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +50,10 @@ et::task<> drain_stderr(et::pipe stderr_pipe, std::string prefix) {
|
||||
bool WorkerPool::spawn_worker(const std::string& self_path,
|
||||
bool stateful,
|
||||
std::uint64_t memory_limit) {
|
||||
auto& workers = stateful ? stateful_workers : stateless_workers;
|
||||
auto worker_index = workers.size();
|
||||
std::string worker_name = std::string(stateful ? "SF-" : "SL-") + std::to_string(worker_index);
|
||||
|
||||
et::process::options opts;
|
||||
opts.file = self_path;
|
||||
if(stateful) {
|
||||
@@ -63,6 +65,15 @@ bool WorkerPool::spawn_worker(const std::string& self_path,
|
||||
} else {
|
||||
opts.args = {self_path, "--mode", "stateless-worker"};
|
||||
}
|
||||
|
||||
opts.args.push_back("--worker-name");
|
||||
opts.args.push_back(worker_name);
|
||||
|
||||
if(!log_dir_.empty()) {
|
||||
opts.args.push_back("--log-dir");
|
||||
opts.args.push_back(log_dir_);
|
||||
}
|
||||
|
||||
opts.streams = {
|
||||
et::process::stdio::pipe(true, false), // stdin: child reads
|
||||
et::process::stdio::pipe(false, true), // stdout: child writes
|
||||
@@ -85,14 +96,8 @@ bool WorkerPool::spawn_worker(const std::string& self_path,
|
||||
std::move(spawn.stdin_pipe));
|
||||
auto peer = std::make_unique<et::ipc::BincodePeer>(loop, std::move(transport));
|
||||
|
||||
auto& workers = stateful ? stateful_workers : stateless_workers;
|
||||
auto worker_index = workers.size();
|
||||
|
||||
// Build log prefix: [SF-0] for stateful, [SL-0] for stateless
|
||||
std::string prefix =
|
||||
std::string("[") + (stateful ? "SF-" : "SL-") + std::to_string(worker_index) + "]";
|
||||
|
||||
// Schedule stderr log collection
|
||||
std::string prefix = "[" + worker_name + "]";
|
||||
loop.schedule(drain_stderr(std::move(spawn.stderr_pipe), prefix));
|
||||
|
||||
workers.push_back(WorkerProcess{
|
||||
@@ -108,6 +113,8 @@ bool WorkerPool::spawn_worker(const std::string& self_path,
|
||||
}
|
||||
|
||||
bool WorkerPool::start(const WorkerPoolOptions& options) {
|
||||
log_dir_ = options.log_dir;
|
||||
|
||||
for(std::uint32_t i = 0; i < options.stateless_count; ++i) {
|
||||
if(!spawn_worker(options.self_path, false, 0)) {
|
||||
return false;
|
||||
|
||||
@@ -15,9 +15,6 @@
|
||||
|
||||
namespace clice {
|
||||
|
||||
/// Default timeout for IPC requests to worker processes.
|
||||
constexpr inline auto kWorkerRequestTimeout = std::chrono::milliseconds(30000);
|
||||
|
||||
namespace et = eventide;
|
||||
using et::ipc::RequestResult;
|
||||
|
||||
@@ -26,6 +23,7 @@ struct WorkerPoolOptions {
|
||||
std::uint32_t stateless_count = 2;
|
||||
std::uint32_t stateful_count = 2;
|
||||
std::uint64_t worker_memory_limit = 4ULL * 1024 * 1024 * 1024; // 4GB default
|
||||
std::string log_dir;
|
||||
};
|
||||
|
||||
class WorkerPool {
|
||||
@@ -81,11 +79,10 @@ private:
|
||||
void clear_owner(std::size_t worker_index);
|
||||
std::size_t pick_least_loaded();
|
||||
|
||||
std::string log_dir_;
|
||||
bool spawn_worker(const std::string& self_path, bool stateful, std::uint64_t memory_limit);
|
||||
};
|
||||
|
||||
// --- Template implementations ---------------------------------------------------
|
||||
|
||||
template <typename Params>
|
||||
RequestResult<Params> WorkerPool::send_stateful(std::uint32_t path_id,
|
||||
const Params& params,
|
||||
@@ -93,9 +90,10 @@ RequestResult<Params> WorkerPool::send_stateful(std::uint32_t path_id,
|
||||
if(stateful_workers.empty()) {
|
||||
co_return et::outcome_error(et::ipc::Error{"No stateful workers available"});
|
||||
}
|
||||
if(!opts.timeout.has_value()) {
|
||||
opts.timeout = kWorkerRequestTimeout;
|
||||
}
|
||||
// No timeout: compile tasks run as detached tasks (loop.schedule) that
|
||||
// are immune to LSP $/cancelRequest. Adding a timeout here would use
|
||||
// eventide's with_token/when_any which has a spurious-cancellation bug
|
||||
// that kills requests within milliseconds instead of the configured period.
|
||||
auto idx = assign_worker(path_id);
|
||||
co_return co_await stateful_workers[idx].peer->send_request(params, opts);
|
||||
}
|
||||
@@ -106,9 +104,6 @@ RequestResult<Params> WorkerPool::send_stateless(const Params& params,
|
||||
if(stateless_workers.empty()) {
|
||||
co_return et::outcome_error(et::ipc::Error{"No stateless workers available"});
|
||||
}
|
||||
if(!opts.timeout.has_value()) {
|
||||
opts.timeout = kWorkerRequestTimeout;
|
||||
}
|
||||
auto idx = next_stateless;
|
||||
next_stateless = (next_stateless + 1) % stateless_workers.size();
|
||||
co_return co_await stateless_workers[idx].peer->send_request(params, opts);
|
||||
|
||||
376
src/server/workspace.cpp
Normal file
376
src/server/workspace.cpp
Normal file
@@ -0,0 +1,376 @@
|
||||
#include "server/workspace.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
|
||||
#include "eventide/ipc/lsp/position.h"
|
||||
#include "eventide/ipc/lsp/protocol.h"
|
||||
#include "eventide/serde/json/json.h"
|
||||
#include "support/filesystem.h"
|
||||
#include "support/logging.h"
|
||||
#include "syntax/scan.h"
|
||||
|
||||
#include "llvm/Support/Chrono.h"
|
||||
#include "llvm/Support/FileSystem.h"
|
||||
#include "llvm/Support/MemoryBuffer.h"
|
||||
#include "llvm/Support/Path.h"
|
||||
#include "llvm/Support/xxhash.h"
|
||||
|
||||
namespace clice {
|
||||
|
||||
namespace lsp = eventide::ipc::lsp;
|
||||
|
||||
/// Find the tightest (innermost) occurrence containing `offset` via binary search.
|
||||
const static index::Occurrence* lookup_occurrence(const std::vector<index::Occurrence>& occs,
|
||||
std::uint32_t offset) {
|
||||
auto it = std::ranges::lower_bound(occs, offset, {}, [](const index::Occurrence& o) {
|
||||
return o.range.end;
|
||||
});
|
||||
const index::Occurrence* best = nullptr;
|
||||
while(it != occs.end() && it->range.contains(offset)) {
|
||||
if(!best || (it->range.end - it->range.begin) < (best->range.end - best->range.begin)) {
|
||||
best = &*it;
|
||||
}
|
||||
++it;
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
std::optional<std::pair<index::SymbolHash, protocol::Range>>
|
||||
OpenFileIndex::find_occurrence(std::uint32_t offset) const {
|
||||
if(!mapper)
|
||||
return std::nullopt;
|
||||
auto* occ = lookup_occurrence(file_index.occurrences, offset);
|
||||
if(!occ)
|
||||
return std::nullopt;
|
||||
auto start = mapper->to_position(occ->range.begin);
|
||||
auto end = mapper->to_position(occ->range.end);
|
||||
if(!start || !end)
|
||||
return std::nullopt;
|
||||
return std::pair{
|
||||
occ->target,
|
||||
protocol::Range{*start, *end}
|
||||
};
|
||||
}
|
||||
|
||||
std::optional<std::pair<index::SymbolHash, protocol::Range>>
|
||||
MergedIndexShard::find_occurrence(std::uint32_t offset) const {
|
||||
auto* m = mapper();
|
||||
if(!m)
|
||||
return std::nullopt;
|
||||
std::optional<std::pair<index::SymbolHash, protocol::Range>> result;
|
||||
index.lookup(offset, [&](const index::Occurrence& o) {
|
||||
auto start = m->to_position(o.range.begin);
|
||||
auto end = m->to_position(o.range.end);
|
||||
if(start && end) {
|
||||
result = {
|
||||
o.target,
|
||||
protocol::Range{*start, *end}
|
||||
};
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
llvm::SmallVector<std::uint32_t> Workspace::on_file_saved(std::uint32_t path_id) {
|
||||
llvm::SmallVector<std::uint32_t> dirtied;
|
||||
|
||||
// Re-scan the saved file for module declarations and update path_to_module.
|
||||
auto file_path = path_pool.resolve(path_id);
|
||||
if(auto buf = llvm::MemoryBuffer::getFile(file_path)) {
|
||||
auto result = scan((*buf)->getBuffer());
|
||||
if(!result.module_name.empty()) {
|
||||
path_to_module[path_id] = std::move(result.module_name);
|
||||
} else {
|
||||
path_to_module.erase(path_id);
|
||||
}
|
||||
}
|
||||
|
||||
if(compile_graph) {
|
||||
auto result = compile_graph->update(path_id);
|
||||
for(auto id: result) {
|
||||
dirtied.push_back(id);
|
||||
pcm_paths.erase(id);
|
||||
pcm_cache.erase(id);
|
||||
}
|
||||
}
|
||||
return dirtied;
|
||||
}
|
||||
|
||||
void Workspace::on_file_closed(std::uint32_t path_id) {
|
||||
if(compile_graph && compile_graph->has_unit(path_id)) {
|
||||
compile_graph->update(path_id);
|
||||
}
|
||||
pch_cache.erase(path_id);
|
||||
}
|
||||
|
||||
std::uint64_t hash_file(llvm::StringRef path) {
|
||||
auto buf = llvm::MemoryBuffer::getFile(path);
|
||||
if(!buf)
|
||||
return 0;
|
||||
return llvm::xxh3_64bits((*buf)->getBuffer());
|
||||
}
|
||||
|
||||
DepsSnapshot capture_deps_snapshot(PathPool& pool, llvm::ArrayRef<std::string> deps) {
|
||||
DepsSnapshot snap;
|
||||
// Capture timestamp BEFORE hashing to avoid TOCTOU: if a file is modified
|
||||
// during hashing, its mtime will be > build_at, triggering Layer 2 re-hash.
|
||||
snap.build_at = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
|
||||
snap.path_ids.reserve(deps.size());
|
||||
snap.hashes.reserve(deps.size());
|
||||
for(const auto& file: deps) {
|
||||
snap.path_ids.push_back(pool.intern(file));
|
||||
snap.hashes.push_back(hash_file(file));
|
||||
}
|
||||
return snap;
|
||||
}
|
||||
|
||||
bool deps_changed(const PathPool& pool, const DepsSnapshot& snap) {
|
||||
for(std::size_t i = 0; i < snap.path_ids.size(); ++i) {
|
||||
auto path = pool.resolve(snap.path_ids[i]);
|
||||
llvm::sys::fs::file_status status;
|
||||
if(auto ec = llvm::sys::fs::status(path, status)) {
|
||||
// File disappeared — definitely changed.
|
||||
if(snap.hashes[i] != 0)
|
||||
return true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Layer 1: mtime check (cheap, stat only).
|
||||
auto current_mtime = llvm::sys::toTimeT(status.getLastModificationTime());
|
||||
if(current_mtime <= snap.build_at)
|
||||
continue;
|
||||
|
||||
// Layer 2: mtime is newer — re-hash content to confirm actual change.
|
||||
auto current_hash = hash_file(path);
|
||||
if(current_hash != snap.hashes[i])
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
struct CacheDepEntry {
|
||||
std::uint32_t path; // index into CacheData::paths
|
||||
std::uint64_t hash;
|
||||
};
|
||||
|
||||
struct CachePCHEntry {
|
||||
std::string filename;
|
||||
std::uint32_t source_file; // index into CacheData::paths
|
||||
std::uint64_t hash;
|
||||
std::uint32_t bound;
|
||||
std::int64_t build_at;
|
||||
std::vector<CacheDepEntry> deps;
|
||||
};
|
||||
|
||||
struct CachePCMEntry {
|
||||
std::string filename;
|
||||
std::uint32_t source_file;
|
||||
std::string module_name;
|
||||
std::int64_t build_at;
|
||||
std::vector<CacheDepEntry> deps;
|
||||
};
|
||||
|
||||
struct CacheData {
|
||||
std::vector<std::string> paths;
|
||||
std::vector<CachePCHEntry> pch;
|
||||
std::vector<CachePCMEntry> pcm;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
void Workspace::load_cache() {
|
||||
if(config.cache_dir.empty())
|
||||
return;
|
||||
|
||||
auto cache_path = path::join(config.cache_dir, "cache", "cache.json");
|
||||
auto content = fs::read(cache_path);
|
||||
if(!content) {
|
||||
LOG_DEBUG("No cache.json found at {}", cache_path);
|
||||
return;
|
||||
}
|
||||
|
||||
CacheData data;
|
||||
auto status = eventide::serde::json::from_json(*content, data);
|
||||
if(!status) {
|
||||
LOG_WARN("Failed to parse cache.json");
|
||||
return;
|
||||
}
|
||||
|
||||
auto resolve = [&](std::uint32_t idx) -> llvm::StringRef {
|
||||
return idx < data.paths.size() ? llvm::StringRef(data.paths[idx]) : "";
|
||||
};
|
||||
|
||||
auto load_deps = [&](std::int64_t build_at, const auto& dep_entries) -> DepsSnapshot {
|
||||
DepsSnapshot deps;
|
||||
deps.build_at = build_at;
|
||||
for(auto& dep: dep_entries) {
|
||||
auto dep_path = resolve(dep.path);
|
||||
if(dep_path.empty())
|
||||
continue;
|
||||
deps.path_ids.push_back(path_pool.intern(dep_path));
|
||||
deps.hashes.push_back(dep.hash);
|
||||
}
|
||||
return deps;
|
||||
};
|
||||
|
||||
for(auto& entry: data.pch) {
|
||||
auto pch_path = path::join(config.cache_dir, "cache", "pch", entry.filename);
|
||||
auto source = resolve(entry.source_file);
|
||||
if(!llvm::sys::fs::exists(pch_path) || source.empty())
|
||||
continue;
|
||||
|
||||
auto path_id = path_pool.intern(source);
|
||||
auto& st = pch_cache[path_id];
|
||||
st.path = pch_path;
|
||||
st.hash = entry.hash;
|
||||
st.bound = entry.bound;
|
||||
st.deps = load_deps(entry.build_at, entry.deps);
|
||||
|
||||
LOG_DEBUG("Loaded cached PCH: {} -> {}", source, pch_path);
|
||||
}
|
||||
|
||||
for(auto& entry: data.pcm) {
|
||||
auto pcm_path = path::join(config.cache_dir, "cache", "pcm", entry.filename);
|
||||
auto source = resolve(entry.source_file);
|
||||
if(!llvm::sys::fs::exists(pcm_path) || source.empty())
|
||||
continue;
|
||||
|
||||
auto path_id = path_pool.intern(source);
|
||||
pcm_cache[path_id] = {pcm_path, load_deps(entry.build_at, entry.deps)};
|
||||
pcm_paths[path_id] = pcm_path;
|
||||
|
||||
LOG_DEBUG("Loaded cached PCM: {} (module {}) -> {}", source, entry.module_name, pcm_path);
|
||||
}
|
||||
|
||||
LOG_INFO("Loaded cache.json: {} PCH entries, {} PCM entries",
|
||||
pch_cache.size(),
|
||||
pcm_cache.size());
|
||||
}
|
||||
|
||||
void Workspace::save_cache() {
|
||||
if(config.cache_dir.empty())
|
||||
return;
|
||||
|
||||
CacheData data;
|
||||
std::unordered_map<std::string, std::uint32_t> index_map;
|
||||
|
||||
auto intern = [&](std::uint32_t runtime_path_id) -> std::uint32_t {
|
||||
auto path = std::string(path_pool.resolve(runtime_path_id));
|
||||
auto [it, inserted] =
|
||||
index_map.try_emplace(path, static_cast<std::uint32_t>(data.paths.size()));
|
||||
if(inserted) {
|
||||
data.paths.push_back(path);
|
||||
}
|
||||
return it->second;
|
||||
};
|
||||
|
||||
for(auto& [path_id, st]: pch_cache) {
|
||||
if(st.path.empty())
|
||||
continue;
|
||||
|
||||
CachePCHEntry entry;
|
||||
entry.filename = std::string(path::filename(st.path));
|
||||
entry.source_file = intern(path_id);
|
||||
entry.hash = st.hash;
|
||||
entry.bound = st.bound;
|
||||
entry.build_at = st.deps.build_at;
|
||||
for(std::size_t i = 0; i < st.deps.path_ids.size(); ++i) {
|
||||
entry.deps.push_back({intern(st.deps.path_ids[i]), st.deps.hashes[i]});
|
||||
}
|
||||
data.pch.push_back(std::move(entry));
|
||||
}
|
||||
|
||||
for(auto& [path_id, st]: pcm_cache) {
|
||||
if(st.path.empty())
|
||||
continue;
|
||||
|
||||
CachePCMEntry entry;
|
||||
entry.filename = std::string(path::filename(st.path));
|
||||
entry.source_file = intern(path_id);
|
||||
auto mod_it = path_to_module.find(path_id);
|
||||
entry.module_name = mod_it != path_to_module.end() ? mod_it->second : "";
|
||||
entry.build_at = st.deps.build_at;
|
||||
for(std::size_t i = 0; i < st.deps.path_ids.size(); ++i) {
|
||||
entry.deps.push_back({intern(st.deps.path_ids[i]), st.deps.hashes[i]});
|
||||
}
|
||||
data.pcm.push_back(std::move(entry));
|
||||
}
|
||||
|
||||
auto json_str = eventide::serde::json::to_json(data);
|
||||
if(!json_str) {
|
||||
LOG_WARN("Failed to serialize cache.json");
|
||||
return;
|
||||
}
|
||||
|
||||
auto cache_path = path::join(config.cache_dir, "cache", "cache.json");
|
||||
auto tmp_path = cache_path + ".tmp";
|
||||
auto write_result = fs::write(tmp_path, *json_str);
|
||||
if(!write_result) {
|
||||
LOG_WARN("Failed to write cache.json.tmp: {}", write_result.error().message());
|
||||
return;
|
||||
}
|
||||
auto rename_result = fs::rename(tmp_path, cache_path);
|
||||
if(!rename_result) {
|
||||
LOG_WARN("Failed to rename cache.json.tmp to cache.json: {}",
|
||||
rename_result.error().message());
|
||||
}
|
||||
}
|
||||
|
||||
void Workspace::cleanup_cache(int max_age_days) {
|
||||
if(config.cache_dir.empty())
|
||||
return;
|
||||
|
||||
auto now = std::chrono::system_clock::now();
|
||||
auto max_age = std::chrono::hours(max_age_days * 24);
|
||||
|
||||
for(auto* subdir: {"cache/pch", "cache/pcm"}) {
|
||||
auto dir = path::join(config.cache_dir, subdir);
|
||||
std::error_code ec;
|
||||
for(auto it = llvm::sys::fs::directory_iterator(dir, ec);
|
||||
!ec && it != llvm::sys::fs::directory_iterator();
|
||||
it.increment(ec)) {
|
||||
llvm::sys::fs::file_status status;
|
||||
if(auto stat_ec = llvm::sys::fs::status(it->path(), status))
|
||||
continue;
|
||||
|
||||
auto mtime = status.getLastModificationTime();
|
||||
auto age = now - mtime;
|
||||
if(age > max_age) {
|
||||
llvm::sys::fs::remove(it->path());
|
||||
LOG_DEBUG("Cleaned up stale cache file: {}", it->path());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Workspace::build_module_map() {
|
||||
for(auto& [module_name, path_ids]: dep_graph.modules()) {
|
||||
for(auto path_id: path_ids) {
|
||||
path_to_module[path_id] = module_name.str();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Workspace::fill_pcm_deps(std::unordered_map<std::string, std::string>& pcms,
|
||||
std::uint32_t exclude_path_id) const {
|
||||
for(auto& [pid, pcm_path]: pcm_paths) {
|
||||
if(pid == exclude_path_id)
|
||||
continue;
|
||||
auto mod_it = path_to_module.find(pid);
|
||||
if(mod_it != path_to_module.end()) {
|
||||
pcms[mod_it->second] = pcm_path;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Workspace::cancel_all() {
|
||||
if(compile_graph) {
|
||||
compile_graph->cancel_all();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace clice
|
||||
248
src/server/workspace.h
Normal file
248
src/server/workspace.h
Normal file
@@ -0,0 +1,248 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
|
||||
#include "command/command.h"
|
||||
#include "eventide/ipc/lsp/position.h"
|
||||
#include "eventide/ipc/lsp/protocol.h"
|
||||
#include "index/merged_index.h"
|
||||
#include "index/project_index.h"
|
||||
#include "semantic/relation_kind.h"
|
||||
#include "server/compile_graph.h"
|
||||
#include "server/config.h"
|
||||
#include "support/path_pool.h"
|
||||
#include "syntax/dependency_graph.h"
|
||||
|
||||
#include "llvm/ADT/ArrayRef.h"
|
||||
#include "llvm/ADT/DenseMap.h"
|
||||
#include "llvm/ADT/SmallVector.h"
|
||||
#include "llvm/ADT/StringRef.h"
|
||||
|
||||
namespace clice {
|
||||
|
||||
namespace et = eventide;
|
||||
namespace protocol = et::ipc::protocol;
|
||||
namespace lsp = et::ipc::lsp;
|
||||
|
||||
/// Two-layer staleness snapshot for compilation artifacts (PCH, AST, etc.).
|
||||
///
|
||||
/// Layer 1 (fast): compare each file's current mtime against build_at.
|
||||
/// If all mtimes <= build_at, the artifact is fresh (zero I/O beyond stat).
|
||||
///
|
||||
/// Layer 2 (precise): for files whose mtime changed, re-hash their content
|
||||
/// and compare against the stored hash. If the hash matches, the file was
|
||||
/// "touched" but not actually modified — skip the rebuild.
|
||||
struct DepsSnapshot {
|
||||
llvm::SmallVector<std::uint32_t> path_ids;
|
||||
llvm::SmallVector<std::uint64_t> hashes;
|
||||
std::int64_t build_at = 0;
|
||||
};
|
||||
|
||||
/// Context for compiling a header file that lacks its own CDB entry.
|
||||
struct HeaderFileContext {
|
||||
std::uint32_t host_path_id; ///< Source file acting as host.
|
||||
std::string preamble_path; ///< Path to generated preamble file on disk.
|
||||
std::uint64_t preamble_hash; ///< Hash of preamble content for staleness.
|
||||
};
|
||||
|
||||
/// In-memory index for an open file. Kept separate from MergedIndex because
|
||||
/// open files change frequently, are based on unsaved buffer content, and only
|
||||
/// need to track the main file (headers are covered by PCH/PCM indexing).
|
||||
struct OpenFileIndex {
|
||||
index::FileIndex file_index;
|
||||
index::SymbolTable symbols;
|
||||
std::string content; ///< Buffer text at index time (for position mapping).
|
||||
|
||||
/// Cached PositionMapper built from `content`. Avoids re-scanning line
|
||||
/// offsets on every query. Initialized by Indexer::set_open_file().
|
||||
std::optional<lsp::PositionMapper> mapper;
|
||||
|
||||
/// Find the tightest occurrence containing `offset`.
|
||||
/// Returns (symbol_hash, LSP range) with positions already converted.
|
||||
std::optional<std::pair<index::SymbolHash, protocol::Range>>
|
||||
find_occurrence(std::uint32_t offset) const;
|
||||
|
||||
/// Iterate relations matching `kind`, calling back with pre-converted ranges.
|
||||
/// Callback: (const index::Relation&, protocol::Range) -> bool (true = continue).
|
||||
template <typename Fn>
|
||||
void find_relations(index::SymbolHash hash, RelationKind kind, Fn&& fn) const {
|
||||
if(!mapper)
|
||||
return;
|
||||
auto it = file_index.relations.find(hash);
|
||||
if(it == file_index.relations.end())
|
||||
return;
|
||||
for(auto& r: it->second) {
|
||||
if(r.kind & kind) {
|
||||
auto start = mapper->to_position(r.range.begin);
|
||||
auto end = mapper->to_position(r.range.end);
|
||||
if(start && end) {
|
||||
if(!fn(r, protocol::Range{*start, *end}))
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// Wraps index::MergedIndex with a lazily-cached PositionMapper.
|
||||
struct MergedIndexShard {
|
||||
index::MergedIndex index;
|
||||
mutable std::optional<lsp::PositionMapper> cached_mapper;
|
||||
|
||||
/// Get or lazily build a PositionMapper from the index's stored content.
|
||||
const lsp::PositionMapper* mapper() const {
|
||||
if(!cached_mapper) {
|
||||
auto c = index.content();
|
||||
if(!c.empty()) {
|
||||
cached_mapper.emplace(c, lsp::PositionEncoding::UTF16);
|
||||
}
|
||||
}
|
||||
return cached_mapper ? &*cached_mapper : nullptr;
|
||||
}
|
||||
|
||||
/// Invalidate the cached mapper (call after merge changes content).
|
||||
void invalidate_mapper() {
|
||||
cached_mapper.reset();
|
||||
}
|
||||
|
||||
/// Find occurrence at byte offset.
|
||||
/// Returns (symbol_hash, LSP range) with positions already converted.
|
||||
std::optional<std::pair<index::SymbolHash, protocol::Range>>
|
||||
find_occurrence(std::uint32_t offset) const;
|
||||
|
||||
/// Iterate relations matching `kind`, calling back with pre-converted ranges.
|
||||
/// Callback: (const index::Relation&, protocol::Range) -> bool (true = continue).
|
||||
template <typename Fn>
|
||||
void find_relations(index::SymbolHash hash, RelationKind kind, Fn&& fn) const {
|
||||
auto* m = mapper();
|
||||
if(!m)
|
||||
return;
|
||||
index.lookup(hash, kind, [&](const index::Relation& r) {
|
||||
auto start = m->to_position(r.range.begin);
|
||||
auto end = m->to_position(r.range.end);
|
||||
if(start && end) {
|
||||
return fn(r, protocol::Range{*start, *end});
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/// Cached PCH state. Content-addressed by preamble hash — shared across all
|
||||
/// files (open or on-disk) that have the same preamble content.
|
||||
struct PCHState {
|
||||
std::string path;
|
||||
std::uint32_t bound = 0;
|
||||
std::uint64_t hash = 0;
|
||||
DepsSnapshot deps;
|
||||
std::string document_links_json; ///< Pre-serialized DocumentLink[] from PCH build
|
||||
std::shared_ptr<eventide::event> building;
|
||||
};
|
||||
|
||||
/// Cached PCM state for a single C++20 module. Shared across all files that
|
||||
/// import the same module.
|
||||
struct PCMState {
|
||||
std::string path;
|
||||
DepsSnapshot deps;
|
||||
};
|
||||
|
||||
/// All persistent, project-wide state derived from files on disk.
|
||||
///
|
||||
/// Design principle: open files are never depended upon by other files.
|
||||
/// Dependencies always point to disk files. This enforces a clean two-layer
|
||||
/// architecture:
|
||||
/// - Global layer (Workspace): tracks disk truth, shared by all files
|
||||
/// - Per-file layer (Session): tracks buffer truth, isolated per TU
|
||||
///
|
||||
/// Workspace is the single source of truth for:
|
||||
/// - dependency relationships (include graph, module DAG)
|
||||
/// - compilation artifacts shared across files (PCH/PCM caches)
|
||||
/// - symbol index (ProjectIndex + per-file MergedIndex shards)
|
||||
/// - compilation database and configuration
|
||||
///
|
||||
/// Workspace is NEVER modified by unsaved buffer content. The only mutation
|
||||
/// paths are:
|
||||
/// - Initialization (load_workspace at startup)
|
||||
/// - didSave (on_file_saved: rescan disk, cascade invalidation)
|
||||
/// - Background index (merge TUIndex results from stateless workers)
|
||||
struct Workspace {
|
||||
CliceConfig config;
|
||||
CompilationDatabase cdb;
|
||||
|
||||
PathPool path_pool;
|
||||
|
||||
/// Include relationships between files on disk (#include edges).
|
||||
/// Built once at startup from CDB scan; updated incrementally on didSave.
|
||||
DependencyGraph dep_graph;
|
||||
|
||||
/// C++20 module compilation ordering DAG.
|
||||
/// Lazily resolves module dependencies; updated on didSave via cascade.
|
||||
std::unique_ptr<CompileGraph> compile_graph;
|
||||
|
||||
/// Reverse mapping: file path_id → module name (e.g. "std", "foo.bar").
|
||||
/// Built from dep_graph at startup; updated on didSave when module
|
||||
/// declarations change.
|
||||
llvm::DenseMap<std::uint32_t, std::string> path_to_module;
|
||||
|
||||
/// PCH cache, keyed by file path_id.
|
||||
/// TODO: re-key by preamble content hash to enable cross-file sharing and
|
||||
/// add LRU eviction. Compile flags should also be part of the key.
|
||||
llvm::DenseMap<std::uint32_t, PCHState> pch_cache;
|
||||
|
||||
/// PCM cache, keyed by module source path_id.
|
||||
llvm::DenseMap<std::uint32_t, PCMState> pcm_cache;
|
||||
|
||||
/// PCM output paths, keyed by module source path_id.
|
||||
/// Maps to the .pcm file on disk used as -fmodule-file argument.
|
||||
llvm::DenseMap<std::uint32_t, std::string> pcm_paths;
|
||||
|
||||
/// Global symbol table across all indexed translation units.
|
||||
index::ProjectIndex project_index;
|
||||
|
||||
/// Per-file index shards from background indexing, keyed by project-level
|
||||
/// path_id. Contains symbol occurrences, relations, and stored content
|
||||
/// for position mapping.
|
||||
llvm::DenseMap<std::uint32_t, MergedIndexShard> merged_indices;
|
||||
|
||||
/// Called when a file is saved to disk. Cascades invalidation through
|
||||
/// compile_graph and clears affected PCM caches.
|
||||
/// Returns path_ids of all files dirtied by the cascade.
|
||||
llvm::SmallVector<std::uint32_t> on_file_saved(std::uint32_t path_id);
|
||||
|
||||
/// Called when a file is closed. Notifies compile_graph if this file
|
||||
/// is a module unit so dependents can be re-evaluated on next compile.
|
||||
void on_file_closed(std::uint32_t path_id);
|
||||
|
||||
/// Load PCH/PCM cache from cache.json on disk.
|
||||
void load_cache();
|
||||
/// Save PCH/PCM cache to cache.json on disk.
|
||||
void save_cache();
|
||||
/// Remove stale PCH/PCM files older than max_age_days.
|
||||
void cleanup_cache(int max_age_days = 7);
|
||||
/// Build path_to_module reverse mapping from dep_graph.
|
||||
void build_module_map();
|
||||
/// Fill PCM paths for all built modules, excluding exclude_path_id.
|
||||
void fill_pcm_deps(std::unordered_map<std::string, std::string>& pcms,
|
||||
std::uint32_t exclude_path_id = UINT32_MAX) const;
|
||||
/// Cancel all in-flight compilations.
|
||||
void cancel_all();
|
||||
};
|
||||
|
||||
/// Hash a file's content using xxh3_64bits. Returns 0 on read failure.
|
||||
std::uint64_t hash_file(llvm::StringRef path);
|
||||
|
||||
/// Capture a two-layer staleness snapshot after a successful compilation.
|
||||
/// Interns dependency paths into the PathPool and hashes each file's content.
|
||||
DepsSnapshot capture_deps_snapshot(PathPool& pool, llvm::ArrayRef<std::string> deps);
|
||||
|
||||
/// Two-layer staleness check.
|
||||
/// Layer 1 (fast): stat each dep file, compare mtime against build_at.
|
||||
/// Layer 2 (precise): for files with mtime > build_at, re-hash content.
|
||||
bool deps_changed(const PathPool& pool, const DepsSnapshot& snap);
|
||||
|
||||
} // namespace clice
|
||||
@@ -74,6 +74,14 @@ inline std::expected<std::string, std::error_code> read(llvm::StringRef path) {
|
||||
return buffer.get()->getBuffer().str();
|
||||
}
|
||||
|
||||
inline std::expected<void, std::error_code> rename(llvm::StringRef from, llvm::StringRef to) {
|
||||
auto error = llvm::sys::fs::rename(from, to);
|
||||
if(error) {
|
||||
return std::unexpected(error);
|
||||
}
|
||||
return std::expected<void, std::error_code>();
|
||||
}
|
||||
|
||||
} // namespace fs
|
||||
|
||||
namespace vfs = llvm::vfs;
|
||||
@@ -124,7 +132,7 @@ public:
|
||||
}
|
||||
|
||||
llvm::StringRef filename = path::filename(Path);
|
||||
if(filename.starts_with("preamble-") && filename.ends_with(".pch")) {
|
||||
if(filename.ends_with(".pch")) {
|
||||
return file;
|
||||
}
|
||||
return std::make_unique<VolatileFile>(std::move(*file));
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "spdlog/sinks/ringbuffer_sink.h"
|
||||
#include "spdlog/sinks/stdout_color_sinks.h"
|
||||
#include "spdlog/sinks/stdout_sinks.h"
|
||||
#include "llvm/Support/Signals.h"
|
||||
|
||||
namespace clice::logging {
|
||||
|
||||
@@ -38,27 +39,73 @@ void stderr_logger(std::string_view name, const Options& options) {
|
||||
spdlog::set_default_logger(std::move(logger));
|
||||
}
|
||||
|
||||
void file_loggger(std::string_view name, std::string_view dir, const Options& options) {
|
||||
auto now = std::chrono::system_clock::now();
|
||||
auto filename = std::format("{:%Y-%m-%d_%H-%M-%S}.log", now);
|
||||
auto sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>(path::join(dir, filename));
|
||||
void file_logger(std::string_view name, std::string_view dir, const Options& options) {
|
||||
if(auto ec = llvm::sys::fs::create_directories(dir)) {
|
||||
spdlog::error("Failed to create log directory {}: {}", std::string(dir), ec.message());
|
||||
return;
|
||||
}
|
||||
auto filepath = path::join(dir, std::format("{}.log", name));
|
||||
// Verify we can write to the file before constructing the sink.
|
||||
// (spdlog would throw on failure, but exceptions are disabled in this project.)
|
||||
{
|
||||
std::error_code ec;
|
||||
llvm::raw_fd_ostream test(filepath, ec, llvm::sys::fs::OF_Append);
|
||||
if(ec) {
|
||||
spdlog::error("Failed to open log file {}: {}", filepath, ec.message());
|
||||
return;
|
||||
}
|
||||
}
|
||||
auto file_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>(filepath);
|
||||
|
||||
if(options.replay_console && ringbuffer_sink) {
|
||||
sink->set_level(options.level);
|
||||
sink->set_pattern(pattern);
|
||||
auto replay_buffer = ringbuffer_sink;
|
||||
|
||||
for(auto& log: ringbuffer_sink->last_raw()) {
|
||||
sink->log(log);
|
||||
auto console_sink = std::make_shared<spdlog::sinks::stderr_color_sink_mt>(options.color);
|
||||
std::array<spdlog::sink_ptr, 2> sinks = {file_sink, console_sink};
|
||||
auto logger = std::make_shared<spdlog::logger>(std::string(name), sinks.begin(), sinks.end());
|
||||
logger->set_level(options.level);
|
||||
logger->set_pattern(pattern);
|
||||
logger->flush_on(Level::trace);
|
||||
spdlog::set_default_logger(std::move(logger));
|
||||
|
||||
// Replay buffered logs after swapping the default logger, so no messages
|
||||
// emitted between the snapshot and the swap are lost.
|
||||
if(options.replay_console && replay_buffer) {
|
||||
file_sink->set_level(options.level);
|
||||
file_sink->set_pattern(pattern);
|
||||
|
||||
for(auto& log: replay_buffer->last_raw()) {
|
||||
file_sink->log(log);
|
||||
}
|
||||
|
||||
ringbuffer_sink.reset();
|
||||
}
|
||||
|
||||
auto logger = std::make_shared<spdlog::logger>(std::string(name), std::move(sink));
|
||||
logger->set_level(options.level);
|
||||
logger->set_pattern(pattern);
|
||||
logger->flush_on(Level::trace);
|
||||
spdlog::set_default_logger(std::move(logger));
|
||||
install_crash_handler(filepath);
|
||||
}
|
||||
|
||||
static std::unique_ptr<llvm::raw_fd_ostream> crash_log_stream;
|
||||
|
||||
static void crash_handler(void*) {
|
||||
if(crash_log_stream) {
|
||||
*crash_log_stream << "\n=== CRASH STACK TRACE ===\n";
|
||||
llvm::sys::PrintStackTrace(*crash_log_stream);
|
||||
crash_log_stream->flush();
|
||||
}
|
||||
}
|
||||
|
||||
void install_crash_handler(std::string_view log_path) {
|
||||
std::error_code ec;
|
||||
crash_log_stream =
|
||||
std::make_unique<llvm::raw_fd_ostream>(llvm::StringRef(log_path.data(), log_path.size()),
|
||||
ec,
|
||||
llvm::sys::fs::OF_Append);
|
||||
if(ec) {
|
||||
LOG_WARN("Failed to install crash handler for {}: {}", log_path, ec.message());
|
||||
crash_log_stream.reset();
|
||||
return;
|
||||
}
|
||||
llvm::sys::AddSignalHandler(crash_handler, nullptr);
|
||||
llvm::sys::PrintStackTraceOnErrorSignal("clice");
|
||||
}
|
||||
|
||||
} // namespace clice::logging
|
||||
|
||||
@@ -26,7 +26,12 @@ extern Options options;
|
||||
|
||||
void stderr_logger(std::string_view name, const Options& options);
|
||||
|
||||
void file_loggger(std::string_view name, std::string_view dir, const Options& options);
|
||||
void file_logger(std::string_view name, std::string_view dir, const Options& options);
|
||||
|
||||
/// Install a signal handler that writes crash stacktraces to the given log file.
|
||||
/// Also enables LLVM's default stderr stacktrace output.
|
||||
/// Must be called after file_logger so the log file path is known.
|
||||
void install_crash_handler(std::string_view log_path);
|
||||
|
||||
template <typename... Args>
|
||||
struct logging_rformat {
|
||||
@@ -95,9 +100,11 @@ void critical [[noreturn]] (logging_format<Args...> fmt, Args&&... args) {
|
||||
} // namespace clice::logging
|
||||
|
||||
#define LOG_MESSAGE(name, fmt, ...) \
|
||||
if(clice::logging::options.level <= clice::logging::Level::name) { \
|
||||
clice::logging::name(fmt __VA_OPT__(, ) __VA_ARGS__); \
|
||||
}
|
||||
do { \
|
||||
if(clice::logging::options.level <= clice::logging::Level::name) { \
|
||||
clice::logging::name(fmt __VA_OPT__(, ) __VA_ARGS__); \
|
||||
} \
|
||||
} while(0)
|
||||
|
||||
#define LOG_TRACE(fmt, ...) LOG_MESSAGE(trace, fmt, __VA_ARGS__)
|
||||
#define LOG_DEBUG(fmt, ...) LOG_MESSAGE(debug, fmt, __VA_ARGS__)
|
||||
|
||||
121
src/syntax/completion.cpp
Normal file
121
src/syntax/completion.cpp
Normal file
@@ -0,0 +1,121 @@
|
||||
#include "syntax/completion.h"
|
||||
|
||||
#include "syntax/include_resolver.h"
|
||||
|
||||
#include "llvm/ADT/SmallString.h"
|
||||
#include "llvm/ADT/StringSet.h"
|
||||
#include "llvm/Support/FileSystem.h"
|
||||
#include "llvm/Support/Path.h"
|
||||
|
||||
namespace clice {
|
||||
|
||||
PreambleCompletionContext detect_completion_context(llvm::StringRef text, std::uint32_t offset) {
|
||||
auto line_start = text.rfind('\n', offset > 0 ? offset - 1 : 0);
|
||||
line_start = (line_start == llvm::StringRef::npos) ? 0 : line_start + 1;
|
||||
|
||||
auto line = text.slice(line_start, offset);
|
||||
auto trimmed = line.ltrim();
|
||||
|
||||
if(trimmed.starts_with("#")) {
|
||||
auto directive = trimmed.drop_front(1).ltrim();
|
||||
if(directive.consume_front("include")) {
|
||||
directive = directive.ltrim();
|
||||
if(directive.consume_front("\"")) {
|
||||
return {CompletionContext::IncludeQuoted, directive.str()};
|
||||
}
|
||||
if(directive.consume_front("<")) {
|
||||
return {CompletionContext::IncludeAngled, directive.str()};
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
auto import_check = trimmed;
|
||||
if(import_check.consume_front("export") && !import_check.empty() &&
|
||||
!std::isalnum(import_check[0])) {
|
||||
import_check = import_check.ltrim();
|
||||
}
|
||||
if(import_check.consume_front("import") &&
|
||||
(import_check.empty() || !std::isalnum(import_check[0]))) {
|
||||
import_check = import_check.ltrim();
|
||||
|
||||
auto line_end = text.find('\n', offset);
|
||||
if(line_end == llvm::StringRef::npos)
|
||||
line_end = text.size();
|
||||
auto rest_of_line = text.slice(line_start, line_end);
|
||||
if(!rest_of_line.contains(';')) {
|
||||
return {CompletionContext::Import, import_check.str()};
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<std::string>
|
||||
complete_module_import(const llvm::DenseMap<std::uint32_t, std::string>& modules,
|
||||
llvm::StringRef prefix) {
|
||||
std::vector<std::string> results;
|
||||
for(auto& [path_id, module_name]: modules) {
|
||||
if(llvm::StringRef(module_name).starts_with(prefix)) {
|
||||
results.push_back(module_name);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
std::vector<IncludeCandidate> complete_include_path(const ResolvedSearchConfig& resolved,
|
||||
llvm::StringRef prefix,
|
||||
bool angled,
|
||||
DirListingCache& dir_cache) {
|
||||
llvm::StringRef dir_prefix;
|
||||
llvm::StringRef file_prefix = prefix;
|
||||
auto slash_pos = prefix.rfind('/');
|
||||
if(slash_pos != llvm::StringRef::npos) {
|
||||
dir_prefix = prefix.slice(0, slash_pos);
|
||||
file_prefix = prefix.slice(slash_pos + 1, llvm::StringRef::npos);
|
||||
}
|
||||
|
||||
unsigned start_idx = angled ? resolved.angled_start_idx : 0;
|
||||
|
||||
std::vector<IncludeCandidate> results;
|
||||
llvm::StringSet<> seen;
|
||||
|
||||
for(unsigned i = start_idx; i < resolved.dirs.size(); ++i) {
|
||||
auto& search_dir = resolved.dirs[i];
|
||||
|
||||
const llvm::StringSet<>* entries = nullptr;
|
||||
if(!dir_prefix.empty()) {
|
||||
llvm::SmallString<256> sub_path(search_dir.path);
|
||||
llvm::sys::path::append(sub_path, dir_prefix);
|
||||
entries = resolve_dir(sub_path, dir_cache);
|
||||
} else {
|
||||
entries = search_dir.entries;
|
||||
}
|
||||
|
||||
if(!entries)
|
||||
continue;
|
||||
|
||||
for(auto& entry: *entries) {
|
||||
auto name = entry.getKey();
|
||||
if(!name.starts_with(file_prefix))
|
||||
continue;
|
||||
if(!seen.insert(name).second)
|
||||
continue;
|
||||
|
||||
llvm::SmallString<256> full_path(search_dir.path);
|
||||
if(!dir_prefix.empty()) {
|
||||
llvm::sys::path::append(full_path, dir_prefix);
|
||||
}
|
||||
llvm::sys::path::append(full_path, name);
|
||||
|
||||
bool is_dir = false;
|
||||
llvm::sys::fs::is_directory(llvm::Twine(full_path), is_dir);
|
||||
|
||||
results.push_back({name.str(), is_dir});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
} // namespace clice
|
||||
57
src/syntax/completion.h
Normal file
57
src/syntax/completion.h
Normal file
@@ -0,0 +1,57 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "llvm/ADT/DenseMap.h"
|
||||
#include "llvm/ADT/StringRef.h"
|
||||
|
||||
namespace clice {
|
||||
|
||||
struct ResolvedSearchConfig;
|
||||
struct DirListingCache;
|
||||
|
||||
/// What kind of preamble-level completion is being requested.
|
||||
enum class CompletionContext : std::uint8_t {
|
||||
None,
|
||||
IncludeQuoted,
|
||||
IncludeAngled,
|
||||
Import,
|
||||
};
|
||||
|
||||
/// Result of detecting the completion context from source text.
|
||||
struct PreambleCompletionContext {
|
||||
CompletionContext kind = CompletionContext::None;
|
||||
std::string prefix;
|
||||
};
|
||||
|
||||
/// Detect whether the cursor is inside a #include or import directive.
|
||||
/// Pure text parsing — no compiler state needed.
|
||||
PreambleCompletionContext detect_completion_context(llvm::StringRef text, std::uint32_t offset);
|
||||
|
||||
/// Return module names matching a prefix, suitable for `import` completion.
|
||||
/// @param modules Module name map (path_id → module name).
|
||||
/// @param prefix Partially-typed module name to match against.
|
||||
std::vector<std::string>
|
||||
complete_module_import(const llvm::DenseMap<std::uint32_t, std::string>& modules,
|
||||
llvm::StringRef prefix);
|
||||
|
||||
/// Entry in the include path completion result.
|
||||
struct IncludeCandidate {
|
||||
std::string name;
|
||||
bool is_directory = false;
|
||||
};
|
||||
|
||||
/// Return file/directory names matching a prefix in the given search paths.
|
||||
/// @param resolved Pre-resolved search directories with cached directory listings.
|
||||
/// @param angled_start Index where angled (<>) search dirs begin.
|
||||
/// @param prefix Partially-typed include path (e.g. "vec" or "sys/").
|
||||
/// @param angled True for <> includes, false for "" includes.
|
||||
/// @param dir_cache Shared directory listing cache (for subdirectory lookups).
|
||||
std::vector<IncludeCandidate> complete_include_path(const ResolvedSearchConfig& resolved,
|
||||
llvm::StringRef prefix,
|
||||
bool angled,
|
||||
DirListingCache& dir_cache);
|
||||
|
||||
} // namespace clice
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "syntax/dependency_graph.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
|
||||
#include "command/toolchain.h"
|
||||
@@ -19,9 +20,7 @@ namespace clice {
|
||||
|
||||
namespace et = eventide;
|
||||
|
||||
// ============================================================================
|
||||
// DependencyGraph implementation
|
||||
// ============================================================================
|
||||
|
||||
void DependencyGraph::add_module(llvm::StringRef module_name, std::uint32_t path_id) {
|
||||
auto& ids = module_to_path[module_name];
|
||||
@@ -101,9 +100,110 @@ std::size_t DependencyGraph::edge_count() const {
|
||||
return count;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
void DependencyGraph::build_reverse_map() {
|
||||
reverse_includes_.clear();
|
||||
for(auto& [key, ids]: includes) {
|
||||
for(auto flagged_id: ids) {
|
||||
auto included_id = flagged_id & PATH_ID_MASK;
|
||||
auto& vec = reverse_includes_[included_id];
|
||||
if(llvm::find(vec, key.path_id) == vec.end()) {
|
||||
vec.push_back(key.path_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
llvm::ArrayRef<std::uint32_t> DependencyGraph::get_includers(std::uint32_t path_id) const {
|
||||
auto it = reverse_includes_.find(path_id);
|
||||
if(it != reverse_includes_.end()) {
|
||||
return it->second;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
llvm::SmallVector<std::uint32_t, 4>
|
||||
DependencyGraph::find_host_sources(std::uint32_t header_path_id) const {
|
||||
llvm::SmallVector<std::uint32_t, 4> result;
|
||||
llvm::DenseSet<std::uint32_t> visited;
|
||||
llvm::SmallVector<std::uint32_t, 16> queue;
|
||||
|
||||
queue.push_back(header_path_id);
|
||||
visited.insert(header_path_id);
|
||||
|
||||
while(!queue.empty()) {
|
||||
auto current = queue.pop_back_val();
|
||||
auto includers = get_includers(current);
|
||||
if(includers.empty()) {
|
||||
// No includers: this is a root (source file).
|
||||
// Exclude the starting header itself.
|
||||
if(current != header_path_id) {
|
||||
result.push_back(current);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
for(auto includer: includers) {
|
||||
if(visited.insert(includer).second) {
|
||||
queue.push_back(includer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<std::uint32_t> DependencyGraph::find_include_chain(std::uint32_t host_path_id,
|
||||
std::uint32_t target_path_id) const {
|
||||
if(host_path_id == target_path_id) {
|
||||
return {host_path_id};
|
||||
}
|
||||
|
||||
// BFS: predecessor map for path reconstruction.
|
||||
llvm::DenseMap<std::uint32_t, std::uint32_t> prev;
|
||||
llvm::SmallVector<std::uint32_t, 16> queue;
|
||||
|
||||
prev[host_path_id] = host_path_id;
|
||||
queue.push_back(host_path_id);
|
||||
|
||||
bool found = false;
|
||||
while(!queue.empty() && !found) {
|
||||
llvm::SmallVector<std::uint32_t, 16> next_queue;
|
||||
for(auto current: queue) {
|
||||
auto includes_union = get_all_includes(current);
|
||||
for(auto flagged_id: includes_union) {
|
||||
auto child = flagged_id & PATH_ID_MASK;
|
||||
if(prev.find(child) == prev.end()) {
|
||||
prev[child] = current;
|
||||
if(child == target_path_id) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
next_queue.push_back(child);
|
||||
}
|
||||
}
|
||||
if(found) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
queue = std::move(next_queue);
|
||||
}
|
||||
|
||||
if(!found) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Reconstruct path from target back to host.
|
||||
std::vector<std::uint32_t> chain;
|
||||
auto node = target_path_id;
|
||||
while(node != host_path_id) {
|
||||
chain.push_back(node);
|
||||
node = prev[node];
|
||||
}
|
||||
chain.push_back(host_path_id);
|
||||
std::reverse(chain.begin(), chain.end());
|
||||
return chain;
|
||||
}
|
||||
|
||||
// Wavefront BFS scanner — async implementation
|
||||
// ============================================================================
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -523,8 +623,9 @@ et::task<> scan_impl(CompilationDatabase& cdb,
|
||||
auto contexts =
|
||||
cdb.lookup(file_path, {.query_toolchain = true, .suppress_logging = true});
|
||||
if(!contexts.empty()) {
|
||||
auto& ctx = contexts[0];
|
||||
auto fallback = scan_module_decl(ctx.arguments, ctx.directory, /*content=*/{});
|
||||
auto& cmd = contexts[0];
|
||||
auto fallback =
|
||||
scan_module_decl(cmd.to_argv(), cmd.resolved.directory, /*content=*/{});
|
||||
if(!fallback.module_name.empty()) {
|
||||
scan_result.scan_result.module_name = std::move(fallback.module_name);
|
||||
scan_result.scan_result.is_interface_unit = fallback.is_interface_unit;
|
||||
@@ -715,9 +816,7 @@ et::task<> scan_impl(CompilationDatabase& cdb,
|
||||
|
||||
} // namespace
|
||||
|
||||
// ============================================================================
|
||||
// Public sync entry point
|
||||
// ============================================================================
|
||||
|
||||
ScanReport scan_dependency_graph(CompilationDatabase& cdb,
|
||||
PathPool& path_pool,
|
||||
|
||||
@@ -70,6 +70,24 @@ public:
|
||||
/// Get the union of includes across all configs for a file.
|
||||
llvm::SmallVector<std::uint32_t> get_all_includes(std::uint32_t path_id) const;
|
||||
|
||||
/// Build the reverse include map from the forward includes.
|
||||
/// Must be called after all set_includes() calls are complete.
|
||||
void build_reverse_map();
|
||||
|
||||
/// Get the direct includers of a file (files that directly include path_id).
|
||||
llvm::ArrayRef<std::uint32_t> get_includers(std::uint32_t path_id) const;
|
||||
|
||||
/// BFS upward through reverse edges to find all source files (roots)
|
||||
/// that transitively include header_path_id.
|
||||
/// Source files are those that have no includers (i.e. they are roots in the graph).
|
||||
llvm::SmallVector<std::uint32_t, 4> find_host_sources(std::uint32_t header_path_id) const;
|
||||
|
||||
/// BFS forward through include edges to find the shortest include chain
|
||||
/// from host_path_id to target_path_id.
|
||||
/// Returns [host, intermediate1, ..., target], or empty if no path exists.
|
||||
std::vector<std::uint32_t> find_include_chain(std::uint32_t host_path_id,
|
||||
std::uint32_t target_path_id) const;
|
||||
|
||||
/// Number of files with include entries.
|
||||
std::size_t file_count() const;
|
||||
|
||||
@@ -94,6 +112,10 @@ private:
|
||||
|
||||
/// Track which files have any include entries (for file_count).
|
||||
llvm::DenseMap<std::uint32_t, llvm::SmallVector<std::uint32_t>> file_configs;
|
||||
|
||||
/// Reverse include map: PathID -> list of PathIDs that directly include it.
|
||||
/// Populated by build_reverse_map().
|
||||
llvm::DenseMap<std::uint32_t, llvm::SmallVector<std::uint32_t, 4>> reverse_includes_;
|
||||
};
|
||||
|
||||
/// A (file, search-config) pair used to track per-wave work items.
|
||||
|
||||
@@ -53,7 +53,8 @@ void Lexer::lex(Token& token) {
|
||||
}
|
||||
} else if(parse_pp_keyword) {
|
||||
parse_pp_keyword = false;
|
||||
parse_header_name = token.text(content) == "include";
|
||||
auto kw = token.text(content);
|
||||
parse_header_name = kw == "include" || kw == "include_next" || kw == "embed";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,6 +51,15 @@ public:
|
||||
|
||||
Token advance_until(TokenKind kind);
|
||||
|
||||
/// Force the lexer into header-name mode so the next token is lexed
|
||||
/// via LexIncludeFilename (correctly handling both "..." and <...>).
|
||||
/// Use this before lexing filename arguments in contexts like
|
||||
/// __has_include() or __has_embed() where the lexer cannot detect
|
||||
/// the mode automatically.
|
||||
void set_header_name_mode() {
|
||||
parse_header_name = true;
|
||||
}
|
||||
|
||||
private:
|
||||
bool ignore_end_of_directive = true;
|
||||
bool parse_pp_keyword = false;
|
||||
|
||||
@@ -476,4 +476,60 @@ std::vector<std::uint32_t> compute_preamble_bounds(llvm::StringRef content) {
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Check if a preprocessor #include/#import directive line is complete.
|
||||
static bool is_include_directive_complete(llvm::StringRef directive) {
|
||||
if(directive.contains('"')) {
|
||||
auto after_keyword = directive.drop_front(directive.starts_with("import") ? 6 : 7);
|
||||
return after_keyword.count('"') >= 2;
|
||||
}
|
||||
if(directive.contains('<')) {
|
||||
return directive.contains('>');
|
||||
}
|
||||
// No " or < — might be a macro (#include FOO) or just incomplete (#include ).
|
||||
auto after_keyword = directive.drop_front(directive.starts_with("import") ? 6 : 7).ltrim();
|
||||
return !after_keyword.empty();
|
||||
}
|
||||
|
||||
/// Check if a C++20 module statement line (import/export module) is complete.
|
||||
/// A complete statement must end with ';'.
|
||||
static bool is_module_statement_complete(llvm::StringRef trimmed) {
|
||||
return trimmed.rtrim().ends_with(";");
|
||||
}
|
||||
|
||||
bool is_preamble_complete(llvm::StringRef content, std::uint32_t bound) {
|
||||
auto preamble = content.substr(0, bound);
|
||||
|
||||
while(!preamble.empty()) {
|
||||
auto [line, rest] = preamble.split('\n');
|
||||
preamble = rest;
|
||||
|
||||
auto trimmed = line.ltrim();
|
||||
|
||||
// Preprocessor directive: #include or #import
|
||||
if(trimmed.starts_with("#")) {
|
||||
auto directive = trimmed.drop_front(1).ltrim();
|
||||
if(directive.starts_with("include") || directive.starts_with("import")) {
|
||||
if(!is_include_directive_complete(directive)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// C++20 module statements: import, export module, export import
|
||||
// Check word boundary to avoid matching identifiers like "important".
|
||||
auto is_keyword = [](llvm::StringRef s, llvm::StringRef keyword) {
|
||||
return s.starts_with(keyword) &&
|
||||
(s.size() == keyword.size() || !llvm::isAlnum(s[keyword.size()]));
|
||||
};
|
||||
if(is_keyword(trimmed, "import") || is_keyword(trimmed, "export")) {
|
||||
if(!is_module_statement_complete(trimmed)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace clice
|
||||
|
||||
@@ -100,4 +100,10 @@ std::uint32_t compute_preamble_bound(llvm::StringRef content);
|
||||
|
||||
std::vector<std::uint32_t> compute_preamble_bounds(llvm::StringRef content);
|
||||
|
||||
/// Check if the preamble region contains only syntactically complete directives.
|
||||
/// Returns false if any #include/#import has an unclosed "" or <>, or any
|
||||
/// C++20 module statement (import/export) is missing a trailing ';',
|
||||
/// indicating the user is still typing and PCH/PCM rebuild should be deferred.
|
||||
bool is_preamble_complete(llvm::StringRef content, std::uint32_t bound);
|
||||
|
||||
} // namespace clice
|
||||
|
||||
@@ -44,10 +44,6 @@ struct LocalSourceRange {
|
||||
std::uint32_t begin = static_cast<std::uint32_t>(-1);
|
||||
std::uint32_t end = static_cast<std::uint32_t>(-1);
|
||||
|
||||
constexpr LocalSourceRange() = default;
|
||||
|
||||
constexpr LocalSourceRange(std::uint32_t begin, std::uint32_t end) : begin(begin), end(end) {}
|
||||
|
||||
constexpr bool operator==(const LocalSourceRange& other) const = default;
|
||||
|
||||
constexpr std::uint32_t length() const {
|
||||
|
||||
@@ -1,31 +1,13 @@
|
||||
"""Fixtures and shared helpers for clice LSP integration tests using pygls LanguageClient."""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from collections.abc import AsyncGenerator
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from lsprotocol.types import (
|
||||
PROGRESS,
|
||||
TEXT_DOCUMENT_PUBLISH_DIAGNOSTICS,
|
||||
WINDOW_WORK_DONE_PROGRESS_CREATE,
|
||||
ClientCapabilities,
|
||||
Diagnostic,
|
||||
DidOpenTextDocumentParams,
|
||||
InitializeParams,
|
||||
InitializeResult,
|
||||
InitializedParams,
|
||||
ProgressParams,
|
||||
PublishDiagnosticsParams,
|
||||
TextDocumentItem,
|
||||
WorkDoneProgressCreateParams,
|
||||
WorkspaceFolder,
|
||||
)
|
||||
from pygls.lsp.client import BaseLanguageClient
|
||||
|
||||
from tests.integration.utils.client import CliceClient
|
||||
|
||||
|
||||
def pytest_addoption(parser: pytest.Parser) -> None:
|
||||
@@ -55,91 +37,6 @@ def pytest_addoption(parser: pytest.Parser) -> None:
|
||||
)
|
||||
|
||||
|
||||
class CliceClient(BaseLanguageClient):
|
||||
"""Language client that tracks server-sent notifications."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__("clice-test-client", "0.1.0")
|
||||
self.diagnostics: dict[str, list[Diagnostic]] = {}
|
||||
self.diagnostics_events: dict[str, asyncio.Event] = {}
|
||||
self.progress_tokens: list[str] = []
|
||||
self.progress_events: list[dict] = []
|
||||
self.init_result: InitializeResult | None = None
|
||||
|
||||
@self.feature(TEXT_DOCUMENT_PUBLISH_DIAGNOSTICS)
|
||||
def on_diagnostics(params: PublishDiagnosticsParams) -> None:
|
||||
self.diagnostics[params.uri] = list(params.diagnostics)
|
||||
if params.uri in self.diagnostics_events:
|
||||
self.diagnostics_events[params.uri].set()
|
||||
|
||||
@self.feature(WINDOW_WORK_DONE_PROGRESS_CREATE)
|
||||
def on_create_progress(params: WorkDoneProgressCreateParams) -> None:
|
||||
token = str(params.token) if isinstance(params.token, int) else params.token
|
||||
self.progress_tokens.append(token)
|
||||
return None
|
||||
|
||||
@self.feature(PROGRESS)
|
||||
def on_progress(params: ProgressParams) -> None:
|
||||
token = str(params.token) if isinstance(params.token, int) else params.token
|
||||
self.progress_events.append({"token": token, "value": params.value})
|
||||
|
||||
def wait_for_diagnostics(self, uri: str) -> asyncio.Event:
|
||||
"""Get or create an event that fires when diagnostics arrive for uri."""
|
||||
if uri not in self.diagnostics_events:
|
||||
self.diagnostics_events[uri] = asyncio.Event()
|
||||
else:
|
||||
self.diagnostics_events[uri].clear()
|
||||
return self.diagnostics_events[uri]
|
||||
|
||||
async def initialize(self, workspace: Path) -> InitializeResult:
|
||||
"""Initialize the LSP server with a workspace folder and return the result."""
|
||||
result = await self.initialize_async(
|
||||
InitializeParams(
|
||||
capabilities=ClientCapabilities(),
|
||||
root_uri=workspace.as_uri(),
|
||||
workspace_folders=[
|
||||
WorkspaceFolder(uri=workspace.as_uri(), name="test")
|
||||
],
|
||||
)
|
||||
)
|
||||
self.initialized(InitializedParams())
|
||||
self.init_result = result
|
||||
return result
|
||||
|
||||
def open(self, filepath: Path, version: int = 0) -> tuple[str, str]:
|
||||
"""Open a text document and return (uri, content)."""
|
||||
# Read in binary mode to preserve CRLF on Windows, matching real LSP clients.
|
||||
content = filepath.read_bytes().decode("utf-8")
|
||||
uri = filepath.as_uri()
|
||||
self.text_document_did_open(
|
||||
DidOpenTextDocumentParams(
|
||||
text_document=TextDocumentItem(
|
||||
uri=uri, language_id="cpp", version=version, text=content
|
||||
)
|
||||
)
|
||||
)
|
||||
return uri, content
|
||||
|
||||
async def wait_diagnostics(self, uri: str, timeout: float = 30.0) -> None:
|
||||
"""Wait for diagnostics on the given URI."""
|
||||
if uri in self.diagnostics:
|
||||
return
|
||||
event = self.wait_for_diagnostics(uri)
|
||||
if uri in self.diagnostics:
|
||||
return
|
||||
await asyncio.wait_for(event.wait(), timeout=timeout)
|
||||
|
||||
async def open_and_wait(
|
||||
self, filepath: Path, timeout: float = 60.0
|
||||
) -> tuple[str, str]:
|
||||
"""Open a file and wait for compilation diagnostics."""
|
||||
uri = filepath.as_uri()
|
||||
event = self.wait_for_diagnostics(uri)
|
||||
_, content = self.open(filepath)
|
||||
await asyncio.wait_for(event.wait(), timeout=timeout)
|
||||
return uri, content
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def executable(request: pytest.FixtureRequest) -> Path:
|
||||
exe = request.config.getoption("--executable")
|
||||
@@ -165,51 +62,10 @@ def executable(request: pytest.FixtureRequest) -> Path:
|
||||
def test_data_dir() -> Path:
|
||||
path = Path(__file__).parent / "data"
|
||||
data_dir = path.resolve()
|
||||
|
||||
# Generate compile_commands.json for hello_world
|
||||
hw_dir = data_dir / "hello_world"
|
||||
main_cpp = hw_dir / "main.cpp"
|
||||
cdb_path = hw_dir / "compile_commands.json"
|
||||
if main_cpp.exists() and not cdb_path.exists():
|
||||
cdb = [
|
||||
{
|
||||
"directory": hw_dir.as_posix(),
|
||||
"file": main_cpp.as_posix(),
|
||||
"arguments": [
|
||||
"clang++",
|
||||
"-std=c++17",
|
||||
"-fsyntax-only",
|
||||
main_cpp.as_posix(),
|
||||
],
|
||||
}
|
||||
]
|
||||
cdb_path.write_text(json.dumps(cdb, indent=2))
|
||||
|
||||
_generate_test_data_cdbs(data_dir)
|
||||
return data_dir
|
||||
|
||||
|
||||
def generate_cdb(workspace: Path) -> None:
|
||||
"""Generate compile_commands.json using CMake with Ninja backend."""
|
||||
cmake = shutil.which("cmake")
|
||||
if cmake is None:
|
||||
raise RuntimeError("cmake executable not found in PATH")
|
||||
toolchain = Path(__file__).resolve().parent.parent / "cmake" / "toolchain.cmake"
|
||||
cmd = [
|
||||
cmake,
|
||||
"-G",
|
||||
"Ninja",
|
||||
"-DCMAKE_EXPORT_COMPILE_COMMANDS=ON",
|
||||
f"-DCMAKE_TOOLCHAIN_FILE={toolchain}",
|
||||
"-S",
|
||||
str(workspace),
|
||||
"-B",
|
||||
str(workspace / "build"),
|
||||
]
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"cmake failed:\n{result.stderr}")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def workspace(request: pytest.FixtureRequest, test_data_dir: Path) -> Path | None:
|
||||
"""Resolve workspace path from @pytest.mark.workspace("subdir") marker.
|
||||
@@ -257,7 +113,41 @@ async def client(
|
||||
|
||||
yield c
|
||||
|
||||
# Graceful shutdown
|
||||
await _shutdown_client(c)
|
||||
|
||||
|
||||
def generate_cdb(workspace: Path) -> None:
|
||||
"""Generate compile_commands.json using CMake with Ninja backend."""
|
||||
cmake = shutil.which("cmake")
|
||||
if cmake is None:
|
||||
raise RuntimeError("cmake executable not found in PATH")
|
||||
toolchain = Path(__file__).resolve().parent.parent / "cmake" / "toolchain.cmake"
|
||||
cmd = [
|
||||
cmake,
|
||||
"-G",
|
||||
"Ninja",
|
||||
"-DCMAKE_EXPORT_COMPILE_COMMANDS=ON",
|
||||
f"-DCMAKE_TOOLCHAIN_FILE={toolchain}",
|
||||
"-S",
|
||||
str(workspace),
|
||||
"-B",
|
||||
str(workspace / "build"),
|
||||
]
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"cmake failed:\n{result.stderr}")
|
||||
|
||||
|
||||
async def make_client(executable: Path, workspace: Path) -> CliceClient:
|
||||
"""Spawn a fresh clice server and initialize it. For multi-session tests."""
|
||||
c = CliceClient()
|
||||
await c.start_io(str(executable), "--mode", "pipe")
|
||||
await c.initialize(workspace)
|
||||
return c
|
||||
|
||||
|
||||
async def _shutdown_client(c: CliceClient) -> None:
|
||||
"""Gracefully shut down a client, force-kill if needed."""
|
||||
try:
|
||||
await asyncio.wait_for(c.shutdown_async(None), timeout=3.0)
|
||||
except Exception:
|
||||
@@ -267,12 +157,10 @@ async def client(
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Wait briefly, then force-kill if still running
|
||||
await asyncio.sleep(0.3)
|
||||
if hasattr(c, "_server") and c._server is not None and c._server.returncode is None:
|
||||
c._server.kill()
|
||||
|
||||
# Dump server stderr warnings for diagnostics.
|
||||
try:
|
||||
server = getattr(c, "_server", None)
|
||||
if server and server.stderr:
|
||||
@@ -284,7 +172,6 @@ async def client(
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Stop pygls client (with timeout to avoid hanging)
|
||||
try:
|
||||
c._stop_event.set()
|
||||
for task in c._async_tasks:
|
||||
@@ -292,3 +179,73 @@ async def client(
|
||||
await asyncio.sleep(0.1)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
shutdown_client = _shutdown_client # Public alias for multi-session tests
|
||||
|
||||
|
||||
def _generate_test_data_cdbs(data_dir: Path) -> None:
|
||||
"""Generate compile_commands.json for all static test data directories."""
|
||||
|
||||
def _write(directory: Path, entries: list[dict]) -> None:
|
||||
(directory / "compile_commands.json").write_text(json.dumps(entries, indent=2))
|
||||
|
||||
def _entry(directory: Path, source: Path, extra_args: list[str] | None = None):
|
||||
args = ["clang++", "-std=c++17", "-fsyntax-only"]
|
||||
if extra_args:
|
||||
args.extend(extra_args)
|
||||
args.append(source.as_posix())
|
||||
return {
|
||||
"directory": directory.as_posix(),
|
||||
"file": source.as_posix(),
|
||||
"arguments": args,
|
||||
}
|
||||
|
||||
# hello_world
|
||||
hw_dir = data_dir / "hello_world"
|
||||
hw_main = hw_dir / "main.cpp"
|
||||
if hw_main.exists():
|
||||
_write(hw_dir, [_entry(hw_dir, hw_main)])
|
||||
|
||||
# header_context (always regenerate — absolute paths)
|
||||
hc_dir = data_dir / "header_context"
|
||||
hc_main = hc_dir / "main.cpp"
|
||||
if hc_main.exists():
|
||||
_write(hc_dir, [_entry(hc_dir, hc_main, [f"-I{hc_dir.as_posix()}"])])
|
||||
|
||||
# multi_context (same file, two configs)
|
||||
mc_dir = data_dir / "multi_context"
|
||||
mc_main = mc_dir / "main.cpp"
|
||||
if mc_main.exists():
|
||||
_write(
|
||||
mc_dir,
|
||||
[
|
||||
_entry(mc_dir, mc_main, ["-DCONFIG_A"]),
|
||||
_entry(mc_dir, mc_main, ["-DCONFIG_B"]),
|
||||
],
|
||||
)
|
||||
|
||||
# include_completion
|
||||
ic_dir = data_dir / "include_completion"
|
||||
ic_main = ic_dir / "main.cpp"
|
||||
if ic_main.exists():
|
||||
_write(ic_dir, [_entry(ic_dir, ic_main, ["-I."])])
|
||||
|
||||
# document_links
|
||||
dl_dir = data_dir / "document_links"
|
||||
dl_main = dl_dir / "main.cpp"
|
||||
if dl_main.exists():
|
||||
_write(
|
||||
dl_dir, [_entry(dl_dir, dl_main, [f"-I{dl_dir.as_posix()}", "-std=c++23"])]
|
||||
)
|
||||
|
||||
# pch_test
|
||||
pt_dir = data_dir / "pch_test"
|
||||
if pt_dir.exists():
|
||||
entries = []
|
||||
for src_name in ["main.cpp", "no_includes.cpp"]:
|
||||
src = pt_dir / src_name
|
||||
if src.exists():
|
||||
entries.append(_entry(pt_dir, src))
|
||||
if entries:
|
||||
_write(pt_dir, entries)
|
||||
|
||||
1
tests/data/document_links/data.bin
Normal file
1
tests/data/document_links/data.bin
Normal file
@@ -0,0 +1 @@
|
||||
0123456789
|
||||
3
tests/data/document_links/header_a.h
Normal file
3
tests/data/document_links/header_a.h
Normal file
@@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
int a = 1;
|
||||
3
tests/data/document_links/header_b.h
Normal file
3
tests/data/document_links/header_b.h
Normal file
@@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
int b = 2;
|
||||
3
tests/data/document_links/header_c.h
Normal file
3
tests/data/document_links/header_c.h
Normal file
@@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
int c = 3;
|
||||
20
tests/data/document_links/main.cpp
Normal file
20
tests/data/document_links/main.cpp
Normal file
@@ -0,0 +1,20 @@
|
||||
#include "header_a.h"
|
||||
#include "header_b.h"
|
||||
int x = 1;
|
||||
#include "header_c.h"
|
||||
|
||||
const char data[] = {
|
||||
#embed "data.bin"
|
||||
};
|
||||
|
||||
#if __has_embed("data.bin")
|
||||
int has_embed_found = 1;
|
||||
#endif
|
||||
|
||||
#if __has_embed("no_such_file.bin")
|
||||
int has_embed_not_found = 1;
|
||||
#endif
|
||||
|
||||
int main() {
|
||||
return a + b + c;
|
||||
}
|
||||
6
tests/data/header_context/inner.h
Normal file
6
tests/data/header_context/inner.h
Normal file
@@ -0,0 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
// Non self-contained: uses Point from the include chain.
|
||||
inline Point inner_origin() {
|
||||
return Point{0, 0};
|
||||
}
|
||||
8
tests/data/header_context/main.cpp
Normal file
8
tests/data/header_context/main.cpp
Normal file
@@ -0,0 +1,8 @@
|
||||
#include "types.h"
|
||||
#include "utils.h"
|
||||
|
||||
int main() {
|
||||
Point p{3, 4};
|
||||
int d = calc(p);
|
||||
return d;
|
||||
}
|
||||
12
tests/data/header_context/types.h
Normal file
12
tests/data/header_context/types.h
Normal file
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
struct Point {
|
||||
int x;
|
||||
int y;
|
||||
};
|
||||
|
||||
inline int distance(Point a, Point b) {
|
||||
int dx = a.x - b.x;
|
||||
int dy = a.y - b.y;
|
||||
return dx * dx + dy * dy;
|
||||
}
|
||||
9
tests/data/header_context/utils.h
Normal file
9
tests/data/header_context/utils.h
Normal file
@@ -0,0 +1,9 @@
|
||||
// Non self-contained header: uses Point from types.h without including it.
|
||||
// Depends on the including source file to provide the types.h include.
|
||||
#pragma once
|
||||
|
||||
#include "inner.h"
|
||||
|
||||
inline int calc(Point p) {
|
||||
return distance(p, inner_origin());
|
||||
}
|
||||
@@ -1,8 +1,12 @@
|
||||
#include <iostream>
|
||||
|
||||
int add(int a, int b) {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
int main() {
|
||||
std::cout << "hello world" << std::endl;
|
||||
|
||||
int result = add(1, 2);
|
||||
return result;
|
||||
}
|
||||
|
||||
1
tests/data/include_completion/main.cpp
Normal file
1
tests/data/include_completion/main.cpp
Normal file
@@ -0,0 +1 @@
|
||||
#include "
|
||||
2
tests/data/include_completion/myheader.h
Normal file
2
tests/data/include_completion/myheader.h
Normal file
@@ -0,0 +1,2 @@
|
||||
#pragma once
|
||||
int my_func();
|
||||
2
tests/data/include_completion/subdir/nested.h
Normal file
2
tests/data/include_completion/subdir/nested.h
Normal file
@@ -0,0 +1,2 @@
|
||||
#pragma once
|
||||
int nested_func();
|
||||
15
tests/data/multi_context/main.cpp
Normal file
15
tests/data/multi_context/main.cpp
Normal file
@@ -0,0 +1,15 @@
|
||||
#ifdef CONFIG_A
|
||||
int config_value() {
|
||||
return 1;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef CONFIG_B
|
||||
int config_value() {
|
||||
return 2;
|
||||
}
|
||||
#endif
|
||||
|
||||
int main() {
|
||||
return config_value();
|
||||
}
|
||||
0
tests/integration/compilation/__init__.py
Normal file
0
tests/integration/compilation/__init__.py
Normal file
138
tests/integration/compilation/test_pch.py
Normal file
138
tests/integration/compilation/test_pch.py
Normal file
@@ -0,0 +1,138 @@
|
||||
"""Integration tests for PCH (precompiled header) functionality in MasterServer."""
|
||||
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
from lsprotocol.types import (
|
||||
CompletionParams,
|
||||
DidCloseTextDocumentParams,
|
||||
HoverParams,
|
||||
Position,
|
||||
)
|
||||
|
||||
from tests.integration.utils import doc
|
||||
from tests.integration.utils.workspace import did_change
|
||||
from tests.integration.utils.wait import wait_for_recompile
|
||||
from tests.integration.utils.assertions import assert_clean_compile, assert_no_errors
|
||||
|
||||
|
||||
@pytest.mark.workspace("pch_test")
|
||||
async def test_pch_diagnostics_on_open(client, workspace):
|
||||
"""Opening a file with #include should trigger PCH build and return clean diagnostics."""
|
||||
uri, _ = await client.open_and_wait(workspace / "main.cpp")
|
||||
assert uri in client.diagnostics
|
||||
# main.cpp is well-formed, so diagnostics list should be empty (no errors).
|
||||
assert_clean_compile(client, uri)
|
||||
client.text_document_did_close(DidCloseTextDocumentParams(text_document=doc(uri)))
|
||||
|
||||
|
||||
@pytest.mark.workspace("pch_test")
|
||||
async def test_pch_body_edit_triggers_recompile(client, workspace):
|
||||
"""Editing only the body (not the preamble) should trigger recompilation."""
|
||||
uri, content = await client.open_and_wait(workspace / "main.cpp")
|
||||
|
||||
# Edit only the function body — preamble (#include "common.h") unchanged.
|
||||
new_content = content.replace("return result;", "return result + 1;")
|
||||
did_change(client, uri, 1, new_content)
|
||||
# Send hover to trigger recompilation via pull-based model.
|
||||
await wait_for_recompile(client, uri, timeout=30.0)
|
||||
assert uri in client.diagnostics
|
||||
client.text_document_did_close(DidCloseTextDocumentParams(text_document=doc(uri)))
|
||||
|
||||
|
||||
@pytest.mark.workspace("pch_test")
|
||||
async def test_no_pch_for_no_includes(client, workspace):
|
||||
"""A file with no #include directives should compile without PCH."""
|
||||
uri, _ = await client.open_and_wait(workspace / "no_includes.cpp")
|
||||
assert uri in client.diagnostics
|
||||
assert_clean_compile(client, uri)
|
||||
client.text_document_did_close(DidCloseTextDocumentParams(text_document=doc(uri)))
|
||||
|
||||
|
||||
@pytest.mark.workspace("pch_test")
|
||||
async def test_hover_on_local_symbol(client, workspace):
|
||||
"""Hover on a locally defined symbol should work when PCH is active."""
|
||||
uri, _ = await client.open_and_wait(workspace / "main.cpp")
|
||||
|
||||
# Hover over "add" on line 2 (0-indexed): "int add(int a, int b) {"
|
||||
result = await client.text_document_hover_async(
|
||||
HoverParams(text_document=doc(uri), position=Position(line=2, character=4))
|
||||
)
|
||||
assert result is not None
|
||||
client.text_document_did_close(DidCloseTextDocumentParams(text_document=doc(uri)))
|
||||
|
||||
|
||||
@pytest.mark.workspace("pch_test")
|
||||
async def test_completion_with_pch(client, workspace):
|
||||
"""Completion should see symbols from PCH headers."""
|
||||
uri, content = await client.open_and_wait(workspace / "main.cpp")
|
||||
|
||||
# Add a line that starts typing "Poi" to trigger completion for Point.
|
||||
new_content = content + "\nPoi"
|
||||
lines = new_content.split("\n")
|
||||
last_line = len(lines) - 1
|
||||
|
||||
did_change(client, uri, 1, new_content)
|
||||
|
||||
# The completion request itself triggers compilation via ensure_compiled().
|
||||
result = await client.text_document_completion_async(
|
||||
CompletionParams(
|
||||
text_document=doc(uri),
|
||||
position=Position(line=last_line, character=3),
|
||||
)
|
||||
)
|
||||
# Completion should return results.
|
||||
assert result is not None
|
||||
client.text_document_did_close(DidCloseTextDocumentParams(text_document=doc(uri)))
|
||||
|
||||
|
||||
@pytest.mark.workspace("pch_test")
|
||||
async def test_preamble_edit_then_hover(client, workspace):
|
||||
"""After editing the preamble (adding an #include), AST should still work."""
|
||||
uri, content = await client.open_and_wait(workspace / "main.cpp")
|
||||
|
||||
# Verify initial state is clean.
|
||||
assert_clean_compile(client, uri)
|
||||
|
||||
# Edit the preamble: add a second #include (triggers PCH rebuild).
|
||||
# Use project-local header instead of system header (<cstdio>) to avoid
|
||||
# slow PCH rebuilds on macOS CI that cause SIGPIPE timeouts.
|
||||
new_content = '#include "common.h"\n#include "common.h"\n' + "\n".join(
|
||||
content.split("\n")[1:]
|
||||
)
|
||||
did_change(client, uri, 1, new_content)
|
||||
|
||||
# Trigger recompilation via hover — this will rebuild PCH with new preamble.
|
||||
await wait_for_recompile(client, uri)
|
||||
|
||||
# AST should still be valid — no errors.
|
||||
assert_no_errors(client, uri, "Expected no errors after preamble edit")
|
||||
|
||||
# Hover should still work on a symbol.
|
||||
result = await client.text_document_hover_async(
|
||||
HoverParams(text_document=doc(uri), position=Position(line=3, character=4))
|
||||
)
|
||||
assert result is not None, "Hover failed after preamble edit"
|
||||
client.text_document_did_close(DidCloseTextDocumentParams(text_document=doc(uri)))
|
||||
|
||||
|
||||
@pytest.mark.workspace("pch_test")
|
||||
async def test_preamble_edit_multiple_times(client, workspace):
|
||||
"""Multiple preamble edits should not break AST building."""
|
||||
uri, content = await client.open_and_wait(workspace / "main.cpp")
|
||||
|
||||
for i in range(3):
|
||||
# Add progressively more includes.
|
||||
includes = '#include "common.h"\n'
|
||||
for j in range(i + 1):
|
||||
includes += f"// edit {j}\n"
|
||||
new_content = includes + "\n".join(content.split("\n")[1:])
|
||||
|
||||
version = i + 1
|
||||
did_change(client, uri, version, new_content)
|
||||
|
||||
await wait_for_recompile(client, uri)
|
||||
|
||||
# After multiple edits, should still be clean.
|
||||
assert_no_errors(client, uri, "Expected no errors after multiple preamble edits")
|
||||
client.text_document_did_close(DidCloseTextDocumentParams(text_document=doc(uri)))
|
||||
282
tests/integration/compilation/test_persistent_cache.py
Normal file
282
tests/integration/compilation/test_persistent_cache.py
Normal file
@@ -0,0 +1,282 @@
|
||||
"""Integration tests for persistent PCH/PCM cache.
|
||||
|
||||
Verifies that PCH/PCM artifacts are written to .clice/cache/pch/ and .clice/cache/pcm/
|
||||
with content-addressed filenames, survive server restarts via cache.json,
|
||||
and are properly reused across sessions.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
from lsprotocol.types import (
|
||||
DidCloseTextDocumentParams,
|
||||
HoverParams,
|
||||
Position,
|
||||
)
|
||||
|
||||
from tests.conftest import make_client, shutdown_client
|
||||
from tests.integration.utils import write_cdb, doc
|
||||
from tests.integration.utils.cache import (
|
||||
list_pch_files,
|
||||
list_pcm_files,
|
||||
read_cache_json,
|
||||
)
|
||||
from tests.integration.utils.assertions import assert_clean_compile
|
||||
|
||||
|
||||
async def test_pch_written_to_cache_dir(client, tmp_path):
|
||||
"""After opening a file with #include, a .pch file should appear
|
||||
in .clice/cache/pch/ with a hex-hash filename."""
|
||||
(tmp_path / "header.h").write_text("#pragma once\nstruct Foo { int x; };\n")
|
||||
(tmp_path / "main.cpp").write_text(
|
||||
'#include "header.h"\nint main() { Foo f; return f.x; }\n'
|
||||
)
|
||||
write_cdb(tmp_path, ["main.cpp"])
|
||||
await client.initialize(tmp_path)
|
||||
|
||||
uri, _ = await client.open_and_wait(tmp_path / "main.cpp")
|
||||
assert_clean_compile(client, uri)
|
||||
|
||||
# Verify PCH file exists in the cache directory.
|
||||
pch_files = list_pch_files(tmp_path)
|
||||
assert len(pch_files) >= 1, "Expected at least one .pch file in .clice/cache/pch/"
|
||||
# Filename should be a 16-char hex hash + .pch
|
||||
assert pch_files[0].stem and len(pch_files[0].stem) == 16, (
|
||||
f"Expected 16-char hex filename, got: {pch_files[0].name}"
|
||||
)
|
||||
|
||||
|
||||
async def test_cache_json_persisted(client, tmp_path):
|
||||
"""After a PCH build, cache.json should be written with the entry."""
|
||||
(tmp_path / "header.h").write_text("#pragma once\nint global_val = 42;\n")
|
||||
(tmp_path / "main.cpp").write_text(
|
||||
'#include "header.h"\nint main() { return global_val; }\n'
|
||||
)
|
||||
write_cdb(tmp_path, ["main.cpp"])
|
||||
await client.initialize(tmp_path)
|
||||
|
||||
uri, _ = await client.open_and_wait(tmp_path / "main.cpp")
|
||||
assert_clean_compile(client, uri)
|
||||
|
||||
cache = read_cache_json(tmp_path)
|
||||
assert cache is not None, "cache.json should exist after PCH build"
|
||||
assert "pch" in cache, "cache.json should have 'pch' section"
|
||||
assert len(cache["pch"]) >= 1, "Expected at least one PCH entry in cache.json"
|
||||
|
||||
# Verify the entry has expected fields.
|
||||
entry = cache["pch"][0]
|
||||
assert "hash" in entry
|
||||
assert "build_at" in entry
|
||||
assert "deps" in entry
|
||||
assert "source_file" in entry
|
||||
|
||||
|
||||
async def test_pch_reused_on_close_reopen(client, tmp_path):
|
||||
"""Closing and reopening a file within the same session should reuse
|
||||
the cached PCH — no additional .pch files should be created."""
|
||||
(tmp_path / "header.h").write_text("#pragma once\nstruct Bar { int y; };\n")
|
||||
(tmp_path / "main.cpp").write_text(
|
||||
'#include "header.h"\nint main() { Bar b; return b.y; }\n'
|
||||
)
|
||||
write_cdb(tmp_path, ["main.cpp"])
|
||||
await client.initialize(tmp_path)
|
||||
|
||||
# First open — builds PCH.
|
||||
uri, _ = await client.open_and_wait(tmp_path / "main.cpp")
|
||||
assert_clean_compile(client, uri)
|
||||
|
||||
pch_after_first = list_pch_files(tmp_path)
|
||||
assert len(pch_after_first) >= 1
|
||||
|
||||
# Close.
|
||||
client.text_document_did_close(DidCloseTextDocumentParams(text_document=doc(uri)))
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
# Clear diagnostics so we can wait for fresh ones.
|
||||
client.diagnostics.pop(uri, None)
|
||||
|
||||
# Reopen — should reuse cached PCH.
|
||||
uri2, _ = await client.open_and_wait(tmp_path / "main.cpp")
|
||||
assert_clean_compile(client, uri2)
|
||||
|
||||
pch_after_reopen = list_pch_files(tmp_path)
|
||||
assert pch_after_first == pch_after_reopen, (
|
||||
"PCH file set should be identical after close+reopen"
|
||||
)
|
||||
|
||||
|
||||
async def test_pch_survives_server_restart(executable, tmp_path):
|
||||
"""PCH cache should survive a full server restart — cache.json is
|
||||
loaded on startup and the existing .pch file is reused."""
|
||||
(tmp_path / "header.h").write_text("#pragma once\nstruct Baz { int z; };\n")
|
||||
(tmp_path / "main.cpp").write_text(
|
||||
'#include "header.h"\nint main() { Baz b; return b.z; }\n'
|
||||
)
|
||||
write_cdb(tmp_path, ["main.cpp"])
|
||||
|
||||
# Session 1: build PCH.
|
||||
c1 = await make_client(executable, tmp_path)
|
||||
uri, _ = await c1.open_and_wait(tmp_path / "main.cpp")
|
||||
assert_clean_compile(c1, uri)
|
||||
|
||||
pch_files_s1 = list_pch_files(tmp_path)
|
||||
assert len(pch_files_s1) >= 1, "PCH should be created in session 1"
|
||||
pch_mtime_s1 = pch_files_s1[0].stat().st_mtime
|
||||
|
||||
cache_s1 = read_cache_json(tmp_path)
|
||||
assert cache_s1 is not None, "cache.json should exist after session 1"
|
||||
|
||||
await shutdown_client(c1)
|
||||
|
||||
# Session 2: restart server, reopen file.
|
||||
c2 = await make_client(executable, tmp_path)
|
||||
# Clear so we can detect fresh diagnostics.
|
||||
uri2, _ = await c2.open_and_wait(tmp_path / "main.cpp")
|
||||
assert_clean_compile(c2, uri2)
|
||||
|
||||
# The same PCH file should still exist, not overwritten.
|
||||
pch_files_s2 = list_pch_files(tmp_path)
|
||||
assert len(pch_files_s2) == len(pch_files_s1), (
|
||||
"No new PCH files should be created in session 2"
|
||||
)
|
||||
pch_mtime_s2 = pch_files_s2[0].stat().st_mtime
|
||||
assert pch_mtime_s1 == pch_mtime_s2, (
|
||||
"PCH file should not be rebuilt (mtime should be unchanged)"
|
||||
)
|
||||
|
||||
await shutdown_client(c2)
|
||||
|
||||
|
||||
async def test_shared_preamble_shares_pch(client, tmp_path):
|
||||
"""Two files with identical preambles should share the same PCH file
|
||||
(content-addressed by preamble hash)."""
|
||||
(tmp_path / "header.h").write_text("#pragma once\nint shared_val = 1;\n")
|
||||
(tmp_path / "a.cpp").write_text(
|
||||
'#include "header.h"\nint fa() { return shared_val; }\n'
|
||||
)
|
||||
(tmp_path / "b.cpp").write_text(
|
||||
'#include "header.h"\nint fb() { return shared_val + 1; }\n'
|
||||
)
|
||||
write_cdb(tmp_path, ["a.cpp", "b.cpp"])
|
||||
await client.initialize(tmp_path)
|
||||
|
||||
uri_a, _ = await client.open_and_wait(tmp_path / "a.cpp")
|
||||
uri_b, _ = await client.open_and_wait(tmp_path / "b.cpp")
|
||||
assert_clean_compile(client, uri_a)
|
||||
assert_clean_compile(client, uri_b)
|
||||
|
||||
# Both files have the same preamble (#include "header.h").
|
||||
# Content-addressed naming means only ONE .pch file should exist.
|
||||
pch_files = list_pch_files(tmp_path)
|
||||
assert len(pch_files) == 1, (
|
||||
f"Expected exactly 1 PCH file for shared preamble, got {len(pch_files)}: "
|
||||
f"{[f.name for f in pch_files]}"
|
||||
)
|
||||
|
||||
|
||||
async def test_different_preamble_different_pch(client, tmp_path):
|
||||
"""Files with different preambles should produce different PCH files."""
|
||||
(tmp_path / "a.h").write_text("#pragma once\nint val_a = 1;\n")
|
||||
(tmp_path / "b.h").write_text("#pragma once\nint val_b = 2;\n")
|
||||
(tmp_path / "a.cpp").write_text('#include "a.h"\nint fa() { return val_a; }\n')
|
||||
(tmp_path / "b.cpp").write_text('#include "b.h"\nint fb() { return val_b; }\n')
|
||||
write_cdb(tmp_path, ["a.cpp", "b.cpp"])
|
||||
await client.initialize(tmp_path)
|
||||
|
||||
uri_a, _ = await client.open_and_wait(tmp_path / "a.cpp")
|
||||
uri_b, _ = await client.open_and_wait(tmp_path / "b.cpp")
|
||||
assert_clean_compile(client, uri_a)
|
||||
assert_clean_compile(client, uri_b)
|
||||
|
||||
# Different preambles → different hash → two separate .pch files.
|
||||
pch_files = list_pch_files(tmp_path)
|
||||
assert len(pch_files) == 2, (
|
||||
f"Expected 2 PCH files for different preambles, got {len(pch_files)}: "
|
||||
f"{[f.name for f in pch_files]}"
|
||||
)
|
||||
|
||||
|
||||
async def test_pch_rebuilt_on_header_change(client, tmp_path):
|
||||
"""When a preamble header changes, a new PCH should be built
|
||||
(different hash → different filename). The old one remains for cleanup."""
|
||||
(tmp_path / "header.h").write_text("#pragma once\nstruct V1 { int a; };\n")
|
||||
(tmp_path / "main.cpp").write_text(
|
||||
'#include "header.h"\nint main() { V1 v; return v.a; }\n'
|
||||
)
|
||||
write_cdb(tmp_path, ["main.cpp"])
|
||||
await client.initialize(tmp_path)
|
||||
|
||||
uri, _ = await client.open_and_wait(tmp_path / "main.cpp")
|
||||
assert_clean_compile(client, uri)
|
||||
|
||||
pch_before = list_pch_files(tmp_path)
|
||||
assert len(pch_before) >= 1
|
||||
|
||||
# Modify header — changes preamble content hash.
|
||||
await asyncio.sleep(1.1)
|
||||
(tmp_path / "header.h").write_text("#pragma once\nstruct V2 { int b; };\n")
|
||||
# Also update main.cpp to use V2 so it compiles cleanly.
|
||||
(tmp_path / "main.cpp").write_text(
|
||||
'#include "header.h"\nint main() { V2 v; return v.b; }\n'
|
||||
)
|
||||
|
||||
# Close and reopen to get fresh preamble.
|
||||
client.text_document_did_close(DidCloseTextDocumentParams(text_document=doc(uri)))
|
||||
await asyncio.sleep(0.5)
|
||||
client.diagnostics.pop(uri, None)
|
||||
|
||||
uri2, _ = await client.open_and_wait(tmp_path / "main.cpp")
|
||||
assert_clean_compile(client, uri2)
|
||||
|
||||
pch_after = list_pch_files(tmp_path)
|
||||
# The preamble content changed (#include "header.h" is the same text,
|
||||
# but the preamble hash is computed from the preamble TEXT in the source file,
|
||||
# not from the header content). Since the #include line is identical,
|
||||
# the preamble hash is the same → same PCH filename, but deps changed
|
||||
# so PCH gets rebuilt (overwritten at the same path).
|
||||
# Either way, compilation should succeed.
|
||||
assert len(pch_after) >= 1
|
||||
|
||||
|
||||
async def test_no_tmp_files_after_build(client, tmp_path):
|
||||
"""After a successful PCH build, no .tmp files should remain in the cache dir."""
|
||||
(tmp_path / "header.h").write_text("#pragma once\nint val = 1;\n")
|
||||
(tmp_path / "main.cpp").write_text(
|
||||
'#include "header.h"\nint main() { return val; }\n'
|
||||
)
|
||||
write_cdb(tmp_path, ["main.cpp"])
|
||||
await client.initialize(tmp_path)
|
||||
|
||||
uri, _ = await client.open_and_wait(tmp_path / "main.cpp")
|
||||
assert_clean_compile(client, uri)
|
||||
|
||||
# No .tmp files should linger.
|
||||
pch_dir = tmp_path / ".clice" / "cache" / "pch"
|
||||
if pch_dir.exists():
|
||||
tmp_files = list(pch_dir.glob("*.tmp"))
|
||||
assert len(tmp_files) == 0, f"Stale .tmp files found: {tmp_files}"
|
||||
|
||||
pcm_dir = tmp_path / ".clice" / "cache" / "pcm"
|
||||
if pcm_dir.exists():
|
||||
tmp_files = list(pcm_dir.glob("*.tmp"))
|
||||
assert len(tmp_files) == 0, f"Stale .tmp files found: {tmp_files}"
|
||||
|
||||
|
||||
async def test_cache_dirs_created_on_startup(client, tmp_path):
|
||||
"""The .clice/cache/pch/ and .clice/cache/pcm/ directories should be created
|
||||
when the server initializes a workspace."""
|
||||
(tmp_path / "main.cpp").write_text("int main() { return 0; }\n")
|
||||
write_cdb(tmp_path, ["main.cpp"])
|
||||
await client.initialize(tmp_path)
|
||||
|
||||
# Trigger a compilation to ensure load_workspace() has completed
|
||||
# (it runs asynchronously after initialization).
|
||||
uri, _ = await client.open_and_wait(tmp_path / "main.cpp")
|
||||
assert_clean_compile(client, uri)
|
||||
|
||||
assert (tmp_path / ".clice" / "cache" / "pch").is_dir(), (
|
||||
".clice/cache/pch/ should be created"
|
||||
)
|
||||
assert (tmp_path / ".clice" / "cache" / "pcm").is_dir(), (
|
||||
".clice/cache/pcm/ should be created"
|
||||
)
|
||||
398
tests/integration/compilation/test_staleness.py
Normal file
398
tests/integration/compilation/test_staleness.py
Normal file
@@ -0,0 +1,398 @@
|
||||
"""Integration tests for mtime-based staleness tracking.
|
||||
|
||||
Verifies that ensure_compiled() and ensure_pch() detect dependency file
|
||||
changes via mtime snapshots, triggering recompilation without relying
|
||||
on didSave to mark everything dirty.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import shutil
|
||||
|
||||
import pytest
|
||||
from lsprotocol.types import (
|
||||
DidChangeTextDocumentParams,
|
||||
DidCloseTextDocumentParams,
|
||||
DidSaveTextDocumentParams,
|
||||
HoverParams,
|
||||
Position,
|
||||
TextDocumentContentChangeWholeDocument,
|
||||
TextDocumentIdentifier,
|
||||
VersionedTextDocumentIdentifier,
|
||||
)
|
||||
|
||||
from tests.integration.utils import write_cdb, doc
|
||||
from tests.integration.utils.wait import wait_for_recompile
|
||||
from tests.integration.utils.assertions import assert_clean_compile, assert_has_errors
|
||||
|
||||
|
||||
async def test_header_change_invalidates_ast(client, tmp_path):
|
||||
"""Modifying a header on disk should cause recompilation on next hover,
|
||||
even though didSave was never called (mtime-based detection)."""
|
||||
# Setup: main.cpp includes header.h
|
||||
(tmp_path / "header.h").write_text("inline int value() { return 1; }\n")
|
||||
(tmp_path / "main.cpp").write_text(
|
||||
'#include "header.h"\nint main() { return value(); }\n'
|
||||
)
|
||||
write_cdb(tmp_path, ["main.cpp"])
|
||||
await client.initialize(tmp_path)
|
||||
|
||||
# First compile — should succeed with no diagnostics.
|
||||
uri, _ = await client.open_and_wait(tmp_path / "main.cpp")
|
||||
assert_clean_compile(client, uri)
|
||||
|
||||
# Modify header on disk — introduce an error.
|
||||
# Ensure mtime advances past filesystem granularity (1s on some FSes).
|
||||
await asyncio.sleep(1.1)
|
||||
(tmp_path / "header.h").write_text(
|
||||
"inline int value() { return }\n"
|
||||
) # syntax error
|
||||
|
||||
# Send another hover — ensure_compiled should detect mtime change
|
||||
# in deps and trigger recompilation. The recompilation publishes
|
||||
# fresh diagnostics as a side effect.
|
||||
await wait_for_recompile(client, uri)
|
||||
|
||||
# Should now have diagnostics from the broken header.
|
||||
assert_has_errors(client, uri, "Expected diagnostics after header change")
|
||||
|
||||
|
||||
async def test_header_change_invalidates_pch(client, tmp_path):
|
||||
"""Modifying a preamble header on disk should trigger PCH rebuild."""
|
||||
(tmp_path / "header.h").write_text("#pragma once\nstruct Foo { int x; };\n")
|
||||
(tmp_path / "main.cpp").write_text(
|
||||
'#include "header.h"\nint main() { Foo f; return f.x; }\n'
|
||||
)
|
||||
write_cdb(tmp_path, ["main.cpp"])
|
||||
await client.initialize(tmp_path)
|
||||
|
||||
# First compile — success.
|
||||
uri, _ = await client.open_and_wait(tmp_path / "main.cpp")
|
||||
assert_clean_compile(client, uri)
|
||||
|
||||
# Modify header — rename struct field.
|
||||
# Ensure mtime advances past filesystem granularity (1s on some FSes).
|
||||
await asyncio.sleep(1.1)
|
||||
(tmp_path / "header.h").write_text(
|
||||
"#pragma once\nstruct Foo { int y; };\n" # x -> y
|
||||
)
|
||||
|
||||
# Hover again — PCH should rebuild, AST should recompile.
|
||||
# main.cpp uses f.x which no longer exists → diagnostics expected.
|
||||
await wait_for_recompile(client, uri, timeout=30.0)
|
||||
|
||||
assert_has_errors(client, uri, "Expected error after header field rename")
|
||||
|
||||
|
||||
async def test_no_change_skips_recompile(client, tmp_path):
|
||||
"""When no dependency has changed, ensure_compiled should fast-path."""
|
||||
(tmp_path / "main.cpp").write_text("int main() { return 0; }\n")
|
||||
write_cdb(tmp_path, ["main.cpp"])
|
||||
await client.initialize(tmp_path)
|
||||
|
||||
uri, _ = await client.open_and_wait(tmp_path / "main.cpp")
|
||||
assert_clean_compile(client, uri)
|
||||
|
||||
# Second hover — should use cached AST (no recompilation).
|
||||
# Verify it returns quickly and doesn't crash.
|
||||
hover = await client.text_document_hover_async(
|
||||
HoverParams(text_document=doc(uri), position=Position(line=0, character=4))
|
||||
)
|
||||
# "main" should be hoverable.
|
||||
assert hover is not None
|
||||
|
||||
|
||||
async def test_touch_without_content_change_skips_recompile(client, tmp_path):
|
||||
"""Layer 2: touching a header (mtime changes) without modifying content
|
||||
should NOT trigger recompilation — the hash check catches this."""
|
||||
(tmp_path / "header.h").write_text("inline int value() { return 1; }\n")
|
||||
(tmp_path / "main.cpp").write_text(
|
||||
'#include "header.h"\nint main() { return value(); }\n'
|
||||
)
|
||||
write_cdb(tmp_path, ["main.cpp"])
|
||||
await client.initialize(tmp_path)
|
||||
|
||||
uri, _ = await client.open_and_wait(tmp_path / "main.cpp")
|
||||
assert_clean_compile(client, uri)
|
||||
|
||||
# Touch the header — mtime changes but content stays the same.
|
||||
await asyncio.sleep(1.1)
|
||||
original_content = (tmp_path / "header.h").read_text()
|
||||
(tmp_path / "header.h").write_text(original_content)
|
||||
|
||||
# Hover triggers ensure_compiled which runs deps_changed.
|
||||
# Layer 2 hash confirms nothing actually changed → cached AST reused.
|
||||
# Hover on "main" (line 1, col 4) which should be hoverable.
|
||||
hover = await client.text_document_hover_async(
|
||||
HoverParams(text_document=doc(uri), position=Position(line=1, character=4))
|
||||
)
|
||||
assert hover is not None
|
||||
|
||||
# No new diagnostics should appear — the file is still clean.
|
||||
assert_clean_compile(client, uri)
|
||||
|
||||
|
||||
async def test_header_replaced_with_different_content(client, tmp_path):
|
||||
"""Replacing a header file with different content should be detected
|
||||
and trigger recompilation reflecting the new content."""
|
||||
(tmp_path / "header.h").write_text("inline int value() { return 1; }\n")
|
||||
(tmp_path / "main.cpp").write_text(
|
||||
'#include "header.h"\nint main() { return value(); }\n'
|
||||
)
|
||||
write_cdb(tmp_path, ["main.cpp"])
|
||||
await client.initialize(tmp_path)
|
||||
|
||||
uri, _ = await client.open_and_wait(tmp_path / "main.cpp")
|
||||
assert_clean_compile(client, uri)
|
||||
|
||||
# Replace header — delete and recreate with a breaking change.
|
||||
await asyncio.sleep(1.1)
|
||||
(tmp_path / "header.h").unlink()
|
||||
(tmp_path / "header.h").write_text("inline int renamed_value() { return 1; }\n")
|
||||
|
||||
# main.cpp still calls value() which no longer exists → error.
|
||||
await wait_for_recompile(client, uri)
|
||||
|
||||
assert_has_errors(client, uri, "Expected diagnostics after header replacement")
|
||||
|
||||
|
||||
async def test_fix_error_clears_diagnostics(client, tmp_path):
|
||||
"""After introducing and fixing an error in a header, diagnostics
|
||||
should clear on the next recompilation cycle."""
|
||||
(tmp_path / "header.h").write_text("inline int value() { return }\n") # broken
|
||||
(tmp_path / "main.cpp").write_text(
|
||||
'#include "header.h"\nint main() { return value(); }\n'
|
||||
)
|
||||
write_cdb(tmp_path, ["main.cpp"])
|
||||
await client.initialize(tmp_path)
|
||||
|
||||
# First compile — should produce diagnostics.
|
||||
uri, _ = await client.open_and_wait(tmp_path / "main.cpp")
|
||||
assert_has_errors(client, uri, "Expected diagnostics from broken header")
|
||||
|
||||
# Fix the header.
|
||||
await asyncio.sleep(1.1)
|
||||
(tmp_path / "header.h").write_text("inline int value() { return 1; }\n")
|
||||
|
||||
# Hover triggers recompilation — diagnostics should clear.
|
||||
await wait_for_recompile(client, uri)
|
||||
|
||||
assert_clean_compile(client, uri)
|
||||
|
||||
|
||||
async def test_multiple_files_share_header(client, tmp_path):
|
||||
"""When a shared header changes, all open files that depend on it
|
||||
should detect the staleness independently."""
|
||||
(tmp_path / "shared.h").write_text("inline int shared() { return 1; }\n")
|
||||
(tmp_path / "a.cpp").write_text(
|
||||
'#include "shared.h"\nint fa() { return shared(); }\n'
|
||||
)
|
||||
(tmp_path / "b.cpp").write_text(
|
||||
'#include "shared.h"\nint fb() { return shared(); }\n'
|
||||
)
|
||||
write_cdb(tmp_path, ["a.cpp", "b.cpp"])
|
||||
await client.initialize(tmp_path)
|
||||
|
||||
uri_a, _ = await client.open_and_wait(tmp_path / "a.cpp")
|
||||
uri_b, _ = await client.open_and_wait(tmp_path / "b.cpp")
|
||||
assert_clean_compile(client, uri_a)
|
||||
assert_clean_compile(client, uri_b)
|
||||
|
||||
# Break the shared header.
|
||||
await asyncio.sleep(1.1)
|
||||
(tmp_path / "shared.h").write_text("inline int shared() { return }\n")
|
||||
|
||||
# Both files should get diagnostics after hover.
|
||||
await wait_for_recompile(client, uri_a)
|
||||
assert_has_errors(client, uri_a, "File A should have diagnostics")
|
||||
|
||||
await wait_for_recompile(client, uri_b)
|
||||
assert_has_errors(client, uri_b, "File B should have diagnostics")
|
||||
|
||||
|
||||
async def test_transitive_header_change(client, tmp_path):
|
||||
"""A change to a transitively included header should be detected."""
|
||||
(tmp_path / "base.h").write_text("inline int base() { return 1; }\n")
|
||||
(tmp_path / "mid.h").write_text('#include "base.h"\n')
|
||||
(tmp_path / "main.cpp").write_text(
|
||||
'#include "mid.h"\nint main() { return base(); }\n'
|
||||
)
|
||||
write_cdb(tmp_path, ["main.cpp"])
|
||||
await client.initialize(tmp_path)
|
||||
|
||||
uri, _ = await client.open_and_wait(tmp_path / "main.cpp")
|
||||
assert_clean_compile(client, uri)
|
||||
|
||||
# Modify the transitive dep (base.h).
|
||||
await asyncio.sleep(1.1)
|
||||
(tmp_path / "base.h").write_text("inline int base() { return }\n") # broken
|
||||
|
||||
await wait_for_recompile(client, uri)
|
||||
|
||||
assert_has_errors(client, uri, "Expected diagnostics from transitive header change")
|
||||
|
||||
|
||||
async def test_didchange_body_edit_recompiles(client, tmp_path):
|
||||
"""Editing the body (not preamble) via didChange should trigger
|
||||
recompilation and update diagnostics."""
|
||||
(tmp_path / "main.cpp").write_text("int main() { return 0; }\n")
|
||||
write_cdb(tmp_path, ["main.cpp"])
|
||||
await client.initialize(tmp_path)
|
||||
|
||||
uri, _ = await client.open_and_wait(tmp_path / "main.cpp")
|
||||
assert_clean_compile(client, uri)
|
||||
|
||||
# Introduce a body error via didChange.
|
||||
event = client.wait_for_diagnostics(uri)
|
||||
client.text_document_did_change(
|
||||
DidChangeTextDocumentParams(
|
||||
text_document=VersionedTextDocumentIdentifier(uri=uri, version=1),
|
||||
content_changes=[
|
||||
TextDocumentContentChangeWholeDocument(
|
||||
text="int main() { return }\n" # missing expression
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
await client.text_document_hover_async(
|
||||
HoverParams(text_document=doc(uri), position=Position(line=0, character=4))
|
||||
)
|
||||
await asyncio.wait_for(event.wait(), timeout=30.0)
|
||||
|
||||
assert_has_errors(client, uri, "Expected diagnostics after body error")
|
||||
|
||||
|
||||
async def test_didchange_preamble_edit_recompiles(client, tmp_path):
|
||||
"""Changing a preamble #include via didChange should trigger PCH rebuild
|
||||
and recompilation reflecting the new header's declarations."""
|
||||
(tmp_path / "a.h").write_text("#pragma once\ninline int from_a() { return 1; }\n")
|
||||
(tmp_path / "b.h").write_text("#pragma once\ninline int from_b() { return 2; }\n")
|
||||
(tmp_path / "main.cpp").write_text(
|
||||
'#include "a.h"\nint main() { return from_a(); }\n'
|
||||
)
|
||||
write_cdb(tmp_path, ["main.cpp"])
|
||||
await client.initialize(tmp_path)
|
||||
|
||||
uri, _ = await client.open_and_wait(tmp_path / "main.cpp")
|
||||
assert_clean_compile(client, uri)
|
||||
|
||||
# Switch from a.h to b.h and call from_b() instead.
|
||||
event = client.wait_for_diagnostics(uri)
|
||||
client.text_document_did_change(
|
||||
DidChangeTextDocumentParams(
|
||||
text_document=VersionedTextDocumentIdentifier(uri=uri, version=1),
|
||||
content_changes=[
|
||||
TextDocumentContentChangeWholeDocument(
|
||||
text='#include "b.h"\nint main() { return from_b(); }\n'
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
await client.text_document_hover_async(
|
||||
HoverParams(text_document=doc(uri), position=Position(line=1, character=4))
|
||||
)
|
||||
await asyncio.wait_for(event.wait(), timeout=30.0)
|
||||
|
||||
# Should compile cleanly — from_b() is available via b.h.
|
||||
assert_clean_compile(client, uri)
|
||||
|
||||
|
||||
async def test_didclose_then_reopen(client, tmp_path):
|
||||
"""Closing and reopening a file should work correctly — the server
|
||||
should not retain stale state from the previous session."""
|
||||
(tmp_path / "main.cpp").write_text("int main() { return 0; }\n")
|
||||
write_cdb(tmp_path, ["main.cpp"])
|
||||
await client.initialize(tmp_path)
|
||||
|
||||
uri, _ = await client.open_and_wait(tmp_path / "main.cpp")
|
||||
assert_clean_compile(client, uri)
|
||||
|
||||
# Close the file.
|
||||
client.text_document_did_close(DidCloseTextDocumentParams(text_document=doc(uri)))
|
||||
|
||||
# Modify on disk while closed.
|
||||
await asyncio.sleep(1.1)
|
||||
(tmp_path / "main.cpp").write_text("int main() { return }\n") # broken
|
||||
|
||||
# Reopen — should compile the new (broken) content from disk.
|
||||
uri2, _ = await client.open_and_wait(tmp_path / "main.cpp")
|
||||
assert_has_errors(
|
||||
client, uri2, "Expected diagnostics after reopen with broken content"
|
||||
)
|
||||
|
||||
|
||||
async def test_didclose_clears_hover(client, tmp_path):
|
||||
"""After didClose, hover on the closed file should return None."""
|
||||
(tmp_path / "main.cpp").write_text("int main() { return 0; }\n")
|
||||
write_cdb(tmp_path, ["main.cpp"])
|
||||
await client.initialize(tmp_path)
|
||||
|
||||
uri, _ = await client.open_and_wait(tmp_path / "main.cpp")
|
||||
|
||||
client.text_document_did_close(DidCloseTextDocumentParams(text_document=doc(uri)))
|
||||
|
||||
hover = await client.text_document_hover_async(
|
||||
HoverParams(text_document=doc(uri), position=Position(line=0, character=4))
|
||||
)
|
||||
assert hover is None, "Hover on closed file should return None"
|
||||
|
||||
|
||||
async def test_didsave_triggers_recompile_for_dependents(client, tmp_path):
|
||||
"""didSave on a header file should mark dependent documents dirty."""
|
||||
(tmp_path / "header.h").write_text("inline int value() { return 1; }\n")
|
||||
(tmp_path / "main.cpp").write_text(
|
||||
'#include "header.h"\nint main() { return value(); }\n'
|
||||
)
|
||||
write_cdb(tmp_path, ["main.cpp"])
|
||||
await client.initialize(tmp_path)
|
||||
|
||||
uri, _ = await client.open_and_wait(tmp_path / "main.cpp")
|
||||
assert_clean_compile(client, uri)
|
||||
|
||||
# Modify header on disk and send didSave.
|
||||
await asyncio.sleep(1.1)
|
||||
(tmp_path / "header.h").write_text("inline int value() { return }\n") # broken
|
||||
client.text_document_did_save(
|
||||
DidSaveTextDocumentParams(
|
||||
text_document=TextDocumentIdentifier(uri=(tmp_path / "header.h").as_uri())
|
||||
)
|
||||
)
|
||||
|
||||
# Hover should detect the change and recompile.
|
||||
await wait_for_recompile(client, uri)
|
||||
|
||||
assert_has_errors(
|
||||
client, uri, "Expected diagnostics after didSave on broken header"
|
||||
)
|
||||
|
||||
|
||||
async def test_didsave_with_module_deps(client, test_data_dir, tmp_path):
|
||||
"""didSave on a module file should invalidate CompileGraph dependents."""
|
||||
src = test_data_dir / "modules" / "save_recompile"
|
||||
for f in src.iterdir():
|
||||
if f.is_file():
|
||||
shutil.copy2(f, tmp_path / f.name)
|
||||
|
||||
from tests.conftest import generate_cdb
|
||||
|
||||
generate_cdb(tmp_path)
|
||||
await client.initialize(tmp_path)
|
||||
|
||||
# Open and compile Mid (which imports Leaf).
|
||||
mid_uri, _ = await client.open_and_wait(tmp_path / "mid.cppm")
|
||||
assert_clean_compile(client, mid_uri)
|
||||
|
||||
# Modify Leaf on disk and send didSave — should invalidate Mid's deps.
|
||||
new_leaf = "export module Leaf;\nexport int leaf() { return 999; }\n"
|
||||
(tmp_path / "leaf.cppm").write_text(new_leaf)
|
||||
|
||||
leaf_path = tmp_path / "leaf.cppm"
|
||||
client.text_document_did_save(
|
||||
DidSaveTextDocumentParams(
|
||||
text_document=TextDocumentIdentifier(uri=leaf_path.as_uri())
|
||||
)
|
||||
)
|
||||
|
||||
# Hover on Mid should trigger recompilation (Leaf PCM was invalidated).
|
||||
await wait_for_recompile(client, mid_uri)
|
||||
|
||||
assert_clean_compile(client, mid_uri)
|
||||
0
tests/integration/extensions/__init__.py
Normal file
0
tests/integration/extensions/__init__.py
Normal file
216
tests/integration/extensions/test_header_context.py
Normal file
216
tests/integration/extensions/test_header_context.py
Normal file
@@ -0,0 +1,216 @@
|
||||
"""Integration tests for header context LSP extension commands.
|
||||
|
||||
Tests the clice/queryContext, clice/currentContext, and clice/switchContext
|
||||
extension commands that allow switching the compilation context for header files.
|
||||
|
||||
utils.h uses Point without including types.h itself -- it depends on
|
||||
main.cpp to provide that include. Without header context resolution, the
|
||||
server cannot compile utils.h at all.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
from lsprotocol.types import (
|
||||
HoverParams,
|
||||
Position,
|
||||
TextDocumentIdentifier,
|
||||
)
|
||||
|
||||
from tests.integration.utils import doc
|
||||
|
||||
|
||||
def _get(obj, key, default=None):
|
||||
"""Access a field from either a dict or an object with attributes."""
|
||||
if isinstance(obj, dict):
|
||||
return obj.get(key, default)
|
||||
return getattr(obj, key, default)
|
||||
|
||||
|
||||
@pytest.mark.workspace("header_context")
|
||||
async def test_query_context_returns_host_sources(client, workspace):
|
||||
"""clice/queryContext on a header should return source files that include it."""
|
||||
await client.open_and_wait(workspace / "main.cpp")
|
||||
|
||||
utils_h = workspace / "utils.h"
|
||||
utils_uri, _ = client.open(utils_h)
|
||||
|
||||
result = await client.query_context(utils_uri)
|
||||
assert result is not None
|
||||
total = _get(result, "total")
|
||||
contexts = _get(result, "contexts", [])
|
||||
assert total >= 1, f"Should find at least main.cpp as context, got total={total}"
|
||||
# Check that main.cpp is among the contexts.
|
||||
uris = [_get(c, "uri") for c in contexts]
|
||||
assert any("main.cpp" in u for u in uris), (
|
||||
f"main.cpp should be listed as a context option, got: {uris}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.workspace("header_context")
|
||||
async def test_query_context_source_file_returns_cdb_entries(client, workspace):
|
||||
"""clice/queryContext on a source file should return its CDB entries."""
|
||||
main_uri, _ = await client.open_and_wait(workspace / "main.cpp")
|
||||
|
||||
result = await client.query_context(main_uri)
|
||||
assert result is not None
|
||||
# header_context workspace has exactly 1 CDB entry for main.cpp.
|
||||
assert _get(result, "total") == 1
|
||||
contexts = _get(result, "contexts", [])
|
||||
assert len(contexts) == 1
|
||||
|
||||
|
||||
@pytest.mark.workspace("header_context")
|
||||
async def test_current_context_default_null(client, workspace):
|
||||
"""clice/currentContext should return null context by default."""
|
||||
await client.open_and_wait(workspace / "main.cpp")
|
||||
|
||||
utils_h = workspace / "utils.h"
|
||||
utils_uri, _ = client.open(utils_h)
|
||||
|
||||
result = await client.current_context(utils_uri)
|
||||
assert result is not None
|
||||
assert _get(result, "context") is None, (
|
||||
"Default context should be null (no explicit override)"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.workspace("header_context")
|
||||
async def test_switch_context_and_current_context(client, workspace):
|
||||
"""switchContext should set the active context, currentContext should reflect it."""
|
||||
main_uri, _ = await client.open_and_wait(workspace / "main.cpp")
|
||||
|
||||
utils_h = workspace / "utils.h"
|
||||
utils_uri, _ = client.open(utils_h)
|
||||
|
||||
# Switch context to main.cpp.
|
||||
switch_result = await client.switch_context(utils_uri, main_uri)
|
||||
assert switch_result is not None
|
||||
assert _get(switch_result, "success") is True
|
||||
|
||||
# Verify currentContext now returns main.cpp.
|
||||
current = await client.current_context(utils_uri)
|
||||
assert current is not None
|
||||
ctx = _get(current, "context")
|
||||
assert ctx is not None, (
|
||||
"After switchContext, currentContext should return the active context"
|
||||
)
|
||||
assert "main.cpp" in _get(ctx, "uri")
|
||||
|
||||
|
||||
@pytest.mark.workspace("header_context")
|
||||
async def test_full_context_flow(client, workspace):
|
||||
"""Full flow: open, query, switch, verify hover works in header context."""
|
||||
# 1. Open main.cpp, wait for initial compile.
|
||||
main_uri, _ = await client.open_and_wait(workspace / "main.cpp")
|
||||
|
||||
# 2. Open utils.h (non self-contained header using Point from types.h).
|
||||
utils_h = workspace / "utils.h"
|
||||
utils_uri, _ = client.open(utils_h)
|
||||
|
||||
# 3. queryContext on utils.h -> should return main.cpp as a context option.
|
||||
query = await client.query_context(utils_uri)
|
||||
assert _get(query, "total") >= 1
|
||||
contexts = _get(query, "contexts", [])
|
||||
context_uris = [_get(c, "uri") for c in contexts]
|
||||
assert any("main.cpp" in u for u in context_uris)
|
||||
|
||||
# 4. currentContext on utils.h -> should be null (default).
|
||||
current = await client.current_context(utils_uri)
|
||||
assert _get(current, "context") is None
|
||||
|
||||
# 5. switchContext on utils.h to main.cpp.
|
||||
switch = await client.switch_context(utils_uri, main_uri)
|
||||
assert _get(switch, "success") is True
|
||||
|
||||
# 6. currentContext on utils.h -> should now be main.cpp.
|
||||
current2 = await client.current_context(utils_uri)
|
||||
ctx = _get(current2, "context")
|
||||
assert ctx is not None
|
||||
assert "main.cpp" in _get(ctx, "uri")
|
||||
|
||||
# 7. Hover on 'calc' function in utils.h -> should work (proves header compiled).
|
||||
diag_event = client.wait_for_diagnostics(utils_uri)
|
||||
hover = await asyncio.wait_for(
|
||||
client.text_document_hover_async(
|
||||
HoverParams(
|
||||
text_document=doc(utils_uri),
|
||||
position=Position(line=6, character=12), # 'calc' function
|
||||
)
|
||||
),
|
||||
timeout=30.0,
|
||||
)
|
||||
assert hover is not None, (
|
||||
"Hover on 'calc' in header should work after switchContext"
|
||||
)
|
||||
|
||||
# 8. Check diagnostics on utils.h -> should have 0 errors.
|
||||
await asyncio.wait_for(diag_event.wait(), timeout=30.0)
|
||||
diags = client.diagnostics.get(utils_uri, [])
|
||||
errors = [d for d in diags if d.severity == 1]
|
||||
assert len(errors) == 0, (
|
||||
f"Header should have no errors after switchContext, got: {errors}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.workspace("header_context")
|
||||
async def test_deep_nested_header_context(client, workspace):
|
||||
"""queryContext on a deeply nested header (main.cpp -> utils.h -> inner.h)
|
||||
should still find main.cpp as the host source."""
|
||||
await client.open_and_wait(workspace / "main.cpp")
|
||||
|
||||
inner_h = workspace / "inner.h"
|
||||
inner_uri, _ = client.open(inner_h)
|
||||
|
||||
# queryContext on inner.h should find main.cpp through the chain.
|
||||
result = await client.query_context(inner_uri)
|
||||
assert result is not None
|
||||
total = _get(result, "total")
|
||||
assert total >= 1, f"Deep nested header should find host sources, got total={total}"
|
||||
contexts = _get(result, "contexts", [])
|
||||
uris = [_get(c, "uri") for c in contexts]
|
||||
assert any("main.cpp" in u for u in uris), (
|
||||
f"main.cpp should be a context for inner.h, got: {uris}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.workspace("header_context")
|
||||
async def test_deep_nested_switch_context_and_hover(client, workspace):
|
||||
"""switchContext + hover on deeply nested header (main.cpp -> utils.h -> inner.h)."""
|
||||
main_uri, _ = await client.open_and_wait(workspace / "main.cpp")
|
||||
|
||||
inner_h = workspace / "inner.h"
|
||||
inner_uri, _ = client.open(inner_h)
|
||||
|
||||
# Switch inner.h context to main.cpp.
|
||||
switch = await client.switch_context(inner_uri, main_uri)
|
||||
assert _get(switch, "success") is True
|
||||
|
||||
# Hover on 'inner_origin' in inner.h should work (Point available via preamble).
|
||||
hover = await asyncio.wait_for(
|
||||
client.text_document_hover_async(
|
||||
HoverParams(
|
||||
text_document=doc(inner_uri),
|
||||
position=Position(line=3, character=14), # 'inner_origin'
|
||||
)
|
||||
),
|
||||
timeout=30.0,
|
||||
)
|
||||
assert hover is not None, "Hover on inner_origin should work after switchContext"
|
||||
|
||||
|
||||
@pytest.mark.workspace("multi_context")
|
||||
async def test_query_context_multiple_cdb_entries(client, workspace):
|
||||
"""queryContext on a source file with multiple CDB entries should return all."""
|
||||
main_cpp = workspace / "main.cpp"
|
||||
main_uri, _ = await client.open_and_wait(main_cpp)
|
||||
|
||||
result = await client.query_context(main_uri)
|
||||
assert result is not None
|
||||
total = _get(result, "total")
|
||||
assert total >= 2, f"Should find at least 2 CDB entries, got total={total}"
|
||||
contexts = _get(result, "contexts", [])
|
||||
labels = [_get(c, "label") for c in contexts]
|
||||
# Each entry should have distinguishing flags in the label.
|
||||
assert any("CONFIG_A" in l for l in labels), f"Should find CONFIG_A, got: {labels}"
|
||||
assert any("CONFIG_B" in l for l in labels), f"Should find CONFIG_B, got: {labels}"
|
||||
0
tests/integration/features/__init__.py
Normal file
0
tests/integration/features/__init__.py
Normal file
191
tests/integration/features/test_completion.py
Normal file
191
tests/integration/features/test_completion.py
Normal file
@@ -0,0 +1,191 @@
|
||||
"""Integration tests for #include completion and import completion in clice."""
|
||||
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
from lsprotocol.types import (
|
||||
HoverParams,
|
||||
Position,
|
||||
TextDocumentIdentifier,
|
||||
)
|
||||
|
||||
from tests.integration.utils import doc
|
||||
from tests.integration.utils.workspace import did_change
|
||||
|
||||
|
||||
@pytest.mark.workspace("include_completion")
|
||||
async def test_include_completion_quoted(client, workspace):
|
||||
"""Completion after #include " should list local headers."""
|
||||
uri, _ = await client.open_and_wait(workspace / "main.cpp")
|
||||
|
||||
# Update content to trigger include completion for "my" prefix.
|
||||
did_change(client, uri, 1, '#include "my')
|
||||
|
||||
result = await client.completion_at(uri, 0, 12) # After "my"
|
||||
|
||||
assert result is not None
|
||||
items = result.items if hasattr(result, "items") else result
|
||||
labels = [item.label for item in items]
|
||||
assert "myheader.h" in labels
|
||||
|
||||
client.close(uri)
|
||||
|
||||
|
||||
@pytest.mark.workspace("include_completion")
|
||||
async def test_include_completion_subdirectory(client, workspace):
|
||||
"""Completion for #include "subdir/ should list files in subdir."""
|
||||
uri, _ = await client.open_and_wait(workspace / "main.cpp")
|
||||
|
||||
did_change(client, uri, 1, '#include "subdir/')
|
||||
|
||||
result = await client.completion_at(uri, 0, 17) # After "subdir/"
|
||||
|
||||
assert result is not None
|
||||
items = result.items if hasattr(result, "items") else result
|
||||
labels = [item.label for item in items]
|
||||
assert "nested.h" in labels
|
||||
|
||||
client.close(uri)
|
||||
|
||||
|
||||
@pytest.mark.workspace("include_completion")
|
||||
async def test_include_completion_angle_bracket(client, workspace):
|
||||
"""Completion after #include < should list system headers."""
|
||||
uri, _ = await client.open_and_wait(workspace / "main.cpp")
|
||||
|
||||
did_change(client, uri, 1, "#include <cstd")
|
||||
|
||||
result = await client.completion_at(uri, 0, 14) # After "cstd"
|
||||
|
||||
assert result is not None
|
||||
items = result.items if hasattr(result, "items") else result
|
||||
labels = [item.label for item in items]
|
||||
# Should contain at least some standard library headers starting with "cstd".
|
||||
cstd_labels = [name for name in labels if name.startswith("cstd")]
|
||||
assert len(cstd_labels) > 0, f"Expected cstd* headers, got: {labels}"
|
||||
|
||||
client.close(uri)
|
||||
|
||||
|
||||
@pytest.mark.workspace("include_completion")
|
||||
async def test_no_include_completion_on_regular_code(client, workspace):
|
||||
"""Regular code should NOT trigger include completion (goes to worker)."""
|
||||
uri, _ = await client.open_and_wait(workspace / "main.cpp")
|
||||
|
||||
did_change(client, uri, 1, "int x = ")
|
||||
|
||||
result = await client.completion_at(uri, 0, 8)
|
||||
|
||||
# Should return results from clang (keywords, etc.), not include paths.
|
||||
# Verify none of the results look like header filenames.
|
||||
assert result is not None
|
||||
items = result.items if hasattr(result, "items") else result
|
||||
labels = [item.label for item in items]
|
||||
assert "myheader.h" not in labels
|
||||
assert "nested.h" not in labels
|
||||
|
||||
client.close(uri)
|
||||
|
||||
|
||||
@pytest.mark.workspace("include_completion")
|
||||
async def test_include_completion_empty_prefix(client, workspace):
|
||||
"""Completion after #include " with no prefix should list all local headers."""
|
||||
uri, _ = await client.open_and_wait(workspace / "main.cpp")
|
||||
|
||||
did_change(client, uri, 1, '#include "')
|
||||
|
||||
result = await client.completion_at(uri, 0, 10) # Right after the quote
|
||||
|
||||
assert result is not None
|
||||
items = result.items if hasattr(result, "items") else result
|
||||
labels = [item.label for item in items]
|
||||
# With empty prefix, should list available headers including myheader.h
|
||||
# and the subdir/ directory entry.
|
||||
assert "myheader.h" in labels
|
||||
|
||||
client.close(uri)
|
||||
|
||||
|
||||
@pytest.mark.workspace("modules/chained_modules")
|
||||
async def test_import_completion_basic(client, workspace):
|
||||
"""Import completion should list known modules."""
|
||||
# First open mod_a to ensure it's scanned and module A is registered.
|
||||
await client.open_and_wait(workspace / "mod_a.cppm")
|
||||
|
||||
# Open mod_b and change its content to an incomplete import line.
|
||||
uri_b, _ = client.open(workspace / "mod_b.cppm")
|
||||
did_change(client, uri_b, 1, "import ")
|
||||
|
||||
result = await client.completion_at(uri_b, 0, 7)
|
||||
|
||||
assert result is not None
|
||||
items = result.items if hasattr(result, "items") else result
|
||||
labels = [item.label for item in items]
|
||||
assert "A" in labels, f"Expected 'A' in completion labels, got: {labels}"
|
||||
|
||||
|
||||
@pytest.mark.workspace("modules/chained_modules")
|
||||
async def test_import_completion_with_prefix(client, workspace):
|
||||
"""Import completion with prefix should filter to matching modules."""
|
||||
# Open mod_a to register module A.
|
||||
await client.open_and_wait(workspace / "mod_a.cppm")
|
||||
|
||||
# Open mod_b and type 'import A' (with prefix).
|
||||
uri_b, _ = client.open(workspace / "mod_b.cppm")
|
||||
did_change(client, uri_b, 1, "import A")
|
||||
|
||||
result = await client.completion_at(uri_b, 0, 8)
|
||||
|
||||
assert result is not None
|
||||
items = result.items if hasattr(result, "items") else result
|
||||
labels = [item.label for item in items]
|
||||
assert "A" in labels, f"Expected 'A' in completion labels, got: {labels}"
|
||||
|
||||
|
||||
@pytest.mark.workspace("modules/dotted_module_name")
|
||||
async def test_import_completion_dotted_names(client, workspace):
|
||||
"""Import completion should return dotted module names like my.app and my.io."""
|
||||
# Open both module files to register them.
|
||||
await client.open_and_wait(workspace / "io.cppm")
|
||||
await client.open_and_wait(workspace / "app.cppm")
|
||||
|
||||
# Change app.cppm to an incomplete import with dotted prefix.
|
||||
uri_app, _ = client.open(workspace / "app.cppm")
|
||||
did_change(client, uri_app, 1, "import my.")
|
||||
|
||||
result = await client.completion_at(uri_app, 0, 10)
|
||||
|
||||
assert result is not None
|
||||
items = result.items if hasattr(result, "items") else result
|
||||
labels = [item.label for item in items]
|
||||
assert "my.app" in labels or "my.io" in labels, (
|
||||
f"Expected dotted module names in completion labels, got: {labels}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.workspace("modules/consumer_imports_module")
|
||||
async def test_buffer_aware_module_deps(client, workspace):
|
||||
"""Adding import in buffer (unsaved) should still build the needed PCM."""
|
||||
# Open the module file first so it gets scanned.
|
||||
await client.open_and_wait(workspace / "math.cppm")
|
||||
|
||||
# Open main.cpp with new content that imports Math (simulating unsaved edit).
|
||||
uri, _ = client.open(workspace / "main.cpp")
|
||||
did_change(client, uri, 1, "import Math;\nint x = add(1, 2);\n")
|
||||
|
||||
# Trigger compilation via hover (pull-based model).
|
||||
event = client.wait_for_diagnostics(uri)
|
||||
await client.text_document_hover_async(
|
||||
HoverParams(
|
||||
text_document=TextDocumentIdentifier(uri=uri),
|
||||
position=Position(line=0, character=0),
|
||||
)
|
||||
)
|
||||
|
||||
# Wait for diagnostics.
|
||||
await asyncio.wait_for(event.wait(), timeout=60.0)
|
||||
|
||||
diags = client.diagnostics.get(uri, [])
|
||||
# Should have no errors if Math PCM was built successfully from buffer scan.
|
||||
errors = [d for d in diags if d.severity == 1]
|
||||
assert len(errors) == 0, f"Expected no errors, got: {errors}"
|
||||
103
tests/integration/features/test_document_links.py
Normal file
103
tests/integration/features/test_document_links.py
Normal file
@@ -0,0 +1,103 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.workspace("document_links")
|
||||
async def test_document_links_with_pch(client, workspace):
|
||||
uri, content = await client.open_and_wait(workspace / "main.cpp")
|
||||
links = await client.document_links(uri)
|
||||
|
||||
assert links is not None, "document_links returned None"
|
||||
|
||||
targets = sorted(Path(link.target).name for link in links)
|
||||
assert targets == [
|
||||
"data.bin",
|
||||
"data.bin",
|
||||
"header_a.h",
|
||||
"header_b.h",
|
||||
"header_c.h",
|
||||
], f"Unexpected targets: {targets}"
|
||||
|
||||
client.close(uri)
|
||||
|
||||
|
||||
@pytest.mark.workspace("document_links")
|
||||
async def test_document_links_pch_portion(client, workspace):
|
||||
uri, _ = await client.open_and_wait(workspace / "main.cpp")
|
||||
links = await client.document_links(uri)
|
||||
|
||||
pch_links = [link for link in links if link.range.start.line < 2]
|
||||
assert len(pch_links) == 2, (
|
||||
f"Expected 2 PCH links (lines 0-1), got {len(pch_links)}"
|
||||
)
|
||||
|
||||
pch_targets = sorted(Path(link.target).name for link in pch_links)
|
||||
assert pch_targets == ["header_a.h", "header_b.h"]
|
||||
|
||||
client.close(uri)
|
||||
|
||||
|
||||
@pytest.mark.workspace("document_links")
|
||||
async def test_document_links_main_portion(client, workspace):
|
||||
uri, _ = await client.open_and_wait(workspace / "main.cpp")
|
||||
links = await client.document_links(uri)
|
||||
|
||||
main_links = [link for link in links if link.range.start.line >= 2]
|
||||
assert len(main_links) == 3, (
|
||||
f"Expected 3 main-file links (lines 3, 6, 9), got {len(main_links)}"
|
||||
)
|
||||
|
||||
main_targets = sorted(Path(link.target).name for link in main_links)
|
||||
assert main_targets == ["data.bin", "data.bin", "header_c.h"]
|
||||
|
||||
client.close(uri)
|
||||
|
||||
|
||||
@pytest.mark.workspace("document_links")
|
||||
async def test_document_links_embed(client, workspace):
|
||||
uri, _ = await client.open_and_wait(workspace / "main.cpp")
|
||||
links = await client.document_links(uri)
|
||||
|
||||
embed_links = [
|
||||
link
|
||||
for link in links
|
||||
if Path(link.target).name == "data.bin" and link.range.start.line == 6
|
||||
]
|
||||
assert len(embed_links) == 1, (
|
||||
f"Expected 1 embed link at line 6, got {len(embed_links)}"
|
||||
)
|
||||
|
||||
client.close(uri)
|
||||
|
||||
|
||||
@pytest.mark.workspace("document_links")
|
||||
async def test_document_links_has_embed_exists(client, workspace):
|
||||
uri, _ = await client.open_and_wait(workspace / "main.cpp")
|
||||
links = await client.document_links(uri)
|
||||
|
||||
has_embed_links = [
|
||||
link
|
||||
for link in links
|
||||
if Path(link.target).name == "data.bin" and link.range.start.line == 9
|
||||
]
|
||||
assert len(has_embed_links) == 1, (
|
||||
f"Expected 1 has_embed link at line 9, got {len(has_embed_links)}"
|
||||
)
|
||||
|
||||
client.close(uri)
|
||||
|
||||
|
||||
@pytest.mark.workspace("document_links")
|
||||
async def test_document_links_has_embed_missing(client, workspace):
|
||||
uri, _ = await client.open_and_wait(workspace / "main.cpp")
|
||||
links = await client.document_links(uri)
|
||||
|
||||
missing_links = [
|
||||
link for link in links if Path(link.target).name == "no_such_file.bin"
|
||||
]
|
||||
assert len(missing_links) == 0, (
|
||||
f"Expected 0 links for non-existent file, got {len(missing_links)}"
|
||||
)
|
||||
|
||||
client.close(uri)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user