fish-lsp
Version:
LSP implementation for fish/fish-shell
199 lines (178 loc) • 6.62 kB
text/typescript
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;
}