Files
clice/src/feature/code_completion.cpp
ykiko 6d3b6acc82 feat: initial CompileGraph integration into MasterServer (#376)
## 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>
2026-03-29 20:05:58 +08:00

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