Skip to content

Commit c5b27ed

Browse files
authored
fix: Cursor position after delete (#261)
* fix: Have LineCursor decide where to move itself upon delete Refactor to have LineCursor decide where it will move when a block deletion occurs (if it needs to move). The cursor is moved to one of the following locations (in order of preference) that is valid: - The connection to which the deleted block attached. - The block connected to the next connection of the deleted block. - The parent block of the deleted block. - A location on the workspace beneath the deleted block. BUG: because the deletion has not yet occurred, the "if valid" check is not accurate, and e.g. the cursor will get moved to the parent block when deleting the last/only block connected to an input, instead of getting moved to the input itself (which would normally be allowed for empty inputs). Fixes #254. * fix: Check post-delete node after deletion Split prepareForDelete into preDelete and postDelete, with the former making a list of possible places to move the cursor to, and the latter checking to see if they are still valid locations after the deletion has occurred. Introduces a new validNode method to check for validity. * chore: Format
1 parent 412e66b commit c5b27ed

File tree

5 files changed

+107
-64
lines changed

5 files changed

+107
-64
lines changed

src/actions/clipboard.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
} from 'blockly';
1414
import * as Constants from '../constants';
1515
import type {BlockSvg, WorkspaceSvg} from 'blockly';
16+
import {LineCursor} from '../line_cursor';
1617
import {Navigation} from '../navigation';
1718

1819
const KeyCodes = blocklyUtils.KeyCodes;
@@ -163,14 +164,15 @@ export class Clipboard {
163164
* @returns True if this function successfully handled cutting.
164165
*/
165166
private cutCallback(workspace: WorkspaceSvg) {
166-
const sourceBlock = workspace
167-
.getCursor()
168-
?.getCurNode()
169-
.getSourceBlock() as BlockSvg;
167+
const cursor = workspace.getCursor();
168+
if (!cursor) throw new TypeError('no cursor');
169+
const sourceBlock = cursor.getCurNode().getSourceBlock() as BlockSvg | null;
170+
if (!sourceBlock) throw new TypeError('no source block');
170171
this.copyData = sourceBlock.toCopyData();
171172
this.copyWorkspace = sourceBlock.workspace;
172-
this.navigation.moveCursorOnBlockDelete(workspace, sourceBlock);
173+
if (cursor instanceof LineCursor) cursor.preDelete(sourceBlock);
173174
sourceBlock.checkAndDelete();
175+
if (cursor instanceof LineCursor) cursor.postDelete();
174176
return true;
175177
}
176178

src/actions/delete.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
} from 'blockly';
1313
import * as Constants from '../constants';
1414
import type {BlockSvg, WorkspaceSvg} from 'blockly';
15+
import {LineCursor} from '../line_cursor';
1516
import {Navigation} from '../navigation';
1617

1718
const KeyCodes = BlocklyUtils.KeyCodes;
@@ -181,8 +182,9 @@ export class DeleteAction {
181182
// Don't delete while dragging. Jeez.
182183
if (Gesture.inProgress()) false;
183184

184-
this.navigation.moveCursorOnBlockDelete(workspace, sourceBlock);
185+
if (cursor instanceof LineCursor) cursor.preDelete(sourceBlock);
185186
sourceBlock.checkAndDelete();
187+
if (cursor instanceof LineCursor) cursor.postDelete();
186188
return true;
187189
}
188190
}

src/actions/insert.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,9 @@ export class InsertAction {
8888
const insertAboveItem: ContextMenuRegistry.RegistryItem = {
8989
displayText: (scope) => {
9090
if (scope.block?.previousConnection) {
91-
return 'Insert Block Above (I)'
91+
return 'Insert Block Above (I)';
9292
} else {
93-
return 'Insert Block (I)'
93+
return 'Insert Block (I)';
9494
}
9595
},
9696
preconditionFn: (scope: Scope) => {

src/line_cursor.ts

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ export class LineCursor extends Marker {
4646
/** Old Cursor instance, saved during installation. */
4747
private oldCursor: Blockly.Cursor | null = null;
4848

49+
/** Locations to try moving the cursor to after a deletion. */
50+
private potentialNodes: Blockly.ASTNode[] | null = null;
51+
4952
/**
5053
* @param workspace The workspace this cursor belongs to.
5154
*/
@@ -226,7 +229,7 @@ export class LineCursor extends Marker {
226229
/**
227230
* Returns true iff the given node can be visited by the cursor when
228231
* using the left/right arrow keys. Specifically, if the node is
229-
* for any node for which valideLineNode would return true, plus:
232+
* any node for which valideLineNode would return true, plus:
230233
*
231234
* - Any block.
232235
* - Any field that is not a full block field.
@@ -259,6 +262,21 @@ export class LineCursor extends Marker {
259262
}
260263
}
261264

265+
/**
266+
* Returns true iff the given node can be visited by the cursor.
267+
* Specifically, if the node is any for which validInLineNode woudl
268+
* return true, or if it is a workspace node.
269+
*
270+
* @param node The AST node to check whether it is valid.
271+
* @returns True if the node should be visited, false otherwise.
272+
*/
273+
protected validNode(node: ASTNode | null): boolean {
274+
return (
275+
!!node &&
276+
(this.validInLineNode(node) || node.getType() === ASTNode.types.WORKSPACE)
277+
);
278+
}
279+
262280
/**
263281
* Moves the cursor to the next sibling that is at the same level
264282
* of nesting.
@@ -487,6 +505,82 @@ export class LineCursor extends Marker {
487505
return this.getRightMostChild(newNode);
488506
}
489507

508+
/**
509+
* Prepare for the deletion of a block by making a list of nodes we
510+
* could move the cursor to afterwards and save it to
511+
* this.potentialNodes.
512+
*
513+
* After the deletion has occurred, call postDelete to move it to
514+
* the first valid node on that list.
515+
*
516+
* The locations to try (in order of preference) are:
517+
*
518+
* - The current location.
519+
* - The connection to which the deleted block is attached.
520+
* - The block connected to the next connection of the deleted block.
521+
* - The parent block of the deleted block.
522+
* - A location on the workspace beneath the deleted block.
523+
*
524+
* N.B.: When block is deleted, all of the blocks conneccted to that
525+
* block's inputs are also deleted, but not blocks connected to its
526+
* next connection.
527+
*
528+
* @param deletedBlock The block that is being deleted.
529+
*/
530+
preDelete(deletedBlock: Blockly.Block) {
531+
const curNode = this.getCurNode();
532+
533+
const nodes: Blockly.ASTNode[] = [curNode];
534+
// The connection to which the deleted block is attached.
535+
const parentConnection =
536+
deletedBlock.previousConnection?.targetConnection ??
537+
deletedBlock.outputConnection?.targetConnection;
538+
if (parentConnection) {
539+
const parentNode = Blockly.ASTNode.createConnectionNode(parentConnection);
540+
if (parentNode) nodes.push(parentNode);
541+
}
542+
// The block connected to the next connection of the deleted block.
543+
const nextBlock = deletedBlock.getNextBlock();
544+
if (nextBlock) {
545+
const nextNode = Blockly.ASTNode.createBlockNode(nextBlock);
546+
if (nextNode) nodes.push(nextNode);
547+
}
548+
// The parent block of the deleted block.
549+
const parentBlock = deletedBlock.getParent();
550+
if (parentBlock) {
551+
const parentNode = Blockly.ASTNode.createBlockNode(parentBlock);
552+
if (parentNode) nodes.push(parentNode);
553+
}
554+
// A location on the workspace beneath the deleted block.
555+
// Move to the workspace.
556+
const curBlock = curNode.getSourceBlock();
557+
if (curBlock) {
558+
const workspaceNode = Blockly.ASTNode.createWorkspaceNode(
559+
this.workspace,
560+
curBlock.getRelativeToSurfaceXY(),
561+
);
562+
if (workspaceNode) nodes.push(workspaceNode);
563+
}
564+
this.potentialNodes = nodes;
565+
}
566+
567+
/**
568+
* Move the cursor to the first valid location in
569+
* this.potentialNodes, following a block deletion.
570+
*/
571+
postDelete() {
572+
const nodes = this.potentialNodes;
573+
this.potentialNodes = null;
574+
if (!nodes) throw new Error('must call preDelete first');
575+
for (const node of nodes) {
576+
if (this.validNode(node) && !node.getSourceBlock()?.disposed) {
577+
this.setCurNode(node);
578+
return;
579+
}
580+
}
581+
throw new Error('no valid nodes in this.potentialNodes');
582+
}
583+
490584
/**
491585
* Get the current location of the cursor.
492586
*

src/navigation.ts

Lines changed: 0 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -407,61 +407,6 @@ export class Navigation {
407407
this.setState(mainWorkspace, Constants.STATE.FLYOUT);
408408
}
409409

410-
/**
411-
* Moves the cursor to the appropriate location before a block is deleted.
412-
* This is used when the user deletes a block using the delete or backspace
413-
* key.
414-
*
415-
* @param workspace The workspace the block is being deleted on.
416-
* @param deletedBlock The block that is being deleted.
417-
*/
418-
moveCursorOnBlockDelete(
419-
workspace: Blockly.WorkspaceSvg,
420-
deletedBlock: Blockly.BlockSvg,
421-
) {
422-
const cursor = workspace.getCursor();
423-
if (!cursor) {
424-
return;
425-
}
426-
const curNode = cursor.getCurNode();
427-
const block = curNode ? curNode.getSourceBlock() : null;
428-
429-
if (block === deletedBlock) {
430-
// If the block has a parent move the cursor to their connection point.
431-
if (block.getParent()) {
432-
const topConnection =
433-
block.previousConnection || block.outputConnection;
434-
if (topConnection?.targetConnection) {
435-
cursor.setCurNode(
436-
Blockly.ASTNode.createConnectionNode(
437-
topConnection.targetConnection,
438-
)!,
439-
);
440-
}
441-
} else {
442-
// If the block is by itself move the cursor to the workspace.
443-
cursor.setCurNode(
444-
Blockly.ASTNode.createWorkspaceNode(
445-
block.workspace,
446-
block.getRelativeToSurfaceXY(),
447-
)!,
448-
);
449-
}
450-
// If the cursor is on a block whose parent is being deleted, move the
451-
// cursor to the workspace.
452-
} else if (
453-
block &&
454-
deletedBlock.getChildren(false).includes(block as Blockly.BlockSvg)
455-
) {
456-
cursor.setCurNode(
457-
Blockly.ASTNode.createWorkspaceNode(
458-
block.workspace,
459-
block.getRelativeToSurfaceXY(),
460-
)!,
461-
);
462-
}
463-
}
464-
465410
/**
466411
* Sets the navigation state to toolbox and selects the first category in the
467412
* toolbox. No-op if a toolbox does not exist on the given workspace.

0 commit comments

Comments
 (0)