fish-lsp
Version:
LSP implementation for fish/fish-shell
562 lines (561 loc) • 19.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.getChildNodes = getChildNodes;
exports.getNamedChildNodes = getNamedChildNodes;
exports.findChildNodes = findChildNodes;
exports.getParentNodes = getParentNodes;
exports.findFirstParent = findFirstParent;
exports.getSiblingNodes = getSiblingNodes;
exports.findFirstNamedSibling = findFirstNamedSibling;
exports.findFirstSibling = findFirstSibling;
exports.findEnclosingScope = findEnclosingScope;
exports.getNodeText = getNodeText;
exports.getNodesTextAsSingleLine = getNodesTextAsSingleLine;
exports.firstAncestorMatch = firstAncestorMatch;
exports.ancestorMatch = ancestorMatch;
exports.descendantMatch = descendantMatch;
exports.hasNode = hasNode;
exports.getNamedNeighbors = getNamedNeighbors;
exports.getRange = getRange;
exports.findNodeAt = findNodeAt;
exports.equalRanges = equalRanges;
exports.containsRange = containsRange;
exports.getNodeAt = getNodeAt;
exports.getNodeAtRange = getNodeAtRange;
exports.positionToPoint = positionToPoint;
exports.pointToPosition = pointToPosition;
exports.rangeToPoint = rangeToPoint;
exports.getRangeWithPrecedingComments = getRangeWithPrecedingComments;
exports.getPrecedingComments = getPrecedingComments;
exports.isFishExtension = isFishExtension;
exports.isPositionWithinRange = isPositionWithinRange;
exports.isPositionAfter = isPositionAfter;
exports.isNodeWithinRange = isNodeWithinRange;
exports.nodesGen = nodesGen;
exports.getLeafs = getLeafs;
exports.getLastLeaf = getLastLeaf;
exports.matchesArgument = matchesArgument;
exports.getCommandArgumentValue = getCommandArgumentValue;
exports.getNodeAtPosition = getNodeAtPosition;
//import { existsSync } from 'fs'
const path_1 = require("path");
//import { pathToFileURL, URL } from 'url'
const vscode_languageserver_1 = require("vscode-languageserver");
// import { pathToFileURL } from 'url'; // typescript-language-server -> https://github.com/typescript-language-server/typescript-language-server/blob/master/src/document.ts
// import vscodeUri from 'vscode-uri'; // typescript-language-server -> https://github.com/typescript-language-server/typescript-language-server/blob/master/src/document.ts
// import { existsSync } from 'fs-extra';
const node_types_1 = require("./node-types");
/**
* Returns an array for all the nodes in the tree (@see also nodesGen)
*
* @param {SyntaxNode} root - the root node to search from
* @returns {SyntaxNode[]} all children of the root node (flattened)
*/
function getChildNodes(root) {
const queue = [root];
const result = [];
while (queue.length) {
const current = queue.shift();
if (current) {
result.push(current);
}
if (current && current.children) {
queue.unshift(...current.children);
}
}
return result;
}
function getNamedChildNodes(root) {
const queue = [root];
const result = [];
while (queue.length) {
const current = queue.shift();
if (current && current.isNamed) {
result.push(current);
}
if (current && current.children) {
queue.unshift(...current.children);
}
}
return result;
}
function findChildNodes(root, predicate) {
const queue = [root];
const result = [];
while (queue.length) {
const current = queue.shift();
if (current && predicate(current)) {
result.push(current);
}
if (current && current.children) {
queue.unshift(...current.children);
}
}
return result;
}
/**
* Gets path to root starting where index 0 is child node passed in.
* Format: [child, child.parent, ..., root]
*
* @param {SyntaxNode} child - the lowest child of root
* @returns {SyntaxNode[]} an array of ancestors to the descendent node passed in.
*/
function getParentNodes(child) {
const result = [];
let current = child;
while (current !== null) {
// result.unshift(current); // unshift would be used for [root, ..., child]
if (current) {
result.push(current);
}
current = current?.parent || null;
}
return result;
}
function findFirstParent(node, predicate) {
let current = node.parent;
while (current !== null) {
if (predicate(current)) {
return current;
}
current = current.parent;
}
return null;
}
//const getSiblingFunc = (n: SyntaxNode, direction: 'before' | 'after') => {
//if (direction === 'before') return n.nextNamedSibling
//if (direction === 'after') return n.previousNamedSibling
//return null
//}
/**
* collects all siblings either before or after the current node.
*
* @param {SyntaxNode} node - the node to start from
* @param {'forward' | 'backward'} [lookForward] - if 'backward' (DEFAULT), looks nodes after the current node.
* otherwise if specified false, looks for nodes before the current node.
* @returns {SyntaxNode[]} - an array of either previous siblings or next siblings.
*/
function getSiblingNodes(node, predicate, direction = 'before') {
const siblingFunc = (n) => direction === 'before' ? n.previousNamedSibling : n.nextNamedSibling;
let current = node;
const result = [];
while (current) {
current = siblingFunc(current);
if (current && predicate(current)) {
result.push(current);
}
}
return result;
}
/**
* Similar to getSiblingNodes. Only returns first node matching the predicate
*/
function findFirstNamedSibling(node, predicate, direction = 'before') {
const siblingFunc = (n) => direction === 'before' ? n.previousNamedSibling : n.nextNamedSibling;
let current = node;
while (current) {
current = siblingFunc(current);
if (current && predicate(current)) {
return current;
}
}
return null;
}
function findFirstSibling(node, predicate, direction = 'before') {
const siblingFunc = (n) => direction === 'before' ? n.previousSibling : n.nextSibling;
let current = node;
while (current) {
// console.log('curr: ', current.text);
current = siblingFunc(current);
if (current && predicate(current)) {
return current;
}
}
return null;
}
function findEnclosingScope(node) {
let parent = node.parent || node;
if ((0, node_types_1.isFunctionDefinitionName)(node)) {
return findFirstParent(parent, n => (0, node_types_1.isFunctionDefinition)(n) || (0, node_types_1.isProgram)(n)) || parent;
}
else if (node.text === 'argv') {
parent = findFirstParent(node, n => (0, node_types_1.isFunctionDefinition)(n) || (0, node_types_1.isProgram)(n)) || parent;
return (0, node_types_1.isFunctionDefinition)(parent) ? parent.firstNamedChild || parent : parent;
}
else if ((0, node_types_1.isVariable)(node)) {
parent = findFirstParent(node, n => (0, node_types_1.isScope)(n)) || parent;
return (0, node_types_1.isForLoop)(parent) && (0, node_types_1.findForLoopVariable)(parent)?.text === node.text
? parent
: findFirstParent(node, n => (0, node_types_1.isProgram)(n) || (0, node_types_1.isFunctionDefinitionName)(n))
|| parent;
}
else if ((0, node_types_1.isCommandName)(node)) {
return findFirstParent(node, n => (0, node_types_1.isProgram)(n)) || parent;
}
else {
return findFirstParent(node, n => (0, node_types_1.isScope)(n)) || parent;
}
}
// some nodes (such as commands) to get their text, you will need
// the first named child.
// other nodes (such as flags) need just the actual text.
function getNodeText(node) {
if (!node) {
return '';
}
if ((0, node_types_1.isFunctionDefinition)(node)) {
return node.child(1)?.text || '';
}
if ((0, node_types_1.isVariableDefinition)(node)) {
const defVar = (0, node_types_1.findSetDefinedVariable)(node);
return defVar.text || '';
}
return node.text !== null ? node.text.trim() : '';
}
function getNodesTextAsSingleLine(nodes) {
let text = '';
for (const node of nodes) {
text += ' ' + node.text.split('\n').map(n => n.split(' ').map(n => n.trim()).join(' ')).map(n => n.trim()).join(';');
if (!text.endsWith(';')) {
text += ';';
}
}
return text.replaceAll(/;+/g, ';').trim();
}
function firstAncestorMatch(start, predicate) {
const ancestors = getParentNodes(start) || [];
const root = ancestors[ancestors.length - 1];
//if (ancestors.length < 1) return root;
for (const p of ancestors) {
if (!predicate(p)) {
continue;
}
return p;
}
return !!root && predicate(root) ? root : null;
}
/**
* finds all ancestors (parent nodes) of a node that match a predicate
*
* @param {SyntaxNode} start - the leaf/deepest child node to start searching from
* @param {(n: SyntaxNode) => boolean} predicate - a function that returns true if the node matches
* @param {boolean} [inclusive] - if true, the start node can be included in the results
* @returns {SyntaxNode[]} - an array of nodes that match the predicate
*/
function ancestorMatch(start, predicate, inclusive = true) {
const ancestors = getParentNodes(start) || [];
const searchNodes = [];
for (const p of ancestors) {
searchNodes.push(...getChildNodes(p));
}
const results = searchNodes.filter(neighbor => predicate(neighbor));
return inclusive ? results : results.filter(ancestor => ancestor !== start);
}
/**
* searches for all children nodes that match the predicate passed in
*
* @param {SyntaxNode} start - the root node to search from
* @param {(n: SyntaxNode) => boolean} predicate - a function that returns a bollean
* incating whether the node passed in matches the search criteria
* @param {boolean} inclusive: boolean = true,
* @returns {SyntaxNode[]} - all child nodes that match the predicate
*/
function descendantMatch(start, predicate, inclusive = true) {
const descendants = [];
descendants.push(...getChildNodes(start));
const results = descendants.filter(descendant => predicate(descendant));
return inclusive ? results : results.filter(r => r !== start);
}
function hasNode(allNodes, matchNode) {
for (const node of allNodes) {
if (node.equals(matchNode)) {
return true;
}
}
return false;
}
function getNamedNeighbors(node) {
return node.parent?.namedChildren || [];
}
/**
* uses nodesGen to build an array.
*
* @param {SyntaxNode} node - the root node of a document (where to begin search)
* @returns {SyntaxNode[]} - all nodes seen in the document.
*/
//function getChildrenArray(node: SyntaxNode): SyntaxNode[] {
// let root = nodesGen(node);
// const result: SyntaxNode[] = [];
//
// var currNode = root.next();
// while (!currNode.done) {
// if (currNode.value) {
// result.push(currNode.value)
// }
// currNode = root.next()
// }
// return result
//}
//
//function _findNodes(root: SyntaxNode): SyntaxNode[] {
// let queue: SyntaxNode[] = [root]
// let result: SyntaxNode[] = []
//
// while (queue.length) {
// let current : SyntaxNode | undefined = queue.pop();
// if (current && current.namedChildCount > 0) {
// result.push(current)
// queue.unshift(...current.namedChildren.filter(child => child))
// } else if (current && current.childCount > 0){
// result.push(current)
// queue.unshift(...current.children)
// } else {
// continue
// }
// }
// return result
//}
function getRange(node) {
return vscode_languageserver_1.Range.create(node.startPosition.row, node.startPosition.column, node.endPosition.row, node.endPosition.column);
}
/**
* findNodeAt() - handles moving backwards if the cursor is not currently on a node (safer version of getNodeAt)
*/
function findNodeAt(tree, line, column) {
if (!tree.rootNode) {
return null;
}
let currentCol = column;
const currentLine = line;
while (currentLine > 0) {
const currentNode = tree.rootNode.descendantForPosition({ row: currentLine, column: currentCol });
if (currentNode) {
return currentNode;
}
currentCol--;
}
return tree.rootNode.descendantForPosition({ row: line, column });
}
function equalRanges(a, b) {
return (a.start.line === b.start.line &&
a.start.character === b.start.character &&
a.end.line === b.end.line &&
a.end.character === b.end.character);
}
function containsRange(a, b) {
return (a.start.line <= b.start.line &&
a.start.character <= b.start.character &&
a.end.line >= b.end.line &&
a.end.character >= b.end.character);
}
/**
* getNodeAt() - handles moving backwards if the cursor i
*/
function getNodeAt(tree, line, column) {
if (!tree.rootNode) {
return null;
}
return tree.rootNode.descendantForPosition({ row: line, column });
}
function getNodeAtRange(root, range) {
return root.descendantForPosition(positionToPoint(range.start), positionToPoint(range.end));
}
// export function getDependencyUrl(node: SyntaxNode, baseUri: string): URL {
// let filename = node.children[1]?.text.replaceAll('"', '')!
//
//
// if (!!filename && !filename.endsWith('.fish')) {
// filename += '.fish'
// }
//
// const paths = process.env.PATH?.split(':') || []
//
// for (const p of paths) {
// const url = pathToFileURL(join(p, filename))
//
// // if (existsSync(url)) return new URL(url).toString()
// }
//
// return new URL(filename, baseUri)
// }
function positionToPoint(pos) {
return {
row: pos.line,
column: pos.character,
};
}
function pointToPosition(point) {
return {
line: point.row,
character: point.column,
};
}
function rangeToPoint(range) {
return {
row: range.start.line,
column: range.start.character,
};
}
function getRangeWithPrecedingComments(node) {
let currentNode = node.previousNamedSibling;
let previousNode = node;
while (currentNode?.type === 'comment') {
previousNode = currentNode;
currentNode = currentNode.previousNamedSibling;
}
return vscode_languageserver_1.Range.create(pointToPosition(previousNode.startPosition), pointToPosition(node.endPosition));
}
function getPrecedingComments(node) {
if (!node) {
return '';
}
const comments = commentsHelper(node);
if (!comments) {
return node.text;
}
return [
commentsHelper(node),
node.text,
].join('\n');
}
function commentsHelper(node) {
if (!node) {
return '';
}
const comment = [];
let currentNode = node.previousNamedSibling;
while (currentNode?.type === 'comment') {
//comment.unshift(currentNode.text.replaceAll(/#+\s?/g, ''))
comment.unshift(currentNode.text);
currentNode = currentNode.previousNamedSibling;
}
return comment.join('\n');
}
function isFishExtension(path) {
const ext = (0, path_1.extname)(path).toLowerCase();
return ext === '.fish';
}
function isPositionWithinRange(position, range) {
const doesStartInside = position.line > range.start.line ||
position.line === range.start.line && position.character >= range.start.character;
const doesEndInside = position.line < range.end.line ||
position.line === range.end.line && position.character <= range.end.character;
return doesStartInside && doesEndInside;
}
function isPositionAfter(first, second) {
return (first.line < second.line ||
first.line === second.line && first.character < second.character);
}
function isNodeWithinRange(node, range) {
const doesStartInside = node.startPosition.row > range.start.line ||
node.startPosition.row === range.start.line &&
node.startPosition.column >= range.start.character;
const doesEndInside = node.endPosition.row < range.end.line ||
node.endPosition.row === range.end.line &&
node.endPosition.column <= range.end.character;
return doesStartInside && doesEndInside;
}
function* nodesGen(node) {
const queue = [node];
while (queue.length) {
const n = queue.shift();
if (!n) {
return;
}
if (n.children.length) {
queue.unshift(...n.children);
}
yield n;
}
}
function getLeafs(node) {
function gatherLeafs(node, leafs = []) {
if (node.childCount === 0 && node.text !== '') {
leafs.push(node);
return leafs;
}
for (const child of node.children) {
leafs = gatherLeafs(child, leafs);
}
return leafs;
}
return gatherLeafs(node);
}
function getLastLeaf(node, maxIndex = Infinity) {
const allLeafs = getLeafs(node).filter(leaf => leaf.startPosition.column < maxIndex);
return allLeafs[allLeafs.length - 1];
}
function matchesArgument(node, argName) {
const splitNode = node.text.slice(0, node.text.lastIndexOf('='));
if (argName.startsWith('-') && !argName.startsWith('--')) {
return splitNode.startsWith('-') && splitNode.includes(argName.slice(1));
}
if (argName.startsWith('--')) {
return splitNode.startsWith('--') && splitNode.startsWith(argName.slice(2));
}
return splitNode === argName;
}
/**
* @param command - the command node to search it's children, accepts both command and command name nodes
* @param argName - the name of the argument to search for
* @returns the value of the argument if found, otherwise null
*/
function getCommandArgumentValue(command, argName) {
function getCommand(node) {
if (node.type === 'name' && node.parent) {
return node.parent;
}
return node;
}
const arg = getCommand(command).children.find(child => matchesArgument(child, argName));
if (!arg) {
return null;
}
const value = arg.text.includes('=')
? arg
: arg.nextSibling;
return value;
}
// Check out awk-language-server:
// • https://github.com/Beaglefoot/awk-language-server/tree/master/server/src/utils.ts
// • https://github.com/bash-lsp/bash-language-server/blob/main/server/src/util/tree-sitter.ts
//
//export function getQueriesList(queriesRawText: string): string[] {
// const result: string[] = []
//
// let openParenCount = 0
// let openBracketCount = 0
// let isQuoteCharMet = false
// let isComment = false
// let currentQuery = ''
//
// for (const char of queriesRawText) {
// if (char === '"') isQuoteCharMet = !isQuoteCharMet
// if (isQuoteCharMet) {
// currentQuery += char
// continue
// } else if (!isQuoteCharMet && char === ';') isComment = true
// else if (isComment && char !== '\n') continue
// else if (char === '(') openParenCount++
// else if (char === ')') openParenCount--
// else if (char === '[') openBracketCount++
// else if (char === ']') openBracketCount--
// else if (char === '\n') {
// isComment = false
//
// if (!openParenCount && !openBracketCount && currentQuery) {
// result.push(currentQuery.trim())
// currentQuery = ''
// }
//
// continue
// }
//
// if (!isComment) currentQuery += char
// }
//
// return result
//}
function getNodeAtPosition(tree, position) {
return tree.rootNode.descendantForPosition({ row: position.line, column: position.character });
}