UNPKG

perlnavigator-server

Version:

Perl language server

315 lines 15 kB
"use strict"; 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