easycoder 251104.2__py2.py3-none-any.whl → 260110.1__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.
Potentially problematic release.
This version of easycoder might be problematic. Click here for more details.
- easycoder/__init__.py +5 -3
- easycoder/debugger/__init__.py +5 -0
- easycoder/debugger/ec_dbg_value_display copy.py +195 -0
- easycoder/debugger/ec_dbg_value_display.py +24 -0
- easycoder/debugger/ec_dbg_watch_list copy.py +219 -0
- easycoder/debugger/ec_dbg_watchlist.py +293 -0
- easycoder/debugger/ec_debug.py +1025 -0
- easycoder/ec_border.py +15 -11
- easycoder/ec_classes.py +487 -11
- easycoder/ec_compiler.py +81 -44
- easycoder/ec_condition.py +1 -1
- easycoder/ec_core.py +1044 -1090
- easycoder/ec_gclasses.py +236 -0
- easycoder/ec_graphics.py +1683 -0
- easycoder/ec_handler.py +18 -13
- easycoder/ec_keyboard.py +50 -50
- easycoder/ec_program.py +299 -156
- easycoder/ec_psutil.py +48 -0
- easycoder/ec_timestamp.py +2 -1
- easycoder/ec_value.py +65 -47
- easycoder/icons/exit.png +0 -0
- easycoder/icons/run.png +0 -0
- easycoder/icons/step.png +0 -0
- easycoder/icons/stop.png +0 -0
- easycoder/pre/README.md +3 -0
- easycoder/pre/__init__.py +17 -0
- easycoder/pre/debugger/__init__.py +5 -0
- easycoder/pre/debugger/ec_dbg_value_display copy.py +195 -0
- easycoder/pre/debugger/ec_dbg_value_display.py +24 -0
- easycoder/pre/debugger/ec_dbg_watch_list copy.py +219 -0
- easycoder/pre/debugger/ec_dbg_watchlist.py +293 -0
- easycoder/pre/debugger/ec_debug.py +1014 -0
- easycoder/pre/ec_border.py +67 -0
- easycoder/pre/ec_classes.py +470 -0
- easycoder/pre/ec_compiler.py +291 -0
- easycoder/pre/ec_condition.py +27 -0
- easycoder/pre/ec_core.py +2772 -0
- easycoder/pre/ec_gclasses.py +230 -0
- easycoder/{ec_pyside.py → pre/ec_graphics.py} +631 -496
- easycoder/pre/ec_handler.py +79 -0
- easycoder/pre/ec_keyboard.py +439 -0
- easycoder/pre/ec_program.py +557 -0
- easycoder/pre/ec_psutil.py +48 -0
- easycoder/pre/ec_timestamp.py +11 -0
- easycoder/pre/ec_value.py +124 -0
- easycoder/pre/icons/close.png +0 -0
- easycoder/pre/icons/exit.png +0 -0
- easycoder/pre/icons/run.png +0 -0
- easycoder/pre/icons/step.png +0 -0
- easycoder/pre/icons/stop.png +0 -0
- easycoder/pre/icons/tick.png +0 -0
- {easycoder-251104.2.dist-info → easycoder-260110.1.dist-info}/METADATA +11 -1
- easycoder-260110.1.dist-info/RECORD +58 -0
- easycoder/ec_debug.py +0 -464
- easycoder-251104.2.dist-info/RECORD +0 -20
- /easycoder/{close.png → icons/close.png} +0 -0
- /easycoder/{tick.png → icons/tick.png} +0 -0
- {easycoder-251104.2.dist-info → easycoder-260110.1.dist-info}/WHEEL +0 -0
- {easycoder-251104.2.dist-info → easycoder-260110.1.dist-info}/entry_points.txt +0 -0
- {easycoder-251104.2.dist-info → easycoder-260110.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,1014 @@
|
|
|
1
|
+
import sys, os, json, html
|
|
2
|
+
from PySide6.QtWidgets import (
|
|
3
|
+
QMainWindow,
|
|
4
|
+
QWidget,
|
|
5
|
+
QFrame,
|
|
6
|
+
QHBoxLayout,
|
|
7
|
+
QGridLayout,
|
|
8
|
+
QVBoxLayout,
|
|
9
|
+
QLabel,
|
|
10
|
+
QSplitter,
|
|
11
|
+
QMessageBox,
|
|
12
|
+
QScrollArea,
|
|
13
|
+
QScrollBar,
|
|
14
|
+
QSizePolicy,
|
|
15
|
+
QToolBar,
|
|
16
|
+
QPushButton,
|
|
17
|
+
QInputDialog,
|
|
18
|
+
QTabWidget
|
|
19
|
+
)
|
|
20
|
+
from PySide6.QtGui import QTextCursor, QIcon
|
|
21
|
+
from PySide6.QtCore import Qt, QTimer
|
|
22
|
+
from typing import Any, Optional
|
|
23
|
+
from .ec_dbg_value_display import ValueDisplay
|
|
24
|
+
from .ec_dbg_watchlist import WatchListWidget
|
|
25
|
+
|
|
26
|
+
class Object():
|
|
27
|
+
def __setattr__(self, name: str, value: Any) -> None:
|
|
28
|
+
self.__dict__[name] = value
|
|
29
|
+
|
|
30
|
+
def __getattr__(self, name: str) -> Any:
|
|
31
|
+
return self.__dict__.get(name)
|
|
32
|
+
|
|
33
|
+
class Debugger(QMainWindow):
|
|
34
|
+
# Help type-checkers know these attributes exist
|
|
35
|
+
_flush_timer: Optional[QTimer]
|
|
36
|
+
|
|
37
|
+
class ConsoleWriter:
|
|
38
|
+
def __init__(self, debugger: 'Debugger'):
|
|
39
|
+
self.debugger = debugger
|
|
40
|
+
self._buf: list[str] = []
|
|
41
|
+
|
|
42
|
+
def write(self, text: str):
|
|
43
|
+
if not text:
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
# Echo all output to original stdout with proper line breaks
|
|
47
|
+
try:
|
|
48
|
+
if self.debugger._orig_stdout:
|
|
49
|
+
self.debugger._orig_stdout.write(text)
|
|
50
|
+
self.debugger._orig_stdout.flush()
|
|
51
|
+
except Exception:
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
# Check if this looks like an error message - if so, also write to original stderr
|
|
55
|
+
if any(err_marker in text for err_marker in ['Error', 'Traceback', 'Exception']):
|
|
56
|
+
try:
|
|
57
|
+
if self.debugger._orig_stderr:
|
|
58
|
+
self.debugger._orig_stderr.write(text)
|
|
59
|
+
self.debugger._orig_stderr.flush()
|
|
60
|
+
except Exception:
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
# Buffer text and request a flush on the GUI timer
|
|
64
|
+
self._buf.append(text)
|
|
65
|
+
if self.debugger._flush_timer and not self.debugger._flush_timer.isActive():
|
|
66
|
+
self.debugger._flush_timer.start()
|
|
67
|
+
|
|
68
|
+
def flush(self):
|
|
69
|
+
# Explicit flush request
|
|
70
|
+
self.debugger._flush_console_buffer()
|
|
71
|
+
|
|
72
|
+
###########################################################################
|
|
73
|
+
# The left-hand column of the main window
|
|
74
|
+
class MainLeftColumn(QWidget):
|
|
75
|
+
def __init__(self, parent=None):
|
|
76
|
+
super().__init__(parent)
|
|
77
|
+
self.debugger = parent
|
|
78
|
+
layout = QVBoxLayout(self)
|
|
79
|
+
|
|
80
|
+
# Header panel
|
|
81
|
+
variable_panel = QFrame()
|
|
82
|
+
variable_panel.setFrameShape(QFrame.Shape.StyledPanel)
|
|
83
|
+
variable_panel.setStyleSheet("background-color: white;")
|
|
84
|
+
variable_panel.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
|
85
|
+
watch_layout = QHBoxLayout(variable_panel)
|
|
86
|
+
watch_layout.setContentsMargins(4, 4, 4, 4)
|
|
87
|
+
watch_layout.setSpacing(4)
|
|
88
|
+
|
|
89
|
+
title_label = QLabel("VARIABLES")
|
|
90
|
+
title_label.setStyleSheet("font-weight: bold; letter-spacing: 1px;")
|
|
91
|
+
watch_layout.addWidget(title_label)
|
|
92
|
+
watch_layout.addStretch()
|
|
93
|
+
|
|
94
|
+
add_btn = QPushButton("+")
|
|
95
|
+
add_btn.setToolTip("Add variable to watch")
|
|
96
|
+
add_btn.setFixedSize(24, 24)
|
|
97
|
+
add_btn.clicked.connect(self.on_add_clicked)
|
|
98
|
+
watch_layout.addWidget(add_btn)
|
|
99
|
+
|
|
100
|
+
layout.addWidget(variable_panel)
|
|
101
|
+
|
|
102
|
+
# Watch list widget
|
|
103
|
+
self.watch_list = WatchListWidget(self.debugger)
|
|
104
|
+
layout.addWidget(self.watch_list, 1)
|
|
105
|
+
|
|
106
|
+
def on_add_clicked(self):
|
|
107
|
+
try:
|
|
108
|
+
program = self.debugger.program # type: ignore[attr-defined]
|
|
109
|
+
items = []
|
|
110
|
+
if hasattr(program, 'symbols') and isinstance(program.symbols, dict) and program.symbols:
|
|
111
|
+
items = sorted([name for name in program.symbols.keys() if name and not name.endswith(':')])
|
|
112
|
+
else:
|
|
113
|
+
for cmd in getattr(program, 'code', []):
|
|
114
|
+
try:
|
|
115
|
+
if cmd.get('type') == 'symbol' and 'name' in cmd:
|
|
116
|
+
items.append(cmd['name'])
|
|
117
|
+
except Exception:
|
|
118
|
+
pass
|
|
119
|
+
items = sorted(set(items))
|
|
120
|
+
if not items:
|
|
121
|
+
QMessageBox.information(self, "Add Watch", "No variables found in this program.")
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
# Use a custom chooser dialog with a visible list (already open)
|
|
125
|
+
from PySide6.QtWidgets import QDialog, QVBoxLayout, QListWidget, QDialogButtonBox
|
|
126
|
+
dlg = QDialog(self)
|
|
127
|
+
dlg.setWindowTitle("Add Watch")
|
|
128
|
+
v = QVBoxLayout(dlg)
|
|
129
|
+
lst = QListWidget(dlg)
|
|
130
|
+
lst.addItems(items)
|
|
131
|
+
lst.setSelectionMode(QListWidget.SelectionMode.SingleSelection)
|
|
132
|
+
if items:
|
|
133
|
+
lst.setCurrentRow(0)
|
|
134
|
+
# Allow double-click to accept immediately
|
|
135
|
+
def accept_double(item):
|
|
136
|
+
if item:
|
|
137
|
+
lst.setCurrentItem(item)
|
|
138
|
+
dlg.accept()
|
|
139
|
+
lst.itemDoubleClicked.connect(accept_double)
|
|
140
|
+
v.addWidget(lst)
|
|
141
|
+
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel, parent=dlg)
|
|
142
|
+
v.addWidget(buttons)
|
|
143
|
+
buttons.accepted.connect(dlg.accept)
|
|
144
|
+
buttons.rejected.connect(dlg.reject)
|
|
145
|
+
if dlg.exec() == QDialog.DialogCode.Accepted:
|
|
146
|
+
cur = lst.currentItem()
|
|
147
|
+
choice = cur.text() if cur else None
|
|
148
|
+
if choice:
|
|
149
|
+
self.watch_list.addVariable(choice)
|
|
150
|
+
try:
|
|
151
|
+
self.debugger.console.append(f"Watching: {choice}") # type: ignore[attr-defined]
|
|
152
|
+
except Exception:
|
|
153
|
+
pass
|
|
154
|
+
except Exception as exc:
|
|
155
|
+
QMessageBox.warning(self, "Add Watch", f"Could not list variables: {exc}")
|
|
156
|
+
|
|
157
|
+
###########################################################################
|
|
158
|
+
# A single script panel that displays one script's lines
|
|
159
|
+
class ScriptPanel(QWidget):
|
|
160
|
+
scroll: QScrollArea
|
|
161
|
+
layout: QHBoxLayout # type: ignore[assignment]
|
|
162
|
+
|
|
163
|
+
def __init__(self, parent=None):
|
|
164
|
+
super().__init__(parent)
|
|
165
|
+
|
|
166
|
+
# Set white background
|
|
167
|
+
self.setStyleSheet("background-color: white;")
|
|
168
|
+
|
|
169
|
+
# Main layout
|
|
170
|
+
panel_layout = QVBoxLayout(self)
|
|
171
|
+
panel_layout.setContentsMargins(0, 0, 0, 0)
|
|
172
|
+
panel_layout.setSpacing(0)
|
|
173
|
+
|
|
174
|
+
# Create a scroll area - its content widget holds the lines
|
|
175
|
+
self.scroll = QScrollArea()
|
|
176
|
+
self.scroll.setStyleSheet("background-color: white;")
|
|
177
|
+
self.scroll.setWidgetResizable(True)
|
|
178
|
+
|
|
179
|
+
# Ensure this widget and the scroll area expand to fill available space
|
|
180
|
+
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
|
181
|
+
self.scroll.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
|
182
|
+
|
|
183
|
+
self.content = QWidget()
|
|
184
|
+
# let the content expand horizontally but have flexible height
|
|
185
|
+
self.content.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
|
|
186
|
+
|
|
187
|
+
self.inner_layout = QVBoxLayout(self.content)
|
|
188
|
+
# spacing and small top/bottom margins to separate lines
|
|
189
|
+
self.inner_layout.setSpacing(0)
|
|
190
|
+
self.inner_layout.setContentsMargins(0, 0, 0, 0)
|
|
191
|
+
|
|
192
|
+
self.scroll.setWidget(self.content)
|
|
193
|
+
|
|
194
|
+
# Add scroll area to the panel layout
|
|
195
|
+
panel_layout.addWidget(self.scroll)
|
|
196
|
+
|
|
197
|
+
# Store script lines for this panel
|
|
198
|
+
self.scriptLines = []
|
|
199
|
+
|
|
200
|
+
#######################################################################
|
|
201
|
+
# Add a line to this script panel
|
|
202
|
+
def addLine(self, spec):
|
|
203
|
+
|
|
204
|
+
# is_command will be set later by enableBreakpoints() after compilation
|
|
205
|
+
# Initialize to False for now
|
|
206
|
+
spec.is_command = False
|
|
207
|
+
|
|
208
|
+
class Label(QLabel):
|
|
209
|
+
def __init__(self, text, fixed_width=None, align=Qt.AlignmentFlag.AlignLeft, on_click=None):
|
|
210
|
+
super().__init__()
|
|
211
|
+
self.setText(text)
|
|
212
|
+
self.setMargin(0)
|
|
213
|
+
self.setContentsMargins(0, 0, 0, 0)
|
|
214
|
+
self.setStyleSheet("padding:0px; margin:0px; font-family: mono")
|
|
215
|
+
fm = self.fontMetrics()
|
|
216
|
+
self.setFixedHeight(fm.height())
|
|
217
|
+
if fixed_width is not None:
|
|
218
|
+
self.setFixedWidth(fixed_width)
|
|
219
|
+
self.setAlignment(align | Qt.AlignmentFlag.AlignVCenter)
|
|
220
|
+
self._on_click = on_click
|
|
221
|
+
|
|
222
|
+
def mousePressEvent(self, event):
|
|
223
|
+
if self._on_click:
|
|
224
|
+
try:
|
|
225
|
+
self._on_click()
|
|
226
|
+
except Exception:
|
|
227
|
+
pass
|
|
228
|
+
super().mousePressEvent(event)
|
|
229
|
+
|
|
230
|
+
spec.label = self
|
|
231
|
+
panel = QWidget()
|
|
232
|
+
# ensure the panel itself has no margins
|
|
233
|
+
try:
|
|
234
|
+
panel.setContentsMargins(0, 0, 0, 0)
|
|
235
|
+
except Exception:
|
|
236
|
+
pass
|
|
237
|
+
# tidy layout: remove spacing/margins so lines sit flush
|
|
238
|
+
layout = QHBoxLayout(panel)
|
|
239
|
+
layout.setSpacing(0)
|
|
240
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
|
241
|
+
self.layout: QHBoxLayout = layout # type: ignore
|
|
242
|
+
# make panel take minimal vertical space
|
|
243
|
+
panel.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
|
244
|
+
# compute width to fit a 4-digit line number using this widget's font
|
|
245
|
+
fm_main = self.fontMetrics()
|
|
246
|
+
width_4 = fm_main.horizontalAdvance('0000') + 8
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
# create the red blob (always present). We'll toggle its opacity
|
|
250
|
+
# by changing the stylesheet (rgba alpha 255/0). Do NOT store it
|
|
251
|
+
# on the MainRightColumn instance — keep it per-line.
|
|
252
|
+
|
|
253
|
+
class ClickableBlob(QLabel):
|
|
254
|
+
def __init__(self, on_click=None):
|
|
255
|
+
super().__init__()
|
|
256
|
+
self._on_click = on_click
|
|
257
|
+
def mousePressEvent(self, event):
|
|
258
|
+
if self._on_click:
|
|
259
|
+
try:
|
|
260
|
+
self._on_click()
|
|
261
|
+
except Exception:
|
|
262
|
+
pass
|
|
263
|
+
super().mousePressEvent(event)
|
|
264
|
+
|
|
265
|
+
blob_size = 10
|
|
266
|
+
blob = ClickableBlob(on_click=lambda: spec.onClick(spec.lino))
|
|
267
|
+
blob.setFixedSize(blob_size, blob_size)
|
|
268
|
+
|
|
269
|
+
def set_blob_visible(widget, visible):
|
|
270
|
+
alpha = 255 if visible else 0
|
|
271
|
+
widget.setStyleSheet(f"background-color: rgba(255,0,0,{alpha}); border-radius: {blob_size//2}px; margin:0px; padding:0px;")
|
|
272
|
+
widget._blob_visible = visible
|
|
273
|
+
# force repaint
|
|
274
|
+
widget.update()
|
|
275
|
+
|
|
276
|
+
# attach methods to this blob so callers can toggle it via spec.label
|
|
277
|
+
blob.showBlob = lambda: set_blob_visible(blob, True) # type: ignore[attr-defined]
|
|
278
|
+
blob.hideBlob = lambda: set_blob_visible(blob, False) # type: ignore[attr-defined]
|
|
279
|
+
|
|
280
|
+
# initialize according to spec flag
|
|
281
|
+
if spec.bp:
|
|
282
|
+
blob.showBlob() # type: ignore[attr-defined]
|
|
283
|
+
else:
|
|
284
|
+
blob.hideBlob() # type: ignore[attr-defined]
|
|
285
|
+
|
|
286
|
+
# expose the blob to the outside via spec['label'] so onClick can call showBlob/hideBlob
|
|
287
|
+
spec.label = blob
|
|
288
|
+
|
|
289
|
+
# create the line-number label; clicking it reports back to the caller
|
|
290
|
+
lino_label = Label(str(spec.lino+1), fixed_width=width_4, align=Qt.AlignmentFlag.AlignRight,
|
|
291
|
+
on_click=lambda: spec.onClick(spec.lino))
|
|
292
|
+
lino_label.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
|
|
293
|
+
# create the text label for the line itself
|
|
294
|
+
text_label = Label(spec.line, fixed_width=None, align=Qt.AlignmentFlag.AlignLeft)
|
|
295
|
+
text_label.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
|
296
|
+
layout.addWidget(lino_label)
|
|
297
|
+
layout.addSpacing(10)
|
|
298
|
+
layout.addWidget(blob, 0, Qt.AlignmentFlag.AlignVCenter)
|
|
299
|
+
layout.addSpacing(3)
|
|
300
|
+
layout.addWidget(text_label)
|
|
301
|
+
self.inner_layout.addWidget(panel)
|
|
302
|
+
return panel
|
|
303
|
+
|
|
304
|
+
def addStretch(self):
|
|
305
|
+
self.inner_layout.addStretch()
|
|
306
|
+
|
|
307
|
+
###########################################################################
|
|
308
|
+
# The right-hand column of the main window
|
|
309
|
+
class MainRightColumn(QWidget):
|
|
310
|
+
|
|
311
|
+
def __init__(self, parent=None):
|
|
312
|
+
super().__init__(parent)
|
|
313
|
+
|
|
314
|
+
# Set white background for the entire column
|
|
315
|
+
self.setStyleSheet("background-color: white;")
|
|
316
|
+
|
|
317
|
+
# Main layout for this column
|
|
318
|
+
column_layout = QVBoxLayout(self)
|
|
319
|
+
column_layout.setContentsMargins(0, 0, 0, 0)
|
|
320
|
+
column_layout.setSpacing(0)
|
|
321
|
+
|
|
322
|
+
# Create toolbar with icon buttons
|
|
323
|
+
toolbar = QToolBar()
|
|
324
|
+
toolbar.setMovable(False)
|
|
325
|
+
|
|
326
|
+
# Get the icons directory path
|
|
327
|
+
icons_dir = os.path.join(os.path.dirname(__file__), '../icons')
|
|
328
|
+
|
|
329
|
+
# Get the parent debugger for callbacks
|
|
330
|
+
debugger = parent
|
|
331
|
+
|
|
332
|
+
# Run button
|
|
333
|
+
run_btn = QPushButton()
|
|
334
|
+
run_icon_path = os.path.join(icons_dir, 'run.png')
|
|
335
|
+
run_btn.setIcon(QIcon(run_icon_path))
|
|
336
|
+
run_btn.setToolTip("Run")
|
|
337
|
+
run_btn.clicked.connect(lambda: debugger.doRun() if debugger else None) # type: ignore[attr-defined]
|
|
338
|
+
toolbar.addWidget(run_btn)
|
|
339
|
+
|
|
340
|
+
# Step button
|
|
341
|
+
step_btn = QPushButton()
|
|
342
|
+
step_icon_path = os.path.join(icons_dir, 'step.png')
|
|
343
|
+
step_btn.setIcon(QIcon(step_icon_path))
|
|
344
|
+
step_btn.setToolTip("Step")
|
|
345
|
+
step_btn.clicked.connect(lambda: debugger.doStep() if debugger else None) # type: ignore[attr-defined]
|
|
346
|
+
toolbar.addWidget(step_btn)
|
|
347
|
+
|
|
348
|
+
# Stop button
|
|
349
|
+
stop_btn = QPushButton()
|
|
350
|
+
stop_icon_path = os.path.join(icons_dir, 'stop.png')
|
|
351
|
+
stop_btn.setIcon(QIcon(stop_icon_path))
|
|
352
|
+
stop_btn.setToolTip("Stop")
|
|
353
|
+
stop_btn.clicked.connect(lambda: debugger.doStop() if debugger else None) # type: ignore[attr-defined]
|
|
354
|
+
toolbar.addWidget(stop_btn)
|
|
355
|
+
|
|
356
|
+
# Exit button
|
|
357
|
+
exit_btn = QPushButton()
|
|
358
|
+
exit_icon_path = os.path.join(icons_dir, 'exit.png')
|
|
359
|
+
exit_btn.setIcon(QIcon(exit_icon_path))
|
|
360
|
+
exit_btn.setToolTip("Exit")
|
|
361
|
+
exit_btn.clicked.connect(lambda: debugger.doClose() if debugger else None) # type: ignore[attr-defined]
|
|
362
|
+
toolbar.addWidget(exit_btn)
|
|
363
|
+
|
|
364
|
+
column_layout.addWidget(toolbar)
|
|
365
|
+
|
|
366
|
+
# Create a tab widget to hold multiple script panels
|
|
367
|
+
self.tabWidget = QTabWidget()
|
|
368
|
+
self.tabWidget.setTabsClosable(False) # Don't allow closing tabs for now
|
|
369
|
+
|
|
370
|
+
# Ensure tab widget expands
|
|
371
|
+
self.tabWidget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
|
372
|
+
|
|
373
|
+
column_layout.addWidget(self.tabWidget)
|
|
374
|
+
|
|
375
|
+
# Dictionary to map program -> script panel
|
|
376
|
+
self.programPanels = {}
|
|
377
|
+
|
|
378
|
+
#######################################################################
|
|
379
|
+
# Add a new script tab
|
|
380
|
+
def addScriptTab(self, program, filename):
|
|
381
|
+
"""Add a new tab for a script"""
|
|
382
|
+
panel = Debugger.ScriptPanel(self)
|
|
383
|
+
# Extract just the filename from the full path
|
|
384
|
+
tab_label = os.path.basename(filename)
|
|
385
|
+
self.tabWidget.addTab(panel, tab_label)
|
|
386
|
+
self.programPanels[id(program)] = panel
|
|
387
|
+
return panel
|
|
388
|
+
|
|
389
|
+
#######################################################################
|
|
390
|
+
# Get the current active script panel
|
|
391
|
+
def getCurrentPanel(self):
|
|
392
|
+
"""Get the currently active script panel"""
|
|
393
|
+
return self.tabWidget.currentWidget()
|
|
394
|
+
|
|
395
|
+
#######################################################################
|
|
396
|
+
# Get the panel for a specific program
|
|
397
|
+
def getPanelForProgram(self, program):
|
|
398
|
+
"""Get the panel associated with a program"""
|
|
399
|
+
return self.programPanels.get(id(program))
|
|
400
|
+
|
|
401
|
+
#######################################################################
|
|
402
|
+
# Legacy method - add a line to the current panel
|
|
403
|
+
def addLine(self, spec):
|
|
404
|
+
"""Delegate to the current panel's addLine method"""
|
|
405
|
+
panel = self.getCurrentPanel()
|
|
406
|
+
if panel and isinstance(panel, Debugger.ScriptPanel):
|
|
407
|
+
return panel.addLine(spec)
|
|
408
|
+
return None
|
|
409
|
+
|
|
410
|
+
#######################################################################
|
|
411
|
+
# Add stretch to current panel
|
|
412
|
+
def addStretch(self):
|
|
413
|
+
"""Delegate to the current panel's addStretch method"""
|
|
414
|
+
panel = self.getCurrentPanel()
|
|
415
|
+
if panel and isinstance(panel, Debugger.ScriptPanel):
|
|
416
|
+
panel.addStretch()
|
|
417
|
+
|
|
418
|
+
###########################################################################
|
|
419
|
+
# Main debugger class initializer
|
|
420
|
+
def __init__(self, program, width=800, height=600, ratio=0.2):
|
|
421
|
+
super().__init__()
|
|
422
|
+
self.program = program
|
|
423
|
+
self.setWindowTitle("EasyCoder Debugger")
|
|
424
|
+
self.setMinimumSize(width, height)
|
|
425
|
+
# Disable the window close button
|
|
426
|
+
self.setWindowFlags(self.windowFlags() & ~Qt.WindowType.WindowCloseButtonHint)
|
|
427
|
+
self.stopped = True
|
|
428
|
+
self.skip_next_breakpoint = False # Flag to skip breakpoint check on resume
|
|
429
|
+
self.saved_queue = [] # Save queue state when stopped to preserve forked threads
|
|
430
|
+
self._highlighted: set[int] = set()
|
|
431
|
+
|
|
432
|
+
# try to load saved geometry from ~/.ecdebug.conf
|
|
433
|
+
cfg_path = os.path.join(os.path.expanduser("~"), ".ecdebug.conf")
|
|
434
|
+
initial_width = width
|
|
435
|
+
# default console height (pixels) if not stored in cfg
|
|
436
|
+
console_height = 150
|
|
437
|
+
try:
|
|
438
|
+
if os.path.exists(cfg_path):
|
|
439
|
+
with open(cfg_path, "r", encoding="utf-8") as f:
|
|
440
|
+
cfg = json.load(f)
|
|
441
|
+
x = int(cfg.get("x", 0))
|
|
442
|
+
y = int(cfg.get("y", 0))
|
|
443
|
+
w = int(cfg.get("width", width))
|
|
444
|
+
h = int(cfg.get("height", height))
|
|
445
|
+
ratio =float(cfg.get("ratio", ratio))
|
|
446
|
+
# load console height if present
|
|
447
|
+
console_height = int(cfg.get("console_height", console_height))
|
|
448
|
+
# Apply loaded geometry
|
|
449
|
+
self.setGeometry(x, y, w, h)
|
|
450
|
+
initial_width = w
|
|
451
|
+
except Exception:
|
|
452
|
+
# ignore errors and continue with defaults
|
|
453
|
+
initial_width = width
|
|
454
|
+
|
|
455
|
+
# process handle for running scripts
|
|
456
|
+
self._proc = None
|
|
457
|
+
# in-process Program instance and writer
|
|
458
|
+
self._program = None
|
|
459
|
+
self._writer = None
|
|
460
|
+
self._orig_stdout = None
|
|
461
|
+
self._orig_stderr = None
|
|
462
|
+
self._flush_timer = QTimer(self)
|
|
463
|
+
self._flush_timer.setInterval(50)
|
|
464
|
+
self._flush_timer.timeout.connect(self._flush_console_buffer)
|
|
465
|
+
self._flush_timer.stop()
|
|
466
|
+
|
|
467
|
+
# Keep a ratio so proportions are preserved when window is resized
|
|
468
|
+
self.ratio = ratio
|
|
469
|
+
|
|
470
|
+
# Central horizontal splitter (left/right)
|
|
471
|
+
self.hsplitter = QSplitter(Qt.Orientation.Horizontal, self)
|
|
472
|
+
self.hsplitter.setHandleWidth(8)
|
|
473
|
+
self.hsplitter.splitterMoved.connect(self.on_splitter_moved)
|
|
474
|
+
|
|
475
|
+
# Left pane
|
|
476
|
+
left = QFrame()
|
|
477
|
+
left.setFrameShape(QFrame.Shape.StyledPanel)
|
|
478
|
+
left_layout = QVBoxLayout(left)
|
|
479
|
+
left_layout.setContentsMargins(8, 8, 8, 8)
|
|
480
|
+
left_layout.setSpacing(0)
|
|
481
|
+
self.leftColumn = self.MainLeftColumn(self)
|
|
482
|
+
self.leftColumn.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
|
483
|
+
left_layout.addWidget(self.leftColumn, 1)
|
|
484
|
+
|
|
485
|
+
# Right pane
|
|
486
|
+
right = QFrame()
|
|
487
|
+
right.setFrameShape(QFrame.Shape.StyledPanel)
|
|
488
|
+
right_layout = QVBoxLayout(right)
|
|
489
|
+
right_layout.setContentsMargins(8, 8, 8, 8)
|
|
490
|
+
self.rightColumn = self.MainRightColumn(self)
|
|
491
|
+
# Give the rightColumn a stretch factor so its scroll area fills the vertical space
|
|
492
|
+
right_layout.addWidget(self.rightColumn, 1)
|
|
493
|
+
|
|
494
|
+
# Add panes to horizontal splitter
|
|
495
|
+
self.hsplitter.addWidget(left)
|
|
496
|
+
self.hsplitter.addWidget(right)
|
|
497
|
+
|
|
498
|
+
# Initial sizes (proportional) for horizontal splitter
|
|
499
|
+
total = initial_width
|
|
500
|
+
self.hsplitter.setSizes([int(self.ratio * total), int((1 - self.ratio) * total)])
|
|
501
|
+
|
|
502
|
+
# Create a vertical splitter so we can add a resizable console panel at the bottom
|
|
503
|
+
self.vsplitter = QSplitter(Qt.Orientation.Vertical, self)
|
|
504
|
+
self.vsplitter.setHandleWidth(6)
|
|
505
|
+
# top: the existing horizontal splitter
|
|
506
|
+
self.vsplitter.addWidget(self.hsplitter)
|
|
507
|
+
|
|
508
|
+
# bottom: console panel
|
|
509
|
+
console_frame = QFrame()
|
|
510
|
+
console_frame.setFrameShape(QFrame.Shape.StyledPanel)
|
|
511
|
+
console_layout = QVBoxLayout(console_frame)
|
|
512
|
+
console_layout.setContentsMargins(4, 4, 4, 4)
|
|
513
|
+
# simple read-only text console for script output and messages
|
|
514
|
+
from PySide6.QtWidgets import QTextEdit
|
|
515
|
+
self.console = QTextEdit()
|
|
516
|
+
self.console.setReadOnly(True)
|
|
517
|
+
console_layout.addWidget(self.console)
|
|
518
|
+
self.vsplitter.addWidget(console_frame)
|
|
519
|
+
|
|
520
|
+
# Redirect stdout/stderr so all program output is captured in the console
|
|
521
|
+
try:
|
|
522
|
+
self._orig_stdout = sys.stdout
|
|
523
|
+
self._orig_stderr = sys.stderr
|
|
524
|
+
self._writer = self.ConsoleWriter(self)
|
|
525
|
+
sys.stdout = self._writer # type: ignore[assignment]
|
|
526
|
+
sys.stderr = self._writer # type: ignore[assignment]
|
|
527
|
+
except Exception:
|
|
528
|
+
# Best effort; if redirection fails, continue without it
|
|
529
|
+
self._writer = None
|
|
530
|
+
|
|
531
|
+
# Set initial vertical sizes: prefer saved console_height if available
|
|
532
|
+
try:
|
|
533
|
+
total_h = int(h) if 'h' in locals() else max(300, self.height())
|
|
534
|
+
ch = max(50, min(total_h - 50, console_height))
|
|
535
|
+
self.vsplitter.setSizes([int(total_h - ch), int(ch)])
|
|
536
|
+
except Exception:
|
|
537
|
+
pass
|
|
538
|
+
|
|
539
|
+
# Use the vertical splitter as the central widget
|
|
540
|
+
self.setCentralWidget(self.vsplitter)
|
|
541
|
+
self.parse(program.script.lines, program, program.scriptName)
|
|
542
|
+
self.show()
|
|
543
|
+
|
|
544
|
+
def _flush_console_buffer(self):
|
|
545
|
+
try:
|
|
546
|
+
writer = self._writer
|
|
547
|
+
if not writer:
|
|
548
|
+
return
|
|
549
|
+
if getattr(writer, '_buf', None):
|
|
550
|
+
text = ''.join(writer._buf)
|
|
551
|
+
writer._buf.clear()
|
|
552
|
+
# Append to the console and scroll to bottom
|
|
553
|
+
self.console.moveCursor(QTextCursor.MoveOperation.End)
|
|
554
|
+
self.console.insertPlainText(text)
|
|
555
|
+
self.console.moveCursor(QTextCursor.MoveOperation.End)
|
|
556
|
+
except Exception:
|
|
557
|
+
pass
|
|
558
|
+
|
|
559
|
+
def on_splitter_moved(self, pos, index):
|
|
560
|
+
# Update stored ratio when user drags the splitter
|
|
561
|
+
left_width = self.hsplitter.widget(0).width()
|
|
562
|
+
total = max(1, sum(w.width() for w in (self.hsplitter.widget(0), self.hsplitter.widget(1))))
|
|
563
|
+
self.ratio = left_width / total
|
|
564
|
+
|
|
565
|
+
def resizeEvent(self, event):
|
|
566
|
+
# Preserve the proportional widths when the window is resized
|
|
567
|
+
total_width = max(1, self.width())
|
|
568
|
+
left_w = max(0, int(self.ratio * total_width))
|
|
569
|
+
right_w = max(0, total_width - left_w)
|
|
570
|
+
self.hsplitter.setSizes([left_w, right_w])
|
|
571
|
+
super().resizeEvent(event)
|
|
572
|
+
|
|
573
|
+
###########################################################################
|
|
574
|
+
# Parse a script into the right-hand column
|
|
575
|
+
def parse(self, script, program=None, filename=None):
|
|
576
|
+
"""Parse a script and add it as a tab
|
|
577
|
+
|
|
578
|
+
Args:
|
|
579
|
+
script: List of script lines
|
|
580
|
+
program: The Program instance this script belongs to
|
|
581
|
+
filename: The filename to use as the tab label
|
|
582
|
+
"""
|
|
583
|
+
self.scriptLines = []
|
|
584
|
+
|
|
585
|
+
# Get or create the panel for this program
|
|
586
|
+
panel = None
|
|
587
|
+
if program:
|
|
588
|
+
panel = self.rightColumn.getPanelForProgram(program)
|
|
589
|
+
if not panel:
|
|
590
|
+
# Create a new tab for this program
|
|
591
|
+
if not filename:
|
|
592
|
+
filename = getattr(program, 'path', 'Untitled')
|
|
593
|
+
panel = self.rightColumn.addScriptTab(program, filename)
|
|
594
|
+
|
|
595
|
+
# If no program specified or panel creation failed, use current panel
|
|
596
|
+
if not panel:
|
|
597
|
+
panel = self.rightColumn.getCurrentPanel()
|
|
598
|
+
if not panel:
|
|
599
|
+
# Create a default tab
|
|
600
|
+
panel = self.rightColumn.addScriptTab(None, filename or 'Untitled')
|
|
601
|
+
|
|
602
|
+
# Clear existing lines from the panel
|
|
603
|
+
if panel and isinstance(panel, Debugger.ScriptPanel):
|
|
604
|
+
layout = panel.inner_layout
|
|
605
|
+
while layout.count():
|
|
606
|
+
item = layout.takeAt(0)
|
|
607
|
+
widget = item.widget()
|
|
608
|
+
if widget:
|
|
609
|
+
widget.deleteLater()
|
|
610
|
+
|
|
611
|
+
# Parse and add new lines
|
|
612
|
+
lino = 0
|
|
613
|
+
for line in script:
|
|
614
|
+
orig_line = line
|
|
615
|
+
if len(line) > 0:
|
|
616
|
+
line = line.replace("\t", " ")
|
|
617
|
+
color_line = self.coloriseLine(line, lino)
|
|
618
|
+
else:
|
|
619
|
+
# still need to call coloriseLine to keep token list in sync
|
|
620
|
+
color_line = self.coloriseLine(line, lino)
|
|
621
|
+
lineSpec = Object()
|
|
622
|
+
lineSpec.lino = lino
|
|
623
|
+
lineSpec.line = color_line
|
|
624
|
+
lineSpec.orig_line = orig_line
|
|
625
|
+
lineSpec.bp = False
|
|
626
|
+
lineSpec.onClick = self.onClickLino
|
|
627
|
+
lino += 1
|
|
628
|
+
self.scriptLines.append(lineSpec)
|
|
629
|
+
lineSpec.panel = self.rightColumn.addLine(lineSpec)
|
|
630
|
+
self.rightColumn.addStretch()
|
|
631
|
+
|
|
632
|
+
###########################################################################
|
|
633
|
+
# Colorise a line of script for HTML display
|
|
634
|
+
def coloriseLine(self, line, lino=None):
|
|
635
|
+
output = ''
|
|
636
|
+
|
|
637
|
+
# Preserve leading spaces (render as except the first)
|
|
638
|
+
if len(line) > 0 and line[0] == ' ':
|
|
639
|
+
output += '<span>'
|
|
640
|
+
n = 0
|
|
641
|
+
while n < len(line) and line[n] == ' ': n += 1
|
|
642
|
+
output += ' ' * (n - 1)
|
|
643
|
+
output += '</span>'
|
|
644
|
+
|
|
645
|
+
# Find the first unquoted ! (not inside backticks)
|
|
646
|
+
comment_start = None
|
|
647
|
+
in_backtick = False
|
|
648
|
+
for idx, c in enumerate(line):
|
|
649
|
+
if c == '`':
|
|
650
|
+
in_backtick = not in_backtick
|
|
651
|
+
elif c == '!' and not in_backtick:
|
|
652
|
+
comment_start = idx
|
|
653
|
+
break
|
|
654
|
+
|
|
655
|
+
if comment_start is not None:
|
|
656
|
+
code_part = line[:comment_start]
|
|
657
|
+
comment_part = line[comment_start:]
|
|
658
|
+
else:
|
|
659
|
+
code_part = line
|
|
660
|
+
comment_part = None
|
|
661
|
+
|
|
662
|
+
# Tokenize code_part as before (respecting backticks)
|
|
663
|
+
tokens = []
|
|
664
|
+
i = 0
|
|
665
|
+
L = len(code_part)
|
|
666
|
+
while i < L:
|
|
667
|
+
if code_part[i].isspace():
|
|
668
|
+
i += 1
|
|
669
|
+
continue
|
|
670
|
+
if code_part[i] == '`':
|
|
671
|
+
j = code_part.find('`', i + 1)
|
|
672
|
+
if j == -1:
|
|
673
|
+
tokens.append(code_part[i:])
|
|
674
|
+
break
|
|
675
|
+
else:
|
|
676
|
+
tokens.append(code_part[i:j+1])
|
|
677
|
+
i = j + 1
|
|
678
|
+
else:
|
|
679
|
+
j = i
|
|
680
|
+
while j < L and not code_part[j].isspace():
|
|
681
|
+
j += 1
|
|
682
|
+
tokens.append(code_part[i:j])
|
|
683
|
+
i = j
|
|
684
|
+
|
|
685
|
+
# Colour code tokens and generate a list of elements
|
|
686
|
+
for token in tokens:
|
|
687
|
+
if token == '':
|
|
688
|
+
continue
|
|
689
|
+
elif token[0].isupper():
|
|
690
|
+
esc = html.escape(token)
|
|
691
|
+
element = f' <span style="color: purple; font-weight: bold;">{esc}</span>'
|
|
692
|
+
elif token[0].isdigit():
|
|
693
|
+
esc = html.escape(token)
|
|
694
|
+
element = f' <span style="color: green;">{esc}</span>'
|
|
695
|
+
elif token[0] == '`':
|
|
696
|
+
esc = html.escape(token)
|
|
697
|
+
element = f' <span style="color: peru;">{esc}</span>'
|
|
698
|
+
else:
|
|
699
|
+
esc = html.escape(token)
|
|
700
|
+
element = f' <span>{esc}</span>'
|
|
701
|
+
output += element
|
|
702
|
+
# Colour comment if present
|
|
703
|
+
if comment_part is not None:
|
|
704
|
+
esc = html.escape(comment_part)
|
|
705
|
+
output += f'<span style="color: green;"> {esc}</span>'
|
|
706
|
+
|
|
707
|
+
return output
|
|
708
|
+
|
|
709
|
+
###########################################################################
|
|
710
|
+
# Enable breakpoints to be set on any line that is classed as a command
|
|
711
|
+
def enableBreakpoints(self):
|
|
712
|
+
"""
|
|
713
|
+
Examine each line and set is_command flag based on compiled code.
|
|
714
|
+
A line is a command if:
|
|
715
|
+
- It's not empty or a comment
|
|
716
|
+
- It's not a label (single word ending with colon)
|
|
717
|
+
- It corresponds to a command in program.code with type != 'symbol'
|
|
718
|
+
"""
|
|
719
|
+
# First, mark all lines as non-commands by default
|
|
720
|
+
for lineSpec in self.scriptLines:
|
|
721
|
+
lineSpec.is_command = False
|
|
722
|
+
|
|
723
|
+
# Now iterate through compiled commands and mark those that are executable
|
|
724
|
+
for command in self.program.code:
|
|
725
|
+
if 'lino' not in command:
|
|
726
|
+
continue
|
|
727
|
+
|
|
728
|
+
lino = command['lino']
|
|
729
|
+
if lino < 0 or lino >= len(self.scriptLines):
|
|
730
|
+
continue
|
|
731
|
+
|
|
732
|
+
# Check if this is a symbol declaration (variable/constant definition)
|
|
733
|
+
if command.get('type') == 'symbol':
|
|
734
|
+
continue
|
|
735
|
+
|
|
736
|
+
# Check if this is a structural keyword that shouldn't have breakpoints
|
|
737
|
+
if command.get('keyword') in ['begin', 'end']:
|
|
738
|
+
continue
|
|
739
|
+
|
|
740
|
+
# This is an executable command
|
|
741
|
+
self.scriptLines[lino].is_command = True
|
|
742
|
+
|
|
743
|
+
###########################################################################
|
|
744
|
+
# Here when the user clicks a line number
|
|
745
|
+
def onClickLino(self, lino):
|
|
746
|
+
# Check if this line is a command - if not, take no action
|
|
747
|
+
lineSpec = self.scriptLines[lino]
|
|
748
|
+
if not getattr(lineSpec, 'is_command', True):
|
|
749
|
+
return
|
|
750
|
+
|
|
751
|
+
# Show or hide the red blob next to this line
|
|
752
|
+
lineSpec.bp = not lineSpec.bp
|
|
753
|
+
if lineSpec.bp: lineSpec.label.showBlob()
|
|
754
|
+
else: lineSpec.label.hideBlob()
|
|
755
|
+
# Set or clear a breakpoint on this command
|
|
756
|
+
for command in self.program.code:
|
|
757
|
+
if 'lino' in command and command['lino'] == lino:
|
|
758
|
+
command['bp'] = lineSpec.bp
|
|
759
|
+
break
|
|
760
|
+
|
|
761
|
+
###########################################################################
|
|
762
|
+
# Scroll to a given line number
|
|
763
|
+
def scrollTo(self, lino):
|
|
764
|
+
# Ensure the line number is valid
|
|
765
|
+
if lino < 0 or lino >= len(self.scriptLines):
|
|
766
|
+
return
|
|
767
|
+
|
|
768
|
+
# Get the panel widget for this line
|
|
769
|
+
lineSpec = self.scriptLines[lino]
|
|
770
|
+
panel = lineSpec.panel
|
|
771
|
+
|
|
772
|
+
if not panel:
|
|
773
|
+
return
|
|
774
|
+
|
|
775
|
+
# Get the current script panel
|
|
776
|
+
script_panel = self.rightColumn.getCurrentPanel()
|
|
777
|
+
if not script_panel or not isinstance(script_panel, Debugger.ScriptPanel):
|
|
778
|
+
return
|
|
779
|
+
|
|
780
|
+
# Get the scroll area from the script panel
|
|
781
|
+
scroll_area = script_panel.scroll
|
|
782
|
+
|
|
783
|
+
# Get the vertical position of the panel relative to the content widget
|
|
784
|
+
panel_y = panel.y()
|
|
785
|
+
panel_height = panel.height()
|
|
786
|
+
|
|
787
|
+
# Get the viewport height (visible area)
|
|
788
|
+
viewport_height = scroll_area.viewport().height()
|
|
789
|
+
|
|
790
|
+
# Calculate the target scroll position to center the panel
|
|
791
|
+
# We want the panel's center to align with the viewport's center
|
|
792
|
+
target_scroll = panel_y + (panel_height // 2) - (viewport_height // 2)
|
|
793
|
+
|
|
794
|
+
# Clamp to valid scroll range
|
|
795
|
+
scrollbar = scroll_area.verticalScrollBar()
|
|
796
|
+
target_scroll = max(scrollbar.minimum(), min(target_scroll, scrollbar.maximum()))
|
|
797
|
+
|
|
798
|
+
# Smoothly scroll to the target position
|
|
799
|
+
scrollbar.setValue(target_scroll)
|
|
800
|
+
|
|
801
|
+
# Bring the window to the front
|
|
802
|
+
self.raise_()
|
|
803
|
+
self.activateWindow()
|
|
804
|
+
|
|
805
|
+
###########################################################################
|
|
806
|
+
# Set the background color of one line of the script
|
|
807
|
+
def setBackground(self, lino, color):
|
|
808
|
+
# Set the background color of the given line and track highlighted lines
|
|
809
|
+
if lino < 0 or lino >= len(self.scriptLines):
|
|
810
|
+
return
|
|
811
|
+
lineSpec = self.scriptLines[lino]
|
|
812
|
+
panel = lineSpec.panel
|
|
813
|
+
if not panel:
|
|
814
|
+
return
|
|
815
|
+
if color == 'none':
|
|
816
|
+
panel.setStyleSheet("")
|
|
817
|
+
self._highlighted.discard(lino)
|
|
818
|
+
else:
|
|
819
|
+
panel.setStyleSheet(f"background-color: {color};")
|
|
820
|
+
self._highlighted.add(lino)
|
|
821
|
+
|
|
822
|
+
def _clearHighlights(self):
|
|
823
|
+
# Remove highlighting from all previously highlighted lines
|
|
824
|
+
for lino in list(self._highlighted):
|
|
825
|
+
self.setBackground(lino, 'none')
|
|
826
|
+
self._highlighted.clear()
|
|
827
|
+
|
|
828
|
+
###########################################################################
|
|
829
|
+
# Here before each instruction is run
|
|
830
|
+
# Returns True if the program should halt and wait for user interaction
|
|
831
|
+
def checkIfHalt(self, is_first_command=False):
|
|
832
|
+
self.pc = self.program.pc
|
|
833
|
+
command = self.program.code[self.pc]
|
|
834
|
+
lino = command['lino'] if 'lino' in command else 0
|
|
835
|
+
bp = command.get('bp', False)
|
|
836
|
+
|
|
837
|
+
# Check if we should skip this breakpoint check (resuming from same location)
|
|
838
|
+
if self.skip_next_breakpoint:
|
|
839
|
+
self.skip_next_breakpoint = False
|
|
840
|
+
return False
|
|
841
|
+
|
|
842
|
+
# Labels should never halt execution - they're just markers
|
|
843
|
+
# A label is a symbol whose name ends with ':'
|
|
844
|
+
if command.get('type') == 'symbol' and command.get('name', '').endswith(':'):
|
|
845
|
+
return False
|
|
846
|
+
|
|
847
|
+
# Determine if we should halt
|
|
848
|
+
should_halt = False
|
|
849
|
+
|
|
850
|
+
# If this is the first real command (pc==1), always halt to initialize display
|
|
851
|
+
if is_first_command:
|
|
852
|
+
should_halt = True
|
|
853
|
+
self.stopped = True
|
|
854
|
+
print(f"Program ready at line {lino + 1}")
|
|
855
|
+
# If we're in stopped (step) mode, halt after each command
|
|
856
|
+
elif self.stopped:
|
|
857
|
+
should_halt = True
|
|
858
|
+
# If there's a breakpoint on this line, halt
|
|
859
|
+
elif bp:
|
|
860
|
+
print(f"Hit breakpoint at line {lino + 1}")
|
|
861
|
+
self.stopped = True
|
|
862
|
+
should_halt = True
|
|
863
|
+
|
|
864
|
+
# If halting, update the UI and save queue state
|
|
865
|
+
if should_halt:
|
|
866
|
+
self.scrollTo(lino)
|
|
867
|
+
self._clearHighlights()
|
|
868
|
+
self.setBackground(lino, 'Yellow')
|
|
869
|
+
# Refresh variable values when halted
|
|
870
|
+
self.refreshVariables()
|
|
871
|
+
# Save the current queue state to preserve forked threads
|
|
872
|
+
self._saveQueueState()
|
|
873
|
+
|
|
874
|
+
return should_halt
|
|
875
|
+
|
|
876
|
+
def refreshVariables(self):
|
|
877
|
+
"""Update all watched variable values"""
|
|
878
|
+
try:
|
|
879
|
+
if hasattr(self, 'leftColumn') and hasattr(self.leftColumn, 'watch_list'):
|
|
880
|
+
self.leftColumn.watch_list.refreshVariables(self.program)
|
|
881
|
+
except Exception as ex:
|
|
882
|
+
print(f"Error refreshing variables: {ex}")
|
|
883
|
+
|
|
884
|
+
def _saveQueueState(self):
|
|
885
|
+
"""Save the current global queue state (preserves forked threads)"""
|
|
886
|
+
try:
|
|
887
|
+
# Import the module to access the global queue
|
|
888
|
+
from easycoder import ec_program
|
|
889
|
+
# Save a copy of the queue
|
|
890
|
+
self.saved_queue = list(ec_program.queue)
|
|
891
|
+
except Exception as ex:
|
|
892
|
+
print(f"Error saving queue state: {ex}")
|
|
893
|
+
|
|
894
|
+
def _restoreQueueState(self):
|
|
895
|
+
"""Restore the saved queue state (resume all forked threads)"""
|
|
896
|
+
try:
|
|
897
|
+
# Import here to avoid circular dependency
|
|
898
|
+
from easycoder import ec_program
|
|
899
|
+
# Restore the queue from saved state
|
|
900
|
+
if self.saved_queue:
|
|
901
|
+
ec_program.queue.clear()
|
|
902
|
+
ec_program.queue.extend(self.saved_queue)
|
|
903
|
+
except Exception as ex:
|
|
904
|
+
print(f"Error restoring queue state: {ex}")
|
|
905
|
+
|
|
906
|
+
def doRun(self):
|
|
907
|
+
"""Resume free-running execution from current PC"""
|
|
908
|
+
command = self.program.code[self.pc]
|
|
909
|
+
lino = command.get('lino', 0)
|
|
910
|
+
print(f"Continuing execution at line {lino + 1}")
|
|
911
|
+
|
|
912
|
+
# Clear the highlight on the current line
|
|
913
|
+
self.setBackground(lino, 'none')
|
|
914
|
+
|
|
915
|
+
# Switch to free-running mode
|
|
916
|
+
self.stopped = False
|
|
917
|
+
|
|
918
|
+
# Skip the breakpoint check for the current instruction (the one we're resuming from)
|
|
919
|
+
self.skip_next_breakpoint = True
|
|
920
|
+
|
|
921
|
+
# Restore the saved queue state to resume all forked threads
|
|
922
|
+
self._restoreQueueState()
|
|
923
|
+
|
|
924
|
+
# Enqueue the current thread, then flush immediately
|
|
925
|
+
self.program.run(self.pc)
|
|
926
|
+
from easycoder.ec_program import flush
|
|
927
|
+
flush()
|
|
928
|
+
|
|
929
|
+
def doStep(self):
|
|
930
|
+
"""Execute one instruction and halt again"""
|
|
931
|
+
command = self.program.code[self.pc]
|
|
932
|
+
lino = command.get('lino', 0)
|
|
933
|
+
|
|
934
|
+
# Clear the highlight on the current line
|
|
935
|
+
self.setBackground(lino, 'none')
|
|
936
|
+
|
|
937
|
+
# Stay in stopped mode (will halt after next instruction)
|
|
938
|
+
self.stopped = True
|
|
939
|
+
|
|
940
|
+
# Skip the breakpoint check for the current instruction (the one we're stepping from)
|
|
941
|
+
self.skip_next_breakpoint = True
|
|
942
|
+
|
|
943
|
+
# Restore the saved queue state to resume all forked threads
|
|
944
|
+
self._restoreQueueState()
|
|
945
|
+
|
|
946
|
+
# Enqueue the current thread, then flush a single cycle
|
|
947
|
+
self.program.run(self.pc)
|
|
948
|
+
from easycoder.ec_program import flush
|
|
949
|
+
flush()
|
|
950
|
+
|
|
951
|
+
def doStop(self):
|
|
952
|
+
try:
|
|
953
|
+
lino = self.program.code[self.pc].get('lino', 0) + 1
|
|
954
|
+
print(f"Stopped by user at line {lino}")
|
|
955
|
+
except Exception:
|
|
956
|
+
print("Stopped by user")
|
|
957
|
+
# Clear all previous highlights and mark the current line
|
|
958
|
+
try:
|
|
959
|
+
self._clearHighlights()
|
|
960
|
+
current_lino = self.program.code[self.pc].get('lino', 0)
|
|
961
|
+
self.setBackground(current_lino, 'LightYellow')
|
|
962
|
+
except Exception:
|
|
963
|
+
pass
|
|
964
|
+
self.stopped = True
|
|
965
|
+
|
|
966
|
+
def doClose(self):
|
|
967
|
+
self.closeEvent(None)
|
|
968
|
+
|
|
969
|
+
###########################################################################
|
|
970
|
+
# Override closeEvent to save window geometry
|
|
971
|
+
def closeEvent(self, event):
|
|
972
|
+
"""Save window position and size to ~/.ecdebug.conf as JSON on exit."""
|
|
973
|
+
cfg = {
|
|
974
|
+
"x": self.x(),
|
|
975
|
+
"y": self.y(),
|
|
976
|
+
"width": self.width(),
|
|
977
|
+
"height": self.height(),
|
|
978
|
+
"ratio": self.ratio
|
|
979
|
+
}
|
|
980
|
+
# try to persist console height (bottom pane) if present
|
|
981
|
+
try:
|
|
982
|
+
ch = None
|
|
983
|
+
if hasattr(self, 'vsplitter'):
|
|
984
|
+
sizes = self.vsplitter.sizes()
|
|
985
|
+
if len(sizes) >= 2:
|
|
986
|
+
ch = int(sizes[1])
|
|
987
|
+
if ch is not None:
|
|
988
|
+
cfg['console_height'] = ch
|
|
989
|
+
except Exception:
|
|
990
|
+
pass
|
|
991
|
+
try:
|
|
992
|
+
cfg_path = os.path.join(os.path.expanduser("~"), ".ecdebug.conf")
|
|
993
|
+
with open(cfg_path, "w", encoding="utf-8") as f:
|
|
994
|
+
json.dump(cfg, f, indent=2)
|
|
995
|
+
except Exception as exc:
|
|
996
|
+
# best-effort only; avoid blocking shutdown
|
|
997
|
+
try:
|
|
998
|
+
self.statusBar().showMessage(f"Could not save config: {exc}", 3000)
|
|
999
|
+
except Exception:
|
|
1000
|
+
pass
|
|
1001
|
+
# Restore stdout/stderr and stop timers
|
|
1002
|
+
try:
|
|
1003
|
+
if self._orig_stdout is not None:
|
|
1004
|
+
sys.stdout = self._orig_stdout
|
|
1005
|
+
if self._orig_stderr is not None:
|
|
1006
|
+
sys.stderr = self._orig_stderr
|
|
1007
|
+
if self._flush_timer is not None:
|
|
1008
|
+
try:
|
|
1009
|
+
self._flush_timer.stop()
|
|
1010
|
+
except Exception:
|
|
1011
|
+
pass
|
|
1012
|
+
except Exception:
|
|
1013
|
+
pass
|
|
1014
|
+
super().close()
|