fix(vscode): repair WRN language diagnostics and activation
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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*$"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+2
-2
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
+126
-31
@@ -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, "&")
|
||||
.replace(/"/g, """)
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">");
|
||||
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, "&")
|
||||
.replace(/"/g, """)
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">");
|
||||
}`);
|
||||
}
|
||||
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)}`;
|
||||
}
|
||||
|
||||
@@ -517,7 +517,6 @@ function registerCompletionProvider(context) {
|
||||
const provider = vscode.languages.registerCompletionItemProvider(
|
||||
{
|
||||
language: "wrn",
|
||||
scheme: "file",
|
||||
},
|
||||
{
|
||||
provideCompletionItems,
|
||||
|
||||
@@ -99,7 +99,6 @@ function registerDefinitionProvider(context) {
|
||||
const disposable = vscode.languages.registerDefinitionProvider(
|
||||
{
|
||||
language: "wrn",
|
||||
scheme: "file",
|
||||
},
|
||||
{
|
||||
provideDefinition,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -582,39 +582,6 @@
|
||||
"name": "punctuation.definition.string.begin.wrn"
|
||||
}
|
||||
},
|
||||
"conditional-class-single": {
|
||||
"name": "meta.attribute.class-directive.wrn",
|
||||
"begin": "(?<=\\s)(class)(:)((?:\\[[^\\]\\r\\n]*\\]|[^\\s=])+)(\\s*=\\s*)(')",
|
||||
"beginCaptures": {
|
||||
"1": {
|
||||
"name": "entity.other.attribute-name.class.wrn"
|
||||
},
|
||||
"2": {
|
||||
"name": "punctuation.separator.key-value.wrn"
|
||||
},
|
||||
"3": {
|
||||
"name": "string.unquoted.class-name.wrn"
|
||||
},
|
||||
"4": {
|
||||
"name": "keyword.operator.assignment.wrn"
|
||||
},
|
||||
"5": {
|
||||
"name": "punctuation.definition.string.begin.wrn"
|
||||
}
|
||||
},
|
||||
"end": "'",
|
||||
"endCaptures": {
|
||||
"0": {
|
||||
"name": "punctuation.definition.string.end.wrn"
|
||||
}
|
||||
},
|
||||
"contentName": "meta.embedded.expression.wrn",
|
||||
"patterns": [
|
||||
{
|
||||
"include": "source.ts"
|
||||
}
|
||||
]
|
||||
},
|
||||
"end": "\"",
|
||||
"endCaptures": {
|
||||
"0": {
|
||||
@@ -628,6 +595,39 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"conditional-class-single": {
|
||||
"name": "meta.attribute.class-directive.wrn",
|
||||
"begin": "(?<=\\s)(class)(:)((?:\\[[^\\]\\r\\n]*\\]|[^\\s=])+)(\\s*=\\s*)(')",
|
||||
"beginCaptures": {
|
||||
"1": {
|
||||
"name": "entity.other.attribute-name.class.wrn"
|
||||
},
|
||||
"2": {
|
||||
"name": "punctuation.separator.key-value.wrn"
|
||||
},
|
||||
"3": {
|
||||
"name": "string.unquoted.class-name.wrn"
|
||||
},
|
||||
"4": {
|
||||
"name": "keyword.operator.assignment.wrn"
|
||||
},
|
||||
"5": {
|
||||
"name": "punctuation.definition.string.begin.wrn"
|
||||
}
|
||||
},
|
||||
"end": "'",
|
||||
"endCaptures": {
|
||||
"0": {
|
||||
"name": "punctuation.definition.string.end.wrn"
|
||||
}
|
||||
},
|
||||
"contentName": "meta.embedded.expression.wrn",
|
||||
"patterns": [
|
||||
{
|
||||
"include": "source.ts"
|
||||
}
|
||||
]
|
||||
},
|
||||
"interpolation": {
|
||||
"patterns": [
|
||||
{
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
"use strict";
|
||||
|
||||
const assert = require("node:assert");
|
||||
const { test } = require("node:test");
|
||||
const Module = require("node:module");
|
||||
|
||||
// These extraction helpers are pure, but their module also registers VS Code
|
||||
// providers at runtime. Supply a minimal host shim for unit tests.
|
||||
const originalLoad = Module._load;
|
||||
Module._load = function load(request, parent, isMain) {
|
||||
if (request === "vscode") return {};
|
||||
return originalLoad.call(this, request, parent, isMain);
|
||||
};
|
||||
const { extractRouteParams, extractStates } = require("../src/completion");
|
||||
Module._load = originalLoad;
|
||||
|
||||
test("extracts dynamic route params from filename", () => {
|
||||
const document = {
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
"use strict";
|
||||
|
||||
const assert = require("node:assert");
|
||||
const { test } = require("node:test");
|
||||
const Module = require("node:module");
|
||||
|
||||
const originalLoad = Module._load;
|
||||
Module._load = function load(request, parent, isMain) {
|
||||
if (request === "vscode") {
|
||||
return {
|
||||
Diagnostic: class Diagnostic {
|
||||
constructor(range, message, severity) {
|
||||
this.range = range;
|
||||
this.message = message;
|
||||
this.severity = severity;
|
||||
}
|
||||
},
|
||||
DiagnosticSeverity: { Error: 0, Warning: 1 },
|
||||
Range: class Range {
|
||||
constructor(start, end) {
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
return originalLoad.call(this, request, parent, isMain);
|
||||
};
|
||||
const {
|
||||
findTopLevelDeclaration,
|
||||
maskLeadingTrivia,
|
||||
validateBalancedCharacters,
|
||||
} = require("../src/diagnostics");
|
||||
Module._load = originalLoad;
|
||||
|
||||
function mockDocument(source) {
|
||||
return {
|
||||
positionAt(offset) {
|
||||
return offset;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test("recognizes a WRN declaration after leading line comments", () => {
|
||||
const source = `// Component summary
|
||||
// More details
|
||||
|
||||
component ThemeToggle {
|
||||
view { <button>Toggle</button> }
|
||||
}`;
|
||||
|
||||
const declaration = findTopLevelDeclaration({}, source);
|
||||
|
||||
assert.equal(declaration.kind, "component");
|
||||
assert.equal(declaration.name, "ThemeToggle");
|
||||
assert.equal(declaration.diagnostic, undefined);
|
||||
});
|
||||
|
||||
test("masks only leading trivia and preserves source offsets", () => {
|
||||
const source = "\uFEFF// Summary\r\npage Home {\n // member comment\n}";
|
||||
const masked = maskLeadingTrivia(source);
|
||||
|
||||
assert.equal(masked.length, source.length);
|
||||
assert.equal(masked.indexOf("page Home"), source.indexOf("page Home"));
|
||||
assert.match(masked, /\/\/ member comment/);
|
||||
});
|
||||
|
||||
test("ignores apostrophes and brackets in line comments when checking balance", () => {
|
||||
const source = `// The framework's theme runtime binds the click }
|
||||
component ThemeToggle {
|
||||
props {
|
||||
label = "Toggle theme"
|
||||
class = ""
|
||||
}
|
||||
view { <button class="{class}" data-wire-theme-toggle><slot>{label}</slot></button> }
|
||||
}`;
|
||||
|
||||
const diagnostics = validateBalancedCharacters(mockDocument(source), source);
|
||||
|
||||
assert.deepEqual(diagnostics, []);
|
||||
});
|
||||
@@ -37,10 +37,48 @@ for (const rel of [
|
||||
const grammar = JSON.parse(readFileSync(join(root, "syntaxes/wrn.tmLanguage.json"), "utf8"));
|
||||
grammar.scopeName === "source.wrn" ? ok("grammar scopeName") : bad("grammar scopeName");
|
||||
|
||||
const localIncludes = [];
|
||||
const visitGrammarNode = (value) => {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(visitGrammarNode);
|
||||
return;
|
||||
}
|
||||
if (!value || typeof value !== "object") return;
|
||||
if (typeof value.include === "string" && value.include.startsWith("#")) {
|
||||
localIncludes.push(value.include.slice(1));
|
||||
}
|
||||
Object.values(value).forEach(visitGrammarNode);
|
||||
};
|
||||
visitGrammarNode(grammar.patterns);
|
||||
visitGrammarNode(grammar.repository);
|
||||
const missingIncludes = [...new Set(localIncludes)].filter((name) => !grammar.repository?.[name]);
|
||||
missingIncludes.length === 0
|
||||
? ok("all local grammar includes resolve")
|
||||
: bad("all local grammar includes resolve", missingIncludes.join(", "));
|
||||
grammar.repository?.["conditional-class-single"]
|
||||
? ok("single-quoted class directives are registered")
|
||||
: bad("single-quoted class directives are registered");
|
||||
|
||||
// 3. Marketplace metadata and the gallery icon meet vsce requirements.
|
||||
try {
|
||||
const manifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
||||
manifest.publisher === "wrnexus" ? ok("publisher id") : bad("publisher id");
|
||||
const language = manifest.contributes?.languages?.find((entry) => entry.id === "wrn");
|
||||
language?.extensions?.includes(".wrn")
|
||||
? ok(".wrn extension maps to the wrn language")
|
||||
: bad(".wrn extension maps to the wrn language");
|
||||
manifest.contributes?.configurationDefaults?.["files.associations"]?.["*.wrn"] === "wrn"
|
||||
? ok("default file association uses the wrn language id")
|
||||
: bad("default file association uses the wrn language id");
|
||||
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");
|
||||
language?.firstLine
|
||||
? ok("first-line language detection is configured")
|
||||
: bad("first-line language detection is configured");
|
||||
manifest.icon?.endsWith(".png") ? ok("Marketplace icon is PNG") : bad("Marketplace icon is PNG");
|
||||
const icon = readFileSync(join(root, manifest.icon));
|
||||
const isPng = icon.subarray(1, 4).toString("ascii") === "PNG";
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user