fish-lsp
Version:
LSP implementation for fish/fish-shell
448 lines (447 loc) • 16.4 kB
JavaScript
"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;