Skip to content

Commit de034b3

Browse files
16bit-ykikoclaude
andcommitted
feat: add queryContext/currentContext/switchContext protocol extensions
- clice/queryContext: returns all possible compilation contexts for a file (header files → host source files, source files → CDB entries), paginated - clice/currentContext: returns the currently active context for a file - clice/switchContext: switches the active context, invalidates caches, triggers recompile - Non self-contained header test data (utils.h depends on host's <vector>) - 5 integration tests covering the full query→switch→verify flow - CDB lookup helper (command.h/cpp) for querying multiple compilation entries Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ce08a25 commit de034b3

File tree

10 files changed

+414
-64
lines changed

10 files changed

+414
-64
lines changed

src/command/command.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -650,6 +650,11 @@ std::uint32_t CompilationDatabase::intern_path(llvm::StringRef path) {
650650
return paths.intern(path);
651651
}
652652

653+
bool CompilationDatabase::has_entry(llvm::StringRef file) {
654+
auto path_id = paths.intern(file);
655+
return !find_entries(path_id).empty();
656+
}
657+
653658
llvm::ArrayRef<CompilationEntry> CompilationDatabase::get_entries() const {
654659
return entries;
655660
}

src/command/command.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,10 @@ class CompilationDatabase {
191191
/// Intern a file path and return its path_id.
192192
std::uint32_t intern_path(llvm::StringRef path);
193193

194+
/// Check if a file has an explicit entry in the compilation database
195+
/// (as opposed to a synthesized default).
196+
bool has_entry(llvm::StringRef file);
197+
194198
/// All compilation entries (sorted by path_id).
195199
llvm::ArrayRef<CompilationEntry> get_entries() const;
196200

src/server/master_server.cpp

Lines changed: 169 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -574,20 +574,37 @@ std::optional<HeaderFileContext>
574574
return std::nullopt;
575575
}
576576

577-
// Pick the first available host that has a CDB entry.
577+
// If there's an active context override, prefer that host.
578578
std::uint32_t host_path_id = 0;
579579
std::vector<std::uint32_t> chain;
580-
for(auto candidate: hosts) {
581-
auto candidate_path = path_pool.resolve(candidate);
582-
auto results = cdb.lookup(candidate_path, {.suppress_logging = true});
583-
if(results.empty())
584-
continue;
585-
auto c = dependency_graph.find_include_chain(candidate, header_path_id);
586-
if(c.empty())
587-
continue;
588-
host_path_id = candidate;
589-
chain = std::move(c);
590-
break;
580+
auto active_it = active_contexts.find(header_path_id);
581+
if(active_it != active_contexts.end()) {
582+
auto preferred = active_it->second;
583+
auto preferred_path = path_pool.resolve(preferred);
584+
auto results = cdb.lookup(preferred_path, {.suppress_logging = true});
585+
if(!results.empty()) {
586+
auto c = dependency_graph.find_include_chain(preferred, header_path_id);
587+
if(!c.empty()) {
588+
host_path_id = preferred;
589+
chain = std::move(c);
590+
}
591+
}
592+
}
593+
594+
// Fall back to the first available host that has a CDB entry.
595+
if(chain.empty()) {
596+
for(auto candidate: hosts) {
597+
auto candidate_path = path_pool.resolve(candidate);
598+
auto results = cdb.lookup(candidate_path, {.suppress_logging = true});
599+
if(results.empty())
600+
continue;
601+
auto c = dependency_graph.find_include_chain(candidate, header_path_id);
602+
if(c.empty())
603+
continue;
604+
host_path_id = candidate;
605+
chain = std::move(c);
606+
break;
607+
}
591608
}
592609

593610
if(chain.empty()) {
@@ -687,26 +704,38 @@ std::optional<HeaderFileContext>
687704
bool MasterServer::fill_compile_args(llvm::StringRef path,
688705
std::string& directory,
689706
std::vector<std::string>& arguments) {
690-
auto results = cdb.lookup(path, {.query_toolchain = true});
691-
if(!results.empty()) {
692-
auto& ctx = results.front();
693-
directory = ctx.directory.str();
694-
arguments.clear();
695-
for(auto* arg: ctx.arguments) {
696-
arguments.emplace_back(arg);
707+
if(cdb.has_entry(path)) {
708+
auto results = cdb.lookup(path, {.query_toolchain = true});
709+
if(!results.empty()) {
710+
auto& ctx = results.front();
711+
directory = ctx.directory.str();
712+
arguments.clear();
713+
for(auto* arg: ctx.arguments) {
714+
arguments.emplace_back(arg);
715+
}
716+
return true;
697717
}
698-
return true;
699718
}
700719

701720
// No direct CDB entry — try to compile the header in context of a host source.
702721
auto path_id = path_pool.intern(path);
703722

704723
// Use cached context if available; otherwise resolve.
724+
// If an active context override exists, invalidate cache if it points to
725+
// a different host so we re-resolve with the correct one.
705726
const HeaderFileContext* ctx_ptr = nullptr;
706727
auto ctx_it = header_file_contexts.find(path_id);
728+
auto active_it = active_contexts.find(path_id);
707729
if(ctx_it != header_file_contexts.end()) {
708-
ctx_ptr = &ctx_it->second;
709-
} else {
730+
// Check if the cached context matches the active context override.
731+
if(active_it != active_contexts.end() && ctx_it->second.host_path_id != active_it->second) {
732+
header_file_contexts.erase(ctx_it);
733+
ctx_it = header_file_contexts.end();
734+
} else {
735+
ctx_ptr = &ctx_it->second;
736+
}
737+
}
738+
if(!ctx_ptr) {
710739
auto resolved = resolve_header_context(path_id);
711740
if(!resolved) {
712741
LOG_WARN("No CDB entry and no header context for {}", path);
@@ -727,9 +756,8 @@ bool MasterServer::fill_compile_args(llvm::StringRef path,
727756
directory = host_ctx.directory.str();
728757
arguments.clear();
729758

730-
// Copy host arguments, skipping the last element if it looks like a source
731-
// file path (i.e. not a flag). The header path is used as the main file by
732-
// the caller via CompileParams::path.
759+
// Copy host arguments, replacing the source file path (last non-flag arg)
760+
// with the header file path, so the compiler processes the header in context.
733761
auto num_args = host_ctx.arguments.size();
734762
std::size_t copy_count = num_args;
735763
if(copy_count > 0) {
@@ -740,11 +768,19 @@ bool MasterServer::fill_compile_args(llvm::StringRef path,
740768
for(std::size_t i = 0; i < copy_count; ++i) {
741769
arguments.emplace_back(host_ctx.arguments[i]);
742770
}
771+
// Append the header file path as the source file.
772+
arguments.emplace_back(path);
743773

744-
// Inject the preamble before the header so the compiler sees all context
745-
// code that normally precedes this header in the host translation unit.
746-
arguments.insert(arguments.begin() + 1, ctx_ptr->preamble_path);
747-
arguments.insert(arguments.begin() + 1, "-include");
774+
// Inject the preamble so the compiler sees all context code that normally
775+
// precedes this header in the host translation unit.
776+
// For cc1 args (["clang++", "-cc1", ...]), insert after "-cc1" at position 2.
777+
// For driver args, insert after the driver binary at position 1.
778+
std::size_t inject_pos = 1;
779+
if(arguments.size() >= 2 && arguments[1] == "-cc1") {
780+
inject_pos = 2;
781+
}
782+
arguments.insert(arguments.begin() + inject_pos, ctx_ptr->preamble_path);
783+
arguments.insert(arguments.begin() + inject_pos, "-include");
748784

749785
LOG_INFO("fill_compile_args: using header context for {} (host={}, preamble={})",
750786
path,
@@ -932,7 +968,9 @@ et::task<bool> MasterServer::ensure_deps(std::uint32_t path_id,
932968
et::task<bool> MasterServer::ensure_compiled(std::uint32_t path_id) {
933969
auto it = documents.find(path_id);
934970
if(it == documents.end()) {
935-
LOG_DEBUG("ensure_compiled: doc not found for path_id={}", path_id);
971+
LOG_WARN("ensure_compiled: doc not found for path_id={} path={}",
972+
path_id,
973+
path_pool.resolve(path_id));
936974
co_return false;
937975
}
938976

@@ -2705,6 +2743,107 @@ void MasterServer::register_handlers() {
27052743
co_return serde_raw{"null"};
27062744
co_return to_raw(results);
27072745
});
2746+
2747+
// === clice/ Extension Commands ===
2748+
2749+
// --- clice/queryContext ---
2750+
peer.on_request(
2751+
"clice/queryContext",
2752+
[this](RequestContext& ctx, const ext::QueryContextParams& params) -> RawResult {
2753+
auto path = uri_to_path(params.uri);
2754+
auto path_id = path_pool.intern(path);
2755+
int offset_val = params.offset.value_or(0);
2756+
constexpr int page_size = 10;
2757+
2758+
ext::QueryContextResult result;
2759+
2760+
// Find source files that transitively include this file.
2761+
// For source files (roots in the include graph) this returns empty.
2762+
auto hosts = dependency_graph.find_host_sources(path_id);
2763+
std::vector<ext::ContextItem> all_items;
2764+
for(auto host_id: hosts) {
2765+
auto host_path = path_pool.resolve(host_id);
2766+
auto host_cdb = cdb.lookup(host_path, {.suppress_logging = true});
2767+
if(host_cdb.empty())
2768+
continue;
2769+
auto host_uri_opt = lsp::URI::from_file_path(std::string(host_path));
2770+
if(!host_uri_opt)
2771+
continue;
2772+
ext::ContextItem item;
2773+
item.label = llvm::sys::path::filename(host_path).str();
2774+
item.description = std::string(host_path);
2775+
item.uri = host_uri_opt->str();
2776+
all_items.push_back(std::move(item));
2777+
}
2778+
2779+
result.total = static_cast<int>(all_items.size());
2780+
int end = std::min(offset_val + page_size, static_cast<int>(all_items.size()));
2781+
for(int i = offset_val; i < end; ++i) {
2782+
result.contexts.push_back(std::move(all_items[i]));
2783+
}
2784+
co_return to_raw(result);
2785+
});
2786+
2787+
// --- clice/currentContext ---
2788+
peer.on_request(
2789+
"clice/currentContext",
2790+
[this](RequestContext& ctx, const ext::CurrentContextParams& params) -> RawResult {
2791+
auto path = uri_to_path(params.uri);
2792+
auto path_id = path_pool.intern(path);
2793+
2794+
ext::CurrentContextResult result;
2795+
2796+
auto it = active_contexts.find(path_id);
2797+
if(it != active_contexts.end()) {
2798+
auto ctx_path = path_pool.resolve(it->second);
2799+
auto ctx_uri_opt = lsp::URI::from_file_path(std::string(ctx_path));
2800+
if(ctx_uri_opt) {
2801+
ext::ContextItem item;
2802+
item.label = llvm::sys::path::filename(ctx_path).str();
2803+
item.description = std::string(ctx_path);
2804+
item.uri = ctx_uri_opt->str();
2805+
result.context = std::move(item);
2806+
}
2807+
}
2808+
co_return to_raw(result);
2809+
});
2810+
2811+
// --- clice/switchContext ---
2812+
peer.on_request(
2813+
"clice/switchContext",
2814+
[this](RequestContext& ctx, const ext::SwitchContextParams& params) -> RawResult {
2815+
auto path = uri_to_path(params.uri);
2816+
auto path_id = path_pool.intern(path);
2817+
auto context_path = uri_to_path(params.context_uri);
2818+
auto context_path_id = path_pool.intern(context_path);
2819+
2820+
ext::SwitchContextResult result;
2821+
2822+
// Verify the context file has a CDB entry.
2823+
auto context_cdb = cdb.lookup(context_path, {.suppress_logging = true});
2824+
if(context_cdb.empty()) {
2825+
result.success = false;
2826+
co_return to_raw(result);
2827+
}
2828+
2829+
// Set active context and invalidate cached header context so
2830+
// resolve_header_context will pick the new host on next compile.
2831+
active_contexts[path_id] = context_path_id;
2832+
header_file_contexts.erase(path_id);
2833+
2834+
// Also invalidate the PCH for the old context (if any) so it
2835+
// gets rebuilt with the new host's preamble.
2836+
pch_states.erase(path_id);
2837+
2838+
// Mark the document as dirty so it gets recompiled.
2839+
auto doc_it = documents.find(path_id);
2840+
if(doc_it != documents.end()) {
2841+
doc_it->second.ast_dirty = true;
2842+
}
2843+
2844+
result.success = true;
2845+
co_return to_raw(result);
2846+
});
27082847
}
27092848

27102849
} // namespace clice

src/server/master_server.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,11 @@ class MasterServer {
226226
/// Header context cache: header_path_id -> context
227227
llvm::DenseMap<std::uint32_t, HeaderFileContext> header_file_contexts;
228228

229+
/// Active compilation context overrides: path_id -> context_path_id.
230+
/// When a file has an entry here, it is compiled using the context file's
231+
/// compile command (e.g. a header compiled through a specific source file).
232+
llvm::DenseMap<std::uint32_t, std::uint32_t> active_contexts;
233+
229234
// === Helpers ===
230235

231236
/// Convert a file:// URI to a local file path.

src/server/protocol.h

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,45 @@ struct EvictedParams {
155155

156156
} // namespace clice::worker
157157

158+
// === clice/ LSP Extension Types ===
159+
160+
namespace clice::ext {
161+
162+
struct ContextItem {
163+
std::string label;
164+
std::string description;
165+
std::string uri;
166+
};
167+
168+
struct QueryContextParams {
169+
std::string uri;
170+
std::optional<int> offset;
171+
};
172+
173+
struct QueryContextResult {
174+
std::vector<ContextItem> contexts;
175+
int total;
176+
};
177+
178+
struct CurrentContextParams {
179+
std::string uri;
180+
};
181+
182+
struct CurrentContextResult {
183+
std::optional<ContextItem> context;
184+
};
185+
186+
struct SwitchContextParams {
187+
std::string uri;
188+
std::string context_uri;
189+
};
190+
191+
struct SwitchContextResult {
192+
bool success;
193+
};
194+
195+
} // namespace clice::ext
196+
158197
namespace eventide::ipc::protocol {
159198

160199
// === Stateful Requests ===

tests/conftest.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,27 @@ def test_data_dir() -> Path:
220220
]
221221
cdb_path.write_text(json.dumps(cdb, indent=2))
222222

223+
# Generate compile_commands.json for header_context (always regenerate
224+
# because it contains absolute paths).
225+
hc_dir = data_dir / "header_context"
226+
hc_main = hc_dir / "main.cpp"
227+
hc_cdb = hc_dir / "compile_commands.json"
228+
if hc_main.exists():
229+
cdb = [
230+
{
231+
"directory": hc_dir.as_posix(),
232+
"file": hc_main.as_posix(),
233+
"arguments": [
234+
"clang++",
235+
"-std=c++17",
236+
f"-I{hc_dir.as_posix()}",
237+
"-fsyntax-only",
238+
hc_main.as_posix(),
239+
],
240+
}
241+
]
242+
hc_cdb.write_text(json.dumps(cdb, indent=2))
243+
223244
# Generate compile_commands.json for include_completion
224245
ic_dir = data_dir / "include_completion"
225246
ic_main = ic_dir / "main.cpp"
Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,13 @@
1-
[{"directory": ".", "command": "clang++ -std=c++17 -I. main.cpp", "file": "main.cpp"}]
1+
[
2+
{
3+
"directory": "/home/ykiko/C++/clice/tests/data/header_context",
4+
"file": "/home/ykiko/C++/clice/tests/data/header_context/main.cpp",
5+
"arguments": [
6+
"clang++",
7+
"-std=c++17",
8+
"-I/home/ykiko/C++/clice/tests/data/header_context",
9+
"-fsyntax-only",
10+
"/home/ykiko/C++/clice/tests/data/header_context/main.cpp"
11+
]
12+
}
13+
]

tests/data/header_context/main.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
#include "utils.h"
44

55
int main() {
6-
std::vector<int> v = {1, 2, 3};
7-
int sum = add(v[0], v[1]);
8-
return sum;
6+
auto v = make_range(5);
7+
int s = sum(v);
8+
return s;
99
}

0 commit comments

Comments
 (0)