add signature help
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user