fish-lsp
Version:
LSP implementation for fish/fish-shell
179 lines (178 loc) • 7.49 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.ErrorNodeTypes = void 0;
exports.findErrorCause = findErrorCause;
exports.isExtraEnd = isExtraEnd;
exports.isZeroIndex = isZeroIndex;
exports.isSingleQuoteVariableExpansion = isSingleQuoteVariableExpansion;
exports.isAlias = isAlias;
exports.isUniversalDefinition = isUniversalDefinition;
exports.isSourceFilename = isSourceFilename;
exports.isTestCommandVariableExpansionWithoutString = isTestCommandVariableExpansionWithoutString;
exports.isConditionalWithoutQuietCommand = isConditionalWithoutQuietCommand;
exports.isVariableDefinitionWithExpansionCharacter = isVariableDefinitionWithExpansionCharacter;
exports.isMatchingCompleteOptionIsCommand = isMatchingCompleteOptionIsCommand;
exports.isArgparseWithoutEndStdin = isArgparseWithoutEndStdin;
const node_types_1 = require("../utils/node-types");
const tree_sitter_1 = require("../utils/tree-sitter");
exports.ErrorNodeTypes = {
['function']: 'end',
['while']: 'end',
['begin']: 'end',
['for']: 'end',
['if']: 'end',
['"']: '"',
["'"]: "'",
['{']: '}',
['[']: ']',
['(']: ')',
};
function isStartTokenType(str) {
return ['function', 'while', 'if', 'for', 'begin', '[', '{', '(', "'", '"'].includes(str);
}
function findErrorCause(children) {
const stack = [];
for (const node of children) {
if (isStartTokenType(node.type)) {
const expectedEndToken = exports.ErrorNodeTypes[node.type];
const matchIndex = stack.findIndex(item => item.type === expectedEndToken);
if (matchIndex !== -1) {
stack.splice(matchIndex, 1); // Remove the matched end token
}
else {
stack.push({ node, type: expectedEndToken }); // Push the current node and expected end token to the stack
}
}
else if (Object.values(exports.ErrorNodeTypes).includes(node.type)) {
stack.push({ node, type: node.type }); // Track all end tokens
}
}
// Return the first unmatched start token from the stack, if any
return stack.length > 0 ? stack[0]?.node || null : null;
}
function isExtraEnd(node) {
return node.type === 'command' && node.text === 'end';
}
function isZeroIndex(node) {
return node.type === 'index' && node.text === '0';
}
function isSingleQuoteVariableExpansion(node) {
if (node.type !== 'single_quote_string') {
return false;
}
const variableRegex = /(?<!\\)\$\w+/; // Matches $variable, not preceded by a backslash
return variableRegex.test(node.text);
}
function isAlias(node) {
return (0, node_types_1.isCommandWithName)(node, 'alias');
}
function isUniversalDefinition(node) {
const parent = node.parent;
if (!parent)
return false;
if ((0, node_types_1.isCommandWithName)(parent, 'read') || (0, node_types_1.isCommandWithName)(parent, 'set')) {
return (0, node_types_1.isMatchingOption)(node, { shortOption: '-U', longOption: '--universal' });
}
return false;
}
function isSourceFilename(node) {
const parent = node.parent;
if (!parent)
return false;
if ((0, node_types_1.isCommandWithName)(parent, 'source') && parent.childCount === 2) {
return parent.child(1)?.equals(node) || false;
}
return false;
}
function isTestCommandVariableExpansionWithoutString(node) {
const parent = node.parent;
const previousSibling = node.previousSibling;
if (!parent || !previousSibling)
return false;
if (!(0, node_types_1.isCommandWithName)(parent, 'test', '['))
return false;
if ((0, node_types_1.isMatchingOption)(previousSibling, { shortOption: '-n' }) || (0, node_types_1.isMatchingOption)(previousSibling, { shortOption: '-z' })) {
return !(0, node_types_1.isString)(node) && !!parent.child(2) && parent.child(2).equals(node);
}
return false;
}
/**
* util for collecting if conditional_statement commands
* Necessary because there is two types of conditional statements:
* 1.) if cmd_1 || cmd_2; ...; end;
* 2.) if cmd_1; or cmd_2; ...; end;
* Case two is handled by the if statement, checking for the parent type of conditional_execution
* @param node - the current node to check (should be a command)
* @returns true if the node is a conditional statement, otherwise false
*/
function isConditionalStatement(node) {
if (['\n', ';'].includes(node?.previousSibling?.type || ''))
return false;
let curr = node.parent;
while (curr) {
if (curr.type === 'conditional_execution') {
curr = curr?.parent;
}
else if ((0, node_types_1.isIfOrElseIfConditional)(curr)) {
return true;
}
else {
break;
}
}
return false;
}
/**
* Checks if a command has a command substitution. For example,
*
* ```fish
* if set -l fishdir (status fish-path | string match -vr /bin/)
* echo $fishdir
* end
* ```
*
* @param node - the current node to check (should be a `set` command)
* @returns true if the command has a command substitution, otherwise false
*/
function hasCommandSubstitution(node) {
return node.childrenForFieldName('argument').filter(c => c.type === 'command_substitution').length > 0;
}
/**
* Check if -q,--quiet/--query flags are present for commands which follow an `if/else if` conditional statement
* @param node - the current node to check (should be a command)
* @returns true if the command is a conditional statement without -q,--quiet/--query flags, otherwise false
*/
function isConditionalWithoutQuietCommand(node) {
if (!(0, node_types_1.isCommandWithName)(node, 'command', 'type', 'read', 'set', 'string', 'abbr', 'builtin', 'functions', 'jobs'))
return false;
if (!isConditionalStatement(node))
return false;
// skip `set` commands with command substitution
if ((0, node_types_1.isCommandWithName)(node, 'set') && hasCommandSubstitution(node)) {
return false;
}
const flags = node?.childrenForFieldName('argument')
.filter(n => (0, node_types_1.isMatchingOption)(n, { shortOption: '-q', longOption: '--quiet' })
|| (0, node_types_1.isMatchingOption)(n, { shortOption: '-q', longOption: '--query' })) || [];
return flags.length === 0;
}
function isVariableDefinitionWithExpansionCharacter(node) {
if (node.parent && (0, node_types_1.isCommandWithName)(node.parent, 'set', 'read')) {
const definition = (0, tree_sitter_1.getChildNodes)(node.parent).filter(n => !(0, node_types_1.isCommand)(n) && !(0, node_types_1.isCommandName)(n) && !(0, node_types_1.isOption)(n)).shift();
return (node.type === 'variable_expansion' || node.text.startsWith('$')) && definition?.equals(node);
}
return false;
}
function isMatchingCompleteOptionIsCommand(node) {
return (0, node_types_1.isMatchingOption)(node, { shortOption: '-n', longOption: '--condition' })
|| (0, node_types_1.isMatchingOption)(node, { shortOption: '-a', longOption: '--arguments' })
|| (0, node_types_1.isMatchingOption)(node, { shortOption: '-c', longOption: '--command' });
}
function isArgparseWithoutEndStdin(node) {
if (!(0, node_types_1.isCommandWithName)(node, 'argparse'))
return false;
const endStdin = (0, tree_sitter_1.getChildNodes)(node).find(n => (0, node_types_1.isEndStdinCharacter)(n));
if (!endStdin)
return true;
return false;
}