Source code for simvx.ide.panels.symbol_outline

"""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)