Skip to content

Commit fac7504

Browse files
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
1 parent 3160e3d commit fac7504

File tree

2 files changed

+528
-42
lines changed

2 files changed

+528
-42
lines changed

core/keyboard_nav/line_cursor.ts

Lines changed: 88 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,11 @@ export class LineCursor extends Marker {
9999
if (!curNode) {
100100
return null;
101101
}
102-
const newNode = this.getNextNode(curNode, this.validLineNode.bind(this));
102+
const newNode = this.getNextNode(
103+
curNode,
104+
this.validLineNode.bind(this),
105+
true,
106+
);
103107

104108
if (newNode) {
105109
this.setCurNode(newNode);
@@ -119,7 +123,11 @@ export class LineCursor extends Marker {
119123
if (!curNode) {
120124
return null;
121125
}
122-
const newNode = this.getNextNode(curNode, this.validInLineNode.bind(this));
126+
const newNode = this.getNextNode(
127+
curNode,
128+
this.validInLineNode.bind(this),
129+
true,
130+
);
123131

124132
if (newNode) {
125133
this.setCurNode(newNode);
@@ -138,11 +146,10 @@ export class LineCursor extends Marker {
138146
if (!curNode) {
139147
return null;
140148
}
141-
const newNode = this.getPreviousNode(
149+
const newNode = this.getPreviousNodeImpl(
142150
curNode,
143151
this.validLineNode.bind(this),
144152
);
145-
146153
if (newNode) {
147154
this.setCurNode(newNode);
148155
}
@@ -161,7 +168,7 @@ export class LineCursor extends Marker {
161168
if (!curNode) {
162169
return null;
163170
}
164-
const newNode = this.getPreviousNode(
171+
const newNode = this.getPreviousNodeImpl(
165172
curNode,
166173
this.validInLineNode.bind(this),
167174
);
@@ -184,6 +191,7 @@ export class LineCursor extends Marker {
184191
const rightNode = this.getNextNode(
185192
curNode,
186193
this.validInLineNode.bind(this),
194+
false,
187195
);
188196
return this.validLineNode(rightNode);
189197
}
@@ -299,28 +307,46 @@ export class LineCursor extends Marker {
299307
* should be traversed.
300308
* @returns The next node in the traversal.
301309
*/
302-
getNextNode(
310+
private getNextNodeImpl(
303311
node: ASTNode | null,
304312
isValid: (p1: ASTNode | null) => boolean,
305313
): ASTNode | null {
306-
if (!node) {
307-
return null;
308-
}
309-
const newNode = node.in() || node.next();
310-
if (isValid(newNode)) {
311-
return newNode;
312-
} else if (newNode) {
313-
return this.getNextNode(newNode, isValid);
314-
}
315-
const siblingOrParentSibling = this.findSiblingOrParentSibling(node.out());
316-
if (isValid(siblingOrParentSibling)) {
317-
return siblingOrParentSibling;
318-
} else if (siblingOrParentSibling) {
319-
return this.getNextNode(siblingOrParentSibling, isValid);
320-
}
314+
if (!node) return null;
315+
let newNode = node.in() || node.next();
316+
if (isValid(newNode)) return newNode;
317+
if (newNode) return this.getNextNodeImpl(newNode, isValid);
318+
319+
newNode = this.findSiblingOrParentSibling(node.out());
320+
if (isValid(newNode)) return newNode;
321+
if (newNode) return this.getNextNodeImpl(newNode, isValid);
321322
return null;
322323
}
323324

325+
/**
326+
* Get the next node in the AST, optionally allowing for loopback.
327+
*
328+
* @param node The current position in the AST.
329+
* @param isValid A function true/false depending on whether the given node
330+
* should be traversed.
331+
* @param loop Whether to loop around to the beginning of the workspace if
332+
* novalid node was found.
333+
* @returns The next node in the traversal.
334+
*/
335+
getNextNode(
336+
node: ASTNode | null,
337+
isValid: (p1: ASTNode | null) => boolean,
338+
loop: boolean,
339+
): ASTNode | null {
340+
if (!node) return null;
341+
342+
const potential = this.getNextNodeImpl(node, isValid);
343+
if (potential || !loop) return potential;
344+
// Loop back.
345+
const firstNode = this.getFirstNode();
346+
if (isValid(firstNode)) return firstNode;
347+
return this.getNextNodeImpl(firstNode, isValid);
348+
}
349+
324350
/**
325351
* Reverses the pre order traversal in order to find the previous node. This
326352
* will allow a user to easily navigate the entire Blockly AST without having
@@ -332,28 +358,50 @@ export class LineCursor extends Marker {
332358
* @returns The previous node in the traversal or null if no previous node
333359
* exists.
334360
*/
335-
getPreviousNode(
361+
private getPreviousNodeImpl(
336362
node: ASTNode | null,
337363
isValid: (p1: ASTNode | null) => boolean,
338364
): ASTNode | null {
339-
if (!node) {
340-
return null;
341-
}
365+
if (!node) return null;
342366
let newNode: ASTNode | null = node.prev();
343367

344368
if (newNode) {
345369
newNode = this.getRightMostChild(newNode);
346370
} else {
347371
newNode = node.out();
348372
}
349-
if (isValid(newNode)) {
350-
return newNode;
351-
} else if (newNode) {
352-
return this.getPreviousNode(newNode, isValid);
353-
}
373+
374+
if (isValid(newNode)) return newNode;
375+
if (newNode) return this.getPreviousNodeImpl(newNode, isValid);
354376
return null;
355377
}
356378

379+
/**
380+
* Get the previous node in the AST, optionally allowing for loopback.
381+
*
382+
* @param node The current position in the AST.
383+
* @param isValid A function true/false depending on whether the given node
384+
* should be traversed.
385+
* @param loop Whether to loop around to the end of the workspace if no
386+
* valid node was found.
387+
* @returns The previous node in the traversal or null if no previous node
388+
* exists.
389+
*/
390+
getPreviousNode(
391+
node: ASTNode | null,
392+
isValid: (p1: ASTNode | null) => boolean,
393+
loop: boolean,
394+
): ASTNode | null {
395+
if (!node) return null;
396+
397+
const potential = this.getPreviousNodeImpl(node, isValid);
398+
if (potential || !loop) return potential;
399+
// Loop back.
400+
const lastNode = this.getLastNode();
401+
if (isValid(lastNode)) return lastNode;
402+
return this.getPreviousNodeImpl(lastNode, isValid);
403+
}
404+
357405
/**
358406
* From the given node find either the next valid sibling or the parent's
359407
* next sibling.
@@ -362,13 +410,9 @@ export class LineCursor extends Marker {
362410
* @returns The next sibling node, the parent's next sibling, or null.
363411
*/
364412
private findSiblingOrParentSibling(node: ASTNode | null): ASTNode | null {
365-
if (!node) {
366-
return null;
367-
}
413+
if (!node) return null;
368414
const nextNode = node.next();
369-
if (nextNode) {
370-
return nextNode;
371-
}
415+
if (nextNode) return nextNode;
372416
return this.findSiblingOrParentSibling(node.out());
373417
}
374418

@@ -381,9 +425,7 @@ export class LineCursor extends Marker {
381425
*/
382426
private getRightMostChild(node: ASTNode): ASTNode | null {
383427
let newNode = node.in();
384-
if (!newNode) {
385-
return node;
386-
}
428+
if (!newNode) return node;
387429
for (
388430
let nextNode: ASTNode | null = newNode;
389431
nextNode;
@@ -787,9 +829,13 @@ export class LineCursor extends Marker {
787829
// Iterate until you fall off the end of the stack.
788830
while (nextNode) {
789831
prevNode = nextNode;
790-
nextNode = this.getNextNode(prevNode, (node) => {
791-
return !!node;
792-
});
832+
nextNode = this.getNextNode(
833+
prevNode,
834+
(node) => {
835+
return !!node;
836+
},
837+
false,
838+
);
793839
}
794840
return prevNode;
795841
}

0 commit comments

Comments
 (0)