Files
nx_post_support/server/src/tools/inlay_hint.py
T
Christoph Brandau b2e6e9d250 perf(server): avoid unnecessary reparses and cache navigation definitions
Introduce several changes to reduce full-document reparses, lock contention and
redundant work when handling TclOO analysis and navigation:

- Add a cheap may_contain_classes pre-check and several cursor/receiver
  heuristics so completions, signature help and tcloo definitions skip the
  expensive marker reparse when the document cannot contain useful OO info.
- Allow passing an existing parsed tree into tcloo completion/signature/definition
  helpers; update callers to use the server's cached tree when available.
- Use a thread-local parser for request-time parse_source to avoid blocking the
  shared parser during background indexing, and add navigation_state() which
  returns cached definition identities (invalidated on index generation changes).
- Add a cheap name pre-filter (_may_resolve_to) for symbol matching and only
  run class highlighting when classes may exist.

These changes reduce contention and repeated parsing, improve responsiveness for
requests during background indexing, and cache navigation definition identities.
Tests were added/updated to assert caching and non-blocking behavior.
2026-09-23 12:04:36 +02:00

285 lines
10 KiB
Python

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
from tools.tcloo_arguments import method_parameters
from tools.tcloo_completion import resolved_method_calls
@dataclass(frozen=True)
class InlayHintParameter:
name: str
documentation: str | None = None
variadic: bool = False
@dataclass(frozen=True)
class InlayHintSignature:
parameters: tuple[InlayHintParameter, ...]
display_label: str
documentation: str | None = None
location: lsp.Location | None = None
def _normalized_path(path: str) -> str:
return os.path.normcase(os.path.abspath(path))
def _definition_locations(
index: FileSymbolIndex | None,
) -> dict[str, lsp.Location]:
if index is None:
return {}
locations = {}
for occurrence in index.occurrences:
if (
occurrence.is_definition
and occurrence.identity.kind == "proc"
):
locations[occurrence.placeholder] = lsp.Location(
uri=index.uri, range=occurrence.range
)
return locations
def build_custom_inlay_signatures(
signatures_by_path: dict[str, dict[str, list[str]]],
docs_by_path: dict[str, dict[str, str]],
indexes_by_path: dict[str, FileSymbolIndex],
current_path: str,
) -> dict[str, InlayHintSignature]:
"""Merge workspace signatures deterministically, preferring the current file."""
current_normalized = _normalized_path(current_path)
paths = sorted(
signatures_by_path, key=lambda path: _normalized_path(path).casefold()
)
paths.sort(key=lambda path: _normalized_path(path) == current_normalized)
result: dict[str, InlayHintSignature] = {}
for path in paths:
docs = docs_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(
name=parameter_name,
variadic=(
parameter_name == "args"
and parameter_index == len(parameter_names) - 1
),
)
for parameter_index, parameter_name in enumerate(parameter_names)
)
result[proc_name] = InlayHintSignature(
parameters=parameters,
display_label=" ".join([proc_name, *parameter_names]),
documentation=docs.get(proc_name),
location=definition_locations.get(
proc_name.removeprefix("::").rsplit("::", 1)[-1]
),
)
return result
def _is_builtin_variadic(item: dict[str, Any], parameter_name: str, index: int) -> bool:
parameters = item.get("parameters", [])
if index != len(parameters) - 1:
return False
if "..." in parameter_name or "…" in parameter_name:
return True
format_label = item.get("format", "")
return (
f"<{parameter_name}>+" in format_label or f"[{parameter_name}]+" in format_label
)
def _builtin_parameter_label(parameter_name: str) -> str:
if "..." not in parameter_name and "…" not in parameter_name:
return parameter_name.strip("<>[]")
first_name = parameter_name.split()[0].strip("<>[]")
return re.sub(r"(?:_?1)$", "", first_name) or first_name
def build_builtin_inlay_signatures(
items: list[dict[str, Any]],
) -> dict[str, InlayHintSignature]:
result = {}
for item in items:
proc_name = item.get("label")
if not proc_name:
continue
parameters = tuple(
InlayHintParameter(
name=_builtin_parameter_label(parameter.get("name", "")),
documentation=parameter.get("desc") or None,
variadic=_is_builtin_variadic(
item, parameter.get("name", ""), parameter_index
),
)
for parameter_index, parameter in enumerate(item.get("parameters", []))
if parameter.get("name")
)
result[proc_name] = InlayHintSignature(
parameters=parameters,
display_label=item.get("format") or proc_name,
documentation=item.get("description") or None,
)
return result
def _position_in_range(
position: lsp.Position, requested_range: lsp.Range | None
) -> bool:
if requested_range is None:
return True
value = (position.line, position.character)
start = (requested_range.start.line, requested_range.start.character)
end = (requested_range.end.line, requested_range.end.character)
return start <= value < end
class InlayHintGenerator(Visitor):
def __init__(
self,
source: str,
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,
external_classes=None,
):
self.source_lines = (
source_lines if source_lines is not None else source.splitlines()
)
self.source = source
self.external_classes = external_classes
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)
if self.parameter_names != "none":
for call in resolved_method_calls(self.source, self.external_classes, tree):
if not self._node_intersects_requested_range(call.command):
continue
parameters = method_parameters(call.parameters)
signature = InlayHintSignature(
parameters=tuple(InlayHintParameter(p.name, variadic=p.variadic) for p in parameters),
display_label=" ".join([call.label, *(p.label for p in parameters)]),
)
self._argument_hints(signature, call.command.args[call.argument_offset:])
self.hints.sort(key=lambda hint: (hint.position.line, hint.position.character))
return self.hints
def _position(self, line: int, column: int) -> lsp.Position:
line_index = line - 1
character_index = column - 1
if 0 <= line_index < len(self.source_lines):
prefix = self.source_lines[line_index][:character_index]
character_index = len(prefix.encode("utf-16-le")) // 2
return lsp.Position(line=line_index, character=character_index)
@staticmethod
def _parameter_for_argument(
signature: InlayHintSignature, argument_index: int
) -> InlayHintParameter | None:
if argument_index < len(signature.parameters):
return signature.parameters[argument_index]
if signature.parameters and signature.parameters[-1].variadic:
return signature.parameters[-1]
return None
def _should_show(self, argument, parameter: InlayHintParameter) -> bool:
if self.parameter_names == "none":
return False
if self.parameter_names == "literals" and isinstance(argument, VarSub):
return False
if not self.suppress_when_argument_matches_name or not isinstance(
argument, VarSub
):
return True
return getattr(argument, "value", None) != parameter.name
@staticmethod
def _tooltip(signature: InlayHintSignature) -> lsp.MarkupContent:
value = f"`{signature.display_label}`"
if signature.documentation:
value += f"\n\n{signature.documentation}"
return lsp.MarkupContent(kind=lsp.MarkupKind.Markdown, value=value)
def visit_command(self, command: Command):
name = getattr(command.routine, "contents", None)
signature = self.proc_signatures.get(name)
if signature is None or self.parameter_names == "none":
return
self._argument_hints(signature, command.args)
def _argument_hints(self, signature, arguments):
for argument_index, argument in enumerate(arguments):
parameter = self._parameter_for_argument(signature, argument_index)
if parameter is None:
break
if not argument.pos or not self._should_show(argument, parameter):
continue
line, column = argument.pos
position = self._position(line, column)
if not _position_in_range(position, self.requested_range):
continue
label = lsp.InlayHintLabelPart(
value=f"{parameter.name}:",
tooltip=parameter.documentation,
location=signature.location,
)
self.hints.append(
lsp.InlayHint(
position=position,
label=[label],
kind=lsp.InlayHintKind.Parameter,
tooltip=self._tooltip(signature),
)
)