perlnavigator-server
Version:
Perl language server
170 lines (143 loc) • 6.59 kB
text/typescript
import { DefinitionParams, Location, WorkspaceFolder } from "vscode-languageserver/node";
import { TextDocument } from "vscode-languageserver-textdocument";
import { PerlDocument, PerlElem, NavigatorSettings, ElemSource, ParseType } from "./types";
import Uri from "vscode-uri";
import { realpathSync, existsSync, realpath, promises } from "fs";
import { getIncPaths, async_execFile, getSymbol, lookupSymbol, nLog, isFile } from "./utils";
import { dirname, join } from "path";
import { getPerlAssetsPath } from "./assets";
import { refineElement } from "./refinement";
export async function getDefinition(params: DefinitionParams, perlDoc: PerlDocument, txtDoc: TextDocument, modMap: Map<string, string>): Promise<Location[] | undefined> {
let position = params.position;
const symbol = getSymbol(position, txtDoc);
if (!symbol) return;
const foundElems = lookupSymbol(perlDoc, modMap, symbol, position.line);
if (foundElems.length == 0) {
return;
}
let locationsFound: Location[] = [];
for (const elem of foundElems) {
const elemResolved: PerlElem | undefined = await resolveElemForNav(perlDoc, elem, symbol);
if (!elemResolved) continue;
let uri: string;
if (perlDoc.uri !== elemResolved.uri) {
// If sending to a different file, let's make sure it exists and clean up the path
const file = Uri.parse(elemResolved.uri).fsPath;
if (!(await isFile(file))) continue; // Make sure the file exists and hasn't been deleted.
uri = Uri.file(realpathSync(file)).toString(); // Resolve symlinks
} else {
// Sending to current file (including untitled files)
uri = perlDoc.uri;
}
const newLoc: Location = {
uri: uri,
range: {
start: { line: elemResolved.line, character: 0 },
end: { line: elemResolved.line, character: 500 },
},
};
locationsFound.push(newLoc);
}
// const count = locationsFound
return locationsFound;
}
async function resolveElemForNav(perlDoc: PerlDocument, elem: PerlElem, symbol: string): Promise<PerlElem | undefined> {
let refined = await refineElement(elem, perlDoc);
elem = refined || elem;
if (!badFile(elem.uri)) {
if (perlDoc.uri == elem.uri && symbol.includes("->")) {
// Corinna methods don't have line numbers. Let's hunt for them. If you dont find anything better, just return the original element.
const method = symbol.split("->").pop();
if (method) {
// Shouldn't this always be defined? Double check
const found = perlDoc.elems.get(method);
if (found) {
if (elem.line == 0 && elem.type == "x") {
if (found[0].uri == perlDoc.uri) return found[0];
} else if (elem.line > 0 && elem.type == "t") {
// Solve the off-by-one error at least for these. Eventually, you could consult a tagger for this step.
for (let potentialElem of found) {
if (Math.abs(potentialElem.line - elem.line) <= 1) {
return potentialElem;
}
}
}
}
}
// Otherwise give-up
}
// Normal path; file is good
return elem;
} else {
// Try looking it up by package instead of file.
// Happens with XS subs and Moo subs
if (elem.package) {
const elemResolved = perlDoc.elems.get(elem.package);
if (elemResolved) {
for (let potentialElem of elemResolved) {
if (potentialElem.uri && !badFile(potentialElem.uri)) {
return potentialElem;
}
}
}
}
// Finding the module with the stored mod didn't work. Let's try navigating to the package itself instead of Foo::Bar->method().
// Many Moose methods end up here.
// Not very helpful, since the user can simply click on the module manually if they want
// const base_module = symbol.match(/^([\w:]+)->\w+$/);
// if(base_module){
// const elemResolved = perlDoc.elems.get(base_module);
// if(elemResolved && elemResolved.file && !badFile(elem.file)){
// return elemResolved;
// }
// }
}
return;
}
function badFile(uri: string): boolean {
if (!uri) {
return true;
}
const fsPath = Uri.parse(uri).fsPath;
if (!fsPath || fsPath.length <= 1) {
// Single forward slashes seem to sneak in here.
return true;
}
return /(?:Sub[\\\/]Defer\.pm|Moo[\\\/]Object\.pm|Moose[\\\/]Object\.pm|\w+\.c|Inspectorito\.pm)$/.test(fsPath);
}
export async function getAvailableMods(workspaceFolders: WorkspaceFolder[] | null, settings: NavigatorSettings): Promise<Map<string, string>> {
let perlParams = settings.perlParams;
perlParams = perlParams.concat(getIncPaths(workspaceFolders, settings));
const modHunterPath = join(await getPerlAssetsPath(), "lib_bs22", "ModHunter.pl");
perlParams.push(modHunterPath);
nLog("Starting to look for perl modules with " + perlParams.join(" "), settings);
const mods: Map<string, string> = new Map();
let output: string;
try {
// This can be slow, especially if reading modules over a network or on windows.
const out = await async_execFile(settings.perlPath, perlParams, { timeout: 90000, maxBuffer: 20 * 1024 * 1024 });
output = out.stdout;
nLog("Success running mod hunter", settings);
} catch (error: any) {
nLog("ModHunter failed. You will lose autocomplete on importing modules. Not a huge deal", settings);
nLog(error, settings);
return mods;
}
output.split("\n").forEach((mod) => {
var items = mod.split("\t");
if (items.length != 5 || items[1] != "M" || !items[2] || !items[3]) {
return;
}
// Load file
realpath(items[3], function (err, path) {
if (err) {
// Skip if error
} else {
if (!path) return; // Could file be empty, but no error?
let uri = Uri.file(path).toString(); // Resolve symlinks
mods.set(items[2], uri);
}
});
});
return mods;
}