web-a2e 1.0.0

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.
Files changed (295) hide show
  1. package/.clangd +5 -0
  2. package/.mcp.json +12 -0
  3. package/CLAUDE.md +362 -0
  4. package/CMakeLists.txt +774 -0
  5. package/LICENSE +21 -0
  6. package/README.md +392 -0
  7. package/build-wasm/generated/roms.cpp +2447 -0
  8. package/docker-compose.staging.yml +9 -0
  9. package/docs/basic-rom-disassembly.md +6663 -0
  10. package/docs/softswitch-comparison.md +273 -0
  11. package/docs/thunderclock-debug.md +89 -0
  12. package/examples/cube.bas +72 -0
  13. package/examples/hello.s +55 -0
  14. package/examples/scroll.s +140 -0
  15. package/package.json +18 -0
  16. package/public/assets/apple-logo-old.png +0 -0
  17. package/public/assets/apple-logo.png +0 -0
  18. package/public/assets/drive-closed-light-on.png +0 -0
  19. package/public/assets/drive-closed.png +0 -0
  20. package/public/assets/drive-open-light-on.png +0 -0
  21. package/public/assets/drive-open.png +0 -0
  22. package/public/audio-worklet.js +82 -0
  23. package/public/disks/Apple DOS 3.3 January 1983.dsk +0 -0
  24. package/public/disks/ProDOS 2.4.3.po +0 -0
  25. package/public/disks/h32mb.2mg +0 -0
  26. package/public/disks/library.json +26 -0
  27. package/public/docs/llms/llm-assembler.txt +90 -0
  28. package/public/docs/llms/llm-basic-program.txt +256 -0
  29. package/public/docs/llms/llm-disk-drives.txt +72 -0
  30. package/public/docs/llms/llm-file-explorer.txt +50 -0
  31. package/public/docs/llms/llm-hard-drives.txt +80 -0
  32. package/public/docs/llms/llm-main.txt +51 -0
  33. package/public/docs/llms/llm-slot-configuration.txt +66 -0
  34. package/public/icons/icon-192.svg +4 -0
  35. package/public/icons/icon-512.svg +4 -0
  36. package/public/index.html +661 -0
  37. package/public/llms.txt +49 -0
  38. package/public/manifest.json +29 -0
  39. package/public/shaders/burnin.glsl +22 -0
  40. package/public/shaders/crt.glsl +706 -0
  41. package/public/shaders/edge.glsl +109 -0
  42. package/public/shaders/vertex.glsl +8 -0
  43. package/public/sw.js +186 -0
  44. package/roms/341-0027.bin +0 -0
  45. package/roms/341-0160-A-US-UK.bin +0 -0
  46. package/roms/341-0160-A.bin +0 -0
  47. package/roms/342-0273-A-US-UK.bin +0 -0
  48. package/roms/342-0349-B-C0-FF.bin +0 -0
  49. package/roms/Apple Mouse Interface Card ROM - 342-0270-C.bin +0 -0
  50. package/roms/Thunderclock Plus ROM.bin +0 -0
  51. package/scripts/generate_roms.sh +69 -0
  52. package/src/bindings/wasm_interface.cpp +1940 -0
  53. package/src/core/assembler/assembler.cpp +1239 -0
  54. package/src/core/assembler/assembler.hpp +115 -0
  55. package/src/core/audio/audio.cpp +160 -0
  56. package/src/core/audio/audio.hpp +81 -0
  57. package/src/core/basic/basic_detokenizer.cpp +436 -0
  58. package/src/core/basic/basic_detokenizer.hpp +41 -0
  59. package/src/core/basic/basic_tokenizer.cpp +286 -0
  60. package/src/core/basic/basic_tokenizer.hpp +26 -0
  61. package/src/core/basic/basic_tokens.hpp +295 -0
  62. package/src/core/cards/disk2_card.cpp +568 -0
  63. package/src/core/cards/disk2_card.hpp +316 -0
  64. package/src/core/cards/expansion_card.hpp +185 -0
  65. package/src/core/cards/mockingboard/ay8910.cpp +616 -0
  66. package/src/core/cards/mockingboard/ay8910.hpp +159 -0
  67. package/src/core/cards/mockingboard/via6522.cpp +530 -0
  68. package/src/core/cards/mockingboard/via6522.hpp +163 -0
  69. package/src/core/cards/mockingboard_card.cpp +312 -0
  70. package/src/core/cards/mockingboard_card.hpp +159 -0
  71. package/src/core/cards/mouse_card.cpp +654 -0
  72. package/src/core/cards/mouse_card.hpp +190 -0
  73. package/src/core/cards/smartport/block_device.cpp +202 -0
  74. package/src/core/cards/smartport/block_device.hpp +60 -0
  75. package/src/core/cards/smartport/smartport_card.cpp +603 -0
  76. package/src/core/cards/smartport/smartport_card.hpp +120 -0
  77. package/src/core/cards/thunderclock_card.cpp +237 -0
  78. package/src/core/cards/thunderclock_card.hpp +122 -0
  79. package/src/core/cpu/cpu6502.cpp +1609 -0
  80. package/src/core/cpu/cpu6502.hpp +203 -0
  81. package/src/core/debug/condition_evaluator.cpp +470 -0
  82. package/src/core/debug/condition_evaluator.hpp +87 -0
  83. package/src/core/disassembler/disassembler.cpp +552 -0
  84. package/src/core/disassembler/disassembler.hpp +171 -0
  85. package/src/core/disk-image/disk_image.hpp +267 -0
  86. package/src/core/disk-image/dsk_disk_image.cpp +827 -0
  87. package/src/core/disk-image/dsk_disk_image.hpp +204 -0
  88. package/src/core/disk-image/gcr_encoding.cpp +147 -0
  89. package/src/core/disk-image/gcr_encoding.hpp +78 -0
  90. package/src/core/disk-image/woz_disk_image.cpp +1049 -0
  91. package/src/core/disk-image/woz_disk_image.hpp +343 -0
  92. package/src/core/emulator.cpp +2126 -0
  93. package/src/core/emulator.hpp +434 -0
  94. package/src/core/filesystem/dos33.cpp +178 -0
  95. package/src/core/filesystem/dos33.hpp +66 -0
  96. package/src/core/filesystem/pascal.cpp +262 -0
  97. package/src/core/filesystem/pascal.hpp +87 -0
  98. package/src/core/filesystem/prodos.cpp +369 -0
  99. package/src/core/filesystem/prodos.hpp +119 -0
  100. package/src/core/input/keyboard.cpp +227 -0
  101. package/src/core/input/keyboard.hpp +111 -0
  102. package/src/core/mmu/mmu.cpp +1387 -0
  103. package/src/core/mmu/mmu.hpp +236 -0
  104. package/src/core/types.hpp +196 -0
  105. package/src/core/video/video.cpp +680 -0
  106. package/src/core/video/video.hpp +156 -0
  107. package/src/css/assembler-editor.css +1617 -0
  108. package/src/css/base.css +470 -0
  109. package/src/css/basic-debugger.css +791 -0
  110. package/src/css/basic-editor.css +792 -0
  111. package/src/css/controls.css +783 -0
  112. package/src/css/cpu-debugger.css +1413 -0
  113. package/src/css/debug-base.css +160 -0
  114. package/src/css/debug-windows.css +6455 -0
  115. package/src/css/disk-drives.css +406 -0
  116. package/src/css/documentation.css +392 -0
  117. package/src/css/file-explorer.css +867 -0
  118. package/src/css/hard-drive.css +180 -0
  119. package/src/css/layout.css +217 -0
  120. package/src/css/memory-windows.css +798 -0
  121. package/src/css/modals.css +510 -0
  122. package/src/css/monitor.css +425 -0
  123. package/src/css/release-notes.css +101 -0
  124. package/src/css/responsive.css +400 -0
  125. package/src/css/rule-builder.css +340 -0
  126. package/src/css/save-states.css +201 -0
  127. package/src/css/settings-windows.css +1231 -0
  128. package/src/css/window-switcher.css +150 -0
  129. package/src/js/agent/agent-manager.js +643 -0
  130. package/src/js/agent/agent-tools.js +293 -0
  131. package/src/js/agent/agent-version-tools.js +131 -0
  132. package/src/js/agent/assembler-tools.js +357 -0
  133. package/src/js/agent/basic-program-tools.js +894 -0
  134. package/src/js/agent/disk-tools.js +417 -0
  135. package/src/js/agent/file-explorer-tools.js +269 -0
  136. package/src/js/agent/index.js +13 -0
  137. package/src/js/agent/main-tools.js +222 -0
  138. package/src/js/agent/slot-tools.js +303 -0
  139. package/src/js/agent/smartport-tools.js +257 -0
  140. package/src/js/agent/window-tools.js +80 -0
  141. package/src/js/audio/audio-driver.js +417 -0
  142. package/src/js/audio/audio-worklet.js +85 -0
  143. package/src/js/audio/index.js +8 -0
  144. package/src/js/config/default-layout.js +34 -0
  145. package/src/js/config/version.js +8 -0
  146. package/src/js/data/apple2-rom-routines.js +577 -0
  147. package/src/js/debug/assembler-editor-window.js +2993 -0
  148. package/src/js/debug/basic-breakpoint-manager.js +529 -0
  149. package/src/js/debug/basic-program-parser.js +436 -0
  150. package/src/js/debug/basic-program-window.js +2594 -0
  151. package/src/js/debug/basic-variable-inspector.js +447 -0
  152. package/src/js/debug/breakpoint-manager.js +472 -0
  153. package/src/js/debug/cpu-debugger-window.js +2396 -0
  154. package/src/js/debug/index.js +22 -0
  155. package/src/js/debug/label-manager.js +238 -0
  156. package/src/js/debug/memory-browser-window.js +416 -0
  157. package/src/js/debug/memory-heat-map-window.js +481 -0
  158. package/src/js/debug/memory-map-window.js +206 -0
  159. package/src/js/debug/mockingboard-window.js +882 -0
  160. package/src/js/debug/mouse-card-window.js +355 -0
  161. package/src/js/debug/rule-builder-window.js +648 -0
  162. package/src/js/debug/soft-switch-window.js +458 -0
  163. package/src/js/debug/stack-viewer-window.js +221 -0
  164. package/src/js/debug/symbols.js +416 -0
  165. package/src/js/debug/trace-panel.js +291 -0
  166. package/src/js/debug/zero-page-watch-window.js +297 -0
  167. package/src/js/disk-manager/disk-drives-window.js +212 -0
  168. package/src/js/disk-manager/disk-operations.js +284 -0
  169. package/src/js/disk-manager/disk-persistence.js +301 -0
  170. package/src/js/disk-manager/disk-surface-renderer.js +388 -0
  171. package/src/js/disk-manager/drive-sounds.js +139 -0
  172. package/src/js/disk-manager/hard-drive-manager.js +481 -0
  173. package/src/js/disk-manager/hard-drive-persistence.js +187 -0
  174. package/src/js/disk-manager/hard-drive-window.js +57 -0
  175. package/src/js/disk-manager/index.js +890 -0
  176. package/src/js/display/display-settings-window.js +383 -0
  177. package/src/js/display/index.js +10 -0
  178. package/src/js/display/screen-window.js +342 -0
  179. package/src/js/display/webgl-renderer.js +705 -0
  180. package/src/js/file-explorer/disassembler.js +574 -0
  181. package/src/js/file-explorer/dos33.js +266 -0
  182. package/src/js/file-explorer/file-viewer.js +359 -0
  183. package/src/js/file-explorer/index.js +1261 -0
  184. package/src/js/file-explorer/prodos.js +549 -0
  185. package/src/js/file-explorer/utils.js +67 -0
  186. package/src/js/help/documentation-window.js +1096 -0
  187. package/src/js/help/index.js +10 -0
  188. package/src/js/help/release-notes-window.js +85 -0
  189. package/src/js/help/release-notes.js +612 -0
  190. package/src/js/input/gamepad-handler.js +176 -0
  191. package/src/js/input/index.js +12 -0
  192. package/src/js/input/input-handler.js +396 -0
  193. package/src/js/input/joystick-window.js +404 -0
  194. package/src/js/input/mouse-handler.js +99 -0
  195. package/src/js/input/text-selection.js +462 -0
  196. package/src/js/main.js +653 -0
  197. package/src/js/state/index.js +15 -0
  198. package/src/js/state/save-states-window.js +393 -0
  199. package/src/js/state/state-manager.js +409 -0
  200. package/src/js/state/state-persistence.js +218 -0
  201. package/src/js/ui/confirm.js +43 -0
  202. package/src/js/ui/disk-drive-positioner.js +347 -0
  203. package/src/js/ui/reminder-controller.js +129 -0
  204. package/src/js/ui/slot-configuration-window.js +560 -0
  205. package/src/js/ui/theme-manager.js +61 -0
  206. package/src/js/ui/toast.js +44 -0
  207. package/src/js/ui/ui-controller.js +897 -0
  208. package/src/js/ui/window-switcher.js +275 -0
  209. package/src/js/utils/basic-autocomplete.js +832 -0
  210. package/src/js/utils/basic-highlighting.js +473 -0
  211. package/src/js/utils/basic-tokenizer.js +153 -0
  212. package/src/js/utils/basic-tokens.js +117 -0
  213. package/src/js/utils/constants.js +28 -0
  214. package/src/js/utils/indexeddb-helper.js +225 -0
  215. package/src/js/utils/merlin-editor-support.js +905 -0
  216. package/src/js/utils/merlin-highlighting.js +551 -0
  217. package/src/js/utils/storage.js +125 -0
  218. package/src/js/utils/string-utils.js +19 -0
  219. package/src/js/utils/wasm-memory.js +54 -0
  220. package/src/js/windows/base-window.js +690 -0
  221. package/src/js/windows/index.js +9 -0
  222. package/src/js/windows/window-manager.js +375 -0
  223. package/tests/catch2/catch.hpp +17976 -0
  224. package/tests/common/basic_program_builder.cpp +119 -0
  225. package/tests/common/basic_program_builder.hpp +209 -0
  226. package/tests/common/disk_image_builder.cpp +444 -0
  227. package/tests/common/disk_image_builder.hpp +141 -0
  228. package/tests/common/test_helpers.hpp +118 -0
  229. package/tests/gcr/gcr-test.cpp +142 -0
  230. package/tests/integration/check-rom.js +70 -0
  231. package/tests/integration/compare-boot.js +239 -0
  232. package/tests/integration/crash-trace.js +102 -0
  233. package/tests/integration/disk-boot-test.js +264 -0
  234. package/tests/integration/memory-crash.js +108 -0
  235. package/tests/integration/nibble-read-test.js +249 -0
  236. package/tests/integration/phase-test.js +159 -0
  237. package/tests/integration/test_emulator.cpp +291 -0
  238. package/tests/integration/test_emulator_basic.cpp +91 -0
  239. package/tests/integration/test_emulator_debug.cpp +344 -0
  240. package/tests/integration/test_emulator_disk.cpp +153 -0
  241. package/tests/integration/test_emulator_state.cpp +163 -0
  242. package/tests/klaus/6502_functional_test.bin +0 -0
  243. package/tests/klaus/65C02_extended_opcodes_test.bin +0 -0
  244. package/tests/klaus/klaus_6502_test.cpp +184 -0
  245. package/tests/klaus/klaus_65c02_test.cpp +197 -0
  246. package/tests/thunderclock/thunderclock_mmu_test.cpp +304 -0
  247. package/tests/thunderclock/thunderclock_test.cpp +550 -0
  248. package/tests/unit/test_assembler.cpp +521 -0
  249. package/tests/unit/test_audio.cpp +196 -0
  250. package/tests/unit/test_ay8910.cpp +311 -0
  251. package/tests/unit/test_basic_detokenizer.cpp +265 -0
  252. package/tests/unit/test_basic_tokenizer.cpp +382 -0
  253. package/tests/unit/test_block_device.cpp +259 -0
  254. package/tests/unit/test_condition_evaluator.cpp +219 -0
  255. package/tests/unit/test_cpu6502.cpp +1301 -0
  256. package/tests/unit/test_cpu_addressing.cpp +361 -0
  257. package/tests/unit/test_cpu_cycle_counts.cpp +409 -0
  258. package/tests/unit/test_cpu_decimal.cpp +166 -0
  259. package/tests/unit/test_cpu_interrupts.cpp +285 -0
  260. package/tests/unit/test_disassembler.cpp +323 -0
  261. package/tests/unit/test_disk2_card.cpp +330 -0
  262. package/tests/unit/test_dos33.cpp +273 -0
  263. package/tests/unit/test_dsk_disk_image.cpp +315 -0
  264. package/tests/unit/test_expansion_card.cpp +178 -0
  265. package/tests/unit/test_gcr_encoding.cpp +232 -0
  266. package/tests/unit/test_keyboard.cpp +262 -0
  267. package/tests/unit/test_mmu.cpp +555 -0
  268. package/tests/unit/test_mmu_slots.cpp +323 -0
  269. package/tests/unit/test_mockingboard.cpp +352 -0
  270. package/tests/unit/test_mouse_card.cpp +386 -0
  271. package/tests/unit/test_pascal.cpp +248 -0
  272. package/tests/unit/test_prodos.cpp +259 -0
  273. package/tests/unit/test_smartport_card.cpp +321 -0
  274. package/tests/unit/test_thunderclock.cpp +354 -0
  275. package/tests/unit/test_via6522.cpp +323 -0
  276. package/tests/unit/test_video.cpp +319 -0
  277. package/tests/unit/test_woz_disk_image.cpp +257 -0
  278. package/vite.config.js +96 -0
  279. package/wiki/AI-Agent.md +372 -0
  280. package/wiki/Architecture-Overview.md +303 -0
  281. package/wiki/Audio-System.md +449 -0
  282. package/wiki/CPU-Emulation.md +477 -0
  283. package/wiki/Debugger.md +516 -0
  284. package/wiki/Disk-Drives.md +161 -0
  285. package/wiki/Disk-System-Internals.md +547 -0
  286. package/wiki/Display-Settings.md +88 -0
  287. package/wiki/Expansion-Slots.md +187 -0
  288. package/wiki/File-Explorer.md +259 -0
  289. package/wiki/Getting-Started.md +156 -0
  290. package/wiki/Home.md +69 -0
  291. package/wiki/Input-Devices.md +183 -0
  292. package/wiki/Keyboard-Shortcuts.md +158 -0
  293. package/wiki/Memory-System.md +364 -0
  294. package/wiki/Save-States.md +172 -0
  295. package/wiki/Video-Rendering.md +658 -0
@@ -0,0 +1,2594 @@
1
+ /*
2
+ * basic-program-window.js - BASIC program editor with integrated debugger
3
+ *
4
+ * Written by
5
+ * Mike Daley <michael_daley@icloud.com>
6
+ */
7
+
8
+ import { BaseWindow } from "../windows/base-window.js";
9
+ import {
10
+ highlightBasicSourceWithIndent,
11
+ formatBasicSource,
12
+ } from "../utils/basic-highlighting.js";
13
+ import { escapeHtml } from "../utils/string-utils.js";
14
+ import { BasicAutocomplete } from "../utils/basic-autocomplete.js";
15
+ import { BasicBreakpointManager } from "./basic-breakpoint-manager.js";
16
+ import { RuleBuilderWindow } from "./rule-builder-window.js";
17
+ import { BasicVariableInspector } from "./basic-variable-inspector.js";
18
+ import { BasicProgramParser } from "./basic-program-parser.js";
19
+ import { showToast } from "../ui/toast.js";
20
+ import { showConfirm } from "../ui/confirm.js";
21
+
22
+ const BASIC_ERRORS = {
23
+ 0x00: "NEXT WITHOUT FOR",
24
+ 0x10: "SYNTAX ERROR",
25
+ 0x16: "RETURN WITHOUT GOSUB",
26
+ 0x2A: "OUT OF DATA",
27
+ 0x35: "ILLEGAL QUANTITY",
28
+ 0x45: "OVERFLOW",
29
+ 0x4D: "OUT OF MEMORY",
30
+ 0x5A: "UNDEF'D STATEMENT",
31
+ 0x6B: "BAD SUBSCRIPT",
32
+ 0x78: "REDIM'D ARRAY",
33
+ 0x85: "DIVISION BY ZERO",
34
+ 0x95: "ILLEGAL DIRECT",
35
+ 0xA3: "TYPE MISMATCH",
36
+ 0xB0: "STRING TOO LONG",
37
+ 0xBF: "FORMULA TOO COMPLEX",
38
+ 0xD2: "CAN'T CONTINUE",
39
+ 0xE0: "UNDEF'D FUNCTION",
40
+ };
41
+
42
+ export class BasicProgramWindow extends BaseWindow {
43
+ constructor(wasmModule, inputHandler, isRunningCallback) {
44
+ super({
45
+ id: "basic-program",
46
+ title: "Applesoft BASIC",
47
+ defaultWidth: 700,
48
+ defaultHeight: 500,
49
+ minWidth: 450,
50
+ minHeight: 400,
51
+ });
52
+ this.wasmModule = wasmModule;
53
+ this.inputHandler = inputHandler;
54
+ this.isRunningCallback = isRunningCallback;
55
+
56
+ // Debugger components
57
+ this.breakpointManager = new BasicBreakpointManager(wasmModule);
58
+ this.variableInspector = new BasicVariableInspector(wasmModule);
59
+ this.programParser = new BasicProgramParser(wasmModule);
60
+
61
+ // Debugger state
62
+ this.previousVariables = new Map();
63
+ this.changeTimestamps = new Map();
64
+ this.currentLineNumber = null;
65
+ this.currentStatementInfo = null;
66
+ this.expandedArrays = new Set(); // Track which arrays are expanded
67
+ this._varAutoRefresh = false; // Auto-refresh variables while program is running
68
+
69
+ // Heat map state
70
+ this.lineHeatMap = new Map(); // BASIC line number -> heat value (0.0–1.0)
71
+ this.heatMapEnabled = false;
72
+ this.traceEnabled = true; // Highlight current line while running
73
+
74
+ // Runtime error state
75
+ this.errorLineNumber = null;
76
+ this.errorStatementInfo = null;
77
+ this.errorMessage = null;
78
+ this.errorLineContent = null; // Code portion (after line number) for tracking across renumbers
79
+
80
+ // Editor line map (text line index -> BASIC line number)
81
+ this.lineMap = [];
82
+
83
+ this.breakpointManager.onChange(() => {
84
+ this.updateGutter();
85
+ this.updateHighlighting();
86
+ this.renderBreakpointList();
87
+ });
88
+ }
89
+
90
+ renderContent() {
91
+ return `
92
+ <div class="basic-unified-container">
93
+ <div class="basic-dbg-toolbar">
94
+ <button class="basic-dbg-btn basic-dbg-run" title="Run Applesoft BASIC program">
95
+ <span class="basic-dbg-icon">▶</span> Run
96
+ </button>
97
+ <button class="basic-dbg-btn basic-dbg-pause" title="Pause at next BASIC line">
98
+ <span class="basic-dbg-icon">❚❚</span> Pause
99
+ </button>
100
+ <button class="basic-dbg-btn basic-dbg-stop" title="Stop program (Ctrl+C)">
101
+ <span class="basic-dbg-icon">■</span> Stop
102
+ </button>
103
+ <button class="basic-dbg-btn basic-dbg-step-line" title="Step to next BASIC statement">
104
+ <span class="basic-dbg-icon">↓</span> Step
105
+ </button>
106
+ <div style="width:1px;height:16px;background:var(--separator-bg);margin:0 2px;flex-shrink:0;"></div>
107
+ <label class="toggle-label basic-heat-toggle" title="Highlight current line while running">
108
+ <input type="checkbox" class="basic-trace-checkbox" checked>
109
+ <span class="toggle-switch"></span>
110
+ <span style="font-size:10px">Trace</span>
111
+ </label>
112
+ <label class="toggle-label basic-heat-toggle" title="Show line execution heat map">
113
+ <input type="checkbox" class="basic-heat-checkbox">
114
+ <span class="toggle-switch"></span>
115
+ <span style="font-size:10px">Heat</span>
116
+ </label>
117
+ <div style="width:1px;height:16px;background:var(--separator-bg);margin:0 2px;flex-shrink:0;"></div>
118
+ <button class="basic-dbg-btn basic-load-btn" title="Read program from memory">Read</button>
119
+ <button class="basic-dbg-btn basic-insert-btn" title="Write program into memory">Write</button>
120
+ <button class="basic-dbg-btn basic-format-btn" title="Format code">Format</button>
121
+ <button class="basic-dbg-btn basic-renumber-btn" title="Renumber lines">Renum</button>
122
+ <div style="width:1px;height:16px;background:var(--separator-bg);margin:0 2px;flex-shrink:0;"></div>
123
+ <button class="basic-dbg-btn basic-dbg-new-btn" title="New">New</button>
124
+ <button class="basic-dbg-btn basic-dbg-open-btn" title="Open File">Open</button>
125
+ <button class="basic-dbg-btn basic-dbg-save-btn" title="Save File">Save</button>
126
+ </div>
127
+ <div class="basic-dbg-status-bar" data-state="idle">
128
+ <div class="basic-dbg-status">
129
+ <span class="basic-dbg-status-dot"></span>
130
+ <span class="basic-dbg-status-chip basic-dbg-status-idle">Idle</span>
131
+ </div>
132
+ <div class="basic-dbg-info">
133
+ <span class="basic-dbg-line">LINE: ---</span>
134
+ <span class="basic-dbg-ptr">PTR: $----</span>
135
+ <span class="basic-lines">0 lines</span>
136
+ <span class="basic-chars">0 chars</span>
137
+ </div>
138
+ </div>
139
+
140
+ <div class="basic-main-area">
141
+ <div class="basic-editor-section">
142
+ <div class="basic-editor-with-gutter">
143
+ <div class="basic-gutter"></div>
144
+ <div class="basic-editor-container">
145
+ <div class="basic-line-highlight"></div>
146
+ <pre class="basic-highlight" aria-hidden="true"></pre>
147
+ <textarea class="basic-textarea" placeholder="Enter your Applesoft BASIC program...
148
+
149
+ 10 REM EXAMPLE PROGRAM
150
+ 20 HOME
151
+ 30 FOR I = 1 TO 10
152
+ 40 PRINT &quot;HELLO WORLD &quot;;I
153
+ 50 NEXT I
154
+ 60 END" spellcheck="false"></textarea>
155
+ </div>
156
+ </div>
157
+ </div>
158
+
159
+ <div class="basic-splitter" title="Drag to resize"><div class="basic-splitter-handle"></div></div>
160
+
161
+ <div class="basic-dbg-sidebar">
162
+ <div class="basic-dbg-var-section">
163
+ <div class="basic-dbg-var-header">Variables</div>
164
+ <div class="basic-dbg-var-panel"></div>
165
+ </div>
166
+ <div class="basic-sidebar-splitter" title="Drag to resize"><div class="basic-sidebar-splitter-handle"></div></div>
167
+ <div class="basic-dbg-breakpoints">
168
+ <div class="basic-dbg-bp-header">
169
+ <span>Breakpoints</span>
170
+ </div>
171
+ <div class="basic-dbg-bp-toolbar">
172
+ <input type="text" class="basic-dbg-bp-input" placeholder="Line #" maxlength="5">
173
+ <button class="basic-dbg-bp-add-btn" title="Add line breakpoint">+</button>
174
+ <button class="basic-dbg-bp-add-rule-btn" title="Add condition rule">if\u2026</button>
175
+ <button class="basic-dbg-bp-info-btn" title="Breakpoint help">i</button>
176
+ </div>
177
+ <div class="basic-dbg-bp-list"></div>
178
+ </div>
179
+ </div>
180
+ </div>
181
+ </div>
182
+ `;
183
+ }
184
+
185
+ onContentRendered() {
186
+ // Editor elements
187
+ this.textarea = this.contentElement.querySelector(".basic-textarea");
188
+ this.highlight = this.contentElement.querySelector(".basic-highlight");
189
+ this.lineHighlight = this.contentElement.querySelector(
190
+ ".basic-line-highlight",
191
+ );
192
+ this.gutter = this.contentElement.querySelector(".basic-gutter");
193
+ this.linesSpan = this.contentElement.querySelector(".basic-lines");
194
+ this.charsSpan = this.contentElement.querySelector(".basic-chars");
195
+ this.loadBtn = this.contentElement.querySelector(".basic-load-btn");
196
+ this.insertBtn = this.contentElement.querySelector(".basic-insert-btn");
197
+ this.formatBtn = this.contentElement.querySelector(".basic-format-btn");
198
+ this.renumberBtn = this.contentElement.querySelector(".basic-renumber-btn");
199
+
200
+ this.infoBtn = this.contentElement.querySelector(".basic-dbg-bp-info-btn");
201
+
202
+ // Debugger elements
203
+ this.varPanel = this.contentElement.querySelector(".basic-dbg-var-panel");
204
+ // Handle array expand/collapse and variable edit clicks via delegation
205
+ this.varPanel.addEventListener("click", (e) => {
206
+ const header = e.target.closest(".basic-dbg-arr-header");
207
+ if (header) {
208
+ const arrName = header.dataset.arrName;
209
+ if (arrName) {
210
+ if (this.expandedArrays.has(arrName)) {
211
+ this.expandedArrays.delete(arrName);
212
+ } else {
213
+ this.expandedArrays.add(arrName);
214
+ }
215
+ // Force full rebuild since expanded/collapsed state changed
216
+ this._varStructureKey = null;
217
+ this.renderVariables();
218
+ }
219
+ return;
220
+ }
221
+
222
+ // Handle editable variable value clicks
223
+ const valueSpan = e.target.closest(".basic-dbg-var-value.editable");
224
+ if (valueSpan) {
225
+ e.stopPropagation();
226
+ this._startVariableEdit(valueSpan);
227
+ return;
228
+ }
229
+
230
+ // Handle editable array value clicks
231
+ const arrVal = e.target.closest(".basic-dbg-arr-val.editable");
232
+ if (arrVal) {
233
+ e.stopPropagation();
234
+ this._startArrayElementEdit(arrVal);
235
+ }
236
+ });
237
+ this.bpList = this.contentElement.querySelector(".basic-dbg-bp-list");
238
+ this.bpInput = this.contentElement.querySelector(".basic-dbg-bp-input");
239
+ this.lineSpan = this.contentElement.querySelector(".basic-dbg-line");
240
+ this.ptrSpan = this.contentElement.querySelector(".basic-dbg-ptr");
241
+ this.statusChip = this.contentElement.querySelector(".basic-dbg-status-chip");
242
+
243
+ // Track current editing line for auto-format on line change
244
+ this.lastEditLine = -1;
245
+
246
+ // Editor event listeners
247
+ this.textarea.addEventListener("input", () => {
248
+ this.updateGutter();
249
+ this._trackErrorLine();
250
+ this.updateHighlighting();
251
+ this.updateStats();
252
+ this.updateCurrentLineHighlight();
253
+ });
254
+
255
+ this.textarea.addEventListener("scroll", () => {
256
+ this.highlight.scrollTop = this.textarea.scrollTop;
257
+ this.highlight.scrollLeft = this.textarea.scrollLeft;
258
+ this.gutter.scrollTop = this.textarea.scrollTop;
259
+ this.updateCurrentLineHighlight();
260
+ });
261
+
262
+ this.textarea.addEventListener("keydown", (e) => {
263
+ // Handle Enter: format current line and insert next line number
264
+ if (e.key === "Enter") {
265
+ e.preventDefault();
266
+ this.handleEnterKey();
267
+ }
268
+ });
269
+
270
+ this.textarea.addEventListener("keyup", (e) => {
271
+ // Check for line change on navigation keys
272
+ if (
273
+ ["ArrowUp", "ArrowDown", "Home", "End", "PageUp", "PageDown"].includes(
274
+ e.key,
275
+ )
276
+ ) {
277
+ this.checkLineChange();
278
+ }
279
+ this.updateCurrentLineHighlight();
280
+ });
281
+
282
+ this.textarea.addEventListener("click", () => {
283
+ this.checkLineChange();
284
+ this.updateCurrentLineHighlight();
285
+ });
286
+
287
+ this.textarea.addEventListener("focus", () => {
288
+ this.lineHighlight.classList.add("visible");
289
+ this.lastEditLine = this.getCurrentLineIndex();
290
+ this.updateCurrentLineHighlight();
291
+ });
292
+
293
+ this.textarea.addEventListener("blur", () => {
294
+ this.lineHighlight.classList.remove("visible");
295
+ // Auto-format when leaving the editor
296
+ this.autoFormatCode();
297
+ });
298
+
299
+ this.textarea.addEventListener("paste", () => {
300
+ // Format after the pasted content has been inserted
301
+ setTimeout(() => this.autoFormatCode(), 0);
302
+ });
303
+
304
+ this.insertBtn.addEventListener("click", () => {
305
+ if (!this._isInBasic()) {
306
+ showToast("Emulator must be at the BASIC prompt to write a program to memory", "warning");
307
+ return;
308
+ }
309
+ this.loadIntoMemory();
310
+ });
311
+
312
+ this.loadBtn.addEventListener("click", () => {
313
+ if (!this._isInBasic()) {
314
+ showToast("Emulator must be at the BASIC prompt to read a program from memory", "warning");
315
+ return;
316
+ }
317
+ if (this.programParser.getLines().length === 0) {
318
+ showToast("No BASIC program found in memory", "warning");
319
+ return;
320
+ }
321
+ this.loadFromMemory();
322
+ });
323
+
324
+ // File management buttons
325
+ this.contentElement.querySelector(".basic-dbg-new-btn").addEventListener("click", () => this.newFile());
326
+ this.contentElement.querySelector(".basic-dbg-open-btn").addEventListener("click", () => this.openFile());
327
+ this.contentElement.querySelector(".basic-dbg-save-btn").addEventListener("click", () => this.saveFile());
328
+
329
+ this.formatBtn.addEventListener("click", () => {
330
+ this.autoFormatCode();
331
+ });
332
+
333
+ this.renumberBtn.addEventListener("click", () => {
334
+ this.renumberProgram();
335
+ });
336
+
337
+ this.infoBtn.addEventListener("click", () => {
338
+ this._showBreakpointHelp();
339
+ });
340
+
341
+ // Option+click on textarea to toggle statement-level breakpoints.
342
+ // Calculates which line/statement was clicked by matching coordinates
343
+ // against the highlight overlay's .basic-statement span positions.
344
+ this.textarea.addEventListener('click', (e) => {
345
+ if (!e.altKey) return;
346
+ if (!this.highlight || !this.lineMap) return;
347
+
348
+ // Find which highlight line div the click Y falls into
349
+ const lineDivs = this.highlight.children;
350
+ let targetLineDiv = null;
351
+ let lineIndex = -1;
352
+ for (let i = 0; i < lineDivs.length; i++) {
353
+ const rect = lineDivs[i].getBoundingClientRect();
354
+ if (e.clientY >= rect.top && e.clientY <= rect.bottom) {
355
+ targetLineDiv = lineDivs[i];
356
+ lineIndex = i;
357
+ break;
358
+ }
359
+ }
360
+ if (!targetLineDiv) return;
361
+
362
+ const lineNumber = this.lineMap[lineIndex];
363
+ if (lineNumber === null || lineNumber === undefined) return;
364
+
365
+ // Find which statement span the click X falls into
366
+ const stmtSpans = targetLineDiv.querySelectorAll('.basic-statement');
367
+ if (stmtSpans.length === 0) return;
368
+
369
+ let stmtIndex = -1;
370
+ for (let i = 0; i < stmtSpans.length; i++) {
371
+ const rect = stmtSpans[i].getBoundingClientRect();
372
+ if (e.clientX >= rect.left && e.clientX <= rect.right) {
373
+ stmtIndex = i;
374
+ break;
375
+ }
376
+ }
377
+ if (stmtIndex < 0) return;
378
+
379
+ this.breakpointManager.toggle(lineNumber, stmtIndex);
380
+ this.updateGutter();
381
+ this.updateHighlighting();
382
+ this.renderBreakpointList();
383
+ e.preventDefault();
384
+ });
385
+
386
+ // Highlight statement under cursor on hover (shows what Option+click would target)
387
+ this.textarea.addEventListener('mousemove', (e) => {
388
+ if (!this.highlight || !this.lineMap) return;
389
+
390
+ // Find which highlight line div the cursor Y falls into
391
+ const lineDivs = this.highlight.children;
392
+ let targetLineDiv = null;
393
+ let lineIndex = -1;
394
+ for (let i = 0; i < lineDivs.length; i++) {
395
+ const rect = lineDivs[i].getBoundingClientRect();
396
+ if (e.clientY >= rect.top && e.clientY <= rect.bottom) {
397
+ targetLineDiv = lineDivs[i];
398
+ lineIndex = i;
399
+ break;
400
+ }
401
+ }
402
+
403
+ // Clear previous hover
404
+ const prev = this.highlight.querySelector('.basic-statement-hover');
405
+ if (prev) prev.classList.remove('basic-statement-hover');
406
+
407
+ if (!targetLineDiv || this.lineMap[lineIndex] === null) return;
408
+
409
+ // Find which statement span the cursor falls into
410
+ const stmtSpans = targetLineDiv.querySelectorAll('.basic-statement');
411
+ if (stmtSpans.length === 0) return;
412
+
413
+ for (const span of stmtSpans) {
414
+ const rect = span.getBoundingClientRect();
415
+ if (e.clientX >= rect.left && e.clientX <= rect.right) {
416
+ span.classList.add('basic-statement-hover');
417
+ break;
418
+ }
419
+ }
420
+ });
421
+
422
+ this.textarea.addEventListener('mouseleave', () => {
423
+ const prev = this.highlight?.querySelector('.basic-statement-hover');
424
+ if (prev) prev.classList.remove('basic-statement-hover');
425
+ });
426
+
427
+ // Initialize autocomplete
428
+ const editorContainer = this.contentElement.querySelector(
429
+ ".basic-editor-container",
430
+ );
431
+ this.autocomplete = new BasicAutocomplete(this.textarea, editorContainer);
432
+
433
+ // Trace toggle
434
+ this.traceCheckbox = this.contentElement.querySelector(".basic-trace-checkbox");
435
+ this.traceCheckbox.addEventListener("change", () => {
436
+ this.traceEnabled = this.traceCheckbox.checked;
437
+ if (!this.traceEnabled && this.currentLineNumber !== null) {
438
+ this.currentLineNumber = null;
439
+ this.currentStatementInfo = null;
440
+ this._updateGutterStyles();
441
+ this.updateHighlighting();
442
+ }
443
+ });
444
+
445
+ // Heat map toggle
446
+ this.heatCheckbox = this.contentElement.querySelector(".basic-heat-checkbox");
447
+ this.heatCheckbox.addEventListener("change", () => {
448
+ this.heatMapEnabled = this.heatCheckbox.checked;
449
+ this.wasmModule._setBasicHeatMapEnabled(this.heatMapEnabled);
450
+ if (!this.heatMapEnabled) {
451
+ this.lineHeatMap.clear();
452
+ this.wasmModule._clearBasicHeatMap();
453
+ this.updateGutter();
454
+ this.updateHighlighting();
455
+ }
456
+ });
457
+
458
+ // Debugger toolbar buttons
459
+ this.contentElement
460
+ .querySelector(".basic-dbg-run")
461
+ .addEventListener("click", () => this.handleRun());
462
+ this.contentElement
463
+ .querySelector(".basic-dbg-pause")
464
+ .addEventListener("click", () => this.handlePause());
465
+ this.contentElement
466
+ .querySelector(".basic-dbg-stop")
467
+ .addEventListener("click", () => this.handleStop());
468
+ this.contentElement
469
+ .querySelector(".basic-dbg-step-line")
470
+ .addEventListener("click", () => this.handleStepLine());
471
+
472
+ // Breakpoint input
473
+ this.contentElement
474
+ .querySelector(".basic-dbg-bp-add-btn")
475
+ .addEventListener("click", () => this.addBreakpointFromInput());
476
+ this.bpInput.addEventListener("keydown", (e) => {
477
+ if (e.key === "Enter") this.addBreakpointFromInput();
478
+ });
479
+
480
+ // Add condition-only rule button
481
+ this.contentElement
482
+ .querySelector(".basic-dbg-bp-add-rule-btn")
483
+ .addEventListener("click", () => this.addConditionRule());
484
+
485
+ // Splitter for resizable panels (editor <-> sidebar)
486
+ this.splitter = this.contentElement.querySelector(".basic-splitter");
487
+ this.sidebar = this.contentElement.querySelector(".basic-dbg-sidebar");
488
+ this.editorSection = this.contentElement.querySelector(
489
+ ".basic-editor-section",
490
+ );
491
+ this.setupSplitter();
492
+
493
+ // Splitter for sidebar panels (variables <-> breakpoints)
494
+ this.sidebarSplitter = this.contentElement.querySelector(
495
+ ".basic-sidebar-splitter",
496
+ );
497
+ this.varSection = this.contentElement.querySelector(
498
+ ".basic-dbg-var-section",
499
+ );
500
+ this.bpSection = this.contentElement.querySelector(
501
+ ".basic-dbg-breakpoints",
502
+ );
503
+ this.setupSidebarSplitter();
504
+
505
+ this.setupGutterClickHandler();
506
+ this.updateGutter();
507
+ this.updateHighlighting();
508
+ this.updateStats();
509
+ this.renderVariables();
510
+ this.renderBreakpointList();
511
+ }
512
+
513
+ /**
514
+ * Set up a drag-to-resize splitter. Returns a cleanup function to remove
515
+ * document-level listeners when the window is destroyed.
516
+ * @param {Object} options
517
+ * @param {HTMLElement} options.handle - The splitter element to drag
518
+ * @param {string} options.cursor - Cursor style during drag ('col-resize' or 'row-resize')
519
+ * @param {function} options.onDrag - Called with (startValue, delta) during drag
520
+ * @param {function} options.getStartValue - Returns the initial size value on drag start
521
+ */
522
+ setupDragSplitter({ handle, cursor, getStartValue, onDrag }) {
523
+ let isDragging = false;
524
+ let startPos = 0;
525
+ let startValue = 0;
526
+ const isHorizontal = cursor === "col-resize";
527
+
528
+ const onMouseDown = (e) => {
529
+ isDragging = true;
530
+ startPos = isHorizontal ? e.clientX : e.clientY;
531
+ startValue = getStartValue();
532
+ document.body.style.cursor = cursor;
533
+ document.body.style.userSelect = "none";
534
+ e.preventDefault();
535
+ };
536
+
537
+ const onMouseMove = (e) => {
538
+ if (!isDragging) return;
539
+ const delta = startPos - (isHorizontal ? e.clientX : e.clientY);
540
+ onDrag(startValue, delta);
541
+ };
542
+
543
+ const onMouseUp = () => {
544
+ if (!isDragging) return;
545
+ isDragging = false;
546
+ document.body.style.cursor = "";
547
+ document.body.style.userSelect = "";
548
+ if (this.onStateChange) this.onStateChange();
549
+ };
550
+
551
+ handle.addEventListener("mousedown", onMouseDown);
552
+ document.addEventListener("mousemove", onMouseMove);
553
+ document.addEventListener("mouseup", onMouseUp);
554
+
555
+ return () => {
556
+ document.removeEventListener("mousemove", onMouseMove);
557
+ document.removeEventListener("mouseup", onMouseUp);
558
+ };
559
+ }
560
+
561
+ setupSplitter() {
562
+ const MIN_EDITOR_WIDTH = 380;
563
+ const MIN_SIDEBAR_WIDTH = 120;
564
+ const MAX_SIDEBAR_WIDTH = 600;
565
+ const SPLITTER_WIDTH = 8;
566
+
567
+ this._cleanupSplitter = this.setupDragSplitter({
568
+ handle: this.splitter,
569
+ cursor: "col-resize",
570
+ getStartValue: () => this.sidebar.offsetWidth,
571
+ onDrag: (startWidth, delta) => {
572
+ let newWidth = startWidth + delta;
573
+ const mainArea = this.contentElement.querySelector(".basic-main-area");
574
+ const containerWidth = mainArea ? mainArea.offsetWidth : 800;
575
+ const maxSidebarForEditor = containerWidth - MIN_EDITOR_WIDTH - SPLITTER_WIDTH;
576
+ newWidth = Math.max(newWidth, MIN_SIDEBAR_WIDTH);
577
+ newWidth = Math.min(newWidth, MAX_SIDEBAR_WIDTH);
578
+ newWidth = Math.min(newWidth, maxSidebarForEditor);
579
+ this.sidebar.style.width = `${newWidth}px`;
580
+ },
581
+ });
582
+ }
583
+
584
+ setupSidebarSplitter() {
585
+ this._cleanupSidebarSplitter = this.setupDragSplitter({
586
+ handle: this.sidebarSplitter,
587
+ cursor: "row-resize",
588
+ getStartValue: () => this.bpSection.offsetHeight,
589
+ onDrag: (startHeight, delta) => {
590
+ const newHeight = Math.min(Math.max(startHeight + delta, 80), 400);
591
+ this.bpSection.style.height = `${newHeight}px`;
592
+ },
593
+ });
594
+ }
595
+
596
+ // ========================================
597
+ // Gutter Methods
598
+ // ========================================
599
+
600
+ /**
601
+ * Set up event delegation for gutter clicks (called once during init)
602
+ * Using delegation so clicks work even when DOM is updated during program execution
603
+ */
604
+ setupGutterClickHandler() {
605
+ this.gutter.addEventListener("click", (e) => {
606
+ // Find the gutter line element that was clicked
607
+ const gutterLine = e.target.closest(".basic-gutter-line");
608
+ if (!gutterLine) return;
609
+
610
+ const index = parseInt(gutterLine.dataset.index, 10);
611
+ if (isNaN(index)) return;
612
+
613
+ const lineNumber = this.lineMap[index];
614
+ if (lineNumber !== null) {
615
+ // If line has any breakpoints (whole-line or statement), remove them all
616
+ if (this.breakpointManager.hasAnyForLine(lineNumber)) {
617
+ this.breakpointManager.removeAllForLine(lineNumber);
618
+ } else {
619
+ // Otherwise add a whole-line breakpoint
620
+ this.breakpointManager.add(lineNumber, -1);
621
+ }
622
+ this.updateHighlighting();
623
+ this.renderBreakpointList();
624
+ }
625
+ });
626
+ }
627
+
628
+ updateGutter() {
629
+ const text = this.textarea.value;
630
+ const rawLines = text.split(/\r?\n/);
631
+
632
+ // Build line map (text line index -> BASIC line number) and gutter HTML
633
+ this.lineMap = [];
634
+ let html = "";
635
+
636
+ for (let i = 0; i < rawLines.length; i++) {
637
+ const line = rawLines[i];
638
+ const trimmed = line.trim();
639
+ const match = trimmed.match(/^(\d+)/);
640
+ const lineNumber = match ? parseInt(match[1], 10) : null;
641
+
642
+ this.lineMap[i] = lineNumber;
643
+
644
+ const hasBp =
645
+ lineNumber !== null && this.breakpointManager.hasAnyForLine(lineNumber);
646
+ const isCurrent =
647
+ lineNumber !== null && this.currentLineNumber === lineNumber;
648
+ const isError =
649
+ lineNumber !== null && this.errorLineNumber === lineNumber;
650
+
651
+ const bpClass = hasBp ? "has-bp" : "";
652
+ const currentClass = isCurrent ? "is-current" : "";
653
+ const errorClass = isError ? "has-error" : "";
654
+ const clickable = lineNumber !== null ? "clickable" : "";
655
+
656
+ // Gutter shows breakpoint markers with subtle line number tooltip
657
+ const marker = isError ? "!" : (hasBp ? "●" : "");
658
+
659
+ // Heat map background
660
+ let heatStyle = "";
661
+ if (this.heatMapEnabled && lineNumber !== null && !hasBp && !isCurrent && !isError) {
662
+ const heat = this.lineHeatMap.get(lineNumber) || 0;
663
+ if (heat > 0.005) {
664
+ heatStyle = ` style="background:${this._heatColor(heat)}"`;
665
+ }
666
+ }
667
+
668
+ html += `
669
+ <div class="basic-gutter-line ${bpClass} ${currentClass} ${errorClass} ${clickable}"${heatStyle} data-index="${i}">
670
+ <span class="basic-gutter-bp" title="${lineNumber !== null ? `Line ${lineNumber} - Click to toggle breakpoint` : ""}">${marker}</span>
671
+ <span class="basic-gutter-current">${isCurrent ? "►" : ""}</span>
672
+ </div>
673
+ `;
674
+ }
675
+
676
+ // Ensure at least one line for empty editor
677
+ if (rawLines.length === 0) {
678
+ html =
679
+ '<div class="basic-gutter-line"><span class="basic-gutter-bp"></span><span class="basic-gutter-current"></span></div>';
680
+ }
681
+
682
+ this.gutter.innerHTML = html;
683
+
684
+ this._scrollToCurrentLine();
685
+ }
686
+
687
+ _scrollToCurrentLine() {
688
+ if (this.currentLineNumber !== null && this.lineMap) {
689
+ const lineIndex = this.lineMap.indexOf(this.currentLineNumber);
690
+ if (lineIndex >= 0) {
691
+ const style = getComputedStyle(this.textarea);
692
+ const lineHeight =
693
+ parseFloat(style.lineHeight) || parseFloat(style.fontSize) * 1.5;
694
+ const paddingTop = parseFloat(style.paddingTop) || 8;
695
+ const lineTop = paddingTop + lineIndex * lineHeight;
696
+ const scrollTop = this.textarea.scrollTop;
697
+ const viewHeight = this.textarea.clientHeight;
698
+
699
+ if (lineTop < scrollTop || lineTop + lineHeight > scrollTop + viewHeight) {
700
+ this.textarea.scrollTop = lineTop - viewHeight / 2 + lineHeight / 2;
701
+ this.highlight.scrollTop = this.textarea.scrollTop;
702
+ this.gutter.scrollTop = this.textarea.scrollTop;
703
+ }
704
+ }
705
+ }
706
+ }
707
+
708
+ /**
709
+ * Update gutter line styles in-place (current-line marker, heat map backgrounds)
710
+ * without rebuilding the DOM, which would cause scroll desync.
711
+ */
712
+ _updateGutterStyles() {
713
+ const gutterLines = this.gutter.querySelectorAll(".basic-gutter-line");
714
+ for (let i = 0; i < gutterLines.length; i++) {
715
+ const lineNumber = this.lineMap[i];
716
+ const el = gutterLines[i];
717
+ const isCurrent = lineNumber !== null && this.currentLineNumber === lineNumber;
718
+
719
+ // Update current-line class
720
+ el.classList.toggle("is-current", isCurrent);
721
+
722
+ // Update current-line indicator
723
+ const currentSpan = el.querySelector(".basic-gutter-current");
724
+ if (currentSpan) currentSpan.textContent = isCurrent ? "►" : "";
725
+
726
+ // Heat map background (skip bp/current/error lines)
727
+ if (el.classList.contains("has-bp") || isCurrent || el.classList.contains("has-error")) {
728
+ el.style.background = "";
729
+ } else if (this.heatMapEnabled && lineNumber !== null && lineNumber !== undefined) {
730
+ const heat = this.lineHeatMap.get(lineNumber) || 0;
731
+ el.style.background = heat > 0.005 ? this._heatColor(heat) : "";
732
+ } else {
733
+ el.style.background = "";
734
+ }
735
+ }
736
+ }
737
+
738
+ /**
739
+ * Read heat map data from the C++ emulator into this.lineHeatMap.
740
+ * Converts raw execution counts to normalized 0.0–1.0 heat values.
741
+ */
742
+ _readHeatMapFromWasm() {
743
+ const size = this.wasmModule._getBasicHeatMapSize();
744
+ if (size === 0) {
745
+ this.lineHeatMap.clear();
746
+ return;
747
+ }
748
+
749
+ const maxEntries = Math.min(size, 1024);
750
+ // Allocate buffers: uint16_t[] for lines, uint32_t[] for counts
751
+ const linesPtr = this.wasmModule._malloc(maxEntries * 2);
752
+ const countsPtr = this.wasmModule._malloc(maxEntries * 4);
753
+
754
+ const count = this.wasmModule._getBasicHeatMapData(linesPtr, countsPtr, maxEntries);
755
+
756
+ // Read data from WASM heap
757
+ const lines = new Uint16Array(this.wasmModule.HEAPU8.buffer, linesPtr, count);
758
+ const counts = new Uint32Array(this.wasmModule.HEAPU8.buffer, countsPtr, count);
759
+
760
+ // Find max count for normalization
761
+ let maxCount = 0;
762
+ for (let i = 0; i < count; i++) {
763
+ if (counts[i] > maxCount) maxCount = counts[i];
764
+ }
765
+
766
+ // Build new normalized values from C++ counts
767
+ const decay = 0.92; // Per-frame decay (~30fps: fades to ~8% in 1 second)
768
+ const activeLines = new Set();
769
+
770
+ if (maxCount > 0) {
771
+ for (let i = 0; i < count; i++) {
772
+ const lineNum = lines[i];
773
+ const normalized = Math.log(1 + counts[i]) / Math.log(1 + maxCount);
774
+ const prev = this.lineHeatMap.get(lineNum) || 0;
775
+ // Blend: take the max of decayed previous and new normalized value
776
+ this.lineHeatMap.set(lineNum, Math.max(prev * decay, normalized));
777
+ activeLines.add(lineNum);
778
+ }
779
+ }
780
+
781
+ // Decay lines no longer in C++ map and prune dead entries
782
+ for (const [line, heat] of this.lineHeatMap) {
783
+ if (!activeLines.has(line)) {
784
+ const decayed = heat * decay;
785
+ if (decayed < 0.005) {
786
+ this.lineHeatMap.delete(line);
787
+ } else {
788
+ this.lineHeatMap.set(line, decayed);
789
+ }
790
+ }
791
+ }
792
+
793
+ this.wasmModule._free(linesPtr);
794
+ this.wasmModule._free(countsPtr);
795
+ }
796
+
797
+ /**
798
+ * Convert heat value (0.0–1.0) to a colour string.
799
+ * Gradient: transparent → blue → green → yellow → red
800
+ */
801
+ _heatColor(heat) {
802
+ const h = Math.max(0, Math.min(1, heat));
803
+ let r, g, b;
804
+ if (h < 0.25) {
805
+ // blue (0,100,200) → cyan-ish (0,180,180)
806
+ const t = h / 0.25;
807
+ r = 0; g = Math.round(100 + 80 * t); b = Math.round(200 - 20 * t);
808
+ } else if (h < 0.5) {
809
+ // green (0,180,80)
810
+ const t = (h - 0.25) / 0.25;
811
+ r = 0; g = Math.round(180 - 0 * t); b = Math.round(180 - 100 * t);
812
+ } else if (h < 0.75) {
813
+ // yellow (200,200,0)
814
+ const t = (h - 0.5) / 0.25;
815
+ r = Math.round(200 * t); g = Math.round(180 + 20 * t); b = Math.round(80 - 80 * t);
816
+ } else {
817
+ // red (220,60,40)
818
+ const t = (h - 0.75) / 0.25;
819
+ r = Math.round(200 + 20 * t); g = Math.round(200 - 140 * t); b = Math.round(0 + 40 * t);
820
+ }
821
+ const alpha = 0.15 + 0.35 * h;
822
+ return `rgba(${r},${g},${b},${alpha})`;
823
+ }
824
+
825
+ // ========================================
826
+ // Editor Methods
827
+ // ========================================
828
+
829
+ updateCurrentLineHighlight() {
830
+ if (!this.lineHighlight || !this.textarea) return;
831
+ const text = this.textarea.value.substring(0, this.textarea.selectionStart);
832
+ const lineIndex = text.split("\n").length - 1;
833
+ const style = getComputedStyle(this.textarea);
834
+ // Use computed line height or calculate from font size (11px * 1.5 = 16.5px)
835
+ const lineHeight =
836
+ parseFloat(style.lineHeight) || parseFloat(style.fontSize) * 1.5;
837
+ const paddingTop = parseFloat(style.paddingTop) || 8;
838
+ const top = paddingTop + lineIndex * lineHeight - this.textarea.scrollTop;
839
+ this.lineHighlight.style.top = `${top}px`;
840
+ this.lineHighlight.style.height = `${lineHeight}px`;
841
+ }
842
+
843
+ /**
844
+ * Get the current line index (0-based) based on cursor position
845
+ */
846
+ getCurrentLineIndex() {
847
+ if (!this.textarea) return 0;
848
+ const text = this.textarea.value.substring(0, this.textarea.selectionStart);
849
+ return text.split("\n").length - 1;
850
+ }
851
+
852
+ /**
853
+ * Check if cursor moved to a different line and auto-format if so
854
+ */
855
+ checkLineChange() {
856
+ const currentLine = this.getCurrentLineIndex();
857
+ if (this.lastEditLine !== -1 && currentLine !== this.lastEditLine) {
858
+ // Line changed - auto-format preserving cursor position
859
+ this.autoFormatPreserveCursor();
860
+ }
861
+ this.lastEditLine = currentLine;
862
+ }
863
+
864
+ /**
865
+ * Handle Enter key: split line at cursor, moving text after cursor to new line
866
+ */
867
+ handleEnterKey() {
868
+ const text = this.textarea.value;
869
+ const cursorPos = this.textarea.selectionStart;
870
+
871
+ // Get current line info
872
+ const beforeCursor = text.substring(0, cursorPos);
873
+ const lines = text.split("\n");
874
+ const currentLineIndex = beforeCursor.split("\n").length - 1;
875
+ const currentLine = lines[currentLineIndex] || "";
876
+
877
+ // Find where cursor is within the current line
878
+ const lineStart = beforeCursor.lastIndexOf("\n") + 1;
879
+ const cursorOffsetInLine = cursorPos - lineStart;
880
+
881
+ // Split current line at cursor position
882
+ const lineBeforeCursor = currentLine.substring(0, cursorOffsetInLine);
883
+ const lineAfterCursor = currentLine.substring(cursorOffsetInLine);
884
+
885
+ // Find line number for the new line
886
+ const lineNumMatch = currentLine.trim().match(/^(\d+)/);
887
+ let nextLineNum = 10; // Default starting line number
888
+
889
+ if (lineNumMatch) {
890
+ const currentLineNum = parseInt(lineNumMatch[1], 10);
891
+ nextLineNum = currentLineNum + 10;
892
+ } else {
893
+ // No line number on current line, look at previous lines
894
+ for (let i = currentLineIndex - 1; i >= 0; i--) {
895
+ const prevMatch = lines[i].trim().match(/^(\d+)/);
896
+ if (prevMatch) {
897
+ nextLineNum = parseInt(prevMatch[1], 10) + 10;
898
+ break;
899
+ }
900
+ }
901
+ }
902
+
903
+ // Build new content:
904
+ // - Keep text before cursor on current line
905
+ // - Create new line with next line number + text after cursor (trimmed)
906
+ const textAfterTrimmed = lineAfterCursor.trimStart();
907
+ lines[currentLineIndex] = lineBeforeCursor;
908
+ const newLine = `${nextLineNum} ${textAfterTrimmed}`;
909
+ lines.splice(currentLineIndex + 1, 0, newLine);
910
+
911
+ const newText = lines.join("\n");
912
+
913
+ // Format the result
914
+ const formatted = formatBasicSource(newText);
915
+
916
+ // Update textarea
917
+ this.textarea.value = formatted;
918
+
919
+ // Position cursor at the start of the code on the new line (after line number and space)
920
+ const formattedLines = formatted.split("\n");
921
+ let newCursorPos = 0;
922
+ for (let i = 0; i <= currentLineIndex; i++) {
923
+ newCursorPos += formattedLines[i].length + 1; // +1 for newline
924
+ }
925
+ // Position after line number and space on the new line
926
+ const newLineContent = formattedLines[currentLineIndex + 1] || "";
927
+ const newLineMatch = newLineContent.match(/^(\s*\d+\s*)/);
928
+ if (newLineMatch) {
929
+ newCursorPos += newLineMatch[1].length;
930
+ }
931
+
932
+ this.textarea.selectionStart = this.textarea.selectionEnd = newCursorPos;
933
+
934
+ // Update displays
935
+ this.updateHighlighting();
936
+ this.updateStats();
937
+ this.updateGutter();
938
+ this.updateCurrentLineHighlight();
939
+
940
+ // Update line tracking
941
+ this.lastEditLine = this.getCurrentLineIndex();
942
+ }
943
+
944
+ /**
945
+ * Auto-format while preserving the cursor position on the current line
946
+ */
947
+ autoFormatPreserveCursor() {
948
+ const text = this.textarea.value;
949
+ if (!text.trim()) return;
950
+
951
+ // Get current cursor position info
952
+ const cursorPos = this.textarea.selectionStart;
953
+ const beforeCursor = text.substring(0, cursorPos);
954
+ const currentLineIndex = beforeCursor.split("\n").length - 1;
955
+ const lines = text.split("\n");
956
+ const currentLineStart = beforeCursor.lastIndexOf("\n") + 1;
957
+ const cursorOffsetInLine = cursorPos - currentLineStart;
958
+
959
+ // Format the code
960
+ const formatted = formatBasicSource(text);
961
+
962
+ // Only update if different
963
+ if (formatted !== text) {
964
+ // Calculate new cursor position
965
+ const formattedLines = formatted.split("\n");
966
+ let newCursorPos = 0;
967
+
968
+ // Sum up lengths of lines before current line
969
+ for (let i = 0; i < currentLineIndex && i < formattedLines.length; i++) {
970
+ newCursorPos += formattedLines[i].length + 1; // +1 for newline
971
+ }
972
+
973
+ // Add offset within current line (clamped to line length)
974
+ if (currentLineIndex < formattedLines.length) {
975
+ const newLineLength = formattedLines[currentLineIndex].length;
976
+ newCursorPos += Math.min(cursorOffsetInLine, newLineLength);
977
+ }
978
+
979
+ this.textarea.value = formatted;
980
+ this.textarea.selectionStart = this.textarea.selectionEnd = newCursorPos;
981
+
982
+ this.updateHighlighting();
983
+ this.updateStats();
984
+ this.updateGutter();
985
+ }
986
+ }
987
+
988
+ updateHighlighting() {
989
+ const text = this.textarea.value;
990
+
991
+ // Use indent-aware highlighting for smart formatting
992
+ const highlightedData = highlightBasicSourceWithIndent(text, {
993
+ preserveCase: true,
994
+ });
995
+
996
+ const highlightedLines = [];
997
+
998
+ for (let i = 0; i < highlightedData.length; i++) {
999
+ const { html, indent } = highlightedData[i];
1000
+
1001
+ // Build classes for the line
1002
+ const isCurrentLine =
1003
+ this.currentLineNumber !== null &&
1004
+ this.lineMap[i] === this.currentLineNumber;
1005
+
1006
+ let lineClass = "basic-line";
1007
+ if (isCurrentLine) {
1008
+ lineClass += " basic-current-line";
1009
+ if (this._breakpointHitLine) lineClass += " basic-breakpoint-hit";
1010
+ }
1011
+ if (indent > 0) lineClass += ` indent-${Math.min(indent, 4)}`;
1012
+
1013
+ // For multi-statement lines, wrap each statement in a span
1014
+ let lineHtml = html || "&nbsp;";
1015
+ const lineNumber = this.lineMap[i];
1016
+ const stmtBPs = lineNumber !== null ? this.breakpointManager.getForLine(lineNumber) : [];
1017
+ const isMultiStatement = html && html.includes('<span class="bas-punct">:</span>');
1018
+
1019
+ const isErrorLine = this.errorLineNumber !== null && lineNumber === this.errorLineNumber;
1020
+
1021
+ if (isErrorLine) {
1022
+ lineClass += " basic-error-line";
1023
+ }
1024
+
1025
+ if (isCurrentLine && this.currentStatementInfo && this.currentStatementInfo.statementCount > 1) {
1026
+ lineClass += " basic-has-statements";
1027
+ lineHtml = this._wrapStatements(html, this.currentStatementInfo, stmtBPs);
1028
+ } else if (isErrorLine && this.errorStatementInfo && this.errorStatementInfo.statementCount > 1 && isMultiStatement) {
1029
+ lineHtml = this._wrapStatementsForError(html, this.errorStatementInfo, stmtBPs);
1030
+ } else if (isMultiStatement && lineNumber !== null) {
1031
+ // Wrap all multi-statement lines so statements are clickable for breakpoints
1032
+ lineHtml = this._wrapStatementsForBreakpoints(html, stmtBPs);
1033
+ } else if (lineNumber !== null && html) {
1034
+ // Wrap single-statement lines so hover highlighting works
1035
+ const hasBP = stmtBPs.some(bp => bp.statementIndex >= 0 && bp.statementIndex === 0);
1036
+ const cls = hasBP ? " basic-statement-bp" : "";
1037
+ lineHtml = `<span class="basic-statement${cls}">${html}</span>`;
1038
+ }
1039
+
1040
+ if (isErrorLine && this.errorMessage) {
1041
+ lineHtml += `<span class="basic-error-msg">${this.errorMessage}</span>`;
1042
+ }
1043
+
1044
+ // Wrap each line in a div with appropriate classes
1045
+ const highlighted = `<div class="${lineClass}">${lineHtml}</div>`;
1046
+ highlightedLines.push(highlighted);
1047
+ }
1048
+
1049
+ this.highlight.innerHTML = highlightedLines.join("");
1050
+ }
1051
+
1052
+ updateStats() {
1053
+ const text = this.textarea.value;
1054
+ const lines = text ? text.split(/\r?\n/).filter((l) => l.trim()).length : 0;
1055
+ const chars = text.length;
1056
+
1057
+ this.linesSpan.textContent = `${lines} line${lines !== 1 ? "s" : ""}`;
1058
+ this.charsSpan.textContent = `${chars} char${chars !== 1 ? "s" : ""}`;
1059
+ }
1060
+
1061
+ /**
1062
+ * Auto-format the BASIC code in the editor
1063
+ * - Aligns line numbers to the right
1064
+ * - Adds indentation for FOR/NEXT loops
1065
+ */
1066
+ autoFormatCode() {
1067
+ const text = this.textarea.value;
1068
+ if (!text.trim()) return;
1069
+
1070
+ // Format the code
1071
+ const formatted = formatBasicSource(text);
1072
+
1073
+ // Only update if different (prevents unnecessary cursor reset)
1074
+ if (formatted !== text) {
1075
+ this.textarea.value = formatted;
1076
+ this.updateGutter();
1077
+ this.updateHighlighting();
1078
+ this.updateStats();
1079
+ }
1080
+ }
1081
+
1082
+ /**
1083
+ * Renumber all lines in increments of 10, updating GOTO/GOSUB references
1084
+ */
1085
+ renumberProgram() {
1086
+ const text = this.textarea.value;
1087
+ if (!text.trim()) return;
1088
+
1089
+ const rawLines = text.split(/\r?\n/);
1090
+
1091
+ // Parse lines and extract line numbers
1092
+ const parsedLines = [];
1093
+ for (const line of rawLines) {
1094
+ const trimmed = line.trim();
1095
+ if (!trimmed) continue;
1096
+
1097
+ const match = trimmed.match(/^(\d+)\s*(.*)/);
1098
+ if (match) {
1099
+ parsedLines.push({
1100
+ oldLineNumber: parseInt(match[1], 10),
1101
+ code: match[2] || "",
1102
+ });
1103
+ }
1104
+ }
1105
+
1106
+ if (parsedLines.length === 0) return;
1107
+
1108
+ // Sort by old line number
1109
+ parsedLines.sort((a, b) => a.oldLineNumber - b.oldLineNumber);
1110
+
1111
+ // Build mapping from old line numbers to new line numbers
1112
+ const lineMap = new Map();
1113
+ for (let i = 0; i < parsedLines.length; i++) {
1114
+ const newLineNum = (i + 1) * 10;
1115
+ lineMap.set(parsedLines[i].oldLineNumber, newLineNum);
1116
+ parsedLines[i].newLineNumber = newLineNum;
1117
+ }
1118
+
1119
+ // Update line references in code
1120
+ for (const line of parsedLines) {
1121
+ line.code = this.updateLineReferences(line.code, lineMap);
1122
+ }
1123
+
1124
+ // Rebuild the program
1125
+ const newLines = parsedLines.map((p) => `${p.newLineNumber} ${p.code}`);
1126
+ const renumbered = newLines.join("\n");
1127
+
1128
+ // Format and update
1129
+ this.textarea.value = formatBasicSource(renumbered);
1130
+ this.updateGutter();
1131
+ this.updateHighlighting();
1132
+ this.updateStats();
1133
+
1134
+ // Update breakpoints to new line numbers
1135
+ this.updateBreakpointsAfterRenumber(lineMap);
1136
+ }
1137
+
1138
+ /**
1139
+ * Update line number references in BASIC code
1140
+ * Handles: GOTO, GOSUB, THEN, ON...GOTO, ON...GOSUB, ONERR GOTO, RESUME
1141
+ * @param {string} code - The BASIC code (without line number)
1142
+ * @param {Map<number, number>} lineMap - Mapping from old to new line numbers
1143
+ * @returns {string} Code with updated line references
1144
+ */
1145
+ updateLineReferences(code, lineMap) {
1146
+ // Convert to uppercase for matching, but preserve original for strings
1147
+ let result = "";
1148
+ let i = 0;
1149
+ let inString = false;
1150
+
1151
+ while (i < code.length) {
1152
+ const char = code[i];
1153
+
1154
+ // Track string state
1155
+ if (char === '"') {
1156
+ inString = !inString;
1157
+ result += char;
1158
+ i++;
1159
+ continue;
1160
+ }
1161
+
1162
+ // Inside string - don't modify
1163
+ if (inString) {
1164
+ result += char;
1165
+ i++;
1166
+ continue;
1167
+ }
1168
+
1169
+ // Check for keywords that take line numbers
1170
+ const remaining = code.substring(i).toUpperCase();
1171
+
1172
+ // GOTO linenum
1173
+ if (remaining.startsWith("GOTO")) {
1174
+ result += code.substring(i, i + 4);
1175
+ i += 4;
1176
+ const updated = this.updateLineNumberList(code.substring(i), lineMap);
1177
+ result += updated.text;
1178
+ i += updated.consumed;
1179
+ continue;
1180
+ }
1181
+
1182
+ // GOSUB linenum
1183
+ if (remaining.startsWith("GOSUB")) {
1184
+ result += code.substring(i, i + 5);
1185
+ i += 5;
1186
+ const updated = this.updateLineNumberList(code.substring(i), lineMap);
1187
+ result += updated.text;
1188
+ i += updated.consumed;
1189
+ continue;
1190
+ }
1191
+
1192
+ // THEN can be followed by line number (short GOTO) or statements
1193
+ if (remaining.startsWith("THEN")) {
1194
+ result += code.substring(i, i + 4);
1195
+ i += 4;
1196
+ // Skip whitespace and check if followed by a digit
1197
+ let ws = "";
1198
+ let j = i;
1199
+ while (j < code.length && /\s/.test(code[j])) {
1200
+ ws += code[j];
1201
+ j++;
1202
+ }
1203
+ if (j < code.length && /\d/.test(code[j])) {
1204
+ // It's THEN linenum
1205
+ result += ws;
1206
+ i = j;
1207
+ const updated = this.updateSingleLineNumber(
1208
+ code.substring(i),
1209
+ lineMap,
1210
+ );
1211
+ result += updated.text;
1212
+ i += updated.consumed;
1213
+ }
1214
+ continue;
1215
+ }
1216
+
1217
+ // RESUME linenum (optional - can be just RESUME)
1218
+ if (remaining.startsWith("RESUME")) {
1219
+ result += code.substring(i, i + 6);
1220
+ i += 6;
1221
+ // Skip whitespace and check if followed by a digit
1222
+ let ws = "";
1223
+ let j = i;
1224
+ while (j < code.length && /\s/.test(code[j])) {
1225
+ ws += code[j];
1226
+ j++;
1227
+ }
1228
+ if (j < code.length && /\d/.test(code[j])) {
1229
+ result += ws;
1230
+ i = j;
1231
+ const updated = this.updateSingleLineNumber(
1232
+ code.substring(i),
1233
+ lineMap,
1234
+ );
1235
+ result += updated.text;
1236
+ i += updated.consumed;
1237
+ }
1238
+ continue;
1239
+ }
1240
+
1241
+ // Default: copy character
1242
+ result += char;
1243
+ i++;
1244
+ }
1245
+
1246
+ return result;
1247
+ }
1248
+
1249
+ /**
1250
+ * Update a comma-separated list of line numbers (for ON...GOTO/GOSUB)
1251
+ * @param {string} text - Text starting after GOTO/GOSUB keyword
1252
+ * @param {Map<number, number>} lineMap - Mapping from old to new line numbers
1253
+ * @returns {{text: string, consumed: number}} Updated text and characters consumed
1254
+ */
1255
+ updateLineNumberList(text, lineMap) {
1256
+ let result = "";
1257
+ let i = 0;
1258
+
1259
+ // Skip leading whitespace
1260
+ while (i < text.length && /\s/.test(text[i])) {
1261
+ result += text[i];
1262
+ i++;
1263
+ }
1264
+
1265
+ // Parse line numbers separated by commas
1266
+ while (i < text.length) {
1267
+ // Check for digit
1268
+ if (/\d/.test(text[i])) {
1269
+ let numStr = "";
1270
+ while (i < text.length && /\d/.test(text[i])) {
1271
+ numStr += text[i];
1272
+ i++;
1273
+ }
1274
+ const oldNum = parseInt(numStr, 10);
1275
+ const newNum = lineMap.has(oldNum) ? lineMap.get(oldNum) : oldNum;
1276
+ result += newNum.toString();
1277
+
1278
+ // Skip whitespace after number
1279
+ while (i < text.length && /\s/.test(text[i])) {
1280
+ result += text[i];
1281
+ i++;
1282
+ }
1283
+
1284
+ // Check for comma (more line numbers follow)
1285
+ if (i < text.length && text[i] === ",") {
1286
+ result += ",";
1287
+ i++;
1288
+ // Skip whitespace after comma
1289
+ while (i < text.length && /\s/.test(text[i])) {
1290
+ result += text[i];
1291
+ i++;
1292
+ }
1293
+ continue;
1294
+ }
1295
+ }
1296
+ // End of line number list
1297
+ break;
1298
+ }
1299
+
1300
+ return { text: result, consumed: i };
1301
+ }
1302
+
1303
+ /**
1304
+ * Update a single line number
1305
+ * @param {string} text - Text starting at the line number
1306
+ * @param {Map<number, number>} lineMap - Mapping from old to new line numbers
1307
+ * @returns {{text: string, consumed: number}} Updated text and characters consumed
1308
+ */
1309
+ updateSingleLineNumber(text, lineMap) {
1310
+ let numStr = "";
1311
+ let i = 0;
1312
+
1313
+ while (i < text.length && /\d/.test(text[i])) {
1314
+ numStr += text[i];
1315
+ i++;
1316
+ }
1317
+
1318
+ if (numStr) {
1319
+ const oldNum = parseInt(numStr, 10);
1320
+ const newNum = lineMap.has(oldNum) ? lineMap.get(oldNum) : oldNum;
1321
+ return { text: newNum.toString(), consumed: i };
1322
+ }
1323
+
1324
+ return { text: "", consumed: 0 };
1325
+ }
1326
+
1327
+ /**
1328
+ * Update breakpoints to new line numbers after renumbering
1329
+ * @param {Map<number, number>} lineMap - Mapping from old to new line numbers
1330
+ */
1331
+ updateBreakpointsAfterRenumber(lineMap) {
1332
+ const allEntries = this.breakpointManager.getAllEntries();
1333
+
1334
+ // Save all entries with their states
1335
+ const savedEntries = allEntries.map(e => ({
1336
+ lineNumber: e.lineNumber,
1337
+ statementIndex: e.statementIndex,
1338
+ enabled: e.enabled,
1339
+ }));
1340
+
1341
+ // Clear all breakpoints
1342
+ this.breakpointManager.clear();
1343
+
1344
+ // Re-add at new line numbers
1345
+ for (const entry of savedEntries) {
1346
+ const newLine = lineMap.get(entry.lineNumber);
1347
+ if (newLine !== undefined) {
1348
+ this.breakpointManager.add(newLine, entry.statementIndex);
1349
+ if (!entry.enabled) {
1350
+ this.breakpointManager.setEnabled(newLine, entry.statementIndex, false);
1351
+ }
1352
+ }
1353
+ }
1354
+
1355
+ this.renderBreakpointList();
1356
+ }
1357
+
1358
+ parseProgram(text) {
1359
+ const lines = [];
1360
+ const rawLines = text.split(/\r?\n/);
1361
+
1362
+ for (const rawLine of rawLines) {
1363
+ const trimmed = rawLine.trim().toUpperCase();
1364
+ if (!trimmed) continue;
1365
+
1366
+ const match = trimmed.match(/^(\d+)\s*(.*)/);
1367
+ if (!match) {
1368
+ console.warn("Skipping line without line number:", rawLine);
1369
+ continue;
1370
+ }
1371
+
1372
+ const lineNum = parseInt(match[1], 10);
1373
+ if (lineNum < 0 || lineNum > 63999) {
1374
+ console.warn("Invalid line number:", lineNum);
1375
+ continue;
1376
+ }
1377
+
1378
+ lines.push({
1379
+ lineNumber: lineNum,
1380
+ content: match[2] || "",
1381
+ });
1382
+ }
1383
+
1384
+ lines.sort((a, b) => a.lineNumber - b.lineNumber);
1385
+ return lines;
1386
+ }
1387
+
1388
+ /**
1389
+ * Load the current BASIC program from emulator memory into the textarea
1390
+ */
1391
+ async newFile() {
1392
+ if (this.textarea.value.trim()) {
1393
+ const confirmed = await showConfirm("Clear current source and start new file?");
1394
+ if (!confirmed) return;
1395
+ }
1396
+ this.textarea.value = "";
1397
+ this._fileHandle = null;
1398
+ this.updateGutter();
1399
+ this.updateHighlighting();
1400
+ this.updateStats();
1401
+ }
1402
+
1403
+ async openFile() {
1404
+ try {
1405
+ const [handle] = await window.showOpenFilePicker({
1406
+ types: [{ description: "BASIC source files", accept: { "text/plain": [".bas", ".txt"] } }],
1407
+ multiple: false,
1408
+ });
1409
+ const file = await handle.getFile();
1410
+ this.textarea.value = await file.text();
1411
+ this._fileHandle = handle;
1412
+ this.updateGutter();
1413
+ this.updateHighlighting();
1414
+ this.updateStats();
1415
+ } catch (err) {
1416
+ if (err.name !== "AbortError") console.error("Failed to open file:", err);
1417
+ }
1418
+ }
1419
+
1420
+ async saveFile() {
1421
+ const content = this.textarea.value;
1422
+ if (!content.trim()) return;
1423
+ try {
1424
+ if (!this._fileHandle) {
1425
+ this._fileHandle = await window.showSaveFilePicker({
1426
+ suggestedName: "program.bas",
1427
+ types: [{ description: "BASIC source files", accept: { "text/plain": [".bas", ".txt"] } }],
1428
+ });
1429
+ }
1430
+ const writable = await this._fileHandle.createWritable();
1431
+ await writable.write(content);
1432
+ await writable.close();
1433
+ } catch (err) {
1434
+ if (err.name !== "AbortError") console.error("Failed to save file:", err);
1435
+ }
1436
+ }
1437
+
1438
+ _isInBasic() {
1439
+ const emulatorOn = this.isRunningCallback ? this.isRunningCallback() : false;
1440
+ return emulatorOn && this.wasmModule._peekMemory(0x33) === 0xDD;
1441
+ }
1442
+
1443
+ loadFromMemory() {
1444
+ const lines = this.programParser.getLines();
1445
+ if (lines.length === 0) {
1446
+ console.log("No BASIC program in memory");
1447
+ return;
1448
+ }
1449
+
1450
+ if (this.errorLineNumber !== null) this._clearError();
1451
+
1452
+ // Build textarea content from parsed program lines
1453
+ const textLines = lines.map((line) => `${line.lineNumber} ${line.text}`);
1454
+ const rawContent = textLines.join("\n");
1455
+
1456
+ // Auto-format the loaded content
1457
+ this.textarea.value = formatBasicSource(rawContent);
1458
+
1459
+ this.updateGutter();
1460
+ this.updateHighlighting();
1461
+ this.updateStats();
1462
+
1463
+ console.log(`Loaded ${lines.length} lines from memory`);
1464
+ }
1465
+
1466
+ loadIntoMemory() {
1467
+ if (this.isRunningCallback && !this.isRunningCallback()) {
1468
+ showToast("Emulator is off", "error");
1469
+ return;
1470
+ }
1471
+
1472
+ const text = this.textarea.value;
1473
+ if (!text.trim()) {
1474
+ showToast("No program to load", "warning");
1475
+ return;
1476
+ }
1477
+
1478
+ // Allocate source string on WASM heap and call C++ tokenizer
1479
+ const encoder = new TextEncoder();
1480
+ const encoded = encoder.encode(text);
1481
+ const ptr = this.wasmModule._malloc(encoded.length + 1);
1482
+ this.wasmModule.HEAPU8.set(encoded, ptr);
1483
+ this.wasmModule.HEAPU8[ptr + encoded.length] = 0; // null terminator
1484
+ const result = this.wasmModule._loadBasicProgram(ptr);
1485
+ this.wasmModule._free(ptr);
1486
+
1487
+ if (result === -1) {
1488
+ showToast("Error loading program into memory", "error");
1489
+ return;
1490
+ }
1491
+ if (result === 0) {
1492
+ showToast("No valid BASIC lines found", "error");
1493
+ return;
1494
+ }
1495
+
1496
+ // Invalidate the program parser cache so Load from Memory sees new data
1497
+ this.programParser.invalidateCache();
1498
+
1499
+ showToast(`Loaded ${result} lines into emulator`, "info");
1500
+ }
1501
+
1502
+ _showBreakpointHelp() {
1503
+ // Remove existing popover if shown
1504
+ const existing = this.contentElement.querySelector(".basic-info-popover");
1505
+ if (existing) {
1506
+ existing.remove();
1507
+ return;
1508
+ }
1509
+
1510
+ const popover = document.createElement("div");
1511
+ popover.className = "basic-info-popover";
1512
+ popover.innerHTML = `
1513
+ <div class="basic-info-popover-title">Breakpoints</div>
1514
+ <div class="basic-info-popover-row">
1515
+ <span class="basic-info-popover-key">Click gutter</span>
1516
+ <span class="basic-info-popover-desc">Toggle line breakpoint</span>
1517
+ </div>
1518
+ <div class="basic-info-popover-row">
1519
+ <span class="basic-info-popover-key">${navigator.platform.includes("Mac") ? "Option" : "Alt"}+Click statement</span>
1520
+ <span class="basic-info-popover-desc">Toggle statement breakpoint</span>
1521
+ </div>
1522
+ `;
1523
+
1524
+ // Position below the toolbar button
1525
+ const btnRect = this.infoBtn.getBoundingClientRect();
1526
+ popover.style.top = `${btnRect.bottom + 4}px`;
1527
+ popover.style.left = `${btnRect.right - 260}px`;
1528
+ document.body.appendChild(popover);
1529
+
1530
+ // Close on click outside
1531
+ const close = (e) => {
1532
+ if (!popover.contains(e.target) && e.target !== this.infoBtn) {
1533
+ popover.remove();
1534
+ document.removeEventListener("mousedown", close);
1535
+ }
1536
+ };
1537
+ setTimeout(() => document.addEventListener("mousedown", close), 0);
1538
+ }
1539
+
1540
+ // ========================================
1541
+ // Debugger Methods
1542
+ // ========================================
1543
+
1544
+ /**
1545
+ * Run - Type RUN command into emulator to start BASIC program
1546
+ */
1547
+ handleRun() {
1548
+ if (!this.isRunningCallback || !this.isRunningCallback()) {
1549
+ console.log("Emulator not running");
1550
+ return;
1551
+ }
1552
+
1553
+ const isPaused = this.wasmModule._isPaused();
1554
+
1555
+ if (isPaused) {
1556
+ // Continue from pause
1557
+ this._clearBreakpointPulse();
1558
+ this._varAutoRefresh = true;
1559
+ this._lastBasicBreakpointHit = false;
1560
+ this.currentLineNumber = null;
1561
+ this.currentStatementInfo = null;
1562
+ this.updateGutter();
1563
+ this.updateHighlighting();
1564
+
1565
+ // Just unpause - the C++ setPaused(false) automatically handles:
1566
+ // - Setting skipBasicBreakpointLine_ if we're at a BASIC breakpoint
1567
+ // - Clearing the breakpoint hit flags
1568
+ this.wasmModule._setPaused(false);
1569
+ } else {
1570
+ // Fresh run — write editor contents to memory first, then type RUN
1571
+ this._clearBreakpointPulse();
1572
+ this._lastBasicBreakpointHit = false;
1573
+ this._lastProgramRunning = false;
1574
+ this.currentLineNumber = null;
1575
+ this.currentStatementInfo = null;
1576
+ if (this.errorLineNumber !== null) this._clearError();
1577
+ this.lineHeatMap.clear();
1578
+ this.wasmModule._clearBasicHeatMap();
1579
+
1580
+ this.wasmModule._setPaused(false);
1581
+
1582
+ if (this.wasmModule._clearBasicBreakpointHit) {
1583
+ this.wasmModule._clearBasicBreakpointHit();
1584
+ }
1585
+
1586
+ this.updateGutter();
1587
+ this.updateHighlighting();
1588
+ this._varAutoRefresh = true;
1589
+
1590
+ this.inputHandler.queueTextInput("RUN\r");
1591
+ }
1592
+ }
1593
+
1594
+ /**
1595
+ * Stop - Send Ctrl+C to the emulator to break the running program.
1596
+ */
1597
+ _clearBreakpointPulse() {
1598
+ const items = this.bpList?.querySelectorAll(".basic-dbg-bp-item.bp-triggered");
1599
+ if (items) items.forEach((el) => el.classList.remove("bp-triggered"));
1600
+ this._breakpointHitLine = false;
1601
+ }
1602
+
1603
+ handleStop() {
1604
+ if (!this.isRunningCallback || !this.isRunningCallback()) {
1605
+ return;
1606
+ }
1607
+
1608
+ this._clearBreakpointPulse();
1609
+ // Unpause if paused so the keystroke is processed
1610
+ if (this.wasmModule._isPaused()) {
1611
+ this.wasmModule._setPaused(false);
1612
+ }
1613
+
1614
+ // Send Ctrl+C (ASCII 3) to the emulator
1615
+ this.wasmModule._keyDown(3);
1616
+ }
1617
+
1618
+ /**
1619
+ * Pause - Pause the emulator, then step to the next BASIC line so
1620
+ * the program stops cleanly at a line boundary with highlighting.
1621
+ */
1622
+ handlePause() {
1623
+ if (!this.isRunningCallback || !this.isRunningCallback()) {
1624
+ return;
1625
+ }
1626
+
1627
+ // Stop auto-refreshing variables (breakpoint hit will do a final render)
1628
+ this._clearBreakpointPulse();
1629
+ this._varAutoRefresh = false;
1630
+
1631
+ // Pause first so the step machinery can read consistent state
1632
+ this.wasmModule._setPaused(true);
1633
+
1634
+ // Reset tracking flag so we detect the next breakpoint hit
1635
+ this._lastBasicBreakpointHit = false;
1636
+
1637
+ // Now step to the next BASIC statement - unpauses and runs until
1638
+ // PC hits $D820 (JSR EXECUTE_STATEMENT), the start of the next statement
1639
+ if (this.wasmModule._stepBasicStatement) {
1640
+ this.wasmModule._stepBasicStatement();
1641
+ }
1642
+ }
1643
+
1644
+ /**
1645
+ * Step - Execute current BASIC statement and stop at next statement.
1646
+ * Uses C++ stepBasicStatement() which breaks at $D820 (JSR EXECUTE_STATEMENT),
1647
+ * the ROM point where both new-line and colon paths converge.
1648
+ */
1649
+ handleStepLine() {
1650
+ // Check if BASIC is actually running (not in direct mode)
1651
+ const state = this.programParser.getExecutionState();
1652
+ if (state.currentLine === null) {
1653
+ // BASIC not running - clear any stale state
1654
+ // Unpause first, then clear to remove any skip logic
1655
+ this.wasmModule._setPaused(false);
1656
+ if (this.wasmModule._clearBasicBreakpointHit) {
1657
+ this.wasmModule._clearBasicBreakpointHit();
1658
+ }
1659
+ return;
1660
+ }
1661
+
1662
+ // Reset tracking flag so we detect the next breakpoint hit
1663
+ this._clearBreakpointPulse();
1664
+ this._lastBasicBreakpointHit = false;
1665
+
1666
+ // Use C++ statement stepping - breaks at ROM's EXECUTE_STATEMENT entry
1667
+ if (this.wasmModule._stepBasicStatement) {
1668
+ this.wasmModule._stepBasicStatement();
1669
+ } else {
1670
+ console.error("_stepBasicStatement not available - rebuild WASM");
1671
+ }
1672
+ }
1673
+
1674
+ addBreakpointFromInput() {
1675
+ const value = this.bpInput.value.trim();
1676
+ if (!value) return;
1677
+
1678
+ const lineNumber = parseInt(value, 10);
1679
+ if (isNaN(lineNumber) || lineNumber < 0 || lineNumber > 63999) {
1680
+ this.bpInput.classList.add("error");
1681
+ setTimeout(() => this.bpInput.classList.remove("error"), 500);
1682
+ return;
1683
+ }
1684
+
1685
+ this.breakpointManager.add(lineNumber, -1);
1686
+ this.bpInput.value = "";
1687
+ this.updateGutter();
1688
+ this.updateHighlighting();
1689
+ this.renderBreakpointList();
1690
+ }
1691
+
1692
+ /**
1693
+ * Build a structural fingerprint of the current variable set.
1694
+ * When this changes, a full DOM rebuild is needed; otherwise values update in-place.
1695
+ */
1696
+ _getVarStructureKey(variables, arrays) {
1697
+ let key = "";
1698
+ for (const v of variables) key += `${v.name}:${v.type};`;
1699
+ key += "|";
1700
+ for (const a of arrays) key += `${a.name}:${a.type}:${a.dimensions.join(",")};`;
1701
+ return key;
1702
+ }
1703
+
1704
+ renderVariables() {
1705
+ const now = Date.now();
1706
+ const fadeTime = 1000;
1707
+
1708
+ const variables = this.variableInspector.getSimpleVariables();
1709
+ const arrays = this.variableInspector.getArrayVariables();
1710
+
1711
+ if (variables.length === 0 && arrays.length === 0) {
1712
+ if (this._varStructureKey !== "empty") {
1713
+ this._varStructureKey = "empty";
1714
+ this.varPanel.innerHTML =
1715
+ '<div class="basic-dbg-empty">No variables</div>';
1716
+ }
1717
+ return;
1718
+ }
1719
+
1720
+ const structureKey = this._getVarStructureKey(variables, arrays);
1721
+ const structureChanged = structureKey !== this._varStructureKey;
1722
+
1723
+ if (structureChanged) {
1724
+ this._varStructureKey = structureKey;
1725
+ this._fullRenderVariables(variables, arrays);
1726
+ return;
1727
+ }
1728
+
1729
+ // In-place value update — no DOM rebuild
1730
+ this._updateVariableValues(variables, arrays);
1731
+ }
1732
+
1733
+ _fullRenderVariables(variables, arrays) {
1734
+ let html = "";
1735
+
1736
+ if (variables.length > 0) {
1737
+ html += '<div class="basic-dbg-var-list">';
1738
+ for (const v of variables) {
1739
+ const displayValue = this.variableInspector.formatValue(v);
1740
+ this.previousVariables.set(v.name, displayValue);
1741
+
1742
+ const typeClass = `var-type-${v.type}`;
1743
+ const isPaused = this.wasmModule._isPaused && this.wasmModule._isPaused();
1744
+ const editableClass = isPaused ? "editable" : "";
1745
+ html += `
1746
+ <div class="basic-dbg-var-row ${typeClass}" data-var-addr="${v.addr}" data-var-type="${v.type}">
1747
+ <span class="basic-dbg-var-name">${v.name}</span>
1748
+ <span class="basic-dbg-var-value ${editableClass}">${escapeHtml(displayValue)}</span>
1749
+ </div>
1750
+ `;
1751
+ }
1752
+ html += "</div>";
1753
+ }
1754
+
1755
+ if (arrays.length > 0) {
1756
+ html += '<div class="basic-dbg-arr-list">';
1757
+ for (const arr of arrays) {
1758
+ const dimsStr = arr.dimensions.map((d) => d - 1).join(",");
1759
+ const isExpanded = this.expandedArrays.has(arr.name);
1760
+
1761
+ html += `
1762
+ <div class="basic-dbg-arr-item ${isExpanded ? "expanded" : ""}" data-arr-addr="${arr.addr}" data-arr-type="${arr.type}" data-arr-numdims="${arr.numDims}">
1763
+ <div class="basic-dbg-arr-header" data-arr-name="${arr.name}">
1764
+ <span class="basic-dbg-arr-toggle">${isExpanded ? "▼" : "▶"}</span>
1765
+ <span class="basic-dbg-arr-name">${arr.name}(${dimsStr})</span>
1766
+ <span class="basic-dbg-arr-info">${arr.totalElements} el</span>
1767
+ </div>
1768
+ <div class="basic-dbg-arr-body" style="display: ${isExpanded ? "block" : "none"}">
1769
+ ${this.renderArrayContents(arr)}
1770
+ </div>
1771
+ </div>
1772
+ `;
1773
+ }
1774
+ html += "</div>";
1775
+ }
1776
+
1777
+ this.varPanel.innerHTML = html;
1778
+ }
1779
+
1780
+ _updateVariableValues(variables, arrays) {
1781
+ const isPaused = this.wasmModule._isPaused && this.wasmModule._isPaused();
1782
+ // Update simple variable values in-place
1783
+ const rows = this.varPanel.querySelectorAll(".basic-dbg-var-row");
1784
+ for (let i = 0; i < variables.length && i < rows.length; i++) {
1785
+ const v = variables[i];
1786
+ const row = rows[i];
1787
+ const key = v.name;
1788
+ const prevValue = this.previousVariables.get(key);
1789
+ const displayValue = this.variableInspector.formatValue(v);
1790
+ const changed = prevValue !== undefined && prevValue !== displayValue;
1791
+ this.previousVariables.set(key, displayValue);
1792
+
1793
+ // Update value text
1794
+ const valueSpan = row.querySelector(".basic-dbg-var-value");
1795
+ if (valueSpan && !valueSpan.querySelector("input")) {
1796
+ valueSpan.textContent = displayValue;
1797
+ }
1798
+
1799
+ // Flash on change — restart animation by removing and re-adding class
1800
+ if (changed) {
1801
+ row.classList.remove("changed");
1802
+ void row.offsetWidth;
1803
+ row.classList.add("changed");
1804
+ }
1805
+
1806
+ // Update editable state based on pause status
1807
+ if (valueSpan) {
1808
+ if (isPaused) {
1809
+ valueSpan.classList.add("editable");
1810
+ } else {
1811
+ valueSpan.classList.remove("editable");
1812
+ }
1813
+ }
1814
+
1815
+ // Update address in case memory shifted
1816
+ row.dataset.varAddr = v.addr;
1817
+ }
1818
+
1819
+ // Update expanded array values in-place
1820
+ const arrItems = this.varPanel.querySelectorAll(".basic-dbg-arr-item");
1821
+ for (let i = 0; i < arrays.length && i < arrItems.length; i++) {
1822
+ const arr = arrays[i];
1823
+
1824
+ // Update array address data in case memory shifted
1825
+ arrItems[i].dataset.arrAddr = arr.addr;
1826
+
1827
+ if (!this.expandedArrays.has(arr.name)) continue;
1828
+
1829
+ const valEls = arrItems[i].querySelectorAll(".basic-dbg-arr-val");
1830
+ for (let j = 0; j < arr.values.length && j < valEls.length; j++) {
1831
+ const formatted = this.variableInspector.formatValue({ type: arr.type, value: arr.values[j] });
1832
+ if (valEls[j].textContent !== formatted && !valEls[j].querySelector("input")) {
1833
+ valEls[j].textContent = formatted;
1834
+ // Flash on change
1835
+ valEls[j].classList.remove("changed");
1836
+ void valEls[j].offsetWidth;
1837
+ valEls[j].classList.add("changed");
1838
+ }
1839
+ // Update editable state
1840
+ if (isPaused) {
1841
+ valEls[j].classList.add("editable");
1842
+ } else {
1843
+ valEls[j].classList.remove("editable");
1844
+ }
1845
+ }
1846
+ }
1847
+ }
1848
+
1849
+ /**
1850
+ * Start inline editing of a variable value
1851
+ */
1852
+ _startVariableEdit(valueSpan) {
1853
+ if (valueSpan.querySelector("input")) return; // already editing
1854
+
1855
+ const row = valueSpan.closest(".basic-dbg-var-row");
1856
+ const addr = parseInt(row.dataset.varAddr, 10);
1857
+ const type = row.dataset.varType;
1858
+ const currentText = valueSpan.textContent;
1859
+
1860
+ const input = document.createElement("input");
1861
+ input.type = "text";
1862
+ input.className = "basic-dbg-var-edit";
1863
+ input.value = currentText;
1864
+
1865
+ valueSpan.textContent = "";
1866
+ valueSpan.appendChild(input);
1867
+ input.focus();
1868
+ input.select();
1869
+
1870
+ const commit = () => {
1871
+ const newVal = input.value.trim();
1872
+ if (newVal !== currentText) {
1873
+ const varInfo = { addr, type };
1874
+ this.variableInspector.setVariableValue(varInfo, newVal);
1875
+ }
1876
+ // Force full rebuild to remove the input element from the DOM
1877
+ this._varStructureKey = null;
1878
+ this.renderVariables();
1879
+ };
1880
+
1881
+ input.addEventListener("keydown", (e) => {
1882
+ if (e.key === "Enter") { e.preventDefault(); commit(); }
1883
+ if (e.key === "Escape") { e.preventDefault(); this._varStructureKey = null; this.renderVariables(); }
1884
+ e.stopPropagation();
1885
+ });
1886
+ input.addEventListener("blur", commit);
1887
+ }
1888
+
1889
+ /**
1890
+ * Start inline editing of an array element value
1891
+ */
1892
+ _startArrayElementEdit(valEl) {
1893
+ if (valEl.querySelector("input")) return;
1894
+
1895
+ const arrItem = valEl.closest(".basic-dbg-arr-item");
1896
+ if (!arrItem) return;
1897
+
1898
+ const addr = parseInt(arrItem.dataset.arrAddr, 10);
1899
+ const type = arrItem.dataset.arrType;
1900
+ const numDims = parseInt(arrItem.dataset.arrNumdims, 10);
1901
+ const elementIndex = parseInt(valEl.dataset.elemIdx, 10);
1902
+ const currentText = valEl.textContent;
1903
+
1904
+ const input = document.createElement("input");
1905
+ input.type = "text";
1906
+ input.className = "basic-dbg-var-edit";
1907
+ input.value = currentText;
1908
+
1909
+ valEl.textContent = "";
1910
+ valEl.appendChild(input);
1911
+ input.focus();
1912
+ input.select();
1913
+
1914
+ const commit = () => {
1915
+ const newVal = input.value.trim();
1916
+ if (newVal !== currentText) {
1917
+ this.variableInspector.setArrayElementValue(
1918
+ { addr, type, numDims, elementIndex },
1919
+ newVal,
1920
+ );
1921
+ }
1922
+ // Force full rebuild to reflect changes
1923
+ this._varStructureKey = null;
1924
+ this.renderVariables();
1925
+ };
1926
+
1927
+ input.addEventListener("keydown", (e) => {
1928
+ if (e.key === "Enter") { e.preventDefault(); commit(); }
1929
+ if (e.key === "Escape") { e.preventDefault(); this._varStructureKey = null; this.renderVariables(); }
1930
+ e.stopPropagation();
1931
+ });
1932
+ input.addEventListener("blur", commit);
1933
+ }
1934
+
1935
+ /**
1936
+ * Render array contents as a grid or table
1937
+ */
1938
+ renderArrayContents(arr) {
1939
+ const dims = arr.dimensions;
1940
+ const values = arr.values;
1941
+ const type = arr.type;
1942
+ const isPaused = this.wasmModule._isPaused && this.wasmModule._isPaused();
1943
+ const editClass = isPaused ? " editable" : "";
1944
+
1945
+ // Format a single value using the same logic as simple variables
1946
+ const formatVal = (v) => {
1947
+ return escapeHtml(this.variableInspector.formatValue({ type, value: v }));
1948
+ };
1949
+
1950
+ // 1D array - simple indexed list
1951
+ if (dims.length === 1) {
1952
+ let html = '<div class="basic-dbg-arr-contents basic-dbg-arr-1d">';
1953
+ for (let i = 0; i < values.length; i++) {
1954
+ html += `<div class="basic-dbg-arr-cell">
1955
+ <span class="basic-dbg-arr-idx">${i}</span>
1956
+ <span class="basic-dbg-arr-val${editClass}" data-elem-idx="${i}">${formatVal(values[i])}</span>
1957
+ </div>`;
1958
+ }
1959
+ html += "</div>";
1960
+ return html;
1961
+ }
1962
+
1963
+ // 2D array - table view
1964
+ if (dims.length === 2) {
1965
+ const rows = dims[0];
1966
+ const cols = dims[1];
1967
+ let html = '<div class="basic-dbg-arr-contents basic-dbg-arr-2d">';
1968
+ html += '<table class="basic-dbg-arr-table"><thead><tr><th></th>';
1969
+ for (let c = 0; c < cols; c++) {
1970
+ html += `<th>${c}</th>`;
1971
+ }
1972
+ html += "</tr></thead><tbody>";
1973
+ for (let r = 0; r < rows; r++) {
1974
+ html += `<tr><th>${r}</th>`;
1975
+ for (let c = 0; c < cols; c++) {
1976
+ const idx = r * cols + c;
1977
+ const val = idx < values.length ? formatVal(values[idx]) : "?";
1978
+ html += `<td class="basic-dbg-arr-val${editClass}" data-elem-idx="${idx}">${val}</td>`;
1979
+ }
1980
+ html += "</tr>";
1981
+ }
1982
+ html += "</tbody></table></div>";
1983
+ return html;
1984
+ }
1985
+
1986
+ // 3D+ array - indexed list with full indices
1987
+ let html = '<div class="basic-dbg-arr-contents basic-dbg-arr-nd">';
1988
+ for (let i = 0; i < values.length; i++) {
1989
+ // Calculate multi-dimensional index
1990
+ const indices = [];
1991
+ let remaining = i;
1992
+ for (let d = dims.length - 1; d >= 0; d--) {
1993
+ indices.unshift(remaining % dims[d]);
1994
+ remaining = Math.floor(remaining / dims[d]);
1995
+ }
1996
+ const idxStr = indices.join(",");
1997
+ html += `<div class="basic-dbg-arr-cell">
1998
+ <span class="basic-dbg-arr-idx">(${idxStr})</span>
1999
+ <span class="basic-dbg-arr-val${editClass}" data-elem-idx="${i}">${formatVal(values[i])}</span>
2000
+ </div>`;
2001
+ }
2002
+ html += "</div>";
2003
+ return html;
2004
+ }
2005
+
2006
+ /**
2007
+ * Add a condition-only rule via the Rule Builder
2008
+ */
2009
+ addConditionRule() {
2010
+ if (this.ruleBuilder) {
2011
+ // Create a temporary entry, open rule builder, on apply create the real breakpoint
2012
+ const tempEntry = { condition: null, conditionRules: null };
2013
+ this.ruleBuilder.editBasicBreakpoint("__new_rule__", tempEntry, "Condition Rule");
2014
+ }
2015
+ }
2016
+
2017
+ /**
2018
+ * Get the statement index for the current break position
2019
+ */
2020
+ _getBreakStatementIndex(breakLine) {
2021
+ if (this.wasmModule._getBasicTxtptr) {
2022
+ const txtptr = this.wasmModule._getBasicTxtptr();
2023
+ const info = this.programParser.getCurrentStatementInfo(breakLine, txtptr);
2024
+ if (info) return info.statementIndex;
2025
+ }
2026
+ return -1;
2027
+ }
2028
+
2029
+ /**
2030
+ * Set the Rule Builder window reference
2031
+ */
2032
+ setRuleBuilder(ruleBuilderWindow) {
2033
+ this.ruleBuilder = ruleBuilderWindow;
2034
+ }
2035
+
2036
+ /**
2037
+ * Edit condition on a BASIC breakpoint via Rule Builder
2038
+ */
2039
+ editBreakpointCondition(lineNumber, statementIndex) {
2040
+ const entry = this.breakpointManager.get(lineNumber, statementIndex);
2041
+ if (!entry) return;
2042
+
2043
+ if (this.ruleBuilder) {
2044
+ const key = `${lineNumber}:${statementIndex}`;
2045
+ const label = statementIndex >= 0
2046
+ ? `Line ${lineNumber} : ${statementIndex}`
2047
+ : `Line ${lineNumber}`;
2048
+ this.ruleBuilder.editBasicBreakpoint(key, entry, label);
2049
+ } else {
2050
+ const condition = prompt(
2051
+ `Condition for breakpoint at Line ${lineNumber}:\n` +
2052
+ `Examples: BV(73,0)==50, PEEK($00)==#$42`,
2053
+ entry.condition || "",
2054
+ );
2055
+ if (condition !== null) {
2056
+ this.breakpointManager.setCondition(lineNumber, statementIndex, condition);
2057
+ this.renderBreakpointList();
2058
+ }
2059
+ }
2060
+ }
2061
+
2062
+ renderBreakpointList() {
2063
+ const entries = this.breakpointManager.getAllEntries();
2064
+
2065
+ if (entries.length === 0) {
2066
+ this.bpList.innerHTML =
2067
+ '<div class="basic-dbg-bp-empty">Click a line number in the gutter to toggle a breakpoint, or add one above.</div>';
2068
+ return;
2069
+ }
2070
+
2071
+ let html = "";
2072
+ for (const entry of entries) {
2073
+ const enabledClass = entry.enabled ? "" : "disabled";
2074
+ let label;
2075
+ if (entry.lineNumber === -1) {
2076
+ // Condition-only rule - show human-readable label from rule tree
2077
+ label = entry.conditionRules
2078
+ ? RuleBuilderWindow.toDisplayLabel(entry.conditionRules)
2079
+ : (entry.condition || "Rule");
2080
+ } else if (entry.statementIndex >= 0) {
2081
+ label = `Line ${entry.lineNumber} : ${entry.statementIndex}`;
2082
+ } else {
2083
+ label = `Line ${entry.lineNumber}`;
2084
+ }
2085
+
2086
+ let condBadge = "";
2087
+ if (entry.condition && entry.lineNumber >= 0) {
2088
+ const condLabel = entry.conditionRules
2089
+ ? RuleBuilderWindow.toDisplayLabel(entry.conditionRules)
2090
+ : entry.condition;
2091
+ condBadge = `<span class="basic-dbg-bp-cond" title="${condLabel}">if</span>`;
2092
+ }
2093
+
2094
+ const isRule = entry.lineNumber === -1;
2095
+
2096
+ html += `
2097
+ <div class="basic-dbg-bp-item ${enabledClass} ${isRule ? "condition-rule" : ""}" data-line="${entry.lineNumber}" data-stmt="${entry.statementIndex}">
2098
+ <input type="checkbox" class="basic-dbg-bp-enabled"
2099
+ ${entry.enabled ? "checked" : ""} data-line="${entry.lineNumber}" data-stmt="${entry.statementIndex}">
2100
+ ${isRule ? '<span class="basic-dbg-bp-rule-badge">if</span>' : ""}
2101
+ <span class="basic-dbg-bp-line ${isRule ? "basic-dbg-bp-rule-expr" : ""}" title="${isRule && entry.condition ? entry.condition : ""}">${label}</span>
2102
+ ${condBadge}
2103
+ <button class="basic-dbg-bp-edit" data-line="${entry.lineNumber}" data-stmt="${entry.statementIndex}" title="Edit condition">if\u2026</button>
2104
+ <button class="basic-dbg-bp-remove" data-line="${entry.lineNumber}" data-stmt="${entry.statementIndex}" title="Remove">×</button>
2105
+ </div>
2106
+ `;
2107
+ }
2108
+
2109
+ this.bpList.innerHTML = html;
2110
+
2111
+ this.bpList.querySelectorAll(".basic-dbg-bp-enabled").forEach((cb) => {
2112
+ cb.addEventListener("change", () => {
2113
+ const lineNum = parseInt(cb.dataset.line, 10);
2114
+ const stmtIdx = parseInt(cb.dataset.stmt, 10);
2115
+ this.breakpointManager.setEnabled(lineNum, stmtIdx, cb.checked);
2116
+ this.updateGutter();
2117
+ this.updateHighlighting();
2118
+ });
2119
+ });
2120
+
2121
+ this.bpList.querySelectorAll(".basic-dbg-bp-remove").forEach((btn) => {
2122
+ btn.addEventListener("click", () => {
2123
+ const lineNum = parseInt(btn.dataset.line, 10);
2124
+ const stmtIdx = parseInt(btn.dataset.stmt, 10);
2125
+ this.breakpointManager.remove(lineNum, stmtIdx);
2126
+ this.updateGutter();
2127
+ this.updateHighlighting();
2128
+ this.renderBreakpointList();
2129
+ });
2130
+ });
2131
+
2132
+ this.bpList.querySelectorAll(".basic-dbg-bp-edit").forEach((btn) => {
2133
+ btn.addEventListener("click", () => {
2134
+ const lineNum = parseInt(btn.dataset.line, 10);
2135
+ const stmtIdx = parseInt(btn.dataset.stmt, 10);
2136
+ this.editBreakpointCondition(lineNum, stmtIdx);
2137
+ });
2138
+ });
2139
+ }
2140
+
2141
+ // ========================================
2142
+ // Update Loop
2143
+ // ========================================
2144
+
2145
+ update(wasmModule) {
2146
+ if (!this.isVisible) return;
2147
+
2148
+ const now = Date.now();
2149
+ const state = this.programParser.getExecutionState();
2150
+ const isPaused = wasmModule._isPaused();
2151
+
2152
+ // Track BASIC program running state from C++ ROM hooks ($D912=RUN, $D43C=RESTART)
2153
+ const isEditing = this.varPanel && this.varPanel.querySelector(".basic-dbg-var-edit");
2154
+ const programRunning = this.wasmModule._isBasicProgramRunning
2155
+ ? this.wasmModule._isBasicProgramRunning()
2156
+ : false;
2157
+
2158
+ const emulatorOn = this.isRunningCallback ? this.isRunningCallback() : false;
2159
+
2160
+ // Update program status indicator
2161
+ if (isPaused && programRunning) {
2162
+ this.setStatus("paused");
2163
+ } else if (programRunning) {
2164
+ this.setStatus("running");
2165
+ } else {
2166
+ this.setStatus("idle");
2167
+ }
2168
+
2169
+ // Auto-start refresh when program starts running (even from emulator directly)
2170
+ if (programRunning && !this._varAutoRefresh && !isPaused) {
2171
+ this._varAutoRefresh = true;
2172
+ }
2173
+
2174
+ // Auto-stop refresh when program ends or pauses
2175
+ if (this._varAutoRefresh && (!programRunning || isPaused)) {
2176
+ this._varAutoRefresh = false;
2177
+ this.renderVariables();
2178
+ }
2179
+
2180
+ // Refresh variables at 30fps while auto-refresh is active
2181
+ if (this._varAutoRefresh) {
2182
+ if (!this._lastVarUpdateTime || now - this._lastVarUpdateTime >= 33) {
2183
+ this._lastVarUpdateTime = now;
2184
+ this.renderVariables();
2185
+ }
2186
+ }
2187
+
2188
+ // Throttle other updates to 30fps (33ms) to reduce CPU load
2189
+ if (this._lastUpdateTime && now - this._lastUpdateTime < 33) {
2190
+ return;
2191
+ }
2192
+ this._lastUpdateTime = now;
2193
+
2194
+ // Check for BASIC breakpoint hit (breakpoint or step completion)
2195
+ // Track state transition to detect NEW breakpoint hits (not just being paused at one)
2196
+ const basicBreakpointHit = isPaused && wasmModule._isBasicBreakpointHit();
2197
+ if (basicBreakpointHit && !this._lastBasicBreakpointHit) {
2198
+ const breakLine = wasmModule._getBasicBreakLine();
2199
+
2200
+ // Check if this was a line/statement breakpoint match
2201
+ const stmtIdx = this._getBreakStatementIndex(breakLine);
2202
+ const bpEntry = this.breakpointManager.get(breakLine, stmtIdx)
2203
+ || this.breakpointManager.get(breakLine, -1);
2204
+
2205
+ if (bpEntry) {
2206
+ // Line breakpoint matched - evaluate its condition if it has one
2207
+ if (bpEntry.condition) {
2208
+ if (!this.breakpointManager.shouldBreak(bpEntry.lineNumber, bpEntry.statementIndex)) {
2209
+ wasmModule._setPaused(false);
2210
+ if (wasmModule._clearBasicBreakpointHit) wasmModule._clearBasicBreakpointHit();
2211
+ this._lastBasicBreakpointHit = false;
2212
+ return;
2213
+ }
2214
+ }
2215
+ }
2216
+ // Condition-only rules are evaluated in C++ - no JS evaluation needed
2217
+
2218
+ this.currentLineNumber = breakLine;
2219
+ this._breakpointHitLine = true;
2220
+ // Get statement info using TXTPTR from execution state
2221
+ if (wasmModule._getBasicTxtptr) {
2222
+ const txtptr = wasmModule._getBasicTxtptr();
2223
+ this.currentStatementInfo = this.programParser.getCurrentStatementInfo(breakLine, txtptr);
2224
+ } else {
2225
+ this.currentStatementInfo = null;
2226
+ }
2227
+
2228
+ this.updateGutter();
2229
+ this.updateHighlighting();
2230
+ this.renderVariables();
2231
+
2232
+ // Pulse the triggered breakpoint item
2233
+ this._clearBreakpointPulse();
2234
+ const condRuleId = wasmModule._getBasicConditionRuleHitId
2235
+ ? wasmModule._getBasicConditionRuleHitId() : -1;
2236
+ if (condRuleId >= 0) {
2237
+ // Condition-only rule hit
2238
+ const el = this.bpList?.querySelector(
2239
+ `.basic-dbg-bp-item[data-line="-1"][data-stmt="${condRuleId}"]`);
2240
+ if (el) el.classList.add("bp-triggered");
2241
+ } else {
2242
+ // Line/statement breakpoint hit
2243
+ const stmtAttr = bpEntry ? bpEntry.statementIndex : -1;
2244
+ const el = this.bpList?.querySelector(
2245
+ `.basic-dbg-bp-item[data-line="${breakLine}"][data-stmt="${stmtAttr}"]`);
2246
+ if (el) el.classList.add("bp-triggered");
2247
+ }
2248
+ }
2249
+ this._lastBasicBreakpointHit = basicBreakpointHit;
2250
+
2251
+ // Reset breakpoint tracking when BASIC stops running (program ended)
2252
+ // so next RUN will properly detect the first breakpoint hit
2253
+ if (!state.running && this._lastProgramRunning) {
2254
+ // Check if program stopped due to a runtime error
2255
+ if (this.wasmModule._isBasicErrorHit && this.wasmModule._isBasicErrorHit()) {
2256
+ const errorLine = this.wasmModule._getBasicErrorLine();
2257
+ const errorTxtptr = this.wasmModule._getBasicErrorTxtptr();
2258
+ const errorCode = this.wasmModule._getBasicErrorCode();
2259
+ this._setError(errorLine, errorTxtptr, errorCode);
2260
+ this.wasmModule._clearBasicError();
2261
+ }
2262
+
2263
+ this._varAutoRefresh = false;
2264
+ this._lastBasicBreakpointHit = false;
2265
+ // First unpause, then clear all breakpoint state
2266
+ // This order ensures any skip logic from unpausing gets cleared
2267
+ this.wasmModule._setPaused(false);
2268
+ if (this.wasmModule._clearBasicBreakpointHit) {
2269
+ this.wasmModule._clearBasicBreakpointHit();
2270
+ }
2271
+ // Clear current line highlight when program ends
2272
+ if (this.currentLineNumber !== null) {
2273
+ this.currentLineNumber = null;
2274
+ this.currentStatementInfo = null;
2275
+ this.updateGutter();
2276
+ this.updateHighlighting();
2277
+ }
2278
+ }
2279
+ this._lastProgramRunning = state.running;
2280
+
2281
+ // Update line/ptr display (only show when paused, otherwise it flickers too fast)
2282
+ if (
2283
+ isPaused &&
2284
+ state.currentLine !== null
2285
+ ) {
2286
+ let lineText = `LINE: ${state.currentLine}`;
2287
+ if (this.currentStatementInfo && this.currentStatementInfo.statementCount > 1) {
2288
+ lineText += ` [${this.currentStatementInfo.statementIndex + 1}/${this.currentStatementInfo.statementCount}]`;
2289
+ }
2290
+ this.lineSpan.textContent = lineText;
2291
+ this.ptrSpan.textContent = `PTR: $${this.formatHex(state.txtptr, 4)}`;
2292
+ } else {
2293
+ this.lineSpan.textContent = "LINE: ---";
2294
+ this.ptrSpan.textContent = "PTR: $----";
2295
+ }
2296
+
2297
+ // Only update gutter/highlighting for current line when paused
2298
+ // This prevents constant rebuilding that interferes with click events
2299
+ if (isPaused) {
2300
+ // Update statement info even if line hasn't changed (statement may have changed)
2301
+ let stmtInfo = null;
2302
+ if (wasmModule._getBasicTxtptr && state.currentLine !== null) {
2303
+ const txtptr = wasmModule._getBasicTxtptr();
2304
+ stmtInfo = this.programParser.getCurrentStatementInfo(state.currentLine, txtptr);
2305
+ }
2306
+ const stmtChanged = this._statementInfoChanged(this.currentStatementInfo, stmtInfo);
2307
+
2308
+ if (state.currentLine !== this.currentLineNumber || stmtChanged) {
2309
+ this.currentLineNumber = state.currentLine;
2310
+ this.currentStatementInfo = stmtInfo;
2311
+ this.updateGutter();
2312
+ this.updateHighlighting();
2313
+ }
2314
+ } else if (programRunning) {
2315
+ // While running, track the current line via CURLIN ($75-$76)
2316
+ const curlinLo = this.wasmModule._peekMemory(0x75);
2317
+ const curlinHi = this.wasmModule._peekMemory(0x76);
2318
+ // $FF in high byte means direct/immediate mode - not in a program line
2319
+ if (curlinHi !== 0xFF) {
2320
+ const runningLine = curlinLo | (curlinHi << 8);
2321
+ const lineChanged = this.traceEnabled && runningLine !== this.currentLineNumber;
2322
+
2323
+ if (lineChanged) {
2324
+ this.currentLineNumber = runningLine;
2325
+ this.currentStatementInfo = null;
2326
+ this.updateHighlighting();
2327
+ this._scrollToCurrentLine();
2328
+ }
2329
+
2330
+ // Heat map: read from C++ counters
2331
+ if (this.heatMapEnabled) {
2332
+ this._readHeatMapFromWasm();
2333
+ }
2334
+
2335
+ // Update gutter styles in-place when line changed or heat map active
2336
+ if (lineChanged || this.heatMapEnabled) {
2337
+ this._updateGutterStyles();
2338
+ }
2339
+ }
2340
+ } else if (this.currentLineNumber !== null) {
2341
+ // Clear line highlighting when not running
2342
+ this.currentLineNumber = null;
2343
+ this.currentStatementInfo = null;
2344
+ this.updateHighlighting();
2345
+ }
2346
+ }
2347
+
2348
+ /**
2349
+ * Wrap statement segments in spans for multi-statement line highlighting.
2350
+ * Finds colon boundaries in the text content (ignoring HTML tags) and wraps
2351
+ * each statement. The active statement gets the basic-current-statement class.
2352
+ */
2353
+ /**
2354
+ * Wrap statement segments in spans for multi-statement line highlighting.
2355
+ * Splits the highlighted HTML at colon punctuation spans (<span class="bas-punct">:</span>)
2356
+ * which the syntax highlighter produces for statement-separating colons.
2357
+ * The colon span is included as the last element of the preceding statement segment.
2358
+ */
2359
+ /**
2360
+ * Split HTML at colon boundaries and return segments
2361
+ */
2362
+ _splitAtColons(html) {
2363
+ const colonPattern = /<span class="bas-punct">:<\/span>/g;
2364
+ const colonMatches = [];
2365
+ let match;
2366
+ while ((match = colonPattern.exec(html)) !== null) {
2367
+ colonMatches.push({ index: match.index, length: match[0].length });
2368
+ }
2369
+
2370
+ if (colonMatches.length === 0) {
2371
+ return [html];
2372
+ }
2373
+
2374
+ const segments = [];
2375
+ let pos = 0;
2376
+ for (let i = 0; i < colonMatches.length; i++) {
2377
+ const colonEnd = colonMatches[i].index + colonMatches[i].length;
2378
+ segments.push(html.substring(pos, colonEnd));
2379
+ pos = colonEnd;
2380
+ }
2381
+ if (pos < html.length) {
2382
+ segments.push(html.substring(pos));
2383
+ }
2384
+ return segments;
2385
+ }
2386
+
2387
+ _wrapStatements(html, stmtInfo, stmtBPs = []) {
2388
+ const segments = this._splitAtColons(html);
2389
+
2390
+ // Build a set of statement indices with breakpoints
2391
+ const bpStmtSet = new Set();
2392
+ for (const bp of stmtBPs) {
2393
+ if (bp.statementIndex >= 0) bpStmtSet.add(bp.statementIndex);
2394
+ }
2395
+
2396
+ let result = "";
2397
+ for (let i = 0; i < segments.length; i++) {
2398
+ let cls = "";
2399
+ if (i === stmtInfo.statementIndex) cls += " basic-current-statement";
2400
+ if (bpStmtSet.has(i)) cls += " basic-statement-bp";
2401
+ result += `<span class="basic-statement${cls}">${segments[i]}</span>`;
2402
+ }
2403
+ return result;
2404
+ }
2405
+
2406
+ /**
2407
+ * Wrap statements for breakpoint display on non-current lines.
2408
+ * Similar to _wrapStatements but without current-statement highlighting.
2409
+ */
2410
+ _wrapStatementsForBreakpoints(html, stmtBPs) {
2411
+ const segments = this._splitAtColons(html);
2412
+
2413
+ // If only 1 segment, no colons found - can't split into statements
2414
+ if (segments.length <= 1) return html;
2415
+
2416
+ const bpStmtSet = new Set();
2417
+ for (const bp of stmtBPs) {
2418
+ if (bp.statementIndex >= 0) bpStmtSet.add(bp.statementIndex);
2419
+ }
2420
+
2421
+ let result = "";
2422
+ for (let i = 0; i < segments.length; i++) {
2423
+ const cls = bpStmtSet.has(i) ? " basic-statement-bp" : "";
2424
+ result += `<span class="basic-statement${cls}">${segments[i]}</span>`;
2425
+ }
2426
+ return result;
2427
+ }
2428
+
2429
+ _statementInfoChanged(a, b) {
2430
+ if (a === b) return false;
2431
+ if (!a || !b) return true;
2432
+ return a.statementIndex !== b.statementIndex ||
2433
+ a.statementCount !== b.statementCount;
2434
+ }
2435
+
2436
+ _setError(lineNumber, txtptr, errorCode) {
2437
+ this.errorLineNumber = lineNumber;
2438
+ this.errorMessage = BASIC_ERRORS[errorCode] || `ERROR ${errorCode}`;
2439
+ this.errorStatementInfo = this.programParser.getCurrentStatementInfo(lineNumber, txtptr);
2440
+
2441
+ // Capture the code portion of the error line for tracking across renumbers
2442
+ this.errorLineContent = this._getLineContent(lineNumber);
2443
+
2444
+ this.setStatus("error");
2445
+ this.updateGutter();
2446
+ this.updateHighlighting();
2447
+ }
2448
+
2449
+ _clearError() {
2450
+ this.errorLineNumber = null;
2451
+ this.errorStatementInfo = null;
2452
+ this.errorMessage = null;
2453
+ this.errorLineContent = null;
2454
+ this.setStatus("idle");
2455
+ }
2456
+
2457
+ /**
2458
+ * Get the code portion (after the line number) for a given BASIC line number
2459
+ */
2460
+ _getLineContent(lineNumber) {
2461
+ if (!this.textarea) return null;
2462
+ const rawLines = this.textarea.value.split(/\r?\n/);
2463
+ for (const line of rawLines) {
2464
+ const match = line.trim().match(/^(\d+)\s*(.*)/);
2465
+ if (match && parseInt(match[1], 10) === lineNumber) {
2466
+ return match[2];
2467
+ }
2468
+ }
2469
+ return null;
2470
+ }
2471
+
2472
+ /**
2473
+ * Track the error line across edits. If the BASIC line number still exists,
2474
+ * keep it. If it was renumbered, find the line with matching code content
2475
+ * and follow it. If the code content itself was edited or removed, clear.
2476
+ */
2477
+ _trackErrorLine() {
2478
+ if (this.errorLineNumber === null) return;
2479
+
2480
+ const rawLines = this.textarea.value.split(/\r?\n/);
2481
+
2482
+ // Check if the original BASIC line number still exists with the same content
2483
+ for (const line of rawLines) {
2484
+ const match = line.trim().match(/^(\d+)\s*(.*)/);
2485
+ if (match && parseInt(match[1], 10) === this.errorLineNumber) {
2486
+ const content = match[2];
2487
+ if (content === this.errorLineContent) {
2488
+ return; // Line still exists with same content, keep error
2489
+ }
2490
+ // Line number exists but content changed — error no longer applies
2491
+ this._clearError();
2492
+ return;
2493
+ }
2494
+ }
2495
+
2496
+ // Line number not found — check if it was renumbered (same content, different number)
2497
+ if (this.errorLineContent !== null) {
2498
+ for (const line of rawLines) {
2499
+ const match = line.trim().match(/^(\d+)\s*(.*)/);
2500
+ if (match && match[2] === this.errorLineContent) {
2501
+ // Found the same code under a new line number
2502
+ this.errorLineNumber = parseInt(match[1], 10);
2503
+ return;
2504
+ }
2505
+ }
2506
+ }
2507
+
2508
+ // Line was removed entirely
2509
+ this._clearError();
2510
+ }
2511
+
2512
+ _wrapStatementsForError(html, stmtInfo, stmtBPs = []) {
2513
+ const segments = this._splitAtColons(html);
2514
+
2515
+ const bpStmtSet = new Set();
2516
+ for (const bp of stmtBPs) {
2517
+ if (bp.statementIndex >= 0) bpStmtSet.add(bp.statementIndex);
2518
+ }
2519
+
2520
+ let result = "";
2521
+ for (let i = 0; i < segments.length; i++) {
2522
+ let cls = "";
2523
+ if (i === stmtInfo.statementIndex) cls += " basic-error-statement";
2524
+ if (bpStmtSet.has(i)) cls += " basic-statement-bp";
2525
+ result += `<span class="basic-statement${cls}">${segments[i]}</span>`;
2526
+ }
2527
+ return result;
2528
+ }
2529
+
2530
+ setStatus(status) {
2531
+ if (!this.statusChip || this._currentStatus === status) return;
2532
+ this._currentStatus = status;
2533
+ this.statusChip.className = "basic-dbg-status-chip";
2534
+ this.statusChip.classList.add(`basic-dbg-status-${status}`);
2535
+ const labels = { idle: "Idle", running: "Running", paused: "Paused", error: "Error" };
2536
+ this.statusChip.textContent = labels[status] || status;
2537
+ const statusBar = this.contentElement.querySelector(".basic-dbg-status-bar");
2538
+ if (statusBar) statusBar.dataset.state = status;
2539
+ }
2540
+
2541
+ /**
2542
+ * Get the breakpoint manager for external access
2543
+ */
2544
+ getBreakpointManager() {
2545
+ return this.breakpointManager;
2546
+ }
2547
+
2548
+ // ========================================
2549
+ // State Persistence
2550
+ // ========================================
2551
+
2552
+ getState() {
2553
+ const baseState = super.getState();
2554
+ return {
2555
+ ...baseState,
2556
+ content: this.textarea ? this.textarea.value : "",
2557
+ sidebarWidth: this.sidebar ? this.sidebar.offsetWidth : 200,
2558
+ breakpointsHeight: this.bpSection ? this.bpSection.offsetHeight : 150,
2559
+ heatMapEnabled: this.heatMapEnabled,
2560
+ traceEnabled: this.traceEnabled,
2561
+ };
2562
+ }
2563
+
2564
+ restoreState(state) {
2565
+ super.restoreState(state);
2566
+ if (state.content !== undefined && this.textarea) {
2567
+ this.textarea.value = state.content;
2568
+ this.updateGutter();
2569
+ this.updateHighlighting();
2570
+ this.updateStats();
2571
+ }
2572
+ if (state.sidebarWidth && this.sidebar) {
2573
+ this.sidebar.style.width = `${state.sidebarWidth}px`;
2574
+ }
2575
+ if (state.breakpointsHeight && this.bpSection) {
2576
+ this.bpSection.style.height = `${state.breakpointsHeight}px`;
2577
+ }
2578
+ if (state.traceEnabled !== undefined) {
2579
+ this.traceEnabled = state.traceEnabled;
2580
+ if (this.traceCheckbox) this.traceCheckbox.checked = state.traceEnabled;
2581
+ }
2582
+ if (state.heatMapEnabled !== undefined) {
2583
+ this.heatMapEnabled = state.heatMapEnabled;
2584
+ if (this.heatCheckbox) this.heatCheckbox.checked = state.heatMapEnabled;
2585
+ this.wasmModule._setBasicHeatMapEnabled(this.heatMapEnabled);
2586
+ }
2587
+ }
2588
+
2589
+ destroy() {
2590
+ if (this._cleanupSplitter) this._cleanupSplitter();
2591
+ if (this._cleanupSidebarSplitter) this._cleanupSidebarSplitter();
2592
+ super.destroy();
2593
+ }
2594
+ }