feat(indexing): add persistent index cache and incremental reparse

- Pass extension storage path to the server (client/ changes) so the
  server can persist a workspace index.
- Introduce IndexCache (server/tools/index_cache.py) and load/save it on
  initialization and after background indexing. Index entries are stored
  only when the file's stat hasn't changed while being read.
- Add incremental reparse logic (server/tools/incremental_parse.py) and
  use a per-file _last_parse cache in the language server to reparse only
  the top-level Tcl commands touched by an edit, falling back to a full
  parse when necessary.
- Use a new _FileIndex dataclass and _build_file_index helper to unify
  what is stored/loaded for a file; update update_poco_completion_for_file
  to use the persistent cache for disk-read files (from_disk/source_stat).
- Keep background indexing non-blocking and persist the index at the
  end of the run. Add basic unit tests for incremental parse and index cache.

Before: edits and background work always required full parsing of files
and no persistent cross-restart index. After: some edits reuse previous
ASTs and files read from disk can use a persisted index to skip
re-indexing across restarts.
This commit is contained in:
Christoph Brandau
2026-09-23 13:20:09 +02:00
parent b2e6e9d250
commit 01e8670cc1
10 changed files with 674 additions and 42 deletions
+10 -4
View File
@@ -265,6 +265,12 @@ def build_file_symbol_index(
filepath: str, uri: str, tree: Node
) -> FileSymbolIndex:
occurrences: list[SymbolOccurrence] = []
# Most occurrences repeat a few identities; sharing one object per identity
# keeps the index (and its persistent cache) small.
identities: dict[SymbolIdentity, SymbolIdentity] = {}
def shared(identity: SymbolIdentity | None) -> SymbolIdentity | None:
return None if identity is None else identities.setdefault(identity, identity)
def add_proc(
node: Node,
@@ -274,7 +280,7 @@ def build_file_symbol_index(
is_definition: bool,
declaration_range: lsp.Range | None = None,
) -> None:
identity = _proc_identity(raw_name, scope.namespace)
identity = shared(_proc_identity(raw_name, scope.namespace))
caller = None
if not is_definition:
caller = (
@@ -288,14 +294,14 @@ def build_file_symbol_index(
fallback_identity=(
None
if is_definition
else _proc_fallback(raw_name, scope.namespace)
else shared(_proc_fallback(raw_name, scope.namespace))
),
range=_name_range(node, raw_name),
placeholder=_basename(raw_name),
is_definition=is_definition,
symbol_kind=lsp.SymbolKind.Function,
container_name=_container_name(identity),
caller=caller,
caller=shared(caller),
declaration_range=declaration_range,
)
)
@@ -309,7 +315,7 @@ def build_file_symbol_index(
variable_sub: bool = False,
identity: SymbolIdentity | None = None,
) -> None:
symbol_identity = identity or _variable_identity(raw_name, scope)
symbol_identity = shared(identity or _variable_identity(raw_name, scope))
occurrences.append(
SymbolOccurrence(
identity=symbol_identity,