release: WRNexusJS 0.8.0
This commit is contained in:
@@ -1,5 +1,13 @@
|
||||
# Changelog
|
||||
|
||||
## 0.8.0
|
||||
|
||||
- Fixed excessive extension-host and language-server memory growth in large workspaces.
|
||||
- Activate only when a WRN document is used and removed the unused unrestricted workspace watcher.
|
||||
- Added bounded, ignored-directory-aware workspace indexing with a short-lived completion cache.
|
||||
- Debounced TypeScript diagnostics during typing, bounded open-document state, and skipped compiler
|
||||
graph creation for WRN files larger than 1 MiB.
|
||||
|
||||
## 0.5.1
|
||||
|
||||
- Updated compiler-backed diagnostics for native array and object state values.
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# WRNexus Language Support
|
||||
|
||||
For AI tools that support Model Context Protocol, run `wrnexus mcp <app-dir>` (or
|
||||
`bunx wrnexus-mcp --root=<app-dir>`). The same read-only framework context works from VS Code,
|
||||
JetBrains IDEs, Neovim and standalone MCP clients.
|
||||
|
||||
Complete VS Code language support for [WRNexus](https://wrnexusjs.dev) `.wrn` files.
|
||||
|
||||
WRNexus is an SSR-first, Bun-powered full-stack framework with reactive components, layouts, colocated APIs, browser functions, lifecycle hooks, state watchers, client-side navigation, and reusable UI components.
|
||||
@@ -851,3 +855,13 @@ bun run build:compiler
|
||||
Without the compiler bundle, syntax highlighting, snippets, formatting, and autocomplete continue to work, but compiler diagnostics are unavailable.
|
||||
|
||||
See the [Marketplace publishing guide](https://git.workroot.in/WorkRoot/WRNexusJS/src/branch/main/editors/vscode/PUBLISHING.md) for publishing instructions.
|
||||
The extension now runs the same bundled `@wrnexus/language-server` used by other
|
||||
editors. Diagnostics, accessibility checks, formatting, completion, hover,
|
||||
definitions, references, rename, symbols, and quick fixes therefore share the
|
||||
canonical parser. Set `wrnexus.languageServer.enable` to `false` only to use the
|
||||
legacy in-extension fallback providers.
|
||||
|
||||
The extension activates only after a WRN document is used. Workspace indexing ignores dependency,
|
||||
generated, build, cache, and coverage directories; applies file/count/size limits; and reuses a
|
||||
short-lived bounded index. Type diagnostics are debounced while typing to prevent repeated
|
||||
TypeScript compiler graphs from causing memory spikes.
|
||||
|
||||
Generated
+58
-13
@@ -1,13 +1,16 @@
|
||||
{
|
||||
"name": "wrnexus",
|
||||
"version": "0.7.0",
|
||||
"version": "0.8.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "wrnexus",
|
||||
"version": "0.7.0",
|
||||
"version": "0.8.0",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"dependencies": {
|
||||
"vscode-languageclient": "^10.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vscode/vsce": "^3.9.2"
|
||||
},
|
||||
@@ -866,7 +869,6 @@
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
@@ -938,16 +940,15 @@
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "5.0.7",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz",
|
||||
"integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==",
|
||||
"dev": true,
|
||||
"version": "5.0.9",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
|
||||
"integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
@@ -1559,9 +1560,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/fast-uri": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz",
|
||||
"integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==",
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz",
|
||||
"integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -2437,7 +2438,6 @@
|
||||
"version": "10.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
|
||||
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^5.0.5"
|
||||
@@ -3111,7 +3111,6 @@
|
||||
"version": "7.8.5",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
|
||||
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
@@ -3705,6 +3704,52 @@
|
||||
"url": "https://bevry.me/fund"
|
||||
}
|
||||
},
|
||||
"node_modules/vscode-jsonrpc": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-9.0.1.tgz",
|
||||
"integrity": "sha512-rfuA6T75H6m5EkbhtEPzre9pT0HPcDI2MMy4+nPFIBks5J8JBAUHD4tRYSgaBOijIEC7SRkC1kKyXTLqbmh9jw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vscode-languageclient": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-10.1.0.tgz",
|
||||
"integrity": "sha512-XXRx6lqVitQy/oOLr9MfNYRG+MbQkhXkDaxbQMiKxEm8zZNfheRFUKNb8UYNh2stn9btl2wQM5wZFJjJvoc+jA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"minimatch": "^10.2.5",
|
||||
"semver": "^7.8.1",
|
||||
"vscode-languageserver-protocol": "3.18.2",
|
||||
"vscode-languageserver-textdocument": "1.0.13"
|
||||
},
|
||||
"engines": {
|
||||
"vscode": "^1.91.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vscode-languageserver-protocol": {
|
||||
"version": "3.18.2",
|
||||
"resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.18.2.tgz",
|
||||
"integrity": "sha512-XRyDbT0Pp3sSNti3JmxVEUMySWCSi1hhM+/KUlCy1hV1zmrqpM1OwO12EAki8blhmLuIMpaJrYbo0OzGVfK2Qg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"vscode-jsonrpc": "9.0.1",
|
||||
"vscode-languageserver-types": "3.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vscode-languageserver-textdocument": {
|
||||
"version": "1.0.13",
|
||||
"resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.13.tgz",
|
||||
"integrity": "sha512-nx0ZHwMGIsVkzFG3/VLeJYBLTaFBRuNdGDvevvjuoayU5EOS2fEYazOhtCM3PI9ClMMg5igc0uwXtAq4tJj+Dw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vscode-languageserver-types": {
|
||||
"version": "3.18.0",
|
||||
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.18.0.tgz",
|
||||
"integrity": "sha512-8TsGPNMIMiiBdkORgRSvLjuiEIiAFtO+KssmYWxQ+uSVvlf7RjK8YKCOjPzZ+YA04jXEV7+7LvkSmHkhpNS99g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/whatwg-encoding": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "wrnexus",
|
||||
"displayName": "WRNexus Language Support",
|
||||
"description": "Complete WRNexus v0.6 language support for typed imports, props, state, outputs, runtime functions, stores, diagnostics, formatting, navigation, and migration assistance.",
|
||||
"version": "0.7.0",
|
||||
"version": "0.8.0",
|
||||
"publisher": "wrnexus",
|
||||
"private": true,
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
@@ -50,11 +50,9 @@
|
||||
"csr",
|
||||
"bun"
|
||||
],
|
||||
"main": "./src/extension.js",
|
||||
"main": "./src/extension.bundle.cjs",
|
||||
"activationEvents": [
|
||||
"onLanguage:wrn",
|
||||
"workspaceContains:**/*.wrn",
|
||||
"onStartupFinished"
|
||||
"onLanguage:wrn"
|
||||
],
|
||||
"contributes": {
|
||||
"languages": [
|
||||
@@ -111,6 +109,12 @@
|
||||
"scope": "resource",
|
||||
"description": "Enable WRNexus editor and compiler diagnostics for .wrn files."
|
||||
},
|
||||
"wrnexus.languageServer.enable": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"scope": "window",
|
||||
"description": "Use the shared WRNexus Language Server for diagnostics, formatting, completion, navigation, rename, and quick fixes."
|
||||
},
|
||||
"wrnexus.format.enable": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
@@ -256,9 +260,11 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build:compiler": "node ../../scripts/build-editor-compiler.mjs",
|
||||
"build": "bun run build:compiler",
|
||||
"build:language-server": "node ../../scripts/build-editor-language-server.mjs",
|
||||
"build:extension": "node ../../scripts/build-editor-extension.mjs",
|
||||
"build": "bun run build:compiler && bun run build:language-server && bun run build:extension",
|
||||
"test": "node --test test/*.test.js",
|
||||
"validate": "node test/validate.mjs",
|
||||
"validate": "node test/validate.mjs && node ../../scripts/build-editor-compiler.mjs --check && node ../../scripts/build-editor-language-server.mjs --check && node ../../scripts/build-editor-extension.mjs --check",
|
||||
"check": "bun run build && bun run test && bun run validate",
|
||||
"vscode:prepublish": "bun run check",
|
||||
"package": "vsce package --no-dependencies",
|
||||
@@ -267,5 +273,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vscode/vsce": "^3.9.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"vscode-languageclient": "^10.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
+1748
-119
File diff suppressed because it is too large
Load Diff
@@ -236,7 +236,11 @@ function registerComponentIntelligence(context) {
|
||||
diagnostics.set(document.uri, results);
|
||||
};
|
||||
|
||||
const watcher = vscode.workspace.createFileSystemWatcher("**/*.wrn");
|
||||
// Watch only application sources. A workspace-wide **/*.wrn watcher can
|
||||
// recursively subscribe to dependency and generated trees in large repos.
|
||||
const watchers = (vscode.workspace.workspaceFolders || []).map((folder) =>
|
||||
vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(folder, "app/**/*.wrn")),
|
||||
);
|
||||
const refreshOpenDocuments = () => {
|
||||
invalidateCatalog();
|
||||
for (const document of vscode.workspace.textDocuments) void update(document);
|
||||
@@ -244,10 +248,12 @@ function registerComponentIntelligence(context) {
|
||||
|
||||
context.subscriptions.push(
|
||||
diagnostics,
|
||||
watcher,
|
||||
watcher.onDidCreate(refreshOpenDocuments),
|
||||
watcher.onDidChange(refreshOpenDocuments),
|
||||
watcher.onDidDelete(refreshOpenDocuments),
|
||||
...watchers,
|
||||
...watchers.flatMap((watcher) => [
|
||||
watcher.onDidCreate(refreshOpenDocuments),
|
||||
watcher.onDidChange(refreshOpenDocuments),
|
||||
watcher.onDidDelete(refreshOpenDocuments),
|
||||
]),
|
||||
vscode.workspace.onDidOpenTextDocument(update),
|
||||
vscode.workspace.onDidChangeTextDocument((event) => void update(event.document)),
|
||||
vscode.workspace.onDidSaveTextDocument((document) => {
|
||||
|
||||
@@ -254,6 +254,16 @@ function validateBalancedCharacters(document, source) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Apostrophes in rendered copy (for example, "person's name") are text,
|
||||
// not the beginning of a WRN/HTML quoted value.
|
||||
if (
|
||||
character === "'" &&
|
||||
/[\p{L}\p{N}]/u.test(source[index - 1] ?? "") &&
|
||||
/[\p{L}\p{N}]/u.test(source[index + 1] ?? "")
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (character === '"' || character === "'") {
|
||||
quote = character;
|
||||
continue;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,8 @@
|
||||
"use strict";
|
||||
|
||||
const vscode = require("vscode");
|
||||
const path = require("node:path");
|
||||
const { LanguageClient, TransportKind } = require("vscode-languageclient/node");
|
||||
|
||||
const { formatWrn } = require("./formatter");
|
||||
|
||||
@@ -329,7 +331,7 @@ async function recoverWrnLanguage(document) {
|
||||
*
|
||||
* @param {vscode.ExtensionContext} context
|
||||
*/
|
||||
function activate(context) {
|
||||
async function activate(context) {
|
||||
for (const document of vscode.workspace.textDocuments) {
|
||||
void recoverWrnLanguage(document);
|
||||
}
|
||||
@@ -340,12 +342,43 @@ function activate(context) {
|
||||
}),
|
||||
);
|
||||
|
||||
registerDiagnostics(context);
|
||||
registerCompilerDiagnostics(context);
|
||||
registerCompletionProvider(context);
|
||||
registerDefinitionProvider(context);
|
||||
registerComponentIntelligence(context);
|
||||
registerFormatter(context);
|
||||
const useLanguageServer = vscode.workspace
|
||||
.getConfiguration("wrnexus")
|
||||
.get("languageServer.enable", true);
|
||||
let languageServerStarted = false;
|
||||
if (useLanguageServer) {
|
||||
const module = path.join(context.extensionPath, "src", "language-server.cjs");
|
||||
const serverOptions = {
|
||||
run: { module, transport: TransportKind.stdio },
|
||||
debug: { module, transport: TransportKind.stdio, options: { execArgv: ["--nolazy"] } },
|
||||
};
|
||||
const client = new LanguageClient(
|
||||
"wrnexusLanguageServer",
|
||||
"WRNexus Language Server",
|
||||
serverOptions,
|
||||
{
|
||||
documentSelector: [{ scheme: "file", language: WRN_LANGUAGE_ID }],
|
||||
},
|
||||
);
|
||||
try {
|
||||
await client.start();
|
||||
languageServerStarted = true;
|
||||
context.subscriptions.push({ dispose: () => void client.stop() });
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"[wrnexus] language server failed to start; using built-in providers.",
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!languageServerStarted) {
|
||||
registerDiagnostics(context);
|
||||
registerCompilerDiagnostics(context);
|
||||
registerCompletionProvider(context);
|
||||
registerDefinitionProvider(context);
|
||||
registerComponentIntelligence(context);
|
||||
registerFormatter(context);
|
||||
}
|
||||
registerSemanticTokens(context);
|
||||
registerV060LanguageFeatures(context);
|
||||
}
|
||||
|
||||
@@ -1,777 +1,7 @@
|
||||
"use strict";
|
||||
|
||||
const VOID_ELEMENTS = new Set([
|
||||
"area",
|
||||
"base",
|
||||
"br",
|
||||
"col",
|
||||
"embed",
|
||||
"hr",
|
||||
"img",
|
||||
"input",
|
||||
"link",
|
||||
"meta",
|
||||
"param",
|
||||
"source",
|
||||
"track",
|
||||
"wbr",
|
||||
]);
|
||||
// Compatibility facade. The formatter implementation is owned by
|
||||
// @wrnexus/syntax and bundled into compiler.cjs for the extension runtime.
|
||||
const { formatWrn } = require("./compiler.cjs");
|
||||
|
||||
function splitPropDeclarations(value) {
|
||||
const declarations = [];
|
||||
let start = 0;
|
||||
let index = 0;
|
||||
let quote = null;
|
||||
let escaped = false;
|
||||
let square = 0;
|
||||
let brace = 0;
|
||||
let paren = 0;
|
||||
let segmentHasColon = false;
|
||||
let segmentHasEquals = false;
|
||||
|
||||
const isIdentifierStart = (character) => /[A-Za-z_]/.test(character || "");
|
||||
const isIdentifierPart = (character) => /[A-Za-z0-9_]/.test(character || "");
|
||||
|
||||
const beginsDeclaration = (position) => {
|
||||
let cursor = position;
|
||||
while (cursor < value.length && /[ \t]/.test(value[cursor])) cursor += 1;
|
||||
if (value.slice(cursor).startsWith("@event")) {
|
||||
cursor += "@event".length;
|
||||
if (!/\s/.test(value[cursor] || "")) return false;
|
||||
while (cursor < value.length && /\s/.test(value[cursor])) cursor += 1;
|
||||
if (!isIdentifierStart(value[cursor])) return false;
|
||||
cursor += 1;
|
||||
while (cursor < value.length && isIdentifierPart(value[cursor])) cursor += 1;
|
||||
while (cursor < value.length && /[ \t]/.test(value[cursor])) cursor += 1;
|
||||
return value[cursor] === "=" ? "=" : null;
|
||||
}
|
||||
if (!isIdentifierStart(value[cursor])) return false;
|
||||
cursor += 1;
|
||||
while (cursor < value.length && isIdentifierPart(value[cursor])) cursor += 1;
|
||||
if (value[cursor] === "?") cursor += 1;
|
||||
while (cursor < value.length && /[ \t]/.test(value[cursor])) cursor += 1;
|
||||
return value[cursor] === "=" || value[cursor] === ":" ? value[cursor] : null;
|
||||
};
|
||||
|
||||
while (index < value.length) {
|
||||
const character = value[index];
|
||||
|
||||
if (quote !== null) {
|
||||
if (escaped) escaped = false;
|
||||
else if (character === "\\") escaped = true;
|
||||
else if (character === quote) quote = null;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (character === '"' || character === "'" || character === "`") {
|
||||
quote = character;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (character === "[") square += 1;
|
||||
else if (character === "]" && square > 0) square -= 1;
|
||||
else if (character === "{") brace += 1;
|
||||
else if (character === "}" && brace > 0) brace -= 1;
|
||||
else if (character === "(") paren += 1;
|
||||
else if (character === ")" && paren > 0) paren -= 1;
|
||||
|
||||
const topLevel = square === 0 && brace === 0 && paren === 0;
|
||||
if (topLevel && character === ":") segmentHasColon = true;
|
||||
if (topLevel && character === "=") segmentHasEquals = true;
|
||||
|
||||
const candidateDelimiter = topLevel && /\s/.test(character) ? beginsDeclaration(index) : null;
|
||||
const beginsNext =
|
||||
candidateDelimiter === ":" ||
|
||||
(candidateDelimiter === "=" && (segmentHasEquals || !segmentHasColon));
|
||||
|
||||
if (
|
||||
topLevel &&
|
||||
/\s/.test(character) &&
|
||||
value.slice(start, index).trim() !== "@event" &&
|
||||
beginsNext
|
||||
) {
|
||||
const declaration = value.slice(start, index).trim();
|
||||
if (declaration) declarations.push(declaration);
|
||||
while (index < value.length && /\s/.test(value[index])) index += 1;
|
||||
start = index;
|
||||
segmentHasColon = false;
|
||||
segmentHasEquals = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
index += 1;
|
||||
}
|
||||
|
||||
const declaration = value.slice(start).trim();
|
||||
if (declaration) declarations.push(declaration);
|
||||
return declarations;
|
||||
}
|
||||
|
||||
function splitOutputDeclarations(value) {
|
||||
const declarations = [];
|
||||
let start = 0;
|
||||
let paren = 0;
|
||||
let angle = 0;
|
||||
let square = 0;
|
||||
let quote = null;
|
||||
let escaped = false;
|
||||
const startsOutput = (position) => {
|
||||
let cursor = position;
|
||||
while (cursor < value.length && /\s/.test(value[cursor])) cursor += 1;
|
||||
if (!/[A-Za-z_$]/.test(value[cursor] || "")) return false;
|
||||
cursor += 1;
|
||||
while (cursor < value.length && /[A-Za-z0-9_$]/.test(value[cursor] || "")) cursor += 1;
|
||||
while (cursor < value.length && /\s/.test(value[cursor])) cursor += 1;
|
||||
return value[cursor] === "(";
|
||||
};
|
||||
for (let index = 0; index < value.length; index += 1) {
|
||||
const character = value[index];
|
||||
if (quote !== null) {
|
||||
if (escaped) escaped = false;
|
||||
else if (character === "\\") escaped = true;
|
||||
else if (character === quote) quote = null;
|
||||
continue;
|
||||
}
|
||||
if (character === '"' || character === "'" || character === "`") {
|
||||
quote = character;
|
||||
continue;
|
||||
}
|
||||
if (character === "(") paren += 1;
|
||||
else if (character === ")" && paren > 0) paren -= 1;
|
||||
else if (character === "[") square += 1;
|
||||
else if (character === "]" && square > 0) square -= 1;
|
||||
else if (character === "<") angle += 1;
|
||||
else if (character === ">" && angle > 0) angle -= 1;
|
||||
if (paren === 0 && square === 0 && angle === 0 && /\s/.test(character) && startsOutput(index)) {
|
||||
const declaration = value.slice(start, index).trim();
|
||||
if (declaration) declarations.push(declaration);
|
||||
while (index < value.length && /\s/.test(value[index])) index += 1;
|
||||
start = index;
|
||||
index -= 1;
|
||||
}
|
||||
}
|
||||
const finalDeclaration = value.slice(start).trim();
|
||||
if (finalDeclaration) declarations.push(finalDeclaration);
|
||||
return declarations;
|
||||
}
|
||||
|
||||
function formatInlineDeclarationBlock(value, unit, depth) {
|
||||
const match = /^(props|state|computed|outputs)\s*\{([\s\S]*)\}$/.exec(value.trim());
|
||||
if (!match) return null;
|
||||
const declarations =
|
||||
match[1] === "outputs"
|
||||
? splitOutputDeclarations(match[2].trim())
|
||||
: splitPropDeclarations(match[2].trim());
|
||||
return [
|
||||
`${unit.repeat(depth)}${match[1]} {`,
|
||||
...declarations.map((declaration) => `${unit.repeat(depth + 1)}${declaration}`),
|
||||
`${unit.repeat(depth)}}`,
|
||||
];
|
||||
}
|
||||
|
||||
function formatInlinePropsBlock(value, unit, depth) {
|
||||
const match = /^props\s*\{([\s\S]*)\}$/.exec(value.trim());
|
||||
if (!match) return null;
|
||||
|
||||
const declarations = splitPropDeclarations(match[1].trim());
|
||||
if (declarations.length === 0) {
|
||||
return [`${unit.repeat(depth)}props {`, `${unit.repeat(depth)}}`];
|
||||
}
|
||||
|
||||
return [
|
||||
`${unit.repeat(depth)}props {`,
|
||||
...declarations.map((declaration) => `${unit.repeat(depth + 1)}${declaration}`),
|
||||
`${unit.repeat(depth)}}`,
|
||||
];
|
||||
}
|
||||
|
||||
function findOpeningTagEnd(value) {
|
||||
let quote = null;
|
||||
let escaped = false;
|
||||
|
||||
for (let index = 0; index < value.length; index += 1) {
|
||||
const character = value[index];
|
||||
|
||||
if (quote !== null) {
|
||||
if (escaped) {
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (character === "\\") {
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (character === quote) {
|
||||
quote = null;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (character === '"' || character === "'") {
|
||||
quote = character;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (character === ">") {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
function parseAttributes(value) {
|
||||
const attributes = [];
|
||||
let index = 0;
|
||||
|
||||
while (index < value.length) {
|
||||
while (index < value.length && /\s/.test(value[index])) index += 1;
|
||||
if (index >= value.length) break;
|
||||
|
||||
const start = index;
|
||||
while (index < value.length && !/[\s=]/.test(value[index])) index += 1;
|
||||
while (index < value.length && /\s/.test(value[index])) index += 1;
|
||||
|
||||
if (value[index] === "=") {
|
||||
index += 1;
|
||||
while (index < value.length && /\s/.test(value[index])) index += 1;
|
||||
|
||||
const quote = value[index];
|
||||
if (quote === '"' || quote === "'") {
|
||||
index += 1;
|
||||
let escaped = false;
|
||||
while (index < value.length) {
|
||||
const character = value[index++];
|
||||
if (escaped) escaped = false;
|
||||
else if (character === "\\") escaped = true;
|
||||
else if (character === quote) break;
|
||||
}
|
||||
} else if (value[index] === "{") {
|
||||
let depth = 0;
|
||||
let expressionQuote = null;
|
||||
let escaped = false;
|
||||
while (index < value.length) {
|
||||
const character = value[index++];
|
||||
if (expressionQuote !== null) {
|
||||
if (escaped) escaped = false;
|
||||
else if (character === "\\") escaped = true;
|
||||
else if (character === expressionQuote) expressionQuote = null;
|
||||
continue;
|
||||
}
|
||||
if (character === '"' || character === "'" || character === "`") {
|
||||
expressionQuote = character;
|
||||
} else if (character === "{") {
|
||||
depth += 1;
|
||||
} else if (character === "}" && --depth === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
while (index < value.length && !/\s/.test(value[index])) index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
const attribute = value.slice(start, index).trim();
|
||||
if (attribute) attributes.push(attribute);
|
||||
}
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
||||
function parseStructuredAttribute(attribute) {
|
||||
const match = /^([^\s=]+)\s*=\s*\{([\s\S]*)\}$/.exec(attribute);
|
||||
if (!match) return null;
|
||||
|
||||
const expression = match[2].trim();
|
||||
if (!expression.startsWith("[") && !expression.startsWith("{")) return null;
|
||||
|
||||
try {
|
||||
return {
|
||||
name: match[1],
|
||||
value: JSON.parse(expression),
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function formatAttribute(attribute, indentation, unit) {
|
||||
const structured = parseStructuredAttribute(attribute);
|
||||
if (!structured) return [`${indentation}${attribute}`];
|
||||
|
||||
const jsonLines = JSON.stringify(structured.value, null, unit).split("\n");
|
||||
if (jsonLines.length === 1) {
|
||||
return [`${indentation}${structured.name}={${jsonLines[0]}}`];
|
||||
}
|
||||
|
||||
return [
|
||||
`${indentation}${structured.name}={${jsonLines[0]}`,
|
||||
...jsonLines.slice(1, -1).map((line) => `${indentation}${line}`),
|
||||
`${indentation}${jsonLines.at(-1)}}`,
|
||||
];
|
||||
}
|
||||
|
||||
function escapeRegExp(value) {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
function parseOpeningTag(value) {
|
||||
const endIndex = findOpeningTagEnd(value);
|
||||
|
||||
if (endIndex === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const openingPart = value.slice(0, endIndex + 1);
|
||||
const remainder = value.slice(endIndex + 1).trim();
|
||||
|
||||
const match = /^<([A-Za-z][\w$:.-]*)([\s\S]*?)(\/?)>$/.exec(openingPart);
|
||||
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tagName = match[1];
|
||||
const attributes = parseAttributes(match[2].trim());
|
||||
const selfClosing = match[3] === "/";
|
||||
|
||||
const escapedTagName = escapeRegExp(tagName);
|
||||
|
||||
const immediateClosing = new RegExp(`^<\\/${escapedTagName}\\s*>`, "i").test(remainder);
|
||||
|
||||
const trailingClosingMatch = new RegExp(`^([\\s\\S]*?)<\\/${escapedTagName}\\s*>$`, "i").exec(
|
||||
remainder,
|
||||
);
|
||||
|
||||
const trailingClosing = trailingClosingMatch !== null;
|
||||
|
||||
const inlineContent = trailingClosing ? trailingClosingMatch[1].trim() : "";
|
||||
|
||||
const inlineClosing = trailingClosing && inlineContent.length === 0;
|
||||
|
||||
const closesInRemainder = immediateClosing || trailingClosing;
|
||||
|
||||
return {
|
||||
tagName,
|
||||
attributes,
|
||||
selfClosing,
|
||||
inlineClosing,
|
||||
immediateClosing,
|
||||
trailingClosing,
|
||||
inlineContent,
|
||||
closesInRemainder,
|
||||
remainder,
|
||||
};
|
||||
}
|
||||
|
||||
function formatOpeningTag(value, unit, depth, printWidth = 100, multilineAttributes = true) {
|
||||
const parsed = parseOpeningTag(value);
|
||||
|
||||
if (!parsed) {
|
||||
return {
|
||||
lines: [`${unit.repeat(depth)}${value.trim()}`],
|
||||
opensElement: false,
|
||||
};
|
||||
}
|
||||
|
||||
const baseIndent = unit.repeat(depth);
|
||||
const childIndent = unit.repeat(depth + 1);
|
||||
|
||||
const normalizedOpening =
|
||||
`<${parsed.tagName}` +
|
||||
`${parsed.attributes.length ? ` ${parsed.attributes.join(" ")}` : ""}` +
|
||||
`${parsed.selfClosing ? " /" : ""}>`;
|
||||
|
||||
const normalizedSingleLine = `${normalizedOpening}${parsed.remainder}`;
|
||||
|
||||
const shouldBreak =
|
||||
value.includes("\n") ||
|
||||
(multilineAttributes && parsed.attributes.length > 0) ||
|
||||
baseIndent.length + normalizedSingleLine.length > printWidth;
|
||||
|
||||
const opensElement =
|
||||
!parsed.selfClosing &&
|
||||
!parsed.closesInRemainder &&
|
||||
!VOID_ELEMENTS.has(parsed.tagName.toLowerCase());
|
||||
|
||||
if (!shouldBreak) {
|
||||
return {
|
||||
lines: [`${baseIndent}${normalizedSingleLine}`],
|
||||
opensElement,
|
||||
};
|
||||
}
|
||||
|
||||
const lines = [
|
||||
`${baseIndent}<${parsed.tagName}`,
|
||||
...parsed.attributes.flatMap((attribute) => formatAttribute(attribute, childIndent, unit)),
|
||||
];
|
||||
|
||||
if (parsed.selfClosing) {
|
||||
lines.push(`${baseIndent}/>`);
|
||||
return {
|
||||
lines,
|
||||
opensElement,
|
||||
};
|
||||
}
|
||||
|
||||
lines.push(`${baseIndent}>`);
|
||||
|
||||
if (parsed.trailingClosing) {
|
||||
if (parsed.inlineContent) {
|
||||
lines.push(`${childIndent}${parsed.inlineContent}`);
|
||||
}
|
||||
|
||||
lines.push(`${baseIndent}</${parsed.tagName}>`);
|
||||
} else if (parsed.remainder) {
|
||||
lines.push(`${parsed.immediateClosing ? baseIndent : childIndent}${parsed.remainder}`);
|
||||
}
|
||||
|
||||
return {
|
||||
lines,
|
||||
opensElement,
|
||||
};
|
||||
}
|
||||
|
||||
function isMultilineOpeningTagStart(value) {
|
||||
if (!value.startsWith("<")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
value.startsWith("</") ||
|
||||
value.startsWith("<!--") ||
|
||||
value.startsWith("<!") ||
|
||||
value.startsWith("<?")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return findOpeningTagEnd(value) === -1;
|
||||
}
|
||||
|
||||
function isClosingTag(value) {
|
||||
return /^<\/[A-Za-z][\w$:.-]*\s*>/.test(value);
|
||||
}
|
||||
|
||||
function isControlBlockOpen(value) {
|
||||
return /^\{#(?:if|each)\b[\s\S]*\}$/.test(value);
|
||||
}
|
||||
|
||||
function isControlBlockMiddle(value) {
|
||||
return /^\{:(?:else(?:\s+if\b[\s\S]*)?|empty)\}$/.test(value);
|
||||
}
|
||||
|
||||
function isControlBlockClose(value) {
|
||||
return /^\{\/(?:if|each)\}$/.test(value);
|
||||
}
|
||||
|
||||
function countLeadingClosingBraces(value) {
|
||||
let index = 0;
|
||||
let count = 0;
|
||||
|
||||
while (index < value.length) {
|
||||
while (index < value.length && /\s/.test(value[index])) {
|
||||
index += 1;
|
||||
}
|
||||
|
||||
if (value[index] !== "}" && value[index] !== "]") {
|
||||
break;
|
||||
}
|
||||
|
||||
count += 1;
|
||||
index += 1;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count braces outside strings and HTML comments.
|
||||
*
|
||||
* This supports WRN blocks, function bodies, lifecycle hooks,
|
||||
* watcher bodies and multiline JavaScript object literals.
|
||||
*/
|
||||
function countStructuralBraces(value) {
|
||||
let openings = 0;
|
||||
let closings = 0;
|
||||
|
||||
let quote = null;
|
||||
let escaped = false;
|
||||
let htmlComment = false;
|
||||
|
||||
for (let index = 0; index < value.length; index += 1) {
|
||||
if (!quote && !htmlComment && value.startsWith("<!--", index)) {
|
||||
htmlComment = true;
|
||||
index += 3;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (htmlComment && value.startsWith("-->", index)) {
|
||||
htmlComment = false;
|
||||
index += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (htmlComment) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const character = value[index];
|
||||
|
||||
if (quote !== null) {
|
||||
if (escaped) {
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (character === "\\") {
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (character === quote) {
|
||||
quote = null;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (character === '"' || character === "'" || character === "`") {
|
||||
quote = character;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (character === "{") {
|
||||
openings += 1;
|
||||
} else if (character === "}") {
|
||||
closings += 1;
|
||||
} else if (character === "[") {
|
||||
openings += 1;
|
||||
} else if (character === "]") {
|
||||
closings += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
openings,
|
||||
closings,
|
||||
};
|
||||
}
|
||||
|
||||
function collectOpeningTag(inputLines, startIndex) {
|
||||
const collected = [inputLines[startIndex].trim()];
|
||||
|
||||
let index = startIndex;
|
||||
|
||||
while (index + 1 < inputLines.length) {
|
||||
const joined = collected.join(" ");
|
||||
|
||||
if (findOpeningTagEnd(joined) !== -1) {
|
||||
break;
|
||||
}
|
||||
|
||||
index += 1;
|
||||
collected.push(inputLines[index].trim());
|
||||
}
|
||||
|
||||
return {
|
||||
// Preserve the fact that the opening tag was already multiline so a
|
||||
// second formatter pass cannot collapse it back to one line.
|
||||
value: collected.join("\n"),
|
||||
endIndex: index,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Put WRN template control markers on their own lines before indentation.
|
||||
*
|
||||
* Authors commonly write compact fragments such as
|
||||
* `{#if loading}<span>…</span>{/if}`. Treating that as one line prevents the
|
||||
* normal HTML and control-block formatters from seeing its structure.
|
||||
*/
|
||||
function expandInlineControlBlocks(lines) {
|
||||
const marker =
|
||||
/(\{#(?:if|each)\b[^}]*\}|\{:(?:else(?:\s+if\b[^}]*)?|empty)\}|\{\/(?:if|each)\})/g;
|
||||
|
||||
return lines.flatMap((line) => {
|
||||
if (!marker.test(line)) return [line];
|
||||
marker.lastIndex = 0;
|
||||
|
||||
const indentation = line.match(/^\s*/)?.[0] ?? "";
|
||||
const segments = line
|
||||
.split(marker)
|
||||
.map((segment) => segment.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
return segments.map((segment) => `${indentation}${segment}`);
|
||||
});
|
||||
}
|
||||
|
||||
function expandStructuredStateDeclarations(lines, unit) {
|
||||
return lines.flatMap((line) => {
|
||||
const match = /^(\s*state\s+[A-Za-z_$][\w$]*\s*=\s*)([\\[{][\s\S]*)$/.exec(line);
|
||||
if (!match) return [line];
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(match[2].trim());
|
||||
const jsonLines = JSON.stringify(parsed, null, unit).split("\n");
|
||||
if (jsonLines.length === 1) return [`${match[1]}${jsonLines[0]}`];
|
||||
|
||||
const leading = match[1].match(/^\s*/)?.[0] ?? "";
|
||||
return [
|
||||
`${match[1]}${jsonLines[0]}`,
|
||||
...jsonLines.slice(1).map((jsonLine) => `${leading}${jsonLine}`),
|
||||
];
|
||||
} catch {
|
||||
return [line];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function formatWrn(source, options = {}) {
|
||||
const unit = options.insertSpaces === false ? "\t" : " ".repeat(options.tabSize ?? 4);
|
||||
|
||||
const printWidth = options.printWidth ?? 100;
|
||||
|
||||
const multilineAttributes = options.multilineAttributes !== false;
|
||||
|
||||
let codeDepth = 0;
|
||||
let htmlDepth = 0;
|
||||
let controlDepth = 0;
|
||||
let index = 0;
|
||||
|
||||
const sourceLines = source.replace(/\r\n/g, "\n").split("\n");
|
||||
const inputLines = expandInlineControlBlocks(
|
||||
expandStructuredStateDeclarations(sourceLines, unit),
|
||||
);
|
||||
|
||||
const output = [];
|
||||
|
||||
let previousWasBlank = false;
|
||||
|
||||
while (index < inputLines.length) {
|
||||
const originalLine = inputLines[index];
|
||||
|
||||
let value = originalLine.trim();
|
||||
|
||||
if (value === "") {
|
||||
if (!previousWasBlank && output.length > 0) {
|
||||
output.push("");
|
||||
}
|
||||
|
||||
previousWasBlank = true;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
previousWasBlank = false;
|
||||
|
||||
if (/^import\b/.test(value)) {
|
||||
const importLines = [value];
|
||||
while (
|
||||
!/(?:\bfrom\s+)?["'][^"']+["']\s*;?$/.test(importLines[importLines.length - 1]) &&
|
||||
index + 1 < inputLines.length
|
||||
) {
|
||||
index += 1;
|
||||
importLines.push(inputLines[index].trim());
|
||||
}
|
||||
output.push(importLines[0], ...importLines.slice(1).map((line) => `${unit}${line}`));
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isMultilineOpeningTagStart(value)) {
|
||||
const collected = collectOpeningTag(inputLines, index);
|
||||
|
||||
value = collected.value;
|
||||
index = collected.endIndex;
|
||||
}
|
||||
|
||||
const inlineDeclaration = formatInlineDeclarationBlock(value, unit, codeDepth + htmlDepth);
|
||||
const inlineProps =
|
||||
inlineDeclaration ?? formatInlinePropsBlock(value, unit, codeDepth + htmlDepth);
|
||||
if (inlineProps) {
|
||||
output.push(...inlineProps);
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
const leadingClosingBraces = countLeadingClosingBraces(value);
|
||||
|
||||
const closesControlBlock = isControlBlockClose(value);
|
||||
|
||||
const continuesControlBlock = isControlBlockMiddle(value);
|
||||
|
||||
const lineControlDepth =
|
||||
closesControlBlock || continuesControlBlock ? Math.max(0, controlDepth - 1) : controlDepth;
|
||||
|
||||
const lineCodeDepth = Math.max(0, codeDepth - leadingClosingBraces);
|
||||
|
||||
let lineHtmlDepth = htmlDepth;
|
||||
|
||||
if (isClosingTag(value)) {
|
||||
lineHtmlDepth = Math.max(0, htmlDepth - 1);
|
||||
}
|
||||
|
||||
const depth = lineCodeDepth + lineHtmlDepth + lineControlDepth;
|
||||
|
||||
if (
|
||||
value.startsWith("<") &&
|
||||
!value.startsWith("</") &&
|
||||
!value.startsWith("<!--") &&
|
||||
!value.startsWith("<!") &&
|
||||
!value.startsWith("<?")
|
||||
) {
|
||||
const formattedTag = formatOpeningTag(value, unit, depth, printWidth, multilineAttributes);
|
||||
|
||||
output.push(...formattedTag.lines);
|
||||
|
||||
if (formattedTag.opensElement) {
|
||||
htmlDepth += 1;
|
||||
}
|
||||
} else {
|
||||
output.push(`${unit.repeat(depth)}${value}`);
|
||||
}
|
||||
|
||||
if (isClosingTag(value)) {
|
||||
htmlDepth = lineHtmlDepth;
|
||||
}
|
||||
|
||||
const braces = countStructuralBraces(value);
|
||||
|
||||
codeDepth = Math.max(
|
||||
0,
|
||||
lineCodeDepth + braces.openings - Math.max(0, braces.closings - leadingClosingBraces),
|
||||
);
|
||||
|
||||
if (isControlBlockOpen(value) || continuesControlBlock) {
|
||||
controlDepth = lineControlDepth + 1;
|
||||
} else if (closesControlBlock) {
|
||||
controlDepth = lineControlDepth;
|
||||
}
|
||||
|
||||
index += 1;
|
||||
}
|
||||
|
||||
while (output.length > 0 && output[output.length - 1] === "") {
|
||||
output.pop();
|
||||
}
|
||||
|
||||
return `${output.join("\n")}\n`;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
countStructuralBraces,
|
||||
formatOpeningTag,
|
||||
formatAttribute,
|
||||
formatWrn,
|
||||
parseAttributes,
|
||||
parseOpeningTag,
|
||||
splitOutputDeclarations,
|
||||
splitPropDeclarations,
|
||||
};
|
||||
module.exports = { formatWrn };
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -63,7 +63,7 @@ function declarationRanges(document, name) {
|
||||
}
|
||||
|
||||
async function wrnDocuments() {
|
||||
const uris = await vscode.workspace.findFiles(WRN_GLOB, EXCLUDE_GLOB);
|
||||
const uris = await vscode.workspace.findFiles(WRN_GLOB, EXCLUDE_GLOB, 1_000);
|
||||
const open = new Map(
|
||||
vscode.workspace.textDocuments
|
||||
.filter((item) => item.languageId === "wrn")
|
||||
|
||||
@@ -96,6 +96,21 @@ component ThemeToggle {
|
||||
assert.deepEqual(diagnostics, []);
|
||||
});
|
||||
|
||||
test("ignores apostrophes in rendered HTML copy when checking balance", () => {
|
||||
const source = `import LanguageSwitcher from "@wrnexus/i18n/components/LanguageSwitcher.wrn"
|
||||
|
||||
page InternationalizationShowcase {
|
||||
view {
|
||||
<main>
|
||||
<p>Parameters such as a person's name are inserted safely.</p>
|
||||
<LanguageSwitcher />
|
||||
</main>
|
||||
}
|
||||
}`;
|
||||
|
||||
assert.deepEqual(validateBalancedCharacters(mockDocument(source), source), []);
|
||||
});
|
||||
|
||||
test("accepts all WRN 0.3 root members without false unknown-member errors", () => {
|
||||
const source = `page Dashboard {
|
||||
runtime = "universal"
|
||||
|
||||
@@ -73,9 +73,20 @@ try {
|
||||
manifest.activationEvents?.includes("onLanguage:wrn")
|
||||
? ok("extension activates for the wrn language")
|
||||
: bad("extension activates for the wrn language");
|
||||
manifest.activationEvents?.includes("onStartupFinished")
|
||||
? ok("extension can repair legacy associations at startup")
|
||||
: bad("extension can repair legacy associations at startup");
|
||||
manifest.activationEvents?.length === 1 && manifest.activationEvents[0] === "onLanguage:wrn"
|
||||
? ok("extension activates only when a WRN document is used")
|
||||
: bad("extension avoids workspace-wide startup activation");
|
||||
const extensionSource = readFileSync(join(root, "src/extension.js"), "utf8");
|
||||
!extensionSource.includes('createFileSystemWatcher("**/*.wrn")')
|
||||
? ok("language client avoids an unrestricted workspace watcher")
|
||||
: bad("language client avoids an unrestricted workspace watcher");
|
||||
manifest.contributes?.configuration?.properties?.["wrnexus.languageServer.enable"]?.default ===
|
||||
true
|
||||
? ok("shared language server is enabled by default")
|
||||
: bad("shared language server is enabled by default");
|
||||
readFileSync(join(root, "src/language-server.cjs"), "utf8").includes("WRNexus Language Server")
|
||||
? ok("bundled language server exists")
|
||||
: bad("bundled language server exists");
|
||||
language?.firstLine
|
||||
? ok("first-line language detection is configured")
|
||||
: bad("first-line language detection is configured");
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user