summaryrefslogtreecommitdiff
path: root/browser-ext
diff options
context:
space:
mode:
authorbscan <10503608+bscan@users.noreply.github.com>2023-01-16 15:07:10 -0500
committerbscan <10503608+bscan@users.noreply.github.com>2023-01-16 15:07:10 -0500
commit432d50536a3ab5c43c195c674381329a3fa00c6c (patch)
treefe1c34b2f07146fe21a3218f28151096903a52e0 /browser-ext
parent0ff5950f5c7e513b95489372044e9d26dc5550bc (diff)
downloadPerlNavigator-432d50536a3ab5c43c195c674381329a3fa00c6c.zip
Web extension: adding tags, go-to definition, hover, completion, and outline view
Diffstat (limited to 'browser-ext')
-rw-r--r--browser-ext/src/browserServerMain.ts149
-rw-r--r--browser-ext/src/web-completion.ts244
-rw-r--r--browser-ext/src/web-hover.ts78
-rw-r--r--browser-ext/src/web-navigation.ts95
-rw-r--r--browser-ext/src/web-parse.ts262
-rw-r--r--browser-ext/src/web-symbols.ts81
-rw-r--r--browser-ext/src/web-types.ts96
-rw-r--r--browser-ext/src/web-utils.ts157
8 files changed, 1100 insertions, 62 deletions
diff --git a/browser-ext/src/browserServerMain.ts b/browser-ext/src/browserServerMain.ts
index 5596469..e2bf515 100644
--- a/browser-ext/src/browserServerMain.ts
+++ b/browser-ext/src/browserServerMain.ts
@@ -2,10 +2,19 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-import { createConnection, BrowserMessageReader, BrowserMessageWriter } from 'vscode-languageserver/browser';
+import { createConnection, BrowserMessageReader, BrowserMessageWriter, SymbolInformation, SymbolKind, CompletionList, TextDocumentPositionParams, CompletionItem, Hover, Location } from 'vscode-languageserver/browser';
import { Color, ColorInformation, Range, InitializeParams, InitializeResult, ServerCapabilities, TextDocuments, ColorPresentation, TextEdit, TextDocumentIdentifier } from 'vscode-languageserver';
import { TextDocument } from 'vscode-languageserver-textdocument';
+import { PerlDocument, PerlElem } from "./web-types";
+import { buildNav } from "./web-parse";
+
+import { getDefinition } from "./web-navigation";
+import { getSymbols } from "./web-symbols";
+import { getHover } from "./web-hover";
+import { getCompletions } from './web-completion';
+
+var LRU = require("lru-cache");
console.log('running server perl-navigator web');
@@ -17,95 +26,111 @@ const messageWriter = new BrowserMessageWriter(self);
const connection = createConnection(messageReader, messageWriter);
+// My ballpark estimate is that 350k symbols will be about 35MB. Huge map, but a reasonable limit.
+const navSymbols = new LRU({max: 350000, length: function (value:PerlDocument , key:string) { return value.elems.size }});
+const timers: Map<string, ReturnType<typeof setTimeout>> = new Map();
+
+
/* from here on, all code is non-browser specific and could be shared with a regular extension */
connection.onInitialize((params: InitializeParams): InitializeResult => {
const capabilities: ServerCapabilities = {
- colorProvider: {} // provide a color provider
+ completionProvider: {
+ resolveProvider: false,
+ triggerCharacters: ['$','@','%','-', '>',':']
+ },
+
+ definitionProvider: true, // goto definition
+ documentSymbolProvider: true, // Outline view and breadcrumbs
+ hoverProvider: true,
};
return { capabilities };
});
+
// Track open, change and close text document events
const documents = new TextDocuments(TextDocument);
documents.listen(connection);
-// Register providers
-connection.onDocumentColor(params => getColorInformation(params.textDocument));
-connection.onColorPresentation(params => getColorPresentation(params.color, params.range));
-
// Listen on the connection
connection.listen();
-const colorRegExp = /#([0-9A-Fa-f]{6})/g;
+// Only keep symbols for open documents
+documents.onDidClose(e => {
+ navSymbols.del(e.document.uri);
+});
-function getColorInformation(textDocument: TextDocumentIdentifier) {
- const colorInfos: ColorInformation[] = [];
- const document = documents.get(textDocument.uri);
- if (document) {
- const text = document.getText();
+documents.onDidOpen(change => {
+ validatePerlDocument(change.document);
+});
- colorRegExp.lastIndex = 0;
- let match;
- while ((match = colorRegExp.exec(text)) != null) {
- const offset = match.index;
- const length = match[0].length;
- const range = Range.create(document.positionAt(offset), document.positionAt(offset + length));
- const color = parseColor(text, offset);
- colorInfos.push({ color, range });
- }
- }
- console.log(`Found this many colors: ${colorInfos.length}`)
- //return colorInfos;
- return [];
-}
+documents.onDidSave(change => {
+ validatePerlDocument(change.document);
+});
-function getColorPresentation(color: Color, range: Range) {
- const result: ColorPresentation[] = [];
- const red256 = Math.round(color.red * 255), green256 = Math.round(color.green * 255), blue256 = Math.round(color.blue * 255);
+documents.onDidChangeContent(change => {
+ // VSCode sends a firehose of change events. Only check after it's been quiet for 1 second.
+ const timer = timers.get(change.document.uri)
+ if(timer) clearTimeout(timer);
+ const newTimer: ReturnType<typeof setTimeout> = setTimeout(function(){ validatePerlDocument(change.document)}, 1000);
+ timers.set(change.document.uri, newTimer);
+});
- function toTwoDigitHex(n: number): string {
- const r = n.toString(16);
- return r.length !== 2 ? '0' + r : r;
- }
- const label = `#${toTwoDigitHex(red256)}${toTwoDigitHex(green256)}${toTwoDigitHex(blue256)}`;
- result.push({ label: label, textEdit: TextEdit.replace(range, label) });
+async function validatePerlDocument(textDocument: TextDocument): Promise<void> {
+ console.log("Rebuilding symbols for " + textDocument.uri + "");
+ const perlDoc = await buildNav(textDocument);
+ navSymbols.set(textDocument.uri, perlDoc);
+ return;
- return result;
}
-const enum CharCode {
- Digit0 = 48,
- Digit9 = 57,
+connection.onDocumentSymbol(params => {
+ console.log("Navigator: Getting document symbols");
+ return getSymbols(navSymbols, params.textDocument.uri);
+});
+
+// This handler provides the initial list of the completion items.
+connection.onCompletion((params: TextDocumentPositionParams): CompletionList | undefined => {
+ console.log("Navigator: Getting completion results");
- A = 65,
- F = 70,
+ let document = documents.get(params.textDocument.uri);
+ let perlDoc = navSymbols.get(params.textDocument.uri);
+ if(!document || !perlDoc) return;
- a = 97,
- f = 102,
-}
+ const completions: CompletionItem[] = getCompletions(params, perlDoc, document);
-function parseHexDigit(charCode: CharCode): number {
- if (charCode >= CharCode.Digit0 && charCode <= CharCode.Digit9) {
- return charCode - CharCode.Digit0;
- }
- if (charCode >= CharCode.A && charCode <= CharCode.F) {
- return charCode - CharCode.A + 10;
- }
- if (charCode >= CharCode.a && charCode <= CharCode.f) {
- return charCode - CharCode.a + 10;
- }
- return 0;
-}
+ return {
+ items: completions,
+ isIncomplete: false,
+ };
+});
+
+
+connection.onHover(params => {
+ console.log("Trying to hover");
+ console.log(params.position);
+ let document = documents.get(params.textDocument.uri);
+ let perlDoc = navSymbols.get(params.textDocument.uri);
+ if(!document || !perlDoc) return;
-function parseColor(content: string, offset: number): Color {
- const r = (16 * parseHexDigit(content.charCodeAt(offset + 1)) + parseHexDigit(content.charCodeAt(offset + 2))) / 255;
- const g = (16 * parseHexDigit(content.charCodeAt(offset + 3)) + parseHexDigit(content.charCodeAt(offset + 4))) / 255;
- const b = (16 * parseHexDigit(content.charCodeAt(offset + 5)) + parseHexDigit(content.charCodeAt(offset + 6))) / 255;
- return Color.create(r, g, b, 1);
-} \ No newline at end of file
+ return getHover(params, perlDoc, document);
+});
+
+
+connection.onDefinition(params => {
+ console.log("Navigator: Getting definition results");
+
+ let document = documents.get(params.textDocument.uri);
+ let perlDoc = navSymbols.get(params.textDocument.uri);
+ if(!document || !perlDoc) return;
+
+ let locOut: Location | Location[] | undefined = getDefinition(params, perlDoc, document);
+ console.log("Got definition results");
+ console.log(locOut);
+ return locOut;
+});
diff --git a/browser-ext/src/web-completion.ts b/browser-ext/src/web-completion.ts
new file mode 100644
index 0000000..8701383
--- /dev/null
+++ b/browser-ext/src/web-completion.ts
@@ -0,0 +1,244 @@
+import {
+ TextDocumentPositionParams,
+ CompletionItem,
+ CompletionItemKind,
+ Range,
+ MarkupContent
+} from 'vscode-languageserver/node';
+import { TextDocument } from 'vscode-languageserver-textdocument';
+import { PerlDocument, PerlElem, CompletionPrefix, PerlSymbolKind } from "./web-types";
+
+
+export function getCompletions(params: TextDocumentPositionParams, perlDoc: PerlDocument, txtDoc: TextDocument): CompletionItem[] {
+
+ 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 prefix = getPrefix(text, index);
+
+ if(!prefix.symbol) return [];
+
+ const replace: Range = {
+ start: { line: position.line, character: prefix.charStart },
+ end: { line: position.line, character: prefix.charEnd }
+ };
+
+ const matches = getMatches(perlDoc, prefix.symbol, replace);
+ return matches;
+
+}
+
+
+// Similar to getSymbol for navigation, but don't "move right".
+function getPrefix(text: string, position: number): CompletionPrefix {
+
+ const leftAllow = (c: string) => /[\w\:\>\-]/.exec(c);
+
+ let left = position - 1;
+
+ while (left >= 0 && leftAllow(text[left])) {
+ left -= 1;
+ }
+ left = Math.max(0, left + 1);
+
+ let symbol = text.substring(left, position);
+ const lChar = left > 0 ? text[left-1] : "";
+
+ if(lChar === '$' || lChar === '@' || lChar === '%'){
+ symbol = lChar + symbol;
+ left -= 1;
+ }
+
+ return {symbol: symbol, charStart: left, charEnd: position};
+}
+
+
+function getMatches(perlDoc: PerlDocument, symbol: string, replace: Range): CompletionItem[] {
+
+ let matches: CompletionItem[] = [];
+
+ let qualifiedSymbol = symbol.replace(/->/g, "::"); // Module->method() can be found via Module::method
+ qualifiedSymbol = qualifiedSymbol.replace(/-$/g, ":"); // Maybe I just started typing Module-
+
+ 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.type);
+ 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: PerlElem[], elemName: string) => {
+ 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 "t" here because imports clutter the list, despite perl allowing them called on $self->
+ if(bSelf && ["s", "i", "o", "f"].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.replace(/([\$])/g, '\\$1'); // quotemeta for $self->FOO
+ let aligned = elemName.replace(new RegExp(`^${quotedSymbol}`, 'gi'), symbol);
+
+ if(symbol.endsWith('-')) aligned = aligned.replace(new RegExp(`-:`, 'gi'), '->'); // Half-arrows count too
+
+ // Don't send invalid constructs
+ if(/\-\>\w+::/.test(aligned) || // like FOO->BAR::BAZ
+ (/\-\>\w+$/.test(aligned) && !["s", "t", "i", "o", "x", "f", "d"].includes(element.type)) || // FOO->BAR if Bar is not a sub/method.
+ (!/^\$.*\-\>\w+$/.test(aligned) && ["o", "x", "f", "d"].includes(element.type)) || // FOO::BAR if Bar is a instance method or attribute (I assume them to be instance methods/attributes, not class)
+ (/-:/.test(aligned)) || // We look things up like this, but don't let them slip through
+ (/^\$.*::/.test(aligned)) // $Foo::Bar, I don't really hunt for these anyway
+ ) return;
+
+ matches = matches.concat(buildMatches(aligned, element, replace));
+ }
+ });
+
+ return matches;
+
+}
+
+// TODO: preprocess all "allowed" matches so we don't waste time iterating over them for every autocomplete.
+function goodMatch(perlDoc: PerlDocument, elemName: string, qualifiedSymbol: string, origSymbol: string, bKnownObj: boolean): boolean {
+
+ 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((/^(?!\$self)\$/.test(origSymbol) && /(?:::|->)__\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: string, elem: PerlElem, range: Range): CompletionItem[] {
+
+ let kind: CompletionItemKind;
+ let detail: string | undefined = undefined;
+ let documentation: MarkupContent | undefined = undefined;
+ let docs: string[] = [];
+
+ if (elem.type.length > 1 || ( ["v", "c"].includes(elem.type) && lookupName == '$self')) {
+ // We either know the object type, or it's $self
+ kind = CompletionItemKind.Variable;
+ if(elem.type.length > 1 ){
+ detail = `${lookupName}: ${elem.type}`;
+ } else if (lookupName == '$self') {
+ // 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}`;
+ }
+ } else if(elem.type == PerlSymbolKind.LocalVar){
+ kind = CompletionItemKind.Variable;
+ } else if(elem.type == PerlSymbolKind.ImportedVar){
+ kind = CompletionItemKind.Constant;
+ // detail = elem.name;
+ docs.push(elem.name);
+ docs.push(`Value: ${elem.value}`);
+ } else if(elem.type == PerlSymbolKind.ImportedHash || elem.type == PerlSymbolKind.Constant) {
+ kind = CompletionItemKind.Constant;
+ } else if (elem.type == PerlSymbolKind.LocalSub){
+ if(/^\$self\-/.test(lookupName)) docs.push(elem.name); // For consistency with the other $self methods. VScode seems to hide documentation if less populated?
+ kind = CompletionItemKind.Function;
+ } else if (elem.type == PerlSymbolKind.ImportedSub || elem.type == PerlSymbolKind.Inherited || elem.type == PerlSymbolKind.Method || elem.type == PerlSymbolKind.LocalMethod){
+ kind = CompletionItemKind.Method;
+ docs.push(elem.name);
+ if(elem.typeDetail && elem.typeDetail != elem.name) docs.push(`\nDefined as:\n ${elem.typeDetail}`);
+ }else if (elem.type == PerlSymbolKind.Package || elem.type == PerlSymbolKind.Module){
+ kind = CompletionItemKind.Module;
+ }else if (elem.type == PerlSymbolKind.Label){ // Loop labels
+ kind = CompletionItemKind.Reference;
+ } else if (elem.type == PerlSymbolKind.Class){
+ kind = CompletionItemKind.Class;
+ } else if (elem.type == PerlSymbolKind.Role){
+ kind = CompletionItemKind.Interface;
+ } else if (elem.type == PerlSymbolKind.Field || elem.type == PerlSymbolKind.PathedField){
+ kind = CompletionItemKind.Field;
+ } else if (elem.type == PerlSymbolKind.Phaser){
+ return [];
+ } else { // A sign that something needs fixing. Everything should've been enumerated.
+ kind = CompletionItemKind.Property;
+ }
+
+ if(docs.length>0){
+ documentation = {kind: "markdown", value: "```\n" + docs.join("\n") + "\n```" };
+ }
+
+ let labelsToBuild = [lookupName];
+
+ if(/::new$/.test(lookupName)){
+ // 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: CompletionItem[] = [];
+
+ labelsToBuild.forEach(label => {
+ matches.push({
+ label: label,
+ textEdit: {newText: label, range},
+ kind: kind,
+ sortText: getSortText(label),
+ detail: detail,
+ documentation: documentation,
+ });
+ });
+
+ return matches
+}
+
+function getSortText(label: string): string {
+ // 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: string;
+
+ if(/^[@\$%]?[a-z]?[a-z]?[A-Z][A-Z_]*$/.test(label) || /(?:::|->)[A-Z][A-Z_]+$/.test(label)){
+ sortText = "4" + label;
+ } else if(/^_$/.test(label) || /(?:::|->)_\w+$/.test(label)){
+ sortText = "3" + label;
+ } else if(/^\w$/.test(label) || /(?:::|->)\w+$/.test(label)){
+ // Public methods / functions
+ sortText = "2";
+ // Prioritize '->new'
+ if (/->new/.test(label)) { sortText += "1" }
+ sortText += label;
+ } else {
+ // Variables and regex mistakes
+ sortText = "1" + label;
+ }
+ return sortText;
+} \ No newline at end of file
diff --git a/browser-ext/src/web-hover.ts b/browser-ext/src/web-hover.ts
new file mode 100644
index 0000000..de4aac3
--- /dev/null
+++ b/browser-ext/src/web-hover.ts
@@ -0,0 +1,78 @@
+import {
+ TextDocumentPositionParams,
+ Hover,
+} from 'vscode-languageserver/node';
+import { TextDocument } from 'vscode-languageserver-textdocument';
+import { PerlDocument, PerlElem } from "./web-types";
+import { getSymbol, lookupSymbol } from "./web-utils";
+
+export function getHover(params: TextDocumentPositionParams, perlDoc: PerlDocument, txtDoc: TextDocument): Hover | undefined {
+
+ let position = params.position
+ const symbol = getSymbol(position, txtDoc);
+
+ let elem = perlDoc.canonicalElems.get(symbol);
+
+ if(!elem){
+ const elems = lookupSymbol(perlDoc, symbol, position.line);
+ if(elems.length != 1) return; // Nothing or too many things.
+ elem = elems[0];
+ }
+
+ let hoverStr = buildHoverDoc(symbol, elem);
+ if(!hoverStr) return; // Sometimes, there's nothing worth showing.
+
+ const documentation = {contents: hoverStr};
+
+ return documentation;
+}
+
+function buildHoverDoc(symbol: string, elem: PerlElem){
+
+ let desc = "";
+ if (elem.type.length > 1 || ( ["v", "c"].includes(elem.type) && /^\$self/.test(symbol))) {
+ // We either know the object type, or it's $self
+ desc = "(object) ";
+ if(elem.type.length > 1 ){
+ desc += `${elem.type}`;
+ } else if (/^\$self/.test(symbol)) {
+ desc += `${elem.package}`;
+ }
+ } else if(elem.type == 'v'){
+ // desc = `(variable) ${symbol}`; // Not very interesting info
+ } else if (elem.type == 'n'){
+ desc = `(constant) ${symbol}`;
+ } else if(elem.type == 'c'){
+ desc = `${elem.name}: ${elem.value}`;
+ if(elem.package) desc += ` (${elem.package})` ; // Is this ever known?
+ } else if(elem.type == 'h'){
+ desc = `${elem.name} (${elem.package})`;
+ } else if (elem.type == 's'){
+ desc = `(subroutine) ${symbol}`;
+ } else if (['o','x'].includes(elem.type)){
+ desc = `(method) ${symbol}`;
+ } else if (['t','i'].includes(elem.type)){ // inherited methods can still be subs (e.g. new from a parent)
+ desc = `(subroutine) ${elem.name}`;
+ if(elem.typeDetail && elem.typeDetail != elem.name) desc = desc + ` (${elem.typeDetail})`;
+ }else if (elem.type == 'p'){
+ desc = `(package) ${elem.name}`;
+ } else if (elem.type == 'm'){
+ desc = `(module) ${elem.name}: ${elem.file}`;
+ } else if (elem.type == 'l'){
+ desc = `(label) ${symbol}`;
+ } else if (elem.type == 'a'){
+ desc = `(class) ${symbol}`;
+ } else if (elem.type == 'b'){
+ desc = `(role) ${symbol}`;
+ } else if (elem.type == 'f' || elem.type == 'd'){
+ desc = `(attribute) ${symbol}`;
+ } else if (elem.type == 'e'){
+ desc = `(phase) ${symbol}`;
+ } else {
+ // We should never get here
+ desc = `Unknown: ${symbol}`;
+ }
+
+
+ return desc;
+}
diff --git a/browser-ext/src/web-navigation.ts b/browser-ext/src/web-navigation.ts
new file mode 100644
index 0000000..1f27f44
--- /dev/null
+++ b/browser-ext/src/web-navigation.ts
@@ -0,0 +1,95 @@
+import {
+ DefinitionParams,
+ Location,
+ WorkspaceFolder
+} from 'vscode-languageserver/browser';
+import {
+ TextDocument
+} from 'vscode-languageserver-textdocument';
+import { PerlDocument, PerlElem, NavigatorSettings } from "./web-types";
+import { realpathSync, existsSync, realpath } from 'fs';
+import { getSymbol, lookupSymbol } from "./web-utils";
+
+
+export function getDefinition(params: DefinitionParams, perlDoc: PerlDocument, txtDoc: TextDocument): Location[] | undefined {
+
+ let position = params.position
+ const symbol = getSymbol(position, txtDoc);
+ console.log("Trying to get defintion for symbol: " + symbol);
+ if(!symbol) return;
+
+ const foundElems = lookupSymbol(perlDoc, symbol, position.line);
+ console.log("Found elements" + foundElems.length);
+
+ if(foundElems.length == 0){
+ return;
+ }
+ console.log("Still here");
+
+ let locationsFound: Location[] = [];
+
+ foundElems.forEach(elem => {
+ const elemResolved: PerlElem | undefined = resolveElemForNav(perlDoc, elem, symbol);
+ if(!elemResolved) return;
+
+ let uri: string;
+ if(perlDoc.uri !== elemResolved.file){ // TODO Compare URI instead
+ // If sending to a different file, let's make sure it exists and clean up the path
+ // if(!existsSync(elemResolved.file)) return; // Make sure the file exists and hasn't been deleted.
+ //uri = elemResolved.file
+ console.log("Different file");
+ console.log(elemResolved.file);
+ console.log(perlDoc.filePath);
+ return;
+ } 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);
+ });
+ return locationsFound;
+}
+
+
+function resolveElemForNav (perlDoc: PerlDocument, elem: PerlElem, symbol: string): PerlElem | undefined {
+
+ if(elem.file && !badFile(elem.file)){
+ // Have file and 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?.length && elemResolved[0].file && !badFile(elem.file)){
+ return elemResolved[0];
+ }
+ }
+
+ // 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 (file: string){
+ return /(?:Sub[\\\/]Defer\.pm|Moo[\\\/]Object\.pm|Moose[\\\/]Object\.pm)$/.test(file);
+}
diff --git a/browser-ext/src/web-parse.ts b/browser-ext/src/web-parse.ts
new file mode 100644
index 0000000..57842bd
--- /dev/null
+++ b/browser-ext/src/web-parse.ts
@@ -0,0 +1,262 @@
+
+import { PerlDocument, PerlElem, PerlImport, PerlSymbolKind} from "./web-types";
+import { TextDocument } from 'vscode-languageserver-textdocument';
+
+
+// Why is this async? It doesn't do anything async yet
+export async function buildNav(textDocument: TextDocument): Promise<PerlDocument> {
+
+ let perlDoc: PerlDocument = {
+ elems: new Map(),
+ canonicalElems: new Map(),
+ imported: new Map(),
+ parents: new Map(),
+ filePath: '',
+ uri: textDocument.uri,
+ };
+
+ buildPlTags(textDocument, perlDoc);
+
+ return perlDoc;
+}
+
+
+
+function MakeElem(name: string, type: PerlSymbolKind | 'u' | '1' | '2', typeDetail: string, file: string, pack:string, line:number, perlDoc: PerlDocument) : void{
+
+ if (type == '1'){
+ // This object is only intended as the canonicalLookup, not for anything else.
+ return;
+ }
+
+ if (type == 'u'){
+ // Explictly loaded module. Helpful for focusing autocomplete results
+ perlDoc.imported.set(name, line);
+ // if(/\bDBI$/.exec(name)) perlDoc.imported.set(name + "::db", true); // TODO: Build mapping of common constructors to types
+ return; // Don't store it as an element
+ }
+
+ if (type == '2'){
+ perlDoc.parents.set(name, typeDetail);
+ return; // Don't store it as an element
+ }
+
+ const newElem: PerlElem = {
+ name: name,
+ type: type,
+ typeDetail: typeDetail,
+ file: file,
+ package: pack,
+ line: line,
+ lineEnd: line,
+ value: "",
+ };
+
+ // Move fancy object types into the typeDetail field????
+ if (type.length > 1){
+ // We overwrite, so the last typed element is the canonical one. No reason for this.
+ perlDoc.canonicalElems.set(name, newElem);
+ }
+
+ let array = perlDoc.elems.get(name) || [];
+ array.push(newElem)
+ perlDoc.elems.set(name, array);
+
+ return;
+}
+
+
+
+function buildPlTags(textDocument: TextDocument, perlDoc: PerlDocument) {
+ const codeArray = cleanCode(textDocument);
+ let sActiveOO: Map<string, boolean> = new Map(); // Keep track of OO frameworks in use to keep down false alarms on field vs has vs attr
+ // Loop through file
+ const file = textDocument.uri;
+ let package_name = "";
+ let var_continues: boolean = false;
+
+ for (let i = 0; i < codeArray.length; i++) {
+ let line_number = i;
+ let stmt = codeArray[i];
+ // Nothing left? Never mind.
+ if (!stmt) {
+ continue;
+ }
+
+ // TODO, allow specifying list of constructor names as config
+ // Declaring an object. Let's store the type
+ let match;
+ if ((match = stmt.match(/^(?:my|our|local|state)\s+(\$\w+)\s*\=\s*([\w\:]+)\-\>new\s*(?:\((?!.*\)\->)|;)/ )) ||
+ (match = stmt.match(/^(?:my|our|local|state)\s+(\$\w+)\s*\=\s*new (\w[\w\:]+)\s*(?:\((?!.*\)\->)|;)/))) {
+ let varName = match[1];
+ let objName = match[2];
+ MakeElem(varName, PerlSymbolKind.LocalVar, objName, file, package_name, line_number, perlDoc);
+
+ var_continues = false; // We skipped ahead of the line here.
+ }
+ // This is a variable declaration if one was started on the previous
+ // line, or if this line starts with my or local
+ else if (var_continues || (match = stmt.match(/^(?:my|our|local|state)\b/))) {
+ // The declaration continues if the line does not end with ;
+ var_continues = (!stmt.endsWith(";") && !stmt.match(/[\)\=\}\{]/));
+
+ // Remove my or local from statement, if present
+ stmt = stmt.replace(/^(my|our|local|state)\s+/, "");
+
+ // Remove any assignment piece
+ stmt = stmt.replace(/\s*=.*/, "");
+
+ // Remove part where sub starts (for signatures). Consider other options here.
+ stmt = stmt.replace(/\s*\}.*/, "");
+
+ // Now find all variable names, i.e. "words" preceded by $, @ or %
+ let vars = stmt.matchAll(/([\$\@\%][\w:]+)\b/g);
+
+ for (let match of vars) {
+ MakeElem(match[1], PerlSymbolKind.LocalVar, '', file, package_name, line_number, perlDoc);
+ }
+ }
+
+ // Lexical loop variables, potentially with labels in front. foreach my $foo
+ else if ((match = stmt.match(/^(?:(\w+)\s*:(?!\:))?\s*(?:for|foreach)\s+my\s+(\$[\w]+)\b/))) {
+ if (match[1]) {
+ MakeElem(match[1], PerlSymbolKind.Label, '', file, package_name, line_number, perlDoc);
+ }
+ MakeElem(match[1], PerlSymbolKind.LocalVar, '', file, package_name, line_number, perlDoc);
+ }
+
+ // Lexical match variables if(my ($foo, $bar) ~= ). Optional to detect (my $newstring = $oldstring) =~ s/foo/bar/g;
+ else if ((match = stmt.match(/^(?:\}\s*elsif|if|unless|while|until|for)?\s*\(\s*my\b(.*)$/))) {
+ // Remove any assignment piece
+ stmt = stmt.replace(/\s*=.*/, "");
+ let vars = stmt.matchAll(/([\$\@\%][\w]+)\b/g);
+ for (let match of vars) {
+ MakeElem(match[1], PerlSymbolKind.LocalVar, '', file, package_name, line_number, perlDoc);
+ }
+ }
+
+ // This is a package declaration if the line starts with package
+ else if ((match = stmt.match(/^package\s+([\w:]+)/))) {
+ // Get name of the package
+ package_name = match[1];
+ MakeElem(package_name, PerlSymbolKind.Package, '', file, package_name, line_number, perlDoc);
+ }
+
+ // This is a class decoration for Object::Pad, Corinna, or Moops
+ else if((match = stmt.match(/^class\s+([\w:]+)/))){
+ let class_name = match[1];
+ MakeElem(class_name, PerlSymbolKind.Class, '', file, package_name, line_number, perlDoc);
+ }
+
+ // This is a sub declaration if the line starts with sub
+ else if ((match = stmt.match(/^(?:async\s+)?(sub)\s+([\w:]+)(\s+:method)?([^{]*)/)) ||
+ (match = stmt.match(/^(?:async\s+)?(method)\s+\$?([\w:]+)()([^{]*)/)) ||
+ (sActiveOO.get("Function::Parameters") && (match = stmt.match(/^(fun)\s+([\w:]+)()([^{]*)/ )))
+ ) {
+ const subName = match[2];
+ const signature = match[4];
+ const kind = (match[1] === 'method' || match[3]) ? PerlSymbolKind.LocalMethod : PerlSymbolKind.LocalSub;
+ MakeElem(subName, kind, '', file, package_name, line_number, perlDoc);
+
+ // Match the after the sub declaration and before the start of the actual sub for signatures (if any)
+ const vars = signature.matchAll(/([\$\@\%][\w:]+)\b/g);
+
+ // Define subrountine signatures, but exclude prototypes
+ // The declaration continues if the line does not end with ;
+ var_continues = !(stmt.match(/;$/) || stmt.match(/[\)\=\}\{]/));
+
+ for (const matchvar of vars) {
+ MakeElem(matchvar[1], PerlSymbolKind.LocalVar,'', file, package_name, line_number, perlDoc);
+ }
+ }
+
+ // Phaser block
+ else if ((match = stmt.match(/^(BEGIN|INIT|CHECK|UNITCHECK|END)\s*\{/))) {
+ const phaser = match[1];
+ MakeElem(phaser, PerlSymbolKind.Phaser, '', file, package_name, line_number, perlDoc);
+ }
+
+ // Label line
+ else if ((match = stmt.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*:[^:].*{\s*$/))) {
+ const label = match[1];
+ MakeElem(label, PerlSymbolKind.Label, '', file, package_name, line_number, perlDoc);
+ }
+
+ // Constants. Important because they look like subs (and technically are), so I'll tags them as such
+ else if ((match = stmt.match(/^use\s+constant\s+(\w+)\b/))) {
+ MakeElem(match[1], PerlSymbolKind.Constant, '', file, package_name, line_number, perlDoc);
+ MakeElem("constant", 'u', '', file, package_name, line_number, perlDoc);
+ }
+
+ // Moo/Moose/Object::Pad/Moops/Corinna attributes
+ else if ((match = stmt.match(/^has(?:\s+|\()["']?([\$@%]?\w+)\b/))) {
+ const attr = match[1];
+ let type;
+ if(attr.match(/^\w/)){
+ type = PerlSymbolKind.Field;
+ // If you have a locally defined package/class Foo want to reference the attributes as Foo::attr or foo->attr, you need the full path.
+ // Subs don't need this since we find them at compile time. We also find "d" types from imported packages in Inquisitor.pm
+ MakeElem(package_name + "::" + attr, PerlSymbolKind.PathedField, '', file, package_name, line_number, perlDoc);
+ } else {
+ type = PerlSymbolKind.LocalVar;
+ }
+ // TODO: Define new type. Class variables should probably be shown in the Outline view even though lexical variables are not
+ MakeElem(attr, type, '', file, package_name, line_number, perlDoc);
+ }
+
+ else if (sActiveOO.get("Object::Pad") &&
+ (match = stmt.match(/^field\s+([\$@%]\w+)\b/))) { // Object::Pad field
+ const attr = match[1];
+ MakeElem(attr, PerlSymbolKind.LocalVar, '', file, package_name, line_number, perlDoc);
+ }
+
+ else if ((sActiveOO.get("Mars::Class") || sActiveOO.get("Venus::Class"))
+ && (match = stmt.match(/^attr\s+["'](\w+)\b/))) { // Mars attributes
+ const attr = match[1];
+ MakeElem(attr, PerlSymbolKind.Field, '', file, package_name, line_number, perlDoc);
+ MakeElem(package_name + "::" + attr, PerlSymbolKind.PathedField, '', file, package_name, line_number, perlDoc);
+ }
+
+ else if ((match = stmt.match(/^around\s+["']?(\w+)\b/))) { // Moo/Moose overriding subs.
+ MakeElem(match[1], PerlSymbolKind.LocalSub, '', file, package_name, line_number, perlDoc);
+ }
+
+ else if ((match = stmt.match(/^use\s+([\w:]+)\b/))) { // Keep track of explicit imports for filtering
+ const importPkg = match[1];
+ MakeElem(importPkg, "u", '', file, package_name, line_number, perlDoc);
+ sActiveOO.set(importPkg, true);
+ }
+
+ }
+
+}
+
+
+function cleanCode(textDocument: TextDocument): String[] {
+ const code = textDocument.getText();
+ const codeArray = code.split("\n");
+ const offset = textDocument.offsetAt(textDocument.positionAt(0));
+
+
+ let line_number = -offset;
+
+ let codeClean = [];
+
+ for (let i=0; i<codeArray.length;i++){
+ line_number++;
+
+ let stmt = codeArray[i];
+
+ if (stmt.match(/^(__END__|__DATA__)\s*$/)) {
+ break;
+ }
+
+ // Statement will be line with comments, whitespace and POD trimmed
+ stmt.replace(/^\s*#.*/, "");
+ stmt.replace(/^\s*/, "");
+ stmt.replace(/\s*$/, "");
+ codeClean.push(stmt);
+ }
+ return codeClean;
+}
+
diff --git a/browser-ext/src/web-symbols.ts b/browser-ext/src/web-symbols.ts
new file mode 100644
index 0000000..0e89217
--- /dev/null
+++ b/browser-ext/src/web-symbols.ts
@@ -0,0 +1,81 @@
+import {
+ SymbolInformation,
+ SymbolKind,
+ Location,
+} from 'vscode-languageserver/node';
+import { PerlDocument, PerlElem, PerlSymbolKind } from "./web-types";
+
+function waitForDoc (navSymbols: any, uri: string): Promise<PerlDocument> {
+ let retries = 0;
+
+ return new Promise((resolve, reject) => {
+ const interval = setInterval(() => {
+
+ if (++retries > 100) { // Wait for 10 seconds looking for the document.
+ reject("Found no document");
+ clearInterval(interval);
+ }
+ const perlDoc = navSymbols.get(uri);
+
+ if (perlDoc) {
+ resolve(perlDoc);
+ clearInterval(interval);
+ };
+ }, 100);
+ });
+}
+
+export function getSymbols (navSymbols: any, uri: string ): Promise<SymbolInformation[]> {
+
+ return waitForDoc(navSymbols, uri).then((perlDoc) => {
+ let symbols: SymbolInformation[] = [];
+ perlDoc.elems?.forEach((elements: PerlElem[], elemName: string) => {
+
+ elements.forEach(element => {
+ let kind: SymbolKind;
+ if (element.type == PerlSymbolKind.LocalSub){
+ kind = SymbolKind.Function;
+ } else if (element.type == PerlSymbolKind.LocalMethod){
+ kind = SymbolKind.Method;
+ } else if (element.type == PerlSymbolKind.Package){
+ kind = SymbolKind.Package;
+ } else if (element.type == PerlSymbolKind.Class){
+ kind = SymbolKind.Class;
+ } else if (element.type == PerlSymbolKind.Role){
+ kind = SymbolKind.Interface;
+ } else if (element.type == PerlSymbolKind.Field){
+ kind = SymbolKind.Field;
+ } else if (element.type == PerlSymbolKind.Label){
+ kind = SymbolKind.Key;
+ } else if (element.type == PerlSymbolKind.Phaser){
+ kind = SymbolKind.Event;
+ } else if (element.type == PerlSymbolKind.Constant){
+ kind = SymbolKind.Constant;
+ } else {
+ return;
+ }
+ const location: Location = {
+ range: {
+ start: { line: element.line, character: 0 },
+ end: { line: element.lineEnd, character: 100 }
+ },
+ uri: uri
+ };
+ const newSymbol: SymbolInformation = {
+ kind: kind,
+ location: location,
+ name: elemName
+ }
+
+ symbols.push(newSymbol);
+ });
+ });
+
+ return symbols;
+ }).catch((reason)=>{
+ // TODO: Add logging back, but detect STDIO mode first
+ console.log("Failed in getSymbols");
+ console.log(reason);
+ return [];
+ });
+}
diff --git a/browser-ext/src/web-types.ts b/browser-ext/src/web-types.ts
new file mode 100644
index 0000000..9515cf8
--- /dev/null
+++ b/browser-ext/src/web-types.ts
@@ -0,0 +1,96 @@
+// Settings for perlnavigator,
+// defaults for configurable editors stored in package.json
+// defaults for non-configurable editors in server.ts
+
+import {
+ Diagnostic,
+} from 'vscode-languageserver/browser';
+
+
+
+export interface NavigatorSettings {
+ perlPath: string;
+ enableWarnings: boolean;
+ perlcriticProfile: string;
+ perlcriticEnabled: boolean;
+ perlcriticSeverity: undefined | number;
+ perlcriticTheme: undefined | string;
+ perlcriticExclude: undefined | string;
+ perlcriticInclude: undefined | string;
+ perlimportsLintEnabled: boolean;
+ perlimportsTidyEnabled: boolean;
+ perlimportsProfile: string;
+ perltidyEnabled: boolean;
+ perltidyProfile: string;
+ severity5: string;
+ severity4: string;
+ severity3: string;
+ severity2: string;
+ severity1: string;
+ includePaths: string[];
+ includeLib: boolean;
+ logging: boolean;
+ enableProgress: boolean;
+}
+
+
+
+export interface PerlElem {
+ name: string,
+ type: PerlSymbolKind,
+ typeDetail: string,
+ file: string;
+ package: string;
+ line: number;
+ lineEnd: number;
+ value: string;
+};
+
+// Used for keeping track of what has been imported
+export interface PerlImport {
+ mod: string;
+};
+
+
+export interface PerlDocument {
+ elems: Map<string, PerlElem[]>;
+ canonicalElems: Map<string, PerlElem>;
+ imported: Map<string, number>;
+ parents: Map<string, string>;
+ filePath: string;
+ uri: string;
+}
+
+export interface CompilationResults {
+ diags: Diagnostic[],
+ perlDoc: PerlDocument,
+}
+
+export interface CompletionPrefix {
+ symbol: string,
+ charStart: number,
+ charEnd: number,
+}
+
+export enum PerlSymbolKind {
+ Module = "m",
+ Package = "p",
+ Class = "a",
+ Role = "b",
+ ImportedSub = "t",
+ Inherited = "i",
+ Field = "f", // Instance fields
+ PathedField = "d", // Instance fields
+ LocalSub = "s",
+ LocalMethod = "o", // Assumed to be instance methods
+ Method = "x", // Assumed to be instance methods
+ LocalVar = "v",
+ Constant = "n",
+ Label = "l",
+ Phaser = "e",
+ Canonical = "1",
+ // UseStatement = "u", // Reserved: used in pltags, but removed before symbol assignment.
+ ImportedVar = "c",
+ ImportedHash = "h",
+}
+
diff --git a/browser-ext/src/web-utils.ts b/browser-ext/src/web-utils.ts
new file mode 100644
index 0000000..0ccad67
--- /dev/null
+++ b/browser-ext/src/web-utils.ts
@@ -0,0 +1,157 @@
+import {
+ WorkspaceFolder
+} from 'vscode-languageserver-protocol';
+
+import {
+ TextDocument,
+ Position
+} from 'vscode-languageserver-textdocument';
+import { PerlDocument, PerlElem, NavigatorSettings, PerlSymbolKind } from "./web-types";
+import * as path from 'path';
+
+
+
+
+export function getSymbol(position: Position, txtDoc: TextDocument) {
+ // 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: string) => leftRg.exec(c);
+ const rightAllow = (c: string) => 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 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
+ }
+
+ return symbol;
+}
+
+function findRecent (found: PerlElem[], line: number){
+ let best = found[0];
+ for (var i = 0; i < found.length; i++){
+ // 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 == PerlSymbolKind.Package) ){
+ best = found[i];
+ }
+ };
+ return best;
+}
+
+export function lookupSymbol(perlDoc: PerlDocument, symbol: string, line: number): PerlElem[] {
+
+ 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 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.type);
+ }
+
+ // 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.replace(/->/g, "::"); // Module->method() can be found via Module::method
+ 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, or explictly imported.
+ found = perlDoc.elems.get(method);
+ if(found?.length) return [found[0]];
+ // 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: PerlElem[] = [];
+ perlDoc.elems.forEach((elements: PerlElem[], elemName: string) => {
+ 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;
+ }
+ }
+
+ return [];
+}
+