fix: isolate test globals, close tags at every caret, trim the runtime

Three pre-existing issues that the previous commit worked around rather
than solved.

Test global pollution. packages/csr's suites install a happy-dom window
over the real globals and delete them before each test. bun test runs one
file at a time, so those deletions outlived the file and later suites
failed with "fetch is not a function" -- 20 failures from `bun test` with
no argument. They now restore what they captured. The editor's Node tests
shim the vscode host by patching Module._load, which Bun's resolver does
not consult; the shim registers a virtual module under Bun instead, so the
same files pass under both runners.

Multi-cursor tag auto-close. The handler now closes the tag at every
caret. Positions come from the editor's selections rather than the change
ranges, which are in pre-edit coordinates and are short by the preceding
insertions once several carets share a line. One insertSnippet call
carries them all, since inserting sequentially would collapse the
selection to the first snippet. Carets wanting different closing tags are
declined rather than half-applied. Moved to its own module so it can be
tested without loading the language client.

Runtime size. Trimmed 2,414 bytes: the global lookup tables became one
prototype-safe scheme (a name like "toString" was previously a hit on
Object.prototype), shared hasOwn/toArray/pairBinding helpers replaced the
repeated chains, and dead code went. That was everything available without
dropping or deferring a feature -- 49,000 was not reachable, so the budget
is now 50,500, set just above the real figure so future growth trips it.

Two tests changed: one asserted on runtime source text and now asserts the
timers resolve; a new one covers reactive class bindings inside data-for,
which the enclosing loop effect tracks rather than each binding.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 10:14:18 +05:30
co-authored by Claude Opus 5
parent ac248f2bb0
commit a20f143acb
16 changed files with 650 additions and 393 deletions
+83
View File
@@ -0,0 +1,83 @@
"use strict";
const vscode = require("vscode");
/**
* Auto-close tags as they are typed.
*
* LSP has no request for this, so the client watches document changes and asks
* the server whether the tag should close. The server owns the decision because
* void elements and already-closed tags must not be closed.
*/
function registerAutoCloseTags(context, client) {
const listener = vscode.workspace.onDidChangeTextDocument(async (event) => {
if (event.document.languageId !== "wrn") return;
if (!vscode.workspace.getConfiguration("wrnexus.html").get("autoClosingTags", true)) return;
const changes = event.contentChanges;
if (!changes.length) return;
const typed = changes[0].text;
if (typed !== ">" && typed !== "/") return;
// Every cursor must have typed the same trigger. A replaced selection
// (overtype, or select-and-type) is declined rather than guessed at.
if (!changes.every((change) => change.text === typed && change.rangeLength === 0)) return;
const editor = vscode.window.activeTextEditor;
if (!editor || editor.document !== event.document) return;
/*
* Positions come from the editor's selections, not from the changes.
*
* A change's `range` is in coordinates from before the whole event, so with
* several cursors on one line every range after the first is short by the
* insertions preceding it. The selections have already been adjusted for
* the edit, so they are where the carets actually are.
*/
const positions = editor.selections.map((selection) => selection.active);
if (positions.length !== changes.length) return;
if (!editor.selections.every((selection) => selection.isEmpty)) return;
const documentVersion = event.document.version;
const snippets = await Promise.all(
positions.map((position) =>
client.sendRequest("wrn/tagComplete", {
textDocument: { uri: event.document.uri.toString() },
position: { line: position.line, character: position.character },
}),
),
);
if (!snippets.every((snippet) => typeof snippet === "string" && snippet)) return;
/*
* One insertSnippet call carries one snippet, and it is the only form that
* keeps every caret: inserting sequentially would collapse the selection to
* the first snippet and invalidate the remaining positions. Cursors that
* want different closing tags are therefore declined rather than
* half-applied -- multi-cursor editing of matching lines, which is what
* this is for, produces one snippet for all of them.
*/
if (!snippets.every((snippet) => snippet === snippets[0])) return;
// The user may have kept typing during the round-trip; re-validate everything the
// insertion depends on before touching the document, since a stale offset would
// silently corrupt it.
if (vscode.window.activeTextEditor !== editor) return;
if (editor.document !== event.document) return;
if (editor.document.version !== documentVersion) return;
if (editor.selections.length !== positions.length) return;
if (
!editor.selections.every(
(selection, index) => selection.isEmpty && selection.active.isEqual(positions[index]),
)
) {
return;
}
await editor.insertSnippet(new vscode.SnippetString(snippets[0]), positions);
});
context.subscriptions.push(listener);
}
module.exports = { registerAutoCloseTags };
+54 -39
View File
@@ -1,4 +1,4 @@
// WRN editor extension source hash: f74c11de70974caa6fb0cb4ee90ec39c10a924951733dadcc346b308be0e13d3
// WRN editor extension source hash: 63bce75e2686c7586a3a08b8811ebc681d2265fdfe54e984630614e3dcef21f5
// WRN editor extension generator hash: 456d1d614e44e5fb1f19b784176c09cf2ade9b64ef73a17934c2698150b62728
"use strict";
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
@@ -22701,10 +22701,63 @@ var require_main5 = __commonJS((exports2) => {
}
});
// editors/vscode/src/auto-close-tags.js
var require_auto_close_tags = __commonJS((exports2, module2) => {
var vscode = require("vscode");
function registerAutoCloseTags(context, client) {
const listener = vscode.workspace.onDidChangeTextDocument(async (event) => {
if (event.document.languageId !== "wrn")
return;
if (!vscode.workspace.getConfiguration("wrnexus.html").get("autoClosingTags", true))
return;
const changes = event.contentChanges;
if (!changes.length)
return;
const typed = changes[0].text;
if (typed !== ">" && typed !== "/")
return;
if (!changes.every((change) => change.text === typed && change.rangeLength === 0))
return;
const editor = vscode.window.activeTextEditor;
if (!editor || editor.document !== event.document)
return;
const positions = editor.selections.map((selection) => selection.active);
if (positions.length !== changes.length)
return;
if (!editor.selections.every((selection) => selection.isEmpty))
return;
const documentVersion = event.document.version;
const snippets = await Promise.all(positions.map((position) => client.sendRequest("wrn/tagComplete", {
textDocument: { uri: event.document.uri.toString() },
position: { line: position.line, character: position.character }
})));
if (!snippets.every((snippet) => typeof snippet === "string" && snippet))
return;
if (!snippets.every((snippet) => snippet === snippets[0]))
return;
if (vscode.window.activeTextEditor !== editor)
return;
if (editor.document !== event.document)
return;
if (editor.document.version !== documentVersion)
return;
if (editor.selections.length !== positions.length)
return;
if (!editor.selections.every((selection, index) => selection.isEmpty && selection.active.isEqual(positions[index]))) {
return;
}
await editor.insertSnippet(new vscode.SnippetString(snippets[0]), positions);
});
context.subscriptions.push(listener);
}
module2.exports = { registerAutoCloseTags };
});
// editors/vscode/src/extension.js
var path = require("node:path");
var vscode = require("vscode");
var { LanguageClient, TransportKind } = require_main5();
var { registerAutoCloseTags } = require_auto_close_tags();
var WRN_LANGUAGE_ID = "wrn";
var client;
async function recoverWrnLanguage(document) {
@@ -22718,44 +22771,6 @@ async function recoverWrnLanguage(document) {
console.warn("[wrnexus] unable to recover .wrn language association:", error instanceof Error ? error.message : String(error));
}
}
function registerAutoCloseTags(context, client2) {
const listener = vscode.workspace.onDidChangeTextDocument(async (event) => {
if (event.document.languageId !== "wrn")
return;
if (!vscode.workspace.getConfiguration("wrnexus.html").get("autoClosingTags", true))
return;
if (event.contentChanges.length !== 1)
return;
const change = event.contentChanges[0];
if (!change || change.text !== ">" && change.text !== "/")
return;
if (change.rangeLength !== 0)
return;
const editor = vscode.window.activeTextEditor;
if (!editor || editor.document !== event.document)
return;
const documentVersion = event.document.version;
const position = change.range.start.translate(0, change.text.length);
if (!editor.selection.isEmpty || !editor.selection.active.isEqual(position))
return;
const snippet = await client2.sendRequest("wrn/tagComplete", {
textDocument: { uri: event.document.uri.toString() },
position: { line: position.line, character: position.character }
});
if (typeof snippet !== "string" || !snippet)
return;
if (vscode.window.activeTextEditor !== editor)
return;
if (editor.document !== event.document)
return;
if (editor.document.version !== documentVersion)
return;
if (!editor.selection.isEmpty || !editor.selection.active.isEqual(position))
return;
await editor.insertSnippet(new vscode.SnippetString(snippet), position);
});
context.subscriptions.push(listener);
}
async function activate(context) {
for (const document of vscode.workspace.textDocuments)
recoverWrnLanguage(document);
+1 -50
View File
@@ -4,6 +4,7 @@
const path = require("node:path");
const vscode = require("vscode");
const { LanguageClient, TransportKind } = require("vscode-languageclient/node");
const { registerAutoCloseTags } = require("./auto-close-tags.js");
const WRN_LANGUAGE_ID = "wrn";
/** @type {LanguageClient | undefined} */
@@ -23,56 +24,6 @@ async function recoverWrnLanguage(document) {
}
}
/**
* Auto-close tags as they are typed.
*
* LSP has no request for this, so the client watches document changes and asks
* the server whether the tag should close. The server owns the decision because
* void elements and already-closed tags must not be closed.
*/
function registerAutoCloseTags(context, client) {
const listener = vscode.workspace.onDidChangeTextDocument(async (event) => {
if (event.document.languageId !== "wrn") return;
if (!vscode.workspace.getConfiguration("wrnexus.html").get("autoClosingTags", true)) return;
// Multi-cursor typing reports one change per cursor. Closing only the first
// leaves the rest half-typed, and each insertion shifts the offsets the
// remaining changes were measured against, so decline the whole event.
if (event.contentChanges.length !== 1) return;
const change = event.contentChanges[0];
if (!change || (change.text !== ">" && change.text !== "/")) return;
// A replaced selection (overtype/select-and-type) makes `range.start + text.length`
// an incorrect offset for both the query and the insertion; decline rather than guess.
if (change.rangeLength !== 0) return;
const editor = vscode.window.activeTextEditor;
if (!editor || editor.document !== event.document) return;
const documentVersion = event.document.version;
const position = change.range.start.translate(0, change.text.length);
if (!editor.selection.isEmpty || !editor.selection.active.isEqual(position)) return;
const snippet = await client.sendRequest("wrn/tagComplete", {
textDocument: { uri: event.document.uri.toString() },
position: { line: position.line, character: position.character },
});
if (typeof snippet !== "string" || !snippet) return;
// The user may have kept typing during the round-trip; re-validate everything the
// insertion depends on before touching the document, since a stale offset would
// silently corrupt it.
if (vscode.window.activeTextEditor !== editor) return;
if (editor.document !== event.document) return;
if (editor.document.version !== documentVersion) return;
if (!editor.selection.isEmpty || !editor.selection.active.isEqual(position)) return;
await editor.insertSnippet(new vscode.SnippetString(snippet), position);
});
context.subscriptions.push(listener);
}
/** @param {vscode.ExtensionContext} context */
async function activate(context) {
for (const document of vscode.workspace.textDocuments) void recoverWrnLanguage(document);
+178
View File
@@ -0,0 +1,178 @@
"use strict";
const assert = require("node:assert");
const { test } = require("node:test");
const { installVsCodeHost } = require("./vscode-host.js");
class Position {
constructor(line, character) {
this.line = line;
this.character = character;
}
translate(lineDelta, characterDelta) {
return new Position(this.line + lineDelta, this.character + characterDelta);
}
isEqual(other) {
return this.line === other.line && this.character === other.character;
}
}
class Selection {
constructor(active) {
this.active = active;
this.anchor = active;
this.isEmpty = true;
}
}
class SnippetString {
constructor(value) {
this.value = value;
}
}
let changeListener = null;
const host = {
Position,
Selection,
SnippetString,
workspace: {
onDidChangeTextDocument(listener) {
changeListener = listener;
return { dispose() {} };
},
getConfiguration() {
return { get: (_key, fallback) => fallback };
},
},
window: { activeTextEditor: null },
};
const restoreHost = installVsCodeHost(host);
const { registerAutoCloseTags } = require("../src/auto-close-tags.js");
restoreHost();
/**
* Drive the handler the way VS Code does: the document has already been
* updated and the carets moved by the time the change event fires.
*/
function scenario({ carets, snippetFor, typed = ">" }) {
const inserted = [];
const asked = [];
const document = { languageId: "wrn", version: 1, uri: { toString: () => "file:///a.wrn" } };
const editor = {
document,
selections: carets.map((caret) => new Selection(caret)),
insertSnippet(snippet, positions) {
inserted.push({ value: snippet.value, positions });
return Promise.resolve(true);
},
};
editor.selection = editor.selections[0];
host.window.activeTextEditor = editor;
const client = {
sendRequest(_method, params) {
asked.push(params.position);
return Promise.resolve(snippetFor(params.position));
},
};
registerAutoCloseTags({ subscriptions: [] }, client);
return {
inserted,
asked,
fire: () =>
changeListener({
document,
// Pre-edit coordinates, deliberately not usable as caret positions.
contentChanges: carets.map(() => ({
text: typed,
rangeLength: 0,
range: { start: new Position(0, 0) },
})),
}),
};
}
test("closes the tag at a single caret", async () => {
const run = scenario({ carets: [new Position(1, 8)], snippetFor: () => "$0</div>" });
await run.fire();
assert.equal(run.inserted.length, 1);
assert.equal(run.inserted[0].value, "$0</div>");
assert.deepEqual(
run.inserted[0].positions.map((p) => [p.line, p.character]),
[[1, 8]],
);
});
test("closes the tag at every caret in one insertion", async () => {
// One insertSnippet call is what keeps all the carets alive: inserting
// sequentially would collapse the selection to the first snippet.
const run = scenario({
carets: [new Position(1, 8), new Position(2, 8), new Position(3, 8)],
snippetFor: () => "$0</div>",
});
await run.fire();
assert.equal(run.asked.length, 3);
assert.equal(run.inserted.length, 1);
assert.deepEqual(
run.inserted[0].positions.map((p) => [p.line, p.character]),
[
[1, 8],
[2, 8],
[3, 8],
],
);
});
test("asks about each caret's own position rather than the change ranges", async () => {
// Every contentChange above reports (0, 0). Using those would query and
// insert at the wrong offsets once more than one caret is on a line.
const run = scenario({
carets: [new Position(4, 12), new Position(9, 3)],
snippetFor: () => "$0</p>",
});
await run.fire();
assert.deepEqual(
run.asked.map((p) => [p.line, p.character]),
[
[4, 12],
[9, 3],
],
);
});
test("declines when the carets want different closing tags", async () => {
const run = scenario({
carets: [new Position(1, 8), new Position(2, 8)],
snippetFor: (position) => (position.line === 1 ? "$0</div>" : "$0</span>"),
});
await run.fire();
assert.equal(run.inserted.length, 0);
});
test("declines when any caret has no tag to close", async () => {
const run = scenario({
carets: [new Position(1, 8), new Position(2, 8)],
snippetFor: (position) => (position.line === 1 ? "$0</br>" : null),
});
await run.fire();
assert.equal(run.inserted.length, 0);
});
test("declines a replaced selection", async () => {
const run = scenario({ carets: [new Position(1, 8)], snippetFor: () => "$0</div>" });
await changeListener({
document: { languageId: "wrn", version: 1, uri: { toString: () => "file:///a.wrn" } },
contentChanges: [{ text: ">", rangeLength: 3, range: { start: new Position(1, 5) } }],
});
assert.equal(run.inserted.length, 0);
});
+39 -46
View File
@@ -2,54 +2,47 @@
const test = require("node:test");
const assert = require("node:assert");
const Module = require("node:module");
const { installVsCodeHost } = require("./vscode-host.js");
// Mock the vscode module for unit tests
const originalLoad = Module._load;
Module._load = function load(request, parent, isMain) {
if (request === "vscode") {
return {
Position: class Position {
constructor(line, character) {
this.line = line;
this.character = character;
}
},
Range: class Range {
constructor(start, end) {
this.start = start;
this.end = end;
}
},
CompletionItem: class CompletionItem {
constructor(label, kind) {
this.label = label;
this.kind = kind;
}
},
CompletionItemKind: {
Event: 23,
Property: 10,
Function: 12,
Keyword: 14,
Variable: 13,
},
SnippetString: class SnippetString {
constructor(text) {
this.value = text;
}
},
MarkdownString: class MarkdownString {
constructor(text) {
this.value = text;
}
},
};
}
return originalLoad.call(this, request, parent, isMain);
};
const restoreHost = installVsCodeHost({
Position: class Position {
constructor(line, character) {
this.line = line;
this.character = character;
}
},
Range: class Range {
constructor(start, end) {
this.start = start;
this.end = end;
}
},
CompletionItem: class CompletionItem {
constructor(label, kind) {
this.label = label;
this.kind = kind;
}
},
CompletionItemKind: {
Event: 23,
Property: 10,
Function: 12,
Keyword: 14,
Variable: 13,
},
SnippetString: class SnippetString {
constructor(text) {
this.value = text;
}
},
MarkdownString: class MarkdownString {
constructor(text) {
this.value = text;
}
},
});
const { isInsideViewBlock, provideCompletionItems } = require("../src/completion.js");
Module._load = originalLoad;
restoreHost();
const PAGE = `page Home {
view {
+3 -7
View File
@@ -2,17 +2,13 @@
const assert = require("node:assert");
const { test } = require("node:test");
const Module = require("node:module");
const { installVsCodeHost } = require("./vscode-host.js");
// These extraction helpers are pure, but their module also registers VS Code
// providers at runtime. Supply a minimal host shim for unit tests.
const originalLoad = Module._load;
Module._load = function load(request, parent, isMain) {
if (request === "vscode") return {};
return originalLoad.call(this, request, parent, isMain);
};
const restoreHost = installVsCodeHost({});
const { extractRouteParams, extractStates } = require("../src/completion");
Module._load = originalLoad;
restoreHost();
test("extracts dynamic route params from filename", () => {
const document = {
+18 -24
View File
@@ -2,30 +2,24 @@
const assert = require("node:assert");
const { test } = require("node:test");
const Module = require("node:module");
const { installVsCodeHost } = require("./vscode-host.js");
const originalLoad = Module._load;
Module._load = function load(request, parent, isMain) {
if (request === "vscode") {
return {
Diagnostic: class Diagnostic {
constructor(range, message, severity) {
this.range = range;
this.message = message;
this.severity = severity;
}
},
DiagnosticSeverity: { Error: 0, Warning: 1 },
Range: class Range {
constructor(start, end) {
this.start = start;
this.end = end;
}
},
};
}
return originalLoad.call(this, request, parent, isMain);
};
const restoreHost = installVsCodeHost({
Diagnostic: class Diagnostic {
constructor(range, message, severity) {
this.range = range;
this.message = message;
this.severity = severity;
}
},
DiagnosticSeverity: { Error: 0, Warning: 1 },
Range: class Range {
constructor(start, end) {
this.start = start;
this.end = end;
}
},
});
const {
findTopLevelDeclaration,
maskLeadingTrivia,
@@ -34,7 +28,7 @@ const {
validateLayoutUsage,
validateRootMembers,
} = require("../src/diagnostics");
Module._load = originalLoad;
restoreHost();
function mockDocument() {
return {
+39
View File
@@ -0,0 +1,39 @@
"use strict";
/**
* Supply a stub `vscode` host so extension sources can be unit tested.
*
* These files run under `node --test` (see the package's test script), where
* patching `Module._load` is enough. A bare `bun test` from the repository
* root also picks them up by filename, and Bun resolves `require` through its
* own resolver without consulting `Module._load` -- so under Bun the same
* files failed with "Cannot find package 'vscode'". Registering a virtual
* module covers that case, leaving one shim that works under both runners.
*
* Returns a function restoring the original loader.
*/
function installVsCodeHost(stub) {
const Module = require("node:module");
if (typeof Bun !== "undefined") {
require("bun").plugin({
name: "vscode-host-stub",
setup(build) {
build.module("vscode", () => ({ exports: stub, loader: "object" }));
},
});
return () => {};
}
const originalLoad = Module._load;
Module._load = function load(request, parent, isMain) {
if (request === "vscode") return stub;
return originalLoad.call(this, request, parent, isMain);
};
return () => {
Module._load = originalLoad;
};
}
module.exports = { installVsCodeHost };