feat(tcloo): add document-local TclOO completions, signature help and hints

Add conservative, document-local TclOO type inference and tooling so the server
can offer method completions, signature help, and inlay parameter hints for
statically resolvable TclOO receivers (including `new`/`create`, `my`, and
simple return-chains). Also surface class names as completion items and emit
semantic tokens for class declarations/references.

Notable changes:
- New tcloo_* tools: completion, symbols, and argument parsing; integrated into
  on_completion, signature_help, inlay hint generation and semantic token
  highlighting. Completions are returned early when an OO receiver context is
  detected.
- Use a completion-friendly parser fallback when the main AST fails (TclSyntaxError)
  so editing-in-progress code still yields useful completions.
- Add CompletionItemKind.Class to command kinds, exclude class items from the
  poco completion name cache, and include new unit tests for the TclOO helpers.
This commit is contained in:
Christoph Brandau
2026-09-11 23:38:42 +02:00
parent c28836933c
commit f88a50d4ab
12 changed files with 1007 additions and 42 deletions
+79
View File
@@ -0,0 +1,79 @@
"""Parameter presentation for statically resolved TclOO calls."""
from dataclasses import dataclass
import lsprotocol.types as lsp
from tclint.lexer import TclSyntaxError
from tclint.syntax_tree import BracedWord, Command
from tools.parser import CustomParser
from tools.signature_help import _active_argument, _contains_cursor
from tools.tcloo_completion import resolved_method_calls
@dataclass(frozen=True)
class MethodParameter:
name: str
label: str
variadic: bool = False
def method_parameters(parameters: str) -> list[MethodParameter]:
parser = CustomParser()
try:
words = parser.parse_list(BracedWord(parameters, pos=(1, 1))).children
result = []
for index, word in enumerate(words):
parts = parser.parse_list(word).children
if not parts or len(parts) > 2:
return []
name = parts[0].contents
if name is None:
return []
variadic = name == "args" and len(parts) == 1 and index == len(words) - 1
label = "{" + word.contents + "}" if len(parts) == 2 else name
result.append(MethodParameter(name, label, variadic))
return result
except TclSyntaxError:
return []
def method_signature_help(source: str, position: lsp.Position) -> lsp.SignatureHelp | None:
lines = source.split("\n")
if position.line >= len(lines):
return None
# AST columns are codepoints; LSP columns are UTF-16 code units.
prefix = lines[position.line].encode("utf-16-le")[:position.character * 2].decode("utf-16-le", errors="ignore")
cursor = (position.line, len(prefix))
candidates = [call for call in resolved_method_calls(source)
if _contains_cursor(call.command, lines, cursor)]
if not candidates:
return None
call = max(candidates, key=lambda candidate: candidate.command.pos)
def nested_active(node):
return any(
isinstance(child, Command) and _contains_cursor(child, lines, cursor)
or nested_active(child)
for child in node.children
)
# Let the inner command's own signature provider handle its arguments.
if nested_active(call.command):
return None
argument = _active_argument(call.command, cursor) - call.argument_offset
if argument < 0:
return None
parameters = method_parameters(call.parameters)
label = call.label
infos = []
for parameter in parameters:
label += " "
start = len(label.encode("utf-16-le")) // 2
label += parameter.label
infos.append(lsp.ParameterInformation(label=(start, len(label.encode("utf-16-le")) // 2)))
active = min(argument, len(parameters) - 1) if parameters else None
return lsp.SignatureHelp(
signatures=[lsp.SignatureInformation(label=label, parameters=infos, active_parameter=active)],
active_signature=0, active_parameter=active,
)