import enum from typing import List from tclint.syntax_tree import Visitor, QuotedWord, Command, BareWord 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): 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", "variable", "function", "operator", "parameter", "type", "class", "string", "parameter", ] class _Highlighter(Visitor): def __init__(self, plugins, custom_functions: dict[str : list[lsp.CompletionItem]]): self._commands = get_commands(plugins) self._tokens = [] self.custom_functions = custom_functions 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._tokens.append(((line - 1, col - 1), len(word.contents), "string", [])) pass def visit_bare_word(self, word: BareWord): 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(name), "function", []))) 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], ) ) ) 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._tokens.append( ( ( (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._tokens.append( ( ( (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._tokens.append( ( (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._tokens.append( ( (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._tokens.append((((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