From bb0b160a28fceb3e718952bf72a12a99cffd808e Mon Sep 17 00:00:00 2001 From: ykiko Date: Tue, 7 Apr 2026 13:30:12 +0800 Subject: [PATCH] refactor(server): extract Indexer and Compiler from MasterServer (#403) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - **Extract `Indexer` class** — owns all index state (ProjectIndex, MergedIndex shards, OpenFileIndex) and query methods (definition, references, call/type hierarchy, workspace symbol search) - **Extract `Compiler` class** — owns document state, PCH/PCM cache, compile argument resolution, header context, `ensure_compiled`, and worker forwarding - **MasterServer is now a pure LSP handler registration layer** (~700 lines, down from ~3200) - **`MergedIndexShard`** wraps `index::MergedIndex` with a lazily-cached PositionMapper; `OpenFileIndex` gains matching `find_occurrence()`/`find_relations()` APIs — callers get pre-converted LSP ranges directly - **Indexer returns typed values** (`vector`, `vector`, etc.) instead of pre-serialized JSON, fixing the references handler from JSON string surgery to simple vector concatenation - **Fix**: duplicate `workspace/symbol` loop in the original code ## Test plan - [x] 465 unit tests pass - [x] 113 integration tests pass - [x] 2/2 smoke tests pass - [x] `clang-format` applied 🤖 Generated with [Claude Code](https://claude.com/claude-code) ## Summary by CodeRabbit * **New Features** * Server-side C++ compilation orchestration (module & precompiled header builds) with LSP-integrated document handling. * **Improvements** * Deterministic, persistent, dependency-aware caching to avoid redundant rebuilds and speed up incremental work. * Better cross-file indexing and navigation, improved diagnostics and more reliable include/import-aware completions. * **Tests** * Unit tests updated to the unified worker query/build request shapes. --------- Co-authored-by: Claude Opus 4.6 (1M context) --- src/server/compiler.cpp | 1471 +++++++++ src/server/compiler.h | 259 ++ src/server/indexer.cpp | 698 +++++ src/server/indexer.h | 251 ++ src/server/master_server.cpp | 2900 ++---------------- src/server/master_server.h | 338 +- src/server/protocol.h | 229 +- src/server/stateful_worker.cpp | 134 +- src/server/stateless_worker.cpp | 540 ++-- src/server/worker_common.h | 44 + tests/unit/server/module_worker_tests.cpp | 20 +- tests/unit/server/pch_worker_tests.cpp | 7 +- tests/unit/server/stateful_worker_tests.cpp | 54 +- tests/unit/server/stateless_worker_tests.cpp | 26 +- 14 files changed, 3359 insertions(+), 3612 deletions(-) create mode 100644 src/server/compiler.cpp create mode 100644 src/server/compiler.h create mode 100644 src/server/indexer.cpp create mode 100644 src/server/indexer.h create mode 100644 src/server/worker_common.h diff --git a/src/server/compiler.cpp b/src/server/compiler.cpp new file mode 100644 index 00000000..70541266 --- /dev/null +++ b/src/server/compiler.cpp @@ -0,0 +1,1471 @@ +#include "server/compiler.h" + +#include +#include +#include +#include +#include +#include + +#include "command/search_config.h" +#include "eventide/ipc/lsp/position.h" +#include "eventide/ipc/lsp/uri.h" +#include "eventide/serde/json/json.h" +#include "index/tu_index.h" +#include "server/protocol.h" +#include "support/filesystem.h" +#include "support/logging.h" +#include "syntax/include_resolver.h" +#include "syntax/scan.h" + +#include "llvm/Support/Chrono.h" +#include "llvm/Support/FileSystem.h" +#include "llvm/Support/MemoryBuffer.h" +#include "llvm/Support/Path.h" +#include "llvm/Support/raw_ostream.h" +#include "llvm/Support/xxhash.h" + +namespace clice { + +/// Hash a file's content using xxh3_64bits. Returns 0 on read failure. +static std::uint64_t hash_file(llvm::StringRef path) { + auto buf = llvm::MemoryBuffer::getFile(path); + if(!buf) + return 0; + return llvm::xxh3_64bits((*buf)->getBuffer()); +} + +/// Capture a two-layer staleness snapshot after a successful compilation. +/// Interns dependency paths into the PathPool and hashes each file's content. +static DepsSnapshot capture_deps_snapshot(PathPool& pool, llvm::ArrayRef deps) { + DepsSnapshot snap; + // Capture timestamp BEFORE hashing to avoid TOCTOU: if a file is modified + // during hashing, its mtime will be > build_at, triggering Layer 2 re-hash. + snap.build_at = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); + snap.path_ids.reserve(deps.size()); + snap.hashes.reserve(deps.size()); + for(const auto& file: deps) { + snap.path_ids.push_back(pool.intern(file)); + snap.hashes.push_back(hash_file(file)); + } + return snap; +} + +/// Two-layer staleness check. +/// +/// Layer 1 (fast): stat each dep file, compare mtime against build_at. +/// If all mtimes <= build_at → nothing changed, return false immediately. +/// +/// Layer 2 (precise): for files with mtime > build_at, re-hash their content. +/// If the hash matches the stored hash → file was touched but not modified. +/// If any hash differs → truly changed, return true. +static bool deps_changed(const PathPool& pool, const DepsSnapshot& snap) { + for(std::size_t i = 0; i < snap.path_ids.size(); ++i) { + auto path = pool.resolve(snap.path_ids[i]); + llvm::sys::fs::file_status status; + if(auto ec = llvm::sys::fs::status(path, status)) { + // File disappeared — definitely changed. + if(snap.hashes[i] != 0) + return true; + continue; + } + + // Layer 1: mtime check (cheap, stat only). + auto current_mtime = llvm::sys::toTimeT(status.getLastModificationTime()); + if(current_mtime <= snap.build_at) + continue; + + // Layer 2: mtime is newer — re-hash content to confirm actual change. + auto current_hash = hash_file(path); + if(current_hash != snap.hashes[i]) + return true; + } + return false; +} + +/// Serializable cache structures for cache.json persistence. +/// Paths are stored in a shared table and referenced by index to avoid +/// redundant storage (a single file can depend on thousands of headers, +/// many of which are shared across entries). +namespace { + +struct CacheDepEntry { + std::uint32_t path; // index into CacheData::paths + std::uint64_t hash; +}; + +struct CachePCHEntry { + std::string filename; + std::uint32_t source_file; // index into CacheData::paths + std::uint64_t hash; + std::uint32_t bound; + std::int64_t build_at; + std::vector deps; +}; + +struct CachePCMEntry { + std::string filename; + std::uint32_t source_file; + std::string module_name; + std::int64_t build_at; + std::vector deps; +}; + +struct CacheData { + std::vector paths; + std::vector pch; + std::vector pcm; +}; + +} // namespace + +namespace lsp = eventide::ipc::lsp; +using serde_raw = et::serde::RawValue; + +Compiler::Compiler(et::event_loop& loop, + et::ipc::JsonPeer& peer, + PathPool& path_pool, + WorkerPool& pool, + Indexer& indexer, + const CliceConfig& config, + CompilationDatabase& cdb, + DependencyGraph& dep_graph) : + loop(loop), peer(peer), path_pool(path_pool), pool(pool), indexer(indexer), config(config), + cdb(cdb), dep_graph(dep_graph) {} + +Compiler::~Compiler() { + cancel_all(); +} + +void Compiler::fill_pcm_deps(std::unordered_map& pcms, + std::uint32_t exclude_path_id) const { + for(auto& [pid, pcm_path]: pcm_paths) { + if(pid == exclude_path_id) + continue; + auto mod_it = path_to_module.find(pid); + if(mod_it != path_to_module.end()) { + pcms[mod_it->second] = pcm_path; + } + } +} + +void Compiler::cancel_all() { + if(compile_graph) { + compile_graph->cancel_all(); + } +} + +void Compiler::load_cache() { + if(config.cache_dir.empty()) + return; + + auto cache_path = path::join(config.cache_dir, "cache", "cache.json"); + auto content = fs::read(cache_path); + if(!content) { + LOG_DEBUG("No cache.json found at {}", cache_path); + return; + } + + CacheData data; + auto status = et::serde::json::from_json(*content, data); + if(!status) { + LOG_WARN("Failed to parse cache.json"); + return; + } + + auto resolve = [&](std::uint32_t idx) -> llvm::StringRef { + return idx < data.paths.size() ? llvm::StringRef(data.paths[idx]) : ""; + }; + + auto load_deps = [&](std::int64_t build_at, const auto& dep_entries) -> DepsSnapshot { + DepsSnapshot deps; + deps.build_at = build_at; + for(auto& dep: dep_entries) { + auto dep_path = resolve(dep.path); + if(dep_path.empty()) + continue; + deps.path_ids.push_back(path_pool.intern(dep_path)); + deps.hashes.push_back(dep.hash); + } + return deps; + }; + + for(auto& entry: data.pch) { + auto pch_path = path::join(config.cache_dir, "cache", "pch", entry.filename); + auto source = resolve(entry.source_file); + if(!llvm::sys::fs::exists(pch_path) || source.empty()) + continue; + + auto path_id = path_pool.intern(source); + auto& st = pch_states[path_id]; + st.path = pch_path; + st.hash = entry.hash; + st.bound = entry.bound; + st.deps = load_deps(entry.build_at, entry.deps); + + LOG_DEBUG("Loaded cached PCH: {} -> {}", source, pch_path); + } + + for(auto& entry: data.pcm) { + auto pcm_path = path::join(config.cache_dir, "cache", "pcm", entry.filename); + auto source = resolve(entry.source_file); + if(!llvm::sys::fs::exists(pcm_path) || source.empty()) + continue; + + auto path_id = path_pool.intern(source); + pcm_states[path_id] = {pcm_path, load_deps(entry.build_at, entry.deps)}; + pcm_paths[path_id] = pcm_path; + + LOG_DEBUG("Loaded cached PCM: {} (module {}) -> {}", source, entry.module_name, pcm_path); + } + + LOG_INFO("Loaded cache.json: {} PCH entries, {} PCM entries", + pch_states.size(), + pcm_states.size()); +} + +void Compiler::save_cache() { + if(config.cache_dir.empty()) + return; + + CacheData data; + std::unordered_map index_map; + + auto intern = [&](std::uint32_t runtime_path_id) -> std::uint32_t { + auto path = std::string(path_pool.resolve(runtime_path_id)); + auto [it, inserted] = + index_map.try_emplace(path, static_cast(data.paths.size())); + if(inserted) { + data.paths.push_back(path); + } + return it->second; + }; + + for(auto& [path_id, st]: pch_states) { + if(st.path.empty()) + continue; + + CachePCHEntry entry; + entry.filename = std::string(path::filename(st.path)); + entry.source_file = intern(path_id); + entry.hash = st.hash; + entry.bound = st.bound; + entry.build_at = st.deps.build_at; + for(std::size_t i = 0; i < st.deps.path_ids.size(); ++i) { + entry.deps.push_back({intern(st.deps.path_ids[i]), st.deps.hashes[i]}); + } + data.pch.push_back(std::move(entry)); + } + + for(auto& [path_id, st]: pcm_states) { + if(st.path.empty()) + continue; + + CachePCMEntry entry; + entry.filename = std::string(path::filename(st.path)); + entry.source_file = intern(path_id); + auto mod_it = path_to_module.find(path_id); + entry.module_name = mod_it != path_to_module.end() ? mod_it->second : ""; + entry.build_at = st.deps.build_at; + for(std::size_t i = 0; i < st.deps.path_ids.size(); ++i) { + entry.deps.push_back({intern(st.deps.path_ids[i]), st.deps.hashes[i]}); + } + data.pcm.push_back(std::move(entry)); + } + + auto json_str = et::serde::json::to_json(data); + if(!json_str) { + LOG_WARN("Failed to serialize cache.json"); + return; + } + + auto cache_path = path::join(config.cache_dir, "cache", "cache.json"); + auto tmp_path = cache_path + ".tmp"; + auto write_result = fs::write(tmp_path, *json_str); + if(!write_result) { + LOG_WARN("Failed to write cache.json.tmp: {}", write_result.error().message()); + return; + } + auto rename_result = fs::rename(tmp_path, cache_path); + if(!rename_result) { + LOG_WARN("Failed to rename cache.json.tmp to cache.json: {}", + rename_result.error().message()); + } +} + +void Compiler::cleanup_cache(int max_age_days) { + if(config.cache_dir.empty()) + return; + + auto now = std::chrono::system_clock::now(); + auto max_age = std::chrono::hours(max_age_days * 24); + + for(auto* subdir: {"cache/pch", "cache/pcm"}) { + auto dir = path::join(config.cache_dir, subdir); + std::error_code ec; + for(auto it = llvm::sys::fs::directory_iterator(dir, ec); + !ec && it != llvm::sys::fs::directory_iterator(); + it.increment(ec)) { + llvm::sys::fs::file_status status; + if(auto stat_ec = llvm::sys::fs::status(it->path(), status)) + continue; + + auto mtime = status.getLastModificationTime(); + auto age = now - mtime; + if(age > max_age) { + llvm::sys::fs::remove(it->path()); + LOG_DEBUG("Cleaned up stale cache file: {}", it->path()); + } + } + } +} + +void Compiler::build_module_map() { + for(auto& [module_name, path_ids]: dep_graph.modules()) { + for(auto path_id: path_ids) { + path_to_module[path_id] = module_name.str(); + } + } +} + +void Compiler::init_compile_graph() { + if(path_to_module.empty()) { + LOG_INFO("No C++20 modules detected, skipping CompileGraph"); + return; + } + + // Lazy dependency resolver: scans a module file on demand to discover imports. + auto resolve = [this](std::uint32_t path_id) -> llvm::SmallVector { + auto file_path = path_pool.resolve(path_id); + auto results = cdb.lookup(file_path, {.query_toolchain = true, .suppress_logging = true}); + if(results.empty()) + return {}; + + auto& ctx = results[0]; + auto scan_result = scan_precise(ctx.arguments, ctx.directory); + + llvm::SmallVector deps; + for(auto& mod_name: scan_result.modules) { + auto mod_ids = dep_graph.lookup_module(mod_name); + if(!mod_ids.empty()) { + deps.push_back(mod_ids[0]); + } + } + + // Module implementation units implicitly depend on their interface unit. + if(!scan_result.module_name.empty() && !scan_result.is_interface_unit) { + auto mod_ids = dep_graph.lookup_module(scan_result.module_name); + if(!mod_ids.empty()) { + deps.push_back(mod_ids[0]); + } + } + + return deps; + }; + + // Dispatch: sends BuildPCM request to a stateless worker. + auto dispatch = [this](std::uint32_t path_id) -> et::task { + auto mod_it = path_to_module.find(path_id); + if(mod_it == path_to_module.end()) + co_return false; + + auto file_path = std::string(path_pool.resolve(path_id)); + + worker::BuildParams bp; + bp.kind = worker::BuildKind::BuildPCM; + bp.file = file_path; + if(!fill_compile_args(file_path, bp.directory, bp.arguments)) + co_return false; + + // Compute deterministic content-addressed PCM path. + auto safe_module_name = mod_it->second; + std::ranges::replace(safe_module_name, ':', '-'); + std::string hash_input = file_path; + for(auto& arg: bp.arguments) { + hash_input += arg; + } + auto args_hash = llvm::xxh3_64bits(llvm::StringRef(hash_input)); + auto pcm_filename = std::format("{}-{:016x}.pcm", safe_module_name, args_hash); + auto pcm_path = path::join(config.cache_dir, "cache", "pcm", pcm_filename); + + // Check if cached PCM is still valid. + if(auto pcm_it = pcm_states.find(path_id); pcm_it != pcm_states.end()) { + if(!pcm_it->second.path.empty() && llvm::sys::fs::exists(pcm_it->second.path) && + !deps_changed(path_pool, pcm_it->second.deps)) { + pcm_paths[path_id] = pcm_it->second.path; + co_return true; + } + } + + bp.module_name = mod_it->second; + bp.output_path = pcm_path; + + // Clang needs ALL transitive PCM deps, not just direct imports. + fill_pcm_deps(bp.pcms); + + auto result = co_await pool.send_stateless(bp); + if(!result.has_value() || !result.value().success) { + LOG_WARN("BuildPCM failed for module {}: {}", + mod_it->second, + result.has_value() ? result.value().error : result.error().message); + co_return false; + } + + pcm_paths[path_id] = result.value().output_path; + pcm_states[path_id] = {result.value().output_path, + capture_deps_snapshot(path_pool, result.value().deps)}; + LOG_INFO("Built PCM for module {}: {}", mod_it->second, result.value().output_path); + + // Merge module index into ProjectIndex/MergedIndex. + if(!result.value().tu_index_data.empty()) { + indexer.merge(result.value().tu_index_data.data(), result.value().tu_index_data.size()); + } + + // Persist cache metadata after successful build. + save_cache(); + co_return true; + }; + + compile_graph = std::make_unique(std::move(dispatch), std::move(resolve)); + LOG_INFO("CompileGraph initialized with {} module(s)", path_to_module.size()); +} + +bool Compiler::fill_compile_args(llvm::StringRef path, + std::string& directory, + std::vector& arguments) { + auto path_id = path_pool.intern(path); + + // 1. If the user has set an active header context via switchContext, + // use the host source's CDB entry with file path replaced and preamble injected. + auto active_it = active_contexts.find(path_id); + if(active_it != active_contexts.end()) { + return fill_header_context_args(path, path_id, directory, arguments); + } + + // 2. Normal CDB lookup for the file itself. + auto results = cdb.lookup(path, {.query_toolchain = true}); + if(!results.empty()) { + auto& ctx = results.front(); + directory = ctx.directory.str(); + arguments.clear(); + for(auto* arg: ctx.arguments) { + arguments.emplace_back(arg); + } + return true; + } + + // 3. No CDB entry — try automatic header context resolution. + return fill_header_context_args(path, path_id, directory, arguments); +} + +bool Compiler::fill_header_context_args(llvm::StringRef path, + std::uint32_t path_id, + std::string& directory, + std::vector& arguments) { + // Use cached context if available; otherwise resolve. + // If an active context override exists, invalidate cache if it points to + // a different host so we re-resolve with the correct one. + const HeaderFileContext* ctx_ptr = nullptr; + auto ctx_it = header_file_contexts.find(path_id); + auto active_it = active_contexts.find(path_id); + if(ctx_it != header_file_contexts.end()) { + if(active_it != active_contexts.end() && ctx_it->second.host_path_id != active_it->second) { + header_file_contexts.erase(ctx_it); + } else { + ctx_ptr = &ctx_it->second; + } + } + if(!ctx_ptr) { + auto resolved = resolve_header_context(path_id); + if(!resolved) { + LOG_WARN("No CDB entry and no header context for {}", path); + return false; + } + header_file_contexts[path_id] = std::move(*resolved); + ctx_ptr = &header_file_contexts[path_id]; + } + + auto host_path = path_pool.resolve(ctx_ptr->host_path_id); + auto host_results = cdb.lookup(host_path, {.query_toolchain = true}); + if(host_results.empty()) { + LOG_WARN("fill_header_context_args: host {} has no CDB entry", host_path); + return false; + } + + auto& host_ctx = host_results.front(); + directory = host_ctx.directory.str(); + arguments.clear(); + + // Copy host arguments, replacing the host source file path with the header. + bool replaced = false; + for(auto& arg: host_ctx.arguments) { + if(llvm::StringRef(arg) == host_path) { + arguments.emplace_back(path); + replaced = true; + } else { + arguments.emplace_back(arg); + } + } + if(!replaced) { + LOG_WARN("fill_header_context_args: host path {} not found in arguments, appending header", + host_path); + arguments.emplace_back(path); + } + + // Inject preamble: for cc1 args insert after "-cc1", otherwise after driver. + std::size_t inject_pos = 1; + if(arguments.size() >= 2 && arguments[1] == "-cc1") { + inject_pos = 2; + } + arguments.insert(arguments.begin() + inject_pos, ctx_ptr->preamble_path); + arguments.insert(arguments.begin() + inject_pos, "-include"); + + LOG_INFO("fill_compile_args: header context for {} (host={}, preamble={})", + path, + host_path, + ctx_ptr->preamble_path); + return true; +} + +std::optional Compiler::resolve_header_context(std::uint32_t header_path_id) { + // Find source files that transitively include this header. + auto hosts = dep_graph.find_host_sources(header_path_id); + if(hosts.empty()) { + LOG_DEBUG("resolve_header_context: no host sources for path_id={}", header_path_id); + return std::nullopt; + } + + // If there's an active context override, prefer that host. + std::uint32_t host_path_id = 0; + std::vector chain; + auto active_it = active_contexts.find(header_path_id); + if(active_it != active_contexts.end()) { + auto preferred = active_it->second; + auto preferred_path = path_pool.resolve(preferred); + auto results = cdb.lookup(preferred_path, {.suppress_logging = true}); + if(!results.empty()) { + auto c = dep_graph.find_include_chain(preferred, header_path_id); + if(!c.empty()) { + host_path_id = preferred; + chain = std::move(c); + } + } + } + + // Fall back to the first available host that has a CDB entry. + if(chain.empty()) { + for(auto candidate: hosts) { + auto candidate_path = path_pool.resolve(candidate); + auto results = cdb.lookup(candidate_path, {.suppress_logging = true}); + if(results.empty()) + continue; + auto c = dep_graph.find_include_chain(candidate, header_path_id); + if(c.empty()) + continue; + host_path_id = candidate; + chain = std::move(c); + break; + } + } + + if(chain.empty()) { + LOG_DEBUG("resolve_header_context: no usable host with include chain for path_id={}", + header_path_id); + return std::nullopt; + } + + // Build preamble text: for each file in the chain except the last (target), + // append all content up to (but not including) the line that includes the + // next file in the chain. + std::string preamble; + for(std::size_t i = 0; i + 1 < chain.size(); ++i) { + auto cur_id = chain[i]; + auto next_id = chain[i + 1]; + + auto cur_path = path_pool.resolve(cur_id); + auto next_path = path_pool.resolve(next_id); + auto next_filename = llvm::sys::path::filename(next_path); + + // Prefer in-memory document text over disk content. + std::string content; + if(auto doc_it = documents.find(cur_id); doc_it != documents.end()) { + content = doc_it->second.text; + } else { + auto buf = llvm::MemoryBuffer::getFile(cur_path); + if(!buf) { + LOG_WARN("resolve_header_context: cannot read {}", cur_path); + return std::nullopt; + } + content = (*buf)->getBuffer().str(); + } + + // Scan line by line for the #include that brings in next_filename. + llvm::StringRef content_ref(content); + std::size_t line_start = 0; + std::size_t include_line_start = std::string::npos; + while(line_start <= content_ref.size()) { + auto newline_pos = content_ref.find('\n', line_start); + auto line_end = + (newline_pos == llvm::StringRef::npos) ? content_ref.size() : newline_pos; + auto line = content_ref.slice(line_start, line_end).trim(); + + if(line.starts_with("#include") || line.starts_with("# include")) { + // Extract the filename from the #include directive. + // Handles: #include "foo.h", #include , # include "foo.h" + auto quote_start = line.find_first_of("\"<"); + auto quote_end = llvm::StringRef::npos; + if(quote_start != llvm::StringRef::npos) { + char close = (line[quote_start] == '"') ? '"' : '>'; + quote_end = line.find(close, quote_start + 1); + } + if(quote_start != llvm::StringRef::npos && quote_end != llvm::StringRef::npos) { + auto included = line.slice(quote_start + 1, quote_end); + auto included_filename = llvm::sys::path::filename(included); + if(included_filename == next_filename) { + include_line_start = line_start; + break; + } + } + } + + line_start = + (newline_pos == llvm::StringRef::npos) ? content_ref.size() + 1 : newline_pos + 1; + } + + // Emit a #line marker then all content before the include line. + preamble += std::format("#line 1 \"{}\"\n", cur_path.str()); + if(include_line_start != std::string::npos) { + preamble += content_ref.substr(0, include_line_start).str(); + } else { + // No matching include line found — emit the whole file to be safe. + LOG_DEBUG("resolve_header_context: include line for {} not found in {}, emitting full", + next_filename, + cur_path); + preamble += content; + } + } + + // Hash the preamble and write to cache directory. + auto preamble_hash = llvm::xxh3_64bits(llvm::StringRef(preamble)); + auto preamble_filename = std::format("{:016x}.h", preamble_hash); + auto preamble_dir = path::join(config.cache_dir, "header_context"); + auto preamble_path = path::join(preamble_dir, preamble_filename); + + if(!llvm::sys::fs::exists(preamble_path)) { + auto ec = llvm::sys::fs::create_directories(preamble_dir); + if(ec) { + LOG_WARN("resolve_header_context: cannot create dir {}: {}", + preamble_dir, + ec.message()); + return std::nullopt; + } + if(auto result = fs::write(preamble_path, preamble); !result) { + LOG_WARN("resolve_header_context: cannot write preamble {}: {}", + preamble_path, + result.error().message()); + return std::nullopt; + } + LOG_INFO("resolve_header_context: wrote preamble {} for header path_id={}", + preamble_path, + header_path_id); + } + + return HeaderFileContext{host_path_id, preamble_path, preamble_hash}; +} + +void Compiler::switch_context(std::uint32_t path_id, std::uint32_t context_path_id) { + active_contexts[path_id] = context_path_id; + header_file_contexts.erase(path_id); + pch_states.erase(path_id); + ast_deps.erase(path_id); + auto doc_it = documents.find(path_id); + if(doc_it != documents.end()) { + doc_it->second.ast_dirty = true; + } +} + +std::optional Compiler::get_active_context(std::uint32_t path_id) const { + auto it = active_contexts.find(path_id); + if(it != active_contexts.end()) + return it->second; + return std::nullopt; +} + +void Compiler::invalidate_host_contexts(std::uint32_t host_path_id, + llvm::SmallVectorImpl& stale_headers) { + for(auto& [hdr_id, hdr_ctx]: header_file_contexts) { + if(hdr_ctx.host_path_id == host_path_id) + stale_headers.push_back(hdr_id); + } + for(auto hdr_id: stale_headers) { + header_file_contexts.erase(hdr_id); + } +} + +et::task Compiler::ensure_pch(std::uint32_t path_id, + llvm::StringRef path, + const std::string& text, + const std::string& directory, + const std::vector& arguments) { + auto bound = compute_preamble_bound(text); + if(bound == 0) { + // No preamble directives — PCH would be empty. Clear any stale entry. + pch_states.erase(path_id); + co_return true; + } + + auto preamble_hash = llvm::xxh3_64bits(llvm::StringRef(text).substr(0, bound)); + + // Deterministic content-addressed PCH path. + auto pch_path = + path::join(config.cache_dir, "cache", "pch", std::format("{:016x}.pch", preamble_hash)); + + // Reuse existing PCH if preamble content and deps haven't changed. + if(auto it = pch_states.find(path_id); it != pch_states.end()) { + auto& st = it->second; + if(st.hash == preamble_hash && !st.path.empty() && !deps_changed(path_pool, st.deps)) { + st.bound = bound; + co_return true; + } + } + + // Preamble incomplete (user still typing) — defer rebuild, reuse old PCH if available. + if(!is_preamble_complete(text, bound)) { + LOG_DEBUG("Preamble incomplete for {}, deferring PCH rebuild", path); + co_return pch_states.count(path_id) && !pch_states[path_id].path.empty(); + } + + // If another coroutine is already building PCH for this file, wait for it. + if(auto it = pch_states.find(path_id); it != pch_states.end() && it->second.building) { + co_await it->second.building->wait(); + co_return !pch_states[path_id].path.empty(); + } + + // Register in-flight build so concurrent requests wait on us. + auto completion = std::make_shared(); + pch_states[path_id].building = completion; + + // Build a new PCH via stateless worker. + worker::BuildParams bp; + bp.kind = worker::BuildKind::BuildPCH; + bp.file = std::string(path); + bp.directory = directory; + bp.arguments = arguments; + bp.text = text; + bp.preamble_bound = bound; + bp.output_path = pch_path; + + LOG_DEBUG("Building PCH for {}, bound={}, output={}", path, bound, pch_path); + + auto result = co_await pool.send_stateless(bp); + + if(!result.has_value() || !result.value().success) { + LOG_WARN("PCH build failed for {}: {}", + path, + result.has_value() ? result.value().error : result.error().message); + pch_states[path_id].building.reset(); + completion->set(); + co_return false; + } + + auto& st = pch_states[path_id]; + st.path = result.value().output_path; + st.bound = bound; + st.hash = preamble_hash; + st.deps = capture_deps_snapshot(path_pool, result.value().deps); + st.building.reset(); + + LOG_INFO("PCH built for {}: {}", path, result.value().output_path); + + if(!result.value().tu_index_data.empty()) { + indexer.merge(result.value().tu_index_data.data(), result.value().tu_index_data.size()); + } + + // Persist cache metadata after successful build. + save_cache(); + + completion->set(); + co_return true; +} + +/// Compile module dependencies, build/reuse PCH, and fill PCM paths. +/// Shared preparation step used by both ensure_compiled() (stateful path) +/// and forward_stateless() (completion/signatureHelp path). +et::task Compiler::ensure_deps(std::uint32_t path_id, + llvm::StringRef path, + const std::string& text, + const std::string& directory, + const std::vector& arguments, + std::pair& pch, + std::unordered_map& pcms) { + // Compile C++20 module dependencies (PCMs). + if(compile_graph && !co_await compile_graph->compile_deps(path_id)) { + co_return false; + } + + // Scan buffer text for module imports that might not be in compile_graph yet. + // When a user adds `import std;` without saving, the compile_graph (disk-based) + // doesn't know about the new dependency. Scan the in-memory text to find them. + { + auto scan_result = scan(text); + for(auto& mod_name: scan_result.modules) { + if(mod_name.empty()) + continue; + bool found = false; + for(auto& [pid, name]: path_to_module) { + if(name == mod_name) { + // If PCM not already built, try to build it. + if(pcm_paths.find(pid) == pcm_paths.end()) { + if(compile_graph && compile_graph->has_unit(pid)) { + co_await compile_graph->compile_deps(pid); + } + } + found = true; + break; + } + } + if(!found) { + LOG_DEBUG("Buffer imports unknown module '{}', skipping", mod_name); + } + } + } + + // Build or reuse PCH. + auto pch_ok = co_await ensure_pch(path_id, path, text, directory, arguments); + if(pch_ok) { + if(auto pch_it = pch_states.find(path_id); pch_it != pch_states.end()) { + pch = {pch_it->second.path, pch_it->second.bound}; + } + } + + // Fill all available PCM paths, excluding the file's own PCM + // to avoid "multiple module declarations". + fill_pcm_deps(pcms, path_id); + + co_return true; +} + +bool Compiler::is_stale(std::uint32_t path_id) { + auto ast_deps_it = ast_deps.find(path_id); + if(ast_deps_it != ast_deps.end() && deps_changed(path_pool, ast_deps_it->second)) + return true; + + auto pch_it = pch_states.find(path_id); + if(pch_it != pch_states.end() && deps_changed(path_pool, pch_it->second.deps)) + return true; + + return false; +} + +void Compiler::record_deps(std::uint32_t path_id, llvm::ArrayRef deps) { + ast_deps[path_id] = capture_deps_snapshot(path_pool, deps); +} + +void Compiler::on_file_closed(std::uint32_t path_id) { + if(compile_graph && compile_graph->has_unit(path_id)) { + compile_graph->update(path_id); + } + pch_states.erase(path_id); + ast_deps.erase(path_id); +} + +llvm::SmallVector Compiler::on_file_saved(std::uint32_t path_id) { + llvm::SmallVector dirtied; + if(compile_graph) { + auto result = compile_graph->update(path_id); + for(auto id: result) { + dirtied.push_back(id); + pcm_paths.erase(id); + pcm_states.erase(id); + } + } + return dirtied; +} + +std::string Compiler::uri_to_path(const std::string& uri) { + auto parsed = lsp::URI::parse(uri); + if(parsed.has_value()) { + auto path = parsed->file_path(); + if(path.has_value()) { + return std::move(*path); + } + } + return uri; +} + +void Compiler::open_document(const std::string& uri, std::string text, int version) { + auto path = uri_to_path(uri); + auto path_id = path_pool.intern(path); + + auto& doc = documents[path_id]; + doc.version = version; + doc.text = std::move(text); + doc.generation++; + + LOG_DEBUG("didOpen: {} (v{})", path, version); +} + +void Compiler::apply_changes(const protocol::DidChangeTextDocumentParams& params) { + auto path = uri_to_path(params.text_document.uri); + auto path_id = path_pool.intern(path); + + auto it = documents.find(path_id); + if(it == documents.end()) + return; + + auto& doc = it->second; + doc.version = params.text_document.version; + + for(auto& change: params.content_changes) { + std::visit( + [&](auto& c) { + using T = std::remove_cvref_t; + if constexpr(std::is_same_v) { + doc.text = c.text; + } else { + auto& range = c.range; + lsp::PositionMapper mapper(doc.text, lsp::PositionEncoding::UTF16); + auto start = mapper.to_offset(range.start); + auto end = mapper.to_offset(range.end); + if(start && end && *start <= *end) { + doc.text.replace(*start, *end - *start, c.text); + } + } + }, + change); + } + + doc.generation++; + doc.ast_dirty = true; + + LOG_DEBUG("didChange: path={} version={} gen={}", path, doc.version, doc.generation); + + worker::DocumentUpdateParams update; + update.path = path; + update.version = doc.version; + pool.notify_stateful(path_id, update); +} + +std::uint32_t Compiler::close_document(const std::string& uri) { + auto path = uri_to_path(uri); + auto path_id = path_pool.intern(path); + + on_file_closed(path_id); + indexer.remove_open_file(path_id, path); + pool.notify_stateful(path_id, worker::EvictParams{path}); + documents.erase(path_id); + clear_diagnostics(uri); + + LOG_DEBUG("didClose: {}", path); + return path_id; +} + +llvm::SmallVector Compiler::on_save(const std::string& uri) { + auto path = uri_to_path(uri); + auto path_id = path_pool.intern(path); + + llvm::SmallVector to_index; + + auto dirtied = on_file_saved(path_id); + for(auto dirty_id: dirtied) { + auto doc_it = documents.find(dirty_id); + if(doc_it != documents.end()) { + doc_it->second.ast_dirty = true; + } else { + to_index.push_back(dirty_id); + } + } + + llvm::SmallVector stale_headers; + invalidate_host_contexts(path_id, stale_headers); + for(auto hdr_id: stale_headers) { + auto doc_it = documents.find(hdr_id); + if(doc_it != documents.end()) { + doc_it->second.ast_dirty = true; + LOG_DEBUG("didSave: invalidated header context for path_id={}", hdr_id); + } + } + + LOG_DEBUG("didSave: {}", uri); + return to_index; +} + +bool Compiler::is_file_open(std::uint32_t path_id) const { + return documents.count(path_id); +} + +const DocumentState* Compiler::get_document(std::uint32_t path_id) const { + auto it = documents.find(path_id); + return it != documents.end() ? &it->second : nullptr; +} + +void Compiler::publish_diagnostics(const std::string& uri, + int version, + const et::serde::RawValue& diagnostics_json) { + std::vector diagnostics; + if(!diagnostics_json.empty()) { + auto status = et::serde::json::from_json(diagnostics_json.data, diagnostics); + if(!status) { + LOG_WARN("Failed to deserialize diagnostics JSON for {}", uri); + } + } + protocol::PublishDiagnosticsParams params; + params.uri = uri; + params.version = version; + params.diagnostics = std::move(diagnostics); + peer.send_notification(params); +} + +void Compiler::clear_diagnostics(const std::string& uri) { + protocol::PublishDiagnosticsParams params; + params.uri = uri; + params.diagnostics = {}; + peer.send_notification(params); +} + +/// Pull-based compilation entry point for user-opened files. +/// +/// Called lazily by forward_stateful() / forward_stateless() before every +/// feature request (hover, semantic tokens, etc.). Guarantees that when it +/// returns true the stateful worker assigned to `path_id` holds an up-to-date +/// AST and diagnostics have been published to the client. +/// +/// Lifecycle overview (pull-based model): +/// +/// didOpen / didChange – only update DocumentState, mark ast_dirty +/// didSave – mark dependents dirty, queue indexing +/// feature request arrives – calls ensure_compiled() first +/// 1. Fast-path exit if AST is already clean (!ast_dirty). +/// 2. Compile any C++20 module dependencies (PCMs) via CompileGraph. +/// 3. Build / reuse the precompiled header (PCH) via ensure_pch(). +/// 4. Send CompileParams to the stateful worker, which builds the AST. +/// 5. On success: publish diagnostics, clear ast_dirty, schedule indexing. +/// 6. On generation mismatch (user edited during compile): keep dirty, +/// the next feature request will trigger another compile cycle. +/// +/// Only the opened file itself is remapped (its in-memory text is sent to the +/// worker); every other file is read from disk by the compiler. +/// +/// Concurrency: multiple concurrent feature requests for the same file will +/// each call ensure_compiled(). The first one launches a detached compile +/// task via loop.schedule(); subsequent ones wait on the shared event. +/// The detached task cannot be cancelled by LSP $/cancelRequest, preventing +/// the race where cancellation wakes all waiters and they all start compiles. +et::task Compiler::ensure_compiled(std::uint32_t path_id) { + auto it = documents.find(path_id); + if(it == documents.end()) { + LOG_WARN("ensure_compiled: doc not found for path_id={} path={}", + path_id, + path_pool.resolve(path_id)); + co_return false; + } + + auto& doc = it->second; + LOG_DEBUG("ensure_compiled: path_id={} version={} gen={} ast_dirty={}", + path_id, + doc.version, + doc.generation, + doc.ast_dirty); + + if(!doc.ast_dirty) { + if(!is_stale(path_id)) { + co_return true; + } + doc.ast_dirty = true; + } + + // If another compile is already in flight, wait for it. + // This co_await may be cancelled by LSP $/cancelRequest — that's fine, + // it just means this particular feature request is abandoned. The + // detached compile task keeps running independently. + while(it->second.compiling) { + auto pending = it->second.compiling; + co_await pending->done.wait(); + it = documents.find(path_id); + if(it == documents.end()) + co_return false; + if(!it->second.ast_dirty) + co_return true; + } + + // No compile in flight and AST is dirty — launch a detached compile task. + // The detached task is scheduled via loop.schedule() so it is NOT subject + // to LSP $/cancelRequest cancellation. This eliminates the race where + // cancellation fires the RAII guard, waking all waiters simultaneously + // and causing them all to start new compiles. + auto pending_compile = std::make_shared(); + it->second.compiling = pending_compile; + + LOG_INFO("ensure_compiled: launching detached compile path_id={} gen={}", + path_id, + doc.generation); + + loop.schedule([](Compiler* self, + std::uint32_t pid, + std::shared_ptr pc) -> et::task<> { + auto finish_compile = [&]() { + if(auto it = self->documents.find(pid); it != self->documents.end()) { + if(it->second.compiling == pc) { + it->second.compiling.reset(); + } + } + LOG_INFO("ensure_compiled: finish_compile (detached) path_id={}", pid); + pc->done.set(); + }; + + auto it = self->documents.find(pid); + if(it == self->documents.end()) { + finish_compile(); + co_return; + } + + auto gen = it->second.generation; + LOG_INFO("ensure_compiled: starting compile (detached) path_id={} gen={}", pid, gen); + + auto file_path = std::string(self->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 = it->second.version; + params.text = it->second.text; + if(!self->fill_compile_args(file_path, params.directory, params.arguments)) { + finish_compile(); + co_return; + } + + if(!co_await self->ensure_deps(pid, + params.path, + params.text, + params.directory, + params.arguments, + params.pch, + params.pcms)) { + LOG_WARN("Dependency preparation failed for {}, skipping compile", uri_str); + finish_compile(); + co_return; + } + + it = self->documents.find(pid); + if(it == self->documents.end()) { + finish_compile(); + co_return; + } + + auto result = co_await self->pool.send_stateful(pid, params); + + // Re-lookup: the document may have been closed while we were compiling. + it = self->documents.find(pid); + if(it == self->documents.end()) { + finish_compile(); + co_return; + } + + auto& doc2 = it->second; + + if(doc2.generation != gen) { + LOG_INFO("ensure_compiled: generation mismatch ({} vs {}) for {}", + doc2.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; + } + + doc2.ast_dirty = false; + pc->succeeded = true; + self->record_deps(pid, 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 = doc2.text; + self->indexer.set_open_file(pid, file_path, std::move(ofi)); + } + + finish_compile(); + + // Publish diagnostics AFTER marking compile as done, so that concurrent + // forward_stateful() calls can proceed immediately. + self->publish_diagnostics(uri_str, doc2.version, result.value().diagnostics); + if(self->on_indexing_needed) + self->on_indexing_needed(); + }(this, path_id, pending_compile)); + + // Wait for the detached compile to finish. If this wait is cancelled + // by LSP $/cancelRequest, the detached task continues unaffected. + co_await pending_compile->done.wait(); + + it = documents.find(path_id); + if(it == documents.end()) + co_return false; + + co_return !it->second.ast_dirty; +} + +Compiler::RawResult Compiler::forward_query(worker::QueryKind kind, const std::string& uri) { + auto path = uri_to_path(uri); + auto path_id = path_pool.intern(path); + + if(!co_await ensure_compiled(path_id)) { + co_return serde_raw{"null"}; + } + + auto dit = documents.find(path_id); + if(dit != documents.end() && dit->second.ast_dirty) { + co_return serde_raw{"null"}; + } + + worker::QueryParams wp{kind, path}; + auto result = co_await pool.send_stateful(path_id, wp); + if(!result.has_value()) { + co_return serde_raw{}; + } + co_return std::move(result.value()); +} + +Compiler::RawResult Compiler::forward_query(worker::QueryKind kind, + const std::string& uri, + const protocol::Position& position) { + auto path = uri_to_path(uri); + auto path_id = path_pool.intern(path); + + if(!co_await ensure_compiled(path_id)) { + co_return serde_raw{"null"}; + } + + auto doc_it = documents.find(path_id); + if(doc_it == documents.end() || doc_it->second.ast_dirty) { + co_return serde_raw{"null"}; + } + + lsp::PositionMapper mapper(doc_it->second.text, lsp::PositionEncoding::UTF16); + auto offset = mapper.to_offset(position); + if(!offset) + co_return serde_raw{"null"}; + + worker::QueryParams wp{kind, path, *offset}; + auto result = co_await pool.send_stateful(path_id, wp); + if(!result.has_value()) { + co_return serde_raw{}; + } + co_return std::move(result.value()); +} + +Compiler::RawResult Compiler::forward_build(worker::BuildKind kind, + const std::string& uri, + const protocol::Position& position) { + auto path = uri_to_path(uri); + auto path_id = path_pool.intern(path); + + auto doc_it = documents.find(path_id); + if(doc_it == documents.end()) { + co_return serde_raw{}; + } + + auto& doc = doc_it->second; + + worker::BuildParams wp; + wp.kind = kind; + wp.file = path; + wp.version = doc.version; + wp.text = doc.text; + if(!fill_compile_args(path, wp.directory, wp.arguments)) { + co_return serde_raw{}; + } + + if(!co_await ensure_deps(path_id, path, wp.text, wp.directory, wp.arguments, wp.pch, wp.pcms)) { + co_return serde_raw{}; + } + + lsp::PositionMapper mapper(wp.text, lsp::PositionEncoding::UTF16); + auto offset = mapper.to_offset(position); + if(!offset) + co_return serde_raw{"null"}; + wp.offset = *offset; + + auto result = co_await pool.send_stateless(wp); + if(!result.has_value()) { + co_return serde_raw{}; + } + co_return std::move(result.value().result_json); +} + +Compiler::RawResult Compiler::handle_completion(const std::string& uri, + const protocol::Position& position) { + auto path = uri_to_path(uri); + auto path_id = path_pool.intern(path); + auto doc_it = documents.find(path_id); + if(doc_it != documents.end()) { + lsp::PositionMapper mapper(doc_it->second.text, lsp::PositionEncoding::UTF16); + auto offset = mapper.to_offset(position); + if(offset) { + auto pctx = detect_completion_context(doc_it->second.text, *offset); + if(pctx.kind == CompletionContext::IncludeQuoted || + pctx.kind == CompletionContext::IncludeAngled) { + co_return complete_include(pctx, path); + } + if(pctx.kind == CompletionContext::Import) { + co_return complete_import(pctx); + } + } + } + + co_return co_await forward_build(worker::BuildKind::Completion, uri, position); +} + +PreambleCompletionContext Compiler::detect_completion_context(const std::string& text, + uint32_t offset) { + auto line_start = text.rfind('\n', offset > 0 ? offset - 1 : 0); + line_start = (line_start == std::string::npos) ? 0 : line_start + 1; + + auto line_end = text.find('\n', offset); + if(line_end == std::string::npos) + line_end = text.size(); + + auto line = llvm::StringRef(text).slice(line_start, offset); + auto trimmed = line.ltrim(); + + if(trimmed.starts_with("#")) { + auto directive = trimmed.drop_front(1).ltrim(); + if(directive.consume_front("include")) { + directive = directive.ltrim(); + if(directive.consume_front("\"")) { + return {CompletionContext::IncludeQuoted, directive.str()}; + } + if(directive.consume_front("<")) { + return {CompletionContext::IncludeAngled, directive.str()}; + } + } + return {}; + } + + auto import_check = trimmed; + if(import_check.consume_front("export") && !import_check.empty() && + !std::isalnum(import_check[0])) { + import_check = import_check.ltrim(); + } + if(import_check.consume_front("import") && + (import_check.empty() || !std::isalnum(import_check[0]))) { + import_check = import_check.ltrim(); + auto rest_of_line = llvm::StringRef(text).slice(line_start, line_end); + if(!rest_of_line.contains(';')) { + return {CompletionContext::Import, import_check.str()}; + } + } + + return {}; +} + +et::serde::RawValue Compiler::complete_include(const PreambleCompletionContext& ctx, + llvm::StringRef path) { + std::string directory; + std::vector arguments; + if(!fill_compile_args(path, directory, arguments)) + return serde_raw{"[]"}; + + std::vector args_ptrs; + args_ptrs.reserve(arguments.size()); + for(auto& arg: arguments) { + args_ptrs.push_back(arg.c_str()); + } + + auto search_config = extract_search_config(args_ptrs, directory); + DirListingCache dir_cache; + auto resolved = resolve_search_config(search_config, dir_cache); + + unsigned start_idx = 0; + if(ctx.kind == CompletionContext::IncludeAngled) { + start_idx = resolved.angled_start_idx; + } + + llvm::StringRef prefix_ref(ctx.prefix); + llvm::StringRef dir_prefix; + llvm::StringRef file_prefix = prefix_ref; + auto slash_pos = prefix_ref.rfind('/'); + if(slash_pos != llvm::StringRef::npos) { + dir_prefix = prefix_ref.slice(0, slash_pos); + file_prefix = prefix_ref.slice(slash_pos + 1, llvm::StringRef::npos); + } + + std::vector items; + llvm::StringSet<> seen; + + for(unsigned i = start_idx; i < resolved.dirs.size(); ++i) { + auto& search_dir = resolved.dirs[i]; + + const llvm::StringSet<>* entries = nullptr; + if(!dir_prefix.empty()) { + llvm::SmallString<256> sub_path(search_dir.path); + llvm::sys::path::append(sub_path, dir_prefix); + entries = resolve_dir(sub_path, dir_cache); + } else { + entries = search_dir.entries; + } + + if(!entries) + continue; + + for(auto& entry: *entries) { + auto name = entry.getKey(); + if(!name.starts_with(file_prefix)) + continue; + if(!seen.insert(name).second) + continue; + + llvm::SmallString<256> full_path(search_dir.path); + if(!dir_prefix.empty()) { + llvm::sys::path::append(full_path, dir_prefix); + } + llvm::sys::path::append(full_path, name); + + bool is_dir = false; + llvm::sys::fs::is_directory(llvm::Twine(full_path), is_dir); + + protocol::CompletionItem item; + if(is_dir) { + item.label = (name + "/").str(); + } else { + item.label = name.str(); + } + item.kind = protocol::CompletionItemKind::File; + items.push_back(std::move(item)); + } + } + + auto json = et::serde::json::to_json(items); + return serde_raw{json ? std::move(*json) : "[]"}; +} + +et::serde::RawValue Compiler::complete_import(const PreambleCompletionContext& ctx) { + std::vector items; + llvm::StringRef prefix_ref(ctx.prefix); + + for(auto& [path_id, module_name]: path_to_module) { + llvm::StringRef name_ref(module_name); + if(!name_ref.starts_with(prefix_ref)) + continue; + + protocol::CompletionItem item; + item.label = module_name; + item.kind = protocol::CompletionItemKind::Module; + item.insert_text = module_name + ";"; + items.push_back(std::move(item)); + } + + auto json = et::serde::json::to_json(items); + return serde_raw{json ? std::move(*json) : "[]"}; +} + +} // namespace clice diff --git a/src/server/compiler.h b/src/server/compiler.h new file mode 100644 index 00000000..f3012e4b --- /dev/null +++ b/src/server/compiler.h @@ -0,0 +1,259 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "command/command.h" +#include "eventide/async/async.h" +#include "eventide/ipc/lsp/protocol.h" +#include "eventide/ipc/peer.h" +#include "eventide/serde/serde/raw_value.h" +#include "server/compile_graph.h" +#include "server/config.h" +#include "server/indexer.h" +#include "server/worker_pool.h" +#include "support/path_pool.h" +#include "syntax/dependency_graph.h" + +#include "llvm/ADT/ArrayRef.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/StringRef.h" + +namespace clice { + +namespace et = eventide; +namespace protocol = et::ipc::protocol; + +/// State of a document opened by the client. +struct DocumentState { + int version = 0; + std::string text; + std::uint64_t generation = 0; + bool ast_dirty = true; + + /// Non-null while a compile is in flight. Callers wait on the event; + /// the compile task runs independently and cannot be cancelled by LSP + /// $/cancelRequest. + struct PendingCompile { + et::event done; + bool succeeded = false; + }; + + std::shared_ptr compiling; +}; + +/// Context for compiling a header file that lacks its own CDB entry. +struct HeaderFileContext { + std::uint32_t host_path_id; /// Source file acting as host + std::string preamble_path; /// Path to generated preamble file on disk + std::uint64_t preamble_hash; /// Hash of preamble content for staleness +}; + +/// Two-layer staleness snapshot for compilation artifacts (PCH, AST, etc.). +/// +/// Layer 1 (fast): compare each file's current mtime against build_at. +/// If all mtimes <= build_at, the artifact is fresh (zero I/O beyond stat). +/// +/// Layer 2 (precise): for files whose mtime changed, re-hash their content +/// and compare against the stored hash. If the hash matches, the file was +/// "touched" but not actually modified — skip the rebuild. +struct DepsSnapshot { + llvm::SmallVector path_ids; + llvm::SmallVector hashes; + std::int64_t build_at = 0; +}; + +/// Cached PCH state for a single source file. +struct PCHState { + std::string path; + std::uint32_t bound = 0; + std::uint64_t hash = 0; + DepsSnapshot deps; + std::shared_ptr building; +}; + +/// Cached PCM state for a single module file. +struct PCMState { + std::string path; + DepsSnapshot deps; +}; + +enum class CompletionContext { None, IncludeQuoted, IncludeAngled, Import }; + +struct PreambleCompletionContext { + CompletionContext kind = CompletionContext::None; + std::string prefix; +}; + +/// Manages the full compilation lifecycle: document state, compilation +/// artifacts (PCH/PCM cache), compile argument resolution, header context, +/// and feature request forwarding to workers. +/// +/// MasterServer delegates all document and compilation operations here, +/// keeping itself as a pure LSP handler registration layer. +class Compiler { +public: + Compiler(et::event_loop& loop, + et::ipc::JsonPeer& peer, + PathPool& path_pool, + WorkerPool& pool, + Indexer& indexer, + const CliceConfig& config, + CompilationDatabase& cdb, + DependencyGraph& dep_graph); + + ~Compiler(); + + /// Convert a file:// URI to a local file path. + static std::string uri_to_path(const std::string& uri); + + /// Document lifecycle — called from MasterServer handlers. + void open_document(const std::string& uri, std::string text, int version); + void apply_changes(const protocol::DidChangeTextDocumentParams& params); + std::uint32_t close_document(const std::string& uri); + llvm::SmallVector on_save(const std::string& uri); + + /// Document accessors. + bool is_file_open(std::uint32_t path_id) const; + const DocumentState* get_document(std::uint32_t path_id) const; + + /// Cache persistence. + void load_cache(); + void save_cache(); + void cleanup_cache(int max_age_days = 7); + + /// Build path_to_module reverse mapping from dependency graph. + void build_module_map(); + + /// Initialize the CompileGraph for C++20 module compilation ordering. + void init_compile_graph(); + + /// Fill compile arguments for a file (CDB lookup + header context fallback). + bool fill_compile_args(llvm::StringRef path, + std::string& directory, + std::vector& arguments); + + /// Fill PCM paths for all built modules (for background indexing). + void fill_pcm_deps(std::unordered_map& pcms, + std::uint32_t exclude_path_id = UINT32_MAX) const; + + /// Pull-based compilation entry point for user-opened files. + et::task ensure_compiled(std::uint32_t path_id); + + /// Feature request forwarding to workers. + using RawResult = et::task; + + /// Forward a stateful AST query to the worker owning this file. + RawResult forward_query(worker::QueryKind kind, const std::string& uri); + RawResult forward_query(worker::QueryKind kind, + const std::string& uri, + const protocol::Position& position); + + /// Forward a stateless build request (completion/signatureHelp). + RawResult forward_build(worker::BuildKind kind, + const std::string& uri, + const protocol::Position& position); + + /// Completion with preamble-aware include/import handling. + RawResult handle_completion(const std::string& uri, const protocol::Position& position); + + /// Header context management. + void switch_context(std::uint32_t path_id, std::uint32_t context_path_id); + std::optional get_active_context(std::uint32_t path_id) const; + void invalidate_host_contexts(std::uint32_t host_path_id, + llvm::SmallVectorImpl& stale_headers); + + CompileGraph* compile_graph_ptr() { + return compile_graph.get(); + } + + const llvm::DenseMap& module_map() const { + return path_to_module; + } + + void cancel_all(); + + /// Callback invoked when indexing should be scheduled (e.g. after compile success). + std::function on_indexing_needed; + +private: + /// Compile module dependencies, build/reuse PCH, and fill PCM paths. + et::task ensure_deps(std::uint32_t path_id, + llvm::StringRef path, + const std::string& text, + const std::string& directory, + const std::vector& arguments, + std::pair& pch, + std::unordered_map& pcms); + + /// Build or reuse PCH for a source file. + et::task ensure_pch(std::uint32_t path_id, + llvm::StringRef path, + const std::string& text, + const std::string& directory, + const std::vector& arguments); + + /// Check if a file's AST or PCH deps have changed since last compile. + bool is_stale(std::uint32_t path_id); + + /// Record dependency snapshot after a successful compile. + void record_deps(std::uint32_t path_id, llvm::ArrayRef deps); + + void publish_diagnostics(const std::string& uri, int version, const et::serde::RawValue& diags); + void clear_diagnostics(const std::string& uri); + + /// Clean up compilation state for a closed file. + void on_file_closed(std::uint32_t path_id); + + /// Invalidate artifacts after a file save. + /// Returns path_ids of all files dirtied (via compile_graph cascade). + llvm::SmallVector on_file_saved(std::uint32_t path_id); + + /// Header context resolution. + std::optional resolve_header_context(std::uint32_t header_path_id); + bool fill_header_context_args(llvm::StringRef path, + std::uint32_t path_id, + std::string& directory, + std::vector& arguments); + + /// Include/import completion helpers. + PreambleCompletionContext detect_completion_context(const std::string& text, uint32_t offset); + et::serde::RawValue complete_include(const PreambleCompletionContext& ctx, + llvm::StringRef path); + et::serde::RawValue complete_import(const PreambleCompletionContext& ctx); + +private: + et::event_loop& loop; + et::ipc::JsonPeer& peer; + PathPool& path_pool; + WorkerPool& pool; + Indexer& indexer; + const CliceConfig& config; + CompilationDatabase& cdb; + DependencyGraph& dep_graph; + + /// Open document state, keyed by server-level path_id. + llvm::DenseMap documents; + + /// PCH/PCM cache state. + llvm::DenseMap pch_states; + llvm::DenseMap pcm_states; + llvm::DenseMap pcm_paths; + + /// Module compilation ordering. + llvm::DenseMap path_to_module; + std::unique_ptr compile_graph; + + /// Per-file compilation state. + llvm::DenseMap ast_deps; + llvm::DenseMap header_file_contexts; + llvm::DenseMap active_contexts; +}; + +} // namespace clice diff --git a/src/server/indexer.cpp b/src/server/indexer.cpp new file mode 100644 index 00000000..bae5fb20 --- /dev/null +++ b/src/server/indexer.cpp @@ -0,0 +1,698 @@ +#include "server/indexer.h" + +#include +#include +#include +#include + +#include "eventide/ipc/lsp/position.h" +#include "eventide/ipc/lsp/protocol.h" +#include "eventide/ipc/lsp/uri.h" +#include "index/tu_index.h" +#include "support/filesystem.h" +#include "support/logging.h" + +#include "llvm/Support/FileSystem.h" +#include "llvm/Support/MemoryBuffer.h" +#include "llvm/Support/Path.h" +#include "llvm/Support/raw_ostream.h" + +namespace clice { + +namespace lsp = eventide::ipc::lsp; + +/// Find the tightest (innermost) occurrence containing `offset` via binary search. +const static index::Occurrence* lookup_occurrence(const std::vector& occs, + std::uint32_t offset) { + auto it = std::ranges::lower_bound(occs, offset, {}, [](const index::Occurrence& o) { + return o.range.end; + }); + const index::Occurrence* best = nullptr; + while(it != occs.end() && it->range.contains(offset)) { + if(!best || (it->range.end - it->range.begin) < (best->range.end - best->range.begin)) { + best = &*it; + } + ++it; + } + return best; +} + +// ── OpenFileIndex ──────────────────────────────────────────────────────── + +std::optional> + OpenFileIndex::find_occurrence(std::uint32_t offset) const { + if(!mapper) + return std::nullopt; + auto* occ = lookup_occurrence(file_index.occurrences, offset); + if(!occ) + return std::nullopt; + auto start = mapper->to_position(occ->range.begin); + auto end = mapper->to_position(occ->range.end); + if(!start || !end) + return std::nullopt; + return std::pair{ + occ->target, + protocol::Range{*start, *end} + }; +} + +// ── MergedIndexShard ───────────────────────────────────────────────────── + +std::optional> + MergedIndexShard::find_occurrence(std::uint32_t offset) const { + auto* m = mapper(); + if(!m) + return std::nullopt; + std::optional> result; + index.lookup(offset, [&](const index::Occurrence& o) { + auto start = m->to_position(o.range.begin); + auto end = m->to_position(o.range.end); + if(start && end) { + result = { + o.target, + protocol::Range{*start, *end} + }; + } + return false; + }); + return result; +} + +// ── Indexer: data management ───────────────────────────────────────────── + +void Indexer::merge(const void* tu_index_data, std::size_t size) { + auto tu_index = index::TUIndex::from(tu_index_data); + if(tu_index.graph.paths.empty()) { + LOG_WARN("Ignoring TUIndex with empty path graph"); + return; + } + auto file_ids_map = project_index.merge(tu_index); + auto main_tu_path_id = static_cast(tu_index.graph.paths.size() - 1); + + auto merge_file_index = [&](std::uint32_t tu_path_id, index::FileIndex& file_idx) { + auto global_path_id = file_ids_map[tu_path_id]; + auto& shard = merged_indices[global_path_id]; + + if(tu_path_id == main_tu_path_id) { + std::vector include_locs; + for(auto& loc: tu_index.graph.locations) { + index::IncludeLocation remapped = loc; + remapped.path_id = file_ids_map[loc.path_id]; + include_locs.push_back(remapped); + } + auto file_path = project_index.path_pool.path(global_path_id); + llvm::StringRef file_content; + std::string file_content_storage; + auto buf = llvm::MemoryBuffer::getFile(file_path); + if(buf) { + file_content_storage = (*buf)->getBuffer().str(); + file_content = file_content_storage; + } + shard.index.merge(global_path_id, + tu_index.built_at, + std::move(include_locs), + file_idx, + file_content); + } else { + std::optional include_id; + for(std::uint32_t i = 0; i < tu_index.graph.locations.size(); ++i) { + if(tu_index.graph.locations[i].path_id == tu_path_id) { + include_id = i; + break; + } + } + if(!include_id) { + LOG_WARN("Skip merge for path {}: include location not found", global_path_id); + return; + } + auto header_path = project_index.path_pool.path(global_path_id); + llvm::StringRef header_content; + std::string header_content_storage; + auto header_buf = llvm::MemoryBuffer::getFile(header_path); + if(header_buf) { + header_content_storage = (*header_buf)->getBuffer().str(); + header_content = header_content_storage; + } + shard.index.merge(global_path_id, *include_id, file_idx, header_content); + } + shard.invalidate_mapper(); + }; + + for(auto& [tu_path_id, file_idx]: tu_index.path_file_indices) { + merge_file_index(tu_path_id, file_idx); + } + merge_file_index(main_tu_path_id, tu_index.main_file_index); + + LOG_INFO("Merged TUIndex: {} paths, {} symbols, {} merged_shards", + tu_index.graph.paths.size(), + tu_index.symbols.size(), + merged_indices.size()); +} + +void Indexer::save(llvm::StringRef index_dir) { + if(index_dir.empty()) + return; + + auto ec = llvm::sys::fs::create_directories(index_dir); + if(ec) { + LOG_WARN("Failed to create index directory {}: {}", std::string(index_dir), ec.message()); + return; + } + + auto project_path = path::join(index_dir, "project.idx"); + { + std::error_code write_ec; + llvm::raw_fd_ostream os(project_path, write_ec); + if(!write_ec) { + project_index.serialize(os); + LOG_INFO("Saved ProjectIndex to {}", project_path); + } else { + LOG_WARN("Failed to save ProjectIndex: {}", write_ec.message()); + } + } + + auto shards_dir = path::join(index_dir, "shards"); + ec = llvm::sys::fs::create_directories(shards_dir); + if(ec) { + LOG_WARN("Failed to create shards directory: {}", ec.message()); + return; + } + + std::size_t saved = 0; + for(auto& [path_id, shard]: merged_indices) { + if(!shard.index.need_rewrite()) + continue; + auto shard_path = path::join(shards_dir, std::to_string(path_id) + ".idx"); + std::error_code write_ec; + llvm::raw_fd_ostream os(shard_path, write_ec); + if(!write_ec) { + shard.index.serialize(os); + ++saved; + } + } + LOG_INFO("Saved {} MergedIndex shards (of {} total)", saved, merged_indices.size()); +} + +void Indexer::load(llvm::StringRef index_dir) { + if(index_dir.empty()) + return; + + auto project_path = path::join(index_dir, "project.idx"); + auto buf = llvm::MemoryBuffer::getFile(project_path); + if(buf) { + project_index = index::ProjectIndex::from((*buf)->getBufferStart()); + LOG_INFO("Loaded ProjectIndex: {} symbols", project_index.symbols.size()); + } + + auto shards_dir = path::join(index_dir, "shards"); + std::error_code ec; + for(auto it = llvm::sys::fs::directory_iterator(shards_dir, ec); + !ec && it != llvm::sys::fs::directory_iterator(); + it.increment(ec)) { + auto filename = llvm::sys::path::filename(it->path()); + if(!filename.ends_with(".idx")) + continue; + auto stem = filename.drop_back(4); + std::uint32_t path_id = 0; + if(stem.getAsInteger(10, path_id)) + continue; + merged_indices[path_id] = MergedIndexShard{index::MergedIndex::load(it->path())}; + } + + if(!merged_indices.empty()) { + LOG_INFO("Loaded {} MergedIndex shards", merged_indices.size()); + } +} + +bool Indexer::need_update(llvm::StringRef file_path) { + auto cache_it = project_index.path_pool.find(file_path); + if(cache_it == project_index.path_pool.cache.end()) + return true; + + auto merged_it = merged_indices.find(cache_it->second); + if(merged_it == merged_indices.end()) + return true; + + llvm::SmallVector path_mapping; + for(auto& p: project_index.path_pool.paths) { + path_mapping.push_back(p); + } + return merged_it->second.index.need_update(path_mapping); +} + +void Indexer::set_open_file(std::uint32_t server_path_id, + llvm::StringRef file_path, + OpenFileIndex index) { + auto& stored = (open_file_indices[server_path_id] = std::move(index)); + stored.mapper.emplace(stored.content, lsp::PositionEncoding::UTF16); + auto proj_cache_it = project_index.path_pool.find(file_path); + if(proj_cache_it != project_index.path_pool.cache.end()) { + open_proj_path_ids.insert(proj_cache_it->second); + } +} + +void Indexer::remove_open_file(std::uint32_t server_path_id, llvm::StringRef file_path) { + open_file_indices.erase(server_path_id); + auto proj_cache_it = project_index.path_pool.find(file_path); + if(proj_cache_it != project_index.path_pool.cache.end()) { + open_proj_path_ids.erase(proj_cache_it->second); + } +} + +// ── Indexer: symbol queries ────────────────────────────────────────────── + +bool Indexer::find_symbol_info(index::SymbolHash hash, std::string& name, SymbolKind& kind) const { + for(auto& [_, index]: open_file_indices) { + auto it = index.symbols.find(hash); + if(it != index.symbols.end()) { + name = it->second.name; + kind = it->second.kind; + return true; + } + } + auto it = project_index.symbols.find(hash); + if(it != project_index.symbols.end()) { + name = it->second.name; + kind = it->second.kind; + return true; + } + return false; +} + +Indexer::CursorHit Indexer::resolve_cursor(llvm::StringRef path, + std::uint32_t server_path_id, + const protocol::Position& position, + const std::string* doc_text) { + // Try open file index first. + auto it = open_file_indices.find(server_path_id); + if(it != open_file_indices.end()) { + auto& index = it->second; + if(!index.mapper) + return {}; + auto offset = index.mapper->to_offset(position); + if(!offset) + return {}; + if(auto found = index.find_occurrence(*offset)) + return {found->first, found->second}; + return {}; + } + + // Fallback to MergedIndex, using doc_text for position → offset. + if(!doc_text) + return {}; + lsp::PositionMapper doc_mapper(*doc_text, lsp::PositionEncoding::UTF16); + auto offset = doc_mapper.to_offset(position); + if(!offset) + return {}; + + auto proj_it = project_index.path_pool.find(path); + if(proj_it == project_index.path_pool.cache.end()) + return {}; + auto shard_it = merged_indices.find(proj_it->second); + if(shard_it == merged_indices.end()) + return {}; + + if(auto found = shard_it->second.find_occurrence(*offset)) + return {found->first, found->second}; + return {}; +} + +std::vector Indexer::query_relations(llvm::StringRef path, + std::uint32_t server_path_id, + const protocol::Position& position, + RelationKind kind, + const std::string* doc_text) { + auto hit = resolve_cursor(path, server_path_id, position, doc_text); + if(hit.hash == 0) + return {}; + + std::vector locations; + + auto sym_it = project_index.symbols.find(hit.hash); + if(sym_it != project_index.symbols.end()) { + for(auto file_id: sym_it->second.reference_files) { + if(open_proj_path_ids.contains(file_id)) + continue; + auto shard_it = merged_indices.find(file_id); + if(shard_it == merged_indices.end()) + continue; + auto uri = lsp::URI::from_file_path(project_index.path_pool.path(file_id)); + if(!uri) + continue; + shard_it->second.find_relations(hit.hash, + kind, + [&](const auto&, protocol::Range range) { + locations.push_back({uri->str(), range}); + return true; + }); + } + } + + for(auto& [id, index]: open_file_indices) { + auto uri = lsp::URI::from_file_path(std::string(path_pool.resolve(id))); + if(!uri) + continue; + index.find_relations(hit.hash, kind, [&](const auto&, protocol::Range range) { + locations.push_back({uri->str(), range}); + return true; + }); + } + + return locations; +} + +std::optional Indexer::lookup_symbol(const std::string& uri, + llvm::StringRef path, + std::uint32_t server_path_id, + const protocol::Position& position, + const std::string* doc_text) { + auto hit = resolve_cursor(path, server_path_id, position, doc_text); + if(hit.hash == 0) + return std::nullopt; + + std::string name; + SymbolKind sym_kind; + if(!find_symbol_info(hit.hash, name, sym_kind)) + return std::nullopt; + + return SymbolInfo{hit.hash, std::move(name), sym_kind, uri, hit.range}; +} + +std::optional Indexer::find_definition_location(index::SymbolHash hash) { + // Open file indices first (fresher data for actively-edited files). + for(auto& [id, index]: open_file_indices) { + auto uri = lsp::URI::from_file_path(std::string(path_pool.resolve(id))); + if(!uri) + continue; + std::optional result; + index.find_relations(hash, + RelationKind::Definition, + [&](const auto&, protocol::Range range) { + result = protocol::Location{uri->str(), range}; + return false; + }); + if(result) + return result; + } + + // Fall back to ProjectIndex reference files. + auto sym_it = project_index.symbols.find(hash); + if(sym_it == project_index.symbols.end()) + return std::nullopt; + + for(auto file_id: sym_it->second.reference_files) { + if(open_proj_path_ids.contains(file_id)) + continue; + auto shard_it = merged_indices.find(file_id); + if(shard_it == merged_indices.end()) + continue; + auto uri = lsp::URI::from_file_path(project_index.path_pool.path(file_id)); + if(!uri) + continue; + std::optional result; + shard_it->second.find_relations(hash, + RelationKind::Definition, + [&](const auto&, protocol::Range range) { + result = protocol::Location{uri->str(), range}; + return false; + }); + if(result) + return result; + } + + return std::nullopt; +} + +std::optional + Indexer::resolve_hierarchy_item(const std::string& uri, + llvm::StringRef path, + std::uint32_t server_path_id, + const protocol::Range& range, + const std::optional& data, + const std::string* doc_text) { + if(data) { + if(auto* int_val = std::get_if(&*data)) { + auto hash = static_cast(*int_val); + std::string name; + SymbolKind kind; + if(find_symbol_info(hash, name, kind)) { + return SymbolInfo{hash, std::move(name), kind, uri, range}; + } + } + } + return lookup_symbol(uri, path, server_path_id, range.start, doc_text); +} + +// ── Indexer: relation collection helpers ───────────────────────────────── + +void Indexer::collect_grouped_relations( + index::SymbolHash hash, + RelationKind kind, + llvm::DenseMap>& target_ranges) { + auto sym_it = project_index.symbols.find(hash); + if(sym_it != project_index.symbols.end()) { + for(auto file_id: sym_it->second.reference_files) { + if(open_proj_path_ids.contains(file_id)) + continue; + auto shard_it = merged_indices.find(file_id); + if(shard_it == merged_indices.end()) + continue; + shard_it->second.find_relations(hash, kind, [&](const auto& r, protocol::Range range) { + target_ranges[r.target_symbol].push_back(range); + return true; + }); + } + } + for(auto& [_, index]: open_file_indices) { + index.find_relations(hash, kind, [&](const auto& r, protocol::Range range) { + target_ranges[r.target_symbol].push_back(range); + return true; + }); + } +} + +void Indexer::collect_unique_targets(index::SymbolHash hash, + RelationKind kind, + llvm::SmallVectorImpl& targets) { + llvm::DenseSet seen; + auto sym_it = project_index.symbols.find(hash); + if(sym_it != project_index.symbols.end()) { + for(auto file_id: sym_it->second.reference_files) { + if(open_proj_path_ids.contains(file_id)) + continue; + auto shard_it = merged_indices.find(file_id); + if(shard_it == merged_indices.end()) + continue; + /// No position conversion needed — just collect target symbol hashes. + shard_it->second.index.lookup(hash, kind, [&](const index::Relation& r) { + if(seen.insert(r.target_symbol).second) { + targets.push_back(r.target_symbol); + } + return true; + }); + } + } + for(auto& [_, index]: open_file_indices) { + auto rel_it = index.file_index.relations.find(hash); + if(rel_it == index.file_index.relations.end()) + continue; + for(auto& r: rel_it->second) { + if(r.kind & kind) { + if(seen.insert(r.target_symbol).second) { + targets.push_back(r.target_symbol); + } + } + } + } +} + +// ── Indexer: hierarchy queries ─────────────────────────────────────────── + +/// Resolve a symbol hash into a SymbolInfo with definition location. +/// Returns nullopt if the symbol or its definition cannot be found. +std::optional Indexer::resolve_symbol(index::SymbolHash hash) { + std::string name; + SymbolKind kind; + if(!find_symbol_info(hash, name, kind)) + return std::nullopt; + auto def_loc = find_definition_location(hash); + if(!def_loc) + return std::nullopt; + return SymbolInfo{hash, std::move(name), kind, def_loc->uri, def_loc->range}; +} + +std::vector + Indexer::find_incoming_calls(index::SymbolHash hash) { + llvm::DenseMap> caller_ranges; + collect_grouped_relations(hash, RelationKind::Caller, caller_ranges); + + std::vector results; + for(auto& [caller_hash, ranges]: caller_ranges) { + auto info = resolve_symbol(caller_hash); + if(!info) + continue; + results.push_back({build_call_hierarchy_item(*info), std::move(ranges)}); + } + return results; +} + +std::vector + Indexer::find_outgoing_calls(index::SymbolHash hash) { + llvm::DenseMap> callee_ranges; + collect_grouped_relations(hash, RelationKind::Callee, callee_ranges); + + std::vector results; + for(auto& [callee_hash, ranges]: callee_ranges) { + auto info = resolve_symbol(callee_hash); + if(!info) + continue; + results.push_back({build_call_hierarchy_item(*info), std::move(ranges)}); + } + return results; +} + +std::vector Indexer::find_supertypes(index::SymbolHash hash) { + llvm::SmallVector base_hashes; + collect_unique_targets(hash, RelationKind::Base, base_hashes); + + std::vector results; + for(auto target_hash: base_hashes) { + auto info = resolve_symbol(target_hash); + if(!info) + continue; + results.push_back(build_type_hierarchy_item(*info)); + } + return results; +} + +std::vector Indexer::find_subtypes(index::SymbolHash hash) { + llvm::SmallVector derived_hashes; + collect_unique_targets(hash, RelationKind::Derived, derived_hashes); + + std::vector results; + for(auto target_hash: derived_hashes) { + auto info = resolve_symbol(target_hash); + if(!info) + continue; + results.push_back(build_type_hierarchy_item(*info)); + } + return results; +} + +std::vector Indexer::search_symbols(llvm::StringRef query, + std::size_t max_results) { + std::string query_lower = query.lower(); + + auto is_indexable_kind = [](SymbolKind sk) { + return sk == SymbolKind::Namespace || sk == SymbolKind::Class || sk == SymbolKind::Struct || + sk == SymbolKind::Union || sk == SymbolKind::Enum || sk == SymbolKind::Type || + sk == SymbolKind::Field || sk == SymbolKind::EnumMember || + sk == SymbolKind::Function || sk == SymbolKind::Method || + sk == SymbolKind::Variable || sk == SymbolKind::Parameter || + sk == SymbolKind::Macro || sk == SymbolKind::Concept || sk == SymbolKind::Module || + sk == SymbolKind::Operator || sk == SymbolKind::MacroParameter || + sk == SymbolKind::Label || sk == SymbolKind::Attribute; + }; + + auto matches_query = [&](llvm::StringRef name) { + if(query_lower.empty()) + return true; + return llvm::StringRef(name).lower().find(query_lower) != std::string::npos; + }; + + std::vector results; + llvm::DenseSet seen; + + for(auto& [hash, symbol]: project_index.symbols) { + if(results.size() >= max_results) + break; + if(!is_indexable_kind(symbol.kind) || symbol.name.empty()) + continue; + if(!matches_query(symbol.name)) + continue; + auto def_loc = find_definition_location(hash); + if(!def_loc) + continue; + + protocol::SymbolInformation info; + info.name = symbol.name; + info.kind = to_lsp_symbol_kind(symbol.kind); + info.location = std::move(*def_loc); + results.push_back(std::move(info)); + seen.insert(hash); + } + + for(auto& [_, index]: open_file_indices) { + if(results.size() >= max_results) + break; + for(auto& [hash, symbol]: index.symbols) { + if(results.size() >= max_results) + break; + if(seen.contains(hash)) + continue; + if(!is_indexable_kind(symbol.kind) || symbol.name.empty()) + continue; + if(!matches_query(symbol.name)) + continue; + auto def_loc = find_definition_location(hash); + if(!def_loc) + continue; + + protocol::SymbolInformation info; + info.name = symbol.name; + info.kind = to_lsp_symbol_kind(symbol.kind); + info.location = std::move(*def_loc); + results.push_back(std::move(info)); + seen.insert(hash); + } + } + return results; +} + +// ── Indexer: static utilities ──────────────────────────────────────────── + +protocol::SymbolKind Indexer::to_lsp_symbol_kind(SymbolKind kind) { + switch(kind) { + case SymbolKind::Namespace: return protocol::SymbolKind::Namespace; + case SymbolKind::Class: return protocol::SymbolKind::Class; + case SymbolKind::Struct: return protocol::SymbolKind::Struct; + case SymbolKind::Union: return protocol::SymbolKind::Class; + case SymbolKind::Enum: return protocol::SymbolKind::Enum; + case SymbolKind::Type: return protocol::SymbolKind::TypeParameter; + case SymbolKind::Field: return protocol::SymbolKind::Field; + case SymbolKind::EnumMember: return protocol::SymbolKind::EnumMember; + case SymbolKind::Function: return protocol::SymbolKind::Function; + case SymbolKind::Method: return protocol::SymbolKind::Method; + case SymbolKind::Variable: return protocol::SymbolKind::Variable; + case SymbolKind::Parameter: return protocol::SymbolKind::Variable; + case SymbolKind::Macro: return protocol::SymbolKind::Function; + case SymbolKind::Concept: return protocol::SymbolKind::Interface; + case SymbolKind::Module: return protocol::SymbolKind::Module; + case SymbolKind::Operator: return protocol::SymbolKind::Operator; + default: return protocol::SymbolKind::Variable; + } +} + +protocol::CallHierarchyItem Indexer::build_call_hierarchy_item(const SymbolInfo& info) { + protocol::CallHierarchyItem item; + item.name = info.name; + item.kind = to_lsp_symbol_kind(info.kind); + item.uri = info.uri; + item.range = info.range; + item.selection_range = info.range; + item.data = protocol::LSPAny(static_cast(info.hash)); + return item; +} + +protocol::TypeHierarchyItem Indexer::build_type_hierarchy_item(const SymbolInfo& info) { + protocol::TypeHierarchyItem item; + item.name = info.name; + item.kind = to_lsp_symbol_kind(info.kind); + item.uri = info.uri; + item.range = info.range; + item.selection_range = info.range; + item.data = protocol::LSPAny(static_cast(info.hash)); + return item; +} + +} // namespace clice diff --git a/src/server/indexer.h b/src/server/indexer.h new file mode 100644 index 00000000..214e10d8 --- /dev/null +++ b/src/server/indexer.h @@ -0,0 +1,251 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "eventide/ipc/lsp/position.h" +#include "eventide/ipc/lsp/protocol.h" +#include "index/merged_index.h" +#include "index/project_index.h" +#include "semantic/relation_kind.h" +#include "semantic/symbol_kind.h" +#include "support/path_pool.h" + +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/StringRef.h" + +namespace clice { + +namespace et = eventide; +namespace protocol = et::ipc::protocol; +namespace lsp = et::ipc::lsp; + +/// In-memory index for an open file. Kept separate from MergedIndex because +/// open files change frequently, are based on unsaved buffer content, and only +/// need to track the main file (headers are covered by PCH/PCM indexing). +struct OpenFileIndex { + index::FileIndex file_index; + index::SymbolTable symbols; + std::string content; ///< Buffer text at index time (for position mapping). + + /// Cached PositionMapper built from `content`. Avoids re-scanning line + /// offsets on every query. Initialized by Indexer::set_open_file(). + std::optional mapper; + + /// Find the tightest occurrence containing `offset`. + /// Returns (symbol_hash, LSP range) with positions already converted. + std::optional> + find_occurrence(std::uint32_t offset) const; + + /// Iterate relations matching `kind`, calling back with pre-converted ranges. + /// Callback: (const index::Relation&, protocol::Range) -> bool (true = continue). + template + void find_relations(index::SymbolHash hash, RelationKind kind, Fn&& fn) const { + if(!mapper) + return; + auto it = file_index.relations.find(hash); + if(it == file_index.relations.end()) + return; + for(auto& r: it->second) { + if(r.kind & kind) { + auto start = mapper->to_position(r.range.begin); + auto end = mapper->to_position(r.range.end); + if(start && end) { + if(!fn(r, protocol::Range{*start, *end})) + return; + } + } + } + } +}; + +/// Wraps index::MergedIndex with a lazily-cached PositionMapper. +struct MergedIndexShard { + index::MergedIndex index; + mutable std::optional cached_mapper; + + /// Get or lazily build a PositionMapper from the index's stored content. + const lsp::PositionMapper* mapper() const { + if(!cached_mapper) { + auto c = index.content(); + if(!c.empty()) { + cached_mapper.emplace(c, lsp::PositionEncoding::UTF16); + } + } + return cached_mapper ? &*cached_mapper : nullptr; + } + + /// Invalidate the cached mapper (call after merge changes content). + void invalidate_mapper() { + cached_mapper.reset(); + } + + /// Find occurrence at byte offset. + /// Returns (symbol_hash, LSP range) with positions already converted. + std::optional> + find_occurrence(std::uint32_t offset) const; + + /// Iterate relations matching `kind`, calling back with pre-converted ranges. + /// Callback: (const index::Relation&, protocol::Range) -> bool (true = continue). + template + void find_relations(index::SymbolHash hash, RelationKind kind, Fn&& fn) const { + auto* m = mapper(); + if(!m) + return; + index.lookup(hash, kind, [&](const index::Relation& r) { + auto start = m->to_position(r.range.begin); + auto end = m->to_position(r.range.end); + if(start && end) { + return fn(r, protocol::Range{*start, *end}); + } + return true; + }); + } +}; + +/// Information about a symbol at a given position. +struct SymbolInfo { + index::SymbolHash hash = 0; + std::string name; + SymbolKind kind; + std::string uri; + protocol::Range range; +}; + +/// Owns all index state (ProjectIndex, MergedIndex shards, open file indices) +/// and provides query methods for cross-file navigation. +/// +/// Background indexing scheduling is driven by MasterServer; Indexer is the +/// pure data + query layer. +class Indexer { +public: + explicit Indexer(PathPool& path_pool) : path_pool(path_pool) {} + + /// Merge a TUIndex result into ProjectIndex and MergedIndex shards. + void merge(const void* tu_index_data, std::size_t size); + + /// Save ProjectIndex and MergedIndex shards to disk. + void save(llvm::StringRef index_dir); + + /// Load ProjectIndex and MergedIndex shards from disk. + void load(llvm::StringRef index_dir); + + /// Check whether a file needs re-indexing (stale or missing shard). + bool need_update(llvm::StringRef file_path); + + /// Store or replace the open file index for a server-level path_id. + void set_open_file(std::uint32_t server_path_id, + llvm::StringRef file_path, + OpenFileIndex index); + + /// Remove the open file index and untrack project-level path_id. + void remove_open_file(std::uint32_t server_path_id, llvm::StringRef file_path); + + /// Query relations (Definition, Reference, etc.) for a symbol at cursor. + /// @param doc_text Fallback text when the file has no open file index yet. + std::vector query_relations(llvm::StringRef path, + std::uint32_t server_path_id, + const protocol::Position& position, + RelationKind kind, + const std::string* doc_text); + + /// Look up symbol info (hash, name, kind, range) at a cursor position. + std::optional lookup_symbol(const std::string& uri, + llvm::StringRef path, + std::uint32_t server_path_id, + const protocol::Position& position, + const std::string* doc_text); + + /// Find the definition location of a symbol by hash. + std::optional find_definition_location(index::SymbolHash hash); + + /// Find a symbol's name and kind by hash. + bool find_symbol_info(index::SymbolHash hash, std::string& name, SymbolKind& kind) const; + + /// Resolve a hierarchy item (from stored data or by position lookup). + std::optional resolve_hierarchy_item(const std::string& uri, + llvm::StringRef path, + std::uint32_t server_path_id, + const protocol::Range& range, + const std::optional& data, + const std::string* doc_text); + + /// Find incoming calls to a function. + std::vector find_incoming_calls(index::SymbolHash hash); + + /// Find outgoing calls from a function. + std::vector find_outgoing_calls(index::SymbolHash hash); + + /// Find supertypes (base classes) of a type. + std::vector find_supertypes(index::SymbolHash hash); + + /// Find subtypes (derived classes) of a type. + std::vector find_subtypes(index::SymbolHash hash); + + /// Search symbols by name substring. + std::vector search_symbols(llvm::StringRef query, + std::size_t max_results = 100); + + /// Convert internal SymbolKind to LSP SymbolKind. + static protocol::SymbolKind to_lsp_symbol_kind(SymbolKind kind); + + /// Build hierarchy items from SymbolInfo. + static protocol::CallHierarchyItem build_call_hierarchy_item(const SymbolInfo& info); + static protocol::TypeHierarchyItem build_type_hierarchy_item(const SymbolInfo& info); + + /// Direct access to ProjectIndex for background indexing. + index::ProjectIndex& project_index_ref() { + return project_index; + } + +private: + /// Result of resolving a symbol at a cursor position. + struct CursorHit { + index::SymbolHash hash = 0; + protocol::Range range{}; + }; + + /// Resolve the symbol at (position), checking open file index first then + /// falling back to MergedIndex. + CursorHit resolve_cursor(llvm::StringRef path, + std::uint32_t server_path_id, + const protocol::Position& position, + const std::string* doc_text); + + /// Collect relations grouped by target symbol, across all index sources. + void collect_grouped_relations( + index::SymbolHash hash, + RelationKind kind, + llvm::DenseMap>& target_ranges); + + /// Collect unique target symbol hashes for a relation kind. + void collect_unique_targets(index::SymbolHash hash, + RelationKind kind, + llvm::SmallVectorImpl& targets); + + /// Resolve a symbol hash into a SymbolInfo with definition location. + std::optional resolve_symbol(index::SymbolHash hash); + +private: + PathPool& path_pool; + + /// Global symbol table and path pool shared across all TUs. + index::ProjectIndex project_index; + + /// Per-file MergedIndex shards (keyed by project-level path_id). + llvm::DenseMap merged_indices; + + /// In-memory indices for currently open files (keyed by server-level path_id). + llvm::DenseMap open_file_indices; + + /// Project-level path_ids of open files, used to skip stale MergedIndex + /// shards when fresher OpenFileIndex data is available. + llvm::DenseSet open_proj_path_ids; +}; + +} // namespace clice diff --git a/src/server/master_server.cpp b/src/server/master_server.cpp index 43f036a2..efe85988 100644 --- a/src/server/master_server.cpp +++ b/src/server/master_server.cpp @@ -1,37 +1,21 @@ #include "server/master_server.h" #include -#include #include -#include #include -#include -#include -#include -#include "command/search_config.h" -#include "eventide/ipc/json_codec.h" -#include "eventide/ipc/lsp/position.h" #include "eventide/ipc/lsp/protocol.h" #include "eventide/ipc/lsp/uri.h" #include "eventide/reflection/enum.h" #include "eventide/serde/json/json.h" -#include "eventide/serde/serde/raw_value.h" -#include "index/tu_index.h" #include "semantic/symbol_kind.h" #include "server/protocol.h" #include "support/filesystem.h" #include "support/logging.h" -#include "syntax/dependency_graph.h" -#include "syntax/include_resolver.h" -#include "syntax/scan.h" -#include "llvm/Support/Chrono.h" #include "llvm/Support/FileSystem.h" #include "llvm/Support/Path.h" #include "llvm/Support/Process.h" -#include "llvm/Support/raw_ostream.h" -#include "llvm/Support/xxhash.h" namespace clice { @@ -40,320 +24,26 @@ namespace lsp = eventide::ipc::lsp; namespace refl = eventide::refl; using et::ipc::RequestResult; using RequestContext = et::ipc::JsonPeer::RequestContext; +using serde_raw = et::serde::RawValue; -/// Hash a file's content using xxh3_64bits. Returns 0 on read failure. -static std::uint64_t hash_file(llvm::StringRef path) { - auto buf = llvm::MemoryBuffer::getFile(path); - if(!buf) - return 0; - return llvm::xxh3_64bits((*buf)->getBuffer()); -} - -/// Capture a two-layer staleness snapshot after a successful compilation. -/// Interns dependency paths into the PathPool and hashes each file's content. -static DepsSnapshot capture_deps_snapshot(PathPool& pool, llvm::ArrayRef deps) { - DepsSnapshot snap; - // Capture timestamp BEFORE hashing to avoid TOCTOU: if a file is modified - // during hashing, its mtime will be > build_at, triggering Layer 2 re-hash. - snap.build_at = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); - snap.path_ids.reserve(deps.size()); - snap.hashes.reserve(deps.size()); - for(const auto& file: deps) { - snap.path_ids.push_back(pool.intern(file)); - snap.hashes.push_back(hash_file(file)); - } - return snap; -} - -/// Two-layer staleness check. -/// -/// Layer 1 (fast): stat each dep file, compare mtime against build_at. -/// If all mtimes <= build_at → nothing changed, return false immediately. -/// -/// Layer 2 (precise): for files with mtime > build_at, re-hash their content. -/// If the hash matches the stored hash → file was touched but not modified. -/// If any hash differs → truly changed, return true. -static bool deps_changed(const PathPool& pool, const DepsSnapshot& snap) { - for(std::size_t i = 0; i < snap.path_ids.size(); ++i) { - auto path = pool.resolve(snap.path_ids[i]); - llvm::sys::fs::file_status status; - if(auto ec = llvm::sys::fs::status(path, status)) { - // File disappeared — definitely changed. - if(snap.hashes[i] != 0) - return true; - continue; - } - - // Layer 1: mtime check (cheap, stat only). - auto current_mtime = llvm::sys::toTimeT(status.getLastModificationTime()); - if(current_mtime <= snap.build_at) - continue; - - // Layer 2: mtime is newer — re-hash content to confirm actual change. - auto current_hash = hash_file(path); - if(current_hash != snap.hashes[i]) - return true; - } - return false; +/// Serialize a value to a JSON RawValue using LSP config. +template +static serde_raw to_raw(const T& value) { + auto json = et::serde::json::to_json(value); + return serde_raw{json ? std::move(*json) : "null"}; } MasterServer::MasterServer(et::event_loop& loop, et::ipc::JsonPeer& peer, std::string self_path) : - loop(loop), peer(peer), pool(loop), self_path(std::move(self_path)) {} + loop(loop), peer(peer), pool(loop), indexer(path_pool), + compiler(loop, peer, path_pool, pool, indexer, config, cdb, dependency_graph), + self_path(std::move(self_path)) {} -MasterServer::~MasterServer() { - if(compile_graph) { - compile_graph->cancel_all(); - } -} - -std::string MasterServer::uri_to_path(const std::string& uri) { - auto parsed = lsp::URI::parse(uri); - if(parsed.has_value()) { - auto path = parsed->file_path(); - if(path.has_value()) { - return std::move(*path); - } - } - return uri; -} - -void MasterServer::publish_diagnostics(const std::string& uri, - int version, - const et::serde::RawValue& diagnostics_json) { - std::vector diagnostics; - if(!diagnostics_json.empty()) { - auto status = et::serde::json::from_json(diagnostics_json.data, diagnostics); - if(!status) { - LOG_WARN("Failed to deserialize diagnostics JSON for {}", uri); - } - } - protocol::PublishDiagnosticsParams params; - params.uri = uri; - params.version = version; - params.diagnostics = std::move(diagnostics); - peer.send_notification(params); -} - -void MasterServer::clear_diagnostics(const std::string& uri) { - protocol::PublishDiagnosticsParams params; - params.uri = uri; - params.diagnostics = {}; - peer.send_notification(params); -} - -/// Serializable cache structures for cache.json persistence. -/// Paths are stored in a shared table and referenced by index to avoid -/// redundant storage (a single file can depend on thousands of headers, -/// many of which are shared across entries). -namespace { - -struct CacheDepEntry { - std::uint32_t path; // index into CacheData::paths - std::uint64_t hash; -}; - -struct CachePCHEntry { - std::string filename; - std::uint32_t source_file; // index into CacheData::paths - std::uint64_t hash; - std::uint32_t bound; - std::int64_t build_at; - std::vector deps; -}; - -struct CachePCMEntry { - std::string filename; - std::uint32_t source_file; // index into CacheData::paths - std::string module_name; - std::int64_t build_at; - std::vector deps; -}; - -struct CacheData { - std::vector paths; - std::vector pch; - std::vector pcm; -}; - -} // namespace - -void MasterServer::load_cache() { - if(config.cache_dir.empty()) - return; - - auto cache_path = path::join(config.cache_dir, "cache", "cache.json"); - auto content = fs::read(cache_path); - if(!content) { - LOG_DEBUG("No cache.json found at {}", cache_path); - return; - } - - CacheData data; - auto status = et::serde::json::from_json(*content, data); - if(!status) { - LOG_WARN("Failed to parse cache.json"); - return; - } - - auto resolve = [&](std::uint32_t idx) -> llvm::StringRef { - return idx < data.paths.size() ? llvm::StringRef(data.paths[idx]) : ""; - }; - - for(auto& entry: data.pch) { - auto pch_path = path::join(config.cache_dir, "cache", "pch", entry.filename); - auto source = resolve(entry.source_file); - if(!llvm::sys::fs::exists(pch_path) || source.empty()) - continue; - - DepsSnapshot deps; - deps.build_at = entry.build_at; - for(auto& dep: entry.deps) { - auto dep_path = resolve(dep.path); - if(dep_path.empty()) - continue; - deps.path_ids.push_back(path_pool.intern(dep_path)); - deps.hashes.push_back(dep.hash); - } - - auto path_id = path_pool.intern(source); - auto& st = pch_states[path_id]; - st.path = pch_path; - st.hash = entry.hash; - st.bound = entry.bound; - st.deps = std::move(deps); - - LOG_DEBUG("Loaded cached PCH: {} -> {}", source, pch_path); - } - - for(auto& entry: data.pcm) { - auto pcm_path = path::join(config.cache_dir, "cache", "pcm", entry.filename); - auto source = resolve(entry.source_file); - if(!llvm::sys::fs::exists(pcm_path) || source.empty()) - continue; - - DepsSnapshot deps; - deps.build_at = entry.build_at; - for(auto& dep: entry.deps) { - auto dep_path = resolve(dep.path); - if(dep_path.empty()) - continue; - deps.path_ids.push_back(path_pool.intern(dep_path)); - deps.hashes.push_back(dep.hash); - } - - auto path_id = path_pool.intern(source); - pcm_states[path_id] = {pcm_path, std::move(deps)}; - pcm_paths[path_id] = pcm_path; - - LOG_DEBUG("Loaded cached PCM: {} (module {}) -> {}", source, entry.module_name, pcm_path); - } - - LOG_INFO("Loaded cache.json: {} PCH entries, {} PCM entries", - pch_states.size(), - pcm_states.size()); -} - -void MasterServer::save_cache() { - if(config.cache_dir.empty()) - return; - - CacheData data; - std::unordered_map index_map; - - auto intern = [&](std::uint32_t runtime_path_id) -> std::uint32_t { - auto path = std::string(path_pool.resolve(runtime_path_id)); - auto [it, inserted] = - index_map.try_emplace(path, static_cast(data.paths.size())); - if(inserted) { - data.paths.push_back(path); - } - return it->second; - }; - - for(auto& [path_id, st]: pch_states) { - if(st.path.empty()) - continue; - - CachePCHEntry entry; - entry.filename = std::string(path::filename(st.path)); - entry.source_file = intern(path_id); - entry.hash = st.hash; - entry.bound = st.bound; - entry.build_at = st.deps.build_at; - for(std::size_t i = 0; i < st.deps.path_ids.size(); ++i) { - entry.deps.push_back({intern(st.deps.path_ids[i]), st.deps.hashes[i]}); - } - data.pch.push_back(std::move(entry)); - } - - for(auto& [path_id, st]: pcm_states) { - if(st.path.empty()) - continue; - - CachePCMEntry entry; - entry.filename = std::string(path::filename(st.path)); - entry.source_file = intern(path_id); - auto mod_it = path_to_module.find(path_id); - entry.module_name = mod_it != path_to_module.end() ? mod_it->second : ""; - entry.build_at = st.deps.build_at; - for(std::size_t i = 0; i < st.deps.path_ids.size(); ++i) { - entry.deps.push_back({intern(st.deps.path_ids[i]), st.deps.hashes[i]}); - } - data.pcm.push_back(std::move(entry)); - } - - auto json_str = et::serde::json::to_json(data); - if(!json_str) { - LOG_WARN("Failed to serialize cache.json"); - return; - } - - auto cache_path = path::join(config.cache_dir, "cache", "cache.json"); - auto tmp_path = cache_path + ".tmp"; - auto write_result = fs::write(tmp_path, *json_str); - if(!write_result) { - LOG_WARN("Failed to write cache.json.tmp: {}", write_result.error().message()); - return; - } - auto rename_result = fs::rename(tmp_path, cache_path); - if(!rename_result) { - LOG_WARN("Failed to rename cache.json.tmp to cache.json: {}", - rename_result.error().message()); - } -} - -void MasterServer::cleanup_cache(int max_age_days) { - if(config.cache_dir.empty()) - return; - - auto now = std::chrono::system_clock::now(); - auto max_age = std::chrono::hours(max_age_days * 24); - - for(auto* subdir: {"cache/pch", "cache/pcm"}) { - auto dir = path::join(config.cache_dir, subdir); - std::error_code ec; - for(auto it = llvm::sys::fs::directory_iterator(dir, ec); - !ec && it != llvm::sys::fs::directory_iterator(); - it.increment(ec)) { - llvm::sys::fs::file_status status; - if(auto stat_ec = llvm::sys::fs::status(it->path(), status)) - continue; - - auto mtime = status.getLastModificationTime(); - auto age = now - mtime; - if(age > max_age) { - llvm::sys::fs::remove(it->path()); - LOG_DEBUG("Cleaned up stale cache file: {}", it->path()); - } - } - } -} +MasterServer::~MasterServer() = default; et::task<> MasterServer::load_workspace() { if(workspace_root.empty()) co_return; - // Create cache directory if configured if(!config.cache_dir.empty()) { auto ec = llvm::sys::fs::create_directories(config.cache_dir); if(ec) { @@ -362,7 +52,6 @@ et::task<> MasterServer::load_workspace() { LOG_INFO("Cache directory: {}", config.cache_dir); } - // Create cache/pch/ and cache/pcm/ subdirectories for(auto* subdir: {"cache/pch", "cache/pcm"}) { auto dir = path::join(config.cache_dir, subdir); auto ec2 = llvm::sys::fs::create_directories(dir); @@ -373,14 +62,11 @@ et::task<> MasterServer::load_workspace() { // Clean up stale files first, then load — load_cache() only restores // entries still listed in cache.json, so cleanup won't delete live files. - cleanup_cache(); - load_cache(); + compiler.cleanup_cache(); + compiler.load_cache(); } - // Search for compile_commands.json std::string cdb_path; - - // If the config specifies a CDB path, use it if(!config.compile_commands_path.empty()) { if(llvm::sys::fs::exists(config.compile_commands_path)) { cdb_path = config.compile_commands_path; @@ -390,7 +76,6 @@ et::task<> MasterServer::load_workspace() { } } - // Otherwise auto-detect in common locations if(cdb_path.empty()) { for(auto* subdir: {"build", "cmake-build-debug", "cmake-build-release", "out", "."}) { auto candidate = path::join(workspace_root, subdir, "compile_commands.json"); @@ -410,8 +95,6 @@ et::task<> MasterServer::load_workspace() { LOG_INFO("Loaded CDB from {} with {} entries", cdb_path, count); auto report = scan_dependency_graph(cdb, path_pool, dependency_graph); - - // Build reverse include map so headers can find their host source files. dependency_graph.build_reverse_map(); auto unresolved = report.includes_found - report.includes_resolved; @@ -434,18 +117,9 @@ et::task<> MasterServer::load_workspace() { LOG_WARN("{} unresolved includes", unresolved); } - // Build reverse mapping: path_id -> module name. - for(auto& [module_name, path_ids]: dependency_graph.modules()) { - for(auto path_id: path_ids) { - path_to_module[path_id] = module_name.str(); - } - } + compiler.build_module_map(); + indexer.load(config.index_dir); - // Load persisted index from disk. - load_index(); - - // Build index queue from CDB entries (all source files). - // CDB entries use the CDB's internal path_ids; convert to server path_ids. if(config.enable_indexing) { for(auto& entry: cdb.get_entries()) { auto file = cdb.resolve_path(entry.file); @@ -458,886 +132,7 @@ et::task<> MasterServer::load_workspace() { } } - if(path_to_module.empty()) { - LOG_INFO("No C++20 modules detected, skipping CompileGraph"); - co_return; - } - - // Lazy dependency resolver: scans a module file on demand to discover imports. - auto resolve = [this](std::uint32_t path_id) -> llvm::SmallVector { - auto file_path = path_pool.resolve(path_id); - auto results = cdb.lookup(file_path, {.query_toolchain = true, .suppress_logging = true}); - if(results.empty()) { - return {}; - } - - auto& ctx = results[0]; - auto scan_result = scan_precise(ctx.arguments, ctx.directory); - - llvm::SmallVector deps; - for(auto& mod_name: scan_result.modules) { - auto mod_ids = dependency_graph.lookup_module(mod_name); - if(!mod_ids.empty()) { - deps.push_back(mod_ids[0]); - } - } - - // Module implementation units implicitly depend on their interface unit. - if(!scan_result.module_name.empty() && !scan_result.is_interface_unit) { - auto mod_ids = dependency_graph.lookup_module(scan_result.module_name); - if(!mod_ids.empty()) { - deps.push_back(mod_ids[0]); - } - } - - return deps; - }; - - // Dispatch: sends BuildPCM request to a stateless worker. - auto dispatch = [this](std::uint32_t path_id) -> et::task { - auto mod_it = path_to_module.find(path_id); - if(mod_it == path_to_module.end()) { - co_return false; - } - - auto file_path = std::string(path_pool.resolve(path_id)); - - worker::BuildPCMParams pcm_params; - pcm_params.file = file_path; - if(!fill_compile_args(file_path, pcm_params.directory, pcm_params.arguments)) { - co_return false; - } - - // Compute deterministic content-addressed PCM path. - // Replace ':' with '-' in module name for filesystem safety. - // Hash includes file path AND compile arguments so that argument - // changes (e.g. -DFOO) invalidate the cached PCM. - auto safe_module_name = mod_it->second; - std::ranges::replace(safe_module_name, ':', '-'); - std::string hash_input = file_path; - for(auto& arg: pcm_params.arguments) { - hash_input += arg; - } - auto args_hash = llvm::xxh3_64bits(llvm::StringRef(hash_input)); - auto pcm_filename = std::format("{}-{:016x}.pcm", safe_module_name, args_hash); - auto pcm_path = path::join(config.cache_dir, "cache", "pcm", pcm_filename); - - // Check if cached PCM is still valid. - if(auto pcm_it = pcm_states.find(path_id); pcm_it != pcm_states.end()) { - if(!pcm_it->second.path.empty() && llvm::sys::fs::exists(pcm_it->second.path) && - !deps_changed(path_pool, pcm_it->second.deps)) { - pcm_paths[path_id] = pcm_it->second.path; - co_return true; - } - } - - pcm_params.module_name = mod_it->second; - pcm_params.output_path = pcm_path; - - // Clang needs ALL transitive PCM deps, not just direct imports. - for(auto& [pid, existing_pcm_path]: pcm_paths) { - auto dep_mod_it = path_to_module.find(pid); - if(dep_mod_it != path_to_module.end()) { - pcm_params.pcms[dep_mod_it->second] = existing_pcm_path; - } - } - - auto result = co_await pool.send_stateless(pcm_params); - if(!result.has_value() || !result.value().success) { - LOG_WARN("BuildPCM failed for module {}: {}", - mod_it->second, - result.has_value() ? result.value().error : result.error().message); - co_return false; - } - - pcm_paths[path_id] = result.value().pcm_path; - pcm_states[path_id] = {result.value().pcm_path, - capture_deps_snapshot(path_pool, result.value().deps)}; - LOG_INFO("Built PCM for module {}: {}", mod_it->second, result.value().pcm_path); - - // Merge module index into ProjectIndex/MergedIndex. - if(!result.value().tu_index_data.empty()) { - merge_index_result(result.value().tu_index_data.data(), - result.value().tu_index_data.size()); - } - - // Persist cache metadata after successful build. - save_cache(); - - co_return true; - }; - - compile_graph = std::make_unique(std::move(dispatch), std::move(resolve)); - LOG_INFO("CompileGraph initialized with {} module(s)", path_to_module.size()); -} - -std::optional - MasterServer::resolve_header_context(std::uint32_t header_path_id) { - // Find source files that transitively include this header. - auto hosts = dependency_graph.find_host_sources(header_path_id); - if(hosts.empty()) { - LOG_DEBUG("resolve_header_context: no host sources for path_id={}", header_path_id); - return std::nullopt; - } - - // If there's an active context override, prefer that host. - std::uint32_t host_path_id = 0; - std::vector chain; - auto active_it = active_contexts.find(header_path_id); - if(active_it != active_contexts.end()) { - auto preferred = active_it->second; - auto preferred_path = path_pool.resolve(preferred); - auto results = cdb.lookup(preferred_path, {.suppress_logging = true}); - if(!results.empty()) { - auto c = dependency_graph.find_include_chain(preferred, header_path_id); - if(!c.empty()) { - host_path_id = preferred; - chain = std::move(c); - } - } - } - - // Fall back to the first available host that has a CDB entry. - if(chain.empty()) { - for(auto candidate: hosts) { - auto candidate_path = path_pool.resolve(candidate); - auto results = cdb.lookup(candidate_path, {.suppress_logging = true}); - if(results.empty()) - continue; - auto c = dependency_graph.find_include_chain(candidate, header_path_id); - if(c.empty()) - continue; - host_path_id = candidate; - chain = std::move(c); - break; - } - } - - if(chain.empty()) { - LOG_DEBUG("resolve_header_context: no usable host with include chain for path_id={}", - header_path_id); - return std::nullopt; - } - - // Build preamble text: for each file in the chain except the last (target), - // append all content up to (but not including) the line that includes the - // next file in the chain. - std::string preamble; - for(std::size_t i = 0; i + 1 < chain.size(); ++i) { - auto cur_id = chain[i]; - auto next_id = chain[i + 1]; - - auto cur_path = path_pool.resolve(cur_id); - auto next_path = path_pool.resolve(next_id); - auto next_filename = llvm::sys::path::filename(next_path); - - // Prefer in-memory document text over disk content. - std::string content; - if(auto doc_it = documents.find(cur_id); doc_it != documents.end()) { - content = doc_it->second.text; - } else { - auto buf = llvm::MemoryBuffer::getFile(cur_path); - if(!buf) { - LOG_WARN("resolve_header_context: cannot read {}", cur_path); - return std::nullopt; - } - content = (*buf)->getBuffer().str(); - } - - // Scan line by line for the #include that brings in next_filename. - llvm::StringRef content_ref(content); - std::size_t line_start = 0; - std::size_t include_line_start = std::string::npos; - while(line_start <= content_ref.size()) { - auto newline_pos = content_ref.find('\n', line_start); - auto line_end = - (newline_pos == llvm::StringRef::npos) ? content_ref.size() : newline_pos; - auto line = content_ref.slice(line_start, line_end).trim(); - - if(line.starts_with("#include") || line.starts_with("# include")) { - // Extract the filename from the #include directive. - // Handles: #include "foo.h", #include , # include "foo.h" - auto quote_start = line.find_first_of("\"<"); - auto quote_end = llvm::StringRef::npos; - if(quote_start != llvm::StringRef::npos) { - char close = (line[quote_start] == '"') ? '"' : '>'; - quote_end = line.find(close, quote_start + 1); - } - if(quote_start != llvm::StringRef::npos && quote_end != llvm::StringRef::npos) { - auto included = line.slice(quote_start + 1, quote_end); - auto included_filename = llvm::sys::path::filename(included); - if(included_filename == next_filename) { - include_line_start = line_start; - break; - } - } - } - - line_start = - (newline_pos == llvm::StringRef::npos) ? content_ref.size() + 1 : newline_pos + 1; - } - - // Emit a #line marker then all content before the include line. - preamble += std::format("#line 1 \"{}\"\n", cur_path.str()); - if(include_line_start != std::string::npos) { - preamble += content_ref.substr(0, include_line_start).str(); - } else { - // No matching include line found — emit the whole file to be safe. - LOG_DEBUG("resolve_header_context: include line for {} not found in {}, emitting full", - next_filename, - cur_path); - preamble += content; - } - } - - // Hash the preamble and write to cache directory. - auto preamble_hash = llvm::xxh3_64bits(llvm::StringRef(preamble)); - auto preamble_filename = std::format("{:016x}.h", preamble_hash); - auto preamble_dir = path::join(config.cache_dir, "header_context"); - auto preamble_path = path::join(preamble_dir, preamble_filename); - - if(!llvm::sys::fs::exists(preamble_path)) { - auto ec = llvm::sys::fs::create_directories(preamble_dir); - if(ec) { - LOG_WARN("resolve_header_context: cannot create dir {}: {}", - preamble_dir, - ec.message()); - return std::nullopt; - } - if(auto result = fs::write(preamble_path, preamble); !result) { - LOG_WARN("resolve_header_context: cannot write preamble {}: {}", - preamble_path, - result.error().message()); - return std::nullopt; - } - LOG_INFO("resolve_header_context: wrote preamble {} for header path_id={}", - preamble_path, - header_path_id); - } - - return HeaderFileContext{host_path_id, preamble_path, preamble_hash}; -} - -bool MasterServer::fill_compile_args(llvm::StringRef path, - std::string& directory, - std::vector& arguments) { - auto path_id = path_pool.intern(path); - - // 1. If the user has set an active header context via switchContext, - // use the host source's CDB entry with file path replaced and preamble injected. - auto active_it = active_contexts.find(path_id); - if(active_it != active_contexts.end()) { - return fill_header_context_args(path, path_id, directory, arguments); - } - - // 2. Normal CDB lookup for the file itself. - auto results = cdb.lookup(path, {.query_toolchain = true}); - if(!results.empty()) { - auto& ctx = results.front(); - directory = ctx.directory.str(); - arguments.clear(); - for(auto* arg: ctx.arguments) { - arguments.emplace_back(arg); - } - return true; - } - - // 3. No CDB entry — try automatic header context resolution. - return fill_header_context_args(path, path_id, directory, arguments); -} - -bool MasterServer::fill_header_context_args(llvm::StringRef path, - std::uint32_t path_id, - std::string& directory, - std::vector& arguments) { - // Use cached context if available; otherwise resolve. - // If an active context override exists, invalidate cache if it points to - // a different host so we re-resolve with the correct one. - const HeaderFileContext* ctx_ptr = nullptr; - auto ctx_it = header_file_contexts.find(path_id); - auto active_it = active_contexts.find(path_id); - if(ctx_it != header_file_contexts.end()) { - if(active_it != active_contexts.end() && ctx_it->second.host_path_id != active_it->second) { - header_file_contexts.erase(ctx_it); - } else { - ctx_ptr = &ctx_it->second; - } - } - if(!ctx_ptr) { - auto resolved = resolve_header_context(path_id); - if(!resolved) { - LOG_WARN("No CDB entry and no header context for {}", path); - return false; - } - header_file_contexts[path_id] = std::move(*resolved); - ctx_ptr = &header_file_contexts[path_id]; - } - - auto host_path = path_pool.resolve(ctx_ptr->host_path_id); - auto host_results = cdb.lookup(host_path, {.query_toolchain = true}); - if(host_results.empty()) { - LOG_WARN("fill_header_context_args: host {} has no CDB entry", host_path); - return false; - } - - auto& host_ctx = host_results.front(); - directory = host_ctx.directory.str(); - arguments.clear(); - - // Copy host arguments, replacing the host source file path with the header. - bool replaced = false; - for(auto& arg: host_ctx.arguments) { - if(llvm::StringRef(arg) == host_path) { - arguments.emplace_back(path); - replaced = true; - } else { - arguments.emplace_back(arg); - } - } - if(!replaced) { - LOG_WARN("fill_header_context_args: host path {} not found in arguments, appending header", - host_path); - arguments.emplace_back(path); - } - - // Inject preamble: for cc1 args insert after "-cc1", otherwise after driver. - std::size_t inject_pos = 1; - if(arguments.size() >= 2 && arguments[1] == "-cc1") { - inject_pos = 2; - } - arguments.insert(arguments.begin() + inject_pos, ctx_ptr->preamble_path); - arguments.insert(arguments.begin() + inject_pos, "-include"); - - LOG_INFO("fill_compile_args: header context for {} (host={}, preamble={})", - path, - host_path, - ctx_ptr->preamble_path); - return true; -} - -et::task MasterServer::ensure_pch(std::uint32_t path_id, - llvm::StringRef path, - const std::string& text, - const std::string& directory, - const std::vector& arguments) { - auto bound = compute_preamble_bound(text); - if(bound == 0) { - // No preamble directives — PCH would be empty. Clear any stale entry. - pch_states.erase(path_id); - co_return true; - } - - auto preamble_hash = llvm::xxh3_64bits(llvm::StringRef(text).substr(0, bound)); - - // Deterministic content-addressed PCH path. - auto pch_path = - path::join(config.cache_dir, "cache", "pch", std::format("{:016x}.pch", preamble_hash)); - - // Reuse existing PCH if preamble content and deps haven't changed. - if(auto it = pch_states.find(path_id); it != pch_states.end()) { - auto& st = it->second; - if(st.hash == preamble_hash && !st.path.empty() && !deps_changed(path_pool, st.deps)) { - st.bound = bound; - co_return true; - } - } - - // Preamble incomplete (user still typing) — defer rebuild, reuse old PCH if available. - if(!is_preamble_complete(text, bound)) { - LOG_DEBUG("Preamble incomplete for {}, deferring PCH rebuild", path); - co_return pch_states.count(path_id) && !pch_states[path_id].path.empty(); - } - - // If another coroutine is already building PCH for this file, wait for it. - if(auto it = pch_states.find(path_id); it != pch_states.end() && it->second.building) { - co_await it->second.building->wait(); - co_return !pch_states[path_id].path.empty(); - } - - // Register in-flight build so concurrent requests wait on us. - auto completion = std::make_shared(); - pch_states[path_id].building = completion; - - // Build a new PCH via stateless worker. - worker::BuildPCHParams pch_params; - pch_params.file = std::string(path); - pch_params.directory = directory; - pch_params.arguments = arguments; - pch_params.content = text; - pch_params.preamble_bound = bound; - pch_params.output_path = pch_path; - - LOG_DEBUG("Building PCH for {}, bound={}, output={}", path, bound, pch_path); - - auto result = co_await pool.send_stateless(pch_params); - - if(!result.has_value() || !result.value().success) { - LOG_WARN("PCH build failed for {}: {}", - path, - result.has_value() ? result.value().error : result.error().message); - pch_states[path_id].building.reset(); - completion->set(); - co_return false; - } - - // Update state — no need to delete old file; content-addressed names differ - // when content differs, and the 7-day cleanup handles orphaned files. - auto& st = pch_states[path_id]; - st.path = result.value().pch_path; - st.bound = bound; - st.hash = preamble_hash; - st.deps = capture_deps_snapshot(path_pool, result.value().deps); - st.building.reset(); - - LOG_INFO("PCH built for {}: {}", path, result.value().pch_path); - - // Merge preamble header index into ProjectIndex/MergedIndex. - if(!result.value().tu_index_data.empty()) { - merge_index_result(result.value().tu_index_data.data(), - result.value().tu_index_data.size()); - } - - // Persist cache metadata after successful build. - save_cache(); - - completion->set(); - co_return true; -} - -/// Compile module dependencies, build/reuse PCH, and fill PCM paths. -/// Shared preparation step used by both ensure_compiled() (stateful path) -/// and forward_stateless() (completion/signatureHelp path). -et::task MasterServer::ensure_deps(std::uint32_t path_id, - llvm::StringRef path, - const std::string& text, - const std::string& directory, - const std::vector& arguments, - std::pair& pch, - std::unordered_map& pcms) { - // Compile C++20 module dependencies (PCMs). - if(compile_graph && !co_await compile_graph->compile_deps(path_id)) { - co_return false; - } - - // Scan buffer text for module imports that might not be in compile_graph yet. - // When a user adds `import std;` without saving, the compile_graph (disk-based) - // doesn't know about the new dependency. Scan the in-memory text to find them. - { - auto scan_result = scan(text); - for(auto& mod_name: scan_result.modules) { - if(mod_name.empty()) { - continue; - } - bool found = false; - for(auto& [pid, name]: path_to_module) { - if(name == mod_name) { - // If PCM not already built, try to build it. - if(pcm_paths.find(pid) == pcm_paths.end()) { - if(compile_graph && compile_graph->has_unit(pid)) { - co_await compile_graph->compile_deps(pid); - } - } - found = true; - break; - } - } - if(!found) { - LOG_DEBUG("Buffer imports unknown module '{}', skipping", mod_name); - } - } - } - - // Build or reuse PCH. - auto pch_ok = co_await ensure_pch(path_id, path, text, directory, arguments); - if(pch_ok) { - if(auto pch_it = pch_states.find(path_id); pch_it != pch_states.end()) { - pch = {pch_it->second.path, pch_it->second.bound}; - } - } - - // Fill all available PCM paths so clang can resolve transitive imports. - // Exclude the file's own PCM to avoid "multiple module declarations". - for(auto& [pid, pcm_path]: pcm_paths) { - if(pid == path_id) - continue; - auto mod_it = path_to_module.find(pid); - if(mod_it != path_to_module.end()) { - pcms[mod_it->second] = pcm_path; - } - } - - co_return true; -} - -/// Pull-based compilation entry point for user-opened files. -/// -/// Called lazily by forward_stateful() / forward_stateless() before every -/// feature request (hover, semantic tokens, etc.). Guarantees that when it -/// returns true the stateful worker assigned to `path_id` holds an up-to-date -/// AST and diagnostics have been published to the client. -/// -/// Lifecycle overview (pull-based model): -/// -/// didOpen / didChange – only update DocumentState, mark ast_dirty -/// didSave – mark dependents dirty, queue indexing -/// feature request arrives – calls ensure_compiled() first -/// 1. Fast-path exit if AST is already clean (!ast_dirty). -/// 2. Compile any C++20 module dependencies (PCMs) via CompileGraph. -/// 3. Build / reuse the precompiled header (PCH) via ensure_pch(). -/// 4. Send CompileParams to the stateful worker, which builds the AST. -/// 5. On success: publish diagnostics, clear ast_dirty, schedule indexing. -/// 6. On generation mismatch (user edited during compile): keep dirty, -/// the next feature request will trigger another compile cycle. -/// -/// Only the opened file itself is remapped (its in-memory text is sent to the -/// worker); every other file is read from disk by the compiler. -/// -/// Concurrency: multiple concurrent feature requests for the same file will -/// each call ensure_compiled(). The first one launches a detached compile -/// task via loop.schedule(); subsequent ones wait on the shared event. -/// The detached task cannot be cancelled by LSP $/cancelRequest, preventing -/// the race where cancellation wakes all waiters and they all start compiles. -et::task MasterServer::ensure_compiled(std::uint32_t path_id) { - auto it = documents.find(path_id); - if(it == documents.end()) { - LOG_WARN("ensure_compiled: doc not found for path_id={} path={}", - path_id, - path_pool.resolve(path_id)); - co_return false; - } - - auto& doc = it->second; - LOG_DEBUG("ensure_compiled: path_id={} version={} gen={} ast_dirty={}", - path_id, - doc.version, - doc.generation, - doc.ast_dirty); - - if(!doc.ast_dirty) { - bool changed = false; - auto ast_deps_it = ast_deps.find(path_id); - if(ast_deps_it != ast_deps.end() && deps_changed(path_pool, ast_deps_it->second)) { - changed = true; - } - if(!changed) { - auto pch_it = pch_states.find(path_id); - if(pch_it != pch_states.end() && deps_changed(path_pool, pch_it->second.deps)) { - changed = true; - } - } - if(!changed) { - co_return true; - } - doc.ast_dirty = true; - } - - // If another compile is already in flight, wait for it. - // This co_await may be cancelled by LSP $/cancelRequest — that's fine, - // it just means this particular feature request is abandoned. The - // detached compile task keeps running independently. - while(it->second.compiling) { - auto pending = it->second.compiling; - co_await pending->done.wait(); - it = documents.find(path_id); - if(it == documents.end()) - co_return false; - if(!it->second.ast_dirty) - co_return true; - } - - // No compile in flight and AST is dirty — launch a detached compile task. - // The detached task is scheduled via loop.schedule() so it is NOT subject - // to LSP $/cancelRequest cancellation. This eliminates the race where - // cancellation fires the RAII guard, waking all waiters simultaneously - // and causing them all to start new compiles. - auto pending_compile = std::make_shared(); - it->second.compiling = pending_compile; - - LOG_INFO("ensure_compiled: launching detached compile path_id={} gen={}", - path_id, - doc.generation); - - loop.schedule([](MasterServer* self, - std::uint32_t pid, - std::shared_ptr pc) -> et::task<> { - // All parameters are copied into the coroutine frame as function args, - // so they survive the lambda temporary's destruction. - auto finish_compile = [&]() { - if(auto it = self->documents.find(pid); it != self->documents.end()) { - if(it->second.compiling == pc) { - it->second.compiling.reset(); - } - } - LOG_INFO("ensure_compiled: finish_compile (detached) path_id={}", pid); - pc->done.set(); - }; - - auto it = self->documents.find(pid); - if(it == self->documents.end()) { - finish_compile(); - co_return; - } - - auto gen = it->second.generation; - LOG_INFO("ensure_compiled: starting compile (detached) path_id={} gen={}", pid, gen); - - auto file_path = std::string(self->path_pool.resolve(pid)); - auto uri = lsp::URI::from_file_path(file_path); - std::string uri_str = uri.has_value() ? uri->str() : file_path; - - // ── Phase 1–3: Module deps, PCH, PCM paths ───────────────────── - worker::CompileParams params; - params.path = file_path; - params.version = it->second.version; - params.text = it->second.text; - if(!self->fill_compile_args(self->path_pool.resolve(pid), - params.directory, - params.arguments)) { - finish_compile(); - co_return; - } - - if(!co_await self->ensure_deps(pid, - params.path, - params.text, - params.directory, - params.arguments, - params.pch, - params.pcms)) { - LOG_WARN("Dependency preparation failed for {}, skipping compile", uri_str); - finish_compile(); - co_return; - } - - it = self->documents.find(pid); - if(it == self->documents.end()) { - finish_compile(); - co_return; - } - - // ── Phase 4: Dispatch to stateful worker ──────────────────────── - auto result = co_await self->pool.send_stateful(pid, params); - - // Re-lookup: the document may have been closed while we were compiling. - it = self->documents.find(pid); - if(it == self->documents.end()) { - finish_compile(); - co_return; - } - - auto& doc2 = it->second; - - // ── Phase 5: Handle result ────────────────────────────────────── - if(doc2.generation != gen) { - LOG_INFO("ensure_compiled: generation mismatch ({} vs {}) for {}", - doc2.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; - } - - doc2.ast_dirty = false; - pc->succeeded = true; - self->ast_deps[pid] = capture_deps_snapshot(self->path_pool, 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 = doc2.text; - self->open_file_indices[pid] = std::move(ofi); - - // Track project-level path_id for cross-file query filtering. - auto proj_cache_it = self->project_index.path_pool.find(file_path); - if(proj_cache_it != self->project_index.path_pool.cache.end()) { - self->open_proj_path_ids.insert(proj_cache_it->second); - } - } - - finish_compile(); - - // Publish diagnostics AFTER marking compile as done, so that concurrent - // forward_stateful() calls can proceed immediately. - self->publish_diagnostics(uri_str, doc2.version, result.value().diagnostics); - self->schedule_indexing(); - }(this, path_id, pending_compile)); - - // Wait for the detached compile to finish. If this wait is cancelled - // by LSP $/cancelRequest, the detached task continues unaffected. - co_await pending_compile->done.wait(); - - it = documents.find(path_id); - if(it == documents.end()) - co_return false; - - co_return !it->second.ast_dirty; -} - -// ========================================================================= -// Index integration -// ========================================================================= - -void MasterServer::merge_index_result(const void* tu_index_data, std::size_t size) { - auto tu_index = index::TUIndex::from(tu_index_data); - - // Merge symbols into ProjectIndex, get TU-local path_id -> global path_id mapping. - auto file_ids_map = project_index.merge(tu_index); - - auto main_tu_path_id = static_cast(tu_index.graph.paths.size() - 1); - - // Merge a single file's index into the corresponding MergedIndex shard. - auto merge_file_index = [&](std::uint32_t tu_path_id, index::FileIndex& file_idx) { - auto global_path_id = file_ids_map[tu_path_id]; - auto& merged = merged_indices[global_path_id]; - - if(tu_path_id == main_tu_path_id) { - // Main file (source file) gets a compilation context with include locations. - // Collect ALL include locations with path_ids remapped to project-level ids. - std::vector include_locs; - for(auto& loc: tu_index.graph.locations) { - index::IncludeLocation remapped = loc; - remapped.path_id = file_ids_map[loc.path_id]; - include_locs.push_back(remapped); - } - // Read the file content from disk for position mapping in queries. - auto file_path = project_index.path_pool.path(global_path_id); - llvm::StringRef file_content; - std::string file_content_storage; - auto buf = llvm::MemoryBuffer::getFile(file_path); - if(buf) { - file_content_storage = (*buf)->getBuffer().str(); - file_content = file_content_storage; - } - merged.merge(global_path_id, - tu_index.built_at, - std::move(include_locs), - file_idx, - file_content); - } else { - // Header files get a header context keyed by include location. - std::uint32_t include_id = 0; - for(std::uint32_t i = 0; i < tu_index.graph.locations.size(); ++i) { - if(tu_index.graph.locations[i].path_id == tu_path_id) { - include_id = i; - break; - } - } - // Read header file content for position mapping in queries. - auto header_path = project_index.path_pool.path(global_path_id); - llvm::StringRef header_content; - std::string header_content_storage; - auto header_buf = llvm::MemoryBuffer::getFile(header_path); - if(header_buf) { - header_content_storage = (*header_buf)->getBuffer().str(); - header_content = header_content_storage; - } - merged.merge(global_path_id, include_id, file_idx, header_content); - } - }; - - // Merge from path_file_indices (deserialized TUIndex from IPC). - for(auto& [tu_path_id, file_idx]: tu_index.path_file_indices) { - merge_file_index(tu_path_id, file_idx); - } - - // Merge main file index. - merge_file_index(main_tu_path_id, tu_index.main_file_index); - - LOG_INFO("Merged TUIndex: {} paths, {} symbols, {} merged_shards", - tu_index.graph.paths.size(), - tu_index.symbols.size(), - merged_indices.size()); -} - -void MasterServer::save_index() { - if(config.index_dir.empty()) - return; - - auto ec = llvm::sys::fs::create_directories(config.index_dir); - if(ec) { - LOG_WARN("Failed to create index directory {}: {}", config.index_dir, ec.message()); - return; - } - - // Save ProjectIndex. - auto project_path = path::join(config.index_dir, "project.idx"); - { - std::error_code write_ec; - llvm::raw_fd_ostream os(project_path, write_ec); - if(!write_ec) { - project_index.serialize(os); - LOG_INFO("Saved ProjectIndex to {}", project_path); - } else { - LOG_WARN("Failed to save ProjectIndex: {}", write_ec.message()); - } - } - - // Save MergedIndex shards. - auto shards_dir = path::join(config.index_dir, "shards"); - ec = llvm::sys::fs::create_directories(shards_dir); - if(ec) { - LOG_WARN("Failed to create shards directory: {}", ec.message()); - return; - } - - std::size_t saved = 0; - for(auto& [path_id, merged]: merged_indices) { - if(!merged.need_rewrite()) - continue; - auto shard_path = path::join(shards_dir, std::to_string(path_id) + ".idx"); - std::error_code write_ec; - llvm::raw_fd_ostream os(shard_path, write_ec); - if(!write_ec) { - merged.serialize(os); - ++saved; - } - } - LOG_INFO("Saved {} MergedIndex shards (of {} total)", saved, merged_indices.size()); -} - -void MasterServer::load_index() { - if(config.index_dir.empty()) - return; - - // Load ProjectIndex. - auto project_path = path::join(config.index_dir, "project.idx"); - auto buf = llvm::MemoryBuffer::getFile(project_path); - if(buf) { - project_index = index::ProjectIndex::from((*buf)->getBufferStart()); - LOG_INFO("Loaded ProjectIndex: {} symbols", project_index.symbols.size()); - } - - // Load MergedIndex shards. - auto shards_dir = path::join(config.index_dir, "shards"); - std::error_code ec; - for(auto it = llvm::sys::fs::directory_iterator(shards_dir, ec); - !ec && it != llvm::sys::fs::directory_iterator(); - it.increment(ec)) { - auto filename = llvm::sys::path::filename(it->path()); - if(!filename.ends_with(".idx")) - continue; - - auto stem = filename.drop_back(4); // remove ".idx" - std::uint32_t path_id = 0; - if(stem.getAsInteger(10, path_id)) - continue; - - merged_indices[path_id] = index::MergedIndex::load(it->path()); - } - - if(!merged_indices.empty()) { - LOG_INFO("Loaded {} MergedIndex shards", merged_indices.size()); - } + compiler.init_compile_graph(); } void MasterServer::schedule_indexing() { @@ -1345,7 +140,6 @@ void MasterServer::schedule_indexing() { return; indexing_scheduled = true; - // Create or reset idle timer. if(!index_idle_timer) { index_idle_timer = std::make_shared(et::timer::create(loop)); } @@ -1354,7 +148,6 @@ void MasterServer::schedule_indexing() { } et::task<> MasterServer::run_background_indexing() { - // Wait for idle timeout before starting. if(index_idle_timer) { co_await index_idle_timer->wait(); } @@ -1374,44 +167,21 @@ et::task<> MasterServer::run_background_indexing() { auto file_path = std::string(path_pool.resolve(server_path_id)); - // Skip open files — their index comes from the stateful worker and is - // stored in open_file_indices. When closed, they rejoin the queue. - if(documents.count(server_path_id)) { + /// Skip open files — their index comes from the stateful worker. + if(compiler.is_file_open(server_path_id)) { continue; } - // Check if the index needs update by checking mtime against existing shard. - // If the file is not yet in the project_index path pool, it has never been - // indexed — always proceed. Only skip when we already have a shard that is - // still fresh. - auto cache_it = project_index.path_pool.find(file_path); - if(cache_it != project_index.path_pool.cache.end()) { - auto proj_path_id = cache_it->second; - auto merged_it = merged_indices.find(proj_path_id); - if(merged_it != merged_indices.end()) { - // Build path mapping for need_update check. - llvm::SmallVector path_mapping; - for(auto& p: project_index.path_pool.paths) { - path_mapping.push_back(p); - } - if(!merged_it->second.need_update(path_mapping)) - continue; - } - } + if(!indexer.need_update(file_path)) + continue; - // Prepare IndexParams for the stateless worker. - worker::IndexParams params; + worker::BuildParams params; + params.kind = worker::BuildKind::Index; params.file = file_path; - if(!fill_compile_args(file_path, params.directory, params.arguments)) + if(!compiler.fill_compile_args(file_path, params.directory, params.arguments)) continue; - // Fill PCM deps for module-aware indexing. - for(auto& [pid, pcm_path]: pcm_paths) { - auto mod_it = path_to_module.find(pid); - if(mod_it != path_to_module.end()) { - params.pcms[mod_it->second] = pcm_path; - } - } + compiler.fill_pcm_deps(params.pcms); LOG_INFO("Background indexing: {}", file_path); @@ -1420,8 +190,7 @@ et::task<> MasterServer::run_background_indexing() { LOG_INFO("Background indexing got TUIndex for {}: {} bytes", file_path, result.value().tu_index_data.size()); - merge_index_result(result.value().tu_index_data.data(), - result.value().tu_index_data.size()); + indexer.merge(result.value().tu_index_data.data(), result.value().tu_index_data.size()); ++processed; } else if(result.has_value() && !result.value().success) { LOG_WARN("Background index failed for {}: {}", file_path, result.value().error); @@ -1434,724 +203,40 @@ et::task<> MasterServer::run_background_indexing() { indexing_active = false; LOG_INFO("Background indexing complete: {} files processed", processed); - - // Persist index to disk after a full pass. - save_index(); -} - -// ========================================================================= -// Include/import completion (handled in master) -// ========================================================================= - -PreambleCompletionContext MasterServer::detect_completion_context(const std::string& text, - uint32_t offset) { - // Find the start of the line containing offset. - auto line_start = text.rfind('\n', offset > 0 ? offset - 1 : 0); - line_start = (line_start == std::string::npos) ? 0 : line_start + 1; - - // Find the end of the line. - auto line_end = text.find('\n', offset); - if(line_end == std::string::npos) - line_end = text.size(); - - // Extract the line up to the cursor position. - auto line = llvm::StringRef(text).slice(line_start, offset); - - // Strip leading whitespace. - auto trimmed = line.ltrim(); - - // Check for #include "prefix or #include arguments; - if(!fill_compile_args(path, directory, arguments)) - return et::serde::RawValue{"[]"}; - - // Convert arguments to const char* array. - std::vector args_ptrs; - args_ptrs.reserve(arguments.size()); - for(auto& arg: arguments) { - args_ptrs.push_back(arg.c_str()); - } - - auto config = extract_search_config(args_ptrs, directory); - DirListingCache dir_cache; - auto resolved = resolve_search_config(config, dir_cache); - - // Determine search range based on context. - unsigned start_idx = 0; - if(ctx.kind == CompletionContext::IncludeAngled) { - start_idx = resolved.angled_start_idx; - } - - // Split prefix into dir_prefix and file_prefix if it contains '/'. - llvm::StringRef prefix_ref(ctx.prefix); - llvm::StringRef dir_prefix; - llvm::StringRef file_prefix = prefix_ref; - auto slash_pos = prefix_ref.rfind('/'); - if(slash_pos != llvm::StringRef::npos) { - dir_prefix = prefix_ref.slice(0, slash_pos); - file_prefix = prefix_ref.slice(slash_pos + 1, llvm::StringRef::npos); - } - - std::vector items; - llvm::StringSet<> seen; // Deduplicate entries across search dirs. - - for(unsigned i = start_idx; i < resolved.dirs.size(); ++i) { - auto& search_dir = resolved.dirs[i]; - - // If there's a dir_prefix, resolve the subdirectory. - const llvm::StringSet<>* entries = nullptr; - if(!dir_prefix.empty()) { - llvm::SmallString<256> sub_path(search_dir.path); - llvm::sys::path::append(sub_path, dir_prefix); - entries = resolve_dir(sub_path, dir_cache); - } else { - entries = search_dir.entries; - } - - if(!entries) - continue; - - for(auto& entry: *entries) { - auto name = entry.getKey(); - if(!name.starts_with(file_prefix)) - continue; - if(!seen.insert(name).second) - continue; - - // Check if this entry is a directory. - llvm::SmallString<256> full_path(search_dir.path); - if(!dir_prefix.empty()) { - llvm::sys::path::append(full_path, dir_prefix); - } - llvm::sys::path::append(full_path, name); - - bool is_dir = false; - llvm::sys::fs::is_directory(llvm::Twine(full_path), is_dir); - - protocol::CompletionItem item; - if(is_dir) { - item.label = (name + "/").str(); - } else { - item.label = name.str(); - } - item.kind = protocol::CompletionItemKind::File; - items.push_back(std::move(item)); - } - } - - auto json = et::serde::json::to_json(items); - return et::serde::RawValue{json ? std::move(*json) : "[]"}; -} - -et::serde::RawValue MasterServer::complete_import(const PreambleCompletionContext& ctx) { - std::vector items; - llvm::StringRef prefix_ref(ctx.prefix); - - for(auto& [path_id, module_name]: path_to_module) { - llvm::StringRef name_ref(module_name); - if(!name_ref.starts_with(prefix_ref)) - continue; - - protocol::CompletionItem item; - item.label = module_name; - item.kind = protocol::CompletionItemKind::Module; - item.insert_text = module_name + ";"; - items.push_back(std::move(item)); - } - - auto json = et::serde::json::to_json(items); - return et::serde::RawValue{json ? std::move(*json) : "[]"}; -} - -// ========================================================================= -// Forwarding helpers -// ========================================================================= - -using serde_raw = et::serde::RawValue; - -template -MasterServer::RawResult MasterServer::forward_stateful(const std::string& uri) { - auto path = uri_to_path(uri); - auto path_id = path_pool.intern(path); - - if(!co_await ensure_compiled(path_id)) { - co_return serde_raw{"null"}; - } - - // After ensure_compiled returns, a new didChange may have arrived making - // the AST stale again. Sending a feature request with stale state is - // wasteful and — more importantly — the queued IPC writes can fill up - // the pipe buffer and deadlock the worker. Drop the request instead. - auto dit = documents.find(path_id); - if(dit != documents.end() && dit->second.ast_dirty) { - co_return serde_raw{"null"}; - } - - WorkerParams wp; - wp.path = path; - - auto result = co_await pool.send_stateful(path_id, wp); - if(!result.has_value()) { - co_return serde_raw{}; - } - co_return std::move(result.value()); -} - -template -MasterServer::RawResult MasterServer::forward_stateful(const std::string& uri, - const protocol::Position& position) { - auto path = uri_to_path(uri); - auto path_id = path_pool.intern(path); - - if(!co_await ensure_compiled(path_id)) { - co_return serde_raw{"null"}; - } - - auto doc_it = documents.find(path_id); - if(doc_it == documents.end()) { - co_return serde_raw{"null"}; - } - - // Drop stale requests — see comment in the other overload. - if(doc_it->second.ast_dirty) { - co_return serde_raw{"null"}; - } - - WorkerParams wp; - wp.path = path; - - lsp::PositionMapper mapper(doc_it->second.text, lsp::PositionEncoding::UTF16); - auto offset = mapper.to_offset(position); - if(!offset) - co_return serde_raw{"null"}; - wp.offset = *offset; - - auto result = co_await pool.send_stateful(path_id, wp); - if(!result.has_value()) { - co_return serde_raw{}; - } - co_return std::move(result.value()); -} - -template -MasterServer::RawResult MasterServer::forward_stateless(const std::string& uri, - const protocol::Position& position) { - auto path = uri_to_path(uri); - auto path_id = path_pool.intern(path); - - LOG_DEBUG("forward_stateless: {} path={} pos={}:{}", - "request", - path, - position.line, - position.character); - - auto doc_it = documents.find(path_id); - if(doc_it == documents.end()) { - LOG_DEBUG("forward_stateless: doc not found for {}", path); - co_return serde_raw{}; - } - - auto& doc = doc_it->second; - - WorkerParams wp; - wp.path = path; - wp.version = doc.version; - wp.text = doc.text; - if(!fill_compile_args(path, wp.directory, wp.arguments)) { - LOG_DEBUG("forward_stateless: no CDB for {}", path); - co_return serde_raw{}; - } - - // Ensure module deps, PCH, and PCM paths are ready for stateless compilation. - if(!co_await ensure_deps(path_id, path, wp.text, wp.directory, wp.arguments, wp.pch, wp.pcms)) { - LOG_DEBUG("forward_stateless: ensure_deps failed for {}", path); - co_return serde_raw{}; - } - - lsp::PositionMapper mapper(wp.text, lsp::PositionEncoding::UTF16); - auto offset = mapper.to_offset(position); - if(!offset) - co_return serde_raw{"null"}; - wp.offset = *offset; - - auto result = co_await pool.send_stateless(wp); - if(!result.has_value()) { - LOG_DEBUG("forward_stateless: worker error for {}: {}", path, result.error().message); - co_return serde_raw{}; - } - LOG_DEBUG("forward_stateless: done {}", path); - co_return std::move(result.value()); -} - -// Serialize a value to a JSON RawValue using LSP config. -template -static serde_raw to_raw(const T& value) { - auto json = et::serde::json::to_json(value); - return serde_raw{json ? std::move(*json) : "null"}; -} - -/// Look up the first occurrence containing `offset` in a sorted occurrence list. -/// Uses lower_bound on range.end, then scans forward through overlapping -/// occurrences (e.g. nested templates, macro expansions) to find the tightest -/// (innermost) match. Returns nullptr if no occurrence contains the offset. -const static index::Occurrence* lookup_occurrence(const std::vector& occs, - std::uint32_t offset) { - auto it = std::ranges::lower_bound(occs, offset, {}, [](const index::Occurrence& o) { - return o.range.end; - }); - - const index::Occurrence* best = nullptr; - while(it != occs.end() && it->range.contains(offset)) { - // Prefer the narrowest (innermost) occurrence that contains the offset. - if(!best || (it->range.end - it->range.begin) < (best->range.end - best->range.begin)) { - best = &*it; - } - ++it; - } - return best; -} - -/// Find a symbol's name and kind by hash. Searches open file indices first -/// (fresher data for actively-edited files), then falls back to ProjectIndex. -/// Returns false if the symbol is not found anywhere. -bool MasterServer::find_symbol_info(index::SymbolHash hash, - std::string& name, - SymbolKind& kind) const { - // Open file indices may have symbols not yet in ProjectIndex. - for(auto& [_, ofi]: open_file_indices) { - auto it = ofi.symbols.find(hash); - if(it != ofi.symbols.end()) { - name = it->second.name; - kind = it->second.kind; - return true; - } - } - auto it = project_index.symbols.find(hash); - if(it != project_index.symbols.end()) { - name = it->second.name; - kind = it->second.kind; - return true; - } - return false; -} - -MasterServer::RawResult MasterServer::query_index_relations(const std::string& uri, - const protocol::Position& position, - RelationKind kind) { - auto path = uri_to_path(uri); - auto server_path_id = path_pool.intern(path); - - // Step 1: Find occurrence at the cursor position. - index::SymbolHash symbol_hash = 0; - - auto ofi_it = open_file_indices.find(server_path_id); - if(ofi_it != open_file_indices.end()) { - // Open file: use in-memory index with buffer content. - lsp::PositionMapper mapper(ofi_it->second.content, lsp::PositionEncoding::UTF16); - auto offset_opt = mapper.to_offset(position); - if(!offset_opt) - co_return serde_raw{"null"}; - - if(auto* occ = lookup_occurrence(ofi_it->second.file_index.occurrences, *offset_opt)) { - symbol_hash = occ->target; - } - } else { - // Non-open file (or open but not yet compiled): use MergedIndex. - auto doc_it = documents.find(server_path_id); - if(doc_it == documents.end()) - co_return serde_raw{"null"}; - - lsp::PositionMapper mapper(doc_it->second.text, lsp::PositionEncoding::UTF16); - auto offset_opt = mapper.to_offset(position); - if(!offset_opt) - co_return serde_raw{"null"}; - - auto proj_cache_it = project_index.path_pool.find(path); - if(proj_cache_it == project_index.path_pool.cache.end()) - co_return serde_raw{"null"}; - - auto merged_it = merged_indices.find(proj_cache_it->second); - if(merged_it == merged_indices.end()) - co_return serde_raw{"null"}; - - merged_it->second.lookup(*offset_opt, [&](const index::Occurrence& o) { - symbol_hash = o.target; - return false; - }); - } - - if(symbol_hash == 0) - co_return serde_raw{"null"}; - - // Step 2: Collect relations from all sources. - std::vector locations; - - // 2a: From ProjectIndex reference files (MergedIndex shards for disk-indexed files). - auto sym_it = project_index.symbols.find(symbol_hash); - if(sym_it != project_index.symbols.end()) { - for(auto file_id: sym_it->second.reference_files) { - // Skip files that have a fresher open file index. - if(open_proj_path_ids.contains(file_id)) - continue; - - auto file_merged_it = merged_indices.find(file_id); - if(file_merged_it == merged_indices.end()) - continue; - - auto file_path = project_index.path_pool.path(file_id); - auto file_uri = lsp::URI::from_file_path(file_path); - if(!file_uri) - continue; - - auto file_content = file_merged_it->second.content(); - if(file_content.empty()) - continue; - - lsp::PositionMapper file_mapper(file_content, lsp::PositionEncoding::UTF16); - - file_merged_it->second.lookup(symbol_hash, kind, [&](const index::Relation& r) { - auto start = file_mapper.to_position(r.range.begin); - auto end = file_mapper.to_position(r.range.end); - if(start && end) { - protocol::Location loc; - loc.uri = file_uri->str(); - loc.range = protocol::Range{*start, *end}; - locations.push_back(std::move(loc)); - } - return true; - }); - } - } - - // 2b: From all open file indices (not tracked in ProjectIndex.reference_files). - for(auto& [ofi_server_id, ofi]: open_file_indices) { - auto rel_it = ofi.file_index.relations.find(symbol_hash); - if(rel_it == ofi.file_index.relations.end()) - continue; - - auto ofi_path = std::string(path_pool.resolve(ofi_server_id)); - auto ofi_uri = lsp::URI::from_file_path(ofi_path); - if(!ofi_uri) - continue; - - lsp::PositionMapper ofi_mapper(ofi.content, lsp::PositionEncoding::UTF16); - - for(auto& relation: rel_it->second) { - if(relation.kind & kind) { - auto start = ofi_mapper.to_position(relation.range.begin); - auto end = ofi_mapper.to_position(relation.range.end); - if(start && end) { - protocol::Location loc; - loc.uri = ofi_uri->str(); - loc.range = protocol::Range{*start, *end}; - locations.push_back(std::move(loc)); - } - } - } - } - - if(locations.empty()) - co_return serde_raw{"null"}; - - co_return to_raw(locations); -} - -protocol::SymbolKind MasterServer::to_lsp_symbol_kind(SymbolKind kind) { - switch(kind) { - case SymbolKind::Namespace: return protocol::SymbolKind::Namespace; - case SymbolKind::Class: return protocol::SymbolKind::Class; - case SymbolKind::Struct: return protocol::SymbolKind::Struct; - case SymbolKind::Union: return protocol::SymbolKind::Class; - case SymbolKind::Enum: return protocol::SymbolKind::Enum; - case SymbolKind::Type: return protocol::SymbolKind::TypeParameter; - case SymbolKind::Field: return protocol::SymbolKind::Field; - case SymbolKind::EnumMember: return protocol::SymbolKind::EnumMember; - case SymbolKind::Function: return protocol::SymbolKind::Function; - case SymbolKind::Method: return protocol::SymbolKind::Method; - case SymbolKind::Variable: return protocol::SymbolKind::Variable; - case SymbolKind::Parameter: return protocol::SymbolKind::Variable; - case SymbolKind::Macro: return protocol::SymbolKind::Function; - case SymbolKind::Concept: return protocol::SymbolKind::Interface; - case SymbolKind::Module: return protocol::SymbolKind::Module; - case SymbolKind::Operator: return protocol::SymbolKind::Operator; - default: return protocol::SymbolKind::Variable; - } -} - -et::task> - MasterServer::lookup_symbol_at_position(const std::string& uri, - const protocol::Position& position) { - auto path = uri_to_path(uri); - auto server_path_id = path_pool.intern(path); - - index::SymbolHash symbol_hash = 0; - index::Range occ_range{}; - lsp::PositionMapper* mapper_ptr = nullptr; - - // Try open file index first. - std::optional ofi_mapper; - auto ofi_it = open_file_indices.find(server_path_id); - if(ofi_it != open_file_indices.end()) { - ofi_mapper.emplace(ofi_it->second.content, lsp::PositionEncoding::UTF16); - mapper_ptr = &*ofi_mapper; - auto offset_opt = ofi_mapper->to_offset(position); - if(!offset_opt) - co_return std::nullopt; - - if(auto* occ = lookup_occurrence(ofi_it->second.file_index.occurrences, *offset_opt)) { - symbol_hash = occ->target; - occ_range = occ->range; - } - } else { - // Fall back to MergedIndex. - auto doc_it = documents.find(server_path_id); - if(doc_it == documents.end()) - co_return std::nullopt; - - ofi_mapper.emplace(doc_it->second.text, lsp::PositionEncoding::UTF16); - mapper_ptr = &*ofi_mapper; - auto offset_opt = ofi_mapper->to_offset(position); - if(!offset_opt) - co_return std::nullopt; - - auto proj_cache_it = project_index.path_pool.find(path); - if(proj_cache_it == project_index.path_pool.cache.end()) - co_return std::nullopt; - - auto merged_it = merged_indices.find(proj_cache_it->second); - if(merged_it == merged_indices.end()) - co_return std::nullopt; - - merged_it->second.lookup(*offset_opt, [&](const index::Occurrence& o) { - symbol_hash = o.target; - occ_range = o.range; - return false; - }); - } - - if(symbol_hash == 0) - co_return std::nullopt; - - // Get symbol info: open file indices first (fresher), then ProjectIndex. - std::string name; - SymbolKind sym_kind; - if(!find_symbol_info(symbol_hash, name, sym_kind)) - co_return std::nullopt; - - auto start = mapper_ptr->to_position(occ_range.begin); - auto end = mapper_ptr->to_position(occ_range.end); - if(!start || !end) - co_return std::nullopt; - - SymbolInfo info; - info.hash = symbol_hash; - info.name = std::move(name); - info.kind = sym_kind; - info.uri = uri; - info.range = protocol::Range{*start, *end}; - co_return info; -} - -std::optional - MasterServer::find_symbol_definition_location(index::SymbolHash hash) { - // Check open file indices first (may have the most up-to-date definition). - for(auto& [ofi_server_id, ofi]: open_file_indices) { - auto rel_it = ofi.file_index.relations.find(hash); - if(rel_it == ofi.file_index.relations.end()) - continue; - - auto ofi_path = std::string(path_pool.resolve(ofi_server_id)); - auto ofi_uri = lsp::URI::from_file_path(ofi_path); - if(!ofi_uri) - continue; - - lsp::PositionMapper mapper(ofi.content, lsp::PositionEncoding::UTF16); - for(auto& relation: rel_it->second) { - if(relation.kind.is_one_of(RelationKind::Definition)) { - auto start = mapper.to_position(relation.range.begin); - auto end = mapper.to_position(relation.range.end); - if(start && end) { - protocol::Location loc; - loc.uri = ofi_uri->str(); - loc.range = protocol::Range{*start, *end}; - return loc; - } - } - } - } - - // Fall back to ProjectIndex reference files (MergedIndex shards). - auto sym_it = project_index.symbols.find(hash); - if(sym_it == project_index.symbols.end()) - return std::nullopt; - - for(auto file_id: sym_it->second.reference_files) { - if(open_proj_path_ids.contains(file_id)) - continue; - - auto file_merged_it = merged_indices.find(file_id); - if(file_merged_it == merged_indices.end()) - continue; - - auto file_path = project_index.path_pool.path(file_id); - auto file_uri = lsp::URI::from_file_path(file_path); - if(!file_uri) - continue; - - auto file_content = file_merged_it->second.content(); - if(file_content.empty()) - continue; - lsp::PositionMapper file_mapper(file_content, lsp::PositionEncoding::UTF16); - - std::optional result; - file_merged_it->second.lookup(hash, - RelationKind::Definition, - [&](const index::Relation& r) { - auto start = file_mapper.to_position(r.range.begin); - auto end = file_mapper.to_position(r.range.end); - if(start && end) { - protocol::Location loc; - loc.uri = file_uri->str(); - loc.range = protocol::Range{*start, *end}; - result = std::move(loc); - return false; - } - return true; - }); - - if(result) - return result; - } - - return std::nullopt; -} - -protocol::CallHierarchyItem MasterServer::build_call_hierarchy_item(const SymbolInfo& info) { - protocol::CallHierarchyItem item; - item.name = info.name; - item.kind = to_lsp_symbol_kind(info.kind); - item.uri = info.uri; - item.range = info.range; - item.selection_range = info.range; - // Store the symbol hash in data for later use in incoming/outgoing calls. - item.data = protocol::LSPAny(static_cast(info.hash)); - return item; -} - -protocol::TypeHierarchyItem MasterServer::build_type_hierarchy_item(const SymbolInfo& info) { - protocol::TypeHierarchyItem item; - item.name = info.name; - item.kind = to_lsp_symbol_kind(info.kind); - item.uri = info.uri; - item.range = info.range; - item.selection_range = info.range; - item.data = protocol::LSPAny(static_cast(info.hash)); - return item; -} - -et::task> - MasterServer::resolve_hierarchy_item(const std::string& uri, - const protocol::Range& range, - const std::optional& data) { - // Try to extract symbol hash from the stored data field first. - // Check both open file indices and ProjectIndex for the symbol info, - // since open files may have symbols not yet in ProjectIndex. - if(data) { - if(auto* int_val = std::get_if(&*data)) { - auto hash = static_cast(*int_val); - std::string name; - SymbolKind kind; - if(find_symbol_info(hash, name, kind)) { - SymbolInfo info; - info.hash = hash; - info.name = std::move(name); - info.kind = kind; - info.uri = uri; - info.range = range; - co_return info; - } - } - } - - // Fallback: re-lookup from position (requires document to be open). - co_return co_await lookup_symbol_at_position(uri, range.start); + indexer.save(config.index_dir); } void MasterServer::register_handlers() { using StringVec = std::vector; - // === initialize === peer.on_request([this](RequestContext& ctx, const protocol::InitializeParams& params) -> RequestResult { if(lifecycle != ServerLifecycle::Uninitialized) { co_return et::outcome_error(protocol::Error{"Server already initialized"}); } - // Extract workspace root auto& init = params.lsp__initialize_params; if(init.root_uri.has_value()) { - workspace_root = uri_to_path(*init.root_uri); + workspace_root = Compiler::uri_to_path(*init.root_uri); } lifecycle = ServerLifecycle::Initialized; - LOG_INFO("Initialized with workspace: {}", workspace_root); - // Build capabilities protocol::InitializeResult result; auto& caps = result.capabilities; - // Text document sync: incremental caps.text_document_sync = protocol::TextDocumentSyncOptions{ .open_close = true, .change = protocol::TextDocumentSyncKind::Incremental, .save = protocol::variant{true}, }; - // watch workspace folder changes. caps.workspace = protocol::WorkspaceOptions{}; caps.workspace->workspace_folders = protocol::WorkspaceFoldersServerCapabilities{ .supported = true, .change_notifications = true, }; - // Feature capabilities caps.hover_provider = true; caps.completion_provider = protocol::CompletionOptions{ .trigger_characters = StringVec{".", "<", ">", ":", "\"", "/", "*"}, @@ -2184,7 +269,6 @@ void MasterServer::register_handlers() { caps.type_hierarchy_provider = true; caps.workspace_symbol_provider = true; - // Semantic tokens protocol::SemanticTokensOptions sem_opts; { auto lower_first = [](std::string_view name) -> std::string { @@ -2207,7 +291,6 @@ void MasterServer::register_handlers() { sem_opts.full = true; result.capabilities.semantic_tokens_provider = std::move(sem_opts); - // Server info protocol::ServerInfo info; info.name = "clice"; info.version = "0.1.0"; @@ -2216,12 +299,9 @@ void MasterServer::register_handlers() { co_return result; }); - // === initialized === peer.on_notification([this](const protocol::InitializedParams& params) { - // Load configuration from workspace config = CliceConfig::load_from_workspace(workspace_root); - // Switch master to file logging under a session-timestamped directory if(!config.logging_dir.empty()) { auto now = std::chrono::system_clock::now(); auto pid = llvm::sys::Process::getProcessId(); @@ -2236,7 +316,6 @@ void MasterServer::register_handlers() { config.stateless_worker_count, config.idle_timeout_ms); - // Start worker pool WorkerPoolOptions pool_opts; pool_opts.self_path = self_path; pool_opts.stateful_count = config.stateful_worker_count; @@ -2250,11 +329,13 @@ void MasterServer::register_handlers() { lifecycle = ServerLifecycle::Ready; - // Load CDB in background + compiler.on_indexing_needed = [this]() { + schedule_indexing(); + }; + loop.schedule(load_workspace()); }); - // === shutdown === peer.on_request( [this](RequestContext& ctx, const protocol::ShutdownParams& params) -> RequestResult { @@ -2263,814 +344,297 @@ void MasterServer::register_handlers() { co_return nullptr; }); - // === exit === peer.on_notification([this](const protocol::ExitParams& params) { lifecycle = ServerLifecycle::Exited; LOG_INFO("Exit notification received"); - // Persist index and cache state before stopping. - save_index(); - save_cache(); + indexer.save(config.index_dir); + compiler.save_cache(); - // Graceful shutdown: cancel compilations, stop workers, then stop loop loop.schedule([this]() -> et::task<> { co_await pool.stop(); loop.stop(); }()); }); - // === textDocument/didOpen === + /// Document lifecycle — delegate to Compiler. peer.on_notification([this](const protocol::DidOpenTextDocumentParams& params) { if(lifecycle != ServerLifecycle::Ready) return; - - auto& td = params.text_document; - auto path = uri_to_path(td.uri); - auto path_id = path_pool.intern(path); - - auto& doc = documents[path_id]; - doc.version = td.version; - doc.text = td.text; - doc.generation++; - - LOG_DEBUG("didOpen: {} (v{})", path, td.version); + compiler.open_document(params.text_document.uri, + params.text_document.text, + params.text_document.version); }); - // === textDocument/didChange === peer.on_notification([this](const protocol::DidChangeTextDocumentParams& params) { if(lifecycle != ServerLifecycle::Ready) return; - - auto path = uri_to_path(params.text_document.uri); - auto path_id = path_pool.intern(path); - - auto it = documents.find(path_id); - if(it == documents.end()) - return; - - auto& doc = it->second; - doc.version = params.text_document.version; - - // Apply content changes. - for(auto& change: params.content_changes) { - std::visit( - [&](auto& c) { - using T = std::remove_cvref_t; - if constexpr(std::is_same_v) { - doc.text = c.text; - } else { - // Incremental change: replace range - auto& range = c.range; - lsp::PositionMapper mapper(doc.text, lsp::PositionEncoding::UTF16); - auto start = mapper.to_offset(range.start); - auto end = mapper.to_offset(range.end); - if(start && end && *start <= *end) { - doc.text.replace(*start, *end - *start, c.text); - } - } - }, - change); - } - - doc.generation++; - doc.ast_dirty = true; - - LOG_DEBUG("didChange: path={} version={} gen={}", path, doc.version, doc.generation); - - // Notify the owning stateful worker so it marks the document dirty - worker::DocumentUpdateParams update; - update.path = path; - update.version = doc.version; - pool.notify_stateful(path_id, update); + compiler.apply_changes(params); }); - // === textDocument/didClose === peer.on_notification([this](const protocol::DidCloseTextDocumentParams& params) { if(lifecycle != ServerLifecycle::Ready) return; - - auto path = uri_to_path(params.text_document.uri); - auto path_id = path_pool.intern(path); - - // Cancel in-flight module compilations for this file. - if(compile_graph && compile_graph->has_unit(path_id)) { - compile_graph->update(path_id); - } - - // Clean up open file index and proj_path_id tracking. - open_file_indices.erase(path_id); - auto proj_cache_it = project_index.path_pool.find(path); - if(proj_cache_it != project_index.path_pool.cache.end()) { - open_proj_path_ids.erase(proj_cache_it->second); - } - - documents.erase(path_id); - pch_states.erase(path_id); - ast_deps.erase(path_id); - - // Queue for background indexing to produce a proper MergedIndex shard. + auto path_id = compiler.close_document(params.text_document.uri); index_queue.push_back(path_id); schedule_indexing(); - - // Clear diagnostics for closed file - clear_diagnostics(params.text_document.uri); - - LOG_DEBUG("didClose: {}", path); }); - // === textDocument/didSave === peer.on_notification([this](const protocol::DidSaveTextDocumentParams& params) { if(lifecycle != ServerLifecycle::Ready) return; - - auto path = uri_to_path(params.text_document.uri); - auto path_id = path_pool.intern(path); - - // Invalidate this file and cascade to dependents in the compile graph. - if(compile_graph) { - auto dirtied = compile_graph->update(path_id); - // Remove stale PCMs for all invalidated units. - for(auto dirty_id: dirtied) { - pcm_paths.erase(dirty_id); - pcm_states.erase(dirty_id); - } - // Mark ast_dirty for open documents that depend on the saved file. - // Re-queue non-open dependents for background re-indexing so their - // stale MergedIndex shards get refreshed after a header change. - for(auto dirty_id: dirtied) { - auto doc_it = documents.find(dirty_id); - if(doc_it != documents.end()) { - doc_it->second.ast_dirty = true; - } else { - // Non-open dependent: needs background re-indexing. - index_queue.push_back(dirty_id); - } - } - } - - // Invalidate header contexts whose host is the saved file. - // Collect entries to erase to avoid modifying the map while iterating. - llvm::SmallVector stale_headers; - for(auto& [hdr_id, hdr_ctx]: header_file_contexts) { - if(hdr_ctx.host_path_id == path_id) - stale_headers.push_back(hdr_id); - } - for(auto hdr_id: stale_headers) { - header_file_contexts.erase(hdr_id); - auto doc_it = documents.find(hdr_id); - if(doc_it != documents.end()) { - doc_it->second.ast_dirty = true; - LOG_DEBUG("didSave: invalidated header context for path_id={}", hdr_id); - } - } - - // Trigger background indexing after save. + auto to_index = compiler.on_save(params.text_document.uri); + for(auto id: to_index) + index_queue.push_back(id); schedule_indexing(); - - LOG_DEBUG("didSave: {}", params.text_document.uri); }); - // ========================================================================= - // Feature requests routed to stateful workers (RawValue passthrough) - // ========================================================================= - - // --- textDocument/hover --- + /// Feature requests — stateful forwarding. peer.on_request([this](RequestContext& ctx, const protocol::HoverParams& params) -> RawResult { - co_return co_await forward_stateful( + co_return co_await compiler.forward_query( + worker::QueryKind::Hover, params.text_document_position_params.text_document.uri, params.text_document_position_params.position); }); - // --- textDocument/semanticTokens/full --- - peer.on_request([this](RequestContext& ctx, - const protocol::SemanticTokensParams& params) -> RawResult { - co_return co_await forward_stateful(params.text_document.uri); - }); + peer.on_request( + [this](RequestContext& ctx, const protocol::SemanticTokensParams& params) -> RawResult { + co_return co_await compiler.forward_query(worker::QueryKind::SemanticTokens, + params.text_document.uri); + }); - // --- textDocument/inlayHint --- peer.on_request( [this](RequestContext& ctx, const protocol::InlayHintParams& params) -> RawResult { - co_return co_await forward_stateful(params.text_document.uri); + co_return co_await compiler.forward_query(worker::QueryKind::InlayHints, + params.text_document.uri); }); - // --- textDocument/foldingRange --- - peer.on_request([this](RequestContext& ctx, - const protocol::FoldingRangeParams& params) -> RawResult { - co_return co_await forward_stateful(params.text_document.uri); - }); + peer.on_request( + [this](RequestContext& ctx, const protocol::FoldingRangeParams& params) -> RawResult { + co_return co_await compiler.forward_query(worker::QueryKind::FoldingRange, + params.text_document.uri); + }); - // --- textDocument/documentSymbol --- - peer.on_request([this](RequestContext& ctx, - const protocol::DocumentSymbolParams& params) -> RawResult { - co_return co_await forward_stateful(params.text_document.uri); - }); + peer.on_request( + [this](RequestContext& ctx, const protocol::DocumentSymbolParams& params) -> RawResult { + co_return co_await compiler.forward_query(worker::QueryKind::DocumentSymbol, + params.text_document.uri); + }); - // --- textDocument/documentLink --- - peer.on_request([this](RequestContext& ctx, - const protocol::DocumentLinkParams& params) -> RawResult { - co_return co_await forward_stateful(params.text_document.uri); - }); + peer.on_request( + [this](RequestContext& ctx, const protocol::DocumentLinkParams& params) -> RawResult { + co_return co_await compiler.forward_query(worker::QueryKind::DocumentLink, + params.text_document.uri); + }); - // --- textDocument/codeAction --- peer.on_request( [this](RequestContext& ctx, const protocol::CodeActionParams& params) -> RawResult { - co_return co_await forward_stateful(params.text_document.uri); + co_return co_await compiler.forward_query(worker::QueryKind::CodeAction, + params.text_document.uri); }); - // --- textDocument/definition --- - peer.on_request( - [this](RequestContext& ctx, const protocol::DefinitionParams& params) -> RawResult { - auto& uri = params.text_document_position_params.text_document.uri; - auto& pos = params.text_document_position_params.position; + /// Resolve URI to the context needed for index queries. + auto resolve_uri = [this](const std::string& uri) { + struct Result { + std::string path; + std::uint32_t path_id; + const std::string* doc_text; + }; + auto path = Compiler::uri_to_path(uri); + auto path_id = path_pool.intern(path); + auto* doc = compiler.get_document(path_id); + return Result{std::move(path), path_id, doc ? &doc->text : nullptr}; + }; - // Try index-based lookup first. - auto result = co_await query_index_relations(uri, pos, RelationKind::Definition); - if(result.has_value() && !result.value().empty() && result.value().data != "null") { - co_return std::move(result).value(); - } + auto lookup_at = [this, resolve_uri](const std::string& uri, const protocol::Position& pos) { + auto [path, path_id, doc_text] = resolve_uri(uri); + return indexer.lookup_symbol(uri, path, path_id, pos, doc_text); + }; - // Fall back to stateful worker AST query. - co_return co_await forward_stateful(uri, pos); - }); + auto query_at = [this, resolve_uri](const std::string& uri, + const protocol::Position& pos, + RelationKind kind) -> std::vector { + auto [path, path_id, doc_text] = resolve_uri(uri); + return indexer.query_relations(path, path_id, pos, kind, doc_text); + }; - // --- textDocument/references --- - peer.on_request( - [this](RequestContext& ctx, const protocol::ReferenceParams& params) -> RawResult { - auto& uri = params.text_document_position_params.text_document.uri; - auto& pos = params.text_document_position_params.position; + auto resolve_item = + [this, + resolve_uri](const std::string& uri, + const protocol::Range& range, + const std::optional& data) -> std::optional { + auto [path, path_id, doc_text] = resolve_uri(uri); + return indexer.resolve_hierarchy_item(uri, path, path_id, range, data, doc_text); + }; - auto refs = co_await query_index_relations(uri, pos, RelationKind::Reference); + /// 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; + auto& pos = params.text_document_position_params.position; - if(params.context.include_declaration) { - // Also include Definition locations when the client requests the declaration. - auto defs = co_await query_index_relations(uri, pos, RelationKind::Definition); - if(defs.has_value() && !defs.value().empty() && defs.value().data != "null") { - if(!refs.has_value() || refs.value().empty() || refs.value().data == "null") { - co_return std::move(defs).value(); - } - // Merge: parse both JSON arrays and concatenate. - auto& ref_json = refs.value().data; - auto& def_json = defs.value().data; - // Both are JSON arrays like "[...]". Merge them. - if(ref_json.size() > 2 && def_json.size() > 2) { - // Remove trailing ']' from refs, add comma, add def content without leading - // '[' - std::string merged = ref_json.substr(0, ref_json.size() - 1); - merged += ','; - merged += def_json.substr(1); - co_return serde_raw{std::move(merged)}; - } - } - } + auto result = query_at(uri, pos, RelationKind::Definition); + if(!result.empty()) { + co_return to_raw(result); + } - co_return refs; - }); + co_return co_await compiler.forward_query(worker::QueryKind::GoToDefinition, uri, pos); + }); + + peer.on_request([this, query_at](RequestContext& ctx, + const protocol::ReferenceParams& params) -> RawResult { + auto& uri = params.text_document_position_params.text_document.uri; + auto& pos = params.text_document_position_params.position; + + auto locations = query_at(uri, pos, RelationKind::Reference); + + if(params.context.include_declaration) { + auto defs = query_at(uri, pos, RelationKind::Definition); + locations.insert(locations.end(), + std::make_move_iterator(defs.begin()), + std::make_move_iterator(defs.end())); + } + + if(locations.empty()) + co_return serde_raw{"null"}; + co_return to_raw(locations); + }); - // --- textDocument/typeDefinition --- peer.on_request( [this](RequestContext& ctx, const protocol::TypeDefinitionParams& params) -> RawResult { - co_return serde_raw{"null"}; // not supported yet + co_return serde_raw{"null"}; }); - // --- textDocument/implementation --- peer.on_request( [this](RequestContext& ctx, const protocol::ImplementationParams& params) -> RawResult { - co_return serde_raw{"null"}; // not supported yet + co_return serde_raw{"null"}; }); - // --- textDocument/declaration --- peer.on_request( [this](RequestContext& ctx, const protocol::DeclarationParams& params) -> RawResult { - co_return serde_raw{"null"}; // not supported yet + co_return serde_raw{"null"}; }); - // ========================================================================= - // Feature requests routed to stateless workers - // ========================================================================= - - // --- textDocument/completion --- + /// Feature requests — stateless forwarding. peer.on_request( [this](RequestContext& ctx, const protocol::CompletionParams& params) -> RawResult { - auto uri = params.text_document_position_params.text_document.uri; - auto position = params.text_document_position_params.position; - - // Check if cursor is on an #include or import line. - auto path = uri_to_path(uri); - auto path_id = path_pool.intern(path); - auto doc_it = documents.find(path_id); - if(doc_it != documents.end()) { - lsp::PositionMapper mapper(doc_it->second.text, lsp::PositionEncoding::UTF16); - auto offset = mapper.to_offset(position); - if(offset) { - auto pctx = detect_completion_context(doc_it->second.text, *offset); - if(pctx.kind == CompletionContext::IncludeQuoted || - pctx.kind == CompletionContext::IncludeAngled) { - co_return complete_include(pctx, path); - } - if(pctx.kind == CompletionContext::Import) { - co_return complete_import(pctx); - } - } - } - - // Default: forward to stateless worker. - co_return co_await forward_stateless(uri, position); - }); - - // --- textDocument/signatureHelp --- - peer.on_request( - [this](RequestContext& ctx, const protocol::SignatureHelpParams& params) -> RawResult { - co_return co_await forward_stateless( + co_return co_await compiler.handle_completion( params.text_document_position_params.text_document.uri, params.text_document_position_params.position); }); - // ========================================================================= - // Hierarchy and workspace symbol handlers (index-based) - // ========================================================================= + peer.on_request( + [this](RequestContext& ctx, const protocol::SignatureHelpParams& params) -> RawResult { + co_return co_await compiler.forward_build( + worker::BuildKind::SignatureHelp, + params.text_document_position_params.text_document.uri, + params.text_document_position_params.position); + }); - // --- textDocument/prepareCallHierarchy --- - peer.on_request([this](RequestContext& ctx, - const protocol::CallHierarchyPrepareParams& params) -> RawResult { - auto info = co_await lookup_symbol_at_position( - params.text_document_position_params.text_document.uri, - params.text_document_position_params.position); + /// Hierarchy queries — index-based. + peer.on_request( + [this, lookup_at](RequestContext& ctx, + const protocol::CallHierarchyPrepareParams& params) -> RawResult { + auto& uri = params.text_document_position_params.text_document.uri; + auto& pos = params.text_document_position_params.position; + + auto info = lookup_at(uri, pos); + if(!info) + co_return serde_raw{"null"}; + if(!(info->kind == SymbolKind::Function || info->kind == SymbolKind::Method)) + co_return serde_raw{"null"}; + + std::vector items; + items.push_back(Indexer::build_call_hierarchy_item(*info)); + co_return to_raw(items); + }); + + peer.on_request([this, resolve_item]( + RequestContext& ctx, + const protocol::CallHierarchyIncomingCallsParams& params) -> RawResult { + auto info = resolve_item(params.item.uri, params.item.range, params.item.data); if(!info) co_return serde_raw{"null"}; - - // Only functions/methods are valid for call hierarchy. - if(!(info->kind == SymbolKind::Function || info->kind == SymbolKind::Method)) - co_return serde_raw{"null"}; - - std::vector items; - items.push_back(build_call_hierarchy_item(*info)); - co_return to_raw(items); - }); - - // --- callHierarchy/incomingCalls --- - peer.on_request([this](RequestContext& ctx, - const protocol::CallHierarchyIncomingCallsParams& params) -> RawResult { - // Re-lookup the symbol from the item. - auto info = - co_await resolve_hierarchy_item(params.item.uri, params.item.range, params.item.data); - if(!info) - co_return serde_raw{"null"}; - - // Collect all Caller relations across reference files. - // Caller relation: on the callee symbol, target_symbol is the caller's hash. - std::vector results; - - // Group call sites by caller symbol hash. - llvm::DenseMap> caller_ranges; - - auto sym_it = project_index.symbols.find(info->hash); - if(sym_it != project_index.symbols.end()) - for(auto file_id: sym_it->second.reference_files) { - if(open_proj_path_ids.contains(file_id)) - continue; - - auto file_merged_it = merged_indices.find(file_id); - if(file_merged_it == merged_indices.end()) - continue; - - auto file_content = file_merged_it->second.content(); - if(file_content.empty()) - continue; - - lsp::PositionMapper file_mapper(file_content, lsp::PositionEncoding::UTF16); - - file_merged_it->second.lookup( - info->hash, - RelationKind::Caller, - [&](const index::Relation& r) { - auto start = file_mapper.to_position(r.range.begin); - auto end = file_mapper.to_position(r.range.end); - if(start && end) { - caller_ranges[r.target_symbol].push_back(protocol::Range{*start, *end}); - } - return true; - }); - } - - // Also check open file indices. - for(auto& [ofi_id, ofi]: open_file_indices) { - auto rel_it = ofi.file_index.relations.find(info->hash); - if(rel_it == ofi.file_index.relations.end()) - continue; - lsp::PositionMapper ofi_mapper(ofi.content, lsp::PositionEncoding::UTF16); - for(auto& r: rel_it->second) { - if(r.kind.is_one_of(RelationKind::Caller)) { - auto start = ofi_mapper.to_position(r.range.begin); - auto end = ofi_mapper.to_position(r.range.end); - if(start && end) { - caller_ranges[r.target_symbol].push_back(protocol::Range{*start, *end}); - } - } - } - } - - // Build incoming call items from grouped caller symbols. - for(auto& [caller_hash, ranges]: caller_ranges) { - auto def_loc = find_symbol_definition_location(caller_hash); - if(!def_loc) - continue; - - std::string caller_name; - SymbolKind caller_kind; - if(!find_symbol_info(caller_hash, caller_name, caller_kind)) - continue; - - protocol::CallHierarchyItem caller_item; - caller_item.name = caller_name; - caller_item.kind = to_lsp_symbol_kind(caller_kind); - caller_item.uri = def_loc->uri; - caller_item.range = def_loc->range; - caller_item.selection_range = def_loc->range; - caller_item.data = protocol::LSPAny(static_cast(caller_hash)); - - protocol::CallHierarchyIncomingCall call; - call.from = std::move(caller_item); - call.from_ranges = std::move(ranges); - results.push_back(std::move(call)); - } - + auto results = indexer.find_incoming_calls(info->hash); if(results.empty()) co_return serde_raw{"null"}; co_return to_raw(results); }); - // --- callHierarchy/outgoingCalls --- - peer.on_request([this](RequestContext& ctx, - const protocol::CallHierarchyOutgoingCallsParams& params) -> RawResult { - auto info = - co_await resolve_hierarchy_item(params.item.uri, params.item.range, params.item.data); + peer.on_request([this, resolve_item]( + RequestContext& ctx, + const protocol::CallHierarchyOutgoingCallsParams& params) -> RawResult { + auto info = resolve_item(params.item.uri, params.item.range, params.item.data); if(!info) co_return serde_raw{"null"}; - - // Collect Callee relations (outgoing calls from this function). - // Group call sites by callee symbol hash. - llvm::DenseMap> callee_ranges; - - auto sym_it = project_index.symbols.find(info->hash); - if(sym_it != project_index.symbols.end()) - for(auto file_id: sym_it->second.reference_files) { - if(open_proj_path_ids.contains(file_id)) - continue; - - auto file_merged_it = merged_indices.find(file_id); - if(file_merged_it == merged_indices.end()) - continue; - - auto file_content = file_merged_it->second.content(); - if(file_content.empty()) - continue; - - lsp::PositionMapper file_mapper(file_content, lsp::PositionEncoding::UTF16); - - file_merged_it->second.lookup( - info->hash, - RelationKind::Callee, - [&](const index::Relation& r) { - auto start = file_mapper.to_position(r.range.begin); - auto end = file_mapper.to_position(r.range.end); - if(start && end) { - callee_ranges[r.target_symbol].push_back(protocol::Range{*start, *end}); - } - return true; - }); - } - - // Also check open file indices. - for(auto& [ofi_id, ofi]: open_file_indices) { - auto rel_it = ofi.file_index.relations.find(info->hash); - if(rel_it == ofi.file_index.relations.end()) - continue; - lsp::PositionMapper ofi_mapper(ofi.content, lsp::PositionEncoding::UTF16); - for(auto& r: rel_it->second) { - if(r.kind.is_one_of(RelationKind::Callee)) { - auto start = ofi_mapper.to_position(r.range.begin); - auto end = ofi_mapper.to_position(r.range.end); - if(start && end) { - callee_ranges[r.target_symbol].push_back(protocol::Range{*start, *end}); - } - } - } - } - - std::vector results; - for(auto& [callee_hash, ranges]: callee_ranges) { - auto def_loc = find_symbol_definition_location(callee_hash); - if(!def_loc) - continue; - - std::string callee_name; - SymbolKind callee_kind; - if(!find_symbol_info(callee_hash, callee_name, callee_kind)) - continue; - - protocol::CallHierarchyItem callee_item; - callee_item.name = callee_name; - callee_item.kind = to_lsp_symbol_kind(callee_kind); - callee_item.uri = def_loc->uri; - callee_item.range = def_loc->range; - callee_item.selection_range = def_loc->range; - callee_item.data = protocol::LSPAny(static_cast(callee_hash)); - - protocol::CallHierarchyOutgoingCall call; - call.to = std::move(callee_item); - call.from_ranges = std::move(ranges); - results.push_back(std::move(call)); - } - + auto results = indexer.find_outgoing_calls(info->hash); if(results.empty()) co_return serde_raw{"null"}; co_return to_raw(results); }); - // --- textDocument/prepareTypeHierarchy --- - peer.on_request([this](RequestContext& ctx, - const protocol::TypeHierarchyPrepareParams& params) -> RawResult { - auto info = co_await lookup_symbol_at_position( - params.text_document_position_params.text_document.uri, - params.text_document_position_params.position); - if(!info) - co_return serde_raw{"null"}; + peer.on_request( + [this, lookup_at](RequestContext& ctx, + const protocol::TypeHierarchyPrepareParams& params) -> RawResult { + auto& uri = params.text_document_position_params.text_document.uri; + auto& pos = params.text_document_position_params.position; - // Only class-like types are valid for type hierarchy. - if(!(info->kind == SymbolKind::Class || info->kind == SymbolKind::Struct || - info->kind == SymbolKind::Enum || info->kind == SymbolKind::Union)) - co_return serde_raw{"null"}; + auto info = lookup_at(uri, pos); + if(!info) + co_return serde_raw{"null"}; + if(!(info->kind == SymbolKind::Class || info->kind == SymbolKind::Struct || + info->kind == SymbolKind::Enum || info->kind == SymbolKind::Union)) + co_return serde_raw{"null"}; - std::vector items; - items.push_back(build_type_hierarchy_item(*info)); - co_return to_raw(items); - }); + std::vector items; + items.push_back(Indexer::build_type_hierarchy_item(*info)); + co_return to_raw(items); + }); - // --- typeHierarchy/supertypes --- - peer.on_request([this](RequestContext& ctx, - const protocol::TypeHierarchySupertypesParams& params) -> RawResult { - auto info = - co_await resolve_hierarchy_item(params.item.uri, params.item.range, params.item.data); - if(!info) - co_return serde_raw{"null"}; + peer.on_request( + [this, resolve_item](RequestContext& ctx, + const protocol::TypeHierarchySupertypesParams& params) -> RawResult { + 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); + if(results.empty()) + co_return serde_raw{"null"}; + co_return to_raw(results); + }); - // Find Base relations: supertypes of this class. - std::vector results; + peer.on_request( + [this, resolve_item](RequestContext& ctx, + const protocol::TypeHierarchySubtypesParams& params) -> RawResult { + 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); + if(results.empty()) + co_return serde_raw{"null"}; + co_return to_raw(results); + }); - auto sym_it = project_index.symbols.find(info->hash); + peer.on_request( + [this](RequestContext& ctx, const protocol::WorkspaceSymbolParams& params) -> RawResult { + auto results = indexer.search_symbols(params.query); + if(results.empty()) + co_return serde_raw{"null"}; + co_return to_raw(results); + }); - auto collect_base = [&](const index::Relation& r) { - if(!r.kind.is_one_of(RelationKind::Base)) - return; - auto base_hash = r.target_symbol; - - std::string base_name; - SymbolKind base_kind; - if(!find_symbol_info(base_hash, base_name, base_kind)) - return; - - auto def_loc = find_symbol_definition_location(base_hash); - if(!def_loc) - return; - - protocol::TypeHierarchyItem item; - item.name = std::move(base_name); - item.kind = to_lsp_symbol_kind(base_kind); - item.uri = def_loc->uri; - item.range = def_loc->range; - item.selection_range = def_loc->range; - item.data = protocol::LSPAny(static_cast(base_hash)); - results.push_back(std::move(item)); - }; - - if(sym_it != project_index.symbols.end()) - for(auto file_id: sym_it->second.reference_files) { - if(open_proj_path_ids.contains(file_id)) - continue; - - auto file_merged_it = merged_indices.find(file_id); - if(file_merged_it == merged_indices.end()) - continue; - - file_merged_it->second.lookup(info->hash, - RelationKind::Base, - [&](const index::Relation& r) { - collect_base(r); - return true; - }); - } - - // Also check open file indices. - for(auto& [ofi_id, ofi]: open_file_indices) { - auto rel_it = ofi.file_index.relations.find(info->hash); - if(rel_it == ofi.file_index.relations.end()) - continue; - for(auto& r: rel_it->second) { - collect_base(r); - } - } - - if(results.empty()) - co_return serde_raw{"null"}; - co_return to_raw(results); - }); - - // --- typeHierarchy/subtypes --- - peer.on_request([this](RequestContext& ctx, - const protocol::TypeHierarchySubtypesParams& params) -> RawResult { - auto info = - co_await resolve_hierarchy_item(params.item.uri, params.item.range, params.item.data); - if(!info) - co_return serde_raw{"null"}; - - // Find Derived relations across all reference files: subtypes of this class. - std::vector results; - - auto sym_it = project_index.symbols.find(info->hash); - - auto collect_derived = [&](const index::Relation& r) { - if(!r.kind.is_one_of(RelationKind::Derived)) - return; - auto derived_hash = r.target_symbol; - - std::string derived_name; - SymbolKind derived_kind; - if(!find_symbol_info(derived_hash, derived_name, derived_kind)) - return; - - auto def_loc = find_symbol_definition_location(derived_hash); - if(!def_loc) - return; - - protocol::TypeHierarchyItem item; - item.name = std::move(derived_name); - item.kind = to_lsp_symbol_kind(derived_kind); - item.uri = def_loc->uri; - item.range = def_loc->range; - item.selection_range = def_loc->range; - item.data = protocol::LSPAny(static_cast(derived_hash)); - results.push_back(std::move(item)); - }; - - if(sym_it != project_index.symbols.end()) - for(auto file_id: sym_it->second.reference_files) { - if(open_proj_path_ids.contains(file_id)) - continue; - - auto file_merged_it = merged_indices.find(file_id); - if(file_merged_it == merged_indices.end()) - continue; - - file_merged_it->second.lookup(info->hash, - RelationKind::Derived, - [&](const index::Relation& r) { - collect_derived(r); - return true; - }); - } - - // Also check open file indices. - for(auto& [ofi_id, ofi]: open_file_indices) { - auto rel_it = ofi.file_index.relations.find(info->hash); - if(rel_it == ofi.file_index.relations.end()) - continue; - for(auto& r: rel_it->second) { - collect_derived(r); - } - } - - if(results.empty()) - co_return serde_raw{"null"}; - co_return to_raw(results); - }); - - // --- workspace/symbol --- - peer.on_request([this](RequestContext& ctx, - const protocol::WorkspaceSymbolParams& params) -> RawResult { - auto query = llvm::StringRef(params.query); - std::vector results; - - // Case-insensitive substring match on symbol names. - std::string query_lower = query.lower(); - - auto is_indexable_kind = [](SymbolKind sk) { - return sk == SymbolKind::Namespace || sk == SymbolKind::Class || - sk == SymbolKind::Struct || sk == SymbolKind::Union || sk == SymbolKind::Enum || - sk == SymbolKind::Type || sk == SymbolKind::Field || - sk == SymbolKind::EnumMember || sk == SymbolKind::Function || - sk == SymbolKind::Method || sk == SymbolKind::Variable || - sk == SymbolKind::Parameter || sk == SymbolKind::Macro || - sk == SymbolKind::Concept || sk == SymbolKind::Module || - sk == SymbolKind::Operator || sk == SymbolKind::MacroParameter || - sk == SymbolKind::Label || sk == SymbolKind::Attribute; - }; - - auto matches_query = [&](llvm::StringRef name) { - if(query_lower.empty()) - return true; - return llvm::StringRef(name).lower().find(query_lower) != std::string::npos; - }; - - // Collect symbols already seen (by hash) to avoid duplicates - // between ProjectIndex and open file indices. - llvm::DenseSet seen; - - for(auto& [hash, symbol]: project_index.symbols) { - if(results.size() >= 100) - break; - if(!is_indexable_kind(symbol.kind) || symbol.name.empty()) - continue; - if(!matches_query(symbol.name)) - continue; - - auto def_loc = find_symbol_definition_location(hash); - if(!def_loc) - continue; - - protocol::SymbolInformation info; - info.name = symbol.name; - info.kind = to_lsp_symbol_kind(symbol.kind); - info.location = std::move(*def_loc); - results.push_back(std::move(info)); - seen.insert(hash); - } - - // Also search open file indices for symbols not in ProjectIndex. - for(auto& [ofi_server_id, ofi]: open_file_indices) { - if(results.size() >= 100) - break; - for(auto& [hash, symbol]: ofi.symbols) { - if(results.size() >= 100) - break; - if(seen.contains(hash)) - continue; - if(!is_indexable_kind(symbol.kind) || symbol.name.empty()) - continue; - if(!matches_query(symbol.name)) - continue; - - auto def_loc = find_symbol_definition_location(hash); - if(!def_loc) - continue; - - protocol::SymbolInformation info; - info.name = symbol.name; - info.kind = to_lsp_symbol_kind(symbol.kind); - info.location = std::move(*def_loc); - results.push_back(std::move(info)); - seen.insert(hash); - } - } - - // Also search open file indices for symbols not in ProjectIndex. - for(auto& [ofi_server_id, ofi]: open_file_indices) { - if(results.size() >= 100) - break; - for(auto& [hash, symbol]: ofi.symbols) { - if(results.size() >= 100) - break; - if(seen.contains(hash)) - continue; - if(!is_indexable_kind(symbol.kind) || symbol.name.empty()) - continue; - if(!matches_query(symbol.name)) - continue; - - auto def_loc = find_symbol_definition_location(hash); - if(!def_loc) - continue; - - protocol::SymbolInformation info; - info.name = symbol.name; - info.kind = to_lsp_symbol_kind(symbol.kind); - info.location = std::move(*def_loc); - results.push_back(std::move(info)); - seen.insert(hash); - } - } - - if(results.empty()) - co_return serde_raw{"null"}; - co_return to_raw(results); - }); - - // === clice/ Extension Commands === - - // --- clice/queryContext --- + /// clice/ extension commands. peer.on_request( "clice/queryContext", [this](RequestContext& ctx, const ext::QueryContextParams& params) -> RawResult { - auto path = uri_to_path(params.uri); + auto path = Compiler::uri_to_path(params.uri); auto path_id = 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; - // For headers: find source files that transitively include this file. auto hosts = dependency_graph.find_host_sources(path_id); for(auto host_id: hosts) { auto host_path = path_pool.resolve(host_id); @@ -3087,12 +651,10 @@ void MasterServer::register_handlers() { all_items.push_back(std::move(item)); } - // For source files: list distinct CDB entries (e.g. debug/release). if(hosts.empty()) { auto entries = cdb.lookup(path, {.suppress_logging = true}); for(std::size_t i = 0; i < entries.size(); ++i) { auto& entry = entries[i]; - // Build a description from distinguishing flags. std::string desc; for(std::size_t j = 0; j < entry.arguments.size(); ++j) { llvm::StringRef a(entry.arguments[j]); @@ -3101,7 +663,6 @@ void MasterServer::register_handlers() { if(!desc.empty()) desc += ' '; desc += entry.arguments[j]; - // Handle split args like "-D" "CONFIG_A" if((a == "-D" || a == "-O") && j + 1 < entry.arguments.size()) { desc += entry.arguments[++j]; } @@ -3129,18 +690,16 @@ void MasterServer::register_handlers() { co_return to_raw(result); }); - // --- clice/currentContext --- peer.on_request( "clice/currentContext", [this](RequestContext& ctx, const ext::CurrentContextParams& params) -> RawResult { - auto path = uri_to_path(params.uri); + auto path = Compiler::uri_to_path(params.uri); auto path_id = path_pool.intern(path); ext::CurrentContextResult result; - - auto it = active_contexts.find(path_id); - if(it != active_contexts.end()) { - auto ctx_path = path_pool.resolve(it->second); + auto active_ctx = compiler.get_active_context(path_id); + if(active_ctx) { + auto ctx_path = path_pool.resolve(*active_ctx); auto ctx_uri_opt = lsp::URI::from_file_path(std::string(ctx_path)); if(ctx_uri_opt) { ext::ContextItem item; @@ -3153,40 +712,23 @@ void MasterServer::register_handlers() { co_return to_raw(result); }); - // --- clice/switchContext --- peer.on_request( "clice/switchContext", [this](RequestContext& ctx, const ext::SwitchContextParams& params) -> RawResult { - auto path = uri_to_path(params.uri); + auto path = Compiler::uri_to_path(params.uri); auto path_id = path_pool.intern(path); - auto context_path = uri_to_path(params.context_uri); + auto context_path = Compiler::uri_to_path(params.context_uri); auto context_path_id = path_pool.intern(context_path); ext::SwitchContextResult result; - // Verify the context file has a CDB entry. auto context_cdb = cdb.lookup(context_path, {.suppress_logging = true}); if(context_cdb.empty()) { result.success = false; co_return to_raw(result); } - // Set active context and invalidate cached header context so - // resolve_header_context will pick the new host on next compile. - active_contexts[path_id] = context_path_id; - header_file_contexts.erase(path_id); - - // Also invalidate the PCH and AST deps for the old context so - // they get rebuilt with the new host's preamble. - pch_states.erase(path_id); - ast_deps.erase(path_id); - - // Mark the document as dirty so it gets recompiled. - auto doc_it = documents.find(path_id); - if(doc_it != documents.end()) { - doc_it->second.ast_dirty = true; - } - + compiler.switch_context(path_id, context_path_id); result.success = true; co_return to_raw(result); }); diff --git a/src/server/master_server.h b/src/server/master_server.h index bd39ec6a..99a2a717 100644 --- a/src/server/master_server.h +++ b/src/server/master_server.h @@ -3,142 +3,21 @@ #include #include #include +#include -#include "command/command.h" #include "eventide/async/async.h" -#include "eventide/ipc/lsp/protocol.h" #include "eventide/ipc/peer.h" #include "eventide/serde/serde/raw_value.h" -#include "index/merged_index.h" -#include "index/project_index.h" -#include "semantic/relation_kind.h" -#include "server/compile_graph.h" +#include "server/compiler.h" #include "server/config.h" +#include "server/indexer.h" #include "server/worker_pool.h" #include "support/path_pool.h" #include "syntax/dependency_graph.h" -#include "llvm/ADT/DenseMap.h" -#include "llvm/ADT/DenseSet.h" -#include "llvm/ADT/SmallVector.h" -#include "llvm/ADT/StringMap.h" -#include "llvm/ADT/StringRef.h" - namespace clice { namespace et = eventide; -namespace protocol = et::ipc::protocol; - -enum class CompletionContext { None, IncludeQuoted, IncludeAngled, Import }; - -struct PreambleCompletionContext { - CompletionContext kind = CompletionContext::None; - std::string prefix; -}; - -struct DocumentState { - int version = 0; - - std::string text; - - std::uint64_t generation = 0; - - bool ast_dirty = true; - - /// Non-null while a compile is in flight. Callers wait on the event; - /// the compile task runs independently and cannot be cancelled by LSP - /// $/cancelRequest. - struct PendingCompile { - et::event done; - bool succeeded = false; - }; - - std::shared_ptr compiling; -}; - -/// Context for compiling a header file that lacks its own CDB entry. -struct HeaderFileContext { - std::uint32_t host_path_id; // Source file acting as host - std::string preamble_path; // Path to generated preamble file on disk - std::uint64_t preamble_hash; // Hash of preamble content for staleness -}; - -/// Two-layer staleness snapshot for compilation artifacts (PCH, AST, etc.). -/// -/// Layer 1 (fast): compare each file's current mtime against build_at. -/// If all mtimes <= build_at, the artifact is fresh (zero I/O beyond stat). -/// -/// Layer 2 (precise): for files whose mtime changed, re-hash their content -/// and compare against the stored hash. If the hash matches, the file was -/// "touched" but not actually modified — skip the rebuild. -/// -/// This avoids unnecessary recompilation from timestamp-only changes (e.g. -/// git checkout, touch, backup restore) while remaining cheap in the common -/// case where nothing changed. -struct DepsSnapshot { - /// File path IDs interned via PathPool. - llvm::SmallVector path_ids; - - /// xxh3_64bits of file content at build time. - llvm::SmallVector hashes; - - /// time_t when this snapshot was captured. - std::int64_t build_at = 0; -}; - -/// Cached PCH state for a single source file. -struct PCHState { - /// Built PCH file path. - std::string path; - - /// Preamble byte offset used when building. - std::uint32_t bound = 0; - - /// xxh3 hash of preamble content. - std::uint64_t hash = 0; - - /// Dependency snapshot for staleness detection. - DepsSnapshot deps; - - /// Non-null while a build is in flight. - std::shared_ptr building; -}; - -/// Cached PCM state for a single module file. -struct PCMState { - /// Built PCM file path. - std::string path; - - /// Dependency snapshot for staleness detection. - DepsSnapshot deps; -}; - -/// Information about a symbol at a given position. -struct SymbolInfo { - /// Unique hash identifying this symbol across the project. - index::SymbolHash hash = 0; - - /// Human-readable symbol name. - std::string name; - - /// Symbol kind (function, class, variable, etc.). - SymbolKind kind; - - /// URI of the file containing this symbol. - std::string uri; - - /// Source range of the symbol's identifier. - protocol::Range range; -}; - -/// In-memory index for an open file. Kept separate from MergedIndex because -/// open files change frequently, are based on unsaved buffer content, and only -/// need to track the main file (headers are covered by PCH/PCM indexing). -struct OpenFileIndex { - index::FileIndex file_index; - index::SymbolTable symbols; - std::string content; ///< Buffer text at index time (for position mapping). -}; enum class ServerLifecycle : std::uint8_t { Uninitialized, @@ -148,6 +27,11 @@ enum class ServerLifecycle : std::uint8_t { Exited, }; +/// Top-level LSP server. +/// +/// Registers LSP handlers and delegates all compilation, document management, +/// and index queries to Compiler and Indexer respectively. MasterServer +/// itself only owns workspace initialization and background indexing scheduling. class MasterServer { public: MasterServer(et::event_loop& loop, et::ipc::JsonPeer& peer, std::string self_path); @@ -156,235 +40,35 @@ public: void register_handlers(); private: - /// Event loop for scheduling async tasks. et::event_loop& loop; - - /// JSON-RPC peer for LSP communication. et::ipc::JsonPeer& peer; - - /// Pool of stateful/stateless worker processes. WorkerPool pool; - - /// Interning pool for file paths (path string -> uint32_t ID). PathPool path_pool; + Indexer indexer; - /// Current server lifecycle state. ServerLifecycle lifecycle = ServerLifecycle::Uninitialized; - - /// Path to the clice binary itself. std::string self_path; - - /// Root directory of the opened workspace. std::string workspace_root; - - /// User/project configuration. CliceConfig config; - - /// Session-specific log directory (e.g. .clice/logs/2026-04-05_10-30-00/). std::string session_log_dir; - /// Compilation database (compile_commands.json). CompilationDatabase cdb; - - /// Include/module dependency graph built from fast lexer scanning. DependencyGraph dependency_graph; - /// Module compilation graph (lazy dependency resolution). - std::unique_ptr compile_graph; + Compiler compiler; - /// path_id -> built PCM output path (set after successful module build). - llvm::DenseMap pcm_paths; - - /// path_id -> module name (for files that provide a module interface). - llvm::DenseMap path_to_module; - - /// path_id -> cached PCH state (path, preamble hash, deps, build event). - llvm::DenseMap pch_states; - - /// path_id -> cached PCM state (path, deps). - llvm::DenseMap pcm_states; - - // === Index state === - - /// Global symbol table and path mapping for the project. - index::ProjectIndex project_index; - - /// Per-file merged index shards (keyed by project-level path_id). - llvm::DenseMap merged_indices; - - /// Files queued for background indexing (server-level path_ids from CDB). + /// Background indexing state. std::vector index_queue; - - /// Index of next file to process in index_queue. std::size_t index_queue_pos = 0; - - /// Whether background indexing is currently in progress. bool indexing_active = false; - - /// Whether a background indexing coroutine has been scheduled (waiting on timer). bool indexing_scheduled = false; - - /// Timer for idle-triggered background indexing. std::shared_ptr index_idle_timer; - /// In-memory index for open files (server-level path_id -> OpenFileIndex). - llvm::DenseMap open_file_indices; - - /// Project-level path_ids of files that have an OpenFileIndex. - /// Used to skip stale MergedIndex shards during cross-file queries. - llvm::DenseSet open_proj_path_ids; - - /// path_id -> open document state (text, version, generation, dirty flag). - llvm::DenseMap documents; - - /// Per-file dependency snapshots from last successful AST compilation. - llvm::DenseMap ast_deps; - - /// Header context cache: header_path_id -> context - llvm::DenseMap header_file_contexts; - - /// Active compilation context overrides: path_id -> context_path_id. - /// When a file has an entry here, it is compiled using the context file's - /// compile command (e.g. a header compiled through a specific source file). - llvm::DenseMap active_contexts; - - // === Helpers === - - /// Convert a file:// URI to a local file path. - std::string uri_to_path(const std::string& uri); - - /// Publish diagnostics to the LSP client. - void publish_diagnostics(const std::string& uri, - int version, - const eventide::serde::RawValue& diagnostics_json); - - /// Clear diagnostics for a file (publish empty array). - void clear_diagnostics(const std::string& uri); - - /// Pull-based compilation entry point. Ensures AST is up-to-date. - et::task ensure_compiled(std::uint32_t path_id); - - /// Load CDB and build initial include/module dependency graph. et::task<> load_workspace(); - - /// Fill compile arguments from CDB for a given file. - bool fill_compile_args(llvm::StringRef path, - std::string& directory, - std::vector& arguments); - - /// Fill compile arguments using header context (host source's CDB entry - /// with file path replaced and preamble injected). - bool fill_header_context_args(llvm::StringRef path, - std::uint32_t path_id, - std::string& directory, - std::vector& arguments); - - /// Generate a preamble file for compiling a header in context. - /// The preamble contains all code from the host source (and intermediate - /// headers) that comes BEFORE the #include of the target header. - std::optional resolve_header_context(std::uint32_t header_path_id); - - /// Build or reuse PCH for a source file. Returns true if PCH is available. - et::task ensure_pch(std::uint32_t path_id, - llvm::StringRef path, - const std::string& text, - const std::string& directory, - const std::vector& arguments); - - /// Compile module dependencies, build/reuse PCH, and fill PCM paths. - /// Shared preparation step for ensure_compiled() and forward_stateless(). - et::task ensure_deps(std::uint32_t path_id, - llvm::StringRef path, - const std::string& text, - const std::string& directory, - const std::vector& arguments, - std::pair& pch, - std::unordered_map& pcms); - - /// Schedule background indexing when idle. void schedule_indexing(); - - /// Background indexing coroutine: picks files from queue and dispatches to workers. et::task<> run_background_indexing(); - /// Merge a TUIndex result into ProjectIndex and MergedIndex shards. - void merge_index_result(const void* tu_index_data, std::size_t size); - - /// Persist index state to disk. - void save_index(); - - /// Load index state from disk. - void load_index(); - - /// Load PCH/PCM cache metadata from cache.json. - void load_cache(); - - /// Save PCH/PCM cache metadata to cache.json. - void save_cache(); - - /// Clean up stale cache files older than max_age_days. - void cleanup_cache(int max_age_days = 7); - - // === Include/import completion (handled in master) === - - /// Detect whether the cursor is on an #include or import line. - PreambleCompletionContext detect_completion_context(const std::string& text, uint32_t offset); - - /// Complete #include paths by enumerating search directories. - et::serde::RawValue complete_include(const PreambleCompletionContext& ctx, - llvm::StringRef path); - - /// Complete import module names from the known module map. - et::serde::RawValue complete_import(const PreambleCompletionContext& ctx); - - // === Feature request forwarding === - using RawResult = et::task; - - /// Forward a simple stateful request (path-only worker params). - template - RawResult forward_stateful(const std::string& uri); - - /// Forward a stateful request with position-to-offset conversion. - template - RawResult forward_stateful(const std::string& uri, const protocol::Position& position); - - /// Forward a stateless request with document content and compile args. - template - RawResult forward_stateless(const std::string& uri, const protocol::Position& position); - - // === Index query helpers === - - /// Query index for symbol relations (GoToDefinition, FindReferences, etc.). - RawResult query_index_relations(const std::string& uri, - const protocol::Position& position, - RelationKind kind); - - /// Look up a symbol at a position, returning its hash, name, kind, and range. - et::task> - lookup_symbol_at_position(const std::string& uri, const protocol::Position& position); - - /// Find the definition location (uri + range) of a symbol by its hash. - std::optional find_symbol_definition_location(index::SymbolHash hash); - - /// Find a symbol's name and kind by hash, searching open file indices - /// first (fresher), then ProjectIndex. Returns false if not found. - bool find_symbol_info(index::SymbolHash hash, std::string& name, SymbolKind& kind) const; - - /// Convert clice::SymbolKind to LSP protocol::SymbolKind. - static protocol::SymbolKind to_lsp_symbol_kind(SymbolKind kind); - - /// Build a CallHierarchyItem from a SymbolInfo. - protocol::CallHierarchyItem build_call_hierarchy_item(const SymbolInfo& info); - - /// Build a TypeHierarchyItem from a SymbolInfo. - protocol::TypeHierarchyItem build_type_hierarchy_item(const SymbolInfo& info); - - /// Resolve SymbolInfo from a hierarchy item's stored data (symbol hash). - et::task> - resolve_hierarchy_item(const std::string& uri, - const protocol::Range& range, - const std::optional& data); }; } // namespace clice diff --git a/src/server/protocol.h b/src/server/protocol.h index 1857303b..5ea5c565 100644 --- a/src/server/protocol.h +++ b/src/server/protocol.h @@ -15,8 +15,27 @@ namespace clice::worker { namespace protocol = eventide::ipc::protocol; -// === StatefulWorker Requests === +/// Kind of AST query dispatched to a stateful worker. +enum class QueryKind : uint8_t { + Hover, + GoToDefinition, + SemanticTokens, + InlayHints, + FoldingRange, + DocumentSymbol, + DocumentLink, + CodeAction, +}; +/// Unified parameters for all stateful AST queries. +/// The worker dispatches to the appropriate feature handler based on `kind`. +struct QueryParams { + QueryKind kind; + std::string path; + uint32_t offset = 0; ///< Used by Hover and GoToDefinition. +}; + +/// Parameters for stateful compilation (builds AST, publishes diagnostics). struct CompileParams { std::string path; int version; @@ -37,111 +56,51 @@ struct CompileResult { std::string tu_index_data; }; -struct HoverParams { - std::string path; - uint32_t offset; +/// Kind of build task dispatched to a stateless worker. +enum class BuildKind : uint8_t { + BuildPCH, + BuildPCM, + Index, + Completion, + SignatureHelp, }; -struct SemanticTokensParams { - std::string path; -}; - -struct InlayHintsParams { - std::string path; -}; - -struct FoldingRangeParams { - std::string path; -}; - -struct DocumentSymbolParams { - std::string path; -}; - -struct DocumentLinkParams { - std::string path; -}; - -struct CodeActionParams { - std::string path; -}; - -struct GoToDefinitionParams { - std::string path; - uint32_t offset; -}; - -// === StatelessWorker Requests === - -struct CompletionParams { - std::string path; - int version; - std::string text; +/// Unified parameters for all stateless build/compilation tasks. +/// Fields are used selectively based on `kind`: +/// - All: file, directory, arguments +/// - BuildPCH: + content, preamble_bound, output_path +/// - BuildPCM: + module_name, pcms, output_path +/// - Index: + pcms +/// - Completion: + text, version, offset, pch, pcms +/// - SignatureHelp: + text, version, offset, pch, pcms +struct BuildParams { + BuildKind kind; + std::string file; std::string directory; std::vector arguments; + + /// Source text for Completion/SignatureHelp, preamble content for BuildPCH. + std::string text; + int version = 0; + uint32_t offset = 0; std::pair pch; std::unordered_map pcms; - uint32_t offset; + + std::string output_path; ///< BuildPCH, BuildPCM + std::string module_name; ///< BuildPCM + uint32_t preamble_bound = UINT32_MAX; ///< BuildPCH }; -struct SignatureHelpParams { - std::string path; - int version; - std::string text; - std::string directory; - std::vector arguments; - std::pair pch; - std::unordered_map pcms; - uint32_t offset; -}; - -struct BuildPCHParams { - std::string file; - std::string directory; - std::vector arguments; - std::string content; - std::uint32_t preamble_bound = UINT32_MAX; - std::string output_path; -}; - -struct BuildPCHResult { - bool success; +/// Unified result for stateless build tasks. +/// For Completion/SignatureHelp, the result JSON is in `result_json`. +/// For BuildPCH/BuildPCM/Index, structured fields are used. +struct BuildResult { + bool success = true; std::string error; - std::string pch_path; + std::string output_path; ///< PCH or PCM path std::vector deps; - /// Serialized TUIndex for preamble headers (main file index cleared). - std::string tu_index_data; -}; - -struct BuildPCMParams { - std::string file; - std::string directory; - std::vector arguments; - std::string module_name; - std::unordered_map pcms; - std::string output_path; -}; - -struct BuildPCMResult { - bool success; - std::string error; - std::string pcm_path; - std::vector deps; - /// Serialized TUIndex for the module interface file. - std::string tu_index_data; -}; - -struct IndexParams { - std::string file; - std::string directory; - std::vector arguments; - std::unordered_map pcms; -}; - -struct IndexResult { - bool success; - std::string error; std::string tu_index_data; + eventide::serde::RawValue result_json; ///< Completion/SignatureHelp result }; // === Notifications === @@ -202,8 +161,6 @@ struct SwitchContextResult { namespace eventide::ipc::protocol { -// === Stateful Requests === - template <> struct RequestTraits { using Result = clice::worker::CompileResult; @@ -211,83 +168,15 @@ struct RequestTraits { }; template <> -struct RequestTraits { +struct RequestTraits { using Result = eventide::serde::RawValue; - constexpr inline static std::string_view method = "clice/worker/hover"; + constexpr inline static std::string_view method = "clice/worker/query"; }; template <> -struct RequestTraits { - using Result = eventide::serde::RawValue; - constexpr inline static std::string_view method = "clice/worker/semanticTokens"; -}; - -template <> -struct RequestTraits { - using Result = eventide::serde::RawValue; - constexpr inline static std::string_view method = "clice/worker/inlayHints"; -}; - -template <> -struct RequestTraits { - using Result = eventide::serde::RawValue; - constexpr inline static std::string_view method = "clice/worker/foldingRange"; -}; - -template <> -struct RequestTraits { - using Result = eventide::serde::RawValue; - constexpr inline static std::string_view method = "clice/worker/documentSymbol"; -}; - -template <> -struct RequestTraits { - using Result = eventide::serde::RawValue; - constexpr inline static std::string_view method = "clice/worker/documentLink"; -}; - -template <> -struct RequestTraits { - using Result = eventide::serde::RawValue; - constexpr inline static std::string_view method = "clice/worker/codeAction"; -}; - -template <> -struct RequestTraits { - using Result = eventide::serde::RawValue; - constexpr inline static std::string_view method = "clice/worker/goToDefinition"; -}; - -// === Stateless Requests === - -template <> -struct RequestTraits { - using Result = eventide::serde::RawValue; - constexpr inline static std::string_view method = "clice/worker/completion"; -}; - -template <> -struct RequestTraits { - using Result = eventide::serde::RawValue; - constexpr inline static std::string_view method = "clice/worker/signatureHelp"; -}; - -template <> -struct RequestTraits { - using Result = clice::worker::BuildPCHResult; - constexpr inline static std::string_view method = "clice/worker/buildPCH"; -}; - -template <> -struct RequestTraits { - using Result = clice::worker::BuildPCMResult; - constexpr inline static std::string_view method = "clice/worker/buildPCM"; -}; - -template <> -struct RequestTraits { - using Result = clice::worker::IndexResult; - constexpr inline static std::string_view method = "clice/worker/index"; +struct RequestTraits { + using Result = clice::worker::BuildResult; + constexpr inline static std::string_view method = "clice/worker/build"; }; // === Notifications === diff --git a/src/server/stateful_worker.cpp b/src/server/stateful_worker.cpp index fd377150..9e93afa4 100644 --- a/src/server/stateful_worker.cpp +++ b/src/server/stateful_worker.cpp @@ -1,7 +1,6 @@ #include "server/stateful_worker.h" #include -#include #include #include #include @@ -10,14 +9,12 @@ #include "compile/compilation.h" #include "eventide/async/async.h" -#include "eventide/ipc/json_codec.h" #include "eventide/ipc/peer.h" #include "eventide/ipc/transport.h" -#include "eventide/serde/json/serializer.h" -#include "eventide/serde/serde/raw_value.h" #include "feature/feature.h" #include "index/tu_index.h" #include "server/protocol.h" +#include "server/worker_common.h" #include "support/logging.h" #include "llvm/ADT/StringMap.h" @@ -50,32 +47,6 @@ struct DocumentEntry { et::mutex strand; }; -struct ScopedTimer { - std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now(); - - long long ms() const { - return std::chrono::duration_cast( - std::chrono::steady_clock::now() - start) - .count(); - } -}; - -static void fill_args(CompilationParams& cp, - const std::string& directory, - const std::vector& arguments) { - cp.directory = directory; - for(auto& arg: arguments) { - cp.arguments.push_back(arg.c_str()); - } -} - -/// Serialize any value to LSP JSON RawValue. -template -static et::serde::RawValue to_raw(const T& value) { - auto json = et::serde::json::to_json(value); - return et::serde::RawValue{json ? std::move(*json) : "null"}; -} - class StatefulWorker { et::ipc::BincodePeer& peer; std::uint64_t memory_limit; @@ -258,74 +229,47 @@ void StatefulWorker::register_handlers() { documents.erase(params.path); }); - // === Hover === + // === Query (hover, definition, semantic tokens, etc.) === peer.on_request( [this](RequestContext& ctx, - const worker::HoverParams& params) -> RequestResult { - co_return co_await with_ast(params.path, [&](DocumentEntry& doc) { - auto result = feature::hover(doc.unit, params.offset); - return result ? to_raw(*result) : et::serde::RawValue{"null"}; - }); + const worker::QueryParams& params) -> RequestResult { + using K = worker::QueryKind; + switch(params.kind) { + case K::Hover: + co_return co_await with_ast(params.path, [&](DocumentEntry& doc) { + auto result = feature::hover(doc.unit, params.offset); + return result ? to_raw(*result) : et::serde::RawValue{"null"}; + }); + case K::GoToDefinition: + // TODO: Implement go-to-definition + co_return et::serde::RawValue{"[]"}; + case K::SemanticTokens: + co_return co_await with_ast(params.path, [&](DocumentEntry& doc) { + return to_raw(feature::semantic_tokens(doc.unit)); + }); + case K::InlayHints: + co_return co_await with_ast(params.path, [&](DocumentEntry& doc) { + LocalSourceRange range{0, static_cast(doc.text.size())}; + return to_raw(feature::inlay_hints(doc.unit, range)); + }); + case K::FoldingRange: + co_return co_await with_ast(params.path, [&](DocumentEntry& doc) { + return to_raw(feature::folding_ranges(doc.unit)); + }); + case K::DocumentSymbol: + co_return co_await with_ast(params.path, [&](DocumentEntry& doc) { + return to_raw(feature::document_symbols(doc.unit)); + }); + case K::DocumentLink: + co_return co_await with_ast(params.path, [&](DocumentEntry& doc) { + return to_raw(feature::document_links(doc.unit)); + }); + case K::CodeAction: + // TODO: Implement code actions + co_return et::serde::RawValue{"[]"}; + } + co_return et::serde::RawValue{"null"}; }); - - // === SemanticTokens === - peer.on_request([this](RequestContext& ctx, const worker::SemanticTokensParams& params) - -> RequestResult { - co_return co_await with_ast(params.path, [&](DocumentEntry& doc) { - return to_raw(feature::semantic_tokens(doc.unit)); - }); - }); - - // === InlayHints === - peer.on_request( - [this](RequestContext& ctx, - const worker::InlayHintsParams& params) -> RequestResult { - co_return co_await with_ast(params.path, [&](DocumentEntry& doc) { - LocalSourceRange range{0, static_cast(doc.text.size())}; - return to_raw(feature::inlay_hints(doc.unit, range)); - }); - }); - - // === FoldingRange === - peer.on_request([this](RequestContext& ctx, const worker::FoldingRangeParams& params) - -> RequestResult { - co_return co_await with_ast(params.path, [&](DocumentEntry& doc) { - return to_raw(feature::folding_ranges(doc.unit)); - }); - }); - - // === DocumentSymbol === - peer.on_request([this](RequestContext& ctx, const worker::DocumentSymbolParams& params) - -> RequestResult { - co_return co_await with_ast(params.path, [&](DocumentEntry& doc) { - return to_raw(feature::document_symbols(doc.unit)); - }); - }); - - // === DocumentLink === - peer.on_request([this](RequestContext& ctx, const worker::DocumentLinkParams& params) - -> RequestResult { - co_return co_await with_ast(params.path, [&](DocumentEntry& doc) { - return to_raw(feature::document_links(doc.unit)); - }); - }); - - // === CodeAction === - peer.on_request( - [this](RequestContext& ctx, - const worker::CodeActionParams& params) -> RequestResult { - LOG_TRACE("CodeAction request: path={}", params.path); - // TODO: Implement code actions - co_return et::serde::RawValue{"[]"}; - }); - - // === GoToDefinition === - peer.on_request([this](RequestContext& ctx, const worker::GoToDefinitionParams& params) - -> RequestResult { - LOG_TRACE("GoToDefinition request: path={}, offset={}", params.path, params.offset); - // TODO: Implement go-to-definition - co_return et::serde::RawValue{"[]"}; - }); } int run_stateful_worker_mode(std::uint64_t memory_limit, diff --git a/src/server/stateless_worker.cpp b/src/server/stateless_worker.cpp index 0dbb55e9..bd849342 100644 --- a/src/server/stateless_worker.cpp +++ b/src/server/stateless_worker.cpp @@ -1,17 +1,13 @@ #include "server/stateless_worker.h" -#include - #include "compile/compilation.h" #include "eventide/async/async.h" -#include "eventide/ipc/json_codec.h" #include "eventide/ipc/peer.h" #include "eventide/ipc/transport.h" -#include "eventide/serde/json/serializer.h" -#include "eventide/serde/serde/raw_value.h" #include "feature/feature.h" #include "index/tu_index.h" #include "server/protocol.h" +#include "server/worker_common.h" #include "support/logging.h" #include "llvm/Support/raw_ostream.h" @@ -22,29 +18,238 @@ namespace et = eventide; using et::ipc::RequestResult; using RequestContext = et::ipc::BincodePeer::RequestContext; -struct ScopedTimer { - std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now(); - - long long ms() const { - return std::chrono::duration_cast( - std::chrono::steady_clock::now() - start) - .count(); +/// Extract error messages from compilation diagnostics. +static std::string collect_errors(CompilationUnit& unit) { + std::string errors; + for(auto& diag: unit.diagnostics()) { + if(diag.id.level >= DiagnosticLevel::Error) { + if(!errors.empty()) + errors += "; "; + errors += diag.message; + } } -}; + return errors; +} -static void fill_args(CompilationParams& cp, - const std::string& directory, - const std::vector& arguments) { - cp.directory = directory; - for(auto& arg: arguments) { - cp.arguments.push_back(arg.c_str()); +/// Build a TUIndex, serialize it, and return as a string. +static std::string serialize_tu_index(CompilationUnit& unit, bool interested_only = false) { + auto tu_index = index::TUIndex::build(unit, interested_only); + if(!interested_only) { + tu_index.main_file_index = index::FileIndex(); + } + std::string serialized; + llvm::raw_string_ostream os(serialized); + tu_index.serialize(os); + return serialized; +} + +/// Write compilation output to disk, handling tmp+rename pattern. +/// Returns the final path on success, or empty string on failure. +static std::string finalize_output(const std::string& output_path, + const std::string& tmp_path, + const std::string& file, + const char* label) { + if(!output_path.empty()) { + auto ec = fs::rename(tmp_path, output_path); + if(ec) { + return output_path; + } else { + LOG_WARN("{}: rename {} -> {} failed: {}", + label, + tmp_path, + output_path, + ec.error().message()); + return tmp_path; + } + } + return tmp_path; +} + +static worker::BuildResult handle_build_pch(const worker::BuildParams& params) { + ScopedTimer timer; + + CompilationParams cp; + cp.kind = CompilationKind::Preamble; + fill_args(cp, params.directory, params.arguments); + cp.add_remapped_file(params.file, params.text, params.preamble_bound); + + std::string tmp_path; + bool has_output = !params.output_path.empty(); + if(has_output) { + tmp_path = params.output_path + ".tmp"; + } else { + auto tmp = fs::createTemporaryFile("clice-pch", "pch"); + if(!tmp) { + LOG_ERROR("BuildPCH: failed to create temp file"); + return {false, "Failed to create temporary PCH file"}; + } + tmp_path = *tmp; + } + cp.output_file = tmp_path; + + PCHInfo pch_info; + auto unit = compile(cp, pch_info); + bool success = unit.completed(); + + std::string errors; + if(!success) + errors = collect_errors(unit); + + std::string tu_index_data; + if(success) + tu_index_data = serialize_tu_index(unit); + + // Destroy CompilationUnit to flush PCH to disk. + unit = CompilationUnit(nullptr); + + if(success) { + auto final_path = finalize_output(params.output_path, tmp_path, params.file, "BuildPCH"); + LOG_INFO("BuildPCH done: file={}, output={}, {}ms", params.file, final_path, timer.ms()); + worker::BuildResult result; + result.success = true; + result.output_path = std::move(final_path); + result.deps = pch_info.deps; + result.tu_index_data = std::move(tu_index_data); + return result; + } else { + LOG_WARN("BuildPCH failed: file={}, {}ms, errors=[{}]", params.file, timer.ms(), errors); + fs::remove(tmp_path); + return {false, errors.empty() ? "PCH compilation failed" : errors}; } } -template -static et::serde::RawValue to_raw(const T& value) { - auto json = et::serde::json::to_json(value); - return et::serde::RawValue{json ? std::move(*json) : "null"}; +static worker::BuildResult handle_build_pcm(const worker::BuildParams& params) { + ScopedTimer timer; + + CompilationParams cp; + cp.kind = CompilationKind::ModuleInterface; + fill_args(cp, params.directory, params.arguments); + for(auto& [name, path]: params.pcms) { + cp.pcms.try_emplace(name, path); + } + + std::string tmp_path; + bool has_output = !params.output_path.empty(); + if(has_output) { + tmp_path = params.output_path + ".tmp"; + } else { + auto tmp = fs::createTemporaryFile("clice-pcm", "pcm"); + if(!tmp) { + LOG_ERROR("BuildPCM: failed to create temp file"); + return {false, "Failed to create temporary PCM file"}; + } + tmp_path = *tmp; + } + cp.output_file = tmp_path; + + PCMInfo pcm_info; + auto unit = compile(cp, pcm_info); + bool success = unit.completed(); + + std::string errors; + if(!success) + errors = collect_errors(unit); + + std::string tu_index_data; + if(success) + tu_index_data = serialize_tu_index(unit, true); + + unit = CompilationUnit(nullptr); + + if(success) { + auto final_path = finalize_output(params.output_path, tmp_path, params.file, "BuildPCM"); + LOG_INFO("BuildPCM done: module={}, {}ms", params.module_name, timer.ms()); + worker::BuildResult result; + result.success = true; + result.output_path = std::move(final_path); + result.deps = pcm_info.deps; + result.tu_index_data = std::move(tu_index_data); + return result; + } else { + LOG_WARN("BuildPCM failed: module={}, {}ms, errors=[{}]", + params.module_name, + timer.ms(), + errors); + fs::remove(tmp_path); + return {false, errors.empty() ? "PCM compilation failed" : errors}; + } +} + +static worker::BuildResult handle_index(const worker::BuildParams& params) { + ScopedTimer timer; + + CompilationParams cp; + cp.kind = CompilationKind::Indexing; + fill_args(cp, params.directory, params.arguments); + for(auto& [name, path]: params.pcms) { + cp.pcms.try_emplace(name, path); + } + + auto unit = compile(cp); + if(!unit.completed()) { + LOG_WARN("Index failed: file={}, {}ms", params.file, timer.ms()); + return {false, "Index compilation failed"}; + } + + auto tu_index = index::TUIndex::build(unit); + std::string serialized; + llvm::raw_string_ostream os(serialized); + tu_index.serialize(os); + + LOG_INFO("Index done: file={}, {} symbols, {}ms", + params.file, + tu_index.symbols.size(), + timer.ms()); + worker::BuildResult result; + result.success = true; + result.tu_index_data = std::move(serialized); + return result; +} + +static worker::BuildResult handle_completion(const worker::BuildParams& params) { + ScopedTimer timer; + + CompilationParams cp; + cp.kind = CompilationKind::Completion; + fill_args(cp, params.directory, params.arguments); + if(!params.pch.first.empty()) { + cp.pch = params.pch; + } + for(auto& [name, path]: params.pcms) { + cp.pcms.try_emplace(name, path); + } + cp.add_remapped_file(params.file, params.text); + cp.completion = {params.file, params.offset}; + + auto items = feature::code_complete(cp); + LOG_DEBUG("Completion done: {} items, {}ms", items.size(), timer.ms()); + + worker::BuildResult result; + result.result_json = to_raw(items); + return result; +} + +static worker::BuildResult handle_signature_help(const worker::BuildParams& params) { + ScopedTimer timer; + + CompilationParams cp; + cp.kind = CompilationKind::Completion; + fill_args(cp, params.directory, params.arguments); + if(!params.pch.first.empty()) { + cp.pch = params.pch; + } + for(auto& [name, path]: params.pcms) { + cp.pcms.try_emplace(name, path); + } + cp.add_remapped_file(params.file, params.text); + cp.completion = {params.file, params.offset}; + + auto help = feature::signature_help(cp); + LOG_DEBUG("SignatureHelp done: {}ms", timer.ms()); + + worker::BuildResult result; + result.result_json = to_raw(help); + return result; } int run_stateless_worker_mode(const std::string& worker_name, const std::string& log_dir) { @@ -65,287 +270,18 @@ int run_stateless_worker_mode(const std::string& worker_name, const std::string& et::ipc::BincodePeer peer(loop, std::move(*transport_result)); - // === BuildPCH === - peer.on_request( - [&](RequestContext& ctx, - const worker::BuildPCHParams& params) -> RequestResult { - LOG_INFO("BuildPCH request: file={}", params.file); - - auto result = co_await et::queue([&]() -> worker::BuildPCHResult { - ScopedTimer timer; - - CompilationParams cp; - cp.kind = CompilationKind::Preamble; - fill_args(cp, params.directory, params.arguments); - cp.add_remapped_file(params.file, params.content, params.preamble_bound); - - // When output_path is set, write to .tmp then rename — avoids - // Windows file-lock failures when the target is held by another - // process. When empty, fall back to a temporary file. - std::string tmp_path; - bool has_output = !params.output_path.empty(); - if(has_output) { - tmp_path = params.output_path + ".tmp"; - } else { - auto tmp = fs::createTemporaryFile("clice-pch", "pch"); - if(!tmp) { - LOG_ERROR("BuildPCH: failed to create temp file"); - return {false, "Failed to create temporary PCH file", ""}; - } - tmp_path = *tmp; - } - cp.output_file = tmp_path; - - PCHInfo pch_info; - auto unit = compile(cp, pch_info); - bool success = unit.completed(); - - // Extract error messages before destroying the unit. - std::string errors; - if(!success) { - for(auto& diag: unit.diagnostics()) { - if(diag.id.level >= DiagnosticLevel::Error) { - if(!errors.empty()) - errors += "; "; - errors += diag.message; - } - } - } - - // Index preamble headers before destroying the unit. - // - // Uses interested_only=false to traverse all AST nodes (every - // header pulled in by the preamble). This can be expensive for - // large preambles (LLVM/Qt/Boost) but only runs once per unique - // preamble content (content-addressed PCH path), and avoids a - // separate full recompilation in background indexing. - // - // The main file index is cleared afterwards — only headers - // matter here; the main file's own index comes from the - // stateful worker compile (interested_only=true). - std::string tu_index_serialized; - if(success) { - auto tu_index = index::TUIndex::build(unit); - tu_index.main_file_index = index::FileIndex(); - llvm::raw_string_ostream os(tu_index_serialized); - tu_index.serialize(os); - } - - // Destroy CompilationUnit to flush PCH file to disk - // (EndSourceFile serializes the AST on destruction). - unit = CompilationUnit(nullptr); - - if(success) { - std::string final_path; - if(has_output) { - auto ec = fs::rename(tmp_path, params.output_path); - if(ec) { - final_path = params.output_path; - } else { - LOG_WARN("BuildPCH: rename {} -> {} failed: {}", - tmp_path, - params.output_path, - ec.error().message()); - final_path = tmp_path; - } - } else { - final_path = tmp_path; - } - LOG_INFO("BuildPCH done: file={}, output={}, {}ms", - params.file, - final_path, - timer.ms()); - worker::BuildPCHResult pch_result{true, "", std::move(final_path)}; - pch_result.deps = pch_info.deps; - pch_result.tu_index_data = std::move(tu_index_serialized); - return pch_result; - } else { - LOG_WARN("BuildPCH failed: file={}, {}ms, errors=[{}]", - params.file, - timer.ms(), - errors); - fs::remove(tmp_path); - return {false, errors.empty() ? "PCH compilation failed" : errors, ""}; - } - }); - co_return result.value(); - }); - - // === BuildPCM === - peer.on_request( - [&](RequestContext& ctx, - const worker::BuildPCMParams& params) -> RequestResult { - LOG_INFO("BuildPCM request: file={}, module={}", params.file, params.module_name); - - auto result = co_await et::queue([&]() -> worker::BuildPCMResult { - ScopedTimer timer; - - CompilationParams cp; - cp.kind = CompilationKind::ModuleInterface; - fill_args(cp, params.directory, params.arguments); - for(auto& [name, path]: params.pcms) { - cp.pcms.try_emplace(name, path); - } - - std::string tmp_path; - bool has_output = !params.output_path.empty(); - if(has_output) { - tmp_path = params.output_path + ".tmp"; - } else { - auto tmp = fs::createTemporaryFile("clice-pcm", "pcm"); - if(!tmp) { - LOG_ERROR("BuildPCM: failed to create temp file"); - return {false, "Failed to create temporary PCM file", ""}; - } - tmp_path = *tmp; - } - cp.output_file = tmp_path; - - PCMInfo pcm_info; - auto unit = compile(cp, pcm_info); - bool success = unit.completed(); - - std::string errors; - if(!success) { - for(auto& diag: unit.diagnostics()) { - if(diag.id.level >= DiagnosticLevel::Error) { - if(!errors.empty()) - errors += "; "; - errors += diag.message; - } - } - } - - // Index module content before destroying the unit. - // Use interested_only=true — we only need the module interface's - // own symbols and relations, not transitive header content. - std::string tu_index_serialized; - if(success) { - auto tu_index = index::TUIndex::build(unit, true); - llvm::raw_string_ostream os(tu_index_serialized); - tu_index.serialize(os); - } - - unit = CompilationUnit(nullptr); - - if(success) { - std::string final_path = tmp_path; - if(has_output) { - auto ec = fs::rename(tmp_path, params.output_path); - if(ec) { - final_path = params.output_path; - } else { - LOG_WARN("BuildPCM: rename {} -> {} failed: {}", - tmp_path, - params.output_path, - ec.error().message()); - } - } - LOG_INFO("BuildPCM done: module={}, {}ms", params.module_name, timer.ms()); - worker::BuildPCMResult pcm_result{true, "", std::move(final_path)}; - pcm_result.deps = pcm_info.deps; - pcm_result.tu_index_data = std::move(tu_index_serialized); - return pcm_result; - } else { - LOG_WARN("BuildPCM failed: module={}, {}ms, errors=[{}]", - params.module_name, - timer.ms(), - errors); - fs::remove(tmp_path); - return {false, errors.empty() ? "PCM compilation failed" : errors, ""}; - } - }); - co_return result.value(); - }); - - // === Completion === - peer.on_request( - [&](RequestContext& ctx, - const worker::CompletionParams& params) -> RequestResult { - LOG_DEBUG("Completion request: path={}, offset={}", params.path, params.offset); - - auto result = co_await et::queue([&]() -> et::serde::RawValue { - ScopedTimer timer; - - CompilationParams cp; - cp.kind = CompilationKind::Completion; - fill_args(cp, params.directory, params.arguments); - if(!params.pch.first.empty()) { - cp.pch = params.pch; - } - for(auto& [name, path]: params.pcms) { - cp.pcms.try_emplace(name, path); - } - cp.add_remapped_file(params.path, params.text); - cp.completion = {params.path, params.offset}; - - auto items = feature::code_complete(cp); - LOG_DEBUG("Completion done: {} items, {}ms", items.size(), timer.ms()); - return to_raw(items); - }); - co_return result.value(); - }); - - // === SignatureHelp === - peer.on_request([&](RequestContext& ctx, const worker::SignatureHelpParams& params) - -> RequestResult { - LOG_DEBUG("SignatureHelp request: path={}, offset={}", params.path, params.offset); - - auto result = co_await et::queue([&]() -> et::serde::RawValue { - ScopedTimer timer; - - CompilationParams cp; - cp.kind = CompilationKind::Completion; - fill_args(cp, params.directory, params.arguments); - if(!params.pch.first.empty()) { - cp.pch = params.pch; - } - for(auto& [name, path]: params.pcms) { - cp.pcms.try_emplace(name, path); - } - cp.add_remapped_file(params.path, params.text); - cp.completion = {params.path, params.offset}; - - auto help = feature::signature_help(cp); - LOG_DEBUG("SignatureHelp done: {}ms", timer.ms()); - return to_raw(help); - }); - co_return result.value(); - }); - - // === Index === peer.on_request([&](RequestContext& ctx, - const worker::IndexParams& params) -> RequestResult { - LOG_INFO("Index request: file={}", params.file); - - auto result = co_await et::queue([&]() -> worker::IndexResult { - ScopedTimer timer; - - CompilationParams cp; - cp.kind = CompilationKind::Indexing; - fill_args(cp, params.directory, params.arguments); - for(auto& [name, path]: params.pcms) { - cp.pcms.try_emplace(name, path); + const worker::BuildParams& params) -> RequestResult { + using K = worker::BuildKind; + auto result = co_await et::queue([&]() -> worker::BuildResult { + switch(params.kind) { + case K::BuildPCH: return handle_build_pch(params); + case K::BuildPCM: return handle_build_pcm(params); + case K::Index: return handle_index(params); + case K::Completion: return handle_completion(params); + case K::SignatureHelp: return handle_signature_help(params); } - - auto unit = compile(cp); - - if(!unit.completed()) { - LOG_WARN("Index failed: file={}, {}ms", params.file, timer.ms()); - return {false, "Index compilation failed", ""}; - } - - auto tu_index = index::TUIndex::build(unit); - - std::string serialized; - llvm::raw_string_ostream os(serialized); - tu_index.serialize(os); - - LOG_INFO("Index done: file={}, {} symbols, {}ms", - params.file, - tu_index.symbols.size(), - timer.ms()); - return {true, "", std::move(serialized)}; + return {false, "Unknown build kind"}; }); co_return result.value(); }); diff --git a/src/server/worker_common.h b/src/server/worker_common.h new file mode 100644 index 00000000..5d2a4faf --- /dev/null +++ b/src/server/worker_common.h @@ -0,0 +1,44 @@ +#pragma once + +/// Shared utilities for stateful and stateless worker processes. + +#include +#include +#include + +#include "compile/compilation.h" +#include "eventide/ipc/json_codec.h" +#include "eventide/serde/json/serializer.h" +#include "eventide/serde/serde/raw_value.h" + +namespace clice { + +/// RAII timer for measuring elapsed milliseconds. +struct ScopedTimer { + std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now(); + + long long ms() const { + return std::chrono::duration_cast( + std::chrono::steady_clock::now() - start) + .count(); + } +}; + +/// Fill CompilationParams directory and arguments from worker request fields. +inline void fill_args(CompilationParams& cp, + const std::string& directory, + const std::vector& arguments) { + cp.directory = directory; + for(auto& arg: arguments) { + cp.arguments.push_back(arg.c_str()); + } +} + +/// Serialize a value to JSON RawValue using LSP config. +template +inline eventide::serde::RawValue to_raw(const T& value) { + auto json = eventide::serde::json::to_json(value); + return eventide::serde::RawValue{json ? std::move(*json) : "null"}; +} + +} // namespace clice diff --git a/tests/unit/server/module_worker_tests.cpp b/tests/unit/server/module_worker_tests.cpp index c945b571..8a48aae0 100644 --- a/tests/unit/server/module_worker_tests.cpp +++ b/tests/unit/server/module_worker_tests.cpp @@ -39,7 +39,8 @@ TEST_CASE(BuildPCMThenCompileWithImport) { bool phase1_done = false; sl.run([&]() -> et::task<> { - worker::BuildPCMParams params; + worker::BuildParams params; + params.kind = worker::BuildKind::BuildPCM; params.file = iface; params.directory = "/tmp"; params.arguments = {"clang++", @@ -54,7 +55,7 @@ TEST_CASE(BuildPCMThenCompileWithImport) { auto result = co_await sl.peer->send_request(params); CO_ASSERT_TRUE(result.has_value()); CO_ASSERT_TRUE(result.value().success); - pcm_path = result.value().pcm_path; + pcm_path = result.value().output_path; EXPECT_FALSE(pcm_path.empty()); phase1_done = true; @@ -125,7 +126,8 @@ TEST_CASE(BuildPCMChainThenCompile) { sl.run([&]() -> et::task<> { // Build PCM for A first. { - worker::BuildPCMParams params; + worker::BuildParams params; + params.kind = worker::BuildKind::BuildPCM; params.file = mod_a; params.directory = "/tmp"; params.arguments = {"clang++", @@ -139,12 +141,13 @@ TEST_CASE(BuildPCMChainThenCompile) { auto result = co_await sl.peer->send_request(params); CO_ASSERT_TRUE(result.has_value() && result.value().success); - pcm_a = result.value().pcm_path; + pcm_a = result.value().output_path; } // Build PCM for B, passing A's PCM (transitive dep). { - worker::BuildPCMParams params; + worker::BuildParams params; + params.kind = worker::BuildKind::BuildPCM; params.file = mod_b; params.directory = "/tmp"; params.arguments = {"clang++", @@ -161,7 +164,7 @@ TEST_CASE(BuildPCMChainThenCompile) { auto result = co_await sl.peer->send_request(params); CO_ASSERT_TRUE(result.has_value() && result.value().success); - pcm_b = result.value().pcm_path; + pcm_b = result.value().output_path; } pcm_done = true; @@ -225,7 +228,8 @@ TEST_CASE(ModuleImplementationUnitWithWorker) { bool pcm_done = false; sl.run([&]() -> et::task<> { - worker::BuildPCMParams params; + worker::BuildParams params; + params.kind = worker::BuildKind::BuildPCM; params.file = iface; params.directory = "/tmp"; params.arguments = {"clang++", @@ -239,7 +243,7 @@ TEST_CASE(ModuleImplementationUnitWithWorker) { auto result = co_await sl.peer->send_request(params); CO_ASSERT_TRUE(result.has_value() && result.value().success); - pcm_path = result.value().pcm_path; + pcm_path = result.value().output_path; pcm_done = true; sl.peer->close_output(); diff --git a/tests/unit/server/pch_worker_tests.cpp b/tests/unit/server/pch_worker_tests.cpp index 2eb279e9..d520dd8d 100644 --- a/tests/unit/server/pch_worker_tests.cpp +++ b/tests/unit/server/pch_worker_tests.cpp @@ -40,7 +40,8 @@ TEST_CASE(BuildPCHThenCompile) { bool phase1_done = false; sl.run([&]() -> et::task<> { - worker::BuildPCHParams params; + worker::BuildParams params; + params.kind = worker::BuildKind::BuildPCH; params.file = main_file; params.directory = dir; params.arguments = {"clang++", @@ -51,13 +52,13 @@ TEST_CASE(BuildPCHThenCompile) { "-I", dir, main_file}; - params.content = main_text; + params.text = main_text; params.output_path = tmp.path("preamble.pch"); auto result = co_await sl.peer->send_request(params); CO_ASSERT_TRUE(result.has_value()); CO_ASSERT_TRUE(result.value().success); - pch_path = result.value().pch_path; + pch_path = result.value().output_path; EXPECT_FALSE(pch_path.empty()); phase1_done = true; diff --git a/tests/unit/server/stateful_worker_tests.cpp b/tests/unit/server/stateful_worker_tests.cpp index f03c763f..49109aa3 100644 --- a/tests/unit/server/stateful_worker_tests.cpp +++ b/tests/unit/server/stateful_worker_tests.cpp @@ -61,7 +61,8 @@ TEST_CASE(HoverWithoutCompile) { w.run([&]() -> et::task<> { // Hover on a file that hasn't been compiled should return null. - worker::HoverParams params; + worker::QueryParams params; + params.kind = worker::QueryKind::Hover; params.path = "/tmp/nonexistent.cpp"; params.offset = 0; @@ -101,7 +102,8 @@ TEST_CASE(CompileThenHover) { // After successful compilation, hover should return info. // "int foo() { return 42; }\n" is 25 chars, then char 22 on line 1 = offset 47 - worker::HoverParams hp; + worker::QueryParams hp; + hp.kind = worker::QueryKind::Hover; hp.path = src; hp.offset = 47; // position of 'foo' in 'return foo();' @@ -147,7 +149,8 @@ TEST_CASE(DocumentUpdate) { w.peer->send_notification(up); // After update, hover still returns stale AST results (not null). - worker::HoverParams hp; + worker::QueryParams hp; + hp.kind = worker::QueryKind::Hover; hp.path = src; hp.offset = 4; @@ -168,7 +171,8 @@ TEST_CASE(CodeActionReturnsEmpty) { bool test_done = false; w.run([&]() -> et::task<> { - worker::CodeActionParams params; + worker::QueryParams params; + params.kind = worker::QueryKind::CodeAction; params.path = "/tmp/test.cpp"; auto result = co_await w.peer->send_request(params); @@ -189,7 +193,8 @@ TEST_CASE(GoToDefinitionReturnsEmpty) { bool test_done = false; w.run([&]() -> et::task<> { - worker::GoToDefinitionParams params; + worker::QueryParams params; + params.kind = worker::QueryKind::GoToDefinition; params.path = "/tmp/test.cpp"; params.offset = 0; @@ -211,7 +216,8 @@ TEST_CASE(SemanticTokensWithoutCompile) { bool test_done = false; w.run([&]() -> et::task<> { - worker::SemanticTokensParams params; + worker::QueryParams params; + params.kind = worker::QueryKind::SemanticTokens; params.path = "/tmp/nonexistent.cpp"; auto result = co_await w.peer->send_request(params); @@ -231,7 +237,8 @@ TEST_CASE(FoldingRangeWithoutCompile) { bool test_done = false; w.run([&]() -> et::task<> { - worker::FoldingRangeParams params; + worker::QueryParams params; + params.kind = worker::QueryKind::FoldingRange; params.path = "/tmp/nonexistent.cpp"; auto result = co_await w.peer->send_request(params); @@ -251,7 +258,8 @@ TEST_CASE(DocumentSymbolWithoutCompile) { bool test_done = false; w.run([&]() -> et::task<> { - worker::DocumentSymbolParams params; + worker::QueryParams params; + params.kind = worker::QueryKind::DocumentSymbol; params.path = "/tmp/nonexistent.cpp"; auto result = co_await w.peer->send_request(params); @@ -271,7 +279,8 @@ TEST_CASE(DocumentLinkWithoutCompile) { bool test_done = false; w.run([&]() -> et::task<> { - worker::DocumentLinkParams params; + worker::QueryParams params; + params.kind = worker::QueryKind::DocumentLink; params.path = "/tmp/nonexistent.cpp"; auto result = co_await w.peer->send_request(params); @@ -291,7 +300,8 @@ TEST_CASE(InlayHintsWithoutCompile) { bool test_done = false; w.run([&]() -> et::task<> { - worker::InlayHintsParams params; + worker::QueryParams params; + params.kind = worker::QueryKind::InlayHints; params.path = "/tmp/nonexistent.cpp"; auto result = co_await w.peer->send_request(params); @@ -333,13 +343,15 @@ TEST_CASE(MultipleSequentialRequests) { CO_ASSERT_TRUE(cr.has_value()); // Now send multiple different feature requests sequentially. - worker::HoverParams hp; + worker::QueryParams hp; + hp.kind = worker::QueryKind::Hover; hp.path = src; hp.offset = 4; // 'foo' on line 0 auto r1 = co_await w.peer->send_request(hp); EXPECT_TRUE(r1.has_value()); - worker::CodeActionParams cap; + worker::QueryParams cap; + cap.kind = worker::QueryKind::CodeAction; cap.path = src; auto r2 = co_await w.peer->send_request(cap); EXPECT_TRUE(r2.has_value()); @@ -347,18 +359,21 @@ TEST_CASE(MultipleSequentialRequests) { // 'foo' in 'return foo(0);' at line 4, char 11 // lines: "int foo(int x) {\n"=17, " return x + 1;\n"=18, "}\n"=2, "int main() {\n"=14 // offset = 17+18+2+14+11 = 62 - worker::GoToDefinitionParams gdp; + worker::QueryParams gdp; + gdp.kind = worker::QueryKind::GoToDefinition; gdp.path = src; gdp.offset = 62; auto r3 = co_await w.peer->send_request(gdp); EXPECT_TRUE(r3.has_value()); - worker::SemanticTokensParams stp; + worker::QueryParams stp; + stp.kind = worker::QueryKind::SemanticTokens; stp.path = src; auto r4 = co_await w.peer->send_request(stp); EXPECT_TRUE(r4.has_value()); - worker::FoldingRangeParams frp; + worker::QueryParams frp; + frp.kind = worker::QueryKind::FoldingRange; frp.path = src; auto r5 = co_await w.peer->send_request(frp); EXPECT_TRUE(r5.has_value()); @@ -403,7 +418,8 @@ TEST_CASE(MultipleDocuments) { // Hover on each document after compilation. for(int i = 0; i < 3; i++) { - worker::HoverParams hp; + worker::QueryParams hp; + hp.kind = worker::QueryKind::Hover; hp.path = paths[i]; hp.offset = 4; // 'var_N' @@ -431,7 +447,8 @@ TEST_CASE(EvictNotification) { w.peer->send_notification(ep); // Hover on the evicted document should return null (document doesn't exist). - worker::HoverParams hp; + worker::QueryParams hp; + hp.kind = worker::QueryKind::Hover; hp.path = "/tmp/evict_test.cpp"; hp.offset = 0; @@ -470,7 +487,8 @@ TEST_CASE(SpawnWithMemoryLimit) { EXPECT_TRUE(cr.has_value()); // Feature request should work after compilation. - worker::HoverParams hp; + worker::QueryParams hp; + hp.kind = worker::QueryKind::Hover; hp.path = src; hp.offset = 4; // 'memlimit_var' diff --git a/tests/unit/server/stateless_worker_tests.cpp b/tests/unit/server/stateless_worker_tests.cpp index f35e110e..380bdae8 100644 --- a/tests/unit/server/stateless_worker_tests.cpp +++ b/tests/unit/server/stateless_worker_tests.cpp @@ -93,12 +93,13 @@ TEST_CASE(BuildPCHRequest) { bool test_done = false; w.run([&]() -> et::task<> { - worker::BuildPCHParams params; + worker::BuildParams params; + params.kind = worker::BuildKind::BuildPCH; params.file = hdr; params.directory = "/tmp"; params.arguments = {"clang++", "-resource-dir", std::string(resource_dir()), "-x", "c++-header", hdr}; - params.content = "#pragma once\nint pch_global = 42;\n"; + params.text = "#pragma once\nint pch_global = 42;\n"; params.output_path = tmp.path("test_pch.pch"); auto result = co_await w.peer->send_request(params); @@ -108,7 +109,7 @@ TEST_CASE(BuildPCHRequest) { co_return; } EXPECT_TRUE(result.value().success); - EXPECT_FALSE(result.value().pch_path.empty()); + EXPECT_FALSE(result.value().output_path.empty()); test_done = true; w.peer->close_output(); }); @@ -127,7 +128,8 @@ TEST_CASE(IndexRequest) { bool test_done = false; w.run([&]() -> et::task<> { - worker::IndexParams params; + worker::BuildParams params; + params.kind = worker::BuildKind::Index; params.file = src; params.directory = "/tmp"; params.arguments = make_args(src); @@ -161,7 +163,8 @@ TEST_CASE(BuildPCMRequest) { bool test_done = false; w.run([&]() -> et::task<> { - worker::BuildPCMParams params; + worker::BuildParams params; + params.kind = worker::BuildKind::BuildPCM; params.file = src; params.directory = "/tmp"; params.arguments = {"clang++", @@ -194,8 +197,9 @@ TEST_CASE(CompletionRequest) { bool test_done = false; w.run([&]() -> et::task<> { - worker::CompletionParams params; - params.path = src; + worker::BuildParams params; + params.kind = worker::BuildKind::Completion; + params.file = src; params.version = 1; params.text = text; params.directory = "/tmp"; @@ -223,8 +227,9 @@ TEST_CASE(SignatureHelpRequest) { bool test_done = false; w.run([&]() -> et::task<> { - worker::SignatureHelpParams params; - params.path = src; + worker::BuildParams params; + params.kind = worker::BuildKind::SignatureHelp; + params.file = src; params.version = 1; params.text = text; params.directory = "/tmp"; @@ -259,7 +264,8 @@ TEST_CASE(MultipleStatelessRequests) { w.run([&]() -> et::task<> { // Send multiple index requests to test stateless worker handles them sequentially. for(int i = 0; i < 3; i++) { - worker::IndexParams params; + worker::BuildParams params; + params.kind = worker::BuildKind::Index; params.file = paths[i]; params.directory = "/tmp"; params.arguments = make_args(paths[i]);