fish-lsp
Version:
LSP implementation for fish/fish-shell
175 lines (174 loc) • 7.04 kB
JavaScript
;
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;
}