# Textarea Code Editor v2 (CodeMirror 6) — Design **Status:** Approved 2026-05-17. Supersedes `2026-05-16-textarea-code-editor-design.md` (historical reference only — its functional requirements remain authoritative; its architectural choices, CodeJar + Prism over `contenteditable`, are stale). **History.** A 35-commit implementation of the v1 design was rolled back in commit `f14d352` after four classes of bug surfaced (copy collapses multi-line selections, Enter sometimes needs two presses, autocomplete fails silently at end-of-line, manual caret save/restore is unreliable). All four are characteristic of `contenteditable` + tokenized DOM and proved resistant to incremental fixes. The full post-mortem lives in [`2026-05-17-textarea-editor-handoff.md`](./2026-05-17-textarea-editor-handoff.md). ## Goal Upgrade three plain-text ` │ │ └──────────────────────────┬────────────────────────────┘ │ │ hidden, courier-only │ mounted next to│ ▲ on form `submit` (capture): │ │ │ textarea.value = │ │ │ view.state.doc.toString() │ ▼ │ │ ┌────────────────────────────┴──────────────────────┐ │ │
◄── EditorView (cm6) ─┤ │ │ cm-content (contenteditable, hardened by cm6) │ │ │
│ │ └───────────────────────────────────────────────────┘ │ │ controller.getValue() │ ▼ called at JSON-save time │ fetch('/files/save', …) ────────────────┘ ``` Why submit-time copy and not a per-keystroke live mirror: live mirroring keeps two sources of truth (cm6 doc + textarea) in sync, fires `input` events on the hidden textarea on every keystroke (a semantic muddle — those events historically mean "the user typed", here they also mean "cm6 dispatched"), and requires guard code to prevent mirror writes from re-entering cm6. Submit-time copy collapses to a single source of truth (cm6 owns the doc), with the textarea written once per submission. Cost: 5 read sites + 3 write sites in `files-overlay.js` switch from `textarea.value` to `controller.getValue()` / `controller.setContent()` — bounded, mechanical, single file. ### Subsystems | Unit | Purpose | Depends on | |---|---|---| | `editor.js` | Un-bundled. Bootstraps cm6 on every `textarea[data-editor-language]`, hides the textarea, installs the submit-capture handler, exposes per-textarea controllers. | bundled cm6 + `tokens.css` | | `editor.bundle.js` | Pre-built cm6 + extensions + custom srccfg grammar + light/dark themes, exposed as `window.__editor` (façade). | esbuild + npm deps | | `editor.css` | Skins cm6 classes (`.cm-keyword`, `.cm-string`, gutter, selection, caret) against `tokens.css` CSS variables. | `tokens.css` | | `srccfg-vocab.json` | Autocomplete corpus (~2196 cvars/cmds). | `build-vocab.py` reading `cvar_list` | | `_editor_assets.html` | Jinja partial injecting nonce-tagged `