feat(editor): highlight the apis block
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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,18 @@
|
||||
"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": "keyword.control.wrn"
|
||||
},
|
||||
"2": {
|
||||
"name": "entity.name.function.wrn"
|
||||
},
|
||||
"3": {
|
||||
"2": {
|
||||
"name": "constant.language.http-method.wrn"
|
||||
},
|
||||
"4": {
|
||||
"3": {
|
||||
"name": "string.unquoted.route.wrn"
|
||||
},
|
||||
"5": {
|
||||
"4": {
|
||||
"name": "punctuation.definition.block.begin.wrn"
|
||||
}
|
||||
},
|
||||
@@ -558,8 +555,36 @@
|
||||
"name": "punctuation.definition.block.end.wrn"
|
||||
}
|
||||
},
|
||||
"contentName": "meta.embedded.block.ts",
|
||||
"patterns": [
|
||||
{
|
||||
"include": "#comments"
|
||||
},
|
||||
{
|
||||
"begin": "\\b(request|response|error)\\b\\s*(\\{)",
|
||||
"beginCaptures": {
|
||||
"1": {
|
||||
"name": "keyword.control.wrn"
|
||||
},
|
||||
"2": {
|
||||
"name": "punctuation.definition.block.begin.wrn"
|
||||
}
|
||||
},
|
||||
"end": "\\}",
|
||||
"endCaptures": {
|
||||
"0": {
|
||||
"name": "punctuation.definition.block.end.wrn"
|
||||
}
|
||||
},
|
||||
"contentName": "meta.embedded.block.ts",
|
||||
"patterns": [
|
||||
{
|
||||
"include": "#ts-braces"
|
||||
},
|
||||
{
|
||||
"include": "source.ts"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"include": "#ts-braces"
|
||||
},
|
||||
@@ -567,9 +592,6 @@
|
||||
"include": "source.ts"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"include": "#functions-block"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -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'",
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user