easycoder 251105.1__py2.py3-none-any.whl → 251215.2__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,20 +4,24 @@ from PySide6.QtWidgets import (
4
4
  QWidget,
5
5
  QFrame,
6
6
  QHBoxLayout,
7
+ QGridLayout,
7
8
  QVBoxLayout,
8
9
  QLabel,
9
10
  QSplitter,
10
11
  QMessageBox,
11
12
  QScrollArea,
13
+ QScrollBar,
12
14
  QSizePolicy,
13
15
  QToolBar,
14
16
  QPushButton,
15
- QInputDialog
17
+ QInputDialog,
18
+ QTabWidget
16
19
  )
17
20
  from PySide6.QtGui import QTextCursor, QIcon
18
21
  from PySide6.QtCore import Qt, QTimer
19
- from typing import Any
20
22
  from typing import Any, Optional
23
+ from .ec_dbg_value_display import ValueDisplay
24
+ from .ec_dbg_watchlist import WatchListWidget
21
25
 
22
26
  class Object():
23
27
  def __setattr__(self, name: str, value: Any) -> None:
@@ -38,6 +42,16 @@ class Debugger(QMainWindow):
38
42
  def write(self, text: str):
39
43
  if not text:
40
44
  return
45
+
46
+ # Check if this looks like an error message - if so, also write to original stderr
47
+ if any(err_marker in text for err_marker in ['Error', 'Traceback', 'Exception']):
48
+ try:
49
+ if self.debugger._orig_stderr:
50
+ self.debugger._orig_stderr.write(text)
51
+ self.debugger._orig_stderr.flush()
52
+ except Exception:
53
+ pass
54
+
41
55
  # Buffer text and request a flush on the GUI timer
42
56
  self._buf.append(text)
43
57
  if self.debugger._flush_timer and not self.debugger._flush_timer.isActive():
@@ -54,100 +68,40 @@ class Debugger(QMainWindow):
54
68
  super().__init__(parent)
55
69
  self.debugger = parent
56
70
  layout = QVBoxLayout(self)
57
-
58
- # Create toolbar with icon buttons
59
- toolbar = QToolBar()
60
- toolbar.setMovable(False)
61
-
62
- # Get the icons directory path
63
- icons_dir = os.path.join(os.path.dirname(__file__), 'icons')
64
-
65
- # Run button
66
- run_btn = QPushButton()
67
- run_icon_path = os.path.join(icons_dir, 'run.png')
68
- run_btn.setIcon(QIcon(run_icon_path))
69
- run_btn.setToolTip("Run")
70
- run_btn.clicked.connect(self.on_run_clicked)
71
- toolbar.addWidget(run_btn)
72
-
73
- # Step button
74
- step_btn = QPushButton()
75
- step_icon_path = os.path.join(icons_dir, 'step.png')
76
- step_btn.setIcon(QIcon(step_icon_path))
77
- step_btn.setToolTip("Step")
78
- step_btn.clicked.connect(self.on_step_clicked)
79
- toolbar.addWidget(step_btn)
80
-
81
- # Stop button
82
- stop_btn = QPushButton()
83
- stop_icon_path = os.path.join(icons_dir, 'stop.png')
84
- stop_btn.setIcon(QIcon(stop_icon_path))
85
- stop_btn.setToolTip("Stop")
86
- stop_btn.clicked.connect(self.on_stop_clicked)
87
- toolbar.addWidget(stop_btn)
88
-
89
- # Exit button
90
- exit_btn = QPushButton()
91
- exit_icon_path = os.path.join(icons_dir, 'exit.png')
92
- exit_btn.setIcon(QIcon(exit_icon_path))
93
- exit_btn.setToolTip("Exit")
94
- exit_btn.clicked.connect(self.on_exit_clicked)
95
- toolbar.addWidget(exit_btn)
96
-
97
71
 
98
- layout.addWidget(toolbar)
99
-
100
- # --- Watch panel (like VS Code) ---
101
- watch_panel = QFrame()
102
- watch_panel.setFrameShape(QFrame.Shape.StyledPanel)
103
- # Ensure the VARIABLES bar stretches to full available width
104
- watch_panel.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
105
- watch_layout = QHBoxLayout(watch_panel)
72
+ # Header panel
73
+ variable_panel = QFrame()
74
+ variable_panel.setFrameShape(QFrame.Shape.StyledPanel)
75
+ variable_panel.setStyleSheet("background-color: white;")
76
+ variable_panel.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
77
+ watch_layout = QHBoxLayout(variable_panel)
106
78
  watch_layout.setContentsMargins(4, 4, 4, 4)
107
79
  watch_layout.setSpacing(4)
108
80
 
109
-
110
- # Title label
111
81
  title_label = QLabel("VARIABLES")
112
82
  title_label.setStyleSheet("font-weight: bold; letter-spacing: 1px;")
113
83
  watch_layout.addWidget(title_label)
114
-
115
- # Stretch to push buttons right
116
84
  watch_layout.addStretch()
117
85
 
118
- # Placeholder add/remove icons (replace with real icons later)
119
- add_btn = QPushButton()
86
+ add_btn = QPushButton("+")
120
87
  add_btn.setToolTip("Add variable to watch")
121
- # TODO: set add_btn.setIcon(QIcon(path)) when icon is available
122
- add_btn.setText("+")
123
88
  add_btn.setFixedSize(24, 24)
124
89
  add_btn.clicked.connect(self.on_add_clicked)
125
90
  watch_layout.addWidget(add_btn)
126
91
 
127
- layout.addWidget(watch_panel)
128
-
129
- # Watch list area (renders selected variables beneath the toolbar)
130
- self.watch_list_widget = QWidget()
131
- self.watch_list_layout = QVBoxLayout(self.watch_list_widget)
132
- self.watch_list_layout.setContentsMargins(6, 2, 6, 2)
133
- self.watch_list_layout.setSpacing(2)
134
- layout.addWidget(self.watch_list_widget)
135
-
136
- # Keep a simple set to prevent duplicate labels
137
- self._watch_set = set()
92
+ layout.addWidget(variable_panel)
138
93
 
139
- layout.addStretch()
94
+ # Watch list widget
95
+ self.watch_list = WatchListWidget(self.debugger)
96
+ layout.addWidget(self.watch_list, 1)
140
97
 
141
98
  def on_add_clicked(self):
142
- # Build the variable list from the program. Prefer Program.symbols mapping.
143
99
  try:
144
100
  program = self.debugger.program # type: ignore[attr-defined]
145
- # Fallback to scanning code if symbols is empty
146
101
  items = []
147
102
  if hasattr(program, 'symbols') and isinstance(program.symbols, dict) and program.symbols:
148
103
  items = sorted([name for name in program.symbols.keys() if name and not name.endswith(':')])
149
104
  else:
150
- # Fallback heuristic: look for commands whose 'type' == 'symbol' (as per requirement)
151
105
  for cmd in getattr(program, 'code', []):
152
106
  try:
153
107
  if cmd.get('type') == 'symbol' and 'name' in cmd:
@@ -158,80 +112,60 @@ class Debugger(QMainWindow):
158
112
  if not items:
159
113
  QMessageBox.information(self, "Add Watch", "No variables found in this program.")
160
114
  return
161
- choice, ok = QInputDialog.getItem(self, "Add Watch", "Select a variable:", items, 0, False)
162
- if ok and choice:
163
- # Record the choice for future use (UI for list will be added later)
164
- if not hasattr(self.debugger, 'watched'):
165
- self.debugger.watched = [] # type: ignore[attr-defined]
166
- if choice not in self.debugger.watched: # type: ignore[attr-defined]
167
- self.debugger.watched.append(choice) # type: ignore[attr-defined]
168
- # Render as a plain label beneath the toolbar if not already present
169
- if choice not in self._watch_set:
170
- self._add_watch_row(choice)
171
- self._watch_set.add(choice)
172
- # Optionally echo to console for now
173
- try:
174
- self.debugger.console.append(f"Watching: {choice}") # type: ignore[attr-defined]
175
- except Exception:
176
- pass
115
+
116
+ # Use a custom chooser dialog with a visible list (already open)
117
+ from PySide6.QtWidgets import QDialog, QVBoxLayout, QListWidget, QDialogButtonBox
118
+ dlg = QDialog(self)
119
+ dlg.setWindowTitle("Add Watch")
120
+ v = QVBoxLayout(dlg)
121
+ lst = QListWidget(dlg)
122
+ lst.addItems(items)
123
+ lst.setSelectionMode(QListWidget.SelectionMode.SingleSelection)
124
+ if items:
125
+ lst.setCurrentRow(0)
126
+ # Allow double-click to accept immediately
127
+ def accept_double(item):
128
+ if item:
129
+ lst.setCurrentItem(item)
130
+ dlg.accept()
131
+ lst.itemDoubleClicked.connect(accept_double)
132
+ v.addWidget(lst)
133
+ buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel, parent=dlg)
134
+ v.addWidget(buttons)
135
+ buttons.accepted.connect(dlg.accept)
136
+ buttons.rejected.connect(dlg.reject)
137
+ if dlg.exec() == QDialog.DialogCode.Accepted:
138
+ cur = lst.currentItem()
139
+ choice = cur.text() if cur else None
140
+ if choice:
141
+ self.watch_list.addVariable(choice)
142
+ try:
143
+ self.debugger.console.append(f"Watching: {choice}") # type: ignore[attr-defined]
144
+ except Exception:
145
+ pass
177
146
  except Exception as exc:
178
147
  QMessageBox.warning(self, "Add Watch", f"Could not list variables: {exc}")
179
148
 
180
- def _add_watch_row(self, name: str):
181
- row = QWidget()
182
- h = QHBoxLayout(row)
183
- h.setContentsMargins(0, 0, 0, 0)
184
- h.setSpacing(4)
185
- lbl = QLabel(name)
186
- lbl.setStyleSheet("font-family: mono; padding: 1px 2px;")
187
- h.addWidget(lbl)
188
- h.addStretch()
189
- btn = QPushButton()
190
- btn.setText("–") # placeholder until icon provided
191
- btn.setToolTip(f"Remove '{name}' from watch")
192
- btn.setFixedSize(20, 20)
193
-
194
- def on_remove():
195
- try:
196
- # update internal structures
197
- if hasattr(self.debugger, 'watched') and name in self.debugger.watched: # type: ignore[attr-defined]
198
- self.debugger.watched.remove(name) # type: ignore[attr-defined]
199
- if name in self._watch_set:
200
- self._watch_set.remove(name)
201
- # remove row from layout/UI
202
- row.setParent(None)
203
- row.deleteLater()
204
- except Exception:
205
- pass
206
-
207
- btn.clicked.connect(on_remove)
208
- h.addWidget(btn)
209
- self.watch_list_layout.addWidget(row)
210
-
211
- def on_run_clicked(self):
212
- self.debugger.doRun() # type: ignore[attr-defined]
213
-
214
- def on_step_clicked(self):
215
- self.debugger.doStep() # type: ignore[attr-defined]
216
-
217
- def on_stop_clicked(self):
218
- self.debugger.doStop() # type: ignore[attr-defined]
219
-
220
- def on_exit_clicked(self):
221
- self.debugger.doClose() # type: ignore[attr-defined]
222
-
223
149
  ###########################################################################
224
- # The right-hand column of the main window
225
- class MainRightColumn(QWidget):
150
+ # A single script panel that displays one script's lines
151
+ class ScriptPanel(QWidget):
226
152
  scroll: QScrollArea
227
153
  layout: QHBoxLayout # type: ignore[assignment]
228
- blob: QLabel
229
154
 
230
155
  def __init__(self, parent=None):
231
156
  super().__init__(parent)
157
+
158
+ # Set white background
159
+ self.setStyleSheet("background-color: white;")
160
+
161
+ # Main layout
162
+ panel_layout = QVBoxLayout(self)
163
+ panel_layout.setContentsMargins(0, 0, 0, 0)
164
+ panel_layout.setSpacing(0)
232
165
 
233
166
  # Create a scroll area - its content widget holds the lines
234
- self.scroll = QScrollArea(self)
167
+ self.scroll = QScrollArea()
168
+ self.scroll.setStyleSheet("background-color: white;")
235
169
  self.scroll.setWidgetResizable(True)
236
170
 
237
171
  # Ensure this widget and the scroll area expand to fill available space
@@ -249,21 +183,19 @@ class Debugger(QMainWindow):
249
183
 
250
184
  self.scroll.setWidget(self.content)
251
185
 
252
- # outer layout for this widget contains only the scroll area
253
- main_layout = QVBoxLayout(self)
254
- main_layout.setContentsMargins(0, 0, 0, 0)
255
- main_layout.addWidget(self.scroll)
256
- # ensure the scroll area gets the stretch so it fills the parent
257
- main_layout.setStretch(0, 1)
186
+ # Add scroll area to the panel layout
187
+ panel_layout.addWidget(self.scroll)
188
+
189
+ # Store script lines for this panel
190
+ self.scriptLines = []
258
191
 
259
192
  #######################################################################
260
- # Add a line to the right-hand column
193
+ # Add a line to this script panel
261
194
  def addLine(self, spec):
262
195
 
263
- # Determine if this line is a command (not empty, not a comment), using the original script line
264
- orig_line = getattr(spec, 'orig_line', spec.line) if hasattr(spec, 'orig_line') or 'orig_line' in spec.__dict__ else spec.line
265
- line_lstripped = orig_line.lstrip()
266
- is_command = bool(line_lstripped and not line_lstripped.startswith('!'))
196
+ # is_command will be set later by enableBreakpoints() after compilation
197
+ # Initialize to False for now
198
+ spec.is_command = False
267
199
 
268
200
  class Label(QLabel):
269
201
  def __init__(self, text, fixed_width=None, align=Qt.AlignmentFlag.AlignLeft, on_click=None):
@@ -277,7 +209,7 @@ class Debugger(QMainWindow):
277
209
  if fixed_width is not None:
278
210
  self.setFixedWidth(fixed_width)
279
211
  self.setAlignment(align | Qt.AlignmentFlag.AlignVCenter)
280
- self._on_click = on_click if is_command else None
212
+ self._on_click = on_click
281
213
 
282
214
  def mousePressEvent(self, event):
283
215
  if self._on_click:
@@ -313,7 +245,7 @@ class Debugger(QMainWindow):
313
245
  class ClickableBlob(QLabel):
314
246
  def __init__(self, on_click=None):
315
247
  super().__init__()
316
- self._on_click = on_click if is_command else None
248
+ self._on_click = on_click
317
249
  def mousePressEvent(self, event):
318
250
  if self._on_click:
319
251
  try:
@@ -323,7 +255,7 @@ class Debugger(QMainWindow):
323
255
  super().mousePressEvent(event)
324
256
 
325
257
  blob_size = 10
326
- blob = ClickableBlob(on_click=(lambda: spec.onClick(spec.lino)) if is_command else None)
258
+ blob = ClickableBlob(on_click=lambda: spec.onClick(spec.lino))
327
259
  blob.setFixedSize(blob_size, blob_size)
328
260
 
329
261
  def set_blob_visible(widget, visible):
@@ -348,7 +280,7 @@ class Debugger(QMainWindow):
348
280
 
349
281
  # create the line-number label; clicking it reports back to the caller
350
282
  lino_label = Label(str(spec.lino+1), fixed_width=width_4, align=Qt.AlignmentFlag.AlignRight,
351
- on_click=(lambda: spec.onClick(spec.lino)) if is_command else None)
283
+ on_click=lambda: spec.onClick(spec.lino))
352
284
  lino_label.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
353
285
  # create the text label for the line itself
354
286
  text_label = Label(spec.line, fixed_width=None, align=Qt.AlignmentFlag.AlignLeft)
@@ -361,14 +293,119 @@ class Debugger(QMainWindow):
361
293
  self.inner_layout.addWidget(panel)
362
294
  return panel
363
295
 
364
- def showBlob(self):
365
- self.blob.setStyleSheet("background-color: red; border-radius: 5px; margin:0px; padding:0px;")
296
+ def addStretch(self):
297
+ self.inner_layout.addStretch()
298
+
299
+ ###########################################################################
300
+ # The right-hand column of the main window
301
+ class MainRightColumn(QWidget):
366
302
 
367
- def hideBlob(self):
368
- self.blob.setStyleSheet("background-color: none; border-radius: 5px; margin:0px; padding:0px;")
303
+ def __init__(self, parent=None):
304
+ super().__init__(parent)
305
+
306
+ # Set white background for the entire column
307
+ self.setStyleSheet("background-color: white;")
308
+
309
+ # Main layout for this column
310
+ column_layout = QVBoxLayout(self)
311
+ column_layout.setContentsMargins(0, 0, 0, 0)
312
+ column_layout.setSpacing(0)
313
+
314
+ # Create toolbar with icon buttons
315
+ toolbar = QToolBar()
316
+ toolbar.setMovable(False)
317
+
318
+ # Get the icons directory path
319
+ icons_dir = os.path.join(os.path.dirname(__file__), '../icons')
320
+
321
+ # Get the parent debugger for callbacks
322
+ debugger = parent
323
+
324
+ # Run button
325
+ run_btn = QPushButton()
326
+ run_icon_path = os.path.join(icons_dir, 'run.png')
327
+ run_btn.setIcon(QIcon(run_icon_path))
328
+ run_btn.setToolTip("Run")
329
+ run_btn.clicked.connect(lambda: debugger.doRun() if debugger else None) # type: ignore[attr-defined]
330
+ toolbar.addWidget(run_btn)
331
+
332
+ # Step button
333
+ step_btn = QPushButton()
334
+ step_icon_path = os.path.join(icons_dir, 'step.png')
335
+ step_btn.setIcon(QIcon(step_icon_path))
336
+ step_btn.setToolTip("Step")
337
+ step_btn.clicked.connect(lambda: debugger.doStep() if debugger else None) # type: ignore[attr-defined]
338
+ toolbar.addWidget(step_btn)
339
+
340
+ # Stop button
341
+ stop_btn = QPushButton()
342
+ stop_icon_path = os.path.join(icons_dir, 'stop.png')
343
+ stop_btn.setIcon(QIcon(stop_icon_path))
344
+ stop_btn.setToolTip("Stop")
345
+ stop_btn.clicked.connect(lambda: debugger.doStop() if debugger else None) # type: ignore[attr-defined]
346
+ toolbar.addWidget(stop_btn)
347
+
348
+ # Exit button
349
+ exit_btn = QPushButton()
350
+ exit_icon_path = os.path.join(icons_dir, 'exit.png')
351
+ exit_btn.setIcon(QIcon(exit_icon_path))
352
+ exit_btn.setToolTip("Exit")
353
+ exit_btn.clicked.connect(lambda: debugger.doClose() if debugger else None) # type: ignore[attr-defined]
354
+ toolbar.addWidget(exit_btn)
355
+
356
+ column_layout.addWidget(toolbar)
357
+
358
+ # Create a tab widget to hold multiple script panels
359
+ self.tabWidget = QTabWidget()
360
+ self.tabWidget.setTabsClosable(False) # Don't allow closing tabs for now
361
+
362
+ # Ensure tab widget expands
363
+ self.tabWidget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
364
+
365
+ column_layout.addWidget(self.tabWidget)
366
+
367
+ # Dictionary to map program -> script panel
368
+ self.programPanels = {}
369
+
370
+ #######################################################################
371
+ # Add a new script tab
372
+ def addScriptTab(self, program, filename):
373
+ """Add a new tab for a script"""
374
+ panel = Debugger.ScriptPanel(self)
375
+ # Extract just the filename from the full path
376
+ tab_label = os.path.basename(filename)
377
+ self.tabWidget.addTab(panel, tab_label)
378
+ self.programPanels[id(program)] = panel
379
+ return panel
380
+
381
+ #######################################################################
382
+ # Get the current active script panel
383
+ def getCurrentPanel(self):
384
+ """Get the currently active script panel"""
385
+ return self.tabWidget.currentWidget()
386
+
387
+ #######################################################################
388
+ # Get the panel for a specific program
389
+ def getPanelForProgram(self, program):
390
+ """Get the panel associated with a program"""
391
+ return self.programPanels.get(id(program))
392
+
393
+ #######################################################################
394
+ # Legacy method - add a line to the current panel
395
+ def addLine(self, spec):
396
+ """Delegate to the current panel's addLine method"""
397
+ panel = self.getCurrentPanel()
398
+ if panel and isinstance(panel, Debugger.ScriptPanel):
399
+ return panel.addLine(spec)
400
+ return None
369
401
 
402
+ #######################################################################
403
+ # Add stretch to current panel
370
404
  def addStretch(self):
371
- self.layout.addStretch()
405
+ """Delegate to the current panel's addStretch method"""
406
+ panel = self.getCurrentPanel()
407
+ if panel and isinstance(panel, Debugger.ScriptPanel):
408
+ panel.addStretch()
372
409
 
373
410
  ###########################################################################
374
411
  # Main debugger class initializer
@@ -380,6 +417,7 @@ class Debugger(QMainWindow):
380
417
  # Disable the window close button
381
418
  self.setWindowFlags(self.windowFlags() & ~Qt.WindowType.WindowCloseButtonHint)
382
419
  self.stopped = True
420
+ self.skip_next_breakpoint = False # Flag to skip breakpoint check on resume
383
421
 
384
422
  # try to load saved geometry from ~/.ecdebug.conf
385
423
  cfg_path = os.path.join(os.path.expanduser("~"), ".ecdebug.conf")
@@ -489,7 +527,7 @@ class Debugger(QMainWindow):
489
527
 
490
528
  # Use the vertical splitter as the central widget
491
529
  self.setCentralWidget(self.vsplitter)
492
- self.parse(program.script.lines)
530
+ self.parse(program.script.lines, program, program.scriptName)
493
531
  self.show()
494
532
 
495
533
  def _flush_console_buffer(self):
@@ -523,15 +561,41 @@ class Debugger(QMainWindow):
523
561
 
524
562
  ###########################################################################
525
563
  # Parse a script into the right-hand column
526
- def parse(self, script):
564
+ def parse(self, script, program=None, filename=None):
565
+ """Parse a script and add it as a tab
566
+
567
+ Args:
568
+ script: List of script lines
569
+ program: The Program instance this script belongs to
570
+ filename: The filename to use as the tab label
571
+ """
527
572
  self.scriptLines = []
528
- # Clear existing lines from the right column layout
529
- layout = self.rightColumn.inner_layout
530
- while layout.count():
531
- item = layout.takeAt(0)
532
- widget = item.widget()
533
- if widget:
534
- widget.deleteLater()
573
+
574
+ # Get or create the panel for this program
575
+ panel = None
576
+ if program:
577
+ panel = self.rightColumn.getPanelForProgram(program)
578
+ if not panel:
579
+ # Create a new tab for this program
580
+ if not filename:
581
+ filename = getattr(program, 'path', 'Untitled')
582
+ panel = self.rightColumn.addScriptTab(program, filename)
583
+
584
+ # If no program specified or panel creation failed, use current panel
585
+ if not panel:
586
+ panel = self.rightColumn.getCurrentPanel()
587
+ if not panel:
588
+ # Create a default tab
589
+ panel = self.rightColumn.addScriptTab(None, filename or 'Untitled')
590
+
591
+ # Clear existing lines from the panel
592
+ if panel and isinstance(panel, Debugger.ScriptPanel):
593
+ layout = panel.inner_layout
594
+ while layout.count():
595
+ item = layout.takeAt(0)
596
+ widget = item.widget()
597
+ if widget:
598
+ widget.deleteLater()
535
599
 
536
600
  # Parse and add new lines
537
601
  lino = 0
@@ -631,11 +695,49 @@ class Debugger(QMainWindow):
631
695
 
632
696
  return output
633
697
 
698
+ ###########################################################################
699
+ # Enable breakpoints to be set on any line that is classed as a command
700
+ def enableBreakpoints(self):
701
+ """
702
+ Examine each line and set is_command flag based on compiled code.
703
+ A line is a command if:
704
+ - It's not empty or a comment
705
+ - It's not a label (single word ending with colon)
706
+ - It corresponds to a command in program.code with type != 'symbol'
707
+ """
708
+ # First, mark all lines as non-commands by default
709
+ for lineSpec in self.scriptLines:
710
+ lineSpec.is_command = False
711
+
712
+ # Now iterate through compiled commands and mark those that are executable
713
+ for command in self.program.code:
714
+ if 'lino' not in command:
715
+ continue
716
+
717
+ lino = command['lino']
718
+ if lino < 0 or lino >= len(self.scriptLines):
719
+ continue
720
+
721
+ # Check if this is a symbol declaration (variable/constant definition)
722
+ if command.get('type') == 'symbol':
723
+ continue
724
+
725
+ # Check if this is a structural keyword that shouldn't have breakpoints
726
+ if command.get('keyword') in ['begin', 'end']:
727
+ continue
728
+
729
+ # This is an executable command
730
+ self.scriptLines[lino].is_command = True
731
+
634
732
  ###########################################################################
635
733
  # Here when the user clicks a line number
636
734
  def onClickLino(self, lino):
637
- # Show or hide the red blob next to this line
735
+ # Check if this line is a command - if not, take no action
638
736
  lineSpec = self.scriptLines[lino]
737
+ if not getattr(lineSpec, 'is_command', True):
738
+ return
739
+
740
+ # Show or hide the red blob next to this line
639
741
  lineSpec.bp = not lineSpec.bp
640
742
  if lineSpec.bp: lineSpec.label.showBlob()
641
743
  else: lineSpec.label.hideBlob()
@@ -659,8 +761,13 @@ class Debugger(QMainWindow):
659
761
  if not panel:
660
762
  return
661
763
 
662
- # Get the scroll area from the right column
663
- scroll_area = self.rightColumn.scroll
764
+ # Get the current script panel
765
+ script_panel = self.rightColumn.getCurrentPanel()
766
+ if not script_panel or not isinstance(script_panel, Debugger.ScriptPanel):
767
+ return
768
+
769
+ # Get the scroll area from the script panel
770
+ scroll_area = script_panel.scroll
664
771
 
665
772
  # Get the vertical position of the panel relative to the content widget
666
773
  panel_y = panel.y()
@@ -700,31 +807,91 @@ class Debugger(QMainWindow):
700
807
  panel.setStyleSheet(f"background-color: {color};")
701
808
 
702
809
  ###########################################################################
703
- # Here after each instruction has run
704
- def continueExecution(self):
705
- result = True
810
+ # Here before each instruction is run
811
+ # Returns True if the program should halt and wait for user interaction
812
+ def checkIfHalt(self, is_first_command=False):
706
813
  self.pc = self.program.pc
707
814
  command = self.program.code[self.pc]
708
- lino = command['lino'] + 1
709
- if self.stopped: result = False
710
- elif command['bp']:
711
- print(f"Hit breakpoint at line {lino}")
815
+ lino = command['lino'] if 'lino' in command else 0
816
+ bp = command.get('bp', False)
817
+
818
+ # Check if we should skip this breakpoint check (resuming from same location)
819
+ if self.skip_next_breakpoint:
820
+ self.skip_next_breakpoint = False
821
+ return False
822
+
823
+ # Labels should never halt execution - they're just markers
824
+ # A label is a symbol whose name ends with ':'
825
+ if command.get('type') == 'symbol' and command.get('name', '').endswith(':'):
826
+ return False
827
+
828
+ # Determine if we should halt
829
+ should_halt = False
830
+
831
+ # If this is the first real command (pc==1), always halt to initialize display
832
+ if is_first_command:
833
+ should_halt = True
712
834
  self.stopped = True
713
- result = False
714
- if not result:
835
+ print(f"Program ready at line {lino + 1}")
836
+ # If we're in stopped (step) mode, halt after each command
837
+ elif self.stopped:
838
+ should_halt = True
839
+ # If there's a breakpoint on this line, halt
840
+ elif bp:
841
+ print(f"Hit breakpoint at line {lino + 1}")
842
+ self.stopped = True
843
+ should_halt = True
844
+
845
+ # If halting, update the UI
846
+ if should_halt:
715
847
  self.scrollTo(lino)
716
- self.setBackground(command['lino'], 'LightYellow')
717
- return result
848
+ self.setBackground(lino, 'LightYellow')
849
+ # Refresh variable values when halted
850
+ self.refreshVariables()
851
+
852
+ return should_halt
853
+
854
+ def refreshVariables(self):
855
+ """Update all watched variable values"""
856
+ try:
857
+ if hasattr(self, 'leftColumn') and hasattr(self.leftColumn, 'watch_list'):
858
+ self.leftColumn.watch_list.refreshVariables(self.program)
859
+ except Exception as ex:
860
+ print(f"Error refreshing variables: {ex}")
718
861
 
719
862
  def doRun(self):
863
+ """Resume free-running execution from current PC"""
864
+ command = self.program.code[self.pc]
865
+ lino = command.get('lino', 0)
866
+ print(f"Continuing execution at line {lino + 1}")
867
+
868
+ # Clear the highlight on the current line
869
+ self.setBackground(lino, 'none')
870
+
871
+ # Switch to free-running mode
720
872
  self.stopped = False
721
- print("Continuing execution at line", self.program.pc + 1)
873
+
874
+ # Skip the breakpoint check for the current instruction (the one we're resuming from)
875
+ self.skip_next_breakpoint = True
876
+
877
+ # Resume execution at current PC
722
878
  self.program.run(self.pc)
723
879
 
724
880
  def doStep(self):
881
+ """Execute one instruction and halt again"""
725
882
  command = self.program.code[self.pc]
726
- # print("Stepping at line", command['lino'] + 1)
727
- self.setBackground(command['lino'], 'none')
883
+ lino = command.get('lino', 0)
884
+
885
+ # Clear the highlight on the current line
886
+ self.setBackground(lino, 'none')
887
+
888
+ # Stay in stopped mode (will halt after next instruction)
889
+ self.stopped = True
890
+
891
+ # Skip the breakpoint check for the current instruction (the one we're stepping from)
892
+ self.skip_next_breakpoint = True
893
+
894
+ # Execute the current instruction
728
895
  self.program.run(self.pc)
729
896
 
730
897
  def doStop(self):