# Textarea Code Editor (Highlighting + Autocomplete) Design

**Goal:** Upgrade three plain-text `<textarea>` fields in left4me into a
lightweight code editor with syntax highlighting and identifier-as-you-type
autocomplete. The motivating case is the blueprint config field, where users
edit Source-engine `server.cfg`-style content but have to remember hundreds of
L4D2 cvar names from memory.

**Architecture:** One reusable vanilla-JS widget. Each `<textarea>` opts in via
a `data-editor-language` attribute. The textarea stays in the DOM as the value
carrier — the widget mounts a `contenteditable` sibling that mirrors content
back into the textarea on every input, so HTML form submission and existing
JS that reads `textarea.value` keep working unchanged. Library stack is two
single-file self-hosted dependencies (CodeJar + Prism) plus a small custom
grammar — no bundler, no build step.

---

## Call sites

| Template | Line | Language | Save path |
|---|---|---|---|
| `l4d2web/l4d2web/templates/blueprint_detail.html` | 52 | `srccfg` (preset) | HTML form POST → `blueprint_routes.update_blueprint_form` |
| `l4d2web/l4d2web/templates/overlay_detail.html` | 25 | `bash` (preset) | HTML form POST → overlay script update route |
| `l4d2web/l4d2web/templates/overlay_detail.html` | 178 | `auto` (filename-derived; dropdown override) | `fetch('/files/save', {path, content})` from `files-overlay.js:450-557` |

## Widget contract

Every textarea with `data-editor-language="<lang>"` gets upgraded on
`DOMContentLoaded`:

- Sibling `<div class="editor-shell">` holding `<code class="editor-code"
  contenteditable="true">`, initialized with the textarea's value.
- Textarea set to `display: none` (kept in DOM as form/value carrier).
- CodeJar mounted on the `<code>`, with highlighter →
  `Prism.highlightElement` for the active language.
- CodeJar `onUpdate(code)` writes back to `textarea.value` and dispatches an
  `input` event on the textarea.
- Floating autocomplete `<ul>` positioned at the caret via
  `getSelection().getRangeAt(0).getBoundingClientRect()`.
- Exposes `setLanguage(name)` for the files-editor language dropdown.

If JS fails to bootstrap, the textarea is shown (matching today's behavior) —
graceful no-JS fallback for free.

## Autocomplete behavior

- **Trigger:** word fragment before caret matches `[A-Za-z0-9_]{2,}` and has
  ≥1 hit in the active language's vocabulary.
- **Filter:** case-insensitive prefix match first, then substring match. Cap
  50 candidates; render top 8 with scroll for the rest.
- **Keys (popup open):** ↑/↓ navigate · Tab/Enter accept · Esc close · any
  other key continues editing and re-filters.
- **Mouse:** click to accept.
- **Display:** identifier line + muted description line if `desc` present.

## Languages

- **`srccfg`** — custom Prism grammar (~30 lines). Tokens: comment (`//…`),
  string (`"…"` with escapes), number, keyword (`exec | alias | bind`),
  identifier. Purely visual — no semantic validation.
- **`bash`** — Prism's stock `bash` grammar. No project-owned code.
- **`auto`** — sentinel resolved on mount from the filename: `.cfg → srccfg`,
  `.sh → bash`, otherwise `plain`. Re-evaluated when the filename input
  changes (only while the dropdown is in its initial auto state).
- **`plain`** — no highlighter, no autocomplete. The widget still mounts so
  the language dropdown remains usable; setting language back to `srccfg`
  or `bash` re-activates highlighting.

## Vocabulary

Lives at `l4d2web/l4d2web/static/data/srccfg-vocab.json`:

```json
{
  "cvars":    [{"name": "sv_cheats", "desc": "Allow cheat cvars (0/1)"}, …],
  "commands": [{"name": "exec",      "desc": "Execute a .cfg file"},    …]
}
```

**Sourcing:** dump `cvarlist` / `cmdlist` from a freshly-started L4D2
server with the project's common SourceMod plugins loaded. Hand-trim engine
internals nobody touches. Descriptions come from the `cvarlist` output's
trailing help text where present; otherwise omitted. Generated once,
committed verbatim. Document the regeneration procedure in the top-of-file
comment of `srccfg-vocab.json`.

**Loading:** lazy fetch on first `srccfg` editor mount; cached on
`window.__srccfgVocab` so multiple editors on the same page share the load.

## Asset layout (new)

```
l4d2web/l4d2web/static/
  vendor/
    prism.js          # prismjs.com custom build: core + clike + bash, pinned
    prism.css
    codejar.js        # github.com/antonmedv/codejar release, pinned
    README.md         # source URLs + versions + SHA256 per file
  js/
    editor.js         # widget: mount, popup, sync, language switch
    srccfg-grammar.js # Prism.languages.srccfg
  data/
    srccfg-vocab.json # curated cvars + commands
```

All vendored files are self-hosted; the existing CSP
(`l4d2web/l4d2web/app.py:99` — `default-src 'self'; script-src 'self'
'nonce-…'`) requires it. Template `<script>` tags use `nonce="{{ g.csp_nonce }}"`
per the established pattern (`app.py:86`).

## Files Touched

| File | Change |
|---|---|
| `l4d2web/l4d2web/templates/blueprint_detail.html` | Add `data-editor-language="srccfg"` to line 52; include editor asset block |
| `l4d2web/l4d2web/templates/overlay_detail.html` | Add `data-editor-language="bash"` to line 25; add `data-editor-language="auto"` + language `<select>` near line 178 filename field; include editor asset block |
| `l4d2web/l4d2web/templates/_editor_assets.html` | New Jinja partial: the five script/link tags with nonces |
| `l4d2web/l4d2web/static/vendor/prism.{js,css}` | New (vendored) |
| `l4d2web/l4d2web/static/vendor/codejar.js` | New (vendored) |
| `l4d2web/l4d2web/static/vendor/README.md` | New — versions + SHA256s |
| `l4d2web/l4d2web/static/js/editor.js` | New — ~250 LOC widget |
| `l4d2web/l4d2web/static/js/srccfg-grammar.js` | New — ~30 LOC Prism grammar |
| `l4d2web/l4d2web/static/data/srccfg-vocab.json` | New — curated cvars/commands |
| `l4d2web/l4d2web/static/css/editor.css` | New — dedicated stylesheet for `.editor-shell`, `.editor-code`, `.editor-popup`; reuses `tokens.css` color variables; preserves textarea visual to avoid layout shift. Loaded by the `_editor_assets.html` partial so it ships only on pages that mount an editor. |
| `l4d2web/pyproject.toml` | Add `playwright` dev dep |
| `l4d2web/tests/e2e/conftest.py` | New — boot Flask app under test, expose URL fixture |
| `l4d2web/tests/e2e/test_editor.py` | New — Playwright test: type `sv_che`, assert popup, accept, assert textarea value |
| `l4d2web/tests/test_blueprints.py` | Extend — assert editor attrs in GET, assert form contract on POST |

**Untouched by design:**
`l4d2web/l4d2web/blueprint_routes.py`, `l4d2web/l4d2web/static/js/files-overlay.js`,
`l4d2web/l4d2web/app.py` — the form contract and the JSON-fetch save path
remain identical, which is the central reason this design uses
"textarea-as-value-carrier" rather than replacing the textarea.

## Reusable patterns referenced

- `l4d2web/l4d2web/static/js/blueprint-overlay-picker.js:20-26,63` —
  progressive enhancement via DOM-manipulated form fields. This editor
  follows the same pattern (no submit interception, native form serialization).
- `l4d2web/l4d2web/app.py:86` — `g.csp_nonce` accessor for template `<script>`
  tags.
- `l4d2web/l4d2web/static/js/files-overlay.js:450-557` — JSON-fetch save
  path that continues to read `textarea.value` unchanged.

## Verification

Manual (Chrome MCP during development):

1. `/blueprints/<id>` — textarea replaced by editor; existing content preserved.
2. Type `sv_che` → popup shows `sv_cheats`; Tab accepts; line highlights.
3. Submit form; reload; saved value matches editor content.
4. Script-type overlay → bash highlighting.
5. Files-type overlay → create `test.cfg`, srccfg highlighting; dropdown to
   bash, re-highlights; save; persisted content matches.
6. Disable JS, reload blueprint → textarea visible, form submits.
7. View-source → no inline scripts, all assets nonce'd.

Automated:

- `cd l4d2web && uv run pytest tests/test_blueprints.py`
- `cd l4d2web && uv run pytest -m e2e` (Playwright)

## Open / Closed

- **Closed in v1:** line numbers, search-in-editor, multi-cursor, bracket
  matching, theme switching, SourcePawn highlighting, bash buffer-context
  autocomplete, per-server live `cvarlist` capture.
- **Open as additive future work:** line numbers (deferred per explicit
  scope decision; cheap to add later as a non-wrapping mode for `srccfg` +
  `bash`); SourcePawn grammar; per-server cvar augmentation.
- **No backend changes.** The design is intentionally constrained to static
  assets + template attributes so the change is small to review and easy to
  revert by deleting the asset block.
