mirror of
https://github.com/google/blockly.git
synced 2026-01-10 02:17:09 +01:00
feat: add loopback in cursor navigation, and add tests (#8883)
* chore: tests for cursor getNextNode * chore: add tests for getPreviousNode * feat: add looping to getPreviousNode and getNextNode * chore: inline returns * chore: fix test that results in a stack node * chore: fix annotations
This commit is contained in:
@@ -99,7 +99,11 @@ export class LineCursor extends Marker {
|
||||
if (!curNode) {
|
||||
return null;
|
||||
}
|
||||
const newNode = this.getNextNode(curNode, this.validLineNode.bind(this));
|
||||
const newNode = this.getNextNode(
|
||||
curNode,
|
||||
this.validLineNode.bind(this),
|
||||
true,
|
||||
);
|
||||
|
||||
if (newNode) {
|
||||
this.setCurNode(newNode);
|
||||
@@ -119,7 +123,11 @@ export class LineCursor extends Marker {
|
||||
if (!curNode) {
|
||||
return null;
|
||||
}
|
||||
const newNode = this.getNextNode(curNode, this.validInLineNode.bind(this));
|
||||
const newNode = this.getNextNode(
|
||||
curNode,
|
||||
this.validInLineNode.bind(this),
|
||||
true,
|
||||
);
|
||||
|
||||
if (newNode) {
|
||||
this.setCurNode(newNode);
|
||||
@@ -138,11 +146,10 @@ export class LineCursor extends Marker {
|
||||
if (!curNode) {
|
||||
return null;
|
||||
}
|
||||
const newNode = this.getPreviousNode(
|
||||
const newNode = this.getPreviousNodeImpl(
|
||||
curNode,
|
||||
this.validLineNode.bind(this),
|
||||
);
|
||||
|
||||
if (newNode) {
|
||||
this.setCurNode(newNode);
|
||||
}
|
||||
@@ -161,7 +168,7 @@ export class LineCursor extends Marker {
|
||||
if (!curNode) {
|
||||
return null;
|
||||
}
|
||||
const newNode = this.getPreviousNode(
|
||||
const newNode = this.getPreviousNodeImpl(
|
||||
curNode,
|
||||
this.validInLineNode.bind(this),
|
||||
);
|
||||
@@ -184,6 +191,7 @@ export class LineCursor extends Marker {
|
||||
const rightNode = this.getNextNode(
|
||||
curNode,
|
||||
this.validInLineNode.bind(this),
|
||||
false,
|
||||
);
|
||||
return this.validLineNode(rightNode);
|
||||
}
|
||||
@@ -299,28 +307,46 @@ export class LineCursor extends Marker {
|
||||
* should be traversed.
|
||||
* @returns The next node in the traversal.
|
||||
*/
|
||||
getNextNode(
|
||||
private getNextNodeImpl(
|
||||
node: ASTNode | null,
|
||||
isValid: (p1: ASTNode | null) => boolean,
|
||||
): ASTNode | null {
|
||||
if (!node) {
|
||||
return null;
|
||||
}
|
||||
const newNode = node.in() || node.next();
|
||||
if (isValid(newNode)) {
|
||||
return newNode;
|
||||
} else if (newNode) {
|
||||
return this.getNextNode(newNode, isValid);
|
||||
}
|
||||
const siblingOrParentSibling = this.findSiblingOrParentSibling(node.out());
|
||||
if (isValid(siblingOrParentSibling)) {
|
||||
return siblingOrParentSibling;
|
||||
} else if (siblingOrParentSibling) {
|
||||
return this.getNextNode(siblingOrParentSibling, isValid);
|
||||
}
|
||||
if (!node) return null;
|
||||
let newNode = node.in() || node.next();
|
||||
if (isValid(newNode)) return newNode;
|
||||
if (newNode) return this.getNextNodeImpl(newNode, isValid);
|
||||
|
||||
newNode = this.findSiblingOrParentSibling(node.out());
|
||||
if (isValid(newNode)) return newNode;
|
||||
if (newNode) return this.getNextNodeImpl(newNode, isValid);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next node in the AST, optionally allowing for loopback.
|
||||
*
|
||||
* @param node The current position in the AST.
|
||||
* @param isValid A function true/false depending on whether the given node
|
||||
* should be traversed.
|
||||
* @param loop Whether to loop around to the beginning of the workspace if
|
||||
* novalid node was found.
|
||||
* @returns The next node in the traversal.
|
||||
*/
|
||||
getNextNode(
|
||||
node: ASTNode | null,
|
||||
isValid: (p1: ASTNode | null) => boolean,
|
||||
loop: boolean,
|
||||
): ASTNode | null {
|
||||
if (!node) return null;
|
||||
|
||||
const potential = this.getNextNodeImpl(node, isValid);
|
||||
if (potential || !loop) return potential;
|
||||
// Loop back.
|
||||
const firstNode = this.getFirstNode();
|
||||
if (isValid(firstNode)) return firstNode;
|
||||
return this.getNextNodeImpl(firstNode, isValid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverses the pre order traversal in order to find the previous node. This
|
||||
* will allow a user to easily navigate the entire Blockly AST without having
|
||||
@@ -332,13 +358,11 @@ export class LineCursor extends Marker {
|
||||
* @returns The previous node in the traversal or null if no previous node
|
||||
* exists.
|
||||
*/
|
||||
getPreviousNode(
|
||||
private getPreviousNodeImpl(
|
||||
node: ASTNode | null,
|
||||
isValid: (p1: ASTNode | null) => boolean,
|
||||
): ASTNode | null {
|
||||
if (!node) {
|
||||
return null;
|
||||
}
|
||||
if (!node) return null;
|
||||
let newNode: ASTNode | null = node.prev();
|
||||
|
||||
if (newNode) {
|
||||
@@ -346,14 +370,38 @@ export class LineCursor extends Marker {
|
||||
} else {
|
||||
newNode = node.out();
|
||||
}
|
||||
if (isValid(newNode)) {
|
||||
return newNode;
|
||||
} else if (newNode) {
|
||||
return this.getPreviousNode(newNode, isValid);
|
||||
}
|
||||
|
||||
if (isValid(newNode)) return newNode;
|
||||
if (newNode) return this.getPreviousNodeImpl(newNode, isValid);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the previous node in the AST, optionally allowing for loopback.
|
||||
*
|
||||
* @param node The current position in the AST.
|
||||
* @param isValid A function true/false depending on whether the given node
|
||||
* should be traversed.
|
||||
* @param loop Whether to loop around to the end of the workspace if no
|
||||
* valid node was found.
|
||||
* @returns The previous node in the traversal or null if no previous node
|
||||
* exists.
|
||||
*/
|
||||
getPreviousNode(
|
||||
node: ASTNode | null,
|
||||
isValid: (p1: ASTNode | null) => boolean,
|
||||
loop: boolean,
|
||||
): ASTNode | null {
|
||||
if (!node) return null;
|
||||
|
||||
const potential = this.getPreviousNodeImpl(node, isValid);
|
||||
if (potential || !loop) return potential;
|
||||
// Loop back.
|
||||
const lastNode = this.getLastNode();
|
||||
if (isValid(lastNode)) return lastNode;
|
||||
return this.getPreviousNodeImpl(lastNode, isValid);
|
||||
}
|
||||
|
||||
/**
|
||||
* From the given node find either the next valid sibling or the parent's
|
||||
* next sibling.
|
||||
@@ -362,13 +410,9 @@ export class LineCursor extends Marker {
|
||||
* @returns The next sibling node, the parent's next sibling, or null.
|
||||
*/
|
||||
private findSiblingOrParentSibling(node: ASTNode | null): ASTNode | null {
|
||||
if (!node) {
|
||||
return null;
|
||||
}
|
||||
if (!node) return null;
|
||||
const nextNode = node.next();
|
||||
if (nextNode) {
|
||||
return nextNode;
|
||||
}
|
||||
if (nextNode) return nextNode;
|
||||
return this.findSiblingOrParentSibling(node.out());
|
||||
}
|
||||
|
||||
@@ -381,9 +425,7 @@ export class LineCursor extends Marker {
|
||||
*/
|
||||
private getRightMostChild(node: ASTNode): ASTNode | null {
|
||||
let newNode = node.in();
|
||||
if (!newNode) {
|
||||
return node;
|
||||
}
|
||||
if (!newNode) return node;
|
||||
for (
|
||||
let nextNode: ASTNode | null = newNode;
|
||||
nextNode;
|
||||
@@ -787,9 +829,13 @@ export class LineCursor extends Marker {
|
||||
// Iterate until you fall off the end of the stack.
|
||||
while (nextNode) {
|
||||
prevNode = nextNode;
|
||||
nextNode = this.getNextNode(prevNode, (node) => {
|
||||
return !!node;
|
||||
});
|
||||
nextNode = this.getNextNode(
|
||||
prevNode,
|
||||
(node) => {
|
||||
return !!node;
|
||||
},
|
||||
false,
|
||||
);
|
||||
}
|
||||
return prevNode;
|
||||
}
|
||||
|
||||
@@ -385,4 +385,444 @@ suite('Cursor', function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
suite('Get next node', function () {
|
||||
setup(function () {
|
||||
sharedTestSetup.call(this);
|
||||
Blockly.defineBlocksWithJsonArray([
|
||||
{
|
||||
'type': 'empty_block',
|
||||
'message0': '',
|
||||
},
|
||||
{
|
||||
'type': 'stack_block',
|
||||
'message0': '%1',
|
||||
'args0': [
|
||||
{
|
||||
'type': 'field_input',
|
||||
'name': 'FIELD',
|
||||
'text': 'default',
|
||||
},
|
||||
],
|
||||
'previousStatement': null,
|
||||
'nextStatement': null,
|
||||
},
|
||||
{
|
||||
'type': 'row_block',
|
||||
'message0': '%1 %2',
|
||||
'args0': [
|
||||
{
|
||||
'type': 'field_input',
|
||||
'name': 'FIELD',
|
||||
'text': 'default',
|
||||
},
|
||||
{
|
||||
'type': 'input_value',
|
||||
'name': 'INPUT',
|
||||
},
|
||||
],
|
||||
'output': null,
|
||||
},
|
||||
]);
|
||||
this.workspace = Blockly.inject('blocklyDiv', {});
|
||||
this.cursor = this.workspace.getCursor();
|
||||
this.neverValid = () => false;
|
||||
this.alwaysValid = () => true;
|
||||
this.isConnection = (node) => {
|
||||
return node && node.isConnection();
|
||||
};
|
||||
});
|
||||
teardown(function () {
|
||||
sharedTestTeardown.call(this);
|
||||
});
|
||||
suite('stack', function () {
|
||||
setup(function () {
|
||||
const state = {
|
||||
'blocks': {
|
||||
'languageVersion': 0,
|
||||
'blocks': [
|
||||
{
|
||||
'type': 'stack_block',
|
||||
'id': 'A',
|
||||
'x': 0,
|
||||
'y': 0,
|
||||
'next': {
|
||||
'block': {
|
||||
'type': 'stack_block',
|
||||
'id': 'B',
|
||||
'next': {
|
||||
'block': {
|
||||
'type': 'stack_block',
|
||||
'id': 'C',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
Blockly.serialization.workspaces.load(state, this.workspace);
|
||||
this.blockA = this.workspace.getBlockById('A');
|
||||
this.blockB = this.workspace.getBlockById('B');
|
||||
this.blockC = this.workspace.getBlockById('C');
|
||||
});
|
||||
teardown(function () {
|
||||
this.workspace.clear();
|
||||
});
|
||||
test('Never valid - start at top', function () {
|
||||
const startNode = ASTNode.createConnectionNode(
|
||||
this.blockA.previousConnection,
|
||||
);
|
||||
const nextNode = this.cursor.getNextNode(
|
||||
startNode,
|
||||
this.neverValid,
|
||||
false,
|
||||
);
|
||||
assert.isNull(nextNode);
|
||||
});
|
||||
test('Never valid - start in middle', function () {
|
||||
const startNode = ASTNode.createBlockNode(this.blockB);
|
||||
const nextNode = this.cursor.getNextNode(
|
||||
startNode,
|
||||
this.neverValid,
|
||||
false,
|
||||
);
|
||||
assert.isNull(nextNode);
|
||||
});
|
||||
test('Never valid - start at end', function () {
|
||||
const startNode = ASTNode.createConnectionNode(
|
||||
this.blockC.nextConnection,
|
||||
);
|
||||
const nextNode = this.cursor.getNextNode(
|
||||
startNode,
|
||||
this.neverValid,
|
||||
false,
|
||||
);
|
||||
assert.isNull(nextNode);
|
||||
});
|
||||
|
||||
test('Always valid - start at top', function () {
|
||||
const startNode = ASTNode.createConnectionNode(
|
||||
this.blockA.previousConnection,
|
||||
);
|
||||
const nextNode = this.cursor.getNextNode(
|
||||
startNode,
|
||||
this.alwaysValid,
|
||||
false,
|
||||
);
|
||||
assert.equal(nextNode.getLocation(), this.blockA);
|
||||
});
|
||||
test('Always valid - start in middle', function () {
|
||||
const startNode = ASTNode.createBlockNode(this.blockB);
|
||||
const nextNode = this.cursor.getNextNode(
|
||||
startNode,
|
||||
this.alwaysValid,
|
||||
false,
|
||||
);
|
||||
assert.equal(nextNode.getLocation(), this.blockB.getField('FIELD'));
|
||||
});
|
||||
test('Always valid - start at end', function () {
|
||||
const startNode = ASTNode.createConnectionNode(
|
||||
this.blockC.nextConnection,
|
||||
);
|
||||
const nextNode = this.cursor.getNextNode(
|
||||
startNode,
|
||||
this.alwaysValid,
|
||||
false,
|
||||
);
|
||||
assert.isNull(nextNode);
|
||||
});
|
||||
|
||||
test('Valid if connection - start at top', function () {
|
||||
const startNode = ASTNode.createConnectionNode(
|
||||
this.blockA.previousConnection,
|
||||
);
|
||||
const nextNode = this.cursor.getNextNode(
|
||||
startNode,
|
||||
this.isConnection,
|
||||
false,
|
||||
);
|
||||
assert.equal(nextNode.getLocation(), this.blockA.nextConnection);
|
||||
});
|
||||
test('Valid if connection - start in middle', function () {
|
||||
const startNode = ASTNode.createBlockNode(this.blockB);
|
||||
const nextNode = this.cursor.getNextNode(
|
||||
startNode,
|
||||
this.isConnection,
|
||||
false,
|
||||
);
|
||||
assert.equal(nextNode.getLocation(), this.blockB.nextConnection);
|
||||
});
|
||||
test('Valid if connection - start at end', function () {
|
||||
const startNode = ASTNode.createConnectionNode(
|
||||
this.blockC.nextConnection,
|
||||
);
|
||||
const nextNode = this.cursor.getNextNode(
|
||||
startNode,
|
||||
this.isConnection,
|
||||
false,
|
||||
);
|
||||
assert.isNull(nextNode);
|
||||
});
|
||||
test('Never valid - start at end - with loopback', function () {
|
||||
const startNode = ASTNode.createConnectionNode(
|
||||
this.blockC.nextConnection,
|
||||
);
|
||||
const nextNode = this.cursor.getNextNode(
|
||||
startNode,
|
||||
this.neverValid,
|
||||
true,
|
||||
);
|
||||
assert.isNull(nextNode);
|
||||
});
|
||||
test('Always valid - start at end - with loopback', function () {
|
||||
const startNode = ASTNode.createConnectionNode(
|
||||
this.blockC.nextConnection,
|
||||
);
|
||||
const nextNode = this.cursor.getNextNode(
|
||||
startNode,
|
||||
this.alwaysValid,
|
||||
true,
|
||||
);
|
||||
assert.equal(nextNode.getLocation(), this.blockA.previousConnection);
|
||||
});
|
||||
|
||||
test('Valid if connection - start at end - with loopback', function () {
|
||||
const startNode = ASTNode.createConnectionNode(
|
||||
this.blockC.nextConnection,
|
||||
);
|
||||
const nextNode = this.cursor.getNextNode(
|
||||
startNode,
|
||||
this.isConnection,
|
||||
true,
|
||||
);
|
||||
assert.equal(nextNode.getLocation(), this.blockA.previousConnection);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
suite('Get previous node', function () {
|
||||
setup(function () {
|
||||
sharedTestSetup.call(this);
|
||||
Blockly.defineBlocksWithJsonArray([
|
||||
{
|
||||
'type': 'empty_block',
|
||||
'message0': '',
|
||||
},
|
||||
{
|
||||
'type': 'stack_block',
|
||||
'message0': '%1',
|
||||
'args0': [
|
||||
{
|
||||
'type': 'field_input',
|
||||
'name': 'FIELD',
|
||||
'text': 'default',
|
||||
},
|
||||
],
|
||||
'previousStatement': null,
|
||||
'nextStatement': null,
|
||||
},
|
||||
{
|
||||
'type': 'row_block',
|
||||
'message0': '%1 %2',
|
||||
'args0': [
|
||||
{
|
||||
'type': 'field_input',
|
||||
'name': 'FIELD',
|
||||
'text': 'default',
|
||||
},
|
||||
{
|
||||
'type': 'input_value',
|
||||
'name': 'INPUT',
|
||||
},
|
||||
],
|
||||
'output': null,
|
||||
},
|
||||
]);
|
||||
this.workspace = Blockly.inject('blocklyDiv', {});
|
||||
this.cursor = this.workspace.getCursor();
|
||||
this.neverValid = () => false;
|
||||
this.alwaysValid = () => true;
|
||||
this.isConnection = (node) => {
|
||||
return node && node.isConnection();
|
||||
};
|
||||
});
|
||||
teardown(function () {
|
||||
sharedTestTeardown.call(this);
|
||||
});
|
||||
suite('stack', function () {
|
||||
setup(function () {
|
||||
const state = {
|
||||
'blocks': {
|
||||
'languageVersion': 0,
|
||||
'blocks': [
|
||||
{
|
||||
'type': 'stack_block',
|
||||
'id': 'A',
|
||||
'x': 0,
|
||||
'y': 0,
|
||||
'next': {
|
||||
'block': {
|
||||
'type': 'stack_block',
|
||||
'id': 'B',
|
||||
'next': {
|
||||
'block': {
|
||||
'type': 'stack_block',
|
||||
'id': 'C',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
Blockly.serialization.workspaces.load(state, this.workspace);
|
||||
this.blockA = this.workspace.getBlockById('A');
|
||||
this.blockB = this.workspace.getBlockById('B');
|
||||
this.blockC = this.workspace.getBlockById('C');
|
||||
});
|
||||
teardown(function () {
|
||||
this.workspace.clear();
|
||||
});
|
||||
test('Never valid - start at top', function () {
|
||||
const startNode = ASTNode.createConnectionNode(
|
||||
this.blockA.previousConnection,
|
||||
);
|
||||
const previousNode = this.cursor.getPreviousNode(
|
||||
startNode,
|
||||
this.neverValid,
|
||||
false,
|
||||
);
|
||||
assert.isNull(previousNode);
|
||||
});
|
||||
test('Never valid - start in middle', function () {
|
||||
const startNode = ASTNode.createBlockNode(this.blockB);
|
||||
const previousNode = this.cursor.getPreviousNode(
|
||||
startNode,
|
||||
this.neverValid,
|
||||
false,
|
||||
);
|
||||
assert.isNull(previousNode);
|
||||
});
|
||||
test('Never valid - start at end', function () {
|
||||
const startNode = ASTNode.createConnectionNode(
|
||||
this.blockC.nextConnection,
|
||||
);
|
||||
const previousNode = this.cursor.getPreviousNode(
|
||||
startNode,
|
||||
this.neverValid,
|
||||
false,
|
||||
);
|
||||
assert.isNull(previousNode);
|
||||
});
|
||||
|
||||
test('Always valid - start at top', function () {
|
||||
const startNode = ASTNode.createConnectionNode(
|
||||
this.blockA.previousConnection,
|
||||
);
|
||||
const previousNode = this.cursor.getPreviousNode(
|
||||
startNode,
|
||||
this.alwaysValid,
|
||||
false,
|
||||
);
|
||||
assert.isNotNull(previousNode);
|
||||
});
|
||||
test('Always valid - start in middle', function () {
|
||||
const startNode = ASTNode.createBlockNode(this.blockB);
|
||||
const previousNode = this.cursor.getPreviousNode(
|
||||
startNode,
|
||||
this.alwaysValid,
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
previousNode.getLocation(),
|
||||
this.blockB.previousConnection,
|
||||
);
|
||||
});
|
||||
test('Always valid - start at end', function () {
|
||||
const startNode = ASTNode.createConnectionNode(
|
||||
this.blockC.nextConnection,
|
||||
);
|
||||
const previousNode = this.cursor.getPreviousNode(
|
||||
startNode,
|
||||
this.alwaysValid,
|
||||
false,
|
||||
);
|
||||
assert.equal(previousNode.getLocation(), this.blockC.getField('FIELD'));
|
||||
});
|
||||
|
||||
test('Valid if connection - start at top', function () {
|
||||
const startNode = ASTNode.createConnectionNode(
|
||||
this.blockA.previousConnection,
|
||||
);
|
||||
const previousNode = this.cursor.getPreviousNode(
|
||||
startNode,
|
||||
this.isConnection,
|
||||
false,
|
||||
);
|
||||
assert.isNull(previousNode);
|
||||
});
|
||||
test('Valid if connection - start in middle', function () {
|
||||
const startNode = ASTNode.createBlockNode(this.blockB);
|
||||
const previousNode = this.cursor.getPreviousNode(
|
||||
startNode,
|
||||
this.isConnection,
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
previousNode.getLocation(),
|
||||
this.blockB.previousConnection,
|
||||
);
|
||||
});
|
||||
test('Valid if connection - start at end', function () {
|
||||
const startNode = ASTNode.createConnectionNode(
|
||||
this.blockC.nextConnection,
|
||||
);
|
||||
const previousNode = this.cursor.getPreviousNode(
|
||||
startNode,
|
||||
this.isConnection,
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
previousNode.getLocation(),
|
||||
this.blockC.previousConnection,
|
||||
);
|
||||
});
|
||||
test('Never valid - start at top - with loopback', function () {
|
||||
const startNode = ASTNode.createConnectionNode(
|
||||
this.blockA.previousConnection,
|
||||
);
|
||||
const previousNode = this.cursor.getPreviousNode(
|
||||
startNode,
|
||||
this.neverValid,
|
||||
true,
|
||||
);
|
||||
assert.isNull(previousNode);
|
||||
});
|
||||
test('Always valid - start at top - with loopback', function () {
|
||||
const startNode = ASTNode.createConnectionNode(
|
||||
this.blockA.previousConnection,
|
||||
);
|
||||
const previousNode = this.cursor.getPreviousNode(
|
||||
startNode,
|
||||
this.alwaysValid,
|
||||
true,
|
||||
);
|
||||
// Previous node will be a stack node in this case.
|
||||
assert.equal(previousNode.getLocation(), this.blockA);
|
||||
});
|
||||
test('Valid if connection - start at top - with loopback', function () {
|
||||
const startNode = ASTNode.createConnectionNode(
|
||||
this.blockA.previousConnection,
|
||||
);
|
||||
const previousNode = this.cursor.getPreviousNode(
|
||||
startNode,
|
||||
this.isConnection,
|
||||
true,
|
||||
);
|
||||
assert.equal(previousNode.getLocation(), this.blockC.nextConnection);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user