UNPKG

fish-lsp

Version:

LSP implementation for fish/fish-shell

199 lines (178 loc) 6.62 kB
import { SyntaxNode } from 'web-tree-sitter'; import { findParentFunction, isCommandName, isCommandWithName, isEndStdinCharacter, isMatchingOption, isOption, isString } from '../utils/node-types'; import { getChildNodes, getRange } from '../utils/tree-sitter'; import { LspDocument } from '../document'; import { ChangeAnnotation, CodeAction, CodeActionKind, TextDocumentEdit, TextEdit, VersionedTextDocumentIdentifier, WorkspaceEdit } from 'vscode-languageserver'; import { extractFunctionWithArgparseToCompletionsFile } from './refactors'; export type CompleteFlag = { shortOption?: string; longOption: string; }; function parseArgparseFlag(text: string): CompleteFlag { // Remove any equals and following text const beforeEquals = text.split('=')[0] as string; // Check if it has a short/long split with '/' if (beforeEquals.includes('/')) { const [short, long] = beforeEquals.split('/') as [string, string]; return { shortOption: short, longOption: long === '' ? '' : long, }; } // No short option, just return as long option return { longOption: beforeEquals, }; } function isSkipablePreviousOption(node: SyntaxNode): boolean { // 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 isMatchingOption(node, { shortOption: '-N', longOption: '--min-args' }) || isMatchingOption(node, { shortOption: '-n', longOption: '--name' }) || isMatchingOption(node, { shortOption: '-x', longOption: '--exclusive' }) || isMatchingOption(node, { shortOption: '-X', longOption: '--max-args' }); } export function findFlagsToComplete(node: SyntaxNode): CompleteFlag[] { if (!isCommandWithName(node, 'argparse')) return []; const flags: CompleteFlag[] = []; for (const child of getChildNodes(node)) { // Stop at -- argument separator if (isEndStdinCharacter(child)) break; // skip `argparse` command name if (isCommandName(child)) continue; // Skip command name and actual options (like --ignore-unknown) if (isOption(child)) continue; // skip previous options that are not flags const prev = child.previousSibling; if (prev && isOption(prev) && isSkipablePreviousOption(prev)) continue; // Handle quoted strings if (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; } export function buildCompleteString(commandName: string, flags: CompleteFlag[]): string { 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: SyntaxNode, functionNode: SyntaxNode, functionNameNode: SyntaxNode, doc: LspDocument, ): CodeAction | undefined { // 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: ChangeAnnotation = { label: `Create completions for '${functionName}' in file: ${completionPath}`, description: `Create completions for '${functionName}' to file: ${completionPath}`, }; // build the workspace edit const workspaceEdit: WorkspaceEdit = { documentChanges: [ TextDocumentEdit.create( VersionedTextDocumentIdentifier.create(doc.uri, doc.version + 1), [TextEdit.insert(getRange(functionNode).end, selectedText)]), ], changeAnnotations: { [changeAnnotation.label]: changeAnnotation }, }; return { title: 'Create completions file', kind: CodeActionKind.QuickFix, edit: workspaceEdit, }; } function getNodesForArgparse(selectedNode: SyntaxNode) { const node = selectedNode; if (isCommandWithName(node, 'argparse')) { const functionNode = findParentFunction(node); return { argparseNode: node, functionNode: functionNode, functionNameNode: functionNode?.firstChild, }; } if (node.type === 'word' && node.parent && isCommandWithName(node.parent, 'argparse')) { const functionNode = findParentFunction(node.parent); return { argparseNode: node.parent, functionNode: functionNode, functionNameNode: functionNode?.firstChild, }; } if (node.type === 'function_definition') { return { argparseNode: getChildNodes(node).find(n => isCommandWithName(n, 'argparse')), functionNode: node, functionNameNode: node.firstNamedChild, }; } return { argparseNode: undefined, functionNode: undefined, functionNameNode: undefined, }; } export function createArgparseCompletionsCodeAction( node: SyntaxNode, doc: LspDocument, ): CodeAction | undefined { const autoloadType = doc.getAutoloadType(); const { argparseNode, functionNode, functionNameNode } = getNodesForArgparse(node); if (!argparseNode || !functionNode || !functionNameNode) return undefined; if (autoloadType === 'functions') { return extractFunctionWithArgparseToCompletionsFile(doc, getRange(functionNode), functionNode); } if (autoloadType === 'conf.d') { return buildConfdCompletions(node, functionNode, functionNameNode, doc); } return undefined; }