release: WRNexusJS 0.2.32
This commit is contained in:
+243
-217
@@ -2,257 +2,283 @@
|
||||
"use strict";
|
||||
|
||||
const vscode = require("vscode");
|
||||
|
||||
const { formatWrn } = require("./formatter");
|
||||
|
||||
const { registerCompletionProvider } = require("./completion");
|
||||
|
||||
const { registerDefinitionProvider } = require("./definition");
|
||||
|
||||
const { registerDiagnostics } = require("./diagnostics");
|
||||
|
||||
// The .wrn compiler, bundled to CJS by `bun run build:compiler`. Loaded
|
||||
// defensively so the rest of the extension (highlighting, snippets, completion)
|
||||
// still works even if the bundle is missing.
|
||||
/**
|
||||
* 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 (err) {
|
||||
console.warn("[wrnexus] compiler bundle not found; diagnostics disabled.", err && err.message);
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"[wrnexus] compiler bundle not found; compiler diagnostics disabled.",
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
}
|
||||
|
||||
/** Top-level block keywords offered at file scope. */
|
||||
const BLOCK_KEYWORDS = [
|
||||
["page", "page ${1:Name} {\n\t$0\n}", "A route page"],
|
||||
["component", "component ${1:Name} {\n\t$0\n}", "A reusable, prop-driven component"],
|
||||
["layout", 'layout = "${1:public}"', "Select a layout for this page"],
|
||||
["state", "state ${1:count} = ${2:0}", "Reactive state seeded into the scope"],
|
||||
["props", 'props {\n\t${1:label} = ${2:"Label"}\n}', "Component props with typed defaults"],
|
||||
["view", "view {\n\t$0\n}", "Server-rendered HTML"],
|
||||
["seo", 'seo {\n\ttitle = "$1"\n\tdescription = "$2"\n}', "Per-page SEO metadata"],
|
||||
["api", "api ${1|GET,POST,PUT,PATCH,DELETE|} ${2:/path} {\n\t$0\n}", "Colocated API route"],
|
||||
["ssr", "ssr {\n\tapi ${1:load} GET ${2:/api/data} {\n\t\t$0\n\t}\n}", "Server-side data block"],
|
||||
[
|
||||
"client",
|
||||
"client {\n\tapi ${1:load} GET ${2:/api/data} {\n\t\t$0\n\t}\n}",
|
||||
"Client-side data block",
|
||||
],
|
||||
[
|
||||
"realtime",
|
||||
"realtime ${1:chat} {\n\ton ${2:message}(${3:data}) {\n\t\t$0\n\t}\n}",
|
||||
"Realtime channel",
|
||||
],
|
||||
["functions", "functions {\n\t$0\n}", "Shared helper functions"],
|
||||
["style", "style {\n\t$0\n}", "Scoped CSS"],
|
||||
];
|
||||
|
||||
/** `data-*` attributes understood by the reactive runtime. */
|
||||
const DATA_ATTRS = [
|
||||
["data-component", 'data-component="$1"', "Mount a component by name"],
|
||||
["data-for", 'data-for="${1:item} in ${2:items}"', "Repeat this element per item"],
|
||||
["data-show", 'data-show="${1:condition}"', "Toggle visibility reactively"],
|
||||
["data-text", 'data-text="${1:expr}"', "Bind text content to an expression"],
|
||||
["data-scope", 'data-scope="${1:key}: ${2:value}"', "Declare a local reactive scope"],
|
||||
["data-slot", 'data-slot="${1:name}"', "Fill a named <slot> on a component"],
|
||||
];
|
||||
|
||||
/** Client event bindings. */
|
||||
const EVENTS = [
|
||||
"click",
|
||||
"input",
|
||||
"change",
|
||||
"submit",
|
||||
"keydown",
|
||||
"keyup",
|
||||
"focus",
|
||||
"blur",
|
||||
"mouseenter",
|
||||
"mouseleave",
|
||||
];
|
||||
const WRN_LANGUAGE_ID = "wrn";
|
||||
const COMPILER_DIAGNOSTIC_COLLECTION = "wrnexus-compiler";
|
||||
|
||||
/**
|
||||
* Register the WRN document formatter.
|
||||
*
|
||||
* @param {vscode.ExtensionContext} context
|
||||
*/
|
||||
function activate(context) {
|
||||
registerDiagnostics(context);
|
||||
registerCompletionProvider(context);
|
||||
registerDefinitionProvider(context);
|
||||
|
||||
const diagnostics = vscode.languages.createDiagnosticCollection("wrn");
|
||||
context.subscriptions.push(diagnostics);
|
||||
|
||||
const timers = new Map();
|
||||
const runDiagnostics = (doc) => {
|
||||
if (doc.languageId !== "wrn") return;
|
||||
if (!vscode.workspace.getConfiguration("wrnexus").get("diagnostics.enable", true)) {
|
||||
diagnostics.delete(doc.uri);
|
||||
return;
|
||||
}
|
||||
if (!compiler || typeof compiler.compileWireFile !== "function") return;
|
||||
|
||||
const text = doc.getText();
|
||||
/** @type {vscode.Diagnostic[]} */
|
||||
const found = [];
|
||||
try {
|
||||
compiler.compileWireFile(text);
|
||||
} catch (err) {
|
||||
found.push(toDiagnostic(doc, err));
|
||||
}
|
||||
diagnostics.set(doc.uri, found);
|
||||
};
|
||||
|
||||
const schedule = (doc) => {
|
||||
const key = doc.uri.toString();
|
||||
clearTimeout(timers.get(key));
|
||||
timers.set(
|
||||
key,
|
||||
setTimeout(() => {
|
||||
timers.delete(key);
|
||||
runDiagnostics(doc);
|
||||
}, 250),
|
||||
);
|
||||
};
|
||||
|
||||
// Lint on open, edit and save; clear on close.
|
||||
vscode.workspace.textDocuments.forEach(runDiagnostics);
|
||||
context.subscriptions.push(
|
||||
vscode.workspace.onDidOpenTextDocument(runDiagnostics),
|
||||
vscode.workspace.onDidChangeTextDocument((e) => schedule(e.document)),
|
||||
vscode.workspace.onDidSaveTextDocument(runDiagnostics),
|
||||
vscode.workspace.onDidCloseTextDocument((doc) => diagnostics.delete(doc.uri)),
|
||||
);
|
||||
|
||||
// Completions.
|
||||
context.subscriptions.push(
|
||||
vscode.languages.registerDocumentFormattingEditProvider("wrn", {
|
||||
function registerFormatter(context) {
|
||||
const provider = vscode.languages.registerDocumentFormattingEditProvider(
|
||||
{
|
||||
language: WRN_LANGUAGE_ID,
|
||||
scheme: "file",
|
||||
},
|
||||
{
|
||||
provideDocumentFormattingEdits(document, options) {
|
||||
const configuration = vscode.workspace.getConfiguration("wrnexus", document.uri);
|
||||
|
||||
const enabled = configuration.get("format.enable", true);
|
||||
|
||||
if (!enabled) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const source = document.getText();
|
||||
|
||||
const printWidth = configuration.get("formatting.printWidth", 100);
|
||||
|
||||
const formatted = formatWrn(source, {
|
||||
tabSize: options.tabSize,
|
||||
insertSpaces: options.insertSpaces,
|
||||
printWidth: 100,
|
||||
printWidth,
|
||||
});
|
||||
|
||||
if (formatted === source) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const range = new vscode.Range(document.positionAt(0), document.positionAt(source.length));
|
||||
|
||||
return [vscode.TextEdit.replace(range, formatted)];
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
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.languages.registerCompletionItemProvider(
|
||||
"wrn",
|
||||
{ provideCompletionItems: provideCompletions },
|
||||
"-",
|
||||
"@",
|
||||
"{",
|
||||
":",
|
||||
),
|
||||
|
||||
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();
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a thrown ParseError to a VS Code diagnostic. The compiler encodes the
|
||||
* failure position as `... at offset <N>` in the message; we resolve it to a
|
||||
* range. Errors without an offset (e.g. "Unexpected end of input") anchor to the
|
||||
* end of the document.
|
||||
* @param {vscode.TextDocument} doc
|
||||
* @param {unknown} err
|
||||
* @returns {vscode.Diagnostic}
|
||||
* Activate the WRNexus VS Code extension.
|
||||
*
|
||||
* @param {vscode.ExtensionContext} context
|
||||
*/
|
||||
function toDiagnostic(doc, err) {
|
||||
const message = err && err.message ? String(err.message) : "Failed to parse .wrn file";
|
||||
const match = /offset\s+(\d+)/.exec(message);
|
||||
let range;
|
||||
if (match) {
|
||||
const offset = Number(match[1]);
|
||||
const start = doc.positionAt(offset);
|
||||
const wordRange = doc.getWordRangeAtPosition(start);
|
||||
range = wordRange || new vscode.Range(start, doc.positionAt(offset + 1));
|
||||
} else {
|
||||
const last = doc.lineAt(Math.max(0, doc.lineCount - 1));
|
||||
range = new vscode.Range(last.range.start, last.range.end);
|
||||
}
|
||||
const diag = new vscode.Diagnostic(range, message, vscode.DiagnosticSeverity.Error);
|
||||
diag.source = "wrn";
|
||||
return diag;
|
||||
}
|
||||
function activate(context) {
|
||||
registerDiagnostics(context);
|
||||
registerCompilerDiagnostics(context);
|
||||
|
||||
/**
|
||||
* @param {vscode.TextDocument} document
|
||||
* @param {vscode.Position} position
|
||||
* @returns {vscode.CompletionItem[]}
|
||||
*/
|
||||
function provideCompletions(document, position) {
|
||||
const line = document.lineAt(position).text;
|
||||
const before = line.slice(0, position.character);
|
||||
const full = document.getText();
|
||||
const upto = document.offsetAt(position);
|
||||
const inView = isInsideBlock(full, upto, "view");
|
||||
|
||||
// `{t:` — offer nothing structured, just let the user type the key.
|
||||
// `@` inside a view — event bindings.
|
||||
if (inView && /@[A-Za-z-]*$/.test(before)) {
|
||||
return EVENTS.map((ev) => {
|
||||
const item = new vscode.CompletionItem(ev, vscode.CompletionItemKind.Event);
|
||||
item.insertText = new vscode.SnippetString(`${ev}="$0"`);
|
||||
item.detail = "wrn event binding";
|
||||
// Replace the `@`-less part already typed.
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
// `data-` inside a view — reactive attributes.
|
||||
if (inView && /(^|\s)data-[A-Za-z-]*$/.test(before)) {
|
||||
return DATA_ATTRS.map(([label, snippet, doc]) => {
|
||||
const item = new vscode.CompletionItem(label, vscode.CompletionItemKind.Property);
|
||||
item.insertText = new vscode.SnippetString(String(snippet));
|
||||
item.documentation = new vscode.MarkdownString(String(doc));
|
||||
item.detail = "wrn runtime attribute";
|
||||
const dashIdx = before.lastIndexOf("data-");
|
||||
item.range = new vscode.Range(position.line, dashIdx, position.line, position.character);
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
// Top-level block keywords, when not inside a view/style/functions body.
|
||||
if (!inView && !isInsideBlock(full, upto, "style") && !isInsideBlock(full, upto, "functions")) {
|
||||
return BLOCK_KEYWORDS.map(([label, snippet, doc]) => {
|
||||
const item = new vscode.CompletionItem(String(label), vscode.CompletionItemKind.Keyword);
|
||||
item.insertText = new vscode.SnippetString(String(snippet));
|
||||
item.documentation = new vscode.MarkdownString(String(doc));
|
||||
item.detail = "wrn block";
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Very small heuristic: is `offset` inside a `<name> { ... }` block? Finds the
|
||||
* nearest preceding `name {` opener and checks braces don't balance before the
|
||||
* cursor. Good enough to gate view/style/functions-aware completions.
|
||||
* @param {string} text
|
||||
* @param {number} offset
|
||||
* @param {string} name
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isInsideBlock(text, offset, name) {
|
||||
const re = new RegExp("\\b" + name + "\\b\\s*(?:[^\\n{]*)\\{", "g");
|
||||
let opener = -1;
|
||||
let m;
|
||||
while ((m = re.exec(text)) !== null) {
|
||||
const bracePos = m.index + m[0].length - 1;
|
||||
if (bracePos >= offset) break;
|
||||
opener = bracePos;
|
||||
}
|
||||
if (opener === -1) return false;
|
||||
// Count braces between the opener and the cursor; still open => inside.
|
||||
let depth = 0;
|
||||
for (let i = opener; i < offset && i < text.length; i++) {
|
||||
const c = text[i];
|
||||
if (c === "{") depth++;
|
||||
else if (c === "}") depth--;
|
||||
}
|
||||
return depth > 0;
|
||||
registerCompletionProvider(context);
|
||||
registerDefinitionProvider(context);
|
||||
registerFormatter(context);
|
||||
}
|
||||
|
||||
function deactivate() {}
|
||||
|
||||
module.exports = { activate, deactivate };
|
||||
module.exports = {
|
||||
activate,
|
||||
deactivate,
|
||||
registerCompilerDiagnostics,
|
||||
registerFormatter,
|
||||
toCompilerDiagnostic,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user