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>
This commit is contained in:
ykiko
2026-04-08 14:03:39 +08:00
committed by GitHub
parent bb0b160a28
commit 9c9e6b0bcb
17 changed files with 2038 additions and 1669 deletions

View File

@@ -0,0 +1,125 @@
#include "test/test.h"
#include "syntax/completion.h"
#include "llvm/ADT/DenseMap.h"
namespace clice::testing {
namespace {
TEST_SUITE(DetectCompletionContext) {
TEST_CASE(IncludeAngled) {
auto ctx = detect_completion_context("#include <vec", 13);
EXPECT_EQ(ctx.kind, CompletionContext::IncludeAngled);
EXPECT_EQ(ctx.prefix, "vec");
}
TEST_CASE(IncludeQuoted) {
auto ctx = detect_completion_context("#include \"my_header", 19);
EXPECT_EQ(ctx.kind, CompletionContext::IncludeQuoted);
EXPECT_EQ(ctx.prefix, "my_header");
}
TEST_CASE(IncludeAngledWithSpaces) {
auto ctx = detect_completion_context(" # include <sys/", 19);
EXPECT_EQ(ctx.kind, CompletionContext::IncludeAngled);
EXPECT_EQ(ctx.prefix, "sys/");
}
TEST_CASE(IncludeEmpty) {
auto ctx = detect_completion_context("#include <", 10);
EXPECT_EQ(ctx.kind, CompletionContext::IncludeAngled);
EXPECT_EQ(ctx.prefix, "");
}
TEST_CASE(ImportSimple) {
auto ctx = detect_completion_context("import std", 10);
EXPECT_EQ(ctx.kind, CompletionContext::Import);
EXPECT_EQ(ctx.prefix, "std");
}
TEST_CASE(ExportImport) {
auto ctx = detect_completion_context("export import my_mod", 20);
EXPECT_EQ(ctx.kind, CompletionContext::Import);
EXPECT_EQ(ctx.prefix, "my_mod");
}
TEST_CASE(ImportWithSemicolon) {
auto ctx = detect_completion_context("import std;\n", 7);
EXPECT_EQ(ctx.kind, CompletionContext::None);
}
TEST_CASE(ImportEmpty) {
auto ctx = detect_completion_context("import ", 7);
EXPECT_EQ(ctx.kind, CompletionContext::Import);
EXPECT_EQ(ctx.prefix, "");
}
TEST_CASE(NormalCode) {
auto ctx = detect_completion_context("int main() {", 12);
EXPECT_EQ(ctx.kind, CompletionContext::None);
}
TEST_CASE(MultilineAtSecondLine) {
std::string text = "#include <vector>\n#include <str";
auto ctx = detect_completion_context(text, text.size());
EXPECT_EQ(ctx.kind, CompletionContext::IncludeAngled);
EXPECT_EQ(ctx.prefix, "str");
}
TEST_CASE(NotImportKeyword) {
auto ctx = detect_completion_context("importlib foo", 13);
EXPECT_EQ(ctx.kind, CompletionContext::None);
}
TEST_CASE(HashOnly) {
auto ctx = detect_completion_context("#", 1);
EXPECT_EQ(ctx.kind, CompletionContext::None);
}
}; // TEST_SUITE(DetectCompletionContext)
TEST_SUITE(CompleteModuleImport) {
TEST_CASE(PrefixMatch) {
llvm::DenseMap<std::uint32_t, std::string> modules;
modules[1] = "std";
modules[2] = "std.io";
modules[3] = "std.net";
modules[4] = "my_lib";
auto results = complete_module_import(modules, "std");
EXPECT_EQ(results.size(), 3u);
for(auto& name: results) {
EXPECT_TRUE(name.starts_with("std"));
}
}
TEST_CASE(EmptyPrefix) {
llvm::DenseMap<std::uint32_t, std::string> modules;
modules[1] = "std";
modules[2] = "my_lib";
auto results = complete_module_import(modules, "");
EXPECT_EQ(results.size(), 2u);
}
TEST_CASE(NoMatch) {
llvm::DenseMap<std::uint32_t, std::string> modules;
modules[1] = "std";
modules[2] = "my_lib";
auto results = complete_module_import(modules, "xyz");
EXPECT_TRUE(results.empty());
}
TEST_CASE(EmptyModules) {
llvm::DenseMap<std::uint32_t, std::string> modules;
auto results = complete_module_import(modules, "std");
EXPECT_TRUE(results.empty());
}
}; // TEST_SUITE(CompleteModuleImport)
} // namespace
} // namespace clice::testing