## Summary Introduces a two-layer state model that cleanly separates disk-based project state from per-open-file editing state, and redistributes responsibilities across server components so each has a single, clear role. ## New types **Workspace** — all persistent, project-wide shared state: - CompilationDatabase, PathPool, DependencyGraph, CompileGraph - path_to_module mapping, PCH cache, PCM cache, PCM paths - ProjectIndex, MergedIndex shards - CliceConfig - Methods: on_file_saved(), on_file_closed(), load/save/cleanup_cache(), build_module_map(), fill_pcm_deps(), cancel_all() **Session** — volatile per-open-file editing state: - text, version, generation, ast_dirty - pch_ref (references Workspace.pch_cache), ast_deps, header_context - file_index (OpenFileIndex for unsaved buffer) - path_id member for self-identification ## Component responsibilities after refactor | Component | Role | Owns state? | |-----------|------|-------------| | **Workspace** | Disk truth + shared caches | Yes (all project state) | | **Session** | One open file editing state | Yes (per-file only) | | **Compiler** | Compilation pipeline, worker communication | No (references only) | | **Indexer** | Index queries + background indexing scheduling | Scheduling state only | | **MasterServer** | LSP protocol dispatch + lifecycle coordination | sessions map | ## What moved where **Into Workspace** (from Compiler/MasterServer): - PCH/PCM cache management (load_cache, save_cache, cleanup_cache) - Module map building (build_module_map, fill_pcm_deps) - File lifecycle hooks (on_file_saved, on_file_closed) - cancel_all, OpenFileIndex/MergedIndexShard type definitions **Into Session** (from Compiler documents map): - Document text, version, generation, ast_dirty - PCH reference, dependency snapshot, header context **Into Indexer** (from MasterServer): - Background indexing queue, scheduling state, idle timer - schedule(), enqueue(), run_background_indexing() **Into syntax/completion.h** (from Compiler): - detect_completion_context() — pure text parsing - complete_module_import() — prefix match on module names - complete_include_path() — directory listing against search paths **Inlined into MasterServer** (from Compiler): - didOpen/didChange/didClose/didSave handlers - switchContext/currentContext - publish_diagnostics/clear_diagnostics **Deleted from Compiler** (9 methods): - open_document, apply_changes, close_document, on_save - switch_context, get_active_context, invalidate_host_contexts - on_file_closed, on_file_saved, complete_include, complete_import ## Tests - 481 tests pass (465 existing + 16 new completion tests) - New: tests/unit/syntax/completion_tests.cpp ## Diff stats 15 files changed, +1857, -1555 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes * **New Features** * Enhanced completion support for include paths and module imports with improved context detection. * Added background indexing system for automatic project symbol indexing. * **Bug Fixes** * Improved reliability of document change tracking and compilation state management. * Better handling of header file compilation contexts. * **Tests** * Added unit tests for completion context detection and module/include path completion. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
122 lines
3.9 KiB
C++
122 lines
3.9 KiB
C++
#include "syntax/completion.h"
|
|
|
|
#include "syntax/include_resolver.h"
|
|
|
|
#include "llvm/ADT/SmallString.h"
|
|
#include "llvm/ADT/StringSet.h"
|
|
#include "llvm/Support/FileSystem.h"
|
|
#include "llvm/Support/Path.h"
|
|
|
|
namespace clice {
|
|
|
|
PreambleCompletionContext detect_completion_context(llvm::StringRef text, std::uint32_t offset) {
|
|
auto line_start = text.rfind('\n', offset > 0 ? offset - 1 : 0);
|
|
line_start = (line_start == llvm::StringRef::npos) ? 0 : line_start + 1;
|
|
|
|
auto line = text.slice(line_start, offset);
|
|
auto trimmed = line.ltrim();
|
|
|
|
if(trimmed.starts_with("#")) {
|
|
auto directive = trimmed.drop_front(1).ltrim();
|
|
if(directive.consume_front("include")) {
|
|
directive = directive.ltrim();
|
|
if(directive.consume_front("\"")) {
|
|
return {CompletionContext::IncludeQuoted, directive.str()};
|
|
}
|
|
if(directive.consume_front("<")) {
|
|
return {CompletionContext::IncludeAngled, directive.str()};
|
|
}
|
|
}
|
|
return {};
|
|
}
|
|
|
|
auto import_check = trimmed;
|
|
if(import_check.consume_front("export") && !import_check.empty() &&
|
|
!std::isalnum(import_check[0])) {
|
|
import_check = import_check.ltrim();
|
|
}
|
|
if(import_check.consume_front("import") &&
|
|
(import_check.empty() || !std::isalnum(import_check[0]))) {
|
|
import_check = import_check.ltrim();
|
|
|
|
auto line_end = text.find('\n', offset);
|
|
if(line_end == llvm::StringRef::npos)
|
|
line_end = text.size();
|
|
auto rest_of_line = text.slice(line_start, line_end);
|
|
if(!rest_of_line.contains(';')) {
|
|
return {CompletionContext::Import, import_check.str()};
|
|
}
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
std::vector<std::string>
|
|
complete_module_import(const llvm::DenseMap<std::uint32_t, std::string>& modules,
|
|
llvm::StringRef prefix) {
|
|
std::vector<std::string> results;
|
|
for(auto& [path_id, module_name]: modules) {
|
|
if(llvm::StringRef(module_name).starts_with(prefix)) {
|
|
results.push_back(module_name);
|
|
}
|
|
}
|
|
return results;
|
|
}
|
|
|
|
std::vector<IncludeCandidate> complete_include_path(const ResolvedSearchConfig& resolved,
|
|
llvm::StringRef prefix,
|
|
bool angled,
|
|
DirListingCache& dir_cache) {
|
|
llvm::StringRef dir_prefix;
|
|
llvm::StringRef file_prefix = prefix;
|
|
auto slash_pos = prefix.rfind('/');
|
|
if(slash_pos != llvm::StringRef::npos) {
|
|
dir_prefix = prefix.slice(0, slash_pos);
|
|
file_prefix = prefix.slice(slash_pos + 1, llvm::StringRef::npos);
|
|
}
|
|
|
|
unsigned start_idx = angled ? resolved.angled_start_idx : 0;
|
|
|
|
std::vector<IncludeCandidate> results;
|
|
llvm::StringSet<> seen;
|
|
|
|
for(unsigned i = start_idx; i < resolved.dirs.size(); ++i) {
|
|
auto& search_dir = resolved.dirs[i];
|
|
|
|
const llvm::StringSet<>* entries = nullptr;
|
|
if(!dir_prefix.empty()) {
|
|
llvm::SmallString<256> sub_path(search_dir.path);
|
|
llvm::sys::path::append(sub_path, dir_prefix);
|
|
entries = resolve_dir(sub_path, dir_cache);
|
|
} else {
|
|
entries = search_dir.entries;
|
|
}
|
|
|
|
if(!entries)
|
|
continue;
|
|
|
|
for(auto& entry: *entries) {
|
|
auto name = entry.getKey();
|
|
if(!name.starts_with(file_prefix))
|
|
continue;
|
|
if(!seen.insert(name).second)
|
|
continue;
|
|
|
|
llvm::SmallString<256> full_path(search_dir.path);
|
|
if(!dir_prefix.empty()) {
|
|
llvm::sys::path::append(full_path, dir_prefix);
|
|
}
|
|
llvm::sys::path::append(full_path, name);
|
|
|
|
bool is_dir = false;
|
|
llvm::sys::fs::is_directory(llvm::Twine(full_path), is_dir);
|
|
|
|
results.push_back({name.str(), is_dir});
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
} // namespace clice
|