# 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`:

```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**

```bash
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`:

```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**

```bash
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`:

```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**

```bash
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**

```bash
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`:

```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**

```bash
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**

```bash
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**

```bash
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**

```bash
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.**
