UNPKG

fish-lsp

Version:

LSP implementation for fish/fish-shell

448 lines (447 loc) 16.4 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.SymbolCache = exports.AnalyzedDocumentCache = exports.AnalyzedDocument = exports.GlobalDefinitionCache = exports.Analyzer = void 0; const vscode_languageserver_1 = require("vscode-languageserver"); const tree_sitter_1 = require("./utils/tree-sitter"); const node_types_1 = require("./utils/node-types"); const translation_1 = require("./utils/translation"); const fs_1 = require("fs"); const os_1 = __importDefault(require("os")); const document_symbol_1 = require("./document-symbol"); const generic_tree_1 = require("./utils/generic-tree"); const workspace_symbol_1 = require("./workspace-symbol"); const config_1 = require("./config"); const logger_1 = require("./logger"); class Analyzer { parser; workspaces; cache = new AnalyzedDocumentCache(); globalSymbols = new GlobalDefinitionCache(); amountIndexed = 0; constructor(parser, workspaces = []) { this.parser = parser; this.workspaces = workspaces; } analyze(document) { this.parser.reset(); const analyzedDocument = this.getAnalyzedDocument(this.parser, document); this.cache.setDocument(document.uri, analyzedDocument); const symbols = this.cache.getDocumentSymbols(document.uri); (0, document_symbol_1.filterGlobalSymbols)(symbols).forEach((symbol) => { this.globalSymbols.add(symbol); }); return this.cache.getDocumentSymbols(document.uri); } getAnalyzedDocument(parser, document) { const tree = parser.parse(document.getText()); const documentSymbols = (0, document_symbol_1.getFishDocumentSymbols)(document, tree.rootNode); const commands = this.getCommandNames(document); return AnalyzedDocument.create(document, documentSymbols, commands, tree); } async initiateBackgroundAnalysis(callbackfn) { const startTime = performance.now(); const max_files = config_1.config.fish_lsp_max_background_files; let amount = 0; const analysisPromises = []; for (const workspace of this.workspaces) { const docs = workspace .urisToLspDocuments() .filter((doc) => doc.shouldAnalyzeInBackground()) .slice(0, max_files - amount); // Only take what we need up to max_files // Create promises for each document analysis const workspacePromises = docs.map(async (doc) => { try { this.analyze(doc); amount++; } catch (err) { logger_1.logger.log(err); } }); analysisPromises.push(...workspacePromises); if (amount >= max_files) { break; } } // Wait for all analysis tasks to complete await Promise.all(analysisPromises); this.amountIndexed = amount; const endTime = performance.now(); const duration = ((endTime - startTime) / 1000).toFixed(2); // Convert to seconds with 2 decimal places callbackfn(`[fish-lsp] analyzed ${amount} files in ${duration}s`); logger_1.logger.log(`[fish-lsp] analyzed ${amount} files in ${duration}s`); return { filesParsed: amount }; } findDocumentSymbol(document, position) { const symbols = document_symbol_1.FishDocumentSymbol.flattenArray(this.cache.getDocumentSymbols(document.uri)); const wordAtPoint = this.wordAtPoint(document.uri, position.line, position.character); return symbols.find((symbol) => { if (symbol.kind === vscode_languageserver_1.SymbolKind.Function && wordAtPoint === symbol.name) { return symbol.scope.containsPosition(position); } return (0, tree_sitter_1.isPositionWithinRange)(position, symbol.selectionRange); }); } /** * method that returns all the workspaceSymbols that are in the same scope as the given * shell * @returns {WorkspaceSymbol[]} array of all symbols */ getWorkspaceSymbols(query = '') { return this.globalSymbols.allSymbols .map((s) => document_symbol_1.FishDocumentSymbol.toWorkspaceSymbol(s)) .filter((symbol) => { return symbol.name.startsWith(query); }); } getDefinition(document, position) { const symbols = (0, workspace_symbol_1.findDefinitionSymbols)(this, document, position); return symbols[0]; } getDefinitionLocation(document, position) { const symbol = this.getDefinition(document, position); if (symbol) { return [ vscode_languageserver_1.Location.create(symbol.uri, symbol.selectionRange), ]; } return []; } getHover(document, position) { const tree = this.getTree(document); const node = this.nodeAtPoint(document.uri, position.line, position.character); if (!tree || !node) { return null; } const symbol = this.getDefinition(document, position) || this.globalSymbols.findFirst(node.text); if (symbol) { return { contents: { kind: vscode_languageserver_1.MarkupKind.Markdown, value: symbol.detail, }, }; } return null; } //public findCompletions( // document: LspDocument, // position: Position, // data: FishCompletionData //): FishCompletionItem[] { // const symbols = this.cache.getDocumentSymbols(document.uri); // const localSymbols = findSymbolsForCompletion(symbols, position); // // const globalSymbols = this.globalSymbols // .uniqueSymbols() // .filter((s) => !localSymbols.some((l) => s.name === l.name)) // .map((s) => FishDocumentSymbol.toGlobalCompletion(s, data)); // // return [ // ...localSymbols.map((s) => // FishDocumentSymbol.toLocalCompletion(s, data) // ), // ...globalSymbols, // ]; //} getTree(document) { return this.cache.getDocument(document.uri)?.tree; } /** * Finds the rootnode given a LspDocument. If useCache is set to false, it will * use the parser to parse the document passed in, and then return the rootNode. */ getRootNode(document) { return this.cache.getParsedTree(document.uri)?.rootNode; } getDocument(documentUri) { return this.cache.getDocument(documentUri)?.document; } getDocumentSymbols(documentUri) { return this.cache.getDocumentSymbols(documentUri); } getFlatDocumentSymbols(documentUri) { return this.cache.getFlatDocumentSymbols(documentUri); } parsePosition(document, position) { const root = this.getRootNode(document) || null; return { root: root, currentNode: root?.descendantForPosition({ row: position.line, column: Math.max(0, position.character - 1), }) || null, }; } /** * Returns an object to be deconstructed, for the onComplete function in the server. * This function is necessary because the normal onComplete parse of the LspDocument * will commonly throw errors (user is incomplete typing a command, etc.). To avoid * inaccurate parses for the entire document, we instead parse just the current line * that the user is on, and send it to the shell script to complete. * * @Note: the position should not edited (pass in the direct position from the CompletionParams) * * @returns * line - the string output of the line the cursor is on * lineRootNode - the rootNode for the line that the cursor is on * lineCurrentNode - the last node in the line */ parseCurrentLine(document, position) { //const linePreTrim: string = document.getLineBeforeCursor(position); //const line = linePreTrim.slice(0,linePreTrim.lastIndexOf('\n')); const line = document .getLineBeforeCursor(position) .replace(/^(.*)\n$/, '$1') || ''; const word = this.wordAtPoint(document.uri, position.line, Math.max(position.character - 1, 0)) || ''; const lineRootNode = this.parser.parse(line).rootNode; const lineLastNode = lineRootNode.descendantForPosition({ row: 0, column: line.length - 1, }); return { line, word, lineRootNode, lineLastNode }; } wordAtPoint(uri, line, column) { const node = this.nodeAtPoint(uri, line, column); if (!node || node.childCount > 0 || node.text.trim() === '') { return null; } return node.text.trim(); } /** * Find the node at the given point. */ nodeAtPoint(uri, line, column) { const tree = this.cache.getParsedTree(uri); if (!tree?.rootNode) { // Check for lacking rootNode (due to failed parse?) return null; } return tree.rootNode.descendantForPosition({ row: line, column }); } /** * Find the name of the command at the given point. */ commandNameAtPoint(uri, line, column) { let node = this.nodeAtPoint(uri, line, column); while (node && !(0, node_types_1.isCommand)(node)) { node = node.parent; } if (!node) { return null; } const firstChild = node.firstNamedChild; if (!firstChild || !(0, node_types_1.isCommandName)(firstChild)) { return null; } return firstChild.text.trim(); } getNodes(document) { return (0, tree_sitter_1.getChildNodes)(this.parser.parse(document.getText()).rootNode); } getCommandNames(document) { const allCommands = this.getNodes(document) .filter((node) => (0, node_types_1.isCommandName)(node)) .map((node) => node.text); const result = new Set(allCommands); return Array.from(result); } getExistingAutoloadedFiles(name) { const searchNames = [ `${os_1.default}/.config/functions/${name}.fish`, `${os_1.default}/.config/completions/${name}.fish`, ]; return searchNames .filter((path) => (0, fs_1.existsSync)(path)) .map((path) => (0, translation_1.pathToUri)(path)); } } exports.Analyzer = Analyzer; class GlobalDefinitionCache { _definitions; constructor(_definitions = new Map()) { this._definitions = _definitions; } add(symbol) { const current = this._definitions.get(symbol.name) || []; if (!current.some(s => document_symbol_1.FishDocumentSymbol.equal(s, symbol))) { current.push(symbol); } this._definitions.set(symbol.name, current); } find(name) { return this._definitions.get(name) || []; } findFirst(name) { const symbols = this.find(name); if (symbols.length === 0) { return undefined; } return symbols[0]; } has(name) { return this._definitions.has(name); } uniqueSymbols() { const unique = []; this.allNames.forEach(name => { const u = this.findFirst(name); if (u) { unique.push(u); } }); return unique; } get allSymbols() { const all = []; for (const [_, symbols] of this._definitions.entries()) { all.push(...symbols); } return all; } get allNames() { return [...this._definitions.keys()]; } get map() { return this._definitions; } } exports.GlobalDefinitionCache = GlobalDefinitionCache; var AnalyzedDocument; (function (AnalyzedDocument) { function create(document, documentSymbols, commands, tree) { return { document, documentSymbols, commands, tree, }; } AnalyzedDocument.create = create; })(AnalyzedDocument || (exports.AnalyzedDocument = AnalyzedDocument = {})); class AnalyzedDocumentCache { _documents; constructor(_documents = new Map()) { this._documents = _documents; } uris() { return [...this._documents.keys()]; } setDocument(uri, analyzedDocument) { this._documents.set(uri, analyzedDocument); } getDocument(uri) { if (!this._documents.has(uri)) { return undefined; } return this._documents.get(uri); } updateUri(oldUri, newUri) { const oldValue = this.getDocument(oldUri); if (oldValue) { this._documents.delete(oldUri); this._documents.set(newUri, oldValue); } } getDocumentSymbols(uri) { return this._documents.get(uri)?.documentSymbols || []; } getFlatDocumentSymbols(uri) { return document_symbol_1.FishDocumentSymbol.flattenArray(this.getDocumentSymbols(uri)); } getCommands(uri) { return this._documents.get(uri)?.commands || []; } getRootNode(uri) { return this.getParsedTree(uri)?.rootNode; } getParsedTree(uri) { return this._documents.get(uri)?.tree; } getSymbolTree(uri) { const document = this.getDocument(uri); if (!document) { return new generic_tree_1.GenericTree([]); } return new generic_tree_1.GenericTree(document.documentSymbols); } /** * Name is a string that will be searched across all symbols in cache. tree-sitter-fish * type of symbols that will be searched is 'word' (i.e. variables, functions, commands) * @param {string} name - string SyntaxNode.name to search in cache * @returns {map<URI, SyntaxNode[]>} - map of URIs to SyntaxNodes that match the name */ findMatchingNames(name) { const matches = new Map(); this.forEach((uri, doc) => { const root = doc.tree.rootNode; const nodes = root.descendantsOfType('word').filter(node => node.text === name); if (nodes.length > 0) { matches.set(uri, nodes); } }); return matches; } forEach(callbackfn) { for (const [uri, document] of this._documents) { callbackfn(uri, document); } } filter(callbackfn) { const result = []; this.forEach((currentUri, currentDocument) => { if (callbackfn(currentUri, currentDocument)) { result.push(currentDocument); } }); return result; } mapUris(callbackfn, uris = this.uris()) { const result = []; for (const uri of uris) { const doc = this.getDocument(uri); if (!doc) { continue; } result.push(callbackfn(doc)); } return result; } } exports.AnalyzedDocumentCache = AnalyzedDocumentCache; class SymbolCache { _names; _variables; _functions; constructor(_names = new Set(), _variables = new Map(), _functions = new Map()) { this._names = _names; this._variables = _variables; this._functions = _functions; } add(symbol) { const oldVars = this._variables.get(symbol.name) || []; switch (symbol.kind) { case vscode_languageserver_1.SymbolKind.Variable: this._variables.set(symbol.name, [...oldVars, symbol]); break; case vscode_languageserver_1.SymbolKind.Function: this._functions.set(symbol.name, [...oldVars, symbol]); break; } this._names.add(symbol.name); } isVariable(name) { return this._variables.has(name); } isFunction(name) { return this._functions.has(name); } has(name) { return this._names.has(name); } } exports.SymbolCache = SymbolCache;