401 lines
14 KiB
Markdown
401 lines
14 KiB
Markdown
# 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.**
|