fish-lsp
Version:
LSP implementation for fish/fish-shell
792 lines (706 loc) • 22.2 kB
text/typescript
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
// }
//