Parcourir la source

refactor: abstract the common transaction to commands

Lucas.Xu il y a 2 ans
Parent
commit
f3eeb471e7
32 fichiers modifiés avec 470 ajouts et 510 suppressions
  1. 12 11
      frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/code_block_node_widget.dart
  2. 6 6
      frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/horizontal_rule_node_widget.dart
  3. 13 11
      frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/tex_block_node_widget.dart
  4. 2 2
      frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/underscore_to_italic.dart
  5. 20 0
      frontend/app_flowy/packages/appflowy_editor/lib/src/commands/attributes_commands.dart
  6. 54 0
      frontend/app_flowy/packages/appflowy_editor/lib/src/commands/command_extension.dart
  7. 0 33
      frontend/app_flowy/packages/appflowy_editor/lib/src/commands/edit_text.dart
  8. 0 83
      frontend/app_flowy/packages/appflowy_editor/lib/src/commands/format_built_in_text.dart
  9. 0 64
      frontend/app_flowy/packages/appflowy_editor/lib/src/commands/format_text.dart
  10. 98 0
      frontend/app_flowy/packages/appflowy_editor/lib/src/commands/text/text_commands.dart
  11. 0 43
      frontend/app_flowy/packages/appflowy_editor/lib/src/commands/text_command_infra.dart
  12. 4 4
      frontend/app_flowy/packages/appflowy_editor/lib/src/core/transform/transaction.dart
  13. 4 16
      frontend/app_flowy/packages/appflowy_editor/lib/src/editor_state.dart
  14. 13 10
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_builder.dart
  15. 1 1
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_upload_widget.dart
  16. 2 2
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart
  17. 21 18
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_widget.dart
  18. 14 5
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart
  19. 4 4
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/default_text_operations/format_rich_text_style.dart
  20. 9 7
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart
  21. 8 8
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart
  22. 8 8
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart
  23. 9 9
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart
  24. 138 8
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text.dart
  25. 0 132
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text_handler.dart
  26. 4 4
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/number_list_helper.dart
  27. 4 3
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart
  28. 6 2
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/space_on_web_handler.dart
  29. 5 4
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/tab_handler.dart
  30. 8 8
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/whitespace_handler.dart
  31. 0 1
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart
  32. 3 3
      frontend/app_flowy/packages/appflowy_editor/test/legacy/operation_test.dart

+ 12 - 11
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) {
-    editorState.transaction
-        .insertText(codeBlockNode.first, selection.end.offset, '\n');
-    editorState.commit();
+    final transaction = editorState.transaction
+      ..insertText(codeBlockNode.first, selection.end.offset, '\n');
+    editorState.apply(transaction);
     return KeyEventResult.handled;
   }
   return KeyEventResult.ignored;
@@ -61,16 +61,16 @@ SelectionMenuItem codeBlockMenuItem = SelectionMenuItem(
       return;
     }
     if (textNodes.first.toPlainText().isEmpty) {
-      editorState.transaction
+      final transaction = editorState.transaction
         ..updateNode(textNodes.first, {
           'subtype': 'code_block',
           'theme': 'vs',
           'language': null,
         })
         ..afterSelection = selection;
-      editorState.commit();
+      editorState.apply(transaction);
     } else {
-      editorState.transaction
+      final transaction = editorState.transaction
         ..insertNode(
           selection.end.path.next,
           TextNode(
@@ -84,7 +84,7 @@ SelectionMenuItem codeBlockMenuItem = SelectionMenuItem(
           ),
         )
         ..afterSelection = selection;
-      editorState.commit();
+      editorState.apply(transaction);
     }
   },
 );
@@ -181,10 +181,11 @@ class __CodeBlockNodeWidgeState extends State<_CodeBlockNodeWidge>
       child: DropdownButton<String>(
         value: _detectLanguage,
         onChanged: (value) {
-          widget.editorState.transaction.updateNode(widget.textNode, {
-            'language': value,
-          });
-          widget.editorState.commit();
+          final transaction = widget.editorState.transaction
+            ..updateNode(widget.textNode, {
+              'language': value,
+            });
+          widget.editorState.apply(transaction);
         },
         items: allLanguages.keys.map<DropdownMenuItem<String>>((String value) {
           return DropdownMenuItem<String>(

+ 6 - 6
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() == '--') {
-    editorState.transaction
+    final transaction = editorState.transaction
       ..deleteText(textNode, 0, 2)
       ..insertNode(
         textNode.path,
@@ -30,7 +30,7 @@ ShortcutEventHandler _insertHorzaontalRule = (editorState, event) {
       )
       ..afterSelection =
           Selection.single(path: textNode.path.next, startOffset: 0);
-    editorState.commit();
+    editorState.apply(transaction);
     return KeyEventResult.handled;
   }
   return KeyEventResult.ignored;
@@ -54,7 +54,7 @@ SelectionMenuItem horizontalRuleMenuItem = SelectionMenuItem(
     }
     final textNode = textNodes.first;
     if (textNode.toPlainText().isEmpty) {
-      editorState.transaction
+      final transaction = editorState.transaction
         ..insertNode(
           textNode.path,
           Node(
@@ -65,9 +65,9 @@ SelectionMenuItem horizontalRuleMenuItem = SelectionMenuItem(
         )
         ..afterSelection =
             Selection.single(path: textNode.path.next, startOffset: 0);
-      editorState.commit();
+      editorState.apply(transaction);
     } else {
-      editorState.transaction
+      final transaction = editorState.transaction
         ..insertNode(
           selection.end.path.next,
           TextNode(
@@ -79,7 +79,7 @@ SelectionMenuItem horizontalRuleMenuItem = SelectionMenuItem(
           ),
         )
         ..afterSelection = selection;
-      editorState.commit();
+      editorState.apply(transaction);
     }
   },
 );

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

+ 2 - 2
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.
-  editorState.transaction
+  final transaction = editorState.transaction
     ..deleteText(textNode, firstUnderscore, 1)
     ..formatText(
       textNode,
@@ -47,7 +47,7 @@ ShortcutEventHandler _underscoreToItalicHandler = (editorState, event) {
         offset: selection.end.offset - 1,
       ),
     );
-  editorState.commit();
+  editorState.apply(transaction);
 
   return KeyEventResult.handled;
 };

+ 20 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/commands/attributes_commands.dart

@@ -0,0 +1,20 @@
+import 'package:appflowy_editor/src/commands/command_extension.dart';
+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/editor_state.dart';
+
+extension TextCommands on EditorState {
+  Future<void> updateNodeAttributes(
+    Attributes attributes, {
+    Path? path,
+    Node? node,
+  }) {
+    return futureCommand(() {
+      final n = getNode(path: path, node: node);
+      apply(
+        transaction..updateNode(n, attributes),
+      );
+    });
+  }
+}

+ 54 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/commands/command_extension.dart

@@ -0,0 +1,54 @@
+import 'dart:async';
+
+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:flutter/widgets.dart';
+
+extension CommandExtension on EditorState {
+  Future<void> futureCommand(void Function() fn) async {
+    final completer = Completer<void>();
+    fn();
+    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
+      completer.complete();
+    });
+    return completer.future;
+  }
+
+  Node getNode({
+    Path? path,
+    Node? node,
+  }) {
+    if (node != null) {
+      return node;
+    } else if (path != null) {
+      return document.nodeAtPath(path)!;
+    }
+    throw Exception('path and node cannot be null at the same time');
+  }
+
+  TextNode getTextNode({
+    Path? path,
+    TextNode? textNode,
+  }) {
+    if (textNode != null) {
+      return textNode;
+    } else if (path != null) {
+      return document.nodeAtPath(path)! as TextNode;
+    }
+    throw Exception('path and node cannot be null at the same time');
+  }
+
+  Selection getSelection(
+    Selection? selection,
+  ) {
+    final currentSelection = service.selectionService.currentSelection.value;
+    if (selection != null) {
+      return selection;
+    } else if (currentSelection != null) {
+      return currentSelection;
+    }
+    throw Exception('path and textNode cannot be null at the same time');
+  }
+}

+ 0 - 33
frontend/app_flowy/packages/appflowy_editor/lib/src/commands/edit_text.dart

@@ -1,33 +0,0 @@
-import 'dart:async';
-
-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/core/transform/transaction.dart';
-import 'package:flutter/widgets.dart';
-
-Future<void> insertContextInText(
-  EditorState editorState,
-  int index,
-  String content, {
-  Path? path,
-  TextNode? textNode,
-}) async {
-  final result = getTextNodeToBeFormatted(
-    editorState,
-    path: path,
-    textNode: textNode,
-  );
-
-  final completer = Completer<void>();
-
-  editorState.transaction.insertText(result, index, content);
-  editorState.commit();
-
-  WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
-    completer.complete();
-  });
-
-  return completer.future;
-}

+ 0 - 83
frontend/app_flowy/packages/appflowy_editor/lib/src/commands/format_built_in_text.dart

@@ -1,83 +0,0 @@
-import 'package:appflowy_editor/src/commands/format_text.dart';
-import 'package:appflowy_editor/src/commands/text_command_infra.dart';
-import 'package:appflowy_editor/src/core/document/attributes.dart';
-import 'package:appflowy_editor/src/core/legacy/built_in_attribute_keys.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/selection.dart';
-import 'package:appflowy_editor/src/editor_state.dart';
-
-Future<void> formatBuiltInTextAttributes(
-  EditorState editorState,
-  String key,
-  Attributes attributes, {
-  Selection? selection,
-  Path? path,
-  TextNode? textNode,
-}) async {
-  final result = getTextNodeToBeFormatted(
-    editorState,
-    path: path,
-    textNode: textNode,
-  );
-  if (BuiltInAttributeKey.globalStyleKeys.contains(key)) {
-    // remove all the existing style
-    final newAttributes = result.attributes
-      ..removeWhere((key, value) {
-        if (BuiltInAttributeKey.globalStyleKeys.contains(key)) {
-          return true;
-        }
-        return false;
-      })
-      ..addAll(attributes)
-      ..addAll({
-        BuiltInAttributeKey.subtype: key,
-      });
-    return updateTextNodeAttributes(
-      editorState,
-      newAttributes,
-      textNode: textNode,
-    );
-  } else if (BuiltInAttributeKey.partialStyleKeys.contains(key)) {
-    return updateTextNodeDeltaAttributes(
-      editorState,
-      selection,
-      attributes,
-      textNode: textNode,
-    );
-  }
-}
-
-Future<void> formatTextToCheckbox(
-  EditorState editorState,
-  bool check, {
-  Path? path,
-  TextNode? textNode,
-}) async {
-  return formatBuiltInTextAttributes(
-    editorState,
-    BuiltInAttributeKey.checkbox,
-    {
-      BuiltInAttributeKey.checkbox: check,
-    },
-    path: path,
-    textNode: textNode,
-  );
-}
-
-Future<void> formatLinkInText(
-  EditorState editorState,
-  String? link, {
-  Path? path,
-  TextNode? textNode,
-}) async {
-  return formatBuiltInTextAttributes(
-    editorState,
-    BuiltInAttributeKey.href,
-    {
-      BuiltInAttributeKey.href: link,
-    },
-    path: path,
-    textNode: textNode,
-  );
-}

+ 0 - 64
frontend/app_flowy/packages/appflowy_editor/lib/src/commands/format_text.dart

@@ -1,64 +0,0 @@
-import 'dart:async';
-
-import 'package:appflowy_editor/src/commands/text_command_infra.dart';
-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/selection.dart';
-import 'package:appflowy_editor/src/editor_state.dart';
-import 'package:appflowy_editor/src/core/transform/transaction.dart';
-import 'package:flutter/widgets.dart';
-
-Future<void> updateTextNodeAttributes(
-  EditorState editorState,
-  Attributes attributes, {
-  Path? path,
-  TextNode? textNode,
-}) async {
-  final result = getTextNodeToBeFormatted(
-    editorState,
-    path: path,
-    textNode: textNode,
-  );
-
-  final completer = Completer<void>();
-
-  editorState.transaction.updateNode(result, attributes);
-  editorState.commit();
-
-  WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
-    completer.complete();
-  });
-
-  return completer.future;
-}
-
-Future<void> updateTextNodeDeltaAttributes(
-  EditorState editorState,
-  Selection? selection,
-  Attributes attributes, {
-  Path? path,
-  TextNode? textNode,
-}) {
-  final result = getTextNodeToBeFormatted(
-    editorState,
-    path: path,
-    textNode: textNode,
-  );
-  final newSelection = getSelection(editorState, selection: selection);
-
-  final completer = Completer<void>();
-  editorState.transaction.formatText(
-    result,
-    newSelection.startIndex,
-    newSelection.length,
-    attributes,
-  );
-  editorState.commit();
-
-  WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
-    completer.complete();
-  });
-
-  return completer.future;
-}

+ 98 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/commands/text/text_commands.dart

@@ -0,0 +1,98 @@
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:appflowy_editor/src/commands/command_extension.dart';
+
+extension TextCommands on EditorState {
+  /// Insert text at the given index of the given [TextNode] or the [Path].
+  ///
+  /// [Path] and [TextNode] are mutually exclusive.
+  /// One of these two parameters must have a value.
+  Future<void> insertText(
+    int index,
+    String text, {
+    Path? path,
+    TextNode? textNode,
+  }) async {
+    return futureCommand(() {
+      final n = getTextNode(path: path, textNode: textNode);
+      apply(
+        transaction..insertText(n, index, text),
+      );
+    });
+  }
+
+  Future<void> formatText(
+    EditorState editorState,
+    Selection? selection,
+    Attributes attributes, {
+    Path? path,
+    TextNode? textNode,
+  }) async {
+    return futureCommand(() {
+      final n = getTextNode(path: path, textNode: textNode);
+      final s = getSelection(selection);
+      apply(
+        transaction..formatText(n, s.startIndex, s.length, attributes),
+      );
+    });
+  }
+
+  Future<void> formatTextWithBuiltInAttribute(
+    EditorState editorState,
+    String key,
+    Attributes attributes, {
+    Selection? selection,
+    Path? path,
+    TextNode? textNode,
+  }) async {
+    return futureCommand(() {
+      final n = getTextNode(path: path, textNode: textNode);
+      if (BuiltInAttributeKey.globalStyleKeys.contains(key)) {
+        final attr = n.attributes
+          ..removeWhere(
+              (key, _) => BuiltInAttributeKey.globalStyleKeys.contains(key))
+          ..addAll(attributes)
+          ..addAll({
+            BuiltInAttributeKey.subtype: key,
+          });
+        apply(
+          transaction..updateNode(n, attr),
+        );
+      } else if (BuiltInAttributeKey.partialStyleKeys.contains(key)) {
+        final s = getSelection(selection);
+        apply(
+          transaction..formatText(n, s.startIndex, s.length, attributes),
+        );
+      }
+    });
+  }
+
+  Future<void> formatTextToCheckbox(
+    EditorState editorState,
+    bool check, {
+    Path? path,
+    TextNode? textNode,
+  }) async {
+    return formatTextWithBuiltInAttribute(
+      editorState,
+      BuiltInAttributeKey.checkbox,
+      {BuiltInAttributeKey.checkbox: check},
+      path: path,
+      textNode: textNode,
+    );
+  }
+
+  Future<void> formatLinkInText(
+    EditorState editorState,
+    String? link, {
+    Path? path,
+    TextNode? textNode,
+  }) async {
+    return formatTextWithBuiltInAttribute(
+      editorState,
+      BuiltInAttributeKey.href,
+      {BuiltInAttributeKey.href: link},
+      path: path,
+      textNode: textNode,
+    );
+  }
+}

+ 0 - 43
frontend/app_flowy/packages/appflowy_editor/lib/src/commands/text_command_infra.dart

@@ -1,43 +0,0 @@
-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';
-
-// get formatted [TextNode]
-TextNode getTextNodeToBeFormatted(
-  EditorState editorState, {
-  Path? path,
-  TextNode? textNode,
-}) {
-  final currentSelection =
-      editorState.service.selectionService.currentSelection.value;
-  TextNode result;
-  if (textNode != null) {
-    result = textNode;
-  } else if (path != null) {
-    result = editorState.document.nodeAtPath(path) as TextNode;
-  } else if (currentSelection != null && currentSelection.isCollapsed) {
-    result = editorState.document.nodeAtPath(currentSelection.start.path)
-        as TextNode;
-  } else {
-    throw Exception('path and textNode cannot be null at the same time');
-  }
-  return result;
-}
-
-Selection getSelection(
-  EditorState editorState, {
-  Selection? selection,
-}) {
-  final currentSelection =
-      editorState.service.selectionService.currentSelection.value;
-  Selection result;
-  if (selection != null) {
-    result = selection;
-  } else if (currentSelection != null) {
-    result = currentSelection;
-  } else {
-    throw Exception('path and textNode cannot be null at the same time');
-  }
-  return result;
-}

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

@@ -119,7 +119,7 @@ class Transaction {
   ///
   /// Also, this method will transform the path of the operations
   /// to avoid conflicts.
-  add(Operation op, {bool transform = true}) {
+  void add(Operation op, {bool transform = true}) {
     final Operation? last = operations.isEmpty ? null : operations.last;
     if (last != null) {
       if (op is UpdateTextOperation &&
@@ -199,7 +199,7 @@ extension TextTransaction on Transaction {
   }
 
   /// Assigns a formatting attributes to a range of text.
-  formatText(
+  void formatText(
     TextNode textNode,
     int index,
     int length,
@@ -215,7 +215,7 @@ extension TextTransaction on Transaction {
   }
 
   /// Deletes the text of specified length starting at index.
-  deleteText(
+  void deleteText(
     TextNode textNode,
     int index,
     int length,
@@ -235,7 +235,7 @@ extension TextTransaction on Transaction {
   ///
   /// 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(
+  void replaceText(
     TextNode textNode,
     int index,
     int length,

+ 4 - 16
frontend/app_flowy/packages/appflowy_editor/lib/src/editor_state.dart

@@ -75,21 +75,9 @@ 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;
-    }
+    final transaction = Transaction(document: document);
+    transaction.beforeSelection = _cursorSelection;
+    return transaction;
   }
 
   Selection? get cursorSelection {
@@ -131,7 +119,7 @@ class EditorState {
   /// The options can be used to determine whether the editor
   /// should record the transaction in undo/redo stack.
   apply(Transaction transaction,
-      [ApplyOptions options = const ApplyOptions()]) {
+      [ApplyOptions options = const ApplyOptions(recordUndo: true)]) {
     if (!editable) {
       return;
     }

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

@@ -24,20 +24,23 @@ class ImageNodeBuilder extends NodeWidgetBuilder<Node> {
         RichClipboard.setData(RichClipboardData(text: src));
       },
       onDelete: () {
-        context.editorState.transaction.deleteNode(context.node);
-        context.editorState.commit();
+        final transaction = context.editorState.transaction
+          ..deleteNode(context.node);
+        context.editorState.apply(transaction);
       },
       onAlign: (alignment) {
-        context.editorState.transaction.updateNode(context.node, {
-          'align': _alignmentToText(alignment),
-        });
-        context.editorState.commit();
+        final transaction = context.editorState.transaction
+          ..updateNode(context.node, {
+            'align': _alignmentToText(alignment),
+          });
+        context.editorState.apply(transaction);
       },
       onResize: (width) {
-        context.editorState.transaction.updateNode(context.node, {
-          'width': width,
-        });
-        context.editorState.commit();
+        final transaction = context.editorState.transaction
+          ..updateNode(context.node, {
+            'width': width,
+          });
+        context.editorState.apply(transaction);
       },
     );
   }

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

@@ -195,6 +195,6 @@ extension on EditorState {
       selection.start.path,
       imageNode,
     );
-    commit();
+    apply(transaction);
   }
 }

+ 2 - 2
frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart

@@ -1,5 +1,5 @@
 import 'package:appflowy_editor/appflowy_editor.dart';
-import 'package:appflowy_editor/src/commands/format_built_in_text.dart';
+import 'package:appflowy_editor/src/commands/text/text_commands.dart';
 import 'package:appflowy_editor/src/infra/flowy_svg.dart';
 import 'package:appflowy_editor/src/render/rich_text/built_in_text_widget.dart';
 
@@ -75,7 +75,7 @@ class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
               name: check ? 'check' : 'uncheck',
             ),
             onTap: () async {
-              await formatTextToCheckbox(
+              await widget.editorState.formatTextToCheckbox(
                 widget.editorState,
                 !check,
                 textNode: widget.textNode,

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

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

+ 14 - 5
frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart

@@ -1,5 +1,5 @@
 import 'package:appflowy_editor/appflowy_editor.dart';
-import 'package:appflowy_editor/src/commands/format_built_in_text.dart';
+import 'package:appflowy_editor/src/commands/text/text_commands.dart';
 import 'package:appflowy_editor/src/extensions/url_launcher_extension.dart';
 import 'package:appflowy_editor/src/infra/flowy_svg.dart';
 import 'package:appflowy_editor/src/render/link_menu/link_menu.dart';
@@ -349,7 +349,11 @@ void showLinkMenu(
             await safeLaunchUrl(linkText);
           },
           onSubmitted: (text) async {
-            await formatLinkInText(editorState, text, textNode: textNode);
+            await editorState.formatLinkInText(
+              editorState,
+              text,
+              textNode: textNode,
+            );
             _dismissLinkMenu();
           },
           onCopyLink: () {
@@ -357,9 +361,14 @@ void showLinkMenu(
             _dismissLinkMenu();
           },
           onRemoveLink: () {
-            editorState.transaction.formatText(
-                textNode, index, length, {BuiltInAttributeKey.href: null});
-            editorState.commit();
+            final transaction = editorState.transaction
+              ..formatText(
+                textNode,
+                index,
+                length,
+                {BuiltInAttributeKey.href: null},
+              );
+            editorState.apply(transaction);
             _dismissLinkMenu();
           },
           onFocusChange: (value) {

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

@@ -47,7 +47,7 @@ bool insertTextNodeAfterSelection(
     formatTextNodes(editorState, attributes);
   } else {
     final next = selection.end.path.next;
-    editorState.transaction
+    final transaction = editorState.transaction
       ..insertNode(
         next,
         TextNode.empty(attributes: attributes),
@@ -55,7 +55,7 @@ bool insertTextNodeAfterSelection(
       ..afterSelection = Selection.collapsed(
         Position(path: next, offset: 0),
       );
-    editorState.commit();
+    editorState.apply(transaction);
   }
 
   return true;
@@ -122,7 +122,7 @@ bool formatTextNodes(EditorState editorState, Attributes attributes) {
       );
   }
 
-  editorState.commit();
+  editorState.apply(transaction);
   return true;
 }
 
@@ -240,7 +240,7 @@ bool formatRichTextStyle(EditorState editorState, Attributes attributes) {
     }
   }
 
-  editorState.commit();
+  editorState.apply(transaction);
 
   return true;
 }

+ 9 - 7
frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart

@@ -160,12 +160,13 @@ class _AppFlowyInputState extends State<AppFlowyInput>
     }
     if (currentSelection.isSingle) {
       final textNode = selectionService.currentSelectedNodes.first as TextNode;
-      _editorState.transaction.insertText(
+      final transaction = _editorState.transaction;
+      transaction.insertText(
         textNode,
         delta.insertionOffset,
         delta.textInserted,
       );
-      _editorState.commit();
+      _editorState.apply(transaction);
     } else {
       // TODO: implement
     }
@@ -180,9 +181,9 @@ class _AppFlowyInputState extends State<AppFlowyInput>
     if (currentSelection.isSingle) {
       final textNode = selectionService.currentSelectedNodes.first as TextNode;
       final length = delta.deletedRange.end - delta.deletedRange.start;
-      _editorState.transaction
-          .deleteText(textNode, delta.deletedRange.start, length);
-      _editorState.commit();
+      final transaction = _editorState.transaction;
+      transaction.deleteText(textNode, delta.deletedRange.start, length);
+      _editorState.apply(transaction);
     } else {
       // TODO: implement
     }
@@ -197,9 +198,10 @@ class _AppFlowyInputState extends State<AppFlowyInput>
     if (currentSelection.isSingle) {
       final textNode = selectionService.currentSelectedNodes.first as TextNode;
       final length = delta.replacedRange.end - delta.replacedRange.start;
-      _editorState.transaction.replaceText(
+      final transaction = _editorState.transaction;
+      transaction.replaceText(
           textNode, delta.replacedRange.start, length, delta.replacementText);
-      _editorState.commit();
+      _editorState.apply(transaction);
     } else {
       // TODO: implement
     }

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

@@ -86,13 +86,13 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) {
       if (nonTextNodes.isNotEmpty) {
         transaction.afterSelection = Selection.collapsed(selection.start);
       }
-      editorState.commit();
+      editorState.apply(transaction);
       return KeyEventResult.handled;
     }
     final startPosition = selection.start;
     final nodeAtStart = editorState.document.nodeAtPath(startPosition.path)!;
     _deleteTextNodes(transaction, textNodes, selection);
-    editorState.commit();
+    editorState.apply(transaction);
 
     if (nodeAtStart is TextNode &&
         nodeAtStart.subtype == BuiltInAttributeKey.numberList) {
@@ -109,7 +109,7 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) {
     if (nonTextNodes.isNotEmpty) {
       transaction.afterSelection = Selection.collapsed(selection.start);
     }
-    editorState.commit();
+    editorState.apply(transaction);
   }
 
   if (cancelNumberListPath != null) {
@@ -140,7 +140,7 @@ KeyEventResult _backDeleteToPreviousTextNode(
       ..afterSelection = Selection.collapsed(
         Position(path: textNode.parent!.path.next, offset: 0),
       );
-    editorState.commit();
+    editorState.apply(transaction);
     return KeyEventResult.handled;
   }
 
@@ -171,7 +171,7 @@ KeyEventResult _backDeleteToPreviousTextNode(
     if (nonTextNodes.isNotEmpty) {
       transaction.afterSelection = Selection.collapsed(selection.start);
     }
-    editorState.commit();
+    editorState.apply(transaction);
   }
 
   if (prevIsNumberList) {
@@ -223,12 +223,12 @@ KeyEventResult _handleDelete(EditorState editorState, RawKeyEvent event) {
         selection.end.offset - selection.start.offset,
       );
     }
-    editorState.commit();
+    editorState.apply(transaction);
   } else {
     final startPosition = selection.start;
     final nodeAtStart = editorState.document.nodeAtPath(startPosition.path)!;
     _deleteTextNodes(transaction, textNodes, selection);
-    editorState.commit();
+    editorState.apply(transaction);
 
     if (nodeAtStart is TextNode &&
         nodeAtStart.subtype == BuiltInAttributeKey.numberList) {
@@ -250,7 +250,7 @@ KeyEventResult _mergeNextLineIntoThisLine(EditorState editorState,
     transaction.mergeText(textNode, nextNode);
   }
   transaction.deleteNode(nextNode);
-  editorState.commit();
+  editorState.apply(transaction);
 
   if (textNode.subtype == BuiltInAttributeKey.numberList) {
     makeFollowingNodesIncremental(editorState, textNode.path, selection);

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

@@ -94,7 +94,7 @@ void _pasteHTML(EditorState editorState, String html) {
           textNodeAtPath, (Delta()..retain(startOffset)) + firstTextNode.delta);
       tb.afterSelection = (Selection.collapsed(Position(
           path: path, offset: startOffset + firstTextNode.delta.length)));
-      editorState.commit();
+      editorState.apply(tb);
       return;
     }
   }
@@ -147,7 +147,7 @@ void _pasteMultipleLinesInText(
 
     tb.afterSelection = afterSelection;
     tb.insertNodes(path, tailNodes);
-    editorState.commit();
+    editorState.apply(tb);
 
     if (startNumber != null) {
       makeFollowingNodesIncremental(editorState, originalPath, afterSelection,
@@ -162,7 +162,7 @@ void _pasteMultipleLinesInText(
   path[path.length - 1]++;
   tb.afterSelection = afterSelection;
   tb.insertNodes(path, nodes);
-  editorState.commit();
+  editorState.apply(tb);
 }
 
 void _handlePaste(EditorState editorState) async {
@@ -195,7 +195,7 @@ void _pasteSingleLine(
     EditorState editorState, Selection selection, String line) {
   final node = editorState.document.nodeAtPath(selection.end.path)! as TextNode;
   final beginOffset = selection.end.offset;
-  editorState.transaction
+  final transaction = editorState.transaction
     ..updateText(
         node,
         Delta()
@@ -203,7 +203,7 @@ void _pasteSingleLine(
           ..addAll(_lineContentToDelta(line)))
     ..afterSelection = (Selection.collapsed(
         Position(path: selection.end.path, offset: beginOffset + line.length)));
-  editorState.commit();
+  editorState.apply(transaction);
 }
 
 /// parse url from the line text
@@ -287,7 +287,7 @@ void _handlePastePlainText(EditorState editorState, String plainText) {
     // insert remains
     tb.insertNodes(path, nodes);
     tb.afterSelection = afterSelection;
-    editorState.commit();
+    editorState.apply(tb);
   }
 }
 
@@ -316,7 +316,7 @@ void _deleteSelectedContent(EditorState editorState) {
           ..retain(selection.start.offset)
           ..delete(len));
     tb.afterSelection = Selection.collapsed(selection.start);
-    editorState.commit();
+    editorState.apply(tb);
     return;
   }
   final traverser = NodeIterator(
@@ -347,7 +347,7 @@ void _deleteSelectedContent(EditorState editorState) {
     }
   }
   tb.afterSelection = Selection.collapsed(selection.start);
-  editorState.commit();
+  editorState.apply(tb);
 }
 
 ShortcutEventHandler copyEventHandler = (editorState, event) {

+ 9 - 9
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),
     );
-    editorState.transaction
+    final transaction = editorState.transaction
       ..deleteText(
         textNodes.first,
         selection.start.offset,
@@ -52,7 +52,7 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
         selection.end.offset,
       )
       ..afterSelection = afterSelection;
-    editorState.commit();
+    editorState.apply(transaction);
 
     if (startNode is TextNode &&
         startNode.subtype == BuiltInAttributeKey.numberList) {
@@ -77,12 +77,12 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
       final afterSelection = Selection.collapsed(
         Position(path: textNode.path, offset: 0),
       );
-      editorState.transaction
+      final transaction = editorState.transaction
         ..updateNode(textNode, {
           BuiltInAttributeKey.subtype: null,
         })
         ..afterSelection = afterSelection;
-      editorState.commit();
+      editorState.apply(transaction);
 
       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;
-        editorState.transaction
+        final transaction = editorState.transaction
           ..insertNode(
             insertPath,
             newNode,
           )
           ..afterSelection = afterSelection;
-        editorState.commit();
+        editorState.apply(transaction);
 
         makeFollowingNodesIncremental(editorState, insertPath, afterSelection,
             beginNum: prevNumber);
@@ -120,7 +120,7 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
           BuiltInAttributeKey.heading,
           BuiltInAttributeKey.quote,
         ].contains(subtype);
-        editorState.transaction
+        final transaction = editorState.transaction
           ..insertNode(
             textNode.path,
             textNode.copyWith(
@@ -130,7 +130,7 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
             ),
           )
           ..afterSelection = afterSelection;
-        editorState.commit();
+        editorState.apply(transaction);
       }
     }
     return KeyEventResult.handled;
@@ -163,7 +163,7 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
     transaction.deleteNodes(children);
   }
   transaction.afterSelection = afterSelection;
-  editorState.commit();
+  editorState.apply(transaction);
 
   // If the new type of a text node is number list,
   // the numbers of the following nodes should be incremental.

+ 138 - 8
frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text.dart

@@ -73,7 +73,7 @@ ShortcutEventHandler backquoteToCodeHandler = (editorState, event) {
       return KeyEventResult.ignored;
     }
 
-    editorState.transaction
+    final transaction = editorState.transaction
       ..deleteText(textNode, lastBackquoteIndex, 1)
       ..deleteText(textNode, firstBackquoteIndex, 2)
       ..formatText(
@@ -90,7 +90,7 @@ ShortcutEventHandler backquoteToCodeHandler = (editorState, event) {
           offset: endIndex - 3,
         ),
       );
-    editorState.commit();
+    editorState.apply(transaction);
 
     return KeyEventResult.handled;
   }
@@ -104,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.
-  editorState.transaction
+  final transaction = editorState.transaction
     ..deleteText(textNode, startIndex, 1)
     ..formatText(
       textNode,
@@ -120,7 +120,7 @@ ShortcutEventHandler backquoteToCodeHandler = (editorState, event) {
         offset: endIndex - 1,
       ),
     );
-  editorState.commit();
+  editorState.apply(transaction);
 
   return KeyEventResult.handled;
 };
@@ -166,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.
-  editorState.transaction
+  final transaction = editorState.transaction
     ..deleteText(textNode, lastTildeIndex, 1)
     ..deleteText(textNode, thirdToLastTildeIndex, 2)
     ..formatText(
@@ -183,7 +183,7 @@ ShortcutEventHandler doubleTildeToStrikethrough = (editorState, event) {
         offset: selection.end.offset - 3,
       ),
     );
-  editorState.commit();
+  editorState.apply(transaction);
 
   return KeyEventResult.handled;
 };
@@ -220,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.
-  editorState.transaction
+  final transaction = editorState.transaction
     ..deleteText(textNode, firstOpeningBracket, 1)
     ..formatText(
       textNode,
@@ -238,7 +238,137 @@ ShortcutEventHandler markdownLinkToLinkHandler = (editorState, event) {
         offset: firstOpeningBracket + linkText!.length,
       ),
     );
-  editorState.commit();
+  editorState.apply(transaction);
+
+  return KeyEventResult.handled;
+};
+
+// convert **abc** to bold abc.
+ShortcutEventHandler doubleAsterisksToBold = (editorState, event) {
+  final selectionService = editorState.service.selectionService;
+  final selection = selectionService.currentSelection.value;
+  final textNodes = selectionService.currentSelectedNodes.whereType<TextNode>();
+  if (selection == null || !selection.isSingle || textNodes.length != 1) {
+    return KeyEventResult.ignored;
+  }
+
+  final textNode = textNodes.first;
+  final text = textNode.toPlainText().substring(0, selection.end.offset);
+
+  // make sure the last two characters are **.
+  if (text.length < 2 || text[selection.end.offset - 1] != '*') {
+    return KeyEventResult.ignored;
+  }
+
+  // find all the index of `*`.
+  final asteriskIndexes = <int>[];
+  for (var i = 0; i < text.length; i++) {
+    if (text[i] == '*') {
+      asteriskIndexes.add(i);
+    }
+  }
+
+  if (asteriskIndexes.length < 3) {
+    return KeyEventResult.ignored;
+  }
+
+  // make sure the second to last and third to last asterisks are connected.
+  final thirdToLastAsteriskIndex = asteriskIndexes[asteriskIndexes.length - 3];
+  final secondToLastAsteriskIndex = asteriskIndexes[asteriskIndexes.length - 2];
+  final lastAsterisIndex = asteriskIndexes[asteriskIndexes.length - 1];
+  if (secondToLastAsteriskIndex != thirdToLastAsteriskIndex + 1 ||
+      lastAsterisIndex == secondToLastAsteriskIndex + 1) {
+    return KeyEventResult.ignored;
+  }
+
+  // delete the last three asterisks.
+  // update the style of the text surround by `** **` to bold.
+  // and update the cursor position.
+  final transaction = editorState.transaction
+    ..deleteText(textNode, lastAsterisIndex, 1)
+    ..deleteText(textNode, thirdToLastAsteriskIndex, 2)
+    ..formatText(
+      textNode,
+      thirdToLastAsteriskIndex,
+      selection.end.offset - thirdToLastAsteriskIndex - 3,
+      {
+        BuiltInAttributeKey.bold: true,
+        BuiltInAttributeKey.defaultFormating: true,
+      },
+    )
+    ..afterSelection = Selection.collapsed(
+      Position(
+        path: textNode.path,
+        offset: selection.end.offset - 3,
+      ),
+    );
+  editorState.apply(transaction);
+
+  return KeyEventResult.handled;
+};
+
+// convert __abc__ to bold abc.
+ShortcutEventHandler doubleUnderscoresToBold = (editorState, event) {
+  final selectionService = editorState.service.selectionService;
+  final selection = selectionService.currentSelection.value;
+  final textNodes = selectionService.currentSelectedNodes.whereType<TextNode>();
+  if (selection == null || !selection.isSingle || textNodes.length != 1) {
+    return KeyEventResult.ignored;
+  }
+
+  final textNode = textNodes.first;
+  final text = textNode.toPlainText().substring(0, selection.end.offset);
+
+  // make sure the last two characters are __.
+  if (text.length < 2 || text[selection.end.offset - 1] != '_') {
+    return KeyEventResult.ignored;
+  }
+
+  // find all the index of `_`.
+  final underscoreIndexes = <int>[];
+  for (var i = 0; i < text.length; i++) {
+    if (text[i] == '_') {
+      underscoreIndexes.add(i);
+    }
+  }
+
+  if (underscoreIndexes.length < 3) {
+    return KeyEventResult.ignored;
+  }
+
+  // make sure the second to last and third to last underscores are connected.
+  final thirdToLastUnderscoreIndex =
+      underscoreIndexes[underscoreIndexes.length - 3];
+  final secondToLastUnderscoreIndex =
+      underscoreIndexes[underscoreIndexes.length - 2];
+  final lastAsterisIndex = underscoreIndexes[underscoreIndexes.length - 1];
+  if (secondToLastUnderscoreIndex != thirdToLastUnderscoreIndex + 1 ||
+      lastAsterisIndex == secondToLastUnderscoreIndex + 1) {
+    return KeyEventResult.ignored;
+  }
+
+  // delete the last three underscores.
+  // update the style of the text surround by `__ __` to bold.
+  // and update the cursor position.
+  final transaction = editorState.transaction
+    ..deleteText(textNode, lastAsterisIndex, 1)
+    ..deleteText(textNode, thirdToLastUnderscoreIndex, 2)
+    ..formatText(
+      textNode,
+      thirdToLastUnderscoreIndex,
+      selection.end.offset - thirdToLastUnderscoreIndex - 3,
+      {
+        BuiltInAttributeKey.bold: true,
+        BuiltInAttributeKey.defaultFormating: true,
+      },
+    )
+    ..afterSelection = Selection.collapsed(
+      Position(
+        path: textNode.path,
+        offset: selection.end.offset - 3,
+      ),
+    );
+  editorState.apply(transaction);
 
   return KeyEventResult.handled;
 };

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

@@ -1,132 +0,0 @@
-import 'package:appflowy_editor/appflowy_editor.dart';
-import 'package:flutter/material.dart';
-
-// convert **abc** to bold abc.
-ShortcutEventHandler doubleAsterisksToBold = (editorState, event) {
-  final selectionService = editorState.service.selectionService;
-  final selection = selectionService.currentSelection.value;
-  final textNodes = selectionService.currentSelectedNodes.whereType<TextNode>();
-  if (selection == null || !selection.isSingle || textNodes.length != 1) {
-    return KeyEventResult.ignored;
-  }
-
-  final textNode = textNodes.first;
-  final text = textNode.toPlainText().substring(0, selection.end.offset);
-
-  // make sure the last two characters are **.
-  if (text.length < 2 || text[selection.end.offset - 1] != '*') {
-    return KeyEventResult.ignored;
-  }
-
-  // find all the index of `*`.
-  final asteriskIndexes = <int>[];
-  for (var i = 0; i < text.length; i++) {
-    if (text[i] == '*') {
-      asteriskIndexes.add(i);
-    }
-  }
-
-  if (asteriskIndexes.length < 3) {
-    return KeyEventResult.ignored;
-  }
-
-  // make sure the second to last and third to last asterisks are connected.
-  final thirdToLastAsteriskIndex = asteriskIndexes[asteriskIndexes.length - 3];
-  final secondToLastAsteriskIndex = asteriskIndexes[asteriskIndexes.length - 2];
-  final lastAsterisIndex = asteriskIndexes[asteriskIndexes.length - 1];
-  if (secondToLastAsteriskIndex != thirdToLastAsteriskIndex + 1 ||
-      lastAsterisIndex == secondToLastAsteriskIndex + 1) {
-    return KeyEventResult.ignored;
-  }
-
-  // delete the last three asterisks.
-  // update the style of the text surround by `** **` to bold.
-  // and update the cursor position.
-  editorState.transaction
-    ..deleteText(textNode, lastAsterisIndex, 1)
-    ..deleteText(textNode, thirdToLastAsteriskIndex, 2)
-    ..formatText(
-      textNode,
-      thirdToLastAsteriskIndex,
-      selection.end.offset - thirdToLastAsteriskIndex - 3,
-      {
-        BuiltInAttributeKey.bold: true,
-        BuiltInAttributeKey.defaultFormating: true,
-      },
-    )
-    ..afterSelection = Selection.collapsed(
-      Position(
-        path: textNode.path,
-        offset: selection.end.offset - 3,
-      ),
-    );
-  editorState.commit();
-
-  return KeyEventResult.handled;
-};
-
-// convert __abc__ to bold abc.
-ShortcutEventHandler doubleUnderscoresToBold = (editorState, event) {
-  final selectionService = editorState.service.selectionService;
-  final selection = selectionService.currentSelection.value;
-  final textNodes = selectionService.currentSelectedNodes.whereType<TextNode>();
-  if (selection == null || !selection.isSingle || textNodes.length != 1) {
-    return KeyEventResult.ignored;
-  }
-
-  final textNode = textNodes.first;
-  final text = textNode.toPlainText().substring(0, selection.end.offset);
-
-  // make sure the last two characters are __.
-  if (text.length < 2 || text[selection.end.offset - 1] != '_') {
-    return KeyEventResult.ignored;
-  }
-
-  // find all the index of `_`.
-  final underscoreIndexes = <int>[];
-  for (var i = 0; i < text.length; i++) {
-    if (text[i] == '_') {
-      underscoreIndexes.add(i);
-    }
-  }
-
-  if (underscoreIndexes.length < 3) {
-    return KeyEventResult.ignored;
-  }
-
-  // make sure the second to last and third to last underscores are connected.
-  final thirdToLastUnderscoreIndex =
-      underscoreIndexes[underscoreIndexes.length - 3];
-  final secondToLastUnderscoreIndex =
-      underscoreIndexes[underscoreIndexes.length - 2];
-  final lastAsterisIndex = underscoreIndexes[underscoreIndexes.length - 1];
-  if (secondToLastUnderscoreIndex != thirdToLastUnderscoreIndex + 1 ||
-      lastAsterisIndex == secondToLastUnderscoreIndex + 1) {
-    return KeyEventResult.ignored;
-  }
-
-  // delete the last three underscores.
-  // update the style of the text surround by `__ __` to bold.
-  // and update the cursor position.
-  editorState.transaction
-    ..deleteText(textNode, lastAsterisIndex, 1)
-    ..deleteText(textNode, thirdToLastUnderscoreIndex, 2)
-    ..formatText(
-      textNode,
-      thirdToLastUnderscoreIndex,
-      selection.end.offset - thirdToLastUnderscoreIndex - 3,
-      {
-        BuiltInAttributeKey.bold: true,
-        BuiltInAttributeKey.defaultFormating: true,
-      },
-    )
-    ..afterSelection = Selection.collapsed(
-      Position(
-        path: textNode.path,
-        offset: selection.end.offset - 3,
-      ),
-    );
-  editorState.commit();
-
-  return KeyEventResult.handled;
-};

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

@@ -15,7 +15,7 @@ void makeFollowingNodesIncremental(
   int numPtr = beginNum + 1;
   var ptr = insertNode.next;
 
-  final builder = editorState.transaction;
+  final transaction = editorState.transaction;
 
   while (ptr != null) {
     if (ptr.subtype != BuiltInAttributeKey.numberList) {
@@ -25,13 +25,13 @@ void makeFollowingNodesIncremental(
     if (currentNum != numPtr) {
       Attributes updateAttributes = {};
       updateAttributes[BuiltInAttributeKey.number] = numPtr;
-      builder.updateNode(ptr, updateAttributes);
+      transaction.updateNode(ptr, updateAttributes);
     }
 
     ptr = ptr.next;
     numPtr++;
   }
 
-  builder.afterSelection = afterSelection;
-  editorState.commit();
+  transaction.afterSelection = afterSelection;
+  editorState.apply(transaction);
 }

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

@@ -25,9 +25,10 @@ ShortcutEventHandler slashShortcutHandler = (editorState, event) {
   if (selection == null || context == null || selectable == null) {
     return KeyEventResult.ignored;
   }
-  editorState.transaction.replaceText(textNode, selection.start.offset,
-      selection.end.offset - selection.start.offset, event.character ?? '');
-  editorState.commit();
+  final transaction = editorState.transaction
+    ..replaceText(textNode, selection.start.offset,
+        selection.end.offset - selection.start.offset, event.character ?? '');
+  editorState.apply(transaction);
 
   WidgetsBinding.instance.addPostFrameCallback((_) {
     _selectionMenuService =

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

@@ -1,5 +1,5 @@
 import 'package:appflowy_editor/appflowy_editor.dart';
-import 'package:appflowy_editor/src/commands/edit_text.dart';
+import 'package:appflowy_editor/src/commands/text/text_commands.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 
@@ -15,7 +15,11 @@ ShortcutEventHandler spaceOnWebHandler = (editorState, event) {
     return KeyEventResult.ignored;
   }
 
-  insertContextInText(editorState, selection.startIndex, ' ');
+  editorState.insertText(
+    selection.startIndex,
+    ' ',
+    textNode: textNodes.first,
+  );
 
   return KeyEventResult.handled;
 };

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

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

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

@@ -99,14 +99,14 @@ KeyEventResult _toNumberList(EditorState editorState, TextNode textNode,
   ));
 
   final insertPath = textNode.path;
-  editorState.transaction
+  final transaction = editorState.transaction
     ..deleteText(textNode, 0, matchText.length)
     ..updateNode(textNode, {
       BuiltInAttributeKey.subtype: BuiltInAttributeKey.numberList,
       BuiltInAttributeKey.number: numValue
     })
     ..afterSelection = afterSelection;
-  editorState.commit();
+  editorState.apply(transaction);
 
   makeFollowingNodesIncremental(editorState, insertPath, afterSelection);
 
@@ -117,7 +117,7 @@ KeyEventResult _toBulletedList(EditorState editorState, TextNode textNode) {
   if (textNode.subtype == BuiltInAttributeKey.bulletedList) {
     return KeyEventResult.ignored;
   }
-  editorState.transaction
+  final transaction = editorState.transaction
     ..deleteText(textNode, 0, 1)
     ..updateNode(textNode, {
       BuiltInAttributeKey.subtype: BuiltInAttributeKey.bulletedList,
@@ -128,7 +128,7 @@ KeyEventResult _toBulletedList(EditorState editorState, TextNode textNode) {
         offset: 0,
       ),
     );
-  editorState.commit();
+  editorState.apply(transaction);
   return KeyEventResult.handled;
 }
 
@@ -150,7 +150,7 @@ KeyEventResult _toCheckboxList(EditorState editorState, TextNode textNode) {
     check = false;
   }
 
-  editorState.transaction
+  final transaction = editorState.transaction
     ..deleteText(textNode, 0, symbol.length)
     ..updateNode(textNode, {
       BuiltInAttributeKey.subtype: BuiltInAttributeKey.checkbox,
@@ -162,7 +162,7 @@ KeyEventResult _toCheckboxList(EditorState editorState, TextNode textNode) {
         offset: 0,
       ),
     );
-  editorState.commit();
+  editorState.apply(transaction);
   return KeyEventResult.handled;
 }
 
@@ -176,7 +176,7 @@ KeyEventResult _toHeadingStyle(
   if (textNode.attributes.heading == hX) {
     return KeyEventResult.ignored;
   }
-  editorState.transaction
+  final transaction = editorState.transaction
     ..deleteText(textNode, 0, x)
     ..updateNode(textNode, {
       BuiltInAttributeKey.subtype: BuiltInAttributeKey.heading,
@@ -188,7 +188,7 @@ KeyEventResult _toHeadingStyle(
         offset: 0,
       ),
     );
-  editorState.commit();
+  editorState.apply(transaction);
   return KeyEventResult.handled;
 }
 

+ 0 - 1
frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart

@@ -4,7 +4,6 @@ import 'package:appflowy_editor/src/service/internal_key_event_handlers/arrow_ke
 import 'package:appflowy_editor/src/service/internal_key_event_handlers/backspace_handler.dart';
 import 'package:appflowy_editor/src/service/internal_key_event_handlers/copy_paste_handler.dart';
 import 'package:appflowy_editor/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart';
-import 'package:appflowy_editor/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text_handler.dart';
 import 'package:appflowy_editor/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text.dart';
 import 'package:appflowy_editor/src/service/internal_key_event_handlers/page_up_down_handler.dart';
 import 'package:appflowy_editor/src/service/internal_key_event_handlers/redo_undo_handler.dart';

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

@@ -66,7 +66,7 @@ void main() {
     transaction.deleteNode(item1);
     transaction.deleteNode(item2);
     transaction.deleteNode(item3);
-    state.commit();
+    state.apply(transaction);
     expect(transaction.operations[0].path, [0]);
     expect(transaction.operations[1].path, [0]);
     expect(transaction.operations[2].path, [0]);
@@ -79,7 +79,7 @@ void main() {
       final item1 = Node(type: "node", attributes: {}, children: LinkedList());
       final transaction = state.transaction;
       transaction.insertNode([0], item1);
-      state.commit();
+      state.apply(transaction);
       expect(transaction.toJson(), {
         "operations": [
           {
@@ -103,7 +103,7 @@ void main() {
       final state = EditorState(document: Document(root: root));
       final transaction = state.transaction;
       transaction.deleteNode(item1);
-      state.commit();
+      state.apply(transaction);
       expect(transaction.toJson(), {
         "operations": [
           {