Briefs the next brainstorming session with: what we built, the four contenteditable failure modes that made it unshippable, what's still in the repo (Playwright harness, dev-server, original spec/plan as historical reference), the decision pending (CodeMirror 6 vs textarea-overlay), inputs to load, and an explicit "don't restart this cycle" caveat against trying a third contenteditable variant. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
245 lines
12 KiB
Markdown
245 lines
12 KiB
Markdown
# Textarea code editor — handoff after rollback
|
|
|
|
> Follow-up artifact after a 35-commit attempt at a syntax-highlighting
|
|
> + autocomplete editor (CodeJar + Prism over `contenteditable`) was
|
|
> rolled back in commit `f14d352`. This is **not an implementation plan**
|
|
> — it's a brief for the next brainstorming session to pick a different
|
|
> architecture and produce a new spec. The "Don't restart this cycle"
|
|
> note below is load-bearing.
|
|
|
|
## Context
|
|
|
|
We shipped a 12-task implementation of an in-browser code editor that
|
|
upgraded three `<textarea>` fields in `l4d2web`:
|
|
|
|
- Blueprint config (Source-engine cvar syntax, with cvar autocomplete)
|
|
- Overlay bash script
|
|
- Files-editor modal (auto-detect language from filename + dropdown override)
|
|
|
|
The architecture: vanilla JS, no bundler, CSP-self-hosted. CodeJar
|
|
(~3 KB) provided the editor shell over a `contenteditable` element;
|
|
Prism (~25 KB, custom build of core + clike + bash) provided syntax
|
|
highlighting. A small custom Prism grammar covered Source-engine
|
|
`.cfg` syntax. Autocomplete was a ~150-line popup widget we wrote
|
|
ourselves, backed by a curated 30-cvar + 8-command JSON vocabulary.
|
|
|
|
Every code-review iteration caught real correctness bugs that we
|
|
addressed (capture-phase keydown so CodeJar's Tab handler didn't eat
|
|
the popup-accept; popup leak on destroy; async race in vocab loader;
|
|
try/catch on the Range manipulation; theme-aware syntax-token CSS
|
|
variables). Then a live inspection session surfaced four further
|
|
classes of bug that we couldn't address without more whack-a-mole:
|
|
|
|
1. **Copy collapses multi-line selections to one line.**
|
|
2. **Enter sometimes needs two presses + cursor color shifts.**
|
|
3. **Auto-completion fails silently at end-of-line** (the caret sits
|
|
in a post-Prism-`<span>` text node, so the Range-API subtraction
|
|
goes negative).
|
|
4. **Manual caret save/restore is required around any
|
|
`updateCode` call** — and even with that, the caret can land
|
|
"between" sibling nodes where browsers render it differently.
|
|
|
|
All four are characteristic of `contenteditable` + manual Range API
|
|
manipulation against tokenized DOM. The architecture was wrong for
|
|
the goal. Rolling back to the editor-free baseline was the right call;
|
|
each new fix was reinforcing a sunk-cost path.
|
|
|
|
## What's in the repo now
|
|
|
|
### Removed (in commit `f14d352`)
|
|
|
|
- `l4d2web/l4d2web/static/js/editor.js`
|
|
- `l4d2web/l4d2web/static/js/srccfg-grammar.js`
|
|
- `l4d2web/l4d2web/static/css/editor.css`
|
|
- `l4d2web/l4d2web/static/vendor/{prism.js,prism.css,codejar.js,README.md}`
|
|
- `l4d2web/l4d2web/static/data/srccfg-vocab.json`
|
|
- `l4d2web/l4d2web/templates/_editor_assets.html`
|
|
- `l4d2web/tests/e2e/test_editor.py`
|
|
- The `data-editor-language` attributes and partial includes on
|
|
`blueprint_detail.html`, `overlay_detail.html`
|
|
- The files-overlay.js editor bridge (`setEditorContent`, dropdown
|
|
listener, filename auto-redetect)
|
|
- The create-blueprint modal trim (Arguments + Config textareas restored)
|
|
- The four syntax-color tokens added to `tokens.css`
|
|
- Three form-contract tests in `test_blueprints.py` /
|
|
`test_script_overlay_routes.py`
|
|
|
|
### Kept (informs the next attempt)
|
|
|
|
- `docs/superpowers/specs/2026-05-16-textarea-code-editor-design.md` —
|
|
the original design spec. Still useful as the **functional**
|
|
requirements (which textareas to upgrade, what autocomplete should
|
|
do, what languages to support). The architectural choices in there
|
|
(CodeJar, Prism) are stale; the requirements are not.
|
|
- `docs/superpowers/plans/2026-05-16-textarea-code-editor.md` — the
|
|
12-task plan. Stale architecturally; useful as a reference for the
|
|
testing strategy (form-contract assertions, Playwright e2e shape).
|
|
- `scripts/dev-server.py` — boots Flask locally with all the
|
|
production-equivalent env vars (DATABASE_URL, LEFT4ME_ROOT,
|
|
SESSION_COOKIE_SECURE=0, JOB_WORKER_ENABLED=false). Auto-seeds an
|
|
admin user (`dev`/`devdevdev`) plus a blueprint, two overlays, and a
|
|
server on first run so the next iteration can be inspected
|
|
immediately at `/blueprints/1`, `/overlays/1`, `/overlays/2`, and
|
|
`/servers/1`.
|
|
- `l4d2web/tests/e2e/{__init__.py,conftest.py,test_smoke.py}` —
|
|
Playwright scaffolding. The `live_server` fixture in `conftest.py`
|
|
boots the Flask app on an ephemeral port with a temp SQLite DB,
|
|
seeds a user + blueprint, yields `{base_url, user_id, blueprint_id}`.
|
|
Ready to receive a new `test_editor.py` against whatever shape the
|
|
next editor takes.
|
|
- `pyproject.toml` — `playwright>=1.49.0` + `pytest-playwright>=0.6.0`
|
|
in `[dependency-groups].dev`, `e2e` marker registered,
|
|
`addopts = ["-m", "not e2e"]` so the default fast suite excludes
|
|
browser tests.
|
|
- `AGENTS.md` — documents `uv run playwright install chromium` for
|
|
first-time setup, plus the Claude Code sandbox + Chromium Mach-port
|
|
workaround.
|
|
|
|
## The four bugs (so the next attempt can sanity-check itself)
|
|
|
|
These are the failure modes that surfaced. The next architecture
|
|
should either *avoid the class of bug entirely* or *have evidence of
|
|
having solved it in the chosen library*.
|
|
|
|
1. **Selection.toString() collapses across span topology.** Prism's
|
|
tokenized output emits `<span>for</span>` `<span>vpk</span>`
|
|
`<span>in</span>` … with newlines either inside spans or as bare
|
|
sibling text nodes. The browser's copy implementation walks the
|
|
selection via `Selection.toString()` and doesn't reliably
|
|
reconstruct newlines across that mix. Workarounds need a `copy`
|
|
event handler that overrides the clipboard payload from the
|
|
editor's *source text*, not the DOM walk.
|
|
|
|
2. **Caret can land "between" sibling DOM nodes.** When the caret is
|
|
at end-of-line or between two adjacent tokenized spans, the
|
|
browser's selection has `anchorNode === <code>` (the parent
|
|
element) and `anchorOffset` indexing into its children — NOT a
|
|
text node. Most range-manipulation code assumes text-node
|
|
containers and fails (silently or loudly) in this state. The
|
|
visual cue is a different-color caret.
|
|
|
|
3. **Manual `Range.setStart(endContainer, endOffset - N)` is unsafe.**
|
|
It assumes the trailing N characters live in `endContainer`. When
|
|
the fragment is *upstream* of the caret's actual text node (case
|
|
2), the subtraction goes negative → `IndexSizeError`. We worked
|
|
around it with `Selection.modify('extend', 'backward', 'character')`
|
|
but the underlying fragility persists.
|
|
|
|
4. **CodeJar's `updateCode(text)` does not preserve caret.** It does
|
|
`editor.textContent = code; highlight(editor)` and returns; the
|
|
caret resets to start. Manual `jar.save()` + `jar.restore(pos)` is
|
|
required around it. Even then, `save()` has a special case at
|
|
`codejar.js:122-127` that returns end-of-all-text if the selection
|
|
sits in the editor element rather than a text node — interacting
|
|
badly with case 2.
|
|
|
|
In aggregate, these are the bugs a *generic* contenteditable wrapper
|
|
inherits. A mature library (CodeMirror 6) has invested years in
|
|
handling them. A textarea-overlay approach avoids them by construction
|
|
because the editing surface is a real `<textarea>`.
|
|
|
|
## Decision pending
|
|
|
|
Two finalists for the next attempt:
|
|
|
|
| Concern | Option 2: CodeMirror 6 | Option 3: Textarea-overlay |
|
|
|---|---|---|
|
|
| Bundler aversion | adds a one-time `npm install` + bundle step | no bundler |
|
|
| Editing fluency (selections, IME, mobile, copy/paste) | strong (years of contenteditable hardening) | strong (native `<textarea>`) |
|
|
| IDE features (line numbers, search, multi-cursor) | strong, built-in or import-away | basic only; you'd build them |
|
|
| Autocomplete popup positioning | trivial (built-in `@codemirror/autocomplete`) | medium (mirror-div trick for caret pixel coords; well-known recipe, ~30 LOC) |
|
|
| Risk of recurring contenteditable bugs | low (handled) | none (no contenteditable) |
|
|
| Bundle weight | ~150 KB gzipped | ~10 KB Prism + ~80 LOC overlay JS |
|
|
| Source-cfg language | Lezer parser (more correct, more work) | Prism regex grammar (we already wrote one) |
|
|
| Long-term maintenance | high (heavy dep, but stable) | low (vanilla; font-metric edge cases) |
|
|
|
|
### Recommendation (advisory only)
|
|
|
|
**Option 2 (CodeMirror 6)** — the contenteditable bugs we just hit are
|
|
exactly what CodeMirror 6 was built to solve, and the "no bundler"
|
|
constraint we set originally cost us more in whack-a-mole than the
|
|
one-time build step would have. But Option 3 is also defensible and
|
|
carries zero contenteditable risk.
|
|
|
|
Don't pre-commit to either before the brainstorming session
|
|
re-examines the requirements; this is just the controller's lean
|
|
after living through the failure.
|
|
|
|
## Inputs for the next session
|
|
|
|
When you start brainstorming the next attempt, load these:
|
|
|
|
```
|
|
docs/superpowers/specs/2026-05-16-textarea-code-editor-design.md
|
|
Original requirements (still valid)
|
|
docs/superpowers/plans/2026-05-16-textarea-code-editor.md
|
|
Original plan (architecturally stale, useful for testing strategy)
|
|
docs/superpowers/specs/2026-05-17-textarea-editor-handoff.md
|
|
This document
|
|
```
|
|
|
|
Useful runtime entry points:
|
|
|
|
```bash
|
|
# Local smoke (Flask + seeded demo content)
|
|
./scripts/dev-server.py
|
|
# Then visit http://127.0.0.1:5051/login as dev / devdevdev
|
|
|
|
# Fast test suite (excludes e2e)
|
|
uv run pytest
|
|
|
|
# E2E suite (needs `playwright install chromium` once; pass
|
|
# dangerouslyDisableSandbox: true on Claude Code's Bash tool because
|
|
# Chromium's Mach-port IPC is blocked by the sandbox)
|
|
uv run pytest -m e2e
|
|
```
|
|
|
|
The dev-server seeds (idempotent on `.tmp/dev-server` being clean):
|
|
|
|
- Blueprint `id=1` `demo-srccfg` — has Source-engine `.cfg` content
|
|
ready to highlight
|
|
- Script overlay `id=1` `demo-bash` — has example bash for highlighting
|
|
- Files overlay `id=2` `demo-files` — has `test.cfg` ready to open in
|
|
the files-editor modal
|
|
- Server `id=1` `demo-server` linked to the blueprint
|
|
|
|
## Open questions for the brainstorming session
|
|
|
|
1. **Decision: Option 2 or Option 3.** Don't skip this — it shapes
|
|
everything downstream.
|
|
2. If **Option 2**: where does the `npm install` + `esbuild` step
|
|
live? `l4d2web/scripts/build-editor.sh`? A `Makefile` target?
|
|
How is the bundle's provenance recorded (SHA256s, as we did with
|
|
prism/codejar)? Does the bundle commit to `static/vendor/` or
|
|
gets generated on demand?
|
|
3. If **Option 2**: which CodeMirror packages exactly? Minimum useful
|
|
set is probably `@codemirror/state`, `@codemirror/view`,
|
|
`@codemirror/commands`, `@codemirror/lang-bash`, `@codemirror/autocomplete`,
|
|
plus a custom Source-cfg StreamLanguage or Lezer grammar.
|
|
4. If **Option 3**: how does the autocomplete popup re-position on
|
|
window scroll / page resize? Cross-browser font-metric quirks
|
|
(Safari vs Chrome vs Firefox) — are we OK testing only Chromium?
|
|
5. **Form-contract testing**: the form-POST contract for the blueprint
|
|
config / overlay script doesn't change regardless of which option
|
|
we pick. Pre-write the form-contract tests against the textarea's
|
|
eventual hidden/upgraded state so we know the contract holds early.
|
|
6. **Playwright e2e shape**: the harness is ready. The test that
|
|
types `sv_che`, asserts the popup, accepts via Tab, asserts the
|
|
textarea value — should be re-usable verbatim against either
|
|
approach. Use it as the gate.
|
|
|
|
## Don't restart this cycle
|
|
|
|
The temptation will be to try a third contenteditable variant ("maybe
|
|
if we just add a different library on top of contenteditable…").
|
|
**Resist.** The bug classes we hit are characteristic of
|
|
contenteditable + tokenized DOM, not specific to CodeJar. A new
|
|
contenteditable wrapper that hasn't been hardened against years of
|
|
edge cases will reproduce the same bugs.
|
|
|
|
The two options that survive this constraint:
|
|
|
|
- A library that *has* been hardened (CodeMirror 6).
|
|
- An approach that *doesn't use* contenteditable at all (textarea-overlay).
|
|
|
|
Both are real options. Pick one.
|