diff --git a/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart index 89f4977b21..8d66b25b46 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart @@ -109,8 +109,8 @@ class _MyHomePageState extends State { ..handler = (message) { debugPrint(message); }; - _editorState!.operationStream.listen((event) { - debugPrint('Operation: ${event.toJson()}'); + _editorState!.transactionStream.listen((event) { + debugPrint('Transaction: ${event.toJson()}'); }); return Container( color: darkMode ? Colors.black : Colors.white, diff --git a/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/code_block_node_widget.dart b/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/code_block_node_widget.dart index cdfae8043e..a82d20157e 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/code_block_node_widget.dart +++ b/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( 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>((String value) { return DropdownMenuItem( diff --git a/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/horizontal_rule_node_widget.dart b/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/horizontal_rule_node_widget.dart index b9f9436160..c38cc0846c 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/horizontal_rule_node_widget.dart +++ b/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); } }, ); diff --git a/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/tex_block_node_widget.dart b/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/tex_block_node_widget.dart index e4d0fac186..a6b958b0ed 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/tex_block_node_widget.dart +++ b/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'), diff --git a/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/underscore_to_italic.dart b/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/underscore_to_italic.dart index 4b34adabf1..5efbee91d2 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/underscore_to_italic.dart +++ b/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; }; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/attributes_commands.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/attributes_commands.dart new file mode 100644 index 0000000000..b506bd100c --- /dev/null +++ b/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 updateNodeAttributes( + Attributes attributes, { + Path? path, + Node? node, + }) { + return futureCommand(() { + final n = getNode(path: path, node: node); + apply( + transaction..updateNode(n, attributes), + ); + }); + } +} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/command_extension.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/command_extension.dart new file mode 100644 index 0000000000..71a6aa01de --- /dev/null +++ b/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 futureCommand(void Function() fn) async { + final completer = Completer(); + 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'); + } +} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/edit_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/edit_text.dart deleted file mode 100644 index 91b8a4159e..0000000000 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/edit_text.dart +++ /dev/null @@ -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 insertContextInText( - EditorState editorState, - int index, - String content, { - Path? path, - TextNode? textNode, -}) async { - final result = getTextNodeToBeFormatted( - editorState, - path: path, - textNode: textNode, - ); - - final completer = Completer(); - - editorState.transaction.insertText(result, index, content); - editorState.commit(); - - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - completer.complete(); - }); - - return completer.future; -} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/format_built_in_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/format_built_in_text.dart deleted file mode 100644 index 6a977bf2ef..0000000000 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/format_built_in_text.dart +++ /dev/null @@ -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 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 formatTextToCheckbox( - EditorState editorState, - bool check, { - Path? path, - TextNode? textNode, -}) async { - return formatBuiltInTextAttributes( - editorState, - BuiltInAttributeKey.checkbox, - { - BuiltInAttributeKey.checkbox: check, - }, - path: path, - textNode: textNode, - ); -} - -Future formatLinkInText( - EditorState editorState, - String? link, { - Path? path, - TextNode? textNode, -}) async { - return formatBuiltInTextAttributes( - editorState, - BuiltInAttributeKey.href, - { - BuiltInAttributeKey.href: link, - }, - path: path, - textNode: textNode, - ); -} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/format_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/format_text.dart deleted file mode 100644 index b5f235914c..0000000000 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/format_text.dart +++ /dev/null @@ -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 updateTextNodeAttributes( - EditorState editorState, - Attributes attributes, { - Path? path, - TextNode? textNode, -}) async { - final result = getTextNodeToBeFormatted( - editorState, - path: path, - textNode: textNode, - ); - - final completer = Completer(); - - editorState.transaction.updateNode(result, attributes); - editorState.commit(); - - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - completer.complete(); - }); - - return completer.future; -} - -Future 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(); - editorState.transaction.formatText( - result, - newSelection.startIndex, - newSelection.length, - attributes, - ); - editorState.commit(); - - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - completer.complete(); - }); - - return completer.future; -} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/text/text_commands.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/text/text_commands.dart new file mode 100644 index 0000000000..aa19c50d77 --- /dev/null +++ b/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 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 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 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 formatTextToCheckbox( + EditorState editorState, + bool check, { + Path? path, + TextNode? textNode, + }) async { + return formatTextWithBuiltInAttribute( + editorState, + BuiltInAttributeKey.checkbox, + {BuiltInAttributeKey.checkbox: check}, + path: path, + textNode: textNode, + ); + } + + Future formatLinkInText( + EditorState editorState, + String? link, { + Path? path, + TextNode? textNode, + }) async { + return formatTextWithBuiltInAttribute( + editorState, + BuiltInAttributeKey.href, + {BuiltInAttributeKey.href: link}, + path: path, + textNode: textNode, + ); + } +} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/text_command_infra.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/text_command_infra.dart deleted file mode 100644 index b5df6e4dff..0000000000 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/text_command_infra.dart +++ /dev/null @@ -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; -} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/core/transform/transaction.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/core/transform/transaction.dart index fd602ecd45..297600288d 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/core/transform/transaction.dart +++ b/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, diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/editor_state.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/editor_state.dart index 6c0acb16b9..872dad8e7a 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/editor_state.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/editor_state.dart @@ -9,7 +9,7 @@ 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/core/transform/transaction.dart'; -import 'package:appflowy_editor/src/undo_manager.dart'; +import 'package:appflowy_editor/src/history/undo_manager.dart'; class ApplyOptions { /// This flag indicates that @@ -63,8 +63,8 @@ class EditorState { EditorStyle editorStyle = EditorStyle.defaultStyle(); /// Operation stream. - Stream get operationStream => _observer.stream; - final StreamController _observer = StreamController.broadcast(); + Stream get transactionStream => _observer.stream; + final StreamController _observer = StreamController.broadcast(); final UndoManager undoManager = UndoManager(); Selection? _cursorSelection; @@ -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; } @@ -140,6 +128,8 @@ class EditorState { _applyOperation(op); } + _observer.add(transaction); + WidgetsBinding.instance.addPostFrameCallback((_) { updateCursorSelection(transaction.afterSelection); }); @@ -187,6 +177,5 @@ class EditorState { } else if (op is UpdateTextOperation) { document.updateText(op.path, op.delta); } - _observer.add(op); } } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/undo_manager.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/history/undo_manager.dart similarity index 100% rename from frontend/app_flowy/packages/appflowy_editor/lib/src/undo_manager.dart rename to frontend/app_flowy/packages/appflowy_editor/lib/src/history/undo_manager.dart diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_builder.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_builder.dart index d37a7e6c2a..2c84869fbf 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_builder.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_builder.dart @@ -24,20 +24,23 @@ class ImageNodeBuilder extends NodeWidgetBuilder { 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); }, ); } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_upload_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_upload_widget.dart index afec12220a..6087606c56 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_upload_widget.dart +++ b/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); } } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart index 10b17d6b36..de12388937 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart +++ b/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 name: check ? 'check' : 'uncheck', ), onTap: () async { - await formatTextToCheckbox( + await widget.editorState.formatTextToCheckbox( widget.editorState, !check, textNode: widget.textNode, diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_widget.dart index 98a37acf08..2f64e8a4a6 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_widget.dart +++ b/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 { 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 { 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); } } } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart index 091102805b..359fde0ac0 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart +++ b/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) { diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/default_text_operations/format_rich_text_style.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/default_text_operations/format_rich_text_style.dart index e6e813903c..0229d2270f 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/default_text_operations/format_rich_text_style.dart +++ b/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; } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart index bb0cfb5bd9..4933866c31 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart @@ -160,12 +160,13 @@ class _AppFlowyInputState extends State } 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 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 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 } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart index 686a9bc970..cc0f06a072 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart +++ b/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); diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart index 3dd53a1fb3..d494859957 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart +++ b/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) { diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart index cafe40c27c..44fe6d8587 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart +++ b/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. diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text.dart index e740907f01..259299b964 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text.dart +++ b/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(); + 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 = []; + 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(); + 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 = []; + 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; }; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text_handler.dart deleted file mode 100644 index 8b1c322db3..0000000000 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text_handler.dart +++ /dev/null @@ -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(); - 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 = []; - 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(); - 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 = []; - 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; -}; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/number_list_helper.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/number_list_helper.dart index c41ac07ad8..d4d14bc204 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/number_list_helper.dart +++ b/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); } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart index 259f8bcdce..9ab6d337b1 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart +++ b/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 = diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/space_on_web_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/space_on_web_handler.dart index 6d942135ba..a74e3e3c96 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/space_on_web_handler.dart +++ b/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; }; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/tab_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/tab_handler.dart index 6b92a1bf19..3b3091bbb2 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/tab_handler.dart +++ b/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; }; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/whitespace_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/whitespace_handler.dart index cc63578473..b63f20155c 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/whitespace_handler.dart +++ b/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; } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart index 7cfb7288ba..b07e8bde17 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart +++ b/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'; diff --git a/frontend/app_flowy/packages/appflowy_editor/test/legacy/operation_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/legacy/operation_test.dart index 366ab2fa0f..2516240b17 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/legacy/operation_test.dart +++ b/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": [ { diff --git a/frontend/app_flowy/packages/appflowy_editor/test/legacy/undo_manager_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/legacy/undo_manager_test.dart index bd173ce15f..7fbf4ec162 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/legacy/undo_manager_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/legacy/undo_manager_test.dart @@ -1,6 +1,6 @@ import 'dart:collection'; import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_editor/src/undo_manager.dart'; +import 'package:appflowy_editor/src/history/undo_manager.dart'; import 'package:flutter_test/flutter_test.dart'; void main() async {