release: prepare WRNexusJS 0.8.8
Quality / quality (ubuntu-latest) (push) Failing after 9m49s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
2026-08-11 13:40:03 +05:30
parent 816594a58b
commit feff30a2b5
97 changed files with 769 additions and 10487 deletions
+2
View File
@@ -10,3 +10,5 @@ package-lock.json
bun.lock
../../**
node_modules/**
src/*.js
src/compiler.cjs
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "wrnexus",
"version": "0.8.7",
"version": "0.8.8",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "wrnexus",
"version": "0.8.7",
"version": "0.8.8",
"license": "SEE LICENSE IN LICENSE",
"dependencies": {
"vscode-languageclient": "^10.1.0"
+4 -4
View File
@@ -2,7 +2,7 @@
"name": "wrnexus",
"displayName": "WRNexus Language Support",
"description": "Complete WRNexus v0.8.3 language support for typed imports, props, state, outputs, runtime functions, stores, diagnostics, formatting, navigation, and migration assistance.",
"version": "0.8.7",
"version": "0.8.8",
"publisher": "wrnexus",
"private": true,
"license": "SEE LICENSE IN LICENSE",
@@ -275,9 +275,9 @@
"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",
"publish": "vsce publish --no-dependencies",
"publish:azure": "vsce publish --no-dependencies --azure-credential"
"package": "vsce package --no-dependencies --no-rewrite-relative-links",
"publish": "vsce publish --no-dependencies --no-rewrite-relative-links",
"publish:azure": "vsce publish --no-dependencies --no-rewrite-relative-links --azure-credential"
},
"devDependencies": {
"@vscode/vsce": "^3.9.2"
File diff suppressed because it is too large Load Diff
+23 -575
View File
@@ -1,321 +1,18 @@
// @ts-check
"use strict";
const vscode = require("vscode");
const path = require("node:path");
const vscode = require("vscode");
const { LanguageClient, TransportKind } = require("vscode-languageclient/node");
const { formatWrn } = require("./formatter");
const { registerCompletionProvider } = require("./completion");
const { registerDefinitionProvider } = require("./definition");
const { registerComponentIntelligence } = require("./component-intelligence");
const { registerDiagnostics } = require("./diagnostics");
const { registerV060LanguageFeatures } = require("./v060-language");
/**
* The WRN compiler is bundled to CommonJS using:
*
* bun run build:compiler
*
* Loading is optional so highlighting, formatting, snippets,
* autocomplete and navigation continue working when the compiler
* bundle is temporarily unavailable.
*/
let compiler = null;
try {
compiler = require("./compiler.cjs");
} catch (error) {
console.warn(
"[wrnexus] compiler bundle not found; compiler diagnostics disabled.",
error instanceof Error ? error.message : String(error),
);
}
const WRN_LANGUAGE_ID = "wrn";
const COMPILER_DIAGNOSTIC_COLLECTION = "wrnexus-compiler";
/** @type {LanguageClient | undefined} */
let client;
/**
* Register the WRN document formatter.
*
* @param {vscode.ExtensionContext} context
*/
function registerFormatter(context) {
const selector = {
language: WRN_LANGUAGE_ID,
};
const provider = vscode.languages.registerDocumentFormattingEditProvider(selector, {
provideDocumentFormattingEdits(document, options, token) {
if (token.isCancellationRequested) {
return [];
}
const configuration = vscode.workspace.getConfiguration("wrnexus", document.uri);
const enabled = configuration.get("format.enable", true);
if (!enabled) {
return [];
}
const source = document.getText();
if (!source.trim()) {
return [];
}
const printWidth = configuration.get("formatting.printWidth", 100);
const multilineAttributes = configuration.get("formatting.multilineAttributes", true);
try {
const formatted = formatWrn(source, {
tabSize: options.tabSize || 4,
insertSpaces: options.insertSpaces !== false,
printWidth,
multilineAttributes,
});
if (typeof formatted !== "string" || formatted === source) {
return [];
}
const fullRange = new vscode.Range(
document.positionAt(0),
document.positionAt(source.length),
);
return [vscode.TextEdit.replace(fullRange, formatted)];
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error("[wrnexus] formatting failed:", message);
vscode.window.showErrorMessage(`WRNexus formatting failed: ${message}`);
return [];
}
},
});
context.subscriptions.push(provider);
}
/**
* Convert a compiler ParseError into a VS Code diagnostic.
*
* Compiler errors normally contain:
*
* at offset 123
*
* When no offset exists, the diagnostic is attached to the last
* line of the document.
*
* @param {vscode.TextDocument} document
* @param {unknown} error
* @returns {vscode.Diagnostic}
*/
function toCompilerDiagnostic(document, error) {
const message =
error && typeof error === "object" && "message" in error
? String(error.message)
: "Failed to compile .wrn file.";
const offsetMatch = /offset\s+(\d+)/i.exec(message);
let range;
if (offsetMatch) {
const requestedOffset = Number(offsetMatch[1]);
const safeOffset = Math.max(0, Math.min(requestedOffset, document.getText().length));
const start = document.positionAt(safeOffset);
const wordRange = document.getWordRangeAtPosition(start);
range =
wordRange ||
new vscode.Range(
start,
document.positionAt(Math.min(safeOffset + 1, document.getText().length)),
);
} else {
const lastLine = document.lineAt(Math.max(0, document.lineCount - 1));
range = lastLine.range;
}
const diagnostic = new vscode.Diagnostic(range, message, vscode.DiagnosticSeverity.Error);
diagnostic.source = "WRNexus Compiler";
diagnostic.code = "wrn-compiler-error";
return diagnostic;
}
/**
* Register diagnostics produced by the actual WRN compiler.
*
* The lightweight diagnostics in diagnostics.js provide immediate
* editor feedback. Compiler diagnostics verify that the document
* can also be parsed and generated by the real framework compiler.
*
* @param {vscode.ExtensionContext} context
*/
function registerCompilerDiagnostics(context) {
const collection = vscode.languages.createDiagnosticCollection(COMPILER_DIAGNOSTIC_COLLECTION);
const timers = new Map();
/**
* @param {vscode.TextDocument} document
*/
const run = (document) => {
if (document.languageId !== WRN_LANGUAGE_ID) {
return;
}
const configuration = vscode.workspace.getConfiguration("wrnexus", document.uri);
const enabled = configuration.get("diagnostics.enable", true);
if (!enabled) {
collection.delete(document.uri);
return;
}
if (!compiler || typeof compiler.compileWireFile !== "function") {
collection.delete(document.uri);
return;
}
const source = document.getText();
if (!source.trim()) {
collection.delete(document.uri);
return;
}
try {
compiler.compileWireFile(source);
collection.set(document.uri, []);
} catch (error) {
collection.set(document.uri, [toCompilerDiagnostic(document, error)]);
}
};
/**
* @param {vscode.TextDocument} document
*/
const schedule = (document) => {
if (document.languageId !== WRN_LANGUAGE_ID) {
return;
}
const key = document.uri.toString();
const existing = timers.get(key);
if (existing) {
clearTimeout(existing);
}
const timer = setTimeout(() => {
timers.delete(key);
run(document);
}, 250);
timers.set(key, timer);
};
for (const document of vscode.workspace.textDocuments) {
run(document);
}
context.subscriptions.push(
collection,
vscode.workspace.onDidOpenTextDocument(run),
vscode.workspace.onDidChangeTextDocument((event) => {
schedule(event.document);
}),
vscode.workspace.onDidSaveTextDocument(run),
vscode.workspace.onDidCloseTextDocument((document) => {
const key = document.uri.toString();
const timer = timers.get(key);
if (timer) {
clearTimeout(timer);
timers.delete(key);
}
collection.delete(document.uri);
}),
vscode.workspace.onDidChangeConfiguration((event) => {
if (!event.affectsConfiguration("wrnexus.diagnostics.enable")) {
return;
}
for (const document of vscode.workspace.textDocuments) {
run(document);
}
}),
{
dispose() {
for (const timer of timers.values()) {
clearTimeout(timer);
}
timers.clear();
},
},
);
}
/**
* Register semantic highlighting for WRN state declarations and references.
*
* @param {vscode.ExtensionContext} context
*/
function registerSemanticTokens(context) {
context.subscriptions.push(
vscode.languages.registerDocumentSemanticTokensProvider(
{
language: WRN_LANGUAGE_ID,
},
wrnSemanticTokensProvider,
semanticTokenLegend,
),
);
}
/**
* Recover files affected by older workspace settings that associated `*.wrn`
* with the obsolete `wire` language id. Normal language contribution matching
* happens before activation; this fallback is intentionally limited to Plain
* Text and the legacy id so explicit third-party associations are respected.
*
* @param {vscode.TextDocument} document
*/
/** @param {vscode.TextDocument} document */
async function recoverWrnLanguage(document) {
if (!document.fileName.toLowerCase().endsWith(".wrn")) return;
if (!["plaintext", "wire"].includes(document.languageId)) return;
try {
await vscode.languages.setTextDocumentLanguage(document, WRN_LANGUAGE_ID);
} catch (error) {
@@ -326,281 +23,32 @@ async function recoverWrnLanguage(document) {
}
}
/**
* Activate the WRNexus VS Code extension.
*
* @param {vscode.ExtensionContext} context
*/
/** @param {vscode.ExtensionContext} context */
async function activate(context) {
for (const document of vscode.workspace.textDocuments) {
void recoverWrnLanguage(document);
}
for (const document of vscode.workspace.textDocuments) void recoverWrnLanguage(document);
context.subscriptions.push(
vscode.workspace.onDidOpenTextDocument((document) => {
void recoverWrnLanguage(document);
}),
vscode.workspace.onDidOpenTextDocument((document) => void recoverWrnLanguage(document)),
);
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 = {
if (!vscode.workspace.getConfiguration("wrnexus").get("languageServer.enable", true)) return;
const module = path.join(context.extensionPath, "src", "language-server.cjs");
client = new LanguageClient(
"wrnexusLanguageServer",
"WRNexus Language Server",
{
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);
registerFormatter(context);
}
// Package/local component metadata is not yet part of the LSP workspace
// catalog, so keep prop/event completion, hover, and validation available in
// both LSP and built-in-provider modes.
registerComponentIntelligence(context);
registerSemanticTokens(context);
registerV060LanguageFeatures(context);
},
{ documentSelector: [{ scheme: "file", language: WRN_LANGUAGE_ID }] },
);
await client.start();
}
function deactivate() {}
const semanticTokenLegend = new vscode.SemanticTokensLegend(
["variable"],
["declaration", "modification"],
);
/**
* @param {string} value
* @returns {string}
*/
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
async function deactivate() {
const running = client;
client = undefined;
if (running) await running.stop();
}
/**
* @param {string} text
* @returns {Map<string, Set<number>>}
*/
function collectStateVariables(text) {
const states = new Map();
const statePattern = /\bstate\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*(?==|;|\r?$)/gm;
let match;
while ((match = statePattern.exec(text)) !== null) {
const name = match[1];
if (!name) {
continue;
}
const offset = match.index + match[0].lastIndexOf(name);
const declarations = states.get(name) || new Set();
declarations.add(offset);
states.set(name, declarations);
}
return states;
}
/**
* Comments are excluded, but quoted attribute expressions are deliberately
* included because WRN state references commonly appear in values such as
* class:flex="centered" and @click="enabled = !enabled".
*
* @param {string} text
* @returns {Array<{start: number, end: number}>}
*/
function collectCommentRanges(text) {
const ranges = [];
let index = 0;
let quote = null;
let escaped = false;
while (index < text.length) {
const current = text[index];
const next = text[index + 1];
if (quote) {
if (escaped) {
escaped = false;
index++;
continue;
}
if (current === "\\") {
escaped = true;
index++;
continue;
}
if (current === quote) {
quote = null;
}
index++;
continue;
}
if (current === '"' || current === "'" || current === "`") {
quote = current;
index++;
continue;
}
if (current === "/" && next === "/") {
const start = index;
index += 2;
while (index < text.length && text[index] !== "\n") {
index++;
}
ranges.push({ start, end: index });
continue;
}
if (current === "/" && next === "*") {
const start = index;
index += 2;
while (index < text.length && !(text[index] === "*" && text[index + 1] === "/")) {
index++;
}
index = Math.min(text.length, index + 2);
ranges.push({ start, end: index });
continue;
}
index++;
}
return ranges;
}
/**
* @param {number} offset
* @param {Array<{start: number, end: number}>} ranges
* @returns {boolean}
*/
function isInsideComment(offset, ranges) {
let low = 0;
let high = ranges.length - 1;
while (low <= high) {
const middle = Math.floor((low + high) / 2);
const range = ranges[middle];
if (!range) {
return false;
}
if (offset < range.start) {
high = middle - 1;
} else if (offset >= range.end) {
low = middle + 1;
} else {
return true;
}
}
return false;
}
/**
* @param {string} text
* @param {number} offset
* @param {number} length
* @returns {boolean}
*/
function isStateModification(text, offset, length) {
const after = text.slice(offset + length).match(/^\s*(=|\+=|-=|\*=|\/=|%=|\+\+|--)/);
if (after) {
return true;
}
const before = text.slice(Math.max(0, offset - 8), offset).match(/(\+\+|--)\s*$/);
return Boolean(before);
}
const wrnSemanticTokensProvider = {
/**
* @param {vscode.TextDocument} document
* @param {vscode.CancellationToken} token
*/
provideDocumentSemanticTokens(document, token) {
const builder = new vscode.SemanticTokensBuilder(semanticTokenLegend);
const text = document.getText();
const states = collectStateVariables(text);
const commentRanges = collectCommentRanges(text);
for (const [name, declarationOffsets] of states) {
if (token.isCancellationRequested) {
return builder.build();
}
const pattern = new RegExp(`(?<![A-Za-z0-9_$])${escapeRegExp(name)}(?![A-Za-z0-9_$])`, "g");
let match;
while ((match = pattern.exec(text)) !== null) {
const offset = match.index;
if (isInsideComment(offset, commentRanges)) {
continue;
}
const position = document.positionAt(offset);
let modifiers = [];
if (declarationOffsets.has(offset)) {
modifiers = ["declaration"];
} else if (isStateModification(text, offset, name.length)) {
modifiers = ["modification"];
}
builder.push(position.line, position.character, name.length, "variable", modifiers);
}
}
return builder.build();
},
};
module.exports = {
activate,
deactivate,
registerCompilerDiagnostics,
registerFormatter,
registerSemanticTokens,
recoverWrnLanguage,
toCompilerDiagnostic,
};
module.exports = { activate, deactivate, recoverWrnLanguage };
+191 -30
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env node
// WRN editor language server source hash: eb2126d09754a516bf6846aea69409578879532f74759f66ae37cf60c961b3bf
// WRN editor language server source hash: da83a5e1dbf90523ee0fe9a0d232182b0a78d025f3be8d07c324728d1cdcc17c
// WRN editor language server generator hash: f593a44aaf05495b789ce7a3086bee1eebb951b884d41c0e017bbcfe5f547e72
// @bun @bun-cjs
(function(exports, require, module, __filename, __dirname) {var __create = Object.create;
@@ -171925,6 +171925,7 @@ function virtualTypeScriptModule(source, filePath = "component.wrn", appRoot = f
append(ast.types.join(`
`), ast.types[0]?.trim());
append('declare const ctx: import("@wrnexus/core").Context;');
for (const prop of ast.props) {
if (!safeBindingName(prop.name))
continue;
@@ -172370,7 +172371,7 @@ var SKIPPED_DIRECTORIES = new Set([
]);
var MAX_INDEX_FILES = 5000;
var MAX_SOURCE_BYTES = 1048576;
var INDEX_TTL_MS = 5000;
var INDEX_TTL_MS = 5 * 60000;
var MAX_CACHED_ROOTS = 8;
var workspaceIndexCache = new Map;
function walk3(root, test) {
@@ -172494,6 +172495,36 @@ function workspaceCompletionItems(root) {
}
return items;
}
function clearWorkspaceIndexCache(root) {
if (root)
workspaceIndexCache.delete(import_node_path3.resolve(root));
else
workspaceIndexCache.clear();
}
function workspaceSymbolLocations(root, name, limit = 1000) {
if (!/^[A-Za-z_$][\w$]*$/.test(name))
return [];
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const pattern = new RegExp(`(?<![A-Za-z0-9_$])${escaped}(?![A-Za-z0-9_$])`, "g");
const locations = [];
for (const file of walk3(import_node_path3.resolve(root), (path) => import_node_path3.extname(path) === ".wrn").slice(0, limit)) {
let source;
try {
source = import_node_fs3.readFileSync(file, "utf8");
} catch {
continue;
}
for (const match of source.matchAll(pattern)) {
const before = source.slice(0, match.index).split(/\r?\n/);
const start = { line: before.length - 1, character: before.at(-1)?.length ?? 0 };
locations.push({
uri: `file:///${file.replace(/\\/g, "/")}`,
range: { start, end: { line: start.line, character: start.character + name.length } }
});
}
}
return locations;
}
function extractComponentRefactor(document, range, name) {
if (!/^[A-Z][A-Za-z0-9_$]*$/.test(name))
throw new Error("Component name must use PascalCase");
@@ -172599,7 +172630,7 @@ function diagnosticRange(diagnostic) {
}
};
}
function documentDiagnostics(document) {
function documentDiagnostics(document, options = {}) {
if (Buffer.byteLength(document.text, "utf8") > 1048576) {
return [
{
@@ -172618,7 +172649,7 @@ function documentDiagnostics(document) {
source: "wrnexus",
message: diagnostic.message
}));
if (syntax.some((diagnostic) => diagnostic.severity === 1))
if (syntax.some((diagnostic) => diagnostic.severity === 1) || options.includeTypes === false)
return syntax;
const types = checkWrnSource(document.text, { filePath: documentPath(document.uri) }).map((diagnostic) => ({
range: {
@@ -172688,19 +172719,6 @@ function documentSymbols(document) {
};
});
}
function symbolLocations(document, position) {
const selected = wordAt(document.text, position);
if (!selected)
return [];
const pattern = new RegExp(`(?<![A-Za-z0-9_$])${selected.word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![A-Za-z0-9_$])`, "g");
return [...document.text.matchAll(pattern)].map((match) => ({
uri: document.uri,
range: {
start: positionAt2(document.text, match.index),
end: positionAt2(document.text, match.index + selected.word.length)
}
}));
}
function definitionLocation(document, position) {
const selected = wordAt(document.text, position);
if (!selected)
@@ -172734,6 +172752,55 @@ ${declaration[0]}
function completionItems() {
return WRN_COMPLETIONS.map((label) => ({ label, kind: 14, detail: "WRNexus language keyword" }));
}
var semanticTokenTypes = ["variable"];
var semanticTokenModifiers = ["declaration", "modification"];
var semanticTokensLegend = {
tokenTypes: [...semanticTokenTypes],
tokenModifiers: [...semanticTokenModifiers]
};
function semanticTokens(document) {
const declarations = new Map;
for (const match of document.text.matchAll(/\bstate\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*(?==|;|\r?$)/gm)) {
const name = match[1];
const offset = match.index + match[0].lastIndexOf(name);
const offsets = declarations.get(name) ?? new Set;
offsets.add(offset);
declarations.set(name, offsets);
}
const comments = [...document.text.matchAll(/\/\/[^\n]*|\/\*[\s\S]*?\*\//g)].map((match) => ({
start: match.index,
end: match.index + match[0].length
}));
const tokens = [];
for (const [name, offsets] of declarations) {
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
for (const match of document.text.matchAll(new RegExp(`(?<![A-Za-z0-9_$])${escaped}(?![A-Za-z0-9_$])`, "g"))) {
const offset = match.index;
if (comments.some((range) => offset >= range.start && offset < range.end))
continue;
const position = positionAt2(document.text, offset);
const after = document.text.slice(offset + name.length).match(/^\s*(?:=|\+=|-=|\*=|\/=|%=|\+\+|--)/);
const before = document.text.slice(Math.max(0, offset - 8), offset).match(/(?:\+\+|--)\s*$/);
tokens.push({
...position,
length: name.length,
modifiers: offsets.has(offset) ? 1 : after || before ? 2 : 0
});
}
}
tokens.sort((a, b) => a.line - b.line || a.character - b.character);
const data = [];
let previousLine = 0;
let previousCharacter = 0;
for (const token of tokens) {
const deltaLine = token.line - previousLine;
const deltaCharacter = deltaLine === 0 ? token.character - previousCharacter : token.character;
data.push(deltaLine, deltaCharacter, token.length, 0, token.modifiers);
previousLine = token.line;
previousCharacter = token.character;
}
return { data };
}
// packages/language-server/src/server.ts
var documents = new Map;
@@ -172767,20 +172834,20 @@ function internalDiagnostic(document, error) {
message: `WRNexus language analysis failed safely: ${message}`
};
}
function safeDocumentDiagnostics(document) {
function safeDocumentDiagnostics(document, includeTypes = true) {
try {
return documentDiagnostics(document);
return documentDiagnostics(document, { includeTypes });
} catch (error) {
process.stderr.write(`[wrnexus-lsp] diagnostics failed for ${document.uri}: ${error instanceof Error ? error.stack ?? error.message : String(error)}
`);
return [internalDiagnostic(document, error)];
}
}
function publish(document) {
function publish(document, includeTypes = true) {
send({
jsonrpc: "2.0",
method: "textDocument/publishDiagnostics",
params: { uri: document.uri, diagnostics: safeDocumentDiagnostics(document) }
params: { uri: document.uri, diagnostics: safeDocumentDiagnostics(document, includeTypes) }
});
}
function clearDiagnosticTimer(uri) {
@@ -172796,7 +172863,7 @@ function schedulePublish(document) {
diagnosticTimers.delete(document.uri);
const current = documents.get(document.uri);
if (current && current.version === expectedVersion)
publish(current);
publish(current, false);
}, DIAGNOSTIC_DEBOUNCE_MS));
}
function rememberDocument(document) {
@@ -172818,7 +172885,7 @@ async function handle(message) {
result(message.id, {
serverInfo: { name: "WRNexus Language Server", version: "0.8.3" },
capabilities: {
textDocumentSync: 1,
textDocumentSync: { openClose: true, change: 1, save: true },
documentFormattingProvider: true,
completionProvider: { triggerCharacters: ["<", "@", ":", "."] },
hoverProvider: true,
@@ -172826,8 +172893,14 @@ async function handle(message) {
referencesProvider: true,
renameProvider: { prepareProvider: true },
documentSymbolProvider: true,
semanticTokensProvider: { legend: semanticTokensLegend, full: true },
codeActionProvider: {
codeActionKinds: ["quickfix", "refactor.extract", "refactor.rewrite"]
codeActionKinds: [
"quickfix",
"refactor.extract",
"refactor.rewrite",
"source.organizeImports"
]
},
experimental: { wrnexusVirtualTypeScript: true }
}
@@ -172842,6 +172915,10 @@ async function handle(message) {
result(message.id, null);
break;
case "exit":
for (const timer of diagnosticTimers.values())
clearTimeout(timer);
diagnosticTimers.clear();
documents.clear();
process.exit(0);
break;
case "textDocument/didOpen": {
@@ -172861,6 +172938,15 @@ async function handle(message) {
}
break;
}
case "textDocument/didSave": {
const document = documents.get(params.textDocument.uri);
if (document) {
clearDiagnosticTimer(document.uri);
clearWorkspaceIndexCache(workspaceRoot);
publish(document, true);
}
break;
}
case "textDocument/didClose":
clearDiagnosticTimer(params.textDocument.uri);
documents.delete(params.textDocument.uri);
@@ -172890,6 +172976,11 @@ async function handle(message) {
result(message.id, document ? documentSymbols(document) : []);
break;
}
case "textDocument/semanticTokens/full": {
const document = documents.get(params.textDocument.uri);
result(message.id, document ? semanticTokens(document) : { data: [] });
break;
}
case "textDocument/hover": {
const document = documents.get(params.textDocument.uri);
result(message.id, document ? hover(document, params.position) : null);
@@ -172902,7 +172993,8 @@ async function handle(message) {
}
case "textDocument/references": {
const document = documents.get(params.textDocument.uri);
result(message.id, document ? symbolLocations(document, params.position) : []);
const selected = document ? wordAt(document.text, params.position) : null;
result(message.id, selected ? workspaceSymbolLocations(workspaceRoot, selected.word) : []);
break;
}
case "textDocument/prepareRename": {
@@ -172912,11 +173004,16 @@ async function handle(message) {
}
case "textDocument/rename": {
const document = documents.get(params.textDocument.uri);
const edits = document ? symbolLocations(document, params.position).map(({ range }) => ({
range,
newText: params.newName
})) : [];
result(message.id, document ? { changes: { [document.uri]: edits } } : null);
const selected = document ? wordAt(document.text, params.position) : null;
if (!selected || !/^[A-Za-z_$][\w$]*$/.test(params.newName)) {
result(message.id, null);
break;
}
const changes = {};
for (const location of workspaceSymbolLocations(workspaceRoot, selected.word)) {
(changes[location.uri] ??= []).push({ range: location.range, newText: params.newName });
}
result(message.id, { changes });
break;
}
case "textDocument/codeAction": {
@@ -172937,6 +173034,70 @@ async function handle(message) {
}
}
}));
const imports = [
...document.text.matchAll(/^import(?:\s+type)?[\s\S]*?(?:;\s*|\n(?=import|\s*(?:page|component|layout|global\s+store|page\s+store)\b))/gm)
].map((match) => ({
start: match.index,
end: match.index + match[0].length,
text: match[0].trim()
}));
if (imports.length > 1) {
const ordered = [...imports].sort((a, b) => {
const aType = /^import\s+type\b/.test(a.text) ? 0 : 1;
const bType = /^import\s+type\b/.test(b.text) ? 0 : 1;
return aType - bType || a.text.localeCompare(b.text);
});
const replacement = ordered.map((entry) => entry.text.replace(/;$/, "")).join(`
`) + `
`;
const start = imports[0].start;
const end = imports.at(-1).end;
if (document.text.slice(start, end) !== replacement) {
actions.push({
title: "Organize WRN imports",
kind: "source.organizeImports",
edit: {
changes: {
[document.uri]: [
{
range: {
start: positionAt2(document.text, start),
end: positionAt2(document.text, end)
},
newText: replacement
}
]
}
}
});
}
}
for (const diagnostic of params.context?.diagnostics ?? []) {
if (diagnostic.code !== "WRN-OUTPUT-LEGACY-EMIT")
continue;
const start = offsetAt(document.text, diagnostic.range.start);
const end = offsetAt(document.text, diagnostic.range.end);
const source = document.text.slice(start, end);
const match = /\$emit\(\s*["']([A-Za-z_$][\w$]*)["']\s*,?/.exec(source);
if (!match)
continue;
actions.push({
title: `Convert $emit to output.${match[1]}`,
kind: "quickfix",
diagnostics: [diagnostic],
isPreferred: true,
edit: {
changes: {
[document.uri]: [
{
range: diagnostic.range,
newText: source.replace(/\$emit\(\s*["'][A-Za-z_$][\w$]*["']\s*,?\s*/, `output.${match[1]}(`)
}
]
}
}
});
}
const selected = document.text.slice(document.text.split(/\r?\n/).slice(0, params.range.start.line).reduce((n, line) => n + line.length + 1, 0) + params.range.start.character, document.text.split(/\r?\n/).slice(0, params.range.end.line).reduce((n, line) => n + line.length + 1, 0) + params.range.end.character);
if (selected.trim().startsWith("<")) {
try {
+37 -1
View File
@@ -68,7 +68,13 @@ test("language server starts under Node and keeps official typed components clea
messages.push({ jsonrpc: "2.0", method: "exit", params: {} });
const responses = run(messages);
assert.equal(responses.find((item) => item.id === 1)?.result?.serverInfo?.version, "0.8.3");
const initialization = responses.find((item) => item.id === 1)?.result;
assert.equal(initialization?.serverInfo?.version, "0.8.3");
assert.deepEqual(initialization?.capabilities?.textDocumentSync, {
openClose: true,
change: 1,
save: true,
});
const diagnostics = responses.filter((item) => item.method === "textDocument/publishDiagnostics");
assert.equal(diagnostics.length, 2);
assert.deepEqual(
@@ -77,6 +83,36 @@ test("language server starts under Node and keeps official typed components clea
);
});
test("language server defers expensive type analysis while typing and restores it on save", () => {
const uri = "file:///tmp/wrnexus-save-analysis.wrn";
const valid = `component SaveAnalysis {
outputs { save(payload: { id: string }) }
functions { client function submit(): void { output.save({ id: "ok" }) } }
view { <button @click="submit()"></button> }
}`;
const invalid = valid.replace('id: "ok"', "id: 123");
const responses = run([
{ jsonrpc: "2.0", id: 1, method: "initialize", params: { rootPath: root } },
{ jsonrpc: "2.0", method: "initialized", params: {} },
{
jsonrpc: "2.0",
method: "textDocument/didOpen",
params: { textDocument: { uri, languageId: "wrn", version: 1, text: valid } },
},
{
jsonrpc: "2.0",
method: "textDocument/didChange",
params: { textDocument: { uri, version: 2 }, contentChanges: [{ text: invalid }] },
},
{ jsonrpc: "2.0", method: "textDocument/didSave", params: { textDocument: { uri } } },
{ jsonrpc: "2.0", id: 2, method: "shutdown", params: {} },
{ jsonrpc: "2.0", method: "exit", params: {} },
]);
const diagnostics = responses.filter((item) => item.method === "textDocument/publishDiagnostics");
assert.equal(diagnostics.length, 2);
assert.ok(diagnostics.at(-1).params.diagnostics.some((item) => item.code === "WRN-TYPE-2322"));
});
test("language server formatter preserves code samples and compact sibling markup", () => {
const source = `page Docs {
view {