perlnavigator-server
Version:
Perl language server
418 lines • 17 kB
JavaScript
;
/* Perl Navigator server. See licenses.txt file for licensing and copyright information */
Object.defineProperty(exports, "__esModule", { value: true });
const node_1 = require("vscode-languageserver/node");
const path_1 = require("path");
const os_1 = require("os");
const vscode_languageserver_textdocument_1 = require("vscode-languageserver-textdocument");
const vscode_uri_1 = require("vscode-uri");
const diagnostics_1 = require("./diagnostics");
const assets_1 = require("./assets");
const navigation_1 = require("./navigation");
const symbols_1 = require("./symbols");
const hover_1 = require("./hover");
const completion_1 = require("./completion");
const formatting_1 = require("./formatting");
const utils_1 = require("./utils");
const progress_1 = require("./progress");
const signatures_1 = require("./signatures");
const assets_2 = require("./assets");
var LRU = require("lru-cache");
// It the editor doesn't request node-ipc, use stdio instead. Make sure this runs before createConnection
if (process.argv.length <= 2) {
process.argv.push("--stdio");
}
// Create a connection for the server
// Also include all preview / proposed LSP features.
const connection = (0, node_1.createConnection)(node_1.ProposedFeatures.all);
// Create a simple text document manager.
const documents = new node_1.TextDocuments(vscode_languageserver_textdocument_1.TextDocument);
let hasConfigurationCapability = false;
let hasWorkspaceFolderCapability = false;
connection.onInitialize(async (params) => {
const capabilities = params.capabilities;
// Does the client support the `workspace/configuration` request?
// If not, we fall back using global settings.
hasConfigurationCapability = !!(capabilities.workspace && !!capabilities.workspace.configuration);
hasWorkspaceFolderCapability = !!(capabilities.workspace && !!capabilities.workspace.workspaceFolders);
const result = {
capabilities: {
textDocumentSync: node_1.TextDocumentSyncKind.Incremental,
completionProvider: {
resolveProvider: true,
triggerCharacters: ["$", "@", "%", "-", ">", ":"],
},
definitionProvider: true,
documentSymbolProvider: true,
workspaceSymbolProvider: true,
hoverProvider: true,
documentFormattingProvider: true,
documentRangeFormattingProvider: true,
signatureHelpProvider: {
// Triggers open signature help, switch to next param, and then close help
triggerCharacters: ["(", ",", ")"],
},
},
};
if (hasWorkspaceFolderCapability) {
result.capabilities.workspace = {
workspaceFolders: {
supported: true,
},
};
}
await (0, assets_2.getPerlAssetsPath)(); // Ensures assets are unpacked. Should this be in onInitialized?
return result;
});
connection.onInitialized(() => {
if (hasConfigurationCapability) {
// Register for all configuration changes.
connection.client.register(node_1.DidChangeConfigurationNotification.type, undefined);
}
});
// The global settings, used when the `workspace/configuration` request is not supported by the client.
// Does not happen with the vscode client could happen with other clients.
// The "real" default settings are in the top-level package.json
const defaultSettings = {
perlPath: "perl",
perlParams: [],
enableWarnings: true,
perlimportsProfile: "",
perltidyProfile: "",
perlcriticProfile: "",
perlcriticEnabled: true,
perlcriticSeverity: undefined,
perlcriticTheme: undefined,
perlcriticExclude: undefined,
perlcriticInclude: undefined,
perlimportsLintEnabled: false,
perlimportsTidyEnabled: false,
perltidyEnabled: true,
perlCompileEnabled: true,
perlEnv: undefined,
perlEnvAdd: true,
severity5: "warning",
severity4: "info",
severity3: "hint",
severity2: "hint",
severity1: "hint",
includePaths: [],
includeLib: true,
logging: true,
enableProgress: false,
};
let globalSettings = defaultSettings;
// Cache the settings of all open documents
const documentSettings = new Map();
// Store recent critic diags to prevent blinking of diagnostics
const documentDiags = new Map();
// Store recent compilation diags to prevent old diagnostics from resurfacing
const documentCompDiags = new Map();
// 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, key) {
return value.elems.size;
},
});
const timers = new Map();
// Keep track of modules available for import. Building this is a slow operations and varies based on workspace settings, not documents
const availableMods = new Map();
let modCacheBuilt = false;
async function rebuildModCache() {
const allDocs = documents.all();
if (allDocs.length > 0) {
modCacheBuilt = true;
dispatchForMods(allDocs[allDocs.length - 1]); // Rebuild with recent file
}
return;
}
async function buildModCache(textDocument) {
if (!modCacheBuilt) {
modCacheBuilt = true; // Set true first to prevent other files from building concurrently.
dispatchForMods(textDocument);
}
return;
}
async function dispatchForMods(textDocument) {
// BIG TODO: Resolution of workspace settings? How to do? Maybe build a hash of all include paths.
const settings = await getDocumentSettings(textDocument.uri);
const workspaceFolders = await getWorkspaceFoldersSafe();
const newMods = await (0, navigation_1.getAvailableMods)(workspaceFolders, settings);
availableMods.set("default", newMods);
return;
}
async function getWorkspaceFoldersSafe() {
try {
const workspaceFolders = await connection.workspace.getWorkspaceFolders();
if (!workspaceFolders) {
return [];
}
else {
return workspaceFolders;
}
}
catch (error) {
return [];
}
}
function expandTildePaths(paths, settings) {
const path = paths;
// Consider that this not a Windows feature,
// so, Windows "%USERPROFILE%" currently is ignored (and rarely used).
if (path.startsWith("~/")) {
const newPath = (0, os_1.homedir)() + path.slice(1);
(0, utils_1.nLog)("Expanding tilde path '" + path + "' to '" + newPath + "'", settings);
return newPath;
}
else {
return path;
}
}
async function getDocumentSettings(resource) {
if (!hasConfigurationCapability) {
return globalSettings;
}
let result = documentSettings.get(resource);
if (!result) {
result = await connection.workspace.getConfiguration({
scopeUri: resource,
section: "perlnavigator",
});
if (!result)
return globalSettings;
const resolvedSettings = { ...globalSettings, ...result };
if (resolvedSettings.includePaths) {
resolvedSettings.includePaths = resolvedSettings.includePaths.map((path) => expandTildePaths(path, resolvedSettings));
}
if (resolvedSettings.perlPath) {
resolvedSettings.perlPath = expandTildePaths(resolvedSettings.perlPath, resolvedSettings);
}
if (resolvedSettings.perlimportsProfile) {
resolvedSettings.perlimportsProfile = expandTildePaths(resolvedSettings.perlimportsProfile, resolvedSettings);
}
if (resolvedSettings.perltidyProfile) {
resolvedSettings.perltidyProfile = expandTildePaths(resolvedSettings.perltidyProfile, resolvedSettings);
}
if (resolvedSettings.perlcriticProfile) {
resolvedSettings.perlcriticProfile = expandTildePaths(resolvedSettings.perlcriticProfile, resolvedSettings);
}
if (resolvedSettings.perlEnv) {
resolvedSettings.perlEnv = Object.fromEntries(Object.entries(resolvedSettings.perlEnv).map(([key, value]) => [key, expandTildePaths(value, resolvedSettings)]));
}
documentSettings.set(resource, resolvedSettings);
return resolvedSettings;
}
return result;
}
// Only keep settings for open documents
documents.onDidClose((e) => {
documentSettings.delete(e.document.uri);
documentDiags.delete(e.document.uri);
documentCompDiags.delete(e.document.uri);
navSymbols.del(e.document.uri);
connection.sendDiagnostics({ uri: e.document.uri, diagnostics: [] });
});
documents.onDidOpen((change) => {
validatePerlDocument(change.document);
buildModCache(change.document);
});
documents.onDidSave((change) => {
validatePerlDocument(change.document);
});
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 = setTimeout(function () {
validatePerlDocument(change.document);
}, 1000);
timers.set(change.document.uri, newTimer);
});
async function validatePerlDocument(textDocument) {
const fileName = (0, path_1.basename)(vscode_uri_1.default.parse(textDocument.uri).fsPath);
const settings = await getDocumentSettings(textDocument.uri);
(0, utils_1.nLog)("Found settings", settings);
const progressToken = navSymbols.has(textDocument.uri) ? null : await (0, progress_1.startProgress)(connection, `Initializing ${fileName}`, settings);
const start = Date.now();
const workspaceFolders = await getWorkspaceFoldersSafe();
const pCompile = (0, diagnostics_1.perlcompile)(textDocument, workspaceFolders, settings); // Start compilation
const pCritic = (0, diagnostics_1.perlcritic)(textDocument, workspaceFolders, settings); // Start perlcritic
const pImports = (0, diagnostics_1.perlimports)(textDocument, workspaceFolders, settings); // Start perlimports
let perlOut = await pCompile;
(0, utils_1.nLog)("Compilation Time: " + (Date.now() - start) / 1000 + " seconds", settings);
let oldCriticDiags = documentDiags.get(textDocument.uri);
if (!perlOut) {
documentCompDiags.delete(textDocument.uri);
(0, progress_1.endProgress)(connection, progressToken);
return;
}
documentCompDiags.set(textDocument.uri, perlOut.diags);
let mixOldAndNew = perlOut.diags;
if (oldCriticDiags && settings.perlcriticEnabled) {
// Resend old critic diags to avoid overall file "blinking" in between receiving compilation and critic. TODO: async wait if it's not that long.
mixOldAndNew = perlOut.diags.concat(oldCriticDiags);
}
sendDiags({ uri: textDocument.uri, diagnostics: mixOldAndNew });
navSymbols.set(textDocument.uri, perlOut.perlDoc);
// Perl critic things
const diagCritic = await pCritic;
const diagImports = await pImports;
let newDiags = [];
if (settings.perlcriticEnabled) {
newDiags = newDiags.concat(diagCritic);
(0, utils_1.nLog)("Perl Critic Time: " + (Date.now() - start) / 1000 + " seconds", settings);
}
if (settings.perlimportsLintEnabled) {
newDiags = newDiags.concat(diagImports);
(0, utils_1.nLog)(`perlimports Time: ${(Date.now() - start) / 1000} seconds`, settings);
}
documentDiags.set(textDocument.uri, newDiags); // May need to clear out old ones if a user changed their settings.
let compDiags = documentCompDiags.get(textDocument.uri);
compDiags = compDiags ?? [];
if (newDiags) {
const allNewDiags = compDiags.concat(newDiags);
sendDiags({ uri: textDocument.uri, diagnostics: allNewDiags });
}
(0, progress_1.endProgress)(connection, progressToken);
return;
}
function sendDiags(params) {
// Before sending new diagnostics, check if the file is still open.
if (documents.get(params.uri)) {
connection.sendDiagnostics(params);
}
else {
connection.sendDiagnostics({ uri: params.uri, diagnostics: [] });
}
}
connection.onDidChangeConfiguration(async (change) => {
if (hasConfigurationCapability) {
// Reset all cached document settings
documentSettings.clear();
}
else {
globalSettings = { ...defaultSettings, ...change?.settings?.perlnavigator };
}
if (change?.settings?.perlnavigator) {
// Despite what it looks like, this fires on all settings changes, not just Navigator
await rebuildModCache();
for (const doc of documents.all()) {
// sequential changes
await validatePerlDocument(doc);
}
}
});
// This handler provides the initial list of the completion items.
connection.onCompletion((params) => {
let document = documents.get(params.textDocument.uri);
let perlDoc = navSymbols.get(params.textDocument.uri);
let mods = availableMods.get("default");
if (!document)
return;
if (!perlDoc)
return; // navSymbols is an LRU cache, so the navigation elements will be missing if you open lots of files
if (!mods)
mods = new Map();
const completions = (0, completion_1.getCompletions)(params, perlDoc, document, mods);
return {
items: completions,
isIncomplete: false,
};
});
connection.onCompletionResolve(async (item) => {
const perlElem = item.data.perlElem;
let perlDoc = navSymbols.get(item.data?.docUri);
if (!perlDoc)
return item;
let mods = availableMods.get("default");
if (!mods)
mods = new Map();
const docs = await (0, completion_1.getCompletionDoc)(perlElem, perlDoc, mods);
if (docs?.match(/\w/)) {
item.documentation = { kind: "markdown", value: docs };
;
}
return item;
});
connection.onHover(async (params) => {
let document = documents.get(params.textDocument.uri);
let perlDoc = navSymbols.get(params.textDocument.uri);
let mods = availableMods.get("default");
if (!mods)
mods = new Map();
if (!document || !perlDoc)
return;
return await (0, hover_1.getHover)(params, perlDoc, document, mods);
});
connection.onDefinition(async (params) => {
let document = documents.get(params.textDocument.uri);
let perlDoc = navSymbols.get(params.textDocument.uri);
let mods = availableMods.get("default");
if (!mods)
mods = new Map();
if (!document)
return;
if (!perlDoc)
return; // navSymbols is an LRU cache, so the navigation elements will be missing if you open lots of files
let locOut = await (0, navigation_1.getDefinition)(params, perlDoc, document, mods);
return locOut;
});
connection.onDocumentSymbol(async (params) => {
let document = documents.get(params.textDocument.uri);
// We might need to async wait for the document to be processed, but I suspect the order is fine
if (!document)
return;
return (0, symbols_1.getSymbols)(document, params.textDocument.uri);
});
connection.onWorkspaceSymbol((params) => {
let defaultMods = availableMods.get("default");
if (!defaultMods)
return;
return (0, symbols_1.getWorkspaceSymbols)(params, defaultMods);
});
connection.onDocumentFormatting(async (params) => {
let document = documents.get(params.textDocument.uri);
const settings = await getDocumentSettings(params.textDocument.uri);
const workspaceFolders = await getWorkspaceFoldersSafe();
if (!document || !settings)
return;
const editOut = await (0, formatting_1.formatDoc)(params, document, settings, workspaceFolders, connection);
return editOut;
});
connection.onDocumentRangeFormatting(async (params) => {
let document = documents.get(params.textDocument.uri);
const settings = await getDocumentSettings(params.textDocument.uri);
const workspaceFolders = await getWorkspaceFoldersSafe();
if (!document || !settings)
return;
const editOut = await (0, formatting_1.formatRange)(params, document, settings, workspaceFolders, connection);
return editOut;
});
connection.onSignatureHelp(async (params) => {
let document = documents.get(params.textDocument.uri);
let perlDoc = navSymbols.get(params.textDocument.uri);
let mods = availableMods.get("default");
if (!mods)
mods = new Map();
if (!document || !perlDoc)
return;
const signature = await (0, signatures_1.getSignature)(params, perlDoc, document, mods);
return signature;
});
connection.onShutdown((handler) => {
try {
(0, assets_1.cleanupTemporaryAssetPath)();
}
catch (error) { }
});
process.on("unhandledRejection", function (reason, p) {
console.error("Caught an unhandled Rejection at: Promise ", p, " reason: ", reason);
});
// Make the text document manager listen on the connection
// for open, change and close text document events
documents.listen(connection);
// Listen on the connection
connection.listen();
//# sourceMappingURL=server.js.map