Make supported changes through structural tools while keeping the song's text as the editable source.
Structure Lab and the structural key guide are hidden from launch navigation while they receive further polish. The existing editing commands remain available; the interactive course and keymap documentation will return together.
1. What a structure editor is#
A conventional editor models a program as a sequence of characters. Every edit — insert, delete, paste — is a string operation, and it is entirely the author's job to keep the string parseable: match the brackets, keep the operator where the grammar wants it, don't orphan an argument. The compiler knows the real shape of the program — a tree of declarations containing expressions containing atoms — but the editor never sees it, so the two of you meet only at the error list.
A structure editor closes that gap: it gives the editor the compiler's view. The unit of selection is a node (an argument, a pipe stage, a whole patch), the unit of motion is a tree step (up to the enclosing node, across to a sibling), and the unit of editing is a tree operation (swap two stages, delete a stage, rotate a sequence). Structural edits can't produce the classic character-editing failures — a half-deleted call, an operator with no right-hand side — because they operate on whole nodes by construction.
The idea is old and battle-proven: Lisp programmers have edited s-expressions with paredit since the 1970s-lineage structure editors, and every modern IDE's "expand selection" is the same idea in miniature. Lisp gets it almost for free because Lisp source is its syntax tree written down. Volta source is close enough: three nested grammars —
- top-level declarations (
lead = patch { … },line = play …), - the expression tree inside them — patch fields, calls, and the
|>chain (Volta's answer to the Lisp list: an ordered sequence of stages with a natural head), - mini-notation inside
!"…"literals — steps, groups[ ], alternation< >, euclids( , ),
— and the structure editor spans all three with one span forest, so the same expand keystroke walks 0.4 → drive 0.4 → the pipe stage → the source field → the patch → the decl, and keeps walking into a pattern literal down to a single drum hit.
Volta adds one twist a text-first structure editor can't offer: the verified-edit contract. Every structural verb computes its text transform, runs the candidate source through the real compiler, and only lands the edit if the result still compiles — a rejected verb flashes the range and leaves the buffer untouched. Live-coding corollary: you can resculpt an effect chain mid-performance and the buffer never passes through a broken state (the one deliberate exception is insert stage, which opens an empty |> for you to type into).
2. Motions#
Selection and motion operate on the source tree. The keymap guide is being polished and is currently hidden from the editor and public reference.
3. Verbs#
All verbs are pure text transforms over parser-recorded spans, committed through the verify loop (compile-gated, one in flight, one atomic undo step). Native commands carry an ordered set of guarded splices: ordinary verbs use one minimal splice, while cross-declaration moves use a narrow removal and insertion so unrelated text between them is outside the compare-and-swap contract. Same keys on every platform and in both shells.
4. Where it lives#
| Piece | Where |
|---|---|
| Span forest + parenthesized groups + pipes + pattern trees seam | wasm query_spans ← compiler/src/query.rs, parser Program.expr_groups, mini::pattern_tree |
| Native/browser workbench | ui/src/structedit.rs; analysis tables in ui/src/intel.rs; keymap, guarded mutations, flash, breadcrumb, and STRUCT layer in ui/src/editor/structural.rs + ui/src/editor/input.rs |
| Historical TypeScript implementation | the CodeMirror adapter and its structedit / pipeedit / patternedit / pattern-transpose modules were deleted with the TypeScript workbench on 2026-08-09; see git history |
| LSP parity | textDocument/selectionRange + foldingRange in volta-lsp (lsp/src/server.rs, over the volta_compiler::ide facade lsp/src/analysis.rs re-exports) — expand/shrink and folding for any LSP editor, same collect_types data |
Staleness model: the Rust workbench rebuilds an untyped syntax forest and coarse declaration table for every parseable revision, so name-resolution and type errors retain structural selection, sibling motion, declaration hops, pipe chains, and pattern trees. Typed hovers and refined declaration kinds return when checking succeeds. A syntax error temporarily has no native forest; the per-verb compile gate still rejects any edit that doesn't land clean. If a native command resolves through stale pipe/pattern spans, it flashes the affected range and explains the refusal instead of silently doing nothing. Every splice guard is checked before an atomic command lands. In a live room, accepted structural commands retain those exact guards through the native yrs and browser Yjs bridges and become one collaborative undo item. This includes a Bench cell command after its cell-local offsets are rebased to the master song document, as well as commands in an open import buffer. The batch is validated against one shared snapshot, so a peer edit between two targets survives; any stale target rejects all splices and restores shared truth. The ordinary full-text revision pump remains the fallback for shells that do not implement guarded room edits.
5. Boundaries#
- Voice↔track ownership — slurp/barf supports a direct patch with exactly one
playconsumer. Shared patches are deliberately rejected: the compiler can prove the candidate is valid, but cannot prove that fanning an effect into every consumer (or choosing one consumer for a barf) preserves intent. - Multiple carets — structural motions and verbs act on the primary (last-added) caret. Secondary carets are preserved and mapped through the edit; the command does not independently repeat at every caret.
- Redundant expression parentheses are retained in a syntax side-table, so expand selection includes both delimiters even though the typed AST erases the grouping node.
mixer/bus/groupblocks live outside the typed program and are absent from the forest; interpolated patterns (patInterp) keep their literal as one leaf.