UNPKG

fish-lsp

Version:

LSP implementation for fish/fish-shell

175 lines (174 loc) 7.04 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.findFlagsToComplete = findFlagsToComplete; exports.buildCompleteString = buildCompleteString; exports.createArgparseCompletionsCodeAction = createArgparseCompletionsCodeAction; const node_types_1 = require("../utils/node-types"); const tree_sitter_1 = require("../utils/tree-sitter"); const vscode_languageserver_1 = require("vscode-languageserver"); const refactors_1 = require("./refactors"); function parseArgparseFlag(text) { // Remove any equals and following text const beforeEquals = text.split('=')[0]; // Check if it has a short/long split with '/' if (beforeEquals.includes('/')) { const [short, long] = beforeEquals.split('/'); return { shortOption: short, longOption: long === '' ? '' : long, }; } // No short option, just return as long option return { longOption: beforeEquals, }; } function isSkipablePreviousOption(node) { // don't skip previous nodes when the previous node is of the form: // ```fish // argparse -N=1 --max-args=2 // ``` if (node.text.includes('=')) return false; return (0, node_types_1.isMatchingOption)(node, { shortOption: '-N', longOption: '--min-args' }) || (0, node_types_1.isMatchingOption)(node, { shortOption: '-n', longOption: '--name' }) || (0, node_types_1.isMatchingOption)(node, { shortOption: '-x', longOption: '--exclusive' }) || (0, node_types_1.isMatchingOption)(node, { shortOption: '-X', longOption: '--max-args' }); } function findFlagsToComplete(node) { if (!(0, node_types_1.isCommandWithName)(node, 'argparse')) return []; const flags = []; for (const child of (0, tree_sitter_1.getChildNodes)(node)) { // Stop at -- argument separator if ((0, node_types_1.isEndStdinCharacter)(child)) break; // skip `argparse` command name if ((0, node_types_1.isCommandName)(child)) continue; // Skip command name and actual options (like --ignore-unknown) if ((0, node_types_1.isOption)(child)) continue; // skip previous options that are not flags const prev = child.previousSibling; if (prev && (0, node_types_1.isOption)(prev) && isSkipablePreviousOption(prev)) continue; // Handle quoted strings if ((0, node_types_1.isString)(child)) { // Remove surrounding quotes const text = child.text.slice(1, -1); flags.push(parseArgparseFlag(text)); continue; } // Handle unquoted option strings if (child.type === 'word' && !child.text.startsWith('-')) { flags.push(parseArgparseFlag(child.text)); } } return flags; } function buildCompleteString(commandName, flags) { return flags.map(flag => { let text = `complete -c ${commandName}`; if (flag.shortOption) { text += ` -s ${flag.shortOption}`; } if (flag.longOption) { text += ` -l ${flag.longOption}`; } return text; }).join('\n'); } /** * Helper function to build `argparse` completions for the current function in a * `conf.d/file.fish` file. * ___ * Some example input can be seen below: * ___ * ```fish * # ~/.config/fish/conf.d/file.fish * function some_function * argparse h/help o/option= v/verbose -- $argv * or return * * echo 'do some stuff' * end * ``` * ___ * @param argparseNode The `argparse` node * @param functionNode The `function_definition` node * @param functionNameNode The `functionNode.firstNamedChild` node containing the name of the function * @returns A `CodeAction` object to create the completions file */ function buildConfdCompletions(argparseNode, functionNode, functionNameNode, doc) { // get the path to the completions file. Should be in the conf.d directory const completionPath = doc.getRelativeFilenameToWorkspace(); // get the flags and the function name const flags = findFlagsToComplete(argparseNode); const functionName = functionNameNode.text; // build the `complete -c command -s -l` string const completionText = buildCompleteString(functionName, flags); // Get the text to insert const selectedText = `\n# auto generated by fish-lsp\n${completionText}\n`; // Create a change annotation const changeAnnotation = { label: `Create completions for '${functionName}' in file: ${completionPath}`, description: `Create completions for '${functionName}' to file: ${completionPath}`, }; // build the workspace edit const workspaceEdit = { documentChanges: [ vscode_languageserver_1.TextDocumentEdit.create(vscode_languageserver_1.VersionedTextDocumentIdentifier.create(doc.uri, doc.version + 1), [vscode_languageserver_1.TextEdit.insert((0, tree_sitter_1.getRange)(functionNode).end, selectedText)]), ], changeAnnotations: { [changeAnnotation.label]: changeAnnotation }, }; return { title: 'Create completions file', kind: vscode_languageserver_1.CodeActionKind.QuickFix, edit: workspaceEdit, }; } function getNodesForArgparse(selectedNode) { const node = selectedNode; if ((0, node_types_1.isCommandWithName)(node, 'argparse')) { const functionNode = (0, node_types_1.findParentFunction)(node); return { argparseNode: node, functionNode: functionNode, functionNameNode: functionNode?.firstChild, }; } if (node.type === 'word' && node.parent && (0, node_types_1.isCommandWithName)(node.parent, 'argparse')) { const functionNode = (0, node_types_1.findParentFunction)(node.parent); return { argparseNode: node.parent, functionNode: functionNode, functionNameNode: functionNode?.firstChild, }; } if (node.type === 'function_definition') { return { argparseNode: (0, tree_sitter_1.getChildNodes)(node).find(n => (0, node_types_1.isCommandWithName)(n, 'argparse')), functionNode: node, functionNameNode: node.firstNamedChild, }; } return { argparseNode: undefined, functionNode: undefined, functionNameNode: undefined, }; } function createArgparseCompletionsCodeAction(node, doc) { const autoloadType = doc.getAutoloadType(); const { argparseNode, functionNode, functionNameNode } = getNodesForArgparse(node); if (!argparseNode || !functionNode || !functionNameNode) return undefined; if (autoloadType === 'functions') { return (0, refactors_1.extractFunctionWithArgparseToCompletionsFile)(doc, (0, tree_sitter_1.getRange)(functionNode), functionNode); } if (autoloadType === 'conf.d') { return buildConfdCompletions(node, functionNode, functionNameNode, doc); } return undefined; }