UNPKG

fish-lsp

Version:

LSP implementation for fish/fish-shell

792 lines (706 loc) 22.2 kB
import { SyntaxNode } from 'web-tree-sitter'; import { firstAncestorMatch, getParentNodes, getLeafs } from './tree-sitter'; import * as VariableTypes from './variable-syntax-nodes'; /** * fish shell comment: '# ...' */ export function isComment(node: SyntaxNode): boolean { return node.type === 'comment' && !isShebang(node); } export function isShebang(node: SyntaxNode) { 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() */ export function isFunctionDefinition(node: SyntaxNode): boolean { return node.type === 'function_definition'; } /** * checks for all fish types of SyntaxNodes that are commands. */ export function isCommand(node: SyntaxNode): boolean { 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 */ export function isFunctionDefinitionName(node: SyntaxNode): boolean { // 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'; } export function isTopLevelFunctionDefinition(node: SyntaxNode): boolean { if (isFunctionDefinition(node)) { return node.parent?.type === 'program'; } if (isFunctionDefinitionName(node)) { return node.parent?.parent?.type === 'program'; } return false; } /** * isVariableDefinitionName() || isFunctionDefinitionName() */ export function isDefinition(node: SyntaxNode): boolean { return isFunctionDefinitionName(node) || isVariableDefinitionName(node); } /** * checks if a node is the firstNamedChild of a command */ export function isCommandName(node: SyntaxNode): boolean { 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 */ export function isProgram(node: SyntaxNode): boolean { return node.type === 'program' || node.parent === null; } export function isError(node: SyntaxNode | null = null): boolean { if (node) { return node.type === 'ERROR'; } return false; } export function isForLoop(node: SyntaxNode): boolean { return node.type === 'for_statement'; } export function isIfStatement(node: SyntaxNode): boolean { return node.type === 'if_statement'; } export function isElseStatement(node: SyntaxNode): boolean { return node.type === 'else_clause'; } // strict check for if statement or else clauses export function isConditional(node: SyntaxNode): boolean { return ['if_statement', 'else_if_clause', 'else_clause'].includes(node.type); } export function isIfOrElseIfConditional(node: SyntaxNode): boolean { return ['if_statement', 'else_if_clause'].includes(node.type); } export function isPossibleUnreachableStatement(node: SyntaxNode): boolean { 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; } export function isClause(node: SyntaxNode): boolean { return [ 'case_clause', 'else_clause', 'else_if_clause', ].includes(node.type); } /** * statements contain clauses */ export function isStatement(node: SyntaxNode): boolean { 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; */ export function isBlock(node: SyntaxNode): boolean { return isClause(node) || isStatement(node); } export function isEnd(node: SyntaxNode): boolean { 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 */ export function isScope(node: SyntaxNode): boolean { return isProgram(node) || isFunctionDefinition(node) || isStatement(node); // || isLocalBlock(node)// } export function isSemicolon(node: SyntaxNode): boolean { return node.type === ';' && node.text === ';'; } export function isNewline(node: SyntaxNode): boolean { return node.type === '\n'; } export function isBlockBreak(node: SyntaxNode): boolean { return isEnd(node) || isSemicolon(node) || isNewline(node); } export function isString(node: SyntaxNode) { return [ 'double_quote_string', 'single_quote_string', ].includes(node.type); } export function isStringCharacter(node: SyntaxNode) { return [ "'", '"', ].includes(node.type); } export function isEndStdinCharacter(node: SyntaxNode) { return '--' === node.text && node.type === 'word'; } export function isLongOption(node: SyntaxNode): boolean { return node.text.startsWith('--') && !isEndStdinCharacter(node); } export function isShortOption(node: SyntaxNode): boolean { return node.text.startsWith('-') && !isLongOption(node); } export function isOption(node: SyntaxNode): boolean { return isShortOption(node) || isLongOption(node); } /** careful not to call this on old unix style flags/options */ export function isJoinedShortOption(node: SyntaxNode) { 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 */ export function hasShortOptionCharacter(node: SyntaxNode, findChar: string) { if (isLongOption(node)) return false; return isShortOption(node) && node.text.slice(1).includes(findChar); } export type NodeOptionQueryText = { shortOption?: `-${string}`; oldUnixOption?: `-${string}`; longOption?: `--${string}`; }; /** * @param node - the node to check * @param optionQuery - object of node strings to match * @returns boolean result corresponding to query */ export function isMatchingOption(node: SyntaxNode, optionQuery: NodeOptionQueryText): boolean { 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)); } export function isPipe(node: SyntaxNode): boolean { return node.type === 'pipe'; } export function gatherSiblingsTillEol(node: SyntaxNode): SyntaxNode[] { 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() */ export function isBeforeCommand(node: SyntaxNode) { return [ 'file_redirect', 'redirect', 'redirected_statement', 'conditional_execution', 'stream_redirect', 'pipe', ].includes(node.type) || isFunctionDefinition(node) || isStatement(node) || isSemicolon(node) || isNewline(node) || isEnd(node); } export function isVariable(node: SyntaxNode) { 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 */ export function findPreviousSibling(node?: SyntaxNode): SyntaxNode | null { let currentNode: SyntaxNode | null | undefined = 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 */ export function findParentCommand(node?: SyntaxNode): SyntaxNode | null { let currentNode: SyntaxNode | null | undefined = 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 */ export function findParentFunction(node?: SyntaxNode): SyntaxNode | null { let currentNode: SyntaxNode | null | undefined = 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 export function isVariableDefinitionCommand(node: SyntaxNode): boolean { 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; } export function findParentVariableDefinitionKeyword(node?: SyntaxNode): SyntaxNode | null { const currentNode: SyntaxNode | null | undefined = 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; } export function refinedFindParentVariableDefinitionKeyword(node?: SyntaxNode): SyntaxNode | null { const currentNode: SyntaxNode | null | undefined = 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 export function isVariableDefinitionName(node: SyntaxNode): boolean { 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 */ export function isVariableDefinition(node: SyntaxNode): boolean { return isVariableDefinitionName(node); } function findParentForScope(currentNode: SyntaxNode, switchFound: VariableScope | ''): SyntaxNode | null { switch (switchFound) { case 'local': return firstAncestorMatch(currentNode, (n) => isStatement(n) || isFunctionDefinition(n) || isProgram(n)); case 'function': return firstAncestorMatch(currentNode, (n) => isFunctionDefinition(n)); case '': return firstAncestorMatch(currentNode, (n) => isFunctionDefinition(n) || isProgram(n)); case 'universal': case 'global': case 'export': return firstAncestorMatch(currentNode, (n) => isProgram(n)); default: return null; } } export function findEnclosingVariableScope(currentNode: SyntaxNode): SyntaxNode | null { 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; } } export function findForLoopVariable(node: SyntaxNode): SyntaxNode | null { 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 **/ export function findSetDefinedVariable(node: SyntaxNode): SyntaxNode | null { const parent = findParentCommand(node); if (!parent) { return null; } const children: SyntaxNode[] = parent.children; let i = 1; let child: SyntaxNode = 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: SyntaxNode) { return node.type === 'word' ? node.text === '--argument-names' || node.text === '-a' : false; } export type VariableScope = 'global' | 'local' | 'universal' | 'export' | 'unexport' | 'function'; export const VariableScopeFlags: { [flag: string]: VariableScope; } = { '-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: SyntaxNode[]) { 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: SyntaxNode): VariableScope | '' { let current: SyntaxNode | null = node; while (current !== null) { if (VariableScopeFlags[current.text] !== undefined) { return VariableScopeFlags[current.text] || ''; } else if (current.text.startsWith('-')) { return ''; } current = current.previousSibling; } return 'function'; } export function findReadVariables(node: SyntaxNode) { const variables: SyntaxNode[] = []; 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 (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; } export function hasParent(node: SyntaxNode, callbackfn: (n: SyntaxNode) => boolean) { let currentNode: SyntaxNode = node; while (currentNode !== null) { if (callbackfn(currentNode)) { return true; } currentNode = currentNode.parent!; } return false; } export function findParent(node: SyntaxNode, callbackfn: (n: SyntaxNode) => boolean) { let currentNode: SyntaxNode = node; while (currentNode !== null) { if (callbackfn(currentNode)) { return currentNode; } currentNode = currentNode.parent!; } return null; } export function hasParentFunction(node: SyntaxNode) { let currentNode: SyntaxNode = node; while (currentNode !== null) { if (isFunctionDefinition(currentNode) || currentNode.type === 'function') { return true; } if (currentNode.parent === null) { return false; } currentNode = currentNode?.parent; } return false; } export function findFunctionScope(node: SyntaxNode) { while (node.parent !== null) { if (isFunctionDefinition(node)) { return node; } node = node.parent; } return node; } // node1 encloses node2 export function scopeCheck(node1: SyntaxNode, node2: SyntaxNode): boolean { const scope1 = findFunctionScope(node1); const scope2 = findFunctionScope(node2); if (isProgram(scope1)) { return true; } return scope1 === scope2; } export function isLocalVariable(node: SyntaxNode) { const _parents = getParentNodes(node); //if (pCmd.child(0)?.text === 'read' || pCmd.child(0)?.text === 'set') { // console.log(pCmd.text) //} } export function wordNodeIsCommand(node: SyntaxNode) { if (node.type !== 'word') { return false; } return node.parent ? isCommand(node.parent) && node.parent.firstChild?.text === node.text : false; } export function isSwitchStatement(node: SyntaxNode) { return node.type === 'switch_statement'; } export function isCaseClause(node: SyntaxNode) { return node.type === 'case_clause'; } export function isReturn(node: SyntaxNode) { return node.type === 'return' && node.firstChild?.text === 'return'; //return node.type === 'return' } export function isConditionalCommand(node: SyntaxNode) { return node.type === 'conditional_execution'; } // @TODO: see ./tree-sitter.ts -> getRangeWithPrecedingComments(), // for implementation of chained returns of conditional_executions export function chainedCommandGroup(): SyntaxNode[] { return []; } /* * echo $hello_world * ^--- variable_name * fd --type f * ^------- word * ^--- word */ export function isCommandFlag(node: SyntaxNode) { return [ 'test_option', 'word', 'escape_sequence', ].includes(node.type) || node.text.startsWith('-') || findParentCommand(node) !== null; } export function isRegexArgument(n: SyntaxNode): boolean { return n.text === '--regex' || n.text === '-r'; } export function isUnmatchedStringCharacter(node: SyntaxNode) { if (!isStringCharacter(node)) { return false; } if (node.parent && isString(node.parent)) { return false; } return true; } export function isPartialForLoop(node: SyntaxNode) { const semiCompleteForLoop = ['for', 'i', 'in', '_']; const errorNode = node.parent; if (node.text === 'for' && node.type === 'for') { if (!errorNode) { return true; } if (getLeafs(errorNode).length < semiCompleteForLoop.length) { return true; } return false; } if (!errorNode) { return false; } return ( errorNode.hasError && errorNode.text.startsWith('for') && !errorNode.text.includes(' in ') ); } export function isInlineComment(node: SyntaxNode) { if (!isComment(node)) return false; const previousSibling: SyntaxNode | undefined | null = node.previousNamedSibling; if (!previousSibling) return false; return previousSibling?.startPosition.row === node.startPosition.row && previousSibling?.type !== 'comment'; } export function isCommandWithName(node: SyntaxNode, ...commandNames: string[]) { 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 // } //