UNPKG

fish-lsp

Version:

LSP implementation for fish/fish-shell

208 lines (194 loc) 6.19 kB
import { SyntaxNode } from 'web-tree-sitter'; import { getNamedChildNodes } from '../utils/tree-sitter'; import { isConditional } from '../utils/node-types'; /** * Code Action utility function to convert an if_statement node into a sequence of combiner commands. * ___ * ```fish * if test -f file * echo "file exists" * else * echo "file does not exist" * end * ``` * ___ * Would Become: * ___ * ```fish * test -f file * and echo "file exists" * * or echo "file does not exist" * ``` * ___ * @param node the if_statement node to convert into a sequence of combiner commands. * @returns a string representation of the if_statement node, with the if/else-if/else blocks combined. */ export function convertIfToCombinersString(node: SyntaxNode) { const combiner = new StatementCombiner(); const queue: SyntaxNode[] = getNamedChildNodes(node); while (queue.length > 0) { const n = queue.shift(); if (!n) break; switch (true) { case isConditional(n): combiner.newBlock(n.type as BlockKeywordType); break; case n.type === 'negated_statement': case n.type === 'conditional_execution': combiner.appendCommand(n); skipChildren(n, queue); break; case n.type === 'comment': case n.type === 'command': combiner.appendCommand(n); break; } } return combiner.build(); } /** * Utility function to skip children nodes that are not part of the current node. */ function skipChildren(node: SyntaxNode, queue: SyntaxNode[]) { while (queue.length > 0) { const peek = queue.at(0); if (!peek) break; if (peek.endIndex > node.endIndex || peek.startIndex < node.startIndex) break; queue.shift(); } } /** Types of conditional blocks, defined in tree-sitter-fish grammar **/ type BlockKeywordType = 'if_statement' | 'else_if_clause' | 'else_clause'; /** * Data structure to represent a conditional block in the fish language. * A conditional block is a series of commands that are executed based on a condition. * ___ * ```fish * if test -f file * echo "file exists" * end * ``` * ___ * Would become: * ___ * ```typescript * { * keyword: 'if_statement', * body: [ * { type: 'command', text: 'test -f file' }, * { type: 'command', text: 'echo "file exists"' } * ], * } * ``` */ interface ConditionalBlock { /** the type of conditional block */ keyword: BlockKeywordType; /** the commands that make up the conditional block */ body: SyntaxNode[]; } namespace ConditionalBlock { /** * Creates a new conditional block. Typically the body will be empty, since * a `if`/`else-if`/`else` block will always come before the body it contains. * @param keyword The type of conditional block * @param body The commands that make up the conditional block * @returns The new conditional block */ export function create(keyword: BlockKeywordType, body: SyntaxNode[] = []) { return { keyword, body }; } } /** * Helper class to combine statements together, based on their conditional blocks. * * This class converts if/else-if/else blocks into a single string, with the * appropriate combiners (and/or) between each block. Ideally, output from * this class should keep the original control flow, while removing the * if/else-if/else statements. */ class StatementCombiner { private blocks: ConditionalBlock[] = []; get currentBlock(): undefined | ConditionalBlock { if (this.blocks.length === 0) { return undefined; } return this.blocks[this.blocks.length - 1]; } /** * Creates a new block, based on the keyword type. */ newBlock(keywordType: 'if_statement' | 'else_if_clause' | 'else_clause') { this.blocks.push(ConditionalBlock.create(keywordType)); } /** * Appends a node to the current block. Nodes should be non-leaf nodes for the * most part because the `build()` method will use the `node.text` property to * build combined strings. More specifically, the node's that are appended * should group together child sections of each segment of the conditional * sequence per if/else-if/else block. * ___ * The supported possibilities for `node.type` are: `command`, `comment`, or `conditional_execution` * ___ * NOTE: not calling `newBlock()` before this method will throw an error. * ___ * @param node the node to append on the block's body. */ appendCommand(node: SyntaxNode) { if (!this.currentBlock) { throw new Error('Cannot append command to non-existent block, please create a new block first'); } this.currentBlock.body.push(node); } /** * Helper for retrieving the prefix combiner for a block, based on its keyword. * The prefix is then used to combine the if/else-if/else blocks together. * ___ * `if_statement` -> '' * `else_if_clause` -> 'or ' * `else_clause` -> 'or ' * ___ * @param block The block to get the combiner for (which is the prefix ) * @returns The prefix/combiner for the block */ private getCombinerFromKeyword(block: ConditionalBlock) { switch (block.keyword) { case 'if_statement': return ''; case 'else_if_clause': case 'else_clause': return 'or '; } } /** * Builds the string representation of a block, including the combiner and the comments * @param block The block to build the string for * @returns The string representation of the block */ private buildBlockString(block: ConditionalBlock) { let str = this.getCombinerFromKeyword(block); block.body.forEach((node, idx) => { const nextNode = block.body.length - 1 >= idx ? block.body[idx + 1] : undefined; if (nextNode && nextNode.type === 'comment') { str += node.text + '\n'; } else if (nextNode && nextNode.type === 'command') { str += node.text + '\nand '; } else { str += node.text + '\n'; } }); return str; } /** * Builds the combined string of all the blocks */ build() { return this.blocks .map(block => this.buildBlockString(block)) .join('\n') .trim(); } }