release: WRNexusJS 0.6.0
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
// @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);
|
||||
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,
|
||||
};
|
||||
Reference in New Issue
Block a user