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.
- package/.clangd +5 -0
- package/.mcp.json +12 -0
- package/CLAUDE.md +362 -0
- package/CMakeLists.txt +774 -0
- package/LICENSE +21 -0
- package/README.md +392 -0
- package/build-wasm/generated/roms.cpp +2447 -0
- package/docker-compose.staging.yml +9 -0
- package/docs/basic-rom-disassembly.md +6663 -0
- package/docs/softswitch-comparison.md +273 -0
- package/docs/thunderclock-debug.md +89 -0
- package/examples/cube.bas +72 -0
- package/examples/hello.s +55 -0
- package/examples/scroll.s +140 -0
- package/package.json +18 -0
- package/public/assets/apple-logo-old.png +0 -0
- package/public/assets/apple-logo.png +0 -0
- package/public/assets/drive-closed-light-on.png +0 -0
- package/public/assets/drive-closed.png +0 -0
- package/public/assets/drive-open-light-on.png +0 -0
- package/public/assets/drive-open.png +0 -0
- package/public/audio-worklet.js +82 -0
- package/public/disks/Apple DOS 3.3 January 1983.dsk +0 -0
- package/public/disks/ProDOS 2.4.3.po +0 -0
- package/public/disks/h32mb.2mg +0 -0
- package/public/disks/library.json +26 -0
- package/public/docs/llms/llm-assembler.txt +90 -0
- package/public/docs/llms/llm-basic-program.txt +256 -0
- package/public/docs/llms/llm-disk-drives.txt +72 -0
- package/public/docs/llms/llm-file-explorer.txt +50 -0
- package/public/docs/llms/llm-hard-drives.txt +80 -0
- package/public/docs/llms/llm-main.txt +51 -0
- package/public/docs/llms/llm-slot-configuration.txt +66 -0
- package/public/icons/icon-192.svg +4 -0
- package/public/icons/icon-512.svg +4 -0
- package/public/index.html +661 -0
- package/public/llms.txt +49 -0
- package/public/manifest.json +29 -0
- package/public/shaders/burnin.glsl +22 -0
- package/public/shaders/crt.glsl +706 -0
- package/public/shaders/edge.glsl +109 -0
- package/public/shaders/vertex.glsl +8 -0
- package/public/sw.js +186 -0
- package/roms/341-0027.bin +0 -0
- package/roms/341-0160-A-US-UK.bin +0 -0
- package/roms/341-0160-A.bin +0 -0
- package/roms/342-0273-A-US-UK.bin +0 -0
- package/roms/342-0349-B-C0-FF.bin +0 -0
- package/roms/Apple Mouse Interface Card ROM - 342-0270-C.bin +0 -0
- package/roms/Thunderclock Plus ROM.bin +0 -0
- package/scripts/generate_roms.sh +69 -0
- package/src/bindings/wasm_interface.cpp +1940 -0
- package/src/core/assembler/assembler.cpp +1239 -0
- package/src/core/assembler/assembler.hpp +115 -0
- package/src/core/audio/audio.cpp +160 -0
- package/src/core/audio/audio.hpp +81 -0
- package/src/core/basic/basic_detokenizer.cpp +436 -0
- package/src/core/basic/basic_detokenizer.hpp +41 -0
- package/src/core/basic/basic_tokenizer.cpp +286 -0
- package/src/core/basic/basic_tokenizer.hpp +26 -0
- package/src/core/basic/basic_tokens.hpp +295 -0
- package/src/core/cards/disk2_card.cpp +568 -0
- package/src/core/cards/disk2_card.hpp +316 -0
- package/src/core/cards/expansion_card.hpp +185 -0
- package/src/core/cards/mockingboard/ay8910.cpp +616 -0
- package/src/core/cards/mockingboard/ay8910.hpp +159 -0
- package/src/core/cards/mockingboard/via6522.cpp +530 -0
- package/src/core/cards/mockingboard/via6522.hpp +163 -0
- package/src/core/cards/mockingboard_card.cpp +312 -0
- package/src/core/cards/mockingboard_card.hpp +159 -0
- package/src/core/cards/mouse_card.cpp +654 -0
- package/src/core/cards/mouse_card.hpp +190 -0
- package/src/core/cards/smartport/block_device.cpp +202 -0
- package/src/core/cards/smartport/block_device.hpp +60 -0
- package/src/core/cards/smartport/smartport_card.cpp +603 -0
- package/src/core/cards/smartport/smartport_card.hpp +120 -0
- package/src/core/cards/thunderclock_card.cpp +237 -0
- package/src/core/cards/thunderclock_card.hpp +122 -0
- package/src/core/cpu/cpu6502.cpp +1609 -0
- package/src/core/cpu/cpu6502.hpp +203 -0
- package/src/core/debug/condition_evaluator.cpp +470 -0
- package/src/core/debug/condition_evaluator.hpp +87 -0
- package/src/core/disassembler/disassembler.cpp +552 -0
- package/src/core/disassembler/disassembler.hpp +171 -0
- package/src/core/disk-image/disk_image.hpp +267 -0
- package/src/core/disk-image/dsk_disk_image.cpp +827 -0
- package/src/core/disk-image/dsk_disk_image.hpp +204 -0
- package/src/core/disk-image/gcr_encoding.cpp +147 -0
- package/src/core/disk-image/gcr_encoding.hpp +78 -0
- package/src/core/disk-image/woz_disk_image.cpp +1049 -0
- package/src/core/disk-image/woz_disk_image.hpp +343 -0
- package/src/core/emulator.cpp +2126 -0
- package/src/core/emulator.hpp +434 -0
- package/src/core/filesystem/dos33.cpp +178 -0
- package/src/core/filesystem/dos33.hpp +66 -0
- package/src/core/filesystem/pascal.cpp +262 -0
- package/src/core/filesystem/pascal.hpp +87 -0
- package/src/core/filesystem/prodos.cpp +369 -0
- package/src/core/filesystem/prodos.hpp +119 -0
- package/src/core/input/keyboard.cpp +227 -0
- package/src/core/input/keyboard.hpp +111 -0
- package/src/core/mmu/mmu.cpp +1387 -0
- package/src/core/mmu/mmu.hpp +236 -0
- package/src/core/types.hpp +196 -0
- package/src/core/video/video.cpp +680 -0
- package/src/core/video/video.hpp +156 -0
- package/src/css/assembler-editor.css +1617 -0
- package/src/css/base.css +470 -0
- package/src/css/basic-debugger.css +791 -0
- package/src/css/basic-editor.css +792 -0
- package/src/css/controls.css +783 -0
- package/src/css/cpu-debugger.css +1413 -0
- package/src/css/debug-base.css +160 -0
- package/src/css/debug-windows.css +6455 -0
- package/src/css/disk-drives.css +406 -0
- package/src/css/documentation.css +392 -0
- package/src/css/file-explorer.css +867 -0
- package/src/css/hard-drive.css +180 -0
- package/src/css/layout.css +217 -0
- package/src/css/memory-windows.css +798 -0
- package/src/css/modals.css +510 -0
- package/src/css/monitor.css +425 -0
- package/src/css/release-notes.css +101 -0
- package/src/css/responsive.css +400 -0
- package/src/css/rule-builder.css +340 -0
- package/src/css/save-states.css +201 -0
- package/src/css/settings-windows.css +1231 -0
- package/src/css/window-switcher.css +150 -0
- package/src/js/agent/agent-manager.js +643 -0
- package/src/js/agent/agent-tools.js +293 -0
- package/src/js/agent/agent-version-tools.js +131 -0
- package/src/js/agent/assembler-tools.js +357 -0
- package/src/js/agent/basic-program-tools.js +894 -0
- package/src/js/agent/disk-tools.js +417 -0
- package/src/js/agent/file-explorer-tools.js +269 -0
- package/src/js/agent/index.js +13 -0
- package/src/js/agent/main-tools.js +222 -0
- package/src/js/agent/slot-tools.js +303 -0
- package/src/js/agent/smartport-tools.js +257 -0
- package/src/js/agent/window-tools.js +80 -0
- package/src/js/audio/audio-driver.js +417 -0
- package/src/js/audio/audio-worklet.js +85 -0
- package/src/js/audio/index.js +8 -0
- package/src/js/config/default-layout.js +34 -0
- package/src/js/config/version.js +8 -0
- package/src/js/data/apple2-rom-routines.js +577 -0
- package/src/js/debug/assembler-editor-window.js +2993 -0
- package/src/js/debug/basic-breakpoint-manager.js +529 -0
- package/src/js/debug/basic-program-parser.js +436 -0
- package/src/js/debug/basic-program-window.js +2594 -0
- package/src/js/debug/basic-variable-inspector.js +447 -0
- package/src/js/debug/breakpoint-manager.js +472 -0
- package/src/js/debug/cpu-debugger-window.js +2396 -0
- package/src/js/debug/index.js +22 -0
- package/src/js/debug/label-manager.js +238 -0
- package/src/js/debug/memory-browser-window.js +416 -0
- package/src/js/debug/memory-heat-map-window.js +481 -0
- package/src/js/debug/memory-map-window.js +206 -0
- package/src/js/debug/mockingboard-window.js +882 -0
- package/src/js/debug/mouse-card-window.js +355 -0
- package/src/js/debug/rule-builder-window.js +648 -0
- package/src/js/debug/soft-switch-window.js +458 -0
- package/src/js/debug/stack-viewer-window.js +221 -0
- package/src/js/debug/symbols.js +416 -0
- package/src/js/debug/trace-panel.js +291 -0
- package/src/js/debug/zero-page-watch-window.js +297 -0
- package/src/js/disk-manager/disk-drives-window.js +212 -0
- package/src/js/disk-manager/disk-operations.js +284 -0
- package/src/js/disk-manager/disk-persistence.js +301 -0
- package/src/js/disk-manager/disk-surface-renderer.js +388 -0
- package/src/js/disk-manager/drive-sounds.js +139 -0
- package/src/js/disk-manager/hard-drive-manager.js +481 -0
- package/src/js/disk-manager/hard-drive-persistence.js +187 -0
- package/src/js/disk-manager/hard-drive-window.js +57 -0
- package/src/js/disk-manager/index.js +890 -0
- package/src/js/display/display-settings-window.js +383 -0
- package/src/js/display/index.js +10 -0
- package/src/js/display/screen-window.js +342 -0
- package/src/js/display/webgl-renderer.js +705 -0
- package/src/js/file-explorer/disassembler.js +574 -0
- package/src/js/file-explorer/dos33.js +266 -0
- package/src/js/file-explorer/file-viewer.js +359 -0
- package/src/js/file-explorer/index.js +1261 -0
- package/src/js/file-explorer/prodos.js +549 -0
- package/src/js/file-explorer/utils.js +67 -0
- package/src/js/help/documentation-window.js +1096 -0
- package/src/js/help/index.js +10 -0
- package/src/js/help/release-notes-window.js +85 -0
- package/src/js/help/release-notes.js +612 -0
- package/src/js/input/gamepad-handler.js +176 -0
- package/src/js/input/index.js +12 -0
- package/src/js/input/input-handler.js +396 -0
- package/src/js/input/joystick-window.js +404 -0
- package/src/js/input/mouse-handler.js +99 -0
- package/src/js/input/text-selection.js +462 -0
- package/src/js/main.js +653 -0
- package/src/js/state/index.js +15 -0
- package/src/js/state/save-states-window.js +393 -0
- package/src/js/state/state-manager.js +409 -0
- package/src/js/state/state-persistence.js +218 -0
- package/src/js/ui/confirm.js +43 -0
- package/src/js/ui/disk-drive-positioner.js +347 -0
- package/src/js/ui/reminder-controller.js +129 -0
- package/src/js/ui/slot-configuration-window.js +560 -0
- package/src/js/ui/theme-manager.js +61 -0
- package/src/js/ui/toast.js +44 -0
- package/src/js/ui/ui-controller.js +897 -0
- package/src/js/ui/window-switcher.js +275 -0
- package/src/js/utils/basic-autocomplete.js +832 -0
- package/src/js/utils/basic-highlighting.js +473 -0
- package/src/js/utils/basic-tokenizer.js +153 -0
- package/src/js/utils/basic-tokens.js +117 -0
- package/src/js/utils/constants.js +28 -0
- package/src/js/utils/indexeddb-helper.js +225 -0
- package/src/js/utils/merlin-editor-support.js +905 -0
- package/src/js/utils/merlin-highlighting.js +551 -0
- package/src/js/utils/storage.js +125 -0
- package/src/js/utils/string-utils.js +19 -0
- package/src/js/utils/wasm-memory.js +54 -0
- package/src/js/windows/base-window.js +690 -0
- package/src/js/windows/index.js +9 -0
- package/src/js/windows/window-manager.js +375 -0
- package/tests/catch2/catch.hpp +17976 -0
- package/tests/common/basic_program_builder.cpp +119 -0
- package/tests/common/basic_program_builder.hpp +209 -0
- package/tests/common/disk_image_builder.cpp +444 -0
- package/tests/common/disk_image_builder.hpp +141 -0
- package/tests/common/test_helpers.hpp +118 -0
- package/tests/gcr/gcr-test.cpp +142 -0
- package/tests/integration/check-rom.js +70 -0
- package/tests/integration/compare-boot.js +239 -0
- package/tests/integration/crash-trace.js +102 -0
- package/tests/integration/disk-boot-test.js +264 -0
- package/tests/integration/memory-crash.js +108 -0
- package/tests/integration/nibble-read-test.js +249 -0
- package/tests/integration/phase-test.js +159 -0
- package/tests/integration/test_emulator.cpp +291 -0
- package/tests/integration/test_emulator_basic.cpp +91 -0
- package/tests/integration/test_emulator_debug.cpp +344 -0
- package/tests/integration/test_emulator_disk.cpp +153 -0
- package/tests/integration/test_emulator_state.cpp +163 -0
- package/tests/klaus/6502_functional_test.bin +0 -0
- package/tests/klaus/65C02_extended_opcodes_test.bin +0 -0
- package/tests/klaus/klaus_6502_test.cpp +184 -0
- package/tests/klaus/klaus_65c02_test.cpp +197 -0
- package/tests/thunderclock/thunderclock_mmu_test.cpp +304 -0
- package/tests/thunderclock/thunderclock_test.cpp +550 -0
- package/tests/unit/test_assembler.cpp +521 -0
- package/tests/unit/test_audio.cpp +196 -0
- package/tests/unit/test_ay8910.cpp +311 -0
- package/tests/unit/test_basic_detokenizer.cpp +265 -0
- package/tests/unit/test_basic_tokenizer.cpp +382 -0
- package/tests/unit/test_block_device.cpp +259 -0
- package/tests/unit/test_condition_evaluator.cpp +219 -0
- package/tests/unit/test_cpu6502.cpp +1301 -0
- package/tests/unit/test_cpu_addressing.cpp +361 -0
- package/tests/unit/test_cpu_cycle_counts.cpp +409 -0
- package/tests/unit/test_cpu_decimal.cpp +166 -0
- package/tests/unit/test_cpu_interrupts.cpp +285 -0
- package/tests/unit/test_disassembler.cpp +323 -0
- package/tests/unit/test_disk2_card.cpp +330 -0
- package/tests/unit/test_dos33.cpp +273 -0
- package/tests/unit/test_dsk_disk_image.cpp +315 -0
- package/tests/unit/test_expansion_card.cpp +178 -0
- package/tests/unit/test_gcr_encoding.cpp +232 -0
- package/tests/unit/test_keyboard.cpp +262 -0
- package/tests/unit/test_mmu.cpp +555 -0
- package/tests/unit/test_mmu_slots.cpp +323 -0
- package/tests/unit/test_mockingboard.cpp +352 -0
- package/tests/unit/test_mouse_card.cpp +386 -0
- package/tests/unit/test_pascal.cpp +248 -0
- package/tests/unit/test_prodos.cpp +259 -0
- package/tests/unit/test_smartport_card.cpp +321 -0
- package/tests/unit/test_thunderclock.cpp +354 -0
- package/tests/unit/test_via6522.cpp +323 -0
- package/tests/unit/test_video.cpp +319 -0
- package/tests/unit/test_woz_disk_image.cpp +257 -0
- package/vite.config.js +96 -0
- package/wiki/AI-Agent.md +372 -0
- package/wiki/Architecture-Overview.md +303 -0
- package/wiki/Audio-System.md +449 -0
- package/wiki/CPU-Emulation.md +477 -0
- package/wiki/Debugger.md +516 -0
- package/wiki/Disk-Drives.md +161 -0
- package/wiki/Disk-System-Internals.md +547 -0
- package/wiki/Display-Settings.md +88 -0
- package/wiki/Expansion-Slots.md +187 -0
- package/wiki/File-Explorer.md +259 -0
- package/wiki/Getting-Started.md +156 -0
- package/wiki/Home.md +69 -0
- package/wiki/Input-Devices.md +183 -0
- package/wiki/Keyboard-Shortcuts.md +158 -0
- package/wiki/Memory-System.md +364 -0
- package/wiki/Save-States.md +172 -0
- 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 "HELLO WORLD ";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 || " ";
|
|
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
|
+
}
|