diff --git a/cmake/package.cmake b/cmake/package.cmake index c056e75df..1f7f06da2 100644 --- a/cmake/package.cmake +++ b/cmake/package.cmake @@ -41,8 +41,7 @@ set(FLATBUFFERS_BUILD_FLATHASH OFF CACHE BOOL "" FORCE) FetchContent_Declare( kotatsu GIT_REPOSITORY https://github.com/clice-io/kotatsu - GIT_TAG main - GIT_SHALLOW TRUE + GIT_TAG e024f3b427a554502c4aa015952800a03ca4384b ) set(KOTA_ENABLE_ZEST ON) diff --git a/docs/en/architecture.md b/docs/en/architecture.md index 9a9b0bff4..dacbe21f2 100644 --- a/docs/en/architecture.md +++ b/docs/en/architecture.md @@ -153,7 +153,7 @@ String values support `${workspace}` substitution. ## IPC Protocol -The master and workers communicate using custom RPC messages defined in `src/server/protocol.h`. Each message type has a `RequestTraits` or `NotificationTraits` specialization that defines the method name and result type. +The master and workers communicate using custom RPC messages defined in `src/server/protocol/`. Each message type has a `RequestTraits` or `NotificationTraits` specialization that defines the method name and result type. ### Stateful Worker Messages diff --git a/src/clice.cc b/src/clice.cc index d09e5b127..0f0bfcb77 100644 --- a/src/clice.cc +++ b/src/clice.cc @@ -4,17 +4,13 @@ #include #include -#include "server/master_server.h" -#include "server/stateful_worker.h" -#include "server/stateless_worker.h" +#include "server/service/agentic.h" +#include "server/service/master_server.h" +#include "server/worker/stateful_worker.h" +#include "server/worker/stateless_worker.h" #include "support/logging.h" -#include "kota/async/async.h" #include "kota/deco/deco.h" -#include "kota/ipc/codec/json.h" -#include "kota/ipc/peer.h" -#include "kota/ipc/recording_transport.h" -#include "kota/ipc/transport.h" namespace clice { @@ -22,15 +18,17 @@ using kota::deco::decl::KVStyle; struct Options { DecoKV(style = KVStyle::JoinedOrSeparate, - help = "Running mode: pipe, socket, stateless-worker, stateful-worker", + help = "Running mode: pipe, socket, agentic, stateless-worker, stateful-worker", required = false) mode; DecoKV(style = KVStyle::JoinedOrSeparate, help = "Socket mode address", required = false) host = "127.0.0.1"; - DecoKV(style = KVStyle::JoinedOrSeparate, help = "Socket mode port", required = false) - port = 50051; + DecoKV(style = KVStyle::JoinedOrSeparate, + help = "Agentic TCP port (0 = disabled)", + required = false) + port = 0; DecoKV(style = KVStyle::JoinedOrSeparate, names = {"--log-level", "--log-level="}, @@ -43,6 +41,11 @@ struct Options { required = false) record; + DecoKV(style = KVStyle::JoinedOrSeparate, + help = "File path for agentic queries", + required = false) + path; + // Internal options (passed from master to worker processes) DecoKV(style = KVStyle::JoinedOrSeparate, names = {"--worker-memory-limit", "--worker-memory-limit="}, @@ -68,9 +71,6 @@ struct Options { int main(int argc, const char** argv) { #ifndef _WIN32 - // On POSIX systems, ignore SIGPIPE so that writing to a closed pipe - // (e.g. when the LSP client disconnects) returns EPIPE instead of - // killing the process. This is standard practice for pipe-based servers. signal(SIGPIPE, SIG_IGN); #endif @@ -110,8 +110,6 @@ int main(int argc, const char** argv) { return 1; } - std::string self_path = argv[0]; - auto& mode = *opts.mode; auto worker_name = opts.worker_name.value_or(""); @@ -131,77 +129,29 @@ int main(int argc, const char** argv) { log_dir); } - if(mode == "pipe") { - clice::logging::stderr_logger("master", clice::logging::options); - - kota::event_loop loop; - - auto transport = kota::ipc::StreamTransport::open_stdio(loop); - if(!transport) { - LOG_ERROR("failed to open stdio transport"); - return 1; - } - - std::unique_ptr final_transport = std::move(*transport); - if(opts.record.has_value()) { - final_transport = - std::make_unique(std::move(final_transport), - *opts.record); - } - - kota::ipc::JsonPeer peer(loop, std::move(final_transport)); - clice::MasterServer server(loop, peer, std::move(self_path)); - server.register_handlers(); - - loop.schedule(peer.run()); - loop.run(); - return 0; + if(mode == "pipe" || mode == "socket") { + clice::ServerOptions server_opts; + server_opts.mode = mode; + server_opts.host = opts.host.value_or("127.0.0.1"); + server_opts.port = opts.port.value_or(0); + server_opts.self_path = argv[0]; + server_opts.record = opts.record.value_or(""); + return clice::run_server_mode(server_opts); } - if(mode == "socket") { - clice::logging::stderr_logger("master", clice::logging::options); - - kota::event_loop loop; - + if(mode == "agentic") { auto host = opts.host.value_or("127.0.0.1"); - auto port = opts.port.value_or(50051); - - auto acceptor = kota::tcp::listen(host, port, {}, loop); - if(!acceptor) { - LOG_ERROR("failed to listen on {}:{}", host, port); + auto port = opts.port.value_or(0); + auto path = opts.path.value_or(""); + if(port <= 0) { + LOG_ERROR("--port is required for agentic mode"); return 1; } - - LOG_INFO("Listening on {}:{} ...", host, port); - - auto task = [&]() -> kota::task<> { - auto client = co_await acceptor->accept(); - if(!client.has_value()) { - LOG_ERROR("failed to accept connection"); - loop.stop(); - co_return; - } - - LOG_INFO("Client connected"); - - std::unique_ptr transport = - std::make_unique(std::move(client.value())); - if(opts.record.has_value()) { - transport = std::make_unique(std::move(transport), - *opts.record); - } - kota::ipc::JsonPeer peer(loop, std::move(transport)); - clice::MasterServer server(loop, peer, std::string(self_path)); - server.register_handlers(); - - co_await peer.run(); - peer.close(); - loop.stop(); - }; - - loop.schedule(task()); - loop.run(); - return 0; + if(path.empty()) { + LOG_ERROR("--path is required for agentic mode"); + return 1; + } + return clice::run_agentic_mode(host, port, path); } LOG_ERROR("unknown mode '{}'", mode); diff --git a/src/compile/tidy.cpp b/src/compile/tidy.cpp index 1b516e38d..b0e180e2c 100644 --- a/src/compile/tidy.cpp +++ b/src/compile/tidy.cpp @@ -92,15 +92,11 @@ tidy::ClangTidyOptions create_options() { // include-cleaner is directly integrated in IncludeCleaner.cpp "-misc-include-cleaner", - // ----- False Positives ----- - // Check relies on seeing ifndef/define/endif directives, // clangd doesn't replay those when using a preamble. "-llvm-header-guard", "-modernize-macro-to-enum", - // ----- Crashing Checks ----- - // Check can choke on invalid (intermediate) c++ // code, which is often the case when clangd // tries to build an AST. diff --git a/src/semantic/resolver.cpp b/src/semantic/resolver.cpp index 49875212c..120e523dd 100644 --- a/src/semantic/resolver.cpp +++ b/src/semantic/resolver.cpp @@ -1111,8 +1111,6 @@ class PseudoInstantiator : public clang::TreeTransform { return Base::TransformDecltypeType(TLB, TL); } - // --- State --- - private: clang::Sema& sema; clang::ASTContext& context; diff --git a/src/server/compile_graph.cpp b/src/server/compiler/compile_graph.cpp similarity index 99% rename from src/server/compile_graph.cpp rename to src/server/compiler/compile_graph.cpp index ac7f1f9e0..39d4070dc 100644 --- a/src/server/compile_graph.cpp +++ b/src/server/compiler/compile_graph.cpp @@ -1,4 +1,4 @@ -#include "server/compile_graph.h" +#include "server/compiler/compile_graph.h" #include diff --git a/src/server/compile_graph.h b/src/server/compiler/compile_graph.h similarity index 100% rename from src/server/compile_graph.h rename to src/server/compiler/compile_graph.h diff --git a/src/server/compiler.cpp b/src/server/compiler/compiler.cpp similarity index 87% rename from src/server/compiler.cpp rename to src/server/compiler/compiler.cpp index 0686049b4..346bf4326 100644 --- a/src/server/compiler.cpp +++ b/src/server/compiler/compiler.cpp @@ -1,4 +1,4 @@ -#include "server/compiler.h" +#include "server/compiler/compiler.h" #include #include @@ -6,7 +6,7 @@ #include "command/search_config.h" #include "index/tu_index.h" -#include "server/protocol.h" +#include "server/protocol/worker.h" #include "support/filesystem.h" #include "support/logging.h" #include "syntax/include_resolver.h" @@ -28,16 +28,20 @@ using serde_raw = kota::codec::RawValue; /// Detect whether the cursor is inside a preamble directive (include/import). Compiler::Compiler(kota::event_loop& loop, - kota::ipc::JsonPeer& peer, Workspace& workspace, WorkerPool& pool, llvm::DenseMap& sessions) : - loop(loop), peer(peer), workspace(workspace), pool(pool), sessions(sessions) {} + loop(loop), workspace(workspace), pool(pool), sessions(sessions) {} Compiler::~Compiler() { workspace.cancel_all(); } +kota::task<> Compiler::stop() { + compile_tasks.cancel(); + co_await compile_tasks.join(); +} + void Compiler::init_compile_graph() { if(workspace.path_to_module.empty()) { LOG_INFO("No C++20 modules detected, skipping CompileGraph"); @@ -410,6 +414,8 @@ std::string uri_to_path(const std::string& uri) { void Compiler::publish_diagnostics(const std::string& uri, int version, const kota::codec::RawValue& diagnostics_json) { + if(!peer) + return; std::vector diagnostics; if(!diagnostics_json.empty()) { auto status = kota::codec::json::from_json(diagnostics_json.data, diagnostics); @@ -421,14 +427,16 @@ void Compiler::publish_diagnostics(const std::string& uri, params.uri = uri; params.version = version; params.diagnostics = std::move(diagnostics); - peer.send_notification(params); + peer->send_notification(params); } void Compiler::clear_diagnostics(const std::string& uri) { + if(!peer) + return; protocol::PublishDiagnosticsParams params; params.uri = uri; params.diagnostics = {}; - peer.send_notification(params); + peer->send_notification(params); } kota::task Compiler::ensure_pch(Session& session, @@ -629,6 +637,101 @@ void Compiler::record_deps(Session& session, llvm::ArrayRef deps) { /// Called lazily by forward_query() / forward_build() before every /// feature request (hover, semantic tokens, etc.). Guarantees that when it /// returns true the stateful worker assigned to `path_id` holds an up-to-date +kota::task<> Compiler::run_compile(std::uint32_t pid, std::shared_ptr pc) { + auto find_session = [&]() -> Session* { + auto it = sessions.find(pid); + return it != sessions.end() ? &it->second : nullptr; + }; + + auto* sess = find_session(); + if(!sess) { + pc->done.set(); + co_return; + } + + auto finish_compile = [&]() { + auto* s = find_session(); + if(s && s->compiling == pc) { + s->compiling.reset(); + } + LOG_INFO("ensure_compiled: finish path_id={}", pid); + pc->done.set(); + }; + + auto gen = sess->generation; + LOG_INFO("ensure_compiled: starting compile path_id={} gen={}", pid, gen); + + auto file_path = std::string(workspace.path_pool.resolve(pid)); + auto uri = lsp::URI::from_file_path(file_path); + std::string uri_str = uri.has_value() ? uri->str() : file_path; + + worker::CompileParams params; + params.path = file_path; + params.version = sess->version; + params.text = sess->text; + if(!fill_compile_args(file_path, params.directory, params.arguments, sess)) { + finish_compile(); + co_return; + } + + if(!co_await ensure_deps(*sess, params.directory, params.arguments, params.pch, params.pcms)) { + LOG_WARN("Dependency preparation failed for {}, skipping compile", uri_str); + finish_compile(); + co_return; + } + + sess = find_session(); + if(!sess) { + pc->done.set(); + co_return; + } + + auto result = co_await pool.send_stateful(pid, params); + + sess = find_session(); + if(!sess) { + pc->done.set(); + co_return; + } + + if(sess->generation != gen) { + LOG_INFO("ensure_compiled: generation mismatch ({} vs {}) for {}", + sess->generation, + gen, + uri_str); + finish_compile(); + co_return; + } + + if(!result.has_value()) { + LOG_WARN("Compile failed for {}: {}", uri_str, result.error().message); + clear_diagnostics(uri_str); + finish_compile(); + co_return; + } + + sess->ast_dirty = false; + pc->succeeded = true; + record_deps(*sess, result.value().deps); + + if(!result.value().tu_index_data.empty()) { + auto tu_index = index::TUIndex::from(result.value().tu_index_data.data()); + OpenFileIndex ofi; + ofi.file_index = std::move(tu_index.main_file_index); + ofi.symbols = std::move(tu_index.symbols); + ofi.content = sess->text; + ofi.mapper.emplace(ofi.content, lsp::PositionEncoding::UTF16); + sess->file_index = std::move(ofi); + } + + auto version = sess->version; + finish_compile(); + + publish_diagnostics(uri_str, version, result.value().diagnostics); + if(on_indexing_needed) + on_indexing_needed(); +} + /// AST and diagnostics have been published to the client. /// /// Lifecycle overview (pull-based model): @@ -648,9 +751,9 @@ void Compiler::record_deps(Session& session, llvm::ArrayRef deps) { /// worker); every other file is read from disk by the compiler. /// /// Concurrency: multiple concurrent feature requests for the same file will -/// each call ensure_compiled(). The first one launches a detached compile -/// task via loop.schedule(); subsequent ones wait on the shared event. -/// The detached task cannot be cancelled by LSP $/cancelRequest, preventing +/// each call ensure_compiled(). The first one spawns a compile task into the +/// Compiler's task_group; subsequent ones wait on the shared event. +/// The spawned task is not cancelled by LSP $/cancelRequest, preventing /// the race where cancellation wakes all waiters and they all start compiles. kota::task Compiler::ensure_compiled(Session& session) { auto path_id = session.path_id; @@ -679,124 +782,12 @@ kota::task Compiler::ensure_compiled(Session& session) { co_return true; } - // No compile in flight and AST is dirty — launch a detached compile task. - // The detached task is scheduled via loop.schedule() so it is NOT subject - // to LSP $/cancelRequest cancellation. This eliminates the race where - // cancellation fires the RAII guard, waking all waiters simultaneously - // and causing them all to start new compiles. auto pending_compile = std::make_shared(); session.compiling = pending_compile; - LOG_INFO("ensure_compiled: launching detached compile path_id={} gen={}", - path_id, - session.generation); - - // Capture path_id by value so the detached lambda can re-lookup the session - // from the sessions map after co_await (DenseMap may invalidate pointers). - loop.schedule([](Compiler* self, - std::uint32_t pid, - std::shared_ptr pc) -> kota::task<> { - // Re-lookup session from the sessions map (pointer may have been - // invalidated by DenseMap growth during co_await). - auto find_session = [&]() -> Session* { - auto it = self->sessions.find(pid); - return it != self->sessions.end() ? &it->second : nullptr; - }; - - auto* sess = find_session(); - if(!sess) { - pc->done.set(); - co_return; - } - - auto finish_compile = [&]() { - auto* s = find_session(); - if(s && s->compiling == pc) { - s->compiling.reset(); - } - LOG_INFO("ensure_compiled: finish_compile (detached) path_id={}", pid); - pc->done.set(); - }; - - auto gen = sess->generation; - LOG_INFO("ensure_compiled: starting compile (detached) path_id={} gen={}", pid, gen); - - auto file_path = std::string(self->workspace.path_pool.resolve(pid)); - auto uri = lsp::URI::from_file_path(file_path); - std::string uri_str = uri.has_value() ? uri->str() : file_path; - - worker::CompileParams params; - params.path = file_path; - params.version = sess->version; - params.text = sess->text; - if(!self->fill_compile_args(file_path, params.directory, params.arguments, sess)) { - finish_compile(); - co_return; - } - - if(!co_await self - ->ensure_deps(*sess, params.directory, params.arguments, params.pch, params.pcms)) { - LOG_WARN("Dependency preparation failed for {}, skipping compile", uri_str); - finish_compile(); - co_return; - } - - // Re-lookup after co_await (DenseMap may have grown). - sess = find_session(); - if(!sess) { - pc->done.set(); - co_return; - } - - auto result = co_await self->pool.send_stateful(pid, params); - - // Re-lookup after co_await. - sess = find_session(); - if(!sess) { - pc->done.set(); - co_return; - } - - if(sess->generation != gen) { - LOG_INFO("ensure_compiled: generation mismatch ({} vs {}) for {}", - sess->generation, - gen, - uri_str); - finish_compile(); - co_return; - } - - if(!result.has_value()) { - LOG_WARN("Compile failed for {}: {}", uri_str, result.error().message); - self->clear_diagnostics(uri_str); - finish_compile(); - co_return; - } - - sess->ast_dirty = false; - pc->succeeded = true; - self->record_deps(*sess, result.value().deps); - - // Store open file index from the stateful worker's TUIndex. - if(!result.value().tu_index_data.empty()) { - auto tu_index = index::TUIndex::from(result.value().tu_index_data.data()); - OpenFileIndex ofi; - ofi.file_index = std::move(tu_index.main_file_index); - ofi.symbols = std::move(tu_index.symbols); - ofi.content = sess->text; - ofi.mapper.emplace(ofi.content, lsp::PositionEncoding::UTF16); - sess->file_index = std::move(ofi); - } - - auto version = sess->version; - finish_compile(); + LOG_INFO("ensure_compiled: launching compile path_id={} gen={}", path_id, session.generation); - // Publish diagnostics AFTER marking compile as done, so that concurrent - // forward_query() calls can proceed immediately. - self->publish_diagnostics(uri_str, version, result.value().diagnostics); - if(self->on_indexing_needed) - self->on_indexing_needed(); - }(this, path_id, pending_compile)); + compile_tasks.spawn(run_compile(path_id, pending_compile)); // Wait for the detached compile to finish. If this wait is cancelled // by LSP $/cancelRequest, the detached task continues unaffected. diff --git a/src/server/compiler.h b/src/server/compiler/compiler.h similarity index 91% rename from src/server/compiler.h rename to src/server/compiler/compiler.h index c0c7b71d5..8fdbd69c5 100644 --- a/src/server/compiler.h +++ b/src/server/compiler/compiler.h @@ -8,9 +8,9 @@ #include #include "command/command.h" -#include "server/session.h" -#include "server/worker_pool.h" -#include "server/workspace.h" +#include "server/service/session.h" +#include "server/worker/worker_pool.h" +#include "server/workspace/workspace.h" #include "syntax/completion.h" #include "kota/async/async.h" @@ -50,10 +50,14 @@ std::string uri_to_path(const std::string& uri); class Compiler { public: Compiler(kota::event_loop& loop, - kota::ipc::JsonPeer& peer, Workspace& workspace, WorkerPool& pool, llvm::DenseMap& sessions); + + void set_peer(kota::ipc::JsonPeer* p) { + peer = p; + } + ~Compiler(); void init_compile_graph(); @@ -96,7 +100,12 @@ class Compiler { /// Callback invoked when indexing should be scheduled. std::function on_indexing_needed; + /// Cancel in-flight compile tasks and wait for them to finish. + kota::task<> stop(); + private: + kota::task<> run_compile(std::uint32_t path_id, std::shared_ptr pc); + kota::task ensure_deps(Session& session, const std::string& directory, const std::vector& arguments, @@ -125,10 +134,11 @@ class Compiler { private: kota::event_loop& loop; - kota::ipc::JsonPeer& peer; + kota::ipc::JsonPeer* peer = nullptr; Workspace& workspace; WorkerPool& pool; llvm::DenseMap& sessions; + kota::task_group<> compile_tasks{loop}; }; } // namespace clice diff --git a/src/server/indexer.cpp b/src/server/compiler/indexer.cpp similarity index 95% rename from src/server/indexer.cpp rename to src/server/compiler/indexer.cpp index f46aa386a..4ef2b65fc 100644 --- a/src/server/indexer.cpp +++ b/src/server/compiler/indexer.cpp @@ -1,4 +1,4 @@ -#include "server/indexer.h" +#include "server/compiler/indexer.h" #include #include @@ -6,10 +6,10 @@ #include #include "index/tu_index.h" -#include "server/compiler.h" -#include "server/protocol.h" -#include "server/session.h" -#include "server/worker_pool.h" +#include "server/compiler/compiler.h" +#include "server/protocol/worker.h" +#include "server/service/session.h" +#include "server/worker/worker_pool.h" #include "support/filesystem.h" #include "support/logging.h" @@ -694,18 +694,14 @@ kota::task<> Indexer::index_one(std::uint32_t server_path_id) { } } -kota::task<> Indexer::monitor_resources(std::uint32_t generation) { - while(generation == monitor_generation) { - co_await kota::sleep(std::chrono::milliseconds(3000), loop); - - if(generation != monitor_generation) - break; +kota::task<> Indexer::monitor_resources() { + while(true) { + co_await kota::sleep(std::chrono::milliseconds(3000)); auto mem = kota::sys::memory(); if(mem.total == 0) continue; - // Respect cgroup/container limits when present. auto effective_total = (mem.constrained > 0 && mem.constrained < mem.total) ? mem.constrained : mem.total; auto ratio = static_cast(mem.available) / static_cast(effective_total); @@ -736,22 +732,23 @@ kota::task<> Indexer::run_background_indexing() { } indexing_active = true; - ++monitor_generation; - loop.schedule(monitor_resources(monitor_generation)); - // Put module interface units first so their PCMs are built before - // non-module files that might import them. + kota::cancellation_source monitor_cancel; + kota::task_group<> index_group(loop); + index_group.spawn(kota::with_token(monitor_resources(), monitor_cancel.token())); + std::stable_partition( index_queue.begin() + index_queue_pos, index_queue.end(), [this](std::uint32_t id) { return workspace.path_to_module.contains(id); }); auto batch = index_queue.size() - index_queue_pos; + std::size_t inflight = 0; std::size_t dispatched = 0; std::size_t completed = 0; - finished = 0; + std::size_t finished = 0; + kota::event completion_event; - // Progress reporting via LSP $/progress. std::optional> progress; if(peer) { progress.emplace(*peer, protocol::ProgressToken(std::string("clice/backgroundIndex"))); @@ -764,17 +761,13 @@ kota::task<> Indexer::run_background_indexing() { } while(index_queue_pos < index_queue.size() || inflight > 0) { - // Dispatch new tasks up to max_concurrent. while(index_queue_pos < index_queue.size() && inflight < max_concurrent) { - // Wait if paused by a user request. if(pause_depth > 0) { co_await resume_event.wait(); } auto server_path_id = index_queue[index_queue_pos++]; - // Quick pre-filter: skip open files and fresh files without - // consuming a concurrency slot. auto file_path = std::string(workspace.path_pool.resolve(server_path_id)); if(sessions.contains(server_path_id) || !need_update(file_path)) { ++completed; @@ -784,27 +777,26 @@ kota::task<> Indexer::run_background_indexing() { ++inflight; ++dispatched; - // Launch the index task. On completion it decrements - // inflight, bumps finished, and signals the event. - loop.schedule([](Indexer* self, std::uint32_t id, kota::event& done) -> kota::task<> { + index_group.spawn([](Indexer* self, + std::uint32_t id, + std::size_t& inflight_ref, + std::size_t& finished_ref, + kota::event& done) -> kota::task<> { co_await self->index_one(id); - --self->inflight; - ++self->finished; + --inflight_ref; + ++finished_ref; done.set(); - }(this, server_path_id, completion_event)); + }(this, server_path_id, inflight, finished, completion_event)); } if(inflight == 0) break; - // Wait for at least one task to finish. co_await completion_event.wait(); completion_event.reset(); - // Drain all completions that occurred since last wake. completed += std::exchange(finished, 0); - // Report progress. if(progress) { auto pct = batch > 0 ? static_cast(completed * 100 / batch) : 100; progress->report(std::format("{}/{} files", completed, batch), pct); @@ -815,8 +807,10 @@ kota::task<> Indexer::run_background_indexing() { progress->end(std::format("Indexed {} files", dispatched)); } + monitor_cancel.cancel(); + co_await index_group.join(); + indexing_active = false; - ++monitor_generation; // Stop the monitor coroutine. LOG_INFO("Background indexing complete: {} files dispatched", dispatched); save(workspace.config.project.index_dir); } diff --git a/src/server/indexer.h b/src/server/compiler/indexer.h similarity index 93% rename from src/server/indexer.h rename to src/server/compiler/indexer.h index 7b6a621b1..1e76395b4 100644 --- a/src/server/indexer.h +++ b/src/server/compiler/indexer.h @@ -9,7 +9,7 @@ #include "semantic/relation_kind.h" #include "semantic/symbol_kind.h" -#include "server/workspace.h" +#include "server/workspace/workspace.h" #include "kota/async/async.h" #include "kota/ipc/codec/json.h" @@ -231,27 +231,15 @@ class Indexer { /// Concurrency control for background indexing. std::size_t max_concurrent = 2; std::size_t baseline_concurrent = 2; - std::size_t inflight = 0; - std::size_t finished = 0; ///< Incremented by each completed dispatch task. /// Pause/resume: when paused, new index tasks wait on this event. /// Uses a counter so nested pause/resume pairs work correctly. std::size_t pause_depth = 0; kota::event resume_event{true}; - /// Completion event — signalled by each finished dispatch task so the - /// main loop can wake up. Must be a member (not local to the coroutine) - /// because inflight tasks capture it by reference and may outlive the - /// coroutine frame during server shutdown. - kota::event completion_event; - - /// Generation counter — incremented each run so a stale monitor_resources - /// coroutine can detect that its owning run has ended. - std::uint32_t monitor_generation = 0; - kota::task<> run_background_indexing(); kota::task<> index_one(std::uint32_t server_path_id); - kota::task<> monitor_resources(std::uint32_t generation); + kota::task<> monitor_resources(); }; } // namespace clice diff --git a/src/server/master_server.h b/src/server/master_server.h deleted file mode 100644 index 94106fffa..000000000 --- a/src/server/master_server.h +++ /dev/null @@ -1,81 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include "server/compiler.h" -#include "server/indexer.h" -#include "server/session.h" -#include "server/worker_pool.h" -#include "server/workspace.h" - -#include "kota/async/async.h" -#include "kota/codec/json/json.h" -#include "kota/ipc/peer.h" -#include "llvm/ADT/DenseMap.h" - -namespace clice { - -enum class ServerLifecycle : std::uint8_t { - Uninitialized, - Initialized, - Ready, - ShuttingDown, - Exited, -}; - -/// Top-level LSP server — the single orchestration point for the language -/// server process. -/// -/// Responsibilities: -/// - Owns the two-layer state model: Workspace (disk truth) and Sessions -/// (per-open-file volatile state). -/// - Manages Session lifecycle directly: didOpen creates, didChange mutates, -/// didSave syncs to Workspace, didClose destroys. -/// - Dispatches compilation and feature queries to Compiler. -/// - Dispatches index lookups and background indexing to Indexer. -/// -/// Design principle: -/// Open files are never depended upon by other files. Dependencies always -/// point to disk files. The only path from Session to Workspace is didSave. -class MasterServer { -public: - MasterServer(kota::event_loop& loop, kota::ipc::JsonPeer& peer, std::string self_path); - ~MasterServer(); - - void register_handlers(); - -private: - kota::event_loop& loop; - kota::ipc::JsonPeer& peer; - - /// Persistent project-wide state (config, CDB, path pool, dependency - /// graphs, compilation caches, symbol index). - Workspace workspace; - - /// Per-file editing sessions, keyed by server-level path_id. - llvm::DenseMap sessions; - - /// Worker process pool for offloading compilation and queries. - WorkerPool pool; - - /// Compilation lifecycle manager (reads/writes workspace and sessions). - Compiler compiler; - - /// Index query and background scheduling (reads from workspace and sessions). - Indexer indexer; - - ServerLifecycle lifecycle = ServerLifecycle::Uninitialized; - std::string self_path; - std::string workspace_root; - std::string session_log_dir; - std::string init_options_json; ///< Raw JSON from initializationOptions, consumed once. - - void load_workspace(); - - using RawResult = kota::task; -}; - -} // namespace clice diff --git a/src/server/protocol/agentic.h b/src/server/protocol/agentic.h new file mode 100644 index 000000000..b17fa9689 --- /dev/null +++ b/src/server/protocol/agentic.h @@ -0,0 +1,30 @@ +#pragma once + +#include +#include + +#include "kota/ipc/protocol.h" + +namespace clice::agentic { + +struct CompileCommandParams { + std::string path; +}; + +struct CompileCommandResult { + std::string file; + std::string directory; + std::vector arguments; +}; + +} // namespace clice::agentic + +namespace kota::ipc::protocol { + +template <> +struct RequestTraits { + using Result = clice::agentic::CompileCommandResult; + constexpr inline static std::string_view method = "agentic/compileCommand"; +}; + +} // namespace kota::ipc::protocol diff --git a/src/server/protocol/extension.h b/src/server/protocol/extension.h new file mode 100644 index 000000000..7ed2cbbd4 --- /dev/null +++ b/src/server/protocol/extension.h @@ -0,0 +1,42 @@ +#pragma once + +#include +#include +#include + +namespace clice::ext { + +struct ContextItem { + std::string label; + std::string description; + std::string uri; +}; + +struct QueryContextParams { + std::string uri; + std::optional offset; +}; + +struct QueryContextResult { + std::vector contexts; + int total = 0; +}; + +struct CurrentContextParams { + std::string uri; +}; + +struct CurrentContextResult { + std::optional context; +}; + +struct SwitchContextParams { + std::string uri; + std::string context_uri; +}; + +struct SwitchContextResult { + bool success = false; +}; + +} // namespace clice::ext diff --git a/src/server/protocol.h b/src/server/protocol/worker.h similarity index 87% rename from src/server/protocol.h rename to src/server/protocol/worker.h index 1a2774c78..e5a3a7293 100644 --- a/src/server/protocol.h +++ b/src/server/protocol/worker.h @@ -1,7 +1,6 @@ #pragma once #include -#include #include #include #include @@ -10,7 +9,6 @@ #include "syntax/token.h" #include "kota/codec/json/json.h" -#include "kota/ipc/lsp/protocol.h" #include "kota/ipc/protocol.h" namespace clice::worker { @@ -122,43 +120,6 @@ struct EvictedParams { } // namespace clice::worker -namespace clice::ext { - -struct ContextItem { - std::string label; - std::string description; - std::string uri; -}; - -struct QueryContextParams { - std::string uri; - std::optional offset; -}; - -struct QueryContextResult { - std::vector contexts; - int total; -}; - -struct CurrentContextParams { - std::string uri; -}; - -struct CurrentContextResult { - std::optional context; -}; - -struct SwitchContextParams { - std::string uri; - std::string context_uri; -}; - -struct SwitchContextResult { - bool success; -}; - -} // namespace clice::ext - namespace kota::ipc::protocol { template <> diff --git a/src/server/service/agent_client.cpp b/src/server/service/agent_client.cpp new file mode 100644 index 000000000..bae366def --- /dev/null +++ b/src/server/service/agent_client.cpp @@ -0,0 +1,37 @@ +#include "server/service/agent_client.h" + +#include +#include +#include + +#include "server/protocol/agentic.h" +#include "server/service/master_server.h" + +namespace clice { + +using kota::ipc::RequestResult; +using RequestContext = kota::ipc::JsonPeer::RequestContext; + +AgentClient::AgentClient(MasterServer& server, kota::ipc::JsonPeer& peer) : + server(server), peer(peer) { + using namespace agentic; + + peer.on_request( + [this](RequestContext&, + const CompileCommandParams& params) -> RequestResult { + std::string directory; + std::vector arguments; + if(!this->server.compiler.fill_compile_args(params.path, directory, arguments)) { + co_return kota::outcome_error( + kota::ipc::Error{std::format("no compile command found for {}", params.path)}); + } + + co_return CompileCommandResult{ + .file = params.path, + .directory = std::move(directory), + .arguments = std::move(arguments), + }; + }); +} + +} // namespace clice diff --git a/src/server/service/agent_client.h b/src/server/service/agent_client.h new file mode 100644 index 000000000..a3232b90d --- /dev/null +++ b/src/server/service/agent_client.h @@ -0,0 +1,18 @@ +#pragma once + +#include "kota/ipc/codec/json.h" + +namespace clice { + +class MasterServer; + +class AgentClient { +public: + AgentClient(MasterServer& server, kota::ipc::JsonPeer& peer); + +private: + MasterServer& server; + kota::ipc::JsonPeer& peer; +}; + +} // namespace clice diff --git a/src/server/service/agentic.cpp b/src/server/service/agentic.cpp new file mode 100644 index 000000000..02fd72f64 --- /dev/null +++ b/src/server/service/agentic.cpp @@ -0,0 +1,59 @@ +#include "server/service/agentic.h" + +#include +#include +#include + +#include "server/protocol/agentic.h" +#include "support/logging.h" + +#include "kota/async/async.h" +#include "kota/ipc/codec/json.h" +#include "kota/ipc/transport.h" + +namespace clice { + +static kota::task<> agentic_request(kota::ipc::JsonPeer& peer, int& exit_code, std::string path) { + auto result = + co_await peer.send_request(agentic::CompileCommandParams{.path = std::move(path)}); + + if(!result) { + LOG_ERROR("request failed: {}", result.error().message); + } else { + auto json = kota::codec::json::to_string(*result); + std::println("{}", json ? *json : "null"); + exit_code = 0; + } + + peer.close(); +} + +static kota::task<> agentic_client(int& exit_code, + std::unique_ptr& peer_out, + std::string host, + int port, + std::string path) { + auto& loop = kota::event_loop::current(); + auto transport = co_await kota::ipc::StreamTransport::connect_tcp(host, port, loop); + if(!transport) { + LOG_ERROR("failed to connect to {}:{}", host, port); + co_return; + } + + peer_out = std::make_unique(loop, std::move(*transport)); + co_await kota::when_all(peer_out->run(), + agentic_request(*peer_out, exit_code, std::move(path))); +} + +int run_agentic_mode(llvm::StringRef host, int port, llvm::StringRef path) { + logging::stderr_logger("agentic", logging::options); + + kota::event_loop loop; + int exit_code = 1; + std::unique_ptr peer; + loop.schedule(agentic_client(exit_code, peer, host.str(), port, path.str())); + loop.run(); + return exit_code; +} + +} // namespace clice diff --git a/src/server/service/agentic.h b/src/server/service/agentic.h new file mode 100644 index 000000000..b2c625b45 --- /dev/null +++ b/src/server/service/agentic.h @@ -0,0 +1,11 @@ +#pragma once + +#include + +#include "llvm/ADT/StringRef.h" + +namespace clice { + +int run_agentic_mode(llvm::StringRef host, int port, llvm::StringRef path); + +} // namespace clice diff --git a/src/server/master_server.cpp b/src/server/service/lsp_client.cpp similarity index 51% rename from src/server/master_server.cpp rename to src/server/service/lsp_client.cpp index 6255cec6f..d71e41446 100644 --- a/src/server/master_server.cpp +++ b/src/server/service/lsp_client.cpp @@ -1,4 +1,4 @@ -#include "server/master_server.h" +#include "server/service/lsp_client.h" #include #include @@ -7,7 +7,9 @@ #include #include "semantic/symbol_kind.h" -#include "server/protocol.h" +#include "server/protocol/extension.h" +#include "server/protocol/worker.h" +#include "server/service/master_server.h" #include "support/filesystem.h" #include "support/logging.h" @@ -16,7 +18,6 @@ #include "kota/ipc/lsp/protocol.h" #include "kota/ipc/lsp/uri.h" #include "kota/meta/enum.h" -#include "llvm/Support/FileSystem.h" #include "llvm/Support/Path.h" #include "llvm/Support/Process.h" @@ -29,177 +30,39 @@ using kota::ipc::RequestResult; using RequestContext = kota::ipc::JsonPeer::RequestContext; using serde_raw = kota::codec::RawValue; -/// Serialize a value to a JSON RawValue using LSP config. template static serde_raw to_raw(const T& value) { auto json = kota::codec::json::to_json(value); return serde_raw{json ? std::move(*json) : "null"}; } -MasterServer::MasterServer(kota::event_loop& loop, - kota::ipc::JsonPeer& peer, - std::string self_path) : - loop(loop), peer(peer), pool(loop), compiler(loop, peer, workspace, pool, sessions), - indexer(loop, - workspace, - sessions, - pool, - compiler, - [this](uint32_t proj_path_id) { - // Bridge project-level path_id to server-level path_id. - // The two PathPools may assign different IDs to the same path. - auto path = workspace.project_index.path_pool.path(proj_path_id); - auto server_id = workspace.path_pool.intern(path); - return sessions.contains(server_id); - }), - self_path(std::move(self_path)) {} - -MasterServer::~MasterServer() = default; - -void MasterServer::load_workspace() { - if(workspace_root.empty()) - return; - - auto& cfg = workspace.config.project; - - if(!cfg.cache_dir.empty()) { - auto ec = llvm::sys::fs::create_directories(cfg.cache_dir); - if(ec) { - LOG_WARN("Failed to create cache directory {}: {}", - std::string_view(cfg.cache_dir), - ec.message()); - } else { - LOG_INFO("Cache directory: {}", std::string_view(cfg.cache_dir)); - } - - for(auto* subdir: {"cache/pch", "cache/pcm"}) { - auto dir = path::join(cfg.cache_dir, subdir); - if(auto ec2 = llvm::sys::fs::create_directories(dir)) - LOG_WARN("Failed to create {}: {}", dir, ec2.message()); - } +LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(server), peer(peer) { + server.compiler.set_peer(&peer); + server.indexer.set_peer(&peer); - workspace.cleanup_cache(); - workspace.load_cache(); - } - - // Discover compile_commands.json: configured paths first, then auto-scan. - std::string cdb_path; - for(auto& configured: cfg.compile_commands_paths) { - // Each entry can be a file or a directory containing compile_commands.json. - if(llvm::sys::fs::is_directory(configured)) { - auto candidate = path::join(configured, "compile_commands.json"); - if(llvm::sys::fs::exists(candidate)) { - cdb_path = std::move(candidate); - break; - } - } else if(llvm::sys::fs::exists(configured)) { - cdb_path = configured; - break; - } else { - LOG_WARN("Configured compile_commands_path not found: {}", configured); - } - } - - // Auto-scan: workspace root + all immediate subdirectories. - if(cdb_path.empty()) { - auto try_candidate = [&](llvm::StringRef dir) -> bool { - auto candidate = path::join(dir, "compile_commands.json"); - if(llvm::sys::fs::exists(candidate)) { - cdb_path = std::move(candidate); - return true; - } - return false; - }; - - if(!try_candidate(workspace_root)) { - std::error_code ec; - for(llvm::sys::fs::directory_iterator it(workspace_root, ec), end; it != end && !ec; - it.increment(ec)) { - if(it->type() == llvm::sys::fs::file_type::directory_file) { - if(try_candidate(it->path())) - break; - } - } - } - } - - if(cdb_path.empty()) { - LOG_WARN("No compile_commands.json found in workspace {}", workspace_root); - return; - } - - auto count = workspace.cdb.load(cdb_path); - LOG_INFO("Loaded CDB from {} with {} entries", cdb_path, count); - - auto report = scan_dependency_graph(workspace.cdb, - workspace.path_pool, - workspace.dep_graph, - /*cache=*/nullptr, - [this](llvm::StringRef path, - std::vector& append, - std::vector& remove) { - workspace.config.match_rules(path, append, remove); - }); - workspace.dep_graph.build_reverse_map(); - - auto unresolved = report.includes_found - report.includes_resolved; - double accuracy = - report.includes_found > 0 - ? 100.0 * static_cast(report.includes_resolved) / report.includes_found - : 100.0; - LOG_INFO( - "Dependency scan: {}ms, {} files ({} source + {} header), " "{} edges, {}/{} resolved ({:.1f}%), {} waves", - report.elapsed_ms, - report.total_files, - report.source_files, - report.header_files, - report.total_edges, - report.includes_resolved, - report.includes_found, - accuracy, - report.waves); - if(unresolved > 0) - LOG_WARN("{} unresolved includes", unresolved); - - workspace.build_module_map(); - indexer.load(cfg.index_dir); - - if(*cfg.enable_indexing) { - for(auto& entry: workspace.cdb.get_entries()) { - auto file = workspace.cdb.resolve_path(entry.file); - auto server_id = workspace.path_pool.intern(file); - indexer.enqueue(server_id); - } - indexer.schedule(); - } - - compiler.init_compile_graph(); -} - -void MasterServer::register_handlers() { using StringVec = std::vector; peer.on_request([this](RequestContext& ctx, const protocol::InitializeParams& params) -> RequestResult { - if(lifecycle != ServerLifecycle::Uninitialized) { + auto& srv = this->server; + if(srv.lifecycle != ServerLifecycle::Uninitialized) { co_return kota::outcome_error(protocol::Error{"Server already initialized"}); } auto& init = params.lsp__initialize_params; if(init.root_uri.has_value()) { - workspace_root = uri_to_path(*init.root_uri); + srv.workspace_root = uri_to_path(*init.root_uri); } - // Capture initializationOptions as raw JSON for config loading. if(init.initialization_options.has_value()) { auto json = kota::codec::json::to_json(*init.initialization_options); if(json) - init_options_json = std::move(*json); + srv.init_options_json = std::move(*json); } - lifecycle = ServerLifecycle::Initialized; - LOG_INFO("Initialized with workspace: {}", workspace_root); + srv.lifecycle = ServerLifecycle::Initialized; + LOG_INFO("Initialized with workspace: {}", srv.workspace_root); protocol::InitializeResult result; auto& caps = result.capabilities; @@ -222,7 +85,6 @@ void MasterServer::register_handlers() { caps.signature_help_provider = protocol::SignatureHelpOptions{ .trigger_characters = StringVec{"(", ")", "{", "}", "<", ">", ","}, }; - /// FIXME: In the future, we would support work done progress. caps.declaration_provider = protocol::DeclarationOptions{ .work_done_progress = false, }; @@ -277,103 +139,34 @@ void MasterServer::register_handlers() { co_return result; }); - peer.on_notification([this](const protocol::InitializedParams& params) { - // Config priority: initializationOptions > clice.toml > defaults. - // Load the workspace config (with defaults applied) first, then overlay - // any initializationOptions on top so fields not mentioned in the JSON - // keep the values from clice.toml — kotatsu's deserializer only touches - // fields that are present in the input. - workspace.config = Config::load_from_workspace(workspace_root); - if(!init_options_json.empty()) { - if(auto ov = kota::codec::json::parse(init_options_json, workspace.config); !ov) { - LOG_WARN("Failed to apply initializationOptions: {}", ov.error().to_string()); - } else { - // Re-run apply_defaults so overridden strings get workspace - // substitution and `compiled_rules` is rebuilt if `rules` - // changed. Defaults are gated on zero/empty sentinels, so - // existing values from the overlay are preserved. - workspace.config.apply_defaults(workspace_root); - LOG_INFO("Applied initializationOptions overlay"); - } - init_options_json.clear(); - } - - auto& cfg = workspace.config.project; - - if(!cfg.logging_dir.empty()) { - auto now = std::chrono::system_clock::now(); - auto pid = llvm::sys::Process::getProcessId(); - auto session_dir = - path::join(cfg.logging_dir, std::format("{:%Y-%m-%d_%H-%M-%S}_{}", now, pid)); - logging::file_logger("master", session_dir, logging::options); - session_log_dir = session_dir; - } - - LOG_INFO("Server ready (stateful={}, stateless={}, idle={}ms)", - cfg.stateful_worker_count.value, - cfg.stateless_worker_count.value, - *cfg.idle_timeout_ms); - - WorkerPoolOptions pool_opts; - pool_opts.self_path = self_path; - pool_opts.stateful_count = cfg.stateful_worker_count; - pool_opts.stateless_count = cfg.stateless_worker_count; - pool_opts.worker_memory_limit = cfg.worker_memory_limit; - pool_opts.log_dir = session_log_dir; - if(!pool.start(pool_opts)) { - LOG_ERROR("Failed to start worker pool"); - return; - } - - lifecycle = ServerLifecycle::Ready; - - compiler.on_indexing_needed = [this]() { - indexer.schedule(); - }; - - indexer.set_peer(&peer); - indexer.set_max_concurrency(cfg.stateless_worker_count.value); - - load_workspace(); + peer.on_notification([this]([[maybe_unused]] const protocol::InitializedParams& params) { + this->server.initialize(); }); peer.on_request( [this](RequestContext& ctx, const protocol::ShutdownParams& params) -> RequestResult { - lifecycle = ServerLifecycle::ShuttingDown; + this->server.lifecycle = ServerLifecycle::ShuttingDown; LOG_INFO("Shutdown requested"); co_return nullptr; }); - peer.on_notification([this](const protocol::ExitParams& params) { - lifecycle = ServerLifecycle::Exited; + peer.on_notification([this]([[maybe_unused]] const protocol::ExitParams& params) { + auto& srv = this->server; + srv.lifecycle = ServerLifecycle::Exited; LOG_INFO("Exit notification received"); - - indexer.save(workspace.config.project.index_dir); - workspace.save_cache(); - - loop.schedule([this]() -> kota::task<> { - co_await pool.stop(); - loop.stop(); - }()); + srv.schedule_shutdown(); }); - /// Document lifecycle — handled directly by MasterServer. - peer.on_notification([this](const protocol::DidOpenTextDocumentParams& params) { - if(lifecycle != ServerLifecycle::Ready) + auto& srv = this->server; + if(srv.lifecycle != ServerLifecycle::Ready) return; auto path = uri_to_path(params.text_document.uri); - auto path_id = workspace.path_pool.intern(path); + auto path_id = srv.workspace.path_pool.intern(path); - auto [it, inserted] = sessions.try_emplace(path_id); - auto& session = it->second; - if(!inserted) { - // DenseMap tombstone may retain stale data — reset to a fresh Session. - session = Session{}; - } - session.path_id = path_id; + auto& session = srv.open_session(path_id); session.version = params.text_document.version; session.text = params.text_document.text; session.generation++; @@ -382,18 +175,18 @@ void MasterServer::register_handlers() { }); peer.on_notification([this](const protocol::DidChangeTextDocumentParams& params) { - if(lifecycle != ServerLifecycle::Ready) + auto& srv = this->server; + if(srv.lifecycle != ServerLifecycle::Ready) return; auto path = uri_to_path(params.text_document.uri); - auto path_id = workspace.path_pool.intern(path); + auto path_id = srv.workspace.path_pool.intern(path); - auto it = sessions.find(path_id); - if(it == sessions.end()) + auto* session = srv.find_session(path_id); + if(!session) return; - auto& session = it->second; - session.version = params.text_document.version; + session->version = params.text_document.version; for(auto& change: params.content_changes) { std::visit( @@ -401,186 +194,157 @@ void MasterServer::register_handlers() { using T = std::remove_cvref_t; if constexpr(std::is_same_v) { - session.text = c.text; + session->text = c.text; } else { auto& range = c.range; - lsp::PositionMapper mapper(session.text, lsp::PositionEncoding::UTF16); + lsp::PositionMapper mapper(session->text, lsp::PositionEncoding::UTF16); auto start = mapper.to_offset(range.start); auto end = mapper.to_offset(range.end); if(start && end && *start <= *end) { - session.text.replace(*start, *end - *start, c.text); + session->text.replace(*start, *end - *start, c.text); } } }, change); } - session.generation++; - session.ast_dirty = true; + session->generation++; + session->ast_dirty = true; LOG_DEBUG("didChange: path={} version={} gen={}", path, - session.version, - session.generation); + session->version, + session->generation); worker::DocumentUpdateParams update; update.path = path; - update.version = session.version; - pool.notify_stateful(path_id, update); + update.version = session->version; + srv.pool.notify_stateful(path_id, update); }); peer.on_notification([this](const protocol::DidCloseTextDocumentParams& params) { - if(lifecycle != ServerLifecycle::Ready) + auto& srv = this->server; + if(srv.lifecycle != ServerLifecycle::Ready) return; - auto path = uri_to_path(params.text_document.uri); - auto path_id = workspace.path_pool.intern(path); - - workspace.on_file_closed(path_id); - pool.notify_stateful(path_id, worker::EvictParams{path}); - - // Clear diagnostics for the closed file. - protocol::PublishDiagnosticsParams diag_params; - diag_params.uri = params.text_document.uri; - peer.send_notification(diag_params); - - sessions.erase(path_id); - - indexer.enqueue(path_id); - indexer.schedule(); - - LOG_DEBUG("didClose: {}", path); + auto path_id = srv.workspace.path_pool.intern(uri_to_path(params.text_document.uri)); + srv.close_session(path_id, this->peer); }); peer.on_notification([this](const protocol::DidSaveTextDocumentParams& params) { - if(lifecycle != ServerLifecycle::Ready) + auto& srv = this->server; + if(srv.lifecycle != ServerLifecycle::Ready) return; auto path = uri_to_path(params.text_document.uri); - auto path_id = workspace.path_pool.intern(path); - - auto dirtied = workspace.on_file_saved(path_id); - for(auto dirty_id: dirtied) { - if(auto sit = sessions.find(dirty_id); sit != sessions.end()) { - sit->second.ast_dirty = true; - } else { - indexer.enqueue(dirty_id); - } - } - - // Invalidate header contexts for sessions whose host is this file. - for(auto& [hdr_id, session]: sessions) { - if(session.header_context && session.header_context->host_path_id == path_id) { - session.header_context.reset(); - session.ast_dirty = true; - } - } - - indexer.schedule(); + auto path_id = srv.workspace.path_pool.intern(path); + srv.on_file_saved(path_id); LOG_DEBUG("didSave: {}", path); }); - /// Feature requests — stateful forwarding. - peer.on_request([this](RequestContext& ctx, const protocol::HoverParams& params) -> RawResult { + auto& srv = this->server; auto path = uri_to_path(params.text_document_position_params.text_document.uri); - auto path_id = workspace.path_pool.intern(path); - auto sit = sessions.find(path_id); - if(sit == sessions.end()) + auto path_id = srv.workspace.path_pool.intern(path); + auto* session = srv.find_session(path_id); + if(!session) co_return serde_raw{"null"}; - co_return co_await compiler.forward_query(worker::QueryKind::Hover, - sit->second, - params.text_document_position_params.position); + co_return co_await srv.compiler.forward_query( + worker::QueryKind::Hover, + *session, + params.text_document_position_params.position); }); peer.on_request([this](RequestContext& ctx, const protocol::SemanticTokensParams& params) -> RawResult { + auto& srv = this->server; auto path = uri_to_path(params.text_document.uri); - auto path_id = workspace.path_pool.intern(path); - auto sit = sessions.find(path_id); - if(sit == sessions.end()) + auto path_id = srv.workspace.path_pool.intern(path); + auto* session = srv.find_session(path_id); + if(!session) co_return serde_raw{"null"}; - co_return co_await compiler.forward_query(worker::QueryKind::SemanticTokens, sit->second); + co_return co_await srv.compiler.forward_query(worker::QueryKind::SemanticTokens, *session); }); peer.on_request( [this](RequestContext& ctx, const protocol::InlayHintParams& params) -> RawResult { + auto& srv = this->server; auto path = uri_to_path(params.text_document.uri); - auto path_id = workspace.path_pool.intern(path); - auto sit = sessions.find(path_id); - if(sit == sessions.end()) + auto path_id = srv.workspace.path_pool.intern(path); + auto* session = srv.find_session(path_id); + if(!session) co_return serde_raw{"null"}; - co_return co_await compiler.forward_query(worker::QueryKind::InlayHints, - sit->second, - {}, - params.range); - }); - - peer.on_request( - [this](RequestContext& ctx, const protocol::FoldingRangeParams& params) -> RawResult { - auto path = uri_to_path(params.text_document.uri); - auto path_id = workspace.path_pool.intern(path); - auto sit = sessions.find(path_id); - if(sit == sessions.end()) - co_return serde_raw{"null"}; - co_return co_await compiler.forward_query(worker::QueryKind::FoldingRange, sit->second); + co_return co_await srv.compiler.forward_query(worker::QueryKind::InlayHints, + *session, + {}, + params.range); }); peer.on_request([this](RequestContext& ctx, - const protocol::DocumentSymbolParams& params) -> RawResult { + const protocol::FoldingRangeParams& params) -> RawResult { + auto& srv = this->server; auto path = uri_to_path(params.text_document.uri); - auto path_id = workspace.path_pool.intern(path); - auto sit = sessions.find(path_id); - if(sit == sessions.end()) + auto path_id = srv.workspace.path_pool.intern(path); + auto* session = srv.find_session(path_id); + if(!session) co_return serde_raw{"null"}; - co_return co_await compiler.forward_query(worker::QueryKind::DocumentSymbol, sit->second); + co_return co_await srv.compiler.forward_query(worker::QueryKind::FoldingRange, *session); }); peer.on_request([this](RequestContext& ctx, - const protocol::DocumentLinkParams& params) -> RawResult { + const protocol::DocumentSymbolParams& params) -> RawResult { + auto& srv = this->server; auto path = uri_to_path(params.text_document.uri); - auto path_id = workspace.path_pool.intern(path); - auto sit = sessions.find(path_id); - if(sit == sessions.end()) - co_return serde_raw{"null"}; - auto& session = sit->second; - auto result = co_await compiler.forward_query(worker::QueryKind::DocumentLink, session); - if(!result.has_value()) + auto path_id = srv.workspace.path_pool.intern(path); + auto* session = srv.find_session(path_id); + if(!session) 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 co_await srv.compiler.forward_query(worker::QueryKind::DocumentSymbol, *session); + }); + + peer.on_request( + [this](RequestContext& ctx, const protocol::DocumentLinkParams& params) -> RawResult { + auto& srv = this->server; + auto path = uri_to_path(params.text_document.uri); + auto path_id = srv.workspace.path_pool.intern(path); + auto* session = srv.find_session(path_id); + if(!session) + co_return serde_raw{"null"}; + auto result = + co_await srv.compiler.forward_query(worker::QueryKind::DocumentLink, *session); + if(!result.has_value()) + co_return serde_raw{"null"}; + auto& links = result.value(); + auto* session2 = srv.find_session(path_id); + if(session2 && session2->pch_ref) { + auto& pch_cache = srv.workspace.pch_cache; + auto pch_it = pch_cache.find(session2->pch_ref->path_id); + if(pch_it != pch_cache.end() && !pch_it->second.document_links_json.empty()) { + auto& pch_json = pch_it->second.document_links_json; + if(!links.data.empty() && links.data != "null" && links.data.size() > 2) { + links.data.pop_back(); + links.data += ','; + links.data.append(pch_json.begin() + 1, pch_json.end()); + } else { + links.data = pch_json; + } } } - } - co_return std::move(links); - }); + co_return std::move(links); + }); peer.on_request( [this](RequestContext& ctx, const protocol::CodeActionParams& params) -> RawResult { + auto& srv = this->server; auto path = uri_to_path(params.text_document.uri); - auto path_id = workspace.path_pool.intern(path); - auto sit = sessions.find(path_id); - if(sit == sessions.end()) + auto path_id = srv.workspace.path_pool.intern(path); + auto* session = srv.find_session(path_id); + if(!session) co_return serde_raw{"null"}; - co_return co_await compiler.forward_query(worker::QueryKind::CodeAction, sit->second); + co_return co_await srv.compiler.forward_query(worker::QueryKind::CodeAction, *session); }); - /// Helper: resolve URI to path, path_id, and Session pointer. auto resolve_uri = [this](const std::string& uri) { struct Result { std::string path; @@ -588,22 +352,21 @@ void MasterServer::register_handlers() { Session* session; }; auto path = uri_to_path(uri); - auto path_id = workspace.path_pool.intern(path); - auto sit = sessions.find(path_id); - Session* session = (sit != sessions.end()) ? &sit->second : nullptr; + auto path_id = this->server.workspace.path_pool.intern(path); + auto* session = this->server.find_session(path_id); return Result{std::move(path), path_id, session}; }; auto lookup_at = [this, resolve_uri](const std::string& uri, const protocol::Position& pos) { auto [path, path_id, session] = resolve_uri(uri); - return indexer.lookup_symbol(uri, path, pos, session); + return this->server.indexer.lookup_symbol(uri, path, pos, session); }; auto query_at = [this, resolve_uri](const std::string& uri, const protocol::Position& pos, RelationKind kind) -> std::vector { auto [path, path_id, session] = resolve_uri(uri); - return indexer.query_relations(path, pos, kind, session); + return this->server.indexer.query_relations(path, pos, kind, session); }; auto resolve_item = @@ -612,11 +375,9 @@ void MasterServer::register_handlers() { const protocol::Range& range, const std::optional& data) -> std::optional { auto [path, path_id, session] = resolve_uri(uri); - return indexer.resolve_hierarchy_item(uri, path, range, data, session); + return this->server.indexer.resolve_hierarchy_item(uri, path, range, data, session); }; - /// Feature requests — index-based with AST fallback. - peer.on_request([this, query_at](RequestContext& ctx, const protocol::DefinitionParams& params) -> RawResult { auto& uri = params.text_document_position_params.text_document.uri; @@ -627,14 +388,15 @@ void MasterServer::register_handlers() { co_return to_raw(result); } + auto& srv = this->server; auto path = uri_to_path(uri); - auto path_id = workspace.path_pool.intern(path); - auto sit = sessions.find(path_id); - if(sit == sessions.end()) + auto path_id = srv.workspace.path_pool.intern(path); + auto* session = srv.find_session(path_id); + if(!session) co_return serde_raw{"null"}; - co_return co_await compiler.forward_query(worker::QueryKind::GoToDefinition, - sit->second, - pos); + co_return co_await srv.compiler.forward_query(worker::QueryKind::GoToDefinition, + *session, + pos); }); peer.on_request([this, query_at](RequestContext& ctx, @@ -671,38 +433,37 @@ void MasterServer::register_handlers() { co_return serde_raw{"null"}; }); - /// Feature requests — stateless forwarding. + peer.on_request([this](RequestContext& ctx, + const protocol::CompletionParams& params) -> RawResult { + auto& srv = this->server; + auto path = uri_to_path(params.text_document_position_params.text_document.uri); + auto path_id = srv.workspace.path_pool.intern(path); + auto* session = srv.find_session(path_id); + if(!session) + co_return serde_raw{"null"}; + auto pause = srv.indexer.scoped_pause(); + auto result = + co_await srv.compiler.handle_completion(params.text_document_position_params.position, + *session); + co_return std::move(result); + }); peer.on_request( - [this](RequestContext& ctx, const protocol::CompletionParams& params) -> RawResult { + [this](RequestContext& ctx, const protocol::SignatureHelpParams& params) -> RawResult { + auto& srv = this->server; auto path = uri_to_path(params.text_document_position_params.text_document.uri); - auto path_id = workspace.path_pool.intern(path); - auto sit = sessions.find(path_id); - if(sit == sessions.end()) + auto path_id = srv.workspace.path_pool.intern(path); + auto* session = srv.find_session(path_id); + if(!session) co_return serde_raw{"null"}; - auto pause = indexer.scoped_pause(); + auto pause = srv.indexer.scoped_pause(); auto result = - co_await compiler.handle_completion(params.text_document_position_params.position, - sit->second); + co_await srv.compiler.forward_build(worker::BuildKind::SignatureHelp, + params.text_document_position_params.position, + *session); co_return std::move(result); }); - peer.on_request([this](RequestContext& ctx, - const protocol::SignatureHelpParams& params) -> RawResult { - auto path = uri_to_path(params.text_document_position_params.text_document.uri); - auto path_id = workspace.path_pool.intern(path); - auto sit = sessions.find(path_id); - if(sit == sessions.end()) - co_return serde_raw{"null"}; - auto pause = indexer.scoped_pause(); - auto result = co_await compiler.forward_build(worker::BuildKind::SignatureHelp, - params.text_document_position_params.position, - sit->second); - co_return std::move(result); - }); - - /// Hierarchy queries — index-based. - peer.on_request( [this, lookup_at](RequestContext& ctx, const protocol::CallHierarchyPrepareParams& params) -> RawResult { @@ -726,7 +487,7 @@ void MasterServer::register_handlers() { auto info = resolve_item(params.item.uri, params.item.range, params.item.data); if(!info) co_return serde_raw{"null"}; - auto results = indexer.find_incoming_calls(info->hash); + auto results = this->server.indexer.find_incoming_calls(info->hash); if(results.empty()) co_return serde_raw{"null"}; co_return to_raw(results); @@ -738,7 +499,7 @@ void MasterServer::register_handlers() { auto info = resolve_item(params.item.uri, params.item.range, params.item.data); if(!info) co_return serde_raw{"null"}; - auto results = indexer.find_outgoing_calls(info->hash); + auto results = this->server.indexer.find_outgoing_calls(info->hash); if(results.empty()) co_return serde_raw{"null"}; co_return to_raw(results); @@ -768,7 +529,7 @@ void MasterServer::register_handlers() { auto info = resolve_item(params.item.uri, params.item.range, params.item.data); if(!info) co_return serde_raw{"null"}; - auto results = indexer.find_supertypes(info->hash); + auto results = this->server.indexer.find_supertypes(info->hash); if(results.empty()) co_return serde_raw{"null"}; co_return to_raw(results); @@ -780,7 +541,7 @@ void MasterServer::register_handlers() { auto info = resolve_item(params.item.uri, params.item.range, params.item.data); if(!info) co_return serde_raw{"null"}; - auto results = indexer.find_subtypes(info->hash); + auto results = this->server.indexer.find_subtypes(info->hash); if(results.empty()) co_return serde_raw{"null"}; co_return to_raw(results); @@ -788,29 +549,29 @@ void MasterServer::register_handlers() { peer.on_request( [this](RequestContext& ctx, const protocol::WorkspaceSymbolParams& params) -> RawResult { - auto results = indexer.search_symbols(params.query); + auto results = this->server.indexer.search_symbols(params.query); if(results.empty()) co_return serde_raw{"null"}; co_return to_raw(results); }); - /// clice/ extension commands. - peer.on_request( "clice/queryContext", [this](RequestContext& ctx, const ext::QueryContextParams& params) -> RawResult { + auto& srv = this->server; auto path = uri_to_path(params.uri); - auto path_id = workspace.path_pool.intern(path); + auto path_id = srv.workspace.path_pool.intern(path); int offset_val = std::max(0, params.offset.value_or(0)); constexpr int page_size = 10; ext::QueryContextResult result; std::vector all_items; - auto hosts = workspace.dep_graph.find_host_sources(path_id); + auto& ws = srv.workspace; + auto hosts = ws.dep_graph.find_host_sources(path_id); for(auto host_id: hosts) { - auto host_path = workspace.path_pool.resolve(host_id); - auto host_cdb = workspace.cdb.lookup(host_path, {.suppress_logging = true}); + auto host_path = ws.path_pool.resolve(host_id); + auto host_cdb = ws.cdb.lookup(host_path, {.suppress_logging = true}); if(host_cdb.empty()) continue; auto host_uri_opt = lsp::URI::from_file_path(std::string(host_path)); @@ -824,7 +585,7 @@ void MasterServer::register_handlers() { } if(hosts.empty()) { - auto entries = workspace.cdb.lookup(path, {.suppress_logging = true}); + auto entries = ws.cdb.lookup(path, {.suppress_logging = true}); for(std::size_t i = 0; i < entries.size(); ++i) { auto& cmd = entries[i]; auto argv = cmd.to_argv(); @@ -866,13 +627,14 @@ void MasterServer::register_handlers() { peer.on_request( "clice/currentContext", [this](RequestContext& ctx, const ext::CurrentContextParams& params) -> RawResult { + auto& srv = this->server; auto path = uri_to_path(params.uri); - auto path_id = workspace.path_pool.intern(path); + auto path_id = srv.workspace.path_pool.intern(path); ext::CurrentContextResult result; - auto sit = sessions.find(path_id); - if(sit != sessions.end() && sit->second.active_context) { - auto ctx_path = workspace.path_pool.resolve(*sit->second.active_context); + auto* session = srv.find_session(path_id); + if(session && session->active_context) { + auto ctx_path = srv.workspace.path_pool.resolve(*session->active_context); auto ctx_uri_opt = lsp::URI::from_file_path(std::string(ctx_path)); if(ctx_uri_opt) { ext::ContextItem item; @@ -888,34 +650,41 @@ void MasterServer::register_handlers() { peer.on_request( "clice/switchContext", [this](RequestContext& ctx, const ext::SwitchContextParams& params) -> RawResult { + auto& srv = this->server; auto path = uri_to_path(params.uri); - auto path_id = workspace.path_pool.intern(path); + auto path_id = srv.workspace.path_pool.intern(path); auto context_path = uri_to_path(params.context_uri); - auto context_path_id = workspace.path_pool.intern(context_path); + auto context_path_id = srv.workspace.path_pool.intern(context_path); ext::SwitchContextResult result; - auto context_cdb = workspace.cdb.lookup(context_path, {.suppress_logging = true}); + auto& ws = srv.workspace; + auto context_cdb = ws.cdb.lookup(context_path, {.suppress_logging = true}); if(context_cdb.empty()) { result.success = false; co_return to_raw(result); } - auto sit = sessions.find(path_id); - if(sit == sessions.end()) { + auto* session = srv.find_session(path_id); + if(!session) { result.success = false; co_return to_raw(result); } - sit->second.active_context = context_path_id; - sit->second.header_context.reset(); - sit->second.pch_ref.reset(); - sit->second.ast_deps.reset(); - sit->second.ast_dirty = true; + session->active_context = context_path_id; + session->header_context.reset(); + session->pch_ref.reset(); + session->ast_deps.reset(); + session->ast_dirty = true; result.success = true; co_return to_raw(result); }); } +LSPClient::~LSPClient() { + server.compiler.set_peer(nullptr); + server.indexer.set_peer(nullptr); +} + } // namespace clice diff --git a/src/server/service/lsp_client.h b/src/server/service/lsp_client.h new file mode 100644 index 000000000..9e8a449ef --- /dev/null +++ b/src/server/service/lsp_client.h @@ -0,0 +1,23 @@ +#pragma once + +#include "kota/async/async.h" +#include "kota/codec/json/json.h" +#include "kota/ipc/codec/json.h" + +namespace clice { + +class MasterServer; + +class LSPClient { +public: + LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer); + ~LSPClient(); + +private: + using RawResult = kota::task; + + MasterServer& server; + kota::ipc::JsonPeer& peer; +}; + +} // namespace clice diff --git a/src/server/service/master_server.cpp b/src/server/service/master_server.cpp new file mode 100644 index 000000000..9e6fbbc9a --- /dev/null +++ b/src/server/service/master_server.cpp @@ -0,0 +1,387 @@ +#include "server/service/master_server.h" + +#include +#include +#include +#include + +#include "server/protocol/worker.h" +#include "server/service/agent_client.h" +#include "server/service/lsp_client.h" +#include "support/filesystem.h" +#include "support/logging.h" + +#include "kota/async/async.h" +#include "kota/codec/json/json.h" +#include "kota/ipc/codec/json.h" +#include "kota/ipc/lsp/protocol.h" +#include "kota/ipc/lsp/uri.h" +#include "kota/ipc/recording_transport.h" +#include "kota/ipc/transport.h" +#include "llvm/Support/FileSystem.h" +#include "llvm/Support/Process.h" + +namespace clice { + +namespace lsp = kota::ipc::lsp; +namespace protocol = kota::ipc::protocol; + +MasterServer::MasterServer(kota::event_loop& loop, std::string self_path) : + loop(loop), pool(loop), compiler(loop, workspace, pool, sessions), + indexer(loop, + workspace, + sessions, + pool, + compiler, + [this](uint32_t proj_path_id) { + auto path = workspace.project_index.path_pool.path(proj_path_id); + auto server_id = workspace.path_pool.intern(path); + return sessions.contains(server_id); + }), + self_path(std::move(self_path)) {} + +MasterServer::~MasterServer() = default; + +void MasterServer::initialize() { + workspace.config = Config::load_from_workspace(workspace_root); + if(!init_options_json.empty()) { + if(auto ov = kota::codec::json::parse(init_options_json, workspace.config); !ov) { + LOG_WARN("Failed to apply initializationOptions: {}", ov.error().to_string()); + } else { + workspace.config.apply_defaults(workspace_root); + LOG_INFO("Applied initializationOptions overlay"); + } + init_options_json.clear(); + } + + auto& cfg = workspace.config.project; + + if(!cfg.logging_dir.empty()) { + auto now = std::chrono::system_clock::now(); + auto pid = llvm::sys::Process::getProcessId(); + session_log_dir = + path::join(cfg.logging_dir, std::format("{:%Y-%m-%d_%H-%M-%S}_{}", now, pid)); + logging::file_logger("master", session_log_dir, logging::options); + } + + LOG_INFO("Server ready (stateful={}, stateless={}, idle={}ms)", + cfg.stateful_worker_count.value, + cfg.stateless_worker_count.value, + *cfg.idle_timeout_ms); + + WorkerPoolOptions pool_opts; + pool_opts.self_path = self_path; + pool_opts.stateful_count = cfg.stateful_worker_count; + pool_opts.stateless_count = cfg.stateless_worker_count; + pool_opts.worker_memory_limit = cfg.worker_memory_limit; + pool_opts.log_dir = session_log_dir; + if(!pool.start(pool_opts)) { + LOG_ERROR("Failed to start worker pool"); + return; + } + + lifecycle = ServerLifecycle::Ready; + + compiler.on_indexing_needed = [this]() { + indexer.schedule(); + }; + + indexer.set_max_concurrency(cfg.stateless_worker_count.value); + + load_workspace(); +} + +Session* MasterServer::find_session(std::uint32_t path_id) { + auto it = sessions.find(path_id); + return it != sessions.end() ? &it->second : nullptr; +} + +Session& MasterServer::open_session(std::uint32_t path_id) { + auto [it, inserted] = sessions.try_emplace(path_id); + auto& session = it->second; + if(!inserted) + session = Session{}; + session.path_id = path_id; + return session; +} + +void MasterServer::close_session(std::uint32_t path_id, kota::ipc::JsonPeer& peer) { + namespace protocol = kota::ipc::protocol; + + auto path = workspace.path_pool.resolve(path_id); + workspace.on_file_closed(path_id); + pool.notify_stateful(path_id, worker::EvictParams{std::string(path)}); + + protocol::PublishDiagnosticsParams diag_params; + auto uri = lsp::URI::from_file_path(std::string(path)); + if(uri) + diag_params.uri = uri->str(); + diag_params.diagnostics = {}; + peer.send_notification(diag_params); + + sessions.erase(path_id); + + indexer.enqueue(path_id); + indexer.schedule(); + + LOG_DEBUG("didClose: {}", path); +} + +void MasterServer::on_file_saved(std::uint32_t path_id) { + auto dirtied = workspace.on_file_saved(path_id); + for(auto dirty_id: dirtied) { + if(auto* session = find_session(dirty_id)) { + session->ast_dirty = true; + } else { + indexer.enqueue(dirty_id); + } + } + + for(auto& [hdr_id, session]: sessions) { + if(session.header_context && session.header_context->host_path_id == path_id) { + session.header_context.reset(); + session.ast_dirty = true; + } + } + + indexer.schedule(); +} + +void MasterServer::schedule_shutdown() { + indexer.save(workspace.config.project.index_dir); + workspace.save_cache(); + + loop.schedule([this]() -> kota::task<> { + co_await compiler.stop(); + co_await pool.stop(); + loop.stop(); + }()); +} + +void MasterServer::load_workspace() { + if(workspace_root.empty()) + return; + + auto& cfg = workspace.config.project; + + if(!cfg.cache_dir.empty()) { + auto ec = llvm::sys::fs::create_directories(cfg.cache_dir); + if(ec) { + LOG_WARN("Failed to create cache directory {}: {}", + std::string_view(cfg.cache_dir), + ec.message()); + } else { + LOG_INFO("Cache directory: {}", std::string_view(cfg.cache_dir)); + } + + for(auto* subdir: {"cache/pch", "cache/pcm"}) { + auto dir = path::join(cfg.cache_dir, subdir); + if(auto ec2 = llvm::sys::fs::create_directories(dir)) + LOG_WARN("Failed to create {}: {}", dir, ec2.message()); + } + + workspace.cleanup_cache(); + workspace.load_cache(); + } + + std::string cdb_path; + for(auto& configured: cfg.compile_commands_paths) { + if(llvm::sys::fs::is_directory(configured)) { + auto candidate = path::join(configured, "compile_commands.json"); + if(llvm::sys::fs::exists(candidate)) { + cdb_path = std::move(candidate); + break; + } + } else if(llvm::sys::fs::exists(configured)) { + cdb_path = configured; + break; + } else { + LOG_WARN("Configured compile_commands_path not found: {}", configured); + } + } + + if(cdb_path.empty()) { + auto try_candidate = [&](llvm::StringRef dir) -> bool { + auto candidate = path::join(dir, "compile_commands.json"); + if(llvm::sys::fs::exists(candidate)) { + cdb_path = std::move(candidate); + return true; + } + return false; + }; + + if(!try_candidate(workspace_root)) { + std::error_code ec; + for(llvm::sys::fs::directory_iterator it(workspace_root, ec), end; it != end && !ec; + it.increment(ec)) { + if(it->type() == llvm::sys::fs::file_type::directory_file) { + if(try_candidate(it->path())) + break; + } + } + } + } + + if(cdb_path.empty()) { + LOG_WARN("No compile_commands.json found in workspace {}", workspace_root); + return; + } + + auto count = workspace.cdb.load(cdb_path); + LOG_INFO("Loaded CDB from {} with {} entries", cdb_path, count); + + auto report = scan_dependency_graph(workspace.cdb, + workspace.path_pool, + workspace.dep_graph, + /*cache=*/nullptr, + [this](llvm::StringRef path, + std::vector& append, + std::vector& remove) { + workspace.config.match_rules(path, append, remove); + }); + workspace.dep_graph.build_reverse_map(); + + auto unresolved = report.includes_found - report.includes_resolved; + double accuracy = + report.includes_found > 0 + ? 100.0 * static_cast(report.includes_resolved) / report.includes_found + : 100.0; + LOG_INFO( + "Dependency scan: {}ms, {} files ({} source + {} header), " "{} edges, {}/{} resolved ({:.1f}%), {} waves", + report.elapsed_ms, + report.total_files, + report.source_files, + report.header_files, + report.total_edges, + report.includes_resolved, + report.includes_found, + accuracy, + report.waves); + if(unresolved > 0) + LOG_WARN("{} unresolved includes", unresolved); + + workspace.build_module_map(); + indexer.load(cfg.index_dir); + + if(*cfg.enable_indexing) { + for(auto& entry: workspace.cdb.get_entries()) { + auto file = workspace.cdb.resolve_path(entry.file); + auto server_id = workspace.path_pool.intern(file); + indexer.enqueue(server_id); + } + indexer.schedule(); + } + + compiler.init_compile_graph(); +} + +struct Connection { + std::unique_ptr peer; + std::unique_ptr lsp_client; + std::unique_ptr agent_client; +}; + +static kota::task<> run_connection(kota::ipc::JsonPeer* peer, + std::list& connections, + std::list::iterator pos) { + co_await peer->run(); + LOG_INFO("Client disconnected"); + connections.erase(pos); +} + +static kota::task<> accept_connections(MasterServer& server, + kota::tcp::acceptor acceptor, + bool register_lsp, + std::list& connections) { + auto& loop = kota::event_loop::current(); + kota::task_group<> connection_group(loop); + bool lsp_registered = false; + + while(true) { + auto conn = co_await acceptor.accept(); + if(!conn.has_value()) + break; + + LOG_INFO("Client connected"); + + auto transport = std::make_unique(std::move(*conn)); + auto peer = std::make_unique(loop, std::move(transport)); + + std::unique_ptr lsp; + if(register_lsp && !lsp_registered) { + lsp = std::make_unique(server, *peer); + lsp_registered = true; + } + auto agent = std::make_unique(server, *peer); + + auto* peer_ptr = peer.get(); + auto it = connections.emplace(connections.end(), + Connection{ + .peer = std::move(peer), + .lsp_client = std::move(lsp), + .agent_client = std::move(agent), + }); + + connection_group.spawn(run_connection(peer_ptr, connections, it)); + } + + co_await connection_group.join(); +} + +int run_server_mode(const ServerOptions& opts) { + logging::stderr_logger("master", logging::options); + + kota::event_loop loop; + MasterServer server(loop, opts.self_path); + std::list connections; + + if(opts.mode == "pipe") { + auto transport = kota::ipc::StreamTransport::open_stdio(loop); + if(!transport) { + LOG_ERROR("failed to open stdio transport"); + return 1; + } + + std::unique_ptr final_transport = std::move(*transport); + if(!opts.record.empty()) { + final_transport = + std::make_unique(std::move(final_transport), + opts.record); + } + + kota::ipc::JsonPeer lsp_peer(loop, std::move(final_transport)); + LSPClient lsp_client(server, lsp_peer); + + if(opts.port > 0) { + auto acceptor = kota::tcp::listen(opts.host, opts.port, {}, loop); + if(acceptor) { + LOG_INFO("Agentic protocol listening on {}:{}", opts.host, opts.port); + loop.schedule(accept_connections(server, std::move(*acceptor), false, connections)); + } else { + LOG_WARN("Failed to start agentic listener on {}:{}", opts.host, opts.port); + } + } + + loop.schedule(lsp_peer.run()); + loop.run(); + return 0; + } + + if(opts.mode == "socket") { + auto acceptor = kota::tcp::listen(opts.host, opts.port, {}, loop); + if(!acceptor) { + LOG_ERROR("failed to listen on {}:{}", opts.host, opts.port); + return 1; + } + + LOG_INFO("Listening on {}:{} ...", opts.host, opts.port); + loop.schedule(accept_connections(server, std::move(*acceptor), true, connections)); + loop.run(); + return 0; + } + + LOG_ERROR("unknown server mode '{}'", opts.mode); + return 1; +} + +} // namespace clice diff --git a/src/server/service/master_server.h b/src/server/service/master_server.h new file mode 100644 index 000000000..4e7566ee6 --- /dev/null +++ b/src/server/service/master_server.h @@ -0,0 +1,77 @@ +#pragma once + +#include +#include + +#include "server/compiler/compiler.h" +#include "server/compiler/indexer.h" +#include "server/service/session.h" +#include "server/worker/worker_pool.h" +#include "server/workspace/workspace.h" + +#include "kota/async/async.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/StringRef.h" + +namespace clice { + +enum class ServerLifecycle : std::uint8_t { + Uninitialized, + Initialized, + Ready, + ShuttingDown, + Exited, +}; + +/// Core server state — owns the two-layer state model (Workspace + Sessions), +/// the worker pool, compilation engine, and indexer. +/// +/// Does NOT own any transport or peer. Protocol-specific handler registration +/// is done by LSPClient and AgentClient, which access private members directly. +class MasterServer { + friend class LSPClient; + friend class AgentClient; + +public: + MasterServer(kota::event_loop& loop, std::string self_path); + ~MasterServer(); + + void initialize(); + + Session* find_session(std::uint32_t path_id); + Session& open_session(std::uint32_t path_id); + void close_session(std::uint32_t path_id, kota::ipc::JsonPeer& peer); + + void on_file_saved(std::uint32_t path_id); + + void schedule_shutdown(); + +private: + void load_workspace(); + + kota::event_loop& loop; + + Workspace workspace; + llvm::DenseMap sessions; + WorkerPool pool; + Compiler compiler; + Indexer indexer; + + ServerLifecycle lifecycle = ServerLifecycle::Uninitialized; + std::string self_path; + std::string workspace_root; + std::string session_log_dir; + std::string init_options_json; +}; + +struct ServerOptions { + std::string mode; + std::string host = "127.0.0.1"; + int port = 0; + std::string self_path; + std::string record; +}; + +int run_server_mode(const ServerOptions& opts); + +} // namespace clice diff --git a/src/server/session.h b/src/server/service/session.h similarity index 98% rename from src/server/session.h rename to src/server/service/session.h index 487ed38f3..0d66c688c 100644 --- a/src/server/session.h +++ b/src/server/service/session.h @@ -5,7 +5,7 @@ #include #include -#include "server/workspace.h" +#include "server/workspace/workspace.h" #include "kota/async/async.h" #include "llvm/ADT/SmallVector.h" diff --git a/src/server/stateful_worker.cpp b/src/server/worker/stateful_worker.cpp similarity index 98% rename from src/server/stateful_worker.cpp rename to src/server/worker/stateful_worker.cpp index 8337a0eab..3cda3289f 100644 --- a/src/server/stateful_worker.cpp +++ b/src/server/worker/stateful_worker.cpp @@ -1,4 +1,4 @@ -#include "server/stateful_worker.h" +#include "server/worker/stateful_worker.h" #include #include @@ -10,8 +10,8 @@ #include "compile/compilation.h" #include "feature/feature.h" #include "index/tu_index.h" -#include "server/protocol.h" -#include "server/worker_common.h" +#include "server/protocol/worker.h" +#include "server/worker/worker_common.h" #include "support/logging.h" #include "kota/async/async.h" diff --git a/src/server/stateful_worker.h b/src/server/worker/stateful_worker.h similarity index 100% rename from src/server/stateful_worker.h rename to src/server/worker/stateful_worker.h diff --git a/src/server/stateless_worker.cpp b/src/server/worker/stateless_worker.cpp similarity index 98% rename from src/server/stateless_worker.cpp rename to src/server/worker/stateless_worker.cpp index 7d85f8fcf..91a537042 100644 --- a/src/server/stateless_worker.cpp +++ b/src/server/worker/stateless_worker.cpp @@ -1,10 +1,10 @@ -#include "server/stateless_worker.h" +#include "server/worker/stateless_worker.h" #include "compile/compilation.h" #include "feature/feature.h" #include "index/tu_index.h" -#include "server/protocol.h" -#include "server/worker_common.h" +#include "server/protocol/worker.h" +#include "server/worker/worker_common.h" #include "support/logging.h" #include "kota/async/async.h" diff --git a/src/server/stateless_worker.h b/src/server/worker/stateless_worker.h similarity index 100% rename from src/server/stateless_worker.h rename to src/server/worker/stateless_worker.h diff --git a/src/server/worker_common.h b/src/server/worker/worker_common.h similarity index 100% rename from src/server/worker_common.h rename to src/server/worker/worker_common.h diff --git a/src/server/worker_pool.cpp b/src/server/worker/worker_pool.cpp similarity index 93% rename from src/server/worker_pool.cpp rename to src/server/worker/worker_pool.cpp index e4beb9725..fccd1af8a 100644 --- a/src/server/worker_pool.cpp +++ b/src/server/worker/worker_pool.cpp @@ -1,4 +1,4 @@ -#include "server/worker_pool.h" +#include "server/worker/worker_pool.h" #include #include @@ -108,7 +108,6 @@ bool WorkerPool::spawn_worker(const std::string& self_path, auto& w = workers.back(); w.alive = true; - ++alive_count_; loop.schedule(w.peer->run()); return true; @@ -122,14 +121,14 @@ bool WorkerPool::start(const WorkerPoolOptions& options) { if(!spawn_worker(options.self_path, false, 0)) { return false; } - loop.schedule(monitor_worker(stateless_workers.size() - 1, false)); + monitor_group.spawn(monitor_worker(stateless_workers.size() - 1, false)); } for(std::uint32_t i = 0; i < options.stateful_count; ++i) { if(!spawn_worker(options.self_path, true, options.worker_memory_limit)) { return false; } - loop.schedule(monitor_worker(stateful_workers.size() - 1, true)); + monitor_group.spawn(monitor_worker(stateful_workers.size() - 1, true)); } // Register evicted notification handler for each stateful worker @@ -151,23 +150,17 @@ kota::task<> WorkerPool::stop() { LOG_INFO("WorkerPool stopping..."); shutting_down_ = true; - // Close output pipes to signal workers to exit gracefully. for(auto& w: stateless_workers) w.peer->close_output(); for(auto& w: stateful_workers) w.peer->close_output(); - // Send SIGTERM. monitor_worker coroutines handle the wait. for(auto& w: stateless_workers) w.proc.kill(SIGTERM); for(auto& w: stateful_workers) w.proc.kill(SIGTERM); - // Wait until all monitor_worker coroutines have finished. - if(alive_count_ > 0) { - all_exited_.reset(); - co_await all_exited_.wait(); - } + co_await monitor_group.join(); LOG_INFO("WorkerPool stopped"); } @@ -242,13 +235,9 @@ kota::task<> WorkerPool::monitor_worker(std::size_t index, bool stateful) { auto result = co_await w.proc.wait(); w.alive = false; - --alive_count_; - if(shutting_down_) { - if(alive_count_ == 0) - all_exited_.set(); + if(shutting_down_) co_return; - } if(result.has_value()) { auto& exit = result.value(); @@ -342,7 +331,6 @@ bool WorkerPool::respawn_worker(std::size_t index, bool stateful) { }; auto& w = workers[index]; - ++alive_count_; loop.schedule(w.peer->run()); if(stateful) { @@ -352,7 +340,7 @@ bool WorkerPool::respawn_worker(std::size_t index, bool stateful) { }); } - loop.schedule(monitor_worker(index, stateful)); + monitor_group.spawn(monitor_worker(index, stateful)); LOG_INFO("Worker {} restarted (attempt {})", worker_name, old_restart_count); return true; diff --git a/src/server/worker_pool.h b/src/server/worker/worker_pool.h similarity index 97% rename from src/server/worker_pool.h rename to src/server/worker/worker_pool.h index 0ba58734c..c6948989f 100644 --- a/src/server/worker_pool.h +++ b/src/server/worker/worker_pool.h @@ -6,7 +6,7 @@ #include #include -#include "server/protocol.h" +#include "server/protocol/worker.h" #include "kota/async/async.h" #include "kota/ipc/codec/bincode.h" @@ -83,8 +83,7 @@ class WorkerPool { std::size_t pick_least_loaded(); bool shutting_down_ = false; - std::size_t alive_count_ = 0; - kota::event all_exited_{true}; // Signalled when alive_count_ reaches 0. + kota::task_group<> monitor_group{loop}; WorkerPoolOptions options_; std::string log_dir_; diff --git a/src/server/config.cpp b/src/server/workspace/config.cpp similarity index 99% rename from src/server/config.cpp rename to src/server/workspace/config.cpp index ed767be23..ba44088d9 100644 --- a/src/server/config.cpp +++ b/src/server/workspace/config.cpp @@ -1,4 +1,4 @@ -#include "server/config.h" +#include "server/workspace/config.h" #include diff --git a/src/server/config.h b/src/server/workspace/config.h similarity index 100% rename from src/server/config.h rename to src/server/workspace/config.h diff --git a/src/server/workspace.cpp b/src/server/workspace/workspace.cpp similarity index 99% rename from src/server/workspace.cpp rename to src/server/workspace/workspace.cpp index 3781b7846..5a9640595 100644 --- a/src/server/workspace.cpp +++ b/src/server/workspace/workspace.cpp @@ -1,4 +1,4 @@ -#include "server/workspace.h" +#include "server/workspace/workspace.h" #include #include diff --git a/src/server/workspace.h b/src/server/workspace/workspace.h similarity index 99% rename from src/server/workspace.h rename to src/server/workspace/workspace.h index 75a89ed28..5023eecd1 100644 --- a/src/server/workspace.h +++ b/src/server/workspace/workspace.h @@ -11,8 +11,8 @@ #include "index/merged_index.h" #include "index/project_index.h" #include "semantic/relation_kind.h" -#include "server/compile_graph.h" -#include "server/config.h" +#include "server/compiler/compile_graph.h" +#include "server/workspace/config.h" #include "support/path_pool.h" #include "syntax/dependency_graph.h" diff --git a/tests/conftest.py b/tests/conftest.py index ada56698a..d50097d42 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,7 @@ import asyncio import json import shutil +import socket import subprocess import sys from pathlib import Path @@ -93,17 +94,16 @@ def workspace(request: pytest.FixtureRequest, test_data_dir: Path) -> Path | Non @pytest.fixture async def client( - request: pytest.FixtureRequest, executable: Path, workspace: Path | None + request: pytest.FixtureRequest, + executable: Path, + workspace: Path | None, ): """Spawn clice server, auto-initialize if @pytest.mark.workspace is present.""" config = request.config mode = config.getoption("--mode") + host = config.getoption("--host") - cmd = [str(executable), "--mode", mode] - if mode == "socket": - host = config.getoption("--host") - port = config.getoption("--port") - cmd += ["--host", host, "--port", str(port)] + cmd = [str(executable), "--mode", mode, "--host", host] c = CliceClient() await c.start_io(*cmd) @@ -122,6 +122,39 @@ async def client( await _shutdown_client(c) +def _find_free_port() -> int: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(("127.0.0.1", 0)) + return s.getsockname()[1] + + +@pytest.fixture +async def agentic( + request: pytest.FixtureRequest, + executable: Path, + workspace: Path | None, +): + """Start a server with agentic TCP port, yield (executable, host, port).""" + host = "127.0.0.1" + port = _find_free_port() + cmd = [str(executable), "--mode", "pipe", "--host", host, "--port", str(port)] + + c = CliceClient() + await c.start_io(*cmd) + + if workspace is not None: + init_options_marker = request.node.get_closest_marker("init_options") + init_options = dict(init_options_marker.args[0]) if init_options_marker else {} + project = dict(init_options.get("project", {})) + project.setdefault("cache_dir", str(workspace / ".clice")) + init_options["project"] = project + await c.initialize(workspace, initialization_options=init_options) + + yield executable, host, port + + await _shutdown_client(c) + + def generate_cdb(workspace: Path) -> None: """Generate compile_commands.json using CMake with Ninja backend.""" cmake = shutil.which("cmake") diff --git a/tests/integration/agentic/__init__.py b/tests/integration/agentic/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/integration/agentic/test_agentic.py b/tests/integration/agentic/test_agentic.py new file mode 100644 index 000000000..3eff6c074 --- /dev/null +++ b/tests/integration/agentic/test_agentic.py @@ -0,0 +1,87 @@ +"""Tests for the agentic CLI client.""" + +import json +import socket +import subprocess +from concurrent.futures import ThreadPoolExecutor + +import pytest + + +def run_agentic(executable, host, port, path, timeout=10): + result = subprocess.run( + [ + str(executable), + "--mode", + "agentic", + "--host", + host, + "--port", + str(port), + "--path", + path, + ], + capture_output=True, + text=True, + timeout=timeout, + ) + return result + + +@pytest.mark.workspace("hello_world") +async def test_compile_command(agentic, workspace): + executable, host, port = agentic + main_cpp = (workspace / "main.cpp").as_posix() + result = run_agentic(executable, host, port, main_cpp) + assert result.returncode == 0, f"stderr: {result.stderr}" + data = json.loads(result.stdout) + assert data["file"] == main_cpp + assert data["directory"] == workspace.as_posix() + assert len(data["arguments"]) > 0 + + +@pytest.mark.workspace("hello_world") +async def test_compile_command_fallback(agentic, workspace): + executable, host, port = agentic + result = run_agentic(executable, host, port, "/nonexistent/file.cpp") + assert result.returncode == 0, f"stderr: {result.stderr}" + data = json.loads(result.stdout) + assert data["file"] == "/nonexistent/file.cpp" + + +@pytest.mark.workspace("hello_world") +async def test_multiple_requests(agentic, workspace): + executable, host, port = agentic + main_cpp = (workspace / "main.cpp").as_posix() + for _ in range(3): + result = run_agentic(executable, host, port, main_cpp) + assert result.returncode == 0, f"stderr: {result.stderr}" + data = json.loads(result.stdout) + assert data["file"] == main_cpp + + +async def test_connection_refused(executable): + """Connecting to a port with no server should fail with non-zero exit.""" + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(("127.0.0.1", 0)) + free_port = s.getsockname()[1] + result = run_agentic(executable, "127.0.0.1", free_port, "/some/file.cpp") + assert result.returncode != 0 + + +@pytest.mark.workspace("hello_world") +async def test_concurrent_connections(agentic, workspace): + """Multiple agentic clients connecting simultaneously should all succeed.""" + executable, host, port = agentic + main_cpp = (workspace / "main.cpp").as_posix() + + def do_request(_): + return run_agentic(executable, host, port, main_cpp) + + with ThreadPoolExecutor(max_workers=4) as pool: + results = list(pool.map(do_request, range(4))) + + for r in results: + assert r.returncode == 0, f"stderr: {r.stderr}" + data = json.loads(r.stdout) + assert data["file"] == main_cpp diff --git a/tests/integration/utils/client.py b/tests/integration/utils/client.py index 7b4319c04..58bcace66 100644 --- a/tests/integration/utils/client.py +++ b/tests/integration/utils/client.py @@ -92,13 +92,18 @@ async def initialize( *, initialization_options: dict | None = None, ) -> InitializeResult: + if initialization_options is None: + initialization_options = {} + project = dict(initialization_options.get("project", {})) + project.setdefault("cache_dir", str(workspace / ".clice")) + initialization_options["project"] = project + params = InitializeParams( capabilities=ClientCapabilities(), root_uri=workspace.as_uri(), workspace_folders=[WorkspaceFolder(uri=workspace.as_uri(), name="test")], ) - if initialization_options is not None: - params.initialization_options = initialization_options + params.initialization_options = initialization_options result = await self.initialize_async(params) self.initialized(InitializedParams()) self.init_result = result diff --git a/tests/unit/semantic/template_resolver_tests.cpp b/tests/unit/semantic/template_resolver_tests.cpp index 6bfe3c067..3ca2a4744 100644 --- a/tests/unit/semantic/template_resolver_tests.cpp +++ b/tests/unit/semantic/template_resolver_tests.cpp @@ -456,8 +456,6 @@ TEST_CASE(BasePackExpansion) { )code"); } -// --- Robustness tests for edge cases found during stress testing --- - TEST_CASE(RecursiveBaseClass) { // Regression test: callback_traits inherits callback_traits, // creating infinite recursion through lookupInBases. CTD cycle detection must bail out. diff --git a/tests/unit/server/compile_graph_integration_tests.cpp b/tests/unit/server/compile_graph_integration_tests.cpp index 559e0557f..e1c768f57 100644 --- a/tests/unit/server/compile_graph_integration_tests.cpp +++ b/tests/unit/server/compile_graph_integration_tests.cpp @@ -3,7 +3,7 @@ #include "test/test.h" #include "command/command.h" #include "compile/compilation.h" -#include "server/compile_graph.h" +#include "server/compiler/compile_graph.h" #include "support/path_pool.h" #include "syntax/dependency_graph.h" #include "syntax/scan.h" diff --git a/tests/unit/server/compile_graph_tests.cpp b/tests/unit/server/compile_graph_tests.cpp index 5b562e672..680927f11 100644 --- a/tests/unit/server/compile_graph_tests.cpp +++ b/tests/unit/server/compile_graph_tests.cpp @@ -1,7 +1,7 @@ #include #include "test/test.h" -#include "server/compile_graph.h" +#include "server/compiler/compile_graph.h" namespace clice::testing { namespace { diff --git a/tests/unit/server/config_tests.cpp b/tests/unit/server/config_tests.cpp index d9181fa8a..8fd9adcae 100644 --- a/tests/unit/server/config_tests.cpp +++ b/tests/unit/server/config_tests.cpp @@ -2,7 +2,7 @@ #include "test/temp_dir.h" #include "test/test.h" -#include "server/config.h" +#include "server/workspace/config.h" #include "support/filesystem.h" #include "kota/codec/json/json.h" diff --git a/tests/unit/server/module_worker_tests.cpp b/tests/unit/server/module_worker_tests.cpp index d9e73251b..5d115ded9 100644 --- a/tests/unit/server/module_worker_tests.cpp +++ b/tests/unit/server/module_worker_tests.cpp @@ -2,7 +2,7 @@ #include #include "test/test.h" -#include "server/protocol.h" +#include "server/protocol/worker.h" #include "server/worker_test_helpers.h" namespace clice::testing { @@ -29,7 +29,6 @@ TEST_CASE(BuildPCMThenCompileWithImport) { tmp.touch("consumer.cpp", "import Hello;\n" "int main() { return hello()[0]; }\n"); auto consumer = tmp.path("consumer.cpp"); - // --- Phase 1: Build PCM via stateless worker --- WorkerHandle sl; ASSERT_TRUE(sl.spawn("stateless-worker")); @@ -63,7 +62,6 @@ TEST_CASE(BuildPCMThenCompileWithImport) { ASSERT_TRUE(phase1_done); ASSERT_FALSE(pcm_path.empty()); - // --- Phase 2: Compile consumer with the PCM via stateful worker --- WorkerHandle sf; ASSERT_TRUE(sf.spawn("stateful-worker")); diff --git a/tests/unit/server/pch_worker_tests.cpp b/tests/unit/server/pch_worker_tests.cpp index 37273e3c5..2d2a1cae0 100644 --- a/tests/unit/server/pch_worker_tests.cpp +++ b/tests/unit/server/pch_worker_tests.cpp @@ -2,7 +2,7 @@ #include #include "test/test.h" -#include "server/protocol.h" +#include "server/protocol/worker.h" #include "server/worker_test_helpers.h" #include "syntax/scan.h" @@ -30,7 +30,6 @@ TEST_CASE(BuildPCHThenCompile) { auto dir = std::string(tmp.root); - // --- Phase 1: Build PCH via stateless worker --- WorkerHandle sl; ASSERT_TRUE(sl.spawn("stateless-worker")); @@ -69,7 +68,6 @@ TEST_CASE(BuildPCHThenCompile) { // Verify the PCH file exists on disk. ASSERT_TRUE(llvm::sys::fs::exists(pch_path)); - // --- Phase 2: Compile with PCH via stateful worker --- WorkerHandle sf; ASSERT_TRUE(sf.spawn("stateful-worker")); diff --git a/tests/unit/server/stateful_worker_tests.cpp b/tests/unit/server/stateful_worker_tests.cpp index d16f6c622..a3ed6a4ac 100644 --- a/tests/unit/server/stateful_worker_tests.cpp +++ b/tests/unit/server/stateful_worker_tests.cpp @@ -2,7 +2,7 @@ #include #include "test/test.h" -#include "server/protocol.h" +#include "server/protocol/worker.h" #include "server/worker_test_helpers.h" #include "kota/codec/json/json.h" diff --git a/tests/unit/server/stateless_worker_tests.cpp b/tests/unit/server/stateless_worker_tests.cpp index 0cc8b4848..4782c48e4 100644 --- a/tests/unit/server/stateless_worker_tests.cpp +++ b/tests/unit/server/stateless_worker_tests.cpp @@ -2,7 +2,7 @@ #include #include "test/test.h" -#include "server/protocol.h" +#include "server/protocol/worker.h" #include "server/worker_test_helpers.h" #include "kota/codec/bincode/bincode.h" diff --git a/tests/unit/server/worker_test_helpers.h b/tests/unit/server/worker_test_helpers.h index 729c87dc9..6108b58e3 100644 --- a/tests/unit/server/worker_test_helpers.h +++ b/tests/unit/server/worker_test_helpers.h @@ -11,7 +11,7 @@ #include "test/temp_dir.h" #include "command/argument_parser.h" #include "command/command.h" -#include "server/protocol.h" +#include "server/protocol/worker.h" #include "support/filesystem.h" #include "kota/async/async.h" diff --git a/tests/unit/syntax/scan_tests.cpp b/tests/unit/syntax/scan_tests.cpp index 347fd2843..cca19291f 100644 --- a/tests/unit/syntax/scan_tests.cpp +++ b/tests/unit/syntax/scan_tests.cpp @@ -291,8 +291,6 @@ int x; TEST_SUITE(PreambleComplete) { -// --- #include completeness --- - TEST_CASE(CompleteQuotedInclude) { llvm::StringRef content = "#include \"foo.h\"\nint x;"; auto bound = compute_preamble_bound(content); @@ -341,8 +339,7 @@ TEST_CASE(MultipleIncludesLastIncomplete) { EXPECT_FALSE(is_preamble_complete(content, bound)); } -// --- C++20 module statements --- -// Note: compute_preamble_bound does not include import/export lines in its +// compute_preamble_bound does not include import/export lines in its // bound, so we pass manual bounds covering the relevant lines. TEST_CASE(CompleteImport) { @@ -381,8 +378,6 @@ TEST_CASE(CompleteExportImport) { EXPECT_TRUE(is_preamble_complete(content, 19)); } -// --- Edge cases --- - TEST_CASE(EmptyPreamble) { llvm::StringRef content = "int x;"; EXPECT_TRUE(is_preamble_complete(content, 0));