Files
WRNexusJS/docs/superpowers/plans/2026-08-19-editor-tooling.md
T

14 KiB

Editor Tooling 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: The language server and VS Code extension understand apis { }, complete api.<name>(), flag the removed constructs, and stop offering syntax the compiler rejects.

Architecture: Four surfaces change independently — the TextMate grammar, the keyword and snippet completions, the server's call completion and hover, and diagnostics for removed constructs. A final task answers by observation whether generated type errors surface inside .wrn, which has never been verified.

Tech Stack: Bun, TypeScript, bun:test, node --test, LSP, TextMate grammars.

Spec: docs/superpowers/specs/2026-08-19-editor-tooling-design.md

Global Constraints

  • client is one word with several jobs: client state { }, runtime = "client", and the client function modifier all survive. Only the client { } / ssr { } data-block patterns are removed. Blanket removal would un-highlight constructs that still exist.
  • Offering a construct the compiler rejects is worse than offering nothing.
  • Nothing may claim inline diagnostics work until someone has seen them work.
  • The editor bundles embed the compiler and language server — rebuild with bun run --cwd editors/vscode build, or the check:editor-* gates fail on a stale bundle.
  • bun run format before every commit; the gate is bun run check:production.
  • Do NOT use node -e, shell heredocs, or sed to write code into files.

Task 1: Grammar

Files:

  • Modify: editors/vscode/syntaxes/wrn.tmLanguage.json
  • Test: editors/vscode/test/grammar-apis.test.js

Interfaces:

  • Produces: apis highlights as a block keyword; entries highlight as declarations.

  • Step 1: Write the failing test

Create editors/vscode/test/grammar-apis.test.js:

"use strict";

const assert = require("node:assert");
const { test } = require("node:test");
const { readFileSync } = require("node:fs");
const { join } = require("node:path");

const grammar = readFileSync(join(__dirname, "../syntaxes/wrn.tmLanguage.json"), "utf8");

test("the grammar knows the apis block", () => {
  assert.ok(grammar.includes("apis"), "apis should appear as a block keyword");
});

test("client keeps its highlighting where it is still valid", () => {
  // client state {}, runtime = "client", and client function all survive.
  // Only the client {} data block was removed.
  assert.ok(grammar.includes("client"), "client must still be matched");
  assert.ok(grammar.includes("shared"), "the shared function modifier must still be matched");
});
  • Step 2: Run the test to verify it fails

Run: node --test editors/vscode/test/grammar-apis.test.js Expected: FAIL on the first assertion — apis is absent.

  • Step 3: Add apis, remove only the data-block patterns

Add apis to the block-keyword pattern alongside functions. Then find the patterns matching ssr/client as data blocks and remove only those. Leave every rule that matches client in client state, in runtime values, and as a function modifier.

Add a pattern for an entry — <name> <METHOD> <path> — so a declaration reads as a declaration.

  • Step 4: Run the test and check by eye

Run: node --test editors/vscode/test/grammar-apis.test.js Expected: PASS. Then open a .wrn file using apis { }, client state { }, functions { shared function }, and runtime = "client" in VS Code and confirm each still colours correctly. Record what you saw.

  • Step 5: Commit
bun run format
git add editors/vscode
git commit -m "feat(editor): highlight the apis block"

Task 2: Keyword and snippet completion

Files:

  • Modify: packages/language-server/src/index.ts (WRN_KEYWORDS, ~line 31)
  • Modify: editors/vscode/src/completion.js (block snippets)
  • Test: packages/language-server/test/apis-completion.test.ts

Interfaces:

  • Consumes: nothing.

  • Produces: apis is a known keyword; ssr / client data-block snippets are gone.

  • Step 1: Write the failing test

Create packages/language-server/test/apis-completion.test.ts:

import { expect, test } from "bun:test";
import { WRN_KEYWORDS } from "../src/index.ts";

test("apis is a known page-level keyword", () => {
  expect(WRN_KEYWORDS).toContain("apis");
});
  • Step 2: Run the test to verify it fails

Run: bun test packages/language-server/test/apis-completion.test.ts Expected: FAIL — apis is missing.

  • Step 3: Add the keyword and the snippets

Add "apis" to WRN_KEYWORDS. In editors/vscode/src/completion.js, add a container snippet and an entry snippet including the request / response / error sections, and remove any ssr { / client { data-block snippet.

  • Step 4: Run the tests

Run: bun test packages/language-server && bun run --cwd editors/vscode test Expected: PASS.

  • Step 5: Commit
bun run format
git add packages/language-server editors/vscode
git commit -m "feat(editor): complete the apis block and drop the removed snippets"

Task 3: api. call completion and hover

Files:

  • Modify: packages/language-server/src/server.ts (completion and hover handlers)
  • Test: packages/language-server/test/api-call-completion.test.ts

Interfaces:

  • Consumes: ast.dataApis entries, which carry name, method, path, and sections.
  • Produces: completion items for api. and hover detail for a block name.

This is where the syntax pays off in the editor: the set of legal calls is knowable, so the editor should know it.

  • Step 1: Write the failing test

Create packages/language-server/test/api-call-completion.test.ts:

import { expect, test } from "bun:test";
import { apiCallCompletions, apiCallHover } from "../src/server.ts";

const SOURCE = `page Search {
  apis {
    searchUsers POST /api/users {
      request { body { name?: string } }
      response { return data.users }
    }

    listTeams GET /api/teams {
      response { return data.teams }
    }
  }

  functions {
    client async function go(): Promise<void> {
      await api.
    }
  }

  view { <main>x</main> }
}
`;

test("api. offers every declared block with method and path", () => {
  const items = apiCallCompletions(SOURCE);
  const labels = items.map((item) => item.label);

  expect(labels).toContain("searchUsers");
  expect(labels).toContain("listTeams");

  const search = items.find((item) => item.label === "searchUsers")!;
  expect(search.detail).toContain("POST");
  expect(search.detail).toContain("/api/users");
});

test("hovering a block name reports its method, path and request fields", () => {
  const hover = apiCallHover(SOURCE, "searchUsers");

  expect(hover).toContain("POST");
  expect(hover).toContain("/api/users");
  expect(hover).toContain("name");
});

test("a page with no apis block offers nothing", () => {
  expect(apiCallCompletions(`page P { view { <main>x</main> } }`)).toEqual([]);
});
  • Step 2: Run the test to verify it fails

Run: bun test packages/language-server/test/api-call-completion.test.ts Expected: FAIL — the functions do not exist.

  • Step 3: Implement and export both functions

Add apiCallCompletions(source: string) and apiCallHover(source: string, name: string) to packages/language-server/src/server.ts, parsing with @wrnexus/syntax and reading ast.dataApis. Wire apiCallCompletions into the textDocument/completion handler for positions immediately after api., and apiCallHover into textDocument/hover.

The parser must tolerate the half-typed await api. in the fixture. If it throws, fall back to returning [] rather than failing the request — completion fires while the document does not parse, which is the normal case.

  • Step 4: Run the tests

Run: bun test packages/language-server Expected: PASS.

  • Step 5: Commit
bun run format
git add packages/language-server
git commit -m "feat(language-server): complete and describe api block calls"

Task 4: The api= attribute in markup

Files:

  • Modify: packages/language-server/src/html-service.ts
  • Test: packages/language-server/test/api-attribute.test.ts

Interfaces:

  • Consumes: apiCallCompletions from Task 3.

  • Produces: api="…" is not flagged as unknown, and completion inside the quotes offers block names.

  • Step 1: Write the failing test

Create packages/language-server/test/api-attribute.test.ts asserting that (a) an api="searchUsers" attribute produces no unknown-attribute diagnostic, and (b) completion inside the quotes offers searchUsers. Reuse the fixture shape from Task 3.

  • Step 2: Run the test to verify it fails

Run: bun test packages/language-server/test/api-attribute.test.ts Expected: FAIL.

  • Step 3: Teach the HTML service about the attribute

Treat api as a known attribute on any element, and route completion inside its quotes to apiCallCompletions. The value is a call expression, not text — it must not be spell-checked or reformatted as prose.

  • Step 4: Run the tests and commit
bun test packages/language-server
bun run format
git add packages/language-server
git commit -m "feat(language-server): understand the api binding attribute"

Task 5: Diagnostics for the removed constructs

Files:

  • Modify: packages/language-server/src/diagnostics entry point (wherever .wrn diagnostics are produced)
  • Test: packages/language-server/test/removed-construct-diagnostics.test.ts

Interfaces:

  • Produces: an ssr { api … } or client { api … } block yields a diagnostic naming apis { }, positioned on the block keyword.

The compiler already rejects these. The editor should say so while typing, and say what to do instead.

  • Step 1: Write the failing test

Create packages/language-server/test/removed-construct-diagnostics.test.ts:

import { expect, test } from "bun:test";
import { diagnoseWrn } from "../src/index.ts";

test("an ssr data block is flagged and names the replacement", () => {
  const diagnostics = diagnoseWrn(`page P {
  ssr { api x GET /api/x { response { return data } } }
  view { <main>x</main> }
}
`);

  expect(diagnostics.length).toBeGreaterThan(0);
  expect(diagnostics[0]!.message).toContain("apis");
});

test("client state is not flagged", () => {
  const diagnostics = diagnoseWrn(`page P {
  client state { count = 0 }
  view { <main>x</main> }
}
`);

  expect(diagnostics.filter((item) => item.severity === 1)).toEqual([]);
});

Use whichever diagnostic entry point the language server exports; keep the assertions identical.

  • Step 2: Run the test to verify it fails

Run: bun test packages/language-server/test/removed-construct-diagnostics.test.ts Expected: FAIL — either no diagnostic, or one that does not name apis.

  • Step 3: Surface the parse error as a diagnostic

The parser already throws a message naming apis { } for these blocks. Ensure that message reaches the diagnostic with a position on the offending keyword rather than at offset zero.

  • Step 4: Run the tests and commit
bun test packages/language-server
bun run format
git add packages/language-server
git commit -m "feat(language-server): flag the removed data blocks"

Task 6: Answer the inline-diagnostics question, then the full gate

Files:

  • Modify: whatever the observation in Step 2 shows is needed, or none

Interfaces:

  • Consumes: Tasks 1-5.

The apis plan generates type assertions that make tsc fail when a block declares a field its endpoint rejects. Whether that failure appears inside the .wrn file has never been confirmed — it was inferred from reading source. This task settles it by looking.

  • Step 1: Rebuild the bundles
bun run --cwd editors/vscode build
  • Step 2: Observe, and write down what you see

In examples/basic-app, add a field to an apis { } entry that its endpoint does not accept, and run bun run --cwd examples/basic-app wrnexus generate types. Open the page in VS Code and record exactly where the error appears: on the block, only in app/types/wrnexus.generated.api-checks.ts, or nowhere.

Write the answer into the task report. Do not skip this step and reason about it instead — that is what left the question open the first time.

  • Step 3: Act on what you observed

If the error already surfaces usefully on the block, document it and stop.

If it appears only in the generated file, map the diagnostic back: the generator knows which page and block produced each assertion, so record that mapping when emitting and use it to relocate the diagnostic.

If that mapping proves larger than this task can hold, stop and report it as follow-up work rather than half-building it. Say so plainly in the report.

  • Step 4: Remove the temporary field

Revert the deliberate error and confirm bun run typecheck passes with zero net diff in examples/basic-app.

  • Step 5: Full gate
bun run format
bun test
bun run typecheck
bun run --cwd editors/vscode build
bun run --cwd editors/vscode test
bun run check:production

Expected: exit 0 throughout, including check:editor-compiler, check:editor-language-server, and check:editor-extension.

  • Step 6: Manual pass, recorded

Open the migrated examples/basic-app in VS Code and confirm: apis { } highlights, api. completes with the page's block names, hovering a name shows its method and path, and an ssr { api … } block is flagged. Record what you saw in the report — including anything that did not work.

  • Step 7: Commit
git add -A
git commit -m "feat(editor): complete tooling support for the apis block"

Notes for the executor

  • client is not one thing. Removing every client rule from the grammar would break client state, runtime = "client", and client function. Only the data-block patterns go.
  • The parser must tolerate half-typed input. Completion fires while the document does not parse; a thrown error must become an empty completion list, not a failed request.
  • Step 2 of Task 6 is an observation, not a deduction. Open the editor and look.
  • If a test would still pass with the code it guards deleted, it is not a test.