Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
382 changes: 373 additions & 9 deletions src/server/master_server.cpp

Large diffs are not rendered by default.

20 changes: 20 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,11 @@ class MasterServer {
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
52 changes: 52 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,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 Down
10 changes: 10 additions & 0 deletions tests/data/header_context/inner.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#pragma once

// Non self-contained: uses std::vector from the include chain.
inline int inner_sum(const std::vector<int>& v) {
int total = 0;
for(int x: v) {
total += x;
}
return total;
}
9 changes: 9 additions & 0 deletions tests/data/header_context/main.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#include <vector>

#include "utils.h"

int main() {
auto v = make_range(5);
int s = sum(v);
return s;
}
21 changes: 21 additions & 0 deletions tests/data/header_context/utils.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Non self-contained header: uses std::vector without including it.
// Depends on the including source file to provide the <vector> include.
#pragma once

#include "inner.h"

inline std::vector<int> make_range(int n) {
std::vector<int> result;
for(int i = 0; i < n; ++i) {
result.push_back(i);
}
return result;
}

inline int sum(const std::vector<int>& v) {
int total = 0;
for(int x: v) {
total += x;
}
return total;
}
15 changes: 15 additions & 0 deletions tests/data/multi_context/main.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#ifdef CONFIG_A
int config_value() {
return 1;
}
#endif

#ifdef CONFIG_B
int config_value() {
return 2;
}
#endif

int main() {
return config_value();
}
Loading
Loading