refactor(lsp): cache analysis results and debounce diagnostics
build_and_puplish.yml / build_and_publish (release) Successful in 29s
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:
@@ -2,12 +2,12 @@ from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
from collections.abc import Mapping, Sequence
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import lsprotocol.types as lsp
|
||||
from tclint.syntax_tree import Command, VarSub, Visitor
|
||||
|
||||
from tools.navigation import FileSymbolIndex
|
||||
|
||||
|
||||
@@ -30,21 +30,22 @@ def _normalized_path(path: str) -> str:
|
||||
return os.path.normcase(os.path.abspath(path))
|
||||
|
||||
|
||||
def _definition_location(
|
||||
index: FileSymbolIndex | None, proc_name: str
|
||||
) -> lsp.Location | None:
|
||||
def _definition_locations(
|
||||
index: FileSymbolIndex | None,
|
||||
) -> dict[str, lsp.Location]:
|
||||
if index is None:
|
||||
return None
|
||||
return {}
|
||||
|
||||
basename = proc_name.removeprefix("::").rsplit("::", 1)[-1]
|
||||
for occurrence in reversed(index.occurrences):
|
||||
locations = {}
|
||||
for occurrence in index.occurrences:
|
||||
if (
|
||||
occurrence.is_definition
|
||||
and occurrence.identity.kind == "proc"
|
||||
and occurrence.placeholder == basename
|
||||
):
|
||||
return lsp.Location(uri=index.uri, range=occurrence.range)
|
||||
return None
|
||||
locations[occurrence.placeholder] = lsp.Location(
|
||||
uri=index.uri, range=occurrence.range
|
||||
)
|
||||
return locations
|
||||
|
||||
|
||||
def build_custom_inlay_signatures(
|
||||
@@ -63,7 +64,7 @@ def build_custom_inlay_signatures(
|
||||
result: dict[str, InlayHintSignature] = {}
|
||||
for path in paths:
|
||||
docs = docs_by_path.get(path, {})
|
||||
index = indexes_by_path.get(path)
|
||||
definition_locations = _definition_locations(indexes_by_path.get(path))
|
||||
for proc_name, parameter_names in signatures_by_path[path].items():
|
||||
parameters = tuple(
|
||||
InlayHintParameter(
|
||||
@@ -79,7 +80,9 @@ def build_custom_inlay_signatures(
|
||||
parameters=parameters,
|
||||
display_label=" ".join([proc_name, *parameter_names]),
|
||||
documentation=docs.get(proc_name),
|
||||
location=_definition_location(index, proc_name),
|
||||
location=definition_locations.get(
|
||||
proc_name.removeprefix("::").rsplit("::", 1)[-1]
|
||||
),
|
||||
)
|
||||
return result
|
||||
|
||||
@@ -147,19 +150,52 @@ class InlayHintGenerator(Visitor):
|
||||
def __init__(
|
||||
self,
|
||||
source: str,
|
||||
proc_signatures: dict[str, InlayHintSignature],
|
||||
proc_signatures: Mapping[str, InlayHintSignature],
|
||||
*,
|
||||
source_lines: Sequence[str] | None = None,
|
||||
requested_range: lsp.Range | None = None,
|
||||
parameter_names: str = "all",
|
||||
suppress_when_argument_matches_name: bool = True,
|
||||
):
|
||||
self.source_lines = source.splitlines()
|
||||
self.source_lines = (
|
||||
source_lines if source_lines is not None else source.splitlines()
|
||||
)
|
||||
self.proc_signatures = proc_signatures
|
||||
self.requested_range = requested_range
|
||||
self.parameter_names = parameter_names
|
||||
self.suppress_when_argument_matches_name = suppress_when_argument_matches_name
|
||||
self.hints: list[lsp.InlayHint] = []
|
||||
|
||||
def _node_intersects_requested_range(self, node) -> bool:
|
||||
if self.requested_range is None:
|
||||
return True
|
||||
start = getattr(node, "pos", None)
|
||||
end = getattr(node, "end_pos", None)
|
||||
if start is None or end is None:
|
||||
return True
|
||||
|
||||
node_start_line = start[0] - 1
|
||||
node_end_line = end[0] - 1
|
||||
return not (
|
||||
node_end_line < self.requested_range.start.line
|
||||
or node_start_line > self.requested_range.end.line
|
||||
)
|
||||
|
||||
def generate(self, tree) -> list[lsp.InlayHint]:
|
||||
"""Walk only syntax-tree branches overlapping the requested editor range."""
|
||||
self.hints.clear()
|
||||
|
||||
def walk(node) -> None:
|
||||
if not self._node_intersects_requested_range(node):
|
||||
return
|
||||
if isinstance(node, Command):
|
||||
self.visit_command(node)
|
||||
for child in getattr(node, "children", []):
|
||||
walk(child)
|
||||
|
||||
walk(tree)
|
||||
return self.hints
|
||||
|
||||
def _position(self, line: int, column: int) -> lsp.Position:
|
||||
line_index = line - 1
|
||||
character_index = column - 1
|
||||
|
||||
Reference in New Issue
Block a user