UNPKG

fish-lsp

Version:

LSP implementation for fish/fish-shell

179 lines (178 loc) 7.49 kB
"use strict"; 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; }