vibefpmgui.window.debugging
Debugging (see plan/debugger.md): the Debug menu, the Debug panel, and breakpoints in the editors' gutters.
A program, a test or an example (Start Debugging (F5: the kind debugged
last), Debug a Test…, Debug an Example…, or a row's right-click in the
Tests or Run panel) is run as fpm runs it (fpm run, fpm test,
fpm run --example, with the Build options), but with the collecting
runner (vibefpmgui.test_runner --collect): fpm builds (the Build panel
shows it), then says which executable it would run, and how; that one then
runs under the debugger (vibefpmgui.debug_session). Several (no Target
given): which one is asked.
Breakpoints are kept per file (absolute path → lines), with the open files'
own, which move with their edits (CodeEditor.breakpoint_lines); they're
saved with the project's open files (its session).
1"""Debugging (see plan/debugger.md): the Debug menu, the Debug panel, and 2breakpoints in the editors' gutters. 3 4A program, a test or an example (*Start Debugging* (F5: the kind debugged 5last), *Debug a Test…*, *Debug an Example…*, or a row's right-click in the 6Tests or Run panel) is run as fpm runs it (``fpm run``, ``fpm test``, 7``fpm run --example``, with the Build options), but with the collecting 8runner (`vibefpmgui.test_runner` ``--collect``): fpm builds (the Build panel 9shows it), then says which executable it would run, and how; that one then 10runs under the debugger (`vibefpmgui.debug_session`). Several (no Target 11given): which one is asked. 12 13Breakpoints are kept per file (absolute path → lines), with the open files' 14own, which move with their edits (`CodeEditor.breakpoint_lines`); they're 15saved with the project's open files (its session). 16""" 17 18from __future__ import annotations 19 20import json 21from pathlib import Path 22 23from qtpy.QtCore import QTimer 24from qtpy.QtGui import QAction, QKeySequence, QPalette 25from qtpy.QtWidgets import QApplication, QInputDialog, QMessageBox 26 27from .. import ansi 28from ..breakpoints_list import DISABLED, ENABLED, UNPLACED, BreakpointEntry 29from ..complexity import FORTRAN 30from ..debug_adapters import NOT_FOUND, find_adapter 31from ..debug_panel import DebugPanel 32from ..debug_session import IDLE, STOPPED, DebugSession, Frame 33from ..editor import EditorTab 34from ..fpm_args import runner_command, with_runner 35from ..icons import make_icon 36from ..output_links import OutputLinks 37from ..test_monitor import EXE_RE 38 39# What can be debugged: fpm's command for it, and what it's called. 40KINDS = {"program": (["run"], "program"), "test": (["test"], "test"), "example": (["run", "--example"], "example")} 41 42# The files a debugger can stop in: Fortran (and its include files), and C and C++. 43DEBUGGABLE = FORTRAN | {".inc", ".fi", ".c", ".h", ".cc", ".cpp", ".cxx", ".hpp", ".hh"} 44 45 46def debuggable(path: Path) -> bool: 47 return path.suffix.lower() in DEBUGGABLE 48 49 50class DebuggingMixin: 51 """Debugging (a part of `vibefpmgui.main_window.MainWindow`).""" 52 53 def _build_debugging(self): 54 """The Debug menu, its actions and the Debug panel's widget.""" 55 self.debug_session = DebugSession(self) 56 self.breakpoints: dict[str, list[int]] = {} # file (absolute) → lines, the closed files' too 57 self.disabled_breakpoints: dict[str, list[int]] = {} # file → those of its lines disabled (kept, hollow) 58 self._breakpoint_problems: dict[str, dict[int, str]] = {} # file → {line: why it wasn't placed} 59 self._debug_candidates: list[tuple[str, list[str]]] = [] # (name, command line) fpm said it would run 60 self._debug_adapter = None # the debugger the session being started uses 61 self._debug_kind, self._debug_targets = "program", None # what F5 debugs: the kind debugged last 62 63 def action(text, keys, slot, tip): 64 item = QAction(text, self) 65 if keys: 66 item.setShortcut(QKeySequence(keys)) 67 item.setStatusTip(tip) 68 item.setToolTip(f"{text} ({QKeySequence(keys).toString(QKeySequence.NativeText)})" if keys else text) 69 item.triggered.connect(slot) 70 return item 71 72 self.debug_start_action = action("Start Debugging", "F5", lambda: self.start_debugging(), 73 "Build, then run the program under the debugger (Target: which one; " 74 "a test or an example, if one was debugged last)") 75 self.debug_test_action = action("Debug a Test…", "", lambda: self.start_debugging("test"), 76 "Build the tests, then run one under the debugger (Target: which)") 77 self.debug_example_action = action("Debug an Example…", "", lambda: self.start_debugging("example"), 78 "Build the examples, then run one under the debugger (Target: which)") 79 self.debug_stop_action = action("Stop Debugging", "Shift+F5", self.debug_session.stop, 80 "End the program being debugged") 81 self.debug_pause_action = action("Pause", "", self.debug_session.pause, "Pause the program where it is") 82 self.step_over_action = action("Step Over", "F10", self.debug_session.step_over, 83 "Run the current line, calls and all") 84 self.step_in_action = action("Step Into", "F11", self.debug_session.step_in, 85 "Into the procedure the current line calls") 86 self.step_out_action = action("Step Out", "Shift+F11", self.debug_session.step_out, 87 "On to where the current procedure returns") 88 self.toggle_breakpoint_action = action("Toggle Breakpoint", "F9", self.toggle_breakpoint, 89 "Set or remove a breakpoint on the cursor's line") 90 self.enable_breakpoint_action = action("Enable/Disable Breakpoint", "Ctrl+F9", self.toggle_breakpoint_enabled, 91 "Disable the breakpoint on the cursor's line (kept, drawn hollow: " 92 "the debugger doesn't stop there), or enable it again") 93 self.disable_all_breakpoints_action = action("Disable All Breakpoints", "", 94 lambda: self.set_all_breakpoints_enabled(), 95 "Every breakpoint, in every file, disabled (kept, hollow); " 96 "Enable All Breakpoints, when none is enabled") 97 self.clear_breakpoints_action = action("Remove All Breakpoints", "", self.remove_all_breakpoints, 98 "Every breakpoint, in every file") 99 menu = self.menuBar().addMenu("&Debug") 100 menu.addActions([self.debug_start_action, self.debug_test_action, self.debug_example_action]) 101 menu.addSeparator() 102 menu.addActions([self.debug_pause_action, self.debug_stop_action]) 103 menu.addSeparator() 104 menu.addActions([self.step_over_action, self.step_in_action, self.step_out_action]) 105 menu.addSeparator() 106 menu.addActions([self.toggle_breakpoint_action, self.enable_breakpoint_action, 107 self.disable_all_breakpoints_action, self.clear_breakpoints_action]) 108 # Icons (the panel's buttons show only them; their tooltips say what they do), drawn 109 # in the theme's text color as the toolbar's are, and again when it changes. 110 for item, icon in ((self.debug_start_action, "run"), (self.debug_pause_action, "pause"), 111 (self.step_over_action, "step-over"), (self.step_in_action, "step-into"), 112 (self.step_out_action, "step-out"), (self.debug_stop_action, "stop")): 113 self.command_icons[item] = icon 114 self._update_command_icons() 115 self.debug_panel = DebugPanel(self.debug_session, [ 116 self.debug_start_action, self.debug_pause_action, self.step_over_action, self.step_in_action, 117 self.step_out_action, self.debug_stop_action], 118 [self.disable_all_breakpoints_action, self.clear_breakpoints_action]) 119 listed = self.debug_panel.breakpoints 120 listed.open_requested.connect(lambda path, line: self.navigate_to(path, line, 0)) 121 listed.enabled_toggled.connect(self.toggle_listed_breakpoint) 122 listed.remove_requested.connect(self.remove_breakpoint) 123 listed.mono = self._mono_font() # (the code: as the Search panel's lines; see window.view for changes) 124 listed.lines_for = self._source_lines_of # (its syntax colors: the lines before for context) 125 self._source_lines: dict[str, tuple[float, list[str]]] = {} # closed file -> (mtime, lines), for the list 126 # (An open file's edits move its breakpoints and change their code: the list again, once typing pauses.) 127 self._breakpoint_list_timer = QTimer(self) 128 self._breakpoint_list_timer.setSingleShot(True) 129 self._breakpoint_list_timer.setInterval(300) 130 self._breakpoint_list_timer.timeout.connect(self._refresh_breakpoint_list) 131 self.debug_panel.frame_chosen.connect(self._frame_chosen) 132 # gdb's "Breakpoint 3 at 0x…: file test/x.f90, line 155." (and any other location) open on a click. 133 self.debug_links = OutputLinks(self.debug_panel.console, 134 lambda: self.manifest_path.parent if self.manifest_path else None) 135 self.debug_links.open_requested.connect(lambda path, line, column: self.navigate_to(path, line, column, 136 preview=True)) 137 s = self.debug_session 138 s.state_changed.connect(self._debug_state_changed) 139 s.stopped_at.connect(self._debug_stopped) 140 s.breakpoints_checked.connect(self._breakpoints_checked) 141 s.ended.connect(self._debug_ended) 142 self._debug_state_changed(IDLE) 143 144 # ---------------------------------------------------------- starting 145 def start_debugging(self, kind: str | None = None, targets: list[str] | None = None): 146 """Debug → Start Debugging (Continue, while paused): a ``kind`` of 147 executable ("program", "test", "example"; None: the kind debugged 148 last), ``targets``: these (else the Target field's).""" 149 session = self.debug_session 150 if session.state == STOPPED and kind is None: 151 session.continue_() 152 return 153 if kind is None: 154 kind, targets = self._debug_kind, self._debug_targets 155 if session.is_active() or not self.manifest_path or self.busy(): 156 return 157 adapter = find_adapter() 158 if adapter is None: 159 QMessageBox.information(self, "Start Debugging", NOT_FOUND) 160 return 161 if not self.selected_features and self.profile_combo.currentText() == "release" and QMessageBox.question( 162 self, "Start Debugging", 163 "The release profile builds without debug information: the debugger can't show lines or " 164 "variables. Debug anyway? (Choose the debug profile in Build options to debug properly.)", 165 ) != QMessageBox.Yes: 166 return 167 sub, _word = KINDS[kind] 168 args = self.debug_arguments(kind, targets, runner_command(collect=True)) 169 if args is None: 170 return 171 self._debug_adapter, self._debug_candidates = adapter, [] 172 self._debug_kind, self._debug_targets = kind, targets 173 self.debug_panel.begin("…") 174 self.debug_links.forget() # (which files exist: looked at again) 175 self.statusBar().showMessage("Building, to debug…") 176 self.run_fpm(sub[:1], True, False, phase="debug", args=args) 177 178 def debug_arguments(self, kind: str, targets: list[str] | None, runner: str) -> list[str] | None: 179 """fpm's arguments to build and run a ``kind`` of executable (`KINDS`) 180 with the Build options, with debug information, through ``runner`` 181 (None: a field's quotes don't match, which is said).""" 182 try: 183 args = self.build_arguments(KINDS[kind][0], True, targets=targets) 184 except ValueError as exc: 185 QMessageBox.warning(self, "Invalid arguments", str(exc)) 186 return None 187 if not self.selected_features and self.profile_combo.currentIndex() <= 0: 188 # (No profile: fpm (0.13) then passes the compiler none of its flags, -g 189 # among them: no debug information. The debug profile's, then.) 190 args[1:1] = ["--profile", "debug"] 191 return with_runner(args, runner) 192 193 def debug_named(self, kind: str, name: str): 194 """A test (or program) to debug, picked in the Tests (or Run) panel.""" 195 if self.debug_session.is_active(): 196 self.debug_session.stop() 197 self.start_debugging(kind, [name]) 198 199 def _debug_line(self, raw: str) -> str: 200 """A line of "fpm run --runner '… --collect'": the build's output, or a 201 program's command line (taken out, kept for _after_debug_build).""" 202 line = ansi.strip(raw) 203 match = EXE_RE.search(line) 204 if match is None: 205 if self._test_building: 206 self.build_monitor.feed_line(line) 207 return raw + "\n" 208 if self._test_building: # (built: fpm runs the programs now) 209 self._test_building = False 210 if self._monitoring: 211 self.build_monitor.finish(0) 212 self._update_tree_marks() 213 self._monitoring = False 214 try: 215 argv = [str(a) for a in json.loads(match.group(2))] 216 except ValueError: 217 return raw + "\n" 218 self._debug_candidates.append((match.group(1), argv)) 219 return "" 220 221 def _after_debug_build(self, code: int, stopped: bool): 222 """fpm has built the programs and said how it would run them: debug one.""" 223 candidates = self._debug_candidates 224 if stopped or code != 0 or not candidates: 225 word = KINDS[self._debug_kind][1] 226 why = ("stopped" if stopped else "the build failed (see Build and Output)" if code != 0 227 else f"fpm ran no {word} (Target: a {word}'s name)") 228 self.debug_panel.append("console", f"[not debugging: {why}]\n") 229 self.statusBar().showMessage(f"Not debugging: {why}", 6000) 230 return 231 if len(candidates) > 1: 232 names = [name for name, _argv in candidates] 233 name, ok = QInputDialog.getItem(self, "Start Debugging", f"Debug which {KINDS[self._debug_kind][1]}?", 234 names, 0, False) 235 if not ok: 236 return 237 candidates = [c for c in candidates if c[0] == name] 238 name, argv = candidates[0] 239 self.debug_panel.begin(name) 240 self.show_panel(self.debug_panel) 241 self.debug_session.start(self._debug_adapter, argv[0], argv[1:], str(self.manifest_path.parent), 242 self.all_breakpoints()) 243 self.statusBar().showMessage(f"Debugging {name} ({self._debug_adapter.title})", 5000) 244 245 # ---------------------------------------------------------- while debugging 246 def _debug_state_changed(self, state: str): 247 active, stopped = state != IDLE, state == STOPPED 248 start = self.debug_start_action 249 start.setText("Continue" if stopped else "Start Debugging") 250 start.setToolTip(f"{start.text()} ({start.shortcut().toString(QKeySequence.NativeText)})") 251 icon = "continue" if stopped else "run" 252 if self.command_icons.get(start) != icon: 253 self.command_icons[start] = icon 254 start.setIcon(make_icon(icon, QApplication.palette().color(QPalette.WindowText))) 255 self.debug_start_action.setEnabled(stopped or not active) 256 self.debug_test_action.setEnabled(not active) 257 self.debug_example_action.setEnabled(not active) 258 self.debug_stop_action.setEnabled(active) 259 self.debug_pause_action.setEnabled(state == "running") 260 for step in (self.step_over_action, self.step_in_action, self.step_out_action): 261 step.setEnabled(stopped) 262 if not stopped: 263 self._show_execution_line(None) 264 self.debug_panel.breakpoints.show_hit(None) 265 266 def _debug_stopped(self, frames: list[Frame], why: str): 267 self.show_panel(self.debug_panel) 268 self._show_frame(self.debug_session.frame_index) 269 self.debug_panel.breakpoints.show_hit(self._stopped_at_breakpoint(frames)) 270 if why == "exception": 271 self.statusBar().showMessage("Stopped: an error in the program (see the Debug panel's call stack)", 8000) 272 273 def _stopped_at_breakpoint(self, frames: list[Frame]) -> tuple[str, int] | None: 274 """The breakpoint the program stopped at (file, line as set), if any: 275 the one the debugger says it hit (maybe moved by it to the next line 276 with code), else one on the line it stopped at.""" 277 self._refresh_breakpoints() 278 for path, line in self.debug_session.hit_breakpoints: 279 if line in self.breakpoints.get(path, ()): 280 return path, line 281 top = frames[0] if frames else None 282 if top is not None and top.path: 283 path = str(Path(top.path).resolve()) 284 if top.line in self.breakpoints.get(path, ()): 285 return path, top.line 286 return None 287 288 def _frame_chosen(self, index: int): 289 self.debug_session.select_frame(index) 290 self._show_frame(index) 291 292 def _show_frame(self, index: int): 293 """A frame's place: its file opened at its line, the arrow there.""" 294 frames = self.debug_session.frames 295 frame = frames[index] if 0 <= index < len(frames) else None 296 if frame is None or not frame.path or not Path(frame.path).is_file(): 297 self._show_execution_line(None) 298 return 299 self.navigate_to(frame.path, frame.line, 0, record=False) 300 self._show_execution_line((Path(frame.path).resolve(), frame.line)) 301 302 def _show_execution_line(self, where: tuple[Path, int] | None): 303 if not hasattr(self, "tabs"): 304 return # (the window being built: no editors yet) 305 for editor in self.editors(): 306 editor.editor.set_execution_line(where[1] if where and not editor.untitled and editor.path == where[0] 307 else None) 308 309 def _debug_ended(self, why: str): 310 self._show_execution_line(None) 311 self.statusBar().showMessage(f"Debugging ended: {why}", 6000) 312 313 # ---------------------------------------------------------- breakpoints 314 def _debug_editor_added(self, editor: EditorTab): 315 """A file opened in the editor: its breakpoint column and breakpoints.""" 316 code = editor.editor 317 code.debuggable = not editor.untitled and debuggable(editor.path) 318 code.update_gutter_width() 319 if code.debuggable: 320 key = str(editor.path) 321 code.set_breakpoint_lines(self.breakpoints.get(key, []), self.disabled_breakpoints.get(key, [])) 322 code.set_breakpoint_problems(self._breakpoint_problems.get(key, {})) 323 code.breakpoints_changed.connect(lambda e=editor: self._editor_breakpoints_changed(e)) 324 editor.path_changed.connect(lambda _p, e=editor: self._debug_path_changed(e)) 325 code.document().contentsChanged.connect( 326 lambda c=code: c.breakpoint_lines() and self._breakpoint_list_timer.start()) 327 328 def _debug_path_changed(self, editor: EditorTab): 329 """Saved under another name (or a new file saved): a source file or not now.""" 330 editor.editor.debuggable = not editor.untitled and debuggable(editor.path) 331 editor.editor.refresh_gutter() 332 self._refresh_breakpoint_list() 333 334 def _editor_breakpoints_changed(self, editor: EditorTab): 335 if editor.untitled: 336 return 337 self._keep_breakpoints(editor) 338 key = str(editor.path) 339 self._breakpoint_problems.pop(key, None) 340 editor.editor.set_breakpoint_problems({}) 341 self.debug_session.set_breakpoints(key, editor.editor.enabled_breakpoints()) 342 self._update_breakpoint_actions() 343 344 def _keep_breakpoints(self, editor: EditorTab): 345 """An open file's breakpoints, where its edits moved them, kept (for 346 when it's closed, saved, or debugged).""" 347 if editor.untitled or not editor.editor.debuggable: 348 return 349 key, code = str(editor.path), editor.editor 350 for store, lines in ((self.breakpoints, code.breakpoint_lines()), 351 (self.disabled_breakpoints, code.disabled_breakpoints())): 352 if lines: 353 store[key] = lines 354 else: 355 store.pop(key, None) 356 357 def _debug_editor_closing(self, editor: EditorTab): 358 self._keep_breakpoints(editor) 359 360 def _refresh_breakpoints(self): 361 for editor in self.editors(): 362 self._keep_breakpoints(editor) 363 364 def all_breakpoints(self) -> dict[str, list[int]]: 365 """The breakpoints the debugger stops at: every enabled one (the open 366 files' where their edits moved them).""" 367 self._refresh_breakpoints() 368 out = {} 369 for path, lines in self.breakpoints.items(): 370 off = set(self.disabled_breakpoints.get(path, ())) 371 enabled = [n for n in lines if n not in off] 372 if enabled: 373 out[path] = enabled 374 return out 375 376 def _breakpoints_checked(self, path: str, states: dict): 377 problems = self._breakpoint_problems.setdefault(path, {}) 378 for line, state in states.items(): 379 if state.verified: 380 problems.pop(line, None) 381 else: 382 problems[line] = state.message 383 editor = self._open_editor_for(Path(path)) 384 if editor is not None: 385 editor.editor.set_breakpoint_problems(problems) 386 self._refresh_breakpoint_list() 387 388 def _cursor_line(self) -> tuple[EditorTab | None, int]: 389 editor = self.current_editor() 390 return editor, (editor.editor.textCursor().blockNumber() + 1 if editor is not None else 0) 391 392 def toggle_breakpoint(self): 393 """Debug → Toggle Breakpoint (F9): set or remove the one on the cursor's line.""" 394 editor, line = self._cursor_line() 395 if editor is not None: 396 editor.editor.toggle_breakpoint(line) 397 398 def toggle_breakpoint_enabled(self): 399 """Debug → Enable/Disable Breakpoint (Ctrl+F9): the one on the cursor's 400 line disabled (kept, hollow) or enabled again; none there: one set.""" 401 editor, line = self._cursor_line() 402 if editor is not None: 403 editor.editor.toggle_breakpoint_enabled(line) 404 405 def any_enabled_breakpoint(self) -> bool: 406 return bool(self.all_breakpoints()) 407 408 def set_all_breakpoints_enabled(self, enabled: bool | None = None): 409 """Debug → Disable All Breakpoints (Enable All, when none is enabled): 410 every breakpoint, in every file, kept.""" 411 if enabled is None: 412 enabled = not self.any_enabled_breakpoint() 413 self._refresh_breakpoints() 414 for path, lines in list(self.breakpoints.items()): 415 if enabled: 416 self.disabled_breakpoints.pop(path, None) 417 else: 418 self.disabled_breakpoints[path] = list(lines) 419 editor = self._open_editor_for(Path(path)) 420 if editor is not None: 421 editor.editor.set_breakpoints_enabled(lines, enabled) # (it tells the debugger) 422 else: 423 self.debug_session.set_breakpoints(path, list(lines) if enabled else []) 424 self._update_breakpoint_actions() 425 426 def remove_all_breakpoints(self): 427 self._refresh_breakpoints() 428 for path in list(self.breakpoints): 429 self.debug_session.set_breakpoints(path, []) 430 self.breakpoints.clear() 431 self.disabled_breakpoints.clear() 432 self._breakpoint_problems.clear() 433 for editor in self.editors(): 434 editor.editor.set_breakpoint_lines([]) 435 self._update_breakpoint_actions() 436 437 def _update_breakpoint_actions(self): 438 """Disable All, or Enable All when none is enabled (but some are disabled).""" 439 if not hasattr(self, "disable_all_breakpoints_action"): 440 return 441 none_enabled = not self.any_enabled_breakpoint() and any(self.breakpoints.values()) 442 self.disable_all_breakpoints_action.setText("Enable All Breakpoints" if none_enabled 443 else "Disable All Breakpoints") 444 self._refresh_breakpoint_list() 445 446 # ---------------------------------------------------------- the breakpoints list 447 def _refresh_breakpoint_list(self): 448 """The Debug panel's list: every breakpoint, its state, place and code.""" 449 if not hasattr(self, "debug_panel"): 450 return 451 self._refresh_breakpoints() 452 root = self.manifest_path.parent.resolve() if self.manifest_path else None 453 entries = [] 454 for path, lines in self.breakpoints.items(): 455 off = set(self.disabled_breakpoints.get(path, ())) 456 problems = self._breakpoint_problems.get(path, {}) 457 code = self._source_lines_of(path) 458 try: 459 shown = Path(path).relative_to(root).as_posix() if root else Path(path).name 460 except ValueError: # (outside the project: a dependency's, say) 461 shown = Path(path).name 462 for line in lines: 463 state = DISABLED if line in off else UNPLACED if line in problems else ENABLED 464 entries.append(BreakpointEntry(path, line, state, f"{shown}:{line}", 465 code[line - 1] if 0 < line <= len(code) else "", problems.get(line, ""))) 466 entries.sort(key=lambda e: (e.shown.rsplit(":", 1)[0].lower(), e.line)) 467 self.debug_panel.breakpoints.set_entries(entries) 468 469 def _source_lines_of(self, path: str) -> list[str]: 470 """A file's lines: the editor's if it's open, else the file's (read once, until it changes).""" 471 editor = self._open_editor_for(Path(path)) 472 if editor is not None: 473 return editor.editor.toPlainText().split("\n") 474 try: 475 mtime = Path(path).stat().st_mtime 476 cached = self._source_lines.get(path) 477 if cached is None or cached[0] != mtime: 478 cached = (mtime, Path(path).read_text(errors="replace").splitlines()) 479 self._source_lines[path] = cached 480 return cached[1] 481 except OSError: 482 return [] 483 484 def toggle_listed_breakpoint(self, path: str, line: int): 485 """The list's Enable/Disable Breakpoint.""" 486 self.set_breakpoint_enabled(path, line, line in self.disabled_breakpoints.get(path, ())) 487 488 def set_breakpoint_enabled(self, path: str, line: int, enabled: bool): 489 """One breakpoint enabled or disabled, its file open or not.""" 490 editor = self._open_editor_for(Path(path)) 491 if editor is not None: 492 editor.editor.set_breakpoints_enabled([line], enabled) # (it tells the debugger, and the list) 493 return 494 if line not in self.breakpoints.get(path, ()): 495 return 496 off = set(self.disabled_breakpoints.get(path, ())) 497 if enabled: 498 off.discard(line) 499 else: 500 off.add(line) 501 if off: 502 self.disabled_breakpoints[path] = sorted(off) 503 else: 504 self.disabled_breakpoints.pop(path, None) 505 self._closed_file_breakpoints_changed(path) 506 507 def remove_breakpoint(self, path: str, line: int): 508 """One breakpoint removed, its file open or not.""" 509 editor = self._open_editor_for(Path(path)) 510 if editor is not None: 511 if line in editor.editor.breakpoint_lines(): 512 editor.editor.toggle_breakpoint(line) # (it tells the debugger, and the list) 513 return 514 lines = [n for n in self.breakpoints.get(path, ()) if n != line] 515 off = [n for n in self.disabled_breakpoints.get(path, ()) if n != line] 516 for store, kept in ((self.breakpoints, lines), (self.disabled_breakpoints, off)): 517 if kept: 518 store[path] = kept 519 else: 520 store.pop(path, None) 521 self._closed_file_breakpoints_changed(path) 522 523 def _closed_file_breakpoints_changed(self, path: str): 524 self._breakpoint_problems.pop(path, None) 525 off = set(self.disabled_breakpoints.get(path, ())) 526 self.debug_session.set_breakpoints(path, [n for n in self.breakpoints.get(path, ()) if n not in off]) 527 self._update_breakpoint_actions() 528 529 def set_session_breakpoints(self, entries: list): 530 """The breakpoints saved with the project's session (see _save_session).""" 531 self.breakpoints, self.disabled_breakpoints = {}, {} 532 for entry in entries or []: 533 if isinstance(entry, dict) and isinstance(entry.get("path"), str): 534 key = str(Path(entry["path"])) 535 lines = sorted({int(n) for n in entry.get("lines") or [] if isinstance(n, int) and n > 0}) 536 off = sorted({int(n) for n in entry.get("disabled") or [] if isinstance(n, int)} & set(lines)) 537 if lines: 538 self.breakpoints[key] = lines 539 if off: 540 self.disabled_breakpoints[key] = off 541 self._breakpoint_problems = {} 542 self._update_breakpoint_actions() 543 544 def session_breakpoints(self) -> list[dict]: 545 self._refresh_breakpoints() 546 return [{"path": path, "lines": lines, **({"disabled": self.disabled_breakpoints[path]} 547 if self.disabled_breakpoints.get(path) else {})} 548 for path, lines in sorted(self.breakpoints.items()) if lines]
51class DebuggingMixin: 52 """Debugging (a part of `vibefpmgui.main_window.MainWindow`).""" 53 54 def _build_debugging(self): 55 """The Debug menu, its actions and the Debug panel's widget.""" 56 self.debug_session = DebugSession(self) 57 self.breakpoints: dict[str, list[int]] = {} # file (absolute) → lines, the closed files' too 58 self.disabled_breakpoints: dict[str, list[int]] = {} # file → those of its lines disabled (kept, hollow) 59 self._breakpoint_problems: dict[str, dict[int, str]] = {} # file → {line: why it wasn't placed} 60 self._debug_candidates: list[tuple[str, list[str]]] = [] # (name, command line) fpm said it would run 61 self._debug_adapter = None # the debugger the session being started uses 62 self._debug_kind, self._debug_targets = "program", None # what F5 debugs: the kind debugged last 63 64 def action(text, keys, slot, tip): 65 item = QAction(text, self) 66 if keys: 67 item.setShortcut(QKeySequence(keys)) 68 item.setStatusTip(tip) 69 item.setToolTip(f"{text} ({QKeySequence(keys).toString(QKeySequence.NativeText)})" if keys else text) 70 item.triggered.connect(slot) 71 return item 72 73 self.debug_start_action = action("Start Debugging", "F5", lambda: self.start_debugging(), 74 "Build, then run the program under the debugger (Target: which one; " 75 "a test or an example, if one was debugged last)") 76 self.debug_test_action = action("Debug a Test…", "", lambda: self.start_debugging("test"), 77 "Build the tests, then run one under the debugger (Target: which)") 78 self.debug_example_action = action("Debug an Example…", "", lambda: self.start_debugging("example"), 79 "Build the examples, then run one under the debugger (Target: which)") 80 self.debug_stop_action = action("Stop Debugging", "Shift+F5", self.debug_session.stop, 81 "End the program being debugged") 82 self.debug_pause_action = action("Pause", "", self.debug_session.pause, "Pause the program where it is") 83 self.step_over_action = action("Step Over", "F10", self.debug_session.step_over, 84 "Run the current line, calls and all") 85 self.step_in_action = action("Step Into", "F11", self.debug_session.step_in, 86 "Into the procedure the current line calls") 87 self.step_out_action = action("Step Out", "Shift+F11", self.debug_session.step_out, 88 "On to where the current procedure returns") 89 self.toggle_breakpoint_action = action("Toggle Breakpoint", "F9", self.toggle_breakpoint, 90 "Set or remove a breakpoint on the cursor's line") 91 self.enable_breakpoint_action = action("Enable/Disable Breakpoint", "Ctrl+F9", self.toggle_breakpoint_enabled, 92 "Disable the breakpoint on the cursor's line (kept, drawn hollow: " 93 "the debugger doesn't stop there), or enable it again") 94 self.disable_all_breakpoints_action = action("Disable All Breakpoints", "", 95 lambda: self.set_all_breakpoints_enabled(), 96 "Every breakpoint, in every file, disabled (kept, hollow); " 97 "Enable All Breakpoints, when none is enabled") 98 self.clear_breakpoints_action = action("Remove All Breakpoints", "", self.remove_all_breakpoints, 99 "Every breakpoint, in every file") 100 menu = self.menuBar().addMenu("&Debug") 101 menu.addActions([self.debug_start_action, self.debug_test_action, self.debug_example_action]) 102 menu.addSeparator() 103 menu.addActions([self.debug_pause_action, self.debug_stop_action]) 104 menu.addSeparator() 105 menu.addActions([self.step_over_action, self.step_in_action, self.step_out_action]) 106 menu.addSeparator() 107 menu.addActions([self.toggle_breakpoint_action, self.enable_breakpoint_action, 108 self.disable_all_breakpoints_action, self.clear_breakpoints_action]) 109 # Icons (the panel's buttons show only them; their tooltips say what they do), drawn 110 # in the theme's text color as the toolbar's are, and again when it changes. 111 for item, icon in ((self.debug_start_action, "run"), (self.debug_pause_action, "pause"), 112 (self.step_over_action, "step-over"), (self.step_in_action, "step-into"), 113 (self.step_out_action, "step-out"), (self.debug_stop_action, "stop")): 114 self.command_icons[item] = icon 115 self._update_command_icons() 116 self.debug_panel = DebugPanel(self.debug_session, [ 117 self.debug_start_action, self.debug_pause_action, self.step_over_action, self.step_in_action, 118 self.step_out_action, self.debug_stop_action], 119 [self.disable_all_breakpoints_action, self.clear_breakpoints_action]) 120 listed = self.debug_panel.breakpoints 121 listed.open_requested.connect(lambda path, line: self.navigate_to(path, line, 0)) 122 listed.enabled_toggled.connect(self.toggle_listed_breakpoint) 123 listed.remove_requested.connect(self.remove_breakpoint) 124 listed.mono = self._mono_font() # (the code: as the Search panel's lines; see window.view for changes) 125 listed.lines_for = self._source_lines_of # (its syntax colors: the lines before for context) 126 self._source_lines: dict[str, tuple[float, list[str]]] = {} # closed file -> (mtime, lines), for the list 127 # (An open file's edits move its breakpoints and change their code: the list again, once typing pauses.) 128 self._breakpoint_list_timer = QTimer(self) 129 self._breakpoint_list_timer.setSingleShot(True) 130 self._breakpoint_list_timer.setInterval(300) 131 self._breakpoint_list_timer.timeout.connect(self._refresh_breakpoint_list) 132 self.debug_panel.frame_chosen.connect(self._frame_chosen) 133 # gdb's "Breakpoint 3 at 0x…: file test/x.f90, line 155." (and any other location) open on a click. 134 self.debug_links = OutputLinks(self.debug_panel.console, 135 lambda: self.manifest_path.parent if self.manifest_path else None) 136 self.debug_links.open_requested.connect(lambda path, line, column: self.navigate_to(path, line, column, 137 preview=True)) 138 s = self.debug_session 139 s.state_changed.connect(self._debug_state_changed) 140 s.stopped_at.connect(self._debug_stopped) 141 s.breakpoints_checked.connect(self._breakpoints_checked) 142 s.ended.connect(self._debug_ended) 143 self._debug_state_changed(IDLE) 144 145 # ---------------------------------------------------------- starting 146 def start_debugging(self, kind: str | None = None, targets: list[str] | None = None): 147 """Debug → Start Debugging (Continue, while paused): a ``kind`` of 148 executable ("program", "test", "example"; None: the kind debugged 149 last), ``targets``: these (else the Target field's).""" 150 session = self.debug_session 151 if session.state == STOPPED and kind is None: 152 session.continue_() 153 return 154 if kind is None: 155 kind, targets = self._debug_kind, self._debug_targets 156 if session.is_active() or not self.manifest_path or self.busy(): 157 return 158 adapter = find_adapter() 159 if adapter is None: 160 QMessageBox.information(self, "Start Debugging", NOT_FOUND) 161 return 162 if not self.selected_features and self.profile_combo.currentText() == "release" and QMessageBox.question( 163 self, "Start Debugging", 164 "The release profile builds without debug information: the debugger can't show lines or " 165 "variables. Debug anyway? (Choose the debug profile in Build options to debug properly.)", 166 ) != QMessageBox.Yes: 167 return 168 sub, _word = KINDS[kind] 169 args = self.debug_arguments(kind, targets, runner_command(collect=True)) 170 if args is None: 171 return 172 self._debug_adapter, self._debug_candidates = adapter, [] 173 self._debug_kind, self._debug_targets = kind, targets 174 self.debug_panel.begin("…") 175 self.debug_links.forget() # (which files exist: looked at again) 176 self.statusBar().showMessage("Building, to debug…") 177 self.run_fpm(sub[:1], True, False, phase="debug", args=args) 178 179 def debug_arguments(self, kind: str, targets: list[str] | None, runner: str) -> list[str] | None: 180 """fpm's arguments to build and run a ``kind`` of executable (`KINDS`) 181 with the Build options, with debug information, through ``runner`` 182 (None: a field's quotes don't match, which is said).""" 183 try: 184 args = self.build_arguments(KINDS[kind][0], True, targets=targets) 185 except ValueError as exc: 186 QMessageBox.warning(self, "Invalid arguments", str(exc)) 187 return None 188 if not self.selected_features and self.profile_combo.currentIndex() <= 0: 189 # (No profile: fpm (0.13) then passes the compiler none of its flags, -g 190 # among them: no debug information. The debug profile's, then.) 191 args[1:1] = ["--profile", "debug"] 192 return with_runner(args, runner) 193 194 def debug_named(self, kind: str, name: str): 195 """A test (or program) to debug, picked in the Tests (or Run) panel.""" 196 if self.debug_session.is_active(): 197 self.debug_session.stop() 198 self.start_debugging(kind, [name]) 199 200 def _debug_line(self, raw: str) -> str: 201 """A line of "fpm run --runner '… --collect'": the build's output, or a 202 program's command line (taken out, kept for _after_debug_build).""" 203 line = ansi.strip(raw) 204 match = EXE_RE.search(line) 205 if match is None: 206 if self._test_building: 207 self.build_monitor.feed_line(line) 208 return raw + "\n" 209 if self._test_building: # (built: fpm runs the programs now) 210 self._test_building = False 211 if self._monitoring: 212 self.build_monitor.finish(0) 213 self._update_tree_marks() 214 self._monitoring = False 215 try: 216 argv = [str(a) for a in json.loads(match.group(2))] 217 except ValueError: 218 return raw + "\n" 219 self._debug_candidates.append((match.group(1), argv)) 220 return "" 221 222 def _after_debug_build(self, code: int, stopped: bool): 223 """fpm has built the programs and said how it would run them: debug one.""" 224 candidates = self._debug_candidates 225 if stopped or code != 0 or not candidates: 226 word = KINDS[self._debug_kind][1] 227 why = ("stopped" if stopped else "the build failed (see Build and Output)" if code != 0 228 else f"fpm ran no {word} (Target: a {word}'s name)") 229 self.debug_panel.append("console", f"[not debugging: {why}]\n") 230 self.statusBar().showMessage(f"Not debugging: {why}", 6000) 231 return 232 if len(candidates) > 1: 233 names = [name for name, _argv in candidates] 234 name, ok = QInputDialog.getItem(self, "Start Debugging", f"Debug which {KINDS[self._debug_kind][1]}?", 235 names, 0, False) 236 if not ok: 237 return 238 candidates = [c for c in candidates if c[0] == name] 239 name, argv = candidates[0] 240 self.debug_panel.begin(name) 241 self.show_panel(self.debug_panel) 242 self.debug_session.start(self._debug_adapter, argv[0], argv[1:], str(self.manifest_path.parent), 243 self.all_breakpoints()) 244 self.statusBar().showMessage(f"Debugging {name} ({self._debug_adapter.title})", 5000) 245 246 # ---------------------------------------------------------- while debugging 247 def _debug_state_changed(self, state: str): 248 active, stopped = state != IDLE, state == STOPPED 249 start = self.debug_start_action 250 start.setText("Continue" if stopped else "Start Debugging") 251 start.setToolTip(f"{start.text()} ({start.shortcut().toString(QKeySequence.NativeText)})") 252 icon = "continue" if stopped else "run" 253 if self.command_icons.get(start) != icon: 254 self.command_icons[start] = icon 255 start.setIcon(make_icon(icon, QApplication.palette().color(QPalette.WindowText))) 256 self.debug_start_action.setEnabled(stopped or not active) 257 self.debug_test_action.setEnabled(not active) 258 self.debug_example_action.setEnabled(not active) 259 self.debug_stop_action.setEnabled(active) 260 self.debug_pause_action.setEnabled(state == "running") 261 for step in (self.step_over_action, self.step_in_action, self.step_out_action): 262 step.setEnabled(stopped) 263 if not stopped: 264 self._show_execution_line(None) 265 self.debug_panel.breakpoints.show_hit(None) 266 267 def _debug_stopped(self, frames: list[Frame], why: str): 268 self.show_panel(self.debug_panel) 269 self._show_frame(self.debug_session.frame_index) 270 self.debug_panel.breakpoints.show_hit(self._stopped_at_breakpoint(frames)) 271 if why == "exception": 272 self.statusBar().showMessage("Stopped: an error in the program (see the Debug panel's call stack)", 8000) 273 274 def _stopped_at_breakpoint(self, frames: list[Frame]) -> tuple[str, int] | None: 275 """The breakpoint the program stopped at (file, line as set), if any: 276 the one the debugger says it hit (maybe moved by it to the next line 277 with code), else one on the line it stopped at.""" 278 self._refresh_breakpoints() 279 for path, line in self.debug_session.hit_breakpoints: 280 if line in self.breakpoints.get(path, ()): 281 return path, line 282 top = frames[0] if frames else None 283 if top is not None and top.path: 284 path = str(Path(top.path).resolve()) 285 if top.line in self.breakpoints.get(path, ()): 286 return path, top.line 287 return None 288 289 def _frame_chosen(self, index: int): 290 self.debug_session.select_frame(index) 291 self._show_frame(index) 292 293 def _show_frame(self, index: int): 294 """A frame's place: its file opened at its line, the arrow there.""" 295 frames = self.debug_session.frames 296 frame = frames[index] if 0 <= index < len(frames) else None 297 if frame is None or not frame.path or not Path(frame.path).is_file(): 298 self._show_execution_line(None) 299 return 300 self.navigate_to(frame.path, frame.line, 0, record=False) 301 self._show_execution_line((Path(frame.path).resolve(), frame.line)) 302 303 def _show_execution_line(self, where: tuple[Path, int] | None): 304 if not hasattr(self, "tabs"): 305 return # (the window being built: no editors yet) 306 for editor in self.editors(): 307 editor.editor.set_execution_line(where[1] if where and not editor.untitled and editor.path == where[0] 308 else None) 309 310 def _debug_ended(self, why: str): 311 self._show_execution_line(None) 312 self.statusBar().showMessage(f"Debugging ended: {why}", 6000) 313 314 # ---------------------------------------------------------- breakpoints 315 def _debug_editor_added(self, editor: EditorTab): 316 """A file opened in the editor: its breakpoint column and breakpoints.""" 317 code = editor.editor 318 code.debuggable = not editor.untitled and debuggable(editor.path) 319 code.update_gutter_width() 320 if code.debuggable: 321 key = str(editor.path) 322 code.set_breakpoint_lines(self.breakpoints.get(key, []), self.disabled_breakpoints.get(key, [])) 323 code.set_breakpoint_problems(self._breakpoint_problems.get(key, {})) 324 code.breakpoints_changed.connect(lambda e=editor: self._editor_breakpoints_changed(e)) 325 editor.path_changed.connect(lambda _p, e=editor: self._debug_path_changed(e)) 326 code.document().contentsChanged.connect( 327 lambda c=code: c.breakpoint_lines() and self._breakpoint_list_timer.start()) 328 329 def _debug_path_changed(self, editor: EditorTab): 330 """Saved under another name (or a new file saved): a source file or not now.""" 331 editor.editor.debuggable = not editor.untitled and debuggable(editor.path) 332 editor.editor.refresh_gutter() 333 self._refresh_breakpoint_list() 334 335 def _editor_breakpoints_changed(self, editor: EditorTab): 336 if editor.untitled: 337 return 338 self._keep_breakpoints(editor) 339 key = str(editor.path) 340 self._breakpoint_problems.pop(key, None) 341 editor.editor.set_breakpoint_problems({}) 342 self.debug_session.set_breakpoints(key, editor.editor.enabled_breakpoints()) 343 self._update_breakpoint_actions() 344 345 def _keep_breakpoints(self, editor: EditorTab): 346 """An open file's breakpoints, where its edits moved them, kept (for 347 when it's closed, saved, or debugged).""" 348 if editor.untitled or not editor.editor.debuggable: 349 return 350 key, code = str(editor.path), editor.editor 351 for store, lines in ((self.breakpoints, code.breakpoint_lines()), 352 (self.disabled_breakpoints, code.disabled_breakpoints())): 353 if lines: 354 store[key] = lines 355 else: 356 store.pop(key, None) 357 358 def _debug_editor_closing(self, editor: EditorTab): 359 self._keep_breakpoints(editor) 360 361 def _refresh_breakpoints(self): 362 for editor in self.editors(): 363 self._keep_breakpoints(editor) 364 365 def all_breakpoints(self) -> dict[str, list[int]]: 366 """The breakpoints the debugger stops at: every enabled one (the open 367 files' where their edits moved them).""" 368 self._refresh_breakpoints() 369 out = {} 370 for path, lines in self.breakpoints.items(): 371 off = set(self.disabled_breakpoints.get(path, ())) 372 enabled = [n for n in lines if n not in off] 373 if enabled: 374 out[path] = enabled 375 return out 376 377 def _breakpoints_checked(self, path: str, states: dict): 378 problems = self._breakpoint_problems.setdefault(path, {}) 379 for line, state in states.items(): 380 if state.verified: 381 problems.pop(line, None) 382 else: 383 problems[line] = state.message 384 editor = self._open_editor_for(Path(path)) 385 if editor is not None: 386 editor.editor.set_breakpoint_problems(problems) 387 self._refresh_breakpoint_list() 388 389 def _cursor_line(self) -> tuple[EditorTab | None, int]: 390 editor = self.current_editor() 391 return editor, (editor.editor.textCursor().blockNumber() + 1 if editor is not None else 0) 392 393 def toggle_breakpoint(self): 394 """Debug → Toggle Breakpoint (F9): set or remove the one on the cursor's line.""" 395 editor, line = self._cursor_line() 396 if editor is not None: 397 editor.editor.toggle_breakpoint(line) 398 399 def toggle_breakpoint_enabled(self): 400 """Debug → Enable/Disable Breakpoint (Ctrl+F9): the one on the cursor's 401 line disabled (kept, hollow) or enabled again; none there: one set.""" 402 editor, line = self._cursor_line() 403 if editor is not None: 404 editor.editor.toggle_breakpoint_enabled(line) 405 406 def any_enabled_breakpoint(self) -> bool: 407 return bool(self.all_breakpoints()) 408 409 def set_all_breakpoints_enabled(self, enabled: bool | None = None): 410 """Debug → Disable All Breakpoints (Enable All, when none is enabled): 411 every breakpoint, in every file, kept.""" 412 if enabled is None: 413 enabled = not self.any_enabled_breakpoint() 414 self._refresh_breakpoints() 415 for path, lines in list(self.breakpoints.items()): 416 if enabled: 417 self.disabled_breakpoints.pop(path, None) 418 else: 419 self.disabled_breakpoints[path] = list(lines) 420 editor = self._open_editor_for(Path(path)) 421 if editor is not None: 422 editor.editor.set_breakpoints_enabled(lines, enabled) # (it tells the debugger) 423 else: 424 self.debug_session.set_breakpoints(path, list(lines) if enabled else []) 425 self._update_breakpoint_actions() 426 427 def remove_all_breakpoints(self): 428 self._refresh_breakpoints() 429 for path in list(self.breakpoints): 430 self.debug_session.set_breakpoints(path, []) 431 self.breakpoints.clear() 432 self.disabled_breakpoints.clear() 433 self._breakpoint_problems.clear() 434 for editor in self.editors(): 435 editor.editor.set_breakpoint_lines([]) 436 self._update_breakpoint_actions() 437 438 def _update_breakpoint_actions(self): 439 """Disable All, or Enable All when none is enabled (but some are disabled).""" 440 if not hasattr(self, "disable_all_breakpoints_action"): 441 return 442 none_enabled = not self.any_enabled_breakpoint() and any(self.breakpoints.values()) 443 self.disable_all_breakpoints_action.setText("Enable All Breakpoints" if none_enabled 444 else "Disable All Breakpoints") 445 self._refresh_breakpoint_list() 446 447 # ---------------------------------------------------------- the breakpoints list 448 def _refresh_breakpoint_list(self): 449 """The Debug panel's list: every breakpoint, its state, place and code.""" 450 if not hasattr(self, "debug_panel"): 451 return 452 self._refresh_breakpoints() 453 root = self.manifest_path.parent.resolve() if self.manifest_path else None 454 entries = [] 455 for path, lines in self.breakpoints.items(): 456 off = set(self.disabled_breakpoints.get(path, ())) 457 problems = self._breakpoint_problems.get(path, {}) 458 code = self._source_lines_of(path) 459 try: 460 shown = Path(path).relative_to(root).as_posix() if root else Path(path).name 461 except ValueError: # (outside the project: a dependency's, say) 462 shown = Path(path).name 463 for line in lines: 464 state = DISABLED if line in off else UNPLACED if line in problems else ENABLED 465 entries.append(BreakpointEntry(path, line, state, f"{shown}:{line}", 466 code[line - 1] if 0 < line <= len(code) else "", problems.get(line, ""))) 467 entries.sort(key=lambda e: (e.shown.rsplit(":", 1)[0].lower(), e.line)) 468 self.debug_panel.breakpoints.set_entries(entries) 469 470 def _source_lines_of(self, path: str) -> list[str]: 471 """A file's lines: the editor's if it's open, else the file's (read once, until it changes).""" 472 editor = self._open_editor_for(Path(path)) 473 if editor is not None: 474 return editor.editor.toPlainText().split("\n") 475 try: 476 mtime = Path(path).stat().st_mtime 477 cached = self._source_lines.get(path) 478 if cached is None or cached[0] != mtime: 479 cached = (mtime, Path(path).read_text(errors="replace").splitlines()) 480 self._source_lines[path] = cached 481 return cached[1] 482 except OSError: 483 return [] 484 485 def toggle_listed_breakpoint(self, path: str, line: int): 486 """The list's Enable/Disable Breakpoint.""" 487 self.set_breakpoint_enabled(path, line, line in self.disabled_breakpoints.get(path, ())) 488 489 def set_breakpoint_enabled(self, path: str, line: int, enabled: bool): 490 """One breakpoint enabled or disabled, its file open or not.""" 491 editor = self._open_editor_for(Path(path)) 492 if editor is not None: 493 editor.editor.set_breakpoints_enabled([line], enabled) # (it tells the debugger, and the list) 494 return 495 if line not in self.breakpoints.get(path, ()): 496 return 497 off = set(self.disabled_breakpoints.get(path, ())) 498 if enabled: 499 off.discard(line) 500 else: 501 off.add(line) 502 if off: 503 self.disabled_breakpoints[path] = sorted(off) 504 else: 505 self.disabled_breakpoints.pop(path, None) 506 self._closed_file_breakpoints_changed(path) 507 508 def remove_breakpoint(self, path: str, line: int): 509 """One breakpoint removed, its file open or not.""" 510 editor = self._open_editor_for(Path(path)) 511 if editor is not None: 512 if line in editor.editor.breakpoint_lines(): 513 editor.editor.toggle_breakpoint(line) # (it tells the debugger, and the list) 514 return 515 lines = [n for n in self.breakpoints.get(path, ()) if n != line] 516 off = [n for n in self.disabled_breakpoints.get(path, ()) if n != line] 517 for store, kept in ((self.breakpoints, lines), (self.disabled_breakpoints, off)): 518 if kept: 519 store[path] = kept 520 else: 521 store.pop(path, None) 522 self._closed_file_breakpoints_changed(path) 523 524 def _closed_file_breakpoints_changed(self, path: str): 525 self._breakpoint_problems.pop(path, None) 526 off = set(self.disabled_breakpoints.get(path, ())) 527 self.debug_session.set_breakpoints(path, [n for n in self.breakpoints.get(path, ()) if n not in off]) 528 self._update_breakpoint_actions() 529 530 def set_session_breakpoints(self, entries: list): 531 """The breakpoints saved with the project's session (see _save_session).""" 532 self.breakpoints, self.disabled_breakpoints = {}, {} 533 for entry in entries or []: 534 if isinstance(entry, dict) and isinstance(entry.get("path"), str): 535 key = str(Path(entry["path"])) 536 lines = sorted({int(n) for n in entry.get("lines") or [] if isinstance(n, int) and n > 0}) 537 off = sorted({int(n) for n in entry.get("disabled") or [] if isinstance(n, int)} & set(lines)) 538 if lines: 539 self.breakpoints[key] = lines 540 if off: 541 self.disabled_breakpoints[key] = off 542 self._breakpoint_problems = {} 543 self._update_breakpoint_actions() 544 545 def session_breakpoints(self) -> list[dict]: 546 self._refresh_breakpoints() 547 return [{"path": path, "lines": lines, **({"disabled": self.disabled_breakpoints[path]} 548 if self.disabled_breakpoints.get(path) else {})} 549 for path, lines in sorted(self.breakpoints.items()) if lines]
Debugging (a part of vibefpmgui.main_window.MainWindow).
146 def start_debugging(self, kind: str | None = None, targets: list[str] | None = None): 147 """Debug → Start Debugging (Continue, while paused): a ``kind`` of 148 executable ("program", "test", "example"; None: the kind debugged 149 last), ``targets``: these (else the Target field's).""" 150 session = self.debug_session 151 if session.state == STOPPED and kind is None: 152 session.continue_() 153 return 154 if kind is None: 155 kind, targets = self._debug_kind, self._debug_targets 156 if session.is_active() or not self.manifest_path or self.busy(): 157 return 158 adapter = find_adapter() 159 if adapter is None: 160 QMessageBox.information(self, "Start Debugging", NOT_FOUND) 161 return 162 if not self.selected_features and self.profile_combo.currentText() == "release" and QMessageBox.question( 163 self, "Start Debugging", 164 "The release profile builds without debug information: the debugger can't show lines or " 165 "variables. Debug anyway? (Choose the debug profile in Build options to debug properly.)", 166 ) != QMessageBox.Yes: 167 return 168 sub, _word = KINDS[kind] 169 args = self.debug_arguments(kind, targets, runner_command(collect=True)) 170 if args is None: 171 return 172 self._debug_adapter, self._debug_candidates = adapter, [] 173 self._debug_kind, self._debug_targets = kind, targets 174 self.debug_panel.begin("…") 175 self.debug_links.forget() # (which files exist: looked at again) 176 self.statusBar().showMessage("Building, to debug…") 177 self.run_fpm(sub[:1], True, False, phase="debug", args=args)
Debug → Start Debugging (Continue, while paused): a kind of
executable ("program", "test", "example"; None: the kind debugged
last), targets: these (else the Target field's).
179 def debug_arguments(self, kind: str, targets: list[str] | None, runner: str) -> list[str] | None: 180 """fpm's arguments to build and run a ``kind`` of executable (`KINDS`) 181 with the Build options, with debug information, through ``runner`` 182 (None: a field's quotes don't match, which is said).""" 183 try: 184 args = self.build_arguments(KINDS[kind][0], True, targets=targets) 185 except ValueError as exc: 186 QMessageBox.warning(self, "Invalid arguments", str(exc)) 187 return None 188 if not self.selected_features and self.profile_combo.currentIndex() <= 0: 189 # (No profile: fpm (0.13) then passes the compiler none of its flags, -g 190 # among them: no debug information. The debug profile's, then.) 191 args[1:1] = ["--profile", "debug"] 192 return with_runner(args, runner)
fpm's arguments to build and run a kind of executable (KINDS)
with the Build options, with debug information, through runner
(None: a field's quotes don't match, which is said).
194 def debug_named(self, kind: str, name: str): 195 """A test (or program) to debug, picked in the Tests (or Run) panel.""" 196 if self.debug_session.is_active(): 197 self.debug_session.stop() 198 self.start_debugging(kind, [name])
A test (or program) to debug, picked in the Tests (or Run) panel.
365 def all_breakpoints(self) -> dict[str, list[int]]: 366 """The breakpoints the debugger stops at: every enabled one (the open 367 files' where their edits moved them).""" 368 self._refresh_breakpoints() 369 out = {} 370 for path, lines in self.breakpoints.items(): 371 off = set(self.disabled_breakpoints.get(path, ())) 372 enabled = [n for n in lines if n not in off] 373 if enabled: 374 out[path] = enabled 375 return out
The breakpoints the debugger stops at: every enabled one (the open files' where their edits moved them).
393 def toggle_breakpoint(self): 394 """Debug → Toggle Breakpoint (F9): set or remove the one on the cursor's line.""" 395 editor, line = self._cursor_line() 396 if editor is not None: 397 editor.editor.toggle_breakpoint(line)
Debug → Toggle Breakpoint (F9): set or remove the one on the cursor's line.
399 def toggle_breakpoint_enabled(self): 400 """Debug → Enable/Disable Breakpoint (Ctrl+F9): the one on the cursor's 401 line disabled (kept, hollow) or enabled again; none there: one set.""" 402 editor, line = self._cursor_line() 403 if editor is not None: 404 editor.editor.toggle_breakpoint_enabled(line)
Debug → Enable/Disable Breakpoint (Ctrl+F9): the one on the cursor's line disabled (kept, hollow) or enabled again; none there: one set.
409 def set_all_breakpoints_enabled(self, enabled: bool | None = None): 410 """Debug → Disable All Breakpoints (Enable All, when none is enabled): 411 every breakpoint, in every file, kept.""" 412 if enabled is None: 413 enabled = not self.any_enabled_breakpoint() 414 self._refresh_breakpoints() 415 for path, lines in list(self.breakpoints.items()): 416 if enabled: 417 self.disabled_breakpoints.pop(path, None) 418 else: 419 self.disabled_breakpoints[path] = list(lines) 420 editor = self._open_editor_for(Path(path)) 421 if editor is not None: 422 editor.editor.set_breakpoints_enabled(lines, enabled) # (it tells the debugger) 423 else: 424 self.debug_session.set_breakpoints(path, list(lines) if enabled else []) 425 self._update_breakpoint_actions()
Debug → Disable All Breakpoints (Enable All, when none is enabled): every breakpoint, in every file, kept.
427 def remove_all_breakpoints(self): 428 self._refresh_breakpoints() 429 for path in list(self.breakpoints): 430 self.debug_session.set_breakpoints(path, []) 431 self.breakpoints.clear() 432 self.disabled_breakpoints.clear() 433 self._breakpoint_problems.clear() 434 for editor in self.editors(): 435 editor.editor.set_breakpoint_lines([]) 436 self._update_breakpoint_actions()
485 def toggle_listed_breakpoint(self, path: str, line: int): 486 """The list's Enable/Disable Breakpoint.""" 487 self.set_breakpoint_enabled(path, line, line in self.disabled_breakpoints.get(path, ()))
The list's Enable/Disable Breakpoint.
489 def set_breakpoint_enabled(self, path: str, line: int, enabled: bool): 490 """One breakpoint enabled or disabled, its file open or not.""" 491 editor = self._open_editor_for(Path(path)) 492 if editor is not None: 493 editor.editor.set_breakpoints_enabled([line], enabled) # (it tells the debugger, and the list) 494 return 495 if line not in self.breakpoints.get(path, ()): 496 return 497 off = set(self.disabled_breakpoints.get(path, ())) 498 if enabled: 499 off.discard(line) 500 else: 501 off.add(line) 502 if off: 503 self.disabled_breakpoints[path] = sorted(off) 504 else: 505 self.disabled_breakpoints.pop(path, None) 506 self._closed_file_breakpoints_changed(path)
One breakpoint enabled or disabled, its file open or not.
508 def remove_breakpoint(self, path: str, line: int): 509 """One breakpoint removed, its file open or not.""" 510 editor = self._open_editor_for(Path(path)) 511 if editor is not None: 512 if line in editor.editor.breakpoint_lines(): 513 editor.editor.toggle_breakpoint(line) # (it tells the debugger, and the list) 514 return 515 lines = [n for n in self.breakpoints.get(path, ()) if n != line] 516 off = [n for n in self.disabled_breakpoints.get(path, ()) if n != line] 517 for store, kept in ((self.breakpoints, lines), (self.disabled_breakpoints, off)): 518 if kept: 519 store[path] = kept 520 else: 521 store.pop(path, None) 522 self._closed_file_breakpoints_changed(path)
One breakpoint removed, its file open or not.
530 def set_session_breakpoints(self, entries: list): 531 """The breakpoints saved with the project's session (see _save_session).""" 532 self.breakpoints, self.disabled_breakpoints = {}, {} 533 for entry in entries or []: 534 if isinstance(entry, dict) and isinstance(entry.get("path"), str): 535 key = str(Path(entry["path"])) 536 lines = sorted({int(n) for n in entry.get("lines") or [] if isinstance(n, int) and n > 0}) 537 off = sorted({int(n) for n in entry.get("disabled") or [] if isinstance(n, int)} & set(lines)) 538 if lines: 539 self.breakpoints[key] = lines 540 if off: 541 self.disabled_breakpoints[key] = off 542 self._breakpoint_problems = {} 543 self._update_breakpoint_actions()
The breakpoints saved with the project's session (see _save_session).