## Summary Initial integration of `CompileGraph` (#375) into `MasterServer`, enabling basic end-to-end C++20 module support: on-demand PCM building, dependency-ordered compilation, cascade invalidation on save, and diagnostic integration. This is a **first-pass implementation** — the core pipeline works, but there are known areas for follow-up: - PCM files go to system temp dir instead of `.clice/cache/`; no disk cleanup on invalidation - `run_build_drain` scans imports itself rather than delegating fully to CompileGraph - No incremental/partial rebuild (full PCM rebuild on any change) - Cycle detection is tested at unit level but integration-level coverage is minimal ## Changes ### Module dependency compilation (`master_server.cpp`) Before sending a file to the stateful worker, `run_build_drain` now: 1. Scans imports via `scan_precise()` to discover module dependencies 2. Compiles each dep through `compile_graph->compile()`, which recursively builds transitive PCMs 3. Handles implementation units — `module M;` implicitly needs the interface PCM 4. Passes all built PCMs to the stateful worker, excluding the file's own PCM 5. Skips compile on dep failure and resets `build_running` / `drain_scheduled` 6. Re-lookups iterators after `co_await` to avoid use-after-invalidation ### Cascade invalidation (`didSave` / `didClose`) - `didSave`: calls `compile_graph->update()` to mark transitive dependents dirty, removes stale PCM paths, schedules rebuilds for open dirtied files - `didClose`: cancels in-flight compilations for the closed file ### Other fixes in this PR - Debounce timers switched to `shared_ptr` to prevent use-after-free when `didClose` destroys the timer mid-wait - `fill_compile_args` returns `bool`; callers handle empty CDB gracefully - Adapt all `PositionMapper` call sites to the new `optional` return API from eventide ## Test plan - [x] 25 C++ unit tests for CompileGraph (cycles, partial failure, cancel, update, empty graph) - [x] 24 C++ integration tests with real clang PCM compilation - [x] 3 worker-level module tests (BuildPCM, PCM-dependent compile, multi-module) - [x] 26 Python LSP integration tests (single module through circular deps, hover, error diagnostics) - [x] 371 unit tests + 54 integration tests pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
297 lines
9.8 KiB
C++
297 lines
9.8 KiB
C++
#include <algorithm>
|
|
#include <cassert>
|
|
#include <cstdint>
|
|
#include <format>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <unordered_map>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "feature/feature.h"
|
|
#include "semantic/ast_utility.h"
|
|
#include "support/fuzzy_matcher.h"
|
|
|
|
#include "llvm/ADT/SmallString.h"
|
|
#include "llvm/Support/raw_ostream.h"
|
|
#include "clang/AST/DeclCXX.h"
|
|
#include "clang/AST/DeclTemplate.h"
|
|
#include "clang/Basic/CharInfo.h"
|
|
#include "clang/Sema/CodeCompleteConsumer.h"
|
|
#include "clang/Sema/Sema.h"
|
|
|
|
namespace clice::feature {
|
|
|
|
namespace {
|
|
|
|
struct CompletionPrefix {
|
|
LocalSourceRange range;
|
|
llvm::StringRef spelling;
|
|
|
|
static auto from(llvm::StringRef content, std::uint32_t offset) -> CompletionPrefix {
|
|
assert(offset <= content.size());
|
|
|
|
auto start = offset;
|
|
while(start > 0 && clang::isAsciiIdentifierContinue(content[start - 1])) {
|
|
--start;
|
|
}
|
|
|
|
auto end = offset;
|
|
while(end < content.size() && clang::isAsciiIdentifierContinue(content[end])) {
|
|
++end;
|
|
}
|
|
|
|
return CompletionPrefix{
|
|
.range = LocalSourceRange(start, end),
|
|
.spelling = content.substr(start, offset - start),
|
|
};
|
|
}
|
|
};
|
|
|
|
auto completion_kind(const clang::NamedDecl* decl) -> protocol::CompletionItemKind {
|
|
if(llvm::isa<clang::NamespaceDecl, clang::NamespaceAliasDecl>(decl)) {
|
|
return protocol::CompletionItemKind::Module;
|
|
}
|
|
|
|
if(llvm::isa<clang::FunctionDecl, clang::FunctionTemplateDecl>(decl)) {
|
|
return protocol::CompletionItemKind::Function;
|
|
}
|
|
|
|
if(llvm::isa<clang::CXXMethodDecl,
|
|
clang::CXXConversionDecl,
|
|
clang::CXXDestructorDecl,
|
|
clang::CXXDeductionGuideDecl>(decl)) {
|
|
return protocol::CompletionItemKind::Method;
|
|
}
|
|
|
|
if(llvm::isa<clang::CXXConstructorDecl>(decl)) {
|
|
return protocol::CompletionItemKind::Constructor;
|
|
}
|
|
|
|
if(llvm::isa<clang::FieldDecl, clang::IndirectFieldDecl>(decl)) {
|
|
return protocol::CompletionItemKind::Field;
|
|
}
|
|
|
|
if(llvm::isa<clang::VarDecl,
|
|
clang::ParmVarDecl,
|
|
clang::ImplicitParamDecl,
|
|
clang::BindingDecl,
|
|
clang::NonTypeTemplateParmDecl>(decl)) {
|
|
return protocol::CompletionItemKind::Variable;
|
|
}
|
|
|
|
if(llvm::isa<clang::LabelDecl>(decl)) {
|
|
return protocol::CompletionItemKind::Variable;
|
|
}
|
|
|
|
if(llvm::isa<clang::EnumDecl>(decl)) {
|
|
return protocol::CompletionItemKind::Enum;
|
|
}
|
|
|
|
if(llvm::isa<clang::EnumConstantDecl>(decl)) {
|
|
return protocol::CompletionItemKind::EnumMember;
|
|
}
|
|
|
|
if(llvm::isa<clang::RecordDecl,
|
|
clang::ClassTemplateDecl,
|
|
clang::ClassTemplateSpecializationDecl>(decl)) {
|
|
return protocol::CompletionItemKind::Class;
|
|
}
|
|
|
|
if(llvm::isa<clang::TypedefNameDecl,
|
|
clang::TemplateTypeParmDecl,
|
|
clang::TemplateTemplateParmDecl,
|
|
clang::TypeAliasTemplateDecl,
|
|
clang::ConceptDecl>(decl)) {
|
|
return protocol::CompletionItemKind::TypeParameter;
|
|
}
|
|
|
|
return protocol::CompletionItemKind::Text;
|
|
}
|
|
|
|
struct OverloadItem {
|
|
protocol::CompletionItem item;
|
|
float score = 0.0F;
|
|
std::uint32_t count = 0;
|
|
};
|
|
|
|
class CodeCompletionCollector final : public clang::CodeCompleteConsumer {
|
|
public:
|
|
CodeCompletionCollector(std::uint32_t offset,
|
|
PositionEncoding encoding,
|
|
std::vector<protocol::CompletionItem>& output,
|
|
const CodeCompletionOptions& options) :
|
|
clang::CodeCompleteConsumer({}), offset(offset), encoding(encoding), output(output),
|
|
options(options), info(std::make_shared<clang::GlobalCodeCompletionAllocator>()) {}
|
|
|
|
clang::CodeCompletionAllocator& getAllocator() final {
|
|
return info.getAllocator();
|
|
}
|
|
|
|
clang::CodeCompletionTUInfo& getCodeCompletionTUInfo() final {
|
|
return info;
|
|
}
|
|
|
|
void ProcessCodeCompleteResults(clang::Sema& sema,
|
|
clang::CodeCompletionContext context,
|
|
clang::CodeCompletionResult* candidates,
|
|
unsigned candidate_count) final {
|
|
if(context.getKind() == clang::CodeCompletionContext::CCC_Recovery ||
|
|
candidate_count == 0) {
|
|
return;
|
|
}
|
|
|
|
auto& source_manager = sema.getSourceManager();
|
|
auto content = source_manager.getBufferData(source_manager.getMainFileID());
|
|
auto prefix = CompletionPrefix::from(content, offset);
|
|
FuzzyMatcher matcher(prefix.spelling);
|
|
|
|
PositionMapper converter(content, encoding);
|
|
auto replace_range = protocol::Range{
|
|
.start = *converter.to_position(prefix.range.begin),
|
|
.end = *converter.to_position(prefix.range.end),
|
|
};
|
|
|
|
std::vector<protocol::CompletionItem> collected;
|
|
collected.reserve(candidate_count);
|
|
|
|
std::vector<OverloadItem> overloads;
|
|
overloads.reserve(candidate_count);
|
|
std::unordered_map<std::string, std::size_t> overload_index;
|
|
|
|
auto build_item =
|
|
[&](llvm::StringRef label, protocol::CompletionItemKind kind, llvm::StringRef insert) {
|
|
protocol::CompletionItem item{
|
|
.label = label.str(),
|
|
};
|
|
item.kind = kind;
|
|
|
|
protocol::TextEdit edit{
|
|
.range = replace_range,
|
|
.new_text = insert.empty() ? label.str() : insert.str(),
|
|
};
|
|
item.text_edit = std::move(edit);
|
|
return item;
|
|
};
|
|
|
|
auto try_add = [&](llvm::StringRef label,
|
|
protocol::CompletionItemKind kind,
|
|
llvm::StringRef insert_text,
|
|
llvm::StringRef overload_key) {
|
|
if(label.empty()) {
|
|
return;
|
|
}
|
|
|
|
auto score = matcher.match(label);
|
|
if(!score.has_value()) {
|
|
return;
|
|
}
|
|
|
|
if(!overload_key.empty()) {
|
|
auto [it, inserted] =
|
|
overload_index.try_emplace(overload_key.str(), overloads.size());
|
|
if(inserted) {
|
|
auto item = build_item(label, kind, insert_text);
|
|
item.sort_text = std::format("{}", *score);
|
|
overloads.push_back({
|
|
.item = std::move(item),
|
|
.score = *score,
|
|
.count = 1,
|
|
});
|
|
} else {
|
|
auto& existing = overloads[it->second];
|
|
existing.count += 1;
|
|
if(*score > existing.score) {
|
|
existing.score = *score;
|
|
existing.item.sort_text = std::format("{}", *score);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
auto item = build_item(label, kind, insert_text);
|
|
item.sort_text = std::format("{}", *score);
|
|
collected.push_back(std::move(item));
|
|
};
|
|
|
|
for(auto& candidate: llvm::make_range(candidates, candidates + candidate_count)) {
|
|
switch(candidate.Kind) {
|
|
case clang::CodeCompletionResult::RK_Keyword:
|
|
try_add(candidate.Keyword,
|
|
protocol::CompletionItemKind::Keyword,
|
|
candidate.Keyword,
|
|
"");
|
|
break;
|
|
|
|
case clang::CodeCompletionResult::RK_Pattern: {
|
|
auto text = candidate.Pattern->getAllTypedText();
|
|
try_add(text, protocol::CompletionItemKind::Snippet, text, "");
|
|
break;
|
|
}
|
|
|
|
case clang::CodeCompletionResult::RK_Macro:
|
|
try_add(candidate.Macro->getName(),
|
|
protocol::CompletionItemKind::Unit,
|
|
candidate.Macro->getName(),
|
|
"");
|
|
break;
|
|
|
|
case clang::CodeCompletionResult::RK_Declaration: {
|
|
auto* declaration = candidate.Declaration;
|
|
if(!declaration) {
|
|
break;
|
|
}
|
|
|
|
auto label = ast::name_of(declaration);
|
|
auto kind = completion_kind(declaration);
|
|
|
|
llvm::SmallString<256> qualified_name;
|
|
if(options.bundle_overloads && kind == protocol::CompletionItemKind::Function) {
|
|
llvm::raw_svector_ostream stream(qualified_name);
|
|
declaration->printQualifiedName(stream);
|
|
}
|
|
|
|
try_add(label, kind, label, qualified_name.str());
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
for(auto& entry: overloads) {
|
|
if(entry.count > 1) {
|
|
entry.item.detail = "(...)";
|
|
}
|
|
collected.push_back(std::move(entry.item));
|
|
}
|
|
|
|
output.clear();
|
|
output.swap(collected);
|
|
}
|
|
|
|
private:
|
|
std::uint32_t offset;
|
|
PositionEncoding encoding;
|
|
std::vector<protocol::CompletionItem>& output;
|
|
const CodeCompletionOptions& options;
|
|
clang::CodeCompletionTUInfo info;
|
|
};
|
|
|
|
} // namespace
|
|
|
|
auto code_complete(CompilationParams& params,
|
|
const CodeCompletionOptions& options,
|
|
PositionEncoding encoding) -> std::vector<protocol::CompletionItem> {
|
|
std::vector<protocol::CompletionItem> items;
|
|
|
|
auto& [file, offset] = params.completion;
|
|
(void)file;
|
|
|
|
auto* consumer = new CodeCompletionCollector(offset, encoding, items, options);
|
|
auto unit = complete(params, consumer);
|
|
(void)unit;
|
|
|
|
return items;
|
|
}
|
|
|
|
} // namespace clice::feature
|