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.
285 lines
10 KiB
Python
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),
|
|
)
|
|
)
|