6 Commits

Author SHA1 Message Date
ykiko
8e9d4fa636 feat(completion): smart parenthesis detection to avoid duplicate parens
When the next non-whitespace character after the cursor is already '(',
skip inserting parentheses and parameter placeholders in the snippet.
This prevents duplicate parens when completing a function name that
already has arguments written after it.

Uses the original file content (not Clang's internal buffer) for
lookahead to correctly detect the next token.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-18 14:54:07 +08:00
ykiko
418e190fa0 chore(deps): migrate from eventide to kotatsu (#428)
## Summary

- The `eventide` dep was renamed to
[kotatsu](https://github.com/clice-io/kotatsu) with a broad rename of
CMake identifiers, namespaces, header paths, and a few module reorgs
(`serde` → `codec`, `reflection` → `meta`, `common` → `support`). Align
clice to the new names.
- CMake: FetchContent target, option prefix (`ETD_*` → `KOTA_*`,
`ETD_SERDE_*` → `KOTA_CODEC_*`), target names
(`eventide::{ipc::lsp,serde::toml,deco,zest}` →
`kota::{ipc::lsp,codec::toml,deco,zest}`).
- Namespaces: `eventide::` → `kota::`, `eventide::serde::` →
`kota::codec::`, `eventide::refl::` → `kota::meta::`. The short `et`
alias is dropped — all usages now spell `kota::` directly.
- Headers: `eventide/*` → `kota/*`, including special cases
`serde/serde/raw_value.h` → `codec/raw_value.h`, `ipc/json_codec.h` →
`ipc/codec/json.h`, `common/meta.h` → `support/type_traits.h`,
`common/ranges.h` → `support/ranges.h`.
- Kotatsu split `JsonPeer` / `BincodePeer` out of `ipc/peer.h` into the
codec-specific headers; added `kota/ipc/codec/{json,bincode}.h` includes
where those types are used.
- Depends on clice-io/kotatsu#110 (already merged) to prevent `-Wall
-Wextra -Werror` from transitively propagating out of
`kota::project_options`.

## Test plan

- [x] `pixi run unit-test RelWithDebInfo` — 518/518 pass (9 skipped,
unchanged from main)
- [x] `pixi run integration-test RelWithDebInfo` — 119/119 pass
- [x] `pixi run smoke-test RelWithDebInfo` — 2/2 pass
- [x] `pixi run format` clean

## Notes

- `tests/smoke/rapid_edit.jsonl` was intentionally left untouched: the
embedded `#include "eventide/..."` strings are frozen snapshots of file
contents the client sent at record time, not clice source.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Chores**
* Updated internal dependencies from `eventide` to `kota`, including
async runtime, IPC transport, serialization codec, and metadata
libraries.
* Updated build configuration and CMake variables to align with the new
dependency.

* **Refactor**
* Migrated internal implementation to use `kota` namespace and APIs
throughout the codebase.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 13:49:07 +08:00
ykiko
d42d9d5b29 refactor(document links): use Lexer for unified directive argument scanning (#421)
## Summary
- Replace hand-written character scanning in `document_links.cpp` with
the project's `Lexer` class for finding filename arguments in
preprocessor directives
- Extend `Lexer` to activate `header_name` mode for
`#embed`/`#include_next`, and expose `set_header_name_mode()` for
`__has_include`/`__has_embed` contexts
- Remove unused `Include::filename_range` field (had a latent assert
crash on macro-expanded includes)
- Add `MacroInclude` unit test covering `#include MACRO` scenario

## Test plan
- [x] 498 unit tests pass (including new `MacroInclude` test)
- [x] 119 integration tests pass
- [x] 2/2 smoke tests pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Document links now resolve includes written via macros; directive
parsing recognizes include, include_next, embed and __has_* patterns
more reliably using lexer-driven argument detection.

* **Refactor**
* Removed an internal filename-range field previously stored for include
directives.

* **Tests**
* Added unit tests covering directive argument extraction and
macro-based include linking.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 17:17:10 +08:00
ykiko
9c89d20e76 feat(tests): add compile_with_modules helper to Tester (#420)
## Summary
- Add `add_module()` and `compile_with_modules()` to the `Tester` test
framework
- Supports both separate `add_module()` calls and single-string
`#[filename]` syntax via `add_files()`
- Automatically scans module dependencies with `scan_precise`,
topologically sorts, builds PCMs in order, then compiles the main file
- Temporary PCM files cleaned up automatically in destructor
- Migrated `ModuleImport` and `ModuleReexport` semantic tokens tests to
use the new API

## Test plan
- [x] All 505 unit tests pass
- [x] All 113 integration tests pass
- [x] All 2 smoke tests pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Tests**
* Centralized, module-aware test compilation with automatic module
discovery, dependency ordering, and cycle detection.
* Unified "compile with modules" flow; tests now add module sources
directly and no longer manage temporary module artifacts manually.
* Reduced duplicated compile/diagnostic logic and improved cleanup of
generated artifacts.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 17:16:53 +08:00
ykiko
8bafaa8171 feat(document links): preserve PCH document links and add #embed support (#413)
## Summary
- PCH compilation now serializes document links via `pch_links_json` in
`BuildResult` and stores them in `PCHState`
- Master server merges PCH document links with main-file links on
`textDocument/documentLink` requests, fixing missing links for
`#include` directives inside the preamble
- Adds document link support for `#embed` and `__has_embed` directives

## Test plan
- [x] Unit tests: `DocumentLink.Embed` and `DocumentLink.HasEmbed` added
- [x] Integration tests: `test_document_links.py` verifies PCH + main
merge and `#embed` links
- [x] All 483 unit tests pass
- [x] All 4 integration tests pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Document links now detect embeds and __has_embed directives for both
quoted and angled filenames.
* Document links produced during precompiled builds are cached and
merged into document-link responses for more complete link sets.

* **Tests**
* Added integration tests for merged PCH/main links and embed/has-embed
cases.
  * Added unit tests verifying embed handling under C++23.

* **Chores**
* Added test fixtures and compile command entries for document-links
tests.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:35:10 +08:00
ykiko
92dae18fd4 feat(semantic tokens): highlight module names in declarations and imports (#417)
## Summary
- Highlight module name identifiers (e.g. `foo`, `bar` in `export module
foo.bar;`) as `SymbolKind::Module` in semantic tokens
- Highlight import module names (e.g. `foo` in `import foo;`) using
`directives.imports` name locations
- Module declarations use `getCurrentNamedModule()->DefinitionLoc` +
lexer scan to find name tokens

## Test plan
- [x] `SemanticTokens.ModuleDeclaration` — `export module foo;`
- [x] `SemanticTokens.ModuleDeclarationDotted` — `export module
foo.bar;`
- [x] `SemanticTokens.ModuleImport` — PCM build + `import foo;`
- [x] All 16 SemanticTokens tests pass, no regressions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Enhanced semantic token support for C++20 modules, including dotted
module names, partitions, fragments, imports and re-exports for more
accurate highlighting.

* **Bug Fixes**
* Improved conflict resolution so directive tokens no longer mask other
semantic kinds; ensures `module`/`import` used as identifiers are
tokenized correctly.

* **Tests**
* Added unit tests covering module declarations, imports, partitions and
edge cases.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:34:06 +08:00
64 changed files with 1348 additions and 667 deletions

View File

@@ -100,7 +100,7 @@ SortIncludes: true
SortUsingDeclarations: Never
IncludeBlocks: Regroup
IncludeCategories:
- Regex: '^["<](spdlog|toml\+\+|coraing|cpptrace|flatbuffers)/'
- Regex: '^["<](spdlog|toml\+\+|coraing|cpptrace|flatbuffers|kota)/'
Priority: 30
SortPriority: 31

View File

@@ -151,13 +151,13 @@ target_link_libraries(clice-core PUBLIC
spdlog::spdlog
roaring::roaring
flatbuffers
eventide::ipc::lsp
eventide::serde::toml
kota::ipc::lsp
kota::codec::toml
simdjson::simdjson
)
add_executable(clice "${PROJECT_SOURCE_DIR}/src/clice.cc")
target_link_libraries(clice PRIVATE clice::core eventide::deco)
target_link_libraries(clice PRIVATE clice::core kota::deco)
install(TARGETS clice RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
add_custom_target(copy_clang_resource ALL
@@ -189,7 +189,7 @@ if(CLICE_ENABLE_TEST)
"${PROJECT_SOURCE_DIR}/src"
"${PROJECT_SOURCE_DIR}/tests/unit"
)
target_link_libraries(unit_tests PRIVATE clice::core eventide::zest eventide::deco)
target_link_libraries(unit_tests PRIVATE clice::core kota::zest kota::deco)
endif()
if(CLICE_ENABLE_BENCHMARK)
@@ -199,7 +199,7 @@ if(CLICE_ENABLE_BENCHMARK)
target_include_directories(scan_benchmark PRIVATE
"${PROJECT_SOURCE_DIR}/src"
)
target_link_libraries(scan_benchmark PRIVATE clice::core eventide::deco)
target_link_libraries(scan_benchmark PRIVATE clice::core kota::deco)
endif()
if(CLICE_RELEASE)

View File

@@ -21,17 +21,15 @@
#include <thread>
#include "command/command.h"
#include "eventide/deco/deco.h"
#include "eventide/serde/json/serializer.h"
#include "support/filesystem.h"
#include "support/logging.h"
#include "support/path_pool.h"
#include "syntax/dependency_graph.h"
#include "kota/codec/json/serializer.h"
#include "kota/deco/deco.h"
#include "llvm/Support/FileSystem.h"
namespace et = eventide;
using namespace clice;
struct BenchmarkOptions {
@@ -97,7 +95,7 @@ void export_graph_json(const PathPool& path_pool,
export_data.files.push_back(std::move(node));
}
auto json = et::serde::json::to_json(export_data);
auto json = kota::codec::json::to_json(export_data);
if(!json) {
std::println(stderr, "Failed to serialize dependency graph");
return;
@@ -221,8 +219,8 @@ void print_report(const ScanReport& report) {
}
int main(int argc, const char** argv) {
auto args = deco::util::argvify(argc, argv);
auto result = deco::cli::parse<BenchmarkOptions>(args);
auto args = kota::deco::util::argvify(argc, argv);
auto result = kota::deco::cli::parse<BenchmarkOptions>(args);
if(!result.has_value()) {
std::println(stderr, "Error: {}", result.error().message);
@@ -233,7 +231,7 @@ int main(int argc, const char** argv) {
if(opts.help.value_or(false) || !opts.cdb_path.has_value()) {
std::ostringstream oss;
deco::cli::write_usage_for<BenchmarkOptions>(oss, "scan_benchmark [OPTIONS] <cdb>");
kota::deco::cli::write_usage_for<BenchmarkOptions>(oss, "scan_benchmark [OPTIONS] <cdb>");
std::print("{}", oss.str());
return opts.help.value_or(false) ? 0 : 1;
}

View File

@@ -39,18 +39,18 @@ set(FLATBUFFERS_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(FLATBUFFERS_BUILD_FLATHASH OFF CACHE BOOL "" FORCE)
FetchContent_Declare(
eventide
GIT_REPOSITORY https://github.com/clice-io/eventide
kotatsu
GIT_REPOSITORY https://github.com/clice-io/kotatsu
GIT_TAG main
GIT_SHALLOW TRUE
)
set(ETD_ENABLE_ZEST ON)
set(ETD_ENABLE_TEST OFF)
set(ETD_SERDE_ENABLE_SIMDJSON ON)
set(ETD_SERDE_ENABLE_YYJSON ON)
set(ETD_SERDE_ENABLE_TOML ON)
set(ETD_ENABLE_EXCEPTIONS OFF)
set(ETD_ENABLE_RTTI OFF)
set(KOTA_ENABLE_ZEST ON)
set(KOTA_ENABLE_TEST OFF)
set(KOTA_CODEC_ENABLE_SIMDJSON ON)
set(KOTA_CODEC_ENABLE_YYJSON ON)
set(KOTA_CODEC_ENABLE_TOML ON)
set(KOTA_ENABLE_EXCEPTIONS OFF)
set(KOTA_ENABLE_RTTI OFF)
FetchContent_MakeAvailable(eventide spdlog croaring flatbuffers)
FetchContent_MakeAvailable(kotatsu spdlog croaring flatbuffers)

View File

@@ -91,7 +91,7 @@ The worker pool (`src/server/worker_pool.cpp`) manages spawning and communicatin
### Communication
Workers communicate with the master via **stdio pipes** using a **bincode** serialization format (via `eventide::ipc::BincodePeer`). This is more compact and faster than JSON for internal IPC, while the master handles JSON for the external LSP protocol.
Workers communicate with the master via **stdio pipes** using a **bincode** serialization format (via `kota::ipc::BincodePeer`). This is more compact and faster than JSON for internal IPC, while the master handles JSON for the external LSP protocol.
### Stateful Worker Routing
@@ -111,7 +111,7 @@ The stateful worker (`src/server/stateful_worker.cpp`) caches compiled ASTs in m
- **Feature queries**: Look up the cached AST and invoke the corresponding `feature::*` function (hover, semantic tokens, etc.), serializing the result to JSON
- **Document updates**: Received as notifications — the worker updates the stored text and marks the document as `dirty`, causing feature queries to return `null` until recompilation
- **Eviction**: LRU-based; evicts the oldest document when capacity is exceeded, notifying the master
- **Concurrency**: Each document has a per-document `et::mutex` (strand) to serialize compilation and feature queries. Heavy work (compilation, feature extraction) runs on a thread pool via `et::queue`.
- **Concurrency**: Each document has a per-document `kota::mutex` (strand) to serialize compilation and feature queries. Heavy work (compilation, feature extraction) runs on a thread pool via `kota::queue`.
## Stateless Worker
@@ -123,7 +123,7 @@ The stateless worker (`src/server/stateless_worker.cpp`) handles one-shot reques
- **Build PCM**: Compiles a C++20 module interface to a temporary file
- **Index**: Compiles a file for indexing (TUIndex generation — currently a stub)
All requests are dispatched to a thread pool via `et::queue`.
All requests are dispatched to a thread pool via `kota::queue`.
## Compile Graph
@@ -132,7 +132,7 @@ The compile graph (`src/server/compile_graph.cpp`) tracks compilation unit depen
- **Registration**: Each file registers its included dependencies
- **Cascade invalidation**: When a file changes, all transitive dependents are marked dirty and their ongoing compilations are cancelled
- **Dependency compilation**: Before compiling a file, `compile_deps` ensures all dependencies (PCH, PCMs) are built first
- **Cancellation**: Uses `et::cancellation_source` to abort in-flight compilations when files are invalidated
- **Cancellation**: Uses `kota::cancellation_source` to abort in-flight compilations when files are invalidated
## Configuration

View File

@@ -4,19 +4,21 @@
#include <print>
#include <string>
#include "eventide/async/async.h"
#include "eventide/deco/deco.h"
#include "eventide/ipc/peer.h"
#include "eventide/ipc/recording_transport.h"
#include "eventide/ipc/transport.h"
#include "server/master_server.h"
#include "server/stateful_worker.h"
#include "server/stateless_worker.h"
#include "support/logging.h"
#include "kota/async/async.h"
#include "kota/deco/deco.h"
#include "kota/ipc/codec/json.h"
#include "kota/ipc/peer.h"
#include "kota/ipc/recording_transport.h"
#include "kota/ipc/transport.h"
namespace clice {
using deco::decl::KVStyle;
using kota::deco::decl::KVStyle;
struct Options {
DecoKV(style = KVStyle::JoinedOrSeparate,
@@ -72,8 +74,8 @@ int main(int argc, const char** argv) {
signal(SIGPIPE, SIG_IGN);
#endif
auto args = deco::util::argvify(argc, argv);
auto result = deco::cli::parse<clice::Options>(args);
auto args = kota::deco::util::argvify(argc, argv);
auto result = kota::deco::cli::parse<clice::Options>(args);
if(!result.has_value()) {
LOG_ERROR("{}", result.error().message);
@@ -83,7 +85,7 @@ int main(int argc, const char** argv) {
auto& opts = result->options;
if(opts.help.value_or(false)) {
deco::cli::write_usage_for<clice::Options>(std::cout, "clice [OPTIONS]");
kota::deco::cli::write_usage_for<clice::Options>(std::cout, "clice [OPTIONS]");
return 0;
}
@@ -132,23 +134,22 @@ int main(int argc, const char** argv) {
if(mode == "pipe") {
clice::logging::stderr_logger("master", clice::logging::options);
namespace et = eventide;
et::event_loop loop;
kota::event_loop loop;
auto transport = et::ipc::StreamTransport::open_stdio(loop);
auto transport = kota::ipc::StreamTransport::open_stdio(loop);
if(!transport) {
LOG_ERROR("failed to open stdio transport");
return 1;
}
std::unique_ptr<et::ipc::Transport> final_transport = std::move(*transport);
std::unique_ptr<kota::ipc::Transport> final_transport = std::move(*transport);
if(opts.record.has_value()) {
final_transport =
std::make_unique<et::ipc::RecordingTransport>(std::move(final_transport),
*opts.record);
std::make_unique<kota::ipc::RecordingTransport>(std::move(final_transport),
*opts.record);
}
et::ipc::JsonPeer peer(loop, std::move(final_transport));
kota::ipc::JsonPeer peer(loop, std::move(final_transport));
clice::MasterServer server(loop, peer, std::move(self_path));
server.register_handlers();
@@ -160,13 +161,12 @@ int main(int argc, const char** argv) {
if(mode == "socket") {
clice::logging::stderr_logger("master", clice::logging::options);
namespace et = eventide;
et::event_loop loop;
kota::event_loop loop;
auto host = opts.host.value_or("127.0.0.1");
auto port = opts.port.value_or(50051);
auto acceptor = et::tcp::listen(host, port, {}, loop);
auto acceptor = kota::tcp::listen(host, port, {}, loop);
if(!acceptor) {
LOG_ERROR("failed to listen on {}:{}", host, port);
return 1;
@@ -174,7 +174,7 @@ int main(int argc, const char** argv) {
LOG_INFO("Listening on {}:{} ...", host, port);
auto task = [&]() -> et::task<> {
auto task = [&]() -> kota::task<> {
auto client = co_await acceptor->accept();
if(!client.has_value()) {
LOG_ERROR("failed to accept connection");
@@ -184,13 +184,13 @@ int main(int argc, const char** argv) {
LOG_INFO("Client connected");
std::unique_ptr<et::ipc::Transport> transport =
std::make_unique<et::ipc::StreamTransport>(std::move(client.value()));
std::unique_ptr<kota::ipc::Transport> transport =
std::make_unique<kota::ipc::StreamTransport>(std::move(client.value()));
if(opts.record.has_value()) {
transport = std::make_unique<et::ipc::RecordingTransport>(std::move(transport),
*opts.record);
transport = std::make_unique<kota::ipc::RecordingTransport>(std::move(transport),
*opts.record);
}
et::ipc::JsonPeer peer(loop, std::move(transport));
kota::ipc::JsonPeer peer(loop, std::move(transport));
clice::MasterServer server(loop, peer, std::string(self_path));
server.register_handlers();

View File

@@ -5,10 +5,10 @@
#include <vector>
#include "command/argument_parser.h"
#include "eventide/reflection/enum.h"
#include "support/filesystem.h"
#include "support/logging.h"
#include "kota/meta/enum.h"
#include "llvm/ADT/ScopeExit.h"
#include "llvm/Support/CommandLine.h"
#include "llvm/Support/FileSystem.h"
@@ -363,7 +363,7 @@ std::vector<const char*> query_toolchain(const QueryParams& params) {
case CompilerFamily::Unknown: {
/// TODO: nvcc and intel compilers need further exploration.
LOG_ERROR("Fail to query driver, unknown supported driver kind: {}, driver is {}",
eventide::refl::enum_name(family),
kota::meta::enum_name(family),
driver);
std::vector<const char*> result;

View File

@@ -94,7 +94,7 @@ public:
const clang::Token& include_tok,
llvm::StringRef,
bool,
clang::CharSourceRange filename_range,
clang::CharSourceRange,
clang::OptionalFileEntryRef,
llvm::StringRef,
llvm::StringRef,
@@ -108,7 +108,6 @@ public:
unit->directives[prev_fid].includes.emplace_back(Include{
.fid = {},
.location = include_tok.getLocation(),
.filename_range = filename_range.getAsRange(),
});
}

View File

@@ -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.

View File

@@ -158,12 +158,28 @@ auto extract_signature(const clang::CodeCompletionString& ccs) -> std::string {
return signature;
}
/// Find the first non-whitespace character after the given offset in content.
/// Returns '\0' if none found (end of content).
auto next_token_char(llvm::StringRef content, std::uint32_t offset) -> char {
for(auto i = offset; i < content.size(); ++i) {
char c = content[i];
if(c != ' ' && c != '\t' && c != '\n' && c != '\r') {
return c;
}
}
return '\0';
}
/// 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 {
/// If skip_parens is true, omits everything from '(' onward (when the next
/// token after the cursor is already '(').
auto build_snippet(const clang::CodeCompletionString& ccs, bool skip_parens = false)
-> std::string {
std::string snippet;
unsigned placeholder_index = 0;
bool in_parens = false;
for(const auto& chunk: ccs) {
using CK = clang::CodeCompletionString::ChunkKind;
@@ -174,33 +190,47 @@ auto build_snippet(const clang::CodeCompletionString& ccs) -> std::string {
}
break;
case CK::CK_Placeholder:
if(in_parens && skip_parens) {
break;
}
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_LeftParen:
in_parens = true;
if(!skip_parens) {
snippet += '(';
}
break;
case CK::CK_RightParen:
in_parens = false;
if(!skip_parens) {
snippet += ')';
}
break;
case CK::CK_LeftAngle: snippet += '<'; break;
case CK::CK_RightAngle: snippet += '>'; break;
case CK::CK_Comma: snippet += ", "; break;
case CK::CK_Comma:
if(!(in_parens && skip_parens)) {
snippet += ", ";
}
break;
case CK::CK_Text:
if(chunk.Text) {
if(!(in_parens && skip_parens) && chunk.Text) {
snippet += chunk.Text;
}
break;
case CK::CK_Optional:
// Optional chunks contain default arguments — skip for snippet.
break;
case CK::CK_Optional: break;
case CK::CK_Informative:
case CK::CK_ResultType:
case CK::CK_CurrentParameter:
// Display-only chunks, not part of insertion.
break;
case CK::CK_CurrentParameter: break;
default: break;
}
}
// If no placeholders were generated, return empty to signal plain text.
// If no placeholders were generated and parens were skipped,
// return empty to signal plain text.
if(placeholder_index == 0) {
return {};
}
@@ -229,9 +259,11 @@ public:
CodeCompletionCollector(std::uint32_t offset,
PositionEncoding encoding,
std::vector<protocol::CompletionItem>& output,
const CodeCompletionOptions& options) :
const CodeCompletionOptions& options,
llvm::StringRef original_content) :
clang::CodeCompleteConsumer({}), offset(offset), encoding(encoding), output(output),
options(options), info(std::make_shared<clang::GlobalCodeCompletionAllocator>()) {}
options(options), original_content(original_content),
info(std::make_shared<clang::GlobalCodeCompletionAllocator>()) {}
clang::CodeCompletionAllocator& getAllocator() final {
return info.getAllocator();
@@ -425,7 +457,8 @@ public:
// Generate snippet for non-bundled callables.
if(is_callable && !options.bundle_overloads &&
options.enable_function_arguments_snippet) {
snippet = build_snippet(*ccs);
bool next_is_paren = next_token_char(original_content, offset) == '(';
snippet = build_snippet(*ccs, /*skip_parens=*/next_is_paren);
}
}
@@ -495,6 +528,7 @@ private:
std::uint32_t offset;
PositionEncoding encoding;
std::vector<protocol::CompletionItem>& output;
llvm::StringRef original_content;
const CodeCompletionOptions& options;
clang::CodeCompletionTUInfo info;
};
@@ -509,7 +543,15 @@ auto code_complete(CompilationParams& params,
auto& [file, offset] = params.completion;
(void)file;
auto* consumer = new CodeCompletionCollector(offset, encoding, items, options);
// Get the original file content for lookahead (smart parens detection).
llvm::StringRef original_content;
auto buf_it = params.buffers.find(file);
if(buf_it != params.buffers.end()) {
original_content = buf_it->second->getBuffer();
}
auto* consumer =
new CodeCompletionCollector(offset, encoding, items, options, original_content);
auto unit = complete(params, consumer);
(void)unit;

View File

@@ -2,14 +2,15 @@
#include <string>
#include <vector>
#include "eventide/ipc/lsp/uri.h"
#include "feature/feature.h"
#include "kota/ipc/lsp/uri.h"
namespace clice::feature {
namespace {
namespace lsp = eventide::ipc::lsp;
namespace lsp = kota::ipc::lsp;
auto to_uri(llvm::StringRef file) -> std::string {
const auto file_view = std::string_view(file.data(), file.size());

View File

@@ -1,14 +1,12 @@
#include <algorithm>
#include <cstdint>
#include <string>
#include <vector>
#include "feature/feature.h"
#include "syntax/lexer.h"
namespace clice::feature {
namespace {} // namespace
auto document_links(CompilationUnitRef unit, PositionEncoding encoding)
-> std::vector<protocol::DocumentLink> {
std::vector<protocol::DocumentLink> links;
@@ -22,50 +20,42 @@ auto document_links(CompilationUnitRef unit, PositionEncoding encoding)
auto content = unit.interested_content();
PositionMapper converter(content, encoding);
auto& directives = directives_it->second;
auto* lang_opts = &unit.lang_options();
links.reserve(directives.includes.size() + directives.has_includes.size());
auto add_link = [&](clang::SourceLocation loc, llvm::StringRef target) {
auto [fid, offset] = unit.decompose_location(loc);
if(fid != interested || offset >= content.size())
return;
auto range = find_directive_argument(content, offset, lang_opts);
if(!range)
return;
protocol::DocumentLink link{.range = to_range(converter, *range)};
link.target = target.str();
links.push_back(std::move(link));
};
for(const auto& include: directives.includes) {
auto [fid, range] = unit.decompose_range(include.filename_range);
if(fid != interested || !range.valid()) {
continue;
if(include.fid.isValid()) {
add_link(include.location, unit.file_path(include.fid));
}
protocol::DocumentLink link{
.range = to_range(converter, range),
};
link.target = std::string(unit.file_path(include.fid));
links.push_back(std::move(link));
}
for(const auto& has_include: directives.has_includes) {
if(has_include.fid.isInvalid()) {
continue;
if(has_include.fid.isValid()) {
add_link(has_include.location, unit.file_path(has_include.fid));
}
}
auto [fid, offset] = unit.decompose_location(has_include.location);
if(fid != interested || offset >= content.size()) {
continue;
for(const auto& embed: directives.embeds) {
if(embed.file) {
add_link(embed.loc, embed.file->getName());
}
}
auto tail = content.substr(offset);
char open = tail.front();
if(open != '<' && open != '"') {
continue;
for(const auto& has_embed: directives.has_embeds) {
if(has_embed.file) {
add_link(has_embed.loc, has_embed.file->getName());
}
char close = open == '<' ? '>' : '"';
auto close_index = tail.find(close, 1);
if(close_index == llvm::StringRef::npos) {
continue;
}
LocalSourceRange range(offset, offset + static_cast<std::uint32_t>(close_index + 1));
protocol::DocumentLink link{
.range = to_range(converter, range),
};
link.target = std::string(unit.file_path(has_include.fid));
links.push_back(std::move(link));
}
return links;

View File

@@ -7,8 +7,9 @@
#include "compile/compilation.h"
#include "compile/compilation_unit.h"
#include "eventide/ipc/lsp/position.h"
#include "eventide/ipc/lsp/protocol.h"
#include "kota/ipc/lsp/position.h"
#include "kota/ipc/lsp/protocol.h"
namespace clang {
@@ -18,11 +19,11 @@ class NamedDecl;
namespace clice::feature {
namespace protocol = eventide::ipc::protocol;
namespace protocol = kota::ipc::protocol;
using eventide::ipc::lsp::PositionEncoding;
using eventide::ipc::lsp::PositionMapper;
using eventide::ipc::lsp::parse_position_encoding;
using kota::ipc::lsp::PositionEncoding;
using kota::ipc::lsp::PositionMapper;
using kota::ipc::lsp::parse_position_encoding;
inline auto to_range(const PositionMapper& converter, LocalSourceRange range) -> protocol::Range {
return protocol::Range{

View File

@@ -12,6 +12,7 @@
#include "clang/AST/Attr.h"
#include "clang/Basic/IdentifierTable.h"
#include "clang/Basic/Module.h"
namespace clice::feature {
@@ -168,6 +169,7 @@ public:
auto collect() -> std::vector<RawToken> {
highlight_lexical(unit.interested_file());
run();
highlight_modules();
merge_tokens();
return std::move(tokens);
}
@@ -291,6 +293,58 @@ private:
});
}
void highlight_modules() {
auto interested = unit.interested_file();
auto directives_it = unit.directives().find(interested);
if(directives_it != unit.directives().end()) {
for(const auto& import: directives_it->second.imports) {
add_token(import.location, SymbolKind::Keyword, 0);
for(auto loc: import.name_locations) {
add_token(loc, SymbolKind::Module, 0);
}
}
}
auto* mod = unit.context().getCurrentNamedModule();
if(!mod) {
return;
}
auto def_loc = mod->DefinitionLoc;
if(!def_loc.isValid() || !def_loc.isFileID()) {
return;
}
auto [fid, offset] = unit.decompose_location(def_loc);
if(fid != interested) {
return;
}
auto content = unit.file_content(fid);
auto& lang_opts = unit.lang_options();
Lexer lexer(content.substr(offset), false, &lang_opts);
auto module_token = lexer.advance();
if(module_token.is_identifier()) {
auto range = LocalSourceRange(offset + module_token.range.begin,
offset + module_token.range.end);
tokens.push_back({.range = range, .kind = SymbolKind::Keyword, .modifiers = 0});
}
// Scan for identifiers (module name parts) until semicolon/eof.
while(true) {
auto token = lexer.advance();
if(token.is_eof() || token.kind == clang::tok::semi) {
break;
}
if(token.is_identifier()) {
auto range = LocalSourceRange(offset + token.range.begin, offset + token.range.end);
tokens.push_back({.range = range, .kind = SymbolKind::Module, .modifiers = 0});
}
}
}
void highlight_lexical(clang::FileID fid) {
auto content = unit.file_content(fid);
auto& lang_opts = unit.lang_options();
@@ -345,10 +399,17 @@ private:
}
static void resolve_conflict(RawToken& last, const RawToken& current) {
(void)current;
if(last.kind == SymbolKind::Conflict) {
return;
}
// Directive is a low-priority lexical kind; semantic tokens override it.
if(last.kind == SymbolKind::Directive) {
last = current;
return;
}
if(current.kind == SymbolKind::Directive) {
return;
}
last.kind = SymbolKind::Conflict;
}

View File

@@ -131,33 +131,6 @@ public:
}
}
}
// if(auto module = unit.context().getCurrentNamedModule()) {
// auto keyword = module->DefinitionLoc;
// auto begin = TB.spelledTokenContaining(keyword);
// // assert(begin->kind() == clang::tok::identifier && begin->text(SM) == "module" &&
// // "Invalid module declaration");
//
// begin += 1;
// auto end = TB.spelledTokens(unit.file_id(keyword)).end();
//
// for(auto iter = begin; iter != end; ++iter) {
// if(iter->kind() == clang::tok::identifier) {
// if(auto next = iter + 1; next != end && (next->kind() == clang::tok::period ||
// next->kind() == clang::tok::colon)) {
// iter += 1;
// continue;
// }
//
// end = iter + 1;
// break;
// }
//
// std::unreachable();
// }
//
// handleModuleOccurrence(keyword, llvm::ArrayRef<clang::syntax::Token>(begin, end));
//}
}
public:

View File

@@ -33,19 +33,19 @@ void CompileGraph::ensure_resolved(std::uint32_t path_id) {
}
}
et::task<bool> CompileGraph::compile_deps(std::uint32_t path_id) {
kota::task<bool> CompileGraph::compile_deps(std::uint32_t path_id) {
llvm::DenseSet<std::uint32_t> ancestors;
co_return co_await compile_impl(path_id, ancestors, false);
}
et::task<bool> CompileGraph::compile(std::uint32_t path_id) {
kota::task<bool> CompileGraph::compile(std::uint32_t path_id) {
llvm::DenseSet<std::uint32_t> ancestors;
co_return co_await compile_impl(path_id, ancestors);
}
et::task<bool> CompileGraph::compile_impl(std::uint32_t path_id,
llvm::DenseSet<std::uint32_t> ancestors,
bool dispatch_self) {
kota::task<bool> CompileGraph::compile_impl(std::uint32_t path_id,
llvm::DenseSet<std::uint32_t> ancestors,
bool dispatch_self) {
ensure_resolved(path_id);
// Cycle detection: if this unit is already in the compile chain, bail out.
@@ -63,12 +63,12 @@ et::task<bool> CompileGraph::compile_impl(std::uint32_t path_id,
co_return true;
}
std::vector<et::task<bool>> dep_tasks;
std::vector<kota::task<bool>> dep_tasks;
dep_tasks.reserve(deps.size());
for(auto dep_id: deps) {
dep_tasks.push_back(compile_impl(dep_id, ancestors));
}
auto results = co_await et::when_all(std::move(dep_tasks));
auto results = co_await kota::when_all(std::move(dep_tasks));
for(auto ok: results) {
if(!ok) {
co_return false;
@@ -96,7 +96,7 @@ et::task<bool> CompileGraph::compile_impl(std::uint32_t path_id,
// Begin compilation. The finish lambda ensures compiling/completion state
// is always cleaned up, regardless of how the function exits.
it->second.compiling = true;
it->second.completion = std::make_unique<et::event>();
it->second.completion = std::make_unique<kota::event>();
auto finish = [&, path_id] {
auto& u = units.find(path_id)->second;
@@ -113,17 +113,17 @@ et::task<bool> CompileGraph::compile_impl(std::uint32_t path_id,
// Deadlocks from cross-branch cycles (e.g. 1->{2,3}, 2->3, 3->2) are
// prevented by has_wait_cycle() checking before completion.wait().
if(!deps.empty()) {
std::vector<et::task<bool, void, et::cancellation>> dep_tasks;
std::vector<kota::task<bool, void, kota::cancellation>> dep_tasks;
dep_tasks.reserve(deps.size());
for(auto dep_id: deps) {
dep_tasks.push_back(et::with_token(compile_impl(dep_id, ancestors), token));
dep_tasks.push_back(kota::with_token(compile_impl(dep_id, ancestors), token));
}
auto results = co_await et::when_all(std::move(dep_tasks));
auto results = co_await kota::when_all(std::move(dep_tasks));
if(results.is_cancelled()) {
finish();
co_await et::cancel();
co_await kota::cancel();
}
for(auto ok: *results) {
@@ -135,11 +135,11 @@ et::task<bool> CompileGraph::compile_impl(std::uint32_t path_id,
}
// Dispatch the actual compilation, cancellable via the pre-captured token.
auto result = co_await et::with_token(dispatch(path_id), token);
auto result = co_await kota::with_token(dispatch(path_id), token);
if(!result.has_value()) {
finish();
co_await et::cancel();
co_await kota::cancel();
}
if(!*result) {
@@ -199,7 +199,7 @@ llvm::SmallVector<std::uint32_t> CompileGraph::update(std::uint32_t path_id) {
// Cancel in-flight compilation if running.
if(unit.compiling) {
unit.source->cancel();
unit.source = std::make_unique<et::cancellation_source>();
unit.source = std::make_unique<kota::cancellation_source>();
}
unit.dirty = true;
unit.generation++;
@@ -247,7 +247,7 @@ bool CompileGraph::has_wait_cycle(std::uint32_t target,
void CompileGraph::cancel_all() {
for(auto& [_, unit]: units) {
unit.source->cancel();
unit.source = std::make_unique<et::cancellation_source>();
unit.source = std::make_unique<kota::cancellation_source>();
}
}

View File

@@ -4,16 +4,13 @@
#include <functional>
#include <memory>
#include "eventide/async/async.h"
#include "kota/async/async.h"
#include "llvm/ADT/DenseMap.h"
#include "llvm/ADT/DenseSet.h"
#include "llvm/ADT/SmallVector.h"
namespace clice {
namespace et = eventide;
struct CompileUnit {
std::uint32_t path_id = 0;
@@ -33,14 +30,15 @@ struct CompileUnit {
/// stale completions without ABA risk from raw-pointer comparison.
std::uint64_t generation = 0;
std::unique_ptr<et::cancellation_source> source = std::make_unique<et::cancellation_source>();
std::unique_ptr<et::event> completion;
std::unique_ptr<kota::cancellation_source> source =
std::make_unique<kota::cancellation_source>();
std::unique_ptr<kota::event> completion;
};
class CompileGraph {
public:
/// Performs the actual compilation (e.g. produce PCM file).
using dispatch_fn = std::function<et::task<bool>(std::uint32_t path_id)>;
using dispatch_fn = std::function<kota::task<bool>(std::uint32_t path_id)>;
/// Returns the dependency path_ids for a given path_id (called lazily on first compile).
using resolve_fn = std::function<llvm::SmallVector<std::uint32_t>(std::uint32_t path_id)>;
@@ -48,11 +46,11 @@ public:
CompileGraph(dispatch_fn dispatch, resolve_fn resolve);
/// Compile a unit and all its transitive dependencies.
et::task<bool> compile(std::uint32_t path_id);
kota::task<bool> compile(std::uint32_t path_id);
/// Compile all transitive module dependencies of path_id, but NOT path_id itself.
/// Used for non-module files (plain .cpp) that import modules.
et::task<bool> compile_deps(std::uint32_t path_id);
kota::task<bool> compile_deps(std::uint32_t path_id);
/// Mark path_id and all transitive dependents as dirty,
/// cancelling any in-progress compilations.
@@ -70,9 +68,9 @@ private:
void ensure_resolved(std::uint32_t path_id);
/// Internal compile with ancestor tracking for cycle detection.
et::task<bool> compile_impl(std::uint32_t path_id,
llvm::DenseSet<std::uint32_t> ancestors,
bool dispatch_self = true);
kota::task<bool> compile_impl(std::uint32_t path_id,
llvm::DenseSet<std::uint32_t> ancestors,
bool dispatch_self = true);
/// Check if waiting on `target` would deadlock given our `ancestors` chain.
/// Walks the dependency graph through compiling units to see if any dep

View File

@@ -5,9 +5,6 @@
#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"
@@ -15,6 +12,9 @@
#include "syntax/include_resolver.h"
#include "syntax/scan.h"
#include "kota/codec/json/json.h"
#include "kota/ipc/lsp/position.h"
#include "kota/ipc/lsp/uri.h"
#include "llvm/Support/FileSystem.h"
#include "llvm/Support/MemoryBuffer.h"
#include "llvm/Support/Path.h"
@@ -22,13 +22,13 @@
namespace clice {
namespace lsp = eventide::ipc::lsp;
using serde_raw = et::serde::RawValue;
namespace lsp = kota::ipc::lsp;
using serde_raw = kota::codec::RawValue;
/// Detect whether the cursor is inside a preamble directive (include/import).
Compiler::Compiler(et::event_loop& loop,
et::ipc::JsonPeer& peer,
Compiler::Compiler(kota::event_loop& loop,
kota::ipc::JsonPeer& peer,
Workspace& workspace,
WorkerPool& pool,
llvm::DenseMap<std::uint32_t, Session>& sessions) :
@@ -75,7 +75,7 @@ void Compiler::init_compile_graph() {
};
// Dispatch: sends BuildPCM request to a stateless worker.
auto dispatch = [this](std::uint32_t path_id) -> et::task<bool> {
auto dispatch = [this](std::uint32_t path_id) -> kota::task<bool> {
auto mod_it = workspace.path_to_module.find(path_id);
if(mod_it == workspace.path_to_module.end())
co_return false;
@@ -393,10 +393,10 @@ std::string uri_to_path(const std::string& uri) {
void Compiler::publish_diagnostics(const std::string& uri,
int version,
const et::serde::RawValue& diagnostics_json) {
const kota::codec::RawValue& diagnostics_json) {
std::vector<protocol::Diagnostic> diagnostics;
if(!diagnostics_json.empty()) {
auto status = et::serde::json::from_json(diagnostics_json.data, diagnostics);
auto status = kota::codec::json::from_json(diagnostics_json.data, diagnostics);
if(!status) {
LOG_WARN("Failed to deserialize diagnostics JSON for {}", uri);
}
@@ -415,9 +415,9 @@ void Compiler::clear_diagnostics(const std::string& uri) {
peer.send_notification(params);
}
et::task<bool> Compiler::ensure_pch(Session& session,
const std::string& directory,
const std::vector<std::string>& arguments) {
kota::task<bool> Compiler::ensure_pch(Session& session,
const std::string& directory,
const std::vector<std::string>& arguments) {
auto path_id = session.path_id;
auto path = workspace.path_pool.resolve(path_id);
auto& text = session.text;
@@ -471,7 +471,7 @@ et::task<bool> Compiler::ensure_pch(Session& session,
}
// Register in-flight build so concurrent requests wait on us.
auto completion = std::make_shared<et::event>();
auto completion = std::make_shared<kota::event>();
workspace.pch_cache[path_id].building = completion;
// Build a new PCH via stateless worker.
@@ -502,6 +502,7 @@ et::task<bool> Compiler::ensure_pch(Session& session,
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};
@@ -518,11 +519,11 @@ et::task<bool> Compiler::ensure_pch(Session& session,
/// 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) {
kota::task<bool> Compiler::ensure_deps(Session& session,
const std::string& directory,
const std::vector<std::string>& arguments,
std::pair<std::string, uint32_t>& pch,
std::unordered_map<std::string, std::string>& pcms) {
auto path_id = session.path_id;
// Compile C++20 module dependencies (PCMs).
@@ -619,7 +620,7 @@ void Compiler::record_deps(Session& session, llvm::ArrayRef<std::string> deps) {
/// 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) {
kota::task<bool> Compiler::ensure_compiled(Session& session) {
auto path_id = session.path_id;
LOG_DEBUG("ensure_compiled: path_id={} version={} gen={} ast_dirty={}",
@@ -662,7 +663,7 @@ et::task<bool> Compiler::ensure_compiled(Session& 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<> {
std::shared_ptr<Session::PendingCompile> pc) -> kota::task<> {
// Re-lookup session from the sessions map (pointer may have been
// invalidated by DenseMap growth during co_await).
auto find_session = [&]() -> Session* {
@@ -893,7 +894,7 @@ Compiler::RawResult Compiler::handle_completion(const protocol::Position& positi
item.kind = protocol::CompletionItemKind::File;
items.push_back(std::move(item));
}
auto json = et::serde::json::to_json<et::ipc::lsp_config>(items);
auto json = kota::codec::json::to_json<kota::ipc::lsp_config>(items);
co_return serde_raw{json ? std::move(*json) : "[]"};
}
if(pctx.kind == CompletionContext::Import) {
@@ -908,7 +909,7 @@ Compiler::RawResult Compiler::handle_completion(const protocol::Position& positi
item.insert_text = name + ";";
items.push_back(std::move(item));
}
auto json = et::serde::json::to_json<et::ipc::lsp_config>(items);
auto json = kota::codec::json::to_json<kota::ipc::lsp_config>(items);
co_return serde_raw{json ? std::move(*json) : "[]"};
}
}

View File

@@ -8,15 +8,16 @@
#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 "kota/async/async.h"
#include "kota/codec/raw_value.h"
#include "kota/ipc/codec/json.h"
#include "kota/ipc/lsp/protocol.h"
#include "kota/ipc/peer.h"
#include "llvm/ADT/ArrayRef.h"
#include "llvm/ADT/DenseMap.h"
#include "llvm/ADT/SmallVector.h"
@@ -24,8 +25,7 @@
namespace clice {
namespace et = eventide;
namespace protocol = et::ipc::protocol;
namespace protocol = kota::ipc::protocol;
/// Convert a file:// URI to a local file path.
std::string uri_to_path(const std::string& uri);
@@ -49,8 +49,8 @@ std::string uri_to_path(const std::string& uri);
/// - Background indexing scheduling — handled by Indexer
class Compiler {
public:
Compiler(et::event_loop& loop,
et::ipc::JsonPeer& peer,
Compiler(kota::event_loop& loop,
kota::ipc::JsonPeer& peer,
Workspace& workspace,
WorkerPool& pool,
llvm::DenseMap<std::uint32_t, Session>& sessions);
@@ -67,9 +67,9 @@ public:
/// 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);
kota::task<bool> ensure_compiled(Session& session);
using RawResult = et::task<et::serde::RawValue, et::ipc::Error>;
using RawResult = kota::task<kota::codec::RawValue, kota::ipc::Error>;
/// Forward a query to the stateful worker that holds this file's AST.
/// Ensures compilation first. For position-sensitive queries (hover,
@@ -97,20 +97,22 @@ public:
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);
kota::task<bool> ensure_deps(Session& session,
const std::string& directory,
const std::vector<std::string>& arguments,
std::pair<std::string, uint32_t>& pch,
std::unordered_map<std::string, std::string>& pcms);
et::task<bool> ensure_pch(Session& session,
const std::string& directory,
const std::vector<std::string>& arguments);
kota::task<bool> ensure_pch(Session& session,
const std::string& directory,
const std::vector<std::string>& arguments);
bool is_stale(const Session& session);
void record_deps(Session& session, llvm::ArrayRef<std::string> deps);
void publish_diagnostics(const std::string& uri, int version, const et::serde::RawValue& diags);
void publish_diagnostics(const std::string& uri,
int version,
const kota::codec::RawValue& diags);
std::optional<HeaderFileContext> resolve_header_context(std::uint32_t header_path_id,
Session* session);
@@ -122,8 +124,8 @@ private:
Session* session);
private:
et::event_loop& loop;
et::ipc::JsonPeer& peer;
kota::event_loop& loop;
kota::ipc::JsonPeer& peer;
Workspace& workspace;
WorkerPool& pool;
llvm::DenseMap<std::uint32_t, Session>& sessions;

View File

@@ -3,10 +3,11 @@
#include <algorithm>
#include <thread>
#include "eventide/serde/toml.h"
#include "support/filesystem.h"
#include "support/logging.h"
#include "kota/codec/toml.h"
namespace clice {
/// Replace all occurrences of ${workspace} with the workspace root.
@@ -59,7 +60,7 @@ std::optional<CliceConfig> CliceConfig::load(const std::string& path,
return std::nullopt;
}
auto result = eventide::serde::toml::parse<CliceConfig>(*content);
auto result = kota::codec::toml::parse<CliceConfig>(*content);
if(!result) {
LOG_WARN("Failed to parse config file {}", path);
return std::nullopt;

View File

@@ -4,9 +4,6 @@
#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"
@@ -15,6 +12,9 @@
#include "support/filesystem.h"
#include "support/logging.h"
#include "kota/ipc/lsp/position.h"
#include "kota/ipc/lsp/protocol.h"
#include "kota/ipc/lsp/uri.h"
#include "llvm/Support/FileSystem.h"
#include "llvm/Support/MemoryBuffer.h"
#include "llvm/Support/Path.h"
@@ -22,7 +22,7 @@
namespace clice {
namespace lsp = eventide::ipc::lsp;
namespace lsp = kota::ipc::lsp;
void Indexer::merge(const void* tu_index_data, std::size_t size) {
auto tu_index = index::TUIndex::from(tu_index_data);
@@ -630,13 +630,13 @@ void Indexer::schedule() {
indexing_scheduled = true;
if(!index_idle_timer) {
index_idle_timer = std::make_shared<et::timer>(et::timer::create(loop));
index_idle_timer = std::make_shared<kota::timer>(kota::timer::create(loop));
}
index_idle_timer->start(std::chrono::milliseconds(workspace.config.idle_timeout_ms));
loop.schedule(run_background_indexing());
}
et::task<> Indexer::run_background_indexing() {
kota::task<> Indexer::run_background_indexing() {
if(index_idle_timer) {
co_await index_idle_timer->wait();
}

View File

@@ -7,22 +7,21 @@
#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 "kota/async/async.h"
#include "kota/ipc/lsp/position.h"
#include "kota/ipc/lsp/protocol.h"
#include "llvm/ADT/DenseMap.h"
#include "llvm/ADT/SmallVector.h"
#include "llvm/ADT/StringRef.h"
namespace clice {
namespace et = eventide;
namespace protocol = et::ipc::protocol;
namespace lsp = et::ipc::lsp;
namespace protocol = kota::ipc::protocol;
namespace lsp = kota::ipc::lsp;
struct Session;
class Compiler;
@@ -54,7 +53,7 @@ struct SymbolInfo {
/// - Document lifecycle — handled by MasterServer
class Indexer {
public:
Indexer(et::event_loop& loop,
Indexer(kota::event_loop& loop,
Workspace& workspace,
llvm::DenseMap<std::uint32_t, Session>& sessions,
WorkerPool& pool,
@@ -165,7 +164,7 @@ private:
}
private:
et::event_loop& loop;
kota::event_loop& loop;
Workspace& workspace;
llvm::DenseMap<std::uint32_t, Session>& sessions;
WorkerPool& pool;
@@ -181,9 +180,9 @@ private:
std::size_t index_queue_pos = 0;
bool indexing_active = false;
bool indexing_scheduled = false;
std::shared_ptr<et::timer> index_idle_timer;
std::shared_ptr<kota::timer> index_idle_timer;
et::task<> run_background_indexing();
kota::task<> run_background_indexing();
};
} // namespace clice

View File

@@ -6,37 +6,39 @@
#include <type_traits>
#include <variant>
#include "eventide/ipc/lsp/position.h"
#include "eventide/ipc/lsp/protocol.h"
#include "eventide/ipc/lsp/uri.h"
#include "eventide/reflection/enum.h"
#include "eventide/serde/json/json.h"
#include "semantic/symbol_kind.h"
#include "server/protocol.h"
#include "support/filesystem.h"
#include "support/logging.h"
#include "kota/codec/json/json.h"
#include "kota/ipc/lsp/position.h"
#include "kota/ipc/lsp/protocol.h"
#include "kota/ipc/lsp/uri.h"
#include "kota/meta/enum.h"
#include "llvm/Support/FileSystem.h"
#include "llvm/Support/Path.h"
#include "llvm/Support/Process.h"
namespace clice {
namespace protocol = eventide::ipc::protocol;
namespace lsp = eventide::ipc::lsp;
namespace refl = eventide::refl;
using et::ipc::RequestResult;
using RequestContext = et::ipc::JsonPeer::RequestContext;
using serde_raw = et::serde::RawValue;
namespace protocol = kota::ipc::protocol;
namespace lsp = kota::ipc::lsp;
namespace refl = kota::meta;
using kota::ipc::RequestResult;
using RequestContext = kota::ipc::JsonPeer::RequestContext;
using serde_raw = kota::codec::RawValue;
/// Serialize a value to a JSON RawValue using LSP config.
template <typename T>
static serde_raw to_raw(const T& value) {
auto json = et::serde::json::to_json<et::ipc::lsp_config>(value);
auto json = kota::codec::json::to_json<kota::ipc::lsp_config>(value);
return serde_raw{json ? std::move(*json) : "null"};
}
MasterServer::MasterServer(et::event_loop& loop, et::ipc::JsonPeer& peer, std::string self_path) :
MasterServer::MasterServer(kota::event_loop& loop,
kota::ipc::JsonPeer& peer,
std::string self_path) :
loop(loop), peer(peer), pool(loop), compiler(loop, peer, workspace, pool, sessions),
indexer(loop,
workspace,
@@ -54,7 +56,7 @@ MasterServer::MasterServer(et::event_loop& loop, et::ipc::JsonPeer& peer, std::s
MasterServer::~MasterServer() = default;
et::task<> MasterServer::load_workspace() {
kota::task<> MasterServer::load_workspace() {
if(workspace_root.empty())
co_return;
@@ -154,7 +156,7 @@ void MasterServer::register_handlers() {
peer.on_request([this](RequestContext& ctx, const protocol::InitializeParams& params)
-> RequestResult<protocol::InitializeParams> {
if(lifecycle != ServerLifecycle::Uninitialized) {
co_return et::outcome_error(protocol::Error{"Server already initialized"});
co_return kota::outcome_error(protocol::Error{"Server already initialized"});
}
auto& init = params.lsp__initialize_params;
@@ -293,7 +295,7 @@ void MasterServer::register_handlers() {
indexer.save(workspace.config.index_dir);
workspace.save_cache();
loop.schedule([this]() -> et::task<> {
loop.schedule([this]() -> kota::task<> {
co_await pool.stop();
loop.stop();
}());
@@ -478,15 +480,38 @@ void MasterServer::register_handlers() {
co_return co_await compiler.forward_query(worker::QueryKind::DocumentSymbol, sit->second);
});
peer.on_request(
[this](RequestContext& ctx, const protocol::DocumentLinkParams& params) -> RawResult {
auto path = uri_to_path(params.text_document.uri);
auto path_id = workspace.path_pool.intern(path);
auto sit = sessions.find(path_id);
if(sit == sessions.end())
co_return serde_raw{"null"};
co_return co_await compiler.forward_query(worker::QueryKind::DocumentLink, sit->second);
});
peer.on_request([this](RequestContext& ctx,
const protocol::DocumentLinkParams& params) -> RawResult {
auto path = uri_to_path(params.text_document.uri);
auto path_id = workspace.path_pool.intern(path);
auto sit = sessions.find(path_id);
if(sit == sessions.end())
co_return serde_raw{"null"};
auto& session = sit->second;
auto result = co_await compiler.forward_query(worker::QueryKind::DocumentLink, session);
if(!result.has_value())
co_return serde_raw{"null"};
// Merge document links from PCH if available.
auto& links = result.value();
// Re-lookup session after co_await since iterators may be invalidated.
auto sit2 = sessions.find(path_id);
if(sit2 != sessions.end() && sit2->second.pch_ref) {
auto pch_it = workspace.pch_cache.find(sit2->second.pch_ref->path_id);
if(pch_it != workspace.pch_cache.end() && !pch_it->second.document_links_json.empty()) {
auto& pch_json = pch_it->second.document_links_json;
// Merge two JSON arrays.
if(!links.data.empty() && links.data != "null" && links.data.size() > 2) {
// "[a,b]" + "[c,d]" -> "[a,b,c,d]"
links.data.pop_back(); // remove trailing ']'
links.data += ',';
links.data.append(pch_json.begin() + 1, pch_json.end()); // skip '['
} else {
links.data = pch_json;
}
}
}
co_return std::move(links);
});
peer.on_request(
[this](RequestContext& ctx, const protocol::CodeActionParams& params) -> RawResult {

View File

@@ -5,21 +5,19 @@
#include <string>
#include <vector>
#include "eventide/async/async.h"
#include "eventide/ipc/peer.h"
#include "eventide/serde/serde/raw_value.h"
#include "server/compiler.h"
#include "server/indexer.h"
#include "server/session.h"
#include "server/worker_pool.h"
#include "server/workspace.h"
#include "kota/async/async.h"
#include "kota/codec/raw_value.h"
#include "kota/ipc/peer.h"
#include "llvm/ADT/DenseMap.h"
namespace clice {
namespace et = eventide;
enum class ServerLifecycle : std::uint8_t {
Uninitialized,
Initialized,
@@ -44,14 +42,14 @@ enum class ServerLifecycle : std::uint8_t {
/// point to disk files. The only path from Session to Workspace is didSave.
class MasterServer {
public:
MasterServer(et::event_loop& loop, et::ipc::JsonPeer& peer, std::string self_path);
MasterServer(kota::event_loop& loop, kota::ipc::JsonPeer& peer, std::string self_path);
~MasterServer();
void register_handlers();
private:
et::event_loop& loop;
et::ipc::JsonPeer& peer;
kota::event_loop& loop;
kota::ipc::JsonPeer& peer;
/// Persistent project-wide state (config, CDB, path pool, dependency
/// graphs, compilation caches, symbol index).
@@ -74,9 +72,9 @@ private:
std::string workspace_root;
std::string session_log_dir;
et::task<> load_workspace();
kota::task<> load_workspace();
using RawResult = et::task<et::serde::RawValue, et::ipc::Error>;
using RawResult = kota::task<kota::codec::RawValue, kota::ipc::Error>;
};
} // namespace clice

View File

@@ -7,14 +7,15 @@
#include <utility>
#include <vector>
#include "eventide/ipc/lsp/protocol.h"
#include "eventide/ipc/protocol.h"
#include "eventide/serde/serde/raw_value.h"
#include "syntax/token.h"
#include "kota/codec/raw_value.h"
#include "kota/ipc/lsp/protocol.h"
#include "kota/ipc/protocol.h"
namespace clice::worker {
namespace protocol = eventide::ipc::protocol;
namespace protocol = kota::ipc::protocol;
/// Kind of AST query dispatched to a stateful worker.
enum class QueryKind : uint8_t {
@@ -51,7 +52,7 @@ struct CompileParams {
struct CompileResult {
int version;
/// Diagnostics serialized as JSON (RawValue) to avoid bincode/serde annotation conflicts.
eventide::serde::RawValue diagnostics;
kota::codec::RawValue diagnostics;
std::size_t memory_usage;
std::vector<std::string> deps;
/// Serialized TUIndex for the main file (interested_only=true).
@@ -102,7 +103,8 @@ struct BuildResult {
std::string output_path; ///< PCH or PCM path
std::vector<std::string> deps;
std::string tu_index_data;
eventide::serde::RawValue result_json; ///< Completion/SignatureHelp result
std::string pch_links_json; ///< Pre-serialized DocumentLink[] from PCH
kota::codec::RawValue result_json; ///< Completion/SignatureHelp result
};
struct DocumentUpdateParams {
@@ -157,7 +159,7 @@ struct SwitchContextResult {
} // namespace clice::ext
namespace eventide::ipc::protocol {
namespace kota::ipc::protocol {
template <>
struct RequestTraits<clice::worker::CompileParams> {
@@ -167,7 +169,7 @@ struct RequestTraits<clice::worker::CompileParams> {
template <>
struct RequestTraits<clice::worker::QueryParams> {
using Result = eventide::serde::RawValue;
using Result = kota::codec::RawValue;
constexpr inline static std::string_view method = "clice/worker/query";
};
@@ -192,4 +194,4 @@ struct NotificationTraits<clice::worker::EvictedParams> {
constexpr inline static std::string_view method = "clice/worker/evicted";
};
} // namespace eventide::ipc::protocol
} // namespace kota::ipc::protocol

View File

@@ -5,15 +5,13 @@
#include <optional>
#include <string>
#include "eventide/async/async.h"
#include "server/workspace.h"
#include "kota/async/async.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.
@@ -45,7 +43,7 @@ struct Session {
/// Other queries wait on the event; the compilation task itself
/// runs independently and cannot be cancelled by LSP $/cancelRequest.
struct PendingCompile {
et::event done;
kota::event done;
bool succeeded = false;
};

View File

@@ -8,23 +8,23 @@
#include <vector>
#include "compile/compilation.h"
#include "eventide/async/async.h"
#include "eventide/ipc/peer.h"
#include "eventide/ipc/transport.h"
#include "feature/feature.h"
#include "index/tu_index.h"
#include "server/protocol.h"
#include "server/worker_common.h"
#include "support/logging.h"
#include "kota/async/async.h"
#include "kota/ipc/codec/bincode.h"
#include "kota/ipc/peer.h"
#include "kota/ipc/transport.h"
#include "llvm/ADT/StringMap.h"
#include "llvm/Support/raw_ostream.h"
namespace clice {
namespace et = eventide;
using et::ipc::RequestResult;
using RequestContext = et::ipc::BincodePeer::RequestContext;
using kota::ipc::RequestResult;
using RequestContext = kota::ipc::BincodePeer::RequestContext;
struct DocumentEntry {
int version = 0;
@@ -35,7 +35,7 @@ struct DocumentEntry {
// Signaled when the first compilation completes (has_ast becomes true).
// Feature handlers co_await this before accessing the AST.
et::event ast_ready{false};
kota::event ast_ready{false};
// Compilation context (from CompileParams)
std::string directory;
@@ -44,11 +44,11 @@ struct DocumentEntry {
llvm::StringMap<std::string> pcms;
// Per-document serialization mutex
et::mutex strand;
kota::mutex strand;
};
class StatefulWorker {
et::ipc::BincodePeer& peer;
kota::ipc::BincodePeer& peer;
std::uint64_t memory_limit;
llvm::StringMap<std::shared_ptr<DocumentEntry>> documents;
@@ -91,10 +91,10 @@ class StatefulWorker {
/// Look up document, wait for AST, lock strand, run fn(doc) on thread pool, unlock.
/// Returns "null" if document not found or AST not usable.
template <typename F>
et::task<et::serde::RawValue> with_ast(llvm::StringRef path, F&& fn) {
kota::task<kota::codec::RawValue> with_ast(llvm::StringRef path, F&& fn) {
auto it = documents.find(path);
if(it == documents.end()) {
co_return et::serde::RawValue{"null"};
co_return kota::codec::RawValue{"null"};
}
// Hold shared_ptr so Evict can't destroy the entry mid-request.
@@ -104,9 +104,9 @@ class StatefulWorker {
co_await doc->ast_ready.wait();
co_await doc->strand.lock();
auto result = co_await et::queue([&]() -> et::serde::RawValue {
auto result = co_await kota::queue([&]() -> kota::codec::RawValue {
if(!doc->has_ast || (!doc->unit.completed() && !doc->unit.fatal_error()))
return et::serde::RawValue{"null"};
return kota::codec::RawValue{"null"};
return fn(*doc);
});
@@ -115,7 +115,7 @@ class StatefulWorker {
}
public:
StatefulWorker(et::ipc::BincodePeer& peer, std::uint64_t memory_limit) :
StatefulWorker(kota::ipc::BincodePeer& peer, std::uint64_t memory_limit) :
peer(peer), memory_limit(memory_limit) {}
void register_handlers();
@@ -147,7 +147,7 @@ void StatefulWorker::register_handlers() {
doc->pcms.try_emplace(name, pcm_path);
}
auto compile_result = co_await et::queue([&]() -> worker::CompileResult {
auto compile_result = co_await kota::queue([&]() -> worker::CompileResult {
ScopedTimer timer;
CompilationParams cp;
@@ -169,15 +169,15 @@ void StatefulWorker::register_handlers() {
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) : "[]"};
auto json = kota::codec::json::to_json<kota::ipc::lsp_config>(diags);
result.diagnostics = kota::codec::RawValue{json ? std::move(*json) : "[]"};
LOG_INFO("Compile done: path={}, {}ms, {} diags, fatal={}",
params.path,
timer.ms(),
diags.size(),
doc->unit.fatal_error());
} else {
result.diagnostics = et::serde::RawValue{"[]"};
result.diagnostics = kota::codec::RawValue{"[]"};
LOG_WARN("Compile incomplete: path={}, {}ms", params.path, timer.ms());
}
result.memory_usage = 0; // TODO: query actual memory
@@ -201,7 +201,7 @@ void StatefulWorker::register_handlers() {
// === 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
// here. The kota::queue compilation work may be reading doc.text on the
// thread pool concurrently, so writing it from the event loop would be
// a data race. The next Compile request will bring the correct text
// and update it inside the strand lock.
@@ -238,11 +238,11 @@ void StatefulWorker::register_handlers() {
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"};
return result ? to_raw(*result) : kota::codec::RawValue{"null"};
});
case K::GoToDefinition:
// TODO: Implement go-to-definition
co_return et::serde::RawValue{"[]"};
co_return kota::codec::RawValue{"[]"};
case K::SemanticTokens:
co_return co_await with_ast(params.path, [&](DocumentEntry& doc) {
return to_raw(feature::semantic_tokens(doc.unit));
@@ -268,9 +268,9 @@ void StatefulWorker::register_handlers() {
});
case K::CodeAction:
// TODO: Implement code actions
co_return et::serde::RawValue{"[]"};
co_return kota::codec::RawValue{"[]"};
}
co_return et::serde::RawValue{"null"};
co_return kota::codec::RawValue{"null"};
});
}
@@ -284,15 +284,15 @@ int run_stateful_worker_mode(std::uint64_t memory_limit,
LOG_INFO("Starting stateful worker, memory_limit={}MB", memory_limit / (1024 * 1024));
et::event_loop loop;
kota::event_loop loop;
auto transport_result = et::ipc::StreamTransport::open_stdio(loop);
auto transport_result = kota::ipc::StreamTransport::open_stdio(loop);
if(!transport_result) {
LOG_ERROR("Failed to open stdio transport");
return 1;
}
et::ipc::BincodePeer peer(loop, std::move(*transport_result));
kota::ipc::BincodePeer peer(loop, std::move(*transport_result));
StatefulWorker worker(peer, memory_limit);
worker.register_handlers();

View File

@@ -1,22 +1,22 @@
#include "server/stateless_worker.h"
#include "compile/compilation.h"
#include "eventide/async/async.h"
#include "eventide/ipc/peer.h"
#include "eventide/ipc/transport.h"
#include "feature/feature.h"
#include "index/tu_index.h"
#include "server/protocol.h"
#include "server/worker_common.h"
#include "support/logging.h"
#include "kota/async/async.h"
#include "kota/ipc/codec/bincode.h"
#include "kota/ipc/peer.h"
#include "kota/ipc/transport.h"
#include "llvm/Support/raw_ostream.h"
namespace clice {
namespace et = eventide;
using et::ipc::RequestResult;
using RequestContext = et::ipc::BincodePeer::RequestContext;
using kota::ipc::RequestResult;
using RequestContext = kota::ipc::BincodePeer::RequestContext;
/// Extract error messages from compilation diagnostics.
static std::string collect_errors(CompilationUnit& unit) {
@@ -96,8 +96,13 @@ static worker::BuildResult handle_build_pch(const worker::BuildParams& params) {
errors = collect_errors(unit);
std::string tu_index_data;
if(success)
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);
@@ -110,6 +115,7 @@ static worker::BuildResult handle_build_pch(const worker::BuildParams& params) {
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);
@@ -260,20 +266,20 @@ int run_stateless_worker_mode(const std::string& worker_name, const std::string&
LOG_INFO("Starting stateless worker");
et::event_loop loop;
kota::event_loop loop;
auto transport_result = et::ipc::StreamTransport::open_stdio(loop);
auto transport_result = kota::ipc::StreamTransport::open_stdio(loop);
if(!transport_result) {
LOG_ERROR("Failed to open stdio transport");
return 1;
}
et::ipc::BincodePeer peer(loop, std::move(*transport_result));
kota::ipc::BincodePeer peer(loop, std::move(*transport_result));
peer.on_request([&](RequestContext& ctx,
const worker::BuildParams& params) -> RequestResult<worker::BuildParams> {
using K = worker::BuildKind;
auto result = co_await et::queue([&]() -> worker::BuildResult {
auto result = co_await kota::queue([&]() -> worker::BuildResult {
switch(params.kind) {
case K::BuildPCH: return handle_build_pch(params);
case K::BuildPCM: return handle_build_pcm(params);

View File

@@ -7,9 +7,10 @@
#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"
#include "kota/codec/json/serializer.h"
#include "kota/codec/raw_value.h"
#include "kota/ipc/codec/json.h"
namespace clice {
@@ -36,9 +37,9 @@ inline void fill_args(CompilationParams& cp,
/// 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"};
inline kota::codec::RawValue to_raw(const T& value) {
auto json = kota::codec::json::to_json<kota::ipc::lsp_config>(value);
return kota::codec::RawValue{json ? std::move(*json) : "null"};
}
} // namespace clice

View File

@@ -3,9 +3,10 @@
#include <csignal>
#include <string>
#include "eventide/ipc/transport.h"
#include "support/logging.h"
#include "kota/ipc/transport.h"
namespace clice {
namespace {
@@ -13,7 +14,7 @@ namespace {
/// Coroutine that drains a worker's stderr pipe.
/// Workers write their own log files, so this only captures unexpected output
/// (crash stacktraces, assertion failures, etc.) that bypasses spdlog.
et::task<> drain_stderr(et::pipe stderr_pipe, std::string prefix) {
kota::task<> drain_stderr(kota::pipe stderr_pipe, std::string prefix) {
std::string buffer;
while(true) {
auto result = co_await stderr_pipe.read();
@@ -54,7 +55,7 @@ bool WorkerPool::spawn_worker(const std::string& self_path,
auto worker_index = workers.size();
std::string worker_name = std::string(stateful ? "SF-" : "SL-") + std::to_string(worker_index);
et::process::options opts;
kota::process::options opts;
opts.file = self_path;
if(stateful) {
opts.args = {self_path,
@@ -75,12 +76,12 @@ bool WorkerPool::spawn_worker(const std::string& self_path,
}
opts.streams = {
et::process::stdio::pipe(true, false), // stdin: child reads
et::process::stdio::pipe(false, true), // stdout: child writes
et::process::stdio::pipe(false, true), // stderr: child writes
kota::process::stdio::pipe(true, false), // stdin: child reads
kota::process::stdio::pipe(false, true), // stdout: child writes
kota::process::stdio::pipe(false, true), // stderr: child writes
};
auto result = et::process::spawn(opts, loop);
auto result = kota::process::spawn(opts, loop);
if(!result) {
LOG_ERROR("Failed to spawn {} worker: {}",
stateful ? "stateful" : "stateless",
@@ -92,9 +93,9 @@ bool WorkerPool::spawn_worker(const std::string& self_path,
// StreamTransport: input = child's stdout (parent reads), output = child's stdin (parent
// writes)
auto transport = std::make_unique<et::ipc::StreamTransport>(std::move(spawn.stdout_pipe),
std::move(spawn.stdin_pipe));
auto peer = std::make_unique<et::ipc::BincodePeer>(loop, std::move(transport));
auto transport = std::make_unique<kota::ipc::StreamTransport>(std::move(spawn.stdout_pipe),
std::move(spawn.stdin_pipe));
auto peer = std::make_unique<kota::ipc::BincodePeer>(loop, std::move(transport));
// Schedule stderr log collection
std::string prefix = "[" + worker_name + "]";
@@ -142,7 +143,7 @@ bool WorkerPool::start(const WorkerPoolOptions& options) {
return true;
}
et::task<> WorkerPool::stop() {
kota::task<> WorkerPool::stop() {
LOG_INFO("WorkerPool stopping...");
// Close output pipes to signal workers to exit gracefully

View File

@@ -6,17 +6,17 @@
#include <list>
#include <memory>
#include "eventide/async/async.h"
#include "eventide/ipc/peer.h"
#include "server/protocol.h"
#include "kota/async/async.h"
#include "kota/ipc/codec/bincode.h"
#include "kota/ipc/peer.h"
#include "llvm/ADT/DenseMap.h"
#include "llvm/ADT/SmallVector.h"
namespace clice {
namespace et = eventide;
using et::ipc::RequestResult;
using kota::ipc::RequestResult;
struct WorkerPoolOptions {
std::string self_path;
@@ -28,23 +28,24 @@ struct WorkerPoolOptions {
class WorkerPool {
public:
WorkerPool(et::event_loop& loop) : loop(loop) {}
WorkerPool(kota::event_loop& loop) : loop(loop) {}
/// Spawn all worker processes. Returns false on failure.
bool start(const WorkerPoolOptions& options);
/// Gracefully stop all workers.
et::task<> stop();
kota::task<> stop();
/// Send a request to a stateful worker with path_id affinity routing.
template <typename Params>
RequestResult<Params> send_stateful(std::uint32_t path_id,
const Params& params,
et::ipc::request_options opts = {});
kota::ipc::request_options opts = {});
/// Send a request to a stateless worker with round-robin dispatch.
template <typename Params>
RequestResult<Params> send_stateless(const Params& params, et::ipc::request_options opts = {});
RequestResult<Params> send_stateless(const Params& params,
kota::ipc::request_options opts = {});
/// Send a notification to the stateful worker owning path_id (if any).
template <typename Params>
@@ -60,12 +61,12 @@ public:
private:
struct WorkerProcess {
et::process proc;
std::unique_ptr<et::ipc::BincodePeer> peer;
kota::process proc;
std::unique_ptr<kota::ipc::BincodePeer> peer;
std::size_t owned_documents = 0;
};
et::event_loop& loop;
kota::event_loop& loop;
llvm::SmallVector<WorkerProcess> stateless_workers;
llvm::SmallVector<WorkerProcess> stateful_workers;
std::size_t next_stateless = 0;
@@ -86,13 +87,13 @@ private:
template <typename Params>
RequestResult<Params> WorkerPool::send_stateful(std::uint32_t path_id,
const Params& params,
et::ipc::request_options opts) {
kota::ipc::request_options opts) {
if(stateful_workers.empty()) {
co_return et::outcome_error(et::ipc::Error{"No stateful workers available"});
co_return kota::outcome_error(kota::ipc::Error{"No stateful workers available"});
}
// No timeout: compile tasks run as detached tasks (loop.schedule) that
// are immune to LSP $/cancelRequest. Adding a timeout here would use
// eventide's with_token/when_any which has a spurious-cancellation bug
// kotatsu's with_token/when_any which has a spurious-cancellation bug
// that kills requests within milliseconds instead of the configured period.
auto idx = assign_worker(path_id);
co_return co_await stateful_workers[idx].peer->send_request(params, opts);
@@ -100,9 +101,9 @@ RequestResult<Params> WorkerPool::send_stateful(std::uint32_t path_id,
template <typename Params>
RequestResult<Params> WorkerPool::send_stateless(const Params& params,
et::ipc::request_options opts) {
kota::ipc::request_options opts) {
if(stateless_workers.empty()) {
co_return et::outcome_error(et::ipc::Error{"No stateless workers available"});
co_return kota::outcome_error(kota::ipc::Error{"No stateless workers available"});
}
auto idx = next_stateless;
next_stateless = (next_stateless + 1) % stateless_workers.size();

View File

@@ -3,13 +3,13 @@
#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 "kota/codec/json/json.h"
#include "kota/ipc/lsp/position.h"
#include "kota/ipc/lsp/protocol.h"
#include "llvm/Support/Chrono.h"
#include "llvm/Support/FileSystem.h"
#include "llvm/Support/MemoryBuffer.h"
@@ -18,7 +18,7 @@
namespace clice {
namespace lsp = eventide::ipc::lsp;
namespace lsp = kota::ipc::lsp;
/// Find the tightest (innermost) occurrence containing `offset` via binary search.
const static index::Occurrence* lookup_occurrence(const std::vector<index::Occurrence>& occs,
@@ -194,7 +194,7 @@ void Workspace::load_cache() {
}
CacheData data;
auto status = eventide::serde::json::from_json(*content, data);
auto status = kota::codec::json::from_json(*content, data);
if(!status) {
LOG_WARN("Failed to parse cache.json");
return;
@@ -300,7 +300,7 @@ void Workspace::save_cache() {
data.pcm.push_back(std::move(entry));
}
auto json_str = eventide::serde::json::to_json(data);
auto json_str = kota::codec::json::to_json(data);
if(!json_str) {
LOG_WARN("Failed to serialize cache.json");
return;

View File

@@ -8,8 +8,6 @@
#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"
@@ -18,6 +16,8 @@
#include "support/path_pool.h"
#include "syntax/dependency_graph.h"
#include "kota/ipc/lsp/position.h"
#include "kota/ipc/lsp/protocol.h"
#include "llvm/ADT/ArrayRef.h"
#include "llvm/ADT/DenseMap.h"
#include "llvm/ADT/SmallVector.h"
@@ -25,9 +25,8 @@
namespace clice {
namespace et = eventide;
namespace protocol = et::ipc::protocol;
namespace lsp = et::ipc::lsp;
namespace protocol = kota::ipc::protocol;
namespace lsp = kota::ipc::lsp;
/// Two-layer staleness snapshot for compilation artifacts (PCH, AST, etc.).
///
@@ -140,7 +139,8 @@ struct PCHState {
std::uint32_t bound = 0;
std::uint64_t hash = 0;
DepsSnapshot deps;
std::shared_ptr<eventide::event> building;
std::string document_links_json; ///< Pre-serialized DocumentLink[] from PCH build
std::shared_ptr<kota::event> building;
};
/// Cached PCM state for a single C++20 module. Shared across all files that

View File

@@ -6,11 +6,10 @@
#include <system_error>
#include <type_traits>
#include "eventide/common/meta.h"
#include "eventide/common/ranges.h"
#include "eventide/reflection/enum.h"
#include "eventide/reflection/struct.h"
#include "kota/meta/enum.h"
#include "kota/meta/struct.h"
#include "kota/support/ranges.h"
#include "kota/support/type_traits.h"
#include "llvm/ADT/SmallString.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/Support/Error.h"
@@ -86,7 +85,7 @@ struct std::formatter<std::error_code> : std::formatter<std::string_view> {
}
};
template <eventide::refl::enum_type E>
template <kota::meta::enum_type E>
struct std::formatter<E> : std::formatter<std::string> {
using Base = std::formatter<std::string>;
@@ -97,7 +96,7 @@ struct std::formatter<E> : std::formatter<std::string> {
template <typename FormatContext>
auto format(const E& value, FormatContext& ctx) const {
auto name = eventide::refl::enum_name(value);
auto name = kota::meta::enum_name(value);
if(name.empty()) {
using U = std::underlying_type_t<E>;
return Base::format(std::format("{}", static_cast<U>(value)), ctx);
@@ -107,9 +106,8 @@ struct std::formatter<E> : std::formatter<std::string> {
};
template <typename T>
concept clice_reflectable_class =
eventide::refl::reflectable_class<T> && !eventide::sequence_range<T> &&
!eventide::set_range<T> && !eventide::map_range<T>;
concept clice_reflectable_class = kota::meta::reflectable_class<T> && !kota::sequence_range<T> &&
!kota::set_range<T> && !kota::map_range<T>;
template <clice_reflectable_class T>
struct std::formatter<T> : std::formatter<std::string> {
@@ -138,7 +136,7 @@ std::string dump(const Object& object) {
return std::format("\"{}\"", object);
} else if constexpr(std::is_same_v<T, llvm::StringRef>) {
return std::format("\"{}\"", object);
} else if constexpr(eventide::map_range<T>) {
} else if constexpr(kota::map_range<T>) {
std::string result = "{";
bool first = true;
for(auto&& [key, value]: object) {
@@ -150,8 +148,8 @@ std::string dump(const Object& object) {
}
result += "}";
return result;
} else if constexpr(eventide::set_range<T> || eventide::sequence_range<T>) {
std::string result = eventide::set_range<T> ? "{" : "[";
} else if constexpr(kota::set_range<T> || kota::sequence_range<T>) {
std::string result = kota::set_range<T> ? "{" : "[";
bool first = true;
for(auto&& value: object) {
if(!first) {
@@ -160,10 +158,10 @@ std::string dump(const Object& object) {
first = false;
result += dump(value);
}
result += eventide::set_range<T> ? "}" : "]";
result += kota::set_range<T> ? "}" : "]";
return result;
} else if constexpr(eventide::refl::enum_type<T>) {
auto name = eventide::refl::enum_name(object);
} else if constexpr(kota::meta::enum_type<T>) {
auto name = kota::meta::enum_name(object);
if(!name.empty()) {
return std::format("\"{}\"", name);
}
@@ -172,7 +170,7 @@ std::string dump(const Object& object) {
} else if constexpr(clice_reflectable_class<T>) {
std::string result = "{";
bool first = true;
eventide::refl::for_each(object, [&](auto field) {
kota::meta::for_each(object, [&](auto field) {
if(!first) {
result += ", ";
}
@@ -181,7 +179,7 @@ std::string dump(const Object& object) {
});
result += "}";
return result;
} else if constexpr(eventide::Formattable<T>) {
} else if constexpr(kota::Formattable<T>) {
return std::format("{}", object);
} else {
return "<unformattable>";

View File

@@ -4,11 +4,11 @@
#include <chrono>
#include "command/toolchain.h"
#include "eventide/async/async.h"
#include "support/logging.h"
#include "syntax/include_resolver.h"
#include "syntax/scan.h"
#include "kota/async/async.h"
#include "llvm/ADT/DenseSet.h"
#include "llvm/ADT/StringSet.h"
#include "llvm/Support/FileSystem.h"
@@ -18,8 +18,6 @@
namespace clice {
namespace et = eventide;
// DependencyGraph implementation
void DependencyGraph::add_module(llvm::StringRef module_name, std::uint32_t path_id) {
@@ -253,12 +251,12 @@ FileScanResult scan_file_worker(const char* path, std::uint32_t path_id, std::ui
}
/// The async scan implementation that runs on a local event loop.
et::task<> scan_impl(CompilationDatabase& cdb,
PathPool& path_pool,
DependencyGraph& graph,
ScanReport& report,
ScanCache* ext_cache,
et::event_loop& loop) {
kota::task<> scan_impl(CompilationDatabase& cdb,
PathPool& path_pool,
DependencyGraph& graph,
ScanReport& report,
ScanCache* ext_cache,
kota::event_loop& loop) {
auto start_time = std::chrono::steady_clock::now();
// Reuse context groups and configs from cache when available (warm runs).
@@ -316,10 +314,10 @@ et::task<> scan_impl(CompilationDatabase& cdb,
if(!pending.empty()) {
LOG_INFO("Warming toolchain cache: {} unique queries", pending.size());
std::vector<et::task<ToolchainResult, et::error>> tasks;
std::vector<kota::task<ToolchainResult, kota::error>> tasks;
tasks.reserve(pending.size());
for(auto& query: pending) {
tasks.push_back(et::queue(
tasks.push_back(kota::queue(
[q = std::move(query)]() -> ToolchainResult {
ToolchainResult result;
result.key = q.key;
@@ -337,7 +335,7 @@ et::task<> scan_impl(CompilationDatabase& cdb,
loop));
}
auto outcome = co_await et::when_all(std::move(tasks));
auto outcome = co_await kota::when_all(std::move(tasks));
if(outcome.has_value()) {
cdb.inject_results(*outcome);
} else {
@@ -390,7 +388,7 @@ et::task<> scan_impl(CompilationDatabase& cdb,
llvm::StringSet<> entries;
};
std::vector<et::task<DirEntry, et::error>> pending_dir_tasks;
std::vector<kota::task<DirEntry, kota::error>> pending_dir_tasks;
if(dir_cache.dirs.empty()) {
llvm::StringSet<> unique_dirs;
@@ -412,7 +410,7 @@ et::task<> scan_impl(CompilationDatabase& cdb,
pending_dir_tasks.reserve(unique_dirs.size());
for(auto& entry: unique_dirs) {
auto dir_path = entry.getKey().str();
pending_dir_tasks.push_back(et::queue(
pending_dir_tasks.push_back(kota::queue(
[dir_path = std::move(dir_path)]() -> DirEntry {
DirEntry result;
result.dir_path = dir_path;
@@ -463,7 +461,7 @@ et::task<> scan_impl(CompilationDatabase& cdb,
// queued for scanning on the thread pool. When wave N+1 starts,
// these tasks are already running (or finished), eliminating most
// of the Phase 1 wait time for subsequent waves.
std::vector<et::task<FileScanResult, et::error>> prefetch_tasks;
std::vector<kota::task<FileScanResult, kota::error>> prefetch_tasks;
// Pre-resolved search configs: built once after dir cache is populated,
// then reused for all waves. Eliminates StringMap lookups in Phase 2.
@@ -500,7 +498,7 @@ et::task<> scan_impl(CompilationDatabase& cdb,
if(!prefetch_tasks.empty()) {
// Waves 1+: await prefetched scan tasks from previous Phase 2.
auto scan_outcome = co_await et::when_all(std::move(prefetch_tasks));
auto scan_outcome = co_await kota::when_all(std::move(prefetch_tasks));
prefetch_tasks.clear();
if(scan_outcome.has_error()) {
LOG_ERROR("Prefetch scan failed: {}", scan_outcome.error().message());
@@ -514,7 +512,7 @@ et::task<> scan_impl(CompilationDatabase& cdb,
}
} else {
// Wave 0 (or warm run with all cache hits): create scan tasks now.
std::vector<et::task<FileScanResult, et::error>> scan_tasks;
std::vector<kota::task<FileScanResult, kota::error>> scan_tasks;
scan_tasks.reserve(current_wave.size());
for(auto& entry: current_wave) {
auto pid = entry.path_id;
@@ -525,8 +523,8 @@ et::task<> scan_impl(CompilationDatabase& cdb,
}
auto path = path_pool.resolve(pid).data();
scan_tasks.push_back(
et::queue([path, pid, cid]() { return scan_file_worker(path, pid, cid); },
loop));
kota::queue([path, pid, cid]() { return scan_file_worker(path, pid, cid); },
loop));
}
// Optimization 1: await dir cache tasks concurrently with scan tasks.
@@ -535,7 +533,7 @@ et::task<> scan_impl(CompilationDatabase& cdb,
// max(dir_time, scan_time) instead of dir_time + scan_time.
if(!pending_dir_tasks.empty()) {
auto dir_t0 = std::chrono::steady_clock::now();
auto dir_outcome = co_await et::when_all(std::move(pending_dir_tasks));
auto dir_outcome = co_await kota::when_all(std::move(pending_dir_tasks));
pending_dir_tasks.clear();
if(dir_outcome.has_value()) {
for(auto& entry: *dir_outcome) {
@@ -549,7 +547,7 @@ et::task<> scan_impl(CompilationDatabase& cdb,
}
if(!scan_tasks.empty()) {
auto scan_outcome = co_await et::when_all(std::move(scan_tasks));
auto scan_outcome = co_await kota::when_all(std::move(scan_tasks));
if(scan_outcome.has_error()) {
LOG_ERROR("Parallel scan failed: {}", scan_outcome.error().message());
break;
@@ -749,7 +747,7 @@ et::task<> scan_impl(CompilationDatabase& cdb,
if(!ext_cache ||
ext_cache->scan_results.find(inc_path_id) == ext_cache->scan_results.end()) {
auto inc_path = path_pool.resolve(inc_path_id).data();
prefetch_tasks.push_back(et::queue(
prefetch_tasks.push_back(kota::queue(
[inc_path, inc_path_id, cid = scan_result.config_id]() {
return scan_file_worker(inc_path, inc_path_id, cid);
},
@@ -827,7 +825,7 @@ ScanReport scan_dependency_graph(CompilationDatabase& cdb,
return report;
}
et::event_loop loop;
kota::event_loop loop;
loop.schedule(scan_impl(cdb, path_pool, graph, report, cache, loop));
loop.run();
return report;

View File

@@ -53,7 +53,8 @@ void Lexer::lex(Token& token) {
}
} else if(parse_pp_keyword) {
parse_pp_keyword = false;
parse_header_name = token.text(content) == "include";
auto kw = token.text(content);
parse_header_name = kw == "include" || kw == "include_next" || kw == "embed";
}
}
@@ -105,4 +106,60 @@ Token Lexer::advance_until(TokenKind kind) {
}
}
static bool is_directive_keyword(llvm::StringRef word) {
return word == "include" || word == "include_next" || word == "import" || word == "embed" ||
word == "__has_include" || word == "__has_include_next" || word == "__has_embed";
}
std::optional<LocalSourceRange> find_directive_argument(llvm::StringRef content,
std::uint32_t offset,
const clang::LangOptions* lang_opts) {
std::uint32_t line_start = 0;
if(auto nl = content.rfind('\n', offset); nl != llvm::StringRef::npos)
line_start = static_cast<std::uint32_t>(nl + 1);
auto line = content.substr(line_start);
Lexer lexer(line, true, lang_opts);
bool after_has_keyword = false;
bool ready = false;
while(true) {
auto tok = lexer.advance();
if(tok.is_eof() || tok.is_eod())
break;
auto abs_begin = line_start + tok.range.begin;
auto abs_end = line_start + tok.range.end;
if(tok.is_identifier()) {
auto text = tok.text(line);
if(text == "__has_include" || text == "__has_include_next" || text == "__has_embed") {
after_has_keyword = true;
continue;
}
if(text == "include" || text == "include_next" || text == "embed") {
ready = true;
continue;
}
}
if(tok.kind == clang::tok::l_paren && after_has_keyword) {
after_has_keyword = false;
ready = true;
lexer.set_header_name_mode();
continue;
}
if(abs_begin < offset || !ready)
continue;
if(tok.is_header_name() || tok.kind == clang::tok::string_literal)
return LocalSourceRange(abs_begin, abs_end);
if(tok.is_identifier())
return LocalSourceRange(abs_begin, abs_end);
}
return std::nullopt;
}
} // namespace clice

View File

@@ -51,6 +51,15 @@ public:
Token advance_until(TokenKind kind);
/// Force the lexer into header-name mode so the next token is lexed
/// via LexIncludeFilename (correctly handling both "..." and <...>).
/// Use this before lexing filename arguments in contexts like
/// __has_include() or __has_embed() where the lexer cannot detect
/// the mode automatically.
void set_header_name_mode() {
parse_header_name = true;
}
private:
bool ignore_end_of_directive = true;
bool parse_pp_keyword = false;
@@ -64,4 +73,13 @@ private:
std::unique_ptr<clang::Lexer> lexer;
};
/// Find the range of the filename argument in a preprocessor directive line.
/// `content` is the full source text, `offset` points at or before the directive keyword.
/// Returns the range of the first filename-like token (header name, string literal,
/// or macro identifier) found on the same line, or nullopt if none.
std::optional<LocalSourceRange>
find_directive_argument(llvm::StringRef content,
std::uint32_t offset,
const clang::LangOptions* lang_opts = nullptr);
} // namespace clice

View File

@@ -231,6 +231,14 @@ def _generate_test_data_cdbs(data_dir: Path) -> None:
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():

View File

@@ -0,0 +1 @@
0123456789

View File

@@ -0,0 +1,3 @@
#pragma once
int a = 1;

View File

@@ -0,0 +1,3 @@
#pragma once
int b = 2;

View File

@@ -0,0 +1,3 @@
#pragma once
int c = 3;

View 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;
}

View 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)

View File

@@ -9,7 +9,7 @@ namespace clice::testing {
namespace {
namespace protocol = eventide::ipc::protocol;
namespace protocol = kota::ipc::protocol;
TEST_SUITE(CodeCompletion) {
@@ -335,6 +335,44 @@ int z = fo$(pos)
*it->insert_text_format == protocol::InsertTextFormat::PlainText);
}
TEST_CASE(SmartParensSkip) {
// When next token after cursor is '(', snippet should not insert parens.
feature::CodeCompletionOptions opts;
opts.bundle_overloads = false;
opts.enable_function_arguments_snippet = true;
code_complete(R"cpp(
int foooo(int x, float y);
int z = fo$(pos)(1, 2.0f);
)cpp",
opts);
auto it = find_item("foooo");
ASSERT_TRUE(it != items.end());
// With parens already present, snippet should degrade to plain text
// (no placeholders → build_snippet returns empty → label used).
auto& edit = std::get<protocol::TextEdit>(*it->text_edit);
ASSERT_TRUE(edit.new_text.find("(") == std::string::npos);
}
TEST_CASE(SmartParensInsert) {
// When next token is NOT '(', snippet should include parens normally.
feature::CodeCompletionOptions opts;
opts.bundle_overloads = false;
opts.enable_function_arguments_snippet = true;
code_complete(R"cpp(
int foooo(int x, float y);
int z = fo$(pos);
)cpp",
opts);
auto it = find_item("foooo");
ASSERT_TRUE(it != items.end());
auto& edit = std::get<protocol::TextEdit>(*it->text_edit);
// Should contain '(' since there's no existing paren.
ASSERT_TRUE(edit.new_text.find("(") != std::string::npos);
ASSERT_TRUE(edit.new_text.find("${1:") != std::string::npos);
}
TEST_CASE(SnippetBundleMode) {
// In bundle mode, snippets should NOT be generated even if enabled.
feature::CodeCompletionOptions opts;

View File

@@ -9,15 +9,15 @@ namespace clice::testing {
namespace {
namespace protocol = eventide::ipc::protocol;
namespace protocol = kota::ipc::protocol;
TEST_SUITE(DocumentLink, Tester) {
std::vector<protocol::DocumentLink> links;
void run(llvm::StringRef source) {
void run(llvm::StringRef source, llvm::StringRef standard = "-std=c++17") {
add_files("main.cpp", source);
ASSERT_TRUE(compile());
ASSERT_TRUE(compile(standard));
links = feature::document_links(*unit, feature::PositionEncoding::UTF8);
}
@@ -89,6 +89,53 @@ TEST_CASE(HasInclude) {
EXPECT_LINK(1, "1", TestVFS::path("test.h"));
}
TEST_CASE(MacroInclude) {
run(R"cpp(
#[test.h]
#[main.cpp]
#define HEADER "test.h"
#include @0[HEADER$]
)cpp");
ASSERT_EQ(links.size(), 1U);
EXPECT_LINK(0, "0", TestVFS::path("test.h"));
}
TEST_CASE(Embed) {
run(R"cpp(
#[bytes.bin]
0123456789
#[main.cpp]
const char e[] = {
#embed @0["bytes.bin"$]
};
)cpp",
"-std=c++23");
ASSERT_EQ(links.size(), 1U);
EXPECT_LINK(0, "0", TestVFS::path("bytes.bin"));
}
TEST_CASE(HasEmbed) {
run(R"cpp(
#[data.bin]
ABCDE
#[main.cpp]
#if __has_embed(@0["data.bin"$])
#endif
#if __has_embed("non_existent.bin")
#endif
)cpp",
"-std=c++23");
ASSERT_EQ(links.size(), 1U);
EXPECT_LINK(0, "0", TestVFS::path("data.bin"));
}
}; // TEST_SUITE(DocumentLink)
} // namespace

View File

@@ -11,7 +11,7 @@ namespace clice::testing {
namespace {
namespace protocol = eventide::ipc::protocol;
namespace protocol = kota::ipc::protocol;
TEST_SUITE(DocumentSymbol, Tester) {

View File

@@ -9,7 +9,7 @@ namespace clice::testing {
namespace {
namespace protocol = eventide::ipc::protocol;
namespace protocol = kota::ipc::protocol;
TEST_SUITE(FoldingRange, Tester) {

View File

@@ -8,7 +8,7 @@ namespace clice::testing {
namespace {
namespace protocol = eventide::ipc::protocol;
namespace protocol = kota::ipc::protocol;
TEST_SUITE(Hover, Tester) {

View File

@@ -8,7 +8,7 @@ namespace clice::testing {
namespace {
namespace protocol = eventide::ipc::protocol;
namespace protocol = kota::ipc::protocol;
TEST_SUITE(InlayHint, Tester) {

View File

@@ -13,7 +13,7 @@ namespace clice::testing {
namespace {
namespace protocol = eventide::ipc::protocol;
namespace protocol = kota::ipc::protocol;
struct DecodedToken {
LocalSourceRange range;
@@ -423,6 +423,122 @@ cd*/
ASSERT_EQ(comments[1].length, 4);
}
TEST_CASE(ModuleDeclaration) {
add_main("main.cpp", R"cpp(
export @kw[module] @mod[foo];
)cpp");
ASSERT_TRUE(compile("-std=c++20"));
tokens = feature::semantic_tokens(*unit, feature::PositionEncoding::UTF8);
decoded = decode_utf8_tokens(unit->interested_content(), tokens);
EXPECT_TOKEN("kw", SymbolKind::Keyword);
EXPECT_TOKEN("mod", SymbolKind::Module);
}
TEST_CASE(ModuleDeclarationDotted) {
add_main("main.cpp", R"cpp(
export @kw[module] @m0[foo].@m1[bar];
)cpp");
ASSERT_TRUE(compile("-std=c++20"));
tokens = feature::semantic_tokens(*unit, feature::PositionEncoding::UTF8);
decoded = decode_utf8_tokens(unit->interested_content(), tokens);
EXPECT_TOKEN("kw", SymbolKind::Keyword);
EXPECT_TOKEN("m0", SymbolKind::Module);
EXPECT_TOKEN("m1", SymbolKind::Module);
}
TEST_CASE(ModuleImport) {
add_files("main.cpp", R"(
#[mod.cppm]
export module foo;
export int x = 42;
#[main.cpp]
@kw[import] @mod[foo];
int y = x;
)");
ASSERT_TRUE(compile_with_modules());
tokens = feature::semantic_tokens(*unit, feature::PositionEncoding::UTF8);
decoded = decode_utf8_tokens(unit->interested_content(), tokens);
EXPECT_TOKEN("kw", SymbolKind::Keyword);
EXPECT_TOKEN("mod", SymbolKind::Module);
}
TEST_CASE(ModulePartition) {
add_main("main.cpp", R"cpp(
export module @m0[foo]:@m1[bar];
)cpp");
ASSERT_TRUE(compile("-std=c++20"));
tokens = feature::semantic_tokens(*unit, feature::PositionEncoding::UTF8);
decoded = decode_utf8_tokens(unit->interested_content(), tokens);
EXPECT_TOKEN("m0", SymbolKind::Module);
EXPECT_TOKEN("m1", SymbolKind::Module);
}
TEST_CASE(ModuleReexport) {
add_files("main.cppm", R"(
#[mod.cppm]
export module foo;
export int x = 42;
#[main.cppm]
export module bar;
export @kw[import] @mod[foo];
)");
ASSERT_TRUE(compile_with_modules());
tokens = feature::semantic_tokens(*unit, feature::PositionEncoding::UTF8);
decoded = decode_utf8_tokens(unit->interested_content(), tokens);
EXPECT_TOKEN("kw", SymbolKind::Keyword);
EXPECT_TOKEN("mod", SymbolKind::Module);
}
TEST_CASE(GlobalModuleFragment) {
add_main("main.cpp", R"cpp(
module;
export module @mod[foo];
)cpp");
ASSERT_TRUE(compile("-std=c++20"));
tokens = feature::semantic_tokens(*unit, feature::PositionEncoding::UTF8);
decoded = decode_utf8_tokens(unit->interested_content(), tokens);
EXPECT_TOKEN("mod", SymbolKind::Module);
}
TEST_CASE(PrivateModuleFragment) {
add_main("main.cpp", R"cpp(
export module @mod[foo];
module :private;
int x = 1;
)cpp");
ASSERT_TRUE(compile("-std=c++20"));
tokens = feature::semantic_tokens(*unit, feature::PositionEncoding::UTF8);
decoded = decode_utf8_tokens(unit->interested_content(), tokens);
EXPECT_TOKEN("mod", SymbolKind::Module);
}
TEST_CASE(ModuleKeywordAsIdentifier) {
run_utf8(R"cpp(
void f() {
struct @s0[module] {};
@s1[module] @v0[m];
int @v1[import] = 1;
int @v2[module] = 2;
}
)cpp");
auto definition = modifier_mask({SymbolModifiers::Definition});
EXPECT_TOKEN("s0", SymbolKind::Struct, definition);
EXPECT_TOKEN("s1", SymbolKind::Struct);
EXPECT_TOKEN("v0", SymbolKind::Variable, definition);
EXPECT_TOKEN("v1", SymbolKind::Variable, definition);
EXPECT_TOKEN("v2", SymbolKind::Variable, definition);
}
}; // TEST_SUITE(SemanticTokens)
} // namespace

View File

@@ -6,7 +6,7 @@ namespace clice::testing {
namespace {
namespace protocol = eventide::ipc::protocol;
namespace protocol = kota::ipc::protocol;
TEST_SUITE(SignatureHelp, Tester) {

View File

@@ -11,8 +11,6 @@
namespace clice::testing {
namespace {
namespace et = eventide;
/// Build a dispatch_fn that compiles PCMs in-process (no workers).
/// Clang requires ALL transitive PCM deps (not just direct imports)
/// in PrebuiltModuleFiles, so we pass every available PCM.
@@ -20,7 +18,7 @@ CompileGraph::dispatch_fn make_dispatch(CompilationDatabase& cdb,
PathPool& pool,
DependencyGraph& graph,
llvm::DenseMap<std::uint32_t, std::string>& pcm_paths) {
return [&](std::uint32_t path_id) -> et::task<bool> {
return [&](std::uint32_t path_id) -> kota::task<bool> {
auto file_path = pool.resolve(path_id);
auto results = cdb.lookup(file_path, {.query_toolchain = true, .suppress_logging = true});
if(results.empty()) {
@@ -123,8 +121,8 @@ TEST_CASE(SingleModuleNoDeps) {
CompileGraph cg(make_dispatch(env.cdb, env.pool, env.graph, env.pcm_paths),
make_resolver(env.cdb, env.pool, env.graph));
et::event_loop loop;
auto test = [this, &cg, &env, pid_a]() -> et::task<> {
kota::event_loop loop;
auto test = [this, &cg, &env, pid_a]() -> kota::task<> {
auto result = co_await cg.compile(pid_a).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(*result);
@@ -157,8 +155,8 @@ TEST_CASE(ChainedModules) {
CompileGraph cg(make_dispatch(env.cdb, env.pool, env.graph, env.pcm_paths),
make_resolver(env.cdb, env.pool, env.graph));
et::event_loop loop;
auto test = [this, &cg, &env, pid_a, pid_b]() -> et::task<> {
kota::event_loop loop;
auto test = [this, &cg, &env, pid_a, pid_b]() -> kota::task<> {
auto result = co_await cg.compile(pid_b).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(*result);
@@ -202,8 +200,8 @@ TEST_CASE(DiamondModules) {
CompileGraph cg(make_dispatch(env.cdb, env.pool, env.graph, env.pcm_paths),
make_resolver(env.cdb, env.pool, env.graph));
et::event_loop loop;
auto test = [this, &cg, &env, pid_top]() -> et::task<> {
kota::event_loop loop;
auto test = [this, &cg, &env, pid_top]() -> kota::task<> {
auto result = co_await cg.compile(pid_top).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(*result);
@@ -238,8 +236,8 @@ TEST_CASE(DottedModuleName) {
CompileGraph cg(make_dispatch(env.cdb, env.pool, env.graph, env.pcm_paths),
make_resolver(env.cdb, env.pool, env.graph));
et::event_loop loop;
auto test = [this, &cg, &env, pid_app]() -> et::task<> {
kota::event_loop loop;
auto test = [this, &cg, &env, pid_app]() -> kota::task<> {
auto result = co_await cg.compile(pid_app).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(*result);
@@ -280,8 +278,8 @@ TEST_CASE(ReExport) {
CompileGraph cg(make_dispatch(env.cdb, env.pool, env.graph, env.pcm_paths),
make_resolver(env.cdb, env.pool, env.graph));
et::event_loop loop;
auto test = [this, &cg, &env, pid_user]() -> et::task<> {
kota::event_loop loop;
auto test = [this, &cg, &env, pid_user]() -> kota::task<> {
auto result = co_await cg.compile(pid_user).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(*result);
@@ -324,8 +322,8 @@ TEST_CASE(ExportBlock) {
CompileGraph cg(make_dispatch(env.cdb, env.pool, env.graph, env.pcm_paths),
make_resolver(env.cdb, env.pool, env.graph));
et::event_loop loop;
auto test = [this, &cg, &env, pid]() -> et::task<> {
kota::event_loop loop;
auto test = [this, &cg, &env, pid]() -> kota::task<> {
auto result = co_await cg.compile(pid).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(*result);
@@ -360,8 +358,8 @@ TEST_CASE(GlobalModuleFragment) {
CompileGraph cg(make_dispatch(env.cdb, env.pool, env.graph, env.pcm_paths),
make_resolver(env.cdb, env.pool, env.graph));
et::event_loop loop;
auto test = [this, &cg, &env, pid]() -> et::task<> {
kota::event_loop loop;
auto test = [this, &cg, &env, pid]() -> kota::task<> {
auto result = co_await cg.compile(pid).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(*result);
@@ -396,8 +394,8 @@ TEST_CASE(PrivateModuleFragment) {
CompileGraph cg(make_dispatch(env.cdb, env.pool, env.graph, env.pcm_paths),
make_resolver(env.cdb, env.pool, env.graph));
et::event_loop loop;
auto test = [this, &cg, &env, pid]() -> et::task<> {
kota::event_loop loop;
auto test = [this, &cg, &env, pid]() -> kota::task<> {
auto result = co_await cg.compile(pid).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(*result);
@@ -436,8 +434,8 @@ TEST_CASE(PartitionInterface) {
CompileGraph cg(make_dispatch(env.cdb, env.pool, env.graph, env.pcm_paths),
make_resolver(env.cdb, env.pool, env.graph));
et::event_loop loop;
auto test = [this, &cg, &env, pid_m]() -> et::task<> {
kota::event_loop loop;
auto test = [this, &cg, &env, pid_m]() -> kota::task<> {
auto result = co_await cg.compile(pid_m).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(*result);
@@ -476,8 +474,8 @@ TEST_CASE(MultiplePartitions) {
CompileGraph cg(make_dispatch(env.cdb, env.pool, env.graph, env.pcm_paths),
make_resolver(env.cdb, env.pool, env.graph));
et::event_loop loop;
auto test = [this, &cg, &env, pid_lib]() -> et::task<> {
kota::event_loop loop;
auto test = [this, &cg, &env, pid_lib]() -> kota::task<> {
auto result = co_await cg.compile(pid_lib).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(*result);
@@ -519,8 +517,8 @@ TEST_CASE(PartitionChain) {
CompileGraph cg(make_dispatch(env.cdb, env.pool, env.graph, env.pcm_paths),
make_resolver(env.cdb, env.pool, env.graph));
et::event_loop loop;
auto test = [this, &cg, &env, pid_sys]() -> et::task<> {
kota::event_loop loop;
auto test = [this, &cg, &env, pid_sys]() -> kota::task<> {
auto result = co_await cg.compile(pid_sys).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(*result);
@@ -561,8 +559,8 @@ TEST_CASE(ExportNamespace) {
CompileGraph cg(make_dispatch(env.cdb, env.pool, env.graph, env.pcm_paths),
make_resolver(env.cdb, env.pool, env.graph));
et::event_loop loop;
auto test = [this, &cg, &env, pid]() -> et::task<> {
kota::event_loop loop;
auto test = [this, &cg, &env, pid]() -> kota::task<> {
auto result = co_await cg.compile(pid).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(*result);
@@ -600,8 +598,8 @@ TEST_CASE(GMFWithImport) {
CompileGraph cg(make_dispatch(env.cdb, env.pool, env.graph, env.pcm_paths),
make_resolver(env.cdb, env.pool, env.graph));
et::event_loop loop;
auto test = [this, &cg, &env, pid]() -> et::task<> {
kota::event_loop loop;
auto test = [this, &cg, &env, pid]() -> kota::task<> {
auto result = co_await cg.compile(pid).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(*result);
@@ -651,8 +649,8 @@ TEST_CASE(DeepChain) {
CompileGraph cg(make_dispatch(env.cdb, env.pool, env.graph, env.pcm_paths),
make_resolver(env.cdb, env.pool, env.graph));
et::event_loop loop;
auto test = [this, &cg, &env, pid]() -> et::task<> {
kota::event_loop loop;
auto test = [this, &cg, &env, pid]() -> kota::task<> {
auto result = co_await cg.compile(pid).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(*result);
@@ -686,8 +684,8 @@ TEST_CASE(IndependentModules) {
CompileGraph cg(make_dispatch(env.cdb, env.pool, env.graph, env.pcm_paths),
make_resolver(env.cdb, env.pool, env.graph));
et::event_loop loop;
auto test = [this, &cg, &env, pid_x, pid_y]() -> et::task<> {
kota::event_loop loop;
auto test = [this, &cg, &env, pid_x, pid_y]() -> kota::task<> {
auto r1 = co_await cg.compile(pid_x).catch_cancel();
EXPECT_TRUE(r1.has_value() && *r1);
auto r2 = co_await cg.compile(pid_y).catch_cancel();
@@ -728,8 +726,8 @@ TEST_CASE(TemplateExport) {
CompileGraph cg(make_dispatch(env.cdb, env.pool, env.graph, env.pcm_paths),
make_resolver(env.cdb, env.pool, env.graph));
et::event_loop loop;
auto test = [this, &cg, &env, pid]() -> et::task<> {
kota::event_loop loop;
auto test = [this, &cg, &env, pid]() -> kota::task<> {
auto result = co_await cg.compile(pid).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(*result);
@@ -775,8 +773,8 @@ TEST_CASE(ClassExportAndInheritance) {
CompileGraph cg(make_dispatch(env.cdb, env.pool, env.graph, env.pcm_paths),
make_resolver(env.cdb, env.pool, env.graph));
et::event_loop loop;
auto test = [this, &cg, &env, pid]() -> et::task<> {
kota::event_loop loop;
auto test = [this, &cg, &env, pid]() -> kota::task<> {
auto result = co_await cg.compile(pid).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(*result);
@@ -813,8 +811,8 @@ TEST_CASE(RecompileAfterUpdate) {
CompileGraph cg(make_dispatch(env.cdb, env.pool, env.graph, env.pcm_paths),
make_resolver(env.cdb, env.pool, env.graph));
et::event_loop loop;
auto test = [this, &cg, &env, pid_leaf, pid_mid]() -> et::task<> {
kota::event_loop loop;
auto test = [this, &cg, &env, pid_leaf, pid_mid]() -> kota::task<> {
// First compile.
auto r1 = co_await cg.compile(pid_mid).catch_cancel();
EXPECT_TRUE(r1.has_value() && *r1);
@@ -864,8 +862,8 @@ TEST_CASE(PartitionWithGMF) {
CompileGraph cg(make_dispatch(env.cdb, env.pool, env.graph, env.pcm_paths),
make_resolver(env.cdb, env.pool, env.graph));
et::event_loop loop;
auto test = [this, &cg, &env, pid]() -> et::task<> {
kota::event_loop loop;
auto test = [this, &cg, &env, pid]() -> kota::task<> {
auto result = co_await cg.compile(pid).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(*result);
@@ -905,8 +903,8 @@ TEST_CASE(PartitionWithExternalImport) {
CompileGraph cg(make_dispatch(env.cdb, env.pool, env.graph, env.pcm_paths),
make_resolver(env.cdb, env.pool, env.graph));
et::event_loop loop;
auto test = [this, &cg, &env, pid_app]() -> et::task<> {
kota::event_loop loop;
auto test = [this, &cg, &env, pid_app]() -> kota::task<> {
auto result = co_await cg.compile(pid_app).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(*result);
@@ -958,8 +956,8 @@ TEST_CASE(DiamondUpdateCascade) {
CompileGraph cg(make_dispatch(env.cdb, env.pool, env.graph, env.pcm_paths),
make_resolver(env.cdb, env.pool, env.graph));
et::event_loop loop;
auto test = [this, &cg, &env, pid_base, pid_left, pid_right, pid_top]() -> et::task<> {
kota::event_loop loop;
auto test = [this, &cg, &env, pid_base, pid_left, pid_right, pid_top]() -> kota::task<> {
// Initial compile.
auto r1 = co_await cg.compile(pid_top).catch_cancel();
EXPECT_TRUE(r1.has_value() && *r1);
@@ -1046,8 +1044,8 @@ TEST_CASE(ReResolveAfterUpdate) {
CompileGraph cg(make_dispatch(env.cdb, env.pool, env.graph, env.pcm_paths),
std::move(counting_resolver));
et::event_loop loop;
auto test = [this, &cg, &env, &resolve_count, pid_mid]() -> et::task<> {
kota::event_loop loop;
auto test = [this, &cg, &env, &resolve_count, pid_mid]() -> kota::task<> {
// First compile: resolve_fn called once for Mid.
auto r1 = co_await cg.compile(pid_mid).catch_cancel();
EXPECT_TRUE(r1.has_value() && *r1);
@@ -1092,8 +1090,8 @@ TEST_CASE(CompileFailurePropagation) {
CompileGraph cg(make_dispatch(env.cdb, env.pool, env.graph, env.pcm_paths),
make_resolver(env.cdb, env.pool, env.graph));
et::event_loop loop;
auto test = [this, &cg, &env, pid_bad]() -> et::task<> {
kota::event_loop loop;
auto test = [this, &cg, &env, pid_bad]() -> kota::task<> {
auto result = co_await cg.compile(pid_bad).catch_cancel();
EXPECT_TRUE(result.has_value());
// Compilation should fail due to undefined symbol.
@@ -1133,8 +1131,8 @@ TEST_CASE(ModuleImplementationUnit) {
CompileGraph cg(make_dispatch(env.cdb, env.pool, env.graph, env.pcm_paths),
make_resolver(env.cdb, env.pool, env.graph));
et::event_loop loop;
auto test = [this, &cg, &env, pid_iface]() -> et::task<> {
kota::event_loop loop;
auto test = [this, &cg, &env, pid_iface]() -> kota::task<> {
// Build the interface PCM via CompileGraph.
auto r1 = co_await cg.compile(pid_iface).catch_cancel();
EXPECT_TRUE(r1.has_value() && *r1);

View File

@@ -6,7 +6,6 @@
namespace clice::testing {
namespace {
namespace et = eventide;
namespace ranges = std::ranges;
/// A resolve_fn that always returns no dependencies.
@@ -29,27 +28,27 @@ CompileGraph::resolve_fn
}
CompileGraph::dispatch_fn instant_dispatch() {
return [](std::uint32_t) -> et::task<bool> {
return [](std::uint32_t) -> kota::task<bool> {
co_return true;
};
}
CompileGraph::dispatch_fn tracking_dispatch(std::vector<std::uint32_t>& compiled) {
return [&compiled](std::uint32_t path_id) -> et::task<bool> {
return [&compiled](std::uint32_t path_id) -> kota::task<bool> {
compiled.push_back(path_id);
co_return true;
};
}
CompileGraph::dispatch_fn failing_dispatch() {
return [](std::uint32_t) -> et::task<bool> {
return [](std::uint32_t) -> kota::task<bool> {
co_return false;
};
}
/// Dispatch that fails only for specific path_ids.
CompileGraph::dispatch_fn selective_dispatch(llvm::DenseSet<std::uint32_t> fail_ids) {
return [fail_ids = std::move(fail_ids)](std::uint32_t path_id) -> et::task<bool> {
return [fail_ids = std::move(fail_ids)](std::uint32_t path_id) -> kota::task<bool> {
co_return !fail_ids.contains(path_id);
};
}
@@ -61,7 +60,7 @@ std::optional<CompileGraph> graph;
template <typename F>
void execute(F&& fn) {
et::event_loop loop;
kota::event_loop loop;
auto t = fn();
loop.schedule(t);
loop.run();
@@ -70,7 +69,7 @@ void execute(F&& fn) {
TEST_CASE(CompileNoDeps) {
graph.emplace(tracking_dispatch(compiled), no_deps());
execute([&]() -> et::task<> {
execute([&]() -> kota::task<> {
auto result = co_await graph->compile(1).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(*result);
@@ -87,7 +86,7 @@ TEST_CASE(CompileWithDependency) {
{1, {2}}
}));
execute([&]() -> et::task<> {
execute([&]() -> kota::task<> {
auto result = co_await graph->compile(1).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(*result);
@@ -109,7 +108,7 @@ TEST_CASE(CompileChain) {
{2, {3}}
}));
execute([&]() -> et::task<> {
execute([&]() -> kota::task<> {
auto result = co_await graph->compile(1).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(*result);
@@ -132,7 +131,7 @@ TEST_CASE(DiamondDependency) {
{3, {4} }
}));
execute([&]() -> et::task<> {
execute([&]() -> kota::task<> {
auto result = co_await graph->compile(1).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(*result);
@@ -152,7 +151,7 @@ TEST_CASE(UpdateInvalidates) {
{1, {2}}
}));
execute([&]() -> et::task<> {
execute([&]() -> kota::task<> {
co_await graph->compile(1).catch_cancel();
EXPECT_FALSE(graph->is_dirty(2));
EXPECT_FALSE(graph->is_dirty(1));
@@ -172,7 +171,7 @@ TEST_CASE(UpdateCascade) {
{2, {3}}
}));
execute([&]() -> et::task<> {
execute([&]() -> kota::task<> {
co_await graph->compile(1).catch_cancel();
EXPECT_FALSE(graph->is_dirty(2));
EXPECT_FALSE(graph->is_dirty(3));
@@ -192,7 +191,7 @@ TEST_CASE(CompileAfterUpdate) {
{1, {2}}
}));
execute([&]() -> et::task<> {
execute([&]() -> kota::task<> {
co_await graph->compile(1).catch_cancel();
EXPECT_EQ(compiled.size(), 2u);
@@ -210,7 +209,7 @@ TEST_CASE(DispatchFailure) {
{1, {2}}
}));
execute([&]() -> et::task<> {
execute([&]() -> kota::task<> {
auto result = co_await graph->compile(1).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_FALSE(*result);
@@ -228,7 +227,7 @@ TEST_CASE(CancelAll) {
TEST_CASE(SecondCompileSkips) {
graph.emplace(tracking_dispatch(compiled), no_deps());
execute([&]() -> et::task<> {
execute([&]() -> kota::task<> {
co_await graph->compile(1).catch_cancel();
EXPECT_EQ(compiled.size(), 1u);
// Second compile should skip (already clean).
@@ -245,7 +244,7 @@ TEST_CASE(CascadeThroughAlreadyDirty) {
{2, {3}}
}));
execute([&]() -> et::task<> {
execute([&]() -> kota::task<> {
co_await graph->compile(1).catch_cancel();
// Update node 2: marks 2 and 1 dirty.
@@ -270,7 +269,7 @@ TEST_CASE(CircularDependencyDetection) {
{2, {1}}
}));
execute([&]() -> et::task<> {
execute([&]() -> kota::task<> {
auto result = co_await graph->compile(1).catch_cancel();
// Should return false (cycle detected), not deadlock.
EXPECT_TRUE(result.has_value());
@@ -289,7 +288,7 @@ TEST_CASE(CrossBranchCycleDetection) {
{3, {2} }
}));
execute([&]() -> et::task<> {
execute([&]() -> kota::task<> {
auto result = co_await graph->compile(1).catch_cancel();
// Should return false (cycle detected), not deadlock.
EXPECT_TRUE(result.has_value());
@@ -312,7 +311,7 @@ TEST_CASE(UpdateResetsResolved) {
graph.emplace(tracking_dispatch(compiled), std::move(resolver));
execute([&]() -> et::task<> {
execute([&]() -> kota::task<> {
// First compile: resolves 1 -> {2}.
co_await graph->compile(1).catch_cancel();
EXPECT_EQ(resolve_count, 1);
@@ -344,7 +343,7 @@ TEST_CASE(UpdateCleansBackEdges) {
graph.emplace(tracking_dispatch(compiled), std::move(resolver));
execute([&]() -> et::task<> {
execute([&]() -> kota::task<> {
// First compile: 1 -> {2}.
co_await graph->compile(1).catch_cancel();
EXPECT_FALSE(graph->is_dirty(1));
@@ -373,7 +372,7 @@ TEST_CASE(DiamondUpdateCascade) {
{3, {4} }
}));
execute([&]() -> et::task<> {
execute([&]() -> kota::task<> {
co_await graph->compile(1).catch_cancel();
EXPECT_FALSE(graph->is_dirty(1));
EXPECT_FALSE(graph->is_dirty(4));
@@ -402,7 +401,7 @@ TEST_CASE(UpdateReturnsAllDirtied) {
{2, {3}}
}));
execute([&]() -> et::task<> {
execute([&]() -> kota::task<> {
co_await graph->compile(1).catch_cancel();
auto dirtied = graph->update(3);
@@ -417,7 +416,7 @@ TEST_CASE(UpdateReturnsAllDirtied) {
TEST_CASE(HasUnitAndIsCompiling) {
graph.emplace(instant_dispatch(), no_deps());
execute([&]() -> et::task<> {
execute([&]() -> kota::task<> {
EXPECT_FALSE(graph->has_unit(1));
EXPECT_FALSE(graph->is_compiling(1));
@@ -434,7 +433,7 @@ TEST_CASE(FailureLeavesDepsDirty) {
{1, {2}}
}));
execute([&]() -> et::task<> {
execute([&]() -> kota::task<> {
auto result = co_await graph->compile(1).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_FALSE(*result);
@@ -451,7 +450,7 @@ TEST_CASE(SelfLoop) {
{1, {1}}
}));
execute([&]() -> et::task<> {
execute([&]() -> kota::task<> {
auto result = co_await graph->compile(1).catch_cancel();
// Should detect cycle and return false, not deadlock.
EXPECT_TRUE(result.has_value());
@@ -465,7 +464,7 @@ TEST_CASE(CancelAllAndRecompile) {
{1, {2}}
}));
execute([&]() -> et::task<> {
execute([&]() -> kota::task<> {
co_await graph->compile(1).catch_cancel();
EXPECT_EQ(compiled.size(), 2u);
EXPECT_FALSE(graph->is_dirty(1));
@@ -488,10 +487,10 @@ TEST_CASE(CancelAllAndRecompile) {
}
TEST_CASE(UpdateDuringCompile) {
et::event_loop loop;
et::event gate;
kota::event_loop loop;
kota::event gate;
auto gated_dispatch = [&gate](std::uint32_t) -> et::task<bool> {
auto gated_dispatch = [&gate](std::uint32_t) -> kota::task<bool> {
co_await gate.wait();
co_return true;
};
@@ -502,14 +501,14 @@ TEST_CASE(UpdateDuringCompile) {
bool was_cancelled = false;
// Coroutine 1: compile(1), will suspend inside dispatch waiting on gate.
auto compiler = [&]() -> et::task<> {
auto compiler = [&]() -> kota::task<> {
auto result = co_await graph->compile(1).catch_cancel();
compile_done = true;
was_cancelled = !result.has_value();
};
// Coroutine 2: update(1) while dispatch is in flight, then unblock gate.
auto updater = [&]() -> et::task<> {
auto updater = [&]() -> kota::task<> {
graph->update(1);
gate.set();
co_return;
@@ -534,7 +533,7 @@ TEST_CASE(WhenAllPartialFailure) {
}),
static_resolver({{1, {2, 3}}}));
execute([&]() -> et::task<> {
execute([&]() -> kota::task<> {
auto result = co_await graph->compile(1).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_FALSE(*result);
@@ -566,7 +565,7 @@ TEST_CASE(EmptyGraphNoCompile) {
TEST_CASE(CompileDepsNoDeps) {
graph.emplace(tracking_dispatch(compiled), no_deps());
execute([&]() -> et::task<> {
execute([&]() -> kota::task<> {
auto result = co_await graph->compile_deps(1).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(*result);
@@ -582,7 +581,7 @@ TEST_CASE(CompileDepsWithDependency) {
{1, {2}}
}));
execute([&]() -> et::task<> {
execute([&]() -> kota::task<> {
auto result = co_await graph->compile_deps(1).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(*result);
@@ -602,7 +601,7 @@ TEST_CASE(CompileDepsChain) {
{2, {3}}
}));
execute([&]() -> et::task<> {
execute([&]() -> kota::task<> {
auto result = co_await graph->compile_deps(1).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(*result);
@@ -623,7 +622,7 @@ TEST_CASE(CompileDepsDiamond) {
{3, {4} }
}));
execute([&]() -> et::task<> {
execute([&]() -> kota::task<> {
auto result = co_await graph->compile_deps(1).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(*result);
@@ -640,7 +639,7 @@ TEST_CASE(CompileDepsDiamond) {
TEST_CASE(CompileDepsFailure) {
// 1 -> 2. Dispatch fails for unit 2.
auto fail_and_track = [&](std::uint32_t path_id) -> et::task<bool> {
auto fail_and_track = [&](std::uint32_t path_id) -> kota::task<bool> {
compiled.push_back(path_id);
co_return false;
};
@@ -650,7 +649,7 @@ TEST_CASE(CompileDepsFailure) {
{1, {2}}
}));
execute([&]() -> et::task<> {
execute([&]() -> kota::task<> {
auto result = co_await graph->compile_deps(1).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_FALSE(*result);
@@ -666,7 +665,7 @@ TEST_CASE(CompileDepsPlainCpp) {
{10, {20}}
}));
execute([&]() -> et::task<> {
execute([&]() -> kota::task<> {
auto result = co_await graph->compile_deps(10).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(*result);
@@ -688,11 +687,11 @@ TEST_CASE(CompileDepsConcurrentDedup) {
{2, {3, 5}},
}));
execute([&]() -> et::task<> {
execute([&]() -> kota::task<> {
// Launch both compile_deps concurrently.
auto t1 = graph->compile_deps(1);
auto t2 = graph->compile_deps(2);
auto results = co_await et::when_all(std::move(t1), std::move(t2));
auto results = co_await kota::when_all(std::move(t1), std::move(t2));
auto [r1, r2] = results;
EXPECT_TRUE(r1);
@@ -722,10 +721,10 @@ TEST_CASE(CompileDepsResolveOnce) {
graph.emplace(tracking_dispatch(compiled), std::move(resolve));
execute([&]() -> et::task<> {
execute([&]() -> kota::task<> {
auto t1 = graph->compile_deps(1);
auto t2 = graph->compile_deps(2);
auto results = co_await et::when_all(std::move(t1), std::move(t2));
auto results = co_await kota::when_all(std::move(t1), std::move(t2));
auto [r1, r2] = results;
EXPECT_TRUE(r1);

View File

@@ -9,8 +9,6 @@ namespace clice::testing {
namespace {
namespace et = eventide;
// ============================================================================
// End-to-end module compilation through real workers:
// 1. Stateless worker builds PCM for module interface
@@ -38,7 +36,7 @@ TEST_CASE(BuildPCMThenCompileWithImport) {
std::string pcm_path;
bool phase1_done = false;
sl.run([&]() -> et::task<> {
sl.run([&]() -> kota::task<> {
worker::BuildParams params;
params.kind = worker::BuildKind::BuildPCM;
params.file = iface;
@@ -71,7 +69,7 @@ TEST_CASE(BuildPCMThenCompileWithImport) {
bool phase2_done = false;
sf.run([&]() -> et::task<> {
sf.run([&]() -> kota::task<> {
worker::CompileParams params;
params.path = consumer;
params.version = 1;
@@ -123,7 +121,7 @@ TEST_CASE(BuildPCMChainThenCompile) {
std::string pcm_a, pcm_b;
bool pcm_done = false;
sl.run([&]() -> et::task<> {
sl.run([&]() -> kota::task<> {
// Build PCM for A first.
{
worker::BuildParams params;
@@ -179,7 +177,7 @@ TEST_CASE(BuildPCMChainThenCompile) {
bool compile_done = false;
sf.run([&]() -> et::task<> {
sf.run([&]() -> kota::task<> {
worker::CompileParams params;
params.path = consumer;
params.version = 1;
@@ -227,7 +225,7 @@ TEST_CASE(ModuleImplementationUnitWithWorker) {
std::string pcm_path;
bool pcm_done = false;
sl.run([&]() -> et::task<> {
sl.run([&]() -> kota::task<> {
worker::BuildParams params;
params.kind = worker::BuildKind::BuildPCM;
params.file = iface;
@@ -257,7 +255,7 @@ TEST_CASE(ModuleImplementationUnitWithWorker) {
bool compile_done = false;
sf.run([&]() -> et::task<> {
sf.run([&]() -> kota::task<> {
worker::CompileParams params;
params.path = impl;
params.version = 1;

View File

@@ -10,8 +10,6 @@ namespace clice::testing {
namespace {
namespace et = eventide;
// ============================================================================
// End-to-end PCH compilation through real workers:
// 1. Stateless worker builds PCH for preamble headers
@@ -39,7 +37,7 @@ TEST_CASE(BuildPCHThenCompile) {
std::string pch_path;
bool phase1_done = false;
sl.run([&]() -> et::task<> {
sl.run([&]() -> kota::task<> {
worker::BuildParams params;
params.kind = worker::BuildKind::BuildPCH;
params.file = main_file;
@@ -79,7 +77,7 @@ TEST_CASE(BuildPCHThenCompile) {
auto preamble_bound = compute_preamble_bound(main_text);
sf.run([&]() -> et::task<> {
sf.run([&]() -> kota::task<> {
worker::CompileParams params;
params.path = main_file;
params.version = 1;
@@ -123,7 +121,7 @@ TEST_CASE(CompileWithoutPCHStillWorks) {
bool compile_done = false;
sf.run([&]() -> et::task<> {
sf.run([&]() -> kota::task<> {
worker::CompileParams params;
params.path = main_file;
params.version = 1;

View File

@@ -2,16 +2,15 @@
#include <vector>
#include "test/test.h"
#include "eventide/serde/serde/raw_value.h"
#include "server/protocol.h"
#include "server/worker_test_helpers.h"
#include "kota/codec/raw_value.h"
namespace clice::testing {
namespace {
namespace et = eventide;
TEST_SUITE(StatefulWorker) {
TEST_CASE(SpawnAndExit) {
@@ -33,7 +32,7 @@ TEST_CASE(CompileRequest) {
bool test_done = false;
w.run([&]() -> et::task<> {
w.run([&]() -> kota::task<> {
worker::CompileParams params;
params.path = src;
params.version = 1;
@@ -59,7 +58,7 @@ TEST_CASE(HoverWithoutCompile) {
bool test_done = false;
w.run([&]() -> et::task<> {
w.run([&]() -> kota::task<> {
// Hover on a file that hasn't been compiled should return null.
worker::QueryParams params;
params.kind = worker::QueryKind::Hover;
@@ -88,7 +87,7 @@ TEST_CASE(CompileThenHover) {
bool test_done = false;
w.run([&]() -> et::task<> {
w.run([&]() -> kota::task<> {
// First compile
worker::CompileParams cp;
cp.path = src;
@@ -129,7 +128,7 @@ TEST_CASE(DocumentUpdate) {
bool test_done = false;
w.run([&]() -> et::task<> {
w.run([&]() -> kota::task<> {
// Compile first
worker::CompileParams cp;
cp.path = src;
@@ -170,7 +169,7 @@ TEST_CASE(CodeActionReturnsEmpty) {
bool test_done = false;
w.run([&]() -> et::task<> {
w.run([&]() -> kota::task<> {
worker::QueryParams params;
params.kind = worker::QueryKind::CodeAction;
params.path = "/tmp/test.cpp";
@@ -192,7 +191,7 @@ TEST_CASE(GoToDefinitionReturnsEmpty) {
bool test_done = false;
w.run([&]() -> et::task<> {
w.run([&]() -> kota::task<> {
worker::QueryParams params;
params.kind = worker::QueryKind::GoToDefinition;
params.path = "/tmp/test.cpp";
@@ -215,7 +214,7 @@ TEST_CASE(SemanticTokensWithoutCompile) {
bool test_done = false;
w.run([&]() -> et::task<> {
w.run([&]() -> kota::task<> {
worker::QueryParams params;
params.kind = worker::QueryKind::SemanticTokens;
params.path = "/tmp/nonexistent.cpp";
@@ -236,7 +235,7 @@ TEST_CASE(FoldingRangeWithoutCompile) {
bool test_done = false;
w.run([&]() -> et::task<> {
w.run([&]() -> kota::task<> {
worker::QueryParams params;
params.kind = worker::QueryKind::FoldingRange;
params.path = "/tmp/nonexistent.cpp";
@@ -257,7 +256,7 @@ TEST_CASE(DocumentSymbolWithoutCompile) {
bool test_done = false;
w.run([&]() -> et::task<> {
w.run([&]() -> kota::task<> {
worker::QueryParams params;
params.kind = worker::QueryKind::DocumentSymbol;
params.path = "/tmp/nonexistent.cpp";
@@ -278,7 +277,7 @@ TEST_CASE(DocumentLinkWithoutCompile) {
bool test_done = false;
w.run([&]() -> et::task<> {
w.run([&]() -> kota::task<> {
worker::QueryParams params;
params.kind = worker::QueryKind::DocumentLink;
params.path = "/tmp/nonexistent.cpp";
@@ -299,7 +298,7 @@ TEST_CASE(InlayHintsWithoutCompile) {
bool test_done = false;
w.run([&]() -> et::task<> {
w.run([&]() -> kota::task<> {
worker::QueryParams params;
params.kind = worker::QueryKind::InlayHints;
params.path = "/tmp/nonexistent.cpp";
@@ -330,7 +329,7 @@ TEST_CASE(MultipleSequentialRequests) {
bool test_done = false;
w.run([&]() -> et::task<> {
w.run([&]() -> kota::task<> {
// Compile first so feature requests return real data.
worker::CompileParams cp;
cp.path = src;
@@ -402,7 +401,7 @@ TEST_CASE(MultipleDocuments) {
bool test_done = false;
w.run([&]() -> et::task<> {
w.run([&]() -> kota::task<> {
// Compile 3 different documents.
for(int i = 0; i < 3; i++) {
worker::CompileParams cp;
@@ -440,7 +439,7 @@ TEST_CASE(EvictNotification) {
bool test_done = false;
w.run([&]() -> et::task<> {
w.run([&]() -> kota::task<> {
// Send an evict notification — worker should remove the document without crashing.
worker::EvictParams ep;
ep.path = "/tmp/evict_test.cpp";
@@ -474,7 +473,7 @@ TEST_CASE(SpawnWithMemoryLimit) {
bool test_done = false;
w.run([&]() -> et::task<> {
w.run([&]() -> kota::task<> {
// Compile first.
worker::CompileParams cp;
cp.path = src;

View File

@@ -2,17 +2,16 @@
#include <vector>
#include "test/test.h"
#include "eventide/serde/bincode/bincode.h"
#include "eventide/serde/serde/raw_value.h"
#include "server/protocol.h"
#include "server/worker_test_helpers.h"
#include "kota/codec/bincode/bincode.h"
#include "kota/codec/raw_value.h"
namespace clice::testing {
namespace {
namespace et = eventide;
// ============================================================================
// Bincode Serialization Tests
// ============================================================================
@@ -20,7 +19,7 @@ namespace et = eventide;
TEST_SUITE(BincodeRoundTrip) {
TEST_CASE(CompileParamsRoundTrip) {
namespace bincode = eventide::serde::bincode;
namespace bincode = kota::codec::bincode;
worker::CompileParams params;
params.path = "/tmp/test.cpp";
@@ -47,7 +46,7 @@ TEST_CASE(CompileParamsRoundTrip) {
}
TEST_CASE(CompileResultRoundTrip) {
namespace bincode = eventide::serde::bincode;
namespace bincode = kota::codec::bincode;
worker::CompileResult result;
result.version = 1;
@@ -92,7 +91,7 @@ TEST_CASE(BuildPCHRequest) {
bool test_done = false;
w.run([&]() -> et::task<> {
w.run([&]() -> kota::task<> {
worker::BuildParams params;
params.kind = worker::BuildKind::BuildPCH;
params.file = hdr;
@@ -127,7 +126,7 @@ TEST_CASE(IndexRequest) {
bool test_done = false;
w.run([&]() -> et::task<> {
w.run([&]() -> kota::task<> {
worker::BuildParams params;
params.kind = worker::BuildKind::Index;
params.file = src;
@@ -162,7 +161,7 @@ TEST_CASE(BuildPCMRequest) {
bool test_done = false;
w.run([&]() -> et::task<> {
w.run([&]() -> kota::task<> {
worker::BuildParams params;
params.kind = worker::BuildKind::BuildPCM;
params.file = src;
@@ -196,7 +195,7 @@ TEST_CASE(CompletionRequest) {
bool test_done = false;
w.run([&]() -> et::task<> {
w.run([&]() -> kota::task<> {
worker::BuildParams params;
params.kind = worker::BuildKind::Completion;
params.file = src;
@@ -226,7 +225,7 @@ TEST_CASE(SignatureHelpRequest) {
bool test_done = false;
w.run([&]() -> et::task<> {
w.run([&]() -> kota::task<> {
worker::BuildParams params;
params.kind = worker::BuildKind::SignatureHelp;
params.file = src;
@@ -261,7 +260,7 @@ TEST_CASE(MultipleStatelessRequests) {
bool test_done = false;
w.run([&]() -> et::task<> {
w.run([&]() -> kota::task<> {
// Send multiple index requests to test stateless worker handles them sequentially.
for(int i = 0; i < 3; i++) {
worker::BuildParams params;

View File

@@ -11,12 +11,14 @@
#include "test/temp_dir.h"
#include "command/argument_parser.h"
#include "command/command.h"
#include "eventide/async/async.h"
#include "eventide/ipc/peer.h"
#include "eventide/ipc/transport.h"
#include "server/protocol.h"
#include "support/filesystem.h"
#include "kota/async/async.h"
#include "kota/ipc/codec/bincode.h"
#include "kota/ipc/peer.h"
#include "kota/ipc/transport.h"
namespace clice::testing {
namespace {
@@ -32,8 +34,6 @@ struct SigpipeGuard {
static SigpipeGuard sigpipe_guard;
namespace et = eventide;
/// Resolve path to the clice binary for spawning workers.
inline std::string clice_binary() {
auto res_dir = resource_dir();
@@ -59,10 +59,10 @@ inline std::vector<std::string> make_args(const std::string& file_path,
/// Helper: spawn a worker process and return a BincodePeer connected to it.
struct WorkerHandle {
et::event_loop loop;
et::process proc{};
std::unique_ptr<et::ipc::StreamTransport> transport;
std::unique_ptr<et::ipc::BincodePeer> peer;
kota::event_loop loop;
kota::process proc{};
std::unique_ptr<kota::ipc::StreamTransport> transport;
std::unique_ptr<kota::ipc::BincodePeer> peer;
int stderr_fd = -1;
bool spawn(const std::string& mode, std::uint64_t memory_limit = 0) {
@@ -74,7 +74,7 @@ struct WorkerHandle {
stderr_fd = ::open(stderr_path.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644);
#endif
et::process::options opts;
kota::process::options opts;
opts.file = binary;
opts.args = {binary, "--mode", mode};
if(memory_limit > 0) {
@@ -82,12 +82,13 @@ struct WorkerHandle {
opts.args.push_back(std::to_string(memory_limit));
}
opts.streams = {
et::process::stdio::pipe(true, false), // stdin: child reads
et::process::stdio::pipe(false, true), // stdout: child writes
stderr_fd >= 0 ? et::process::stdio::from_fd(stderr_fd) : et::process::stdio::ignore(),
kota::process::stdio::pipe(true, false), // stdin: child reads
kota::process::stdio::pipe(false, true), // stdout: child writes
stderr_fd >= 0 ? kota::process::stdio::from_fd(stderr_fd)
: kota::process::stdio::ignore(),
};
auto result = et::process::spawn(opts, loop);
auto result = kota::process::spawn(opts, loop);
if(!result) {
#ifndef _WIN32
if(stderr_fd >= 0)
@@ -97,9 +98,9 @@ struct WorkerHandle {
}
auto& spawn = *result;
transport = std::make_unique<et::ipc::StreamTransport>(std::move(spawn.stdout_pipe),
std::move(spawn.stdin_pipe));
peer = std::make_unique<et::ipc::BincodePeer>(loop, std::move(transport));
transport = std::make_unique<kota::ipc::StreamTransport>(std::move(spawn.stdout_pipe),
std::move(spawn.stdin_pipe));
peer = std::make_unique<kota::ipc::BincodePeer>(loop, std::move(transport));
proc = std::move(spawn.proc);
#ifndef _WIN32
if(stderr_fd >= 0)

View File

@@ -83,5 +83,87 @@ int x = 1;
}
}; // TEST_SUITE(SourceText)
TEST_SUITE(DirectiveArgument) {
void EXPECT_RANGE(llvm::StringRef content, std::uint32_t offset, llvm::StringRef expected) {
auto result = find_directive_argument(content, offset);
ASSERT_TRUE(result.has_value());
ASSERT_EQ(content.substr(result->begin, result->length()), expected);
}
void EXPECT_NONE(llvm::StringRef content, std::uint32_t offset) {
auto result = find_directive_argument(content, offset);
ASSERT_FALSE(result.has_value());
}
TEST_CASE(IncludeQuoted) {
llvm::StringRef src = R"(#include "foo.h")";
EXPECT_RANGE(src, 0, R"("foo.h")");
}
TEST_CASE(IncludeAngled) {
llvm::StringRef src = "#include <iostream>";
EXPECT_RANGE(src, 0, "<iostream>");
}
TEST_CASE(IncludeMacro) {
llvm::StringRef src = "#include HEADER";
EXPECT_RANGE(src, 0, "HEADER");
}
TEST_CASE(HasIncludeQuoted) {
llvm::StringRef src = R"(#if __has_include("foo.h"))";
// offset at __has_include
auto pos = src.find("__has_include");
EXPECT_RANGE(src, static_cast<std::uint32_t>(pos), R"("foo.h")");
}
TEST_CASE(HasIncludeAngled) {
llvm::StringRef src = "#if __has_include(<vector>)";
auto pos = src.find("__has_include");
EXPECT_RANGE(src, static_cast<std::uint32_t>(pos), "<vector>");
}
TEST_CASE(EmbedQuoted) {
llvm::StringRef src = R"(#embed "data.bin")";
EXPECT_RANGE(src, 0, R"("data.bin")");
}
TEST_CASE(HasEmbedQuoted) {
llvm::StringRef src = R"(#if __has_embed("data.bin"))";
auto pos = src.find("__has_embed");
EXPECT_RANGE(src, static_cast<std::uint32_t>(pos), R"("data.bin")");
}
TEST_CASE(MultilineOffset) {
llvm::StringRef src = "#include \"a.h\"\n#include \"b.h\"";
// offset pointing into the second line
auto pos = src.find("#include \"b.h\"");
EXPECT_RANGE(src, static_cast<std::uint32_t>(pos), R"("b.h")");
}
TEST_CASE(EmptyDirective) {
llvm::StringRef src = "#include \n";
EXPECT_NONE(src, 0);
}
TEST_CASE(HasIncludeFromLineStart) {
llvm::StringRef src = "#if __has_include(<vector>)";
EXPECT_RANGE(src, 0, "<vector>");
}
TEST_CASE(HasEmbedFromLineStart) {
llvm::StringRef src = R"(#if __has_embed("data.bin"))";
EXPECT_RANGE(src, 0, R"("data.bin")");
}
TEST_CASE(IncludeNext) {
llvm::StringRef src = "#include_next <stdlib.h>";
EXPECT_RANGE(src, 0, "<stdlib.h>");
}
}; // TEST_SUITE(DirectiveArgument)
} // namespace
} // namespace clice::testing

View File

@@ -6,5 +6,6 @@
#include <vector>
#include "test/platform.h"
#include "eventide/zest/macro.h"
#include "support/format.h"
#include "kota/zest/macro.h"

View File

@@ -7,6 +7,44 @@
namespace clice::testing {
namespace {
std::vector<std::string> base_cc1_args(llvm::StringRef standard) {
return {
"clang",
"-cc1",
"-triple",
LLVM_DEFAULT_TARGET_TRIPLE,
standard.str(),
"-ffreestanding",
"-undef",
"-fms-extensions",
"-fsyntax-only",
"-x",
"c++",
};
}
} // namespace
Tester::~Tester() {
for(auto& path: pcm_paths) {
fs::remove(path);
}
}
bool Tester::try_compile() {
auto built = clice::compile(params);
if(!built.completed()) {
for(auto& diag: built.diagnostics()) {
LOG_ERROR("{}", diag.message);
}
return false;
}
unit.emplace(std::move(built));
return true;
}
void Tester::prepare(llvm::StringRef standard) {
params = CompilationParams();
unit.reset();
@@ -16,19 +54,7 @@ void Tester::prepare(llvm::StringRef standard) {
vfs->add(file, source.content);
}
owned_args.clear();
// Use -cc1 mode directly to bypass the slow driver subprocess.
owned_args.push_back("clang");
owned_args.push_back("-cc1");
owned_args.push_back("-triple");
owned_args.push_back(LLVM_DEFAULT_TARGET_TRIPLE);
owned_args.push_back(standard.str());
owned_args.push_back("-ffreestanding");
owned_args.push_back("-undef");
owned_args.push_back("-fms-extensions");
owned_args.push_back("-fsyntax-only");
owned_args.push_back("-x");
owned_args.push_back("c++");
owned_args = base_cc1_args(standard);
owned_args.push_back(TestVFS::path(src_path));
params.arguments.clear();
@@ -42,17 +68,7 @@ void Tester::prepare(llvm::StringRef standard) {
bool Tester::compile(llvm::StringRef standard) {
prepare(standard);
auto built = clice::compile(params);
if(!built.completed()) {
for(auto& diag: built.diagnostics()) {
LOG_ERROR("{}", diag.message);
}
return false;
}
unit.emplace(std::move(built));
return true;
return try_compile();
}
bool Tester::compile_with_pch(llvm::StringRef standard) {
@@ -64,7 +80,6 @@ bool Tester::compile_with_pch(llvm::StringRef standard) {
return false;
}
// Use an overlay VFS so the PCH temp file on real disk is accessible.
auto overlay =
llvm::makeIntrusiveRefCnt<llvm::vfs::OverlayFileSystem>(llvm::vfs::getRealFileSystem());
overlay->pushOverlay(vfs);
@@ -96,16 +111,123 @@ bool Tester::compile_with_pch(llvm::StringRef standard) {
params.pch = {info.path, static_cast<std::uint32_t>(info.preamble.size())};
params.buffers.clear();
auto built = clice::compile(params);
if(!built.completed()) {
for(auto& diag: built.diagnostics()) {
LOG_ERROR("{}", diag.message);
return try_compile();
}
bool Tester::compile_with_modules(llvm::StringRef standard) {
std::vector<ModuleFile> all_modules = module_files;
for(auto& [file, source]: sources.all_files) {
if(file == src_path) {
continue;
}
auto result = scan(source.content);
if(!result.module_name.empty() || result.need_preprocess) {
all_modules.push_back({file.str(), source.content});
}
return false;
}
unit.emplace(std::move(built));
return true;
if(all_modules.empty()) {
return compile(standard);
}
vfs = llvm::makeIntrusiveRefCnt<TestVFS>();
for(auto& [file, source]: sources.all_files) {
vfs->add(file, source.content);
}
for(auto& mod: module_files) {
vfs->add(mod.filename, mod.content);
}
struct ScannedModule {
std::string filename;
std::string content;
std::string module_name;
std::vector<std::string> deps;
};
auto scan_args_base = base_cc1_args(standard);
std::vector<ScannedModule> modules;
for(auto& mod: all_modules) {
auto args = scan_args_base;
args.push_back(TestVFS::path(mod.filename));
std::vector<const char*> argv;
for(auto& arg: args) {
argv.push_back(arg.c_str());
}
auto result = scan_precise(argv, TestVFS::root(), {}, nullptr, vfs);
modules.push_back(
{mod.filename, mod.content, result.module_name, std::move(result.modules)});
}
llvm::StringMap<std::size_t> name_to_index;
for(std::size_t i = 0; i < modules.size(); ++i) {
name_to_index[modules[i].module_name] = i;
}
std::vector<std::size_t> order;
std::vector<int> state(modules.size(), 0);
auto topo_visit = [&](this auto& self, std::size_t i) -> bool {
if(state[i] == 2)
return true;
if(state[i] == 1) {
LOG_ERROR("Circular module dependency involving {}", modules[i].module_name);
return false;
}
state[i] = 1;
for(auto& dep: modules[i].deps) {
auto it = name_to_index.find(dep);
if(it != name_to_index.end()) {
if(!self(it->second))
return false;
}
}
state[i] = 2;
order.push_back(i);
return true;
};
for(std::size_t i = 0; i < modules.size(); ++i) {
if(!topo_visit(i))
return false;
}
auto overlay =
llvm::makeIntrusiveRefCnt<llvm::vfs::OverlayFileSystem>(llvm::vfs::getRealFileSystem());
overlay->pushOverlay(vfs);
llvm::StringMap<std::string> built_pcms;
for(auto idx: order) {
auto& mod = modules[idx];
auto pcm_path = fs::createTemporaryFile("clice", "pcm");
if(!pcm_path) {
LOG_ERROR("{}", pcm_path.error().message());
return false;
}
pcm_paths.push_back(*pcm_path);
Tester builder;
builder.add_main(mod.filename, mod.content);
builder.prepare(standard);
builder.params.kind = CompilationKind::ModuleInterface;
builder.params.output_file = *pcm_path;
builder.params.vfs = overlay;
builder.params.pcms = built_pcms;
if(!builder.try_compile())
return false;
built_pcms.try_emplace(mod.module_name, *pcm_path);
}
prepare(standard);
params.vfs = overlay;
params.pcms = std::move(built_pcms);
return try_compile();
}
std::uint32_t Tester::point(llvm::StringRef name, llvm::StringRef file) {
@@ -166,13 +288,11 @@ void Tester::prepare_driver(llvm::StringRef standard) {
params.kind = CompilationKind::Content;
// Use overlay VFS: real FS (for system headers) + InMemoryFS (for test files).
auto overlay =
llvm::makeIntrusiveRefCnt<llvm::vfs::OverlayFileSystem>(llvm::vfs::getRealFileSystem());
overlay->pushOverlay(vfs);
params.vfs = overlay;
// Remap test files so clang sees our in-memory content.
for(auto& [file, source]: sources.all_files) {
if(file == src_path) {
params.add_remapped_file(file, source.content);
@@ -185,36 +305,11 @@ void Tester::prepare_driver(llvm::StringRef standard) {
bool Tester::compile_driver(llvm::StringRef standard) {
prepare_driver(standard);
auto built = clice::compile(params);
if(!built.completed()) {
for(auto& diag: built.diagnostics()) {
LOG_ERROR("{}", diag.message);
}
return false;
}
unit.emplace(std::move(built));
return true;
return try_compile();
}
bool Tester::compile_driver_with_pch(llvm::StringRef standard) {
params = CompilationParams();
unit.reset();
vfs = llvm::makeIntrusiveRefCnt<TestVFS>();
for(auto& [file, source]: sources.all_files) {
vfs->add(file, source.content);
}
auto command = std::format("clang++ {} {} -fms-extensions", standard, src_path);
database.add_command("fake", src_path, command);
CommandOptions options;
options.query_toolchain = true;
options.suppress_logging = true;
auto commands = database.lookup(src_path, options);
assert(!commands.empty() && "lookup failed after add_command");
params.arguments = commands.front().to_argv();
prepare_driver(standard);
auto pch_path = fs::createTemporaryFile("clice", "pch");
if(!pch_path) {
@@ -222,16 +317,12 @@ bool Tester::compile_driver_with_pch(llvm::StringRef standard) {
return false;
}
// Use overlay VFS: real FS (for system headers + PCH temp) + InMemoryFS.
auto overlay =
llvm::makeIntrusiveRefCnt<llvm::vfs::OverlayFileSystem>(llvm::vfs::getRealFileSystem());
overlay->pushOverlay(vfs);
params.vfs = overlay;
// Phase 1: Build PCH from the preamble portion.
params.kind = CompilationKind::Preamble;
params.output_file = *pch_path;
// Clear buffers from prepare_driver() so we can re-add with preamble bound.
params.buffers.clear();
for(auto& [file, source]: sources.all_files) {
if(file == src_path) {
auto bound = compute_preamble_bound(source.content);
@@ -259,25 +350,7 @@ bool Tester::compile_driver_with_pch(llvm::StringRef standard) {
params.pch = {info.path, static_cast<std::uint32_t>(info.preamble.size())};
params.buffers.clear();
for(auto& [file, source]: sources.all_files) {
if(file == src_path) {
params.add_remapped_file(file, source.content);
} else {
std::string path = path::is_absolute(file) ? file.str() : path::join(".", file);
params.add_remapped_file(path, source.content);
}
}
auto built = clice::compile(params);
if(!built.completed()) {
for(auto& diag: built.diagnostics()) {
LOG_ERROR("{}", diag.message);
}
return false;
}
unit.emplace(std::move(built));
return true;
return try_compile();
}
void Tester::clear() {
@@ -288,6 +361,11 @@ void Tester::clear() {
src_path.clear();
owned_args.clear();
vfs.reset();
module_files.clear();
for(auto& path: pcm_paths) {
fs::remove(path);
}
pcm_paths.clear();
}
} // namespace clice::testing

View File

@@ -2,6 +2,7 @@
#include <optional>
#include <string>
#include <vector>
#include "test/annotation.h"
#include "test/test.h"
@@ -25,6 +26,16 @@ struct Tester {
/// The VFS used for compilation.
llvm::IntrusiveRefCntPtr<TestVFS> vfs;
struct ModuleFile {
std::string filename;
std::string content;
};
std::vector<ModuleFile> module_files;
std::vector<std::string> pcm_paths;
~Tester();
void add_main(llvm::StringRef file, llvm::StringRef content) {
src_path = file.str();
sources.add_source(file, content);
@@ -39,6 +50,10 @@ struct Tester {
sources.add_sources(content);
}
void add_module(llvm::StringRef filename, llvm::StringRef content) {
module_files.push_back({filename.str(), content.str()});
}
/// Fast VFS-only path: uses -cc1 directly, no system headers.
void prepare(llvm::StringRef standard = "-std=c++20");
@@ -46,6 +61,8 @@ struct Tester {
bool compile_with_pch(llvm::StringRef standard = "-std=c++20");
bool compile_with_modules(llvm::StringRef standard = "-std=c++20");
/// Driver path: uses CompilationDatabase + toolchain cache, has system headers.
void prepare_driver(llvm::StringRef standard = "-std=c++20");
@@ -53,6 +70,8 @@ struct Tester {
bool compile_driver_with_pch(llvm::StringRef standard = "-std=c++20");
bool try_compile();
std::uint32_t operator[](llvm::StringRef file, llvm::StringRef pos) {
return sources.all_files.lookup(file).offsets.lookup(pos);
}

View File

@@ -1,13 +1,14 @@
#include <string>
#include <string_view>
#include "eventide/deco/deco.h"
#include "eventide/zest/zest.h"
#include "support/logging.h"
#include "kota/deco/deco.h"
#include "kota/zest/zest.h"
namespace {
using deco::decl::KVStyle;
using kota::deco::decl::KVStyle;
struct TestOptions {
DecoKV(style = KVStyle::JoinedOrSeparate,
@@ -32,8 +33,8 @@ struct TestOptions {
} // namespace
int main(int argc, const char** argv) {
auto args = deco::util::argvify(argc, argv);
auto parsed = deco::cli::parse<TestOptions>(args);
auto args = kota::deco::util::argvify(argc, argv);
auto parsed = kota::deco::cli::parse<TestOptions>(args);
std::string_view filter = {};
if(parsed.has_value() && parsed->options.test_filter.has_value()) {
@@ -57,5 +58,5 @@ int main(int argc, const char** argv) {
clice::logging::stderr_logger("test", clice::logging::options);
return eventide::zest::Runner::instance().run_tests(filter);
return kota::zest::Runner::instance().run_tests(filter);
}