[Enhancement] JavaScript playground: Monaco debugger toolbar — Acorn AST–based stepping, breakpoints, vars, line decorate #32

Closed
opened 2026-04-28 23:15:55 +00:00 by spikerj · 1 comment
Owner

Summary

Add a Monaco-based debugger toolbar to the JavaScript playground, aligned with shared toolbar patterns but implementing JavaScript-specific stepping via an Acorn AST strategy (transform or instrument user code so execution can pause at semantic steps / statements mapping back to editor lines).

Goals — debugger UX

  • Breakpoints, Continue, Step over / Step into / Step out where feasible.
  • Variable inspection (locals / scope chain) surfaced in the UI aligned with Monaco’s current execution context.
  • Execution line highlighting in Monaco (decorations) mapped from VM position → source line.

Technical direction — JavaScript playground

  • Use Acorn (AST) analysis to:
    • Identify step boundaries suitable for pedagogical stepping (statement/expression granularity as designed).
    • Preserve source maps or line attribution so AST span ↔ Monaco line mapping stays coherent.
  • Bridge debugger commands ↔ worker/sandbox iframe (or whichever JS execution isolate we use today) so the toolbar drives real pause/resume rather than simulated-only UI unless unavoidable.
  • Document fallback behavior where full “step into” across async/bundlers is impractical.

Acceptance criteria

  • Dedicated issue/PR checklist: toolbar persists across lesson runs; no regressions to non-debug Run/Submit.
  • Breakpoints respected or clearly documented exceptions (e.g. eval-only paths).
  • Line highlight follows the active paused line in Monaco.
  • Security/sandbox constraints respected (lesson runner remains isolated).

Related

Broader Monaco editor migration tracked separately; this ticket is the debugger toolbar + JS stepping story atop that work.

  • See also #24 (app-language-runner Monaco migration umbrella).
## Summary Add a **Monaco-based debugger toolbar** to the **JavaScript playground**, aligned with shared toolbar patterns but implementing **JavaScript-specific** stepping via an **Acorn AST** strategy (transform or instrument user code so execution can pause at semantic steps / statements mapping back to editor lines). ## Goals — debugger UX - **Breakpoints**, **Continue**, **Step over / Step into / Step out** where feasible. - **Variable inspection** (locals / scope chain) surfaced in the UI aligned with Monaco’s current execution context. - **Execution line highlighting** in Monaco (decorations) mapped from VM position → source line. ## Technical direction — JavaScript playground - Use **Acorn** (AST) analysis to: - Identify step boundaries suitable for pedagogical stepping (statement/expression granularity as designed). - Preserve source maps or line attribution so AST span ↔ **Monaco** line mapping stays coherent. - Bridge **debugger commands** ↔ worker/sandbox iframe (or whichever JS execution isolate we use today) so the toolbar drives real pause/resume rather than simulated-only UI unless unavoidable. - Document fallback behavior where full “step into” across async/bundlers is impractical. ## Acceptance criteria - [ ] Dedicated issue/PR checklist: toolbar persists across lesson runs; **no regressions** to non-debug Run/Submit. - [ ] Breakpoints respected or clearly documented exceptions (e.g. eval-only paths). - [ ] Line highlight follows the active paused line in Monaco. - [ ] Security/sandbox constraints respected (lesson runner remains isolated). ## Related Broader Monaco editor migration tracked separately; **this ticket is the debugger toolbar + JS stepping story** atop that work. - See also `#24` (`app-language-runner` Monaco migration umbrella).
Author
Owner

Implementation summary

Shipped a Visual-Studio-style debugger toolbar for the JavaScript playground built on the existing global js-interpreter / acorn / serialize libraries. The execution engine is a new shared service so Blockly's stepper rides on the same core (no duplicate state machines).

Architecture

  • JsStepDebuggerService (libraries/tools/src/services/js-step-debugger/) — single source of truth for run/pause/resume/stop, statement-boundary stepping, breakpoints, scope, output capture, and history-based step-back. Reactive via Angular Signals: state, currentLine, currentScope, output, error, historySize, capabilities.
  • Statement-boundary stepping — instead of tracking raw line numbers (which spuriously back-track when js-interpreter pops up to the Program node), we track the topmost Statement-/Declaration-typed AST node identity. Stable, correct, and naturally maps to the user's notion of "one source-level statement."
  • BreakpointStoreSet<number>-backed signal store with localStorage persistence so breakpoints survive route navigation within a session.
  • scope-snapshot — converts js-interpreter's pseudo-objects into a flat, UI-friendly ScopeBinding[] tree. Cycle-safe.
  • JsDebuggerMonacoBridge — wires service signals to Monaco: instruction-pointer line decoration, breakpoint glyph dots, glyph-margin click → breakpoints.toggle(line). Plain function (not a service) so it stays out of the DI tree.
  • JavascriptInterpreterService (Blockly) — refactored to delegate execution to JsStepDebuggerService. Kept as a thin shim that orchestrates workspaceToCodecore.prepare and translates core.stepInto loops into block-granularity highlighting.

UI

  • JsDebuggerToolbarComponent — Angular CDK Menu menubar. After UX feedback, redesigned to a flat Visual-Studio layout: dynamic Play / Pause / Continue primary button (state-driven label + icon + handler), Stop, Step Over, Step Into, Step Out, Step Back as direct icon buttons, Breakpoints as the only remaining dropdown (because it carries a dynamic line list).
  • JsDebugVarsPanelComponent — collapsible scope tree with click-to-expand for object handles via service.expandHandle.
  • JsDebugConsolePanelComponent — bottom-of-side-rail Console showing console.log / console.error / alert output streamed live as the program steps. Added so students don't need to crack open Chrome DevTools to see their output. Auto-scrolls to newest line; clear button resets via service.clearOutput().
  • LanguageRunner content-projection slots[debug-toolbar] (above the editor) and [debug-side-panel] (right rail). The slot is a single <ng-content> lifted out of the advanced/beginner branches so Angular's content projection always lands on the visible editor layout. editorReady is re-emitted from both editor instances so the bridge re-attaches when toggling sandbox levels.
  • JavaScriptRunner shell — provides JsStepDebuggerService (route-scoped, not root, so navigation tears down breakpoints + interpreter cleanly), projects toolbar + Variables + Console into the new slots, attaches the Monaco bridge on editorReady, registers a source provider so service.run() can lazily prepare from the live editor without the shell intercepting clicks.
  • hideSandboxRunButton: true on JAVASCRIPT_LANGUAGE_RUNNER_CONFIG removes the redundant legacy "Run Code" button below the editor — the toolbar's Play/Stop is now the canonical run path. Other languages keep the button untouched.

Acceptance criteria

  • Breakpoints, Continue, Step over / into / out — all wired through the toolbar with capability-driven disabled states. Step Back also implemented (snapshot + deserialize via the global serialize.js).
  • Variable inspection — Variables panel shows the live scope chain at the paused statement; expanding an object handle drills into its children.
  • Execution line highlightingjs-debug-current-line row + js-debug-current-line-glyph margin arrow track currentLine(). Late-stage fix: advanceToFirstStatement now seeds _currentLine on prepare so line 1 actually highlights (regression covered by spec).
  • Toolbar persists across lesson runs / no regressions to non-debug Run/Submit — toolbar lives in a sandbox-mode-only slot above the editor; lesson mode flow is untouched. Spikersoft prod build is clean, all 2033 spikersoft tests + 350 tools tests pass.
  • Sandbox/security constraints respected — js-interpreter runs in a pure JS sandbox (no DOM access, no XHR), independent of the lesson runner's QuickJS isolate.

ES5 fallback documentation

js-interpreter is ES5-only; let / const / arrow functions / template literals fail to parse. Documented in detail at libraries/tools/src/services/js-step-debugger/README.md along with the rationale for not adopting Babel pre-transpilation (line-mapping fidelity loss for source-level stepping is the wrong trade-off for an educational debugger).

The service surfaces parse errors via the error signal, and the toolbar's status chip renders them so the student sees "Unexpected token" with the offending construct rather than a blank pause.

Late fixes during review

  • Line-1 highlight + line-1 breakpointsadvanceToFirstStatement set lastStatementNode but never _currentLine, so line 1 was never reported and BPs on line 1 walked straight past. Added a parking-line BP check in tick() plus a one-shot skip-flag on resume so resume-from-BP doesn't ping-pong.
  • Toolbar/vars panel disabled or missing in advanced sandbox mode — capabilities now factor in hasSourceProvider, and the projection slots were hoisted out of the advanced/beginner branch into a single shared wrapper.
  • Pre-existing build issues unblocked along the way.ttf loader for Monaco's codicon font, anyScript budget bump for the worker chunk.
## Implementation summary Shipped a Visual-Studio-style debugger toolbar for the JavaScript playground built on the existing global `js-interpreter` / `acorn` / `serialize` libraries. The execution engine is a new shared service so Blockly's stepper rides on the same core (no duplicate state machines). ### Architecture - **`JsStepDebuggerService`** (`libraries/tools/src/services/js-step-debugger/`) — single source of truth for run/pause/resume/stop, statement-boundary stepping, breakpoints, scope, output capture, and history-based step-back. Reactive via Angular Signals: `state`, `currentLine`, `currentScope`, `output`, `error`, `historySize`, `capabilities`. - **Statement-boundary stepping** — instead of tracking raw line numbers (which spuriously back-track when js-interpreter pops up to the `Program` node), we track the topmost Statement-/Declaration-typed AST node identity. Stable, correct, and naturally maps to the user's notion of "one source-level statement." - **`BreakpointStore`** — `Set<number>`-backed signal store with `localStorage` persistence so breakpoints survive route navigation within a session. - **`scope-snapshot`** — converts js-interpreter's pseudo-objects into a flat, UI-friendly `ScopeBinding[]` tree. Cycle-safe. - **`JsDebuggerMonacoBridge`** — wires service signals to Monaco: instruction-pointer line decoration, breakpoint glyph dots, glyph-margin click → `breakpoints.toggle(line)`. Plain function (not a service) so it stays out of the DI tree. - **`JavascriptInterpreterService`** (Blockly) — refactored to delegate execution to `JsStepDebuggerService`. Kept as a thin shim that orchestrates `workspaceToCode` → `core.prepare` and translates `core.stepInto` loops into block-granularity highlighting. ### UI - **`JsDebuggerToolbarComponent`** — Angular CDK Menu menubar. After UX feedback, redesigned to a flat Visual-Studio layout: dynamic Play / Pause / Continue primary button (state-driven label + icon + handler), Stop, Step Over, Step Into, Step Out, Step Back as direct icon buttons, Breakpoints as the only remaining dropdown (because it carries a dynamic line list). - **`JsDebugVarsPanelComponent`** — collapsible scope tree with click-to-expand for object handles via `service.expandHandle`. - **`JsDebugConsolePanelComponent`** — bottom-of-side-rail Console showing `console.log` / `console.error` / `alert` output streamed live as the program steps. Added so students don't need to crack open Chrome DevTools to see their output. Auto-scrolls to newest line; clear button resets via `service.clearOutput()`. - **`LanguageRunner` content-projection slots** — `[debug-toolbar]` (above the editor) and `[debug-side-panel]` (right rail). The slot is a single `<ng-content>` lifted out of the advanced/beginner branches so Angular's content projection always lands on the visible editor layout. `editorReady` is re-emitted from both editor instances so the bridge re-attaches when toggling sandbox levels. - **`JavaScriptRunner` shell** — provides `JsStepDebuggerService` (route-scoped, not root, so navigation tears down breakpoints + interpreter cleanly), projects toolbar + Variables + Console into the new slots, attaches the Monaco bridge on `editorReady`, registers a source provider so `service.run()` can lazily prepare from the live editor without the shell intercepting clicks. - **`hideSandboxRunButton: true`** on `JAVASCRIPT_LANGUAGE_RUNNER_CONFIG` removes the redundant legacy "Run Code" button below the editor — the toolbar's Play/Stop is now the canonical run path. Other languages keep the button untouched. ### Acceptance criteria - ✅ **Breakpoints, Continue, Step over / into / out** — all wired through the toolbar with capability-driven disabled states. Step Back also implemented (snapshot + deserialize via the global `serialize.js`). - ✅ **Variable inspection** — Variables panel shows the live scope chain at the paused statement; expanding an object handle drills into its children. - ✅ **Execution line highlighting** — `js-debug-current-line` row + `js-debug-current-line-glyph` margin arrow track `currentLine()`. Late-stage fix: `advanceToFirstStatement` now seeds `_currentLine` on prepare so line 1 actually highlights (regression covered by spec). - ✅ **Toolbar persists across lesson runs / no regressions to non-debug Run/Submit** — toolbar lives in a sandbox-mode-only slot above the editor; lesson mode flow is untouched. Spikersoft prod build is clean, all 2033 spikersoft tests + 350 tools tests pass. - ✅ **Sandbox/security constraints respected** — js-interpreter runs in a pure JS sandbox (no DOM access, no XHR), independent of the lesson runner's QuickJS isolate. ### ES5 fallback documentation `js-interpreter` is ES5-only; `let` / `const` / arrow functions / template literals fail to parse. Documented in detail at `libraries/tools/src/services/js-step-debugger/README.md` along with the rationale for not adopting Babel pre-transpilation (line-mapping fidelity loss for source-level stepping is the wrong trade-off for an educational debugger). The service surfaces parse errors via the `error` signal, and the toolbar's status chip renders them so the student sees "Unexpected token" with the offending construct rather than a blank pause. ### Late fixes during review - **Line-1 highlight + line-1 breakpoints** — `advanceToFirstStatement` set `lastStatementNode` but never `_currentLine`, so line 1 was never reported and BPs on line 1 walked straight past. Added a parking-line BP check in `tick()` plus a one-shot skip-flag on resume so resume-from-BP doesn't ping-pong. - **Toolbar/vars panel disabled or missing in advanced sandbox mode** — capabilities now factor in `hasSourceProvider`, and the projection slots were hoisted out of the advanced/beginner branch into a single shared wrapper. - **Pre-existing build issues unblocked along the way** — `.ttf` loader for Monaco's codicon font, `anyScript` budget bump for the worker chunk.
Sign in to join this conversation.