Files
WRNexusJS/editors/vscode/src/v060-language.js
T
Clintchiz 586a6db8ff
Quality / quality (ubuntu-latest) (push) Failing after 21s
Quality / quality (windows-latest) (push) Canceled after 0s
release: WRNexusJS 0.8.0
2026-08-02 23:18:51 +05:30

202 lines
6.6 KiB
JavaScript

// @ts-check
"use strict";
const vscode = require("vscode");
const selector = { language: "wrn" };
const WRN_GLOB = "**/*.wrn";
const EXCLUDE_GLOB = "**/{node_modules,dist,.wrnexus,.git}/**";
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function identifierAt(document, position) {
const range = document.getWordRangeAtPosition(position, /[A-Za-z_$][A-Za-z0-9_$]*/);
if (!range) return null;
return { name: document.getText(range), range };
}
function occurrences(document, name) {
const result = [];
const pattern = new RegExp(`\\b${escapeRegExp(name)}\\b`, "g");
const source = document.getText();
let match;
while ((match = pattern.exec(source)) !== null) {
result.push(
new vscode.Range(
document.positionAt(match.index),
document.positionAt(match.index + name.length),
),
);
}
return result;
}
function declarationRanges(document, name) {
const source = document.getText();
const escaped = escapeRegExp(name);
const patterns = [
new RegExp(`\\b(?:page|component|layout)\\s+(${escaped})\\b`, "g"),
new RegExp(`\\b(?:global|page)\\s+store\\s+(${escaped})\\b`, "g"),
new RegExp(`\\b(?:client|server|shared)?\\s*(?:async\\s+)?function\\s+(${escaped})\\b`, "g"),
new RegExp(`\\b(?:client|server|shared)?\\s*state\\s+(${escaped})\\b`, "g"),
new RegExp(`\\boutputs\\s*\\{[\\s\\S]*?\\b(${escaped})\\s*\\(`, "g"),
new RegExp(
`\\b(?:props|state|computed)\\s*\\{[\\s\\S]*?\\b(${escaped})(?:\\?|\\s)*(?=[:=])`,
"g",
),
new RegExp(`\\bimport(?:\\s+type)?[\\s\\S]*?\\b(${escaped})\\b[\\s\\S]*?\\bfrom\\b`, "g"),
];
const ranges = [];
for (const pattern of patterns) {
let match;
while ((match = pattern.exec(source)) !== null) {
const relative = match[0].lastIndexOf(match[1]);
const start = match.index + relative;
ranges.push(
new vscode.Range(document.positionAt(start), document.positionAt(start + name.length)),
);
}
}
return ranges;
}
async function wrnDocuments() {
const uris = await vscode.workspace.findFiles(WRN_GLOB, EXCLUDE_GLOB, 1_000);
const open = new Map(
vscode.workspace.textDocuments
.filter((item) => item.languageId === "wrn")
.map((item) => [item.uri.toString(), item]),
);
return Promise.all(
uris.map((uri) => open.get(uri.toString()) || vscode.workspace.openTextDocument(uri)),
);
}
async function provideReferences(document, position, context, token) {
const symbol = identifierAt(document, position);
if (!symbol || token.isCancellationRequested) return [];
const locations = [];
for (const candidate of await wrnDocuments()) {
if (token.isCancellationRequested) return locations;
for (const range of occurrences(candidate, symbol.name)) {
if (
!context.includeDeclaration &&
declarationRanges(candidate, symbol.name).some((decl) => decl.isEqual(range))
)
continue;
locations.push(new vscode.Location(candidate.uri, range));
}
}
return locations;
}
async function prepareRename(document, position) {
const symbol = identifierAt(document, position);
if (!symbol) throw new Error("Place the cursor on a WRN identifier.");
return { range: symbol.range, placeholder: symbol.name };
}
async function provideRenameEdits(document, position, newName, token) {
if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(newName))
throw new Error("The new WRN name is not a valid identifier.");
const symbol = identifierAt(document, position);
if (!symbol) return null;
const edit = new vscode.WorkspaceEdit();
for (const candidate of await wrnDocuments()) {
if (token.isCancellationRequested) return edit;
for (const range of occurrences(candidate, symbol.name))
edit.replace(candidate.uri, range, newName);
}
return edit;
}
function importRanges(source) {
const ranges = [];
const pattern =
/^import(?:\s+type)?[\s\S]*?(?:;\s*|\n(?=import|\s*(?:page|component|layout|global\s+store|page\s+store)\b))/gm;
let match;
while ((match = pattern.exec(source)) !== null)
ranges.push({ start: match.index, end: match.index + match[0].length, text: match[0].trim() });
return ranges;
}
function organizeImportsEdit(document) {
const source = document.getText();
const imports = importRanges(source);
if (imports.length < 2) return null;
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("\n") + "\n";
const start = imports[0].start;
const end = imports.at(-1).end;
if (source.slice(start, end) === replacement) return null;
return vscode.TextEdit.replace(
new vscode.Range(document.positionAt(start), document.positionAt(end)),
replacement,
);
}
function provideCodeActions(document, range, context) {
const actions = [];
const organizeEdit = organizeImportsEdit(document);
if (organizeEdit) {
const action = new vscode.CodeAction(
"Organize WRN imports",
vscode.CodeActionKind.SourceOrganizeImports,
);
action.edit = new vscode.WorkspaceEdit();
action.edit.set(document.uri, [organizeEdit]);
actions.push(action);
}
for (const diagnostic of context.diagnostics) {
if (diagnostic.code === "WRN-OUTPUT-LEGACY-EMIT") {
const source = document.getText(diagnostic.range);
const match = /\$emit\(\s*["']([A-Za-z_$][\w$]*)["']\s*,?/.exec(source);
if (!match) continue;
const action = new vscode.CodeAction(
`Convert $emit to output.${match[1]}`,
vscode.CodeActionKind.QuickFix,
);
action.diagnostics = [diagnostic];
action.isPreferred = true;
action.edit = new vscode.WorkspaceEdit();
action.edit.replace(
document.uri,
diagnostic.range,
source.replace(/\$emit\(\s*["'][A-Za-z_$][\w$]*["']\s*,?\s*/, `output.${match[1]}(`),
);
actions.push(action);
}
}
return actions;
}
function registerV060LanguageFeatures(context) {
context.subscriptions.push(
vscode.languages.registerReferenceProvider(selector, { provideReferences }),
vscode.languages.registerRenameProvider(selector, { prepareRename, provideRenameEdits }),
vscode.languages.registerCodeActionsProvider(
selector,
{ provideCodeActions },
{
providedCodeActionKinds: [
vscode.CodeActionKind.QuickFix,
vscode.CodeActionKind.SourceOrganizeImports,
],
},
),
);
}
module.exports = {
declarationRanges,
importRanges,
organizeImportsEdit,
registerV060LanguageFeatures,
};