fish-lsp
Version:
LSP implementation for fish/fish-shell
553 lines (552 loc) • 26.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const parser_1 = require("./parser");
const analyze_1 = require("./analyze");
const vscode_languageserver_1 = require("vscode-languageserver");
const document_1 = require("./document");
const formatting_1 = require("./formatting");
const logger_1 = require("./logger");
const translation_1 = require("./utils/translation");
const tree_sitter_1 = require("./utils/tree-sitter");
const hover_1 = require("./hover");
const validate_1 = require("./diagnostics/validate");
const documentation_cache_1 = require("./utils/documentation-cache");
const workspace_1 = require("./utils/workspace");
const document_symbol_1 = require("./document-symbol");
const workspace_symbol_1 = require("./workspace-symbol");
const pager_1 = require("./utils/completion/pager");
const documentation_1 = require("./utils/completion/documentation");
const list_1 = require("./utils/completion/list");
const snippets_1 = require("./utils/snippets");
const node_types_1 = require("./utils/node-types");
const config_1 = require("./config");
const documentation_2 = require("./documentation");
const signature_1 = require("./signature");
const startup_cache_1 = require("./utils/completion/startup-cache");
const document_highlight_1 = require("./document-highlight");
const comment_completions_1 = require("./utils/completion/comment-completions");
const code_action_handler_1 = require("./code-actions/code-action-handler");
const command_1 = require("./command");
const code_lens_1 = require("./code-lens");
class FishServer {
connection;
parser;
analyzer;
docs;
completion;
completionMap;
documentationCache;
logger;
static async create(connection, _params) {
const documents = new document_1.LspDocuments();
// Run these operations in parallel rather than sequentially
const [parser, cache, workspaces, completionsMap,] = await Promise.all([
(0, parser_1.initializeParser)(),
(0, documentation_cache_1.initializeDocumentationCache)(),
(0, workspace_1.initializeDefaultFishWorkspaces)(),
startup_cache_1.CompletionItemMap.initialize(),
]);
const analyzer = new analyze_1.Analyzer(parser, workspaces);
const completions = await (0, pager_1.initializeCompletionPager)(logger_1.logger, completionsMap);
return new FishServer(connection, parser, analyzer, documents, completions, completionsMap, cache, logger_1.logger);
}
initializeParams;
features;
constructor(
// the connection of the FishServer
connection, parser, analyzer, docs, completion, completionMap, documentationCache, logger) {
this.connection = connection;
this.parser = parser;
this.analyzer = analyzer;
this.docs = docs;
this.completion = completion;
this.completionMap = completionMap;
this.documentationCache = documentationCache;
this.logger = logger;
this.features = { codeActionDisabledSupport: false };
}
async initialize(params) {
logger_1.logger.logAsJson('async server.initialize(params)');
if (params) {
logger_1.logger.log();
logger_1.logger.log({ 'server.initialize.params': params });
logger_1.logger.log();
}
const result = (0, config_1.adjustInitializeResultCapabilitiesFromConfig)(config_1.configHandlers, config_1.config);
logger_1.logger.log({ onInitializedResult: result });
return result;
}
register(connection) {
const codeActionHandler = (0, code_action_handler_1.createCodeActionHandler)(this.docs, this.analyzer);
const executeHandler = (0, command_1.createExecuteCommandHandler)(this.connection, this.docs, this.logger);
//this.connection.window.createWorkDoneProgress();
connection.onInitialized(this.onInitialized.bind(this));
connection.onDidOpenTextDocument(this.didOpenTextDocument.bind(this));
connection.onDidChangeTextDocument(this.didChangeTextDocument.bind(this));
connection.onDidCloseTextDocument(this.didCloseTextDocument.bind(this));
connection.onDidSaveTextDocument(this.didSaveTextDocument.bind(this));
// • for multiple completionProviders -> https://github.com/microsoft/vscode-extension-samples/blob/main/completions-sample/src/extension.ts#L15
// • https://github.com/Dart-Code/Dart-Code/blob/7df6509870d51cc99a90cf220715f4f97c681bbf/src/providers/dart_completion_item_provider.ts#L197-202
connection.onCompletion(this.onCompletion.bind(this));
connection.onCompletionResolve(this.onCompletionResolve.bind(this)),
connection.onDocumentSymbol(this.onDocumentSymbols.bind(this));
connection.onWorkspaceSymbol(this.onWorkspaceSymbol.bind(this));
// this.connection.onWorkspaceSymbolResolve(this.onWorkspaceSymbolResolve.bind(this))
connection.onDefinition(this.onDefinition.bind(this));
connection.onReferences(this.onReferences.bind(this));
connection.onHover(this.onHover.bind(this));
connection.onRenameRequest(this.onRename.bind(this));
connection.onDocumentFormatting(this.onDocumentFormatting.bind(this));
connection.onDocumentRangeFormatting(this.onDocumentRangeFormatting.bind(this));
connection.onCodeAction(codeActionHandler);
connection.onFoldingRanges(this.onFoldingRanges.bind(this));
//this.connection.workspace.applyEdit()
connection.onDocumentHighlight(this.onDocumentHighlight.bind(this));
connection.languages.inlayHint.on(this.onInlayHints.bind(this));
connection.onSignatureHelp(this.onShowSignatureHelp.bind(this));
connection.onExecuteCommand(executeHandler);
logger_1.logger.log({ 'server.register': 'registered' });
}
didOpenTextDocument(params) {
const textDoc = params.textDocument;
const textDocText = textDoc.text.length > 300
? textDoc.text.slice(0, 300) + `\n...[${textDoc.text.length - 300} chars]`
: textDoc.text;
this.logParams('didOpenTextDocument', {
textDocument: {
version: textDoc.version,
uri: textDoc.uri,
text: textDocText,
languageID: textDoc.languageId,
},
});
const uri = (0, translation_1.uriToPath)(params.textDocument.uri);
if (!uri) {
logger_1.logger.logAsJson(`DID NOT OPEN ${uri} \n URI is null or undefined`);
return;
}
if (this.docs.open(uri, params.textDocument)) {
const doc = this.docs.get(uri);
if (doc) {
this.logParams('opened document: ', params.textDocument.uri);
this.analyzer.analyze(doc);
this.logParams('analyzed document: ', params.textDocument.uri);
this.connection.sendDiagnostics(this.sendDiagnostics({ uri: doc.uri, diagnostics: [] }));
}
}
else {
logger_1.logger.logAsJson(`Cannot open already opened doc '${params.textDocument.uri}'.`);
this.didChangeTextDocument({
textDocument: params.textDocument,
contentChanges: [
{
text: params.textDocument.text,
},
],
});
}
}
didChangeTextDocument(params) {
this.logParams('didChangeTextDocument', params);
const uri = (0, translation_1.uriToPath)(params.textDocument.uri);
const doc = this.docs.get(uri);
if (!uri || !doc)
return;
doc.applyEdits(doc.version + 1, ...params.contentChanges);
this.analyzer.analyze(doc);
logger_1.logger.logAsJson(`CHANGED -> ${doc.version}:::${doc.uri}`);
const root = this.analyzer.getRootNode(doc);
if (!root)
return;
this.connection.sendDiagnostics(this.sendDiagnostics({ uri: doc.uri, diagnostics: [] }));
// else ?
}
didCloseTextDocument(params) {
this.logParams('didCloseTextDocument', params);
const uri = (0, translation_1.uriToPath)(params.textDocument.uri);
if (!uri)
return;
logger_1.logger.logAsJson(`[${this.didCloseTextDocument.name}]: ${params.textDocument.uri}`);
this.docs.close(uri);
logger_1.logger.logAsJson(`closed uri: ${uri}`);
}
didSaveTextDocument(params) {
this.logParams('didSaveTextDocument', params);
return;
}
// @see:
// • @link [bash-lsp](https://github.com/bash-lsp/bash-language-server/blob/3a319865af9bd525d8e08cd0dd94504d5b5b7d66/server/src/server.ts#L236)
async onInitialized() {
return {
backgroundAnalysisCompleted: this.startBackgroundAnalysis(),
};
}
// @TODO: REFACTOR THIS OUT OF SERVER
// https://github.com/Dart-Code/Dart-Code/blob/7df6509870d51cc99a90cf220715f4f97c681bbf/src/providers/dart_completion_item_provider.ts#L197-202
// https://github.com/microsoft/vscode-languageserver-node/pull/322
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#insertTextModehttps://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#insertTextMode
// • clean up into completion.ts file & Decompose to state machine, with a function that gets the state machine in this class.
// DART is best example i've seen for this.
// ~ https://github.com/Dart-Code/Dart-Code/blob/7df6509870d51cc99a90cf220715f4f97c681bbf/src/providers/dart_completion_item_provider.ts#L197-202 ~
// • Implement both escapedCompletion script and dump syntax tree script
// • Add default CompletionLists to complete.ts
// • Add local file items.
// • Lastly add parameterInformation items. [ 1477 : ParameterInformation ]
// convert to CompletionItem[]
async onCompletion(params) {
this.logParams('onCompletion', params);
const { doc, uri, current } = this.getDefaults(params);
let list = list_1.FishCompletionList.empty();
if (!uri || !doc) {
logger_1.logger.logAsJson('onComplete got [NOT FOUND]: ' + uri);
return this.completion.empty();
}
const symbols = this.analyzer.cache.getFlatDocumentSymbols(doc.uri);
const { line, word } = this.analyzer.parseCurrentLine(doc, params.position);
if (!line)
return await this.completion.completeEmpty(symbols);
const fishCompletionData = {
uri: doc.uri,
position: params.position,
context: {
triggerKind: params.context?.triggerKind || vscode_languageserver_1.CompletionTriggerKind.Invoked,
triggerCharacter: params.context?.triggerCharacter,
},
};
if (line.trim().startsWith('#') && current) {
logger_1.logger.log('completeComment');
return (0, comment_completions_1.buildCommentCompletions)(line, params.position, current, fishCompletionData, word);
}
if (word.trim().endsWith('$') || line.trim().endsWith('$') || word.trim() === '$') {
logger_1.logger.log('completeVariables');
return this.completion.completeVariables(line, word, fishCompletionData, symbols);
}
try {
logger_1.logger.log('complete');
// logger.log({ uri: uri, symbols: symbols.map(s => s.name) });
list = await this.completion.complete(line, fishCompletionData, symbols);
}
catch (error) {
this.logger.logAsJson('ERROR: onComplete ' + error?.toString() || 'error');
}
return list;
}
/**
* until further reworking, onCompletionResolve requires that when a completionBuilderItem() is .build()
* it it also given the method .kind(FishCompletionItemKind) to set the kind of the item.
* Not seeing a completion result, with typed correctly is likely caused from this.
*/
async onCompletionResolve(item) {
const fishItem = item;
if (fishItem.useDocAsDetail) {
item.documentation = {
kind: vscode_languageserver_1.MarkupKind.Markdown,
value: fishItem.documentation.toString(),
};
return item;
}
const doc = await (0, documentation_1.getDocumentationResolver)(fishItem);
if (doc) {
item.documentation = doc;
}
return item;
}
// • lsp-spec: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_symbol
// • hierarchy of symbols support on line 554: https://github.com/typescript-language-server/typescript-language-server/blob/114d4309cb1450585f991604118d3eff3690237c/src/lsp-server.ts#L554
//
// ResolveWorkspaceResult
// https://github.com/Dart-Code/Dart-Code/blob/master/src/extension/providers/dart_workspace_symbol_provider.ts#L7
//
async onDocumentSymbols(params) {
this.logParams('onDocumentSymbols', params);
const { doc } = this.getDefaultsForPartialParams(params);
if (!doc)
return [];
const symbols = this.analyzer.cache.getDocumentSymbols(doc.uri);
return (0, document_symbol_1.filterLastPerScopeSymbol)(symbols);
}
get supportHierarchicalDocumentSymbol() {
const textDocument = this.initializeParams?.capabilities.textDocument;
const documentSymbol = textDocument && textDocument.documentSymbol;
return (!!documentSymbol &&
!!documentSymbol.hierarchicalDocumentSymbolSupport);
}
/**
* highlight provider
*/
onDocumentHighlight(params) {
this.logParams('onDocumentHighlight', params);
const { doc } = this.getDefaults(params);
if (!doc)
return [];
const text = doc.getText();
const tree = this.parser.parse(text);
const node = (0, tree_sitter_1.getNodeAtPosition)(tree, params.position);
if (!node)
return [];
const highlights = (0, document_highlight_1.getDocumentHighlights)(tree, node);
return highlights;
}
async onWorkspaceSymbol(params) {
this.logParams('onWorkspaceSymbol', params.query);
return this.analyzer.getWorkspaceSymbols(params.query) || [];
}
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#showDocumentParams
async onDefinition(params) {
this.logParams('onDefinition', params);
const { doc } = this.getDefaults(params);
if (!doc)
return [];
return this.analyzer.getDefinitionLocation(doc, params.position);
}
async onReferences(params) {
this.logParams('onReference', params);
const { doc, uri, root, current } = this.getDefaults(params);
if (!doc || !uri || !root || !current)
return [];
return (0, workspace_symbol_1.getReferenceLocations)(this.analyzer, doc, params.position);
}
// Probably should move away from `documentationCache`. It works but is too expensive memory wise.
// REFACTOR into a procedure that conditionally determines output type needed.
// Also plan to get rid of any other cache's, so that the garbage collector can do its job.
async onHover(params) {
this.logParams('onHover', params);
const { doc, uri, root, current } = this.getDefaults(params);
if (!doc || !uri || !root || !current) {
return null;
}
const { kindType, kindString } = (0, translation_1.symbolKindsFromNode)(current);
logger_1.logger.log({ currentText: current.text, currentType: current.type, symbolKind: kindString });
const prebuiltSkipType = [
...snippets_1.PrebuiltDocumentationMap.getByType('pipe'),
...snippets_1.PrebuiltDocumentationMap.getByType('status'),
].find(obj => obj.name === current.text);
// const prebuiltDoc = PrebuiltDocumentationMap.getByName(current.text);
const symbolItem = this.analyzer.getHover(doc, params.position);
if (symbolItem)
return symbolItem;
if (prebuiltSkipType) {
return {
contents: (0, documentation_2.enrichToMarkdown)([
`___${current.text}___ - _${(0, snippets_1.getPrebuiltDocUrl)(prebuiltSkipType)}_`,
'___',
`type - __(${prebuiltSkipType.type})__`,
'___',
`${prebuiltSkipType.description}`,
].join('\n')),
};
}
const symbolType = [
'function',
'class',
'variable',
].includes(kindString) ? kindType : undefined;
const globalItem = await this.documentationCache.resolve(current.text.trim(), uri, symbolType);
logger_1.logger.log({ './src/server.ts:395': `this.documentationCache.resolve() found ${!!globalItem}`, docs: globalItem.docs });
if (globalItem && globalItem.docs) {
logger_1.logger.log(globalItem.docs);
return {
contents: {
kind: vscode_languageserver_1.MarkupKind.Markdown,
value: globalItem.docs,
},
};
}
const fallbackHover = await (0, hover_1.handleHover)(this.analyzer, doc, params.position, current, this.documentationCache);
logger_1.logger.log(fallbackHover?.contents);
return fallbackHover;
}
// workspace.fileOperations.didRename
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.18/specification/#fileEvent
//applyEdits(params: WorkspaceEdit): void {
// this.logParams("applyRenameFile", params);
// const changes : ResoucreOperation = params.
// for (const change of changes) {
// switch (change.kind) {
// case 'rename':
// this.docs.rename(change.oldUri, change.newUri);
// this.analyzer.cache.updateUri(change.oldUri, change.newUri);
//
//
// }
// const newUri = change.
// }
//
// return;
//}
async onRename(params) {
this.logParams('onRename', params);
const { doc } = this.getDefaults(params);
if (!doc)
return null;
return (0, workspace_symbol_1.getRenameWorkspaceEdit)(this.analyzer, doc, params.position, params.newName);
}
async onDocumentFormatting(params) {
this.logParams('onDocumentFormatting', params);
const { doc } = this.getDefaultsForPartialParams(params);
if (!doc)
return [];
const formattedText = await (0, formatting_1.formatDocumentContent)(doc.getText()).catch(error => {
this.connection.console.error(`Formatting error: ${error}`);
if (config_1.config.fish_lsp_show_client_popups) {
this.connection.window.showErrorMessage(`Failed to format range: ${error}`);
}
return doc.getText(); // fallback to original text on error
});
const fullRange = {
start: doc.positionAt(0),
end: doc.positionAt(doc.getText().length),
};
return [vscode_languageserver_1.TextEdit.replace(fullRange, formattedText)];
}
async onDocumentRangeFormatting(params) {
this.logParams('onDocumentRangeFormatting', params);
const { doc } = this.getDefaultsForPartialParams(params);
if (!doc)
return [];
const range = params.range;
const startOffset = doc.offsetAt(range.start);
const endOffset = doc.offsetAt(range.end);
const originalText = doc.getText().slice(startOffset, endOffset);
const formattedText = await (0, formatting_1.formatDocumentContent)(originalText).catch(error => {
this.connection.console.error(`Formatting error: ${error}`);
if (config_1.config.fish_lsp_show_client_popups) {
this.connection.window.showErrorMessage(`Failed to format range: ${error}`);
}
return originalText; // fallback to original text on error
});
return [vscode_languageserver_1.TextEdit.replace(range, formattedText)];
}
async onFoldingRanges(params) {
this.logParams('onFoldingRanges', params);
const file = (0, translation_1.uriToPath)(params.textDocument.uri);
const document = this.docs.get(file);
if (!document) {
throw new Error(`The document should not be opened in the folding range, file: ${file}`);
}
//this.analyzer.analyze(document)
const symbols = this.analyzer.getDocumentSymbols(document.uri);
const flatSymbols = document_symbol_1.FishDocumentSymbol.toTree(symbols).toFlatArray();
logger_1.logger.logPropertiesForEachObject(flatSymbols.filter((s) => s.kind === vscode_languageserver_1.SymbolKind.Function), 'name', 'range');
const folds = flatSymbols
.filter((symbol) => symbol.kind === vscode_languageserver_1.SymbolKind.Function)
.map((symbol) => document_symbol_1.FishDocumentSymbol.toFoldingRange(symbol));
folds.forEach((fold) => logger_1.logger.log({ fold }));
return folds;
}
async onCodeAction(params) {
this.logParams('onCodeAction', params);
const uri = (0, translation_1.uriToPath)(params.textDocument.uri);
const document = this.docs.get(uri);
if (!document || !uri)
return [];
const results = [];
// for (const diagnostic of params.context.diagnostics) {
// const res = handleConversionToCodeAction(
// diagnostic,
// root,
// document,
// );
// if (res) results.push(res);
// }
return results;
}
// works but is super slow and resource intensive, plus it doesn't really display much
async onInlayHints(params) {
logger_1.logger.log({ params });
const uri = (0, translation_1.uriToPath)(params.textDocument.uri);
const document = this.docs.get(uri);
if (!document)
return [];
const root = this.analyzer.getRootNode(document);
if (!root)
return [];
return (0, code_lens_1.getStatusInlayHints)(root);
}
onShowSignatureHelp(params) {
this.logParams('onShowSignatureHelp', params);
const { doc, uri } = this.getDefaults(params);
if (!doc || !uri)
return null;
const { line, lineRootNode, lineLastNode } = this.analyzer.parseCurrentLine(doc, params.position);
if (line.trim() === '')
return null;
const currentCmd = (0, node_types_1.findParentCommand)(lineLastNode);
// const commands = getChildNodes(lineRootNode).filter(isCommand)
const aliasSignature = this.completionMap.allOfKinds('alias').find(a => a.label === currentCmd.text);
if (aliasSignature)
return (0, signature_1.getAliasedCompletionItemSignature)(aliasSignature);
const varNode = (0, tree_sitter_1.getChildNodes)(lineRootNode).find(c => (0, node_types_1.isVariableDefinition)(c));
const lastCmd = (0, tree_sitter_1.getChildNodes)(lineRootNode).filter(c => (0, node_types_1.isCommand)(c)).pop();
logger_1.logger.log({ line, lastCmds: lastCmd?.text });
if (varNode && (line.startsWith('set') || line.startsWith('read')) && lastCmd?.text === lineRootNode.text.trim()) {
const varName = varNode.text;
const varDocs = snippets_1.PrebuiltDocumentationMap.getByName(varNode.text);
if (!varDocs.length)
return null;
return {
signatures: [
{
label: varName,
documentation: {
kind: 'markdown',
value: varDocs.map(d => d.description).join('\n'),
},
},
],
activeSignature: 0,
activeParameter: 0,
};
}
return null;
}
sendDiagnostics(params) {
this.logParams('sendDiagnostics', params);
const { diagnostics } = params;
const uri = (0, translation_1.uriToPath)(params.uri);
const doc = this.docs.get(uri);
if (!doc)
return { uri: params.uri, diagnostics };
const { rootNode } = this.parser.parse(doc.getText());
return { uri: params.uri, diagnostics: (0, validate_1.getDiagnostics)(rootNode, doc) };
}
/////////////////////////////////////////////////////////////////////////////////////
// HELPERS
/////////////////////////////////////////////////////////////////////////////////////
/**
* Logs the params passed into a handler
*
* @param {string} methodName - the FishLsp method name that was called
* @param {any[]} params - the params passed into the method
*/
logParams(methodName, ...params) {
logger_1.logger.log({ handler: methodName, params });
}
// helper to get all the default objects needed when a TextDocumentPositionParam is passed
// into a handler
getDefaults(params) {
const uri = (0, translation_1.uriToPath)(params.textDocument.uri);
const doc = this.docs.get(uri);
if (!doc || !uri)
return {};
const root = this.analyzer.getRootNode(doc);
const current = this.analyzer.nodeAtPoint(doc.uri, params.position.line, params.position.character);
return { doc, uri, root, current };
}
getDefaultsForPartialParams(params) {
const uri = (0, translation_1.uriToPath)(params.textDocument.uri);
const doc = this.docs.get(uri);
const root = doc ? this.analyzer.getRootNode(doc) : undefined;
return { doc, uri, root };
}
async startBackgroundAnalysis() {
// ../node_modules/vscode-languageserver/lib/common/progress.d.ts
const notifyCallback = (text) => {
if (!config_1.config.fish_lsp_show_client_popups)
return;
this.connection.window.showInformationMessage(text);
};
return this.analyzer.initiateBackgroundAnalysis(notifyCallback);
}
}
exports.default = FishServer;