fix(vscode): repair WRN language diagnostics and activation

This commit is contained in:
2026-07-19 14:02:17 +05:30
parent df4c1d3e7d
commit 646d16f83d
16 changed files with 425 additions and 171 deletions
+126 -31
View File
@@ -1023,8 +1023,12 @@ function hasClientBehavior(nodes) {
return nodes.some((node) => {
if (node.type === "text")
return /\{(?!t:)[^{}]+\}/.test(node.value);
if (node.type === "each" || node.type === "if")
return false;
if (node.type === "each") {
return hasClientBehavior(node.body) || hasClientBehavior(node.empty);
}
if (node.type === "if") {
return node.branches.some((branch) => hasClientBehavior(branch.body));
}
return node.attrs.some((attr) => attr.event || attr.name === "csrGet" || attr.name === "csrText") || hasClientBehavior(node.children);
});
}
@@ -1391,6 +1395,18 @@ function viewHasEvents(nodes) {
return node.attrs.some((attr) => attr.event) || viewHasEvents(node.children);
});
}
function viewHasServerEach(nodes) {
return nodes.some((node) => {
if (node.type === "text")
return false;
if (node.type === "each")
return true;
if (node.type === "if") {
return node.branches.some((branch) => viewHasServerEach(branch.body));
}
return viewHasServerEach(node.children);
});
}
function compileText(raw, ctx) {
let out = "";
let last = 0;
@@ -1455,6 +1471,14 @@ function renderComponentEachNode(node, ctx) {
const empty = node.empty.map((child) => renderComponentNode(child, ctx)).join("");
return "${(() => { const __wl = Array.isArray(" + list + ") ? (" + list + ") : []; return __wl.length ? __wl.map((" + item + ", " + index + ") => `" + body + '`).join("") : `' + empty + "`; })()}";
}
function serverLoopLocalsAttribute(ctx) {
const locals = [...ctx.serverLocals ?? []];
if (locals.length === 0) {
return "";
}
const entries = locals.map((name) => `${JSON.stringify(name)}: ${name}`).join(", ");
return ` data-wrn-loop-locals="\${__wrnexusEncodeLoopLocals({ ${entries} })}"`;
}
function renderComponentNode(node, ctx) {
if (node.type === "text")
return compileText(node.value, ctx);
@@ -1467,6 +1491,11 @@ function renderComponentNode(node, ctx) {
if (isComponentTag(node.tag)) {
return renderNestedComponentInvocation(node, ctx);
}
const loopVariables = loopVarsOf(node);
const elementContext = loopVariables.length > 0 ? {
...ctx,
loopVars: new Set([...ctx.loopVars ?? [], ...loopVariables])
} : ctx;
let bindIndex = 0;
const staticClasses = [];
const conditionalClasses = [];
@@ -1488,37 +1517,48 @@ function renderComponentNode(node, ctx) {
if (a.boolean) {
return ` ${a.name}`;
}
const rendered = ` ${a.name}="${compileAttrValue(a.value, ctx)}"`;
if (!a.value.includes("{") || !exprRefsState(a.value, ctx.stateNames)) {
const rendered = ` ${a.name}="${compileAttrValue(a.value, elementContext)}"`;
const referencesState = exprRefsState(a.value, ctx.stateNames);
const referencesLoopVariable = elementContext.loopVars ? exprRefsState(a.value, elementContext.loopVars) : false;
const referencesServerLocal = ctx.serverLocals ? exprRefsState(a.value, ctx.serverLocals) : false;
if (!a.value.includes("{") || !referencesState && !referencesLoopVariable && !referencesServerLocal) {
return rendered;
}
const marker = attrEscape(JSON.stringify([a.name, a.value]));
return `${rendered} data-wrn-bind-${bindIndex++}="${escLit(marker)}"`;
}).join("");
const initialConditionalClasses = conditionalClasses.map(({ className, expression }) => {
const referencesLoopVariable = elementContext.loopVars ? exprRefsState(expression, elementContext.loopVars) : false;
if (referencesLoopVariable) {
return "";
}
return `\${(${ctx.resolveExpr(expression)}) ? ${JSON.stringify(` ${className}`)} : ""}`;
}).join("");
const staticClassValue = staticClasses.join(" ");
const classHasReactiveExpression = staticClassValue.includes("{") && exprRefsState(staticClassValue, ctx.stateNames);
const classAttribute = staticClasses.length > 0 || conditionalClasses.length > 0 ? ` class="${compileAttrValue(staticClassValue, ctx)}${initialConditionalClasses}"` : "";
const classReferencesState = exprRefsState(staticClassValue, ctx.stateNames);
const classReferencesLoopVariable = elementContext.loopVars ? exprRefsState(staticClassValue, elementContext.loopVars) : false;
const classReferencesServerLocal = ctx.serverLocals ? exprRefsState(staticClassValue, ctx.serverLocals) : false;
const classHasReactiveExpression = staticClassValue.includes("{") && (classReferencesState || classReferencesLoopVariable || classReferencesServerLocal);
const classAttribute = staticClasses.length > 0 || conditionalClasses.length > 0 ? ` class="${compileAttrValue(staticClassValue, elementContext)}${initialConditionalClasses}"` : "";
const classReactiveBinding = classHasReactiveExpression ? ` data-wrn-bind-class="${escLit(attrEscape(JSON.stringify(["class", staticClassValue])))}"` : "";
const classBindings = conditionalClasses.filter(({ expression }) => {
return !ctx.serverLocals || !exprRefsState(expression, ctx.serverLocals);
}).map(({ className, expression }, index) => {
const classBindings = conditionalClasses.map(({ className, expression }, index) => {
const marker = attrEscape(JSON.stringify([className, expression]));
return ` data-wrn-class-${index}="${escLit(marker)}"`;
}).join("");
const loops = loopVarsOf(node);
const childCtx = loops.length > 0 ? { ...ctx, loopVars: new Set([...ctx.loopVars ?? [], ...loops]) } : ctx;
const allAttrs = `${classAttribute}` + `${classReactiveBinding}` + `${classBindings}` + `${attrs}`;
const loopLocalsAttribute = serverLoopLocalsAttribute(ctx);
const allAttrs = `${loopLocalsAttribute}` + `${classAttribute}` + `${classReactiveBinding}` + `${classBindings}` + `${attrs}`;
if (VOID_ELEMENTS.has(node.tag.toLowerCase())) {
return `<${node.tag}${allAttrs}>`;
}
const inner = node.children.map((child) => renderComponentNode(child, childCtx)).join("");
const inner = node.children.map((child) => renderComponentNode(child, elementContext)).join("");
return `<${node.tag}${allAttrs}>${inner}</${node.tag}>`;
}
function generateComponent(ast) {
const out = [];
const hasServerEach = viewHasServerEach(ast.view);
if (hasServerEach) {
out.push(`import { Buffer } from "node:buffer";`);
}
const effectiveProps = ast.kind === "layout" && !ast.props.some((prop) => prop.name === "content") ? [
{
name: "content",
@@ -1542,6 +1582,9 @@ function generateComponent(ast) {
return result;
};
const ctx = { stateNames, resolveExpr };
const serverFunctions = ast.functions.map((body) => body.trim()).filter(Boolean).join(`
`);
const viewCode = ast.view.map((node) => renderComponentNode(node, ctx)).join("");
const styles = ast.styles.map((body) => body.trim()).filter(Boolean);
const styleTag = styles.length > 0 ? escLit(`<style data-wrnexus-style="${attrEscape(ast.name)}">
@@ -1560,10 +1603,10 @@ ${styles.map(styleEscape).join(`
decls.push(` const ${nameRefs.get(prop.name)} = __coerce(__p[${JSON.stringify(prop.name)}], (${resolveExpr(prop.default)}));`);
}
for (const state of ast.states) {
decls.push(` const ${nameRefs.get(state.name)} = (${resolveExpr(state.expr)});`);
decls.push(` let ${nameRefs.get(state.name)} = (${resolveExpr(state.expr)});`);
}
const returnExpr = needsScope ? "`" + styleTag + `<div data-scope="\${__scope}"${behaviorAttr}>` + viewCode + "</div>`" : "`" + styleTag + viewCode + "`";
const scopeLine = needsScope && scopeKeys.length > 0 ? ` const __scope = __wrnexusScopeDecl({ ${scopeKeys.map((k) => `${JSON.stringify(k)}: ${nameRefs.get(k)}`).join(", ")} });
const scopeLine = needsScope && scopeKeys.length > 0 ? ` const __scope = __wrnexusScopeDecl({ ${scopeKeys.map((key) => `${JSON.stringify(key)}: ${nameRefs.get(key)}`).join(", ")} });
` : needsScope ? ` const __scope = "";
` : "";
if (ast.kind === "layout") {
@@ -1673,26 +1716,70 @@ function __wireProp(v: any): string {
function __wireRaw(v: any): string {
return String(v == null ? "" : v);
}`);
if (needsScope) {
out.push(`function __wrnexusScopeDecl(obj: Record<string, any>): string {
const lit = (v: any) =>
typeof v === "number" || typeof v === "boolean"
? String(v)
: "'" + String(v).replace(/\\\\/g, "\\\\\\\\").replace(/'/g, "\\\\'").replace(/\\n/g, "\\\\n") + "'";
return Object.keys(obj)
.map((k) => k + ": " + lit(obj[k]))
.join(", ")
.replace(/&/g, "&amp;")
.replace(/"/g, "&quot;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
if (hasServerEach) {
out.push(`function __wrnexusEncodeLoopLocals(value: Record<string, any>): string {
return Buffer.from(JSON.stringify(value), "utf8").toString("base64");
}`);
}
if (needsScope) {
out.push(`function __wrnexusSerializeScopeValue(value: any): string {
if (value === undefined) {
return "undefined";
}
if (value === null) {
return "null";
}
if (typeof value === "number") {
return Number.isFinite(value)
? String(value)
: "null";
}
if (typeof value === "boolean") {
return value ? "true" : "false";
}
if (typeof value === "string") {
return JSON.stringify(value);
}
try {
const serialized = JSON.stringify(value);
return serialized === undefined
? "undefined"
: serialized;
} catch {
return "null";
}
}
function __wrnexusScopeDecl(obj: Record<string, any>): string {
return Object.keys(obj)
.map(
(key) =>
key +
": " +
__wrnexusSerializeScopeValue(
obj[key],
),
)
.join(", ")
.replace(/&/g, "&amp;")
.replace(/"/g, "&quot;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
}`);
}
const serverFunctionSource = serverFunctions ? `${serverFunctions}
` : "";
out.push(`export function render(props: Record<string, any> = {}): string {
` + ` const __p = props || {};
` + (decls.length > 0 ? decls.join(`
`) + `
` : "") + scopeLine + ` return ${returnExpr};
` : "") + serverFunctionSource + scopeLine + ` return ${returnExpr};
` + `}`);
return out.join(`
@@ -1924,8 +2011,16 @@ ${nativeStyles(ast.styles)}
function compileNativeWireFile(source) {
return generateNative(parse(source));
}
function compileWireFile(source) {
const ast = parse(source);
function compileWireFile(source, filePath = "<inline .wrn>") {
let ast;
try {
ast = parse(source);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to parse ${filePath}: ${message}`, {
cause: error
});
}
return `// compiled from .wrn
${generate(ast)}`;
}
-1
View File
@@ -517,7 +517,6 @@ function registerCompletionProvider(context) {
const provider = vscode.languages.registerCompletionItemProvider(
{
language: "wrn",
scheme: "file",
},
{
provideCompletionItems,
-1
View File
@@ -99,7 +99,6 @@ function registerDefinitionProvider(context) {
const disposable = vscode.languages.registerDefinitionProvider(
{
language: "wrn",
scheme: "file",
},
{
provideDefinition,
+53 -2
View File
@@ -76,11 +76,35 @@ function stripComments(source) {
return source.replace(/<!--[\s\S]*?-->/g, (comment) => comment.replace(/[^\n]/g, " "));
}
function maskLeadingTrivia(source) {
const masked = [...source];
let offset = 0;
while (offset < source.length) {
if (/\s/u.test(source[offset])) {
offset += 1;
continue;
}
if (!source.startsWith("//", offset)) break;
while (offset < source.length && source[offset] !== "\n") {
masked[offset] = " ";
offset += 1;
}
}
return masked.join("");
}
function findTopLevelDeclaration(document, source) {
const match = TOP_LEVEL_PATTERN.exec(source);
const sourceWithoutLeadingTrivia = maskLeadingTrivia(source);
const match = TOP_LEVEL_PATTERN.exec(sourceWithoutLeadingTrivia);
if (!match) {
const firstMeaningfulLine = source.split(/\r?\n/).findIndex((line) => line.trim().length > 0);
const firstMeaningfulLine = sourceWithoutLeadingTrivia
.split(/\r?\n/)
.findIndex((line) => line.trim().length > 0);
return {
diagnostic: lineDiagnostic(
@@ -134,6 +158,13 @@ function validateBalancedCharacters(document, source) {
continue;
}
if (source.startsWith("//", index)) {
const lineEnd = source.indexOf("\n", index + 2);
index = lineEnd === -1 ? source.length : lineEnd;
continue;
}
if (character === '"' || character === "'") {
quote = character;
continue;
@@ -1063,6 +1094,14 @@ function registerDiagnostics(context) {
if (previousTimer) {
clearTimeout(previousTimer);
timers.delete(key);
}
const configuration = vscode.workspace.getConfiguration("wrnexus", document.uri);
if (!configuration.get("diagnostics.enable", true)) {
collection.delete(document.uri);
return;
}
const timer = setTimeout(() => {
@@ -1089,6 +1128,16 @@ function registerDiagnostics(context) {
vscode.workspace.onDidSaveTextDocument(update),
vscode.workspace.onDidChangeConfiguration((event) => {
if (!event.affectsConfiguration("wrnexus.diagnostics.enable")) {
return;
}
for (const document of vscode.workspace.textDocuments) {
update(document);
}
}),
vscode.workspace.onDidCloseTextDocument((document) => {
const key = document.uri.toString();
const timer = timers.get(key);
@@ -1114,6 +1163,8 @@ function registerDiagnostics(context) {
}
module.exports = {
findTopLevelDeclaration,
maskLeadingTrivia,
registerDiagnostics,
validateBalancedCharacters,
validateDocument,
+39 -96
View File
@@ -284,95 +284,37 @@ function registerCompilerDiagnostics(context) {
* @param {vscode.ExtensionContext} context
*/
function registerSemanticTokens(context) {
const semanticTokenLegend = new vscode.SemanticTokensLegend(
["variable"],
["declaration", "modification"],
context.subscriptions.push(
vscode.languages.registerDocumentSemanticTokensProvider(
{
language: WRN_LANGUAGE_ID,
},
wrnSemanticTokensProvider,
semanticTokenLegend,
),
);
}
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
/**
* 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
*/
async function recoverWrnLanguage(document) {
if (!document.fileName.toLowerCase().endsWith(".wrn")) return;
if (!["plaintext", "wire"].includes(document.languageId)) return;
const collectStateVariables = (text) => {
const states = new Map();
const pattern = /\bstate\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*(?==|;|\r?$)/gm;
let match;
while ((match = pattern.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;
};
const isModification = (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 provider = {
provideDocumentSemanticTokens(document, token) {
const builder = new vscode.SemanticTokensBuilder(semanticTokenLegend);
const text = document.getText();
const states = collectStateVariables(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;
const position = document.positionAt(offset);
let modifiers = [];
if (declarationOffsets.has(offset)) {
modifiers = ["declaration"];
} else if (isModification(text, offset, name.length)) {
modifiers = ["modification"];
}
builder.push(position.line, position.character, name.length, "variable", modifiers);
}
}
return builder.build();
},
};
const registration = vscode.languages.registerDocumentSemanticTokensProvider(
{
language: WRN_LANGUAGE_ID,
},
provider,
semanticTokenLegend,
);
context.subscriptions.push(registration);
try {
await vscode.languages.setTextDocumentLanguage(document, WRN_LANGUAGE_ID);
} catch (error) {
console.warn(
"[wrnexus] unable to recover .wrn language association:",
error instanceof Error ? error.message : String(error),
);
}
}
/**
@@ -381,22 +323,22 @@ function registerSemanticTokens(context) {
* @param {vscode.ExtensionContext} context
*/
function activate(context) {
for (const document of vscode.workspace.textDocuments) {
void recoverWrnLanguage(document);
}
context.subscriptions.push(
vscode.workspace.onDidOpenTextDocument((document) => {
void recoverWrnLanguage(document);
}),
);
registerDiagnostics(context);
registerCompilerDiagnostics(context);
registerCompletionProvider(context);
registerDefinitionProvider(context);
registerFormatter(context);
registerSemanticTokens(context);
context.subscriptions.push(
vscode.languages.registerDocumentSemanticTokensProvider(
{
language: "wrn",
},
wrnSemanticTokensProvider,
semanticTokenLegend,
),
);
}
function deactivate() {}
@@ -614,5 +556,6 @@ module.exports = {
registerCompilerDiagnostics,
registerFormatter,
registerSemanticTokens,
recoverWrnLanguage,
toCompilerDiagnostic,
};