UNPKG

perlnavigator-server

Version:

Perl language server

418 lines 17 kB
"use strict"; /* 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