202 lines
6.6 KiB
JavaScript
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,
|
|
};
|