import enum from typing import List from tclint.syntax_tree import Visitor, QuotedWord, Command, BareWord from tclint.commands.plugins import PluginManager import attrs from common.load_data import standard_items import lsprotocol.types as lsp # Constructing a PluginManager scans entry points, and get_commands() rebuilds # the builtin command set on every call. Semantic tokens are requested often, so # cache the manager and the resolved commands per plugin set. _PLUGIN_MANAGER = None _COMMANDS_CACHE = {} def _load_commands(plugins): global _PLUGIN_MANAGER if _PLUGIN_MANAGER is None: _PLUGIN_MANAGER = PluginManager() key = tuple(plugins) if key not in _COMMANDS_CACHE: _COMMANDS_CACHE[key] = _PLUGIN_MANAGER.get_commands(list(plugins)) return _COMMANDS_CACHE[key] class TokenModifier(enum.IntFlag): deprecated = enum.auto() readonly = enum.auto() defaultLibrary = enum.auto() definition = enum.auto() declaration = enum.auto() builtin = enum.auto() @attrs.define class Token: line: int offset: int lenght: int tok_type: str = "" tok_modifiers: List[TokenModifier] = attrs.field(factory=list) @property def length(self) -> int: """Compatibility alias for misspelled 'lenght' field.""" return self.lenght TOKEN_TYPES = [ "keyword", "comment", "variable", "function", "operator", "parameter", "type", "class", "string", "parameter", ] class _Highlighter(Visitor): def __init__(self, plugins, custom_functions: dict[str : list[lsp.CompletionItem]]): self._commands = _load_commands(plugins) self._tokens = [] self.custom_functions = custom_functions def _append_token(self, position, length: int, tok_type: str, modifiers: List[TokenModifier] | None = None): if position is None or length <= 0: return self._tokens.append((position, length, tok_type, modifiers or [])) def _get_token_info(self, node): """Hilfsmethode um Token-Informationen aus verschiedenen Node-Typen zu extrahieren.""" if not hasattr(node, "pos"): return None # Einfacher Fall: Node hat direkten value if hasattr(node, "value") and node.value is not None: line, col = node.pos return (line - 1, col - 1), len(node.value) # 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"): line, col = first_segment.pos return (line - 1, col - 1), len(first_segment.value) # Fallback: Gesamtlänge aus Positionen berechnen if hasattr(node, "end_pos"): start_line, start_col = node.pos end_line, end_col = node.end_pos if start_line == end_line: length = end_col - start_col return (start_line - 1, start_col - 1), length return None def visit_quoted_word(self, word: QuotedWord): if not word.contents: return line, col = word.contents_pos self._append_token((line - 1, col - 1), len(word.contents), "string", []) def visit_comment(self, comment): if not hasattr(comment, "pos") or comment.pos is None or comment.end_pos is None: return start_line, start_col = comment.pos end_line, end_col = comment.end_pos if start_line != end_line: return self._append_token((start_line - 1, start_col - 1), end_col - start_col, "comment", []) def visit_bare_word(self, word: BareWord): # Intentionally do not classify bare words as functions here. # Function highlighting is handled in visit_command for the routine only, # using completion items (custom functions) as the source of truth. return def visit_command(self, command: Command): routine = command.routine # Highlight functions (custom or standard) when used as the routine name = getattr(routine, "contents", None) if name: in_custom = any(item.label == name for items in self.custom_functions.values() for item in items) in_standard = any(item.label == name for item in standard_items.nx_procs) if in_custom or in_standard: line, col = routine.contents_pos self._append_token((line - 1, col - 1), len(name), "function", []) if routine.contents in {"global", "variable"}: line, col = routine.contents_pos self._append_token((line - 1, col - 1), len(routine.contents), "keyword", []) for arg in command.args: token_info = self._get_token_info(arg) if token_info: (arg_line, arg_col), length = token_info self._append_token((arg_line, arg_col), length, "variable", []) if routine.contents == "puts": line, col = routine.contents_pos self._append_token((line - 1, col - 1), len(routine.contents), "function", [TokenModifier.builtin]) if routine.contents == "set" and command.args: first_arg = command.args[0] token_info = self._get_token_info(first_arg) if token_info: (line, col), length = token_info self._append_token((line, col), length, "variable", [TokenModifier.declaration]) if routine.contents == "proc" and command.args: first_arg = command.args[0] if hasattr(first_arg, "pos") and hasattr(first_arg, "value"): line, col = first_arg.pos self._append_token((line - 1, col - 1), len(first_arg.value), "function", [TokenModifier.declaration]) if len(command.args) >= 2: param_list = command.args[1] # BracedWord oder Liste erwartet if hasattr(param_list, "children"): for child in param_list.children: # Parameter kann einfaches Wort sein if hasattr(child, "value") and child.value is not None: line, col = child.pos self._append_token((line - 1, col - 1), len(child.value), "parameter", [TokenModifier.declaration]) # 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"): line, col = name_node.pos self._append_token((line - 1, col - 1), len(name_node.value), "parameter", [TokenModifier.declaration]) if routine.contents == "namespace" and command.args: first_arg = command.args[1] if hasattr(first_arg, "pos") and first_arg.value is not None: line, col = first_arg.pos self._append_token((line - 1, col - 1), len(first_arg.value), "class", []) def tokens(self) -> list[Token]: """Encode tokens as described in https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_semanticTokens. """ tokens = [] last_line = 0 last_col = 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: col_delta -= last_col tokens.append(Token(line_delta, col_delta, length, tok_type, tok_modifier)) last_line, last_col = line, col return tokens