diff --git a/frontend/app_flowy/packages/appflowy_editor/README.md b/frontend/app_flowy/packages/appflowy_editor/README.md index a317119829..f6fac062b0 100644 --- a/frontend/app_flowy/packages/appflowy_editor/README.md +++ b/frontend/app_flowy/packages/appflowy_editor/README.md @@ -67,7 +67,7 @@ You can also create an editor from a JSON object in order to configure your init ```dart final json = ...; final editorStyle = EditorStyle.defaultStyle(); -final editorState = EditorState(StateTree.fromJson(data)); +final editorState = EditorState(Document.fromJson(data)); final editor = AppFlowyEditor( editorState: editorState, editorStyle: editorStyle, 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 f6c2fd21ff..89f4977b21 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart @@ -98,7 +98,7 @@ class _MyHomePageState extends State { if (snapshot.hasData && snapshot.connectionState == ConnectionState.done) { _editorState ??= EditorState( - document: StateTree.fromJson( + document: Document.fromJson( Map.from( json.decode(snapshot.data!), ), 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 c40a3f0ece..cdfae8043e 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) { - TransactionBuilder(editorState) - ..insertText(codeBlockNode.first, selection.end.offset, '\n') - ..commit(); + editorState.transaction + .insertText(codeBlockNode.first, selection.end.offset, '\n'); + editorState.commit(); return KeyEventResult.handled; } return KeyEventResult.ignored; @@ -60,21 +60,20 @@ SelectionMenuItem codeBlockMenuItem = SelectionMenuItem( if (selection == null || textNodes.isEmpty) { return; } - if (textNodes.first.toRawString().isEmpty) { - TransactionBuilder(editorState) + if (textNodes.first.toPlainText().isEmpty) { + editorState.transaction ..updateNode(textNodes.first, { 'subtype': 'code_block', 'theme': 'vs', 'language': null, }) - ..afterSelection = selection - ..commit(); + ..afterSelection = selection; + editorState.commit(); } else { - TransactionBuilder(editorState) + editorState.transaction ..insertNode( selection.end.path.next, TextNode( - type: 'text', children: LinkedList(), attributes: { 'subtype': 'code_block', @@ -84,8 +83,8 @@ SelectionMenuItem codeBlockMenuItem = SelectionMenuItem( delta: Delta()..insert('\n'), ), ) - ..afterSelection = selection - ..commit(); + ..afterSelection = selection; + editorState.commit(); } }, ); @@ -149,7 +148,7 @@ class __CodeBlockNodeWidgeState extends State<_CodeBlockNodeWidge> Widget _buildCodeBlock(BuildContext context) { final result = highlight.highlight.parse( - widget.textNode.toRawString(), + widget.textNode.toPlainText(), language: _language, autoDetection: _language == null, ); @@ -182,11 +181,10 @@ class __CodeBlockNodeWidgeState extends State<_CodeBlockNodeWidge> child: DropdownButton( value: _detectLanguage, onChanged: (value) { - TransactionBuilder(widget.editorState) - ..updateNode(widget.textNode, { - 'language': value, - }) - ..commit(); + widget.editorState.transaction.updateNode(widget.textNode, { + 'language': value, + }); + widget.editorState.commit(); }, items: allLanguages.keys.map>((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 fca3df7b64..b9f9436160 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 @@ -17,8 +17,8 @@ ShortcutEventHandler _insertHorzaontalRule = (editorState, event) { return KeyEventResult.ignored; } final textNode = textNodes.first; - if (textNode.toRawString() == '--') { - TransactionBuilder(editorState) + if (textNode.toPlainText() == '--') { + editorState.transaction ..deleteText(textNode, 0, 2) ..insertNode( textNode.path, @@ -29,8 +29,8 @@ ShortcutEventHandler _insertHorzaontalRule = (editorState, event) { ), ) ..afterSelection = - Selection.single(path: textNode.path.next, startOffset: 0) - ..commit(); + Selection.single(path: textNode.path.next, startOffset: 0); + editorState.commit(); return KeyEventResult.handled; } return KeyEventResult.ignored; @@ -53,8 +53,8 @@ SelectionMenuItem horizontalRuleMenuItem = SelectionMenuItem( return; } final textNode = textNodes.first; - if (textNode.toRawString().isEmpty) { - TransactionBuilder(editorState) + if (textNode.toPlainText().isEmpty) { + editorState.transaction ..insertNode( textNode.path, Node( @@ -64,14 +64,13 @@ SelectionMenuItem horizontalRuleMenuItem = SelectionMenuItem( ), ) ..afterSelection = - Selection.single(path: textNode.path.next, startOffset: 0) - ..commit(); + Selection.single(path: textNode.path.next, startOffset: 0); + editorState.commit(); } else { - TransactionBuilder(editorState) + editorState.transaction ..insertNode( selection.end.path.next, TextNode( - type: 'text', children: LinkedList(), attributes: { 'subtype': 'horizontal_rule', @@ -79,8 +78,8 @@ SelectionMenuItem horizontalRuleMenuItem = SelectionMenuItem( delta: Delta()..insert('---'), ), ) - ..afterSelection = selection - ..commit(); + ..afterSelection = selection; + editorState.commit(); } }, ); 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 ac40a31508..e4d0fac186 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 @@ -21,9 +21,9 @@ SelectionMenuItem teXBlockMenuItem = SelectionMenuItem( return; } final Path texNodePath; - if (textNodes.first.toRawString().isEmpty) { + if (textNodes.first.toPlainText().isEmpty) { texNodePath = selection.end.path; - TransactionBuilder(editorState) + editorState.transaction ..insertNode( selection.end.path, Node( @@ -33,11 +33,11 @@ SelectionMenuItem teXBlockMenuItem = SelectionMenuItem( ), ) ..deleteNode(textNodes.first) - ..afterSelection = selection - ..commit(); + ..afterSelection = selection; + editorState.commit(); } else { texNodePath = selection.end.path.next; - TransactionBuilder(editorState) + editorState.transaction ..insertNode( selection.end.path.next, Node( @@ -46,8 +46,8 @@ SelectionMenuItem teXBlockMenuItem = SelectionMenuItem( attributes: {'tex': ''}, ), ) - ..afterSelection = selection - ..commit(); + ..afterSelection = selection; + editorState.commit(); } WidgetsBinding.instance.addPostFrameCallback((timeStamp) { final texState = @@ -142,9 +142,8 @@ class __TeXBlockNodeWidgetState extends State<_TeXBlockNodeWidget> { size: 16, ), onPressed: () { - TransactionBuilder(widget.editorState) - ..deleteNode(widget.node) - ..commit(); + widget.editorState.transaction.deleteNode(widget.node); + widget.editorState.commit(); }, ), ); @@ -175,12 +174,11 @@ class __TeXBlockNodeWidgetState extends State<_TeXBlockNodeWidget> { onPressed: () { Navigator.of(context).pop(); if (controller.text != _tex) { - TransactionBuilder(widget.editorState) - ..updateNode( - widget.node, - {'tex': controller.text}, - ) - ..commit(); + widget.editorState.transaction.updateNode( + widget.node, + {'tex': controller.text}, + ); + widget.editorState.commit(); } }, child: const Text('OK'), 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 2f8c8ac927..4b34adabf1 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 @@ -18,7 +18,7 @@ ShortcutEventHandler _underscoreToItalicHandler = (editorState, event) { } final textNode = textNodes.first; - final text = textNode.toRawString(); + final text = textNode.toPlainText(); // Determine if an 'underscore' already exists in the text node and only once. final firstUnderscore = text.indexOf('_'); final lastUnderscore = text.lastIndexOf('_'); @@ -31,7 +31,7 @@ ShortcutEventHandler _underscoreToItalicHandler = (editorState, event) { // Delete the previous 'underscore', // update the style of the text surrounded by the two underscores to 'italic', // and update the cursor position. - TransactionBuilder(editorState) + editorState.transaction ..deleteText(textNode, firstUnderscore, 1) ..formatText( textNode, @@ -46,8 +46,8 @@ ShortcutEventHandler _underscoreToItalicHandler = (editorState, event) { path: textNode.path, offset: selection.end.offset - 1, ), - ) - ..commit(); + ); + editorState.commit(); return KeyEventResult.handled; }; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/appflowy_editor.dart b/frontend/app_flowy/packages/appflowy_editor/lib/appflowy_editor.dart index b594262e95..04b2714879 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/appflowy_editor.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/appflowy_editor.dart @@ -3,18 +3,17 @@ library appflowy_editor; export 'src/infra/log.dart'; export 'src/render/style/editor_style.dart'; -export 'src/document/node.dart'; -export 'src/document/path.dart'; -export 'src/document/position.dart'; -export 'src/document/selection.dart'; -export 'src/document/state_tree.dart'; -export 'src/document/text_delta.dart'; -export 'src/document/attributes.dart'; -export 'src/document/built_in_attribute_keys.dart'; +export 'src/core/document/node.dart'; +export 'src/core/document/path.dart'; +export 'src/core/location/position.dart'; +export 'src/core/location/selection.dart'; +export 'src/core/document/document.dart'; +export 'src/core/document/text_delta.dart'; +export 'src/core/document/attributes.dart'; +export 'src/core/legacy/built_in_attribute_keys.dart'; export 'src/editor_state.dart'; -export 'src/operation/operation.dart'; -export 'src/operation/transaction.dart'; -export 'src/operation/transaction_builder.dart'; +export 'src/core/transform/operation.dart'; +export 'src/core/transform/transaction.dart'; export 'src/render/selection/selectable.dart'; export 'src/service/editor_service.dart'; export 'src/service/render_plugin_service.dart'; @@ -28,7 +27,6 @@ export 'src/service/shortcut_event/keybinding.dart'; export 'src/service/shortcut_event/shortcut_event.dart'; export 'src/service/shortcut_event/shortcut_event_handler.dart'; export 'src/extensions/attributes_extension.dart'; -export 'src/extensions/path_extensions.dart'; export 'src/render/rich_text/default_selectable.dart'; export 'src/render/rich_text/flowy_rich_text.dart'; export 'src/render/selection_menu/selection_menu_widget.dart'; 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 index 2e1310ca2c..91b8a4159e 100644 --- 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 @@ -1,10 +1,10 @@ import 'dart:async'; import 'package:appflowy_editor/src/commands/text_command_infra.dart'; -import 'package:appflowy_editor/src/document/node.dart'; -import 'package:appflowy_editor/src/document/path.dart'; +import 'package:appflowy_editor/src/core/document/node.dart'; +import 'package:appflowy_editor/src/core/document/path.dart'; import 'package:appflowy_editor/src/editor_state.dart'; -import 'package:appflowy_editor/src/operation/transaction_builder.dart'; +import 'package:appflowy_editor/src/core/transform/transaction.dart'; import 'package:flutter/widgets.dart'; Future insertContextInText( @@ -22,9 +22,8 @@ Future insertContextInText( final completer = Completer(); - TransactionBuilder(editorState) - ..insertText(result, index, content) - ..commit(); + editorState.transaction.insertText(result, index, content); + editorState.commit(); WidgetsBinding.instance.addPostFrameCallback((timeStamp) { completer.complete(); 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 index dcce054351..6a977bf2ef 100644 --- 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 @@ -1,10 +1,10 @@ import 'package:appflowy_editor/src/commands/format_text.dart'; import 'package:appflowy_editor/src/commands/text_command_infra.dart'; -import 'package:appflowy_editor/src/document/attributes.dart'; -import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart'; -import 'package:appflowy_editor/src/document/node.dart'; -import 'package:appflowy_editor/src/document/path.dart'; -import 'package:appflowy_editor/src/document/selection.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( 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 index 0ec9e7b61a..b5f235914c 100644 --- 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 @@ -1,12 +1,12 @@ import 'dart:async'; import 'package:appflowy_editor/src/commands/text_command_infra.dart'; -import 'package:appflowy_editor/src/document/attributes.dart'; -import 'package:appflowy_editor/src/document/node.dart'; -import 'package:appflowy_editor/src/document/path.dart'; -import 'package:appflowy_editor/src/document/selection.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/operation/transaction_builder.dart'; +import 'package:appflowy_editor/src/core/transform/transaction.dart'; import 'package:flutter/widgets.dart'; Future updateTextNodeAttributes( @@ -23,9 +23,8 @@ Future updateTextNodeAttributes( final completer = Completer(); - TransactionBuilder(editorState) - ..updateNode(result, attributes) - ..commit(); + editorState.transaction.updateNode(result, attributes); + editorState.commit(); WidgetsBinding.instance.addPostFrameCallback((timeStamp) { completer.complete(); @@ -49,15 +48,13 @@ Future updateTextNodeDeltaAttributes( final newSelection = getSelection(editorState, selection: selection); final completer = Completer(); - - TransactionBuilder(editorState) - ..formatText( - result, - newSelection.startIndex, - newSelection.length, - attributes, - ) - ..commit(); + editorState.transaction.formatText( + result, + newSelection.startIndex, + newSelection.length, + attributes, + ); + editorState.commit(); WidgetsBinding.instance.addPostFrameCallback((timeStamp) { completer.complete(); 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 index d54a84a3e0..b5df6e4dff 100644 --- 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 @@ -1,6 +1,6 @@ -import 'package:appflowy_editor/src/document/node.dart'; -import 'package:appflowy_editor/src/document/path.dart'; -import 'package:appflowy_editor/src/document/selection.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'; // get formatted [TextNode] diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/core/document/attributes.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/core/document/attributes.dart new file mode 100644 index 0000000000..163e8e9ab6 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/core/document/attributes.dart @@ -0,0 +1,51 @@ +/// Attributes is used to describe the Node's information. +/// +/// Please note: The keywords in [BuiltInAttributeKey] are reserved. +typedef Attributes = Map; + +Attributes? composeAttributes( + Attributes? base, + Attributes? other, { + keepNull = false, +}) { + base ??= {}; + other ??= {}; + Attributes attributes = { + ...base, + ...other, + }; + + if (!keepNull) { + attributes = Attributes.from(attributes) + ..removeWhere((_, value) => value == null); + } + + return attributes.isNotEmpty ? attributes : null; +} + +Attributes invertAttributes(Attributes? from, Attributes? to) { + from ??= {}; + to ??= {}; + final attributes = Attributes.from({}); + + // key in from but not in to, or value is different + for (final entry in from.entries) { + if ((!to.containsKey(entry.key) && entry.value != null) || + to[entry.key] != entry.value) { + attributes[entry.key] = entry.value; + } + } + + // key in to but not in from, or value is different + for (final entry in to.entries) { + if (!from.containsKey(entry.key) && entry.value != null) { + attributes[entry.key] = null; + } + } + + return attributes; +} + +int hashAttributes(Attributes base) => Object.hashAllUnordered( + base.entries.map((e) => Object.hash(e.key, e.value)), + ); diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/core/document/document.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/core/document/document.dart new file mode 100644 index 0000000000..b172e78554 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/core/document/document.dart @@ -0,0 +1,118 @@ +import 'dart:collection'; + +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/document/text_delta.dart'; + +/// [Document] reprensents a AppFlowy Editor document structure. +/// +/// It stores the root of the document. +/// +/// DO NOT directly mutate the properties of a [Document] object. +class Document { + Document({ + required this.root, + }); + + factory Document.fromJson(Map json) { + assert(json['document'] is Map); + + final document = Map.from(json['document'] as Map); + final root = Node.fromJson(document); + return Document(root: root); + } + + /// Creates a empty document with a single text node. + factory Document.empty() { + final root = Node( + type: 'editor', + children: LinkedList()..add(TextNode.empty()), + ); + return Document( + root: root, + ); + } + + final Node root; + + /// Returns the node at the given [path]. + Node? nodeAtPath(Path path) { + return root.childAtPath(path); + } + + /// Inserts a [Node]s at the given [Path]. + bool insert(Path path, Iterable nodes) { + if (path.isEmpty || nodes.isEmpty) { + return false; + } + + final target = nodeAtPath(path); + if (target != null) { + for (final node in nodes) { + target.insertBefore(node); + } + return true; + } + + final parent = nodeAtPath(path.parent); + if (parent != null) { + for (final node in nodes) { + parent.insert(node, index: path.last); + } + return true; + } + + return false; + } + + /// Deletes the [Node]s at the given [Path]. + bool delete(Path path, [int length = 1]) { + if (path.isEmpty || length <= 0) { + return false; + } + var target = nodeAtPath(path); + if (target == null) { + return false; + } + while (target != null && length > 0) { + final next = target.next; + target.unlink(); + target = next; + length--; + } + return true; + } + + /// Updates the [Node] at the given [Path] + bool update(Path path, Attributes attributes) { + if (path.isEmpty) { + return false; + } + final target = nodeAtPath(path); + if (target == null) { + return false; + } + target.updateAttributes(attributes); + return true; + } + + /// Updates the [TextNode] at the given [Path] + bool updateText(Path path, Delta delta) { + if (path.isEmpty) { + return false; + } + final target = nodeAtPath(path); + if (target == null || target is! TextNode) { + return false; + } + target.delta = target.delta.compose(delta); + return true; + } + + Map toJson() { + return { + 'document': root.toJson(), + }; + } +} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/document/node.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/core/document/node.dart similarity index 65% rename from frontend/app_flowy/packages/appflowy_editor/lib/src/document/node.dart rename to frontend/app_flowy/packages/appflowy_editor/lib/src/core/document/node.dart index 63e6525754..975ef00df3 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/document/node.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/core/document/node.dart @@ -1,47 +1,21 @@ import 'dart:collection'; -import 'package:appflowy_editor/src/document/path.dart'; -import 'package:appflowy_editor/src/document/text_delta.dart'; + import 'package:flutter/material.dart'; -import './attributes.dart'; + +import 'package:appflowy_editor/src/core/document/attributes.dart'; +import 'package:appflowy_editor/src/core/document/path.dart'; +import 'package:appflowy_editor/src/core/document/text_delta.dart'; +import 'package:appflowy_editor/src/core/legacy/built_in_attribute_keys.dart'; class Node extends ChangeNotifier with LinkedListEntry { - Node? parent; - final String type; - final LinkedList children; - Attributes _attributes; - - GlobalKey? key; - // TODO: abstract a selectable node?? - final layerLink = LayerLink(); - - String? get subtype { - // TODO: make 'subtype' as a const value. - if (_attributes.containsKey('subtype')) { - assert(_attributes['subtype'] is String?, - 'subtype must be a [String] or [null]'); - return _attributes['subtype'] as String?; - } - return null; - } - - String get id { - if (subtype != null) { - return '$type/$subtype'; - } - return type; - } - - Path get path => _path(); - - Attributes get attributes => _attributes; - Node({ required this.type, - required this.children, - required Attributes attributes, + Attributes? attributes, this.parent, - }) : _attributes = attributes { - for (final child in children) { + LinkedList? children, + }) : children = children ?? LinkedList(), + _attributes = attributes ?? {} { + for (final child in this.children) { child.parent = this; } } @@ -49,14 +23,13 @@ class Node extends ChangeNotifier with LinkedListEntry { factory Node.fromJson(Map json) { assert(json['type'] is String); - // TODO: check the type that not exist on plugins. final jType = json['type'] as String; final jChildren = json['children'] as List?; final jAttributes = json['attributes'] != null ? Attributes.from(json['attributes'] as Map) : Attributes.from({}); - final LinkedList children = LinkedList(); + final children = LinkedList(); if (jChildren != null) { children.addAll( jChildren.map( @@ -69,14 +42,14 @@ class Node extends ChangeNotifier with LinkedListEntry { Node node; - if (jType == "text") { + if (jType == 'text') { final jDelta = json['delta'] as List?; final delta = jDelta == null ? Delta() : Delta.fromJson(jDelta); node = TextNode( - type: jType, - children: children, - attributes: jAttributes, - delta: delta); + children: children, + attributes: jAttributes, + delta: delta, + ); } else { node = Node( type: jType, @@ -92,20 +65,48 @@ class Node extends ChangeNotifier with LinkedListEntry { return node; } + final String type; + final LinkedList children; + Node? parent; + Attributes _attributes; + + // Renderable + GlobalKey? key; + final layerLink = LayerLink(); + + Attributes get attributes => {..._attributes}; + + String get id { + if (subtype != null) { + return '$type/$subtype'; + } + return type; + } + + String? get subtype { + if (attributes[BuiltInAttributeKey.subtype] is String) { + return attributes[BuiltInAttributeKey.subtype] as String; + } + return null; + } + + Path get path => _computePath(); + void updateAttributes(Attributes attributes) { - final oldAttributes = {..._attributes}; - _attributes = composeAttributes(_attributes, attributes) ?? {}; + final oldAttributes = this.attributes; + + _attributes = composeAttributes(this.attributes, attributes) ?? {}; // Notifies the new attributes // if attributes contains 'subtype', should notify parent to rebuild node // else, just notify current node. bool shouldNotifyParent = - _attributes['subtype'] != oldAttributes['subtype']; + this.attributes['subtype'] != oldAttributes['subtype']; shouldNotifyParent ? parent?.notifyListeners() : notifyListeners(); } Node? childAtIndex(int index) { - if (children.length <= index) { + if (children.length <= index || index < 0) { return null; } @@ -121,7 +122,8 @@ class Node extends ChangeNotifier with LinkedListEntry { } void insert(Node entry, {int? index}) { - index ??= children.length; + final length = children.length; + index ??= length; if (children.isEmpty) { entry.parent = this; @@ -130,8 +132,9 @@ class Node extends ChangeNotifier with LinkedListEntry { return; } - final length = children.length; - + // If index is out of range, insert at the end. + // If index is negative, insert at the beginning. + // If index is positive, insert at the index. if (index >= length) { children.last.insertAfter(entry); } else if (index <= 0) { @@ -173,28 +176,14 @@ class Node extends ChangeNotifier with LinkedListEntry { }; if (children.isNotEmpty) { map['children'] = - (children.map((node) => node.toJson())).toList(growable: false); + children.map((node) => node.toJson()).toList(growable: false); } - if (_attributes.isNotEmpty) { - map['attributes'] = _attributes; + if (attributes.isNotEmpty) { + map['attributes'] = attributes; } return map; } - Path _path([Path previous = const []]) { - if (parent == null) { - return previous; - } - var index = 0; - for (var child in parent!.children) { - if (child == this) { - break; - } - index += 1; - } - return parent!._path([index, ...previous]); - } - Node copyWith({ String? type, LinkedList? children, @@ -202,8 +191,8 @@ class Node extends ChangeNotifier with LinkedListEntry { }) { final node = Node( type: type ?? this.type, - attributes: attributes ?? {..._attributes}, - children: children ?? LinkedList(), + attributes: attributes ?? {...this.attributes}, + children: children, ); if (children == null && this.children.isNotEmpty) { for (final child in this.children) { @@ -214,34 +203,43 @@ class Node extends ChangeNotifier with LinkedListEntry { } return node; } + + Path _computePath([Path previous = const []]) { + if (parent == null) { + return previous; + } + var index = 0; + for (final child in parent!.children) { + if (child == this) { + break; + } + index += 1; + } + return parent!._computePath([index, ...previous]); + } } class TextNode extends Node { - Delta _delta; - TextNode({ - required super.type, required Delta delta, LinkedList? children, Attributes? attributes, }) : _delta = delta, super( - children: children ?? LinkedList(), + type: 'text', + children: children, attributes: attributes ?? {}, ); TextNode.empty({Attributes? attributes}) - : _delta = Delta([TextInsert('')]), + : _delta = Delta(operations: [TextInsert('')]), super( type: 'text', - children: LinkedList(), attributes: attributes ?? {}, ); - Delta get delta { - return _delta; - } - + Delta _delta; + Delta get delta => _delta; set delta(Delta v) { _delta = v; notifyListeners(); @@ -250,21 +248,20 @@ class TextNode extends Node { @override Map toJson() { final map = super.toJson(); - map['delta'] = _delta.toJson(); + map['delta'] = delta.toJson(); return map; } @override TextNode copyWith({ - String? type, + String? type = 'text', LinkedList? children, Attributes? attributes, Delta? delta, }) { final textNode = TextNode( - type: type ?? this.type, children: children, - attributes: attributes ?? _attributes, + attributes: attributes ?? this.attributes, delta: delta ?? this.delta, ); if (children == null && this.children.isNotEmpty) { @@ -277,5 +274,28 @@ class TextNode extends Node { return textNode; } - String toRawString() => _delta.toRawString(); + String toPlainText() => _delta.toPlainText(); +} + +extension NodeEquality on Iterable { + bool equals(Iterable other) { + if (length != other.length) { + return false; + } + for (var i = 0; i < length; i++) { + if (!_nodeEquals(elementAt(i), other.elementAt(i))) { + return false; + } + } + return true; + } + + bool _nodeEquals(T base, U other) { + if (identical(this, other)) return true; + + return base is Node && + other is Node && + other.type == base.type && + other.children.equals(base.children); + } } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/document/node_iterator.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/core/document/node_iterator.dart similarity index 68% rename from frontend/app_flowy/packages/appflowy_editor/lib/src/document/node_iterator.dart rename to frontend/app_flowy/packages/appflowy_editor/lib/src/core/document/node_iterator.dart index ccae0f43d1..d275023ba0 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/document/node_iterator.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/core/document/node_iterator.dart @@ -1,23 +1,28 @@ -import 'package:appflowy_editor/src/document/node.dart'; - -import './state_tree.dart'; +import 'package:appflowy_editor/src/core/document/node.dart'; +import 'package:appflowy_editor/src/core/document/document.dart'; /// [NodeIterator] is used to traverse the nodes in visual order. class NodeIterator implements Iterator { - final StateTree stateTree; - final Node _startNode; - final Node? _endNode; + NodeIterator({ + required this.document, + required this.startNode, + this.endNode, + }); + + final Document document; + final Node startNode; + final Node? endNode; + Node? _currentNode; bool _began = false; - NodeIterator(this.stateTree, Node startNode, [Node? endNode]) - : _startNode = startNode, - _endNode = endNode; + @override + Node get current => _currentNode!; @override bool moveNext() { if (!_began) { - _currentNode = _startNode; + _currentNode = startNode; _began = true; return true; } @@ -27,7 +32,7 @@ class NodeIterator implements Iterator { return false; } - if (_endNode != null && _endNode == node) { + if (endNode != null && endNode == node) { _currentNode = null; return false; } @@ -42,32 +47,25 @@ class NodeIterator implements Iterator { if (nextOfParent == null) { _currentNode = null; } else { - _currentNode = _findLeadingChild(nextOfParent); + _currentNode = nextOfParent; } } return _currentNode != null; } + List toList() { + final result = []; + while (moveNext()) { + result.add(current); + } + return result; + } + Node _findLeadingChild(Node node) { while (node.children.isNotEmpty) { node = node.children.first; } return node; } - - @override - Node get current { - return _currentNode!; - } - - List toList() { - final result = []; - - while (moveNext()) { - result.add(current); - } - - return result; - } } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/path_extensions.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/core/document/path.dart similarity index 65% rename from frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/path_extensions.dart rename to frontend/app_flowy/packages/appflowy_editor/lib/src/core/document/path.dart index b7955fe83a..5e411407d5 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/path_extensions.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/core/document/path.dart @@ -1,17 +1,23 @@ -import 'package:appflowy_editor/src/document/path.dart'; - import 'dart:math'; +import 'package:flutter/foundation.dart'; + +typedef Path = List; + extension PathExtensions on Path { + bool equals(Path other) { + return listEquals(this, other); + } + bool operator >=(Path other) { - if (pathEquals(this, other)) { + if (equals(other)) { return true; } return this > other; } bool operator >(Path other) { - if (pathEquals(this, other)) { + if (equals(other)) { return false; } final length = min(this.length, other.length); @@ -29,14 +35,14 @@ extension PathExtensions on Path { } bool operator <=(Path other) { - if (pathEquals(this, other)) { + if (equals(other)) { return true; } return this < other; } bool operator <(Path other) { - if (pathEquals(this, other)) { + if (equals(other)) { return false; } final length = min(this.length, other.length); @@ -63,4 +69,22 @@ extension PathExtensions on Path { ..removeLast() ..add(last + 1); } + + Path get previous { + Path previousPath = Path.from(this, growable: true); + if (isEmpty) { + return previousPath; + } + final last = previousPath.last; + return previousPath + ..removeLast() + ..add(max(0, last - 1)); + } + + Path get parent { + if (isEmpty) { + return this; + } + return Path.from(this, growable: true)..removeLast(); + } } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/document/text_delta.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/core/document/text_delta.dart similarity index 66% rename from frontend/app_flowy/packages/appflowy_editor/lib/src/document/text_delta.dart rename to frontend/app_flowy/packages/appflowy_editor/lib/src/core/document/text_delta.dart index 3ca877cc98..5bf1832f73 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/document/text_delta.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/core/document/text_delta.dart @@ -1,165 +1,472 @@ import 'dart:collection'; import 'dart:math'; -import 'package:appflowy_editor/src/document/attributes.dart'; import 'package:flutter/foundation.dart'; +import 'package:appflowy_editor/src/core/document/attributes.dart'; + // constant number: 2^53 - 1 const int _maxInt = 9007199254740991; -abstract class TextOperation { - bool get isEmpty => length == 0; +List stringIndexes(String text) { + final indexes = List.filled(text.length, 0); + final iterator = text.runes.iterator; + while (iterator.moveNext()) { + for (var i = 0; i < iterator.currentSize; i++) { + indexes[iterator.rawIndex + i] = iterator.rawIndex; + } + } + + return indexes; +} + +abstract class TextOperation { + Attributes? get attributes; int get length; - Attributes? get attributes => null; + bool get isEmpty => length == 0; Map toJson(); } class TextInsert extends TextOperation { - String content; + TextInsert( + this.text, { + Attributes? attributes, + }) : _attributes = attributes; + + String text; final Attributes? _attributes; - TextInsert(this.content, [Attributes? attrs]) : _attributes = attrs; + @override + int get length => text.length; @override - int get length { - return content.length; - } + Attributes? get attributes => _attributes != null ? {..._attributes!} : null; @override - Attributes? get attributes { - return _attributes; + Map toJson() { + final result = { + 'insert': text, + }; + if (_attributes != null) { + result['attributes'] = attributes; + } + return result; } @override bool operator ==(Object other) { - if (other is! TextInsert) { - return false; - } - return content == other.content && + if (identical(this, other)) return true; + + return other is TextInsert && + other.text == text && mapEquals(_attributes, other._attributes); } @override - int get hashCode { - final contentHash = content.hashCode; - final attrs = _attributes; - return Object.hash( - contentHash, attrs == null ? null : hashAttributes(attrs)); - } - - @override - Map toJson() { - final result = { - 'insert': content, - }; - final attrs = _attributes; - if (attrs != null) { - result['attributes'] = {...attrs}; - } - return result; - } + int get hashCode => text.hashCode ^ _attributes.hashCode; } class TextRetain extends TextOperation { - int _length; + TextRetain( + this.length, { + Attributes? attributes, + }) : _attributes = attributes; + + @override + int length; final Attributes? _attributes; - TextRetain(length, [Attributes? attributes]) - : _length = length, - _attributes = attributes; - @override - bool get isEmpty { - return length == 0; - } - - @override - int get length { - return _length; - } - - set length(int v) { - _length = v; - } - - @override - Attributes? get attributes { - return _attributes; - } - - @override - bool operator ==(Object other) { - if (other is! TextRetain) { - return false; - } - return _length == other.length && mapEquals(_attributes, other._attributes); - } - - @override - int get hashCode { - final attrs = _attributes; - return Object.hash(_length, attrs == null ? null : hashAttributes(attrs)); - } + Attributes? get attributes => _attributes != null ? {..._attributes!} : null; @override Map toJson() { final result = { - 'retain': _length, + 'retain': length, }; - final attrs = _attributes; - if (attrs != null) { - result['attributes'] = {...attrs}; + if (_attributes != null) { + result['attributes'] = attributes; } return result; } -} - -class TextDelete extends TextOperation { - int _length; - - TextDelete(int length) : _length = length; - - @override - int get length { - return _length; - } - - set length(int v) { - _length = v; - } @override bool operator ==(Object other) { - if (other is! TextDelete) { - return false; - } - return _length == other.length; + if (identical(this, other)) return true; + + return other is TextRetain && + other.length == length && + mapEquals(_attributes, other._attributes); } @override - int get hashCode { - return _length.hashCode; - } + int get hashCode => length.hashCode ^ _attributes.hashCode; +} + +class TextDelete extends TextOperation { + TextDelete({ + required this.length, + }); + + @override + int length; + + @override + Attributes? get attributes => null; @override Map toJson() { return { - 'delete': _length, + 'delete': length, }; } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is TextDelete && other.length == length; + } + + @override + int get hashCode => length.hashCode; +} + +/// Deltas are a simple, yet expressive format that can be used to describe contents and changes. +/// The format is JSON based, and is human readable, yet easily parsible by machines. +/// Deltas can describe any rich text document, includes all text and formatting information, without the ambiguity and complexity of HTML. +/// + +/// Basically borrowed from: https://github.com/quilljs/delta +class Delta extends Iterable { + Delta({ + List? operations, + }) : _operations = operations ?? []; + + factory Delta.fromJson(List list) { + final operations = []; + + for (final value in list) { + if (value is Map) { + final op = _textOperationFromJson(value); + if (op != null) { + operations.add(op); + } + } + } + + return Delta(operations: operations); + } + + final List _operations; + String? _plainText; + List? _runeIndexes; + + void addAll(Iterable textOperations) { + textOperations.forEach(add); + } + + void add(TextOperation textOperation) { + if (textOperation.isEmpty) { + return; + } + _plainText = null; + + if (_operations.isNotEmpty) { + final lastOp = _operations.last; + if (lastOp is TextDelete && textOperation is TextDelete) { + lastOp.length += textOperation.length; + return; + } + if (mapEquals(lastOp.attributes, textOperation.attributes)) { + if (lastOp is TextInsert && textOperation is TextInsert) { + lastOp.text += textOperation.text; + return; + } + // if there is an delete before the insert + // swap the order + if (lastOp is TextDelete && textOperation is TextInsert) { + _operations.removeLast(); + _operations.add(textOperation); + _operations.add(lastOp); + return; + } + if (lastOp is TextRetain && textOperation is TextRetain) { + lastOp.length += textOperation.length; + return; + } + } + } + + _operations.add(textOperation); + } + + /// The slice() method does not change the original string. + /// The start and end parameters specifies the part of the string to extract. + /// The end position is optional. + Delta slice(int start, [int? end]) { + final result = Delta(); + final iterator = _OpIterator(_operations); + int index = 0; + + while ((end == null || index < end) && iterator.hasNext) { + TextOperation? nextOp; + if (index < start) { + nextOp = iterator._next(start - index); + } else { + nextOp = iterator._next(end == null ? null : end - index); + result.add(nextOp); + } + + index += nextOp.length; + } + + return result; + } + + /// Insert operations have an `insert` key defined. + /// A String value represents inserting text. + void insert(String text, {Attributes? attributes}) => + add(TextInsert(text, attributes: attributes)); + + /// Retain operations have a Number `retain` key defined representing the number of characters to keep (other libraries might use the name keep or skip). + /// An optional `attributes` key can be defined with an Object to describe formatting changes to the character range. + /// A value of `null` in the `attributes` Object represents removal of that key. + /// + /// *Note: It is not necessary to retain the last characters of a document as this is implied.* + void retain(int length, {Attributes? attributes}) => + add(TextRetain(length, attributes: attributes)); + + /// Delete operations have a Number `delete` key defined representing the number of characters to delete. + void delete(int length) => add(TextDelete(length: length)); + + /// The length of the string fo the [Delta]. + @override + int get length { + return _operations.fold( + 0, (previousValue, element) => previousValue + element.length); + } + + /// Returns a Delta that is equivalent to applying the operations of own Delta, followed by another Delta. + Delta compose(Delta other) { + final thisIter = _OpIterator(_operations); + final otherIter = _OpIterator(other._operations); + final operations = []; + + final firstOther = otherIter.peek(); + if (firstOther != null && + firstOther is TextRetain && + firstOther.attributes == null) { + int firstLeft = firstOther.length; + while ( + thisIter.peek() is TextInsert && thisIter.peekLength() <= firstLeft) { + firstLeft -= thisIter.peekLength(); + final next = thisIter._next(); + operations.add(next); + } + if (firstOther.length - firstLeft > 0) { + otherIter._next(firstOther.length - firstLeft); + } + } + + final delta = Delta(operations: operations); + while (thisIter.hasNext || otherIter.hasNext) { + if (otherIter.peek() is TextInsert) { + final next = otherIter._next(); + delta.add(next); + } else if (thisIter.peek() is TextDelete) { + final next = thisIter._next(); + delta.add(next); + } else { + // otherIs + final length = min(thisIter.peekLength(), otherIter.peekLength()); + final thisOp = thisIter._next(length); + final otherOp = otherIter._next(length); + final attributes = composeAttributes( + thisOp.attributes, + otherOp.attributes, + keepNull: thisOp is TextRetain, + ); + + if (otherOp is TextRetain && otherOp.length > 0) { + TextOperation? newOp; + if (thisOp is TextRetain) { + newOp = TextRetain(length, attributes: attributes); + } else if (thisOp is TextInsert) { + newOp = TextInsert(thisOp.text, attributes: attributes); + } + + if (newOp != null) { + delta.add(newOp); + } + + // Optimization if rest of other is just retain + if (!otherIter.hasNext && + delta._operations.isNotEmpty && + delta._operations.last == newOp) { + final rest = Delta(operations: thisIter.rest()); + return (delta + rest)..chop(); + } + } else if (otherOp is TextDelete && (thisOp is TextRetain)) { + delta.add(otherOp); + } + } + } + + return delta..chop(); + } + + /// This method joins two Delta together. + Delta operator +(Delta other) { + var operations = [..._operations]; + if (other._operations.isNotEmpty) { + operations.add(other._operations[0]); + operations.addAll(other._operations.sublist(1)); + } + return Delta(operations: operations); + } + + void chop() { + if (_operations.isEmpty) { + return; + } + _plainText = null; + final lastOp = _operations.last; + if (lastOp is TextRetain && (lastOp.attributes?.length ?? 0) == 0) { + _operations.removeLast(); + } + } + + @override + bool operator ==(Object other) { + if (other is! Delta) { + return false; + } + return listEquals(_operations, other._operations); + } + + @override + int get hashCode { + return Object.hashAll(_operations); + } + + /// Returned an inverted delta that has the opposite effect of against a base document delta. + Delta invert(Delta base) { + final inverted = Delta(); + _operations.fold(0, (int previousValue, op) { + if (op is TextInsert) { + inverted.delete(op.length); + } else if (op is TextRetain && op.attributes == null) { + inverted.retain(op.length); + return previousValue + op.length; + } else if (op is TextDelete || op is TextRetain) { + final length = op.length; + final slice = base.slice(previousValue, previousValue + length); + for (final baseOp in slice._operations) { + if (op is TextDelete) { + inverted.add(baseOp); + } else if (op is TextRetain && op.attributes != null) { + inverted.retain( + baseOp.length, + attributes: invertAttributes(baseOp.attributes, op.attributes), + ); + } + } + return previousValue + length; + } + return previousValue; + }); + return inverted..chop(); + } + + List toJson() { + return _operations.map((e) => e.toJson()).toList(); + } + + /// This method will return the position of the previous rune. + /// + /// Since the encoding of the [String] in Dart is UTF-16. + /// If you want to find the previous character of a position, + /// you can' just use the `position - 1` simply. + /// + /// This method can help you to compute the position of the previous character. + int prevRunePosition(int pos) { + if (pos == 0) { + return pos - 1; + } + _plainText ??= + _operations.whereType().map((op) => op.text).join(); + _runeIndexes ??= stringIndexes(_plainText!); + return _runeIndexes![pos - 1]; + } + + /// This method will return the position of the next rune. + /// + /// Since the encoding of the [String] in Dart is UTF-16. + /// If you want to find the previous character of a position, + /// you can' just use the `position + 1` simply. + /// + /// This method can help you to compute the position of the next character. + int nextRunePosition(int pos) { + final stringContent = toPlainText(); + if (pos >= stringContent.length - 1) { + return stringContent.length; + } + _runeIndexes ??= stringIndexes(_plainText!); + + for (var i = pos + 1; i < _runeIndexes!.length; i++) { + if (_runeIndexes![i] != pos) { + return _runeIndexes![i]; + } + } + + return stringContent.length; + } + + String toPlainText() { + _plainText ??= + _operations.whereType().map((op) => op.text).join(); + return _plainText!; + } + + @override + Iterator get iterator => _operations.iterator; + + static TextOperation? _textOperationFromJson(Map json) { + TextOperation? operation; + + if (json['insert'] is String) { + final attributes = json['attributes'] as Map?; + operation = TextInsert( + json['insert'] as String, + attributes: attributes != null ? {...attributes} : null, + ); + } else if (json['retain'] is int) { + final attrs = json['attributes'] as Map?; + operation = TextRetain( + json['retain'] as int, + attributes: attrs != null ? {...attrs} : null, + ); + } else if (json['delete'] is int) { + operation = TextDelete(length: json['delete'] as int); + } + + return operation; + } } class _OpIterator { + _OpIterator( + Iterable operations, + ) : _operations = UnmodifiableListView(operations); + final UnmodifiableListView _operations; int _index = 0; int _offset = 0; - _OpIterator(List operations) - : _operations = UnmodifiableListView(operations); - bool get hasNext { return peekLength() < _maxInt; } @@ -199,20 +506,17 @@ class _OpIterator { _offset += length; } if (nextOp is TextDelete) { - return TextDelete(length); + return TextDelete(length: length); } if (nextOp is TextRetain) { - return TextRetain( - length, - nextOp.attributes, - ); + return TextRetain(length, attributes: nextOp.attributes); } if (nextOp is TextInsert) { return TextInsert( - nextOp.content.substring(offset, offset + length), - nextOp.attributes, + nextOp.text.substring(offset, offset + length), + attributes: nextOp.attributes, ); } @@ -235,325 +539,3 @@ class _OpIterator { } } } - -TextOperation? _textOperationFromJson(Map json) { - TextOperation? result; - - if (json['insert'] is String) { - final attrs = json['attributes'] as Map?; - result = - TextInsert(json['insert'] as String, attrs == null ? null : {...attrs}); - } else if (json['retain'] is int) { - final attrs = json['attributes'] as Map?; - result = - TextRetain(json['retain'] as int, attrs == null ? null : {...attrs}); - } else if (json['delete'] is int) { - result = TextDelete(json['delete'] as int); - } - - return result; -} - -/// Deltas are a simple, yet expressive format that can be used to describe contents and changes. -/// The format is JSON based, and is human readable, yet easily parsible by machines. -/// Deltas can describe any rich text document, includes all text and formatting information, without the ambiguity and complexity of HTML. -/// - -/// Basically borrowed from: https://github.com/quilljs/delta -class Delta extends Iterable { - final List _operations; - String? _rawString; - List? _runeIndexes; - - factory Delta.fromJson(List list) { - final operations = []; - - for (final obj in list) { - final op = _textOperationFromJson(obj as Map); - if (op != null) { - operations.add(op); - } - } - - return Delta(operations); - } - - Delta([List? ops]) : _operations = ops ?? []; - - void addAll(Iterable textOps) { - textOps.forEach(add); - } - - void add(TextOperation textOp) { - if (textOp.isEmpty) { - return; - } - _rawString = null; - - if (_operations.isNotEmpty) { - final lastOp = _operations.last; - if (lastOp is TextDelete && textOp is TextDelete) { - lastOp.length += textOp.length; - return; - } - if (mapEquals(lastOp.attributes, textOp.attributes)) { - if (lastOp is TextInsert && textOp is TextInsert) { - lastOp.content += textOp.content; - return; - } - // if there is an delete before the insert - // swap the order - if (lastOp is TextDelete && textOp is TextInsert) { - _operations.removeLast(); - _operations.add(textOp); - _operations.add(lastOp); - return; - } - if (lastOp is TextRetain && textOp is TextRetain) { - lastOp.length += textOp.length; - return; - } - } - } - - _operations.add(textOp); - } - - /// The slice() method does not change the original string. - /// The start and end parameters specifies the part of the string to extract. - /// The end position is optional. - Delta slice(int start, [int? end]) { - final result = Delta(); - final iterator = _OpIterator(_operations); - int index = 0; - - while ((end == null || index < end) && iterator.hasNext) { - TextOperation? nextOp; - if (index < start) { - nextOp = iterator._next(start - index); - } else { - nextOp = iterator._next(end == null ? null : end - index); - result.add(nextOp); - } - - index += nextOp.length; - } - - return result; - } - - /// Insert operations have an `insert` key defined. - /// A String value represents inserting text. - void insert(String content, [Attributes? attributes]) => - add(TextInsert(content, attributes)); - - /// Retain operations have a Number `retain` key defined representing the number of characters to keep (other libraries might use the name keep or skip). - /// An optional `attributes` key can be defined with an Object to describe formatting changes to the character range. - /// A value of `null` in the `attributes` Object represents removal of that key. - /// - /// *Note: It is not necessary to retain the last characters of a document as this is implied.* - void retain(int length, [Attributes? attributes]) => - add(TextRetain(length, attributes)); - - /// Delete operations have a Number `delete` key defined representing the number of characters to delete. - void delete(int length) => add(TextDelete(length)); - - /// The length of the string fo the [Delta]. - @override - int get length { - return _operations.fold( - 0, (previousValue, element) => previousValue + element.length); - } - - /// Returns a Delta that is equivalent to applying the operations of own Delta, followed by another Delta. - Delta compose(Delta other) { - final thisIter = _OpIterator(_operations); - final otherIter = _OpIterator(other._operations); - final ops = []; - - final firstOther = otherIter.peek(); - if (firstOther != null && - firstOther is TextRetain && - firstOther.attributes == null) { - int firstLeft = firstOther.length; - while ( - thisIter.peek() is TextInsert && thisIter.peekLength() <= firstLeft) { - firstLeft -= thisIter.peekLength(); - final next = thisIter._next(); - ops.add(next); - } - if (firstOther.length - firstLeft > 0) { - otherIter._next(firstOther.length - firstLeft); - } - } - - final delta = Delta(ops); - while (thisIter.hasNext || otherIter.hasNext) { - if (otherIter.peek() is TextInsert) { - final next = otherIter._next(); - delta.add(next); - } else if (thisIter.peek() is TextDelete) { - final next = thisIter._next(); - delta.add(next); - } else { - // otherIs - final length = min(thisIter.peekLength(), otherIter.peekLength()); - final thisOp = thisIter._next(length); - final otherOp = otherIter._next(length); - final attributes = composeAttributes( - thisOp.attributes, otherOp.attributes, thisOp is TextRetain); - if (otherOp is TextRetain && otherOp.length > 0) { - TextOperation? newOp; - if (thisOp is TextRetain) { - newOp = TextRetain(length, attributes); - } else if (thisOp is TextInsert) { - newOp = TextInsert(thisOp.content, attributes); - } - - if (newOp != null) { - delta.add(newOp); - } - - // Optimization if rest of other is just retain - if (!otherIter.hasNext && - delta._operations.isNotEmpty && - delta._operations.last == newOp) { - final rest = Delta(thisIter.rest()); - return (delta + rest)..chop(); - } - } else if (otherOp is TextDelete && (thisOp is TextRetain)) { - delta.add(otherOp); - } - } - } - - return delta..chop(); - } - - /// This method joins two Delta together. - Delta operator +(Delta other) { - var ops = [..._operations]; - if (other._operations.isNotEmpty) { - ops.add(other._operations[0]); - ops.addAll(other._operations.sublist(1)); - } - return Delta(ops); - } - - void chop() { - if (_operations.isEmpty) { - return; - } - _rawString = null; - final lastOp = _operations.last; - if (lastOp is TextRetain && (lastOp.attributes?.length ?? 0) == 0) { - _operations.removeLast(); - } - } - - @override - bool operator ==(Object other) { - if (other is! Delta) { - return false; - } - return listEquals(_operations, other._operations); - } - - @override - int get hashCode { - return Object.hashAll(_operations); - } - - /// Returned an inverted delta that has the opposite effect of against a base document delta. - Delta invert(Delta base) { - final inverted = Delta(); - _operations.fold(0, (int previousValue, op) { - if (op is TextInsert) { - inverted.delete(op.length); - } else if (op is TextRetain && op.attributes == null) { - inverted.retain(op.length); - return previousValue + op.length; - } else if (op is TextDelete || op is TextRetain) { - final length = op.length; - final slice = base.slice(previousValue, previousValue + length); - for (final baseOp in slice._operations) { - if (op is TextDelete) { - inverted.add(baseOp); - } else if (op is TextRetain && op.attributes != null) { - inverted.retain(baseOp.length, - invertAttributes(op.attributes, baseOp.attributes)); - } - } - return previousValue + length; - } - return previousValue; - }); - return inverted..chop(); - } - - List toJson() { - return _operations.map((e) => e.toJson()).toList(); - } - - /// This method will return the position of the previous rune. - /// - /// Since the encoding of the [String] in Dart is UTF-16. - /// If you want to find the previous character of a position, - /// you can' just use the `position - 1` simply. - /// - /// This method can help you to compute the position of the previous character. - int prevRunePosition(int pos) { - if (pos == 0) { - return pos - 1; - } - _rawString ??= - _operations.whereType().map((op) => op.content).join(); - _runeIndexes ??= stringIndexes(_rawString!); - return _runeIndexes![pos - 1]; - } - - /// This method will return the position of the next rune. - /// - /// Since the encoding of the [String] in Dart is UTF-16. - /// If you want to find the previous character of a position, - /// you can' just use the `position + 1` simply. - /// - /// This method can help you to compute the position of the next character. - int nextRunePosition(int pos) { - final stringContent = toRawString(); - if (pos >= stringContent.length - 1) { - return stringContent.length; - } - _runeIndexes ??= stringIndexes(_rawString!); - - for (var i = pos + 1; i < _runeIndexes!.length; i++) { - if (_runeIndexes![i] != pos) { - return _runeIndexes![i]; - } - } - - return stringContent.length; - } - - String toRawString() { - _rawString ??= - _operations.whereType().map((op) => op.content).join(); - return _rawString!; - } - - @override - Iterator get iterator => _operations.iterator; -} - -List stringIndexes(String content) { - final indexes = List.filled(content.length, 0); - final iterator = content.runes.iterator; - - while (iterator.moveNext()) { - for (var i = 0; i < iterator.currentSize; i++) { - indexes[iterator.rawIndex + i] = iterator.rawIndex; - } - } - - return indexes; -} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/document/built_in_attribute_keys.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/core/legacy/built_in_attribute_keys.dart similarity index 100% rename from frontend/app_flowy/packages/appflowy_editor/lib/src/document/built_in_attribute_keys.dart rename to frontend/app_flowy/packages/appflowy_editor/lib/src/core/legacy/built_in_attribute_keys.dart diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/document/position.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/core/location/position.dart similarity index 59% rename from frontend/app_flowy/packages/appflowy_editor/lib/src/document/position.dart rename to frontend/app_flowy/packages/appflowy_editor/lib/src/core/location/position.dart index cea065fdea..e793faa625 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/document/position.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/core/location/position.dart @@ -1,4 +1,4 @@ -import './path.dart'; +import 'package:appflowy_editor/src/core/document/path.dart'; class Position { final Path path; @@ -11,17 +11,18 @@ class Position { @override bool operator ==(Object other) { - if (other is! Position) { - return false; - } - return pathEquals(path, other.path) && offset == other.offset; + if (identical(this, other)) return true; + + return other is Position && + other.path.equals(path) && + other.offset == offset; } @override - int get hashCode { - final pathHash = Object.hashAll(path); - return Object.hash(pathHash, offset); - } + int get hashCode => Object.hash(offset, Object.hashAll(path)); + + @override + String toString() => 'path = $path, offset = $offset'; Position copyWith({Path? path, int? offset}) { return Position( @@ -30,13 +31,10 @@ class Position { ); } - @override - String toString() => 'path = $path, offset = $offset'; - Map toJson() { return { - "path": path.toList(), - "offset": offset, + 'path': path, + 'offset': offset, }; } } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/document/selection.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/core/location/selection.dart similarity index 55% rename from frontend/app_flowy/packages/appflowy_editor/lib/src/document/selection.dart rename to frontend/app_flowy/packages/appflowy_editor/lib/src/core/location/selection.dart index 99248dc167..b22d743c7d 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/document/selection.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/core/location/selection.dart @@ -1,6 +1,5 @@ -import 'package:appflowy_editor/src/document/path.dart'; -import 'package:appflowy_editor/src/document/position.dart'; -import 'package:appflowy_editor/src/extensions/path_extensions.dart'; +import 'package:appflowy_editor/src/core/document/path.dart'; +import 'package:appflowy_editor/src/core/location/position.dart'; /// Selection represents the selected area or the cursor area in the editor. /// @@ -37,31 +36,58 @@ class Selection { final Position start; final Position end; + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is Selection && other.start == start && other.end == end; + } + + @override + int get hashCode => start.hashCode ^ end.hashCode; + + @override + String toString() => 'start = $start, end = $end'; + + /// Returns a Boolean indicating whether the selection's start and end points + /// are at the same position. bool get isCollapsed => start == end; - bool get isSingle => pathEquals(start.path, end.path); + + /// Returns a Boolean indicating whether the selection's start and end points + /// are at the same path. + bool get isSingle => start.path.equals(end.path); + + /// Returns a Boolean indicating whether the selection is forward. bool get isForward => (start.path > end.path) || (isSingle && start.offset > end.offset); + + /// Returns a Boolean indicating whether the selection is backward. bool get isBackward => (start.path < end.path) || (isSingle && start.offset < end.offset); - Selection get normalize { - if (isForward) { - return reversed; - } - return this; - } + /// Returns a normalized selection that direction is forward. + Selection get normalized => isBackward ? copyWith() : reversed.copyWith(); + /// Returns a reversed selection. Selection get reversed => copyWith(start: end, end: start); - int get startIndex => normalize.start.offset; - int get endIndex => normalize.end.offset; + /// Returns the offset in the starting position under the normalized selection. + int get startIndex => normalized.start.offset; + + /// Returns the offset in the ending position under the normalized selection. + int get endIndex => normalized.end.offset; + int get length => endIndex - startIndex; + /// Collapses the current selection to a single point. + /// + /// If [atStart] is true, the selection will be collapsed to the start point. + /// If [atStart] is false, the selection will be collapsed to the end point. Selection collapse({bool atStart = false}) { if (atStart) { - return Selection(start: start, end: start); + return copyWith(end: start); } else { - return Selection(start: end, end: end); + return copyWith(start: end); } } @@ -72,29 +98,10 @@ class Selection { ); } - Selection copy() => Selection(start: start, end: end); - Map toJson() { return { 'start': start.toJson(), 'end': end.toJson(), }; } - - @override - bool operator ==(Object other) { - if (other is! Selection) { - return false; - } - if (identical(this, other)) { - return true; - } - return start == other.start && end == other.end; - } - - @override - int get hashCode => Object.hash(start, end); - - @override - String toString() => '[Selection] start = $start, end = $end'; } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/core/transform/operation.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/core/transform/operation.dart new file mode 100644 index 0000000000..31662a61ca --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/core/transform/operation.dart @@ -0,0 +1,273 @@ +import 'package:flutter/foundation.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/document/text_delta.dart'; + +/// [Operation] represents a change to a [Document]. +abstract class Operation { + Operation( + this.path, + ); + + factory Operation.fromJson() => throw UnimplementedError(); + + final Path path; + + /// Inverts the operation. + /// + /// Returns the inverted operation. + Operation invert(); + + /// Returns the JSON representation of the operation. + Map toJson(); + + Operation copyWith({Path? path}); +} + +/// [InsertOperation] represents an insert operation. +class InsertOperation extends Operation { + InsertOperation( + super.path, + this.nodes, + ); + + factory InsertOperation.fromJson(Map json) { + final path = json['path'] as Path; + final nodes = (json['nodes'] as List) + .map((n) => Node.fromJson(n)) + .toList(growable: false); + return InsertOperation(path, nodes); + } + + final Iterable nodes; + + @override + Operation invert() => DeleteOperation(path, nodes); + + @override + Map toJson() { + return { + 'op': 'insert', + 'path': path, + 'nodes': nodes.map((n) => n.toJson()).toList(growable: false), + }; + } + + @override + Operation copyWith({Path? path}) { + return InsertOperation(path ?? this.path, nodes); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is InsertOperation && + other.path.equals(path) && + other.nodes.equals(nodes); + } + + @override + int get hashCode => path.hashCode ^ Object.hashAll(nodes); +} + +/// [DeleteOperation] represents a delete operation. +class DeleteOperation extends Operation { + DeleteOperation( + super.path, + this.nodes, + ); + + factory DeleteOperation.fromJson(Map json) { + final path = json['path'] as Path; + final nodes = (json['nodes'] as List) + .map((n) => Node.fromJson(n)) + .toList(growable: false); + return DeleteOperation(path, nodes); + } + + final Iterable nodes; + + @override + Operation invert() => InsertOperation(path, nodes); + + @override + Map toJson() { + return { + 'op': 'delete', + 'path': path, + 'nodes': nodes.map((n) => n.toJson()).toList(growable: false), + }; + } + + @override + Operation copyWith({Path? path}) { + return DeleteOperation(path ?? this.path, nodes); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is DeleteOperation && + other.path.equals(path) && + other.nodes.equals(nodes); + } + + @override + int get hashCode => path.hashCode ^ Object.hashAll(nodes); +} + +/// [UpdateOperation] represents an attributes update operation. +class UpdateOperation extends Operation { + UpdateOperation( + super.path, + this.attributes, + this.oldAttributes, + ); + + factory UpdateOperation.fromJson(Map json) { + final path = json['path'] as Path; + final oldAttributes = json['oldAttributes'] as Attributes; + final attributes = json['attributes'] as Attributes; + return UpdateOperation( + path, + attributes, + oldAttributes, + ); + } + + final Attributes attributes; + final Attributes oldAttributes; + + @override + Operation invert() => UpdateOperation( + path, + oldAttributes, + attributes, + ); + + @override + Map toJson() { + return { + 'op': 'update', + 'path': path, + 'attributes': {...attributes}, + 'oldAttributes': {...oldAttributes}, + }; + } + + @override + Operation copyWith({Path? path}) { + return UpdateOperation( + path ?? this.path, + {...attributes}, + {...oldAttributes}, + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is UpdateOperation && + other.path.equals(path) && + mapEquals(other.attributes, attributes) && + mapEquals(other.oldAttributes, oldAttributes); + } + + @override + int get hashCode => + path.hashCode ^ attributes.hashCode ^ oldAttributes.hashCode; +} + +/// [UpdateTextOperation] represents a text update operation. +class UpdateTextOperation extends Operation { + UpdateTextOperation( + super.path, + this.delta, + this.inverted, + ); + + factory UpdateTextOperation.fromJson(Map json) { + final path = json['path'] as Path; + final delta = Delta.fromJson(json['delta']); + final inverted = Delta.fromJson(json['inverted']); + return UpdateTextOperation(path, delta, inverted); + } + + final Delta delta; + final Delta inverted; + + @override + Operation invert() => UpdateTextOperation(path, inverted, delta); + + @override + Map toJson() { + return { + 'op': 'update_text', + 'path': path, + 'delta': delta.toJson(), + 'inverted': inverted.toJson(), + }; + } + + @override + Operation copyWith({Path? path}) { + return UpdateTextOperation(path ?? this.path, delta, inverted); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is UpdateTextOperation && + other.path.equals(path) && + other.delta == delta && + other.inverted == inverted; + } + + @override + int get hashCode => delta.hashCode ^ inverted.hashCode; +} + +// TODO(Lucas.Xu): refactor this part +Path transformPath(Path preInsertPath, Path b, [int delta = 1]) { + if (preInsertPath.length > b.length) { + return b; + } + if (preInsertPath.isEmpty || b.isEmpty) { + return b; + } + // check the prefix + for (var i = 0; i < preInsertPath.length - 1; i++) { + if (preInsertPath[i] != b[i]) { + return b; + } + } + final prefix = preInsertPath.sublist(0, preInsertPath.length - 1); + final suffix = b.sublist(preInsertPath.length); + final preInsertLast = preInsertPath.last; + final bAtIndex = b[preInsertPath.length - 1]; + if (preInsertLast <= bAtIndex) { + prefix.add(bAtIndex + delta); + } else { + prefix.add(bAtIndex); + } + prefix.addAll(suffix); + return prefix; +} + +Operation transformOperation(Operation a, Operation b) { + if (a is InsertOperation) { + final newPath = transformPath(a.path, b.path, a.nodes.length); + return b.copyWith(path: newPath); + } else if (a is DeleteOperation) { + final newPath = transformPath(a.path, b.path, -1 * a.nodes.length); + return b.copyWith(path: newPath); + } + // TODO: transform update and textedit + return b; +} 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 new file mode 100644 index 0000000000..fd602ecd45 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/core/transform/transaction.dart @@ -0,0 +1,267 @@ +import 'dart:math'; + +import 'package:appflowy_editor/src/core/document/attributes.dart'; +import 'package:appflowy_editor/src/core/document/document.dart'; +import 'package:appflowy_editor/src/core/document/node.dart'; +import 'package:appflowy_editor/src/core/document/path.dart'; +import 'package:appflowy_editor/src/core/document/text_delta.dart'; +import 'package:appflowy_editor/src/core/location/position.dart'; +import 'package:appflowy_editor/src/core/location/selection.dart'; +import 'package:appflowy_editor/src/core/transform/operation.dart'; + +/// A [Transaction] has a list of [Operation] objects that will be applied +/// to the editor. +/// +/// There will be several ways to consume the transaction: +/// 1. Apply to the state to update the UI. +/// 2. Send to the backend to store and do operation transforming. +class Transaction { + Transaction({ + required this.document, + }); + + final Document document; + + /// The operations to be applied. + final List operations = []; + + /// The selection to be applied. + Selection? afterSelection; + + /// The before selection is to be recovered if needed. + Selection? beforeSelection; + + /// Inserts the [Node] at the given [Path]. + void insertNode( + Path path, + Node node, { + bool deepCopy = true, + }) { + insertNodes(path, [node], deepCopy: deepCopy); + } + + /// Inserts a sequence of [Node]s at the given [Path]. + void insertNodes( + Path path, + Iterable nodes, { + bool deepCopy = true, + }) { + if (deepCopy) { + add(InsertOperation(path, nodes.map((e) => e.copyWith()))); + } else { + add(InsertOperation(path, nodes)); + } + } + + /// Updates the attributes of the [Node]. + /// + /// The [attributes] will be merged into the existing attributes. + void updateNode(Node node, Attributes attributes) { + final inverted = invertAttributes(node.attributes, attributes); + add(UpdateOperation( + node.path, + {...attributes}, + inverted, + )); + } + + /// Deletes the [Node] in the document. + void deleteNode(Node node) { + deleteNodesAtPath(node.path); + } + + /// Deletes the [Node]s in the document. + void deleteNodes(Iterable nodes) { + nodes.forEach(deleteNode); + } + + /// Deletes the [Node]s at the given [Path]. + /// + /// The [length] indicates the number of consecutive deletions, + /// including the node of the current path. + void deleteNodesAtPath(Path path, [int length = 1]) { + if (path.isEmpty) return; + final nodes = []; + final parent = path.parent; + for (var i = 0; i < length; i++) { + final node = document.nodeAtPath(parent + [path.last + i]); + if (node == null) { + break; + } + nodes.add(node); + } + add(DeleteOperation(path, nodes)); + } + + /// Update the [TextNode]s with the given [Delta]. + void updateText(TextNode textNode, Delta delta) { + final inverted = delta.invert(textNode.delta); + add(UpdateTextOperation(textNode.path, delta, inverted)); + } + + /// Returns the JSON representation of the transaction. + Map toJson() { + final json = {}; + if (operations.isNotEmpty) { + json['operations'] = operations.map((o) => o.toJson()); + } + if (afterSelection != null) { + json['after_selection'] = afterSelection!.toJson(); + } + if (beforeSelection != null) { + json['before_selection'] = beforeSelection!.toJson(); + } + return json; + } + + /// Adds an operation to the transaction. + /// This method will merge operations if they are both TextEdits. + /// + /// Also, this method will transform the path of the operations + /// to avoid conflicts. + add(Operation op, {bool transform = true}) { + final Operation? last = operations.isEmpty ? null : operations.last; + if (last != null) { + if (op is UpdateTextOperation && + last is UpdateTextOperation && + op.path.equals(last.path)) { + final newOp = UpdateTextOperation( + op.path, + last.delta.compose(op.delta), + op.inverted.compose(last.inverted), + ); + operations[operations.length - 1] = newOp; + return; + } + } + if (transform) { + for (var i = 0; i < operations.length; i++) { + op = transformOperation(operations[i], op); + } + } + if (op is UpdateTextOperation && op.delta.isEmpty) { + return; + } + operations.add(op); + } +} + +extension TextTransaction on Transaction { + void mergeText( + TextNode first, + TextNode second, { + int? firstOffset, + int secondOffset = 0, + }) { + final firstLength = first.delta.length; + final secondLength = second.delta.length; + firstOffset ??= firstLength; + updateText( + first, + Delta() + ..retain(firstOffset) + ..delete(firstLength - firstOffset) + ..addAll(second.delta.slice(secondOffset, secondLength)), + ); + afterSelection = Selection.collapsed(Position( + path: first.path, + offset: firstOffset, + )); + } + + /// Inserts the text content at a specified index. + /// + /// Optionally, you may specify formatting attributes that are applied to the inserted string. + /// By default, the formatting attributes before the insert position will be reused. + void insertText( + TextNode textNode, + int index, + String text, { + Attributes? attributes, + }) { + var newAttributes = attributes; + if (index != 0 && attributes == null) { + newAttributes = + textNode.delta.slice(max(index - 1, 0), index).first.attributes; + if (newAttributes != null) { + newAttributes = {...newAttributes}; // make a copy + } + } + updateText( + textNode, + Delta() + ..retain(index) + ..insert(text, attributes: newAttributes), + ); + afterSelection = Selection.collapsed( + Position(path: textNode.path, offset: index + text.length), + ); + } + + /// Assigns a formatting attributes to a range of text. + formatText( + TextNode textNode, + int index, + int length, + Attributes attributes, + ) { + afterSelection = beforeSelection; + updateText( + textNode, + Delta() + ..retain(index) + ..retain(length, attributes: attributes), + ); + } + + /// Deletes the text of specified length starting at index. + deleteText( + TextNode textNode, + int index, + int length, + ) { + updateText( + textNode, + Delta() + ..retain(index) + ..delete(length), + ); + afterSelection = Selection.collapsed( + Position(path: textNode.path, offset: index), + ); + } + + /// Replaces the text of specified length starting at index. + /// + /// Optionally, you may specify formatting attributes that are applied to the inserted string. + /// By default, the formatting attributes before the insert position will be reused. + replaceText( + TextNode textNode, + int index, + int length, + String text, { + Attributes? attributes, + }) { + var newAttributes = attributes; + if (index != 0 && attributes == null) { + newAttributes = + textNode.delta.slice(max(index - 1, 0), index).first.attributes; + if (newAttributes != null) { + newAttributes = {...newAttributes}; // make a copy + } + } + updateText( + textNode, + Delta() + ..retain(index) + ..delete(length) + ..insert(text, attributes: newAttributes), + ); + afterSelection = Selection.collapsed( + Position( + path: textNode.path, + offset: index + text.length, + ), + ); + } +} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/document/attributes.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/document/attributes.dart deleted file mode 100644 index 1a846eec2c..0000000000 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/document/attributes.dart +++ /dev/null @@ -1,42 +0,0 @@ -typedef Attributes = Map; - -int hashAttributes(Attributes attributes) { - return Object.hashAllUnordered( - attributes.entries.map((e) => Object.hash(e.key, e.value))); -} - -Attributes invertAttributes(Attributes? attr, Attributes? base) { - attr ??= {}; - base ??= {}; - final Attributes baseInverted = base.keys.fold({}, (memo, key) { - if (base![key] != attr![key] && attr.containsKey(key)) { - memo[key] = base[key]; - } - return memo; - }); - return attr.keys.fold(baseInverted, (memo, key) { - if (attr![key] != base![key] && !base.containsKey(key)) { - memo[key] = null; - } - return memo; - }); -} - -Attributes? composeAttributes(Attributes? a, Attributes? b, - [bool keepNull = false]) { - a ??= {}; - b ??= {}; - Attributes attributes = {...b}; - - if (!keepNull) { - attributes = Map.from(attributes)..removeWhere((_, value) => value == null); - } - - for (final entry in a.entries) { - if (!b.containsKey(entry.key)) { - attributes[entry.key] = entry.value; - } - } - - return attributes.isNotEmpty ? attributes : null; -} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/document/path.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/document/path.dart deleted file mode 100644 index a8163f094d..0000000000 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/document/path.dart +++ /dev/null @@ -1,7 +0,0 @@ -import 'package:flutter/foundation.dart'; - -typedef Path = List; - -bool pathEquals(Path path1, Path path2) { - return listEquals(path1, path2); -} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/document/state_tree.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/document/state_tree.dart deleted file mode 100644 index a4a9869df5..0000000000 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/document/state_tree.dart +++ /dev/null @@ -1,116 +0,0 @@ -import 'dart:math'; - -import 'package:appflowy_editor/src/document/node.dart'; -import 'package:appflowy_editor/src/document/path.dart'; -import 'package:appflowy_editor/src/document/text_delta.dart'; -import './attributes.dart'; - -class StateTree { - final Node root; - - StateTree({ - required this.root, - }); - - factory StateTree.empty() { - return StateTree( - root: Node.fromJson({ - 'type': 'editor', - 'children': [ - { - 'type': 'text', - } - ] - }), - ); - } - - factory StateTree.fromJson(Attributes json) { - assert(json['document'] is Map); - - final document = Map.from(json['document'] as Map); - final root = Node.fromJson(document); - return StateTree(root: root); - } - - Map toJson() { - return { - 'document': root.toJson(), - }; - } - - Node? nodeAtPath(Path path) { - return root.childAtPath(path); - } - - bool insert(Path path, List nodes) { - if (path.isEmpty) { - return false; - } - Node? insertedNode = root.childAtPath( - path.sublist(0, path.length - 1) + [max(0, path.last - 1)], - ); - if (insertedNode == null) { - final insertedNode = root.childAtPath( - path.sublist(0, path.length - 1), - ); - if (insertedNode != null) { - for (final node in nodes) { - insertedNode.insert(node); - } - return true; - } - return false; - } - if (path.last <= 0) { - for (var i = 0; i < nodes.length; i++) { - final node = nodes[i]; - insertedNode.insertBefore(node); - } - } else { - for (var i = 0; i < nodes.length; i++) { - final node = nodes[i]; - insertedNode!.insertAfter(node); - insertedNode = node; - } - } - return true; - } - - bool textEdit(Path path, Delta delta) { - if (path.isEmpty) { - return false; - } - final node = root.childAtPath(path); - if (node == null || node is! TextNode) { - return false; - } - node.delta = node.delta.compose(delta); - return false; - } - - delete(Path path, [int length = 1]) { - if (path.isEmpty) { - return null; - } - var deletedNode = root.childAtPath(path); - while (deletedNode != null && length > 0) { - final next = deletedNode.next; - deletedNode.unlink(); - length--; - deletedNode = next; - } - } - - bool update(Path path, Attributes attributes) { - if (path.isEmpty) { - return false; - } - final updatedNode = root.childAtPath(path); - if (updatedNode == null) { - return false; - } - updatedNode.updateAttributes(attributes); - return true; - } -} 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 4aeb7ab599..6c0acb16b9 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 @@ -5,10 +5,10 @@ import 'package:appflowy_editor/src/render/style/editor_style.dart'; import 'package:appflowy_editor/src/service/service.dart'; import 'package:flutter/material.dart'; -import 'package:appflowy_editor/src/document/selection.dart'; -import 'package:appflowy_editor/src/document/state_tree.dart'; -import 'package:appflowy_editor/src/operation/operation.dart'; -import 'package:appflowy_editor/src/operation/transaction.dart'; +import 'package:appflowy_editor/src/core/location/selection.dart'; +import 'package:appflowy_editor/src/core/document/document.dart'; +import 'package:appflowy_editor/src/core/transform/operation.dart'; +import 'package:appflowy_editor/src/core/transform/transaction.dart'; import 'package:appflowy_editor/src/undo_manager.dart'; class ApplyOptions { @@ -46,7 +46,7 @@ enum CursorUpdateReason { /// /// Mutating the document with document's API is not recommended. class EditorState { - final StateTree document; + final Document document; // Service reference. final service = FlowyService(); @@ -74,6 +74,24 @@ class EditorState { bool editable = true; + Transaction get transaction { + if (_transaction != null) { + return _transaction!; + } + _transaction = Transaction(document: document); + _transaction!.beforeSelection = _cursorSelection; + return _transaction!; + } + + Transaction? _transaction; + + void commit() { + if (_transaction != null) { + apply(_transaction!, const ApplyOptions(recordUndo: true)); + _transaction = null; + } + } + Selection? get cursorSelection { return _cursorSelection; } @@ -105,7 +123,7 @@ class EditorState { } factory EditorState.empty() { - return EditorState(document: StateTree.empty()); + return EditorState(document: Document.empty()); } /// Apply the transaction to the state. @@ -166,8 +184,8 @@ class EditorState { document.update(op.path, op.attributes); } else if (op is DeleteOperation) { document.delete(op.path, op.nodes.length); - } else if (op is TextEditOperation) { - document.textEdit(op.path, op.delta); + } 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/extensions/attributes_extension.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/attributes_extension.dart index b8970d8225..816747fdad 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/attributes_extension.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/attributes_extension.dart @@ -1,5 +1,5 @@ -import 'package:appflowy_editor/src/document/attributes.dart'; -import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart'; +import 'package:appflowy_editor/src/core/document/attributes.dart'; +import 'package:appflowy_editor/src/core/legacy/built_in_attribute_keys.dart'; import 'package:flutter/material.dart'; extension NodeAttributesExtensions on Attributes { diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/node_extensions.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/node_extensions.dart index dffca3eaf0..82d28bd469 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/node_extensions.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/node_extensions.dart @@ -1,7 +1,7 @@ -import 'package:appflowy_editor/src/document/node.dart'; -import 'package:appflowy_editor/src/document/selection.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/extensions/object_extensions.dart'; -import 'package:appflowy_editor/src/extensions/path_extensions.dart'; import 'package:appflowy_editor/src/render/selection/selectable.dart'; import 'package:flutter/material.dart'; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart index bcc8722dfa..48538f8bfb 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart @@ -1,9 +1,9 @@ -import 'package:appflowy_editor/src/document/node.dart'; -import 'package:appflowy_editor/src/document/path.dart'; -import 'package:appflowy_editor/src/document/position.dart'; -import 'package:appflowy_editor/src/document/selection.dart'; -import 'package:appflowy_editor/src/document/text_delta.dart'; -import 'package:appflowy_editor/src/document/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/position.dart'; +import 'package:appflowy_editor/src/core/location/selection.dart'; +import 'package:appflowy_editor/src/core/document/text_delta.dart'; +import 'package:appflowy_editor/src/core/legacy/built_in_attribute_keys.dart'; extension TextNodeExtension on TextNode { T? getAttributeInSelection(Selection selection, String styleKey) { @@ -168,18 +168,17 @@ extension TextNodesExtension on List { for (var i = 0; i < length; i++) { final node = this[i]; final Selection newSelection; - if (i == 0 && pathEquals(node.path, selection.start.path)) { + if (i == 0 && node.path.equals(selection.start.path)) { if (selection.isBackward) { newSelection = selection.copyWith( - end: Position(path: node.path, offset: node.toRawString().length), + end: Position(path: node.path, offset: node.toPlainText().length), ); } else { newSelection = selection.copyWith( end: Position(path: node.path, offset: 0), ); } - } else if (i == length - 1 && - pathEquals(node.path, selection.end.path)) { + } else if (i == length - 1 && node.path.equals(selection.end.path)) { if (selection.isBackward) { newSelection = selection.copyWith( start: Position(path: node.path, offset: 0), @@ -187,13 +186,13 @@ extension TextNodesExtension on List { } else { newSelection = selection.copyWith( start: - Position(path: node.path, offset: node.toRawString().length), + Position(path: node.path, offset: node.toPlainText().length), ); } } else { newSelection = Selection( start: Position(path: node.path, offset: 0), - end: Position(path: node.path, offset: node.toRawString().length), + end: Position(path: node.path, offset: node.toPlainText().length), ); } if (!node.allSatisfyInSelection(newSelection, styleKey, test)) { diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/infra/html_converter.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/infra/html_converter.dart index 6e71fc007b..b433657d4e 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/infra/html_converter.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/infra/html_converter.dart @@ -1,14 +1,14 @@ import 'dart:collection'; import 'dart:ui'; -import 'package:appflowy_editor/src/document/attributes.dart'; -import 'package:appflowy_editor/src/document/node.dart'; -import 'package:appflowy_editor/src/document/text_delta.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/text_delta.dart'; import 'package:appflowy_editor/src/extensions/color_extension.dart'; import 'package:flutter/material.dart'; import 'package:html/parser.dart' show parse; import 'package:html/dom.dart' as html; -import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart'; +import 'package:appflowy_editor/src/core/legacy/built_in_attribute_keys.dart'; class HTMLTag { static const h1 = "h1"; @@ -89,7 +89,7 @@ class HTMLToNodesConverter { } } if (delta.isNotEmpty) { - result.add(TextNode(type: "text", delta: delta)); + result.add(TextNode(delta: delta)); } return result; } @@ -134,7 +134,7 @@ class HTMLToNodesConverter { final delta = Delta(); delta.insert(element.text); if (delta.isNotEmpty) { - return [TextNode(type: "text", delta: delta)]; + return [TextNode(delta: delta)]; } } return []; @@ -218,24 +218,29 @@ class HTMLToNodesConverter { _handleRichTextElement(Delta delta, html.Element element) { if (element.localName == HTMLTag.span) { - delta.insert(element.text, - _getDeltaAttributesFromHtmlAttributes(element.attributes)); + delta.insert( + element.text, + attributes: _getDeltaAttributesFromHtmlAttributes(element.attributes), + ); } else if (element.localName == HTMLTag.anchor) { final hyperLink = element.attributes["href"]; Map? attributes; if (hyperLink != null) { attributes = {"href": hyperLink}; } - delta.insert(element.text, attributes); + delta.insert(element.text, attributes: attributes); } else if (element.localName == HTMLTag.strong || element.localName == HTMLTag.bold) { - delta.insert(element.text, {BuiltInAttributeKey.bold: true}); + delta.insert(element.text, attributes: {BuiltInAttributeKey.bold: true}); } else if (element.localName == HTMLTag.underline) { - delta.insert(element.text, {BuiltInAttributeKey.underline: true}); + delta.insert(element.text, + attributes: {BuiltInAttributeKey.underline: true}); } else if (element.localName == HTMLTag.italic) { - delta.insert(element.text, {BuiltInAttributeKey.italic: true}); + delta + .insert(element.text, attributes: {BuiltInAttributeKey.italic: true}); } else if (element.localName == HTMLTag.del) { - delta.insert(element.text, {BuiltInAttributeKey.strikethrough: true}); + delta.insert(element.text, + attributes: {BuiltInAttributeKey.strikethrough: true}); } else { delta.insert(element.text); } @@ -271,8 +276,7 @@ class HTMLToNodesConverter { } } - final textNode = - TextNode(type: "text", delta: delta, attributes: attributes); + final textNode = TextNode(delta: delta, attributes: attributes); if (isCheckbox) { textNode.attributes["subtype"] = BuiltInAttributeKey.checkbox; textNode.attributes["checkbox"] = checked; @@ -315,7 +319,6 @@ class HTMLToNodesConverter { final delta = Delta(); delta.insert(element.text); return TextNode( - type: "text", attributes: {"subtype": "heading", "heading": headingStyle}, delta: delta); } @@ -537,22 +540,22 @@ class NodesToHTMLConverter { if (attributes.length == 1 && attributes[BuiltInAttributeKey.bold] == true) { final strong = html.Element.tag(HTMLTag.strong); - strong.append(html.Text(op.content)); + strong.append(html.Text(op.text)); childNodes.add(strong); } else if (attributes.length == 1 && attributes[BuiltInAttributeKey.underline] == true) { final strong = html.Element.tag(HTMLTag.underline); - strong.append(html.Text(op.content)); + strong.append(html.Text(op.text)); childNodes.add(strong); } else if (attributes.length == 1 && attributes[BuiltInAttributeKey.italic] == true) { final strong = html.Element.tag(HTMLTag.italic); - strong.append(html.Text(op.content)); + strong.append(html.Text(op.text)); childNodes.add(strong); } else if (attributes.length == 1 && attributes[BuiltInAttributeKey.strikethrough] == true) { final strong = html.Element.tag(HTMLTag.del); - strong.append(html.Text(op.content)); + strong.append(html.Text(op.text)); childNodes.add(strong); } else { final span = html.Element.tag(HTMLTag.span); @@ -560,11 +563,11 @@ class NodesToHTMLConverter { if (cssString.isNotEmpty) { span.attributes["style"] = cssString; } - span.append(html.Text(op.content)); + span.append(html.Text(op.text)); childNodes.add(span); } } else { - childNodes.add(html.Text(op.content)); + childNodes.add(html.Text(op.text)); } } } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/infra/infra.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/infra/infra.dart index 346e9331f5..1463f6a97b 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/infra/infra.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/infra/infra.dart @@ -1,4 +1,4 @@ -import 'package:appflowy_editor/src/document/node.dart'; +import 'package:appflowy_editor/src/core/document/node.dart'; class Infra { // find the forward nearest text node diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/operation.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/operation.dart deleted file mode 100644 index af2ec831d4..0000000000 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/operation.dart +++ /dev/null @@ -1,218 +0,0 @@ -import 'package:appflowy_editor/appflowy_editor.dart'; - -abstract class Operation { - factory Operation.fromJson(Map map) { - String t = map["op"] as String; - if (t == "insert") { - return InsertOperation.fromJson(map); - } else if (t == "update") { - return UpdateOperation.fromJson(map); - } else if (t == "delete") { - return DeleteOperation.fromJson(map); - } else if (t == "text-edit") { - return TextEditOperation.fromJson(map); - } - - throw ArgumentError('unexpected type $t'); - } - final Path path; - Operation(this.path); - Operation copyWithPath(Path path); - Operation invert(); - Map toJson(); -} - -class InsertOperation extends Operation { - final List nodes; - - factory InsertOperation.fromJson(Map map) { - final path = map["path"] as List; - final value = - (map["nodes"] as List).map((n) => Node.fromJson(n)).toList(); - return InsertOperation(path, value); - } - - InsertOperation(Path path, this.nodes) : super(path); - - InsertOperation copyWith({Path? path, List? nodes}) => - InsertOperation(path ?? this.path, nodes ?? this.nodes); - - @override - Operation copyWithPath(Path path) => copyWith(path: path); - - @override - Operation invert() { - return DeleteOperation( - path, - nodes, - ); - } - - @override - Map toJson() { - return { - "op": "insert", - "path": path.toList(), - "nodes": nodes.map((n) => n.toJson()), - }; - } -} - -class UpdateOperation extends Operation { - final Attributes attributes; - final Attributes oldAttributes; - - factory UpdateOperation.fromJson(Map map) { - final path = map["path"] as List; - final attributes = map["attributes"] as Map; - final oldAttributes = map["oldAttributes"] as Map; - return UpdateOperation(path, attributes, oldAttributes); - } - - UpdateOperation( - Path path, - this.attributes, - this.oldAttributes, - ) : super(path); - - UpdateOperation copyWith( - {Path? path, Attributes? attributes, Attributes? oldAttributes}) => - UpdateOperation(path ?? this.path, attributes ?? this.attributes, - oldAttributes ?? this.oldAttributes); - - @override - Operation copyWithPath(Path path) => copyWith(path: path); - - @override - Operation invert() { - return UpdateOperation( - path, - oldAttributes, - attributes, - ); - } - - @override - Map toJson() { - return { - "op": "update", - "path": path.toList(), - "attributes": {...attributes}, - "oldAttributes": {...oldAttributes}, - }; - } -} - -class DeleteOperation extends Operation { - final List nodes; - - factory DeleteOperation.fromJson(Map map) { - final path = map["path"] as List; - final List nodes = - (map["nodes"] as List).map((e) => Node.fromJson(e)).toList(); - return DeleteOperation(path, nodes); - } - - DeleteOperation( - Path path, - this.nodes, - ) : super(path); - - DeleteOperation copyWith({Path? path, List? nodes}) => - DeleteOperation(path ?? this.path, nodes ?? this.nodes); - - @override - Operation copyWithPath(Path path) => copyWith(path: path); - - @override - Operation invert() { - return InsertOperation(path, nodes); - } - - @override - Map toJson() { - return { - "op": "delete", - "path": path.toList(), - "nodes": nodes.map((n) => n.toJson()), - }; - } -} - -class TextEditOperation extends Operation { - final Delta delta; - final Delta inverted; - - factory TextEditOperation.fromJson(Map map) { - final path = map["path"] as List; - final delta = Delta.fromJson(map["delta"]); - final invert = Delta.fromJson(map["invert"]); - return TextEditOperation(path, delta, invert); - } - - TextEditOperation( - Path path, - this.delta, - this.inverted, - ) : super(path); - - TextEditOperation copyWith({Path? path, Delta? delta, Delta? inverted}) => - TextEditOperation( - path ?? this.path, delta ?? this.delta, inverted ?? this.inverted); - - @override - Operation copyWithPath(Path path) => copyWith(path: path); - - @override - Operation invert() { - return TextEditOperation(path, inverted, delta); - } - - @override - Map toJson() { - return { - "op": "text-edit", - "path": path.toList(), - "delta": delta.toJson(), - "invert": inverted.toJson(), - }; - } -} - -Path transformPath(Path preInsertPath, Path b, [int delta = 1]) { - if (preInsertPath.length > b.length) { - return b; - } - if (preInsertPath.isEmpty || b.isEmpty) { - return b; - } - // check the prefix - for (var i = 0; i < preInsertPath.length - 1; i++) { - if (preInsertPath[i] != b[i]) { - return b; - } - } - final prefix = preInsertPath.sublist(0, preInsertPath.length - 1); - final suffix = b.sublist(preInsertPath.length); - final preInsertLast = preInsertPath.last; - final bAtIndex = b[preInsertPath.length - 1]; - if (preInsertLast <= bAtIndex) { - prefix.add(bAtIndex + delta); - } else { - prefix.add(bAtIndex); - } - prefix.addAll(suffix); - return prefix; -} - -Operation transformOperation(Operation a, Operation b) { - if (a is InsertOperation) { - final newPath = transformPath(a.path, b.path, a.nodes.length); - return b.copyWithPath(newPath); - } else if (a is DeleteOperation) { - final newPath = transformPath(a.path, b.path, -1 * a.nodes.length); - return b.copyWithPath(newPath); - } - // TODO: transform update and textedit - return b; -} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/transaction.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/transaction.dart deleted file mode 100644 index 8527e15325..0000000000 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/transaction.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'dart:collection'; -import 'package:flutter/material.dart'; -import 'package:appflowy_editor/src/document/selection.dart'; -import './operation.dart'; - -/// A [Transaction] has a list of [Operation] objects that will be applied -/// to the editor. It is an immutable class and used to store and transmit. -/// -/// If you want to build a new [Transaction], use [TransactionBuilder] directly. -/// -/// There will be several ways to consume the transaction: -/// 1. Apply to the state to update the UI. -/// 2. Send to the backend to store and do operation transforming. -/// 3. Used by the UndoManager to implement redo/undo. -@immutable -class Transaction { - final UnmodifiableListView operations; - final Selection? beforeSelection; - final Selection? afterSelection; - - const Transaction({ - required this.operations, - this.beforeSelection, - this.afterSelection, - }); - - Map toJson() { - final Map result = { - "operations": operations.map((e) => e.toJson()), - }; - if (beforeSelection != null) { - result["beforeSelection"] = beforeSelection!.toJson(); - } - if (afterSelection != null) { - result["afterSelection"] = afterSelection!.toJson(); - } - return result; - } -} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/transaction_builder.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/transaction_builder.dart deleted file mode 100644 index 7dfca6bcf7..0000000000 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/transaction_builder.dart +++ /dev/null @@ -1,230 +0,0 @@ -import 'dart:collection'; -import 'dart:math'; - -import 'package:appflowy_editor/src/document/attributes.dart'; -import 'package:appflowy_editor/src/document/node.dart'; -import 'package:appflowy_editor/src/document/path.dart'; -import 'package:appflowy_editor/src/document/position.dart'; -import 'package:appflowy_editor/src/document/selection.dart'; -import 'package:appflowy_editor/src/document/text_delta.dart'; -import 'package:appflowy_editor/src/editor_state.dart'; -import 'package:appflowy_editor/src/operation/operation.dart'; -import 'package:appflowy_editor/src/operation/transaction.dart'; - -/// A [TransactionBuilder] is used to build the transaction from the state. -/// It will save a snapshot of the cursor selection state automatically. -/// The cursor can be restored if the transaction is undo. -class TransactionBuilder { - final List operations = []; - EditorState state; - Selection? beforeSelection; - Selection? afterSelection; - - TransactionBuilder(this.state); - - /// Commits the operations to the state - Future commit() async { - final transaction = finish(); - state.apply(transaction); - } - - /// Inserts the nodes at the position of path. - insertNode(Path path, Node node) { - insertNodes(path, [node]); - } - - /// Inserts a sequence of nodes at the position of path. - insertNodes(Path path, List nodes) { - beforeSelection = state.cursorSelection; - add(InsertOperation(path, nodes.map((node) => node.copyWith()).toList())); - } - - /// Updates the attributes of nodes. - updateNode(Node node, Attributes attributes) { - beforeSelection = state.cursorSelection; - - final inverted = invertAttributes(attributes, node.attributes); - add(UpdateOperation( - node.path, - {...attributes}, - inverted, - )); - } - - /// Deletes a node in the document. - deleteNode(Node node) { - deleteNodesAtPath(node.path); - } - - deleteNodes(List nodes) { - nodes.forEach(deleteNode); - } - - /// Deletes a sequence of nodes at the path of the document. - /// The length specifies the length of the following nodes to delete( - /// including the start one). - deleteNodesAtPath(Path path, [int length = 1]) { - if (path.isEmpty) { - return; - } - final nodes = []; - final prefix = path.sublist(0, path.length - 1); - final last = path.last; - for (var i = 0; i < length; i++) { - final node = state.document.nodeAtPath(prefix + [last + i])!; - nodes.add(node); - } - - add(DeleteOperation(path, nodes.map((node) => node.copyWith()).toList())); - } - - textEdit(TextNode node, Delta Function() f) { - beforeSelection = state.cursorSelection; - final path = node.path; - - final delta = f(); - - final inverted = delta.invert(node.delta); - - add(TextEditOperation(path, delta, inverted)); - } - - setAfterSelection(Selection sel) { - afterSelection = sel; - } - - mergeText(TextNode firstNode, TextNode secondNode, - {int? firstOffset, int secondOffset = 0}) { - final firstLength = firstNode.delta.length; - final secondLength = secondNode.delta.length; - textEdit( - firstNode, - () => Delta() - ..retain(firstOffset ?? firstLength) - ..delete(firstLength - (firstOffset ?? firstLength)) - ..addAll(secondNode.delta.slice(secondOffset, secondLength)), - ); - afterSelection = Selection.collapsed( - Position( - path: firstNode.path, - offset: firstOffset ?? firstLength, - ), - ); - } - - /// Inserts content at a specified index. - /// Optionally, you may specify formatting attributes that are applied to the inserted string. - /// By default, the formatting attributes before the insert position will be used. - insertText( - TextNode node, - int index, - String content, { - Attributes? attributes, - }) { - var newAttributes = attributes; - if (index != 0 && attributes == null) { - newAttributes = - node.delta.slice(max(index - 1, 0), index).first.attributes; - if (newAttributes != null) { - newAttributes = Attributes.from(newAttributes); - } - } - textEdit( - node, - () => Delta() - ..retain(index) - ..insert( - content, - newAttributes, - ), - ); - afterSelection = Selection.collapsed( - Position(path: node.path, offset: index + content.length), - ); - } - - /// Assigns formatting attributes to a range of text. - formatText(TextNode node, int index, int length, Attributes attributes) { - textEdit( - node, - () => Delta() - ..retain(index) - ..retain(length, attributes)); - afterSelection = beforeSelection; - } - - /// Deletes length characters starting from index. - deleteText(TextNode node, int index, int length) { - textEdit( - node, - () => Delta() - ..retain(index) - ..delete(length)); - afterSelection = - Selection.collapsed(Position(path: node.path, offset: index)); - } - - replaceText(TextNode node, int index, int length, String content, - [Attributes? attributes]) { - var newAttributes = attributes; - if (attributes == null) { - final ops = node.delta.slice(index, index + length); - if (ops.isNotEmpty) { - newAttributes = ops.first.attributes; - } - } - textEdit( - node, - () => Delta() - ..retain(index) - ..delete(length) - ..insert(content, newAttributes), - ); - afterSelection = Selection.collapsed( - Position( - path: node.path, - offset: index + content.length, - ), - ); - } - - /// Adds an operation to the transaction. - /// This method will merge operations if they are both TextEdits. - /// - /// Also, this method will transform the path of the operations - /// to avoid conflicts. - add(Operation op, {bool transform = true}) { - final Operation? last = operations.isEmpty ? null : operations.last; - if (last != null) { - if (op is TextEditOperation && - last is TextEditOperation && - pathEquals(op.path, last.path)) { - final newOp = TextEditOperation( - op.path, - last.delta.compose(op.delta), - op.inverted.compose(last.inverted), - ); - operations[operations.length - 1] = newOp; - return; - } - } - if (transform) { - for (var i = 0; i < operations.length; i++) { - op = transformOperation(operations[i], op); - } - } - if (op is TextEditOperation && op.delta.isEmpty) { - return; - } - operations.add(op); - } - - /// Generates a immutable [Transaction] to apply or transmit. - Transaction finish() { - return Transaction( - operations: UnmodifiableListView(operations), - beforeSelection: beforeSelection, - afterSelection: afterSelection, - ); - } -} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/editor/editor_entry.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/editor/editor_entry.dart index 4167ca1b38..f04539d94a 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/editor/editor_entry.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/editor/editor_entry.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:appflowy_editor/src/document/node.dart'; +import 'package:appflowy_editor/src/core/document/node.dart'; import 'package:appflowy_editor/src/editor_state.dart'; import 'package:appflowy_editor/src/service/render_plugin_service.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 ad3cf19d53..d37a7e6c2a 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 @@ -1,5 +1,4 @@ -import 'package:appflowy_editor/src/document/node.dart'; -import 'package:appflowy_editor/src/operation/transaction_builder.dart'; +import 'package:appflowy_editor/src/core/document/node.dart'; import 'package:appflowy_editor/src/service/render_plugin_service.dart'; import 'package:flutter/material.dart'; import 'package:rich_clipboard/rich_clipboard.dart'; @@ -25,23 +24,20 @@ class ImageNodeBuilder extends NodeWidgetBuilder { RichClipboard.setData(RichClipboardData(text: src)); }, onDelete: () { - TransactionBuilder(context.editorState) - ..deleteNode(context.node) - ..commit(); + context.editorState.transaction.deleteNode(context.node); + context.editorState.commit(); }, onAlign: (alignment) { - TransactionBuilder(context.editorState) - ..updateNode(context.node, { - 'align': _alignmentToText(alignment), - }) - ..commit(); + context.editorState.transaction.updateNode(context.node, { + 'align': _alignmentToText(alignment), + }); + context.editorState.commit(); }, onResize: (width) { - TransactionBuilder(context.editorState) - ..updateNode(context.node, { - 'width': width, - }) - ..commit(); + context.editorState.transaction.updateNode(context.node, { + 'width': width, + }); + context.editorState.commit(); }, ); } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart index c720231f6e..5f9b2142fe 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart @@ -1,7 +1,7 @@ import 'package:appflowy_editor/src/extensions/object_extensions.dart'; -import 'package:appflowy_editor/src/document/node.dart'; -import 'package:appflowy_editor/src/document/position.dart'; -import 'package:appflowy_editor/src/document/selection.dart'; +import 'package:appflowy_editor/src/core/document/node.dart'; +import 'package:appflowy_editor/src/core/location/position.dart'; +import 'package:appflowy_editor/src/core/location/selection.dart'; import 'package:appflowy_editor/src/infra/flowy_svg.dart'; import 'package:appflowy_editor/src/render/selection/selectable.dart'; import 'package:flutter/material.dart'; 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 a8728341df..afec12220a 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 @@ -1,9 +1,8 @@ import 'dart:collection'; -import 'package:appflowy_editor/src/document/node.dart'; +import 'package:appflowy_editor/src/core/document/node.dart'; import 'package:appflowy_editor/src/editor_state.dart'; import 'package:appflowy_editor/src/infra/flowy_svg.dart'; -import 'package:appflowy_editor/src/operation/transaction_builder.dart'; import 'package:appflowy_editor/src/render/selection_menu/selection_menu_service.dart'; import 'package:flutter/material.dart'; @@ -192,11 +191,10 @@ extension on EditorState { 'align': 'center', }, ); - TransactionBuilder(this) - ..insertNode( - selection.start.path, - imageNode, - ) - ..commit(); + transaction.insertNode( + selection.start.path, + imageNode, + ); + commit(); } } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/bulleted_list_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/bulleted_list_text.dart index 5558b8934d..36e3568684 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/bulleted_list_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/bulleted_list_text.dart @@ -1,4 +1,4 @@ -import 'package:appflowy_editor/src/document/node.dart'; +import 'package:appflowy_editor/src/core/document/node.dart'; import 'package:appflowy_editor/src/editor_state.dart'; import 'package:appflowy_editor/src/infra/flowy_svg.dart'; import 'package:appflowy_editor/src/render/rich_text/built_in_text_widget.dart'; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/default_selectable.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/default_selectable.dart index fd86f84831..724a45b469 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/default_selectable.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/default_selectable.dart @@ -1,5 +1,5 @@ -import 'package:appflowy_editor/src/document/position.dart'; -import 'package:appflowy_editor/src/document/selection.dart'; +import 'package:appflowy_editor/src/core/location/position.dart'; +import 'package:appflowy_editor/src/core/location/selection.dart'; import 'package:appflowy_editor/src/render/selection/selectable.dart'; import 'package:flutter/material.dart'; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart index 99a6d08918..c49122dea4 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart @@ -5,11 +5,11 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; -import 'package:appflowy_editor/src/document/node.dart'; -import 'package:appflowy_editor/src/document/path.dart'; -import 'package:appflowy_editor/src/document/position.dart'; -import 'package:appflowy_editor/src/document/selection.dart'; -import 'package:appflowy_editor/src/document/text_delta.dart'; +import 'package:appflowy_editor/src/core/document/node.dart'; +import 'package:appflowy_editor/src/core/document/path.dart'; +import 'package:appflowy_editor/src/core/location/position.dart'; +import 'package:appflowy_editor/src/core/location/selection.dart'; +import 'package:appflowy_editor/src/core/document/text_delta.dart'; import 'package:appflowy_editor/src/editor_state.dart'; import 'package:appflowy_editor/src/extensions/url_launcher_extension.dart'; import 'package:appflowy_editor/src/extensions/text_style_extension.dart'; @@ -123,7 +123,7 @@ class _FlowyRichTextState extends State with SelectableMixin { @override List getRectsInSelection(Selection selection) { assert(selection.isSingle && - pathEquals(selection.start.path, widget.textNode.path)); + selection.start.path.equals(widget.textNode.path)); final textSelection = TextSelection( baseOffset: selection.start.offset, @@ -163,7 +163,7 @@ class _FlowyRichTextState extends State with SelectableMixin { Widget _buildRichText(BuildContext context) { return MouseRegion( cursor: SystemMouseCursors.text, - child: widget.textNode.toRawString().isEmpty + child: widget.textNode.toPlainText().isEmpty ? Stack( children: [ _buildPlaceholderText(context), @@ -257,7 +257,7 @@ class _FlowyRichTextState extends State with SelectableMixin { offset += textInsert.length; textSpans.add( TextSpan( - text: textInsert.content, + text: textInsert.text, style: textStyle, recognizer: recognizer, ), diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/heading_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/heading_text.dart index e6307bb409..5b6c75cc6a 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/heading_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/heading_text.dart @@ -1,4 +1,4 @@ -import 'package:appflowy_editor/src/document/node.dart'; +import 'package:appflowy_editor/src/core/document/node.dart'; import 'package:appflowy_editor/src/editor_state.dart'; import 'package:appflowy_editor/src/render/rich_text/built_in_text_widget.dart'; import 'package:appflowy_editor/src/render/rich_text/default_selectable.dart'; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/number_list_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/number_list_text.dart index 59c0551750..6ce4bd0fee 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/number_list_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/number_list_text.dart @@ -1,4 +1,4 @@ -import 'package:appflowy_editor/src/document/node.dart'; +import 'package:appflowy_editor/src/core/document/node.dart'; import 'package:appflowy_editor/src/editor_state.dart'; import 'package:appflowy_editor/src/render/rich_text/built_in_text_widget.dart'; import 'package:appflowy_editor/src/render/rich_text/default_selectable.dart'; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/quoted_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/quoted_text.dart index b54e0ea1cb..b68fc38923 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/quoted_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/quoted_text.dart @@ -1,4 +1,4 @@ -import 'package:appflowy_editor/src/document/node.dart'; +import 'package:appflowy_editor/src/core/document/node.dart'; import 'package:appflowy_editor/src/editor_state.dart'; import 'package:appflowy_editor/src/infra/flowy_svg.dart'; import 'package:appflowy_editor/src/render/rich_text/built_in_text_widget.dart'; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text.dart index 3d7f895fd9..a28270bf7c 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text.dart @@ -1,4 +1,4 @@ -import 'package:appflowy_editor/src/document/node.dart'; +import 'package:appflowy_editor/src/core/document/node.dart'; import 'package:appflowy_editor/src/editor_state.dart'; import 'package:appflowy_editor/src/render/rich_text/built_in_text_widget.dart'; import 'package:appflowy_editor/src/render/rich_text/default_selectable.dart'; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection/selectable.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection/selectable.dart index 6f4f92c2e9..523f60de47 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection/selectable.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection/selectable.dart @@ -1,5 +1,5 @@ -import 'package:appflowy_editor/src/document/position.dart'; -import 'package:appflowy_editor/src/document/selection.dart'; +import 'package:appflowy_editor/src/core/location/position.dart'; +import 'package:appflowy_editor/src/core/location/selection.dart'; import 'package:flutter/material.dart'; enum CursorStyle { diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_service.dart index 4da824db80..614546ef99 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_service.dart @@ -6,7 +6,7 @@ import 'package:appflowy_editor/src/render/selection_menu/selection_menu_widget. import 'package:appflowy_editor/src/service/default_text_operations/format_rich_text_style.dart'; import 'package:flutter/material.dart'; -import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart'; +import 'package:appflowy_editor/src/core/legacy/built_in_attribute_keys.dart'; abstract class SelectionMenuService { Offset get topLeft; 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 6bce03d15a..98a37acf08 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 @@ -44,14 +44,13 @@ class SelectionMenuItem { if (selection != null && nodes.length == 1) { final node = nodes.first as TextNode; final end = selection.start.offset; - final start = node.toRawString().substring(0, end).lastIndexOf('/'); - TransactionBuilder(editorState) - ..deleteText( - node, - start, - selection.start.offset - start, - ) - ..commit(); + final start = node.toPlainText().substring(0, end).lastIndexOf('/'); + editorState.transaction.deleteText( + node, + start, + selection.start.offset - start, + ); + editorState.commit(); } } } @@ -278,13 +277,12 @@ class _SelectionMenuWidgetState extends State { final nodes = selectionService.currentSelectedNodes; if (selection != null && nodes.length == 1) { widget.onSelectionUpdate(); - TransactionBuilder(widget.editorState) - ..deleteText( - nodes.first as TextNode, - selection.start.offset - length, - length, - ) - ..commit(); + widget.editorState.transaction.deleteText( + nodes.first as TextNode, + selection.start.offset - length, + length, + ); + widget.editorState.commit(); } } @@ -295,13 +293,12 @@ class _SelectionMenuWidgetState extends State { widget.editorState.service.selectionService.currentSelectedNodes; if (selection != null && nodes.length == 1) { widget.onSelectionUpdate(); - TransactionBuilder(widget.editorState) - ..insertText( - nodes.first as TextNode, - selection.end.offset, - text, - ) - ..commit(); + widget.editorState.transaction.insertText( + nodes.first as TextNode, + selection.end.offset, + text, + ); + widget.editorState.commit(); } } } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/style/editor_style.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/style/editor_style.dart index 7ec426fff4..a8cc9eb638 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/style/editor_style.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/style/editor_style.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:appflowy_editor/src/document/node.dart'; +import 'package:appflowy_editor/src/core/document/node.dart'; import 'package:appflowy_editor/src/editor_state.dart'; import 'package:appflowy_editor/src/extensions/attributes_extension.dart'; 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 b3ed2e2471..091102805b 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 @@ -357,10 +357,9 @@ void showLinkMenu( _dismissLinkMenu(); }, onRemoveLink: () { - TransactionBuilder(editorState) - ..formatText( - textNode, index, length, {BuiltInAttributeKey.href: null}) - ..commit(); + editorState.transaction.formatText( + textNode, index, length, {BuiltInAttributeKey.href: null}); + editorState.commit(); _dismissLinkMenu(); }, onFocusChange: (value) { 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 adf45bec65..e6e813903c 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 @@ -1,12 +1,5 @@ -import 'package:appflowy_editor/src/document/attributes.dart'; -import 'package:appflowy_editor/src/document/node.dart'; -import 'package:appflowy_editor/src/document/position.dart'; -import 'package:appflowy_editor/src/document/selection.dart'; -import 'package:appflowy_editor/src/editor_state.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/src/extensions/text_node_extensions.dart'; -import 'package:appflowy_editor/src/extensions/path_extensions.dart'; -import 'package:appflowy_editor/src/operation/transaction_builder.dart'; -import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart'; void insertHeadingAfterSelection(EditorState editorState, String heading) { insertTextNodeAfterSelection(editorState, { @@ -54,16 +47,15 @@ bool insertTextNodeAfterSelection( formatTextNodes(editorState, attributes); } else { final next = selection.end.path.next; - final builder = TransactionBuilder(editorState); - builder + editorState.transaction ..insertNode( next, TextNode.empty(attributes: attributes), ) ..afterSelection = Selection.collapsed( Position(path: next, offset: 0), - ) - ..commit(); + ); + editorState.commit(); } return true; @@ -107,7 +99,7 @@ bool formatTextNodes(EditorState editorState, Attributes attributes) { return false; } - final builder = TransactionBuilder(editorState); + final transaction = editorState.transaction; for (final textNode in textNodes) { var newAttributes = {...textNode.attributes}; @@ -117,7 +109,7 @@ bool formatTextNodes(EditorState editorState, Attributes attributes) { } } newAttributes.addAll(attributes); - builder + transaction ..updateNode( textNode, newAttributes, @@ -125,12 +117,12 @@ bool formatTextNodes(EditorState editorState, Attributes attributes) { ..afterSelection = Selection.collapsed( Position( path: textNode.path, - offset: textNode.toRawString().length, + offset: textNode.toPlainText().length, ), ); } - builder.commit(); + editorState.commit(); return true; } @@ -216,13 +208,13 @@ bool formatRichTextStyle(EditorState editorState, Attributes attributes) { return false; } - final builder = TransactionBuilder(editorState); + final transaction = editorState.transaction; // 1. All nodes are text nodes. // 2. The first node is not TextNode. // 3. The last node is not TextNode. if (nodes.length == textNodes.length && textNodes.length == 1) { - builder.formatText( + transaction.formatText( textNodes.first, selection.start.offset, selection.end.offset - selection.start.offset, @@ -232,14 +224,14 @@ bool formatRichTextStyle(EditorState editorState, Attributes attributes) { for (var i = 0; i < textNodes.length; i++) { final textNode = textNodes[i]; var index = 0; - var length = textNode.toRawString().length; + var length = textNode.toPlainText().length; if (i == 0 && textNode == nodes.first) { index = selection.start.offset; - length = textNode.toRawString().length - selection.start.offset; + length = textNode.toPlainText().length - selection.start.offset; } else if (i == textNodes.length - 1 && textNode == nodes.last) { length = selection.end.offset; } - builder.formatText( + transaction.formatText( textNode, index, length, @@ -248,7 +240,7 @@ bool formatRichTextStyle(EditorState editorState, Attributes attributes) { } } - builder.commit(); + editorState.commit(); return true; } 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 d6a3420099..bb0cfb5bd9 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 @@ -1,13 +1,13 @@ import 'package:appflowy_editor/src/infra/log.dart'; +import 'package:appflowy_editor/src/core/transform/transaction.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:appflowy_editor/src/document/node.dart'; -import 'package:appflowy_editor/src/document/selection.dart'; +import 'package:appflowy_editor/src/core/document/node.dart'; +import 'package:appflowy_editor/src/core/location/selection.dart'; import 'package:appflowy_editor/src/editor_state.dart'; import 'package:appflowy_editor/src/extensions/node_extensions.dart'; -import 'package:appflowy_editor/src/operation/transaction_builder.dart'; /// [AppFlowyInputService] is responsible for processing text input, /// including text insertion, deletion and replacement. @@ -160,13 +160,12 @@ class _AppFlowyInputState extends State } if (currentSelection.isSingle) { final textNode = selectionService.currentSelectedNodes.first as TextNode; - TransactionBuilder(_editorState) - ..insertText( - textNode, - delta.insertionOffset, - delta.textInserted, - ) - ..commit(); + _editorState.transaction.insertText( + textNode, + delta.insertionOffset, + delta.textInserted, + ); + _editorState.commit(); } else { // TODO: implement } @@ -181,9 +180,9 @@ class _AppFlowyInputState extends State if (currentSelection.isSingle) { final textNode = selectionService.currentSelectedNodes.first as TextNode; final length = delta.deletedRange.end - delta.deletedRange.start; - TransactionBuilder(_editorState) - ..deleteText(textNode, delta.deletedRange.start, length) - ..commit(); + _editorState.transaction + .deleteText(textNode, delta.deletedRange.start, length); + _editorState.commit(); } else { // TODO: implement } @@ -198,10 +197,9 @@ class _AppFlowyInputState extends State if (currentSelection.isSingle) { final textNode = selectionService.currentSelectedNodes.first as TextNode; final length = delta.replacedRange.end - delta.replacedRange.start; - TransactionBuilder(_editorState) - ..replaceText( - textNode, delta.replacedRange.start, length, delta.replacementText) - ..commit(); + _editorState.transaction.replaceText( + textNode, delta.replacedRange.start, length, delta.replacementText); + _editorState.commit(); } else { // TODO: implement } @@ -282,7 +280,7 @@ class _AppFlowyInputState extends State // FIXME: upward and selection update. if (textNodes.isNotEmpty && selection != null) { final text = textNodes.fold( - '', (sum, textNode) => '$sum${textNode.toRawString()}\n'); + '', (sum, textNode) => '$sum${textNode.toPlainText()}\n'); attach( TextEditingValue( text: text, diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/arrow_keys_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/arrow_keys_handler.dart index c04dafe986..80f139a4fe 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/arrow_keys_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/arrow_keys_handler.dart @@ -220,7 +220,7 @@ ShortcutEventHandler cursorEndSelect = (editorState, event) { KeyEventResult cursorUp(EditorState editorState, RawKeyEvent event) { final nodes = editorState.service.selectionService.currentSelectedNodes; final selection = - editorState.service.selectionService.currentSelection.value?.normalize; + editorState.service.selectionService.currentSelection.value?.normalized; if (nodes.isEmpty || selection == null) { return KeyEventResult.ignored; } @@ -234,7 +234,7 @@ KeyEventResult cursorUp(EditorState editorState, RawKeyEvent event) { KeyEventResult cursorDown(EditorState editorState, RawKeyEvent event) { final nodes = editorState.service.selectionService.currentSelectedNodes; final selection = - editorState.service.selectionService.currentSelection.value?.normalize; + editorState.service.selectionService.currentSelection.value?.normalized; if (nodes.isEmpty || selection == null) { return KeyEventResult.ignored; } @@ -248,7 +248,7 @@ KeyEventResult cursorDown(EditorState editorState, RawKeyEvent event) { KeyEventResult cursorLeft(EditorState editorState, RawKeyEvent event) { final nodes = editorState.service.selectionService.currentSelectedNodes; final selection = - editorState.service.selectionService.currentSelection.value?.normalize; + editorState.service.selectionService.currentSelection.value?.normalized; if (nodes.isEmpty || selection == null) { return KeyEventResult.ignored; } @@ -270,7 +270,7 @@ KeyEventResult cursorLeft(EditorState editorState, RawKeyEvent event) { KeyEventResult cursorRight(EditorState editorState, RawKeyEvent event) { final nodes = editorState.service.selectionService.currentSelectedNodes; final selection = - editorState.service.selectionService.currentSelection.value?.normalize; + editorState.service.selectionService.currentSelection.value?.normalized; if (nodes.isEmpty || selection == null) { return KeyEventResult.ignored; } 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 229d011743..686a9bc970 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 @@ -28,11 +28,11 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) { final List nonTextNodes = nodes.where((node) => node is! TextNode).toList(growable: false); - final transactionBuilder = TransactionBuilder(editorState); + final transaction = editorState.transaction; List? cancelNumberListPath; if (nonTextNodes.isNotEmpty) { - transactionBuilder.deleteNodes(nonTextNodes); + transaction.deleteNodes(nonTextNodes); } if (textNodes.length == 1) { @@ -44,7 +44,7 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) { if (textNode.subtype == BuiltInAttributeKey.numberList) { cancelNumberListPath = textNode.path; } - transactionBuilder + transaction ..updateNode(textNode, { BuiltInAttributeKey.subtype: null, textNode.subtype!: null, @@ -61,20 +61,20 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) { return _backDeleteToPreviousTextNode( editorState, textNode, - transactionBuilder, + transaction, nonTextNodes, selection, ); } } else { if (selection.isCollapsed) { - transactionBuilder.deleteText( + transaction.deleteText( textNode, index, selection.start.offset - index, ); } else { - transactionBuilder.deleteText( + transaction.deleteText( textNode, selection.start.offset, selection.end.offset - selection.start.offset, @@ -84,33 +84,32 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) { } else { if (textNodes.isEmpty) { if (nonTextNodes.isNotEmpty) { - transactionBuilder.afterSelection = - Selection.collapsed(selection.start); + transaction.afterSelection = Selection.collapsed(selection.start); } - transactionBuilder.commit(); + editorState.commit(); return KeyEventResult.handled; } final startPosition = selection.start; final nodeAtStart = editorState.document.nodeAtPath(startPosition.path)!; - _deleteTextNodes(transactionBuilder, textNodes, selection); - transactionBuilder.commit(); + _deleteTextNodes(transaction, textNodes, selection); + editorState.commit(); if (nodeAtStart is TextNode && nodeAtStart.subtype == BuiltInAttributeKey.numberList) { makeFollowingNodesIncremental( editorState, startPosition.path, - transactionBuilder.afterSelection!, + transaction.afterSelection!, ); } return KeyEventResult.handled; } - if (transactionBuilder.operations.isNotEmpty) { + if (transaction.operations.isNotEmpty) { if (nonTextNodes.isNotEmpty) { - transactionBuilder.afterSelection = Selection.collapsed(selection.start); + transaction.afterSelection = Selection.collapsed(selection.start); } - transactionBuilder.commit(); + editorState.commit(); } if (cancelNumberListPath != null) { @@ -128,20 +127,20 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) { KeyEventResult _backDeleteToPreviousTextNode( EditorState editorState, TextNode textNode, - TransactionBuilder transactionBuilder, + Transaction transaction, List nonTextNodes, Selection selection, ) { if (textNode.next == null && textNode.children.isEmpty && textNode.parent?.parent != null) { - transactionBuilder + transaction ..deleteNode(textNode) ..insertNode(textNode.parent!.path.next, textNode) ..afterSelection = Selection.collapsed( Position(path: textNode.parent!.path.next, offset: 0), - ) - ..commit(); + ); + editorState.commit(); return KeyEventResult.handled; } @@ -152,32 +151,32 @@ KeyEventResult _backDeleteToPreviousTextNode( prevIsNumberList = true; } - transactionBuilder.mergeText(previousTextNode, textNode); + transaction.mergeText(previousTextNode, textNode); if (textNode.children.isNotEmpty) { - transactionBuilder.insertNodes( + transaction.insertNodes( previousTextNode.path.next, textNode.children.toList(growable: false), ); } - transactionBuilder.deleteNode(textNode); - transactionBuilder.afterSelection = Selection.collapsed( + transaction.deleteNode(textNode); + transaction.afterSelection = Selection.collapsed( Position( path: previousTextNode.path, - offset: previousTextNode.toRawString().length, + offset: previousTextNode.toPlainText().length, ), ); } - if (transactionBuilder.operations.isNotEmpty) { + if (transaction.operations.isNotEmpty) { if (nonTextNodes.isNotEmpty) { - transactionBuilder.afterSelection = Selection.collapsed(selection.start); + transaction.afterSelection = Selection.collapsed(selection.start); } - transactionBuilder.commit(); + editorState.commit(); } if (prevIsNumberList) { - makeFollowingNodesIncremental(editorState, previousTextNode!.path, - transactionBuilder.afterSelection!); + makeFollowingNodesIncremental( + editorState, previousTextNode!.path, transaction.afterSelection!); } return KeyEventResult.handled; @@ -197,7 +196,7 @@ KeyEventResult _handleDelete(EditorState editorState, RawKeyEvent event) { return KeyEventResult.ignored; } - final transactionBuilder = TransactionBuilder(editorState); + final transaction = editorState.transaction; if (textNodes.length == 1) { final textNode = textNodes.first; // The cursor is at the end of the line, @@ -206,55 +205,52 @@ KeyEventResult _handleDelete(EditorState editorState, RawKeyEvent event) { return _mergeNextLineIntoThisLine( editorState, textNode, - transactionBuilder, + transaction, selection, ); } final index = textNode.delta.nextRunePosition(selection.start.offset); if (selection.isCollapsed) { - transactionBuilder.deleteText( + transaction.deleteText( textNode, selection.start.offset, index - selection.start.offset, ); } else { - transactionBuilder.deleteText( + transaction.deleteText( textNode, selection.start.offset, selection.end.offset - selection.start.offset, ); } - transactionBuilder.commit(); + editorState.commit(); } else { final startPosition = selection.start; final nodeAtStart = editorState.document.nodeAtPath(startPosition.path)!; - _deleteTextNodes(transactionBuilder, textNodes, selection); - transactionBuilder.commit(); + _deleteTextNodes(transaction, textNodes, selection); + editorState.commit(); if (nodeAtStart is TextNode && nodeAtStart.subtype == BuiltInAttributeKey.numberList) { makeFollowingNodesIncremental( - editorState, startPosition.path, transactionBuilder.afterSelection!); + editorState, startPosition.path, transaction.afterSelection!); } } return KeyEventResult.handled; } -KeyEventResult _mergeNextLineIntoThisLine( - EditorState editorState, - TextNode textNode, - TransactionBuilder transactionBuilder, - Selection selection) { +KeyEventResult _mergeNextLineIntoThisLine(EditorState editorState, + TextNode textNode, Transaction transaction, Selection selection) { final nextNode = textNode.next; if (nextNode == null) { return KeyEventResult.ignored; } if (nextNode is TextNode) { - transactionBuilder.mergeText(textNode, nextNode); + transaction.mergeText(textNode, nextNode); } - transactionBuilder.deleteNode(nextNode); - transactionBuilder.commit(); + transaction.deleteNode(nextNode); + editorState.commit(); if (textNode.subtype == BuiltInAttributeKey.numberList) { makeFollowingNodesIncremental(editorState, textNode.path, selection); @@ -263,15 +259,15 @@ KeyEventResult _mergeNextLineIntoThisLine( return KeyEventResult.handled; } -void _deleteTextNodes(TransactionBuilder transactionBuilder, - List textNodes, Selection selection) { +void _deleteTextNodes( + Transaction transaction, List textNodes, Selection selection) { final first = textNodes.first; final last = textNodes.last; - var content = textNodes.last.toRawString(); + var content = textNodes.last.toPlainText(); content = content.substring(selection.end.offset, content.length); // Merge the fist and the last text node content, // and delete the all nodes expect for the first. - transactionBuilder + transaction ..deleteNodes(textNodes.sublist(1)) ..mergeText( first, 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 916ffe317e..3dd53a1fb3 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 @@ -1,6 +1,6 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/src/infra/html_converter.dart'; -import 'package:appflowy_editor/src/document/node_iterator.dart'; +import 'package:appflowy_editor/src/core/document/node_iterator.dart'; import 'package:appflowy_editor/src/service/internal_key_event_handlers/number_list_helper.dart'; import 'package:flutter/material.dart'; import 'package:rich_clipboard/rich_clipboard.dart'; @@ -25,11 +25,11 @@ Selection _computeSelectionAfterPasteMultipleNodes( } void _handleCopy(EditorState editorState) async { - final selection = editorState.cursorSelection?.normalize; + final selection = editorState.cursorSelection?.normalized; if (selection == null || selection.isCollapsed) { return; } - if (pathEquals(selection.start.path, selection.end.path)) { + if (selection.start.path.equals(selection.end.path)) { final nodeAtPath = editorState.document.nodeAtPath(selection.end.path)!; if (nodeAtPath.type == "text") { final textNode = nodeAtPath as TextNode; @@ -49,7 +49,11 @@ void _handleCopy(EditorState editorState) async { final beginNode = editorState.document.nodeAtPath(selection.start.path)!; final endNode = editorState.document.nodeAtPath(selection.end.path)!; - final nodes = NodeIterator(editorState.document, beginNode, endNode).toList(); + final nodes = NodeIterator( + document: editorState.document, + startNode: beginNode, + endNode: endNode, + ).toList(); final copyString = NodesToHTMLConverter( nodes: nodes, @@ -61,7 +65,7 @@ void _handleCopy(EditorState editorState) async { } void _pasteHTML(EditorState editorState, String html) { - final selection = editorState.cursorSelection?.normalize; + final selection = editorState.cursorSelection?.normalized; if (selection == null) { return; } @@ -81,16 +85,16 @@ void _pasteHTML(EditorState editorState, String html) { } else if (nodes.length == 1) { final firstNode = nodes[0]; final nodeAtPath = editorState.document.nodeAtPath(path)!; - final tb = TransactionBuilder(editorState); + final tb = editorState.transaction; final startOffset = selection.start.offset; if (nodeAtPath.type == "text" && firstNode.type == "text") { final textNodeAtPath = nodeAtPath as TextNode; final firstTextNode = firstNode as TextNode; - tb.textEdit(textNodeAtPath, - () => (Delta()..retain(startOffset)) + firstTextNode.delta); - tb.setAfterSelection(Selection.collapsed(Position( + tb.updateText( + textNodeAtPath, (Delta()..retain(startOffset)) + firstTextNode.delta); + tb.afterSelection = (Selection.collapsed(Position( path: path, offset: startOffset + firstTextNode.delta.length))); - tb.commit(); + editorState.commit(); return; } } @@ -100,7 +104,7 @@ void _pasteHTML(EditorState editorState, String html) { void _pasteMultipleLinesInText( EditorState editorState, List path, int offset, List nodes) { - final tb = TransactionBuilder(editorState); + final tb = editorState.transaction; final firstNode = nodes[0]; final nodeAtPath = editorState.document.nodeAtPath(path)!; @@ -116,10 +120,9 @@ void _pasteMultipleLinesInText( final firstTextNode = firstNode as TextNode; final remain = textNodeAtPath.delta.slice(offset); - tb.textEdit( + tb.updateText( textNodeAtPath, - () => - (Delta() + (Delta() ..retain(offset) ..delete(remain.length)) + firstTextNode.delta); @@ -136,15 +139,15 @@ void _pasteMultipleLinesInText( final tailTextNode = tailNodes.last as TextNode; tailTextNode.delta = tailTextNode.delta + remain; } else if (remain.isNotEmpty) { - tailNodes.add(TextNode(type: "text", delta: remain)); + tailNodes.add(TextNode(delta: remain)); } } else { - tailNodes.add(TextNode(type: "text", delta: remain)); + tailNodes.add(TextNode(delta: remain)); } - tb.setAfterSelection(afterSelection); + tb.afterSelection = afterSelection; tb.insertNodes(path, tailNodes); - tb.commit(); + editorState.commit(); if (startNumber != null) { makeFollowingNodesIncremental(editorState, originalPath, afterSelection, @@ -157,9 +160,9 @@ void _pasteMultipleLinesInText( _computeSelectionAfterPasteMultipleNodes(editorState, nodes); path[path.length - 1]++; - tb.setAfterSelection(afterSelection); + tb.afterSelection = afterSelection; tb.insertNodes(path, nodes); - tb.commit(); + editorState.commit(); } void _handlePaste(EditorState editorState) async { @@ -192,15 +195,15 @@ void _pasteSingleLine( EditorState editorState, Selection selection, String line) { final node = editorState.document.nodeAtPath(selection.end.path)! as TextNode; final beginOffset = selection.end.offset; - TransactionBuilder(editorState) - ..textEdit( + editorState.transaction + ..updateText( node, - () => Delta() + Delta() ..retain(beginOffset) ..addAll(_lineContentToDelta(line))) - ..setAfterSelection(Selection.collapsed( - Position(path: selection.end.path, offset: beginOffset + line.length))) - ..commit(); + ..afterSelection = (Selection.collapsed( + Position(path: selection.end.path, offset: beginOffset + line.length))); + editorState.commit(); } /// parse url from the line text @@ -218,7 +221,7 @@ Delta _lineContentToDelta(String lineContent) { delta.insert(lineContent.substring(lastUrlEndOffset, match.start)); } final linkContent = lineContent.substring(match.start, match.end); - delta.insert(linkContent, {"href": linkContent}); + delta.insert(linkContent, attributes: {"href": linkContent}); lastUrlEndOffset = match.end; } @@ -230,7 +233,7 @@ Delta _lineContentToDelta(String lineContent) { } void _handlePastePlainText(EditorState editorState, String plainText) { - final selection = editorState.cursorSelection?.normalize; + final selection = editorState.cursorSelection?.normalized; if (selection == null) { return; } @@ -260,10 +263,9 @@ void _handlePastePlainText(EditorState editorState, String plainText) { final insertedLineSuffix = node.delta.slice(beginOffset); path[path.length - 1]++; - final tb = TransactionBuilder(editorState); - final List nodes = remains - .map((e) => TextNode(type: "text", delta: _lineContentToDelta(e))) - .toList(); + final tb = editorState.transaction; + final List nodes = + remains.map((e) => TextNode(delta: _lineContentToDelta(e))).toList(); final afterSelection = _computeSelectionAfterPasteMultipleNodes(editorState, nodes); @@ -272,20 +274,20 @@ void _handlePastePlainText(EditorState editorState, String plainText) { if (nodes.isNotEmpty) { final last = nodes.last; nodes[nodes.length - 1] = - TextNode(type: "text", delta: last.delta..addAll(insertedLineSuffix)); + TextNode(delta: last.delta..addAll(insertedLineSuffix)); } // insert first line - tb.textEdit( + tb.updateText( node, - () => Delta() + Delta() ..retain(beginOffset) ..insert(firstLine) ..delete(node.delta.length - beginOffset)); // insert remains tb.insertNodes(path, nodes); - tb.setAfterSelection(afterSelection); - tb.commit(); + tb.afterSelection = afterSelection; + editorState.commit(); } } @@ -297,35 +299,38 @@ void _handleCut(EditorState editorState) { } void _deleteSelectedContent(EditorState editorState) { - final selection = editorState.cursorSelection?.normalize; + final selection = editorState.cursorSelection?.normalized; if (selection == null || selection.isCollapsed) { return; } final beginNode = editorState.document.nodeAtPath(selection.start.path)!; final endNode = editorState.document.nodeAtPath(selection.end.path)!; - if (pathEquals(selection.start.path, selection.end.path) && + if (selection.start.path.equals(selection.end.path) && beginNode.type == "text") { final textItem = beginNode as TextNode; - final tb = TransactionBuilder(editorState); + final tb = editorState.transaction; final len = selection.end.offset - selection.start.offset; - tb.textEdit( + tb.updateText( textItem, - () => Delta() + Delta() ..retain(selection.start.offset) ..delete(len)); - tb.setAfterSelection(Selection.collapsed(selection.start)); - tb.commit(); + tb.afterSelection = Selection.collapsed(selection.start); + editorState.commit(); return; } - final traverser = NodeIterator(editorState.document, beginNode, endNode); - - final tb = TransactionBuilder(editorState); + final traverser = NodeIterator( + document: editorState.document, + startNode: beginNode, + endNode: endNode, + ); + final tb = editorState.transaction; while (traverser.moveNext()) { final item = traverser.current; if (item.type == "text" && beginNode == item) { final textItem = item as TextNode; final deleteLen = textItem.delta.length - selection.start.offset; - tb.textEdit(textItem, () { + tb.updateText(textItem, () { final delta = Delta() ..retain(selection.start.offset) ..delete(deleteLen); @@ -336,13 +341,13 @@ void _deleteSelectedContent(EditorState editorState) { } return delta; - }); + }()); } else { tb.deleteNode(item); } } - tb.setAfterSelection(Selection.collapsed(selection.start)); - tb.commit(); + tb.afterSelection = Selection.collapsed(selection.start); + editorState.commit(); } ShortcutEventHandler copyEventHandler = (editorState, event) { 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 2a2d1deedb..cafe40c27c 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,11 +39,11 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler = final afterSelection = Selection.collapsed( Position(path: textNodes.first.path.next, offset: 0), ); - TransactionBuilder(editorState) + editorState.transaction ..deleteText( textNodes.first, selection.start.offset, - textNodes.first.toRawString().length, + textNodes.first.toPlainText().length, ) ..deleteNodes(subTextNodes) ..deleteText( @@ -51,8 +51,8 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler = 0, selection.end.offset, ) - ..afterSelection = afterSelection - ..commit(); + ..afterSelection = afterSelection; + editorState.commit(); if (startNode is TextNode && startNode.subtype == BuiltInAttributeKey.numberList) { @@ -73,16 +73,16 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler = // If selection is collapsed and position.start.offset == 0, // insert a empty text node before. if (selection.isCollapsed && selection.start.offset == 0) { - if (textNode.toRawString().isEmpty && textNode.subtype != null) { + if (textNode.toPlainText().isEmpty && textNode.subtype != null) { final afterSelection = Selection.collapsed( Position(path: textNode.path, offset: 0), ); - TransactionBuilder(editorState) + editorState.transaction ..updateNode(textNode, { BuiltInAttributeKey.subtype: null, }) - ..afterSelection = afterSelection - ..commit(); + ..afterSelection = afterSelection; + editorState.commit(); final nextNode = textNode.next; if (nextNode is TextNode && @@ -105,13 +105,13 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler = BuiltInAttributeKey.numberList; newNode.attributes[BuiltInAttributeKey.number] = prevNumber; final insertPath = textNode.path; - TransactionBuilder(editorState) + editorState.transaction ..insertNode( insertPath, newNode, ) - ..afterSelection = afterSelection - ..commit(); + ..afterSelection = afterSelection; + editorState.commit(); makeFollowingNodesIncremental(editorState, insertPath, afterSelection, beginNum: prevNumber); @@ -120,7 +120,7 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler = BuiltInAttributeKey.heading, BuiltInAttributeKey.quote, ].contains(subtype); - TransactionBuilder(editorState) + editorState.transaction ..insertNode( textNode.path, textNode.copyWith( @@ -129,8 +129,8 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler = attributes: needCopyAttributes ? null : {}, ), ) - ..afterSelection = afterSelection - ..commit(); + ..afterSelection = afterSelection; + editorState.commit(); } } return KeyEventResult.handled; @@ -145,25 +145,25 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler = Position(path: nextPath, offset: 0), ); - final transactionBuilder = TransactionBuilder(editorState); - transactionBuilder.insertNode( + final transaction = editorState.transaction; + transaction.insertNode( textNode.path.next, textNode.copyWith( attributes: attributes, delta: textNode.delta.slice(selection.end.offset), ), ); - transactionBuilder.deleteText( + transaction.deleteText( textNode, selection.start.offset, - textNode.toRawString().length - selection.start.offset, + textNode.toPlainText().length - selection.start.offset, ); if (textNode.children.isNotEmpty) { final children = textNode.children.toList(growable: false); - transactionBuilder.deleteNodes(children); + transaction.deleteNodes(children); } - transactionBuilder.afterSelection = afterSelection; - transactionBuilder.commit(); + transaction.afterSelection = afterSelection; + editorState.commit(); // If the new type of a text node is number list, // the numbers of the following nodes should be incremental. diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/format_style_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/format_style_handler.dart index db5c298526..47e2dc10ab 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/format_style_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/format_style_handler.dart @@ -2,7 +2,7 @@ import 'package:appflowy_editor/src/service/default_text_operations/format_rich_ import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event_handler.dart'; import 'package:flutter/material.dart'; -import 'package:appflowy_editor/src/document/node.dart'; +import 'package:appflowy_editor/src/core/document/node.dart'; ShortcutEventHandler formatBoldEventHandler = (editorState, event) { final selection = editorState.service.selectionService.currentSelection.value; 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 760d7e0b6c..e740907f01 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 @@ -1,6 +1,7 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/src/extensions/text_node_extensions.dart'; import 'package:appflowy_editor/src/service/default_text_operations/format_rich_text_style.dart'; + import 'package:flutter/material.dart'; bool _isCodeStyle(TextNode textNode, int index) { @@ -44,7 +45,7 @@ ShortcutEventHandler backquoteToCodeHandler = (editorState, event) { final textNode = textNodes.first; final selectionText = textNode - .toRawString() + .toPlainText() .substring(selection.start.offset, selection.end.offset); // toggle code style when selected some text @@ -53,7 +54,7 @@ ShortcutEventHandler backquoteToCodeHandler = (editorState, event) { return KeyEventResult.handled; } - final text = textNode.toRawString().substring(0, selection.end.offset); + final text = textNode.toPlainText().substring(0, selection.end.offset); final backquoteIndexes = _findBackquoteIndexes(text, textNode); if (backquoteIndexes.isEmpty) { return KeyEventResult.ignored; @@ -72,7 +73,7 @@ ShortcutEventHandler backquoteToCodeHandler = (editorState, event) { return KeyEventResult.ignored; } - TransactionBuilder(editorState) + editorState.transaction ..deleteText(textNode, lastBackquoteIndex, 1) ..deleteText(textNode, firstBackquoteIndex, 2) ..formatText( @@ -88,8 +89,8 @@ ShortcutEventHandler backquoteToCodeHandler = (editorState, event) { path: textNode.path, offset: endIndex - 3, ), - ) - ..commit(); + ); + editorState.commit(); return KeyEventResult.handled; } @@ -103,7 +104,7 @@ ShortcutEventHandler backquoteToCodeHandler = (editorState, event) { // delete the backquote. // update the style of the text surround by ` ` to code. // and update the cursor position. - TransactionBuilder(editorState) + editorState.transaction ..deleteText(textNode, startIndex, 1) ..formatText( textNode, @@ -118,8 +119,8 @@ ShortcutEventHandler backquoteToCodeHandler = (editorState, event) { path: textNode.path, offset: endIndex - 1, ), - ) - ..commit(); + ); + editorState.commit(); return KeyEventResult.handled; }; @@ -134,7 +135,7 @@ ShortcutEventHandler doubleTildeToStrikethrough = (editorState, event) { } final textNode = textNodes.first; - final text = textNode.toRawString().substring(0, selection.end.offset); + 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] != '~') { @@ -165,7 +166,7 @@ ShortcutEventHandler doubleTildeToStrikethrough = (editorState, event) { // delete the last three tildes. // update the style of the text surround by `~~ ~~` to strikethrough. // and update the cursor position. - TransactionBuilder(editorState) + editorState.transaction ..deleteText(textNode, lastTildeIndex, 1) ..deleteText(textNode, thirdToLastTildeIndex, 2) ..formatText( @@ -181,8 +182,8 @@ ShortcutEventHandler doubleTildeToStrikethrough = (editorState, event) { path: textNode.path, offset: selection.end.offset - 3, ), - ) - ..commit(); + ); + editorState.commit(); return KeyEventResult.handled; }; @@ -199,7 +200,7 @@ ShortcutEventHandler markdownLinkToLinkHandler = (editorState, event) { // find all of the indexs for important characters final textNode = textNodes.first; - final text = textNode.toRawString(); + final text = textNode.toPlainText(); final firstOpeningBracket = text.indexOf('['); final firstClosingBracket = text.indexOf(']'); @@ -219,7 +220,7 @@ ShortcutEventHandler markdownLinkToLinkHandler = (editorState, event) { // update the href attribute of the text surrounded by [ ] to the url, // delete everything after the text, // and update the cursor position. - TransactionBuilder(editorState) + editorState.transaction ..deleteText(textNode, firstOpeningBracket, 1) ..formatText( textNode, @@ -236,8 +237,8 @@ ShortcutEventHandler markdownLinkToLinkHandler = (editorState, event) { path: textNode.path, offset: firstOpeningBracket + linkText!.length, ), - ) - ..commit(); + ); + editorState.commit(); return KeyEventResult.handled; }; 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 index 37a45805ab..8b1c322db3 100644 --- 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 @@ -11,7 +11,7 @@ ShortcutEventHandler doubleAsterisksToBold = (editorState, event) { } final textNode = textNodes.first; - final text = textNode.toRawString().substring(0, selection.end.offset); + 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] != '*') { @@ -42,7 +42,7 @@ ShortcutEventHandler doubleAsterisksToBold = (editorState, event) { // delete the last three asterisks. // update the style of the text surround by `** **` to bold. // and update the cursor position. - TransactionBuilder(editorState) + editorState.transaction ..deleteText(textNode, lastAsterisIndex, 1) ..deleteText(textNode, thirdToLastAsteriskIndex, 2) ..formatText( @@ -59,8 +59,8 @@ ShortcutEventHandler doubleAsterisksToBold = (editorState, event) { path: textNode.path, offset: selection.end.offset - 3, ), - ) - ..commit(); + ); + editorState.commit(); return KeyEventResult.handled; }; @@ -75,7 +75,7 @@ ShortcutEventHandler doubleUnderscoresToBold = (editorState, event) { } final textNode = textNodes.first; - final text = textNode.toRawString().substring(0, selection.end.offset); + 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] != '_') { @@ -108,7 +108,7 @@ ShortcutEventHandler doubleUnderscoresToBold = (editorState, event) { // delete the last three underscores. // update the style of the text surround by `__ __` to bold. // and update the cursor position. - TransactionBuilder(editorState) + editorState.transaction ..deleteText(textNode, lastAsterisIndex, 1) ..deleteText(textNode, thirdToLastUnderscoreIndex, 2) ..formatText( @@ -125,8 +125,8 @@ ShortcutEventHandler doubleUnderscoresToBold = (editorState, event) { path: textNode.path, offset: selection.end.offset - 3, ), - ) - ..commit(); + ); + editorState.commit(); return KeyEventResult.handled; }; 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 ed9018a71e..c41ac07ad8 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 @@ -1,8 +1,7 @@ -import 'package:appflowy_editor/src/document/selection.dart'; +import 'package:appflowy_editor/src/core/location/selection.dart'; import 'package:appflowy_editor/src/editor_state.dart'; -import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart'; -import 'package:appflowy_editor/src/operation/transaction_builder.dart'; -import 'package:appflowy_editor/src/document/attributes.dart'; +import 'package:appflowy_editor/src/core/legacy/built_in_attribute_keys.dart'; +import 'package:appflowy_editor/src/core/document/attributes.dart'; void makeFollowingNodesIncremental( EditorState editorState, List insertPath, Selection afterSelection, @@ -16,7 +15,7 @@ void makeFollowingNodesIncremental( int numPtr = beginNum + 1; var ptr = insertNode.next; - final builder = TransactionBuilder(editorState); + final builder = editorState.transaction; while (ptr != null) { if (ptr.subtype != BuiltInAttributeKey.numberList) { @@ -34,5 +33,5 @@ void makeFollowingNodesIncremental( } builder.afterSelection = afterSelection; - builder.commit(); + editorState.commit(); } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/select_all_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/select_all_handler.dart index 9841ec3167..800877b435 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/select_all_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/select_all_handler.dart @@ -1,6 +1,6 @@ -import 'package:appflowy_editor/src/document/node.dart'; -import 'package:appflowy_editor/src/document/position.dart'; -import 'package:appflowy_editor/src/document/selection.dart'; +import 'package:appflowy_editor/src/core/document/node.dart'; +import 'package:appflowy_editor/src/core/location/position.dart'; +import 'package:appflowy_editor/src/core/location/selection.dart'; import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event_handler.dart'; import 'package:flutter/material.dart'; 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 50e72c25ab..259f8bcdce 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 @@ -1,5 +1,5 @@ -import 'package:appflowy_editor/src/document/node.dart'; -import 'package:appflowy_editor/src/operation/transaction_builder.dart'; +import 'package:appflowy_editor/src/core/document/node.dart'; +import 'package:appflowy_editor/src/core/transform/transaction.dart'; import 'package:appflowy_editor/src/render/selection_menu/selection_menu_service.dart'; import 'package:appflowy_editor/src/extensions/node_extensions.dart'; import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event_handler.dart'; @@ -25,10 +25,9 @@ ShortcutEventHandler slashShortcutHandler = (editorState, event) { if (selection == null || context == null || selectable == null) { return KeyEventResult.ignored; } - TransactionBuilder(editorState) - ..replaceText(textNode, selection.start.offset, - selection.end.offset - selection.start.offset, event.character ?? '') - ..commit(); + editorState.transaction.replaceText(textNode, selection.start.offset, + selection.end.offset - selection.start.offset, event.character ?? ''); + editorState.commit(); WidgetsBinding.instance.addPostFrameCallback((_) { _selectionMenuService = 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 0291fc34a5..6b92a1bf19 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,9 +15,8 @@ ShortcutEventHandler tabHandler = (editorState, event) { final previous = textNode.previous; if (textNode.subtype != BuiltInAttributeKey.bulletedList) { - TransactionBuilder(editorState) - ..insertText(textNode, selection.end.offset, ' ' * 4) - ..commit(); + editorState.transaction.insertText(textNode, selection.end.offset, ' ' * 4); + editorState.commit(); return KeyEventResult.handled; } @@ -31,11 +30,11 @@ ShortcutEventHandler tabHandler = (editorState, event) { start: selection.start.copyWith(path: path), end: selection.end.copyWith(path: path), ); - TransactionBuilder(editorState) + editorState.transaction ..deleteNode(textNode) ..insertNode(path, textNode) - ..setAfterSelection(afterSelection) - ..commit(); + ..afterSelection = afterSelection; + editorState.commit(); return KeyEventResult.handled; }; 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 b6d0fd0d7c..cc63578473 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 @@ -1,12 +1,12 @@ +import 'package:appflowy_editor/src/core/transform/transaction.dart'; import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event_handler.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart'; -import 'package:appflowy_editor/src/document/node.dart'; -import 'package:appflowy_editor/src/document/position.dart'; -import 'package:appflowy_editor/src/document/selection.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/location/position.dart'; +import 'package:appflowy_editor/src/core/location/selection.dart'; import 'package:appflowy_editor/src/editor_state.dart'; -import 'package:appflowy_editor/src/operation/transaction_builder.dart'; import './number_list_helper.dart'; import 'package:appflowy_editor/src/extensions/attributes_extension.dart'; @@ -44,7 +44,7 @@ ShortcutEventHandler whiteSpaceHandler = (editorState, event) { } final textNode = textNodes.first; - final text = textNode.toRawString().substring(0, selection.end.offset); + final text = textNode.toPlainText().substring(0, selection.end.offset); final numberMatch = _numberRegex.firstMatch(text); @@ -99,15 +99,14 @@ KeyEventResult _toNumberList(EditorState editorState, TextNode textNode, )); final insertPath = textNode.path; - - TransactionBuilder(editorState) + editorState.transaction ..deleteText(textNode, 0, matchText.length) ..updateNode(textNode, { BuiltInAttributeKey.subtype: BuiltInAttributeKey.numberList, BuiltInAttributeKey.number: numValue }) - ..afterSelection = afterSelection - ..commit(); + ..afterSelection = afterSelection; + editorState.commit(); makeFollowingNodesIncremental(editorState, insertPath, afterSelection); @@ -118,7 +117,7 @@ KeyEventResult _toBulletedList(EditorState editorState, TextNode textNode) { if (textNode.subtype == BuiltInAttributeKey.bulletedList) { return KeyEventResult.ignored; } - TransactionBuilder(editorState) + editorState.transaction ..deleteText(textNode, 0, 1) ..updateNode(textNode, { BuiltInAttributeKey.subtype: BuiltInAttributeKey.bulletedList, @@ -128,8 +127,8 @@ KeyEventResult _toBulletedList(EditorState editorState, TextNode textNode) { path: textNode.path, offset: 0, ), - ) - ..commit(); + ); + editorState.commit(); return KeyEventResult.handled; } @@ -140,18 +139,18 @@ KeyEventResult _toCheckboxList(EditorState editorState, TextNode textNode) { final String symbol; bool check = false; final symbols = List.from(_checkboxListSymbols) - ..retainWhere(textNode.toRawString().startsWith); + ..retainWhere(textNode.toPlainText().startsWith); if (symbols.isNotEmpty) { symbol = symbols.first; check = true; } else { symbol = (List.from(_unCheckboxListSymbols) - ..retainWhere(textNode.toRawString().startsWith)) + ..retainWhere(textNode.toPlainText().startsWith)) .first; check = false; } - TransactionBuilder(editorState) + editorState.transaction ..deleteText(textNode, 0, symbol.length) ..updateNode(textNode, { BuiltInAttributeKey.subtype: BuiltInAttributeKey.checkbox, @@ -162,22 +161,22 @@ KeyEventResult _toCheckboxList(EditorState editorState, TextNode textNode) { path: textNode.path, offset: 0, ), - ) - ..commit(); + ); + editorState.commit(); return KeyEventResult.handled; } KeyEventResult _toHeadingStyle( EditorState editorState, TextNode textNode, Selection selection) { final x = _countOfSign( - textNode.toRawString(), + textNode.toPlainText(), selection, ); final hX = 'h$x'; if (textNode.attributes.heading == hX) { return KeyEventResult.ignored; } - TransactionBuilder(editorState) + editorState.transaction ..deleteText(textNode, 0, x) ..updateNode(textNode, { BuiltInAttributeKey.subtype: BuiltInAttributeKey.heading, @@ -188,8 +187,8 @@ KeyEventResult _toHeadingStyle( path: textNode.path, offset: 0, ), - ) - ..commit(); + ); + editorState.commit(); return KeyEventResult.handled; } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/render_plugin_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/render_plugin_service.dart index 2ad2989207..8501a16b41 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/render_plugin_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/render_plugin_service.dart @@ -1,4 +1,4 @@ -import 'package:appflowy_editor/src/document/node.dart'; +import 'package:appflowy_editor/src/core/document/node.dart'; import 'package:appflowy_editor/src/editor_state.dart'; import 'package:appflowy_editor/src/infra/log.dart'; import 'package:flutter/material.dart'; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/selection_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/selection_service.dart index 4eacc5674c..a5f27e9664 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/selection_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/selection_service.dart @@ -1,14 +1,14 @@ import 'package:appflowy_editor/src/infra/log.dart'; import 'package:flutter/material.dart'; -import 'package:appflowy_editor/src/document/node.dart'; -import 'package:appflowy_editor/src/document/node_iterator.dart'; -import 'package:appflowy_editor/src/document/position.dart'; -import 'package:appflowy_editor/src/document/selection.dart'; +import 'package:appflowy_editor/src/core/document/node.dart'; +import 'package:appflowy_editor/src/core/document/node_iterator.dart'; +import 'package:appflowy_editor/src/core/document/path.dart'; +import 'package:appflowy_editor/src/core/location/position.dart'; +import 'package:appflowy_editor/src/core/location/selection.dart'; import 'package:appflowy_editor/src/editor_state.dart'; import 'package:appflowy_editor/src/extensions/node_extensions.dart'; import 'package:appflowy_editor/src/extensions/object_extensions.dart'; -import 'package:appflowy_editor/src/extensions/path_extensions.dart'; import 'package:appflowy_editor/src/render/selection/cursor_widget.dart'; import 'package:appflowy_editor/src/render/selection/selectable.dart'; import 'package:appflowy_editor/src/render/selection/selection_widget.dart'; @@ -179,8 +179,11 @@ class _AppFlowySelectionState extends State final startNode = editorState.document.nodeAtPath(start); final endNode = editorState.document.nodeAtPath(end); if (startNode != null && endNode != null) { - final nodes = - NodeIterator(editorState.document, startNode, endNode).toList(); + final nodes = NodeIterator( + document: editorState.document, + startNode: startNode, + endNode: endNode, + ).toList(); if (selection.isBackward) { return nodes; } else { @@ -363,7 +366,7 @@ class _AppFlowySelectionState extends State final backwardNodes = selection.isBackward ? nodes : nodes.reversed.toList(growable: false); - final normalizedSelection = selection.normalize; + final normalizedSelection = selection.normalized; assert(normalizedSelection.isBackward); Log.selection.debug('update selection areas, $normalizedSelection'); @@ -375,7 +378,7 @@ class _AppFlowySelectionState extends State continue; } - var newSelection = normalizedSelection.copy(); + var newSelection = normalizedSelection.copyWith(); /// In the case of multiple selections, /// we need to return a new selection for each selected node individually. diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/undo_manager.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/undo_manager.dart index 737076e930..1ef8bf5c10 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/undo_manager.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/undo_manager.dart @@ -1,10 +1,9 @@ import 'dart:collection'; -import 'package:appflowy_editor/src/document/selection.dart'; +import 'package:appflowy_editor/src/core/location/selection.dart'; import 'package:appflowy_editor/src/infra/log.dart'; -import 'package:appflowy_editor/src/operation/operation.dart'; -import 'package:appflowy_editor/src/operation/transaction_builder.dart'; -import 'package:appflowy_editor/src/operation/transaction.dart'; +import 'package:appflowy_editor/src/core/transform/operation.dart'; +import 'package:appflowy_editor/src/core/transform/transaction.dart'; import 'package:appflowy_editor/src/editor_state.dart'; /// A [HistoryItem] contains list of operations committed by users. @@ -39,7 +38,7 @@ class HistoryItem extends LinkedListEntry { /// Create a new [Transaction] by inverting the operations. Transaction toTransaction(EditorState state) { - final builder = TransactionBuilder(state); + final builder = Transaction(document: state.document); for (var i = operations.length - 1; i >= 0; i--) { final operation = operations[i]; final inverted = operation.invert(); @@ -47,7 +46,7 @@ class HistoryItem extends LinkedListEntry { } builder.afterSelection = beforeSelection; builder.beforeSelection = afterSelection; - return builder.finish(); + return builder; } } diff --git a/frontend/app_flowy/packages/appflowy_editor/test/core/document/attributes_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/core/document/attributes_test.dart new file mode 100644 index 0000000000..873ab2788b --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/test/core/document/attributes_test.dart @@ -0,0 +1,59 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() async { + group('attributes.dart', () { + test('composeAttributes', () { + final base = { + 'a': 1, + 'b': 2, + }; + final other = { + 'b': 3, + 'c': 4, + 'd': null, + }; + expect(composeAttributes(base, other, keepNull: false), { + 'a': 1, + 'b': 3, + 'c': 4, + }); + expect(composeAttributes(base, other, keepNull: true), { + 'a': 1, + 'b': 3, + 'c': 4, + 'd': null, + }); + expect(composeAttributes(null, other, keepNull: false), { + 'b': 3, + 'c': 4, + }); + expect(composeAttributes(base, null, keepNull: false), { + 'a': 1, + 'b': 2, + }); + }); + + test('invertAttributes', () { + final base = { + 'a': 1, + 'b': 2, + }; + final other = { + 'b': 3, + 'c': 4, + 'd': null, + }; + expect(invertAttributes(base, other), { + 'a': 1, + 'b': 2, + 'c': null, + }); + expect(invertAttributes(other, base), { + 'a': null, + 'b': 3, + 'c': 4, + }); + }); + }); +} diff --git a/frontend/app_flowy/packages/appflowy_editor/test/core/document/document_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/core/document/document_test.dart new file mode 100644 index 0000000000..a8059d584a --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/test/core/document/document_test.dart @@ -0,0 +1,77 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() async { + group('documemnt.dart', () { + test('insert', () { + final document = Document.empty(); + + expect(document.insert([-1], []), false); + expect(document.insert([100], []), false); + + final node0 = Node(type: '0'); + final node1 = Node(type: '1'); + expect(document.insert([0], [node0, node1]), true); + expect(document.nodeAtPath([0])?.type, '0'); + expect(document.nodeAtPath([1])?.type, '1'); + }); + + test('delete', () { + final document = Document(root: Node(type: 'root')); + + expect(document.delete([-1], 1), false); + expect(document.delete([100], 1), false); + + for (var i = 0; i < 10; i++) { + final node = Node(type: '$i'); + document.insert([i], [node]); + } + + document.delete([0], 10); + expect(document.root.children.isEmpty, true); + }); + + test('update', () { + final node = Node(type: 'example', attributes: {'a': 'a'}); + final document = Document(root: Node(type: 'root')); + document.insert([0], [node]); + + final attributes = { + 'a': 'b', + 'b': 'c', + }; + + expect(document.update([0], attributes), true); + expect(document.nodeAtPath([0])?.attributes, attributes); + + expect(document.update([-1], attributes), false); + }); + + test('updateText', () { + final delta = Delta()..insert('Editor'); + final textNode = TextNode(delta: delta); + final document = Document(root: Node(type: 'root')); + document.insert([0], [textNode]); + document.updateText([0], Delta()..insert('AppFlowy')); + expect((document.nodeAtPath([0]) as TextNode).toPlainText(), + 'AppFlowyEditor'); + }); + + test('serialize', () { + final json = { + 'document': { + 'type': 'editor', + 'children': [ + { + 'type': 'text', + 'delta': [], + } + ], + 'attributes': {'a': 'a'} + } + }; + final document = Document.fromJson(json); + expect(document.toJson(), json); + }); + }); +} diff --git a/frontend/app_flowy/packages/appflowy_editor/test/core/document/node_iterator_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/core/document/node_iterator_test.dart new file mode 100644 index 0000000000..05a0090ec3 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/test/core/document/node_iterator_test.dart @@ -0,0 +1,33 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/src/core/document/node_iterator.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() async { + group('node_iterator.dart', () { + test('', () { + final root = Node(type: 'root'); + for (var i = 1; i <= 10; i++) { + final node = Node(type: 'node_$i'); + for (var j = 1; j <= i; j++) { + node.insert(Node(type: 'node_${i}_$j')); + } + root.insert(node); + } + final nodes = NodeIterator( + document: Document(root: root), + startNode: root.childAtPath([0])!, + endNode: root.childAtPath([10, 10]), + ); + + for (var i = 1; i <= 10; i++) { + nodes.moveNext(); + expect(nodes.current.type, 'node_$i'); + for (var j = 1; j <= i; j++) { + nodes.moveNext(); + expect(nodes.current.type, 'node_${i}_$j'); + } + } + expect(nodes.moveNext(), false); + }); + }); +} diff --git a/frontend/app_flowy/packages/appflowy_editor/test/document/node_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/core/document/node_test.dart similarity index 62% rename from frontend/app_flowy/packages/appflowy_editor/test/document/node_test.dart rename to frontend/app_flowy/packages/appflowy_editor/test/core/document/node_test.dart index 3ff00eaa7a..4e407fd321 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/document/node_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/core/document/node_test.dart @@ -4,10 +4,6 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter_test/flutter_test.dart'; void main() async { - setUpAll(() { - TestWidgetsFlutterBinding.ensureInitialized(); - }); - group('node.dart', () { test('test node copyWith', () { final node = Node( @@ -57,7 +53,6 @@ void main() async { test('test textNode copyWith', () { final textNode = TextNode( - type: 'example', children: LinkedList(), attributes: { 'example': 'example', @@ -65,7 +60,7 @@ void main() async { delta: Delta()..insert('AppFlowy'), ); expect(textNode.toJson(), { - 'type': 'example', + 'type': 'text', 'attributes': { 'example': 'example', }, @@ -79,7 +74,6 @@ void main() async { ); final textNodeWithChildren = TextNode( - type: 'example', children: LinkedList()..add(textNode), attributes: { 'example': 'example', @@ -87,7 +81,7 @@ void main() async { delta: Delta()..insert('AppFlowy'), ); expect(textNodeWithChildren.toJson(), { - 'type': 'example', + 'type': 'text', 'attributes': { 'example': 'example', }, @@ -96,7 +90,7 @@ void main() async { ], 'children': [ { - 'type': 'example', + 'type': 'text', 'attributes': { 'example': 'example', }, @@ -149,5 +143,90 @@ void main() async { expect(identical(node.children, base.children), false); expect(identical(node.children.first, base.children.first), false); }); + + test('test insert', () { + final base = Node( + type: 'base', + ); + + // insert at the front when node's children is empty + final childA = Node( + type: 'child', + ); + base.insert(childA); + expect( + identical(base.childAtIndex(0), childA), + true, + ); + + // insert at the front + final childB = Node( + type: 'child', + ); + base.insert(childB, index: -1); + expect( + identical(base.childAtIndex(0), childB), + true, + ); + + // insert at the last + final childC = Node( + type: 'child', + ); + base.insert(childC, index: 1000); + expect( + identical(base.childAtIndex(base.children.length - 1), childC), + true, + ); + + // insert at the last + final childD = Node( + type: 'child', + ); + base.insert(childD); + expect( + identical(base.childAtIndex(base.children.length - 1), childD), + true, + ); + + // insert at the second + final childE = Node( + type: 'child', + ); + base.insert(childE, index: 1); + expect( + identical(base.childAtIndex(1), childE), + true, + ); + }); + + test('test fromJson', () { + final node = Node.fromJson({ + 'type': 'text', + 'delta': [ + {'insert': 'example'}, + ], + 'children': [ + { + 'type': 'example', + 'attributes': { + 'example': 'example', + }, + }, + ], + }); + expect(node.type, 'text'); + expect(node is TextNode, true); + expect((node as TextNode).delta.toPlainText(), 'example'); + expect(node.attributes, {}); + expect(node.children.length, 1); + expect(node.children.first.type, 'example'); + expect(node.children.first.attributes, {'example': 'example'}); + }); + + test('test toPlainText', () { + final textNode = TextNode.empty()..delta = (Delta()..insert('AppFlowy')); + expect(textNode.toPlainText(), 'AppFlowy'); + }); }); } diff --git a/frontend/app_flowy/packages/appflowy_editor/test/core/document/path_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/core/document/path_test.dart new file mode 100644 index 0000000000..cf11a96dd6 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/test/core/document/path_test.dart @@ -0,0 +1,33 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() async { + group('path.dart', () { + test('test path equality', () { + var p1 = [0, 0]; + var p2 = [0]; + + expect(p1 > p2, true); + expect(p1 >= p2, true); + expect(p1 < p2, false); + expect(p1 <= p2, false); + + p1 = [1, 1, 2]; + p2 = [1, 1, 3]; + + expect(p2 > p1, true); + expect(p2 >= p1, true); + expect(p2 < p1, false); + expect(p2 <= p1, false); + + p1 = [2, 0, 1]; + p2 = [2, 0, 1]; + + expect(p2 > p1, false); + expect(p1 > p2, false); + expect(p2 >= p1, true); + expect(p2 <= p1, true); + expect(p1.equals(p2), true); + }); + }); +} diff --git a/frontend/app_flowy/packages/appflowy_editor/test/core/document/text_delta_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/core/document/text_delta_test.dart new file mode 100644 index 0000000000..c0c3946636 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/test/core/document/text_delta_test.dart @@ -0,0 +1,332 @@ +import 'package:appflowy_editor/src/core/document/attributes.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:appflowy_editor/src/core/document/text_delta.dart'; + +void main() { + group('text_delta.dart', () { + group('compose', () { + test('test delta', () { + final delta = Delta(operations: [ + TextInsert('Gandalf', attributes: { + 'bold': true, + }), + TextInsert(' the '), + TextInsert('Grey', attributes: { + 'color': '#ccc', + }) + ]); + + final death = Delta() + ..retain(12) + ..insert("White", attributes: { + 'color': '#fff', + }) + ..delete(4); + + final restores = delta.compose(death); + expect(restores.toList(), [ + TextInsert('Gandalf', attributes: {'bold': true}), + TextInsert(' the '), + TextInsert('White', attributes: {'color': '#fff'}), + ]); + }); + test('compose()', () { + final a = Delta()..insert('A'); + final b = Delta()..insert('B'); + final expected = Delta() + ..insert('B') + ..insert('A'); + expect(a.compose(b), expected); + }); + test('insert + retain', () { + final a = Delta()..insert('A'); + final b = Delta() + ..retain(1, attributes: { + 'bold': true, + 'color': 'red', + }); + final expected = Delta() + ..insert('A', attributes: { + 'bold': true, + 'color': 'red', + }); + expect(a.compose(b), expected); + }); + test('insert + delete', () { + final a = Delta()..insert('A'); + final b = Delta()..delete(1); + final expected = Delta(); + expect(a.compose(b), expected); + }); + test('delete + insert', () { + final a = Delta()..delete(1); + final b = Delta()..insert('B'); + final expected = Delta() + ..insert('B') + ..delete(1); + expect(a.compose(b), expected); + }); + test('delete + retain', () { + final a = Delta()..delete(1); + final b = Delta() + ..retain(1, attributes: { + 'bold': true, + 'color': 'red', + }); + final expected = Delta() + ..delete(1) + ..retain(1, attributes: { + 'bold': true, + 'color': 'red', + }); + expect(a.compose(b), expected); + }); + test('delete + delete', () { + final a = Delta()..delete(1); + final b = Delta()..delete(1); + final expected = Delta()..delete(2); + expect(a.compose(b), expected); + }); + test('retain + insert', () { + final a = Delta()..retain(1, attributes: {'color': 'blue'}); + final b = Delta()..insert('B'); + final expected = Delta() + ..insert('B') + ..retain(1, attributes: { + 'color': 'blue', + }); + expect(a.compose(b), expected); + }); + test('retain + retain', () { + final a = Delta() + ..retain(1, attributes: { + 'color': 'blue', + }); + final b = Delta() + ..retain(1, attributes: { + 'bold': true, + 'color': 'red', + }); + final expected = Delta() + ..retain(1, attributes: { + 'bold': true, + 'color': 'red', + }); + expect(a.compose(b), expected); + }); + test('retain + delete', () { + final a = Delta() + ..retain(1, attributes: { + 'color': 'blue', + }); + final b = Delta()..delete(1); + final expected = Delta()..delete(1); + expect(a.compose(b), expected); + }); + test('insert in middle of text', () { + final a = Delta()..insert('Hello'); + final b = Delta() + ..retain(3) + ..insert('X'); + final expected = Delta()..insert('HelXlo'); + expect(a.compose(b), expected); + }); + test('insert and delete ordering', () { + final a = Delta()..insert('Hello'); + final b = Delta()..insert('Hello'); + final insertFirst = Delta() + ..retain(3) + ..insert('X') + ..delete(1); + final deleteFirst = Delta() + ..retain(3) + ..delete(1) + ..insert('X'); + final expected = Delta()..insert('HelXo'); + expect(a.compose(insertFirst), expected); + expect(b.compose(deleteFirst), expected); + }); + test('delete entire text', () { + final a = Delta() + ..retain(4) + ..insert('Hello'); + final b = Delta()..delete(9); + final expected = Delta()..delete(4); + expect(a.compose(b), expected); + }); + test('retain more than length of text', () { + final a = Delta()..insert('Hello'); + final b = Delta()..retain(10); + final expected = Delta()..insert('Hello'); + expect(a.compose(b), expected); + }); + test('retain start optimization', () { + final a = Delta() + ..insert('A', attributes: {'bold': true}) + ..insert('B') + ..insert('C', attributes: {'bold': true}) + ..delete(1); + final b = Delta() + ..retain(3) + ..insert('D'); + final expected = Delta() + ..insert('A', attributes: {'bold': true}) + ..insert('B') + ..insert('C', attributes: {'bold': true}) + ..insert('D') + ..delete(1); + expect(a.compose(b), expected); + }); + test('retain end optimization', () { + final a = Delta() + ..insert('A', attributes: {'bold': true}) + ..insert('B') + ..insert('C', attributes: {'bold': true}); + final b = Delta()..delete(1); + final expected = Delta() + ..insert('B') + ..insert('C', attributes: {'bold': true}); + expect(a.compose(b), expected); + }); + test('retain end optimization join', () { + final a = Delta() + ..insert('A', attributes: {'bold': true}) + ..insert('B') + ..insert('C', attributes: {'bold': true}) + ..insert('D') + ..insert('E', attributes: {'bold': true}) + ..insert('F'); + final b = Delta() + ..retain(1) + ..delete(1); + final expected = Delta() + ..insert('AC', attributes: {'bold': true}) + ..insert('D') + ..insert('E', attributes: {'bold': true}) + ..insert('F'); + expect(a.compose(b), expected); + }); + }); + group('invert', () { + test('insert', () { + final delta = Delta() + ..retain(2) + ..insert('A'); + final base = Delta()..insert('12346'); + final expected = Delta() + ..retain(2) + ..delete(1); + final inverted = delta.invert(base); + expect(expected, inverted); + expect(base.compose(delta).compose(inverted), base); + }); + test('delete', () { + final delta = Delta() + ..retain(2) + ..delete(3); + final base = Delta()..insert('123456'); + final expected = Delta() + ..retain(2) + ..insert('345'); + final inverted = delta.invert(base); + expect(expected, inverted); + expect(base.compose(delta).compose(inverted), base); + }); + test('retain', () { + final delta = Delta() + ..retain(2) + ..retain(3, attributes: {'bold': true}); + final base = Delta()..insert('123456'); + final expected = Delta() + ..retain(2) + ..retain(3, attributes: {'bold': null}); + final inverted = delta.invert(base); + expect(expected, inverted); + final t = base.compose(delta).compose(inverted); + expect(t, base); + }); + }); + group('json', () { + test('toJson()', () { + final delta = Delta() + ..retain(2) + ..insert('A') + ..delete(3); + expect(delta.toJson(), [ + {'retain': 2}, + {'insert': 'A'}, + {'delete': 3} + ]); + }); + test('attributes', () { + final delta = Delta() + ..retain(2, attributes: {'bold': true}) + ..insert('A', attributes: {'italic': true}); + expect(delta.toJson(), [ + { + 'retain': 2, + 'attributes': {'bold': true}, + }, + { + 'insert': 'A', + 'attributes': {'italic': true}, + }, + ]); + }); + test('fromJson()', () { + final delta = Delta.fromJson([ + {'retain': 2}, + {'insert': 'A'}, + {'delete': 3}, + ]); + final expected = Delta() + ..retain(2) + ..insert('A') + ..delete(3); + expect(delta, expected); + }); + }); + group('runes', () { + test("stringIndexes", () { + final indexes = stringIndexes('😊'); + expect(indexes[0], 0); + expect(indexes[1], 0); + }); + test("next rune 1", () { + final delta = Delta()..insert('😊'); + expect(delta.nextRunePosition(0), 2); + }); + test("next rune 2", () { + final delta = Delta()..insert('😊a'); + expect(delta.nextRunePosition(0), 2); + }); + test("next rune 3", () { + final delta = Delta()..insert('😊陈'); + expect(delta.nextRunePosition(2), 3); + }); + test("prev rune 1", () { + final delta = Delta()..insert('😊陈'); + expect(delta.prevRunePosition(2), 0); + }); + test("prev rune 2", () { + final delta = Delta()..insert('😊'); + expect(delta.prevRunePosition(2), 0); + }); + test("prev rune 3", () { + final delta = Delta()..insert('😊'); + expect(delta.prevRunePosition(0), -1); + }); + }); + group("attributes", () { + test("compose", () { + final attrs = + composeAttributes({'a': null}, {'b': null}, keepNull: true); + expect(attrs != null, true); + expect(attrs?.containsKey("a"), true); + expect(attrs?.containsKey("b"), true); + expect(attrs?["a"], null); + expect(attrs?["b"], null); + }); + }); + }); +} diff --git a/frontend/app_flowy/packages/appflowy_editor/test/core/location/position_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/core/location/position_test.dart new file mode 100644 index 0000000000..ded398e968 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/test/core/location/position_test.dart @@ -0,0 +1,26 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() async { + group('position.dart', () { + test('test position equality', () { + final positionA = Position(path: [0, 1, 2], offset: 3); + final positionB = Position(path: [0, 1, 2], offset: 3); + expect(positionA, positionB); + + final positionC = positionA.copyWith(offset: 4); + final positionD = positionB.copyWith(path: [1, 2, 3]); + expect(positionC.offset, 4); + expect(positionD.path, [1, 2, 3]); + + expect(positionA.toJson(), { + 'path': [0, 1, 2], + 'offset': 3, + }); + expect(positionC.toJson(), { + 'path': [0, 1, 2], + 'offset': 4, + }); + }); + }); +} diff --git a/frontend/app_flowy/packages/appflowy_editor/test/core/location/selection_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/core/location/selection_test.dart new file mode 100644 index 0000000000..4361ffcf67 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/test/core/location/selection_test.dart @@ -0,0 +1,77 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() async { + group('selection.dart', () { + test('test selection equality', () { + final position = Position(path: [0, 1, 2], offset: 3); + final selectionA = Selection(start: position, end: position); + final selectionB = Selection.collapsed(position); + expect(selectionA, selectionB); + expect(selectionA.hashCode, selectionB.hashCode); + + final newPosition = Position(path: [1, 2, 3], offset: 4); + + final selectionC = selectionA.copyWith(start: newPosition); + expect(selectionC.start, newPosition); + expect(selectionC.end, position); + expect(selectionC.isCollapsed, false); + + final selectionD = selectionA.copyWith(end: newPosition); + expect(selectionD.start, position); + expect(selectionD.end, newPosition); + expect(selectionD.isCollapsed, false); + + final selectionE = Selection.single(path: [0, 1, 2], startOffset: 3); + expect(selectionE, selectionA); + expect(selectionE.isSingle, true); + expect(selectionE.isCollapsed, true); + }); + + test('test selection direction', () { + final start = Position(path: [0, 1, 2], offset: 3); + final end = Position(path: [1, 2, 3], offset: 3); + final backwardSelection = Selection(start: start, end: end); + expect(backwardSelection.isBackward, true); + final forwardSelection = Selection(start: end, end: start); + expect(forwardSelection.isForward, true); + + expect(backwardSelection.reversed, forwardSelection); + expect(forwardSelection.normalized, backwardSelection); + + expect(backwardSelection.startIndex, 3); + expect(backwardSelection.endIndex, 3); + }); + + test('test selection collapsed', () { + final start = Position(path: [0, 1, 2], offset: 3); + final end = Position(path: [1, 2, 3], offset: 3); + final selection = Selection(start: start, end: end); + final collapsedAtStart = selection.collapse(atStart: true); + expect(collapsedAtStart.isCollapsed, true); + expect(collapsedAtStart.start, start); + expect(collapsedAtStart.end, start); + + final collapsedAtEnd = selection.collapse(atStart: false); + expect(collapsedAtEnd.isCollapsed, true); + expect(collapsedAtEnd.start, end); + expect(collapsedAtEnd.end, end); + }); + + test('test selection toJson', () { + final start = Position(path: [0, 1, 2], offset: 3); + final end = Position(path: [1, 2, 3], offset: 3); + final selection = Selection(start: start, end: end); + expect(selection.toJson(), { + 'start': { + 'path': [0, 1, 2], + 'offset': 3 + }, + 'end': { + 'path': [1, 2, 3], + 'offset': 3 + } + }); + }); + }); +} diff --git a/frontend/app_flowy/packages/appflowy_editor/test/core/transform/operation_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/core/transform/operation_test.dart new file mode 100644 index 0000000000..d52ba43221 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/test/core/transform/operation_test.dart @@ -0,0 +1,79 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() async { + group('operation.dart', () { + test('test insert operation', () { + final node = Node(type: 'example'); + final op = InsertOperation([0], [node]); + final json = op.toJson(); + expect(json, { + 'op': 'insert', + 'path': [0], + 'nodes': [ + { + 'type': 'example', + } + ] + }); + expect(InsertOperation.fromJson(json), op); + expect(op.invert().invert(), op); + expect(op.copyWith(), op); + }); + + test('test update operation', () { + final op = UpdateOperation([0], {'a': 1}, {'a': 0}); + final json = op.toJson(); + expect(json, { + 'op': 'update', + 'path': [0], + 'attributes': {'a': 1}, + 'oldAttributes': {'a': 0} + }); + expect(UpdateOperation.fromJson(json), op); + expect(op.invert().invert(), op); + expect(op.copyWith(), op); + }); + + test('test delete operation', () { + final node = Node(type: 'example'); + final op = DeleteOperation([0], [node]); + final json = op.toJson(); + expect(json, { + 'op': 'delete', + 'path': [0], + 'nodes': [ + { + 'type': 'example', + } + ] + }); + expect(DeleteOperation.fromJson(json), op); + expect(op.invert().invert(), op); + expect(op.copyWith(), op); + }); + + test('test update text operation', () { + final app = Delta()..insert('App'); + final appflowy = Delta() + ..retain(3) + ..insert('Flowy'); + final op = UpdateTextOperation([0], app, appflowy.invert(app)); + final json = op.toJson(); + expect(json, { + 'op': 'update_text', + 'path': [0], + 'delta': [ + {'insert': 'App'} + ], + 'inverted': [ + {'retain': 3}, + {'delete': 5} + ] + }); + expect(UpdateTextOperation.fromJson(json), op); + expect(op.invert().invert(), op); + expect(op.copyWith(), op); + }); + }); +} diff --git a/frontend/app_flowy/packages/appflowy_editor/test/extensions/path_extensions_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/extensions/path_extensions_test.dart index 5d84690641..231be64c0a 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/extensions/path_extensions_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/extensions/path_extensions_test.dart @@ -31,7 +31,7 @@ void main() async { expect(p1 > p2, false); expect(p2 >= p1, true); expect(p2 <= p1, true); - expect(pathEquals(p1, p2), true); + expect(p1.equals(p2), true); }); }); } diff --git a/frontend/app_flowy/packages/appflowy_editor/test/infra/infra_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/infra/infra_test.dart index e03d524d76..9ed4a150b2 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/infra/infra_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/infra/infra_test.dart @@ -15,7 +15,6 @@ void main() async { const text = 'Welcome to Appflowy 😁'; TextNode textNode() { return TextNode( - type: 'text', delta: Delta()..insert(text), ); } diff --git a/frontend/app_flowy/packages/appflowy_editor/test/infra/test_editor.dart b/frontend/app_flowy/packages/appflowy_editor/test/infra/test_editor.dart index b4282a013e..6396e47ebe 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/infra/test_editor.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/infra/test_editor.dart @@ -19,7 +19,7 @@ class EditorWidgetTester { EditorState get editorState => _editorState; Node get root => _editorState.document.root; - StateTree get document => _editorState.document; + Document get document => _editorState.document; int get documentLength => _editorState.document.root.children.length; Selection? get documentSelection => _editorState.service.selectionService.currentSelection.value; @@ -63,8 +63,7 @@ class EditorWidgetTester { void insertTextNode(String? text, {Attributes? attributes, Delta? delta}) { insert( TextNode( - type: 'text', - delta: delta ?? Delta([TextInsert(text ?? 'Test')]), + delta: delta ?? Delta(operations: [TextInsert(text ?? 'Test')]), attributes: attributes, ), ); @@ -103,7 +102,7 @@ class EditorWidgetTester { {Selection? selection}) async { await apply([ TextEditingDeltaInsertion( - oldText: textNode.toRawString(), + oldText: textNode.toPlainText(), textInserted: text, insertionOffset: offset, selection: selection != null @@ -156,7 +155,7 @@ class EditorWidgetTester { EditorState _createEmptyDocument() { return EditorState( - document: StateTree( + document: Document( root: _createEmptyEditorRoot(), ), )..disableSealTimer = true; diff --git a/frontend/app_flowy/packages/appflowy_editor/test/legacy/delta_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/legacy/delta_test.dart deleted file mode 100644 index 1540b18a21..0000000000 --- a/frontend/app_flowy/packages/appflowy_editor/test/legacy/delta_test.dart +++ /dev/null @@ -1,329 +0,0 @@ -import 'package:appflowy_editor/src/document/attributes.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:appflowy_editor/src/document/text_delta.dart'; - -void main() { - group('compose', () { - test('test delta', () { - final delta = Delta([ - TextInsert('Gandalf', { - 'bold': true, - }), - TextInsert(' the '), - TextInsert('Grey', { - 'color': '#ccc', - }) - ]); - - final death = Delta() - ..retain(12) - ..insert("White", { - 'color': '#fff', - }) - ..delete(4); - - final restores = delta.compose(death); - expect(restores.toList(), [ - TextInsert('Gandalf', {'bold': true}), - TextInsert(' the '), - TextInsert('White', {'color': '#fff'}), - ]); - }); - test('compose()', () { - final a = Delta()..insert('A'); - final b = Delta()..insert('B'); - final expected = Delta() - ..insert('B') - ..insert('A'); - expect(a.compose(b), expected); - }); - test('insert + retain', () { - final a = Delta()..insert('A'); - final b = Delta() - ..retain(1, { - 'bold': true, - 'color': 'red', - }); - final expected = Delta() - ..insert('A', { - 'bold': true, - 'color': 'red', - }); - expect(a.compose(b), expected); - }); - test('insert + delete', () { - final a = Delta()..insert('A'); - final b = Delta()..delete(1); - final expected = Delta(); - expect(a.compose(b), expected); - }); - test('delete + insert', () { - final a = Delta()..delete(1); - final b = Delta()..insert('B'); - final expected = Delta() - ..insert('B') - ..delete(1); - expect(a.compose(b), expected); - }); - test('delete + retain', () { - final a = Delta()..delete(1); - final b = Delta() - ..retain(1, { - 'bold': true, - 'color': 'red', - }); - final expected = Delta() - ..delete(1) - ..retain(1, { - 'bold': true, - 'color': 'red', - }); - expect(a.compose(b), expected); - }); - test('delete + delete', () { - final a = Delta()..delete(1); - final b = Delta()..delete(1); - final expected = Delta()..delete(2); - expect(a.compose(b), expected); - }); - test('retain + insert', () { - final a = Delta()..retain(1, {'color': 'blue'}); - final b = Delta()..insert('B'); - final expected = Delta() - ..insert('B') - ..retain(1, { - 'color': 'blue', - }); - expect(a.compose(b), expected); - }); - test('retain + retain', () { - final a = Delta() - ..retain(1, { - 'color': 'blue', - }); - final b = Delta() - ..retain(1, { - 'bold': true, - 'color': 'red', - }); - final expected = Delta() - ..retain(1, { - 'bold': true, - 'color': 'red', - }); - expect(a.compose(b), expected); - }); - test('retain + delete', () { - final a = Delta() - ..retain(1, { - 'color': 'blue', - }); - final b = Delta()..delete(1); - final expected = Delta()..delete(1); - expect(a.compose(b), expected); - }); - test('insert in middle of text', () { - final a = Delta()..insert('Hello'); - final b = Delta() - ..retain(3) - ..insert('X'); - final expected = Delta()..insert('HelXlo'); - expect(a.compose(b), expected); - }); - test('insert and delete ordering', () { - final a = Delta()..insert('Hello'); - final b = Delta()..insert('Hello'); - final insertFirst = Delta() - ..retain(3) - ..insert('X') - ..delete(1); - final deleteFirst = Delta() - ..retain(3) - ..delete(1) - ..insert('X'); - final expected = Delta()..insert('HelXo'); - expect(a.compose(insertFirst), expected); - expect(b.compose(deleteFirst), expected); - }); - test('delete entire text', () { - final a = Delta() - ..retain(4) - ..insert('Hello'); - final b = Delta()..delete(9); - final expected = Delta()..delete(4); - expect(a.compose(b), expected); - }); - test('retain more than length of text', () { - final a = Delta()..insert('Hello'); - final b = Delta()..retain(10); - final expected = Delta()..insert('Hello'); - expect(a.compose(b), expected); - }); - test('retain start optimization', () { - final a = Delta() - ..insert('A', {'bold': true}) - ..insert('B') - ..insert('C', {'bold': true}) - ..delete(1); - final b = Delta() - ..retain(3) - ..insert('D'); - final expected = Delta() - ..insert('A', {'bold': true}) - ..insert('B') - ..insert('C', {'bold': true}) - ..insert('D') - ..delete(1); - expect(a.compose(b), expected); - }); - test('retain end optimization', () { - final a = Delta() - ..insert('A', {'bold': true}) - ..insert('B') - ..insert('C', {'bold': true}); - final b = Delta()..delete(1); - final expected = Delta() - ..insert('B') - ..insert('C', {'bold': true}); - expect(a.compose(b), expected); - }); - test('retain end optimization join', () { - final a = Delta() - ..insert('A', {'bold': true}) - ..insert('B') - ..insert('C', {'bold': true}) - ..insert('D') - ..insert('E', {'bold': true}) - ..insert('F'); - final b = Delta() - ..retain(1) - ..delete(1); - final expected = Delta() - ..insert('AC', {'bold': true}) - ..insert('D') - ..insert('E', {'bold': true}) - ..insert('F'); - expect(a.compose(b), expected); - }); - }); - group('invert', () { - test('insert', () { - final delta = Delta() - ..retain(2) - ..insert('A'); - final base = Delta()..insert('12346'); - final expected = Delta() - ..retain(2) - ..delete(1); - final inverted = delta.invert(base); - expect(expected, inverted); - expect(base.compose(delta).compose(inverted), base); - }); - test('delete', () { - final delta = Delta() - ..retain(2) - ..delete(3); - final base = Delta()..insert('123456'); - final expected = Delta() - ..retain(2) - ..insert('345'); - final inverted = delta.invert(base); - expect(expected, inverted); - expect(base.compose(delta).compose(inverted), base); - }); - test('retain', () { - final delta = Delta() - ..retain(2) - ..retain(3, {'bold': true}); - final base = Delta()..insert('123456'); - final expected = Delta() - ..retain(2) - ..retain(3, {'bold': null}); - final inverted = delta.invert(base); - expect(expected, inverted); - final t = base.compose(delta).compose(inverted); - expect(t, base); - }); - }); - group('json', () { - test('toJson()', () { - final delta = Delta() - ..retain(2) - ..insert('A') - ..delete(3); - expect(delta.toJson(), [ - {'retain': 2}, - {'insert': 'A'}, - {'delete': 3} - ]); - }); - test('attributes', () { - final delta = Delta() - ..retain(2, {'bold': true}) - ..insert('A', {'italic': true}); - expect(delta.toJson(), [ - { - 'retain': 2, - 'attributes': {'bold': true}, - }, - { - 'insert': 'A', - 'attributes': {'italic': true}, - }, - ]); - }); - test('fromJson()', () { - final delta = Delta.fromJson([ - {'retain': 2}, - {'insert': 'A'}, - {'delete': 3}, - ]); - final expected = Delta() - ..retain(2) - ..insert('A') - ..delete(3); - expect(delta, expected); - }); - }); - group('runes', () { - test("stringIndexes", () { - final indexes = stringIndexes('😊'); - expect(indexes[0], 0); - expect(indexes[1], 0); - }); - test("next rune 1", () { - final delta = Delta()..insert('😊'); - expect(delta.nextRunePosition(0), 2); - }); - test("next rune 2", () { - final delta = Delta()..insert('😊a'); - expect(delta.nextRunePosition(0), 2); - }); - test("next rune 3", () { - final delta = Delta()..insert('😊陈'); - expect(delta.nextRunePosition(2), 3); - }); - test("prev rune 1", () { - final delta = Delta()..insert('😊陈'); - expect(delta.prevRunePosition(2), 0); - }); - test("prev rune 2", () { - final delta = Delta()..insert('😊'); - expect(delta.prevRunePosition(2), 0); - }); - test("prev rune 3", () { - final delta = Delta()..insert('😊'); - expect(delta.prevRunePosition(0), -1); - }); - }); - group("attributes", () { - test("compose", () { - final attrs = composeAttributes({"a": null}, {"b": null}, true); - expect(attrs != null, true); - expect(attrs!.containsKey("a"), true); - expect(attrs.containsKey("b"), true); - expect(attrs["a"], null); - expect(attrs["b"], null); - }); - }); -} diff --git a/frontend/app_flowy/packages/appflowy_editor/test/legacy/flowy_editor_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/legacy/flowy_editor_test.dart index df586b3ac0..29c38650f3 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/legacy/flowy_editor_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/legacy/flowy_editor_test.dart @@ -1,6 +1,6 @@ -import 'package:appflowy_editor/src/document/path.dart'; -import 'package:appflowy_editor/src/document/position.dart'; -import 'package:appflowy_editor/src/document/selection.dart'; +import 'package:appflowy_editor/src/core/document/path.dart'; +import 'package:appflowy_editor/src/core/location/position.dart'; +import 'package:appflowy_editor/src/core/location/selection.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { @@ -9,16 +9,16 @@ void main() { test('create state tree', () async { // final String response = await rootBundle.loadString('assets/document.json'); // final data = Map.from(json.decode(response)); - // final stateTree = StateTree.fromJson(data); - // expect(stateTree.root.type, 'root'); - // expect(stateTree.root.toJson(), data['document']); + // final document = Document.fromJson(data); + // expect(document.root.type, 'root'); + // expect(document.root.toJson(), data['document']); }); test('search node by Path in state tree', () async { // final String response = await rootBundle.loadString('assets/document.json'); // final data = Map.from(json.decode(response)); - // final stateTree = StateTree.fromJson(data); - // final checkBoxNode = stateTree.root.childAtPath([1, 0]); + // final document = Document.fromJson(data); + // final checkBoxNode = document.root.childAtPath([1, 0]); // expect(checkBoxNode != null, true); // final textType = checkBoxNode!.attributes['text-type']; // expect(textType != null, true); @@ -27,8 +27,8 @@ void main() { test('search node by Self in state tree', () async { // final String response = await rootBundle.loadString('assets/document.json'); // final data = Map.from(json.decode(response)); - // final stateTree = StateTree.fromJson(data); - // final checkBoxNode = stateTree.root.childAtPath([1, 0]); + // final document = Document.fromJson(data); + // final checkBoxNode = document.root.childAtPath([1, 0]); // expect(checkBoxNode != null, true); // final textType = checkBoxNode!.attributes['text-type']; // expect(textType != null, true); @@ -39,21 +39,21 @@ void main() { test('insert node in state tree', () async { // final String response = await rootBundle.loadString('assets/document.json'); // final data = Map.from(json.decode(response)); - // final stateTree = StateTree.fromJson(data); + // final document = Document.fromJson(data); // final insertNode = Node.fromJson({ // 'type': 'text', // }); - // bool result = stateTree.insert([1, 1], [insertNode]); + // bool result = document.insert([1, 1], [insertNode]); // expect(result, true); - // expect(identical(insertNode, stateTree.nodeAtPath([1, 1])), true); + // expect(identical(insertNode, document.nodeAtPath([1, 1])), true); }); test('delete node in state tree', () async { // final String response = await rootBundle.loadString('assets/document.json'); // final data = Map.from(json.decode(response)); - // final stateTree = StateTree.fromJson(data); - // stateTree.delete([1, 1], 1); - // final node = stateTree.nodeAtPath([1, 1]); + // final document = Document.fromJson(data); + // document.delete([1, 1], 1); + // final node = document.nodeAtPath([1, 1]); // expect(node != null, true); // expect(node!.attributes['tag'], '**'); }); @@ -61,10 +61,10 @@ void main() { test('update node in state tree', () async { // final String response = await rootBundle.loadString('assets/document.json'); // final data = Map.from(json.decode(response)); - // final stateTree = StateTree.fromJson(data); - // final test = stateTree.update([1, 1], {'text-type': 'heading1'}); + // final document = Document.fromJson(data); + // final test = document.update([1, 1], {'text-type': 'heading1'}); // expect(test, true); - // final updatedNode = stateTree.nodeAtPath([1, 1]); + // final updatedNode = document.nodeAtPath([1, 1]); // expect(updatedNode != null, true); // expect(updatedNode!.attributes['text-type'], 'heading1'); }); @@ -72,7 +72,7 @@ void main() { test('test path utils 1', () { final path1 = [1]; final path2 = [1]; - expect(pathEquals(path1, path2), true); + expect(path1.equals(path2), true); expect(Object.hashAll(path1), Object.hashAll(path2)); }); @@ -80,7 +80,7 @@ void main() { test('test path utils 2', () { final path1 = [1]; final path2 = [2]; - expect(pathEquals(path1, path2), false); + expect(path1.equals(path2), false); expect(Object.hashAll(path1) != Object.hashAll(path2), true); }); 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 6c20ebd134..366ab2fa0f 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 @@ -1,11 +1,10 @@ import 'dart:collection'; -import 'package:appflowy_editor/src/document/node.dart'; +import 'package:appflowy_editor/src/core/document/node.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:appflowy_editor/src/operation/operation.dart'; -import 'package:appflowy_editor/src/operation/transaction_builder.dart'; +import 'package:appflowy_editor/src/core/transform/operation.dart'; import 'package:appflowy_editor/src/editor_state.dart'; -import 'package:appflowy_editor/src/document/state_tree.dart'; +import 'package:appflowy_editor/src/core/document/document.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -48,25 +47,26 @@ void main() { final item2 = Node(type: "node", attributes: {}, children: LinkedList()); final item3 = Node(type: "node", attributes: {}, children: LinkedList()); final root = Node( - type: "root", - attributes: {}, - children: LinkedList() - ..addAll([ - item1, - item2, - item3, - ])); - final state = EditorState(document: StateTree(root: root)); + type: "root", + attributes: {}, + children: LinkedList() + ..addAll([ + item1, + item2, + item3, + ]), + ); + final state = EditorState(document: Document(root: root)); expect(item1.path, [0]); expect(item2.path, [1]); expect(item3.path, [2]); - final tb = TransactionBuilder(state); - tb.deleteNode(item1); - tb.deleteNode(item2); - tb.deleteNode(item3); - final transaction = tb.finish(); + final transaction = state.transaction; + transaction.deleteNode(item1); + transaction.deleteNode(item2); + transaction.deleteNode(item3); + state.commit(); expect(transaction.operations[0].path, [0]); expect(transaction.operations[1].path, [0]); expect(transaction.operations[2].path, [0]); @@ -74,13 +74,12 @@ void main() { group("toJson", () { test("insert", () { final root = Node(type: "root", attributes: {}, children: LinkedList()); - final state = EditorState(document: StateTree(root: root)); + final state = EditorState(document: Document(root: root)); final item1 = Node(type: "node", attributes: {}, children: LinkedList()); - final tb = TransactionBuilder(state); - tb.insertNode([0], item1); - - final transaction = tb.finish(); + final transaction = state.transaction; + transaction.insertNode([0], item1); + state.commit(); expect(transaction.toJson(), { "operations": [ { @@ -94,16 +93,17 @@ void main() { test("delete", () { final item1 = Node(type: "node", attributes: {}, children: LinkedList()); final root = Node( - type: "root", - attributes: {}, - children: LinkedList() - ..addAll([ - item1, - ])); - final state = EditorState(document: StateTree(root: root)); - final tb = TransactionBuilder(state); - tb.deleteNode(item1); - final transaction = tb.finish(); + type: "root", + attributes: {}, + children: LinkedList() + ..addAll([ + item1, + ]), + ); + final state = EditorState(document: Document(root: root)); + final transaction = state.transaction; + transaction.deleteNode(item1); + state.commit(); 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 f77e404273..bd173ce15f 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 @@ -17,16 +17,16 @@ void main() async { } test("HistoryItem #1", () { - final document = StateTree(root: _createEmptyEditorRoot()); + final document = Document(root: _createEmptyEditorRoot()); final editorState = EditorState(document: document); final historyItem = HistoryItem(); - historyItem.add(DeleteOperation( - [0], [TextNode(type: 'text', delta: Delta()..insert('0'))])); - historyItem.add(DeleteOperation( - [0], [TextNode(type: 'text', delta: Delta()..insert('1'))])); - historyItem.add(DeleteOperation( - [0], [TextNode(type: 'text', delta: Delta()..insert('2'))])); + historyItem + .add(DeleteOperation([0], [TextNode(delta: Delta()..insert('0'))])); + historyItem + .add(DeleteOperation([0], [TextNode(delta: Delta()..insert('1'))])); + historyItem + .add(DeleteOperation([0], [TextNode(delta: Delta()..insert('2'))])); final transaction = historyItem.toTransaction(editorState); assert(isInsertAndPathEqual(transaction.operations[0], [0], '2')); @@ -35,12 +35,12 @@ void main() async { }); test("HistoryItem #2", () { - final document = StateTree(root: _createEmptyEditorRoot()); + final document = Document(root: _createEmptyEditorRoot()); final editorState = EditorState(document: document); final historyItem = HistoryItem(); - historyItem.add(DeleteOperation( - [0], [TextNode(type: 'text', delta: Delta()..insert('0'))])); + historyItem + .add(DeleteOperation([0], [TextNode(delta: Delta()..insert('0'))])); historyItem .add(UpdateOperation([0], {"subType": "number"}, {"subType": null})); historyItem.add(DeleteOperation([0], [TextNode.empty(), TextNode.empty()])); @@ -59,11 +59,11 @@ bool isInsertAndPathEqual(Operation operation, Path path, [String? content]) { return false; } - if (!pathEquals(operation.path, path)) { + if (!operation.path.equals(path)) { return false; } - final firstNode = operation.nodes[0]; + final firstNode = operation.nodes.first; if (firstNode is! TextNode) { return false; } @@ -72,5 +72,5 @@ bool isInsertAndPathEqual(Operation operation, Path path, [String? content]) { return true; } - return firstNode.delta.toRawString() == content; + return firstNode.delta.toPlainText() == content; } diff --git a/frontend/app_flowy/packages/appflowy_editor/test/render/rich_text/checkbox_text_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/render/rich_text/checkbox_text_test.dart index 29dfbba136..b13fa456c9 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/render/rich_text/checkbox_text_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/render/rich_text/checkbox_text_test.dart @@ -26,8 +26,8 @@ void main() async { BuiltInAttributeKey.subtype: BuiltInAttributeKey.checkbox, BuiltInAttributeKey.checkbox: false, }, - delta: Delta([ - TextInsert(text, { + delta: Delta(operations: [ + TextInsert(text, attributes: { BuiltInAttributeKey.bold: true, BuiltInAttributeKey.italic: true, BuiltInAttributeKey.underline: true, diff --git a/frontend/app_flowy/packages/appflowy_editor/test/render/selection_menu/selection_menu_widget_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/render/selection_menu/selection_menu_widget_test.dart index 9f9046ae52..f60d1610ad 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/render/selection_menu/selection_menu_widget_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/render/selection_menu/selection_menu_widget_test.dart @@ -147,7 +147,7 @@ Future _testDefaultSelectionMenuItems( int index, EditorWidgetTester editor) async { expect(editor.documentLength, 4); expect(editor.documentSelection, Selection.single(path: [2], startOffset: 0)); - expect((editor.nodeAtPath([1]) as TextNode).toRawString(), + expect((editor.nodeAtPath([1]) as TextNode).toPlainText(), 'Welcome to Appflowy 😁'); final node = editor.nodeAtPath([2]); final item = defaultSelectionMenuItems[index]; diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/backspace_handler_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/backspace_handler_test.dart index 1e423c39e0..e786377de0 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/backspace_handler_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/backspace_handler_test.dart @@ -117,7 +117,7 @@ void main() async { expect(editor.documentLength, 1); expect(editor.documentSelection, Selection.single(path: [0], startOffset: text.length)); - expect((editor.nodeAtPath([0]) as TextNode).toRawString(), text * 2); + expect((editor.nodeAtPath([0]) as TextNode).toPlainText(), text * 2); }); // Before @@ -275,7 +275,6 @@ void main() async { // * Welcome to Appflowy 😁 const text = 'Welcome to Appflowy 😁'; final node = TextNode( - type: 'text', delta: Delta()..insert(text), attributes: { BuiltInAttributeKey.subtype: BuiltInAttributeKey.bulletedList, @@ -320,7 +319,7 @@ void main() async { editor.documentSelection, Selection.single(path: [0, 0], startOffset: text.length), ); - expect((editor.nodeAtPath([0, 0]) as TextNode).toRawString(), text * 2); + expect((editor.nodeAtPath([0, 0]) as TextNode).toPlainText(), text * 2); }); testWidgets('Delete the complicated nested bulleted list', (tester) async { @@ -331,7 +330,6 @@ void main() async { // * Welcome to Appflowy 😁 const text = 'Welcome to Appflowy 😁'; final node = TextNode( - type: 'text', delta: Delta()..insert(text), attributes: { BuiltInAttributeKey.subtype: BuiltInAttributeKey.bulletedList, @@ -390,7 +388,7 @@ void main() async { true, ); expect( - (editor.nodeAtPath([0, 0]) as TextNode).toRawString() == text * 2, + (editor.nodeAtPath([0, 0]) as TextNode).toPlainText() == text * 2, true, ); expect( @@ -496,7 +494,7 @@ Future _deleteStyledTextByBackspace( expect(editor.documentSelection, Selection.single(path: [1], startOffset: text.length)); expect(editor.nodeAtPath([1])?.subtype, style); - expect((editor.nodeAtPath([1]) as TextNode).toRawString(), text * 2); + expect((editor.nodeAtPath([1]) as TextNode).toPlainText(), text * 2); await editor.updateSelection( Selection.single(path: [1], startOffset: 0), @@ -538,7 +536,7 @@ Future _deleteStyledTextByDelete( expect( editor.documentSelection, Selection.single(path: [1], startOffset: 0)); expect(editor.nodeAtPath([1])?.subtype, style); - expect((editor.nodeAtPath([1]) as TextNode).toRawString(), + expect((editor.nodeAtPath([1]) as TextNode).toPlainText(), text.safeSubString(i)); } @@ -548,7 +546,7 @@ Future _deleteStyledTextByDelete( expect(editor.documentLength, 2); expect(editor.documentSelection, Selection.single(path: [1], startOffset: 0)); expect(editor.nodeAtPath([1])?.subtype, style); - expect((editor.nodeAtPath([1]) as TextNode).toRawString(), text); + expect((editor.nodeAtPath([1]) as TextNode).toPlainText(), text); } Future _deleteTextByBackspace( @@ -568,7 +566,7 @@ Future _deleteTextByBackspace( expect(editor.documentLength, 3); expect(editor.documentSelection, Selection.single(path: [1], startOffset: 9)); - expect((editor.nodeAtPath([1]) as TextNode).toRawString(), + expect((editor.nodeAtPath([1]) as TextNode).toPlainText(), 'Welcome t Appflowy 😁'); // delete 'to ' @@ -578,7 +576,7 @@ Future _deleteTextByBackspace( await editor.pressLogicKey(LogicalKeyboardKey.backspace); expect(editor.documentLength, 3); expect(editor.documentSelection, Selection.single(path: [2], startOffset: 8)); - expect((editor.nodeAtPath([2]) as TextNode).toRawString(), + expect((editor.nodeAtPath([2]) as TextNode).toPlainText(), 'Welcome Appflowy 😁'); // delete 'Appflowy 😁 @@ -593,7 +591,7 @@ Future _deleteTextByBackspace( expect(editor.documentLength, 1); expect( editor.documentSelection, Selection.single(path: [0], startOffset: 11)); - expect((editor.nodeAtPath([0]) as TextNode).toRawString(), + expect((editor.nodeAtPath([0]) as TextNode).toPlainText(), 'Welcome to Appflowy 😁'); } @@ -614,7 +612,7 @@ Future _deleteTextByDelete( expect(editor.documentLength, 3); expect(editor.documentSelection, Selection.single(path: [1], startOffset: 9)); - expect((editor.nodeAtPath([1]) as TextNode).toRawString(), + expect((editor.nodeAtPath([1]) as TextNode).toPlainText(), 'Welcome t Appflowy 😁'); // delete 'to ' @@ -624,7 +622,7 @@ Future _deleteTextByDelete( await editor.pressLogicKey(LogicalKeyboardKey.delete); expect(editor.documentLength, 3); expect(editor.documentSelection, Selection.single(path: [2], startOffset: 8)); - expect((editor.nodeAtPath([2]) as TextNode).toRawString(), + expect((editor.nodeAtPath([2]) as TextNode).toPlainText(), 'Welcome Appflowy 😁'); // delete 'Appflowy 😁 @@ -639,6 +637,6 @@ Future _deleteTextByDelete( expect(editor.documentLength, 1); expect( editor.documentSelection, Selection.single(path: [0], startOffset: 11)); - expect((editor.nodeAtPath([0]) as TextNode).toRawString(), + expect((editor.nodeAtPath([0]) as TextNode).toPlainText(), 'Welcome to Appflowy 😁'); } diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler_test.dart index 916541025d..74797b8402 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler_test.dart @@ -74,10 +74,10 @@ void main() async { expect(lastNode != null, true); expect(lastNode is TextNode, true); lastNode = lastNode as TextNode; - expect(lastNode.delta.toRawString(), text); - expect((lastNode.previous as TextNode).delta.toRawString(), ''); + expect(lastNode.delta.toPlainText(), text); + expect((lastNode.previous as TextNode).delta.toPlainText(), ''); expect( - (lastNode.previous!.previous as TextNode).delta.toRawString(), text); + (lastNode.previous!.previous as TextNode).delta.toPlainText(), text); }); // Before @@ -134,7 +134,7 @@ void main() async { ); await editor.pressLogicKey(LogicalKeyboardKey.enter); expect(editor.documentLength, 2); - expect((editor.nodeAtPath([1]) as TextNode).toRawString(), text); + expect((editor.nodeAtPath([1]) as TextNode).toPlainText(), text); }); }); } @@ -227,6 +227,6 @@ Future _testMultipleSelection( ); expect(editor.documentLength, 2); - expect((editor.nodeAtPath([0]) as TextNode).toRawString(), 'Welcome'); - expect((editor.nodeAtPath([1]) as TextNode).toRawString(), 'to Appflowy 😁'); + expect((editor.nodeAtPath([0]) as TextNode).toPlainText(), 'Welcome'); + expect((editor.nodeAtPath([1]) as TextNode).toPlainText(), 'to Appflowy 😁'); } diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/markdown_syntax_to_styled_text_handler_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/markdown_syntax_to_styled_text_handler_test.dart index d60239ae49..f39d58e6a3 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/markdown_syntax_to_styled_text_handler_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/markdown_syntax_to_styled_text_handler_test.dart @@ -39,11 +39,11 @@ void main() async { Selection.single( path: [0], startOffset: 0, - endOffset: textNode.toRawString().length, + endOffset: textNode.toPlainText().length, ), ); expect(allBold, true); - expect(textNode.toRawString(), 'AppFlowy'); + expect(textNode.toPlainText(), 'AppFlowy'); }); testWidgets('App**Flowy** to bold AppFlowy', (tester) async { @@ -62,11 +62,11 @@ void main() async { Selection.single( path: [0], startOffset: 3, - endOffset: textNode.toRawString().length, + endOffset: textNode.toPlainText().length, ), ); expect(allBold, true); - expect(textNode.toRawString(), 'AppFlowy'); + expect(textNode.toPlainText(), 'AppFlowy'); }); testWidgets('***AppFlowy** to bold *AppFlowy', (tester) async { @@ -85,11 +85,11 @@ void main() async { Selection.single( path: [0], startOffset: 1, - endOffset: textNode.toRawString().length, + endOffset: textNode.toPlainText().length, ), ); expect(allBold, true); - expect(textNode.toRawString(), '*AppFlowy'); + expect(textNode.toPlainText(), '*AppFlowy'); }); testWidgets('**AppFlowy** application to bold AppFlowy only', @@ -115,7 +115,7 @@ void main() async { ), ); expect(appFlowyBold, true); - expect(textNode.toRawString(), 'AppFlowy'); + expect(textNode.toPlainText(), 'AppFlowy'); }); testWidgets('**** nothing changes', (tester) async { @@ -134,11 +134,11 @@ void main() async { Selection.single( path: [0], startOffset: 0, - endOffset: textNode.toRawString().length, + endOffset: textNode.toPlainText().length, ), ); expect(allBold, false); - expect(textNode.toRawString(), text); + expect(textNode.toPlainText(), text); }); }); @@ -171,11 +171,11 @@ void main() async { Selection.single( path: [0], startOffset: 0, - endOffset: textNode.toRawString().length, + endOffset: textNode.toPlainText().length, ), ); expect(allBold, true); - expect(textNode.toRawString(), 'AppFlowy'); + expect(textNode.toPlainText(), 'AppFlowy'); }); testWidgets('App__Flowy__ to bold AppFlowy', (tester) async { @@ -194,11 +194,11 @@ void main() async { Selection.single( path: [0], startOffset: 3, - endOffset: textNode.toRawString().length, + endOffset: textNode.toPlainText().length, ), ); expect(allBold, true); - expect(textNode.toRawString(), 'AppFlowy'); + expect(textNode.toPlainText(), 'AppFlowy'); }); testWidgets('___AppFlowy__ to bold _AppFlowy', (tester) async { @@ -217,11 +217,11 @@ void main() async { Selection.single( path: [0], startOffset: 1, - endOffset: textNode.toRawString().length, + endOffset: textNode.toPlainText().length, ), ); expect(allBold, true); - expect(textNode.toRawString(), '_AppFlowy'); + expect(textNode.toPlainText(), '_AppFlowy'); }); testWidgets('__AppFlowy__ application to bold AppFlowy only', @@ -247,7 +247,7 @@ void main() async { ), ); expect(appFlowyBold, true); - expect(textNode.toRawString(), 'AppFlowy'); + expect(textNode.toPlainText(), 'AppFlowy'); }); testWidgets('____ nothing changes', (tester) async { @@ -266,11 +266,11 @@ void main() async { Selection.single( path: [0], startOffset: 0, - endOffset: textNode.toRawString().length, + endOffset: textNode.toPlainText().length, ), ); expect(allBold, false); - expect(textNode.toRawString(), text); + expect(textNode.toPlainText(), text); }); }); }); diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/markdown_syntax_to_styled_text_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/markdown_syntax_to_styled_text_test.dart index d0ca407ea1..662c7982b4 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/markdown_syntax_to_styled_text_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/markdown_syntax_to_styled_text_test.dart @@ -38,11 +38,11 @@ void main() async { Selection.single( path: [0], startOffset: 0, - endOffset: textNode.toRawString().length, + endOffset: textNode.toPlainText().length, ), ); expect(allCode, true); - expect(textNode.toRawString(), 'AppFlowy'); + expect(textNode.toPlainText(), 'AppFlowy'); }); testWidgets('App`Flowy` to code AppFlowy', (tester) async { @@ -61,11 +61,11 @@ void main() async { Selection.single( path: [0], startOffset: 3, - endOffset: textNode.toRawString().length, + endOffset: textNode.toPlainText().length, ), ); expect(allCode, true); - expect(textNode.toRawString(), 'AppFlowy'); + expect(textNode.toPlainText(), 'AppFlowy'); }); testWidgets('`` nothing changes', (tester) async { @@ -84,11 +84,11 @@ void main() async { Selection.single( path: [0], startOffset: 0, - endOffset: textNode.toRawString().length, + endOffset: textNode.toPlainText().length, ), ); expect(allCode, false); - expect(textNode.toRawString(), text); + expect(textNode.toPlainText(), text); }); }); @@ -120,11 +120,11 @@ void main() async { Selection.single( path: [0], startOffset: 1, - endOffset: textNode.toRawString().length, + endOffset: textNode.toPlainText().length, ), ); expect(allCode, true); - expect(textNode.toRawString(), '`AppFlowy'); + expect(textNode.toPlainText(), '`AppFlowy'); }); testWidgets('```` nothing changes', (tester) async { @@ -143,11 +143,11 @@ void main() async { Selection.single( path: [0], startOffset: 0, - endOffset: textNode.toRawString().length, + endOffset: textNode.toPlainText().length, ), ); expect(allCode, false); - expect(textNode.toRawString(), text); + expect(textNode.toPlainText(), text); }); }); @@ -180,11 +180,11 @@ void main() async { Selection.single( path: [0], startOffset: 0, - endOffset: textNode.toRawString().length, + endOffset: textNode.toPlainText().length, ), ); expect(allStrikethrough, true); - expect(textNode.toRawString(), 'AppFlowy'); + expect(textNode.toPlainText(), 'AppFlowy'); }); testWidgets('App~~Flowy~~ to strikethrough AppFlowy', (tester) async { @@ -203,11 +203,11 @@ void main() async { Selection.single( path: [0], startOffset: 3, - endOffset: textNode.toRawString().length, + endOffset: textNode.toPlainText().length, ), ); expect(allStrikethrough, true); - expect(textNode.toRawString(), 'AppFlowy'); + expect(textNode.toPlainText(), 'AppFlowy'); }); testWidgets('~~~AppFlowy~~ to bold ~AppFlowy', (tester) async { @@ -226,11 +226,11 @@ void main() async { Selection.single( path: [0], startOffset: 1, - endOffset: textNode.toRawString().length, + endOffset: textNode.toPlainText().length, ), ); expect(allStrikethrough, true); - expect(textNode.toRawString(), '~AppFlowy'); + expect(textNode.toPlainText(), '~AppFlowy'); }); testWidgets('~~~~ nothing changes', (tester) async { @@ -249,11 +249,11 @@ void main() async { Selection.single( path: [0], startOffset: 0, - endOffset: textNode.toRawString().length, + endOffset: textNode.toPlainText().length, ), ); expect(allStrikethrough, false); - expect(textNode.toRawString(), text); + expect(textNode.toPlainText(), text); }); }); }); diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/redo_undo_handler_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/redo_undo_handler_test.dart index b165f279e7..5e4260a4a1 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/redo_undo_handler_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/redo_undo_handler_test.dart @@ -56,7 +56,7 @@ Future _testBackspaceUndoRedo( } expect(editor.documentLength, 3); - expect((editor.nodeAtPath([1]) as TextNode).toRawString(), text); + expect((editor.nodeAtPath([1]) as TextNode).toPlainText(), text); expect(editor.documentSelection, selection); if (Platform.isWindows || Platform.isLinux) { diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/space_on_web_handler_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/space_on_web_handler_test.dart index fbbe016d30..a49d882fa3 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/space_on_web_handler_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/space_on_web_handler_test.dart @@ -26,7 +26,7 @@ void main() async { ); await editor.pressLogicKey(LogicalKeyboardKey.space); expect( - (editor.nodeAtPath([i]) as TextNode).toRawString(), + (editor.nodeAtPath([i]) as TextNode).toPlainText(), 'W elcome to Appflowy 😁', ); } @@ -36,7 +36,7 @@ void main() async { ); await editor.pressLogicKey(LogicalKeyboardKey.space); expect( - (editor.nodeAtPath([i]) as TextNode).toRawString(), + (editor.nodeAtPath([i]) as TextNode).toPlainText(), 'W elcome to Appflowy 😁 ', ); } diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/white_space_handler_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/white_space_handler_test.dart index c0948a93c5..d06a865b64 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/white_space_handler_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/white_space_handler_test.dart @@ -87,7 +87,7 @@ void main() async { expect(textNode.subtype, BuiltInAttributeKey.heading); // BuiltInAttributeKey.h1 ~ BuiltInAttributeKey.h6 expect(textNode.attributes.heading, 'h$i'); - expect(textNode.toRawString().startsWith('##'), true); + expect(textNode.toPlainText().startsWith('##'), true); } }); @@ -211,7 +211,7 @@ void main() async { await editor.pressLogicKey(LogicalKeyboardKey.space); expect(textNode.subtype, BuiltInAttributeKey.checkbox); expect(textNode.attributes.check, true); - expect(textNode.toRawString(), insertedText); + expect(textNode.toPlainText(), insertedText); }); }); } diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/toolbar_service_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/toolbar_service_test.dart index 1e8f2f1f07..2388507003 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/service/toolbar_service_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/service/toolbar_service_test.dart @@ -63,9 +63,9 @@ void main() async { ..insertTextNode(text) ..insertTextNode( null, - delta: Delta([ + delta: Delta(operations: [ TextInsert(text), - TextInsert(text, attributes), + TextInsert(text, attributes: attributes), TextInsert(text), ]), ); @@ -171,8 +171,8 @@ void main() async { BuiltInAttributeKey.subtype: BuiltInAttributeKey.heading, BuiltInAttributeKey.heading: BuiltInAttributeKey.h1, }, - delta: Delta([ - TextInsert(text, { + delta: Delta(operations: [ + TextInsert(text, attributes: { BuiltInAttributeKey.bold: true, }) ]),