fish-lsp
Version:
LSP implementation for fish/fish-shell
805 lines (804 loc) • 25.2 kB
JavaScript
"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
// }
//