UNPKG

fish-lsp

Version:

LSP implementation for fish/fish-shell

805 lines (804 loc) 25.2 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.VariableScopeFlags = void 0; exports.isComment = isComment; exports.isShebang = isShebang; exports.isFunctionDefinition = isFunctionDefinition; exports.isCommand = isCommand; exports.isFunctionDefinitionName = isFunctionDefinitionName; exports.isTopLevelFunctionDefinition = isTopLevelFunctionDefinition; exports.isDefinition = isDefinition; exports.isCommandName = isCommandName; exports.isProgram = isProgram; exports.isError = isError; exports.isForLoop = isForLoop; exports.isIfStatement = isIfStatement; exports.isElseStatement = isElseStatement; exports.isConditional = isConditional; exports.isIfOrElseIfConditional = isIfOrElseIfConditional; exports.isPossibleUnreachableStatement = isPossibleUnreachableStatement; exports.isClause = isClause; exports.isStatement = isStatement; exports.isBlock = isBlock; exports.isEnd = isEnd; exports.isScope = isScope; exports.isSemicolon = isSemicolon; exports.isNewline = isNewline; exports.isBlockBreak = isBlockBreak; exports.isString = isString; exports.isStringCharacter = isStringCharacter; exports.isEndStdinCharacter = isEndStdinCharacter; exports.isLongOption = isLongOption; exports.isShortOption = isShortOption; exports.isOption = isOption; exports.isJoinedShortOption = isJoinedShortOption; exports.hasShortOptionCharacter = hasShortOptionCharacter; exports.isMatchingOption = isMatchingOption; exports.isPipe = isPipe; exports.gatherSiblingsTillEol = gatherSiblingsTillEol; exports.isBeforeCommand = isBeforeCommand; exports.isVariable = isVariable; exports.findPreviousSibling = findPreviousSibling; exports.findParentCommand = findParentCommand; exports.findParentFunction = findParentFunction; exports.isVariableDefinitionCommand = isVariableDefinitionCommand; exports.findParentVariableDefinitionKeyword = findParentVariableDefinitionKeyword; exports.refinedFindParentVariableDefinitionKeyword = refinedFindParentVariableDefinitionKeyword; exports.isVariableDefinitionName = isVariableDefinitionName; exports.isVariableDefinition = isVariableDefinition; exports.findEnclosingVariableScope = findEnclosingVariableScope; exports.findForLoopVariable = findForLoopVariable; exports.findSetDefinedVariable = findSetDefinedVariable; exports.findReadVariables = findReadVariables; exports.hasParent = hasParent; exports.findParent = findParent; exports.hasParentFunction = hasParentFunction; exports.findFunctionScope = findFunctionScope; exports.scopeCheck = scopeCheck; exports.isLocalVariable = isLocalVariable; exports.wordNodeIsCommand = wordNodeIsCommand; exports.isSwitchStatement = isSwitchStatement; exports.isCaseClause = isCaseClause; exports.isReturn = isReturn; exports.isConditionalCommand = isConditionalCommand; exports.chainedCommandGroup = chainedCommandGroup; exports.isCommandFlag = isCommandFlag; exports.isRegexArgument = isRegexArgument; exports.isUnmatchedStringCharacter = isUnmatchedStringCharacter; exports.isPartialForLoop = isPartialForLoop; exports.isInlineComment = isInlineComment; exports.isCommandWithName = isCommandWithName; const tree_sitter_1 = require("./tree-sitter"); const VariableTypes = __importStar(require("./variable-syntax-nodes")); /** * fish shell comment: '# ...' */ function isComment(node) { return node.type === 'comment' && !isShebang(node); } function isShebang(node) { const parent = node.parent; if (!parent || !isProgram(parent)) { return false; } const firstLine = parent.firstChild; if (!firstLine) { return false; } if (!node.equals(firstLine)) { return false; } return (firstLine.type === 'comment' && firstLine.text.startsWith('#!') && firstLine.text.includes('fish')); } /** * function some_fish_func * ... * end * @see isFunctionDefinitionName() */ function isFunctionDefinition(node) { return node.type === 'function_definition'; } /** * checks for all fish types of SyntaxNodes that are commands. */ function isCommand(node) { return [ 'command', 'test_command', 'command_substitution', ].includes(node.type); } /** * essentially avoids having to null check functionDefinition nodes for having a function * name, since * * @param {SyntaxNode} node - the node to check * @returns {boolean} true if the node is the firstNamedChild of a function_definition */ function isFunctionDefinitionName(node) { // function name must have parent which would be `function_definition` if (!node.parent) return false; // function name must be a child of `function_definition` if (!isFunctionDefinition(node.parent)) return false; // `function_definition` must have a firstNamedChild if (!node.parent.firstNamedChild) return false; // function name must be the firstNamedChild of `function_definition` // and must be a `SyntaxNode.type === 'word'` return node.parent.firstNamedChild.equals(node) && node.type === 'word'; } function isTopLevelFunctionDefinition(node) { if (isFunctionDefinition(node)) { return node.parent?.type === 'program'; } if (isFunctionDefinitionName(node)) { return node.parent?.parent?.type === 'program'; } return false; } /** * isVariableDefinitionName() || isFunctionDefinitionName() */ function isDefinition(node) { return isFunctionDefinitionName(node) || isVariableDefinitionName(node); } /** * checks if a node is the firstNamedChild of a command */ function isCommandName(node) { const parent = node.parent || node; const cmdName = parent?.firstNamedChild || node?.firstNamedChild; if (!parent || !cmdName) { return false; } if (!isCommand(parent)) { return false; } return node.type === 'word' && node.equals(cmdName); } /** * the root node of a fish script */ function isProgram(node) { return node.type === 'program' || node.parent === null; } function isError(node = null) { if (node) { return node.type === 'ERROR'; } return false; } function isForLoop(node) { return node.type === 'for_statement'; } function isIfStatement(node) { return node.type === 'if_statement'; } function isElseStatement(node) { return node.type === 'else_clause'; } // strict check for if statement or else clauses function isConditional(node) { return ['if_statement', 'else_if_clause', 'else_clause'].includes(node.type); } function isIfOrElseIfConditional(node) { return ['if_statement', 'else_if_clause'].includes(node.type); } function isPossibleUnreachableStatement(node) { if (isIfStatement(node)) { return node.lastNamedChild?.type === 'else_clause'; } else if (node.type === 'for_statement') { return true; } else if (node.type === 'switch_statement') { return false; } return false; } function isClause(node) { return [ 'case_clause', 'else_clause', 'else_if_clause', ].includes(node.type); } /** * statements contain clauses */ function isStatement(node) { return [ 'for_statement', 'switch_statement', 'while_statement', 'if_statement', 'begin_statement', ].includes(node.type); } /** * since statement SyntaxNodes contains clauses, treats statements and clauses the same: * if ... - if_statement * else if ... --- else_if_clause * else ... --- else_clause * end; */ function isBlock(node) { return isClause(node) || isStatement(node); } function isEnd(node) { return node.type === 'end'; } //export function isLocalBlock(node: SyntaxNode): boolean { //return ['begin_statement'].includes(node.type); //} /** * Any SyntaxNode that will enclose a new local scope: * Program, Function, if, for, while */ function isScope(node) { return isProgram(node) || isFunctionDefinition(node) || isStatement(node); // || isLocalBlock(node)// } function isSemicolon(node) { return node.type === ';' && node.text === ';'; } function isNewline(node) { return node.type === '\n'; } function isBlockBreak(node) { return isEnd(node) || isSemicolon(node) || isNewline(node); } function isString(node) { return [ 'double_quote_string', 'single_quote_string', ].includes(node.type); } function isStringCharacter(node) { return [ "'", '"', ].includes(node.type); } function isEndStdinCharacter(node) { return '--' === node.text && node.type === 'word'; } function isLongOption(node) { return node.text.startsWith('--') && !isEndStdinCharacter(node); } function isShortOption(node) { return node.text.startsWith('-') && !isLongOption(node); } function isOption(node) { return isShortOption(node) || isLongOption(node); } /** careful not to call this on old unix style flags/options */ function isJoinedShortOption(node) { if (isLongOption(node)) return false; return isShortOption(node) && node.text.slice(1).length > 1; } /** careful not to call this on old unix style flags/options */ function hasShortOptionCharacter(node, findChar) { if (isLongOption(node)) return false; return isShortOption(node) && node.text.slice(1).includes(findChar); } /** * @param node - the node to check * @param optionQuery - object of node strings to match * @returns boolean result corresponding to query */ function isMatchingOption(node, optionQuery) { if (!isOption(node)) return false; const nodeText = node.text.includes('=') ? node.text.slice(0, node.text.indexOf('=')) : node.text; if (isLongOption(node) && optionQuery?.longOption === nodeText) return true; if (isShortOption(node) && optionQuery?.oldUnixOption === nodeText) return true; if (!optionQuery.shortOption) return false; return isShortOption(node) && hasShortOptionCharacter(node, optionQuery.shortOption.slice(1)); } function isPipe(node) { return node.type === 'pipe'; } function gatherSiblingsTillEol(node) { const siblings = []; let next = node.nextSibling; while (next && !isNewline(next)) { siblings.push(next); next = next.nextSibling; } return siblings; } /* * Checks for nodes which should stop the search for * command nodes, used in findParentCommand() */ function isBeforeCommand(node) { return [ 'file_redirect', 'redirect', 'redirected_statement', 'conditional_execution', 'stream_redirect', 'pipe', ].includes(node.type) || isFunctionDefinition(node) || isStatement(node) || isSemicolon(node) || isNewline(node) || isEnd(node); } function isVariable(node) { if (isVariableDefinition(node)) { return true; } else { return ['variable_expansion', 'variable_name'].includes(node.type); } } /** * finds the parent command of the current node * * @param {SyntaxNode} node - the node to check for its parent * @returns {SyntaxNode | null} command node or null */ function findPreviousSibling(node) { let currentNode = node; if (!currentNode) { return null; } while (currentNode !== null) { if (isCommand(currentNode)) { return currentNode; } currentNode = currentNode.parent; } return null; } /** * finds the parent command of the current node * * @param {SyntaxNode} node - the node to check for its parent * @returns {SyntaxNode | null} command node or null */ function findParentCommand(node) { let currentNode = node; if (!currentNode) { return null; } while (currentNode !== null) { if (isCommand(currentNode)) { return currentNode; } currentNode = currentNode.parent; } return null; } /** * finds the parent function of the current node * * @param {SyntaxNode} node - the node to check for its parent * @returns {SyntaxNode | null} command node or null */ function findParentFunction(node) { let currentNode = node; if (!currentNode) { return null; } while (currentNode !== null) { if (isFunctionDefinition(currentNode)) { return currentNode; } currentNode = currentNode.parent; } return null; } const definitionKeywords = ['set', 'read', 'function', 'for']; // TODO: check if theres a child node that is a variable definition -> return full command function isVariableDefinitionCommand(node) { if (!isCommand(node)) { return false; } const command = node.firstChild?.text.trim() || ''; if (definitionKeywords.includes(command)) { return true; } // if (isCommand(node) && definitionKeywords.includes(node.firstChild?.text || '')) { // const variableDef = findChildNodes(node, isVariableDefinition) // if (variableDef.length > 0) { // return true; // } // } return false; } function findParentVariableDefinitionKeyword(node) { const currentNode = node; const parent = currentNode?.parent; if (!currentNode || !parent) { return null; } const varKeyword = parent.firstChild?.text.trim() || ''; if (!varKeyword) { return null; } if (definitionKeywords.includes(varKeyword)) { return parent; } return null; } function refinedFindParentVariableDefinitionKeyword(node) { const currentNode = node; const parent = currentNode?.parent; if (!currentNode || !parent) { return null; } const varKeyword = parent.firstChild?.text.trim() || ''; if (!varKeyword) { return null; } if (definitionKeywords.includes(varKeyword)) { return parent.firstChild; } return null; } // @TODO: replace isVariableDefinition with this function isVariableDefinitionName(node) { if (isFunctionDefinition(node) || isCommand(node) || isCommandName(node) || definitionKeywords.includes(node.firstChild?.text || '') || !VariableTypes.isPossible(node)) { return false; } const keyword = refinedFindParentVariableDefinitionKeyword(node); if (!keyword) { return false; } const siblings = VariableTypes.gatherVariableSiblings(keyword); switch (keyword.text) { case 'set': return VariableTypes.isSetDefinitionNode(siblings, node); case 'read': return VariableTypes.isReadDefinitionNode(siblings, node); case 'function': return VariableTypes.isFunctionArgumentDefinitionNode(siblings, node); case 'for': return VariableTypes.isForLoopDefinitionNode(siblings, node); default: return false; } } /** * checks if a node is a variable definition. Current syntax tree from tree-sitter-fish will * only tokenize variable names if they are defined in a for loop. Otherwise, they are tokenized * with the node type of 'name'. Currently does not support argparse. * * @param {SyntaxNode} node - the node to check if it is a variable definition * @returns {boolean} true if the node is a variable definition, false otherwise */ function isVariableDefinition(node) { return isVariableDefinitionName(node); } function findParentForScope(currentNode, switchFound) { switch (switchFound) { case 'local': return (0, tree_sitter_1.firstAncestorMatch)(currentNode, (n) => isStatement(n) || isFunctionDefinition(n) || isProgram(n)); case 'function': return (0, tree_sitter_1.firstAncestorMatch)(currentNode, (n) => isFunctionDefinition(n)); case '': return (0, tree_sitter_1.firstAncestorMatch)(currentNode, (n) => isFunctionDefinition(n) || isProgram(n)); case 'universal': case 'global': case 'export': return (0, tree_sitter_1.firstAncestorMatch)(currentNode, (n) => isProgram(n)); default: return null; } } function findEnclosingVariableScope(currentNode) { if (!isVariableDefinition(currentNode)) { return null; } const parent = findParentVariableDefinitionKeyword(currentNode); const switchFound = findSwitchForVariable(currentNode); //console.log(`switchFound: ${switchFound}`) if (!parent) { return null; } switch (parent.firstChild?.text) { case 'set': return findParentForScope(currentNode, switchFound); // implement firstAncestorMatch for array of functions case 'read': return findParentForScope(currentNode, switchFound); case 'function': return parent; case 'for': return parent; default: return null; } } function findForLoopVariable(node) { for (let i = 0; i < node.children.length; i++) { const child = node.children[i]; if (child?.type === 'variable_name') { return child; } } return null; } /** * @param {SyntaxNode} node - finds the node in a fish command that will * contain the variable definition * * @return {SyntaxNode | null} variable node that was found **/ function findSetDefinedVariable(node) { const parent = findParentCommand(node); if (!parent) { return null; } const children = parent.children; let i = 1; let child = children[i]; while (child !== undefined) { if (!child.text.startsWith('-')) { return child; } if (i === children.length - 1) { return null; } child = children[i++]; } return child; } //// for function variables function _isArgFlags(node) { return node.type === 'word' ? node.text === '--argument-names' || node.text === '-a' : false; } exports.VariableScopeFlags = { '-g': 'global', '--global': 'global', '-l': 'local', '--local': 'local', '-U': 'universal', '--universal': 'universal', '-x': 'export', '-gx': 'global', '--export': 'export', '-u': 'unexport', '--unexport': 'unexport', }; //// for read variables function findLastFlag(nodes) { let maxIdx = 0; for (let i = 0; i < nodes.length; i++) { const child = nodes[i]; if (child?.text.startsWith('-')) { maxIdx = Math.max(i, maxIdx); } } return maxIdx; } function findSwitchForVariable(node) { let current = node; while (current !== null) { if (exports.VariableScopeFlags[current.text] !== undefined) { return exports.VariableScopeFlags[current.text] || ''; } else if (current.text.startsWith('-')) { return ''; } current = current.previousSibling; } return 'function'; } function findReadVariables(node) { const variables = []; const lastFlag = findLastFlag(node.children); variables.push(...node.children.slice(lastFlag + 1).filter(n => n.type === 'word')); const possibleFlags = node.children.slice(0, lastFlag + 1); for (let i = 0; i < possibleFlags.length; i++) { const child = possibleFlags[i]; if (exports.VariableScopeFlags[child?.text || ''] !== undefined) { i++; while (i < possibleFlags.length && possibleFlags[i]?.type === 'word') { if (possibleFlags[i]?.text.startsWith('-')) { break; } else { variables.unshift(possibleFlags[i]); } i++; } } } return variables; } function hasParent(node, callbackfn) { let currentNode = node; while (currentNode !== null) { if (callbackfn(currentNode)) { return true; } currentNode = currentNode.parent; } return false; } function findParent(node, callbackfn) { let currentNode = node; while (currentNode !== null) { if (callbackfn(currentNode)) { return currentNode; } currentNode = currentNode.parent; } return null; } function hasParentFunction(node) { let currentNode = node; while (currentNode !== null) { if (isFunctionDefinition(currentNode) || currentNode.type === 'function') { return true; } if (currentNode.parent === null) { return false; } currentNode = currentNode?.parent; } return false; } function findFunctionScope(node) { while (node.parent !== null) { if (isFunctionDefinition(node)) { return node; } node = node.parent; } return node; } // node1 encloses node2 function scopeCheck(node1, node2) { const scope1 = findFunctionScope(node1); const scope2 = findFunctionScope(node2); if (isProgram(scope1)) { return true; } return scope1 === scope2; } function isLocalVariable(node) { const _parents = (0, tree_sitter_1.getParentNodes)(node); //if (pCmd.child(0)?.text === 'read' || pCmd.child(0)?.text === 'set') { // console.log(pCmd.text) //} } function wordNodeIsCommand(node) { if (node.type !== 'word') { return false; } return node.parent ? isCommand(node.parent) && node.parent.firstChild?.text === node.text : false; } function isSwitchStatement(node) { return node.type === 'switch_statement'; } function isCaseClause(node) { return node.type === 'case_clause'; } function isReturn(node) { return node.type === 'return' && node.firstChild?.text === 'return'; //return node.type === 'return' } function isConditionalCommand(node) { return node.type === 'conditional_execution'; } // @TODO: see ./tree-sitter.ts -> getRangeWithPrecedingComments(), // for implementation of chained returns of conditional_executions function chainedCommandGroup() { return []; } /* * echo $hello_world * ^--- variable_name * fd --type f * ^------- word * ^--- word */ function isCommandFlag(node) { return [ 'test_option', 'word', 'escape_sequence', ].includes(node.type) || node.text.startsWith('-') || findParentCommand(node) !== null; } function isRegexArgument(n) { return n.text === '--regex' || n.text === '-r'; } function isUnmatchedStringCharacter(node) { if (!isStringCharacter(node)) { return false; } if (node.parent && isString(node.parent)) { return false; } return true; } function isPartialForLoop(node) { const semiCompleteForLoop = ['for', 'i', 'in', '_']; const errorNode = node.parent; if (node.text === 'for' && node.type === 'for') { if (!errorNode) { return true; } if ((0, tree_sitter_1.getLeafs)(errorNode).length < semiCompleteForLoop.length) { return true; } return false; } if (!errorNode) { return false; } return (errorNode.hasError && errorNode.text.startsWith('for') && !errorNode.text.includes(' in ')); } function isInlineComment(node) { if (!isComment(node)) return false; const previousSibling = node.previousNamedSibling; if (!previousSibling) return false; return previousSibling?.startPosition.row === node.startPosition.row && previousSibling?.type !== 'comment'; } function isCommandWithName(node, ...commandNames) { if (node.type !== 'command') return false; // const currentCommandName = node.firstChild?.text return !!node.firstChild && commandNames.includes(node.firstChild.text); } // // TODO: either move use or remove // /** // * checks for SyntaxNode.text === '-f1' | '--fields=1' // * but not SyntaxNode.text !== '-1' | '-m1f1' | '--fields-1' // */ // export function isOptionWithValue(node: SyntaxNode) { // if (!isOption(node)) return false // // must be option // // if (isShortOption(node)) { // const lastChar = node.text.charAt(2) || '' // return Number.isInteger(Number.parseInt(lastChar)); // } else if (isLongOption(node)) { // return node.text.includes('=') // } // return false // } //