feat(editor): highlight the apis block

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-20 12:31:19 +05:30
co-authored by Claude Opus 5
parent 7a1b4e5b33
commit 990a8128a7
2 changed files with 173 additions and 15 deletions
+37 -15
View File
@@ -113,10 +113,10 @@
"include": "#action-block"
},
{
"include": "#mode-block"
"include": "#api-block"
},
{
"include": "#api-block"
"include": "#apis-block"
},
{
"include": "#functions-block"
@@ -513,8 +513,8 @@
}
]
},
"mode-block": {
"begin": "\\b(ssr|client)\\b\\s*(\\{)",
"apis-block": {
"begin": "\\b(apis)\\b\\s*(\\{)",
"beginCaptures": {
"1": {
"name": "keyword.control.wrn"
@@ -534,21 +534,38 @@
"include": "#comments"
},
{
"begin": "\\b(api)\\b\\s+([A-Za-z_$][A-Za-z0-9_$]*)\\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\\b\\s*([^\\s{]*)\\s*(\\{)",
"begin": "\\b([A-Za-z_$][A-Za-z0-9_$]*)\\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\\b\\s*([^\\s{]*)\\s*(\\{)",
"beginCaptures": {
"1": {
"name": "entity.name.function.wrn"
},
"2": {
"name": "constant.language.http-method.wrn"
},
"3": {
"name": "string.unquoted.route.wrn"
},
"4": {
"name": "punctuation.definition.block.begin.wrn"
}
},
"end": "\\}",
"endCaptures": {
"0": {
"name": "punctuation.definition.block.end.wrn"
}
},
"patterns": [
{
"include": "#comments"
},
{
"begin": "\\b(request|response|error)\\b\\s*(\\{)",
"beginCaptures": {
"1": {
"name": "keyword.control.wrn"
},
"2": {
"name": "entity.name.function.wrn"
},
"3": {
"name": "constant.language.http-method.wrn"
},
"4": {
"name": "string.unquoted.route.wrn"
},
"5": {
"name": "punctuation.definition.block.begin.wrn"
}
},
@@ -569,7 +586,12 @@
]
},
{
"include": "#functions-block"
"include": "#ts-braces"
},
{
"include": "source.ts"
}
]
}
]
},
+136
View File
@@ -0,0 +1,136 @@
"use strict";
const assert = require("node:assert");
const { test } = require("node:test");
const { readFileSync } = require("node:fs");
const { join } = require("node:path");
const grammarPath = join(__dirname, "../syntaxes/wrn.tmLanguage.json");
const grammar = JSON.parse(readFileSync(grammarPath, "utf8"));
test("the grammar knows the apis block", () => {
assert.ok(grammar.repository["apis-block"], "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.match(JSON.stringify(grammar), /client/, "client must still be matched");
assert.match(
JSON.stringify(grammar),
/shared/,
"the shared function modifier must still be matched",
);
});
// --- Structural assertions: these fail if the rule they name is deleted, ---
// --- even though the substrings "apis"/"client" remain elsewhere in the ---
// --- file (e.g. inside unrelated pattern names or comments). ---
/**
* vscode-textmate is not a dependency of this repo (confirmed via
* `require.resolve`), so real tokenization is unavailable here. Instead we
* pull each rule's own `begin`/`match` regex out of the parsed grammar
* object and exercise it directly with the JS regex engine. The grammar's
* patterns use only `\b`, character classes, and `(?<=...)` lookbehind --
* all supported by native JS regexes -- so this is a faithful proxy for
* "does this specific rule match this specific source line" without
* needing an Oniguruma-backed tokenizer.
*/
function ruleByTopLevelBlockName(name) {
const entry = grammar.repository[name];
assert.ok(entry, `repository.${name} should exist`);
return entry;
}
function topLevelBlockIncludes(name) {
return grammar.repository.blocks.patterns.some((p) => p.include === `#${name}`);
}
test("blocks includes the apis-block pattern", () => {
assert.ok(topLevelBlockIncludes("apis-block"), "#blocks must include #apis-block");
});
test("the apis-block begin pattern matches the container keyword", () => {
const rule = ruleByTopLevelBlockName("apis-block");
const beginRe = new RegExp(rule.begin);
assert.ok(beginRe.test("apis {"), "apis-block begin should match 'apis {'");
assert.ok(!beginRe.test("ssr {"), "apis-block begin must not match 'ssr {'");
assert.ok(!beginRe.test("client {"), "apis-block begin must not match 'client {'");
});
test("the apis-block declares an entry pattern for <name> <METHOD> <path>", () => {
const rule = ruleByTopLevelBlockName("apis-block");
const entryPattern = (rule.patterns || []).find(
(p) => p.name === "apis-entry" || (p.begin && /GET\|POST/.test(p.begin)),
);
assert.ok(entryPattern, "apis-block should contain an entry declaration pattern");
const entryRe = new RegExp(entryPattern.begin || entryPattern.match);
assert.ok(
entryRe.test("searchUsers POST /api/users {"),
"the entry pattern should match '<name> <METHOD> <path> {'",
);
});
test("the ssr/client data-block pattern (mode-block) is gone", () => {
assert.strictEqual(
grammar.repository["mode-block"],
undefined,
"mode-block (the removed ssr {}/client {} data-block rule) must be deleted from the repository",
);
assert.ok(
!topLevelBlockIncludes("mode-block"),
"#blocks must no longer include the removed #mode-block pattern",
);
});
// --- Regression coverage: constructs that must NOT be broken by this change ---
test("REGRESSION: client state {} still highlights via grouped-state-block", () => {
const rule = ruleByTopLevelBlockName("grouped-state-block");
const beginRe = new RegExp(rule.begin);
assert.ok(
beginRe.test("client state {"),
"grouped-state-block must still match 'client state {'",
);
});
test('REGRESSION: runtime = "client" still highlights via execution-decl', () => {
const rule = ruleByTopLevelBlockName("execution-decl");
const beginRe = new RegExp(rule.begin);
assert.ok(
beginRe.test('runtime = "client"'),
"execution-decl must still match 'runtime = \"client\"'",
);
});
test("REGRESSION: the client function modifier still highlights via functions-block", () => {
const rule = ruleByTopLevelBlockName("functions-block");
const modifierPattern = rule.patterns.find(
(p) => (p.match && /storage\.modifier\.async/.test("")) || p.match,
);
const matchRe = new RegExp(
rule.patterns.find(
(p) => p.captures && p.captures["1"]?.name === "storage.modifier.runtime.wrn",
).match,
);
assert.ok(
matchRe.test("client async function go(): Promise<void> {"),
"functions-block must still recognize the 'client' runtime modifier on a function",
);
});
test("REGRESSION: the client function modifier also highlights via v060-keywords", () => {
const rule = ruleByTopLevelBlockName("v060-keywords");
const modifierPattern = rule.patterns.find((p) => p.name === "storage.modifier.runtime.wrn");
assert.ok(modifierPattern, "v060-keywords should have a storage.modifier.runtime.wrn rule");
const matchRe = new RegExp(modifierPattern.match);
assert.ok(
matchRe.test("client function go"),
"v060-keywords must still recognize 'client' before 'function'",
);
assert.ok(
matchRe.test("client state"),
"v060-keywords must still recognize 'client' before 'state'",
);
});