executeHandleBackspace function
Handles the Backspace key.
Supported behaviors:
- If there's an active selection: delete the selection
- If cursor is at the start of a ListItem: outdent (remove from list)
- If cursor is at the start of another node: merge with the previous node
- If cursor is on an image: remove the image
- If ctrl is pressed: delete the previous word
- Otherwise: delete the previous character in the fragment
Implementation
bool executeHandleBackspace(FluentDocument document, {bool ctrl = false}) {
final root = document.content;
final cursor = document.cursor;
// Case 1: If there's an active selection, delete it
final selection = resolveSelection(
root,
cursor.anchorId,
cursor.anchorOffset,
cursor.focusId,
cursor.focusOffset,
cachedStops: document.caretStops,
cachedLines: document.logicalLines,
);
if (selection != null) {
// Delete the selection by replacing it with an empty string
executeHandleReplaceSelection('', document);
return true;
}
// Case 1.5: If ctrl is pressed, delete the previous word
if (ctrl) {
return _handleDeleteWord(document);
}
// Find the fragment and the current container
// Case 2a: If cursor is on a HorizontalRule, remove it.
final currentNode = document.nodeById(cursor.anchorId);
if (currentNode is HorizontalRule) {
final targetStop = _findPreviousStop(
root, cursor.anchorId, 0,
cachedStops: document.caretStops,
cachedLines: document.logicalLines,
);
removeNode(root, currentNode);
if (targetStop != null) {
cursor.moveTo(targetStop.fragmentId, targetStop.offset);
}
document.updateContent();
return true;
}
// Resolve the actual Fragment from the cursor anchor (which may be a
// Paragraph, Image, HorizontalRule, or Fragment itself).
Fragment? currentFrag;
if (currentNode is Fragment) {
currentFrag = currentNode;
} else if (currentNode is InlineContainerNode) {
final containerNode = currentNode as InlineContainerNode;
final children = containerNode.getChildren();
if (cursor.anchorOffset >= 0 && cursor.anchorOffset < children.length) {
final child = children[cursor.anchorOffset];
if (child is Fragment) currentFrag = child;
}
if (currentFrag == null) {
for (final child in children) {
if (child is Fragment) {
currentFrag = child;
break;
}
}
}
}
if (currentFrag == null) return false;
final container = findLogicalContainer(root, cursor.anchorId);
if (container == null) return false;
// Special case: cursor is inside an empty paragraph.
// Remove the paragraph and move the cursor to the end of the previous one.
if (container is Paragraph && container.text.isEmpty) {
final prevStop = _findPreviousStop(
root, cursor.anchorId, 0,
cachedStops: document.caretStops,
cachedLines: document.logicalLines,
);
if (prevStop != null) {
removeNode(root, container as FNode);
cursor.moveTo(prevStop.fragmentId, prevStop.offset);
document.updateContent();
return true;
}
// At the very start of the document: nothing to merge with.
return false;
}
// Case 2b: If it's an image, remove the entire node.
// The cursor should be positioned on the stop that precedes the image
// (offset 0 of the image → moveLeft gives the true previous).
if (currentFrag is FluentImage) {
final targetStop = _findPreviousStop(
root, cursor.anchorId, 0,
cachedStops: document.caretStops,
cachedLines: document.logicalLines,
);
removeNode(root, currentFrag);
if (targetStop != null) {
cursor.moveTo(targetStop.fragmentId, targetStop.offset);
}
document.updateContent();
return true;
}
// Case 3: If we're at the start of the container (offset == 0)
// handle the merge with the previous node or outdent for lists
if (cursor.anchorOffset == 0) {
return _handleBackspaceAtStart(document, container, currentFrag);
}
// Case 4: Normal character deletion
final newOffset = cursor.anchorOffset - 1;
FragmentOperations.deleteTextInFragment(currentFrag, newOffset, count: 1);
// Update the cursor
cursor.moveTo(currentFrag.id, newOffset);
// Notify comment system of the text mutation.
if (container is Paragraph) {
final globalOffset = document.getGlobalOffsetInParagraph(
container.id,
currentFrag.id,
newOffset,
);
if (globalOffset != null) {
document.notifyTextMutation(container.id, globalOffset, -1);
}
}
document.updateContent();
return true;
}