perlnavigator-server
Version:
Perl language server
315 lines • 15 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.getCompletionDoc = exports.getCompletions = void 0;
const node_1 = require("vscode-languageserver/node");
const types_1 = require("./types");
const pod_1 = require("./pod");
const vscode_uri_1 = require("vscode-uri");
function getCompletions(params, perlDoc, txtDoc, modMap) {
let position = params.position;
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 imPrefix = getImportPrefix(text, index);
if (imPrefix) {
const replace = {
start: { line: position.line, character: imPrefix.charStart },
end: { line: position.line, character: imPrefix.charEnd },
};
const matches = getImportMatches(modMap, imPrefix.symbol, replace, perlDoc);
return matches;
}
else {
const prefix = getPrefix(text, index);
if (!prefix.symbol)
return [];
const replace = {
start: { line: position.line, character: prefix.charStart },
end: { line: position.line, character: prefix.charEnd },
};
const matches = getMatches(perlDoc, prefix.symbol, replace, prefix.stripPackage);
return matches;
}
}
exports.getCompletions = getCompletions;
async function getCompletionDoc(elem, perlDoc, modMap) {
let docs = await (0, pod_1.getPod)(elem, perlDoc, modMap);
return docs;
}
exports.getCompletionDoc = getCompletionDoc;
// Similar to getSymbol for navigation, but don't "move right".
function getPrefix(text, position) {
const canShift = (c) => /[\w\:\>\-]/.exec(c);
let l = position - 1; // left
for (; l >= 0 && canShift(text[l]); --l)
;
if (l < 0 || text[l] != "$" && text[l] != "@" && text[l] != "%")
++l;
let symbol = text.substring(l, position);
const prefix = text.substring(0, l);
let stripPackage = false;
if (symbol.match(/^-(?:>\w*)?$/)) { // Matches - or -> or ->\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;
stripPackage = true;
}
}
return { symbol: symbol, charStart: l, charEnd: position, stripPackage: stripPackage };
}
// First we check if it's an import statement, which is a special type of autocomplete with far more options
function getImportPrefix(text, position) {
text = text.substring(0, position);
let partialImport = /^\s*(?:use|require)\s+([\w:]+)$/.exec(text);
if (!partialImport)
return;
const symbol = partialImport[1];
return { symbol: symbol, charStart: position - symbol.length, charEnd: position, stripPackage: false };
}
function getImportMatches(modMap, symbol, replace, perlDoc) {
const matches = [];
const mods = Array.from(modMap.keys());
const lcSymbol = symbol.toLowerCase();
modMap.forEach((modFile, mod) => {
if (mod.toLowerCase().startsWith(lcSymbol)) {
const modUri = vscode_uri_1.default.parse(modFile).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,
};
const newElem = { perlElem: modElem, docUri: perlDoc.uri };
matches.push({
label: mod,
textEdit: { newText: mod, range: replace },
kind: node_1.CompletionItemKind.Module,
data: newElem
});
}
});
return matches;
}
function getMatches(perlDoc, symbol, replace, stripPackage) {
let matches = [];
let qualifiedSymbol = symbol.replaceAll("->", "::"); // Module->method() can be found via Module::method
if (qualifiedSymbol.endsWith('-'))
qualifiedSymbol = qualifiedSymbol.replace('-', ':');
let bKnownObj = false;
// Check if we know the type of this object
let knownObject = /^(\$\w+):(?::\w*)?$/.exec(qualifiedSymbol);
if (knownObject) {
const targetVar = perlDoc.canonicalElems.get(knownObject[1]);
if (targetVar) {
qualifiedSymbol = qualifiedSymbol.replace(/^\$\w+(?=:)/, targetVar.typeDetail);
bKnownObj = true;
}
}
// If the magic variable $self->, then autocomplete to everything in main.
const bSelf = /^(\$self):(?::\w*)?$/.exec(qualifiedSymbol);
if (bSelf)
bKnownObj = true;
// const lcQualifiedSymbol = qualifiedSymbol.toLowerCase(); Case insensitive matches are hard since we restore what you originally matched on
perlDoc.elems.forEach((elements, elemName) => {
if (/^[\$\@\%].$/.test(elemName))
return; // Remove single character magic perl variables. Mostly clutter the list
let element = perlDoc.canonicalElems.get(elemName) || elements[0]; // Get the canonical (typed) element, otherwise just grab the first one.
// All plain and inherited subroutines should match with $self. We're excluding PerlSymbolKind.ImportedSub here because imports clutter the list, despite perl allowing them called on $self->
if (bSelf && [types_1.PerlSymbolKind.LocalSub, types_1.PerlSymbolKind.Inherited, types_1.PerlSymbolKind.LocalMethod, types_1.PerlSymbolKind.Field].includes(element.type))
elemName = `$self::${elemName}`;
if (goodMatch(perlDoc, elemName, qualifiedSymbol, symbol, bKnownObj)) {
// Hooray, it's a match!
// You may have asked for FOO::BAR->BAZ or $qux->BAZ and I found FOO::BAR::BAZ. Let's put back the arrow or variable before sending
const quotedSymbol = qualifiedSymbol.replaceAll("$", "\\$"); // quotemeta for $self->FOO
let aligned = elemName.replace(new RegExp(`^${quotedSymbol}`, "gi"), symbol);
if (symbol.endsWith("-"))
aligned = aligned.replaceAll('-:', "->"); // Half-arrows count too
// Don't send invalid constructs
// like FOO->BAR::BAZ
if (/\-\>\w+::/.test(aligned))
return;
// FOO->BAR if Bar is not a sub/method.
if (/\-\>\w+$/.test(aligned) &&
![
types_1.PerlSymbolKind.LocalSub,
types_1.PerlSymbolKind.ImportedSub,
types_1.PerlSymbolKind.Inherited,
types_1.PerlSymbolKind.LocalMethod,
types_1.PerlSymbolKind.Method,
types_1.PerlSymbolKind.Field,
types_1.PerlSymbolKind.PathedField,
].includes(element.type))
return;
// FOO::BAR if Bar is a instance method or attribute (I assume them to be instance methods/attributes, not class)
if (!/^\$.*\-\>\w+$/.test(aligned) &&
[types_1.PerlSymbolKind.LocalMethod, types_1.PerlSymbolKind.Method, types_1.PerlSymbolKind.Field, types_1.PerlSymbolKind.PathedField].includes(element.type))
return;
if (aligned.indexOf("-:") != -1 || // We look things up like this, but don't let them slip through
aligned.startsWith('$') && aligned.indexOf("::", 1) != -1)
// $Foo::Bar, I don't really hunt for these anyway
return;
matches = matches.concat(buildMatches(aligned, element, replace, stripPackage, perlDoc));
}
});
return matches;
}
// TODO: preprocess all "allowed" matches so we don't waste time iterating over them for every autocomplete.
function goodMatch(perlDoc, elemName, qualifiedSymbol, origSymbol, bKnownObj) {
if (!elemName.startsWith(qualifiedSymbol))
return false;
// All uppercase methods are generally private or autogenerated and unhelpful
if (/(?:::|->)[A-Z][A-Z_]+$/.test(elemName))
return false;
if (bKnownObj) {
// If this is a known object type, we probably aren't importing the package or building a new one.
if (/(?:::|->)(?:new|import)$/.test(elemName))
return false;
// If we known the object type (and variable name is not $self), then exclude the double underscore private variables (rare anyway. single underscore kept, but ranked last in the autocomplete)
if (origSymbol.startsWith('$') && !origSymbol.startsWith("$self") && /(?:::|->)__\w+$/.test(elemName))
return false;
// Otherwise, always autocomplete, even if the module has not been explicitly imported.
return true;
}
// Get the module name to see if it's been imported. Otherwise, don't allow it.
let modRg = /^(.+)::.*?$/;
var match = modRg.exec(elemName);
if (match && !perlDoc.imported.has(match[1])) {
// TODO: Allow completion on packages/class defined within the file itself (e.g. Foo->new, $foo->new already works)
// Thing looks like a module, but was not explicitly imported
return false;
}
else {
// Thing was either explictly imported or not a module function
return true;
}
}
function buildMatches(lookupName, elem, range, stripPackage, perlDoc) {
let kind;
let detail = undefined;
let documentation = undefined;
let docs = [];
if ([types_1.PerlSymbolKind.LocalVar, types_1.PerlSymbolKind.ImportedVar, types_1.PerlSymbolKind.Canonical].includes(elem.type)) {
if (elem.typeDetail.length > 0) {
kind = node_1.CompletionItemKind.Variable;
detail = `${lookupName}: ${elem.typeDetail}`;
}
else if (lookupName == "$self") {
kind = node_1.CompletionItemKind.Variable;
// elem.package can be misleading if you use $self in two different packages in the same module. Get scoped matches will address this
detail = `${lookupName}: ${elem.package}`;
}
}
if (!detail) {
switch (elem.type) {
case types_1.PerlSymbolKind.LocalVar:
kind = node_1.CompletionItemKind.Variable;
break;
case types_1.PerlSymbolKind.ImportedVar:
kind = node_1.CompletionItemKind.Constant;
// detail = elem.name;
docs.push(elem.name);
docs.push(`Value: ${elem.value}`);
break;
case types_1.PerlSymbolKind.ImportedHash:
case types_1.PerlSymbolKind.Constant:
kind = node_1.CompletionItemKind.Constant;
break;
case types_1.PerlSymbolKind.LocalSub:
if (lookupName.startsWith("$self-"))
docs.push(elem.name); // For consistency with the other $self methods. VScode seems to hide documentation if less populated?
kind = node_1.CompletionItemKind.Function;
break;
case types_1.PerlSymbolKind.ImportedSub:
case types_1.PerlSymbolKind.Inherited:
case types_1.PerlSymbolKind.Method:
case types_1.PerlSymbolKind.LocalMethod:
kind = node_1.CompletionItemKind.Method;
docs.push(elem.name);
if (elem.typeDetail && elem.typeDetail != elem.name)
docs.push(`\nDefined as:\n ${elem.typeDetail}`);
break;
case types_1.PerlSymbolKind.Package:
case types_1.PerlSymbolKind.Module:
kind = node_1.CompletionItemKind.Module;
break;
case types_1.PerlSymbolKind.Label: // Loop labels
kind = node_1.CompletionItemKind.Reference;
break;
case types_1.PerlSymbolKind.Class:
kind = node_1.CompletionItemKind.Class;
break;
case types_1.PerlSymbolKind.Role:
kind = node_1.CompletionItemKind.Interface;
break;
case types_1.PerlSymbolKind.Field:
case types_1.PerlSymbolKind.PathedField:
kind = node_1.CompletionItemKind.Field;
break;
case types_1.PerlSymbolKind.Phaser:
case types_1.PerlSymbolKind.HttpRoute:
case types_1.PerlSymbolKind.OutlineOnlySub:
return [];
default: // A sign that something needs fixing. Everything should've been enumerated.
kind = node_1.CompletionItemKind.Property;
break;
}
}
if (docs.length > 0)
documentation = { kind: "markdown", value: "```\n" + docs.join("\n") + "\n```" };
let labelsToBuild = [lookupName];
if (lookupName.endsWith("::new"))
// Having ->new at the top (- sorts before :) is the more common way to call packages (although you can call it either way).
labelsToBuild.push(lookupName.replace(/::new$/, "->new"));
let matches = [];
labelsToBuild.forEach((label) => {
let replaceText = label;
if (stripPackage)
// When autocompleting Foo->new(...)->, we need the dropdown to show Foo->func, but the replacement only to be ->func
replaceText = replaceText.replace(/^(\w(?:\w|::\w)*)(?=->)/, '');
const newElem = { perlElem: elem, docUri: perlDoc.uri };
matches.push({
label: label,
textEdit: { newText: replaceText, range },
kind: kind,
sortText: getSortText(label),
detail: detail,
documentation: documentation,
data: newElem,
});
});
return matches;
}
function getSortText(label) {
// Ensure sorting has public methods up front, followed by private and then capital. (private vs somewhat capital is arbitrary, but public makes sense).
// Variables will still be higher when relevant.
// use English puts a lot of capital variables, so these will end up lower as well (including Hungarian notation capitals)
let sortText;
if (/^[@\$%]?[a-z]?[a-z]?[A-Z][A-Z_]*$/.test(label) || /(?:::|->)[A-Z][A-Z_]+$/.test(label)) {
sortText = "4" + label;
}
else if (label == "_" || /(?:::|->)_\w+$/.test(label)) {
sortText = "3" + label;
}
else if (/^\w$/.test(label) || /(?:::|->)\w+$/.test(label)) {
// Public methods / functions
sortText = "2";
// Prioritize '->new'
if (label.indexOf("->new") != -1)
sortText += "1";
sortText += label;
}
else {
// Variables and regex mistakes
sortText = "1" + label;
}
return sortText;
}
//# sourceMappingURL=completion.js.map