101 lines
3.4 KiB
Python
101 lines
3.4 KiB
Python
import re
|
|
from dataclasses import dataclass
|
|
from typing import Dict, Set, List, Tuple
|
|
|
|
# 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\}]+)")
|
|
|
|
|
|
@dataclass
|
|
class ProcRange:
|
|
name: str
|
|
start_line: int
|
|
end_line: int | None = None
|
|
|
|
|
|
def build_variable_index(source: str) -> 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)
|
|
- 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
|
|
- 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
|
|
|
|
globals_set: Set[str] = set()
|
|
procs: Dict[str, Set[str]] = {}
|
|
proc_ranges: List[ProcRange] = []
|
|
|
|
scope_stack: List[Scope] = [Scope("", "root", 0)]
|
|
|
|
for i, line in enumerate(lines):
|
|
ns_match = NS_RE.match(line)
|
|
proc_match = PROC_RE.match(line)
|
|
set_match = SET_RE.match(line)
|
|
|
|
# Namespace scope
|
|
if ns_match:
|
|
scope_stack.append(Scope(ns_match.group(1), "namespace", i))
|
|
|
|
# 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))
|
|
|
|
# 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
|
|
|
|
# Brace balancing for current top scope
|
|
open_count = line.count("{")
|
|
close_count = line.count("}")
|
|
scope_stack[-1].brace_count += open_count - close_count
|
|
|
|
# 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
|
|
|
|
return globals_set, procs, proc_ranges
|
|
|