Skip to content

Commit 836f415

Browse files
16bit-ykikoclaude
andauthored
feat: header context protocol — queryContext, currentContext, switchContext (#398)
## Summary Add three LSP protocol extensions that allow users to manage compilation contexts for header files and source files with multiple CDB entries. ### Protocol extensions (`protocol.h`) | Command | Purpose | |---------|---------| | `clice/queryContext` | List all possible contexts for a file. Headers → host source files; sources → CDB entries. Paginated (10 per page, `offset` param). | | `clice/currentContext` | Query the active context override for a file (null if default). | | `clice/switchContext` | Set the active context, invalidate caches, trigger recompilation. | ### Header context resolution (`master_server.cpp`, `dependency_graph.cpp`) - `find_host_sources()`: BFS the reverse include graph to find source files that transitively include a header - `find_include_chain()`: BFS the forward include graph to find the shortest include chain from host to header - `resolve_header_context()`: walks the include chain, extracts content before each `#include` directive, concatenates with `#line` markers into a preamble file (hash-addressed under `.clice/header_context/`) - `fill_header_context_args()`: uses the host source's CDB entry, replaces source path with header path, injects `-include preamble.h` ### Compilation flow - Default: headers compile as standalone files (no context) - After `switchContext`: `fill_compile_args` checks `active_contexts` first → uses host's CDB entry + preamble injection - Fallback: if no CDB entry and no active context, auto-resolves via `resolve_header_context` - `#include` directive matching uses precise filename extraction from `"..."` / `<...>`, not substring matching ### Source file multiple contexts (`multi_context` workspace) - `queryContext` on a source file returns all CDB entries with distinguishing labels (extracted from `-D`, `-O`, `-std=` flags) ### Test data - `header_context/`: non-self-contained 3-level chain (`main.cpp` → `utils.h` → `inner.h`), `types.h` provides `Point` struct - `multi_context/`: single source with two CDB entries (`-DCONFIG_A`, `-DCONFIG_B`) ### Tests (9 integration tests) - queryContext returns host sources for headers - queryContext returns CDB entries for source files - currentContext defaults to null - switchContext sets active context, currentContext reflects it - Full flow: open → query → switch → hover works in non-self-contained header - Deep nested: switchContext + hover on `inner.h` (3 levels deep) - Multiple CDB entries: queryContext returns both CONFIG_A and CONFIG_B ## Test plan - [x] Unit tests: 465 passed - [x] Integration tests: 113 passed (9 new header context tests) - [x] Smoke test: 1/1 passed - [ ] Manual VSCode testing 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 1627b96 commit 836f415

File tree

14 files changed

+986
-7
lines changed

14 files changed

+986
-7
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: 391 additions & 7 deletions
Large diffs are not rendered by default.

src/server/master_server.h

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,13 @@ struct DocumentState {
5555
std::shared_ptr<PendingCompile> compiling;
5656
};
5757

58+
/// Context for compiling a header file that lacks its own CDB entry.
59+
struct HeaderFileContext {
60+
std::uint32_t host_path_id; // Source file acting as host
61+
std::string preamble_path; // Path to generated preamble file on disk
62+
std::uint64_t preamble_hash; // Hash of preamble content for staleness
63+
};
64+
5865
/// Two-layer staleness snapshot for compilation artifacts (PCH, AST, etc.).
5966
///
6067
/// Layer 1 (fast): compare each file's current mtime against build_at.
@@ -216,6 +223,14 @@ class MasterServer {
216223
/// Per-file dependency snapshots from last successful AST compilation.
217224
llvm::DenseMap<std::uint32_t, DepsSnapshot> ast_deps;
218225

226+
/// Header context cache: header_path_id -> context
227+
llvm::DenseMap<std::uint32_t, HeaderFileContext> header_file_contexts;
228+
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+
219234
// === Helpers ===
220235

221236
/// Convert a file:// URI to a local file path.
@@ -240,6 +255,18 @@ class MasterServer {
240255
std::string& directory,
241256
std::vector<std::string>& arguments);
242257

258+
/// Fill compile arguments using header context (host source's CDB entry
259+
/// with file path replaced and preamble injected).
260+
bool fill_header_context_args(llvm::StringRef path,
261+
std::uint32_t path_id,
262+
std::string& directory,
263+
std::vector<std::string>& arguments);
264+
265+
/// Generate a preamble file for compiling a header in context.
266+
/// The preamble contains all code from the host source (and intermediate
267+
/// headers) that comes BEFORE the #include of the target header.
268+
std::optional<HeaderFileContext> resolve_header_context(std::uint32_t header_path_id);
269+
243270
/// Build or reuse PCH for a source file. Returns true if PCH is available.
244271
et::task<bool> ensure_pch(std::uint32_t path_id,
245272
llvm::StringRef 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 ===

src/syntax/dependency_graph.cpp

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#include "syntax/dependency_graph.h"
22

3+
#include <algorithm>
34
#include <chrono>
45

56
#include "command/toolchain.h"
@@ -101,6 +102,109 @@ std::size_t DependencyGraph::edge_count() const {
101102
return count;
102103
}
103104

105+
void DependencyGraph::build_reverse_map() {
106+
reverse_includes_.clear();
107+
for(auto& [key, ids]: includes) {
108+
for(auto flagged_id: ids) {
109+
auto included_id = flagged_id & PATH_ID_MASK;
110+
auto& vec = reverse_includes_[included_id];
111+
if(llvm::find(vec, key.path_id) == vec.end()) {
112+
vec.push_back(key.path_id);
113+
}
114+
}
115+
}
116+
}
117+
118+
llvm::ArrayRef<std::uint32_t> DependencyGraph::get_includers(std::uint32_t path_id) const {
119+
auto it = reverse_includes_.find(path_id);
120+
if(it != reverse_includes_.end()) {
121+
return it->second;
122+
}
123+
return {};
124+
}
125+
126+
llvm::SmallVector<std::uint32_t, 4>
127+
DependencyGraph::find_host_sources(std::uint32_t header_path_id) const {
128+
llvm::SmallVector<std::uint32_t, 4> result;
129+
llvm::DenseSet<std::uint32_t> visited;
130+
llvm::SmallVector<std::uint32_t, 16> queue;
131+
132+
queue.push_back(header_path_id);
133+
visited.insert(header_path_id);
134+
135+
while(!queue.empty()) {
136+
auto current = queue.pop_back_val();
137+
auto includers = get_includers(current);
138+
if(includers.empty()) {
139+
// No includers: this is a root (source file).
140+
// Exclude the starting header itself.
141+
if(current != header_path_id) {
142+
result.push_back(current);
143+
}
144+
continue;
145+
}
146+
for(auto includer: includers) {
147+
if(visited.insert(includer).second) {
148+
queue.push_back(includer);
149+
}
150+
}
151+
}
152+
153+
return result;
154+
}
155+
156+
std::vector<std::uint32_t> DependencyGraph::find_include_chain(std::uint32_t host_path_id,
157+
std::uint32_t target_path_id) const {
158+
if(host_path_id == target_path_id) {
159+
return {host_path_id};
160+
}
161+
162+
// BFS: predecessor map for path reconstruction.
163+
llvm::DenseMap<std::uint32_t, std::uint32_t> prev;
164+
llvm::SmallVector<std::uint32_t, 16> queue;
165+
166+
prev[host_path_id] = host_path_id;
167+
queue.push_back(host_path_id);
168+
169+
bool found = false;
170+
while(!queue.empty() && !found) {
171+
llvm::SmallVector<std::uint32_t, 16> next_queue;
172+
for(auto current: queue) {
173+
auto includes_union = get_all_includes(current);
174+
for(auto flagged_id: includes_union) {
175+
auto child = flagged_id & PATH_ID_MASK;
176+
if(prev.find(child) == prev.end()) {
177+
prev[child] = current;
178+
if(child == target_path_id) {
179+
found = true;
180+
break;
181+
}
182+
next_queue.push_back(child);
183+
}
184+
}
185+
if(found) {
186+
break;
187+
}
188+
}
189+
queue = std::move(next_queue);
190+
}
191+
192+
if(!found) {
193+
return {};
194+
}
195+
196+
// Reconstruct path from target back to host.
197+
std::vector<std::uint32_t> chain;
198+
auto node = target_path_id;
199+
while(node != host_path_id) {
200+
chain.push_back(node);
201+
node = prev[node];
202+
}
203+
chain.push_back(host_path_id);
204+
std::reverse(chain.begin(), chain.end());
205+
return chain;
206+
}
207+
104208
// ============================================================================
105209
// Wavefront BFS scanner — async implementation
106210
// ============================================================================

src/syntax/dependency_graph.h

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,24 @@ class DependencyGraph {
7070
/// Get the union of includes across all configs for a file.
7171
llvm::SmallVector<std::uint32_t> get_all_includes(std::uint32_t path_id) const;
7272

73+
/// Build the reverse include map from the forward includes.
74+
/// Must be called after all set_includes() calls are complete.
75+
void build_reverse_map();
76+
77+
/// Get the direct includers of a file (files that directly include path_id).
78+
llvm::ArrayRef<std::uint32_t> get_includers(std::uint32_t path_id) const;
79+
80+
/// BFS upward through reverse edges to find all source files (roots)
81+
/// that transitively include header_path_id.
82+
/// Source files are those that have no includers (i.e. they are roots in the graph).
83+
llvm::SmallVector<std::uint32_t, 4> find_host_sources(std::uint32_t header_path_id) const;
84+
85+
/// BFS forward through include edges to find the shortest include chain
86+
/// from host_path_id to target_path_id.
87+
/// Returns [host, intermediate1, ..., target], or empty if no path exists.
88+
std::vector<std::uint32_t> find_include_chain(std::uint32_t host_path_id,
89+
std::uint32_t target_path_id) const;
90+
7391
/// Number of files with include entries.
7492
std::size_t file_count() const;
7593

@@ -94,6 +112,10 @@ class DependencyGraph {
94112

95113
/// Track which files have any include entries (for file_count).
96114
llvm::DenseMap<std::uint32_t, llvm::SmallVector<std::uint32_t>> file_configs;
115+
116+
/// Reverse include map: PathID -> list of PathIDs that directly include it.
117+
/// Populated by build_reverse_map().
118+
llvm::DenseMap<std::uint32_t, llvm::SmallVector<std::uint32_t, 4>> reverse_includes_;
97119
};
98120

99121
/// A (file, search-config) pair used to track per-wave work items.

tests/conftest.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,10 @@ def open(self, filepath: Path, version: int = 0) -> tuple[str, str]:
140140
)
141141
return self._normalize_uri(wire_uri), content
142142

143+
def path_to_uri(self, filepath: Path) -> str:
144+
"""Convert a file path to a normalized URI without opening it."""
145+
return self._normalize_uri(filepath.as_uri())
146+
143147
async def wait_diagnostics(self, uri: str, timeout: float = 30.0) -> None:
144148
"""Wait for diagnostics on the given URI."""
145149
uri = self._normalize_uri(uri)
@@ -220,6 +224,58 @@ def test_data_dir() -> Path:
220224
]
221225
cdb_path.write_text(json.dumps(cdb, indent=2))
222226

227+
# Generate compile_commands.json for header_context (always regenerate
228+
# because it contains absolute paths).
229+
hc_dir = data_dir / "header_context"
230+
hc_main = hc_dir / "main.cpp"
231+
hc_cdb = hc_dir / "compile_commands.json"
232+
if hc_main.exists():
233+
cdb = [
234+
{
235+
"directory": hc_dir.as_posix(),
236+
"file": hc_main.as_posix(),
237+
"arguments": [
238+
"clang++",
239+
"-std=c++17",
240+
f"-I{hc_dir.as_posix()}",
241+
"-fsyntax-only",
242+
hc_main.as_posix(),
243+
],
244+
}
245+
]
246+
hc_cdb.write_text(json.dumps(cdb, indent=2))
247+
248+
# Generate compile_commands.json for multi_context (same file, two configs)
249+
mc_dir = data_dir / "multi_context"
250+
mc_main = mc_dir / "main.cpp"
251+
mc_cdb = mc_dir / "compile_commands.json"
252+
if mc_main.exists():
253+
cdb = [
254+
{
255+
"directory": mc_dir.as_posix(),
256+
"file": mc_main.as_posix(),
257+
"arguments": [
258+
"clang++",
259+
"-std=c++17",
260+
"-DCONFIG_A",
261+
"-fsyntax-only",
262+
mc_main.as_posix(),
263+
],
264+
},
265+
{
266+
"directory": mc_dir.as_posix(),
267+
"file": mc_main.as_posix(),
268+
"arguments": [
269+
"clang++",
270+
"-std=c++17",
271+
"-DCONFIG_B",
272+
"-fsyntax-only",
273+
mc_main.as_posix(),
274+
],
275+
},
276+
]
277+
mc_cdb.write_text(json.dumps(cdb, indent=2))
278+
223279
# Generate compile_commands.json for include_completion
224280
ic_dir = data_dir / "include_completion"
225281
ic_main = ic_dir / "main.cpp"
@@ -240,6 +296,31 @@ def test_data_dir() -> Path:
240296
]
241297
ic_cdb.write_text(json.dumps(cdb, indent=2))
242298

299+
# Generate compile_commands.json for pch_test (always regenerate for
300+
# absolute paths).
301+
pt_dir = data_dir / "pch_test"
302+
pt_cdb = pt_dir / "compile_commands.json"
303+
for src_name in ["main.cpp", "no_includes.cpp"]:
304+
src = pt_dir / src_name
305+
if not src.exists():
306+
continue
307+
if src_name == "main.cpp":
308+
entries = []
309+
entries.append(
310+
{
311+
"directory": pt_dir.as_posix(),
312+
"file": src.as_posix(),
313+
"arguments": [
314+
"clang++",
315+
"-std=c++17",
316+
"-fsyntax-only",
317+
src.as_posix(),
318+
],
319+
}
320+
)
321+
if pt_dir.exists():
322+
pt_cdb.write_text(json.dumps(entries, indent=2))
323+
243324
return data_dir
244325

245326

tests/data/header_context/inner.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#pragma once
2+
3+
// Non self-contained: uses Point from the include chain.
4+
inline Point inner_origin() {
5+
return Point{0, 0};
6+
}

tests/data/header_context/main.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#include "types.h"
2+
#include "utils.h"
3+
4+
int main() {
5+
Point p{3, 4};
6+
int d = calc(p);
7+
return d;
8+
}

0 commit comments

Comments
 (0)