fish-lsp
Version:
LSP implementation for fish/fish-shell
621 lines (566 loc) • 19.2 kB
text/typescript
//import { existsSync } from 'fs'
import { extname } from 'path';
//import { pathToFileURL, URL } from 'url'
import { Position, Range, URI } from 'vscode-languageserver';
import { Point, SyntaxNode, Tree } from 'web-tree-sitter';
// 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';
import { findSetDefinedVariable, isFunctionDefinition, isVariableDefinition, isFunctionDefinitionName, isVariable, isScope, isProgram, isCommandName, isForLoop, findForLoopVariable } from './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)
*/
export function getChildNodes(root: SyntaxNode): SyntaxNode[] {
const queue: SyntaxNode[] = [root];
const result: SyntaxNode[] = [];
while (queue.length) {
const current : SyntaxNode | undefined = queue.shift();
if (current) {
result.push(current);
}
if (current && current.children) {
queue.unshift(...current.children);
}
}
return result;
}
export function getNamedChildNodes(root: SyntaxNode): SyntaxNode[] {
const queue: SyntaxNode[] = [root];
const result: SyntaxNode[] = [];
while (queue.length) {
const current : SyntaxNode | undefined = queue.shift();
if (current && current.isNamed) {
result.push(current);
}
if (current && current.children) {
queue.unshift(...current.children);
}
}
return result;
}
export function findChildNodes(root: SyntaxNode, predicate: (node: SyntaxNode) => boolean): SyntaxNode[] {
const queue: SyntaxNode[] = [root];
const result: SyntaxNode[] = [];
while (queue.length) {
const current : SyntaxNode | undefined = 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.
*/
export function getParentNodes(child: SyntaxNode): SyntaxNode[] {
const result: SyntaxNode[] = [];
let current: null | SyntaxNode = 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;
}
export function findFirstParent(node: SyntaxNode, predicate: (node: SyntaxNode) => boolean) : SyntaxNode | null {
let current: SyntaxNode | null = 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.
*/
export function getSiblingNodes(
node: SyntaxNode,
predicate : (n: SyntaxNode) => boolean,
direction: 'before' | 'after' = 'before',
): SyntaxNode[] {
const siblingFunc = (n: SyntaxNode) =>
direction === 'before' ? n.previousNamedSibling : n.nextNamedSibling;
let current: SyntaxNode | null = node;
const result: SyntaxNode[] = [];
while (current) {
current = siblingFunc(current);
if (current && predicate(current)) {
result.push(current);
}
}
return result;
}
/**
* Similar to getSiblingNodes. Only returns first node matching the predicate
*/
export function findFirstNamedSibling(
node: SyntaxNode,
predicate: (n: SyntaxNode) => boolean,
direction: 'before' | 'after' = 'before',
): SyntaxNode | null {
const siblingFunc = (n: SyntaxNode) =>
direction === 'before' ? n.previousNamedSibling : n.nextNamedSibling;
let current: SyntaxNode | null = node;
while (current) {
current = siblingFunc(current);
if (current && predicate(current)) {
return current;
}
}
return null;
}
export function findFirstSibling(
node: SyntaxNode,
predicate: (n: SyntaxNode) => boolean,
direction: 'before' | 'after' = 'before',
): SyntaxNode | null {
const siblingFunc = (n: SyntaxNode) =>
direction === 'before' ? n.previousSibling : n.nextSibling;
let current: SyntaxNode | null = node;
while (current) {
// console.log('curr: ', current.text);
current = siblingFunc(current);
if (current && predicate(current)) {
return current;
}
}
return null;
}
export function findEnclosingScope(node: SyntaxNode) : SyntaxNode {
let parent = node.parent || node;
if (isFunctionDefinitionName(node)) {
return findFirstParent(parent, n => isFunctionDefinition(n) || isProgram(n)) || parent;
} else if (node.text === 'argv') {
parent = findFirstParent(node, n => isFunctionDefinition(n) || isProgram(n)) || parent;
return isFunctionDefinition(parent) ? parent.firstNamedChild || parent : parent;
} else if (isVariable(node)) {
parent = findFirstParent(node, n => isScope(n)) || parent;
return isForLoop(parent) && findForLoopVariable(parent)?.text === node.text
? parent
: findFirstParent(node, n => isProgram(n) || isFunctionDefinitionName(n))
|| parent;
} else if (isCommandName(node)) {
return findFirstParent(node, n => isProgram(n)) || parent;
} else {
return findFirstParent(node, n => 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.
export function getNodeText(node: SyntaxNode | null): string {
if (!node) {
return '';
}
if (isFunctionDefinition(node)) {
return node.child(1)?.text || '';
}
if (isVariableDefinition(node)) {
const defVar = findSetDefinedVariable(node)!;
return defVar.text || '';
}
return node.text !== null ? node.text.trim() : '';
}
export function getNodesTextAsSingleLine(nodes: SyntaxNode[]): string {
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();
}
export function firstAncestorMatch(
start: SyntaxNode,
predicate: (n: SyntaxNode) => boolean,
): SyntaxNode | null {
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
*/
export function ancestorMatch(
start: SyntaxNode,
predicate: (n: SyntaxNode) => boolean,
inclusive: boolean = true,
): SyntaxNode[] {
const ancestors = getParentNodes(start) || [];
const searchNodes : SyntaxNode[] = [];
for (const p of ancestors) {
searchNodes.push(...getChildNodes(p));
}
const results: SyntaxNode[] = 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
*/
export function descendantMatch(
start: SyntaxNode,
predicate: (n: SyntaxNode) => boolean,
inclusive = true,
) : SyntaxNode[] {
const descendants: SyntaxNode[] = [];
descendants.push(...getChildNodes(start));
const results = descendants.filter(descendant => predicate(descendant));
return inclusive ? results : results.filter(r => r !== start);
}
export function hasNode(allNodes: SyntaxNode[], matchNode: SyntaxNode) {
for (const node of allNodes) {
if (node.equals(matchNode)) {
return true;
}
}
return false;
}
export function getNamedNeighbors(node: SyntaxNode): SyntaxNode[] {
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
//}
export function getRange(node: SyntaxNode): Range {
return 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)
*/
export function findNodeAt(tree: Tree, line: number, column: number): SyntaxNode | null {
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 });
}
export function equalRanges(a: Range, b: Range): boolean {
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
);
}
export function containsRange(a: Range, b: Range): boolean {
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
*/
export function getNodeAt(tree: Tree, line: number, column: number): SyntaxNode | null {
if (!tree.rootNode) {
return null;
}
return tree.rootNode.descendantForPosition({ row: line, column });
}
export function getNodeAtRange(root: SyntaxNode, range: Range): SyntaxNode | null {
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)
// }
export function positionToPoint(pos: Position): Point {
return {
row: pos.line,
column: pos.character,
};
}
export function pointToPosition(point: Point): Position {
return {
line: point.row,
character: point.column,
};
}
export function rangeToPoint(range: Range): Point {
return {
row: range.start.line,
column: range.start.character,
};
}
export function getRangeWithPrecedingComments(node: SyntaxNode): Range {
let currentNode: SyntaxNode | null = node.previousNamedSibling;
let previousNode: SyntaxNode = node;
while (currentNode?.type === 'comment') {
previousNode = currentNode;
currentNode = currentNode.previousNamedSibling;
}
return Range.create(
pointToPosition(previousNode.startPosition),
pointToPosition(node.endPosition),
);
}
export function getPrecedingComments(node: SyntaxNode | null): string {
if (!node) {
return '';
}
const comments = commentsHelper(node);
if (!comments) {
return node.text;
}
return [
commentsHelper(node),
node.text,
].join('\n');
}
function commentsHelper(node: SyntaxNode | null) : string {
if (!node) {
return '';
}
const comment: string[] = [];
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');
}
export function isFishExtension(path: URI | string): boolean {
const ext = extname(path).toLowerCase();
return ext === '.fish';
}
export function isPositionWithinRange(position: Position, range: Range): boolean {
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;
}
export function isPositionAfter(first: Position, second: Position): boolean {
return (
first.line < second.line ||
first.line === second.line && first.character < second.character
);
}
export function isNodeWithinRange(node: SyntaxNode, range: Range): boolean {
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;
}
export function* nodesGen(node: SyntaxNode) {
const queue: SyntaxNode[] = [node];
while (queue.length) {
const n = queue.shift();
if (!n) {
return;
}
if (n.children.length) {
queue.unshift(...n.children);
}
yield n;
}
}
export function getLeafs(node: SyntaxNode): SyntaxNode[] {
function gatherLeafs(node: SyntaxNode, leafs: SyntaxNode[] = []): SyntaxNode[] {
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);
}
export function getLastLeaf(node: SyntaxNode, maxIndex: number = Infinity): SyntaxNode {
const allLeafs = getLeafs(node).filter(leaf => leaf.startPosition.column < maxIndex);
return allLeafs[allLeafs.length - 1]!;
}
export function matchesArgument(node: SyntaxNode, argName: string) {
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
*/
export function getCommandArgumentValue(command: SyntaxNode, argName: string): SyntaxNode | null {
function getCommand(node: SyntaxNode) {
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
//}
export function getNodeAtPosition(tree: Tree, position: { line: number; character: number; }): SyntaxNode | null {
return tree.rootNode.descendantForPosition({ row: position.line, column: position.character });
}