Files
nx_post_support/server/src/tools/semantic_tokens.py
T
Christoph Brandau f60c4563e4 Delegate TCL folding to server, fix bugs, and enhance stability
- Move TCL folding range computation from client to language server, avoiding duplicate regions.
- Serialize language server restarts to prevent multiple server instances running concurrently.
- Pin server-side Python dependencies to specific minor versions for improved stability and predictability.
- Make debug server connection non-fatal, allowing the language server to start even if the debugger isn't attached.
- Optimize semantic token generation by caching Tclint plugin commands.
2026-06-18 20:38:47 +02:00

203 lines
7.8 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.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