Coverage for vibefpmgui/debug_panel.py: 97%
173 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-11-01 20:51 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-11-01 20:51 +0000
1"""The Debug panel: the session's state and controls (the Debug menu's
2actions), the call stack, the variables (expanded as asked for: an array's
3elements, a type's components) and the console: the program's output and the
4debugger's messages, with a field to evaluate an expression in the frame
5shown. It shows what `vibefpmgui.debug_session.DebugSession` says; the window
6(`vibefpmgui.window.debugging`) connects them.
7"""
9from __future__ import annotations
11from qtpy.QtCore import QSize, Qt, Signal
12from qtpy.QtGui import QAction, QTextCursor
13from qtpy.QtWidgets import (
14 QHBoxLayout,
15 QLabel,
16 QListWidget,
17 QListWidgetItem,
18 QPlainTextEdit,
19 QSplitter,
20 QToolButton,
21 QTreeWidget,
22 QTreeWidgetItem,
23 QVBoxLayout,
24 QWidget,
25)
27from .breakpoints_list import BreakpointsList
28from .build_monitor import PALETTES, TitleIcon, app_is_light
29from .debug_session import (
30 IDLE,
31 RUNNING,
32 STARTING,
33 STOPPED,
34 DebugSession,
35 Frame,
36 Scope,
37 Variable,
38)
39from .widgets import HistoryLineEdit
41REFERENCE_ROLE = Qt.UserRole + 1 # a variable's item: its variablesReference (0: no parts)
42LOADED_ROLE = Qt.UserRole + 2 # its parts asked for already
43PLACEHOLDER = "…" # (an item's child until its parts are asked for)
45STATE_TEXT = {IDLE: "Not debugging", STARTING: "Starting…", RUNNING: "Running", STOPPED: "Paused"}
48class DebugPanel(QWidget):
49 frame_chosen = Signal(int) # a call stack row clicked (its index)
51 def __init__(self, session: DebugSession, actions: list[QAction], breakpoint_actions: list[QAction] = (),
52 parent=None):
53 """``actions``: the buttons (the Debug menu's); ``breakpoint_actions``: in
54 the breakpoints list's right-click menu (Disable All, Remove All)."""
55 super().__init__(parent)
56 self.session = session
57 self.title_icon = TitleIcon("bug") # (as the other panels': see refresh_palette)
58 self.title = QLabel()
59 self.title.setTextInteractionFlags(Qt.TextSelectableByMouse)
60 header = QHBoxLayout()
61 header.setContentsMargins(8, 4, 6, 2)
62 header.addWidget(self.title_icon)
63 header.addSpacing(2)
64 header.addWidget(self.title, 1)
65 for action in actions: # (the Debug menu's: Continue, Step Over…; their icons, tooltips saying which)
66 button = QToolButton()
67 button.setDefaultAction(action)
68 button.setToolButtonStyle(Qt.ToolButtonIconOnly)
69 button.setIconSize(QSize(18, 18))
70 button.setAutoRaise(True)
71 header.addWidget(button)
73 self.stack = QListWidget()
74 self.stack.currentRowChanged.connect(self._frame_clicked)
75 self.variables = QTreeWidget()
76 self.variables.setHeaderLabels(["Name", "Value"])
77 self.variables.setColumnWidth(0, 160)
78 self.variables.itemExpanded.connect(self._expanded)
79 self.console = QPlainTextEdit()
80 self.console.setReadOnly(True)
81 self.console.setMaximumBlockCount(5000)
82 self.evaluate_edit = HistoryLineEdit() # (Up, Down: the expressions evaluated before)
83 self.evaluate_edit.setPlaceholderText("Evaluate in the frame shown (e.g. a(2:3), p%x), Enter; ↑↓ earlier ones")
84 self.evaluate_edit.submitted.connect(self._evaluate)
86 self.breakpoints = BreakpointsList(breakpoint_actions) # (filled by the window: see window.debugging)
87 left = QSplitter(Qt.Vertical)
88 left.addWidget(_titled("Call Stack", self.stack))
89 left.addWidget(_titled("Breakpoints", self.breakpoints))
90 middle = _titled("Variables", self.variables)
91 console_box = QWidget()
92 console_layout = QVBoxLayout(console_box)
93 console_layout.setContentsMargins(0, 0, 0, 0)
94 console_layout.setSpacing(2)
95 console_layout.addWidget(_titled("Console", self.console), 1)
96 console_layout.addWidget(self.evaluate_edit)
97 split = QSplitter(Qt.Horizontal)
98 split.addWidget(left)
99 split.addWidget(middle)
100 split.addWidget(console_box)
101 split.setStretchFactor(0, 2)
102 split.setStretchFactor(1, 3)
103 split.setStretchFactor(2, 3)
104 layout = QVBoxLayout(self)
105 layout.setContentsMargins(0, 0, 0, 0)
106 layout.setSpacing(2)
107 layout.addLayout(header)
108 layout.addWidget(split, 1)
110 session.state_changed.connect(self._state_changed)
111 session.stopped_at.connect(self._stopped)
112 session.scopes_ready.connect(self._scopes)
113 session.output.connect(self.append)
114 session.ended.connect(lambda why: self.append("console", f"[{why}]\n"))
115 self._state_changed(session.state)
116 self.refresh_palette()
118 def refresh_palette(self):
119 """The icon in the theme's text color (again when the theme changes)."""
120 self.title_icon.draw(PALETTES["light" if app_is_light() else "dark"]["text"], self.title)
122 # ---------------------------------------------------------- state
123 def _state_changed(self, state: str):
124 adapter = self.session.adapter
125 debugger = f" · {adapter.title}" if adapter is not None and state != IDLE else ""
126 self.title.setText(f"<b>{STATE_TEXT.get(state, state)}</b>{debugger}")
127 self.evaluate_edit.setEnabled(state == STOPPED)
128 if state != STOPPED:
129 self.stack.clear()
130 self.variables.clear()
132 def begin(self, what: str):
133 """A new session (``what``: the program): the console cleared."""
134 self.console.clear()
135 self.append("console", f"[debugging {what}]\n")
137 def append(self, category: str, text: str):
138 cursor = self.console.textCursor()
139 cursor.movePosition(QTextCursor.End)
140 cursor.insertText(text)
141 self.console.setTextCursor(cursor)
142 self.console.ensureCursorVisible()
144 # ---------------------------------------------------------- the stack
145 def _stopped(self, frames: list[Frame], _why: str):
146 self.stack.blockSignals(True)
147 self.stack.clear()
148 for frame in frames:
149 where = f"{frame.path.rsplit('/', 1)[-1]}:{frame.line}" if frame.path else "(no source)"
150 item = QListWidgetItem(f"{frame_name(frame.name)} · {where}")
151 item.setToolTip(f"{frame.path}:{frame.line}" if frame.path else frame.name)
152 if not frame.path:
153 item.setForeground(self.palette().placeholderText())
154 self.stack.addItem(item)
155 self.stack.setCurrentRow(self.session.frame_index)
156 self.stack.blockSignals(False)
158 def _frame_clicked(self, row: int):
159 if row >= 0:
160 self.frame_chosen.emit(row)
162 # ---------------------------------------------------------- the variables
163 def _scopes(self, _index: int, scopes: list[Scope]):
164 self.variables.clear()
165 for number, scope in enumerate(scopes):
166 item = QTreeWidgetItem([scope.name, ""])
167 item.setData(0, REFERENCE_ROLE, scope.reference)
168 font = item.font(0)
169 font.setBold(True)
170 item.setFont(0, font)
171 self.variables.addTopLevelItem(item)
172 item.addChild(QTreeWidgetItem([PLACEHOLDER, ""]))
173 if number == 0 or not scope.expensive and scope.name.lower() in ("locals", "arguments"):
174 item.setExpanded(True)
176 def _expanded(self, item: QTreeWidgetItem):
177 if item.data(0, LOADED_ROLE): 177 ↛ 178line 177 didn't jump to line 178 because the condition on line 177 was never true
178 return
179 item.setData(0, LOADED_ROLE, True)
180 reference = int(item.data(0, REFERENCE_ROLE) or 0)
181 self.session.variables(reference, lambda variables, more, it=item: self._fill(it, variables, more))
183 def _fill(self, item: QTreeWidgetItem, variables: list[Variable], more: bool):
184 try:
185 item.takeChildren()
186 except RuntimeError:
187 return # (cleared meanwhile: the program moved on)
188 for variable in variables:
189 child = QTreeWidgetItem([variable.name, variable.value])
190 child.setToolTip(1, variable.value)
191 if variable.type:
192 child.setToolTip(0, variable.type)
193 child.setData(0, REFERENCE_ROLE, variable.reference)
194 if variable.reference:
195 child.addChild(QTreeWidgetItem([PLACEHOLDER, ""]))
196 item.addChild(child)
197 if more: 197 ↛ 198line 197 didn't jump to line 198 because the condition on line 197 was never true
198 item.addChild(QTreeWidgetItem(["…", "more: evaluate a section, e.g. a(1001:1100)"]))
199 if not variables: 199 ↛ 200line 199 didn't jump to line 200 because the condition on line 199 was never true
200 item.addChild(QTreeWidgetItem(["(none)", ""]))
202 def variable_rows(self) -> list[tuple[str, str]]:
203 """(name, value) of the variables shown, depth first (for the tests)."""
204 rows = []
206 def visit(item: QTreeWidgetItem):
207 for i in range(item.childCount()):
208 child = item.child(i)
209 if child.text(0) != PLACEHOLDER:
210 rows.append((child.text(0), child.text(1)))
211 visit(child)
213 for i in range(self.variables.topLevelItemCount()):
214 visit(self.variables.topLevelItem(i))
215 return rows
217 # ---------------------------------------------------------- evaluate
218 def _evaluate(self, expression: str):
219 self.append("console", f"> {expression}\n")
220 self.session.evaluate(expression, lambda ok, text, _ref: self.append("console", f"{text}\n"))
223def frame_name(name: str) -> str:
224 """A frame's procedure as a Fortran programmer knows it (gfortran calls
225 the main program MAIN__)."""
226 return "main program" if name == "MAIN__" else name
229def _titled(title: str, widget: QWidget) -> QWidget:
230 box = QWidget()
231 layout = QVBoxLayout(box)
232 layout.setContentsMargins(0, 0, 0, 0)
233 layout.setSpacing(0)
234 label = QLabel(title)
235 label.setContentsMargins(6, 2, 6, 2)
236 font = label.font()
237 font.setBold(True)
238 label.setFont(font)
239 layout.addWidget(label)
240 layout.addWidget(widget, 1)
241 return box