diff --git a/client/src/extension.ts b/client/src/extension.ts index 0474159..a9ee2dd 100644 --- a/client/src/extension.ts +++ b/client/src/extension.ts @@ -179,11 +179,11 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push(formatDefProvider) - const tclOutlineProvider = vscode.languages.registerDocumentSymbolProvider( - { scheme: "file", language: "tcl" }, - { provideDocumentSymbols: tclDocumentSymbolProvider } - ) - context.subscriptions.push(tclOutlineProvider) + // const tclOutlineProvider = vscode.languages.registerDocumentSymbolProvider( + // { scheme: "file", language: "tcl" }, + // { provideDocumentSymbols: tclDocumentSymbolProvider } + // ) + // context.subscriptions.push(tclOutlineProvider) // Diagnostics collection const diagnosticCollectionCdl = vscode.languages.createDiagnosticCollection("cdl") diff --git a/server/src/lsp_server.py b/server/src/lsp_server.py index 4e3d7d1..4a417d4 100644 --- a/server/src/lsp_server.py +++ b/server/src/lsp_server.py @@ -55,6 +55,8 @@ from plugins.poco_plugin import commands from tools import checks, parser from tools.semantic_tokens import _Highlighter, TOKEN_TYPES, TokenModifier from tools.completion_items import completion +from tools.inlay_hint import InlayHintGenerator +from tools.symbols import OutlineVisitor DIAGNOSTIC_SOURCE = "nx-post-support" @@ -259,6 +261,29 @@ def on_completion(params: lsp.CompletionParams) -> list[lsp.CompletionItem]: return lsp.CompletionList(is_incomplete=False, items=items) +@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DOCUMENT_SYMBOL) +def document_symbols(params: lsp.DocumentSymbolParams): + doc = LSP_SERVER.workspace.get_text_document(params.text_document.uri) + tree = LSP_SERVER.parser.parse(doc.source) + + visitor = OutlineVisitor(LSP_SERVER.parser) + tree.accept(visitor, recurse=True) + + return visitor.stack[0] + + +@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_INLAY_HINT) +def inlay_hints(params: lsp.InlayHintParams): + document = LSP_SERVER.workspace.get_text_document(params.text_document.uri) + tree = LSP_SERVER.parser.parse(document.source) + + # Inlay Hints sammeln + generator = InlayHintGenerator(completion.proc_signatures) + tree.accept(generator, recurse=True) + + return generator.hints + + @LSP_SERVER.feature( lsp.TEXT_DOCUMENT_SEMANTIC_TOKENS_FULL, lsp.SemanticTokensLegend( diff --git a/server/src/tools/completion_items.py b/server/src/tools/completion_items.py index 4001ae3..9684812 100644 --- a/server/src/tools/completion_items.py +++ b/server/src/tools/completion_items.py @@ -1,4 +1,4 @@ -from tclint.syntax_tree import Visitor +from tclint.syntax_tree import Visitor, Command, BareWord, List import lsprotocol.types as lsp @@ -19,15 +19,21 @@ class _Completion(Visitor): def __init__(self): super().__init__() self._custom_functions: list[lsp.CompletionItem] = [] + self._proc_signatures = {} @property def custom_functions(self) -> list[lsp.CompletionItem]: return self._custom_functions + @property + def proc_signatures(self): + return self._proc_signatures + def reset(self): self._custom_functions = [] + self._proc_signatures = {} - def visit_command(self, command): + def visit_command(self, command: Command): routine = command.routine if routine.contents == "proc" and command.args: @@ -40,6 +46,23 @@ class _Completion(Visitor): label=first_arg.value, kind=lsp.CompletionItemKind.Function ) ) + if len(command.args) < 2: + return + + param_list_node = command.args[1] + if not hasattr(param_list_node, "children"): + return + + param_names = [] + for arg in param_list_node.children: + if isinstance(arg, BareWord): + param_names.append(arg.value) + elif isinstance(arg, List) and len(arg.children) >= 1: + first = arg.children[0] + if isinstance(first, BareWord): + param_names.append(first.value) + + self._proc_signatures[first_arg.value] = param_names completion = _Completion() diff --git a/server/src/tools/inlay_hint.py b/server/src/tools/inlay_hint.py new file mode 100644 index 0000000..ea4f3e6 --- /dev/null +++ b/server/src/tools/inlay_hint.py @@ -0,0 +1,29 @@ +import lsprotocol.types as lsp +from tclint.syntax_tree import Visitor, Command + + +class InlayHintGenerator(Visitor): + def __init__(self, proc_signatures): + self.proc_signatures = proc_signatures + self.hints = [] + + def visit_command(self, command: Command): + name = getattr(command.routine, "contents", None) + if name not in self.proc_signatures: + return + + param_names = self.proc_signatures[name] + for idx, arg in enumerate(command.args): + if idx >= len(param_names): + break + param_name = param_names[idx] + + if arg.pos: + line, col = arg.pos + self.hints.append( + lsp.InlayHint( + position=lsp.Position(line=line - 1, character=col - 1), + label=f"{param_name}:", + kind=lsp.InlayHintKind.Parameter, + ) + ) diff --git a/server/src/tools/symbols.py b/server/src/tools/symbols.py new file mode 100644 index 0000000..71b3635 --- /dev/null +++ b/server/src/tools/symbols.py @@ -0,0 +1,82 @@ +from tclint.syntax_tree import Visitor, BareWord, BracedWord, Script, Command +from tclint.parser import Parser +import lsprotocol.types as lsp + + +class OutlineVisitor(Visitor): + def __init__(self, parser: Parser): + self.parser = parser + self.stack = [[]] # Root symbol list + + def _range(self, node) -> lsp.Range: + line = node.line - 1 + col = node.col - 1 + if node.end_pos: + end_line = node.end_pos[0] - 1 + end_col = node.end_pos[1] - 1 + else: + end_line = line + end_col = col + 1 + + return lsp.Range( + start=lsp.Position(line=line, character=col), + end=lsp.Position(line=end_line, character=end_col), + ) + + def _add(self, name: str, kind: lsp.SymbolKind, node, children=None): + symbol = lsp.DocumentSymbol( + name=name, + kind=kind, + range=self._range(node), + selection_range=self._range(node), + children=children or [], + ) + self.stack[-1].append(symbol) + return symbol + + def visit_script(self, script): + for child in script.children: + child.accept(self, recurse=False) + + def visit_command(self, command: Command): + if not isinstance(command.routine, BareWord): + return + name = command.routine.contents + + # --- NAMESPACE EVAL --- + if name == "namespace" and len(command.args) >= 3: + subcmd = command.args[0] + if isinstance(subcmd, BareWord) and subcmd.contents == "eval": + ns_arg = command.args[1] + ns_name = ( + ns_arg.contents if isinstance(ns_arg, BareWord) else "" + ) + ns_body = command.args[2] + + ns_symbol = self._add( + ns_name, lsp.SymbolKind.Namespace, command, children=[] + ) + self.stack.append(ns_symbol.children) + + if isinstance(ns_body, BracedWord): + try: + subtree = self.parser.parse_script(ns_body) + subtree.accept(self, recurse=False) + except Exception as e: + print(f"Failed parsing namespace body: {e}") + + self.stack.pop() + + # --- PROC --- + elif name == "proc" and len(command.args) >= 1: + proc_arg = command.args[0] + proc_name = ( + proc_arg.contents if isinstance(proc_arg, BareWord) else "" + ) + self._add(proc_name, lsp.SymbolKind.Function, command) + + # --- SET --- + elif name == "set" and len(command.args) >= 1: + var_arg = command.args[0] + var_name = var_arg.contents if isinstance(var_arg, BareWord) else "" + self._add(var_name, lsp.SymbolKind.Variable, command) diff --git a/test/test.tcl b/test/test.tcl index 4aef606..f53d014 100644 --- a/test/test.tcl +++ b/test/test.tcl @@ -5,21 +5,31 @@ if {$main == 1 && 1 == 1} { proc test {} { puts "main" + proc llll {} {} + set rrrrrrr } LIB_GE_command_buffer_edit_insert MOM_tool_change_LIB TOOL_CHANGE_AUTO {CUSTOM_after_tool_change_call} mytag after @TOOL_CHANGE_AUTO MOM_abort +namespace eval myns { + proc add {a b} { + set sum [expr {$a + $b}] + return $sum + } + set config "debug" +} #_________________________________________________________________________________________________ # # Function to output a spacer line or empty line #_________________________________________________________________________________________________ -proc SERVICE_spacer_output {type {length 20} {line_num 0} {output 1} check} { +proc SERVICE_spacer_output {type {length 20} {line_num 0} {output 1}} { LIB_GE_message [string repeat $type $length] "output_$output" $line_num } - +SERVICE_spacer_output "*" 50 0 1 +SERVICE_remove_file $filename #_________________________________________________________________________________________________ #