From 646d16f83deeb67f716a41b95813112f33dbeb25 Mon Sep 17 00:00:00 2001 From: Ajay Ghanwat Date: Sun, 19 Jul 2026 14:02:17 +0530 Subject: [PATCH] fix(vscode): repair WRN language diagnostics and activation --- .vscode/settings.json | 2 +- editors/vscode/CHANGELOG.md | 13 ++ editors/vscode/README.md | 16 ++ editors/vscode/language-configuration.json | 3 +- editors/vscode/package-lock.json | 4 +- editors/vscode/package.json | 13 +- editors/vscode/src/compiler.cjs | 157 ++++++++++++++++---- editors/vscode/src/completion.js | 1 - editors/vscode/src/definition.js | 1 - editors/vscode/src/diagnostics.js | 55 ++++++- editors/vscode/src/extension.js | 135 +++++------------ editors/vscode/syntaxes/wrn.tmLanguage.json | 66 ++++---- editors/vscode/test/completion.test.js | 11 ++ editors/vscode/test/diagnostics.test.js | 81 ++++++++++ editors/vscode/test/validate.mjs | 38 +++++ editors/vscode/wrnexus-0.2.11.vsix | Bin 0 -> 52301 bytes 16 files changed, 425 insertions(+), 171 deletions(-) create mode 100644 editors/vscode/test/diagnostics.test.js create mode 100644 editors/vscode/wrnexus-0.2.11.vsix diff --git a/.vscode/settings.json b/.vscode/settings.json index b5b7edb5..6f0c2269 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,7 +2,7 @@ // Treat .wrn files as the WrNexus language (redundant with the extension, but // makes highlighting work the moment the repo is opened). "files.associations": { - "*.wrn": "wire" + "*.wrn": "wrn" }, // Distinct colors for WrNexus's own attributes, so they're easy to spot inside diff --git a/editors/vscode/CHANGELOG.md b/editors/vscode/CHANGELOG.md index 19f0e279..0ec47898 100644 --- a/editors/vscode/CHANGELOG.md +++ b/editors/vscode/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## 0.2.11 + +- Fixed `.wrn` files falling back to Plain Text when a legacy `wire` file + association is present. +- Fixed false root-declaration errors when files begin with `//` comments. +- Fixed false unclosed-string errors when `//` comments contain apostrophes or + bracket characters. +- Added explicit default and first-line WRN language detection. +- Removed duplicate semantic-token providers and repaired diagnostics toggling. +- Enabled completions and definition navigation for untitled and remote WRN + documents. +- Improved comment and folding language configuration and package validation. + ## 0.2.2 - Added conditional `class:*` support. diff --git a/editors/vscode/README.md b/editors/vscode/README.md index 706c1b54..ad7cfff8 100644 --- a/editors/vscode/README.md +++ b/editors/vscode/README.md @@ -735,6 +735,22 @@ Recommended VS Code configuration: } ``` +## Troubleshooting language detection + +Files ending in `.wrn` should show `WRNexus` in the VS Code status bar. If an +older workspace setting maps `*.wrn` to the legacy `wire` language ID, replace +it with: + +```json +"files.associations": { + "*.wrn": "wrn" +} +``` + +Then run **Developer: Reload Window**. The extension also repairs open `.wrn` +documents that VS Code classified as Plain Text or as the legacy `wire` +language. + ## Privacy The extension does not collect telemetry. diff --git a/editors/vscode/language-configuration.json b/editors/vscode/language-configuration.json index 9302a17d..db8587a8 100644 --- a/editors/vscode/language-configuration.json +++ b/editors/vscode/language-configuration.json @@ -1,5 +1,6 @@ { "comments": { + "lineComment": "//", "blockComment": [""] }, "brackets": [ @@ -46,7 +47,7 @@ "folding": { "offSide": false, "markers": { - "start": "^\\s*(?:page|component|layout|api|middleware|realtime|view|seo|props|functions)\\b.*\\{\\s*$", + "start": "^\\s*(?:page|component|layout|api|realtime|view|seo|props|style|functions|lifecycle|watch|ssr|client)\\b.*\\{\\s*$", "end": "^\\s*\\}\\s*$" } } diff --git a/editors/vscode/package-lock.json b/editors/vscode/package-lock.json index fd1935c2..9e755324 100644 --- a/editors/vscode/package-lock.json +++ b/editors/vscode/package-lock.json @@ -1,12 +1,12 @@ { "name": "wrnexus", - "version": "0.2.1", + "version": "0.2.11", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "wrnexus", - "version": "0.2.1", + "version": "0.2.11", "license": "SEE LICENSE IN LICENSE", "devDependencies": { "@vscode/vsce": "^3.9.2" diff --git a/editors/vscode/package.json b/editors/vscode/package.json index 841dba5d..3262c011 100644 --- a/editors/vscode/package.json +++ b/editors/vscode/package.json @@ -2,7 +2,7 @@ "name": "wrnexus", "displayName": "WRNexus Language Support", "description": "Complete language support for WRNexus .wrn files, including highlighting, formatting, diagnostics, snippets, lifecycle hooks, state watchers, component functions, completions, and definition navigation.", - "version": "0.2.10", + "version": "0.2.11", "publisher": "wrnexus", "private": true, "license": "SEE LICENSE IN LICENSE", @@ -52,7 +52,9 @@ ], "main": "./src/extension.js", "activationEvents": [ - "onLanguage:wrn" + "onLanguage:wrn", + "workspaceContains:**/*.wrn", + "onStartupFinished" ], "contributes": { "languages": [ @@ -66,6 +68,7 @@ "extensions": [ ".wrn" ], + "firstLine": "^\\s*(?:page|component|layout)\\s+[A-Za-z_$][A-Za-z0-9_$]*\\s*\\{", "configuration": "./language-configuration.json", "icon": { "light": "./icons/wrn.png", @@ -125,6 +128,9 @@ } }, "configurationDefaults": { + "files.associations": { + "*.wrn": "wrn" + }, "[wrn]": { "editor.defaultFormatter": "wrnexus.wrnexus", "editor.formatOnSave": false, @@ -253,8 +259,9 @@ "scripts": { "build:compiler": "bun build ../../packages/compiler/src/index.ts --target=node --format=cjs --outfile=src/compiler.cjs", "build": "bun run build:compiler", + "test": "node --test test/*.test.js", "validate": "node test/validate.mjs", - "check": "bun run build && bun run validate", + "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", diff --git a/editors/vscode/src/compiler.cjs b/editors/vscode/src/compiler.cjs index 4616c95d..b1c90383 100644 --- a/editors/vscode/src/compiler.cjs +++ b/editors/vscode/src/compiler.cjs @@ -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}`; } 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(`