## Summary
- **`[[rules]]`**: TOML array-of-tables config for per-file compilation
flag rules with glob pattern matching (`append`/`remove`). Patterns are
pre-compiled at config load time. Rules whose patterns all fail to
compile are dropped entirely (no silent no-op entries), and rules now
apply uniformly to every compilation — including the header-context
fallback path used when editing a header without its own CDB entry.
- **CDB auto-scan**: Default search scans workspace root + all immediate
subdirectories for `compile_commands.json`, replacing the hardcoded
directory list.
- **LSP `initializationOptions`**: Clients can pass config as JSON via
the LSP initialize request; priority is `initializationOptions >
clice.toml > defaults`.
- **XDG cache paths**: Default cache/index/logging paths prefer
`$XDG_CACHE_HOME/clice/<workspace-hash>/`; fall back to
`$HOME/.cache/clice/<hash>/`, then `<workspace>/.clice/`.
- **`${workspace}` substitution**: supported in `cache_dir`,
`index_dir`, `logging_dir`, and every `compile_commands_paths` entry.
No-op when `workspace_root` is empty.
- **Partial config support**: All TOML/JSON fields are optional via
`kota::meta::defaulted<T>`, so minimal config files work correctly.
- **Detailed diagnostics**: malformed `clice.toml` now logs line, column
and parser description (via toml++ direct parse); a malformed workspace
config surfaces a clear fallback warning instead of silently reverting
to defaults.
## Test plan
- [x] 28 unit tests for config (full suite 545 unit tests pass, Debug)
- [x] 119 integration tests pass
- [x] 2 smoke tests pass
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* XDG-based, workspace-scoped project cache (PCH/PCM and header caches
moved under project cache) with workspace fallback
* Initialization options JSON can override config (takes precedence over
file/defaults)
* Per-file pattern rules to append/remove compile flags; expanded
discovery of compilation databases (multiple paths)
* **Refactor**
* Configuration fields reorganized under a project scope; runtime
behavior now respects project-scoped values
* **Tests**
* New unit and integration tests for config parsing, rule matching, and
persistent cache behavior
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
914 lines
36 KiB
C++
914 lines
36 KiB
C++
#include "server/master_server.h"
|
|
|
|
#include <algorithm>
|
|
#include <format>
|
|
#include <string>
|
|
#include <type_traits>
|
|
#include <variant>
|
|
|
|
#include "semantic/symbol_kind.h"
|
|
#include "server/protocol.h"
|
|
#include "support/filesystem.h"
|
|
#include "support/logging.h"
|
|
|
|
#include "kota/codec/json/json.h"
|
|
#include "kota/ipc/lsp/position.h"
|
|
#include "kota/ipc/lsp/protocol.h"
|
|
#include "kota/ipc/lsp/uri.h"
|
|
#include "kota/meta/enum.h"
|
|
#include "llvm/Support/FileSystem.h"
|
|
#include "llvm/Support/Path.h"
|
|
#include "llvm/Support/Process.h"
|
|
|
|
namespace clice {
|
|
|
|
namespace protocol = kota::ipc::protocol;
|
|
namespace lsp = kota::ipc::lsp;
|
|
namespace refl = kota::meta;
|
|
using kota::ipc::RequestResult;
|
|
using RequestContext = kota::ipc::JsonPeer::RequestContext;
|
|
using serde_raw = kota::codec::RawValue;
|
|
|
|
/// Serialize a value to a JSON RawValue using LSP config.
|
|
template <typename T>
|
|
static serde_raw to_raw(const T& value) {
|
|
auto json = kota::codec::json::to_json<kota::ipc::lsp_config>(value);
|
|
return serde_raw{json ? std::move(*json) : "null"};
|
|
}
|
|
|
|
MasterServer::MasterServer(kota::event_loop& loop,
|
|
kota::ipc::JsonPeer& peer,
|
|
std::string self_path) :
|
|
loop(loop), peer(peer), pool(loop), compiler(loop, peer, workspace, pool, sessions),
|
|
indexer(loop,
|
|
workspace,
|
|
sessions,
|
|
pool,
|
|
compiler,
|
|
[this](uint32_t proj_path_id) {
|
|
// Bridge project-level path_id to server-level path_id.
|
|
// The two PathPools may assign different IDs to the same path.
|
|
auto path = workspace.project_index.path_pool.path(proj_path_id);
|
|
auto server_id = workspace.path_pool.intern(path);
|
|
return sessions.contains(server_id);
|
|
}),
|
|
self_path(std::move(self_path)) {}
|
|
|
|
MasterServer::~MasterServer() = default;
|
|
|
|
kota::task<> MasterServer::load_workspace() {
|
|
if(workspace_root.empty())
|
|
co_return;
|
|
|
|
auto& cfg = workspace.config.project;
|
|
|
|
if(!cfg.cache_dir.empty()) {
|
|
auto ec = llvm::sys::fs::create_directories(cfg.cache_dir);
|
|
if(ec) {
|
|
LOG_WARN("Failed to create cache directory {}: {}",
|
|
std::string_view(cfg.cache_dir),
|
|
ec.message());
|
|
} else {
|
|
LOG_INFO("Cache directory: {}", std::string_view(cfg.cache_dir));
|
|
}
|
|
|
|
for(auto* subdir: {"cache/pch", "cache/pcm"}) {
|
|
auto dir = path::join(cfg.cache_dir, subdir);
|
|
if(auto ec2 = llvm::sys::fs::create_directories(dir))
|
|
LOG_WARN("Failed to create {}: {}", dir, ec2.message());
|
|
}
|
|
|
|
workspace.cleanup_cache();
|
|
workspace.load_cache();
|
|
}
|
|
|
|
// Discover compile_commands.json: configured paths first, then auto-scan.
|
|
std::string cdb_path;
|
|
for(auto& configured: cfg.compile_commands_paths) {
|
|
// Each entry can be a file or a directory containing compile_commands.json.
|
|
if(llvm::sys::fs::is_directory(configured)) {
|
|
auto candidate = path::join(configured, "compile_commands.json");
|
|
if(llvm::sys::fs::exists(candidate)) {
|
|
cdb_path = std::move(candidate);
|
|
break;
|
|
}
|
|
} else if(llvm::sys::fs::exists(configured)) {
|
|
cdb_path = configured;
|
|
break;
|
|
} else {
|
|
LOG_WARN("Configured compile_commands_path not found: {}", configured);
|
|
}
|
|
}
|
|
|
|
// Auto-scan: workspace root + all immediate subdirectories.
|
|
if(cdb_path.empty()) {
|
|
auto try_candidate = [&](llvm::StringRef dir) -> bool {
|
|
auto candidate = path::join(dir, "compile_commands.json");
|
|
if(llvm::sys::fs::exists(candidate)) {
|
|
cdb_path = std::move(candidate);
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
if(!try_candidate(workspace_root)) {
|
|
std::error_code ec;
|
|
for(llvm::sys::fs::directory_iterator it(workspace_root, ec), end; it != end && !ec;
|
|
it.increment(ec)) {
|
|
if(it->type() == llvm::sys::fs::file_type::directory_file) {
|
|
if(try_candidate(it->path()))
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if(cdb_path.empty()) {
|
|
LOG_WARN("No compile_commands.json found in workspace {}", workspace_root);
|
|
co_return;
|
|
}
|
|
|
|
auto count = workspace.cdb.load(cdb_path);
|
|
LOG_INFO("Loaded CDB from {} with {} entries", cdb_path, count);
|
|
|
|
auto report = scan_dependency_graph(workspace.cdb,
|
|
workspace.path_pool,
|
|
workspace.dep_graph,
|
|
/*cache=*/nullptr,
|
|
[this](llvm::StringRef path,
|
|
std::vector<std::string>& append,
|
|
std::vector<std::string>& remove) {
|
|
workspace.config.match_rules(path, append, remove);
|
|
});
|
|
workspace.dep_graph.build_reverse_map();
|
|
|
|
auto unresolved = report.includes_found - report.includes_resolved;
|
|
double accuracy =
|
|
report.includes_found > 0
|
|
? 100.0 * static_cast<double>(report.includes_resolved) / report.includes_found
|
|
: 100.0;
|
|
LOG_INFO(
|
|
"Dependency scan: {}ms, {} files ({} source + {} header), " "{} edges, {}/{} resolved ({:.1f}%), {} waves",
|
|
report.elapsed_ms,
|
|
report.total_files,
|
|
report.source_files,
|
|
report.header_files,
|
|
report.total_edges,
|
|
report.includes_resolved,
|
|
report.includes_found,
|
|
accuracy,
|
|
report.waves);
|
|
if(unresolved > 0)
|
|
LOG_WARN("{} unresolved includes", unresolved);
|
|
|
|
workspace.build_module_map();
|
|
indexer.load(cfg.index_dir);
|
|
|
|
if(*cfg.enable_indexing) {
|
|
for(auto& entry: workspace.cdb.get_entries()) {
|
|
auto file = workspace.cdb.resolve_path(entry.file);
|
|
auto server_id = workspace.path_pool.intern(file);
|
|
indexer.enqueue(server_id);
|
|
}
|
|
indexer.schedule();
|
|
}
|
|
|
|
compiler.init_compile_graph();
|
|
}
|
|
|
|
void MasterServer::register_handlers() {
|
|
using StringVec = std::vector<std::string>;
|
|
|
|
peer.on_request([this](RequestContext& ctx, const protocol::InitializeParams& params)
|
|
-> RequestResult<protocol::InitializeParams> {
|
|
if(lifecycle != ServerLifecycle::Uninitialized) {
|
|
co_return kota::outcome_error(protocol::Error{"Server already initialized"});
|
|
}
|
|
|
|
auto& init = params.lsp__initialize_params;
|
|
if(init.root_uri.has_value()) {
|
|
workspace_root = uri_to_path(*init.root_uri);
|
|
}
|
|
|
|
// Capture initializationOptions as raw JSON for config loading.
|
|
if(init.initialization_options.has_value()) {
|
|
auto json =
|
|
kota::codec::json::to_json<kota::ipc::lsp_config>(*init.initialization_options);
|
|
if(json)
|
|
init_options_json = std::move(*json);
|
|
}
|
|
|
|
lifecycle = ServerLifecycle::Initialized;
|
|
LOG_INFO("Initialized with workspace: {}", workspace_root);
|
|
|
|
protocol::InitializeResult result;
|
|
auto& caps = result.capabilities;
|
|
|
|
caps.text_document_sync = protocol::TextDocumentSyncOptions{
|
|
.open_close = true,
|
|
.change = protocol::TextDocumentSyncKind::Incremental,
|
|
.save = protocol::variant<protocol::boolean, protocol::SaveOptions>{true},
|
|
};
|
|
caps.workspace = protocol::WorkspaceOptions{};
|
|
caps.workspace->workspace_folders = protocol::WorkspaceFoldersServerCapabilities{
|
|
.supported = true,
|
|
.change_notifications = true,
|
|
};
|
|
|
|
caps.hover_provider = true;
|
|
caps.completion_provider = protocol::CompletionOptions{
|
|
.trigger_characters = StringVec{".", "<", ">", ":", "\"", "/", "*"},
|
|
};
|
|
caps.signature_help_provider = protocol::SignatureHelpOptions{
|
|
.trigger_characters = StringVec{"(", ")", "{", "}", "<", ">", ","},
|
|
};
|
|
/// FIXME: In the future, we would support work done progress.
|
|
caps.declaration_provider = protocol::DeclarationOptions{
|
|
.work_done_progress = false,
|
|
};
|
|
caps.definition_provider = protocol::DefinitionOptions{
|
|
.work_done_progress = false,
|
|
};
|
|
caps.implementation_provider = protocol::ImplementationOptions{
|
|
.work_done_progress = false,
|
|
};
|
|
caps.type_definition_provider = protocol::TypeDefinitionOptions{
|
|
.work_done_progress = false,
|
|
};
|
|
caps.references_provider = protocol::ReferenceOptions{
|
|
.work_done_progress = false,
|
|
};
|
|
caps.document_symbol_provider = true;
|
|
caps.document_link_provider = protocol::DocumentLinkOptions{};
|
|
caps.code_action_provider = true;
|
|
caps.folding_range_provider = true;
|
|
caps.inlay_hint_provider = true;
|
|
caps.call_hierarchy_provider = true;
|
|
caps.type_hierarchy_provider = true;
|
|
caps.workspace_symbol_provider = true;
|
|
|
|
protocol::SemanticTokensOptions sem_opts;
|
|
{
|
|
auto lower_first = [](std::string_view name) -> std::string {
|
|
std::string s(name);
|
|
if(!s.empty()) {
|
|
s[0] = static_cast<char>(std::tolower(static_cast<unsigned char>(s[0])));
|
|
}
|
|
return s;
|
|
};
|
|
|
|
auto to_names = [&](auto names) {
|
|
return std::ranges::to<std::vector>(names | std::views::transform(lower_first));
|
|
};
|
|
|
|
sem_opts.legend = protocol::SemanticTokensLegend{
|
|
to_names(refl::reflection<SymbolKind::Kind>::member_names),
|
|
to_names(refl::reflection<SymbolModifiers::Kind>::member_names),
|
|
};
|
|
}
|
|
sem_opts.full = true;
|
|
result.capabilities.semantic_tokens_provider = std::move(sem_opts);
|
|
|
|
protocol::ServerInfo info;
|
|
info.name = "clice";
|
|
info.version = "0.1.0";
|
|
result.server_info = std::move(info);
|
|
|
|
co_return result;
|
|
});
|
|
|
|
peer.on_notification([this](const protocol::InitializedParams& params) {
|
|
// Config priority: initializationOptions > clice.toml > defaults.
|
|
// Load the workspace config (with defaults applied) first, then overlay
|
|
// any initializationOptions on top so fields not mentioned in the JSON
|
|
// keep the values from clice.toml — kotatsu's deserializer only touches
|
|
// fields that are present in the input.
|
|
workspace.config = Config::load_from_workspace(workspace_root);
|
|
if(!init_options_json.empty()) {
|
|
if(auto ov = kota::codec::json::parse(init_options_json, workspace.config); !ov) {
|
|
LOG_WARN("Failed to apply initializationOptions: {}", ov.error().to_string());
|
|
} else {
|
|
// Re-run apply_defaults so overridden strings get workspace
|
|
// substitution and `compiled_rules` is rebuilt if `rules`
|
|
// changed. Defaults are gated on zero/empty sentinels, so
|
|
// existing values from the overlay are preserved.
|
|
workspace.config.apply_defaults(workspace_root);
|
|
LOG_INFO("Applied initializationOptions overlay");
|
|
}
|
|
init_options_json.clear();
|
|
}
|
|
|
|
auto& cfg = workspace.config.project;
|
|
|
|
if(!cfg.logging_dir.empty()) {
|
|
auto now = std::chrono::system_clock::now();
|
|
auto pid = llvm::sys::Process::getProcessId();
|
|
auto session_dir =
|
|
path::join(cfg.logging_dir, std::format("{:%Y-%m-%d_%H-%M-%S}_{}", now, pid));
|
|
logging::file_logger("master", session_dir, logging::options);
|
|
session_log_dir = session_dir;
|
|
}
|
|
|
|
LOG_INFO("Server ready (stateful={}, stateless={}, idle={}ms)",
|
|
cfg.stateful_worker_count.value,
|
|
cfg.stateless_worker_count.value,
|
|
*cfg.idle_timeout_ms);
|
|
|
|
WorkerPoolOptions pool_opts;
|
|
pool_opts.self_path = self_path;
|
|
pool_opts.stateful_count = cfg.stateful_worker_count;
|
|
pool_opts.stateless_count = cfg.stateless_worker_count;
|
|
pool_opts.worker_memory_limit = cfg.worker_memory_limit;
|
|
pool_opts.log_dir = session_log_dir;
|
|
if(!pool.start(pool_opts)) {
|
|
LOG_ERROR("Failed to start worker pool");
|
|
return;
|
|
}
|
|
|
|
lifecycle = ServerLifecycle::Ready;
|
|
|
|
compiler.on_indexing_needed = [this]() {
|
|
indexer.schedule();
|
|
};
|
|
|
|
loop.schedule(load_workspace());
|
|
});
|
|
|
|
peer.on_request(
|
|
[this](RequestContext& ctx,
|
|
const protocol::ShutdownParams& params) -> RequestResult<protocol::ShutdownParams> {
|
|
lifecycle = ServerLifecycle::ShuttingDown;
|
|
LOG_INFO("Shutdown requested");
|
|
co_return nullptr;
|
|
});
|
|
|
|
peer.on_notification([this](const protocol::ExitParams& params) {
|
|
lifecycle = ServerLifecycle::Exited;
|
|
LOG_INFO("Exit notification received");
|
|
|
|
indexer.save(workspace.config.project.index_dir);
|
|
workspace.save_cache();
|
|
|
|
loop.schedule([this]() -> kota::task<> {
|
|
co_await pool.stop();
|
|
loop.stop();
|
|
}());
|
|
});
|
|
|
|
/// Document lifecycle — handled directly by MasterServer.
|
|
|
|
peer.on_notification([this](const protocol::DidOpenTextDocumentParams& params) {
|
|
if(lifecycle != ServerLifecycle::Ready)
|
|
return;
|
|
|
|
auto path = uri_to_path(params.text_document.uri);
|
|
auto path_id = workspace.path_pool.intern(path);
|
|
|
|
auto [it, inserted] = sessions.try_emplace(path_id);
|
|
auto& session = it->second;
|
|
if(!inserted) {
|
|
// DenseMap tombstone may retain stale data — reset to a fresh Session.
|
|
session = Session{};
|
|
}
|
|
session.path_id = path_id;
|
|
session.version = params.text_document.version;
|
|
session.text = params.text_document.text;
|
|
session.generation++;
|
|
|
|
LOG_DEBUG("didOpen: {} (v{})", path, params.text_document.version);
|
|
});
|
|
|
|
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 = workspace.path_pool.intern(path);
|
|
|
|
auto it = sessions.find(path_id);
|
|
if(it == sessions.end())
|
|
return;
|
|
|
|
auto& session = it->second;
|
|
session.version = params.text_document.version;
|
|
|
|
for(auto& change: params.content_changes) {
|
|
std::visit(
|
|
[&](auto& c) {
|
|
using T = std::remove_cvref_t<decltype(c)>;
|
|
if constexpr(std::is_same_v<T,
|
|
protocol::TextDocumentContentChangeWholeDocument>) {
|
|
session.text = c.text;
|
|
} else {
|
|
auto& range = c.range;
|
|
lsp::PositionMapper mapper(session.text, lsp::PositionEncoding::UTF16);
|
|
auto start = mapper.to_offset(range.start);
|
|
auto end = mapper.to_offset(range.end);
|
|
if(start && end && *start <= *end) {
|
|
session.text.replace(*start, *end - *start, c.text);
|
|
}
|
|
}
|
|
},
|
|
change);
|
|
}
|
|
|
|
session.generation++;
|
|
session.ast_dirty = true;
|
|
|
|
LOG_DEBUG("didChange: path={} version={} gen={}",
|
|
path,
|
|
session.version,
|
|
session.generation);
|
|
|
|
worker::DocumentUpdateParams update;
|
|
update.path = path;
|
|
update.version = session.version;
|
|
pool.notify_stateful(path_id, update);
|
|
});
|
|
|
|
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 = workspace.path_pool.intern(path);
|
|
|
|
workspace.on_file_closed(path_id);
|
|
pool.notify_stateful(path_id, worker::EvictParams{path});
|
|
|
|
// Clear diagnostics for the closed file.
|
|
protocol::PublishDiagnosticsParams diag_params;
|
|
diag_params.uri = params.text_document.uri;
|
|
peer.send_notification(diag_params);
|
|
|
|
sessions.erase(path_id);
|
|
|
|
indexer.enqueue(path_id);
|
|
indexer.schedule();
|
|
|
|
LOG_DEBUG("didClose: {}", path);
|
|
});
|
|
|
|
peer.on_notification([this](const protocol::DidSaveTextDocumentParams& params) {
|
|
if(lifecycle != ServerLifecycle::Ready)
|
|
return;
|
|
|
|
auto path = uri_to_path(params.text_document.uri);
|
|
auto path_id = workspace.path_pool.intern(path);
|
|
|
|
auto dirtied = workspace.on_file_saved(path_id);
|
|
for(auto dirty_id: dirtied) {
|
|
if(auto sit = sessions.find(dirty_id); sit != sessions.end()) {
|
|
sit->second.ast_dirty = true;
|
|
} else {
|
|
indexer.enqueue(dirty_id);
|
|
}
|
|
}
|
|
|
|
// Invalidate header contexts for sessions whose host is this file.
|
|
for(auto& [hdr_id, session]: sessions) {
|
|
if(session.header_context && session.header_context->host_path_id == path_id) {
|
|
session.header_context.reset();
|
|
session.ast_dirty = true;
|
|
}
|
|
}
|
|
|
|
indexer.schedule();
|
|
|
|
LOG_DEBUG("didSave: {}", path);
|
|
});
|
|
|
|
/// Feature requests — stateful forwarding.
|
|
|
|
peer.on_request([this](RequestContext& ctx, const protocol::HoverParams& params) -> RawResult {
|
|
auto path = uri_to_path(params.text_document_position_params.text_document.uri);
|
|
auto path_id = workspace.path_pool.intern(path);
|
|
auto sit = sessions.find(path_id);
|
|
if(sit == sessions.end())
|
|
co_return serde_raw{"null"};
|
|
co_return co_await compiler.forward_query(worker::QueryKind::Hover,
|
|
sit->second,
|
|
params.text_document_position_params.position);
|
|
});
|
|
|
|
peer.on_request([this](RequestContext& ctx,
|
|
const protocol::SemanticTokensParams& params) -> RawResult {
|
|
auto path = uri_to_path(params.text_document.uri);
|
|
auto path_id = workspace.path_pool.intern(path);
|
|
auto sit = sessions.find(path_id);
|
|
if(sit == sessions.end())
|
|
co_return serde_raw{"null"};
|
|
co_return co_await compiler.forward_query(worker::QueryKind::SemanticTokens, sit->second);
|
|
});
|
|
|
|
peer.on_request(
|
|
[this](RequestContext& ctx, const protocol::InlayHintParams& params) -> RawResult {
|
|
auto path = uri_to_path(params.text_document.uri);
|
|
auto path_id = workspace.path_pool.intern(path);
|
|
auto sit = sessions.find(path_id);
|
|
if(sit == sessions.end())
|
|
co_return serde_raw{"null"};
|
|
co_return co_await compiler.forward_query(worker::QueryKind::InlayHints,
|
|
sit->second,
|
|
{},
|
|
params.range);
|
|
});
|
|
|
|
peer.on_request(
|
|
[this](RequestContext& ctx, const protocol::FoldingRangeParams& params) -> RawResult {
|
|
auto path = uri_to_path(params.text_document.uri);
|
|
auto path_id = workspace.path_pool.intern(path);
|
|
auto sit = sessions.find(path_id);
|
|
if(sit == sessions.end())
|
|
co_return serde_raw{"null"};
|
|
co_return co_await compiler.forward_query(worker::QueryKind::FoldingRange, sit->second);
|
|
});
|
|
|
|
peer.on_request([this](RequestContext& ctx,
|
|
const protocol::DocumentSymbolParams& params) -> RawResult {
|
|
auto path = uri_to_path(params.text_document.uri);
|
|
auto path_id = workspace.path_pool.intern(path);
|
|
auto sit = sessions.find(path_id);
|
|
if(sit == sessions.end())
|
|
co_return serde_raw{"null"};
|
|
co_return co_await compiler.forward_query(worker::QueryKind::DocumentSymbol, sit->second);
|
|
});
|
|
|
|
peer.on_request([this](RequestContext& ctx,
|
|
const protocol::DocumentLinkParams& params) -> RawResult {
|
|
auto path = uri_to_path(params.text_document.uri);
|
|
auto path_id = workspace.path_pool.intern(path);
|
|
auto sit = sessions.find(path_id);
|
|
if(sit == sessions.end())
|
|
co_return serde_raw{"null"};
|
|
auto& session = sit->second;
|
|
auto result = co_await compiler.forward_query(worker::QueryKind::DocumentLink, session);
|
|
if(!result.has_value())
|
|
co_return serde_raw{"null"};
|
|
// Merge document links from PCH if available.
|
|
auto& links = result.value();
|
|
// Re-lookup session after co_await since iterators may be invalidated.
|
|
auto sit2 = sessions.find(path_id);
|
|
if(sit2 != sessions.end() && sit2->second.pch_ref) {
|
|
auto pch_it = workspace.pch_cache.find(sit2->second.pch_ref->path_id);
|
|
if(pch_it != workspace.pch_cache.end() && !pch_it->second.document_links_json.empty()) {
|
|
auto& pch_json = pch_it->second.document_links_json;
|
|
// Merge two JSON arrays.
|
|
if(!links.data.empty() && links.data != "null" && links.data.size() > 2) {
|
|
// "[a,b]" + "[c,d]" -> "[a,b,c,d]"
|
|
links.data.pop_back(); // remove trailing ']'
|
|
links.data += ',';
|
|
links.data.append(pch_json.begin() + 1, pch_json.end()); // skip '['
|
|
} else {
|
|
links.data = pch_json;
|
|
}
|
|
}
|
|
}
|
|
co_return std::move(links);
|
|
});
|
|
|
|
peer.on_request(
|
|
[this](RequestContext& ctx, const protocol::CodeActionParams& params) -> RawResult {
|
|
auto path = uri_to_path(params.text_document.uri);
|
|
auto path_id = workspace.path_pool.intern(path);
|
|
auto sit = sessions.find(path_id);
|
|
if(sit == sessions.end())
|
|
co_return serde_raw{"null"};
|
|
co_return co_await compiler.forward_query(worker::QueryKind::CodeAction, sit->second);
|
|
});
|
|
|
|
/// Helper: resolve URI to path, path_id, and Session pointer.
|
|
auto resolve_uri = [this](const std::string& uri) {
|
|
struct Result {
|
|
std::string path;
|
|
std::uint32_t path_id;
|
|
Session* session;
|
|
};
|
|
auto path = uri_to_path(uri);
|
|
auto path_id = workspace.path_pool.intern(path);
|
|
auto sit = sessions.find(path_id);
|
|
Session* session = (sit != sessions.end()) ? &sit->second : nullptr;
|
|
return Result{std::move(path), path_id, session};
|
|
};
|
|
|
|
auto lookup_at = [this, resolve_uri](const std::string& uri, const protocol::Position& pos) {
|
|
auto [path, path_id, session] = resolve_uri(uri);
|
|
return indexer.lookup_symbol(uri, path, pos, session);
|
|
};
|
|
|
|
auto query_at = [this, resolve_uri](const std::string& uri,
|
|
const protocol::Position& pos,
|
|
RelationKind kind) -> std::vector<protocol::Location> {
|
|
auto [path, path_id, session] = resolve_uri(uri);
|
|
return indexer.query_relations(path, pos, kind, session);
|
|
};
|
|
|
|
auto resolve_item =
|
|
[this,
|
|
resolve_uri](const std::string& uri,
|
|
const protocol::Range& range,
|
|
const std::optional<protocol::LSPAny>& data) -> std::optional<SymbolInfo> {
|
|
auto [path, path_id, session] = resolve_uri(uri);
|
|
return indexer.resolve_hierarchy_item(uri, path, range, data, session);
|
|
};
|
|
|
|
/// Feature requests — index-based with AST fallback.
|
|
|
|
peer.on_request([this, query_at](RequestContext& ctx,
|
|
const protocol::DefinitionParams& params) -> RawResult {
|
|
auto& uri = params.text_document_position_params.text_document.uri;
|
|
auto& pos = params.text_document_position_params.position;
|
|
|
|
auto result = query_at(uri, pos, RelationKind::Definition);
|
|
if(!result.empty()) {
|
|
co_return to_raw(result);
|
|
}
|
|
|
|
auto path = uri_to_path(uri);
|
|
auto path_id = workspace.path_pool.intern(path);
|
|
auto sit = sessions.find(path_id);
|
|
if(sit == sessions.end())
|
|
co_return serde_raw{"null"};
|
|
co_return co_await compiler.forward_query(worker::QueryKind::GoToDefinition,
|
|
sit->second,
|
|
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);
|
|
});
|
|
|
|
peer.on_request(
|
|
[this](RequestContext& ctx, const protocol::TypeDefinitionParams& params) -> RawResult {
|
|
co_return serde_raw{"null"};
|
|
});
|
|
|
|
peer.on_request(
|
|
[this](RequestContext& ctx, const protocol::ImplementationParams& params) -> RawResult {
|
|
co_return serde_raw{"null"};
|
|
});
|
|
|
|
peer.on_request(
|
|
[this](RequestContext& ctx, const protocol::DeclarationParams& params) -> RawResult {
|
|
co_return serde_raw{"null"};
|
|
});
|
|
|
|
/// Feature requests — stateless forwarding.
|
|
|
|
peer.on_request([this](RequestContext& ctx,
|
|
const protocol::CompletionParams& params) -> RawResult {
|
|
auto path = uri_to_path(params.text_document_position_params.text_document.uri);
|
|
auto path_id = workspace.path_pool.intern(path);
|
|
auto sit = sessions.find(path_id);
|
|
if(sit == sessions.end())
|
|
co_return serde_raw{"null"};
|
|
co_return co_await compiler.handle_completion(params.text_document_position_params.position,
|
|
sit->second);
|
|
});
|
|
|
|
peer.on_request(
|
|
[this](RequestContext& ctx, const protocol::SignatureHelpParams& params) -> RawResult {
|
|
auto path = uri_to_path(params.text_document_position_params.text_document.uri);
|
|
auto path_id = workspace.path_pool.intern(path);
|
|
auto sit = sessions.find(path_id);
|
|
if(sit == sessions.end())
|
|
co_return serde_raw{"null"};
|
|
co_return co_await compiler.forward_build(worker::BuildKind::SignatureHelp,
|
|
params.text_document_position_params.position,
|
|
sit->second);
|
|
});
|
|
|
|
/// 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<protocol::CallHierarchyItem> 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"};
|
|
auto results = indexer.find_incoming_calls(info->hash);
|
|
if(results.empty())
|
|
co_return serde_raw{"null"};
|
|
co_return to_raw(results);
|
|
});
|
|
|
|
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"};
|
|
auto results = indexer.find_outgoing_calls(info->hash);
|
|
if(results.empty())
|
|
co_return serde_raw{"null"};
|
|
co_return to_raw(results);
|
|
});
|
|
|
|
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;
|
|
|
|
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<protocol::TypeHierarchyItem> items;
|
|
items.push_back(Indexer::build_type_hierarchy_item(*info));
|
|
co_return to_raw(items);
|
|
});
|
|
|
|
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);
|
|
});
|
|
|
|
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);
|
|
});
|
|
|
|
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);
|
|
});
|
|
|
|
/// 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_id = workspace.path_pool.intern(path);
|
|
int offset_val = std::max(0, params.offset.value_or(0));
|
|
constexpr int page_size = 10;
|
|
|
|
ext::QueryContextResult result;
|
|
std::vector<ext::ContextItem> all_items;
|
|
|
|
auto hosts = workspace.dep_graph.find_host_sources(path_id);
|
|
for(auto host_id: hosts) {
|
|
auto host_path = workspace.path_pool.resolve(host_id);
|
|
auto host_cdb = workspace.cdb.lookup(host_path, {.suppress_logging = true});
|
|
if(host_cdb.empty())
|
|
continue;
|
|
auto host_uri_opt = lsp::URI::from_file_path(std::string(host_path));
|
|
if(!host_uri_opt)
|
|
continue;
|
|
ext::ContextItem item;
|
|
item.label = llvm::sys::path::filename(host_path).str();
|
|
item.description = std::string(host_path);
|
|
item.uri = host_uri_opt->str();
|
|
all_items.push_back(std::move(item));
|
|
}
|
|
|
|
if(hosts.empty()) {
|
|
auto entries = workspace.cdb.lookup(path, {.suppress_logging = true});
|
|
for(std::size_t i = 0; i < entries.size(); ++i) {
|
|
auto& cmd = entries[i];
|
|
auto argv = cmd.to_argv();
|
|
std::string desc;
|
|
for(std::size_t j = 0; j < argv.size(); ++j) {
|
|
llvm::StringRef a(argv[j]);
|
|
if(a.starts_with("-D") || a.starts_with("-O") || a.starts_with("-std=") ||
|
|
a.starts_with("-g")) {
|
|
if(!desc.empty())
|
|
desc += ' ';
|
|
desc += argv[j];
|
|
if((a == "-D" || a == "-O") && j + 1 < argv.size()) {
|
|
desc += argv[++j];
|
|
}
|
|
}
|
|
}
|
|
if(desc.empty())
|
|
desc = std::format("config #{}", i);
|
|
|
|
auto uri_opt = lsp::URI::from_file_path(std::string(path));
|
|
if(!uri_opt)
|
|
continue;
|
|
ext::ContextItem item;
|
|
item.label = desc;
|
|
item.description = cmd.resolved.directory.str();
|
|
item.uri = uri_opt->str();
|
|
all_items.push_back(std::move(item));
|
|
}
|
|
}
|
|
|
|
result.total = static_cast<int>(all_items.size());
|
|
int end = std::min(offset_val + page_size, static_cast<int>(all_items.size()));
|
|
for(int i = offset_val; i < end; ++i) {
|
|
result.contexts.push_back(std::move(all_items[i]));
|
|
}
|
|
co_return to_raw(result);
|
|
});
|
|
|
|
peer.on_request(
|
|
"clice/currentContext",
|
|
[this](RequestContext& ctx, const ext::CurrentContextParams& params) -> RawResult {
|
|
auto path = uri_to_path(params.uri);
|
|
auto path_id = workspace.path_pool.intern(path);
|
|
|
|
ext::CurrentContextResult result;
|
|
auto sit = sessions.find(path_id);
|
|
if(sit != sessions.end() && sit->second.active_context) {
|
|
auto ctx_path = workspace.path_pool.resolve(*sit->second.active_context);
|
|
auto ctx_uri_opt = lsp::URI::from_file_path(std::string(ctx_path));
|
|
if(ctx_uri_opt) {
|
|
ext::ContextItem item;
|
|
item.label = llvm::sys::path::filename(ctx_path).str();
|
|
item.description = std::string(ctx_path);
|
|
item.uri = ctx_uri_opt->str();
|
|
result.context = std::move(item);
|
|
}
|
|
}
|
|
co_return to_raw(result);
|
|
});
|
|
|
|
peer.on_request(
|
|
"clice/switchContext",
|
|
[this](RequestContext& ctx, const ext::SwitchContextParams& params) -> RawResult {
|
|
auto path = uri_to_path(params.uri);
|
|
auto path_id = workspace.path_pool.intern(path);
|
|
auto context_path = uri_to_path(params.context_uri);
|
|
auto context_path_id = workspace.path_pool.intern(context_path);
|
|
|
|
ext::SwitchContextResult result;
|
|
|
|
auto context_cdb = workspace.cdb.lookup(context_path, {.suppress_logging = true});
|
|
if(context_cdb.empty()) {
|
|
result.success = false;
|
|
co_return to_raw(result);
|
|
}
|
|
|
|
auto sit = sessions.find(path_id);
|
|
if(sit == sessions.end()) {
|
|
result.success = false;
|
|
co_return to_raw(result);
|
|
}
|
|
|
|
sit->second.active_context = context_path_id;
|
|
sit->second.header_context.reset();
|
|
sit->second.pch_ref.reset();
|
|
sit->second.ast_deps.reset();
|
|
sit->second.ast_dirty = true;
|
|
|
|
result.success = true;
|
|
co_return to_raw(result);
|
|
});
|
|
}
|
|
|
|
} // namespace clice
|