# Textarea Code Editor v2 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Wire CodeMirror 6 (bundled, self-hosted) into three textareas in `l4d2web` with syntax highlighting (srccfg + bash) and identifier-as-you-type autocomplete fed by ~2196 cvars/commands generated from `./cvar_list`, without changing the form-POST or `fetch('/files/save')` contracts.
**Architecture:** Pre-built esbuild IIFE bundle (`editor.bundle.js`) exposing `window.__editor.mount(textarea, opts) → controller`. Submit-time copy form bridge: capture-phase `submit` handler copies `controller.getValue()` into the hidden textarea once per submit; JSON-save path calls `getValue()` directly. cm6 owns the live doc. Per-page CSS asset partial keeps the editor off pages that don't mount one.
**Tech Stack:** CodeMirror 6 (`@codemirror/{state,view,commands,language,autocomplete}` + `@codemirror/legacy-modes`), esbuild for bundling, Python `pytest` + Playwright for tests, vanilla JS glue, Jinja templates, Flask backend (untouched).
**Spec:** `docs/superpowers/specs/2026-05-17-textarea-editor-v2-design.md`. **Handoff context (read first):** `docs/superpowers/specs/2026-05-17-textarea-editor-handoff.md`.
---
## File map
What each file owns. Tasks below produce them.
| File | Owner / purpose |
|---|---|
| `l4d2web/scripts/editor-src/package.json` | npm dep manifest (pinned) |
| `l4d2web/scripts/editor-src/package-lock.json` | reproducible build lock |
| `l4d2web/scripts/editor-src/.gitignore` | excludes `node_modules/` |
| `l4d2web/scripts/editor-src/editor-entry.js` | imports cm6 modules; exports `window.__editor` façade |
| `l4d2web/scripts/editor-src/srccfg-mode.js` | `StreamLanguage.define(…)` for Source-engine `.cfg` |
| `l4d2web/scripts/editor-src/themes.js` | light + dark `EditorView.theme()` exports |
| `l4d2web/scripts/editor-src/autocomplete.js` | `CompletionSource` over the vocab JSON |
| `l4d2web/scripts/build-editor.sh` | esbuild invocation; outputs to `static/vendor/` |
| `l4d2web/scripts/build-vocab.py` | parse `./cvar_list` → `srccfg-vocab.json` |
| `l4d2web/l4d2web/static/vendor/editor.bundle.js` | built bundle (committed artifact) |
| `l4d2web/l4d2web/static/vendor/editor.bundle.css` | built CSS (committed artifact) |
| `l4d2web/l4d2web/static/vendor/editor.bundle.sha256` | integrity hashes |
| `l4d2web/l4d2web/static/vendor/README.md` | build cmd, dep versions, regen instructions |
| `l4d2web/l4d2web/static/js/editor.js` | un-bundled glue: mount loop, submit-capture, `__filesEditor` alias |
| `l4d2web/l4d2web/static/css/editor.css` | CSS-variable bridge: cm6 classes ← `tokens.css` |
| `l4d2web/l4d2web/static/css/tokens.css` | gains `--syntax-*` and `--cm-*` variables in light + dark blocks |
| `l4d2web/l4d2web/static/data/srccfg-vocab.json` | generated vocab (committed) |
| `l4d2web/l4d2web/templates/_editor_assets.html` | Jinja partial: nonce'd ``/`
```
`defer` ensures `editor.bundle.js` (which sets `window.__editor`) executes
before `editor.js` (which reads it), since `defer` scripts preserve
document order.
- [ ] **Step 2: Commit**
```bash
git add l4d2web/l4d2web/templates/_editor_assets.html
git commit -m "feat(editor-v2): _editor_assets.html Jinja partial"
```
---
## Task 11: Form-contract pytest (pre-wiring gate)
**Files:**
- Modify: `l4d2web/tests/test_blueprints.py`
- Modify: `l4d2web/tests/test_script_overlay_routes.py`
These tests pin the form-POST contract that the editor must preserve.
They should pass *today*, and continue passing after Task 12's
template wiring.
- [ ] **Step 1: Read the current shape**
```bash
rg -n "def test_" l4d2web/tests/test_blueprints.py | head -20
rg -n "def test_" l4d2web/tests/test_script_overlay_routes.py | head -20
```
Confirm the existing tests use a `client` fixture and the seeded user / blueprint shape.
- [ ] **Step 2: Add the form-contract test to `test_blueprints.py`**
Append (adapt fixtures to match what's already there):
```python
def test_blueprint_config_form_post_contract(client, login_dev, demo_blueprint):
"""The blueprint detail form must accept POST with `config` field
and persist verbatim — pinned regardless of editor-bundle changes."""
config_value = "// pinned by form-contract test\nsv_cheats 0\n"
resp = client.post(
f"/blueprints/{demo_blueprint.id}",
data={"name": demo_blueprint.name, "config": config_value, "arguments": ""},
follow_redirects=False,
)
assert resp.status_code in (302, 303), resp.data
# Reload and verify persistence
from l4d2web.models import Blueprint
refreshed = client.application.extensions["sqlalchemy"].db.session.get(Blueprint, demo_blueprint.id)
assert refreshed.config == config_value
```
Adapt the imports + how to load `Blueprint` from the session to match
the test file's existing conventions. If a similar fixture
(`login_dev`, `demo_blueprint`) doesn't exist yet, write one in
`conftest.py` modeled on the e2e `live_server` seeding.
- [ ] **Step 3: Add the form-contract test to `test_script_overlay_routes.py`**
Same shape:
```python
def test_overlay_script_form_post_contract(client, login_dev, demo_script_overlay):
script_value = "#!/usr/bin/env bash\necho pinned\n"
resp = client.post(
f"/overlays/{demo_script_overlay.id}",
data={"name": demo_script_overlay.name, "script": script_value},
follow_redirects=False,
)
assert resp.status_code in (302, 303), resp.data
# Reload and verify
from l4d2web.models import Overlay
refreshed = client.application.extensions["sqlalchemy"].db.session.get(Overlay, demo_script_overlay.id)
assert refreshed.script == script_value
```
- [ ] **Step 4: Run**
From `l4d2web/`: `uv run pytest tests/test_blueprints.py::test_blueprint_config_form_post_contract tests/test_script_overlay_routes.py::test_overlay_script_form_post_contract -v`
Expected: 2 passed (today, *before* any template change).
- [ ] **Step 5: Commit**
```bash
git add l4d2web/tests/test_blueprints.py l4d2web/tests/test_script_overlay_routes.py
git commit -m "test(editor-v2): pin form-POST contract before wiring"
```
---
## Task 12: Template wiring (the three textareas)
**Files:**
- Modify: `l4d2web/l4d2web/templates/blueprint_detail.html` (line 52)
- Modify: `l4d2web/l4d2web/templates/overlay_detail.html` (lines 25, 178)
- Modify: `l4d2web/tests/test_blueprints.py` (add GET-asserts-markup test)
- Modify: `l4d2web/tests/test_script_overlay_routes.py` (same)
TDD: write the GET assertion test, see it fail, edit templates, see it pass.
- [ ] **Step 1: Write the failing GET test in `test_blueprints.py`**
```python
def test_blueprint_get_includes_editor_markup(client, login_dev, demo_blueprint):
resp = client.get(f"/blueprints/{demo_blueprint.id}")
assert resp.status_code == 200
body = resp.data.decode()
assert 'data-editor-language="srccfg"' in body
assert 'vendor/editor.bundle.js' in body
assert 'js/editor.js' in body
```
- [ ] **Step 2: Run it, watch it fail**
From `l4d2web/`: `uv run pytest tests/test_blueprints.py::test_blueprint_get_includes_editor_markup -v`
Expected: FAIL with `assert 'data-editor-language="srccfg"' in body`.
- [ ] **Step 3: Add the attr + partial to `blueprint_detail.html`**
Find line 52:
```html
```
Change to:
```html
```
Find the `{% block extra_head %}` (or equivalent) block and add:
```html
{% include "_editor_assets.html" %}
```
If no such block exists, include just before the closing `