UNPKG

fish-lsp

Version:

LSP implementation for fish/fish-shell

174 lines (173 loc) 5.83 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.convertIfToCombinersString = convertIfToCombinersString; const tree_sitter_1 = require("../utils/tree-sitter"); const node_types_1 = require("../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. */ function convertIfToCombinersString(node) { const combiner = new StatementCombiner(); const queue = (0, tree_sitter_1.getNamedChildNodes)(node); while (queue.length > 0) { const n = queue.shift(); if (!n) break; switch (true) { case (0, node_types_1.isConditional)(n): combiner.newBlock(n.type); 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, queue) { while (queue.length > 0) { const peek = queue.at(0); if (!peek) break; if (peek.endIndex > node.endIndex || peek.startIndex < node.startIndex) break; queue.shift(); } } var ConditionalBlock; (function (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 */ function create(keyword, body = []) { return { keyword, body }; } ConditionalBlock.create = create; })(ConditionalBlock || (ConditionalBlock = {})); /** * 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 { blocks = []; get currentBlock() { 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) { 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) { 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 */ getCombinerFromKeyword(block) { 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 */ buildBlockString(block) { 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(); } }