diff --git a/server/src/lsp_server.py b/server/src/lsp_server.py index 42b939f..a502b6d 100644 --- a/server/src/lsp_server.py +++ b/server/src/lsp_server.py @@ -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 diff --git a/server/src/tools/completion_items.py b/server/src/tools/completion_items.py index 3cade14..50dad62 100644 --- a/server/src/tools/completion_items.py +++ b/server/src/tools/completion_items.py @@ -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): diff --git a/server/src/tools/signature_help.py b/server/src/tools/signature_help.py new file mode 100644 index 0000000..080b025 --- /dev/null +++ b/server/src/tools/signature_help.py @@ -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)