From e045e1533c48629566affe03ad436117818f55ae Mon Sep 17 00:00:00 2001 From: christoph_xd Date: Fri, 18 Jul 2025 22:49:49 +0200 Subject: [PATCH] add tcl parser --- server/src/lsp_server.py | 27 ++++++++++-------- server/src/parser/parser.py | 57 +++++++++++++++++++++---------------- server/src/parser/tcl.lark | 38 +++++++++++-------------- 3 files changed, 63 insertions(+), 59 deletions(-) diff --git a/server/src/lsp_server.py b/server/src/lsp_server.py index 07cec2a..288306d 100644 --- a/server/src/lsp_server.py +++ b/server/src/lsp_server.py @@ -13,6 +13,7 @@ import sys import sysconfig import traceback from typing import Any, Optional, Sequence +from parser.parser import parse_tcl, format_tree # ********************************************************** @@ -179,18 +180,20 @@ def _get_severity(*_codes: list[str]) -> lsp.DiagnosticSeverity: @LSP_SERVER.feature(lsp.TEXT_DOCUMENT_FORMATTING) def formatting(params: lsp.DocumentFormattingParams) -> list[lsp.TextEdit] | None: """LSP handler for textDocument/formatting request.""" - # If your tool is a formatter you can use this handler to provide - # formatting support on save. You have to return an array of lsp.TextEdit - # objects, to provide your formatted results. - - document = LSP_SERVER.workspace.get_document(params.text_document.uri) - edits = _formatting_helper(document) - if edits: - return edits - - # NOTE: If you provide [] array, VS Code will clear the file of all contents. - # To indicate no changes to file return None. - return None + text = LSP_SERVER.workspace.get_text_document(params.text_document.uri) + tree, _ = parse_tcl(text) + if not tree: + return [] + new_text = format_tree(tree) + return [ + { + "range": { + "start": {"line": 0, "character": 0}, + "end": {"line": len(text.splitlines()), "character": 0}, + }, + "newText": new_text, + } + ] def _formatting_helper(document: workspace.Document) -> list[lsp.TextEdit] | None: diff --git a/server/src/parser/parser.py b/server/src/parser/parser.py index 366230d..0481917 100644 --- a/server/src/parser/parser.py +++ b/server/src/parser/parser.py @@ -1,32 +1,39 @@ -from lark import Lark, Transformer, Token, v_args from pathlib import Path +from lark import Lark, Tree, Token, UnexpectedInput -GRAMMAR_FILE = Path(__file__).parent.joinpath("tcl.lark") -TCL_GRAMMAR = GRAMMAR_FILE.read_text(encoding="utf-8") +tcl_parser = Lark.open( + Path(__file__).parent.joinpath("tcl.lark"), parser="lalr", propagate_positions=True +) -@v_args(inline=True) -class TCLTransformer(Transformer): - def start(self, *stmts): - return list(stmts) +def parse_tcl(source: str): + try: + tree = tcl_parser.parse(source) + return tree, [] + except UnexpectedInput as e: + # e.line, e.column enthalten Position + diagnostic = { + "range": { + "start": {"line": e.line - 1, "character": e.column - 1}, + "end": {"line": e.line - 1, "character": e.column}, + }, + "message": f"Syntaxfehler: {e}", + "severity": 1, # Error + } + return None, [diagnostic] - def command(self, *elements): - return list(elements) - def braced(self, *content): - return "{" + "".join(c for c in content) + "}" +def format_tree(tree): + def fmt(node, indent=0): + if isinstance(node, Token): + return node.value + elif isinstance(node, Tree): + if node.data == "cmd": + parts = [] + for child in node.children: + parts.append(fmt(child, indent)) + return " ".join(parts) + ";\n" + # weitere Node-Typen … + return "" - def quoted(self, *content): - return '"' + "".join(c for c in content) + '"' - - def variable(self, token): - return token.value - - def cmdsubst(self, *inner): - return "[" + " ".join(inner) + "]" - - def bare(self, token): - return token.value - - def COMMENT(self, token): - return token.value + return "".join(fmt(tree)) diff --git a/server/src/parser/tcl.lark b/server/src/parser/tcl.lark index b8a7353..3674b1b 100644 --- a/server/src/parser/tcl.lark +++ b/server/src/parser/tcl.lark @@ -1,27 +1,21 @@ -start: statement* +// tcl.lark +// sehr vereinfachtes Beispiel – für volle Abdeckung musst du die offizielle TCL-Spec implementieren -?statement: command | COMMENT +?start: script -COMMENT: /#[^\n]*/ +script: stmt* +stmt: command ";"? -> cmd + | newline -> empty -command: element+ +command: WORD arg* +arg: WORD -> bareword + | STRING -> string + | BRACED -> braced -?element: braced -| quoted -| variable -| cmdsubst -| bare +WORD: /[^\s\{\}";]+/ +STRING: "\"" /([^"\\]|\\.)*/ "\"" // "…" +BRACED: "{" /([^{}\\]|\\.)*/ "}" // {…} -braced: "{" inner_braced* "}" -inner_braced: /[^{}]+/ | braced - -quoted: '"' inner_quoted* '"' -inner_quoted: /[^"{}]+/ | braced - -variable: "$" /[A-Za-z_][A-Za-z0-9_]*/ -cmdsubst: "[" start "]" - -bare: /[^\s{}$"]+/ - -%import common.WS_INLINE -%ignore WS_INLINE \ No newline at end of file +%import common.NEWLINE -> newline +%import common.WS +%ignore WS