refactor(lsp): cache analysis results and debounce diagnostics
build_and_puplish.yml / build_and_publish (release) Successful in 29s

This change adds cached and incremental analysis for the LSP
server to improve responsiveness. The client now debounces
diagnostic updates to avoid excessive recomputation. The
server introduces per-document line caches and various
caches for completions, inlay hints, and metadata to
support faster, incremental updates.

- Debounce diagnostics on text changes to reduce noise.
- Add caches for completions, inlay hints, and metadata.
- Introduce incremental analysis with per-document line caches.
This commit is contained in:
Christoph Brandau
2026-08-19 13:41:53 +02:00
parent ecb50be2b8
commit af195a577b
11 changed files with 616 additions and 181 deletions
+17 -9
View File
@@ -1,17 +1,17 @@
import enum
from typing import List
from tclint.syntax_tree import Visitor, QuotedWord, Command, BareWord
from tclint.commands.plugins import PluginManager
import attrs
from common.load_data import standard_items
import lsprotocol.types as lsp
from tclint.commands.plugins import PluginManager
from tclint.syntax_tree import BareWord, Command, QuotedWord, Visitor
# Constructing a PluginManager scans entry points, and get_commands() rebuilds
# the builtin command set on every call. Semantic tokens are requested often, so
# cache the manager and the resolved commands per plugin set.
_PLUGIN_MANAGER = None
_COMMANDS_CACHE = {}
_STANDARD_PROC_NAMES = frozenset(item.label for item in standard_items.nx_procs)
def _load_commands(plugins):
@@ -60,13 +60,23 @@ TOKEN_TYPES = [
"string",
"parameter",
]
TOKEN_TYPE_INDEX = {}
for _token_index, _token_name in enumerate(TOKEN_TYPES):
TOKEN_TYPE_INDEX.setdefault(_token_name, _token_index)
class _Highlighter(Visitor):
def __init__(self, plugins, custom_functions: dict[str : list[lsp.CompletionItem]]):
def __init__(self, plugins, custom_functions):
self._commands = _load_commands(plugins)
self._tokens = []
self.custom_functions = custom_functions
if isinstance(custom_functions, dict):
self._custom_function_names = frozenset(
item.label
for items in custom_functions.values()
for item in items
)
else:
self._custom_function_names = frozenset(custom_functions)
def _append_token(self, position, length: int, tok_type: str, modifiers: List[TokenModifier] | None = None):
if position is None or length <= 0:
@@ -129,9 +139,7 @@ class _Highlighter(Visitor):
# Highlight functions (custom or standard) when used as the routine
name = getattr(routine, "contents", None)
if name:
in_custom = any(item.label == name for items in self.custom_functions.values() for item in items)
in_standard = any(item.label == name for item in standard_items.nx_procs)
if in_custom or in_standard:
if name in self._custom_function_names or name in _STANDARD_PROC_NAMES:
line, col = routine.contents_pos
self._append_token((line - 1, col - 1), len(name), "function", [])