Source code for simvx.ide.panels.problems_panel

"""Problems panel -- structured diagnostics list with severity icons and click navigation."""


from __future__ import annotations

import logging
from pathlib import Path
from typing import TYPE_CHECKING

from simvx.core import Node
from simvx.core.ui.core import Control
from simvx.core.ui.theme import get_theme

if TYPE_CHECKING:
    from ..state import Diagnostic, IDEState

log = logging.getLogger(__name__)

_HEADER_H = 28.0
_ROW_H = 22.0
_SEV_LABELS = {1: "E", 2: "W", 3: "I", 4: "H"}


def _sev_colours(theme):
    return {1: theme.error, 2: theme.warning, 3: theme.info, 4: theme.text_dim}


[docs] class ProblemsPanel(Control): """Structured list of all diagnostics across open files.""" def __init__(self, state: IDEState, **kwargs): super().__init__(**kwargs) self.name = "Problems" self._state = state self._entries: list[Diagnostic] = [] self._runtime_errors: list[Diagnostic] = [] self._selected_index: int = -1 self._scroll_y: float = 0.0 self._error_count: int = 0 self._warning_count: int = 0 state.diagnostics_updated.connect(self._on_diagnostics_updated) Node.script_error_raised.connect(self._on_runtime_error) # -- Internal -------------------------------------------------------------- def _on_diagnostics_updated(self, path: str, diagnostics: list): """Rebuild the full entry list from all known diagnostics + runtime errors.""" self._rebuild_entries() def _on_runtime_error(self, node, method_name: str, tb: str): """Add a runtime script error as a diagnostic entry.""" from ..state import Diagnostic first_line = tb.strip().splitlines()[-1] if tb.strip() else f"Error in {method_name}" diag = Diagnostic( path=f"<runtime:{node.name}>", line=0, col_start=0, col_end=0, severity=1, message=f"{node.name}.{method_name}(): {first_line}", source="runtime", ) self._runtime_errors.append(diag) # Cap at 100 runtime errors if len(self._runtime_errors) > 100: self._runtime_errors = self._runtime_errors[-100:] self._rebuild_entries() def _rebuild_entries(self): """Merge LSP diagnostics and runtime errors into the entry list.""" self._entries.clear() self._error_count = 0 self._warning_count = 0 for _p, diags in self._state.get_all_diagnostics().items(): for d in diags: self._entries.append(d) if d.severity == 1: self._error_count += 1 elif d.severity == 2: self._warning_count += 1 for d in self._runtime_errors: self._entries.append(d) self._error_count += 1 self._entries.sort(key=lambda d: (d.severity, d.path, d.line)) def _on_gui_input(self, event): if event.key == "scroll_up": self._scroll_y = max(0, self._scroll_y - 20) return if event.key == "scroll_down": max_scroll = max(0, len(self._entries) * _ROW_H - (self.size.y - _HEADER_H)) self._scroll_y = min(max_scroll, self._scroll_y + 20) return if event.button == 1 and event.pressed: _, y, _, _ = self.get_global_rect() py = event.position.y if hasattr(event.position, "y") else event.position[1] row = int((py - y - _HEADER_H + self._scroll_y) / _ROW_H) if 0 <= row < len(self._entries): self._selected_index = row diag = self._entries[row] self._state.goto_requested.emit(diag.path, diag.line, diag.col_start) # -- Draw ------------------------------------------------------------------
[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 # Header bar with counts renderer.draw_filled_rect(x, y, w, _HEADER_H, theme.header_bg) count_text = f"Problems {self._error_count} errors, {self._warning_count} warnings" renderer.draw_text_coloured(count_text, 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) # Content area content_y = y + _HEADER_H content_h = max(0, h - _HEADER_H) renderer.push_clip(x, content_y, w, content_h) if not self._entries: renderer.draw_text_coloured("No problems detected", x + 8, content_y + 8, scale, theme.text_dim) renderer.pop_clip() return sev_colours = _sev_colours(theme) for i, diag in enumerate(self._entries): ry = content_y + i * _ROW_H - self._scroll_y if ry + _ROW_H < content_y or ry > content_y + content_h: continue # Selection highlight if i == self._selected_index: renderer.draw_filled_rect(x, ry, w, _ROW_H, theme.selection) # Severity badge sev_colour = sev_colours.get(diag.severity, theme.text_dim) sev_label = _SEV_LABELS.get(diag.severity, "?") renderer.draw_filled_rect(x + 4, ry + 3, 16, _ROW_H - 6, sev_colour) renderer.draw_text_coloured(sev_label, x + 7, ry + 4, scale * 0.9, (1.0, 1.0, 1.0, 1.0)) # Message msg_x = x + 26 msg = diag.message if len(msg) > 80: msg = msg[:77] + "..." renderer.draw_text_coloured(msg, msg_x, ry + 3, scale, theme.text) # File:line on the right short_path = Path(diag.path).name if diag.path else "?" location = f"{short_path}:{diag.line}" src = f" [{diag.source}]" if diag.source else "" loc_text = location + src loc_w = renderer.text_width(loc_text, scale * 0.85) renderer.draw_text_coloured(loc_text, x + w - loc_w - 8, ry + 4, scale * 0.85, theme.text_dim) renderer.pop_clip()