Ver Fonte

fix: redo/undo error

Vincent Chan há 2 anos atrás
pai
commit
cfdd891991

+ 6 - 3
frontend/app_flowy/packages/appflowy_editor/lib/src/operation/transaction_builder.dart

@@ -10,6 +10,7 @@ import 'package:appflowy_editor/src/document/text_delta.dart';
 import 'package:appflowy_editor/src/editor_state.dart';
 import 'package:appflowy_editor/src/editor_state.dart';
 import 'package:appflowy_editor/src/operation/operation.dart';
 import 'package:appflowy_editor/src/operation/operation.dart';
 import 'package:appflowy_editor/src/operation/transaction.dart';
 import 'package:appflowy_editor/src/operation/transaction.dart';
+import 'package:logging/logging.dart';
 
 
 /// A [TransactionBuilder] is used to build the transaction from the state.
 /// A [TransactionBuilder] is used to build the transaction from the state.
 /// It will save a snapshot of the cursor selection state automatically.
 /// It will save a snapshot of the cursor selection state automatically.
@@ -193,7 +194,7 @@ class TransactionBuilder {
   ///
   ///
   /// Also, this method will transform the path of the operations
   /// Also, this method will transform the path of the operations
   /// to avoid conflicts.
   /// to avoid conflicts.
-  add(Operation op) {
+  add(Operation op, {bool transform = true}) {
     final Operation? last = operations.isEmpty ? null : operations.last;
     final Operation? last = operations.isEmpty ? null : operations.last;
     if (last != null) {
     if (last != null) {
       if (op is TextEditOperation &&
       if (op is TextEditOperation &&
@@ -208,8 +209,10 @@ class TransactionBuilder {
         return;
         return;
       }
       }
     }
     }
-    for (var i = 0; i < operations.length; i++) {
-      op = transformOperation(operations[i], op);
+    if (transform) {
+      for (var i = 0; i < operations.length; i++) {
+        op = transformOperation(operations[i], op);
+      }
     }
     }
     if (op is TextEditOperation && op.delta.isEmpty) {
     if (op is TextEditOperation && op.delta.isEmpty) {
       return;
       return;

+ 13 - 11
frontend/app_flowy/packages/appflowy_editor/lib/src/undo_manager.dart

@@ -43,7 +43,7 @@ class HistoryItem extends LinkedListEntry<HistoryItem> {
     for (var i = operations.length - 1; i >= 0; i--) {
     for (var i = operations.length - 1; i >= 0; i--) {
       final operation = operations[i];
       final operation = operations[i];
       final inverted = operation.invert();
       final inverted = operation.invert();
-      builder.add(inverted);
+      builder.add(inverted, transform: false);
     }
     }
     builder.afterSelection = beforeSelection;
     builder.afterSelection = beforeSelection;
     builder.beforeSelection = afterSelection;
     builder.beforeSelection = afterSelection;
@@ -123,11 +123,12 @@ class UndoManager {
     }
     }
     final transaction = historyItem.toTransaction(s);
     final transaction = historyItem.toTransaction(s);
     s.apply(
     s.apply(
-        transaction,
-        const ApplyOptions(
-          recordUndo: false,
-          recordRedo: true,
-        ));
+      transaction,
+      const ApplyOptions(
+        recordUndo: false,
+        recordRedo: true,
+      ),
+    );
   }
   }
 
 
   redo() {
   redo() {
@@ -142,10 +143,11 @@ class UndoManager {
     }
     }
     final transaction = historyItem.toTransaction(s);
     final transaction = historyItem.toTransaction(s);
     s.apply(
     s.apply(
-        transaction,
-        const ApplyOptions(
-          recordUndo: true,
-          recordRedo: false,
-        ));
+      transaction,
+      const ApplyOptions(
+        recordUndo: true,
+        recordRedo: false,
+      ),
+    );
   }
   }
 }
 }

+ 76 - 0
frontend/app_flowy/packages/appflowy_editor/test/legacy/undo_manager_test.dart

@@ -0,0 +1,76 @@
+import 'dart:collection';
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:appflowy_editor/src/undo_manager.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() async {
+  setUpAll(() {
+    TestWidgetsFlutterBinding.ensureInitialized();
+  });
+
+  Node _createEmptyEditorRoot() {
+    return Node(
+      type: 'editor',
+      children: LinkedList(),
+      attributes: {},
+    );
+  }
+
+  test("HistoryItem #1", () {
+    final document = StateTree(root: _createEmptyEditorRoot());
+    final editorState = EditorState(document: document);
+
+    final historyItem = HistoryItem();
+    historyItem.add(DeleteOperation(
+        [0], [TextNode(type: 'text', delta: Delta()..insert('0'))]));
+    historyItem.add(DeleteOperation(
+        [0], [TextNode(type: 'text', delta: Delta()..insert('1'))]));
+    historyItem.add(DeleteOperation(
+        [0], [TextNode(type: 'text', delta: Delta()..insert('2'))]));
+
+    final transaction = historyItem.toTransaction(editorState);
+    assert(isInsertAndPathEqual(transaction.operations[0], [0], '2'));
+    assert(isInsertAndPathEqual(transaction.operations[1], [0], '1'));
+    assert(isInsertAndPathEqual(transaction.operations[2], [0], '0'));
+  });
+
+  test("HistoryItem #2", () {
+    final document = StateTree(root: _createEmptyEditorRoot());
+    final editorState = EditorState(document: document);
+
+    final historyItem = HistoryItem();
+    historyItem.add(DeleteOperation(
+        [0], [TextNode(type: 'text', delta: Delta()..insert('0'))]));
+    historyItem
+        .add(UpdateOperation([0], {"subType": "number"}, {"subType": null}));
+    historyItem.add(DeleteOperation([0], [TextNode.empty(), TextNode.empty()]));
+    historyItem.add(DeleteOperation([0], [TextNode.empty()]));
+
+    final transaction = historyItem.toTransaction(editorState);
+    assert(isInsertAndPathEqual(transaction.operations[0], [0]));
+    assert(isInsertAndPathEqual(transaction.operations[1], [0]));
+    assert(transaction.operations[2] is UpdateOperation);
+    assert(isInsertAndPathEqual(transaction.operations[3], [0], '0'));
+  });
+}
+
+bool isInsertAndPathEqual(Operation operation, Path path, [String? content]) {
+  if (operation is! InsertOperation) {
+    return false;
+  }
+
+  if (!pathEquals(operation.path, path)) {
+    return false;
+  }
+
+  final firstNode = operation.nodes[0];
+  if (firstNode is! TextNode) {
+    return false;
+  }
+
+  if (content == null) {
+    return true;
+  }
+
+  return firstNode.delta.toRawString() == content;
+}