"""Symbol outline panel -- displays functions and classes in the current file."""
from __future__ import annotations
import logging
import re
from typing import TYPE_CHECKING
from simvx.core import Signal
from simvx.core.math.types import Vec2
from simvx.core.ui.core import Control
from simvx.core.ui.theme import get_theme
from simvx.core.ui.tree import TreeItem, TreeView
if TYPE_CHECKING:
from ..state import IDEState
log = logging.getLogger(__name__)
_HEADER_H = 28.0
_SYM_RE = re.compile(r"^(\s*)(def|class)\s+(\w+)")
_KIND_COLOURS = {
"class": (0.95, 0.76, 0.19, 1.0), # yellow
"def": (0.4, 0.6, 1.0, 1.0), # blue
}
[docs]
class SymbolOutlinePanel(Control):
"""Displays functions and classes from the current file in a tree."""
def __init__(self, state: IDEState, **kwargs):
super().__init__(**kwargs)
self.name = "Outline"
self._state = state
self._current_path: str = ""
self._current_text: str = ""
self.symbol_selected = Signal()
self._sym_tree = TreeView()
self._sym_tree.row_height = 22.0
self._sym_tree.font_size = 12.0
self._sym_tree.item_selected.connect(self._on_item_selected)
self._apply_tree_theme()
self._sym_tree.root = TreeItem("(no symbols)")
self.add_child(self._sym_tree)
state.active_file_changed.connect(self._on_file_changed)
state.file_saved.connect(self._on_file_saved)
def _apply_tree_theme(self):
"""Apply current theme colours to the symbol tree."""
theme = get_theme()
self._sym_tree.bg_colour = theme.panel_bg
self._sym_tree.text_colour = theme.text
[docs]
def refresh_theme(self):
"""Re-apply theme colours after a theme change."""
self._apply_tree_theme()
[docs]
def set_text(self, text: str, path: str = ""):
"""Parse text and rebuild the symbol tree."""
self._current_text = text
self._current_path = path
self._rebuild_tree(text)
def _on_file_changed(self, path: str):
"""Re-parse when active file changes."""
self._current_path = path
# Read file content
try:
from pathlib import Path
text = Path(path).read_text(encoding="utf-8")
self._current_text = text
self._rebuild_tree(text)
except (OSError, UnicodeDecodeError):
self._sym_tree.root = TreeItem("(no symbols)")
self._current_text = ""
def _on_file_saved(self, path: str):
"""Re-parse when current file is saved."""
if path == self._current_path:
self._on_file_changed(path)
def _rebuild_tree(self, text: str):
"""Parse Python source and build symbol tree."""
root = TreeItem("Symbols")
root.expanded = True
lines = text.split("\n")
# Stack tracks (indent_level, TreeItem) for nesting
stack: list[tuple[int, TreeItem]] = [(-1, root)]
for line_num, line in enumerate(lines):
m = _SYM_RE.match(line)
if not m:
continue
indent = len(m.group(1))
kind = m.group(2) # "def" or "class"
name = m.group(3)
# Pop stack until we find a parent with less indent
while len(stack) > 1 and stack[-1][0] >= indent:
stack.pop()
parent = stack[-1][1]
prefix = "C " if kind == "class" else "f "
item = TreeItem(f"{prefix}{name}")
item.data = {"path": self._current_path, "line": line_num, "kind": kind}
item.expanded = True
parent.add_child(item)
stack.append((indent, item))
self._sym_tree.root = root
[docs]
def get_symbols(self) -> list[tuple[str, str, int]]:
"""Return symbols as (name, kind, line) tuples from the parsed tree."""
symbols: list[tuple[str, str, int]] = []
self._collect_symbols(self._sym_tree.root, symbols)
return symbols
def _collect_symbols(self, item: TreeItem, result: list[tuple[str, str, int]]):
"""Recursively collect symbols from a TreeItem."""
if item.data and "line" in item.data:
display = item.text
name = display[2:] if display.startswith(("C ", "f ")) else display
kind = item.data.get("kind", "def")
line = item.data["line"]
result.append((name, kind, line))
for child in item.children:
self._collect_symbols(child, result)
def _on_item_selected(self, item: TreeItem):
if item.data and "line" in item.data:
path = item.data.get("path", self._current_path)
line = item.data["line"]
self.symbol_selected.emit(path, line)
self._state.goto_requested.emit(path, line, 0)
[docs]
def process(self, dt: float):
_, _, w, h = self.get_rect()
self._sym_tree.position = Vec2(0, _HEADER_H)
self._sym_tree.size = Vec2(w, max(0, h - _HEADER_H))
[docs]
def draw(self, renderer):
theme = get_theme()
x, y, w, h = self.get_global_rect()
renderer.draw_filled_rect(x, y, w, h, theme.panel_bg)
scale = 12.0 / 14.0
renderer.draw_filled_rect(x, y, w, _HEADER_H, theme.header_bg)
renderer.draw_text_coloured("Outline", x + 8, y + (_HEADER_H - 12) / 2, scale, theme.text)
renderer.draw_line_coloured(x, y + _HEADER_H, x + w, y + _HEADER_H, theme.border)