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: