add new features and fix bugs
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List
|
||||
|
||||
import lsprotocol.types as lsp
|
||||
from tclint.syntax_tree import Command, Node, Script
|
||||
|
||||
|
||||
def _add_range(
|
||||
ranges: list[lsp.FoldingRange],
|
||||
seen: set[tuple[int, int]],
|
||||
start_line: int,
|
||||
end_line: int,
|
||||
) -> None:
|
||||
if start_line >= end_line:
|
||||
return
|
||||
|
||||
key = (start_line, end_line)
|
||||
if key in seen:
|
||||
return
|
||||
|
||||
seen.add(key)
|
||||
ranges.append(
|
||||
lsp.FoldingRange(
|
||||
start_line=start_line,
|
||||
end_line=end_line,
|
||||
kind=lsp.FoldingRangeKind.Region,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def build_folding_ranges(tree: Node) -> List[lsp.FoldingRange]:
|
||||
ranges: list[lsp.FoldingRange] = []
|
||||
seen: set[tuple[int, int]] = set()
|
||||
|
||||
def walk(node: Node) -> None:
|
||||
if isinstance(node, Command):
|
||||
previous_node: Node = node.routine
|
||||
for arg in node.args:
|
||||
if isinstance(arg, Script):
|
||||
anchor = getattr(previous_node, "pos", None) or getattr(
|
||||
node, "pos", None
|
||||
)
|
||||
if anchor is not None and arg.end_pos is not None:
|
||||
_add_range(ranges, seen, anchor[0] - 1, arg.end_pos[0] - 1)
|
||||
previous_node = arg
|
||||
|
||||
for child in getattr(node, "children", []):
|
||||
walk(child)
|
||||
|
||||
walk(tree)
|
||||
return sorted(ranges, key=lambda item: (item.start_line, item.end_line))
|
||||
@@ -1,7 +1,7 @@
|
||||
import enum
|
||||
from typing import List
|
||||
from tclint.syntax_tree import Visitor, QuotedWord, Command, BareWord
|
||||
from tclint.commands import get_commands
|
||||
from tclint.commands.plugins import PluginManager
|
||||
import attrs
|
||||
from common.load_data import standard_items
|
||||
import lsprotocol.types as lsp
|
||||
@@ -33,6 +33,7 @@ class Token:
|
||||
|
||||
TOKEN_TYPES = [
|
||||
"keyword",
|
||||
"comment",
|
||||
"variable",
|
||||
"function",
|
||||
"operator",
|
||||
@@ -46,10 +47,15 @@ TOKEN_TYPES = [
|
||||
|
||||
class _Highlighter(Visitor):
|
||||
def __init__(self, plugins, custom_functions: dict[str : list[lsp.CompletionItem]]):
|
||||
self._commands = get_commands(plugins)
|
||||
self._commands = PluginManager().get_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"):
|
||||
@@ -81,8 +87,18 @@ class _Highlighter(Visitor):
|
||||
if not word.contents:
|
||||
return
|
||||
line, col = word.contents_pos
|
||||
self._tokens.append(((line - 1, col - 1), len(word.contents), "string", []))
|
||||
pass
|
||||
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.
|
||||
@@ -100,49 +116,32 @@ class _Highlighter(Visitor):
|
||||
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", [])))
|
||||
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._tokens.append(
|
||||
(
|
||||
(
|
||||
(line - 1, col - 1),
|
||||
len(routine.contents),
|
||||
"function",
|
||||
[TokenModifier.builtin],
|
||||
)
|
||||
)
|
||||
)
|
||||
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._tokens.append(
|
||||
(
|
||||
(
|
||||
(line, col),
|
||||
length,
|
||||
"variable",
|
||||
[TokenModifier.declaration],
|
||||
)
|
||||
)
|
||||
)
|
||||
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._tokens.append(
|
||||
(
|
||||
(
|
||||
(line - 1, col - 1),
|
||||
len(first_arg.value),
|
||||
"function",
|
||||
[TokenModifier.declaration],
|
||||
)
|
||||
)
|
||||
)
|
||||
self._append_token((line - 1, col - 1), len(first_arg.value), "function", [TokenModifier.declaration])
|
||||
|
||||
if len(command.args) >= 2:
|
||||
param_list = command.args[1]
|
||||
@@ -153,33 +152,19 @@ class _Highlighter(Visitor):
|
||||
# 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],
|
||||
)
|
||||
)
|
||||
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._tokens.append(
|
||||
(
|
||||
(line - 1, col - 1),
|
||||
len(name_node.value),
|
||||
"parameter",
|
||||
[TokenModifier.declaration],
|
||||
)
|
||||
)
|
||||
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._tokens.append((((line - 1, col - 1), len(first_arg.value), "class", [])))
|
||||
self._append_token((line - 1, col - 1), len(first_arg.value), "class", [])
|
||||
|
||||
def tokens(self) -> list[Token]:
|
||||
"""Encode tokens as described in
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, Set, List, Tuple
|
||||
from __future__ import annotations
|
||||
|
||||
# Reuse patterns similar to document_symbols
|
||||
NS_RE = re.compile(r"^\s*namespace\s+eval\s+([^\s\{]+)")
|
||||
PROC_RE = re.compile(r"^\s*proc\s+([^\s\{]+)\s+\{.*\}\s+\{")
|
||||
SET_RE = re.compile(r"^\s*set\s+([^\s\}]+)")
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, List, Set
|
||||
|
||||
from tclint.syntax_tree import Command, Node, Script
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -15,86 +13,84 @@ class ProcRange:
|
||||
end_line: int | None = None
|
||||
|
||||
|
||||
def build_variable_index(source: str) -> tuple[Set[str], Dict[str, Set[str]], List[ProcRange]]:
|
||||
def _normalize_var_name(raw_name: str | None) -> str | None:
|
||||
if not raw_name:
|
||||
return None
|
||||
|
||||
base = raw_name.split("(", 1)[0]
|
||||
if base.startswith("::"):
|
||||
base = base[2:]
|
||||
|
||||
return base or None
|
||||
|
||||
|
||||
def build_variable_index(source: str, tree: Node | None = None) -> tuple[Set[str], Dict[str, Set[str]], List[ProcRange]]:
|
||||
"""
|
||||
Parse Tcl source text and build:
|
||||
- globals: set of variable names considered global suggestions
|
||||
- procs: mapping proc_name -> set of local variable names (set without :: inside that proc)
|
||||
- procs: mapping proc_name -> set of local variable names inside that proc
|
||||
- proc_ranges: list of ProcRange (name, start_line, end_line)
|
||||
|
||||
Rules:
|
||||
- set ::var -> global var suggestion (strip leading :: and any array index "(")
|
||||
- set var without :: at top level (not in namespace/proc) -> global suggestion
|
||||
- set var without :: inside proc -> local to that proc
|
||||
- global var1 var2 inside a proc -> global suggestions for those names
|
||||
- set var inside namespace (no ::) is ignored for global suggestions
|
||||
"""
|
||||
lines = source.split("\n")
|
||||
|
||||
class Scope:
|
||||
def __init__(self, name: str, kind: str, start_line: int):
|
||||
self.name = name
|
||||
self.kind = kind # "namespace" or "proc" or "root"
|
||||
self.start_line = start_line
|
||||
self.brace_count = 0
|
||||
_ = source # Kept for signature compatibility with callers.
|
||||
|
||||
globals_set: Set[str] = set()
|
||||
procs: Dict[str, Set[str]] = {}
|
||||
proc_ranges: List[ProcRange] = []
|
||||
|
||||
scope_stack: List[Scope] = [Scope("", "root", 0)]
|
||||
if tree is None:
|
||||
return globals_set, procs, proc_ranges
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
ns_match = NS_RE.match(line)
|
||||
proc_match = PROC_RE.match(line)
|
||||
set_match = SET_RE.match(line)
|
||||
def walk(node: Node, scope_stack: list[tuple[str, str]]) -> None:
|
||||
if isinstance(node, Command):
|
||||
routine = getattr(node.routine, "contents", None)
|
||||
|
||||
# Namespace scope
|
||||
if ns_match:
|
||||
scope_stack.append(Scope(ns_match.group(1), "namespace", i))
|
||||
if routine == "proc" and len(node.args) >= 3 and isinstance(node.args[2], Script):
|
||||
proc_name = _normalize_var_name(getattr(node.args[0], "contents", None))
|
||||
if proc_name is not None:
|
||||
procs.setdefault(proc_name, set())
|
||||
proc_ranges.append(
|
||||
ProcRange(
|
||||
name=proc_name,
|
||||
start_line=node.pos[0] - 1,
|
||||
end_line=node.args[2].end_pos[0] - 1,
|
||||
)
|
||||
)
|
||||
walk(node.args[2], [*scope_stack, ("proc", proc_name)])
|
||||
return
|
||||
|
||||
# Proc scope
|
||||
elif proc_match:
|
||||
pname = proc_match.group(1)
|
||||
scope_stack.append(Scope(pname, "proc", i))
|
||||
proc_ranges.append(ProcRange(name=pname, start_line=i, end_line=None))
|
||||
if routine == "namespace" and len(node.args) >= 3 and getattr(node.args[0], "contents", None) == "eval" and isinstance(node.args[2], Script):
|
||||
namespace_name = _normalize_var_name(getattr(node.args[1], "contents", None)) or ""
|
||||
walk(node.args[2], [*scope_stack, ("namespace", namespace_name)])
|
||||
return
|
||||
|
||||
# Track set statements
|
||||
if set_match:
|
||||
raw_name = set_match.group(1)
|
||||
# Normalize array names and leading ::
|
||||
base = raw_name.split("(", 1)[0]
|
||||
if base.startswith("::"):
|
||||
clean = base[2:]
|
||||
globals_set.add(clean)
|
||||
else:
|
||||
top = scope_stack[-1]
|
||||
if top.kind == "root":
|
||||
globals_set.add(base)
|
||||
elif top.kind == "proc":
|
||||
procs.setdefault(top.name, set()).add(base)
|
||||
else:
|
||||
# inside namespace without :: -> ignore for globals
|
||||
pass
|
||||
if routine == "set" and node.args:
|
||||
raw_name = getattr(node.args[0], "contents", None)
|
||||
base = _normalize_var_name(raw_name)
|
||||
if base is not None:
|
||||
if raw_name and raw_name.startswith("::"):
|
||||
globals_set.add(base)
|
||||
else:
|
||||
scope_kind, scope_name = scope_stack[-1]
|
||||
if scope_kind == "root":
|
||||
globals_set.add(base)
|
||||
elif scope_kind == "proc":
|
||||
procs.setdefault(scope_name, set()).add(base)
|
||||
|
||||
# Brace balancing for current top scope
|
||||
open_count = line.count("{")
|
||||
close_count = line.count("}")
|
||||
scope_stack[-1].brace_count += open_count - close_count
|
||||
if routine == "global":
|
||||
for arg in node.args:
|
||||
base = _normalize_var_name(getattr(arg, "contents", None))
|
||||
if base is not None:
|
||||
globals_set.add(base)
|
||||
|
||||
# Close finished scopes
|
||||
while len(scope_stack) > 1 and scope_stack[-1].brace_count <= 0:
|
||||
finished = scope_stack.pop()
|
||||
if finished.kind == "proc":
|
||||
# Update the last matching proc range end_line
|
||||
for pr in reversed(proc_ranges):
|
||||
if pr.name == finished.name and pr.end_line is None:
|
||||
pr.end_line = i
|
||||
break
|
||||
|
||||
# Finalize any unterminated proc ranges
|
||||
for pr in proc_ranges:
|
||||
if pr.end_line is None:
|
||||
pr.end_line = len(lines) - 1
|
||||
for child in getattr(node, "children", []):
|
||||
walk(child, scope_stack)
|
||||
|
||||
walk(tree, [("root", "")])
|
||||
return globals_set, procs, proc_ranges
|
||||
|
||||
|
||||
Reference in New Issue
Block a user