Skip to content

Commit ce08a25

Browse files
16bit-ykikoclaude
andcommitted
feat: add header context support for IDE features in header files
Header files without compile_commands.json entries can now get hover, completion, diagnostics, and other IDE features by inferring compilation context from a host source file that includes them. Implementation: - Add reverse include map to DependencyGraph (header → includer mapping) - Add find_host_sources() BFS to find source files that include a header - Add find_include_chain() to trace the include path from host to target - Add resolve_header_context() to generate preamble files with #line directives preserving correct source locations - Modify fill_compile_args() to fall back to header context when CDB lookup fails: uses host's compile args + -include <preamble> - Cache preamble files in <cache_dir>/header_context/<hash>.h - Invalidate header contexts when host file is saved Tests: - test_hover_in_header_file: hover on function in header returns result - test_diagnostics_in_header_file: valid header gets zero diagnostics Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e239b0d commit ce08a25

File tree

8 files changed

+426
-7
lines changed

8 files changed

+426
-7
lines changed

src/server/master_server.cpp

Lines changed: 197 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,9 @@ et::task<> MasterServer::load_workspace() {
411411

412412
auto report = scan_dependency_graph(cdb, path_pool, dependency_graph);
413413

414+
// Build reverse include map so headers can find their host source files.
415+
dependency_graph.build_reverse_map();
416+
414417
auto unresolved = report.includes_found - report.includes_resolved;
415418
double accuracy =
416419
report.includes_found > 0
@@ -562,20 +565,191 @@ et::task<> MasterServer::load_workspace() {
562565
LOG_INFO("CompileGraph initialized with {} module(s)", path_to_module.size());
563566
}
564567

568+
std::optional<HeaderFileContext>
569+
MasterServer::resolve_header_context(std::uint32_t header_path_id) {
570+
// Find source files that transitively include this header.
571+
auto hosts = dependency_graph.find_host_sources(header_path_id);
572+
if(hosts.empty()) {
573+
LOG_DEBUG("resolve_header_context: no host sources for path_id={}", header_path_id);
574+
return std::nullopt;
575+
}
576+
577+
// Pick the first available host that has a CDB entry.
578+
std::uint32_t host_path_id = 0;
579+
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;
591+
}
592+
593+
if(chain.empty()) {
594+
LOG_DEBUG("resolve_header_context: no usable host with include chain for path_id={}",
595+
header_path_id);
596+
return std::nullopt;
597+
}
598+
599+
// Build preamble text: for each file in the chain except the last (target),
600+
// append all content up to (but not including) the line that includes the
601+
// next file in the chain.
602+
std::string preamble;
603+
for(std::size_t i = 0; i + 1 < chain.size(); ++i) {
604+
auto cur_id = chain[i];
605+
auto next_id = chain[i + 1];
606+
607+
auto cur_path = path_pool.resolve(cur_id);
608+
auto next_path = path_pool.resolve(next_id);
609+
auto next_filename = llvm::sys::path::filename(next_path);
610+
611+
// Prefer in-memory document text over disk content.
612+
std::string content;
613+
if(auto doc_it = documents.find(cur_id); doc_it != documents.end()) {
614+
content = doc_it->second.text;
615+
} else {
616+
auto buf = llvm::MemoryBuffer::getFile(cur_path);
617+
if(!buf) {
618+
LOG_WARN("resolve_header_context: cannot read {}", cur_path);
619+
return std::nullopt;
620+
}
621+
content = (*buf)->getBuffer().str();
622+
}
623+
624+
// Scan line by line for the #include that brings in next_filename.
625+
llvm::StringRef content_ref(content);
626+
std::size_t line_start = 0;
627+
std::size_t include_line_start = std::string::npos;
628+
while(line_start <= content_ref.size()) {
629+
auto newline_pos = content_ref.find('\n', line_start);
630+
auto line_end =
631+
(newline_pos == llvm::StringRef::npos) ? content_ref.size() : newline_pos;
632+
auto line = content_ref.slice(line_start, line_end).trim();
633+
634+
if(line.starts_with("#include") || line.starts_with("# include")) {
635+
// Check if this line references the next file in the chain.
636+
if(line.contains(next_filename)) {
637+
include_line_start = line_start;
638+
break;
639+
}
640+
}
641+
642+
line_start =
643+
(newline_pos == llvm::StringRef::npos) ? content_ref.size() + 1 : newline_pos + 1;
644+
}
645+
646+
// Emit a #line marker then all content before the include line.
647+
preamble += std::format("#line 1 \"{}\"\n", cur_path.str());
648+
if(include_line_start != std::string::npos) {
649+
preamble += content_ref.substr(0, include_line_start).str();
650+
} else {
651+
// No matching include line found — emit the whole file to be safe.
652+
LOG_DEBUG("resolve_header_context: include line for {} not found in {}, emitting full",
653+
next_filename,
654+
cur_path);
655+
preamble += content;
656+
}
657+
}
658+
659+
// Hash the preamble and write to cache directory.
660+
auto preamble_hash = llvm::xxh3_64bits(llvm::StringRef(preamble));
661+
auto preamble_filename = std::format("{:016x}.h", preamble_hash);
662+
auto preamble_dir = path::join(config.cache_dir, "header_context");
663+
auto preamble_path = path::join(preamble_dir, preamble_filename);
664+
665+
if(!llvm::sys::fs::exists(preamble_path)) {
666+
auto ec = llvm::sys::fs::create_directories(preamble_dir);
667+
if(ec) {
668+
LOG_WARN("resolve_header_context: cannot create dir {}: {}",
669+
preamble_dir,
670+
ec.message());
671+
return std::nullopt;
672+
}
673+
if(auto result = fs::write(preamble_path, preamble); !result) {
674+
LOG_WARN("resolve_header_context: cannot write preamble {}: {}",
675+
preamble_path,
676+
result.error().message());
677+
return std::nullopt;
678+
}
679+
LOG_INFO("resolve_header_context: wrote preamble {} for header path_id={}",
680+
preamble_path,
681+
header_path_id);
682+
}
683+
684+
return HeaderFileContext{host_path_id, preamble_path, preamble_hash};
685+
}
686+
565687
bool MasterServer::fill_compile_args(llvm::StringRef path,
566688
std::string& directory,
567689
std::vector<std::string>& arguments) {
568690
auto results = cdb.lookup(path, {.query_toolchain = true});
569-
if(results.empty()) {
570-
LOG_WARN("No CDB entry for {}", path);
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);
697+
}
698+
return true;
699+
}
700+
701+
// No direct CDB entry — try to compile the header in context of a host source.
702+
auto path_id = path_pool.intern(path);
703+
704+
// Use cached context if available; otherwise resolve.
705+
const HeaderFileContext* ctx_ptr = nullptr;
706+
auto ctx_it = header_file_contexts.find(path_id);
707+
if(ctx_it != header_file_contexts.end()) {
708+
ctx_ptr = &ctx_it->second;
709+
} else {
710+
auto resolved = resolve_header_context(path_id);
711+
if(!resolved) {
712+
LOG_WARN("No CDB entry and no header context for {}", path);
713+
return false;
714+
}
715+
header_file_contexts[path_id] = std::move(*resolved);
716+
ctx_ptr = &header_file_contexts[path_id];
717+
}
718+
719+
auto host_path = path_pool.resolve(ctx_ptr->host_path_id);
720+
auto host_results = cdb.lookup(host_path, {.query_toolchain = true});
721+
if(host_results.empty()) {
722+
LOG_WARN("fill_compile_args: host {} has no CDB entry", host_path);
571723
return false;
572724
}
573-
auto& ctx = results.front();
574-
directory = ctx.directory.str();
725+
726+
auto& host_ctx = host_results.front();
727+
directory = host_ctx.directory.str();
575728
arguments.clear();
576-
for(auto* arg: ctx.arguments) {
577-
arguments.emplace_back(arg);
578-
}
729+
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.
733+
auto num_args = host_ctx.arguments.size();
734+
std::size_t copy_count = num_args;
735+
if(copy_count > 0) {
736+
llvm::StringRef last(host_ctx.arguments[copy_count - 1]);
737+
if(!last.starts_with("-"))
738+
copy_count -= 1;
739+
}
740+
for(std::size_t i = 0; i < copy_count; ++i) {
741+
arguments.emplace_back(host_ctx.arguments[i]);
742+
}
743+
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");
748+
749+
LOG_INFO("fill_compile_args: using header context for {} (host={}, preamble={})",
750+
path,
751+
host_path,
752+
ctx_ptr->preamble_path);
579753
return true;
580754
}
581755

@@ -2018,6 +2192,22 @@ void MasterServer::register_handlers() {
20182192
}
20192193
}
20202194

2195+
// Invalidate header contexts whose host is the saved file.
2196+
// Collect entries to erase to avoid modifying the map while iterating.
2197+
llvm::SmallVector<std::uint32_t, 4> stale_headers;
2198+
for(auto& [hdr_id, hdr_ctx]: header_file_contexts) {
2199+
if(hdr_ctx.host_path_id == path_id)
2200+
stale_headers.push_back(hdr_id);
2201+
}
2202+
for(auto hdr_id: stale_headers) {
2203+
header_file_contexts.erase(hdr_id);
2204+
auto doc_it = documents.find(hdr_id);
2205+
if(doc_it != documents.end()) {
2206+
doc_it->second.ast_dirty = true;
2207+
LOG_DEBUG("didSave: invalidated header context for path_id={}", hdr_id);
2208+
}
2209+
}
2210+
20212211
// Trigger background indexing after save.
20222212
schedule_indexing();
20232213

src/server/master_server.h

Lines changed: 15 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,9 @@ 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+
219229
// === Helpers ===
220230

221231
/// Convert a file:// URI to a local file path.
@@ -240,6 +250,11 @@ class MasterServer {
240250
std::string& directory,
241251
std::vector<std::string>& arguments);
242252

253+
/// Generate a preamble file for compiling a header in context.
254+
/// The preamble contains all code from the host source (and intermediate
255+
/// headers) that comes BEFORE the #include of the target header.
256+
std::optional<HeaderFileContext> resolve_header_context(std::uint32_t header_path_id);
257+
243258
/// Build or reuse PCH for a source file. Returns true if PCH is available.
244259
et::task<bool> ensure_pch(std::uint32_t path_id,
245260
llvm::StringRef path,

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
// ============================================================================

0 commit comments

Comments
 (0)