Files
nx_post_support/server/src/tools/semantic_tokens.py
T

201 lines
7.3 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
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):
# 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._tokens.append((((line - 1, col - 1), len(name), "function", [])))
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