commitSaveState method

void commitSaveState(
  1. FluentDocument document
)

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();
}