Jelajahi Sumber

refactor: move transaction.dart to core/transform

Lucas.Xu 2 tahun lalu
induk
melakukan
19bf8e3b7a
28 mengubah file dengan 566 tambahan dan 585 penghapusan
  1. 13 14
      frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/code_block_node_widget.dart
  2. 9 9
      frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/horizontal_rule_node_widget.dart
  3. 13 15
      frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/tex_block_node_widget.dart
  4. 3 3
      frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/underscore_to_italic.dart
  5. 1 2
      frontend/app_flowy/packages/appflowy_editor/lib/appflowy_editor.dart
  6. 3 4
      frontend/app_flowy/packages/appflowy_editor/lib/src/commands/edit_text.dart
  7. 10 13
      frontend/app_flowy/packages/appflowy_editor/lib/src/commands/format_text.dart
  8. 267 0
      frontend/app_flowy/packages/appflowy_editor/lib/src/core/transform/transaction.dart
  9. 19 1
      frontend/app_flowy/packages/appflowy_editor/lib/src/editor_state.dart
  10. 0 39
      frontend/app_flowy/packages/appflowy_editor/lib/src/operation/transaction.dart
  11. 0 229
      frontend/app_flowy/packages/appflowy_editor/lib/src/operation/transaction_builder.dart
  12. 10 14
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_builder.dart
  13. 6 7
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_upload_widget.dart
  14. 18 21
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_widget.dart
  15. 3 4
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart
  16. 11 19
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/default_text_operations/format_rich_text_style.dart
  17. 13 15
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart
  18. 43 47
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart
  19. 33 34
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart
  20. 18 18
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart
  21. 13 12
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text.dart
  22. 6 6
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text_handler.dart
  23. 2 3
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/number_list_helper.dart
  24. 4 5
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart
  25. 5 6
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/tab_handler.dart
  26. 13 14
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/whitespace_handler.dart
  27. 3 4
      frontend/app_flowy/packages/appflowy_editor/lib/src/undo_manager.dart
  28. 27 27
      frontend/app_flowy/packages/appflowy_editor/test/legacy/operation_test.dart

+ 13 - 14
frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/code_block_node_widget.dart

@@ -26,9 +26,9 @@ ShortcutEventHandler _enterInCodeBlockHandler = (editorState, event) {
     return KeyEventResult.ignored;
   }
   if (selection.isCollapsed) {
-    TransactionBuilder(editorState)
-      ..insertText(codeBlockNode.first, selection.end.offset, '\n')
-      ..commit();
+    editorState.transaction
+        .insertText(codeBlockNode.first, selection.end.offset, '\n');
+    editorState.commit();
     return KeyEventResult.handled;
   }
   return KeyEventResult.ignored;
@@ -61,16 +61,16 @@ SelectionMenuItem codeBlockMenuItem = SelectionMenuItem(
       return;
     }
     if (textNodes.first.toPlainText().isEmpty) {
-      TransactionBuilder(editorState)
+      editorState.transaction
         ..updateNode(textNodes.first, {
           'subtype': 'code_block',
           'theme': 'vs',
           'language': null,
         })
-        ..afterSelection = selection
-        ..commit();
+        ..afterSelection = selection;
+      editorState.commit();
     } else {
-      TransactionBuilder(editorState)
+      editorState.transaction
         ..insertNode(
           selection.end.path.next,
           TextNode(
@@ -83,8 +83,8 @@ SelectionMenuItem codeBlockMenuItem = SelectionMenuItem(
             delta: Delta()..insert('\n'),
           ),
         )
-        ..afterSelection = selection
-        ..commit();
+        ..afterSelection = selection;
+      editorState.commit();
     }
   },
 );
@@ -181,11 +181,10 @@ class __CodeBlockNodeWidgeState extends State<_CodeBlockNodeWidge>
       child: DropdownButton<String>(
         value: _detectLanguage,
         onChanged: (value) {
-          TransactionBuilder(widget.editorState)
-            ..updateNode(widget.textNode, {
-              'language': value,
-            })
-            ..commit();
+          widget.editorState.transaction.updateNode(widget.textNode, {
+            'language': value,
+          });
+          widget.editorState.commit();
         },
         items: allLanguages.keys.map<DropdownMenuItem<String>>((String value) {
           return DropdownMenuItem<String>(

+ 9 - 9
frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/horizontal_rule_node_widget.dart

@@ -18,7 +18,7 @@ ShortcutEventHandler _insertHorzaontalRule = (editorState, event) {
   }
   final textNode = textNodes.first;
   if (textNode.toPlainText() == '--') {
-    TransactionBuilder(editorState)
+    editorState.transaction
       ..deleteText(textNode, 0, 2)
       ..insertNode(
         textNode.path,
@@ -29,8 +29,8 @@ ShortcutEventHandler _insertHorzaontalRule = (editorState, event) {
         ),
       )
       ..afterSelection =
-          Selection.single(path: textNode.path.next, startOffset: 0)
-      ..commit();
+          Selection.single(path: textNode.path.next, startOffset: 0);
+    editorState.commit();
     return KeyEventResult.handled;
   }
   return KeyEventResult.ignored;
@@ -54,7 +54,7 @@ SelectionMenuItem horizontalRuleMenuItem = SelectionMenuItem(
     }
     final textNode = textNodes.first;
     if (textNode.toPlainText().isEmpty) {
-      TransactionBuilder(editorState)
+      editorState.transaction
         ..insertNode(
           textNode.path,
           Node(
@@ -64,10 +64,10 @@ SelectionMenuItem horizontalRuleMenuItem = SelectionMenuItem(
           ),
         )
         ..afterSelection =
-            Selection.single(path: textNode.path.next, startOffset: 0)
-        ..commit();
+            Selection.single(path: textNode.path.next, startOffset: 0);
+      editorState.commit();
     } else {
-      TransactionBuilder(editorState)
+      editorState.transaction
         ..insertNode(
           selection.end.path.next,
           TextNode(
@@ -78,8 +78,8 @@ SelectionMenuItem horizontalRuleMenuItem = SelectionMenuItem(
             delta: Delta()..insert('---'),
           ),
         )
-        ..afterSelection = selection
-        ..commit();
+        ..afterSelection = selection;
+      editorState.commit();
     }
   },
 );

+ 13 - 15
frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/tex_block_node_widget.dart

@@ -23,7 +23,7 @@ SelectionMenuItem teXBlockMenuItem = SelectionMenuItem(
     final Path texNodePath;
     if (textNodes.first.toPlainText().isEmpty) {
       texNodePath = selection.end.path;
-      TransactionBuilder(editorState)
+      editorState.transaction
         ..insertNode(
           selection.end.path,
           Node(
@@ -33,11 +33,11 @@ SelectionMenuItem teXBlockMenuItem = SelectionMenuItem(
           ),
         )
         ..deleteNode(textNodes.first)
-        ..afterSelection = selection
-        ..commit();
+        ..afterSelection = selection;
+      editorState.commit();
     } else {
       texNodePath = selection.end.path.next;
-      TransactionBuilder(editorState)
+      editorState.transaction
         ..insertNode(
           selection.end.path.next,
           Node(
@@ -46,8 +46,8 @@ SelectionMenuItem teXBlockMenuItem = SelectionMenuItem(
             attributes: {'tex': ''},
           ),
         )
-        ..afterSelection = selection
-        ..commit();
+        ..afterSelection = selection;
+      editorState.commit();
     }
     WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
       final texState =
@@ -142,9 +142,8 @@ class __TeXBlockNodeWidgetState extends State<_TeXBlockNodeWidget> {
           size: 16,
         ),
         onPressed: () {
-          TransactionBuilder(widget.editorState)
-            ..deleteNode(widget.node)
-            ..commit();
+          widget.editorState.transaction.deleteNode(widget.node);
+          widget.editorState.commit();
         },
       ),
     );
@@ -175,12 +174,11 @@ class __TeXBlockNodeWidgetState extends State<_TeXBlockNodeWidget> {
               onPressed: () {
                 Navigator.of(context).pop();
                 if (controller.text != _tex) {
-                  TransactionBuilder(widget.editorState)
-                    ..updateNode(
-                      widget.node,
-                      {'tex': controller.text},
-                    )
-                    ..commit();
+                  widget.editorState.transaction.updateNode(
+                    widget.node,
+                    {'tex': controller.text},
+                  );
+                  widget.editorState.commit();
                 }
               },
               child: const Text('OK'),

+ 3 - 3
frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/underscore_to_italic.dart

@@ -31,7 +31,7 @@ ShortcutEventHandler _underscoreToItalicHandler = (editorState, event) {
   // Delete the previous 'underscore',
   // update the style of the text surrounded by the two underscores to 'italic',
   // and update the cursor position.
-  TransactionBuilder(editorState)
+  editorState.transaction
     ..deleteText(textNode, firstUnderscore, 1)
     ..formatText(
       textNode,
@@ -46,8 +46,8 @@ ShortcutEventHandler _underscoreToItalicHandler = (editorState, event) {
         path: textNode.path,
         offset: selection.end.offset - 1,
       ),
-    )
-    ..commit();
+    );
+  editorState.commit();
 
   return KeyEventResult.handled;
 };

+ 1 - 2
frontend/app_flowy/packages/appflowy_editor/lib/appflowy_editor.dart

@@ -13,8 +13,7 @@ export 'src/core/document/attributes.dart';
 export 'src/core/legacy/built_in_attribute_keys.dart';
 export 'src/editor_state.dart';
 export 'src/core/transform/operation.dart';
-export 'src/operation/transaction.dart';
-export 'src/operation/transaction_builder.dart';
+export 'src/core/transform/transaction.dart';
 export 'src/render/selection/selectable.dart';
 export 'src/service/editor_service.dart';
 export 'src/service/render_plugin_service.dart';

+ 3 - 4
frontend/app_flowy/packages/appflowy_editor/lib/src/commands/edit_text.dart

@@ -4,7 +4,7 @@ import 'package:appflowy_editor/src/commands/text_command_infra.dart';
 import 'package:appflowy_editor/src/core/document/node.dart';
 import 'package:appflowy_editor/src/core/document/path.dart';
 import 'package:appflowy_editor/src/editor_state.dart';
-import 'package:appflowy_editor/src/operation/transaction_builder.dart';
+import 'package:appflowy_editor/src/core/transform/transaction.dart';
 import 'package:flutter/widgets.dart';
 
 Future<void> insertContextInText(
@@ -22,9 +22,8 @@ Future<void> insertContextInText(
 
   final completer = Completer<void>();
 
-  TransactionBuilder(editorState)
-    ..insertText(result, index, content)
-    ..commit();
+  editorState.transaction.insertText(result, index, content);
+  editorState.commit();
 
   WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
     completer.complete();

+ 10 - 13
frontend/app_flowy/packages/appflowy_editor/lib/src/commands/format_text.dart

@@ -6,7 +6,7 @@ import 'package:appflowy_editor/src/core/document/node.dart';
 import 'package:appflowy_editor/src/core/document/path.dart';
 import 'package:appflowy_editor/src/core/location/selection.dart';
 import 'package:appflowy_editor/src/editor_state.dart';
-import 'package:appflowy_editor/src/operation/transaction_builder.dart';
+import 'package:appflowy_editor/src/core/transform/transaction.dart';
 import 'package:flutter/widgets.dart';
 
 Future<void> updateTextNodeAttributes(
@@ -23,9 +23,8 @@ Future<void> updateTextNodeAttributes(
 
   final completer = Completer<void>();
 
-  TransactionBuilder(editorState)
-    ..updateNode(result, attributes)
-    ..commit();
+  editorState.transaction.updateNode(result, attributes);
+  editorState.commit();
 
   WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
     completer.complete();
@@ -49,15 +48,13 @@ Future<void> updateTextNodeDeltaAttributes(
   final newSelection = getSelection(editorState, selection: selection);
 
   final completer = Completer<void>();
-
-  TransactionBuilder(editorState)
-    ..formatText(
-      result,
-      newSelection.startIndex,
-      newSelection.length,
-      attributes,
-    )
-    ..commit();
+  editorState.transaction.formatText(
+    result,
+    newSelection.startIndex,
+    newSelection.length,
+    attributes,
+  );
+  editorState.commit();
 
   WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
     completer.complete();

+ 267 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/core/transform/transaction.dart

@@ -0,0 +1,267 @@
+import 'dart:math';
+
+import 'package:appflowy_editor/src/core/document/attributes.dart';
+import 'package:appflowy_editor/src/core/document/document.dart';
+import 'package:appflowy_editor/src/core/document/node.dart';
+import 'package:appflowy_editor/src/core/document/path.dart';
+import 'package:appflowy_editor/src/core/document/text_delta.dart';
+import 'package:appflowy_editor/src/core/location/position.dart';
+import 'package:appflowy_editor/src/core/location/selection.dart';
+import 'package:appflowy_editor/src/core/transform/operation.dart';
+
+/// A [Transaction] has a list of [Operation] objects that will be applied
+/// to the editor.
+///
+/// There will be several ways to consume the transaction:
+/// 1. Apply to the state to update the UI.
+/// 2. Send to the backend to store and do operation transforming.
+class Transaction {
+  Transaction({
+    required this.document,
+  });
+
+  final Document document;
+
+  /// The operations to be applied.
+  final List<Operation> operations = [];
+
+  /// The selection to be applied.
+  Selection? afterSelection;
+
+  /// The before selection is to be recovered if needed.
+  Selection? beforeSelection;
+
+  /// Inserts the [Node] at the given [Path].
+  void insertNode(
+    Path path,
+    Node node, {
+    bool deepCopy = true,
+  }) {
+    insertNodes(path, [node], deepCopy: deepCopy);
+  }
+
+  /// Inserts a sequence of [Node]s at the given [Path].
+  void insertNodes(
+    Path path,
+    Iterable<Node> nodes, {
+    bool deepCopy = true,
+  }) {
+    if (deepCopy) {
+      add(InsertOperation(path, nodes.map((e) => e.copyWith())));
+    } else {
+      add(InsertOperation(path, nodes));
+    }
+  }
+
+  /// Updates the attributes of the [Node].
+  ///
+  /// The [attributes] will be merged into the existing attributes.
+  void updateNode(Node node, Attributes attributes) {
+    final inverted = invertAttributes(node.attributes, attributes);
+    add(UpdateOperation(
+      node.path,
+      {...attributes},
+      inverted,
+    ));
+  }
+
+  /// Deletes the [Node] in the document.
+  void deleteNode(Node node) {
+    deleteNodesAtPath(node.path);
+  }
+
+  /// Deletes the [Node]s in the document.
+  void deleteNodes(Iterable<Node> nodes) {
+    nodes.forEach(deleteNode);
+  }
+
+  /// Deletes the [Node]s at the given [Path].
+  ///
+  /// The [length] indicates the number of consecutive deletions,
+  ///   including the node of the current path.
+  void deleteNodesAtPath(Path path, [int length = 1]) {
+    if (path.isEmpty) return;
+    final nodes = <Node>[];
+    final parent = path.parent;
+    for (var i = 0; i < length; i++) {
+      final node = document.nodeAtPath(parent + [path.last + i]);
+      if (node == null) {
+        break;
+      }
+      nodes.add(node);
+    }
+    add(DeleteOperation(path, nodes));
+  }
+
+  /// Update the [TextNode]s with the given [Delta].
+  void updateText(TextNode textNode, Delta delta) {
+    final inverted = delta.invert(textNode.delta);
+    add(UpdateTextOperation(textNode.path, delta, inverted));
+  }
+
+  /// Returns the JSON representation of the transaction.
+  Map<String, dynamic> toJson() {
+    final json = <String, dynamic>{};
+    if (operations.isNotEmpty) {
+      json['operations'] = operations.map((o) => o.toJson());
+    }
+    if (afterSelection != null) {
+      json['after_selection'] = afterSelection!.toJson();
+    }
+    if (beforeSelection != null) {
+      json['before_selection'] = beforeSelection!.toJson();
+    }
+    return json;
+  }
+
+  /// Adds an operation to the transaction.
+  /// This method will merge operations if they are both TextEdits.
+  ///
+  /// Also, this method will transform the path of the operations
+  /// to avoid conflicts.
+  add(Operation op, {bool transform = true}) {
+    final Operation? last = operations.isEmpty ? null : operations.last;
+    if (last != null) {
+      if (op is UpdateTextOperation &&
+          last is UpdateTextOperation &&
+          op.path.equals(last.path)) {
+        final newOp = UpdateTextOperation(
+          op.path,
+          last.delta.compose(op.delta),
+          op.inverted.compose(last.inverted),
+        );
+        operations[operations.length - 1] = newOp;
+        return;
+      }
+    }
+    if (transform) {
+      for (var i = 0; i < operations.length; i++) {
+        op = transformOperation(operations[i], op);
+      }
+    }
+    if (op is UpdateTextOperation && op.delta.isEmpty) {
+      return;
+    }
+    operations.add(op);
+  }
+}
+
+extension TextTransaction on Transaction {
+  void mergeText(
+    TextNode first,
+    TextNode second, {
+    int? firstOffset,
+    int secondOffset = 0,
+  }) {
+    final firstLength = first.delta.length;
+    final secondLength = second.delta.length;
+    firstOffset ??= firstLength;
+    updateText(
+      first,
+      Delta()
+        ..retain(firstOffset)
+        ..delete(firstLength - firstOffset)
+        ..addAll(second.delta.slice(secondOffset, secondLength)),
+    );
+    afterSelection = Selection.collapsed(Position(
+      path: first.path,
+      offset: firstOffset,
+    ));
+  }
+
+  /// Inserts the text content at a specified index.
+  ///
+  /// Optionally, you may specify formatting attributes that are applied to the inserted string.
+  /// By default, the formatting attributes before the insert position will be reused.
+  void insertText(
+    TextNode textNode,
+    int index,
+    String text, {
+    Attributes? attributes,
+  }) {
+    var newAttributes = attributes;
+    if (index != 0 && attributes == null) {
+      newAttributes =
+          textNode.delta.slice(max(index - 1, 0), index).first.attributes;
+      if (newAttributes != null) {
+        newAttributes = {...newAttributes}; // make a copy
+      }
+    }
+    updateText(
+      textNode,
+      Delta()
+        ..retain(index)
+        ..insert(text, attributes: newAttributes),
+    );
+    afterSelection = Selection.collapsed(
+      Position(path: textNode.path, offset: index + text.length),
+    );
+  }
+
+  /// Assigns a formatting attributes to a range of text.
+  formatText(
+    TextNode textNode,
+    int index,
+    int length,
+    Attributes attributes,
+  ) {
+    afterSelection = beforeSelection;
+    updateText(
+      textNode,
+      Delta()
+        ..retain(index)
+        ..retain(length, attributes: attributes),
+    );
+  }
+
+  /// Deletes the text of specified length starting at index.
+  deleteText(
+    TextNode textNode,
+    int index,
+    int length,
+  ) {
+    updateText(
+      textNode,
+      Delta()
+        ..retain(index)
+        ..delete(length),
+    );
+    afterSelection = Selection.collapsed(
+      Position(path: textNode.path, offset: index),
+    );
+  }
+
+  /// Replaces the text of specified length starting at index.
+  ///
+  /// Optionally, you may specify formatting attributes that are applied to the inserted string.
+  /// By default, the formatting attributes before the insert position will be reused.
+  replaceText(
+    TextNode textNode,
+    int index,
+    int length,
+    String text, {
+    Attributes? attributes,
+  }) {
+    var newAttributes = attributes;
+    if (index != 0 && attributes == null) {
+      newAttributes =
+          textNode.delta.slice(max(index - 1, 0), index).first.attributes;
+      if (newAttributes != null) {
+        newAttributes = {...newAttributes}; // make a copy
+      }
+    }
+    updateText(
+      textNode,
+      Delta()
+        ..retain(index)
+        ..delete(length)
+        ..insert(text, attributes: newAttributes),
+    );
+    afterSelection = Selection.collapsed(
+      Position(
+        path: textNode.path,
+        offset: index + text.length,
+      ),
+    );
+  }
+}

+ 19 - 1
frontend/app_flowy/packages/appflowy_editor/lib/src/editor_state.dart

@@ -8,7 +8,7 @@ import 'package:flutter/material.dart';
 import 'package:appflowy_editor/src/core/location/selection.dart';
 import 'package:appflowy_editor/src/core/document/document.dart';
 import 'package:appflowy_editor/src/core/transform/operation.dart';
-import 'package:appflowy_editor/src/operation/transaction.dart';
+import 'package:appflowy_editor/src/core/transform/transaction.dart';
 import 'package:appflowy_editor/src/undo_manager.dart';
 
 class ApplyOptions {
@@ -74,6 +74,24 @@ class EditorState {
 
   bool editable = true;
 
+  Transaction get transaction {
+    if (_transaction != null) {
+      return _transaction!;
+    }
+    _transaction = Transaction(document: document);
+    _transaction!.beforeSelection = _cursorSelection;
+    return _transaction!;
+  }
+
+  Transaction? _transaction;
+
+  void commit() {
+    if (_transaction != null) {
+      apply(_transaction!, const ApplyOptions(recordUndo: true));
+      _transaction = null;
+    }
+  }
+
   Selection? get cursorSelection {
     return _cursorSelection;
   }

+ 0 - 39
frontend/app_flowy/packages/appflowy_editor/lib/src/operation/transaction.dart

@@ -1,39 +0,0 @@
-import 'dart:collection';
-import 'package:flutter/material.dart';
-import 'package:appflowy_editor/src/core/location/selection.dart';
-import '../core/transform/operation.dart';
-
-/// A [Transaction] has a list of [Operation] objects that will be applied
-/// to the editor. It is an immutable class and used to store and transmit.
-///
-/// If you want to build a new [Transaction], use [TransactionBuilder] directly.
-///
-/// There will be several ways to consume the transaction:
-/// 1. Apply to the state to update the UI.
-/// 2. Send to the backend to store and do operation transforming.
-/// 3. Used by the UndoManager to implement redo/undo.
-@immutable
-class Transaction {
-  final UnmodifiableListView<Operation> operations;
-  final Selection? beforeSelection;
-  final Selection? afterSelection;
-
-  const Transaction({
-    required this.operations,
-    this.beforeSelection,
-    this.afterSelection,
-  });
-
-  Map<String, dynamic> toJson() {
-    final Map<String, dynamic> result = {
-      "operations": operations.map((e) => e.toJson()),
-    };
-    if (beforeSelection != null) {
-      result["beforeSelection"] = beforeSelection!.toJson();
-    }
-    if (afterSelection != null) {
-      result["afterSelection"] = afterSelection!.toJson();
-    }
-    return result;
-  }
-}

+ 0 - 229
frontend/app_flowy/packages/appflowy_editor/lib/src/operation/transaction_builder.dart

@@ -1,229 +0,0 @@
-import 'dart:collection';
-import 'dart:math';
-
-import 'package:appflowy_editor/src/core/document/attributes.dart';
-import 'package:appflowy_editor/src/core/document/node.dart';
-import 'package:appflowy_editor/src/core/document/path.dart';
-import 'package:appflowy_editor/src/core/location/position.dart';
-import 'package:appflowy_editor/src/core/location/selection.dart';
-import 'package:appflowy_editor/src/core/document/text_delta.dart';
-import 'package:appflowy_editor/src/editor_state.dart';
-import 'package:appflowy_editor/src/core/transform/operation.dart';
-import 'package:appflowy_editor/src/operation/transaction.dart';
-
-/// A [TransactionBuilder] is used to build the transaction from the state.
-/// It will save a snapshot of the cursor selection state automatically.
-/// The cursor can be restored if the transaction is undo.
-class TransactionBuilder {
-  final List<Operation> operations = [];
-  EditorState state;
-  Selection? beforeSelection;
-  Selection? afterSelection;
-
-  TransactionBuilder(this.state);
-
-  /// Commits the operations to the state
-  Future<void> commit() async {
-    final transaction = finish();
-    state.apply(transaction);
-  }
-
-  /// Inserts the nodes at the position of path.
-  insertNode(Path path, Node node) {
-    insertNodes(path, [node]);
-  }
-
-  /// Inserts a sequence of nodes at the position of path.
-  insertNodes(Path path, List<Node> nodes) {
-    beforeSelection = state.cursorSelection;
-    add(InsertOperation(path, nodes.map((node) => node.copyWith()).toList()));
-  }
-
-  /// Updates the attributes of nodes.
-  updateNode(Node node, Attributes attributes) {
-    beforeSelection = state.cursorSelection;
-    final inverted = invertAttributes(node.attributes, attributes);
-    add(UpdateOperation(
-      node.path,
-      {...attributes},
-      inverted,
-    ));
-  }
-
-  /// Deletes a node in the document.
-  deleteNode(Node node) {
-    deleteNodesAtPath(node.path);
-  }
-
-  deleteNodes(List<Node> nodes) {
-    nodes.forEach(deleteNode);
-  }
-
-  /// Deletes a sequence of nodes at the path of the document.
-  /// The length specifies the length of the following nodes to delete(
-  /// including the start one).
-  deleteNodesAtPath(Path path, [int length = 1]) {
-    if (path.isEmpty) {
-      return;
-    }
-    final nodes = <Node>[];
-    final prefix = path.sublist(0, path.length - 1);
-    final last = path.last;
-    for (var i = 0; i < length; i++) {
-      final node = state.document.nodeAtPath(prefix + [last + i])!;
-      nodes.add(node);
-    }
-
-    add(DeleteOperation(path, nodes.map((node) => node.copyWith()).toList()));
-  }
-
-  textEdit(TextNode node, Delta Function() f) {
-    beforeSelection = state.cursorSelection;
-    final path = node.path;
-
-    final delta = f();
-
-    final inverted = delta.invert(node.delta);
-
-    add(UpdateTextOperation(path, delta, inverted));
-  }
-
-  setAfterSelection(Selection sel) {
-    afterSelection = sel;
-  }
-
-  mergeText(TextNode firstNode, TextNode secondNode,
-      {int? firstOffset, int secondOffset = 0}) {
-    final firstLength = firstNode.delta.length;
-    final secondLength = secondNode.delta.length;
-    textEdit(
-      firstNode,
-      () => Delta()
-        ..retain(firstOffset ?? firstLength)
-        ..delete(firstLength - (firstOffset ?? firstLength))
-        ..addAll(secondNode.delta.slice(secondOffset, secondLength)),
-    );
-    afterSelection = Selection.collapsed(
-      Position(
-        path: firstNode.path,
-        offset: firstOffset ?? firstLength,
-      ),
-    );
-  }
-
-  /// Inserts content at a specified index.
-  /// Optionally, you may specify formatting attributes that are applied to the inserted string.
-  /// By default, the formatting attributes before the insert position will be used.
-  insertText(
-    TextNode node,
-    int index,
-    String content, {
-    Attributes? attributes,
-  }) {
-    var newAttributes = attributes;
-    if (index != 0 && attributes == null) {
-      newAttributes =
-          node.delta.slice(max(index - 1, 0), index).first.attributes;
-      if (newAttributes != null) {
-        newAttributes = Attributes.from(newAttributes);
-      }
-    }
-    textEdit(
-      node,
-      () => Delta()
-        ..retain(index)
-        ..insert(
-          content,
-          attributes: newAttributes,
-        ),
-    );
-    afterSelection = Selection.collapsed(
-      Position(path: node.path, offset: index + content.length),
-    );
-  }
-
-  /// Assigns formatting attributes to a range of text.
-  formatText(TextNode node, int index, int length, Attributes attributes) {
-    textEdit(
-        node,
-        () => Delta()
-          ..retain(index)
-          ..retain(length, attributes: attributes));
-    afterSelection = beforeSelection;
-  }
-
-  /// Deletes length characters starting from index.
-  deleteText(TextNode node, int index, int length) {
-    textEdit(
-        node,
-        () => Delta()
-          ..retain(index)
-          ..delete(length));
-    afterSelection =
-        Selection.collapsed(Position(path: node.path, offset: index));
-  }
-
-  replaceText(TextNode node, int index, int length, String content,
-      [Attributes? attributes]) {
-    var newAttributes = attributes;
-    if (attributes == null) {
-      final ops = node.delta.slice(index, index + length);
-      if (ops.isNotEmpty) {
-        newAttributes = ops.first.attributes;
-      }
-    }
-    textEdit(
-      node,
-      () => Delta()
-        ..retain(index)
-        ..delete(length)
-        ..insert(content, attributes: newAttributes),
-    );
-    afterSelection = Selection.collapsed(
-      Position(
-        path: node.path,
-        offset: index + content.length,
-      ),
-    );
-  }
-
-  /// Adds an operation to the transaction.
-  /// This method will merge operations if they are both TextEdits.
-  ///
-  /// Also, this method will transform the path of the operations
-  /// to avoid conflicts.
-  add(Operation op, {bool transform = true}) {
-    final Operation? last = operations.isEmpty ? null : operations.last;
-    if (last != null) {
-      if (op is UpdateTextOperation &&
-          last is UpdateTextOperation &&
-          op.path.equals(last.path)) {
-        final newOp = UpdateTextOperation(
-          op.path,
-          last.delta.compose(op.delta),
-          op.inverted.compose(last.inverted),
-        );
-        operations[operations.length - 1] = newOp;
-        return;
-      }
-    }
-    if (transform) {
-      for (var i = 0; i < operations.length; i++) {
-        op = transformOperation(operations[i], op);
-      }
-    }
-    if (op is UpdateTextOperation && op.delta.isEmpty) {
-      return;
-    }
-    operations.add(op);
-  }
-
-  /// Generates a immutable [Transaction] to apply or transmit.
-  Transaction finish() {
-    return Transaction(
-      operations: UnmodifiableListView(operations),
-      beforeSelection: beforeSelection,
-      afterSelection: afterSelection,
-    );
-  }
-}

+ 10 - 14
frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_builder.dart

@@ -1,5 +1,4 @@
 import 'package:appflowy_editor/src/core/document/node.dart';
-import 'package:appflowy_editor/src/operation/transaction_builder.dart';
 import 'package:appflowy_editor/src/service/render_plugin_service.dart';
 import 'package:flutter/material.dart';
 import 'package:rich_clipboard/rich_clipboard.dart';
@@ -25,23 +24,20 @@ class ImageNodeBuilder extends NodeWidgetBuilder<Node> {
         RichClipboard.setData(RichClipboardData(text: src));
       },
       onDelete: () {
-        TransactionBuilder(context.editorState)
-          ..deleteNode(context.node)
-          ..commit();
+        context.editorState.transaction.deleteNode(context.node);
+        context.editorState.commit();
       },
       onAlign: (alignment) {
-        TransactionBuilder(context.editorState)
-          ..updateNode(context.node, {
-            'align': _alignmentToText(alignment),
-          })
-          ..commit();
+        context.editorState.transaction.updateNode(context.node, {
+          'align': _alignmentToText(alignment),
+        });
+        context.editorState.commit();
       },
       onResize: (width) {
-        TransactionBuilder(context.editorState)
-          ..updateNode(context.node, {
-            'width': width,
-          })
-          ..commit();
+        context.editorState.transaction.updateNode(context.node, {
+          'width': width,
+        });
+        context.editorState.commit();
       },
     );
   }

+ 6 - 7
frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_upload_widget.dart

@@ -3,7 +3,7 @@ import 'dart:collection';
 import 'package:appflowy_editor/src/core/document/node.dart';
 import 'package:appflowy_editor/src/editor_state.dart';
 import 'package:appflowy_editor/src/infra/flowy_svg.dart';
-import 'package:appflowy_editor/src/operation/transaction_builder.dart';
+import 'package:appflowy_editor/src/core/transform/transaction.dart';
 import 'package:appflowy_editor/src/render/selection_menu/selection_menu_service.dart';
 import 'package:flutter/material.dart';
 
@@ -192,11 +192,10 @@ extension on EditorState {
         'align': 'center',
       },
     );
-    TransactionBuilder(this)
-      ..insertNode(
-        selection.start.path,
-        imageNode,
-      )
-      ..commit();
+    transaction.insertNode(
+      selection.start.path,
+      imageNode,
+    );
+    commit();
   }
 }

+ 18 - 21
frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_widget.dart

@@ -45,13 +45,12 @@ class SelectionMenuItem {
       final node = nodes.first as TextNode;
       final end = selection.start.offset;
       final start = node.toPlainText().substring(0, end).lastIndexOf('/');
-      TransactionBuilder(editorState)
-        ..deleteText(
-          node,
-          start,
-          selection.start.offset - start,
-        )
-        ..commit();
+      editorState.transaction.deleteText(
+        node,
+        start,
+        selection.start.offset - start,
+      );
+      editorState.commit();
     }
   }
 }
@@ -278,13 +277,12 @@ class _SelectionMenuWidgetState extends State<SelectionMenuWidget> {
     final nodes = selectionService.currentSelectedNodes;
     if (selection != null && nodes.length == 1) {
       widget.onSelectionUpdate();
-      TransactionBuilder(widget.editorState)
-        ..deleteText(
-          nodes.first as TextNode,
-          selection.start.offset - length,
-          length,
-        )
-        ..commit();
+      widget.editorState.transaction.deleteText(
+        nodes.first as TextNode,
+        selection.start.offset - length,
+        length,
+      );
+      widget.editorState.commit();
     }
   }
 
@@ -295,13 +293,12 @@ class _SelectionMenuWidgetState extends State<SelectionMenuWidget> {
         widget.editorState.service.selectionService.currentSelectedNodes;
     if (selection != null && nodes.length == 1) {
       widget.onSelectionUpdate();
-      TransactionBuilder(widget.editorState)
-        ..insertText(
-          nodes.first as TextNode,
-          selection.end.offset,
-          text,
-        )
-        ..commit();
+      widget.editorState.transaction.insertText(
+        nodes.first as TextNode,
+        selection.end.offset,
+        text,
+      );
+      widget.editorState.commit();
     }
   }
 }

+ 3 - 4
frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart

@@ -357,10 +357,9 @@ void showLinkMenu(
             _dismissLinkMenu();
           },
           onRemoveLink: () {
-            TransactionBuilder(editorState)
-              ..formatText(
-                  textNode, index, length, {BuiltInAttributeKey.href: null})
-              ..commit();
+            editorState.transaction.formatText(
+                textNode, index, length, {BuiltInAttributeKey.href: null});
+            editorState.commit();
             _dismissLinkMenu();
           },
           onFocusChange: (value) {

+ 11 - 19
frontend/app_flowy/packages/appflowy_editor/lib/src/service/default_text_operations/format_rich_text_style.dart

@@ -1,12 +1,5 @@
-import 'package:appflowy_editor/src/core/document/attributes.dart';
-import 'package:appflowy_editor/src/core/document/node.dart';
-import 'package:appflowy_editor/src/core/document/path.dart';
-import 'package:appflowy_editor/src/core/location/position.dart';
-import 'package:appflowy_editor/src/core/location/selection.dart';
-import 'package:appflowy_editor/src/editor_state.dart';
+import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:appflowy_editor/src/extensions/text_node_extensions.dart';
-import 'package:appflowy_editor/src/operation/transaction_builder.dart';
-import 'package:appflowy_editor/src/core/legacy/built_in_attribute_keys.dart';
 
 void insertHeadingAfterSelection(EditorState editorState, String heading) {
   insertTextNodeAfterSelection(editorState, {
@@ -54,16 +47,15 @@ bool insertTextNodeAfterSelection(
     formatTextNodes(editorState, attributes);
   } else {
     final next = selection.end.path.next;
-    final builder = TransactionBuilder(editorState);
-    builder
+    editorState.transaction
       ..insertNode(
         next,
         TextNode.empty(attributes: attributes),
       )
       ..afterSelection = Selection.collapsed(
         Position(path: next, offset: 0),
-      )
-      ..commit();
+      );
+    editorState.commit();
   }
 
   return true;
@@ -107,7 +99,7 @@ bool formatTextNodes(EditorState editorState, Attributes attributes) {
     return false;
   }
 
-  final builder = TransactionBuilder(editorState);
+  final transaction = editorState.transaction;
 
   for (final textNode in textNodes) {
     var newAttributes = {...textNode.attributes};
@@ -117,7 +109,7 @@ bool formatTextNodes(EditorState editorState, Attributes attributes) {
       }
     }
     newAttributes.addAll(attributes);
-    builder
+    transaction
       ..updateNode(
         textNode,
         newAttributes,
@@ -130,7 +122,7 @@ bool formatTextNodes(EditorState editorState, Attributes attributes) {
       );
   }
 
-  builder.commit();
+  editorState.commit();
   return true;
 }
 
@@ -216,13 +208,13 @@ bool formatRichTextStyle(EditorState editorState, Attributes attributes) {
     return false;
   }
 
-  final builder = TransactionBuilder(editorState);
+  final transaction = editorState.transaction;
 
   // 1. All nodes are text nodes.
   // 2. The first node is not TextNode.
   // 3. The last node is not TextNode.
   if (nodes.length == textNodes.length && textNodes.length == 1) {
-    builder.formatText(
+    transaction.formatText(
       textNodes.first,
       selection.start.offset,
       selection.end.offset - selection.start.offset,
@@ -239,7 +231,7 @@ bool formatRichTextStyle(EditorState editorState, Attributes attributes) {
       } else if (i == textNodes.length - 1 && textNode == nodes.last) {
         length = selection.end.offset;
       }
-      builder.formatText(
+      transaction.formatText(
         textNode,
         index,
         length,
@@ -248,7 +240,7 @@ bool formatRichTextStyle(EditorState editorState, Attributes attributes) {
     }
   }
 
-  builder.commit();
+  editorState.commit();
 
   return true;
 }

+ 13 - 15
frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart

@@ -1,4 +1,5 @@
 import 'package:appflowy_editor/src/infra/log.dart';
+import 'package:appflowy_editor/src/core/transform/transaction.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
@@ -7,7 +8,6 @@ import 'package:appflowy_editor/src/core/document/node.dart';
 import 'package:appflowy_editor/src/core/location/selection.dart';
 import 'package:appflowy_editor/src/editor_state.dart';
 import 'package:appflowy_editor/src/extensions/node_extensions.dart';
-import 'package:appflowy_editor/src/operation/transaction_builder.dart';
 
 /// [AppFlowyInputService] is responsible for processing text input,
 ///   including text insertion, deletion and replacement.
@@ -160,13 +160,12 @@ class _AppFlowyInputState extends State<AppFlowyInput>
     }
     if (currentSelection.isSingle) {
       final textNode = selectionService.currentSelectedNodes.first as TextNode;
-      TransactionBuilder(_editorState)
-        ..insertText(
-          textNode,
-          delta.insertionOffset,
-          delta.textInserted,
-        )
-        ..commit();
+      _editorState.transaction.insertText(
+        textNode,
+        delta.insertionOffset,
+        delta.textInserted,
+      );
+      _editorState.commit();
     } else {
       // TODO: implement
     }
@@ -181,9 +180,9 @@ class _AppFlowyInputState extends State<AppFlowyInput>
     if (currentSelection.isSingle) {
       final textNode = selectionService.currentSelectedNodes.first as TextNode;
       final length = delta.deletedRange.end - delta.deletedRange.start;
-      TransactionBuilder(_editorState)
-        ..deleteText(textNode, delta.deletedRange.start, length)
-        ..commit();
+      _editorState.transaction
+          .deleteText(textNode, delta.deletedRange.start, length);
+      _editorState.commit();
     } else {
       // TODO: implement
     }
@@ -198,10 +197,9 @@ class _AppFlowyInputState extends State<AppFlowyInput>
     if (currentSelection.isSingle) {
       final textNode = selectionService.currentSelectedNodes.first as TextNode;
       final length = delta.replacedRange.end - delta.replacedRange.start;
-      TransactionBuilder(_editorState)
-        ..replaceText(
-            textNode, delta.replacedRange.start, length, delta.replacementText)
-        ..commit();
+      _editorState.transaction.replaceText(
+          textNode, delta.replacedRange.start, length, delta.replacementText);
+      _editorState.commit();
     } else {
       // TODO: implement
     }

+ 43 - 47
frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart

@@ -28,11 +28,11 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) {
   final List<Node> nonTextNodes =
       nodes.where((node) => node is! TextNode).toList(growable: false);
 
-  final transactionBuilder = TransactionBuilder(editorState);
+  final transaction = editorState.transaction;
   List<int>? cancelNumberListPath;
 
   if (nonTextNodes.isNotEmpty) {
-    transactionBuilder.deleteNodes(nonTextNodes);
+    transaction.deleteNodes(nonTextNodes);
   }
 
   if (textNodes.length == 1) {
@@ -44,7 +44,7 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) {
         if (textNode.subtype == BuiltInAttributeKey.numberList) {
           cancelNumberListPath = textNode.path;
         }
-        transactionBuilder
+        transaction
           ..updateNode(textNode, {
             BuiltInAttributeKey.subtype: null,
             textNode.subtype!: null,
@@ -61,20 +61,20 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) {
         return _backDeleteToPreviousTextNode(
           editorState,
           textNode,
-          transactionBuilder,
+          transaction,
           nonTextNodes,
           selection,
         );
       }
     } else {
       if (selection.isCollapsed) {
-        transactionBuilder.deleteText(
+        transaction.deleteText(
           textNode,
           index,
           selection.start.offset - index,
         );
       } else {
-        transactionBuilder.deleteText(
+        transaction.deleteText(
           textNode,
           selection.start.offset,
           selection.end.offset - selection.start.offset,
@@ -84,33 +84,32 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) {
   } else {
     if (textNodes.isEmpty) {
       if (nonTextNodes.isNotEmpty) {
-        transactionBuilder.afterSelection =
-            Selection.collapsed(selection.start);
+        transaction.afterSelection = Selection.collapsed(selection.start);
       }
-      transactionBuilder.commit();
+      editorState.commit();
       return KeyEventResult.handled;
     }
     final startPosition = selection.start;
     final nodeAtStart = editorState.document.nodeAtPath(startPosition.path)!;
-    _deleteTextNodes(transactionBuilder, textNodes, selection);
-    transactionBuilder.commit();
+    _deleteTextNodes(transaction, textNodes, selection);
+    editorState.commit();
 
     if (nodeAtStart is TextNode &&
         nodeAtStart.subtype == BuiltInAttributeKey.numberList) {
       makeFollowingNodesIncremental(
         editorState,
         startPosition.path,
-        transactionBuilder.afterSelection!,
+        transaction.afterSelection!,
       );
     }
     return KeyEventResult.handled;
   }
 
-  if (transactionBuilder.operations.isNotEmpty) {
+  if (transaction.operations.isNotEmpty) {
     if (nonTextNodes.isNotEmpty) {
-      transactionBuilder.afterSelection = Selection.collapsed(selection.start);
+      transaction.afterSelection = Selection.collapsed(selection.start);
     }
-    transactionBuilder.commit();
+    editorState.commit();
   }
 
   if (cancelNumberListPath != null) {
@@ -128,20 +127,20 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) {
 KeyEventResult _backDeleteToPreviousTextNode(
   EditorState editorState,
   TextNode textNode,
-  TransactionBuilder transactionBuilder,
+  Transaction transaction,
   List<Node> nonTextNodes,
   Selection selection,
 ) {
   if (textNode.next == null &&
       textNode.children.isEmpty &&
       textNode.parent?.parent != null) {
-    transactionBuilder
+    transaction
       ..deleteNode(textNode)
       ..insertNode(textNode.parent!.path.next, textNode)
       ..afterSelection = Selection.collapsed(
         Position(path: textNode.parent!.path.next, offset: 0),
-      )
-      ..commit();
+      );
+    editorState.commit();
     return KeyEventResult.handled;
   }
 
@@ -152,15 +151,15 @@ KeyEventResult _backDeleteToPreviousTextNode(
       prevIsNumberList = true;
     }
 
-    transactionBuilder.mergeText(previousTextNode, textNode);
+    transaction.mergeText(previousTextNode, textNode);
     if (textNode.children.isNotEmpty) {
-      transactionBuilder.insertNodes(
+      transaction.insertNodes(
         previousTextNode.path.next,
         textNode.children.toList(growable: false),
       );
     }
-    transactionBuilder.deleteNode(textNode);
-    transactionBuilder.afterSelection = Selection.collapsed(
+    transaction.deleteNode(textNode);
+    transaction.afterSelection = Selection.collapsed(
       Position(
         path: previousTextNode.path,
         offset: previousTextNode.toPlainText().length,
@@ -168,16 +167,16 @@ KeyEventResult _backDeleteToPreviousTextNode(
     );
   }
 
-  if (transactionBuilder.operations.isNotEmpty) {
+  if (transaction.operations.isNotEmpty) {
     if (nonTextNodes.isNotEmpty) {
-      transactionBuilder.afterSelection = Selection.collapsed(selection.start);
+      transaction.afterSelection = Selection.collapsed(selection.start);
     }
-    transactionBuilder.commit();
+    editorState.commit();
   }
 
   if (prevIsNumberList) {
-    makeFollowingNodesIncremental(editorState, previousTextNode!.path,
-        transactionBuilder.afterSelection!);
+    makeFollowingNodesIncremental(
+        editorState, previousTextNode!.path, transaction.afterSelection!);
   }
 
   return KeyEventResult.handled;
@@ -197,7 +196,7 @@ KeyEventResult _handleDelete(EditorState editorState, RawKeyEvent event) {
     return KeyEventResult.ignored;
   }
 
-  final transactionBuilder = TransactionBuilder(editorState);
+  final transaction = editorState.transaction;
   if (textNodes.length == 1) {
     final textNode = textNodes.first;
     // The cursor is at the end of the line,
@@ -206,55 +205,52 @@ KeyEventResult _handleDelete(EditorState editorState, RawKeyEvent event) {
       return _mergeNextLineIntoThisLine(
         editorState,
         textNode,
-        transactionBuilder,
+        transaction,
         selection,
       );
     }
     final index = textNode.delta.nextRunePosition(selection.start.offset);
     if (selection.isCollapsed) {
-      transactionBuilder.deleteText(
+      transaction.deleteText(
         textNode,
         selection.start.offset,
         index - selection.start.offset,
       );
     } else {
-      transactionBuilder.deleteText(
+      transaction.deleteText(
         textNode,
         selection.start.offset,
         selection.end.offset - selection.start.offset,
       );
     }
-    transactionBuilder.commit();
+    editorState.commit();
   } else {
     final startPosition = selection.start;
     final nodeAtStart = editorState.document.nodeAtPath(startPosition.path)!;
-    _deleteTextNodes(transactionBuilder, textNodes, selection);
-    transactionBuilder.commit();
+    _deleteTextNodes(transaction, textNodes, selection);
+    editorState.commit();
 
     if (nodeAtStart is TextNode &&
         nodeAtStart.subtype == BuiltInAttributeKey.numberList) {
       makeFollowingNodesIncremental(
-          editorState, startPosition.path, transactionBuilder.afterSelection!);
+          editorState, startPosition.path, transaction.afterSelection!);
     }
   }
 
   return KeyEventResult.handled;
 }
 
-KeyEventResult _mergeNextLineIntoThisLine(
-    EditorState editorState,
-    TextNode textNode,
-    TransactionBuilder transactionBuilder,
-    Selection selection) {
+KeyEventResult _mergeNextLineIntoThisLine(EditorState editorState,
+    TextNode textNode, Transaction transaction, Selection selection) {
   final nextNode = textNode.next;
   if (nextNode == null) {
     return KeyEventResult.ignored;
   }
   if (nextNode is TextNode) {
-    transactionBuilder.mergeText(textNode, nextNode);
+    transaction.mergeText(textNode, nextNode);
   }
-  transactionBuilder.deleteNode(nextNode);
-  transactionBuilder.commit();
+  transaction.deleteNode(nextNode);
+  editorState.commit();
 
   if (textNode.subtype == BuiltInAttributeKey.numberList) {
     makeFollowingNodesIncremental(editorState, textNode.path, selection);
@@ -263,15 +259,15 @@ KeyEventResult _mergeNextLineIntoThisLine(
   return KeyEventResult.handled;
 }
 
-void _deleteTextNodes(TransactionBuilder transactionBuilder,
-    List<TextNode> textNodes, Selection selection) {
+void _deleteTextNodes(
+    Transaction transaction, List<TextNode> textNodes, Selection selection) {
   final first = textNodes.first;
   final last = textNodes.last;
   var content = textNodes.last.toPlainText();
   content = content.substring(selection.end.offset, content.length);
   // Merge the fist and the last text node content,
   //  and delete the all nodes expect for the first.
-  transactionBuilder
+  transaction
     ..deleteNodes(textNodes.sublist(1))
     ..mergeText(
       first,

+ 33 - 34
frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart

@@ -85,16 +85,16 @@ void _pasteHTML(EditorState editorState, String html) {
   } else if (nodes.length == 1) {
     final firstNode = nodes[0];
     final nodeAtPath = editorState.document.nodeAtPath(path)!;
-    final tb = TransactionBuilder(editorState);
+    final tb = editorState.transaction;
     final startOffset = selection.start.offset;
     if (nodeAtPath.type == "text" && firstNode.type == "text") {
       final textNodeAtPath = nodeAtPath as TextNode;
       final firstTextNode = firstNode as TextNode;
-      tb.textEdit(textNodeAtPath,
-          () => (Delta()..retain(startOffset)) + firstTextNode.delta);
-      tb.setAfterSelection(Selection.collapsed(Position(
+      tb.updateText(
+          textNodeAtPath, (Delta()..retain(startOffset)) + firstTextNode.delta);
+      tb.afterSelection = (Selection.collapsed(Position(
           path: path, offset: startOffset + firstTextNode.delta.length)));
-      tb.commit();
+      editorState.commit();
       return;
     }
   }
@@ -104,7 +104,7 @@ void _pasteHTML(EditorState editorState, String html) {
 
 void _pasteMultipleLinesInText(
     EditorState editorState, List<int> path, int offset, List<Node> nodes) {
-  final tb = TransactionBuilder(editorState);
+  final tb = editorState.transaction;
 
   final firstNode = nodes[0];
   final nodeAtPath = editorState.document.nodeAtPath(path)!;
@@ -120,10 +120,9 @@ void _pasteMultipleLinesInText(
     final firstTextNode = firstNode as TextNode;
     final remain = textNodeAtPath.delta.slice(offset);
 
-    tb.textEdit(
+    tb.updateText(
         textNodeAtPath,
-        () =>
-            (Delta()
+        (Delta()
               ..retain(offset)
               ..delete(remain.length)) +
             firstTextNode.delta);
@@ -146,9 +145,9 @@ void _pasteMultipleLinesInText(
       tailNodes.add(TextNode(delta: remain));
     }
 
-    tb.setAfterSelection(afterSelection);
+    tb.afterSelection = afterSelection;
     tb.insertNodes(path, tailNodes);
-    tb.commit();
+    editorState.commit();
 
     if (startNumber != null) {
       makeFollowingNodesIncremental(editorState, originalPath, afterSelection,
@@ -161,9 +160,9 @@ void _pasteMultipleLinesInText(
       _computeSelectionAfterPasteMultipleNodes(editorState, nodes);
 
   path[path.length - 1]++;
-  tb.setAfterSelection(afterSelection);
+  tb.afterSelection = afterSelection;
   tb.insertNodes(path, nodes);
-  tb.commit();
+  editorState.commit();
 }
 
 void _handlePaste(EditorState editorState) async {
@@ -196,15 +195,15 @@ void _pasteSingleLine(
     EditorState editorState, Selection selection, String line) {
   final node = editorState.document.nodeAtPath(selection.end.path)! as TextNode;
   final beginOffset = selection.end.offset;
-  TransactionBuilder(editorState)
-    ..textEdit(
+  editorState.transaction
+    ..updateText(
         node,
-        () => Delta()
+        Delta()
           ..retain(beginOffset)
           ..addAll(_lineContentToDelta(line)))
-    ..setAfterSelection(Selection.collapsed(
-        Position(path: selection.end.path, offset: beginOffset + line.length)))
-    ..commit();
+    ..afterSelection = (Selection.collapsed(
+        Position(path: selection.end.path, offset: beginOffset + line.length)));
+  editorState.commit();
 }
 
 /// parse url from the line text
@@ -264,7 +263,7 @@ void _handlePastePlainText(EditorState editorState, String plainText) {
     final insertedLineSuffix = node.delta.slice(beginOffset);
 
     path[path.length - 1]++;
-    final tb = TransactionBuilder(editorState);
+    final tb = editorState.transaction;
     final List<TextNode> nodes =
         remains.map((e) => TextNode(delta: _lineContentToDelta(e))).toList();
 
@@ -279,16 +278,16 @@ void _handlePastePlainText(EditorState editorState, String plainText) {
     }
 
     // insert first line
-    tb.textEdit(
+    tb.updateText(
         node,
-        () => Delta()
+        Delta()
           ..retain(beginOffset)
           ..insert(firstLine)
           ..delete(node.delta.length - beginOffset));
     // insert remains
     tb.insertNodes(path, nodes);
-    tb.setAfterSelection(afterSelection);
-    tb.commit();
+    tb.afterSelection = afterSelection;
+    editorState.commit();
   }
 }
 
@@ -309,15 +308,15 @@ void _deleteSelectedContent(EditorState editorState) {
   if (selection.start.path.equals(selection.end.path) &&
       beginNode.type == "text") {
     final textItem = beginNode as TextNode;
-    final tb = TransactionBuilder(editorState);
+    final tb = editorState.transaction;
     final len = selection.end.offset - selection.start.offset;
-    tb.textEdit(
+    tb.updateText(
         textItem,
-        () => Delta()
+        Delta()
           ..retain(selection.start.offset)
           ..delete(len));
-    tb.setAfterSelection(Selection.collapsed(selection.start));
-    tb.commit();
+    tb.afterSelection = Selection.collapsed(selection.start);
+    editorState.commit();
     return;
   }
   final traverser = NodeIterator(
@@ -325,13 +324,13 @@ void _deleteSelectedContent(EditorState editorState) {
     startNode: beginNode,
     endNode: endNode,
   );
-  final tb = TransactionBuilder(editorState);
+  final tb = editorState.transaction;
   while (traverser.moveNext()) {
     final item = traverser.current;
     if (item.type == "text" && beginNode == item) {
       final textItem = item as TextNode;
       final deleteLen = textItem.delta.length - selection.start.offset;
-      tb.textEdit(textItem, () {
+      tb.updateText(textItem, () {
         final delta = Delta()
           ..retain(selection.start.offset)
           ..delete(deleteLen);
@@ -342,13 +341,13 @@ void _deleteSelectedContent(EditorState editorState) {
         }
 
         return delta;
-      });
+      }());
     } else {
       tb.deleteNode(item);
     }
   }
-  tb.setAfterSelection(Selection.collapsed(selection.start));
-  tb.commit();
+  tb.afterSelection = Selection.collapsed(selection.start);
+  editorState.commit();
 }
 
 ShortcutEventHandler copyEventHandler = (editorState, event) {

+ 18 - 18
frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart

@@ -39,7 +39,7 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
     final afterSelection = Selection.collapsed(
       Position(path: textNodes.first.path.next, offset: 0),
     );
-    TransactionBuilder(editorState)
+    editorState.transaction
       ..deleteText(
         textNodes.first,
         selection.start.offset,
@@ -51,8 +51,8 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
         0,
         selection.end.offset,
       )
-      ..afterSelection = afterSelection
-      ..commit();
+      ..afterSelection = afterSelection;
+    editorState.commit();
 
     if (startNode is TextNode &&
         startNode.subtype == BuiltInAttributeKey.numberList) {
@@ -77,12 +77,12 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
       final afterSelection = Selection.collapsed(
         Position(path: textNode.path, offset: 0),
       );
-      TransactionBuilder(editorState)
+      editorState.transaction
         ..updateNode(textNode, {
           BuiltInAttributeKey.subtype: null,
         })
-        ..afterSelection = afterSelection
-        ..commit();
+        ..afterSelection = afterSelection;
+      editorState.commit();
 
       final nextNode = textNode.next;
       if (nextNode is TextNode &&
@@ -105,13 +105,13 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
             BuiltInAttributeKey.numberList;
         newNode.attributes[BuiltInAttributeKey.number] = prevNumber;
         final insertPath = textNode.path;
-        TransactionBuilder(editorState)
+        editorState.transaction
           ..insertNode(
             insertPath,
             newNode,
           )
-          ..afterSelection = afterSelection
-          ..commit();
+          ..afterSelection = afterSelection;
+        editorState.commit();
 
         makeFollowingNodesIncremental(editorState, insertPath, afterSelection,
             beginNum: prevNumber);
@@ -120,7 +120,7 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
           BuiltInAttributeKey.heading,
           BuiltInAttributeKey.quote,
         ].contains(subtype);
-        TransactionBuilder(editorState)
+        editorState.transaction
           ..insertNode(
             textNode.path,
             textNode.copyWith(
@@ -129,8 +129,8 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
               attributes: needCopyAttributes ? null : {},
             ),
           )
-          ..afterSelection = afterSelection
-          ..commit();
+          ..afterSelection = afterSelection;
+        editorState.commit();
       }
     }
     return KeyEventResult.handled;
@@ -145,25 +145,25 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
     Position(path: nextPath, offset: 0),
   );
 
-  final transactionBuilder = TransactionBuilder(editorState);
-  transactionBuilder.insertNode(
+  final transaction = editorState.transaction;
+  transaction.insertNode(
     textNode.path.next,
     textNode.copyWith(
       attributes: attributes,
       delta: textNode.delta.slice(selection.end.offset),
     ),
   );
-  transactionBuilder.deleteText(
+  transaction.deleteText(
     textNode,
     selection.start.offset,
     textNode.toPlainText().length - selection.start.offset,
   );
   if (textNode.children.isNotEmpty) {
     final children = textNode.children.toList(growable: false);
-    transactionBuilder.deleteNodes(children);
+    transaction.deleteNodes(children);
   }
-  transactionBuilder.afterSelection = afterSelection;
-  transactionBuilder.commit();
+  transaction.afterSelection = afterSelection;
+  editorState.commit();
 
   // If the new type of a text node is number list,
   // the numbers of the following nodes should be incremental.

+ 13 - 12
frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text.dart

@@ -1,6 +1,7 @@
 import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:appflowy_editor/src/extensions/text_node_extensions.dart';
 import 'package:appflowy_editor/src/service/default_text_operations/format_rich_text_style.dart';
+
 import 'package:flutter/material.dart';
 
 bool _isCodeStyle(TextNode textNode, int index) {
@@ -72,7 +73,7 @@ ShortcutEventHandler backquoteToCodeHandler = (editorState, event) {
       return KeyEventResult.ignored;
     }
 
-    TransactionBuilder(editorState)
+    editorState.transaction
       ..deleteText(textNode, lastBackquoteIndex, 1)
       ..deleteText(textNode, firstBackquoteIndex, 2)
       ..formatText(
@@ -88,8 +89,8 @@ ShortcutEventHandler backquoteToCodeHandler = (editorState, event) {
           path: textNode.path,
           offset: endIndex - 3,
         ),
-      )
-      ..commit();
+      );
+    editorState.commit();
 
     return KeyEventResult.handled;
   }
@@ -103,7 +104,7 @@ ShortcutEventHandler backquoteToCodeHandler = (editorState, event) {
   // delete the backquote.
   // update the style of the text surround by ` ` to code.
   // and update the cursor position.
-  TransactionBuilder(editorState)
+  editorState.transaction
     ..deleteText(textNode, startIndex, 1)
     ..formatText(
       textNode,
@@ -118,8 +119,8 @@ ShortcutEventHandler backquoteToCodeHandler = (editorState, event) {
         path: textNode.path,
         offset: endIndex - 1,
       ),
-    )
-    ..commit();
+    );
+  editorState.commit();
 
   return KeyEventResult.handled;
 };
@@ -165,7 +166,7 @@ ShortcutEventHandler doubleTildeToStrikethrough = (editorState, event) {
   // delete the last three tildes.
   // update the style of the text surround by `~~ ~~` to strikethrough.
   // and update the cursor position.
-  TransactionBuilder(editorState)
+  editorState.transaction
     ..deleteText(textNode, lastTildeIndex, 1)
     ..deleteText(textNode, thirdToLastTildeIndex, 2)
     ..formatText(
@@ -181,8 +182,8 @@ ShortcutEventHandler doubleTildeToStrikethrough = (editorState, event) {
         path: textNode.path,
         offset: selection.end.offset - 3,
       ),
-    )
-    ..commit();
+    );
+  editorState.commit();
 
   return KeyEventResult.handled;
 };
@@ -219,7 +220,7 @@ ShortcutEventHandler markdownLinkToLinkHandler = (editorState, event) {
   // update the href attribute of the text surrounded by [ ] to the url,
   // delete everything after the text,
   // and update the cursor position.
-  TransactionBuilder(editorState)
+  editorState.transaction
     ..deleteText(textNode, firstOpeningBracket, 1)
     ..formatText(
       textNode,
@@ -236,8 +237,8 @@ ShortcutEventHandler markdownLinkToLinkHandler = (editorState, event) {
         path: textNode.path,
         offset: firstOpeningBracket + linkText!.length,
       ),
-    )
-    ..commit();
+    );
+  editorState.commit();
 
   return KeyEventResult.handled;
 };

+ 6 - 6
frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text_handler.dart

@@ -42,7 +42,7 @@ ShortcutEventHandler doubleAsterisksToBold = (editorState, event) {
   // delete the last three asterisks.
   // update the style of the text surround by `** **` to bold.
   // and update the cursor position.
-  TransactionBuilder(editorState)
+  editorState.transaction
     ..deleteText(textNode, lastAsterisIndex, 1)
     ..deleteText(textNode, thirdToLastAsteriskIndex, 2)
     ..formatText(
@@ -59,8 +59,8 @@ ShortcutEventHandler doubleAsterisksToBold = (editorState, event) {
         path: textNode.path,
         offset: selection.end.offset - 3,
       ),
-    )
-    ..commit();
+    );
+  editorState.commit();
 
   return KeyEventResult.handled;
 };
@@ -108,7 +108,7 @@ ShortcutEventHandler doubleUnderscoresToBold = (editorState, event) {
   // delete the last three underscores.
   // update the style of the text surround by `__ __` to bold.
   // and update the cursor position.
-  TransactionBuilder(editorState)
+  editorState.transaction
     ..deleteText(textNode, lastAsterisIndex, 1)
     ..deleteText(textNode, thirdToLastUnderscoreIndex, 2)
     ..formatText(
@@ -125,8 +125,8 @@ ShortcutEventHandler doubleUnderscoresToBold = (editorState, event) {
         path: textNode.path,
         offset: selection.end.offset - 3,
       ),
-    )
-    ..commit();
+    );
+  editorState.commit();
 
   return KeyEventResult.handled;
 };

+ 2 - 3
frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/number_list_helper.dart

@@ -1,7 +1,6 @@
 import 'package:appflowy_editor/src/core/location/selection.dart';
 import 'package:appflowy_editor/src/editor_state.dart';
 import 'package:appflowy_editor/src/core/legacy/built_in_attribute_keys.dart';
-import 'package:appflowy_editor/src/operation/transaction_builder.dart';
 import 'package:appflowy_editor/src/core/document/attributes.dart';
 
 void makeFollowingNodesIncremental(
@@ -16,7 +15,7 @@ void makeFollowingNodesIncremental(
   int numPtr = beginNum + 1;
   var ptr = insertNode.next;
 
-  final builder = TransactionBuilder(editorState);
+  final builder = editorState.transaction;
 
   while (ptr != null) {
     if (ptr.subtype != BuiltInAttributeKey.numberList) {
@@ -34,5 +33,5 @@ void makeFollowingNodesIncremental(
   }
 
   builder.afterSelection = afterSelection;
-  builder.commit();
+  editorState.commit();
 }

+ 4 - 5
frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart

@@ -1,5 +1,5 @@
 import 'package:appflowy_editor/src/core/document/node.dart';
-import 'package:appflowy_editor/src/operation/transaction_builder.dart';
+import 'package:appflowy_editor/src/core/transform/transaction.dart';
 import 'package:appflowy_editor/src/render/selection_menu/selection_menu_service.dart';
 import 'package:appflowy_editor/src/extensions/node_extensions.dart';
 import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event_handler.dart';
@@ -25,10 +25,9 @@ ShortcutEventHandler slashShortcutHandler = (editorState, event) {
   if (selection == null || context == null || selectable == null) {
     return KeyEventResult.ignored;
   }
-  TransactionBuilder(editorState)
-    ..replaceText(textNode, selection.start.offset,
-        selection.end.offset - selection.start.offset, event.character ?? '')
-    ..commit();
+  editorState.transaction.replaceText(textNode, selection.start.offset,
+      selection.end.offset - selection.start.offset, event.character ?? '');
+  editorState.commit();
 
   WidgetsBinding.instance.addPostFrameCallback((_) {
     _selectionMenuService =

+ 5 - 6
frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/tab_handler.dart

@@ -15,9 +15,8 @@ ShortcutEventHandler tabHandler = (editorState, event) {
   final previous = textNode.previous;
 
   if (textNode.subtype != BuiltInAttributeKey.bulletedList) {
-    TransactionBuilder(editorState)
-      ..insertText(textNode, selection.end.offset, ' ' * 4)
-      ..commit();
+    editorState.transaction.insertText(textNode, selection.end.offset, ' ' * 4);
+    editorState.commit();
     return KeyEventResult.handled;
   }
 
@@ -31,11 +30,11 @@ ShortcutEventHandler tabHandler = (editorState, event) {
     start: selection.start.copyWith(path: path),
     end: selection.end.copyWith(path: path),
   );
-  TransactionBuilder(editorState)
+  editorState.transaction
     ..deleteNode(textNode)
     ..insertNode(path, textNode)
-    ..setAfterSelection(afterSelection)
-    ..commit();
+    ..afterSelection = afterSelection;
+  editorState.commit();
 
   return KeyEventResult.handled;
 };

+ 13 - 14
frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/whitespace_handler.dart

@@ -1,3 +1,4 @@
+import 'package:appflowy_editor/src/core/transform/transaction.dart';
 import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event_handler.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
@@ -6,7 +7,6 @@ import 'package:appflowy_editor/src/core/document/node.dart';
 import 'package:appflowy_editor/src/core/location/position.dart';
 import 'package:appflowy_editor/src/core/location/selection.dart';
 import 'package:appflowy_editor/src/editor_state.dart';
-import 'package:appflowy_editor/src/operation/transaction_builder.dart';
 import './number_list_helper.dart';
 import 'package:appflowy_editor/src/extensions/attributes_extension.dart';
 
@@ -99,15 +99,14 @@ KeyEventResult _toNumberList(EditorState editorState, TextNode textNode,
   ));
 
   final insertPath = textNode.path;
-
-  TransactionBuilder(editorState)
+  editorState.transaction
     ..deleteText(textNode, 0, matchText.length)
     ..updateNode(textNode, {
       BuiltInAttributeKey.subtype: BuiltInAttributeKey.numberList,
       BuiltInAttributeKey.number: numValue
     })
-    ..afterSelection = afterSelection
-    ..commit();
+    ..afterSelection = afterSelection;
+  editorState.commit();
 
   makeFollowingNodesIncremental(editorState, insertPath, afterSelection);
 
@@ -118,7 +117,7 @@ KeyEventResult _toBulletedList(EditorState editorState, TextNode textNode) {
   if (textNode.subtype == BuiltInAttributeKey.bulletedList) {
     return KeyEventResult.ignored;
   }
-  TransactionBuilder(editorState)
+  editorState.transaction
     ..deleteText(textNode, 0, 1)
     ..updateNode(textNode, {
       BuiltInAttributeKey.subtype: BuiltInAttributeKey.bulletedList,
@@ -128,8 +127,8 @@ KeyEventResult _toBulletedList(EditorState editorState, TextNode textNode) {
         path: textNode.path,
         offset: 0,
       ),
-    )
-    ..commit();
+    );
+  editorState.commit();
   return KeyEventResult.handled;
 }
 
@@ -151,7 +150,7 @@ KeyEventResult _toCheckboxList(EditorState editorState, TextNode textNode) {
     check = false;
   }
 
-  TransactionBuilder(editorState)
+  editorState.transaction
     ..deleteText(textNode, 0, symbol.length)
     ..updateNode(textNode, {
       BuiltInAttributeKey.subtype: BuiltInAttributeKey.checkbox,
@@ -162,8 +161,8 @@ KeyEventResult _toCheckboxList(EditorState editorState, TextNode textNode) {
         path: textNode.path,
         offset: 0,
       ),
-    )
-    ..commit();
+    );
+  editorState.commit();
   return KeyEventResult.handled;
 }
 
@@ -177,7 +176,7 @@ KeyEventResult _toHeadingStyle(
   if (textNode.attributes.heading == hX) {
     return KeyEventResult.ignored;
   }
-  TransactionBuilder(editorState)
+  editorState.transaction
     ..deleteText(textNode, 0, x)
     ..updateNode(textNode, {
       BuiltInAttributeKey.subtype: BuiltInAttributeKey.heading,
@@ -188,8 +187,8 @@ KeyEventResult _toHeadingStyle(
         path: textNode.path,
         offset: 0,
       ),
-    )
-    ..commit();
+    );
+  editorState.commit();
   return KeyEventResult.handled;
 }
 

+ 3 - 4
frontend/app_flowy/packages/appflowy_editor/lib/src/undo_manager.dart

@@ -3,8 +3,7 @@ import 'dart:collection';
 import 'package:appflowy_editor/src/core/location/selection.dart';
 import 'package:appflowy_editor/src/infra/log.dart';
 import 'package:appflowy_editor/src/core/transform/operation.dart';
-import 'package:appflowy_editor/src/operation/transaction_builder.dart';
-import 'package:appflowy_editor/src/operation/transaction.dart';
+import 'package:appflowy_editor/src/core/transform/transaction.dart';
 import 'package:appflowy_editor/src/editor_state.dart';
 
 /// A [HistoryItem] contains list of operations committed by users.
@@ -39,7 +38,7 @@ class HistoryItem extends LinkedListEntry<HistoryItem> {
 
   /// Create a new [Transaction] by inverting the operations.
   Transaction toTransaction(EditorState state) {
-    final builder = TransactionBuilder(state);
+    final builder = Transaction(document: state.document);
     for (var i = operations.length - 1; i >= 0; i--) {
       final operation = operations[i];
       final inverted = operation.invert();
@@ -47,7 +46,7 @@ class HistoryItem extends LinkedListEntry<HistoryItem> {
     }
     builder.afterSelection = beforeSelection;
     builder.beforeSelection = afterSelection;
-    return builder.finish();
+    return builder;
   }
 }
 

+ 27 - 27
frontend/app_flowy/packages/appflowy_editor/test/legacy/operation_test.dart

@@ -3,7 +3,6 @@ import 'dart:collection';
 import 'package:appflowy_editor/src/core/document/node.dart';
 import 'package:flutter_test/flutter_test.dart';
 import 'package:appflowy_editor/src/core/transform/operation.dart';
-import 'package:appflowy_editor/src/operation/transaction_builder.dart';
 import 'package:appflowy_editor/src/editor_state.dart';
 import 'package:appflowy_editor/src/core/document/document.dart';
 
@@ -48,25 +47,26 @@ void main() {
     final item2 = Node(type: "node", attributes: {}, children: LinkedList());
     final item3 = Node(type: "node", attributes: {}, children: LinkedList());
     final root = Node(
-        type: "root",
-        attributes: {},
-        children: LinkedList()
-          ..addAll([
-            item1,
-            item2,
-            item3,
-          ]));
+      type: "root",
+      attributes: {},
+      children: LinkedList()
+        ..addAll([
+          item1,
+          item2,
+          item3,
+        ]),
+    );
     final state = EditorState(document: Document(root: root));
 
     expect(item1.path, [0]);
     expect(item2.path, [1]);
     expect(item3.path, [2]);
 
-    final tb = TransactionBuilder(state);
-    tb.deleteNode(item1);
-    tb.deleteNode(item2);
-    tb.deleteNode(item3);
-    final transaction = tb.finish();
+    final transaction = state.transaction;
+    transaction.deleteNode(item1);
+    transaction.deleteNode(item2);
+    transaction.deleteNode(item3);
+    state.commit();
     expect(transaction.operations[0].path, [0]);
     expect(transaction.operations[1].path, [0]);
     expect(transaction.operations[2].path, [0]);
@@ -77,10 +77,9 @@ void main() {
       final state = EditorState(document: Document(root: root));
 
       final item1 = Node(type: "node", attributes: {}, children: LinkedList());
-      final tb = TransactionBuilder(state);
-      tb.insertNode([0], item1);
-
-      final transaction = tb.finish();
+      final transaction = state.transaction;
+      transaction.insertNode([0], item1);
+      state.commit();
       expect(transaction.toJson(), {
         "operations": [
           {
@@ -94,16 +93,17 @@ void main() {
     test("delete", () {
       final item1 = Node(type: "node", attributes: {}, children: LinkedList());
       final root = Node(
-          type: "root",
-          attributes: {},
-          children: LinkedList()
-            ..addAll([
-              item1,
-            ]));
+        type: "root",
+        attributes: {},
+        children: LinkedList()
+          ..addAll([
+            item1,
+          ]),
+      );
       final state = EditorState(document: Document(root: root));
-      final tb = TransactionBuilder(state);
-      tb.deleteNode(item1);
-      final transaction = tb.finish();
+      final transaction = state.transaction;
+      transaction.deleteNode(item1);
+      state.commit();
       expect(transaction.toJson(), {
         "operations": [
           {