fish-lsp
Version:
LSP implementation for fish/fish-shell
174 lines (173 loc) • 5.83 kB
JavaScript
;
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();
}
}