from __future__ import annotations import re import lsprotocol.types as lsp # Precompiled regex patterns for performance 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\}]+)") EVENT_START_RE = re.compile(r"^\s*LIB_GE_command_buffer_edit_(prepend|append|insert|replace)\b") EVENT_CLOSE_INLINE_RE = re.compile(r"^\s*\}\s*(\S+)\s*.*$") EVENT_NAME_LINE_RE = re.compile(r"^\s*([A-Za-z_][A-Za-z0-9_]*)\b") def build_document_symbols(source: str) -> list[lsp.DocumentSymbol]: """Parse a Tcl document source and return DocumentSymbols. This mirrors the previous inline implementation in lsp_server, but is extracted for readability and reuse. """ lines = source.split("\n") class Scope: def __init__(self, name: str, symbol: lsp.DocumentSymbol, start_line: int): self.name = name self.symbol = symbol self.start_line = start_line self.brace_count = 0 root_symbol = lsp.DocumentSymbol( name="root", detail="", kind=lsp.SymbolKind.Namespace, range=lsp.Range(start=lsp.Position(0, 0), end=lsp.Position(len(lines), 0)), selection_range=lsp.Range(start=lsp.Position(0, 0), end=lsp.Position(0, 0)), children=[], ) scope_stack: list[Scope] = [Scope("", root_symbol, 0)] pending_event: dict | None = None for i, line in enumerate(lines): # If inside an event, update its brace count for this line if pending_event is not None: pe_open = line.count("{") pe_close = line.count("}") pending_event["brace_count"] = pending_event.get("brace_count", 1) + pe_open - pe_close # If this line closed the outer event block, finalize the event if pending_event["brace_count"] <= 0: # Try inline name on the same line close_inline = EVENT_CLOSE_INLINE_RE.match(line) if close_inline: event_name = close_inline.group(1) name_line_index = i else: # Look ahead to next non-empty line for the name j = i + 1 while j < len(lines) and lines[j].strip() == "": j += 1 event_name = None name_line_index = i if j < len(lines): name_line = lines[j] name_match = EVENT_NAME_LINE_RE.match(name_line) if name_match: event_name = name_match.group(1) name_line_index = j if event_name: start_line = pending_event["start"] edit_type = pending_event["edit_type"] children = pending_event.get("children", []) detail = f"Event ({edit_type})" event_symbol = lsp.DocumentSymbol( name=event_name, detail=detail, kind=lsp.SymbolKind.Event, range=lsp.Range(start=lsp.Position(start_line, 0), end=lsp.Position(name_line_index, len(lines[name_line_index]))), selection_range=lsp.Range(start=lsp.Position(name_line_index, 0), end=lsp.Position(name_line_index, len(lines[name_line_index]))), children=children or [], ) if scope_stack[-1].symbol.children is None: scope_stack[-1].symbol.children = [] scope_stack[-1].symbol.children.append(event_symbol) # Clear event tracking and continue pending_event = None continue ns_match = NS_RE.match(line) proc_match = PROC_RE.match(line) set_match = SET_RE.match(line) # Namespace if ns_match: ns_name = ns_match.group(1) start = lsp.Position(i, 0) end = lsp.Position(i, len(line)) sel_start_char = line.find(ns_name) sel_end_char = sel_start_char + len(ns_name) if sel_start_char >= 0 else len(line) ns_symbol = lsp.DocumentSymbol( name=ns_name, detail="Namespace", kind=lsp.SymbolKind.Namespace, range=lsp.Range(start=start, end=end), selection_range=lsp.Range( start=lsp.Position(i, max(sel_start_char, 0)), end=lsp.Position(i, max(sel_end_char, 0)), ), children=[], ) scope = Scope(ns_name, ns_symbol, i) if scope_stack[-1].symbol.children is None: scope_stack[-1].symbol.children = [] scope_stack[-1].symbol.children.append(ns_symbol) scope_stack.append(scope) # Proc elif proc_match: proc_name = proc_match.group(1) start = lsp.Position(i, 0) end = lsp.Position(i, len(line)) sel_start_char = line.find(proc_name) sel_end_char = sel_start_char + len(proc_name) if sel_start_char >= 0 else len(line) proc_symbol = lsp.DocumentSymbol( name=proc_name, detail="Procedure", kind=lsp.SymbolKind.Function, range=lsp.Range(start=start, end=end), selection_range=lsp.Range( start=lsp.Position(i, max(sel_start_char, 0)), end=lsp.Position(i, max(sel_end_char, 0)), ), children=[], ) scope = Scope(proc_name, proc_symbol, i) if scope_stack[-1].symbol.children is None: scope_stack[-1].symbol.children = [] scope_stack[-1].symbol.children.append(proc_symbol) scope_stack.append(scope) # set variable elif set_match: var_name = set_match.group(1) start = lsp.Position(i, 0) end = lsp.Position(i, len(line)) sel_start_char = line.find(var_name) sel_end_char = sel_start_char + len(var_name) if sel_start_char >= 0 else len(line) var_symbol = lsp.DocumentSymbol( name=var_name, detail="Variable", kind=lsp.SymbolKind.Variable, range=lsp.Range(start=start, end=end), selection_range=lsp.Range( start=lsp.Position(i, max(sel_start_char, 0)), end=lsp.Position(i, max(sel_end_char, 0)), ), children=None, ) # Attach to current scope or pending event as child if pending_event is not None: pending_event["children"].append(var_symbol) else: if scope_stack[-1].symbol.children is None: scope_stack[-1].symbol.children = [] scope_stack[-1].symbol.children.append(var_symbol) # Event start (buffer edit) m = EVENT_START_RE.match(line) if m: edit_type = m.group(1) pending_event = {"start": i, "edit_type": edit_type, "children": []} continue # Brace balancing for scopes (namespace/proc) 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() # Set the full range from startLine to current line finished.symbol.range = lsp.Range( start=lsp.Position(finished.start_line, 0), end=lsp.Position(i, len(line)), ) # Return top-level children return root_symbol.children