Files
clice/src/syntax/completion.cpp
ykiko 9c9e6b0bcb refactor: introduce Workspace/Session state model and clarify component responsibilities (#406)
## 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>
2026-04-08 14:03:39 +08:00

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