diff --git a/docs/superpowers/specs/2026-08-18-wrn-html-editing-design.md b/docs/superpowers/specs/2026-08-18-wrn-html-editing-design.md new file mode 100644 index 00000000..438f23c0 --- /dev/null +++ b/docs/superpowers/specs/2026-08-18-wrn-html-editing-design.md @@ -0,0 +1,252 @@ +# HTML editing support for `.wrn` files — Design + +**Date:** 2026-08-18 +**Status:** Approved for implementation +**Scope:** HTML autocomplete, tag closing, hover, Emmet, and folding inside `view { }` blocks. + +## Goal + +Writing markup in a `.wrn` file should feel like writing HTML. Today it does not: there is +syntax highlighting but no tag completion, no attribute completion, no tag closing, and no +tag-level folding. + +The grammar already declares `embeddedLanguages` (`meta.embedded.block.html` → `html`), which is +why markup _highlights_. That mapping only affects tokenization — VS Code's HTML language +service does not run on `.wrn` documents, so none of the editing behaviour follows from it. + +### Non-goals + +- **HTML formatting.** See "Formatting is deliberately excluded" below. +- Editor support outside VS Code beyond what standard LSP gives for free. +- Changing `.wrn` syntax or the compiler. + +## Decisions + +| Question | Decision | +| ------------------- | ---------------------------------------------------------------------------- | +| Features | Tag/attribute completion, auto-close and rename tags, hover + Emmet, folding | +| Placement | Shared language server; only auto-close-on-type is VS Code-specific | +| Completion strategy | One merged list, WRNexus entries ranked above HTML | +| Region detection | Tolerant scanner over a virtual document, not the AST | +| HTML knowledge | `vscode-html-languageservice` | +| Formatting | Excluded — `formatWrn` already owns markup formatting | + +## Architecture + +### Virtual HTML document + +New module: `packages/language-server/src/html-regions.ts`, exporting +`virtualHtmlDocument(document)`. + +Everything outside a `view { }` block is replaced by whitespace of **identical length**, with +newlines preserved. The virtual document therefore has the same size and the same line/column +geometry as the source, so a position in the source _is_ the position in the virtual document. +No mapping table and no translation layer. + +This is deliberately **not** the same shape as the existing `virtualTypeScriptDocument`, which +compacts code and carries line mappings back to source. Compaction is necessary there because +the output must be valid TypeScript. HTML has no such requirement, so the simpler +offset-preserving form applies, and the class of off-by-one bugs that mapping tables produce +does not arise. + +**The load-bearing invariant:** `virtualHtmlDocument(doc).text.length === doc.text.length`, with +newlines at identical offsets. If this breaks, every feature reports positions off by some +amount rather than failing loudly. + +### Region detection + +Region detection is a tolerant scanner, **not** the `@wrnexus/syntax` parser. Completion fires +while the document is being typed, which is exactly when it does not parse. The scanner finds +`view` followed by `{` and tracks brace depth to the matching close. + +Two hazards it must handle, both of which defeat a naive implementation: + +- **Apostrophes in text content.** `
it's fine
` — a scanner treating `'` as a string + delimiter anywhere will consider the rest of the file one open string and lose every later + region. Quotes are tracked only inside attribute values, never in text nodes. +- **Nested braces from interpolation.** `class={cond ? "a" : "b"}` and `{{ a: 1 }}` nest, so + depth must be counted rather than scanning for the next `}`. + +WRNexus-specific syntax (`@click`, `client:visible`, `{expr}`) is **not** blanked. The HTML +service tolerates unknown attributes, and blanking would cost region fidelity for no gain. + +**Caching** is keyed on document URI and version, so a burst of requests from one keystroke +costs a single scan. + +## Completion + +### The server becomes the single authority inside view blocks + +`textDocument/completion` gains a context check: a position is "in HTML" exactly when the +virtual document is non-blank there, which costs one character lookup. + +**Inside a view block**, one list is assembled from two sources: + +| Source | `sortText` prefix | Content | +| ------- | ----------------- | ------------------------------------------------------------------------ | +| WRNexus | `0` | Components, their props/outputs/slots, directives (`@click`, `client:*`) | +| HTML | `1` | Tags, attributes, attribute values | + +`sortText` drives ordering independently of the label, so components rank above HTML tags +without filtering anything out. **Outside a view block**, behaviour is unchanged: WRN keywords +plus workspace items. + +The server already indexes components, props, outputs, and slots +(`buildWorkspaceCompletionItems` in `packages/language-server/src/workspace.ts`), so both halves +of the merge are already available to it. + +**Deduplication on exact label match, WRNexus wins.** A component named `Table` and the HTML +`table` differ in case and both survive; a component that genuinely shadows an HTML tag name +resolves to the component. + +### Trigger characters + +The server currently declares `["<", "@", ":", "."]`. Attributes and values additionally need +`" "`, `"="`, `"\""`, and `"/"`. + +### This fixes an existing bug + +The extension's `completion.js` registers its own provider with `<` among its trigger +characters, and the language server answers `textDocument/completion` as well. VS Code +concatenates both today, producing duplicate entries and unpredictable ordering before HTML is +involved at all. + +As part of this work the extension's provider returns nothing when the position is inside a view +block, and keeps its current behaviour elsewhere. One owner per context. + +**Consequence to accept knowingly:** the server becomes authoritative for the richest completion +context, so future component-intelligence work belongs in the server rather than in +`completion.js`. + +## Hover + +`textDocument/hover` answers from the HTML service over the virtual document when the position +is inside a view region, giving MDN documentation for tags and attributes. Outside a view +region, existing hover behaviour is unchanged. + +Where a position resolves to a WRNexus component or prop, the component's own detail wins over +any HTML entry of the same name, matching the completion precedence rule above. + +## Tag handling + +### Linked editing is standard LSP + +Renaming `