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.
- easycoder/__init__.py +4 -2
- easycoder/debugger/__init__.py +5 -0
- easycoder/debugger/ec_dbg_value_display copy.py +195 -0
- easycoder/debugger/ec_dbg_value_display.py +23 -0
- easycoder/debugger/ec_dbg_watch_list copy.py +219 -0
- easycoder/debugger/ec_dbg_watchlist.py +159 -0
- easycoder/{ec_debug.py → debugger/ec_debug.py} +349 -182
- easycoder/ec_classes.py +284 -11
- easycoder/ec_compiler.py +55 -39
- easycoder/ec_condition.py +1 -1
- easycoder/ec_core.py +846 -1007
- easycoder/ec_gclasses.py +225 -0
- easycoder/{ec_pyside.py → ec_graphics.py} +566 -431
- easycoder/ec_handler.py +16 -13
- easycoder/ec_program.py +262 -168
- easycoder/ec_psutil.py +48 -0
- easycoder/ec_value.py +29 -32
- {easycoder-251105.1.dist-info → easycoder-251215.2.dist-info}/METADATA +11 -1
- easycoder-251215.2.dist-info/RECORD +31 -0
- easycoder-251105.1.dist-info/RECORD +0 -24
- {easycoder-251105.1.dist-info → easycoder-251215.2.dist-info}/WHEEL +0 -0
- {easycoder-251105.1.dist-info → easycoder-251215.2.dist-info}/entry_points.txt +0 -0
- {easycoder-251105.1.dist-info → easycoder-251215.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
#
|
|
225
|
-
class
|
|
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(
|
|
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
|
-
#
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
|
193
|
+
# Add a line to this script panel
|
|
261
194
|
def addLine(self, spec):
|
|
262
195
|
|
|
263
|
-
#
|
|
264
|
-
|
|
265
|
-
|
|
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
|
|
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
|
|
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=
|
|
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=
|
|
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
|
|
365
|
-
self.
|
|
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
|
|
368
|
-
|
|
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
|
-
|
|
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
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
if
|
|
534
|
-
|
|
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
|
-
#
|
|
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
|
|
663
|
-
|
|
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
|
|
704
|
-
|
|
705
|
-
|
|
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']
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
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
|
-
|
|
714
|
-
|
|
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(
|
|
717
|
-
|
|
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
|
-
|
|
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
|
-
|
|
727
|
-
|
|
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):
|