Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/command/command.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -650,6 +650,11 @@ std::uint32_t CompilationDatabase::intern_path(llvm::StringRef path) {
return paths.intern(path);
}

bool CompilationDatabase::has_entry(llvm::StringRef file) {
auto path_id = paths.intern(file);
return !find_entries(path_id).empty();
}

llvm::ArrayRef<CompilationEntry> CompilationDatabase::get_entries() const {
return entries;
}
Expand Down
4 changes: 4 additions & 0 deletions src/command/command.h
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,10 @@ class CompilationDatabase {
/// Intern a file path and return its path_id.
std::uint32_t intern_path(llvm::StringRef path);

/// Check if a file has an explicit entry in the compilation database
/// (as opposed to a synthesized default).
bool has_entry(llvm::StringRef file);

/// All compilation entries (sorted by path_id).
llvm::ArrayRef<CompilationEntry> get_entries() const;

Expand Down
398 changes: 391 additions & 7 deletions src/server/master_server.cpp

Large diffs are not rendered by default.

27 changes: 27 additions & 0 deletions src/server/master_server.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ struct DocumentState {
std::shared_ptr<PendingCompile> 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.
Expand Down Expand Up @@ -216,6 +223,14 @@ class MasterServer {
/// Per-file dependency snapshots from last successful AST compilation.
llvm::DenseMap<std::uint32_t, DepsSnapshot> ast_deps;

/// Header context cache: header_path_id -> context
llvm::DenseMap<std::uint32_t, HeaderFileContext> 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<std::uint32_t, std::uint32_t> active_contexts;

// === Helpers ===

/// Convert a file:// URI to a local file path.
Expand All @@ -240,6 +255,18 @@ class MasterServer {
std::string& directory,
std::vector<std::string>& 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<std::string>& 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<HeaderFileContext> 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<bool> ensure_pch(std::uint32_t path_id,
llvm::StringRef path,
Expand Down
39 changes: 39 additions & 0 deletions src/server/protocol.h
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,45 @@ struct EvictedParams {

} // namespace clice::worker

// === clice/ LSP Extension Types ===

namespace clice::ext {

struct ContextItem {
std::string label;
std::string description;
std::string uri;
};

struct QueryContextParams {
std::string uri;
std::optional<int> offset;
};

struct QueryContextResult {
std::vector<ContextItem> contexts;
int total;
};

struct CurrentContextParams {
std::string uri;
};

struct CurrentContextResult {
std::optional<ContextItem> context;
};

struct SwitchContextParams {
std::string uri;
std::string context_uri;
};

struct SwitchContextResult {
bool success;
};

} // namespace clice::ext

namespace eventide::ipc::protocol {

// === Stateful Requests ===
Expand Down
104 changes: 104 additions & 0 deletions src/syntax/dependency_graph.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "syntax/dependency_graph.h"

#include <algorithm>
#include <chrono>

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

void DependencyGraph::build_reverse_map() {
reverse_includes_.clear();
for(auto& [key, ids]: includes) {
for(auto flagged_id: ids) {
auto included_id = flagged_id & PATH_ID_MASK;
auto& vec = reverse_includes_[included_id];
if(llvm::find(vec, key.path_id) == vec.end()) {
vec.push_back(key.path_id);
}
}
}
}

llvm::ArrayRef<std::uint32_t> DependencyGraph::get_includers(std::uint32_t path_id) const {
auto it = reverse_includes_.find(path_id);
if(it != reverse_includes_.end()) {
return it->second;
}
return {};
}

llvm::SmallVector<std::uint32_t, 4>
DependencyGraph::find_host_sources(std::uint32_t header_path_id) const {
llvm::SmallVector<std::uint32_t, 4> result;
llvm::DenseSet<std::uint32_t> visited;
llvm::SmallVector<std::uint32_t, 16> queue;

queue.push_back(header_path_id);
visited.insert(header_path_id);

while(!queue.empty()) {
auto current = queue.pop_back_val();
auto includers = get_includers(current);
if(includers.empty()) {
// No includers: this is a root (source file).
// Exclude the starting header itself.
if(current != header_path_id) {
result.push_back(current);
}
continue;
}
for(auto includer: includers) {
if(visited.insert(includer).second) {
queue.push_back(includer);
}
}
}

return result;
}

std::vector<std::uint32_t> DependencyGraph::find_include_chain(std::uint32_t host_path_id,
std::uint32_t target_path_id) const {
if(host_path_id == target_path_id) {
return {host_path_id};
}

// BFS: predecessor map for path reconstruction.
llvm::DenseMap<std::uint32_t, std::uint32_t> prev;
llvm::SmallVector<std::uint32_t, 16> queue;

prev[host_path_id] = host_path_id;
queue.push_back(host_path_id);

bool found = false;
while(!queue.empty() && !found) {
llvm::SmallVector<std::uint32_t, 16> next_queue;
for(auto current: queue) {
auto includes_union = get_all_includes(current);
for(auto flagged_id: includes_union) {
auto child = flagged_id & PATH_ID_MASK;
if(prev.find(child) == prev.end()) {
prev[child] = current;
if(child == target_path_id) {
found = true;
break;
}
next_queue.push_back(child);
}
}
if(found) {
break;
}
}
queue = std::move(next_queue);
}

if(!found) {
return {};
}

// Reconstruct path from target back to host.
std::vector<std::uint32_t> chain;
auto node = target_path_id;
while(node != host_path_id) {
chain.push_back(node);
node = prev[node];
}
chain.push_back(host_path_id);
std::reverse(chain.begin(), chain.end());
return chain;
}

// ============================================================================
// Wavefront BFS scanner — async implementation
// ============================================================================
Expand Down
22 changes: 22 additions & 0 deletions src/syntax/dependency_graph.h
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,24 @@ class DependencyGraph {
/// Get the union of includes across all configs for a file.
llvm::SmallVector<std::uint32_t> get_all_includes(std::uint32_t path_id) const;

/// Build the reverse include map from the forward includes.
/// Must be called after all set_includes() calls are complete.
void build_reverse_map();

/// Get the direct includers of a file (files that directly include path_id).
llvm::ArrayRef<std::uint32_t> get_includers(std::uint32_t path_id) const;

/// BFS upward through reverse edges to find all source files (roots)
/// that transitively include header_path_id.
/// Source files are those that have no includers (i.e. they are roots in the graph).
llvm::SmallVector<std::uint32_t, 4> find_host_sources(std::uint32_t header_path_id) const;

/// BFS forward through include edges to find the shortest include chain
/// from host_path_id to target_path_id.
/// Returns [host, intermediate1, ..., target], or empty if no path exists.
std::vector<std::uint32_t> find_include_chain(std::uint32_t host_path_id,
std::uint32_t target_path_id) const;

/// Number of files with include entries.
std::size_t file_count() const;

Expand All @@ -94,6 +112,10 @@ class DependencyGraph {

/// Track which files have any include entries (for file_count).
llvm::DenseMap<std::uint32_t, llvm::SmallVector<std::uint32_t>> file_configs;

/// Reverse include map: PathID -> list of PathIDs that directly include it.
/// Populated by build_reverse_map().
llvm::DenseMap<std::uint32_t, llvm::SmallVector<std::uint32_t, 4>> reverse_includes_;
};

/// A (file, search-config) pair used to track per-wave work items.
Expand Down
81 changes: 81 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,10 @@ def open(self, filepath: Path, version: int = 0) -> tuple[str, str]:
)
return self._normalize_uri(wire_uri), content

def path_to_uri(self, filepath: Path) -> str:
"""Convert a file path to a normalized URI without opening it."""
return self._normalize_uri(filepath.as_uri())

async def wait_diagnostics(self, uri: str, timeout: float = 30.0) -> None:
"""Wait for diagnostics on the given URI."""
uri = self._normalize_uri(uri)
Expand Down Expand Up @@ -220,6 +224,58 @@ def test_data_dir() -> Path:
]
cdb_path.write_text(json.dumps(cdb, indent=2))

# Generate compile_commands.json for header_context (always regenerate
# because it contains absolute paths).
hc_dir = data_dir / "header_context"
hc_main = hc_dir / "main.cpp"
hc_cdb = hc_dir / "compile_commands.json"
if hc_main.exists():
cdb = [
{
"directory": hc_dir.as_posix(),
"file": hc_main.as_posix(),
"arguments": [
"clang++",
"-std=c++17",
f"-I{hc_dir.as_posix()}",
"-fsyntax-only",
hc_main.as_posix(),
],
}
]
hc_cdb.write_text(json.dumps(cdb, indent=2))

# Generate compile_commands.json for multi_context (same file, two configs)
mc_dir = data_dir / "multi_context"
mc_main = mc_dir / "main.cpp"
mc_cdb = mc_dir / "compile_commands.json"
if mc_main.exists():
cdb = [
{
"directory": mc_dir.as_posix(),
"file": mc_main.as_posix(),
"arguments": [
"clang++",
"-std=c++17",
"-DCONFIG_A",
"-fsyntax-only",
mc_main.as_posix(),
],
},
{
"directory": mc_dir.as_posix(),
"file": mc_main.as_posix(),
"arguments": [
"clang++",
"-std=c++17",
"-DCONFIG_B",
"-fsyntax-only",
mc_main.as_posix(),
],
},
]
mc_cdb.write_text(json.dumps(cdb, indent=2))

# Generate compile_commands.json for include_completion
ic_dir = data_dir / "include_completion"
ic_main = ic_dir / "main.cpp"
Expand All @@ -240,6 +296,31 @@ def test_data_dir() -> Path:
]
ic_cdb.write_text(json.dumps(cdb, indent=2))

# Generate compile_commands.json for pch_test (always regenerate for
# absolute paths).
pt_dir = data_dir / "pch_test"
pt_cdb = pt_dir / "compile_commands.json"
for src_name in ["main.cpp", "no_includes.cpp"]:
src = pt_dir / src_name
if not src.exists():
continue
if src_name == "main.cpp":
entries = []
entries.append(
{
"directory": pt_dir.as_posix(),
"file": src.as_posix(),
"arguments": [
"clang++",
"-std=c++17",
"-fsyntax-only",
src.as_posix(),
],
}
)
if pt_dir.exists():
pt_cdb.write_text(json.dumps(entries, indent=2))

return data_dir


Expand Down
6 changes: 6 additions & 0 deletions tests/data/header_context/inner.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#pragma once

// Non self-contained: uses Point from the include chain.
inline Point inner_origin() {
return Point{0, 0};
}
8 changes: 8 additions & 0 deletions tests/data/header_context/main.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#include "types.h"
#include "utils.h"

int main() {
Point p{3, 4};
int d = calc(p);
return d;
}
Loading
Loading