From 6a051a6dee302885498956b43b378ec476cb3f23 Mon Sep 17 00:00:00 2001 From: Christoph Brandau Date: Mon, 4 Aug 2025 16:06:50 +0200 Subject: [PATCH 1/6] del test file --- server/src/test_tcl.py | 78 ------------------------------------------ 1 file changed, 78 deletions(-) delete mode 100644 server/src/test_tcl.py diff --git a/server/src/test_tcl.py b/server/src/test_tcl.py deleted file mode 100644 index 4ffbfeb..0000000 --- a/server/src/test_tcl.py +++ /dev/null @@ -1,78 +0,0 @@ -from tclint.parser import Parser as tcLintParser -from tclint.lexer import Lexer as tclingLexer - - -def main(): - parser = tcLintParser(True) - tree = parser.parse("""puts hello - proc myProc {arg {arg7 0}} {} - set myVar 123 - puts puts - LIB_SPF_prepend MOM_strt Start_Lib { - set somthing 1 - set more 2 -} myTag - -LIB_SPF_prepend MOM_strt Start_Lib { - proc test {} { - puts "Hello" - } - set somthing 1 - set someting 3 -} myTag""") - - print(tree.pretty(2)) - - -def lexer_test(): - lexer = tclingLexer() - tree = lexer.input(""" -if {$oem(custom_clamp_4th) == 1 && $oem(custom_clamp_5th) == 1 \\ - && $oem(status_clamp_4th) == "off" && $oem(status_clamp_5th) == "off"}""") - - # print("Lexing input:\n", code) - # print("\nTokens:\n" + "-" * 30) - - while lexer.type() is not None: - tok_type = lexer.type() - tok_value = lexer.value() - tok_pos = lexer.pos() - print(f"Type: {tok_type:20} | Value: {repr(tok_value):20} | Pos: {tok_pos}") - lexer.next() - - -def test_1(): - from tclint.lexer import Lexer, TOK_BACKSLASH_NEWLINE - - code = "expr {1 == 2 \\\n&& 3 == 4}" - - lexer = Lexer() - lexer.input(code) - - while lexer.type() is not None: - print( - f"Type: {lexer.type():<20} | Value: {lexer.value()!r} | Pos: {lexer.pos()}" - ) - lexer.next() - - -class NodeVisitor: - def visit_script(self, node): - for stmt in node.statements: - stmt.accept(self) - - def visit_proc(self, node): - print(f"Proc: {node.name}, args={node.args}") - - def visit_set(self, node): - print(f"Set: {node.varname} = {node.value}") - - def visit_namespace(self, node): - print(f"Namespace: {node.name}") - - def visit_command(self, node): - print(f"Command: {node.name} {node.args}") - - -if __name__ == "__main__": - test_1() From 1abf89fe5669dc64f3c95d9fe273fd3ba28a5b66 Mon Sep 17 00:00:00 2001 From: christoph_xd Date: Mon, 4 Aug 2025 21:11:20 +0200 Subject: [PATCH 2/6] load all files for completion --- server/src/lsp_server.py | 34 ++++++++++++++++++++++++------ server/src/tools/file_sourcing.py | 35 +++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 6 deletions(-) create mode 100644 server/src/tools/file_sourcing.py diff --git a/server/src/lsp_server.py b/server/src/lsp_server.py index bd1aa55..b05406e 100644 --- a/server/src/lsp_server.py +++ b/server/src/lsp_server.py @@ -50,6 +50,7 @@ from tools.semantic_tokens import _Highlighter, TOKEN_TYPES, TokenModifier from tools.completion_items import completion from tools.inlay_hint import InlayHintGenerator + DIAGNOSTIC_SOURCE = "nx-post-support" @@ -190,8 +191,16 @@ def did_open(params: lsp.DidOpenTextDocumentParams) -> None: 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) + all_tcl_files = get_all_tcl_files(LSP_SERVER.workspace.root_path) + + for filepath in all_tcl_files: + try: + with open(filepath, "r", encoding="utf-8") as f: + source = f.read() + tree = LSP_SERVER.parser.parse(source) + tree.accept(completion, recurse=True) + except Exception as e: + log_to_output(f"Fehler beim Parsen von {filepath}: {e}") @LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DID_SAVE) @@ -205,14 +214,24 @@ def did_close(params: lsp.DidCloseTextDocumentParams) -> None: """LSP handler for textDocument/didClose request.""" +def get_all_tcl_files(root_path: str) -> list[str]: + """Sammelt alle .tcl-Dateien rekursiv im gegebenen Verzeichnis""" + tcl_files = [] + for dirpath, _, filenames in os.walk(root_path): + for filename in filenames: + if filename.endswith(".tcl"): + tcl_files.append(os.path.join(dirpath, filename)) + return tcl_files + + @LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DID_CHANGE) def did_change(params: lsp.DidChangeTextDocumentParams) -> None: """LSP handler for textDocument/didChange 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) + # completion.reset() + # tree = LSP_SERVER.parser.parse(document.source) + # tree.accept(completion, recurse=True) @LSP_SERVER.feature( @@ -256,7 +275,10 @@ def on_completion(params: lsp.CompletionParams) -> list[lsp.CompletionItem]: # @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 [] +# ast = LSP_SERVER.parser.parse(doc.source) +# symbols = LSP_SERVER.extract_tcl_symbols(ast) + +# return symbols @LSP_SERVER.feature(lsp.TEXT_DOCUMENT_INLAY_HINT) diff --git a/server/src/tools/file_sourcing.py b/server/src/tools/file_sourcing.py new file mode 100644 index 0000000..5ed2bbe --- /dev/null +++ b/server/src/tools/file_sourcing.py @@ -0,0 +1,35 @@ +import xml.etree.ElementTree as ET +from dataclasses import dataclass +from typing import List, Optional + + +@dataclass +class SourcedFiles: + layer_name: str + subfolder: Optional[str] + files: List[str] + + +def read_psc_file(psc_file: str) -> List[SourcedFiles]: + tree = ET.parse(psc_file) + root = tree.getroot() + + layers = root.findall(".//Layer") + + layer_info_list: List[SourcedFiles] = [] + for layer in layers: + layer_name = layer.attrib.get("Name") + subfolder = layer.attrib.get("SubFolder") + # Scripts-Filenames + scripts = layer.find("Scripts") + script_names = [] + if scripts is not None: + for filename in scripts.findall("Filename"): + name = filename.attrib.get("Name") + if name: + script_names.append(name) + + layer_info_list.append( + SourcedFiles(layer_name=layer_name, subfolder=subfolder, files=script_names) + ) + return layer_info_list From b680c019890bde4c3a81011a0f0c67ac2ef60c62 Mon Sep 17 00:00:00 2001 From: Christoph Brandau Date: Tue, 5 Aug 2025 18:18:17 +0200 Subject: [PATCH 3/6] update file sourcing --- server/src/lsp_server.py | 33 ++++++++++------------ server/src/tools/file_sourcing.py | 25 ++++++++++++++--- server/src/tools/symbols.py | 46 +++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 23 deletions(-) diff --git a/server/src/lsp_server.py b/server/src/lsp_server.py index b05406e..4946446 100644 --- a/server/src/lsp_server.py +++ b/server/src/lsp_server.py @@ -191,16 +191,16 @@ def did_open(params: lsp.DidOpenTextDocumentParams) -> None: document = LSP_SERVER.workspace.get_text_document(params.text_document.uri) LSP_SERVER.compute_diagnostics(document) completion.reset() - all_tcl_files = get_all_tcl_files(LSP_SERVER.workspace.root_path) + # all_tcl_files = get_all_tcl_files(LSP_SERVER.workspace.root_path) - for filepath in all_tcl_files: - try: - with open(filepath, "r", encoding="utf-8") as f: - source = f.read() - tree = LSP_SERVER.parser.parse(source) - tree.accept(completion, recurse=True) - except Exception as e: - log_to_output(f"Fehler beim Parsen von {filepath}: {e}") + # for filepath in all_tcl_files: + # try: + # with open(filepath, "r", encoding="utf-8") as f: + # source = f.read() + # tree = LSP_SERVER.parser.parse(source) + # tree.accept(completion, recurse=True) + # except Exception as e: + # log_to_output(f"Fehler beim Parsen von {filepath}: {e}") @LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DID_SAVE) @@ -214,16 +214,6 @@ def did_close(params: lsp.DidCloseTextDocumentParams) -> None: """LSP handler for textDocument/didClose request.""" -def get_all_tcl_files(root_path: str) -> list[str]: - """Sammelt alle .tcl-Dateien rekursiv im gegebenen Verzeichnis""" - tcl_files = [] - for dirpath, _, filenames in os.walk(root_path): - for filename in filenames: - if filename.endswith(".tcl"): - tcl_files.append(os.path.join(dirpath, filename)) - return tcl_files - - @LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DID_CHANGE) def did_change(params: lsp.DidChangeTextDocumentParams) -> None: """LSP handler for textDocument/didChange request""" @@ -464,6 +454,11 @@ def initialize(params: lsp.InitializeParams) -> lsp.InitializeResult: ) +@LSP_SERVER.feature(lsp.INITIALIZED) +def initialized(params: lsp.InitializedParams) -> lsp.InitializeResult: + pass + + @LSP_SERVER.feature(lsp.EXIT) def on_exit(_params: Optional[Any] = None) -> None: """Handle clean up on exit.""" diff --git a/server/src/tools/file_sourcing.py b/server/src/tools/file_sourcing.py index 5ed2bbe..1cdeab4 100644 --- a/server/src/tools/file_sourcing.py +++ b/server/src/tools/file_sourcing.py @@ -1,22 +1,23 @@ import xml.etree.ElementTree as ET from dataclasses import dataclass from typing import List, Optional +from pathlib import Path @dataclass -class SourcedFiles: +class SourcedFile: layer_name: str subfolder: Optional[str] files: List[str] -def read_psc_file(psc_file: str) -> List[SourcedFiles]: +def read_psc_file(psc_file: Path) -> List[SourcedFile]: tree = ET.parse(psc_file) root = tree.getroot() layers = root.findall(".//Layer") - layer_info_list: List[SourcedFiles] = [] + layer_info_list: List[SourcedFile] = [] for layer in layers: layer_name = layer.attrib.get("Name") subfolder = layer.attrib.get("SubFolder") @@ -30,6 +31,22 @@ def read_psc_file(psc_file: str) -> List[SourcedFiles]: script_names.append(name) layer_info_list.append( - SourcedFiles(layer_name=layer_name, subfolder=subfolder, files=script_names) + SourcedFile(layer_name=layer_name, subfolder=subfolder, files=script_names) ) return layer_info_list + + +def get_all_psc_files(root_path: Path) -> list[Path]: + """Sammelt alle .tcl-Dateien rekursiv im gegebenen Verzeichnis""" + return [path for path in root_path.rglob("*.psc")] + + +if __name__ == "__main__": + test = get_all_psc_files( + Path( + r"H:\janus-engineering-customers\KSB_Frankenthal\custom\library\machine\installed_machines\ksb_pe_grob_g550_sone\postprocessor" + ) + ) + print(test) + for pp in test: + read_psc_file(pp) diff --git a/server/src/tools/symbols.py b/server/src/tools/symbols.py index e69de29..b34f914 100644 --- a/server/src/tools/symbols.py +++ b/server/src/tools/symbols.py @@ -0,0 +1,46 @@ +import logging +from collections import defaultdict +from typing import List, DefaultDict, Union + +from tclint.syntax_tree import Visitor, Command, CommandSub, Node, Script + + +class SymbolTable: + """Holds a symbol table (links symbols to nodes).""" + + def __init__(self): + self.proc_def: DefaultDict[str, list[Node]] = defaultdict(list) + + def add_proc_definition(self, command: Command) -> None: + """Add definition of procedure""" + # command holds the "proc" keyword, so the proc name is 1st argument + proc_name_node = command.args[0] + proc_name = proc_name_node.contents + if not proc_name: + return + logging.debug( + f"Definition of proc '{proc_name}' at {proc_name_node._pos_str()}" + ) + self.proc_def[proc_name].append(proc_name_node) + + def lookup_proc_definitions(self, symbol_text: str) -> List[Node]: + """Lookup definitions of the procedure pointed at by node""" + if symbol_text is None or symbol_text not in self.proc_def: + return [] + return self.proc_def[symbol_text] + + +class SymbolTableBuilder(Visitor): + """Builds a symbol table.""" + + def __init__(self): + self.table = SymbolTable() + + def build(self, tree: Union[CommandSub, Script]) -> SymbolTable: + """Run the builder visitor through the syntax tree, building a table.""" + tree.accept(self, recurse=True) + return self.table + + def visit_command(self, command: Command) -> None: + if command.routine.contents == "proc": + self.table.add_proc_definition(command) From f5c61267e391fbe42ac799443bc65ef9f870c2f5 Mon Sep 17 00:00:00 2001 From: christoph_xd Date: Tue, 5 Aug 2025 21:45:22 +0200 Subject: [PATCH 4/6] read all procs from psc file --- server/src/lsp_server.py | 49 +++++++++++++++++++------------ server/src/tools/file_sourcing.py | 1 - server/src/tools/formatter.py | 0 3 files changed, 31 insertions(+), 19 deletions(-) create mode 100644 server/src/tools/formatter.py diff --git a/server/src/lsp_server.py b/server/src/lsp_server.py index 4946446..0f45819 100644 --- a/server/src/lsp_server.py +++ b/server/src/lsp_server.py @@ -49,7 +49,7 @@ 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.file_sourcing import get_all_psc_files, read_psc_file, SourcedFile DIAGNOSTIC_SOURCE = "nx-post-support" @@ -61,6 +61,7 @@ class TclLanguageServer(server.LanguageServer): for command in commands: self.parser._commands.update(command) self.diagnostics = {} + self.poco_completion: dict = {} def format( self, @@ -190,17 +191,6 @@ 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() - # all_tcl_files = get_all_tcl_files(LSP_SERVER.workspace.root_path) - - # for filepath in all_tcl_files: - # try: - # with open(filepath, "r", encoding="utf-8") as f: - # source = f.read() - # tree = LSP_SERVER.parser.parse(source) - # tree.accept(completion, recurse=True) - # except Exception as e: - # log_to_output(f"Fehler beim Parsen von {filepath}: {e}") @LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DID_SAVE) @@ -219,9 +209,6 @@ def did_change(params: lsp.DidChangeTextDocumentParams) -> None: """LSP handler for textDocument/didChange 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( @@ -252,12 +239,15 @@ def document_diagnostic(params: lsp.DocumentDiagnosticParams): @LSP_SERVER.feature(lsp.TEXT_DOCUMENT_COMPLETION) def on_completion(params: lsp.CompletionParams) -> list[lsp.CompletionItem]: _ = LSP_SERVER.workspace.get_text_document(params.text_document.uri) + poco = [] + for key, value in LSP_SERVER.poco_completion.items(): + poco.extend(value) items = ( standard_items.tcl_keyword_list + standard_items.nx_procs + standard_items.nx_variables - + completion.custom_functions + + poco ) return lsp.CompletionList(is_incomplete=False, items=items) @@ -455,8 +445,31 @@ def initialize(params: lsp.InitializeParams) -> lsp.InitializeResult: @LSP_SERVER.feature(lsp.INITIALIZED) -def initialized(params: lsp.InitializedParams) -> lsp.InitializeResult: - pass +def initialized(params: lsp.InitializedParams): + root = LSP_SERVER.workspace.root_path + psc_files = get_all_psc_files(pathlib.Path(root)) + for psc_file in psc_files: + poco_files = read_psc_file(psc_file) + for sourced_layer in poco_files: + completion.reset() + try: + file_root = pathlib.Path(root).joinpath( + sourced_layer.subfolder if sourced_layer.subfolder else "" + ) + for tcl_file in sourced_layer.files: + filepath = pathlib.Path(file_root).joinpath(f"{tcl_file}.tcl") + if not filepath.exists(): + continue + completion.reset() + source = filepath.read_text(encoding="utf-8") + tree = LSP_SERVER.parser.parse(source) + tree.accept(completion, recurse=True) + + LSP_SERVER.poco_completion[str(filepath)] = ( + completion.custom_functions + ) + except Exception as e: + log_to_output(f"Fehler beim Parsen von {filepath}: {e}") @LSP_SERVER.feature(lsp.EXIT) diff --git a/server/src/tools/file_sourcing.py b/server/src/tools/file_sourcing.py index 1cdeab4..8db75b3 100644 --- a/server/src/tools/file_sourcing.py +++ b/server/src/tools/file_sourcing.py @@ -37,7 +37,6 @@ def read_psc_file(psc_file: Path) -> List[SourcedFile]: def get_all_psc_files(root_path: Path) -> list[Path]: - """Sammelt alle .tcl-Dateien rekursiv im gegebenen Verzeichnis""" return [path for path in root_path.rglob("*.psc")] diff --git a/server/src/tools/formatter.py b/server/src/tools/formatter.py new file mode 100644 index 0000000..e69de29 From 6bded783a14212804c0e514ae2f65be16662f0b3 Mon Sep 17 00:00:00 2001 From: Christoph Brandau Date: Wed, 6 Aug 2025 15:26:53 +0200 Subject: [PATCH 5/6] add functions to completion --- .vscode/settings.json | 3 +- server/src/_debug_server.py | 2 +- server/src/lsp_server.py | 72 ++++++++++------------------ server/src/tools/completion_items.py | 50 +++++++++++++++---- server/src/tools/semantic_tokens.py | 37 ++++++-------- 5 files changed, 86 insertions(+), 78 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index e4943fd..6a26b6a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,6 +2,7 @@ "ruff.configuration": { "lint": { "extend-ignore": ["F821", "E402"] - } + }, + "line-length": 200 } } diff --git a/server/src/_debug_server.py b/server/src/_debug_server.py index c4c6852..52d3f0c 100644 --- a/server/src/_debug_server.py +++ b/server/src/_debug_server.py @@ -32,7 +32,7 @@ if debugger_path: # This will ensure that execution is paused as soon as the debugger # connects to VS Code. If you don't want to pause here comment this # line and set breakpoints as appropriate. - debugpy.breakpoint() + # debugpy.breakpoint() SERVER_PATH = os.fspath(pathlib.Path(__file__).parent / "lsp_server.py") # NOTE: Set breakpoint in `lsp_server.py` before continuing. diff --git a/server/src/lsp_server.py b/server/src/lsp_server.py index 0f45819..27d8d65 100644 --- a/server/src/lsp_server.py +++ b/server/src/lsp_server.py @@ -47,9 +47,9 @@ 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.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, SourcedFile +from tools.file_sourcing import get_all_psc_files, read_psc_file DIAGNOSTIC_SOURCE = "nx-post-support" @@ -62,6 +62,7 @@ class TclLanguageServer(server.LanguageServer): self.parser._commands.update(command) self.diagnostics = {} self.poco_completion: dict = {} + self.proc_signatures: dict = {} def format( self, @@ -123,12 +124,8 @@ class TclLanguageServer(server.LanguageServer): for violation in violations: message = violation.message severity = lsp.DiagnosticSeverity.Warning - start = lsp.Position( - line=violation.start[0] - 1, character=violation.start[1] - 1 - ) - end = lsp.Position( - line=violation.end[0] - 1, character=violation.end[1] - 1 - ) + start = lsp.Position(line=violation.start[0] - 1, character=violation.start[1] - 1) + end = lsp.Position(line=violation.end[0] - 1, character=violation.end[1] - 1) diagnostics.append( lsp.Diagnostic( @@ -166,9 +163,7 @@ RUNNER = pathlib.Path(__file__).parent / "lsp_runner.py" MAX_WORKERS = 5 -LSP_SERVER = TclLanguageServer( - name="NX Postprocessor Support", version="0.0.1", max_workers=MAX_WORKERS -) +LSP_SERVER = TclLanguageServer(name="NX Postprocessor Support", version="0.0.1", max_workers=MAX_WORKERS) # ********************************************************** # Tool specific code goes below this. @@ -243,12 +238,7 @@ def on_completion(params: lsp.CompletionParams) -> list[lsp.CompletionItem]: for key, value in LSP_SERVER.poco_completion.items(): poco.extend(value) - items = ( - standard_items.tcl_keyword_list - + standard_items.nx_procs - + standard_items.nx_variables - + poco - ) + items = standard_items.tcl_keyword_list + standard_items.nx_procs + standard_items.nx_variables + poco return lsp.CompletionList(is_incomplete=False, items=items) @@ -267,10 +257,12 @@ def inlay_hints(params: lsp.InlayHintParams): tree = LSP_SERVER.parser.parse(document.source) # collect Inlay Hints - generator = InlayHintGenerator(completion.proc_signatures) - tree.accept(generator, recurse=True) - - return generator.hints + hints = [] + for key, value in LSP_SERVER.proc_signatures.items(): + generator = InlayHintGenerator(LSP_SERVER.proc_signatures[key]) + tree.accept(generator, recurse=True) + hints += generator.hints + return hints @LSP_SERVER.feature( @@ -285,7 +277,7 @@ def semantic_tokens(params: lsp.SemanticTokensParams): data = [] plugins = [] - hl = _Highlighter(plugins, log_to_output=log_to_output) + hl = _Highlighter(plugins, LSP_SERVER.poco_completion) tree = LSP_SERVER.parser.parse(document.source) tree.accept(hl, recurse=True) @@ -338,9 +330,7 @@ def hover(params: lsp.HoverParams) -> lsp.Hover: label = match.get("label", "") parameters = match.get("parameters", []) - param_lines = ( - "\n".join(f"- `{p['name']}`: {p['desc']}" for p in parameters) or "_None_" - ) + param_lines = "\n".join(f"- `{p['name']}`: {p['desc']}" for p in parameters) or "_None_" example_data = match.get("example", []) example_md = "\n".join(f"{line}" for line in example_data) @@ -424,12 +414,8 @@ def initialize(params: lsp.InitializeParams) -> lsp.InitializeResult: settings = params.initialization_options["settings"] _update_workspace_settings(settings) - log_to_output( - f"Settings used to run Server:\r\n{json.dumps(settings, indent=4, ensure_ascii=False)}\r\n" - ) - log_to_output( - f"Global settings:\r\n{json.dumps(GLOBAL_SETTINGS, indent=4, ensure_ascii=False)}\r\n" - ) + log_to_output(f"Settings used to run Server:\r\n{json.dumps(settings, indent=4, ensure_ascii=False)}\r\n") + log_to_output(f"Global settings:\r\n{json.dumps(GLOBAL_SETTINGS, indent=4, ensure_ascii=False)}\r\n") semantic_tokens_legend = lsp.SemanticTokensLegend( token_types=TOKEN_TYPES, token_modifiers=TokenModifier, @@ -437,9 +423,7 @@ def initialize(params: lsp.InitializeParams) -> lsp.InitializeResult: return lsp.InitializeResult( capabilities=lsp.ServerCapabilities( document_formatting_provider=GLOBAL_SETTINGS.get("formatter", True), - semantic_tokens_provider=lsp.SemanticTokensOptions( - legend=semantic_tokens_legend, full=True, range=False - ), + semantic_tokens_provider=lsp.SemanticTokensOptions(legend=semantic_tokens_legend, full=True, range=False), ) ) @@ -453,21 +437,19 @@ def initialized(params: lsp.InitializedParams): for sourced_layer in poco_files: completion.reset() try: - file_root = pathlib.Path(root).joinpath( - sourced_layer.subfolder if sourced_layer.subfolder else "" - ) + file_root = pathlib.Path(root).joinpath(sourced_layer.subfolder if sourced_layer.subfolder else "") for tcl_file in sourced_layer.files: filepath = pathlib.Path(file_root).joinpath(f"{tcl_file}.tcl") if not filepath.exists(): continue completion.reset() - source = filepath.read_text(encoding="utf-8") - tree = LSP_SERVER.parser.parse(source) + document = LSP_SERVER.workspace.get_text_document(filepath.as_uri()) # filepath.read_text(encoding="utf-8") + tree = LSP_SERVER.parser.parse(document.source) tree.accept(completion, recurse=True) - - LSP_SERVER.poco_completion[str(filepath)] = ( - completion.custom_functions - ) + remove_existing_items(completion.custom_functions, LSP_SERVER.poco_completion) + LSP_SERVER.poco_completion[str(filepath)] = completion.custom_functions + remove_shared_keys(LSP_SERVER.proc_signatures, completion.proc_signatures) + LSP_SERVER.proc_signatures[str(filepath)] = completion.proc_signatures except Exception as e: log_to_output(f"Fehler beim Parsen von {filepath}: {e}") @@ -563,9 +545,7 @@ def _get_settings_by_document(document: workspace.Document | None): # ***************************************************** # Logging and notification. # ***************************************************** -def log_to_output( - message: str, msg_type: lsp.MessageType = lsp.MessageType.Log -) -> None: +def log_to_output(message: str, msg_type: lsp.MessageType = lsp.MessageType.Log) -> None: LSP_SERVER.show_message_log(message, msg_type) diff --git a/server/src/tools/completion_items.py b/server/src/tools/completion_items.py index 9684812..f5b42fe 100644 --- a/server/src/tools/completion_items.py +++ b/server/src/tools/completion_items.py @@ -1,5 +1,6 @@ from tclint.syntax_tree import Visitor, Command, BareWord, List import lsprotocol.types as lsp +from common.load_data import standard_items class CompletionItems: @@ -38,14 +39,18 @@ class _Completion(Visitor): if routine.contents == "proc" and command.args: first_arg = command.args[0] - if hasattr(first_arg, "value") and not any( - item.label == first_arg.value for item in self._custom_functions - ): - self._custom_functions.append( - lsp.CompletionItem( - label=first_arg.value, kind=lsp.CompletionItemKind.Function - ) - ) + if not first_arg.value: + return + + if any(item.label == first_arg.value for item in standard_items.nx_procs): + return + + try: + self._custom_functions.remove(first_arg.value) + except ValueError: + pass + + self._custom_functions.append(lsp.CompletionItem(label=first_arg.value, kind=lsp.CompletionItemKind.Function)) if len(command.args) < 2: return @@ -65,4 +70,33 @@ class _Completion(Visitor): self._proc_signatures[first_arg.value] = param_names +def remove_existing_items(items: list[lsp.CompletionItem], store: dict) -> None: + """ + Entfernt alle CompletionItems aus dem store, deren label in der items-Liste vorkommt. + Änderungen erfolgen in-place. + """ + labels_to_remove = {item.label for item in items} + + for key in list(store.keys()): + filtered = [ci for ci in store[key] if ci.label not in labels_to_remove] + if filtered: + store[key] = filtered + else: + del store[key] + + +def remove_shared_keys(nested_dict: dict[str, dict[str, list]], flat_dict: dict[str, list]) -> None: + """ + Entfernt alle Keys aus nested_dict[file][func], wenn func auch in flat_dict vorhanden ist. + Änderungen erfolgen in-place. + """ + for file_path, func_dict in list(nested_dict.items()): + for func_name in list(func_dict.keys()): + if func_name in flat_dict: + del nested_dict[file_path][func_name] + + if not nested_dict[file_path]: + del nested_dict[file_path] + + completion = _Completion() diff --git a/server/src/tools/semantic_tokens.py b/server/src/tools/semantic_tokens.py index 80d4197..9c5eb4f 100644 --- a/server/src/tools/semantic_tokens.py +++ b/server/src/tools/semantic_tokens.py @@ -5,6 +5,7 @@ from tclint.commands import get_commands import attrs from common.load_data import standard_items from tools.completion_items import completion +import lsprotocol.types as lsp class TokenModifier(enum.IntFlag): @@ -40,10 +41,10 @@ TOKEN_TYPES = [ class _Highlighter(Visitor): - def __init__(self, plugins, log_to_output): + def __init__(self, plugins, custom_functions: dict[str : list[lsp.CompletionItem]]): self._commands = get_commands(plugins) self._tokens = [] - self.log_to_output = log_to_output + self.custom_functions = custom_functions def _get_token_info(self, node): """Hilfsmethode um Token-Informationen aus verschiedenen Node-Typen zu extrahieren.""" @@ -58,11 +59,7 @@ class _Highlighter(Visitor): # CompoundBareWord: versuche erstes Segment if hasattr(node, "children") and node.children: first_segment = node.children[0] - if ( - hasattr(first_segment, "value") - and first_segment.value is not None - and hasattr(first_segment, "pos") - ): + if hasattr(first_segment, "value") and first_segment.value is not None and hasattr(first_segment, "pos"): line, col = first_segment.pos return (line - 1, col - 1), len(first_segment.value) @@ -84,13 +81,15 @@ class _Highlighter(Visitor): pass def visit_bare_word(self, word: BareWord): - if any(item.label == word.value for item in standard_items.nx_procs) or any( - item.label == word.value for item in completion.custom_functions - ): + name = word.value + + in_standard = any(item.label == name for item in standard_items.nx_procs) + + in_custom = any(item.label == name for items in self.custom_functions.values() for item in items) + + if in_standard or in_custom: line, col = word.pos - self._tokens.append( - (((line - 1, col - 1), len(word.value), "function", [])) - ) + self._tokens.append((((line - 1, col - 1), len(name), "function", []))) def visit_command(self, command: Command): routine = command.routine @@ -158,9 +157,7 @@ class _Highlighter(Visitor): # Parameter mit Default-Wert ist meist eine List (z. B. {arg default}) elif hasattr(child, "children") and len(child.children) >= 1: name_node = child.children[0] - if hasattr(name_node, "value") and hasattr( - name_node, "pos" - ): + if hasattr(name_node, "value") and hasattr(name_node, "pos"): line, col = name_node.pos self._tokens.append( ( @@ -174,9 +171,7 @@ class _Highlighter(Visitor): first_arg = command.args[1] if hasattr(first_arg, "pos") and first_arg.value is not None: line, col = first_arg.pos - self._tokens.append( - (((line - 1, col - 1), len(first_arg.value), "class", [])) - ) + self._tokens.append((((line - 1, col - 1), len(first_arg.value), "class", []))) def tokens(self) -> list[Token]: """Encode tokens as described in @@ -185,9 +180,7 @@ class _Highlighter(Visitor): tokens = [] last_line = 0 last_col = 0 - for (line, col), length, tok_type, tok_modifier in sorted( - self._tokens, key=lambda x: x[0] - ): + for (line, col), length, tok_type, tok_modifier in sorted(self._tokens, key=lambda x: x[0]): line_delta = line - last_line col_delta = col if line == last_line: From 8be9aae05dd86ab8735342ec35bc6d2791703885 Mon Sep 17 00:00:00 2001 From: christoph_xd Date: Wed, 6 Aug 2025 20:09:15 +0200 Subject: [PATCH 6/6] update pipline --- .gitea/workflows/build_and_puplish.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/build_and_puplish.yml b/.gitea/workflows/build_and_puplish.yml index 29bf449..ea15e4d 100644 --- a/.gitea/workflows/build_and_puplish.yml +++ b/.gitea/workflows/build_and_puplish.yml @@ -1,7 +1,7 @@ on: push: tags: - - "*" + - "*" jobs: build_and_publish: runs-on: ubuntu-latest @@ -35,7 +35,13 @@ jobs: git add package.json git commit -m "Update version to ${{ github.ref_name }}" git push origin HEAD:main - - name: Publish to Visual Studio Marketplace + - name: Publish to Visual Studio Marketplace (Pre-Release) + if: contains(github.ref_name, '-') + run: vsce publish --pre-release + env: + VSCE_PAT: ${{ secrets.VSCODE_MARKETPALCE }} + - name: Publish to Visual Studio Marketplace (Stable) + if: ${{ !contains(github.ref_name, '-') }} run: vsce publish env: VSCE_PAT: ${{ secrets.VSCODE_MARKETPALCE }}