From 5477f5436db4c748aeba37a3167728ef8db2f6d9 Mon Sep 17 00:00:00 2001 From: Ajay Ghanwat Date: Tue, 18 Aug 2026 20:23:08 +0530 Subject: [PATCH] docs(html-editing): add design spec for HTML support in .wrn files Markup in a .wrn file highlights but has no tag or attribute completion, no tag closing, and no tag-level folding: the grammar's embeddedLanguages mapping only affects tokenization, and VS Code's HTML language service never runs on these documents. The design extracts view blocks into a virtual HTML document where everything outside them is blanked to whitespace of identical length, so source positions and virtual positions are the same and no mapping table is needed. Region detection is a tolerant scanner rather than the parser, because completion fires while the document is mid-edit and unparseable. Completion merges WRNexus and HTML entries into one list ranked by sortText, which also fixes an existing bug: the extension and the server both answer completion on '<' today, so VS Code concatenates two lists. Two decisions worth review: - HTML formatting is excluded. formatWrn already formats markup, knows WRNexus syntax, and would fight a second formatter that is free to rewrite spacing inside @click={...} and client:visible. - Only auto-close-on-type is client-side. Linked editing is standard LSP and lives in the shared server. Co-Authored-By: Claude Opus 5 --- .../2026-08-18-wrn-html-editing-design.md | 252 ++++++++++++++++++ 1 file changed, 252 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-18-wrn-html-editing-design.md 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 `
` and having `
` follow is `textDocument/linkedEditingRange` (LSP 3.16), so +it lives in the shared server like everything else. + +### Auto-close on type is the one client-side piece + +LSP has no request for "close this tag as I type". VS Code's own HTML extension implements it +client-side, and this follows the same shape: + +1. The extension subscribes to `onDidChangeTextDocument`, filtered to `wrn` documents. +2. When the typed character is `>` or `/`, it sends a custom request, `wrn/tagComplete`. +3. The server runs the HTML service's `doTagComplete` against the virtual document and returns a + snippet or `null`. +4. The client inserts it with `insertSnippet`, so the cursor lands between the tags. + +The decision stays server-side because it needs parse knowledge: void elements (`
`, ``, +``) must not be closed, and an already-closed tag must not be closed twice. Returning +`null` outside a view region is what stops it firing inside `functions { }` or `style { }`. + +Component tags come along for free: `` closes to `` because the HTML service closes +unknown tags like any other, and `` through the same `/` path. + +**New setting:** `wrnexus.html.autoClosingTags`, default `true`, following the existing +`wrnexus.*` naming. + +### Emmet + +A manifest change: `emmet.includeLanguages: { "wrn": "html" }` in `contributes.configurationDefaults`. + +**Known limitation:** `emmet.includeLanguages` is per-language, not per-region, so Emmet is also +live inside `functions { }` and `style { }` blocks. VS Code offers no way to scope it to a +region. Emmet only expands on Tab against an abbreviation pattern, so misfires are rare, but the +edge is real. + +## Folding + +`textDocument/foldingRange` in the server returns tag-level ranges from the HTML service over +the virtual document, filtered to view regions. + +Today folding comes only from `language-configuration.json` markers, which work at block level +(`page`, `component`, `view`, braces). Markup does not fold, so a long `` cannot be +collapsed. VS Code merges marker-based folding with provider ranges, so block folding continues +to work unchanged and tag folding appears inside markup. + +**One rule:** return ranges only where the virtual document is non-blank. A range spanning +outside a view region would let a fold swallow a brace boundary. + +## Formatting is deliberately excluded + +`formatWrn` (`packages/syntax/src/formatter.ts`) is 927 lines, iterates to a fixed point with +cycle detection, and already handles tags, attribute wrapping, `multilineAttributes`, and +`printWidth`. It is a markup formatter that understands WRNexus syntax. + +Adding HTML formatting would do two harmful things: + +- **Two formatters would fight.** Output would depend on which ran last. +- **It would mangle syntax it does not model.** `@click={handler}` and `client:visible` are not + HTML attributes, and an HTML formatter is free to rewrite spacing inside them. + +If markup formatting is unsatisfying, the fix is improving `formatWrn`. That is separate work. + +## Dependencies + +`vscode-html-languageservice` becomes a dependency of **both** `packages/language-server` and +`editors/vscode`. + +The editor bundler (`scripts/build-editor-language-server.mjs`) bundles only workspace sources +and passes other `require`s through to Node, so the package must be resolvable at runtime from +the extension. `editors/vscode` currently ships exactly one runtime dependency +(`vscode-languageclient`); this adds the second. + +`check:editor-language-server` already verifies the bundled `.cjs` starts under Node, so a +missing or unresolvable dependency fails the gate rather than shipping a broken VSIX. + +## Testing + +### Region scanner (`packages/language-server/test/`) + +- **The invariant**, property-style across fixtures: virtual text length equals source length and + newlines sit at identical offsets. +- **Apostrophes in text**: `

it's fine

` followed by a second view block — both regions + found. +- **Nested interpolation**: `class={cond ? "a" : "b"}` and `{{ a: 1 }}` do not end the region. +- **Broken markup**: `