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
+11 -1
View File
@@ -11,6 +11,7 @@ import pathlib
import re
import sys
import threading
import time
from collections import ChainMap
from functools import reduce
from typing import Any, Optional
@@ -52,6 +53,7 @@ from tools.completion_items import (
ranked_completion_items,
)
from tools.folding_ranges import build_folding_ranges
from tools.index_cache import IndexCache
from tools.inlay_hint import (
InlayHintGenerator,
build_builtin_inlay_signatures,
@@ -89,6 +91,8 @@ from tools.tcl_command_completion import (
WORKSPACE_SETTINGS = {}
GLOBAL_SETTINGS = {}
# Extension storage folder of the workspace; without it nothing is persisted.
INDEX_CACHE_PATH: dict[str, str | None] = {}
MAX_WORKERS = 5
@@ -175,6 +179,7 @@ def _index_tcl_file_from_disk(uri: str) -> None:
document,
cache_tree=False,
require_file_exists=True,
from_disk=True,
)
except (OSError, UnicodeError) as error:
log_warning(f"Could not re-index {path}: {error}")
@@ -861,6 +866,7 @@ def initialize(params: lsp.InitializeParams) -> lsp.InitializeResult:
log_to_output(f"sys.path used to run Server:\r\n {paths}")
GLOBAL_SETTINGS.update(**params.initialization_options.get("globalSettings", {}))
INDEX_CACHE_PATH["path"] = params.initialization_options.get("indexCachePath")
settings = params.initialization_options["settings"]
_update_workspace_settings(settings)
@@ -899,6 +905,8 @@ def initialized(_params: lsp.InitializedParams):
log_to_output("Background indexing skipped: no workspace folder is open.")
return
log_to_output("Background indexing started...")
started = time.perf_counter()
LSP_SERVER.index_cache = IndexCache.load(INDEX_CACHE_PATH.get("path"))
root_path = pathlib.Path(root)
skipped_directories = {
".git",
@@ -916,11 +924,13 @@ def initialized(_params: lsp.InitializedParams):
document,
cache_tree=False,
require_file_exists=True,
from_disk=True,
)
except Exception as error:
log_to_output(f"Fehler beim Parsen von {filepath}: {error}")
_refresh_psc_index()
log_to_output("Background indexing completed.")
LSP_SERVER.index_cache.save()
log_to_output(f"Background indexing completed in {time.perf_counter() - started:.1f}s.")
except Exception as e:
log_to_output(f"Background indexing failed: {e}")