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,832 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* basic-autocomplete.js - BASIC keyword autocomplete
|
|
3
|
+
*
|
|
4
|
+
* Written by
|
|
5
|
+
* Mike Daley <michael_daley@icloud.com>
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { APPLESOFT_TOKENS } from "./basic-tokens.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* ProgramContext - Parses BASIC code to extract variables, line numbers, etc.
|
|
12
|
+
*/
|
|
13
|
+
class ProgramContext {
|
|
14
|
+
constructor() {
|
|
15
|
+
this.variables = new Map(); // name -> { type: 'numeric'|'string', isArray: boolean }
|
|
16
|
+
this.lineNumbers = new Set();
|
|
17
|
+
this.functions = new Map(); // name -> { param: string }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Parse the entire program text
|
|
22
|
+
*/
|
|
23
|
+
parse(text) {
|
|
24
|
+
this.variables.clear();
|
|
25
|
+
this.lineNumbers.clear();
|
|
26
|
+
this.functions.clear();
|
|
27
|
+
|
|
28
|
+
const lines = text.split(/\r?\n/);
|
|
29
|
+
for (const line of lines) {
|
|
30
|
+
this.parseLine(line.trim().toUpperCase());
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Parse a single line of BASIC
|
|
36
|
+
*/
|
|
37
|
+
parseLine(line) {
|
|
38
|
+
if (!line) return;
|
|
39
|
+
|
|
40
|
+
// Extract line number
|
|
41
|
+
const lineMatch = line.match(/^(\d+)\s*(.*)/);
|
|
42
|
+
if (lineMatch) {
|
|
43
|
+
const lineNum = parseInt(lineMatch[1], 10);
|
|
44
|
+
if (lineNum >= 0 && lineNum <= 63999) {
|
|
45
|
+
this.lineNumbers.add(lineNum);
|
|
46
|
+
}
|
|
47
|
+
line = lineMatch[2];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Extract DEF FN functions: DEF FN NAME(VAR) = expr
|
|
51
|
+
const fnMatch = line.match(/DEF\s+FN\s*([A-Z][A-Z0-9]*)\s*\(\s*([A-Z][A-Z0-9]*)\s*\)/);
|
|
52
|
+
if (fnMatch) {
|
|
53
|
+
this.functions.set(fnMatch[1], { param: fnMatch[2] });
|
|
54
|
+
// The parameter is a numeric variable
|
|
55
|
+
this.addVariable(fnMatch[2], "numeric", false);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Extract DIM arrays: DIM A(10), B$(5,3), ...
|
|
59
|
+
const dimMatch = line.match(/DIM\s+(.*)/);
|
|
60
|
+
if (dimMatch) {
|
|
61
|
+
const dimPart = dimMatch[1];
|
|
62
|
+
const arrayPattern = /([A-Z][A-Z0-9]*\$?)\s*\(/g;
|
|
63
|
+
let match;
|
|
64
|
+
while ((match = arrayPattern.exec(dimPart)) !== null) {
|
|
65
|
+
const name = match[1];
|
|
66
|
+
const isString = name.endsWith("$");
|
|
67
|
+
this.addVariable(name, isString ? "string" : "numeric", true);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Extract FOR loop variables: FOR I = 1 TO 10
|
|
72
|
+
const forMatch = line.match(/FOR\s+([A-Z][A-Z0-9]*)\s*=/);
|
|
73
|
+
if (forMatch) {
|
|
74
|
+
this.addVariable(forMatch[1], "numeric", false);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Extract INPUT variables: INPUT A, B$, C
|
|
78
|
+
const inputMatch = line.match(/INPUT\s+(?:"[^"]*"\s*[;,])?\s*(.*)/);
|
|
79
|
+
if (inputMatch) {
|
|
80
|
+
this.extractVariableList(inputMatch[1]);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Extract READ variables: READ A, B$, C
|
|
84
|
+
const readMatch = line.match(/READ\s+(.*)/);
|
|
85
|
+
if (readMatch) {
|
|
86
|
+
this.extractVariableList(readMatch[1]);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Extract GET variable: GET A$
|
|
90
|
+
const getMatch = line.match(/GET\s+([A-Z][A-Z0-9]*\$?)/);
|
|
91
|
+
if (getMatch) {
|
|
92
|
+
const name = getMatch[1];
|
|
93
|
+
this.addVariable(name, name.endsWith("$") ? "string" : "numeric", false);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Extract LET assignments: LET A = 5 or A = 5
|
|
97
|
+
// Also handles A$ = "HELLO"
|
|
98
|
+
const letMatch = line.match(/(?:LET\s+)?([A-Z][A-Z0-9]*\$?)\s*=/);
|
|
99
|
+
if (letMatch) {
|
|
100
|
+
const name = letMatch[1];
|
|
101
|
+
// Don't add if it looks like part of FOR statement (already handled)
|
|
102
|
+
if (!line.match(/FOR\s+/)) {
|
|
103
|
+
this.addVariable(name, name.endsWith("$") ? "string" : "numeric", false);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Extract variables from a comma-separated list
|
|
110
|
+
*/
|
|
111
|
+
extractVariableList(text) {
|
|
112
|
+
// Remove anything after a colon (next statement)
|
|
113
|
+
text = text.split(":")[0];
|
|
114
|
+
|
|
115
|
+
const varPattern = /([A-Z][A-Z0-9]*\$?)(?:\s*\([^)]*\))?/g;
|
|
116
|
+
let match;
|
|
117
|
+
while ((match = varPattern.exec(text)) !== null) {
|
|
118
|
+
const name = match[1];
|
|
119
|
+
// Skip keywords
|
|
120
|
+
if (APPLESOFT_TOKENS.includes(name)) continue;
|
|
121
|
+
this.addVariable(name, name.endsWith("$") ? "string" : "numeric", false);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Add a variable to the context
|
|
127
|
+
*/
|
|
128
|
+
addVariable(name, type, isArray) {
|
|
129
|
+
// Skip single-letter tokens that are keywords
|
|
130
|
+
if (APPLESOFT_TOKENS.includes(name)) return;
|
|
131
|
+
|
|
132
|
+
// Store with array info (arrays can be accessed both ways)
|
|
133
|
+
const existing = this.variables.get(name);
|
|
134
|
+
if (existing) {
|
|
135
|
+
// If we see it as array somewhere, mark it as array
|
|
136
|
+
if (isArray) existing.isArray = true;
|
|
137
|
+
} else {
|
|
138
|
+
this.variables.set(name, { type, isArray });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Get variables as autocomplete items
|
|
144
|
+
*/
|
|
145
|
+
getVariableItems(filter = null) {
|
|
146
|
+
const items = [];
|
|
147
|
+
for (const [name, info] of this.variables) {
|
|
148
|
+
if (filter === "string" && info.type !== "string") continue;
|
|
149
|
+
if (filter === "numeric" && info.type !== "numeric") continue;
|
|
150
|
+
|
|
151
|
+
items.push({
|
|
152
|
+
keyword: name,
|
|
153
|
+
syntax: info.isArray ? `${name}(...)` : name,
|
|
154
|
+
category: "program",
|
|
155
|
+
categoryName: info.type === "string" ? "String Var" : "Variable",
|
|
156
|
+
desc: info.isArray ? "Array variable" : "Variable",
|
|
157
|
+
isVariable: true,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
return items.sort((a, b) => a.keyword.localeCompare(b.keyword));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Get line numbers as autocomplete items
|
|
165
|
+
*/
|
|
166
|
+
getLineNumberItems() {
|
|
167
|
+
return Array.from(this.lineNumbers)
|
|
168
|
+
.sort((a, b) => a - b)
|
|
169
|
+
.map(num => ({
|
|
170
|
+
keyword: String(num),
|
|
171
|
+
syntax: `Line ${num}`,
|
|
172
|
+
category: "program",
|
|
173
|
+
categoryName: "Line",
|
|
174
|
+
desc: "Program line",
|
|
175
|
+
isLineNumber: true,
|
|
176
|
+
}));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Get user functions as autocomplete items
|
|
181
|
+
*/
|
|
182
|
+
getFunctionItems() {
|
|
183
|
+
const items = [];
|
|
184
|
+
for (const [name, info] of this.functions) {
|
|
185
|
+
items.push({
|
|
186
|
+
keyword: name,
|
|
187
|
+
syntax: `FN ${name}(${info.param})`,
|
|
188
|
+
category: "program",
|
|
189
|
+
categoryName: "Function",
|
|
190
|
+
desc: "User-defined function",
|
|
191
|
+
isFunction: true,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
return items.sort((a, b) => a.keyword.localeCompare(b.keyword));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Detect what context we're in based on what precedes the cursor
|
|
200
|
+
*/
|
|
201
|
+
function detectContext(text, position) {
|
|
202
|
+
// Get text from start of line to cursor
|
|
203
|
+
const lineStart = text.lastIndexOf("\n", position - 1) + 1;
|
|
204
|
+
const lineToCursor = text.substring(lineStart, position).toUpperCase();
|
|
205
|
+
|
|
206
|
+
// Check if we're after GOTO, GOSUB, or THEN (expecting line number)
|
|
207
|
+
if (/(?:GOTO|GOSUB|THEN)\s*$/.test(lineToCursor)) {
|
|
208
|
+
return { type: "lineNumber" };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Check if we're in ON ... GOTO/GOSUB line list
|
|
212
|
+
if (/ON\s+.*(?:GOTO|GOSUB)\s+[\d,\s]*$/.test(lineToCursor)) {
|
|
213
|
+
return { type: "lineNumber" };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Check if we're after ONERR GOTO
|
|
217
|
+
if (/ONERR\s+GOTO\s*$/.test(lineToCursor)) {
|
|
218
|
+
return { type: "lineNumber" };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Check if we're after FN (expecting function name)
|
|
222
|
+
if (/FN\s*$/.test(lineToCursor)) {
|
|
223
|
+
return { type: "function" };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Check if we're in a string context (after quotes)
|
|
227
|
+
const beforeCursor = lineToCursor.substring(0, lineToCursor.length);
|
|
228
|
+
const quoteCount = (beforeCursor.match(/"/g) || []).length;
|
|
229
|
+
if (quoteCount % 2 !== 0) {
|
|
230
|
+
return { type: "string" }; // Inside a string - no suggestions
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Default: general context (keywords + variables)
|
|
234
|
+
return { type: "general" };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Keywords with their syntax hints and categories
|
|
238
|
+
const KEYWORD_INFO = {
|
|
239
|
+
// Control Flow
|
|
240
|
+
"END": { syntax: "END", category: "control", desc: "End program execution" },
|
|
241
|
+
"FOR": { syntax: "FOR var = start TO end [STEP n]", category: "control", desc: "Start FOR loop" },
|
|
242
|
+
"NEXT": { syntax: "NEXT [var]", category: "control", desc: "End FOR loop" },
|
|
243
|
+
"IF": { syntax: "IF expr THEN statement", category: "control", desc: "Conditional execution" },
|
|
244
|
+
"THEN": { syntax: "THEN statement", category: "control", desc: "Part of IF statement" },
|
|
245
|
+
"GOTO": { syntax: "GOTO linenum", category: "control", desc: "Jump to line" },
|
|
246
|
+
"GOSUB": { syntax: "GOSUB linenum", category: "control", desc: "Call subroutine" },
|
|
247
|
+
"RETURN": { syntax: "RETURN", category: "control", desc: "Return from subroutine" },
|
|
248
|
+
"ON": { syntax: "ON expr GOTO/GOSUB line1,line2,...", category: "control", desc: "Computed branch" },
|
|
249
|
+
"STOP": { syntax: "STOP", category: "control", desc: "Stop execution" },
|
|
250
|
+
"CONT": { syntax: "CONT", category: "control", desc: "Continue after STOP" },
|
|
251
|
+
"RUN": { syntax: "RUN [linenum]", category: "control", desc: "Run program" },
|
|
252
|
+
"ONERR": { syntax: "ONERR GOTO linenum", category: "control", desc: "Error handler" },
|
|
253
|
+
"RESUME": { syntax: "RESUME", category: "control", desc: "Resume after error" },
|
|
254
|
+
"POP": { syntax: "POP", category: "control", desc: "Pop GOSUB return address" },
|
|
255
|
+
|
|
256
|
+
// I/O
|
|
257
|
+
"PRINT": { syntax: "PRINT [expr][;,][expr]...", category: "io", desc: "Print to screen" },
|
|
258
|
+
"INPUT": { syntax: "INPUT [\"prompt\";]var[,var]...", category: "io", desc: "Get user input" },
|
|
259
|
+
"GET": { syntax: "GET var", category: "io", desc: "Get single keypress" },
|
|
260
|
+
"HOME": { syntax: "HOME", category: "io", desc: "Clear screen" },
|
|
261
|
+
"HTAB": { syntax: "HTAB col", category: "io", desc: "Set horizontal position" },
|
|
262
|
+
"VTAB": { syntax: "VTAB row", category: "io", desc: "Set vertical position" },
|
|
263
|
+
"INVERSE": { syntax: "INVERSE", category: "io", desc: "Inverse text mode" },
|
|
264
|
+
"NORMAL": { syntax: "NORMAL", category: "io", desc: "Normal text mode" },
|
|
265
|
+
"FLASH": { syntax: "FLASH", category: "io", desc: "Flashing text mode" },
|
|
266
|
+
"TEXT": { syntax: "TEXT", category: "io", desc: "Text mode" },
|
|
267
|
+
"PR#": { syntax: "PR# slot", category: "io", desc: "Output to slot" },
|
|
268
|
+
"IN#": { syntax: "IN# slot", category: "io", desc: "Input from slot" },
|
|
269
|
+
"TAB(": { syntax: "TAB(col)", category: "io", desc: "Tab to column" },
|
|
270
|
+
"SPC(": { syntax: "SPC(n)", category: "io", desc: "Print n spaces" },
|
|
271
|
+
"POS": { syntax: "POS(0)", category: "io", desc: "Current cursor column" },
|
|
272
|
+
|
|
273
|
+
// Lo-res Graphics
|
|
274
|
+
"GR": { syntax: "GR", category: "lores", desc: "Lo-res graphics mode" },
|
|
275
|
+
"COLOR=": { syntax: "COLOR= n", category: "lores", desc: "Set lo-res color (0-15)" },
|
|
276
|
+
"PLOT": { syntax: "PLOT x,y", category: "lores", desc: "Plot lo-res point" },
|
|
277
|
+
"HLIN": { syntax: "HLIN x1,x2 AT y", category: "lores", desc: "Draw horizontal line" },
|
|
278
|
+
"VLIN": { syntax: "VLIN y1,y2 AT x", category: "lores", desc: "Draw vertical line" },
|
|
279
|
+
"SCRN(": { syntax: "SCRN(x,y)", category: "lores", desc: "Get color at point" },
|
|
280
|
+
|
|
281
|
+
// Hi-res Graphics
|
|
282
|
+
"HGR": { syntax: "HGR", category: "hires", desc: "Hi-res page 1" },
|
|
283
|
+
"HGR2": { syntax: "HGR2", category: "hires", desc: "Hi-res page 2" },
|
|
284
|
+
"HCOLOR=": { syntax: "HCOLOR= n", category: "hires", desc: "Set hi-res color (0-7)" },
|
|
285
|
+
"HPLOT": { syntax: "HPLOT x,y [TO x2,y2]...", category: "hires", desc: "Plot/draw hi-res" },
|
|
286
|
+
"DRAW": { syntax: "DRAW shape AT x,y", category: "hires", desc: "Draw shape" },
|
|
287
|
+
"XDRAW": { syntax: "XDRAW shape AT x,y", category: "hires", desc: "XOR draw shape" },
|
|
288
|
+
"ROT=": { syntax: "ROT= angle", category: "hires", desc: "Set shape rotation" },
|
|
289
|
+
"SCALE=": { syntax: "SCALE= n", category: "hires", desc: "Set shape scale" },
|
|
290
|
+
"SHLOAD": { syntax: "SHLOAD", category: "hires", desc: "Load shape table" },
|
|
291
|
+
|
|
292
|
+
// Variables & Data
|
|
293
|
+
"LET": { syntax: "LET var = expr", category: "vars", desc: "Assign variable" },
|
|
294
|
+
"DIM": { syntax: "DIM var(size)[,var(size)]...", category: "vars", desc: "Dimension array" },
|
|
295
|
+
"DATA": { syntax: "DATA value,value,...", category: "vars", desc: "Define data" },
|
|
296
|
+
"READ": { syntax: "READ var[,var]...", category: "vars", desc: "Read DATA values" },
|
|
297
|
+
"RESTORE": { syntax: "RESTORE", category: "vars", desc: "Reset DATA pointer" },
|
|
298
|
+
"DEF": { syntax: "DEF FN name(var) = expr", category: "vars", desc: "Define function" },
|
|
299
|
+
"FN": { syntax: "FN name(expr)", category: "vars", desc: "Call user function" },
|
|
300
|
+
"CLEAR": { syntax: "CLEAR", category: "vars", desc: "Clear variables" },
|
|
301
|
+
"NEW": { syntax: "NEW", category: "vars", desc: "Clear program" },
|
|
302
|
+
|
|
303
|
+
// Math Functions
|
|
304
|
+
"ABS": { syntax: "ABS(n)", category: "math", desc: "Absolute value" },
|
|
305
|
+
"SGN": { syntax: "SGN(n)", category: "math", desc: "Sign (-1,0,1)" },
|
|
306
|
+
"INT": { syntax: "INT(n)", category: "math", desc: "Integer part" },
|
|
307
|
+
"SQR": { syntax: "SQR(n)", category: "math", desc: "Square root" },
|
|
308
|
+
"RND": { syntax: "RND(n)", category: "math", desc: "Random number" },
|
|
309
|
+
"SIN": { syntax: "SIN(n)", category: "math", desc: "Sine" },
|
|
310
|
+
"COS": { syntax: "COS(n)", category: "math", desc: "Cosine" },
|
|
311
|
+
"TAN": { syntax: "TAN(n)", category: "math", desc: "Tangent" },
|
|
312
|
+
"ATN": { syntax: "ATN(n)", category: "math", desc: "Arctangent" },
|
|
313
|
+
"LOG": { syntax: "LOG(n)", category: "math", desc: "Natural log" },
|
|
314
|
+
"EXP": { syntax: "EXP(n)", category: "math", desc: "e^n" },
|
|
315
|
+
|
|
316
|
+
// String Functions
|
|
317
|
+
"LEN": { syntax: "LEN(str$)", category: "string", desc: "String length" },
|
|
318
|
+
"LEFT$": { syntax: "LEFT$(str$,n)", category: "string", desc: "Left n chars" },
|
|
319
|
+
"RIGHT$": { syntax: "RIGHT$(str$,n)", category: "string", desc: "Right n chars" },
|
|
320
|
+
"MID$": { syntax: "MID$(str$,start[,len])", category: "string", desc: "Substring" },
|
|
321
|
+
"STR$": { syntax: "STR$(n)", category: "string", desc: "Number to string" },
|
|
322
|
+
"VAL": { syntax: "VAL(str$)", category: "string", desc: "String to number" },
|
|
323
|
+
"ASC": { syntax: "ASC(str$)", category: "string", desc: "ASCII code" },
|
|
324
|
+
"CHR$": { syntax: "CHR$(n)", category: "string", desc: "ASCII to char" },
|
|
325
|
+
|
|
326
|
+
// Memory & System
|
|
327
|
+
"PEEK": { syntax: "PEEK(addr)", category: "system", desc: "Read memory byte" },
|
|
328
|
+
"POKE": { syntax: "POKE addr,value", category: "system", desc: "Write memory byte" },
|
|
329
|
+
"CALL": { syntax: "CALL addr", category: "system", desc: "Call machine code" },
|
|
330
|
+
"USR": { syntax: "USR(n)", category: "system", desc: "Call user routine" },
|
|
331
|
+
"WAIT": { syntax: "WAIT addr,mask[,xor]", category: "system", desc: "Wait for memory" },
|
|
332
|
+
"HIMEM:": { syntax: "HIMEM: addr", category: "system", desc: "Set memory top" },
|
|
333
|
+
"LOMEM:": { syntax: "LOMEM: addr", category: "system", desc: "Set variables start" },
|
|
334
|
+
"FRE": { syntax: "FRE(0)", category: "system", desc: "Free memory" },
|
|
335
|
+
"PDL": { syntax: "PDL(n)", category: "system", desc: "Read paddle (0-255)" },
|
|
336
|
+
"SPEED=": { syntax: "SPEED= n", category: "system", desc: "Set output speed" },
|
|
337
|
+
|
|
338
|
+
// File I/O
|
|
339
|
+
"LOAD": { syntax: "LOAD", category: "file", desc: "Load from tape" },
|
|
340
|
+
"SAVE": { syntax: "SAVE", category: "file", desc: "Save to tape" },
|
|
341
|
+
"STORE": { syntax: "STORE", category: "file", desc: "Store array" },
|
|
342
|
+
"RECALL": { syntax: "RECALL", category: "file", desc: "Recall array" },
|
|
343
|
+
|
|
344
|
+
// Other
|
|
345
|
+
"REM": { syntax: "REM comment", category: "other", desc: "Comment" },
|
|
346
|
+
"LIST": { syntax: "LIST [start[-end]]", category: "other", desc: "List program" },
|
|
347
|
+
"DEL": { syntax: "DEL start,end", category: "other", desc: "Delete lines" },
|
|
348
|
+
"TRACE": { syntax: "TRACE", category: "other", desc: "Enable tracing" },
|
|
349
|
+
"NOTRACE": { syntax: "NOTRACE", category: "other", desc: "Disable tracing" },
|
|
350
|
+
"&": { syntax: "& [params]", category: "other", desc: "Machine language hook" },
|
|
351
|
+
|
|
352
|
+
// Operators
|
|
353
|
+
"TO": { syntax: "TO", category: "operator", desc: "Range separator" },
|
|
354
|
+
"STEP": { syntax: "STEP n", category: "operator", desc: "Loop increment" },
|
|
355
|
+
"AT": { syntax: "AT", category: "operator", desc: "Position specifier" },
|
|
356
|
+
"AND": { syntax: "expr AND expr", category: "operator", desc: "Logical AND" },
|
|
357
|
+
"OR": { syntax: "expr OR expr", category: "operator", desc: "Logical OR" },
|
|
358
|
+
"NOT": { syntax: "NOT expr", category: "operator", desc: "Logical NOT" },
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
// Category display names and order
|
|
362
|
+
const CATEGORIES = {
|
|
363
|
+
control: "Control",
|
|
364
|
+
io: "I/O",
|
|
365
|
+
lores: "Lo-Res",
|
|
366
|
+
hires: "Hi-Res",
|
|
367
|
+
vars: "Variables",
|
|
368
|
+
math: "Math",
|
|
369
|
+
string: "Strings",
|
|
370
|
+
system: "System",
|
|
371
|
+
file: "File",
|
|
372
|
+
other: "Other",
|
|
373
|
+
operator: "Operator",
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
// Build list of autocomplete keywords (exclude single-char operators)
|
|
377
|
+
const AUTOCOMPLETE_KEYWORDS = APPLESOFT_TOKENS
|
|
378
|
+
.filter(k => k && k.length > 1 && !/^[+\-*/^<>=]$/.test(k))
|
|
379
|
+
.map(keyword => {
|
|
380
|
+
const info = KEYWORD_INFO[keyword] || {
|
|
381
|
+
syntax: keyword,
|
|
382
|
+
category: "other",
|
|
383
|
+
desc: ""
|
|
384
|
+
};
|
|
385
|
+
return {
|
|
386
|
+
keyword,
|
|
387
|
+
...info,
|
|
388
|
+
categoryName: CATEGORIES[info.category] || "Other",
|
|
389
|
+
};
|
|
390
|
+
})
|
|
391
|
+
.sort((a, b) => a.keyword.localeCompare(b.keyword));
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* BasicAutocomplete - Handles autocomplete UI and logic
|
|
395
|
+
*/
|
|
396
|
+
export class BasicAutocomplete {
|
|
397
|
+
constructor(textarea, container) {
|
|
398
|
+
this.textarea = textarea;
|
|
399
|
+
this.container = container;
|
|
400
|
+
this.dropdown = null;
|
|
401
|
+
this.selectedIndex = 0;
|
|
402
|
+
this.matches = [];
|
|
403
|
+
this.isVisible = false;
|
|
404
|
+
this.currentWord = "";
|
|
405
|
+
this.wordStart = 0;
|
|
406
|
+
this.isInserting = false; // Flag to prevent re-triggering on insert
|
|
407
|
+
this.context = new ProgramContext();
|
|
408
|
+
this.parseDebounceTimer = null;
|
|
409
|
+
|
|
410
|
+
this.createDropdown();
|
|
411
|
+
this.bindEvents();
|
|
412
|
+
// Initial parse
|
|
413
|
+
this.parseProgram();
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Parse the program (debounced)
|
|
418
|
+
*/
|
|
419
|
+
parseProgram() {
|
|
420
|
+
this.context.parse(this.textarea.value);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Schedule a program parse (debounced to avoid parsing on every keystroke)
|
|
425
|
+
*/
|
|
426
|
+
scheduleParse() {
|
|
427
|
+
if (this.parseDebounceTimer) {
|
|
428
|
+
clearTimeout(this.parseDebounceTimer);
|
|
429
|
+
}
|
|
430
|
+
this.parseDebounceTimer = setTimeout(() => {
|
|
431
|
+
this.parseProgram();
|
|
432
|
+
}, 300);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Create the autocomplete dropdown element
|
|
437
|
+
*/
|
|
438
|
+
createDropdown() {
|
|
439
|
+
this.dropdown = document.createElement("div");
|
|
440
|
+
this.dropdown.className = "basic-autocomplete-dropdown";
|
|
441
|
+
this.dropdown.innerHTML = `
|
|
442
|
+
<div class="autocomplete-list"></div>
|
|
443
|
+
<div class="autocomplete-hint"></div>
|
|
444
|
+
`;
|
|
445
|
+
this.container.appendChild(this.dropdown);
|
|
446
|
+
|
|
447
|
+
this.listEl = this.dropdown.querySelector(".autocomplete-list");
|
|
448
|
+
this.hintEl = this.dropdown.querySelector(".autocomplete-hint");
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Bind event listeners
|
|
453
|
+
*/
|
|
454
|
+
bindEvents() {
|
|
455
|
+
// Handle input changes - use a named function so we can identify it
|
|
456
|
+
this.boundOnInput = () => {
|
|
457
|
+
if (this.isInserting) return; // Skip if we're inserting
|
|
458
|
+
this.scheduleParse(); // Update program context
|
|
459
|
+
this.onInput();
|
|
460
|
+
};
|
|
461
|
+
this.textarea.addEventListener("input", this.boundOnInput);
|
|
462
|
+
|
|
463
|
+
// Handle keyboard navigation - use capture phase to intercept before other handlers
|
|
464
|
+
this.boundOnKeyDown = (e) => this.onKeyDown(e);
|
|
465
|
+
this.textarea.addEventListener("keydown", this.boundOnKeyDown, true);
|
|
466
|
+
|
|
467
|
+
// Hide on blur (with delay to allow click on dropdown)
|
|
468
|
+
this.textarea.addEventListener("blur", () => {
|
|
469
|
+
setTimeout(() => this.hide(), 200);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
// Handle click on dropdown item
|
|
473
|
+
this.listEl.addEventListener("mousedown", (e) => {
|
|
474
|
+
// Use mousedown instead of click to fire before blur
|
|
475
|
+
e.preventDefault(); // Prevent blur
|
|
476
|
+
const item = e.target.closest(".autocomplete-item");
|
|
477
|
+
if (item) {
|
|
478
|
+
const index = parseInt(item.dataset.index, 10);
|
|
479
|
+
this.selectedIndex = index;
|
|
480
|
+
this.insertSelected();
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
// Handle hover on dropdown item
|
|
485
|
+
this.listEl.addEventListener("mouseover", (e) => {
|
|
486
|
+
const item = e.target.closest(".autocomplete-item");
|
|
487
|
+
if (item) {
|
|
488
|
+
const index = parseInt(item.dataset.index, 10);
|
|
489
|
+
this.selectItem(index);
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Handle input changes
|
|
496
|
+
*/
|
|
497
|
+
onInput() {
|
|
498
|
+
const { word, start, valid } = this.getCurrentWord();
|
|
499
|
+
|
|
500
|
+
if (valid && word.length >= 2) {
|
|
501
|
+
this.currentWord = word;
|
|
502
|
+
this.wordStart = start;
|
|
503
|
+
this.updateMatches(word);
|
|
504
|
+
|
|
505
|
+
if (this.matches.length > 0) {
|
|
506
|
+
this.show();
|
|
507
|
+
this.render();
|
|
508
|
+
} else {
|
|
509
|
+
this.hide();
|
|
510
|
+
}
|
|
511
|
+
} else {
|
|
512
|
+
this.hide();
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Get the current word being typed
|
|
518
|
+
*/
|
|
519
|
+
getCurrentWord() {
|
|
520
|
+
const pos = this.textarea.selectionStart;
|
|
521
|
+
const text = this.textarea.value;
|
|
522
|
+
|
|
523
|
+
// Find word start (go back until we hit a non-word character)
|
|
524
|
+
let start = pos;
|
|
525
|
+
while (start > 0) {
|
|
526
|
+
const char = text[start - 1];
|
|
527
|
+
// Word characters for BASIC: letters, numbers, $
|
|
528
|
+
if (/[A-Za-z0-9$]/.test(char)) {
|
|
529
|
+
start--;
|
|
530
|
+
} else {
|
|
531
|
+
break;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Check if we're inside a string (don't autocomplete)
|
|
536
|
+
const lineStart = text.lastIndexOf('\n', start - 1) + 1;
|
|
537
|
+
const lineToStart = text.substring(lineStart, start);
|
|
538
|
+
const quoteCount = (lineToStart.match(/"/g) || []).length;
|
|
539
|
+
if (quoteCount % 2 !== 0) {
|
|
540
|
+
return { word: "", start: pos, valid: false };
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Check if the character before the word is valid for starting autocomplete
|
|
544
|
+
// (space, colon, operator, or start of line)
|
|
545
|
+
if (start > 0 && start > lineStart) {
|
|
546
|
+
const prevChar = text[start - 1];
|
|
547
|
+
if (/[A-Za-z0-9$]/.test(prevChar)) {
|
|
548
|
+
// Previous char is alphanumeric - we're in the middle of something
|
|
549
|
+
return { word: "", start: pos, valid: false };
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const word = text.substring(start, pos).toUpperCase();
|
|
554
|
+
return { word, start, valid: true };
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Update matches based on typed word and context
|
|
559
|
+
*/
|
|
560
|
+
updateMatches(word) {
|
|
561
|
+
const upperWord = word.toUpperCase();
|
|
562
|
+
const pos = this.textarea.selectionStart;
|
|
563
|
+
const ctx = detectContext(this.textarea.value, this.wordStart);
|
|
564
|
+
|
|
565
|
+
let items = [];
|
|
566
|
+
|
|
567
|
+
if (ctx.type === "lineNumber") {
|
|
568
|
+
// Only suggest line numbers
|
|
569
|
+
items = this.context.getLineNumberItems().filter(item =>
|
|
570
|
+
item.keyword.startsWith(upperWord)
|
|
571
|
+
);
|
|
572
|
+
} else if (ctx.type === "function") {
|
|
573
|
+
// Only suggest user-defined functions
|
|
574
|
+
items = this.context.getFunctionItems().filter(item =>
|
|
575
|
+
item.keyword.startsWith(upperWord)
|
|
576
|
+
);
|
|
577
|
+
} else if (ctx.type === "string") {
|
|
578
|
+
// Inside a string - no suggestions
|
|
579
|
+
items = [];
|
|
580
|
+
} else {
|
|
581
|
+
// General context: keywords + variables
|
|
582
|
+
// Get matching keywords
|
|
583
|
+
const keywords = AUTOCOMPLETE_KEYWORDS.filter(item =>
|
|
584
|
+
item.keyword.startsWith(upperWord)
|
|
585
|
+
);
|
|
586
|
+
|
|
587
|
+
// Get matching variables
|
|
588
|
+
const variables = this.context.getVariableItems().filter(item =>
|
|
589
|
+
item.keyword.startsWith(upperWord)
|
|
590
|
+
);
|
|
591
|
+
|
|
592
|
+
// Get matching functions (for when typing FN prefix isn't there)
|
|
593
|
+
const functions = this.context.getFunctionItems().filter(item =>
|
|
594
|
+
item.keyword.startsWith(upperWord)
|
|
595
|
+
);
|
|
596
|
+
|
|
597
|
+
// Combine: variables first (they're more specific), then keywords/functions
|
|
598
|
+
items = [...variables, ...functions, ...keywords];
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
this.matches = items.slice(0, 12); // Limit to 12 matches
|
|
602
|
+
this.selectedIndex = 0;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Handle keyboard events
|
|
607
|
+
*/
|
|
608
|
+
onKeyDown(e) {
|
|
609
|
+
if (!this.isVisible) return;
|
|
610
|
+
|
|
611
|
+
switch (e.key) {
|
|
612
|
+
case "ArrowDown":
|
|
613
|
+
e.preventDefault();
|
|
614
|
+
e.stopPropagation();
|
|
615
|
+
this.selectItem(this.selectedIndex + 1);
|
|
616
|
+
break;
|
|
617
|
+
|
|
618
|
+
case "ArrowUp":
|
|
619
|
+
e.preventDefault();
|
|
620
|
+
e.stopPropagation();
|
|
621
|
+
this.selectItem(this.selectedIndex - 1);
|
|
622
|
+
break;
|
|
623
|
+
|
|
624
|
+
case "Tab":
|
|
625
|
+
if (this.matches.length > 0) {
|
|
626
|
+
e.preventDefault();
|
|
627
|
+
e.stopPropagation();
|
|
628
|
+
this.insertSelected();
|
|
629
|
+
}
|
|
630
|
+
break;
|
|
631
|
+
|
|
632
|
+
case "Enter":
|
|
633
|
+
if (this.matches.length > 0) {
|
|
634
|
+
e.preventDefault();
|
|
635
|
+
e.stopPropagation();
|
|
636
|
+
this.insertSelected();
|
|
637
|
+
}
|
|
638
|
+
break;
|
|
639
|
+
|
|
640
|
+
case "Escape":
|
|
641
|
+
e.preventDefault();
|
|
642
|
+
e.stopPropagation();
|
|
643
|
+
this.hide();
|
|
644
|
+
break;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Select an item by index
|
|
650
|
+
*/
|
|
651
|
+
selectItem(index) {
|
|
652
|
+
if (this.matches.length === 0) return;
|
|
653
|
+
|
|
654
|
+
// Wrap around
|
|
655
|
+
if (index < 0) index = this.matches.length - 1;
|
|
656
|
+
if (index >= this.matches.length) index = 0;
|
|
657
|
+
|
|
658
|
+
this.selectedIndex = index;
|
|
659
|
+
this.render();
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Insert the selected keyword
|
|
664
|
+
*/
|
|
665
|
+
insertSelected() {
|
|
666
|
+
if (this.matches.length === 0) return;
|
|
667
|
+
|
|
668
|
+
const selected = this.matches[this.selectedIndex];
|
|
669
|
+
const text = this.textarea.value;
|
|
670
|
+
const pos = this.textarea.selectionStart;
|
|
671
|
+
|
|
672
|
+
// Replace the current word with the selected keyword
|
|
673
|
+
const before = text.substring(0, this.wordStart);
|
|
674
|
+
const after = text.substring(pos);
|
|
675
|
+
|
|
676
|
+
// Set flag to prevent re-triggering autocomplete
|
|
677
|
+
this.isInserting = true;
|
|
678
|
+
|
|
679
|
+
this.textarea.value = before + selected.keyword + after;
|
|
680
|
+
|
|
681
|
+
// Position cursor after inserted keyword
|
|
682
|
+
const newPos = this.wordStart + selected.keyword.length;
|
|
683
|
+
this.textarea.selectionStart = newPos;
|
|
684
|
+
this.textarea.selectionEnd = newPos;
|
|
685
|
+
|
|
686
|
+
this.hide();
|
|
687
|
+
|
|
688
|
+
// Trigger input event for highlighting update
|
|
689
|
+
this.textarea.dispatchEvent(new Event("input", { bubbles: true }));
|
|
690
|
+
|
|
691
|
+
// Clear flag after a small delay
|
|
692
|
+
setTimeout(() => {
|
|
693
|
+
this.isInserting = false;
|
|
694
|
+
}, 10);
|
|
695
|
+
|
|
696
|
+
this.textarea.focus();
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Get the item type for styling
|
|
701
|
+
*/
|
|
702
|
+
getItemType(item) {
|
|
703
|
+
if (item.isVariable) return "variable";
|
|
704
|
+
if (item.isLineNumber) return "line";
|
|
705
|
+
if (item.isFunction) return "function";
|
|
706
|
+
return "keyword";
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* Render the dropdown
|
|
711
|
+
*/
|
|
712
|
+
render() {
|
|
713
|
+
// Render list items
|
|
714
|
+
this.listEl.innerHTML = this.matches.map((item, index) => {
|
|
715
|
+
const itemType = this.getItemType(item);
|
|
716
|
+
return `
|
|
717
|
+
<div class="autocomplete-item${index === this.selectedIndex ? " selected" : ""}" data-index="${index}" data-type="${itemType}">
|
|
718
|
+
<span class="autocomplete-keyword">${this.highlightMatch(item.keyword)}</span>
|
|
719
|
+
<span class="autocomplete-category">${item.categoryName}</span>
|
|
720
|
+
</div>
|
|
721
|
+
`;
|
|
722
|
+
}).join("");
|
|
723
|
+
|
|
724
|
+
// Render hint for selected item
|
|
725
|
+
if (this.matches.length > 0) {
|
|
726
|
+
const selected = this.matches[this.selectedIndex];
|
|
727
|
+
this.hintEl.innerHTML = `
|
|
728
|
+
<div class="hint-syntax">${selected.syntax}</div>
|
|
729
|
+
<div class="hint-desc">${selected.desc}</div>
|
|
730
|
+
`;
|
|
731
|
+
this.hintEl.style.display = "block";
|
|
732
|
+
} else {
|
|
733
|
+
this.hintEl.style.display = "none";
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// Scroll selected item into view
|
|
737
|
+
const selectedEl = this.listEl.querySelector(".selected");
|
|
738
|
+
if (selectedEl) {
|
|
739
|
+
selectedEl.scrollIntoView({ block: "nearest" });
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* Highlight the matching part of the keyword
|
|
745
|
+
*/
|
|
746
|
+
highlightMatch(keyword) {
|
|
747
|
+
const matchLen = this.currentWord.length;
|
|
748
|
+
const matched = keyword.substring(0, matchLen);
|
|
749
|
+
const rest = keyword.substring(matchLen);
|
|
750
|
+
return `<span class="match">${matched}</span>${rest}`;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Show the dropdown
|
|
755
|
+
*/
|
|
756
|
+
show() {
|
|
757
|
+
if (this.isVisible) return;
|
|
758
|
+
|
|
759
|
+
this.isVisible = true;
|
|
760
|
+
this.dropdown.classList.add("visible");
|
|
761
|
+
this.positionDropdown();
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* Hide the dropdown
|
|
766
|
+
*/
|
|
767
|
+
hide() {
|
|
768
|
+
if (!this.isVisible) return;
|
|
769
|
+
this.isVisible = false;
|
|
770
|
+
this.dropdown.classList.remove("visible");
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* Position the dropdown near the cursor
|
|
775
|
+
* Uses a simpler approach - position at bottom of textarea, aligned left
|
|
776
|
+
*/
|
|
777
|
+
positionDropdown() {
|
|
778
|
+
// Simple positioning: below the textarea at the left
|
|
779
|
+
// This avoids complex cursor position calculations
|
|
780
|
+
const textareaRect = this.textarea.getBoundingClientRect();
|
|
781
|
+
const containerRect = this.container.getBoundingClientRect();
|
|
782
|
+
|
|
783
|
+
// Calculate approximate cursor position based on text
|
|
784
|
+
const text = this.textarea.value.substring(0, this.textarea.selectionStart);
|
|
785
|
+
const lines = text.split('\n');
|
|
786
|
+
const currentLine = lines.length - 1;
|
|
787
|
+
const currentCol = lines[lines.length - 1].length;
|
|
788
|
+
|
|
789
|
+
// Approximate character dimensions (monospace font)
|
|
790
|
+
const charWidth = 7.2; // Approximate for 12px monospace
|
|
791
|
+
const lineHeight = 18; // Approximate line height
|
|
792
|
+
|
|
793
|
+
// Calculate position
|
|
794
|
+
let left = currentCol * charWidth;
|
|
795
|
+
let top = (currentLine + 1) * lineHeight - this.textarea.scrollTop;
|
|
796
|
+
|
|
797
|
+
// Constrain to container bounds
|
|
798
|
+
const dropdownWidth = 260;
|
|
799
|
+
const dropdownHeight = 280;
|
|
800
|
+
|
|
801
|
+
if (left + dropdownWidth > this.container.clientWidth - 10) {
|
|
802
|
+
left = this.container.clientWidth - dropdownWidth - 10;
|
|
803
|
+
}
|
|
804
|
+
left = Math.max(5, left);
|
|
805
|
+
|
|
806
|
+
// If dropdown would go below container, show it above the cursor
|
|
807
|
+
if (top + dropdownHeight > this.container.clientHeight) {
|
|
808
|
+
top = Math.max(5, top - dropdownHeight - lineHeight);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
this.dropdown.style.left = left + "px";
|
|
812
|
+
this.dropdown.style.top = top + "px";
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* Clean up
|
|
817
|
+
*/
|
|
818
|
+
destroy() {
|
|
819
|
+
if (this.parseDebounceTimer) {
|
|
820
|
+
clearTimeout(this.parseDebounceTimer);
|
|
821
|
+
}
|
|
822
|
+
if (this.boundOnInput) {
|
|
823
|
+
this.textarea.removeEventListener("input", this.boundOnInput);
|
|
824
|
+
}
|
|
825
|
+
if (this.boundOnKeyDown) {
|
|
826
|
+
this.textarea.removeEventListener("keydown", this.boundOnKeyDown, true);
|
|
827
|
+
}
|
|
828
|
+
if (this.dropdown && this.dropdown.parentNode) {
|
|
829
|
+
this.dropdown.parentNode.removeChild(this.dropdown);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}
|