add new features and fix bugs

This commit is contained in:
Christoph Brandau
2026-06-18 17:31:51 +02:00
parent 41f331d4c4
commit 3ad3847045
14 changed files with 988 additions and 171 deletions
+62 -66
View File
@@ -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