From 0318532308609b6aa2ffa647b29621ce6528e3fe Mon Sep 17 00:00:00 2001 From: christoph_xd Date: Sun, 3 Aug 2025 21:04:14 +0200 Subject: [PATCH] add some features --- client/src/extension.ts | 10 +- server/src/common/formatter.py | 63 ---- server/src/lsp_server.py | 43 +-- server/src/tools/formatter.py | 0 server/src/tools/parser.py | 459 ++++++++++++++++++++-------- server/src/tools/semantic_tokens.py | 16 +- server/src/tools/symbols.py | 82 ----- test/test.tcl | 6 +- 8 files changed, 374 insertions(+), 305 deletions(-) delete mode 100644 server/src/common/formatter.py delete mode 100644 server/src/tools/formatter.py diff --git a/client/src/extension.ts b/client/src/extension.ts index a9ee2dd..0474159 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/common/formatter.py b/server/src/common/formatter.py deleted file mode 100644 index 29f72eb..0000000 --- a/server/src/common/formatter.py +++ /dev/null @@ -1,63 +0,0 @@ -import re - - -def format_tcl(src: str, indent_str=" ") -> str: - """ - Simple Tcl formatter with special handling for: - 1) Single-line 'if {cond} {action}' blocks remain on one line. - 2) Combined closing-and-opening lines like '} else {' dedent then re-indent. - 3) Lines like '} Tag' stay on the same line: '} Tag'. - 4) Standard multi-line blocks for 'if', 'elseif', 'else', '{', '}'. - """ - level = 0 - out_lines = [] - - for raw_line in src.splitlines(): - stripped = raw_line.strip() - - # Comments are ignored - if stripped.startswith("#"): - out_lines.append(indent_str * level + stripped) - continue - - # Single-line 'if {cond} {action}' → no indent change - if re.match(r"^(if|elseif)\s*\{[^}]+\}\s*\{[^}]+\}$", stripped): - out_lines.append(indent_str * level + stripped) - continue - - # Combined '} else {' → dedent, print, then indent - if re.match(r"^\}\s*(elseif|else)\b.*\{$", stripped): - level = max(level - 1, 0) - out_lines.append(indent_str * level + stripped) - level += 1 - continue - - # SPECIAL: closing brace plus tag on same line: '} Tag' - m = re.match(r"^\}\s+(.+)", stripped) - if m and not stripped.startswith("#"): - # close one block - level = max(level - 1, 0) - # stay on one line: "} Tag" - out_lines.append(f"{indent_str * level}}} {m.group(1)}") - continue - - # Pure '}' → dedent then print - if stripped == "}": - level = max(level - 1, 0) - out_lines.append(f"{indent_str * level}{stripped}") - continue - - # 'elseif' or 'else' alone → align with matching 'if' - if re.match(r"^(elseif|else)\b(?!.*\{)", stripped): - level = max(level - 1, 0) - out_lines.append(f"{indent_str * level}{stripped}") - continue - - # Default: print at current indent - out_lines.append(f"{indent_str * level}{stripped}") - - # Open a new block on lines ending with '{' - if re.match(r"^(if|elseif)\b.*\{$", stripped) or stripped.endswith("{"): - level += 1 - - return "\n".join(out_lines) diff --git a/server/src/lsp_server.py b/server/src/lsp_server.py index 4a417d4..bd1aa55 100644 --- a/server/src/lsp_server.py +++ b/server/src/lsp_server.py @@ -4,16 +4,12 @@ from __future__ import annotations -import copy import json import os import pathlib import re import sys -import sysconfig -import traceback -from typing import Any, List, Optional, Sequence, Tuple -import re +from typing import Any, List, Optional, Tuple import operator from functools import reduce @@ -41,22 +37,18 @@ update_sys_path( # ********************************************************** # pylint: disable=wrong-import-position,import-error import lsp_jsonrpc as jsonrpc -import lsp_utils as utils import lsprotocol.types as lsp from pygls import server, uris, workspace from pygls.workspace.text_document import TextDocument from common.load_data import standard_items -from tclint.parser import Parser from tclint.lexer import TclSyntaxError from tclint.format import Formatter, FormatterOpts from tclint.violations import Violation - 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" @@ -64,7 +56,7 @@ DIAGNOSTIC_SOURCE = "nx-post-support" class TclLanguageServer(server.LanguageServer): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.parser = Parser() + self.parser = parser.CustomParser() for command in commands: self.parser._commands.update(command) self.diagnostics = {} @@ -197,14 +189,15 @@ def did_open(params: lsp.DidOpenTextDocumentParams) -> None: """LSP handler for textDocument/didOpen request.""" document = LSP_SERVER.workspace.get_text_document(params.text_document.uri) LSP_SERVER.compute_diagnostics(document) + completion.reset() + tree = LSP_SERVER.parser.parse(document.source) + tree.accept(completion, recurse=True) @LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DID_SAVE) def did_save(params: lsp.DidSaveTextDocumentParams) -> None: """LSP handler for textDocument/didSave request.""" - document = LSP_SERVER.workspace.get_text_document(params.text_document.uri) - tree = LSP_SERVER.parser.parse(document.source) - log_to_output(tree.pretty(2)) + _ = LSP_SERVER.workspace.get_text_document(params.text_document.uri) @LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DID_CLOSE) @@ -220,7 +213,6 @@ def did_change(params: lsp.DidChangeTextDocumentParams) -> None: completion.reset() tree = LSP_SERVER.parser.parse(document.source) tree.accept(completion, recurse=True) - log_to_output(tree.pretty(2)) @LSP_SERVER.feature( @@ -250,7 +242,7 @@ def document_diagnostic(params: lsp.DocumentDiagnosticParams): @LSP_SERVER.feature(lsp.TEXT_DOCUMENT_COMPLETION) def on_completion(params: lsp.CompletionParams) -> list[lsp.CompletionItem]: - document = LSP_SERVER.workspace.get_text_document(params.text_document.uri) + _ = LSP_SERVER.workspace.get_text_document(params.text_document.uri) items = ( standard_items.tcl_keyword_list @@ -261,15 +253,10 @@ 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_DOCUMENT_SYMBOL) +# def document_symbols(params: lsp.DocumentSymbolParams): +# doc = LSP_SERVER.workspace.get_text_document(params.text_document.uri) +# return [] @LSP_SERVER.feature(lsp.TEXT_DOCUMENT_INLAY_HINT) @@ -277,7 +264,7 @@ 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 + # collect Inlay Hints generator = InlayHintGenerator(completion.proc_signatures) tree.accept(generator, recurse=True) @@ -405,12 +392,12 @@ def formatting(params: lsp.DocumentFormattingParams) -> list[lsp.TextEdit] | Non start = lsp.Position(line=0, character=0) last_line = source.rsplit("\n", 1)[-1] end = lsp.Position(line=source.count("\n"), character=len(last_line)) - - formatted = LSP_SERVER.format(doc, params.options) + if GLOBAL_SETTINGS.get("formatter", True): + source = LSP_SERVER.format(doc, params.options) return [ lsp.TextEdit( range=lsp.Range(start=start, end=end), - new_text=formatted, + new_text=source, ) ] diff --git a/server/src/tools/formatter.py b/server/src/tools/formatter.py deleted file mode 100644 index e69de29..0000000 diff --git a/server/src/tools/parser.py b/server/src/tools/parser.py index 5a10ce3..408ab28 100644 --- a/server/src/tools/parser.py +++ b/server/src/tools/parser.py @@ -1,134 +1,347 @@ -from tclint.parser import Parser, _strip_ws -from tclint.lexer import ( - STATE_BRACEDWORD, - TOK_BACKSLASH_NEWLINE, - TOK_EOF, - TOK_LBRACE, - TOK_RBRACE, - TOK_WS, - TOK_NEWLINE, - TOK_ALPHA_CHARS, - TOK_RPAREN, - TOK_LPAREN, - TOK_DOLLAR, - TOK_QUOTE, - TOK_LBRACKET, - TOK_NUM_CHARS, - TclSyntaxError, -) +from tclint.parser import Parser +from tclint.commands import CommandArgError from tclint.syntax_tree import ( BracedWord, BareWord, - BinaryOp, - TernaryOp, - ParenExpression, - UnaryOp, + BracedExpression, + List, + QuotedWord, + Expression, ) +import ply.lex as lex +from typing import Tuple + +TOK_BACKSLASH_NEWLINE = "BACKSLASH_NEWLINE" +TOK_BACKSLASH_SUB = "BACKSLASH_SUB" +TOK_NEWLINE = "NEWLINE" +TOK_SEMI = "SEMI" +TOK_WS = "WS" +TOK_QUOTE = "QUOTE" +TOK_ARG_EXPANSION = "ARG_EXPANSION" +TOK_LBRACE = "LBRACE" +TOK_RBRACE = "RBRACE" +TOK_STAR = "STAR" +TOK_LBRACKET = "LBRACKET" +TOK_RBRACKET = "RBRACKET" +TOK_DOLLAR = "DOLLAR" +TOK_LPAREN = "LPAREN" +TOK_RPAREN = "RPAREN" +TOK_HASH = "HASH" +TOK_ALPHA_CHARS = "ALPHA_CHARS" +TOK_NUM_CHARS = "NUM_CHARS" +TOK_NAMESPACE_SEP = "NAMESPACE_SEP" +TOK_CHAR = "CHAR" +TOK_CONTENTS = "CONTENTS" +TOK_EOF = None + +STATE_BRACEDWORD = "bracedword" + + +class TclSyntaxError(Exception): + def __init__(self, message, start: Tuple[int, int], end: Tuple[int, int]): + super().__init__(message) + self.start = start + self.end = end + + +class _LexTable: + tokens = ( + TOK_BACKSLASH_NEWLINE, + TOK_BACKSLASH_SUB, + TOK_NEWLINE, + TOK_SEMI, + TOK_WS, + TOK_QUOTE, + TOK_ARG_EXPANSION, + TOK_LBRACE, + TOK_RBRACE, + TOK_STAR, + TOK_LBRACKET, + TOK_RBRACKET, + TOK_DOLLAR, + TOK_LPAREN, + TOK_RPAREN, + TOK_HASH, + TOK_ALPHA_CHARS, + TOK_NUM_CHARS, + TOK_NAMESPACE_SEP, + TOK_CHAR, + TOK_CONTENTS, + ) + + # This defines a conditional lexing state for parsing braced words. This is a + # performance optimization; since there are few special characters in this context, + # we can use a smaller set of tokens to parse them faster. This has a large impact + # since most Tcl programs have a large number of braced words. Any token with + # `bracedword` in its name is included in this state. Tokens that are included in + # this state and the default state also include `INITIAL` in their name. + states = ((STATE_BRACEDWORD, "exclusive"),) + + def _tok(self, t): + pos = (t.lexer.lineno, t.lexer.colno) + t.lexer.lineno += t.value.count("\n") + index = t.value.rfind("\n") + if index == -1: + t.lexer.colno += len(t.value) + else: + remaining = t.value[index + 1 :] + t.lexer.colno = len(remaining) + 1 + + t.value = (t.value, pos) + return t + + # Priority important + def t_bracedword_INITIAL_BACKSLASH_NEWLINE(self, t): + r"\\\r?\n" + return self._tok(t) + + # Priority important + def t_bracedword_INITIAL_BACKSLASH_SUB(self, t): + r"\\." + return self._tok(t) + + def t_NEWLINE(self, t): + r"\n" + return self._tok(t) + + def t_SEMI(self, t): + r";" + return self._tok(t) + + # TODO: should use \s? + def t_WS(self, t): + r"[\t\v\f\r ]+" + return self._tok(t) + + def t_QUOTE(self, t): + r'"' + return self._tok(t) + + # Must be higher priority than LBRACE + def t_ARG_EXPANSION(self, t): + r"\{\*\}" + return self._tok(t) + + def t_bracedword_INITIAL_LBRACE(self, t): + r"\{" + return self._tok(t) + + def t_bracedword_INITIAL_RBRACE(self, t): + r"\}" + return self._tok(t) + + def t_STAR(self, t): + r"\*" + return self._tok(t) + + def t_LBRACKET(self, t): + r"\[" + return self._tok(t) + + def t_RBRACKET(self, t): + r"\]" + return self._tok(t) + + def t_DOLLAR(self, t): + r"\$" + return self._tok(t) + + def t_LPAREN(self, t): + r"\(" + return self._tok(t) + + def t_RPAREN(self, t): + r"\)" + return self._tok(t) + + def t_HASH(self, t): + r"\#" + return self._tok(t) + + # Valid non-numeric chars in variable names + def t_ALPHA_CHARS(self, t): + r"[A-Za-z_]+" + return self._tok(t) + + # Valid numeric chars in variable names + # This is split up from the above to facilitate expression parsing, since + # e.g. 1eq1 can't be a single token. + def t_NUM_CHARS(self, t): + r"[0-9]+" + return self._tok(t) + + def t_NAMESPACE_SEP(self, t): + r"::+" + return self._tok(t) + + def t_bracedword_CONTENTS(self, t): + r"[^{}\\]+" + return self._tok(t) + + # Catch-all. TODO: inefficient, should probably munch multiple chars + def t_CHAR(self, t): + r"." + return self._tok(t) + + # Error handling rule + # TODO: do we need this? since we have a catch-all... + # there is a warning + def t_bracedword_INITIAL_error(self, t): + print("Illegal character '%s'" % t.value[0]) + t.lexer.skip(1) + + def __init__(self): + self.lexer = lex.lex(object=self) + self.lexer.lineno = 1 + self.lexer.colno = 1 + + def new_lexer(self, pos=None): + lexer = self.lexer.clone() + lexer.lineno = 1 + lexer.colno = 1 + + if pos is not None: + line, col = pos + lexer.lineno = line + lexer.colno = col + + return lexer + + +# Calling `lex.lex()` performs an expensive reflection process to generate the lexer. +# This singleton class holds a preinitialized lexer that can then be cloned to create +# individual instances. +LexTable = _LexTable() + + +class Lexer: + def __init__(self, pos=None): + self.lexer = LexTable.new_lexer(pos) + self.current = None + + def input(self, text): + self.lexer.input(text) + self.current = self.lexer.token() + + def type(self): + if self.current is None: + return TOK_EOF + return self.current.type + + def value(self): + if self.current is None: + return None + return self.current.value[0] + + def pos(self): + if self.current is None: + return (self.lexer.lineno, self.lexer.colno) + return self.current.value[1] + + def next(self): + self.current = self.lexer.token() + + def expect(self, *tokens, message, pos): + if self.type() not in tokens: + self.next() # munch another token to update position + raise TclSyntaxError(message, pos, self.pos()) + + self.next() + + def assert_(self, *tokens): + assert self.current.type in tokens + self.next() + class CustomParser(Parser): - @_strip_ws - def _parse_expression(self, ts): - op1 = self._parse_operand(ts) - expr = op1 + def parse(self, script, pos=None): + lexer = Lexer(pos=pos) + lexer.input(script) + tree = self._parse_script(lexer, in_command_sub=False) + assert lexer.type() == TOK_EOF, ( + "Didn't reach EOF parsing script, please file a bug report." + ) - # Add TOK_BACKSLASH_NEWLINE to the tokens we need to skip - while ts.type() == TOK_BACKSLASH_NEWLINE: - ts.next() + return tree - # last condition is hack to break out of expression in case we're in ternary op - if ts.type() not in { + def parse_list(self, node): + """Parse contents of node as Tcl list. This is a distinct entry point + that doesn't get used when generating the main syntax tree, but is used + in command-specific argument parsing. + """ + if isinstance(node, List): + return node + + if node.contents is None: + raise CommandArgError( + "expected braced word or word without substitutions in argument" + " interpreted as list" + ) + + ts = Lexer(pos=node.contents_pos) + ts.input(node.contents) + + DELIMITERS = {TOK_WS, TOK_BACKSLASH_NEWLINE, TOK_NEWLINE} + + list_node = List(pos=node.pos, end_pos=node.end_pos) + while ts.type() is not TOK_EOF: + while ts.type() in DELIMITERS: + ts.next() + + if ts.type() is TOK_EOF: + break + + if ts.type() == TOK_LBRACE: + # we can reuse parse_braced_word, since it doesn't use + # substitutions in any case + list_node.add(self.parse_braced_word(ts)) + elif ts.type() == TOK_QUOTE: + quote_word_pos = ts.pos() + + ts.assert_(TOK_QUOTE) + + bare_word_pos = ts.pos() + contents = "" + while ts.type() not in {TOK_QUOTE, TOK_EOF}: + contents += ts.value() + ts.next() + word = BareWord(contents, pos=bare_word_pos, end_pos=ts.pos()) + + ts.expect( + TOK_QUOTE, + message="reached EOF without finding match for quote", + pos=quote_word_pos, + ) + + list_node.add(QuotedWord(word, pos=quote_word_pos, end_pos=ts.pos())) + else: + pos = ts.pos() + contents = "" + while ts.type() not in {*DELIMITERS, TOK_EOF}: + contents += ts.value() + ts.next() + list_node.add(BareWord(contents, pos=pos, end_pos=ts.pos())) + + return list_node + + def parse_expression(self, node): + if node.contents is None: + raise CommandArgError( + "expected braced word or word without substitutions in argument" + " interpreted as expr" + ) + + ts = Lexer(pos=node.contents_pos) + ts.input(node.contents) + + contents = self._parse_expression(ts) + ts.expect( TOK_EOF, - TOK_RPAREN, - TOK_BACKSLASH_NEWLINE, - } and ts.value() not in {":", ","}: - if ts.value() == "?": - # weird hack to record operator - start = ts.pos() - ts.next() - q = BareWord("?", pos=start, end_pos=ts.pos()) + message=f"expected end of expression, got {ts.value()}", + pos=ts.pos(), + ) + if isinstance(node, BracedWord): + return BracedExpression(contents, pos=node.pos, end_pos=node.end_pos) - op2 = self._parse_expression(ts) - if ts.value() != ":": - start = ts.pos() - ts.next() - end = ts.pos() - raise TclSyntaxError( - "expected ':' to continue ternary expression", start, end - ) - - # weird hack again - start = ts.pos() - ts.next() - colon = BareWord(":", pos=start, end_pos=ts.pos()) - - op3 = self._parse_expression(ts) - expr = TernaryOp( - op1, q, op2, colon, op3, pos=op1.pos, end_pos=op3.end_pos - ) - else: - operator = self._parse_operator(ts) - while ts.type() == TOK_BACKSLASH_NEWLINE: - ts.next() - op2 = self._parse_expression(ts) - expr = BinaryOp(op1, operator, op2, pos=op1.pos, end_pos=op2.end_pos) - - if ts.type() not in ( - TOK_RPAREN, - TOK_BACKSLASH_NEWLINE, - ) and ts.value() not in {":", ","}: - ts.expect(TOK_EOF, message="expected end of expression", pos=ts.pos()) - - return expr - - def _parse_operator(self, ts): - pos = ts.pos() - - # hacky logic to handle parsing legal operators - - # Skip any backslash-newlines before the operator - while ts.type() in {TOK_WS, TOK_BACKSLASH_NEWLINE, TOK_NEWLINE}: - ts.next() - - if ts.value() in {"&&", "and"}: # Add explicit handling for logical AND - operator = ts.value() - ts.next() - return BareWord(operator, pos=pos, end_pos=ts.pos()) - elif ts.value() in {"*", "&", "|"}: - # one or two of these characters are legal operators - operator = ts.value() - ts.next() - if ts.value() == operator: - operator += ts.value() - ts.next() - elif ts.value() in {"<", ">"}: - operator = ts.value() - ts.next() - if ts.value() in {operator, "="}: - operator += ts.value() - ts.next() - elif ts.value() in {"=", "!"}: - operator = ts.value() - ts.next() - if ts.value() != "=": - raise TclSyntaxError( - f"invalid operator in expression: {operator}", pos, ts.pos() - ) - operator += ts.value() - ts.next() - elif ts.value() in {"*", "/", "%", "+", "-", "^", "eq", "ne", "in", "ni"}: - operator = ts.value() - ts.next() - else: - while ts.type() in {TOK_WS, TOK_BACKSLASH_NEWLINE, TOK_NEWLINE}: - ts.next() - - if ts.value() in {"&&", "and"}: # Try again after whitespace - operator = ts.value() - ts.next() - else: - raise TclSyntaxError( - f"invalid operator in expression: {ts.value()}", pos, ts.pos() - ) - - return BareWord(operator, pos=pos, end_pos=ts.pos()) + return Expression(contents, pos=node.pos, end_pos=node.end_pos) diff --git a/server/src/tools/semantic_tokens.py b/server/src/tools/semantic_tokens.py index fff965e..a5a0c68 100644 --- a/server/src/tools/semantic_tokens.py +++ b/server/src/tools/semantic_tokens.py @@ -13,6 +13,7 @@ class TokenModifier(enum.IntFlag): defaultLibrary = enum.auto() definition = enum.auto() declaration = enum.auto() + builtin = enum.auto() @attrs.define @@ -63,6 +64,20 @@ class _Highlighter(Visitor): def visit_command(self, command: Command): routine = command.routine + if routine.contents == "puts": + line, col = routine.contents_pos + self._tokens.append( + ( + ( + (line - 1, col - 1), + len(routine.contents), + "function", + [TokenModifier.builtin], + ) + ) + ) + pass + if routine.contents == "set" and command.args: first_arg = command.args[0] if hasattr(first_arg, "pos") and hasattr(first_arg, "value"): @@ -135,7 +150,6 @@ class _Highlighter(Visitor): ) def visit_var_sub(self, var_sub): - self.log_to_output(str(var_sub)) pass def tokens(self) -> list[Token]: diff --git a/server/src/tools/symbols.py b/server/src/tools/symbols.py index 71b3635..e69de29 100644 --- a/server/src/tools/symbols.py +++ b/server/src/tools/symbols.py @@ -1,82 +0,0 @@ -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 f53d014..9a65346 100644 --- a/test/test.tcl +++ b/test/test.tcl @@ -28,8 +28,8 @@ 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 + +SERVICE_spacer_output "*" 2 0 0 #_________________________________________________________________________________________________ # @@ -93,7 +93,7 @@ proc SERVICE_output_handling {handler} { #_________________________________________________________________________________________________ proc SERVICE_get_tool_data {} { global mom_tool_data - global mom_operation_info +global mom_operation_info set mom_tool_data(toollist) "" set operations $::mom_operation_name_list