UNPKG

fish-lsp

Version:

LSP implementation for fish/fish-shell

553 lines (552 loc) 26.6 kB
"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;