commitSaveState method
Called AFTER a mutation (from FluentDocument.updateContent). Compares the current top-level nodes with the pending snapshot, builds a minimal DocumentDelta, and pushes it onto the undo stack.
Implementation
void commitSaveState(FluentDocument document) {
if (_isRestoringState) return;
if (_pending == null) return; // no pending snapshot = nothing to commit
final pending = _pending!;
final newNodes = document.content.nodes;
final oldNodes = pending.oldTopLevelNodes;
final changes = <NodeChange>[];
if (newNodes.length == oldNodes.length) {
// Same node count: fast path for Paragraphs using text-length check.
// Only serialise nodes whose text changed or whose text is the same
// but styles may have changed (rare).
for (int i = 0; i < oldNodes.length; i++) {
final oldJson = oldNodes[i];
final newNode = newNodes[i];
// Fast path: Paragraphs whose text length changed are definitely dirty.
if (newNode is Paragraph) {
final oldText = oldJson['text'] as String? ?? '';
if (newNode.text.length != oldText.length || newNode.text != oldText) {
changes.add(NodeChange(
index: i,
oldJson: oldJson,
newJson: newNode.toJson(),
));
continue;
}
// Text is identical: check alignment/indent/styles quickly.
if (oldJson['textAlign'] != newNode.textAlign ||
oldJson['indent'] != newNode.indent ||
oldJson['styleName'] != newNode.styleName) {
changes.add(NodeChange(
index: i,
oldJson: oldJson,
newJson: newNode.toJson(),
));
continue;
}
// Paragraph text and meta are identical: skip serialisation.
continue;
}
// Non-paragraph nodes: serialise and compare (rare case).
final newJson = newNode.toJson();
if (!_mapsEqual(oldJson, newJson)) {
changes.add(NodeChange(
index: i,
oldJson: oldJson,
newJson: newJson,
));
}
}
} else {
// Node count changed: must compare by serialising each new node.
final maxLen = oldNodes.length > newNodes.length
? oldNodes.length
: newNodes.length;
for (int i = 0; i < maxLen; i++) {
final oldJson = i < oldNodes.length ? oldNodes[i] : null;
final newJson = i < newNodes.length ? newNodes[i].toJson() : null;
if (oldJson == null || newJson == null || !_mapsEqual(oldJson, newJson)) {
changes.add(NodeChange(
index: i,
oldJson: oldJson ?? <String, dynamic>{},
newJson: newJson ?? <String, dynamic>{},
));
}
}
}
// If nothing actually changed, discard the pending snapshot.
if (changes.isEmpty) {
_pending = null;
return;
}
final delta = NodeReplaceDelta(
description: pending.description,
timestamp: pending.timestamp,
changes: changes,
oldCursor: pending.oldCursor,
newCursor: CursorSnapshot.fromDocument(document),
);
_undoStack.add(delta);
_redoStack.clear();
_pending = null;
_enforceMemoryLimit();
}