add signature help

This commit is contained in:
Christoph Brandau
2025-08-13 11:00:22 +02:00
parent 1a542cc9e9
commit 781b240128
3 changed files with 124 additions and 0 deletions
+17
View File
@@ -46,6 +46,7 @@ from tools.semantic_tokens import _Highlighter, TOKEN_TYPES, TokenModifier
from tools.completion_items import completion, remove_existing_items, remove_shared_keys
from tools.inlay_hint import InlayHintGenerator
from tools.file_sourcing import get_all_psc_files, read_psc_file
from tools.signature_help import get_signature_help
from lsp_tclserver import TclLanguageServer
@@ -235,6 +236,22 @@ def semantic_tokens(params: lsp.SemanticTokensParams):
return lsp.SemanticTokens(data=data)
@LSP_SERVER.feature(
lsp.TEXT_DOCUMENT_SIGNATURE_HELP,
lsp.SignatureHelpOptions(trigger_characters=[" ", "\t", "[", ",", "(", "{", '"', "'"], retrigger_characters=list("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_:$\"'{}[]() ,\t")),
)
def signature_help(params: lsp.SignatureHelpParams) -> lsp.SignatureHelp | None:
document = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
tree = LSP_SERVER.get_tree(document)
# Merge proc signatures across files
merged_signatures = {}
for sigs in LSP_SERVER.proc_signatures.values():
merged_signatures.update(sigs)
return get_signature_help(document.source, tree, merged_signatures, params.position)
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_HOVER)
def hover(params: lsp.HoverParams) -> lsp.Hover:
pos = params.position
+3
View File
@@ -39,6 +39,9 @@ class _Completion(Visitor):
def _append_unique(self, item: lsp.CompletionItem):
# Avoid duplicate labels within the same file scan
if not any(ci.label == item.label for ci in self._custom_functions):
# For functions, attach a command to trigger signature help after completion
if item.kind == lsp.CompletionItemKind.Function and item.command is None:
item.command = lsp.Command(title="Trigger Signature Help", command="editor.action.triggerParameterHints")
self._custom_functions.append(item)
def visit_command(self, command: Command):
+104
View File
@@ -0,0 +1,104 @@
from __future__ import annotations
from typing import Dict, List, Optional, Tuple
import lsprotocol.types as lsp
from tclint.syntax_tree import Visitor, Command, Node
class _CommandAtPositionFinder(Visitor):
def __init__(self, line1: int, col1: int, lines: list[str]):
# Cursor position in 1-based line/col to match AST
self.line1 = line1
self.col1 = col1
self.lines = lines
self.match: Optional[Command] = None
def _pos_le(self, a: Tuple[int, int] | None, b: Tuple[int, int] | None) -> bool:
if a is None or b is None:
return False
return a[0] < b[0] or (a[0] == b[0] and a[1] <= b[1])
def _pos_ge(self, a: Tuple[int, int] | None, b: Tuple[int, int] | None) -> bool:
if a is None or b is None:
return False
return a[0] > b[0] or (a[0] == b[0] and a[1] >= b[1])
def _contains_or_trailing(self, node: Node) -> bool:
# Standard containment
if self._pos_le(node.pos, (self.line1, self.col1)) and self._pos_ge(node.end_pos, (self.line1, self.col1)):
return True
# Extend containment to trailing whitespace on same line as the command end
if getattr(node, "end_pos", None) and node.end_pos and node.end_pos[0] == self.line1 and self.col1 >= node.end_pos[1]:
try:
line = self.lines[self.line1 - 1]
except Exception:
line = ""
start = max(0, node.end_pos[1] - 1)
end = max(0, min(len(line), self.col1 - 1))
segment = line[start:end]
# If there is a command terminator (';' or ']') between end_pos and cursor, do not extend
if ";" not in segment and "]" not in segment:
return True
return False
def visit_command(self, command: Command):
if hasattr(command, "pos") and hasattr(command, "end_pos") and self._contains_or_trailing(command):
# Prefer the deepest command: overwrite and continue
self.match = command
# Continue traversal
for ch in getattr(command, "children", []):
ch.accept(self, recurse=True)
def _compute_active_parameter(cmd: Command, line1: int, col1: int) -> int:
# Count which arg contains the cursor; else number of args before cursor
for idx, arg in enumerate(getattr(cmd, "args", []) or []):
if getattr(arg, "pos", None) and getattr(arg, "end_pos", None):
if (arg.pos[0] < line1 or (arg.pos[0] == line1 and arg.pos[1] <= col1)) and (arg.end_pos[0] > line1 or (arg.end_pos[0] == line1 and arg.end_pos[1] >= col1)):
return idx
# Not inside any arg; compute based on separator position
count = 0
for arg in getattr(cmd, "args", []) or []:
if getattr(arg, "end_pos", None):
if arg.end_pos[0] < line1 or (arg.end_pos[0] == line1 and arg.end_pos[1] <= col1):
count += 1
return min(count, max(0, len(getattr(cmd, "args", [])) - 1))
def get_signature_help(
document_text: str,
tree,
merged_signatures: Dict[str, List[str]],
position: lsp.Position,
) -> Optional[lsp.SignatureHelp]:
# Convert to 1-based coordinates expected by AST
line1 = position.line + 1
col1 = position.character + 1
# Find the innermost command that contains the position
# Pass lines for trailing-space containment handling
lines = document_text.splitlines()
finder = _CommandAtPositionFinder(line1, col1, lines)
tree.accept(finder, recurse=True)
cmd = finder.match
if not cmd or not hasattr(cmd, "routine") or not hasattr(cmd.routine, "contents"):
return None
routine_name = cmd.routine.contents
if not routine_name:
return None
params = merged_signatures.get(routine_name)
if not params:
# No known signature for this routine
return None
# Build a signature info
label = f"{routine_name}({', '.join(params)})"
parameters = [lsp.ParameterInformation(label=p) for p in params]
active_param = _compute_active_parameter(cmd, line1, col1)
active_param = max(0, min(active_param, len(params) - 1))
sig = lsp.SignatureInformation(label=label, parameters=parameters)
return lsp.SignatureHelp(signatures=[sig], active_signature=0, active_parameter=active_param)