perlnavigator-server
Version:
Perl language server
258 lines • 11.1 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.isFile = exports.getPerlimportsProfile = exports.nLog = exports.lookupSymbol = exports.getSymbol = exports.getIncPaths = exports.async_execFile = void 0;
const vscode_uri_1 = require("vscode-uri");
const child_process_1 = require("child_process");
const util_1 = require("util");
const types_1 = require("./types");
const path = require("path");
const fs_1 = require("fs");
exports.async_execFile = (0, util_1.promisify)(child_process_1.execFile);
// TODO: This behaviour should be temporary. Review and update treatment of multi-root workspaces
function getIncPaths(workspaceFolders, settings) {
let includePaths = [];
settings.includePaths.forEach((path) => {
if (path.indexOf("$workspaceFolder") != -1) {
if (workspaceFolders) {
workspaceFolders.forEach((workspaceFolder) => {
const incPath = vscode_uri_1.default.parse(workspaceFolder.uri).fsPath;
includePaths = includePaths.concat(["-I", path.replaceAll("$workspaceFolder", incPath)]);
});
}
else {
nLog("You used $workspaceFolder in your config, but didn't add any workspace folders. Skipping " + path, settings);
}
}
else {
includePaths = includePaths.concat(["-I", path]);
}
});
if (settings.includeLib) {
// Add project root / lib for each workspace folder.
if (workspaceFolders) {
workspaceFolders.forEach((workspaceFolder) => {
const rootPath = vscode_uri_1.default.parse(workspaceFolder.uri).fsPath;
includePaths = includePaths.concat(["-I", path.join(rootPath, "lib")]);
});
}
}
return includePaths;
}
exports.getIncPaths = getIncPaths;
function getSymbol(position, txtDoc) {
// Gets symbol from text at position.
// Ignore :: going left, but stop at :: when going to the right. (e.g Foo::bar::baz should be clickable on each spot)
// Todo: Only allow -> once.
// Used for navigation and hover.
const start = { line: position.line, character: 0 };
const end = { line: position.line + 1, character: 0 };
const text = txtDoc.getText({ start, end });
const index = txtDoc.offsetAt(position) - txtDoc.offsetAt(start);
const leftRg = /[\p{L}\p{N}_:>-]/u;
const rightRg = /[\p{L}\p{N}_]/u;
const leftAllow = (c) => leftRg.exec(c);
const rightAllow = (c) => rightRg.exec(c);
let left = index - 1;
let right = index;
if (right < text.length && (["$", "%", "@"].includes(text[right]) || rightAllow(text[right]))) {
// Handles an edge case where the cursor is on the side of a symbol.
// Note that $foo| should find $foo (where | represents cursor), but $foo|$bar should find $bar, and |mysub should find mysub
right += 1;
left += 1;
}
while (left >= 0 && leftAllow(text[left])) {
// Allow for ->, but not => or > (e.g. $foo->bar, but not $foo=>bar or $foo>bar)
if (text[left] === ">" && left - 1 >= 0 && text[left - 1] !== "-") {
break;
}
left -= 1;
}
left = Math.max(0, left + 1);
while (right < text.length && rightAllow(text[right])) {
right += 1;
}
right = Math.max(left, right);
let symbol = text.substring(left, right);
const prefix = text.substring(0, left);
const lChar = left > 0 ? text[left - 1] : "";
const llChar = left > 1 ? text[left - 2] : "";
const rChar = right < text.length ? text[right] : "";
if (lChar === "$") {
if (rChar === "[" && llChar != "$") {
symbol = "@" + symbol; // $foo[1] -> @foo $$foo[1] -> $foo
}
else if (rChar === "{" && llChar != "$") {
symbol = "%" + symbol; // $foo{1} -> %foo $$foo{1} -> $foo
}
else {
symbol = "$" + symbol; // $foo $foo->[1] $foo->{1} -> $foo
}
}
else if (["@", "%"].includes(lChar)) {
symbol = lChar + symbol; // @foo, %foo -> @foo, %foo
}
else if (lChar === "{" && rChar === "}" && ["$", "%", "@"].includes(llChar)) {
symbol = llChar + symbol; // ${foo} -> $foo
}
let match;
if (symbol.match(/^->\w+$/)) {
// If you have Foo::Bar->new(...)->func, the extracted symbol will be ->func
// We can special case this to Foo::Bar->func. The regex allows arguments to new(), including params with matched ()
let match = prefix.match(/(\w(?:\w|::\w)*)->new\((?:\([^()]*\)|[^()])*\)$/);
if (match)
symbol = match[1] + symbol;
}
else if (match = symbol.match(/^(\w(?:\w|::\w)*)->new->(\w+)$/)) {
// If you have Foo::Bar->new->func, the extracted symbol will be Foo::Bar->new->func
symbol = match[1] + "->" + match[2];
}
return symbol;
}
exports.getSymbol = getSymbol;
function findRecent(found, line) {
let best = found[0];
for (var i = 0; i < found.length; i++) {
// TODO: is this flawed because not all lookups are in the same file?
// Find the most recently declared variable. Modules and Packages are both declared at line 0, so Package is tiebreaker (better navigation; modules can be faked by Moose)
if ((found[i].line > best.line && found[i].line <= line) || (found[i].line == best.line && found[i].type == types_1.PerlSymbolKind.Package)) {
best = found[i];
}
}
return best;
}
function lookupSymbol(perlDoc, modMap, symbol, line) {
let found = perlDoc.elems.get(symbol);
if (found?.length) {
// Simple lookup worked. If we have multiple (e.g. 2 lexical variables), find the nearest earlier declaration.
const best = findRecent(found, line);
return [best];
}
let foundMod = modMap.get(symbol);
if (foundMod) {
// Ideally we would've found the module in the PerlDoc, but perhaps it was "required" instead of "use'd"
const modUri = vscode_uri_1.default.parse(foundMod).toString();
const modElem = {
name: symbol,
type: types_1.PerlSymbolKind.Module,
typeDetail: "",
uri: modUri,
package: symbol,
line: 0,
lineEnd: 0,
value: "",
source: types_1.ElemSource.modHunter,
};
return [modElem];
}
let qSymbol = symbol;
let superClass = /^(\$\w+)\-\>SUPER\b/.exec(symbol);
if (superClass) {
// If looking up the superclass of $self->SUPER, we need to find the package in which $self is defined, and then find the parent
let child = perlDoc.elems.get(superClass[1]);
if (child?.length) {
const recentChild = findRecent(child, line);
if (recentChild.package) {
const parentVar = perlDoc.parents.get(recentChild.package);
if (parentVar) {
qSymbol = qSymbol.replace(/^\$\w+\-\>SUPER/, parentVar);
}
}
}
}
let knownObject = /^(\$\w+)\->(?:\w+)$/.exec(symbol);
if (knownObject) {
const targetVar = perlDoc.canonicalElems.get(knownObject[1]);
if (targetVar)
qSymbol = qSymbol.replace(/^\$\w+(?=\->)/, targetVar.typeDetail);
}
// Add what we mean when someone wants ->new().
let synonyms = ["_init", "BUILD"];
for (const synonym of synonyms) {
found = perlDoc.elems.get(symbol.replace(/->new$/, "::" + synonym));
if (found?.length)
return [found[0]];
}
found = perlDoc.elems.get(symbol.replace(/DBI->new$/, "DBI::connect"));
if (found?.length)
return [found[0]];
qSymbol = qSymbol.replaceAll("->", "::"); // Module->method() can be found via Module::method
qSymbol = qSymbol.replace(/^main::(\w+)$/g, "$1"); // main::foo is just tagged as foo
found = perlDoc.elems.get(qSymbol);
if (found?.length)
return [found[0]];
if (qSymbol.includes("::") && symbol.includes("->")) {
// Launching to the wrong explicitly stated module is a bad experience, and common with "require'd" modules
const method = qSymbol.split("::").pop();
if (method) {
// Perhaps the method is within our current scope, explictly imported, or an inherited method (dumper by Inquisitor)
found = perlDoc.elems.get(method);
if (found?.length)
return [found[0]];
// Autoloaded are lower priority than inherited, but higher than random hunting
const foundAuto = perlDoc.autoloads.get(method);
if (foundAuto)
return [foundAuto];
// Haven't found the method yet, let's check if anything could be a possible match since you don't know the object type
let foundElems = [];
perlDoc.elems.forEach((elements, elemName) => {
const element = elements[0]; // All Elements are with same name are normally the same.
const elemMethod = elemName.split("::").pop();
if (elemMethod == method) {
foundElems.push(element);
}
});
if (foundElems.length > 0)
return foundElems;
}
}
if (symbol.match(/^(\w(?:\w|::\w)*)$/)) {
// Running out of options here. Perhaps it's a Package, and the file is in the symbol table under its individual functions.
for (let potentialElem of perlDoc.elems.values()) {
const element = potentialElem[0]; // All Elements are with same name are normally the same.
if (element.package && element.package == symbol) {
const packElem = {
name: symbol,
type: types_1.PerlSymbolKind.Package,
typeDetail: "",
uri: element.uri,
package: symbol,
line: 0,
lineEnd: 0,
value: "",
source: types_1.ElemSource.packageInference,
};
// Just return the first one. The others would likely be the same
return [packElem];
}
}
}
return [];
}
exports.lookupSymbol = lookupSymbol;
function nLog(message, settings) {
// TODO: Remove resource level settings and just use a global logging setting?
if (settings.logging) {
console.error(message);
}
}
exports.nLog = nLog;
function getPerlimportsProfile(settings) {
const profileCmd = [];
if (settings.perlimportsProfile) {
profileCmd.push("--config-file", settings.perlimportsProfile);
}
return profileCmd;
}
exports.getPerlimportsProfile = getPerlimportsProfile;
async function isFile(file) {
try {
const stats = await fs_1.promises.stat(file);
return stats.isFile();
}
catch (err) {
// File or directory doesn't exist
return false;
}
}
exports.isFile = isFile;
//# sourceMappingURL=utils.js.map