Browse Source

Integrate appflowy editor (#1040)

Lucas.Xu 2 years ago
parent
commit
ad9a4b7d71
100 changed files with 3163 additions and 451 deletions
  1. 93 0
      frontend/app_flowy/assets/google_fonts/Poppins/OFL.txt
  2. BIN
      frontend/app_flowy/assets/google_fonts/Poppins/Poppins-Black.ttf
  3. BIN
      frontend/app_flowy/assets/google_fonts/Poppins/Poppins-BlackItalic.ttf
  4. BIN
      frontend/app_flowy/assets/google_fonts/Poppins/Poppins-Bold.ttf
  5. BIN
      frontend/app_flowy/assets/google_fonts/Poppins/Poppins-BoldItalic.ttf
  6. BIN
      frontend/app_flowy/assets/google_fonts/Poppins/Poppins-ExtraBold.ttf
  7. BIN
      frontend/app_flowy/assets/google_fonts/Poppins/Poppins-ExtraBoldItalic.ttf
  8. BIN
      frontend/app_flowy/assets/google_fonts/Poppins/Poppins-ExtraLight.ttf
  9. BIN
      frontend/app_flowy/assets/google_fonts/Poppins/Poppins-ExtraLightItalic.ttf
  10. BIN
      frontend/app_flowy/assets/google_fonts/Poppins/Poppins-Italic.ttf
  11. BIN
      frontend/app_flowy/assets/google_fonts/Poppins/Poppins-Light.ttf
  12. BIN
      frontend/app_flowy/assets/google_fonts/Poppins/Poppins-LightItalic.ttf
  13. BIN
      frontend/app_flowy/assets/google_fonts/Poppins/Poppins-Medium.ttf
  14. BIN
      frontend/app_flowy/assets/google_fonts/Poppins/Poppins-MediumItalic.ttf
  15. BIN
      frontend/app_flowy/assets/google_fonts/Poppins/Poppins-Regular.ttf
  16. BIN
      frontend/app_flowy/assets/google_fonts/Poppins/Poppins-SemiBold.ttf
  17. BIN
      frontend/app_flowy/assets/google_fonts/Poppins/Poppins-SemiBoldItalic.ttf
  18. BIN
      frontend/app_flowy/assets/google_fonts/Poppins/Poppins-Thin.ttf
  19. BIN
      frontend/app_flowy/assets/google_fonts/Poppins/Poppins-ThinItalic.ttf
  20. 1 1
      frontend/app_flowy/lib/plugins/board/board.dart
  21. 78 42
      frontend/app_flowy/lib/plugins/doc/application/doc_bloc.dart
  22. 13 4
      frontend/app_flowy/lib/plugins/doc/application/doc_service.dart
  23. 10 6
      frontend/app_flowy/lib/plugins/doc/application/share_bloc.dart
  24. 13 11
      frontend/app_flowy/lib/plugins/doc/application/share_service.dart
  25. 1 1
      frontend/app_flowy/lib/plugins/doc/document.dart
  26. 23 54
      frontend/app_flowy/lib/plugins/doc/document_page.dart
  27. 61 0
      frontend/app_flowy/lib/plugins/doc/editor_styles.dart
  28. 166 0
      frontend/app_flowy/lib/plugins/doc/presentation/plugins/horizontal_rule_node_widget.dart
  29. 1 1
      frontend/app_flowy/lib/plugins/grid/grid.dart
  30. 1 1
      frontend/app_flowy/lib/startup/plugin/plugin.dart
  31. 4 2
      frontend/app_flowy/lib/startup/tasks/app_widget.dart
  32. 1 1
      frontend/app_flowy/lib/workspace/application/app/app_bloc.dart
  33. 2 2
      frontend/app_flowy/lib/workspace/application/app/app_service.dart
  34. 29 0
      frontend/app_flowy/lib/workspace/application/markdown/document_markdown.dart
  35. 14 0
      frontend/app_flowy/lib/workspace/application/markdown/src/parser/image_node_parser.dart
  36. 39 0
      frontend/app_flowy/lib/workspace/application/markdown/src/parser/markdown_encoder.dart
  37. 8 0
      frontend/app_flowy/lib/workspace/application/markdown/src/parser/node_parser.dart
  38. 68 0
      frontend/app_flowy/lib/workspace/application/markdown/src/parser/text_node_parser.dart
  39. 1 1
      frontend/app_flowy/lib/workspace/application/view/view_bloc.dart
  40. 4 4
      frontend/app_flowy/lib/workspace/application/view/view_service.dart
  41. 4 8
      frontend/app_flowy/packages/appflowy_editor/example/assets/example.json
  42. 0 2
      frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart
  43. 0 53
      frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/underscore_to_italic.dart
  44. 2 2
      frontend/app_flowy/packages/appflowy_editor/lib/src/core/document/document.dart
  45. 1 1
      frontend/app_flowy/packages/appflowy_editor/lib/src/core/transform/transaction.dart
  46. 904 0
      frontend/app_flowy/packages/appflowy_editor/lib/src/flutter/overlay.dart
  47. 1 3
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_upload_widget.dart
  48. 10 7
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart
  49. 8 2
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_service.dart
  50. 5 2
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart
  51. 1 1
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item_widget.dart
  52. 3 1
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_widget.dart
  53. 2 1
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/editor_service.dart
  54. 3 1
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart
  55. 45 0
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text.dart
  56. 2 1
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/whitespace_handler.dart
  57. 7 2
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/scroll_service.dart
  58. 8 2
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/selection_service.dart
  59. 5 0
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart
  60. 2 1
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart
  61. 31 0
      frontend/app_flowy/packages/appflowy_editor/test/render/link_menu/link_menu_test.dart
  62. 14 0
      frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/white_space_handler_test.dart
  63. 0 1
      frontend/app_flowy/packages/flowy_sdk/lib/dispatch/dispatch.dart
  64. 7 0
      frontend/app_flowy/pubspec.lock
  65. 21 0
      frontend/app_flowy/pubspec.yaml
  66. 1 1
      frontend/app_flowy/test/bloc_test/grid_test/util.dart
  67. 2 0
      frontend/rust-lib/Cargo.lock
  68. 1 1
      frontend/rust-lib/dart-ffi/src/lib.rs
  69. 2 0
      frontend/rust-lib/flowy-database/migrations/2022-10-22-033122_document/down.sql
  70. 9 0
      frontend/rust-lib/flowy-database/migrations/2022-10-22-033122_document/up.sql
  71. 12 0
      frontend/rust-lib/flowy-database/src/schema.rs
  72. 1 0
      frontend/rust-lib/flowy-document/Cargo.toml
  73. 419 0
      frontend/rust-lib/flowy-document/src/editor/READ_ME.json
  74. 19 6
      frontend/rust-lib/flowy-document/src/editor/document.rs
  75. 54 17
      frontend/rust-lib/flowy-document/src/editor/document_serde.rs
  76. 28 10
      frontend/rust-lib/flowy-document/src/editor/editor.rs
  77. 419 0
      frontend/rust-lib/flowy-document/src/editor/migration/delta_migration.rs
  78. 3 0
      frontend/rust-lib/flowy-document/src/editor/migration/mod.rs
  79. 10 0
      frontend/rust-lib/flowy-document/src/editor/mod.rs
  80. 18 1
      frontend/rust-lib/flowy-document/src/editor/queue.rs
  81. 30 0
      frontend/rust-lib/flowy-document/src/entities.rs
  82. 10 8
      frontend/rust-lib/flowy-document/src/event_handler.rs
  83. 1 1
      frontend/rust-lib/flowy-document/src/event_map.rs
  84. 2 1
      frontend/rust-lib/flowy-document/src/lib.rs
  85. 104 53
      frontend/rust-lib/flowy-document/src/manager.rs
  86. 28 24
      frontend/rust-lib/flowy-document/src/old_editor/editor.rs
  87. 14 14
      frontend/rust-lib/flowy-document/src/old_editor/queue.rs
  88. 22 14
      frontend/rust-lib/flowy-document/src/old_editor/web_socket.rs
  89. 75 0
      frontend/rust-lib/flowy-document/src/services/migration.rs
  90. 4 0
      frontend/rust-lib/flowy-document/src/services/mod.rs
  91. 23 0
      frontend/rust-lib/flowy-document/src/services/persistence.rs
  92. 10 10
      frontend/rust-lib/flowy-document/tests/editor/attribute_test.rs
  93. 11 11
      frontend/rust-lib/flowy-document/tests/editor/mod.rs
  94. 31 31
      frontend/rust-lib/flowy-document/tests/editor/op_test.rs
  95. 9 9
      frontend/rust-lib/flowy-document/tests/editor/serde_test.rs
  96. 24 0
      frontend/rust-lib/flowy-document/tests/new_document/document_compose_test.rs
  97. 1 0
      frontend/rust-lib/flowy-document/tests/new_document/mod.rs
  98. 40 8
      frontend/rust-lib/flowy-document/tests/new_document/script.rs
  99. 7 7
      frontend/rust-lib/flowy-document/tests/new_document/test.rs
  100. 2 2
      frontend/rust-lib/flowy-document/tests/old_document/script.rs

+ 93 - 0
frontend/app_flowy/assets/google_fonts/Poppins/OFL.txt

@@ -0,0 +1,93 @@
+Copyright 2020 The Poppins Project Authors (https://github.com/itfoundry/Poppins)
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded, 
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.

BIN
frontend/app_flowy/assets/google_fonts/Poppins/Poppins-Black.ttf


BIN
frontend/app_flowy/assets/google_fonts/Poppins/Poppins-BlackItalic.ttf


BIN
frontend/app_flowy/assets/google_fonts/Poppins/Poppins-Bold.ttf


BIN
frontend/app_flowy/assets/google_fonts/Poppins/Poppins-BoldItalic.ttf


BIN
frontend/app_flowy/assets/google_fonts/Poppins/Poppins-ExtraBold.ttf


BIN
frontend/app_flowy/assets/google_fonts/Poppins/Poppins-ExtraBoldItalic.ttf


BIN
frontend/app_flowy/assets/google_fonts/Poppins/Poppins-ExtraLight.ttf


BIN
frontend/app_flowy/assets/google_fonts/Poppins/Poppins-ExtraLightItalic.ttf


BIN
frontend/app_flowy/assets/google_fonts/Poppins/Poppins-Italic.ttf


BIN
frontend/app_flowy/assets/google_fonts/Poppins/Poppins-Light.ttf


BIN
frontend/app_flowy/assets/google_fonts/Poppins/Poppins-LightItalic.ttf


BIN
frontend/app_flowy/assets/google_fonts/Poppins/Poppins-Medium.ttf


BIN
frontend/app_flowy/assets/google_fonts/Poppins/Poppins-MediumItalic.ttf


BIN
frontend/app_flowy/assets/google_fonts/Poppins/Poppins-Regular.ttf


BIN
frontend/app_flowy/assets/google_fonts/Poppins/Poppins-SemiBold.ttf


BIN
frontend/app_flowy/assets/google_fonts/Poppins/Poppins-SemiBoldItalic.ttf


BIN
frontend/app_flowy/assets/google_fonts/Poppins/Poppins-Thin.ttf


BIN
frontend/app_flowy/assets/google_fonts/Poppins/Poppins-ThinItalic.ttf


+ 1 - 1
frontend/app_flowy/lib/plugins/board/board.dart

@@ -24,7 +24,7 @@ class BoardPluginBuilder implements PluginBuilder {
   PluginType get pluginType => PluginType.board;
 
   @override
-  ViewDataTypePB get dataType => ViewDataTypePB.Database;
+  ViewDataFormatPB get dataFormatType => ViewDataFormatPB.DatabaseFormat;
 
   @override
   ViewLayoutTypePB? get layoutType => ViewLayoutTypePB.Board;

+ 78 - 42
frontend/app_flowy/lib/plugins/doc/application/doc_bloc.dart

@@ -2,10 +2,11 @@ import 'dart:convert';
 import 'package:app_flowy/plugins/trash/application/trash_service.dart';
 import 'package:app_flowy/workspace/application/view/view_listener.dart';
 import 'package:app_flowy/plugins/doc/application/doc_service.dart';
+import 'package:appflowy_editor/appflowy_editor.dart'
+    show EditorState, Document, Transaction;
 import 'package:flowy_sdk/protobuf/flowy-folder/trash.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
-import 'package:flutter_quill/flutter_quill.dart' show Document, Delta;
 import 'package:flowy_sdk/log.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
@@ -14,15 +15,13 @@ import 'dart:async';
 
 part 'doc_bloc.freezed.dart';
 
-typedef FlutterQuillDocument = Document;
-
 class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
   final ViewPB view;
   final DocumentService service;
 
   final ViewListener listener;
   final TrashService trashService;
-  late FlutterQuillDocument document;
+  late EditorState editorState;
   StreamSubscription? _subscription;
 
   DocumentBloc({
@@ -35,6 +34,7 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
       await event.map(
         initial: (Initial value) async {
           await _initial(value, emit);
+          _listenOnViewChange();
         },
         deleted: (Deleted value) async {
           emit(state.copyWith(isDeleted: true));
@@ -73,6 +73,29 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
   }
 
   Future<void> _initial(Initial value, Emitter<DocumentState> emit) async {
+    final result = await service.openDocument(view: view);
+    result.fold(
+      (block) {
+        final document = Document.fromJson(jsonDecode(block.snapshot));
+        editorState = EditorState(document: document);
+        _listenOnDocumentChange();
+        emit(
+          state.copyWith(
+            loadingState: DocumentLoadingState.finish(left(unit)),
+          ),
+        );
+      },
+      (err) {
+        emit(
+          state.copyWith(
+            loadingState: DocumentLoadingState.finish(right(err)),
+          ),
+        );
+      },
+    );
+  }
+
+  void _listenOnViewChange() {
     listener.start(
       onViewDeleted: (result) {
         result.fold(
@@ -87,46 +110,18 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
         );
       },
     );
-    final result = await service.openDocument(docId: view.id);
-    result.fold(
-      (block) {
-        document = _decodeJsonToDocument(block.snapshot);
-        _subscription = document.changes.listen((event) {
-          final delta = event.item2;
-          final documentDelta = document.toDelta();
-          _composeDelta(delta, documentDelta);
-        });
-        emit(state.copyWith(
-            loadingState: DocumentLoadingState.finish(left(unit))));
-      },
-      (err) {
-        emit(state.copyWith(
-            loadingState: DocumentLoadingState.finish(right(err))));
-      },
-    );
-  }
-
-  // Document _decodeListToDocument(Uint8List data) {
-  //   final json = jsonDecode(utf8.decode(data));
-  //   final document = Document.fromJson(json);
-  //   return document;
-  // }
-
-  void _composeDelta(Delta composedDelta, Delta documentDelta) async {
-    final json = jsonEncode(composedDelta.toJson());
-    Log.debug("doc_id: $view.id - Send json: $json");
-    final result = await service.applyEdit(docId: view.id, operations: json);
-
-    result.fold(
-      (_) {},
-      (r) => Log.error(r),
-    );
   }
 
-  Document _decodeJsonToDocument(String data) {
-    final json = jsonDecode(data);
-    final document = Document.fromJson(json);
-    return document;
+  void _listenOnDocumentChange() {
+    _subscription = editorState.transactionStream.listen((transaction) {
+      final json = jsonEncode(TransactionAdaptor(transaction).toJson());
+      service.applyEdit(docId: view.id, operations: json).then((result) {
+        result.fold(
+          (l) => null,
+          (err) => Log.error(err),
+        );
+      });
+    });
   }
 }
 
@@ -160,3 +155,44 @@ class DocumentLoadingState with _$DocumentLoadingState {
   const factory DocumentLoadingState.finish(
       Either<Unit, FlowyError> successOrFail) = _Finish;
 }
+
+/// Uses to erase the different between appflowy editor and the backend
+class TransactionAdaptor {
+  final Transaction transaction;
+  TransactionAdaptor(this.transaction);
+
+  Map<String, dynamic> toJson() {
+    final json = <String, dynamic>{};
+    if (transaction.operations.isNotEmpty) {
+      // The backend uses [0,0] as the beginning path, but the editor uses [0].
+      // So it needs to extend the path by inserting `0` at the head for all
+      // operations before passing to the backend.
+      json['operations'] = transaction.operations
+          .map((e) => e.copyWith(path: [0, ...e.path]).toJson())
+          .toList();
+    }
+    if (transaction.afterSelection != null) {
+      final selection = transaction.afterSelection!;
+      final start = selection.start;
+      final end = selection.end;
+      json['after_selection'] = selection
+          .copyWith(
+            start: start.copyWith(path: [0, ...start.path]),
+            end: end.copyWith(path: [0, ...end.path]),
+          )
+          .toJson();
+    }
+    if (transaction.beforeSelection != null) {
+      final selection = transaction.beforeSelection!;
+      final start = selection.start;
+      final end = selection.end;
+      json['before_selection'] = selection
+          .copyWith(
+            start: start.copyWith(path: [0, ...start.path]),
+            end: end.copyWith(path: [0, ...end.path]),
+          )
+          .toJson();
+    }
+    return json;
+  }
+}

+ 13 - 4
frontend/app_flowy/lib/plugins/doc/application/doc_service.dart

@@ -3,16 +3,25 @@ import 'package:flowy_sdk/dispatch/dispatch.dart';
 
 import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
-import 'package:flowy_sdk/protobuf/flowy-sync/document.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-document/entities.pb.dart';
 
 class DocumentService {
   Future<Either<DocumentSnapshotPB, FlowyError>> openDocument({
-    required String docId,
+    required ViewPB view,
   }) async {
-    await FolderEventSetLatestView(ViewIdPB(value: docId)).send();
+    await FolderEventSetLatestView(ViewIdPB(value: view.id)).send();
+
+    final payload = OpenDocumentContextPB()
+      ..documentId = view.id
+      ..documentVersion = DocumentVersionPB.V1;
+    // switch (view.dataFormat) {
+    //   case ViewDataFormatPB.DeltaFormat:
+    //     payload.documentVersion = DocumentVersionPB.V0;
+    //     break;
+    //   default:
+    //     break;
+    // }
 
-    final payload = DocumentIdPB(value: docId);
     return DocumentEventGetDocument(payload).send();
   }
 

+ 10 - 6
frontend/app_flowy/lib/plugins/doc/application/share_bloc.dart

@@ -1,14 +1,16 @@
 import 'dart:async';
+import 'dart:convert';
 import 'dart:io';
 import 'package:app_flowy/startup/tasks/rust_sdk.dart';
-import 'package:app_flowy/workspace/application/markdown/delta_markdown.dart';
 import 'package:app_flowy/plugins/doc/application/share_service.dart';
+import 'package:app_flowy/workspace/application/markdown/document_markdown.dart';
 import 'package:flowy_sdk/protobuf/flowy-document/entities.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:dartz/dartz.dart';
+import 'package:appflowy_editor/appflowy_editor.dart' show Document;
 part 'share_bloc.freezed.dart';
 
 class DocShareBloc extends Bloc<DocShareEvent, DocShareState> {
@@ -19,10 +21,10 @@ class DocShareBloc extends Bloc<DocShareEvent, DocShareState> {
     on<DocShareEvent>((event, emit) async {
       await event.map(
         shareMarkdown: (ShareMarkdown value) async {
-          await service.exportMarkdown(view.id).then((result) {
+          await service.exportMarkdown(view).then((result) {
             result.fold(
-              (value) => emit(
-                  DocShareState.finish(left(_convertDeltaToMarkdown(value)))),
+              (value) => emit(DocShareState.finish(
+                  left(_convertDocumentToMarkdown(value)))),
               (error) => emit(DocShareState.finish(right(error))),
             );
           });
@@ -35,8 +37,10 @@ class DocShareBloc extends Bloc<DocShareEvent, DocShareState> {
     });
   }
 
-  ExportDataPB _convertDeltaToMarkdown(ExportDataPB value) {
-    final result = deltaToMarkdown(value.data);
+  ExportDataPB _convertDocumentToMarkdown(ExportDataPB value) {
+    final json = jsonDecode(value.data);
+    final document = Document.fromJson(json);
+    final result = documentToMarkdown(document);
     value.data = result;
     writeFile(result);
     return value;

+ 13 - 11
frontend/app_flowy/lib/plugins/doc/application/share_service.dart

@@ -3,26 +3,28 @@ import 'package:dartz/dartz.dart';
 import 'package:flowy_sdk/dispatch/dispatch.dart';
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-document/protobuf.dart';
+import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
 
 class ShareService {
   Future<Either<ExportDataPB, FlowyError>> export(
-      String docId, ExportType type) {
-    final request = ExportPayloadPB.create()
-      ..viewId = docId
-      ..exportType = type;
+      ViewPB view, ExportType type) {
+    var payload = ExportPayloadPB.create()
+      ..viewId = view.id
+      ..exportType = type
+      ..documentVersion = DocumentVersionPB.V1;
 
-    return DocumentEventExportDocument(request).send();
+    return DocumentEventExportDocument(payload).send();
   }
 
-  Future<Either<ExportDataPB, FlowyError>> exportText(String docId) {
-    return export(docId, ExportType.Text);
+  Future<Either<ExportDataPB, FlowyError>> exportText(ViewPB view) {
+    return export(view, ExportType.Text);
   }
 
-  Future<Either<ExportDataPB, FlowyError>> exportMarkdown(String docId) {
-    return export(docId, ExportType.Markdown);
+  Future<Either<ExportDataPB, FlowyError>> exportMarkdown(ViewPB view) {
+    return export(view, ExportType.Markdown);
   }
 
-  Future<Either<ExportDataPB, FlowyError>> exportURL(String docId) {
-    return export(docId, ExportType.Link);
+  Future<Either<ExportDataPB, FlowyError>> exportURL(ViewPB view) {
+    return export(view, ExportType.Link);
   }
 }

+ 1 - 1
frontend/app_flowy/lib/plugins/doc/document.dart

@@ -44,7 +44,7 @@ class DocumentPluginBuilder extends PluginBuilder {
   PluginType get pluginType => PluginType.editor;
 
   @override
-  ViewDataTypePB get dataType => ViewDataTypePB.Text;
+  ViewDataFormatPB get dataFormatType => ViewDataFormatPB.TreeFormat;
 }
 
 class DocumentPlugin extends Plugin<int> {

+ 23 - 54
frontend/app_flowy/lib/plugins/doc/document_page.dart

@@ -1,17 +1,14 @@
+import 'package:app_flowy/plugins/doc/editor_styles.dart';
+import 'package:app_flowy/plugins/doc/presentation/plugins/horizontal_rule_node_widget.dart';
 import 'package:app_flowy/startup/startup.dart';
-import 'package:app_flowy/workspace/application/appearance.dart';
 import 'package:app_flowy/plugins/doc/presentation/banner.dart';
-import 'package:app_flowy/plugins/doc/presentation/toolbar/tool_bar.dart';
-import 'package:flowy_infra_ui/style_widget/scrolling/styled_scroll_bar.dart';
-import 'package:flowy_infra_ui/widget/spacing.dart';
-import 'package:flutter_quill/flutter_quill.dart' as quill;
+import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:flowy_infra_ui/widget/error_page.dart';
 import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
-import 'package:provider/provider.dart';
+import 'package:intl/intl.dart';
 import 'application/doc_bloc.dart';
-import 'styles.dart';
 
 class DocumentPage extends StatefulWidget {
   final VoidCallback onDeleted;
@@ -29,11 +26,12 @@ class DocumentPage extends StatefulWidget {
 
 class _DocumentPageState extends State<DocumentPage> {
   late DocumentBloc documentBloc;
-  final scrollController = ScrollController();
   final FocusNode _focusNode = FocusNode();
 
   @override
   void initState() {
+    // The appflowy editor use Intl as locatization, set the default language as fallback.
+    Intl.defaultLocale = 'en_US';
     documentBloc = getIt<DocumentBloc>(param1: super.widget.view)
       ..add(const DocumentEvent.initial());
     super.initState();
@@ -48,9 +46,9 @@ class _DocumentPageState extends State<DocumentPage> {
       child:
           BlocBuilder<DocumentBloc, DocumentState>(builder: (context, state) {
         return state.loadingState.map(
-          // loading: (_) => const FlowyProgressIndicator(),
-          loading: (_) =>
-              SizedBox.expand(child: Container(color: Colors.transparent)),
+          loading: (_) => SizedBox.expand(
+            child: Container(color: Colors.transparent),
+          ),
           finish: (result) => result.successOrFail.fold(
             (_) {
               if (state.forceClose) {
@@ -75,24 +73,11 @@ class _DocumentPageState extends State<DocumentPage> {
   }
 
   Widget _renderDocument(BuildContext context, DocumentState state) {
-    quill.QuillController controller = quill.QuillController(
-      document: context.read<DocumentBloc>().document,
-      selection: const TextSelection.collapsed(offset: 0),
-    );
     return Column(
       children: [
         if (state.isDeleted) _renderBanner(context),
-        Expanded(
-          child: Column(
-            mainAxisAlignment: MainAxisAlignment.spaceBetween,
-            children: [
-              _renderEditor(controller),
-              const VSpace(10),
-              _renderToolbar(controller),
-              const VSpace(10),
-            ],
-          ),
-        ),
+        // AppFlowy Editor
+        _renderAppFlowyEditor(context.read<DocumentBloc>().editorState),
       ],
     );
   }
@@ -107,36 +92,20 @@ class _DocumentPageState extends State<DocumentPage> {
     );
   }
 
-  Widget _renderEditor(quill.QuillController controller) {
-    final editor = quill.QuillEditor(
-      controller: controller,
-      focusNode: _focusNode,
-      scrollable: true,
-      paintCursorAboveText: true,
-      autoFocus: controller.document.isEmpty(),
-      expands: false,
-      padding: const EdgeInsets.symmetric(horizontal: 8.0),
-      readOnly: false,
-      scrollBottomInset: 0,
-      scrollController: scrollController,
-      customStyles: customStyles(context),
+  Widget _renderAppFlowyEditor(EditorState editorState) {
+    final editor = AppFlowyEditor(
+      editorState: editorState,
+      editorStyle: customEditorStyle(context),
+      customBuilders: {
+        'horizontal_rule': HorizontalRuleWidgetBuilder(),
+      },
+      shortcutEvents: [
+        insertHorizontalRule,
+      ],
     );
-
     return Expanded(
-      child: ScrollbarListStack(
-        axis: Axis.vertical,
-        controller: scrollController,
-        barSize: 6.0,
-        child: SizedBox.expand(child: editor),
-      ),
-    );
-  }
-
-  Widget _renderToolbar(quill.QuillController controller) {
-    return ChangeNotifierProvider.value(
-      value: Provider.of<AppearanceSetting>(context, listen: true),
-      child: EditorToolbar.basic(
-        controller: controller,
+      child: SizedBox.expand(
+        child: editor,
       ),
     );
   }

+ 61 - 0
frontend/app_flowy/lib/plugins/doc/editor_styles.dart

@@ -0,0 +1,61 @@
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:flowy_infra/theme.dart';
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+
+EditorStyle customEditorStyle(BuildContext context) {
+  final theme = context.watch<AppTheme>();
+  const baseFontSize = 14.0;
+  const basePadding = 12.0;
+  var textStyle = theme.isDark
+      ? BuiltInTextStyle.builtInDarkMode()
+      : BuiltInTextStyle.builtIn();
+  textStyle = textStyle.copyWith(
+    defaultTextStyle: textStyle.defaultTextStyle.copyWith(
+      fontFamily: 'poppins',
+      fontSize: baseFontSize,
+    ),
+    bold: textStyle.bold.copyWith(
+      fontWeight: FontWeight.w500,
+    ),
+  );
+  return EditorStyle.defaultStyle().copyWith(
+    padding: const EdgeInsets.symmetric(horizontal: 80),
+    textStyle: textStyle,
+    pluginStyles: {
+      'text/heading': builtInPluginStyle
+        ..update(
+          'textStyle',
+          (_) => (EditorState editorState, Node node) {
+            final headingToFontSize = {
+              'h1': baseFontSize + 12,
+              'h2': baseFontSize + 8,
+              'h3': baseFontSize + 4,
+              'h4': baseFontSize,
+              'h5': baseFontSize,
+              'h6': baseFontSize,
+            };
+            final fontSize =
+                headingToFontSize[node.attributes.heading] ?? baseFontSize;
+            return TextStyle(fontSize: fontSize, fontWeight: FontWeight.w600);
+          },
+        )
+        ..update(
+          'padding',
+          (_) => (EditorState editorState, Node node) {
+            final headingToPadding = {
+              'h1': basePadding + 6,
+              'h2': basePadding + 4,
+              'h3': basePadding + 2,
+              'h4': basePadding,
+              'h5': basePadding,
+              'h6': basePadding,
+            };
+            final padding =
+                headingToPadding[node.attributes.heading] ?? basePadding;
+            return EdgeInsets.only(bottom: padding);
+          },
+        )
+    },
+  );
+}

+ 166 - 0
frontend/app_flowy/lib/plugins/doc/presentation/plugins/horizontal_rule_node_widget.dart

@@ -0,0 +1,166 @@
+import 'dart:collection';
+
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:flutter/material.dart';
+
+ShortcutEvent insertHorizontalRule = ShortcutEvent(
+  key: 'Horizontal rule',
+  command: 'Minus',
+  handler: _insertHorzaontalRule,
+);
+
+ShortcutEventHandler _insertHorzaontalRule = (editorState, event) {
+  final selection = editorState.service.selectionService.currentSelection.value;
+  final textNodes = editorState.service.selectionService.currentSelectedNodes
+      .whereType<TextNode>();
+  if (textNodes.length != 1 || selection == null) {
+    return KeyEventResult.ignored;
+  }
+  final textNode = textNodes.first;
+  if (textNode.toPlainText() == '--') {
+    final transaction = editorState.transaction
+      ..deleteText(textNode, 0, 2)
+      ..insertNode(
+        textNode.path,
+        Node(
+          type: 'horizontal_rule',
+          children: LinkedList(),
+          attributes: {},
+        ),
+      )
+      ..afterSelection =
+          Selection.single(path: textNode.path.next, startOffset: 0);
+    editorState.apply(transaction);
+    return KeyEventResult.handled;
+  }
+  return KeyEventResult.ignored;
+};
+
+SelectionMenuItem horizontalRuleMenuItem = SelectionMenuItem(
+  name: () => 'Horizontal rule',
+  icon: const Icon(
+    Icons.horizontal_rule,
+    color: Colors.black,
+    size: 18.0,
+  ),
+  keywords: ['horizontal rule'],
+  handler: (editorState, _, __) {
+    final selection =
+        editorState.service.selectionService.currentSelection.value;
+    final textNodes = editorState.service.selectionService.currentSelectedNodes
+        .whereType<TextNode>();
+    if (selection == null || textNodes.isEmpty) {
+      return;
+    }
+    final textNode = textNodes.first;
+    if (textNode.toPlainText().isEmpty) {
+      final transaction = editorState.transaction
+        ..insertNode(
+          textNode.path,
+          Node(
+            type: 'horizontal_rule',
+            children: LinkedList(),
+            attributes: {},
+          ),
+        )
+        ..afterSelection =
+            Selection.single(path: textNode.path.next, startOffset: 0);
+      editorState.apply(transaction);
+    } else {
+      final transaction = editorState.transaction
+        ..insertNode(
+          selection.end.path.next,
+          TextNode(
+            children: LinkedList(),
+            attributes: {
+              'subtype': 'horizontal_rule',
+            },
+            delta: Delta()..insert('---'),
+          ),
+        )
+        ..afterSelection = selection;
+      editorState.apply(transaction);
+    }
+  },
+);
+
+class HorizontalRuleWidgetBuilder extends NodeWidgetBuilder<Node> {
+  @override
+  Widget build(NodeWidgetContext<Node> context) {
+    return _HorizontalRuleWidget(
+      key: context.node.key,
+      node: context.node,
+      editorState: context.editorState,
+    );
+  }
+
+  @override
+  NodeValidator<Node> get nodeValidator => (node) {
+        return true;
+      };
+}
+
+class _HorizontalRuleWidget extends StatefulWidget {
+  const _HorizontalRuleWidget({
+    Key? key,
+    required this.node,
+    required this.editorState,
+  }) : super(key: key);
+
+  final Node node;
+  final EditorState editorState;
+
+  @override
+  State<_HorizontalRuleWidget> createState() => __HorizontalRuleWidgetState();
+}
+
+class __HorizontalRuleWidgetState extends State<_HorizontalRuleWidget>
+    with SelectableMixin {
+  RenderBox get _renderBox => context.findRenderObject() as RenderBox;
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      padding: const EdgeInsets.symmetric(vertical: 10),
+      child: Container(
+        height: 1,
+        color: Colors.grey,
+      ),
+    );
+  }
+
+  @override
+  Position start() => Position(path: widget.node.path, offset: 0);
+
+  @override
+  Position end() => Position(path: widget.node.path, offset: 1);
+
+  @override
+  Position getPositionInOffset(Offset start) => end();
+
+  @override
+  bool get shouldCursorBlink => false;
+
+  @override
+  CursorStyle get cursorStyle => CursorStyle.borderLine;
+
+  @override
+  Rect? getCursorRectInPosition(Position position) {
+    final size = _renderBox.size;
+    return Rect.fromLTWH(-size.width / 2.0, 0, size.width, size.height);
+  }
+
+  @override
+  List<Rect> getRectsInSelection(Selection selection) =>
+      [Offset.zero & _renderBox.size];
+
+  @override
+  Selection getSelectionInRange(Offset start, Offset end) => Selection.single(
+        path: widget.node.path,
+        startOffset: 0,
+        endOffset: 1,
+      );
+
+  @override
+  Offset localToGlobal(Offset offset) => _renderBox.localToGlobal(offset);
+}

+ 1 - 1
frontend/app_flowy/lib/plugins/grid/grid.dart

@@ -26,7 +26,7 @@ class GridPluginBuilder implements PluginBuilder {
   PluginType get pluginType => PluginType.grid;
 
   @override
-  ViewDataTypePB get dataType => ViewDataTypePB.Database;
+  ViewDataFormatPB get dataFormatType => ViewDataFormatPB.DatabaseFormat;
 
   @override
   ViewLayoutTypePB? get layoutType => ViewLayoutTypePB.Grid;

+ 1 - 1
frontend/app_flowy/lib/startup/plugin/plugin.dart

@@ -49,7 +49,7 @@ abstract class PluginBuilder {
 
   PluginType get pluginType;
 
-  ViewDataTypePB get dataType => ViewDataTypePB.Text;
+  ViewDataFormatPB get dataFormatType => ViewDataFormatPB.TreeFormat;
 
   ViewLayoutTypePB? get layoutType => ViewLayoutTypePB.Document;
 }

+ 4 - 2
frontend/app_flowy/lib/startup/tasks/app_widget.dart

@@ -1,14 +1,15 @@
 import 'package:app_flowy/startup/startup.dart';
 import 'package:app_flowy/user/application/user_settings_service.dart';
 import 'package:app_flowy/workspace/application/appearance.dart';
+import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra/theme.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:flowy_sdk/log.dart';
 import 'package:flutter/material.dart';
 import 'package:provider/provider.dart';
 import 'package:window_size/window_size.dart';
 import 'package:bloc/bloc.dart';
-import 'package:flowy_sdk/log.dart';
 
 class InitAppWidgetTask extends LaunchTask {
   @override
@@ -93,7 +94,8 @@ class ApplicationWidget extends StatelessWidget {
               builder: overlayManagerBuilder(),
               debugShowCheckedModeBanner: false,
               theme: theme.themeData,
-              localizationsDelegates: context.localizationDelegates,
+              localizationsDelegates: context.localizationDelegates +
+                  [AppFlowyEditorLocalizations.delegate],
               supportedLocales: context.supportedLocales,
               locale: locale,
               navigatorKey: AppGlobals.rootNavKey,

+ 1 - 1
frontend/app_flowy/lib/workspace/application/app/app_bloc.dart

@@ -98,7 +98,7 @@ class AppBloc extends Bloc<AppEvent, AppState> {
       appId: app.id,
       name: value.name,
       desc: value.desc ?? "",
-      dataType: value.pluginBuilder.dataType,
+      dataFormatType: value.pluginBuilder.dataFormatType,
       pluginType: value.pluginBuilder.pluginType,
       layoutType: value.pluginBuilder.layoutType!,
     );

+ 2 - 2
frontend/app_flowy/lib/workspace/application/app/app_service.dart

@@ -19,7 +19,7 @@ class AppService {
     required String appId,
     required String name,
     String? desc,
-    required ViewDataTypePB dataType,
+    required ViewDataFormatPB dataFormatType,
     required PluginType pluginType,
     required ViewLayoutTypePB layoutType,
   }) {
@@ -27,7 +27,7 @@ class AppService {
       ..belongToId = appId
       ..name = name
       ..desc = desc ?? ""
-      ..dataType = dataType
+      ..dataFormat = dataFormatType
       ..layout = layoutType;
 
     return FolderEventCreateView(payload).send();

+ 29 - 0
frontend/app_flowy/lib/workspace/application/markdown/document_markdown.dart

@@ -0,0 +1,29 @@
+library delta_markdown;
+
+import 'dart:convert';
+
+import 'package:appflowy_editor/appflowy_editor.dart' show Document;
+import 'package:app_flowy/workspace/application/markdown/src/parser/markdown_encoder.dart';
+
+/// Codec used to convert between Markdown and AppFlowy Editor Document.
+const AppFlowyEditorMarkdownCodec _kCodec = AppFlowyEditorMarkdownCodec();
+
+Document markdownToDocument(String markdown) {
+  return _kCodec.decode(markdown);
+}
+
+String documentToMarkdown(Document document) {
+  return _kCodec.encode(document);
+}
+
+class AppFlowyEditorMarkdownCodec extends Codec<Document, String> {
+  const AppFlowyEditorMarkdownCodec();
+
+  @override
+  Converter<String, Document> get decoder => throw UnimplementedError();
+
+  @override
+  Converter<Document, String> get encoder {
+    return AppFlowyEditorMarkdownEncoder();
+  }
+}

+ 14 - 0
frontend/app_flowy/lib/workspace/application/markdown/src/parser/image_node_parser.dart

@@ -0,0 +1,14 @@
+import 'package:app_flowy/workspace/application/markdown/src/parser/node_parser.dart';
+import 'package:appflowy_editor/appflowy_editor.dart';
+
+class ImageNodeParser extends NodeParser {
+  const ImageNodeParser();
+
+  @override
+  String get id => 'image';
+
+  @override
+  String transform(Node node) {
+    return '![](${node.attributes['image_src']})';
+  }
+}

+ 39 - 0
frontend/app_flowy/lib/workspace/application/markdown/src/parser/markdown_encoder.dart

@@ -0,0 +1,39 @@
+import 'dart:convert';
+
+import 'package:app_flowy/workspace/application/markdown/src/parser/image_node_parser.dart';
+import 'package:app_flowy/workspace/application/markdown/src/parser/node_parser.dart';
+import 'package:app_flowy/workspace/application/markdown/src/parser/text_node_parser.dart';
+import 'package:appflowy_editor/appflowy_editor.dart';
+
+class AppFlowyEditorMarkdownEncoder extends Converter<Document, String> {
+  AppFlowyEditorMarkdownEncoder({
+    this.parsers = const [
+      TextNodeParser(),
+      ImageNodeParser(),
+    ],
+  });
+
+  final List<NodeParser> parsers;
+
+  @override
+  String convert(Document input) {
+    final buffer = StringBuffer();
+    for (final node in input.root.children) {
+      NodeParser? parser =
+          parsers.firstWhereOrNull((element) => element.id == node.type);
+      if (parser != null) {
+        buffer.write(parser.transform(node));
+      }
+    }
+    return buffer.toString();
+  }
+}
+
+extension IterableExtension<T> on Iterable<T> {
+  T? firstWhereOrNull(bool Function(T element) test) {
+    for (var element in this) {
+      if (test(element)) return element;
+    }
+    return null;
+  }
+}

+ 8 - 0
frontend/app_flowy/lib/workspace/application/markdown/src/parser/node_parser.dart

@@ -0,0 +1,8 @@
+import 'package:appflowy_editor/appflowy_editor.dart';
+
+abstract class NodeParser {
+  const NodeParser();
+
+  String get id;
+  String transform(Node node);
+}

+ 68 - 0
frontend/app_flowy/lib/workspace/application/markdown/src/parser/text_node_parser.dart

@@ -0,0 +1,68 @@
+import 'dart:convert';
+
+import 'package:app_flowy/workspace/application/markdown/delta_markdown.dart';
+import 'package:app_flowy/workspace/application/markdown/src/parser/node_parser.dart';
+import 'package:appflowy_editor/appflowy_editor.dart';
+
+class TextNodeParser extends NodeParser {
+  const TextNodeParser();
+
+  @override
+  String get id => 'text';
+
+  @override
+  String transform(Node node) {
+    assert(node is TextNode);
+    final textNode = node as TextNode;
+    final delta = jsonEncode(
+      textNode.delta
+        ..add(TextInsert('\n'))
+        ..toJson(),
+    );
+    final markdown = deltaToMarkdown(delta);
+    final attributes = textNode.attributes;
+    var result = markdown;
+    var suffix = '';
+    if (attributes.isNotEmpty &&
+        attributes.containsKey(BuiltInAttributeKey.subtype)) {
+      final subtype = attributes[BuiltInAttributeKey.subtype];
+      if (node.next?.subtype != subtype) {
+        suffix = '\n';
+      }
+      if (subtype == 'heading') {
+        final heading = attributes[BuiltInAttributeKey.heading];
+        if (heading == 'h1') {
+          result = '# $markdown';
+        } else if (heading == 'h2') {
+          result = '## $markdown';
+        } else if (heading == 'h3') {
+          result = '### $markdown';
+        } else if (heading == 'h4') {
+          result = '#### $markdown';
+        } else if (heading == 'h5') {
+          result = '##### $markdown';
+        } else if (heading == 'h6') {
+          result = '###### $markdown';
+        }
+      } else if (subtype == 'quote') {
+        result = '> $markdown';
+      } else if (subtype == 'code') {
+        result = '`$markdown`';
+      } else if (subtype == 'code-block') {
+        result = '```\n$markdown\n```';
+      } else if (subtype == 'bulleted-list') {
+        result = '- $markdown';
+      } else if (subtype == 'number-list') {
+        final number = attributes['number'];
+        result = '$number. $markdown';
+      } else if (subtype == 'checkbox') {
+        if (attributes[BuiltInAttributeKey.checkbox] == true) {
+          result = '- [x] $markdown';
+        } else {
+          result = '- [ ] $markdown';
+        }
+      }
+    }
+    return '$result$suffix';
+  }
+}

+ 1 - 1
frontend/app_flowy/lib/workspace/application/view/view_bloc.dart

@@ -56,7 +56,7 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
           );
         },
         duplicate: (e) async {
-          final result = await service.duplicate(viewId: view.id);
+          final result = await service.duplicate(view: view);
           emit(
             result.fold(
               (l) => state.copyWith(successOrFailure: left(unit)),

+ 4 - 4
frontend/app_flowy/lib/workspace/application/view/view_service.dart

@@ -10,7 +10,8 @@ class ViewService {
     return FolderEventReadView(request).send();
   }
 
-  Future<Either<ViewPB, FlowyError>> updateView({required String viewId, String? name, String? desc}) {
+  Future<Either<ViewPB, FlowyError>> updateView(
+      {required String viewId, String? name, String? desc}) {
     final request = UpdateViewPayloadPB.create()..viewId = viewId;
 
     if (name != null) {
@@ -29,8 +30,7 @@ class ViewService {
     return FolderEventDeleteView(request).send();
   }
 
-  Future<Either<Unit, FlowyError>> duplicate({required String viewId}) {
-    final request = ViewIdPB(value: viewId);
-    return FolderEventDuplicateView(request).send();
+  Future<Either<Unit, FlowyError>> duplicate({required ViewPB view}) {
+    return FolderEventDuplicateView(view).send();
   }
 }

+ 4 - 8
frontend/app_flowy/packages/appflowy_editor/example/assets/example.json

@@ -2,16 +2,12 @@
   "document": {
     "type": "editor",
     "children": [
-      {
-        "type": "image",
-        "attributes": {
-          "image_src": "https://s1.ax1x.com/2022/08/26/v2sSbR.jpg",
-          "align": "center"
-        }
-      },
       {
         "type": "text",
-        "attributes": { "subtype": "heading", "heading": "h1" },
+        "attributes": {
+          "subtype": "heading",
+          "heading": "h2"
+        },
         "delta": [
           { "insert": "👋 " },
           { "insert": "Welcome to ", "attributes": { "bold": true } },

+ 0 - 2
frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart

@@ -8,7 +8,6 @@ import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 
-import 'package:example/plugin/underscore_to_italic.dart';
 import 'package:file_picker/file_picker.dart';
 import 'package:flutter_localizations/flutter_localizations.dart';
 import 'package:google_fonts/google_fonts.dart';
@@ -141,7 +140,6 @@ class _MyHomePageState extends State<MyHomePage> {
               shortcutEvents: [
                 enterInCodeBlock,
                 ignoreKeysInCodeBlock,
-                underscoreToItalic,
                 insertHorizontalRule,
               ],
               selectionMenuItems: [

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

@@ -1,53 +0,0 @@
-import 'package:appflowy_editor/appflowy_editor.dart';
-import 'package:flutter/material.dart';
-
-ShortcutEvent underscoreToItalic = ShortcutEvent(
-  key: 'Underscore to italic',
-  command: 'shift+underscore',
-  handler: _underscoreToItalicHandler,
-);
-
-ShortcutEventHandler _underscoreToItalicHandler = (editorState, event) {
-  // Obtain the selection and selected nodes of the current document through the 'selectionService'
-  // to determine whether the selection is collapsed and whether the selected node is a text node.
-  final selectionService = editorState.service.selectionService;
-  final selection = selectionService.currentSelection.value;
-  final textNodes = selectionService.currentSelectedNodes.whereType<TextNode>();
-  if (selection == null || !selection.isSingle || textNodes.length != 1) {
-    return KeyEventResult.ignored;
-  }
-
-  final textNode = textNodes.first;
-  final text = textNode.toPlainText();
-  // Determine if an 'underscore' already exists in the text node and only once.
-  final firstUnderscore = text.indexOf('_');
-  final lastUnderscore = text.lastIndexOf('_');
-  if (firstUnderscore == -1 ||
-      firstUnderscore != lastUnderscore ||
-      firstUnderscore == selection.start.offset - 1) {
-    return KeyEventResult.ignored;
-  }
-
-  // Delete the previous 'underscore',
-  // update the style of the text surrounded by the two underscores to 'italic',
-  // and update the cursor position.
-  final transaction = editorState.transaction
-    ..deleteText(textNode, firstUnderscore, 1)
-    ..formatText(
-      textNode,
-      firstUnderscore,
-      selection.end.offset - firstUnderscore - 1,
-      {
-        BuiltInAttributeKey.italic: true,
-      },
-    )
-    ..afterSelection = Selection.collapsed(
-      Position(
-        path: textNode.path,
-        offset: selection.end.offset - 1,
-      ),
-    );
-  editorState.apply(transaction);
-
-  return KeyEventResult.handled;
-};

+ 2 - 2
frontend/app_flowy/packages/appflowy_editor/lib/src/core/document/document.dart

@@ -57,8 +57,8 @@ class Document {
 
     final parent = nodeAtPath(path.parent);
     if (parent != null) {
-      for (final node in nodes) {
-        parent.insert(node, index: path.last);
+      for (var i = 0; i < nodes.length; i++) {
+        parent.insert(nodes.elementAt(i), index: path.last + i);
       }
       return true;
     }

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

@@ -103,7 +103,7 @@ class Transaction {
   Map<String, dynamic> toJson() {
     final json = <String, dynamic>{};
     if (operations.isNotEmpty) {
-      json['operations'] = operations.map((o) => o.toJson());
+      json['operations'] = operations.map((o) => o.toJson()).toList();
     }
     if (afterSelection != null) {
       json['after_selection'] = afterSelection!.toJson();

+ 904 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/flutter/overlay.dart

@@ -0,0 +1,904 @@
+// TODO: Remove this file until we update the flutter version to 3.5.x
+//
+//  This file is copied from flutter(3.5.x) repo.
+//
+//  We Need to commit(https://github.com/flutter/flutter/pull/113770) to fix the
+//  overflow issue.
+
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:collection';
+import 'dart:math' as math;
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/rendering.dart';
+import 'package:flutter/scheduler.dart';
+import 'package:flutter/widgets.dart';
+
+/// A place in an [Overlay] that can contain a widget.
+///
+/// Overlay entries are inserted into an [Overlay] using the
+/// [OverlayState.insert] or [OverlayState.insertAll] functions. To find the
+/// closest enclosing overlay for a given [BuildContext], use the [Overlay.of]
+/// function.
+///
+/// An overlay entry can be in at most one overlay at a time. To remove an entry
+/// from its overlay, call the [remove] function on the overlay entry.
+///
+/// Because an [Overlay] uses a [Stack] layout, overlay entries can use
+/// [Positioned] and [AnimatedPositioned] to position themselves within the
+/// overlay.
+///
+/// For example, [Draggable] uses an [OverlayEntry] to show the drag avatar that
+/// follows the user's finger across the screen after the drag begins. Using the
+/// overlay to display the drag avatar lets the avatar float over the other
+/// widgets in the app. As the user's finger moves, draggable calls
+/// [markNeedsBuild] on the overlay entry to cause it to rebuild. In its build,
+/// the entry includes a [Positioned] with its top and left property set to
+/// position the drag avatar near the user's finger. When the drag is over,
+/// [Draggable] removes the entry from the overlay to remove the drag avatar
+/// from view.
+///
+/// By default, if there is an entirely [opaque] entry over this one, then this
+/// one will not be included in the widget tree (in particular, stateful widgets
+/// within the overlay entry will not be instantiated). To ensure that your
+/// overlay entry is still built even if it is not visible, set [maintainState]
+/// to true. This is more expensive, so should be done with care. In particular,
+/// if widgets in an overlay entry with [maintainState] set to true repeatedly
+/// call [State.setState], the user's battery will be drained unnecessarily.
+///
+/// [OverlayEntry] is a [ChangeNotifier] that notifies when the widget built by
+/// [builder] is mounted or unmounted, whose exact state can be queried by
+/// [mounted].
+///
+/// See also:
+///
+///  * [Overlay]
+///  * [OverlayState]
+///  * [WidgetsApp]
+///  * [MaterialApp]
+class OverlayEntry extends ChangeNotifier {
+  /// Creates an overlay entry.
+  ///
+  /// To insert the entry into an [Overlay], first find the overlay using
+  /// [Overlay.of] and then call [OverlayState.insert]. To remove the entry,
+  /// call [remove] on the overlay entry itself.
+  OverlayEntry({
+    required this.builder,
+    bool opaque = false,
+    bool maintainState = false,
+  })  : _opaque = opaque,
+        _maintainState = maintainState;
+
+  /// This entry will include the widget built by this builder in the overlay at
+  /// the entry's position.
+  ///
+  /// To cause this builder to be called again, call [markNeedsBuild] on this
+  /// overlay entry.
+  final WidgetBuilder builder;
+
+  /// Whether this entry occludes the entire overlay.
+  ///
+  /// If an entry claims to be opaque, then, for efficiency, the overlay will
+  /// skip building entries below that entry unless they have [maintainState]
+  /// set.
+  bool get opaque => _opaque;
+  bool _opaque;
+  set opaque(bool value) {
+    if (_opaque == value) return;
+    _opaque = value;
+    _overlay?._didChangeEntryOpacity();
+  }
+
+  /// Whether this entry must be included in the tree even if there is a fully
+  /// [opaque] entry above it.
+  ///
+  /// By default, if there is an entirely [opaque] entry over this one, then this
+  /// one will not be included in the widget tree (in particular, stateful widgets
+  /// within the overlay entry will not be instantiated). To ensure that your
+  /// overlay entry is still built even if it is not visible, set [maintainState]
+  /// to true. This is more expensive, so should be done with care. In particular,
+  /// if widgets in an overlay entry with [maintainState] set to true repeatedly
+  /// call [State.setState], the user's battery will be drained unnecessarily.
+  ///
+  /// This is used by the [Navigator] and [Route] objects to ensure that routes
+  /// are kept around even when in the background, so that [Future]s promised
+  /// from subsequent routes will be handled properly when they complete.
+  bool get maintainState => _maintainState;
+  bool _maintainState;
+  set maintainState(bool value) {
+    if (_maintainState == value) return;
+    _maintainState = value;
+    assert(_overlay != null);
+    _overlay!._didChangeEntryOpacity();
+  }
+
+  /// Whether the [OverlayEntry] is currently mounted in the widget tree.
+  ///
+  /// The [OverlayEntry] notifies its listeners when this value changes.
+  bool get mounted => _mounted;
+  bool _mounted = false;
+  void _updateMounted(bool value) {
+    if (value == _mounted) {
+      return;
+    }
+    _mounted = value;
+    notifyListeners();
+  }
+
+  OverlayState? _overlay;
+  final GlobalKey<_OverlayEntryWidgetState> _key =
+      GlobalKey<_OverlayEntryWidgetState>();
+
+  /// Remove this entry from the overlay.
+  ///
+  /// This should only be called once.
+  ///
+  /// This method removes this overlay entry from the overlay immediately. The
+  /// UI will be updated in the same frame if this method is called before the
+  /// overlay rebuild in this frame; otherwise, the UI will be updated in the
+  /// next frame. This means that it is safe to call during builds, but	also
+  /// that if you do call this after the overlay rebuild, the UI will not update
+  /// until	the next frame (i.e. many milliseconds later).
+  void remove() {
+    assert(_overlay != null);
+    final OverlayState overlay = _overlay!;
+    _overlay = null;
+    if (!overlay.mounted) return;
+
+    overlay._entries.remove(this);
+    if (SchedulerBinding.instance.schedulerPhase ==
+        SchedulerPhase.persistentCallbacks) {
+      SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
+        overlay._markDirty();
+      });
+    } else {
+      overlay._markDirty();
+    }
+  }
+
+  /// Cause this entry to rebuild during the next pipeline flush.
+  ///
+  /// You need to call this function if the output of [builder] has changed.
+  void markNeedsBuild() {
+    _key.currentState?._markNeedsBuild();
+  }
+
+  @override
+  String toString() =>
+      '${describeIdentity(this)}(opaque: $opaque; maintainState: $maintainState)';
+}
+
+class _OverlayEntryWidget extends StatefulWidget {
+  const _OverlayEntryWidget({
+    required Key key,
+    required this.entry,
+    this.tickerEnabled = true,
+  }) : super(key: key);
+
+  final OverlayEntry entry;
+  final bool tickerEnabled;
+
+  @override
+  _OverlayEntryWidgetState createState() => _OverlayEntryWidgetState();
+}
+
+class _OverlayEntryWidgetState extends State<_OverlayEntryWidget> {
+  @override
+  void initState() {
+    super.initState();
+    widget.entry._updateMounted(true);
+  }
+
+  @override
+  void dispose() {
+    widget.entry._updateMounted(false);
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return TickerMode(
+      enabled: widget.tickerEnabled,
+      child: widget.entry.builder(context),
+    );
+  }
+
+  void _markNeedsBuild() {
+    setState(() {/* the state that changed is in the builder */});
+  }
+}
+
+/// A stack of entries that can be managed independently.
+///
+/// Overlays let independent child widgets "float" visual elements on top of
+/// other widgets by inserting them into the overlay's stack. The overlay lets
+/// each of these widgets manage their participation in the overlay using
+/// [OverlayEntry] objects.
+///
+/// Although you can create an [Overlay] directly, it's most common to use the
+/// overlay created by the [Navigator] in a [WidgetsApp] or a [MaterialApp]. The
+/// navigator uses its overlay to manage the visual appearance of its routes.
+///
+/// The [Overlay] widget uses a custom stack implementation, which is very
+/// similar to the [Stack] widget. The main use case of [Overlay] is related to
+/// navigation and being able to insert widgets on top of the pages in an app.
+/// To simply display a stack of widgets, consider using [Stack] instead.
+///
+/// See also:
+///
+///  * [OverlayEntry], the class that is used for describing the overlay entries.
+///  * [OverlayState], which is used to insert the entries into the overlay.
+///  * [WidgetsApp], which inserts an [Overlay] widget indirectly via its [Navigator].
+///  * [MaterialApp], which inserts an [Overlay] widget indirectly via its [Navigator].
+///  * [Stack], which allows directly displaying a stack of widgets.
+class Overlay extends StatefulWidget {
+  /// Creates an overlay.
+  ///
+  /// The initial entries will be inserted into the overlay when its associated
+  /// [OverlayState] is initialized.
+  ///
+  /// Rather than creating an overlay, consider using the overlay that is
+  /// created by the [Navigator] in a [WidgetsApp] or a [MaterialApp] for the application.
+  const Overlay({
+    Key? key,
+    this.initialEntries = const <OverlayEntry>[],
+    this.clipBehavior = Clip.hardEdge,
+  }) : super(key: key);
+
+  /// The entries to include in the overlay initially.
+  ///
+  /// These entries are only used when the [OverlayState] is initialized. If you
+  /// are providing a new [Overlay] description for an overlay that's already in
+  /// the tree, then the new entries are ignored.
+  ///
+  /// To add entries to an [Overlay] that is already in the tree, use
+  /// [Overlay.of] to obtain the [OverlayState] (or assign a [GlobalKey] to the
+  /// [Overlay] widget and obtain the [OverlayState] via
+  /// [GlobalKey.currentState]), and then use [OverlayState.insert] or
+  /// [OverlayState.insertAll].
+  ///
+  /// To remove an entry from an [Overlay], use [OverlayEntry.remove].
+  final List<OverlayEntry> initialEntries;
+
+  /// {@macro flutter.material.Material.clipBehavior}
+  ///
+  /// Defaults to [Clip.hardEdge], and must not be null.
+  final Clip clipBehavior;
+
+  /// The state from the closest instance of this class that encloses the given context.
+  ///
+  /// In debug mode, if the `debugRequiredFor` argument is provided then this
+  /// function will assert that an overlay was found and will throw an exception
+  /// if not. The exception attempts to explain that the calling [Widget] (the
+  /// one given by the `debugRequiredFor` argument) needs an [Overlay] to be
+  /// present to function.
+  ///
+  /// Typical usage is as follows:
+  ///
+  /// ```dart
+  /// OverlayState overlay = Overlay.of(context);
+  /// ```
+  ///
+  /// If `rootOverlay` is set to true, the state from the furthest instance of
+  /// this class is given instead. Useful for installing overlay entries
+  /// above all subsequent instances of [Overlay].
+  ///
+  /// This method can be expensive (it walks the element tree).
+  static OverlayState? of(
+    BuildContext context, {
+    bool rootOverlay = false,
+    Widget? debugRequiredFor,
+  }) {
+    final OverlayState? result = rootOverlay
+        ? context.findRootAncestorStateOfType<OverlayState>()
+        : context.findAncestorStateOfType<OverlayState>();
+    assert(() {
+      if (debugRequiredFor != null && result == null) {
+        final List<DiagnosticsNode> information = <DiagnosticsNode>[
+          ErrorSummary('No Overlay widget found.'),
+          ErrorDescription(
+              '${debugRequiredFor.runtimeType} widgets require an Overlay widget ancestor for correct operation.'),
+          ErrorHint(
+              'The most common way to add an Overlay to an application is to include a MaterialApp or Navigator widget in the runApp() call.'),
+          DiagnosticsProperty<Widget>(
+              'The specific widget that failed to find an overlay was',
+              debugRequiredFor,
+              style: DiagnosticsTreeStyle.errorProperty),
+          if (context.widget != debugRequiredFor)
+            context.describeElement(
+                'The context from which that widget was searching for an overlay was'),
+        ];
+
+        throw FlutterError.fromParts(information);
+      }
+      return true;
+    }());
+    return result;
+  }
+
+  @override
+  OverlayState createState() => OverlayState();
+}
+
+/// The current state of an [Overlay].
+///
+/// Used to insert [OverlayEntry]s into the overlay using the [insert] and
+/// [insertAll] functions.
+class OverlayState extends State<Overlay> with TickerProviderStateMixin {
+  final List<OverlayEntry> _entries = <OverlayEntry>[];
+
+  @override
+  void initState() {
+    super.initState();
+    insertAll(widget.initialEntries);
+  }
+
+  int _insertionIndex(OverlayEntry? below, OverlayEntry? above) {
+    assert(above == null || below == null);
+    if (below != null) return _entries.indexOf(below);
+    if (above != null) return _entries.indexOf(above) + 1;
+    return _entries.length;
+  }
+
+  /// Insert the given entry into the overlay.
+  ///
+  /// If `below` is non-null, the entry is inserted just below `below`.
+  /// If `above` is non-null, the entry is inserted just above `above`.
+  /// Otherwise, the entry is inserted on top.
+  ///
+  /// It is an error to specify both `above` and `below`.
+  void insert(OverlayEntry entry, {OverlayEntry? below, OverlayEntry? above}) {
+    assert(_debugVerifyInsertPosition(above, below));
+    assert(!_entries.contains(entry),
+        'The specified entry is already present in the Overlay.');
+    assert(entry._overlay == null,
+        'The specified entry is already present in another Overlay.');
+    entry._overlay = this;
+    setState(() {
+      _entries.insert(_insertionIndex(below, above), entry);
+    });
+  }
+
+  /// Insert all the entries in the given iterable.
+  ///
+  /// If `below` is non-null, the entries are inserted just below `below`.
+  /// If `above` is non-null, the entries are inserted just above `above`.
+  /// Otherwise, the entries are inserted on top.
+  ///
+  /// It is an error to specify both `above` and `below`.
+  void insertAll(Iterable<OverlayEntry> entries,
+      {OverlayEntry? below, OverlayEntry? above}) {
+    assert(_debugVerifyInsertPosition(above, below));
+    assert(
+      entries.every((OverlayEntry entry) => !_entries.contains(entry)),
+      'One or more of the specified entries are already present in the Overlay.',
+    );
+    assert(
+      entries.every((OverlayEntry entry) => entry._overlay == null),
+      'One or more of the specified entries are already present in another Overlay.',
+    );
+    if (entries.isEmpty) return;
+    for (final OverlayEntry entry in entries) {
+      assert(entry._overlay == null);
+      entry._overlay = this;
+    }
+    setState(() {
+      _entries.insertAll(_insertionIndex(below, above), entries);
+    });
+  }
+
+  bool _debugVerifyInsertPosition(OverlayEntry? above, OverlayEntry? below,
+      {Iterable<OverlayEntry>? newEntries}) {
+    assert(
+      above == null || below == null,
+      'Only one of `above` and `below` may be specified.',
+    );
+    assert(
+      above == null ||
+          (above._overlay == this &&
+              _entries.contains(above) &&
+              (newEntries?.contains(above) ?? true)),
+      'The provided entry used for `above` must be present in the Overlay${newEntries != null ? ' and in the `newEntriesList`' : ''}.',
+    );
+    assert(
+      below == null ||
+          (below._overlay == this &&
+              _entries.contains(below) &&
+              (newEntries?.contains(below) ?? true)),
+      'The provided entry used for `below` must be present in the Overlay${newEntries != null ? ' and in the `newEntriesList`' : ''}.',
+    );
+    return true;
+  }
+
+  /// Remove all the entries listed in the given iterable, then reinsert them
+  /// into the overlay in the given order.
+  ///
+  /// Entries mention in `newEntries` but absent from the overlay are inserted
+  /// as if with [insertAll].
+  ///
+  /// Entries not mentioned in `newEntries` but present in the overlay are
+  /// positioned as a group in the resulting list relative to the entries that
+  /// were moved, as specified by one of `below` or `above`, which, if
+  /// specified, must be one of the entries in `newEntries`:
+  ///
+  /// If `below` is non-null, the group is positioned just below `below`.
+  /// If `above` is non-null, the group is positioned just above `above`.
+  /// Otherwise, the group is left on top, with all the rearranged entries
+  /// below.
+  ///
+  /// It is an error to specify both `above` and `below`.
+  void rearrange(Iterable<OverlayEntry> newEntries,
+      {OverlayEntry? below, OverlayEntry? above}) {
+    final List<OverlayEntry> newEntriesList = newEntries is List<OverlayEntry>
+        ? newEntries
+        : newEntries.toList(growable: false);
+    assert(
+        _debugVerifyInsertPosition(above, below, newEntries: newEntriesList));
+    assert(
+      newEntriesList.every((OverlayEntry entry) =>
+          entry._overlay == null || entry._overlay == this),
+      'One or more of the specified entries are already present in another Overlay.',
+    );
+    assert(
+      newEntriesList.every((OverlayEntry entry) =>
+          _entries.indexOf(entry) == _entries.lastIndexOf(entry)),
+      'One or more of the specified entries are specified multiple times.',
+    );
+    if (newEntriesList.isEmpty) return;
+    if (listEquals(_entries, newEntriesList)) return;
+    final LinkedHashSet<OverlayEntry> old =
+        LinkedHashSet<OverlayEntry>.of(_entries);
+    for (final OverlayEntry entry in newEntriesList) {
+      entry._overlay ??= this;
+    }
+    setState(() {
+      _entries.clear();
+      _entries.addAll(newEntriesList);
+      old.removeAll(newEntriesList);
+      _entries.insertAll(_insertionIndex(below, above), old);
+    });
+  }
+
+  void _markDirty() {
+    if (mounted) {
+      setState(() {});
+    }
+  }
+
+  /// (DEBUG ONLY) Check whether a given entry is visible (i.e., not behind an
+  /// opaque entry).
+  ///
+  /// This is an O(N) algorithm, and should not be necessary except for debug
+  /// asserts. To avoid people depending on it, this function is implemented
+  /// only in debug mode, and always returns false in release mode.
+  bool debugIsVisible(OverlayEntry entry) {
+    bool result = false;
+    assert(_entries.contains(entry));
+    assert(() {
+      for (int i = _entries.length - 1; i > 0; i -= 1) {
+        final OverlayEntry candidate = _entries[i];
+        if (candidate == entry) {
+          result = true;
+          break;
+        }
+        if (candidate.opaque) break;
+      }
+      return true;
+    }());
+    return result;
+  }
+
+  void _didChangeEntryOpacity() {
+    setState(() {
+      // We use the opacity of the entry in our build function, which means we
+      // our state has changed.
+    });
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    // This list is filled backwards and then reversed below before
+    // it is added to the tree.
+    final List<Widget> children = <Widget>[];
+    bool onstage = true;
+    int onstageCount = 0;
+    for (int i = _entries.length - 1; i >= 0; i -= 1) {
+      final OverlayEntry entry = _entries[i];
+      if (onstage) {
+        onstageCount += 1;
+        children.add(_OverlayEntryWidget(
+          key: entry._key,
+          entry: entry,
+        ));
+        if (entry.opaque) onstage = false;
+      } else if (entry.maintainState) {
+        children.add(_OverlayEntryWidget(
+          key: entry._key,
+          entry: entry,
+          tickerEnabled: false,
+        ));
+      }
+    }
+    return _Theatre(
+      skipCount: children.length - onstageCount,
+      clipBehavior: widget.clipBehavior,
+      children: children.reversed.toList(growable: false),
+    );
+  }
+
+  @override
+  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+    super.debugFillProperties(properties);
+    // TODO(jacobr): use IterableProperty instead as that would
+    // provide a slightly more consistent string summary of the List.
+    properties
+        .add(DiagnosticsProperty<List<OverlayEntry>>('entries', _entries));
+  }
+}
+
+/// Special version of a [Stack], that doesn't layout and render the first
+/// [skipCount] children.
+///
+/// The first [skipCount] children are considered "offstage".
+class _Theatre extends MultiChildRenderObjectWidget {
+  _Theatre({
+    Key? key,
+    this.skipCount = 0,
+    this.clipBehavior = Clip.hardEdge,
+    List<Widget> children = const <Widget>[],
+  })  : assert(skipCount >= 0),
+        assert(children.length >= skipCount),
+        super(key: key, children: children);
+
+  final int skipCount;
+
+  final Clip clipBehavior;
+
+  @override
+  _TheatreElement createElement() => _TheatreElement(this);
+
+  @override
+  _RenderTheatre createRenderObject(BuildContext context) {
+    return _RenderTheatre(
+      skipCount: skipCount,
+      textDirection: Directionality.of(context),
+      clipBehavior: clipBehavior,
+    );
+  }
+
+  @override
+  void updateRenderObject(BuildContext context, _RenderTheatre renderObject) {
+    renderObject
+      ..skipCount = skipCount
+      ..textDirection = Directionality.of(context)
+      ..clipBehavior = clipBehavior;
+  }
+
+  @override
+  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+    super.debugFillProperties(properties);
+    properties.add(IntProperty('skipCount', skipCount));
+  }
+}
+
+class _TheatreElement extends MultiChildRenderObjectElement {
+  _TheatreElement(_Theatre widget) : super(widget);
+
+  @override
+  _RenderTheatre get renderObject => super.renderObject as _RenderTheatre;
+
+  @override
+  void debugVisitOnstageChildren(ElementVisitor visitor) {
+    final _Theatre theatre = widget as _Theatre;
+    assert(children.length >= theatre.skipCount);
+    children.skip(theatre.skipCount).forEach(visitor);
+  }
+}
+
+class _RenderTheatre extends RenderBox
+    with ContainerRenderObjectMixin<RenderBox, StackParentData> {
+  _RenderTheatre({
+    List<RenderBox>? children,
+    required TextDirection textDirection,
+    int skipCount = 0,
+    Clip clipBehavior = Clip.hardEdge,
+  })  : assert(skipCount >= 0),
+        _textDirection = textDirection,
+        _skipCount = skipCount,
+        _clipBehavior = clipBehavior {
+    addAll(children);
+  }
+
+  bool _hasVisualOverflow = false;
+
+  @override
+  void setupParentData(RenderBox child) {
+    if (child.parentData is! StackParentData) {
+      child.parentData = StackParentData();
+    }
+  }
+
+  Alignment? _resolvedAlignment;
+
+  void _resolve() {
+    if (_resolvedAlignment != null) return;
+    _resolvedAlignment = AlignmentDirectional.topStart.resolve(textDirection);
+  }
+
+  void _markNeedResolution() {
+    _resolvedAlignment = null;
+    markNeedsLayout();
+  }
+
+  TextDirection get textDirection => _textDirection;
+  TextDirection _textDirection;
+  set textDirection(TextDirection value) {
+    if (_textDirection == value) return;
+    _textDirection = value;
+    _markNeedResolution();
+  }
+
+  int get skipCount => _skipCount;
+  int _skipCount;
+  set skipCount(int value) {
+    if (_skipCount != value) {
+      _skipCount = value;
+      markNeedsLayout();
+    }
+  }
+
+  /// {@macro flutter.material.Material.clipBehavior}
+  ///
+  /// Defaults to [Clip.hardEdge], and must not be null.
+  Clip get clipBehavior => _clipBehavior;
+  Clip _clipBehavior = Clip.hardEdge;
+  set clipBehavior(Clip value) {
+    if (value != _clipBehavior) {
+      _clipBehavior = value;
+      markNeedsPaint();
+      markNeedsSemanticsUpdate();
+    }
+  }
+
+  RenderBox? get _firstOnstageChild {
+    if (skipCount == super.childCount) {
+      return null;
+    }
+    RenderBox? child = super.firstChild;
+    for (int toSkip = skipCount; toSkip > 0; toSkip--) {
+      final StackParentData childParentData =
+          child!.parentData! as StackParentData;
+      child = childParentData.nextSibling;
+      assert(child != null);
+    }
+    return child;
+  }
+
+  RenderBox? get _lastOnstageChild =>
+      skipCount == super.childCount ? null : lastChild;
+
+  int get _onstageChildCount => childCount - skipCount;
+
+  @override
+  double computeMinIntrinsicWidth(double height) {
+    return RenderStack.getIntrinsicDimension(_firstOnstageChild,
+        (RenderBox child) => child.getMinIntrinsicWidth(height));
+  }
+
+  @override
+  double computeMaxIntrinsicWidth(double height) {
+    return RenderStack.getIntrinsicDimension(_firstOnstageChild,
+        (RenderBox child) => child.getMaxIntrinsicWidth(height));
+  }
+
+  @override
+  double computeMinIntrinsicHeight(double width) {
+    return RenderStack.getIntrinsicDimension(_firstOnstageChild,
+        (RenderBox child) => child.getMinIntrinsicHeight(width));
+  }
+
+  @override
+  double computeMaxIntrinsicHeight(double width) {
+    return RenderStack.getIntrinsicDimension(_firstOnstageChild,
+        (RenderBox child) => child.getMaxIntrinsicHeight(width));
+  }
+
+  @override
+  double? computeDistanceToActualBaseline(TextBaseline baseline) {
+    assert(!debugNeedsLayout);
+    double? result;
+    RenderBox? child = _firstOnstageChild;
+    while (child != null) {
+      assert(!child.debugNeedsLayout);
+      final StackParentData childParentData =
+          child.parentData! as StackParentData;
+      double? candidate = child.getDistanceToActualBaseline(baseline);
+      if (candidate != null) {
+        candidate += childParentData.offset.dy;
+        if (result != null) {
+          result = math.min(result, candidate);
+        } else {
+          result = candidate;
+        }
+      }
+      child = childParentData.nextSibling;
+    }
+    return result;
+  }
+
+  @override
+  bool get sizedByParent => true;
+
+  @override
+  Size computeDryLayout(BoxConstraints constraints) {
+    assert(constraints.biggest.isFinite);
+    return constraints.biggest;
+  }
+
+  @override
+  void performLayout() {
+    _hasVisualOverflow = false;
+
+    if (_onstageChildCount == 0) {
+      return;
+    }
+
+    _resolve();
+    assert(_resolvedAlignment != null);
+
+    // Same BoxConstraints as used by RenderStack for StackFit.expand.
+    final BoxConstraints nonPositionedConstraints =
+        BoxConstraints.tight(constraints.biggest);
+
+    RenderBox? child = _firstOnstageChild;
+    while (child != null) {
+      final StackParentData childParentData =
+          child.parentData! as StackParentData;
+
+      if (!childParentData.isPositioned) {
+        child.layout(nonPositionedConstraints, parentUsesSize: true);
+        childParentData.offset =
+            _resolvedAlignment!.alongOffset(size - child.size as Offset);
+      } else {
+        _hasVisualOverflow = RenderStack.layoutPositionedChild(
+                child, childParentData, size, _resolvedAlignment!) ||
+            _hasVisualOverflow;
+      }
+
+      assert(child.parentData == childParentData);
+      child = childParentData.nextSibling;
+    }
+  }
+
+  @override
+  bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
+    RenderBox? child = _lastOnstageChild;
+    for (int i = 0; i < _onstageChildCount; i++) {
+      assert(child != null);
+      final StackParentData childParentData =
+          child!.parentData! as StackParentData;
+      final bool isHit = result.addWithPaintOffset(
+        offset: childParentData.offset,
+        position: position,
+        hitTest: (BoxHitTestResult result, Offset transformed) {
+          assert(transformed == position - childParentData.offset);
+          return child!.hitTest(result, position: transformed);
+        },
+      );
+      if (isHit) return true;
+      child = childParentData.previousSibling;
+    }
+    return false;
+  }
+
+  @protected
+  void paintStack(PaintingContext context, Offset offset) {
+    RenderBox? child = _firstOnstageChild;
+    while (child != null) {
+      final StackParentData childParentData =
+          child.parentData! as StackParentData;
+      context.paintChild(child, childParentData.offset + offset);
+      child = childParentData.nextSibling;
+    }
+  }
+
+  @override
+  void paint(PaintingContext context, Offset offset) {
+    _hasVisualOverflow = true;
+    if (_hasVisualOverflow && clipBehavior != Clip.none) {
+      _clipRectLayer.layer = context.pushClipRect(
+        needsCompositing,
+        offset,
+        Offset.zero & size,
+        paintStack,
+        clipBehavior: clipBehavior,
+        oldLayer: _clipRectLayer.layer,
+      );
+    } else {
+      _clipRectLayer.layer = null;
+      paintStack(context, offset);
+    }
+  }
+
+  final LayerHandle<ClipRectLayer> _clipRectLayer =
+      LayerHandle<ClipRectLayer>();
+
+  @override
+  void dispose() {
+    _clipRectLayer.layer = null;
+    super.dispose();
+  }
+
+  @override
+  void visitChildrenForSemantics(RenderObjectVisitor visitor) {
+    RenderBox? child = _firstOnstageChild;
+    while (child != null) {
+      visitor(child);
+      final StackParentData childParentData =
+          child.parentData! as StackParentData;
+      child = childParentData.nextSibling;
+    }
+  }
+
+  @override
+  Rect? describeApproximatePaintClip(RenderObject child) =>
+      _hasVisualOverflow ? Offset.zero & size : null;
+
+  @override
+  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+    super.debugFillProperties(properties);
+    properties.add(IntProperty('skipCount', skipCount));
+    properties.add(EnumProperty<TextDirection>('textDirection', textDirection));
+  }
+
+  @override
+  List<DiagnosticsNode> debugDescribeChildren() {
+    final List<DiagnosticsNode> offstageChildren = <DiagnosticsNode>[];
+    final List<DiagnosticsNode> onstageChildren = <DiagnosticsNode>[];
+
+    int count = 1;
+    bool onstage = false;
+    RenderBox? child = firstChild;
+    final RenderBox? firstOnstageChild = _firstOnstageChild;
+    while (child != null) {
+      if (child == firstOnstageChild) {
+        onstage = true;
+        count = 1;
+      }
+
+      if (onstage) {
+        onstageChildren.add(
+          child.toDiagnosticsNode(
+            name: 'onstage $count',
+          ),
+        );
+      } else {
+        offstageChildren.add(
+          child.toDiagnosticsNode(
+            name: 'offstage $count',
+            style: DiagnosticsTreeStyle.offstage,
+          ),
+        );
+      }
+
+      final StackParentData childParentData =
+          child.parentData! as StackParentData;
+      child = childParentData.nextSibling;
+      count += 1;
+    }
+
+    return <DiagnosticsNode>[
+      ...onstageChildren,
+      if (offstageChildren.isNotEmpty)
+        ...offstageChildren
+      else
+        DiagnosticsNode.message(
+          'no offstage children',
+          style: DiagnosticsTreeStyle.offstage,
+        ),
+    ];
+  }
+}

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

@@ -1,5 +1,3 @@
-import 'dart:collection';
-
 import 'package:appflowy_editor/src/core/document/node.dart';
 import 'package:appflowy_editor/src/editor_state.dart';
 import 'package:appflowy_editor/src/infra/flowy_svg.dart';
@@ -185,12 +183,12 @@ extension on EditorState {
     }
     final imageNode = Node(
       type: 'image',
-      children: LinkedList(),
       attributes: {
         'image_src': src,
         'align': 'center',
       },
     );
+    final transaction = this.transaction;
     transaction.insertNode(
       selection.start.path,
       imageNode,

+ 10 - 7
frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart

@@ -26,7 +26,7 @@ class FlowyRichText extends StatefulWidget {
   const FlowyRichText({
     Key? key,
     this.cursorHeight,
-    this.cursorWidth = 1.0,
+    this.cursorWidth = 1.5,
     this.lineHeight = 1.0,
     this.textSpanDecorator,
     this.placeholderText = ' ',
@@ -55,7 +55,7 @@ class _FlowyRichTextState extends State<FlowyRichText> with SelectableMixin {
   RenderParagraph get _renderParagraph =>
       _textKey.currentContext?.findRenderObject() as RenderParagraph;
 
-  RenderParagraph get _placeholderRenderParagraph =>
+  RenderParagraph? get _placeholderRenderParagraph =>
       _placeholderTextKey.currentContext?.findRenderObject() as RenderParagraph;
 
   @override
@@ -79,7 +79,7 @@ class _FlowyRichTextState extends State<FlowyRichText> with SelectableMixin {
 
   @override
   Position end() => Position(
-      path: widget.textNode.path, offset: widget.textNode.delta.length);
+      path: widget.textNode.path, offset: widget.textNode.toPlainText().length);
 
   @override
   Rect? getCursorRectInPosition(Position position) {
@@ -90,12 +90,13 @@ class _FlowyRichTextState extends State<FlowyRichText> with SelectableMixin {
         _renderParagraph.getOffsetForCaret(textPosition, Rect.zero);
     if (cursorHeight == null) {
       cursorHeight =
-          _placeholderRenderParagraph.getFullHeightForCaret(textPosition);
-      cursorOffset = _placeholderRenderParagraph.getOffsetForCaret(
-          textPosition, Rect.zero);
+          _placeholderRenderParagraph?.getFullHeightForCaret(textPosition);
+      cursorOffset = _placeholderRenderParagraph?.getOffsetForCaret(
+              textPosition, Rect.zero) ??
+          Offset.zero;
     }
     final rect = Rect.fromLTWH(
-      cursorOffset.dx - (widget.cursorWidth / 2),
+      cursorOffset.dx - (widget.cursorWidth / 2.0),
       cursorOffset.dy,
       widget.cursorWidth,
       widget.cursorHeight ?? cursorHeight ?? 16.0,
@@ -297,6 +298,8 @@ class _FlowyRichTextState extends State<FlowyRichText> with SelectableMixin {
 
         timer = Timer(const Duration(milliseconds: 200), () {
           tapCount = 0;
+          widget.editorState.service.selectionService
+              .updateSelection(selection);
           WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
             showLinkMenu(
               context,

+ 8 - 2
frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_service.dart

@@ -59,10 +59,16 @@ class SelectionMenu implements SelectionMenuService {
     // Workaround: We can customize the padding through the [EditorStyle],
     //  but the coordinates of overlay are not properly converted currently.
     //  Just subtract the padding here as a result.
+    const menuHeight = 200.0;
+    const menuOffset = Offset(10, 10);
     final baseOffset =
         editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero;
-    final offset =
-        selectionRects.first.bottomRight + const Offset(10, 10) - baseOffset;
+    var offset = selectionRects.first.bottomRight + menuOffset;
+    if (offset.dy >=
+        baseOffset.dy + editorState.renderBox!.size.height - menuHeight) {
+      offset = selectionRects.first.topRight - menuOffset;
+      offset = offset.translate(0, -menuHeight);
+    }
     _topLeft = offset;
 
     _selectionMenuEntry = OverlayEntry(builder: (context) {

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

@@ -1,13 +1,14 @@
 import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:appflowy_editor/src/commands/text/text_commands.dart';
 import 'package:appflowy_editor/src/extensions/url_launcher_extension.dart';
+import 'package:appflowy_editor/src/flutter/overlay.dart';
 import 'package:appflowy_editor/src/infra/flowy_svg.dart';
 import 'package:appflowy_editor/src/render/link_menu/link_menu.dart';
 import 'package:appflowy_editor/src/extensions/text_node_extensions.dart';
 import 'package:appflowy_editor/src/extensions/editor_state_extensions.dart';
 import 'package:appflowy_editor/src/service/default_text_operations/format_rich_text_style.dart';
 
-import 'package:flutter/material.dart';
+import 'package:flutter/material.dart' hide Overlay, OverlayEntry;
 import 'package:rich_clipboard/rich_clipboard.dart';
 
 typedef ToolbarItemEventHandler = void Function(
@@ -206,7 +207,9 @@ List<ToolbarItem> defaultToolbarItems = [
       BuiltInAttributeKey.subtype,
       (value) => value == BuiltInAttributeKey.quote,
     ),
-    handler: (editorState, context) => formatQuote(editorState),
+    handler: (editorState, context) {
+      formatQuote(editorState);
+    },
   ),
   ToolbarItem(
     id: 'appflowy.toolbar.bulleted_list',

+ 1 - 1
frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item_widget.dart

@@ -25,7 +25,7 @@ class ToolbarItemWidget extends StatelessWidget {
         child: MouseRegion(
           cursor: SystemMouseCursors.click,
           child: IconButton(
-            highlightColor: Colors.yellow,
+            highlightColor: Colors.transparent,
             padding: EdgeInsets.zero,
             icon: item.iconBuilder(isHighlight),
             iconSize: 28,

+ 3 - 1
frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_widget.dart

@@ -1,6 +1,7 @@
+import 'package:appflowy_editor/src/flutter/overlay.dart';
 import 'package:appflowy_editor/src/render/toolbar/toolbar_item.dart';
 import 'package:appflowy_editor/src/render/toolbar/toolbar_item_widget.dart';
-import 'package:flutter/material.dart';
+import 'package:flutter/material.dart' hide Overlay, OverlayEntry;
 
 import 'package:appflowy_editor/src/editor_state.dart';
 
@@ -67,6 +68,7 @@ class _ToolbarWidgetState extends State<ToolbarWidget> with ToolbarMixin {
                       isHighlight: item.highlightCallback(widget.editorState),
                       onPressed: () {
                         item.handler(widget.editorState, context);
+                        widget.editorState.service.keyboardService?.enable();
                       },
                     ),
                   ),

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

@@ -1,9 +1,10 @@
+import 'package:appflowy_editor/src/flutter/overlay.dart';
 import 'package:appflowy_editor/src/render/image/image_node_builder.dart';
 import 'package:appflowy_editor/src/render/selection_menu/selection_menu_widget.dart';
 import 'package:appflowy_editor/src/render/style/editor_style.dart';
 import 'package:appflowy_editor/src/service/shortcut_event/built_in_shortcut_events.dart';
 import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event.dart';
-import 'package:flutter/material.dart';
+import 'package:flutter/material.dart' hide Overlay, OverlayEntry;
 
 import 'package:appflowy_editor/src/editor_state.dart';
 import 'package:appflowy_editor/src/render/editor/editor_entry.dart';

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

@@ -97,6 +97,7 @@ void _pasteHTML(EditorState editorState, String html) {
       final firstTextNode = firstNode as TextNode;
       tb.updateText(
           textNodeAtPath, (Delta()..retain(startOffset)) + firstTextNode.delta);
+      tb.updateNode(textNodeAtPath, firstTextNode.attributes);
       tb.afterSelection = (Selection.collapsed(Position(
           path: path, offset: startOffset + firstTextNode.delta.length)));
       editorState.apply(tb);
@@ -114,7 +115,7 @@ void _pasteMultipleLinesInText(
   final firstNode = nodes[0];
   final nodeAtPath = editorState.document.nodeAtPath(path)!;
 
-  if (nodeAtPath.type == "text" && firstNode.type == "text") {
+  if (nodeAtPath.type == 'text' && firstNode.type == 'text') {
     int? startNumber;
     if (nodeAtPath.subtype == BuiltInAttributeKey.numberList) {
       startNumber = nodeAtPath.attributes[BuiltInAttributeKey.number] as int;
@@ -131,6 +132,7 @@ void _pasteMultipleLinesInText(
               ..retain(offset)
               ..delete(remain.length)) +
             firstTextNode.delta);
+    tb.updateNode(textNodeAtPath, firstTextNode.attributes);
 
     final tailNodes = nodes.sublist(1);
     final originalPath = [...path];

+ 45 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text.dart

@@ -393,3 +393,48 @@ ShortcutEventHandler doubleUnderscoresToBold = (editorState, event) {
   editorState.apply(transaction);
   return KeyEventResult.handled;
 };
+
+ShortcutEventHandler underscoreToItalicHandler = (editorState, event) {
+  // Obtain the selection and selected nodes of the current document through the 'selectionService'
+  // to determine whether the selection is collapsed and whether the selected node is a text node.
+  final selectionService = editorState.service.selectionService;
+  final selection = selectionService.currentSelection.value;
+  final textNodes = selectionService.currentSelectedNodes.whereType<TextNode>();
+  if (selection == null || !selection.isSingle || textNodes.length != 1) {
+    return KeyEventResult.ignored;
+  }
+
+  final textNode = textNodes.first;
+  final text = textNode.toPlainText();
+  // Determine if an 'underscore' already exists in the text node and only once.
+  final firstUnderscore = text.indexOf('_');
+  final lastUnderscore = text.lastIndexOf('_');
+  if (firstUnderscore == -1 ||
+      firstUnderscore != lastUnderscore ||
+      firstUnderscore == selection.start.offset - 1) {
+    return KeyEventResult.ignored;
+  }
+
+  // Delete the previous 'underscore',
+  // update the style of the text surrounded by the two underscores to 'italic',
+  // and update the cursor position.
+  final transaction = editorState.transaction
+    ..deleteText(textNode, firstUnderscore, 1)
+    ..formatText(
+      textNode,
+      firstUnderscore,
+      selection.end.offset - firstUnderscore - 1,
+      {
+        BuiltInAttributeKey.italic: true,
+      },
+    )
+    ..afterSelection = Selection.collapsed(
+      Position(
+        path: textNode.path,
+        offset: selection.end.offset - 1,
+      ),
+    );
+  editorState.apply(transaction);
+
+  return KeyEventResult.handled;
+};

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

@@ -189,7 +189,8 @@ KeyEventResult _toHeadingStyle(
 
 int _countOfSign(String text, Selection selection) {
   for (var i = 6; i >= 0; i--) {
-    if (text.substring(0, selection.end.offset).contains('#' * i)) {
+    final heading = text.substring(0, selection.end.offset);
+    if (heading.contains('#' * i) && heading.length == i) {
       return i;
     }
   }

+ 7 - 2
frontend/app_flowy/packages/appflowy_editor/lib/src/service/scroll_service.dart

@@ -92,11 +92,16 @@ class _AppFlowyScrollState extends State<AppFlowyScroll>
   Widget build(BuildContext context) {
     return Listener(
       onPointerSignal: _onPointerSignal,
-      child: SingleChildScrollView(
+      child: CustomScrollView(
         key: _scrollViewKey,
         physics: const NeverScrollableScrollPhysics(),
         controller: _scrollController,
-        child: widget.child,
+        slivers: [
+          SliverFillRemaining(
+            hasScrollBody: false,
+            child: widget.child,
+          )
+        ],
       ),
     );
   }

+ 8 - 2
frontend/app_flowy/packages/appflowy_editor/lib/src/service/selection_service.dart

@@ -1,7 +1,8 @@
+import 'package:appflowy_editor/src/flutter/overlay.dart';
 import 'package:appflowy_editor/src/infra/log.dart';
 import 'package:appflowy_editor/src/service/context_menu/built_in_context_menu_item.dart';
 import 'package:appflowy_editor/src/service/context_menu/context_menu.dart';
-import 'package:flutter/material.dart';
+import 'package:flutter/material.dart' hide Overlay, OverlayEntry;
 
 import 'package:appflowy_editor/src/core/document/node.dart';
 import 'package:appflowy_editor/src/core/document/node_iterator.dart';
@@ -505,9 +506,14 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
   }
 
   void _showContextMenu(TapDownDetails details) {
+    _clearContextMenu();
+
+    final baseOffset =
+        editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero;
+    final offset = details.globalPosition + const Offset(10, 10) - baseOffset;
     final contextMenu = OverlayEntry(
       builder: (context) => ContextMenu(
-        position: details.globalPosition,
+        position: offset,
         editorState: editorState,
         items: builtInContextMenuItems,
         onPressed: () => _clearContextMenu(),

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

@@ -285,6 +285,11 @@ List<ShortcutEvent> builtInShortcutEvents = [
     command: 'escape',
     handler: exitEditingModeEventHandler,
   ),
+  ShortcutEvent(
+    key: 'Underscore to italic',
+    command: 'shift+underscore',
+    handler: underscoreToItalicHandler,
+  ),
   // https://github.com/flutter/flutter/issues/104944
   // Workaround: Using space editing on the web platform often results in errors,
   //  so adding a shortcut event to handle the space input instead of using the

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

@@ -1,5 +1,6 @@
+import 'package:appflowy_editor/src/flutter/overlay.dart';
 import 'package:appflowy_editor/src/render/toolbar/toolbar_item.dart';
-import 'package:flutter/material.dart';
+import 'package:flutter/material.dart' hide Overlay, OverlayEntry;
 
 import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:appflowy_editor/src/render/toolbar/toolbar_widget.dart';

+ 31 - 0
frontend/app_flowy/packages/appflowy_editor/test/render/link_menu/link_menu_test.dart

@@ -1,6 +1,8 @@
+import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:appflowy_editor/src/render/link_menu/link_menu.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_test/flutter_test.dart';
+import '../../infra/test_editor.dart';
 
 void main() async {
   setUpAll(() {
@@ -39,5 +41,34 @@ void main() async {
 
       expect(submittedText, link);
     });
+
+    testWidgets('test tap linked text', (tester) async {
+      const link = 'appflowy.io';
+      // This is a link [appflowy.io](appflowy.io)
+      final editor = tester.editor
+        ..insertTextNode(
+          null,
+          delta: Delta()
+            ..insert(
+              'appflowy.io',
+              attributes: {
+                BuiltInAttributeKey.href: link,
+              },
+            ),
+        );
+      await editor.startTesting();
+      final finder = find.byType(RichText);
+      expect(finder, findsOneWidget);
+
+      // tap the link
+      await editor.updateSelection(
+        Selection.single(path: [0], startOffset: 0, endOffset: link.length),
+      );
+      await tester.tap(finder);
+      await tester.pumpAndSettle(const Duration(milliseconds: 350));
+      final linkMenu = find.byType(LinkMenu);
+      expect(linkMenu, findsOneWidget);
+      expect(find.text(link, findRichText: true), findsNWidgets(2));
+    });
   });
 }

+ 14 - 0
frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/white_space_handler_test.dart

@@ -213,5 +213,19 @@ void main() async {
       expect(textNode.attributes.check, true);
       expect(textNode.toPlainText(), insertedText);
     });
+
+    testWidgets('Presses # at the end of the text', (tester) async {
+      const text = 'Welcome to Appflowy 😁 #';
+      final editor = tester.editor..insertTextNode(text);
+      await editor.startTesting();
+
+      final textNode = editor.nodeAtPath([0]) as TextNode;
+      await editor.updateSelection(
+        Selection.single(path: [0], startOffset: text.length),
+      );
+      await editor.pressLogicKey(LogicalKeyboardKey.space);
+      expect(textNode.subtype, null);
+      expect(textNode.toPlainText(), text);
+    });
   });
 }

+ 0 - 1
frontend/app_flowy/packages/flowy_sdk/lib/dispatch/dispatch.dart

@@ -18,7 +18,6 @@ import 'package:flowy_sdk/protobuf/dart-ffi/protobuf.dart';
 import 'package:flowy_sdk/protobuf/flowy-folder/protobuf.dart';
 import 'package:flowy_sdk/protobuf/flowy-document/protobuf.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/protobuf.dart';
-import 'package:flowy_sdk/protobuf/flowy-sync/protobuf.dart';
 
 // ignore: unused_import
 import 'package:protobuf/protobuf.dart';

+ 7 - 0
frontend/app_flowy/pubspec.lock

@@ -586,6 +586,13 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "2.0.2"
+  google_fonts:
+    dependency: "direct main"
+    description:
+      name: google_fonts
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.0.1"
   graphs:
     dependency: transitive
     description:

+ 21 - 0
frontend/app_flowy/pubspec.yaml

@@ -91,6 +91,7 @@ dependencies:
   bloc: ^8.1.0
   textstyle_extensions: "2.0.0-nullsafety"
   shared_preferences: ^2.0.15
+  google_fonts: ^3.0.1
 
 dev_dependencies:
   flutter_lints: ^2.0.1
@@ -129,6 +130,26 @@ flutter:
     - family: FlowyIconData
       fonts:
         - asset: assets/fonts/FlowyIconData.ttf
+    - family: Poppins
+      fonts:
+        - asset: assets/google_fonts/Poppins/Poppins-ExtraLight.ttf
+          weight: 100
+        - asset: assets/google_fonts/Poppins/Poppins-Thin.ttf
+          weight: 200
+        - asset: assets/google_fonts/Poppins/Poppins-Light.ttf
+          weight: 300          
+        - asset: assets/google_fonts/Poppins/Poppins-Regular.ttf          
+          weight: 400
+        - asset: assets/google_fonts/Poppins/Poppins-Medium.ttf          
+          weight: 500
+        - asset: assets/google_fonts/Poppins/Poppins-SemiBold.ttf          
+          weight: 600
+        - asset: assets/google_fonts/Poppins/Poppins-Bold.ttf
+          weight: 700
+        - asset: assets/google_fonts/Poppins/Poppins-Black.ttf
+          weight: 800
+        - asset: assets/google_fonts/Poppins/Poppins-ExtraBold.ttf
+          weight: 900     
 
   # To add assets to your application, add an assets section, like this:
   assets:

+ 1 - 1
frontend/app_flowy/test/bloc_test/grid_test/util.dart

@@ -30,7 +30,7 @@ class AppFlowyGridTest {
     final result = await AppService().createView(
       appId: app.id,
       name: "Test Grid",
-      dataType: builder.dataType,
+      dataFormatType: builder.dataFormatType,
       pluginType: builder.pluginType,
       layoutType: builder.layoutType!,
     );

+ 2 - 0
frontend/rust-lib/Cargo.lock

@@ -871,6 +871,7 @@ dependencies = [
  "lib-ot",
  "lib-ws",
  "log",
+ "md5",
  "protobuf",
  "rand 0.8.5",
  "serde",
@@ -1773,6 +1774,7 @@ dependencies = [
  "bytes",
  "dashmap",
  "derive_more",
+ "indexmap",
  "indextree",
  "lazy_static",
  "log",

+ 1 - 1
frontend/rust-lib/dart-ffi/src/lib.rs

@@ -23,7 +23,7 @@ pub extern "C" fn init_sdk(path: *mut c_char) -> i64 {
     let path: &str = c_str.to_str().unwrap();
 
     let server_config = get_client_server_configuration().unwrap();
-    let config = FlowySDKConfig::new(path, "appflowy", server_config, false).log_filter("info");
+    let config = FlowySDKConfig::new(path, "appflowy", server_config).log_filter("info");
     FLOWY_SDK.get_or_init(|| FlowySDK::new(config));
 
     0

+ 2 - 0
frontend/rust-lib/flowy-database/migrations/2022-10-22-033122_document/down.sql

@@ -0,0 +1,2 @@
+-- This file should undo anything in `up.sql`
+DROP TABLE grid_view_rev_table;

+ 9 - 0
frontend/rust-lib/flowy-database/migrations/2022-10-22-033122_document/up.sql

@@ -0,0 +1,9 @@
+-- Your SQL goes here
+CREATE TABLE document_rev_table (
+   id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+   document_id TEXT NOT NULL DEFAULT '',
+   base_rev_id BIGINT NOT NULL DEFAULT 0,
+   rev_id BIGINT NOT NULL DEFAULT 0,
+   data BLOB NOT NULL DEFAULT (x''),
+   state INTEGER NOT NULL DEFAULT 0
+);

+ 12 - 0
frontend/rust-lib/flowy-database/src/schema.rs

@@ -13,6 +13,17 @@ table! {
     }
 }
 
+table! {
+    document_rev_table (id) {
+        id -> Integer,
+        document_id -> Text,
+        base_rev_id -> BigInt,
+        rev_id -> BigInt,
+        data -> Binary,
+        state -> Integer,
+    }
+}
+
 table! {
     grid_block_index_table (row_id) {
         row_id -> Text,
@@ -133,6 +144,7 @@ table! {
 
 allow_tables_to_appear_in_same_query!(
     app_table,
+    document_rev_table,
     grid_block_index_table,
     grid_meta_rev_table,
     grid_rev_table,

+ 1 - 0
frontend/rust-lib/flowy-document/Cargo.toml

@@ -28,6 +28,7 @@ tokio = {version = "1", features = ["sync"]}
 tracing = { version = "0.1", features = ["log"] }
 
 bytes = { version = "1.1" }
+md5 = "0.7.0"
 strum = "0.21"
 strum_macros = "0.21"
 dashmap = "5"

+ 419 - 0
frontend/rust-lib/flowy-document/src/editor/READ_ME.json

@@ -0,0 +1,419 @@
+{
+  "document": {
+    "type": "editor",
+    "children": [
+      {
+        "type": "text",
+        "attributes": {
+          "subtype": "heading",
+          "heading": "h1"
+        },
+        "delta": [
+          {
+            "insert": "🌟 Welcome to AppFlowy!"
+          }
+        ]
+      },
+      {
+        "type": "text",
+        "attributes": {
+          "subtype": "heading",
+          "heading": "h2"
+        },
+        "delta": [
+          {
+            "insert": "Here are the basics"
+          }
+        ]
+      },
+      {
+        "type": "text",
+        "attributes": {
+          "subtype": "checkbox",
+          "checkbox": null
+        },
+        "delta": [
+          {
+            "insert": "Click anywhere and just start typing."
+          }
+        ]
+      },
+      {
+        "type": "text",
+        "attributes": {
+          "subtype": "checkbox",
+          "checkbox": null
+        },
+        "delta": [
+          {
+            "insert": "Highlight",
+            "attributes": {
+              "backgroundColor": "0x6000BCF0"
+            }
+          },
+          {
+            "insert": " any text, and use the editing menu to "
+          },
+          {
+            "insert": "style",
+            "attributes": {
+              "italic": true
+            }
+          },
+          {
+            "insert": " "
+          },
+          {
+            "insert": "your",
+            "attributes": {
+              "bold": true
+            }
+          },
+          {
+            "insert": " "
+          },
+          {
+            "insert": "writing",
+            "attributes": {
+              "underline": true
+            }
+          },
+          {
+            "insert": " "
+          },
+          {
+            "insert": "however",
+            "attributes": {
+              "code": true
+            }
+          },
+          {
+            "insert": " you "
+          },
+          {
+            "insert": "like.",
+            "attributes": {
+              "strikethrough": true
+            }
+          }
+        ]
+      },
+      {
+        "type": "text",
+        "attributes": {
+          "subtype": "checkbox",
+          "checkbox": null
+        },
+        "delta": [
+          {
+            "insert": "As soon as you type "
+          },
+          {
+            "insert": "/",
+            "attributes": {
+              "code": true
+            }
+          },
+          {
+            "insert": " a menu will pop up. Select different types of content blocks you can add."
+          }
+        ]
+      },
+      {
+        "type": "text",
+        "attributes": {
+          "subtype": "checkbox",
+          "checkbox": null
+        },
+        "delta": [
+          {
+            "insert": "Type "
+          },
+          {
+            "insert": "/",
+            "attributes": {
+              "code": true
+            }
+          },
+          {
+            "insert": " followed by "
+          },
+          {
+            "insert": "/bullet",
+            "attributes": {
+              "code": true
+            }
+          },
+          {
+            "insert": " or "
+          },
+          {
+            "insert": "/c.",
+            "attributes": {
+              "code": true
+            }
+          }
+        ]
+      },
+      {
+        "type": "text",
+        "attributes": {
+          "subtype": "checkbox",
+          "checkbox": true
+        },
+        "delta": [
+          {
+            "insert": "Click "
+          },
+          {
+            "insert": "+ New Page ",
+            "attributes": {
+              "code": true
+            }
+          },
+          {
+            "insert": "button at the bottom of your sidebar to add a new page."
+          }
+        ]
+      },
+      {
+        "type": "text",
+        "attributes": {
+          "subtype": "checkbox",
+          "checkbox": null
+        },
+        "delta": [
+          {
+            "insert": "Click "
+          },
+          {
+            "insert": "+",
+            "attributes": {
+              "code": true
+            }
+          },
+          {
+            "insert": " next to any page title in the sidebar to quickly add a new subpage."
+          }
+        ]
+      },
+      {
+        "type": "text",
+        "attributes": {
+          "checkbox": null
+        },
+        "delta": []
+      },
+      {
+        "type": "text",
+        "attributes": {
+          "subtype": "heading",
+          "checkbox": null,
+          "heading": "h2"
+        },
+        "delta": [
+          {
+            "insert": "Markdown"
+          }
+        ]
+      },
+      {
+        "type": "text",
+        "attributes": {
+          "subtype": "number-list",
+          "number": 1,
+          "heading": null
+        },
+        "delta": [
+          {
+            "insert": "Heading "
+          }
+        ]
+      },
+      {
+        "type": "text",
+        "attributes": {
+          "subtype": "number-list",
+          "number": 2
+        },
+        "delta": [
+          {
+            "insert": "bold text",
+            "attributes": {
+              "bold": true,
+              "defaultFormating": true
+            }
+          }
+        ]
+      },
+      {
+        "type": "text",
+        "attributes": {
+          "subtype": "number-list",
+          "number": 3
+        },
+        "delta": [
+          {
+            "insert": "italicized text",
+            "attributes": {
+              "italic": true
+            }
+          }
+        ]
+      },
+      {
+        "type": "text",
+        "attributes": {
+          "subtype": "number-list",
+          "number": 4,
+          "number-list": null
+        },
+        "delta": [
+          {
+            "insert": "Ordered List"
+          }
+        ]
+      },
+      {
+        "type": "text",
+        "attributes": {
+          "number": 5,
+          "subtype": "number-list"
+        },
+        "delta": [
+          {
+            "insert": "code",
+            "attributes": {
+              "code": true
+            }
+          }
+        ]
+      },
+      {
+        "type": "text",
+        "attributes": {
+          "number": 6,
+          "subtype": "number-list"
+        },
+        "delta": [
+          {
+            "insert": "Strikethrough",
+            "attributes": {
+              "strikethrough": true
+            }
+          },
+          {
+            "retain": 1,
+            "attributes": {
+              "strikethrough": true
+            }
+          }
+        ]
+      },
+      {
+        "type": "text",
+        "attributes": {
+          "checkbox": null
+        },
+        "delta": []
+      },
+      {
+        "type": "text",
+        "attributes": {
+          "subtype": "heading",
+          "checkbox": null,
+          "heading": "h2"
+        },
+        "delta": [
+          {
+            "insert": "Have a question?"
+          }
+        ]
+      },
+      {
+        "type": "text",
+        "attributes": {
+          "subtype": "quote"
+        },
+        "delta": [
+          {
+            "insert": "Click "
+          },
+          {
+            "insert": "?",
+            "attributes": {
+              "code": true
+            }
+          },
+          {
+            "insert": " at the bottom right for help and support."
+          }
+        ]
+      },
+      {
+        "type": "text",
+        "delta": []
+      },
+      {
+        "type": "text",
+        "attributes": {
+          "subtype": "heading",
+          "heading": "h2"
+        },
+        "delta": [
+          {
+            "insert": "Like AppFlowy? Follow us:"
+          }
+        ]
+      },
+      {
+        "type": "text",
+        "attributes": {
+          "subtype": "bulleted-list",
+          "quote": null
+        },
+        "delta": [
+          {
+            "insert": "GitHub",
+            "attributes": {
+              "href": "https://github.com/AppFlowy-IO/AppFlowy"
+            }
+          }
+        ]
+      },
+      {
+        "type": "text",
+        "attributes": {
+          "subtype": "bulleted-list"
+        },
+        "delta": [
+          {
+            "insert": "Twitter: @appflowy"
+          }
+        ]
+      },
+      {
+        "type": "text",
+        "attributes": {
+          "subtype": "bulleted-list"
+        },
+        "delta": [
+          {
+            "insert": "Newsletter",
+            "attributes": {
+              "href": "https://blog-appflowy.ghost.io/"
+            }
+          }
+        ]
+      },
+      {
+        "type": "text",
+        "attributes": {
+          "subtype": null,
+          "heading": null
+        },
+        "delta": []
+      }
+    ]
+  }
+}

+ 19 - 6
frontend/rust-lib/flowy-document/src/editor/document.rs

@@ -1,11 +1,11 @@
 use bytes::Bytes;
 use flowy_error::{FlowyError, FlowyResult};
-use flowy_revision::{RevisionObjectDeserializer, RevisionObjectSerializer};
+use flowy_revision::{RevisionCompress, RevisionObjectDeserializer, RevisionObjectSerializer};
 use flowy_sync::entities::revision::Revision;
 use lib_ot::core::{
     Body, Extension, NodeDataBuilder, NodeOperation, NodeTree, NodeTreeContext, Selection, Transaction,
 };
-use lib_ot::text_delta::TextOperationBuilder;
+use lib_ot::text_delta::DeltaTextOperationBuilder;
 
 #[derive(Debug)]
 pub struct Document {
@@ -30,6 +30,11 @@ impl Document {
         }
     }
 
+    pub fn md5(&self) -> String {
+        // format!("{:x}", md5::compute(bytes))
+        "".to_owned()
+    }
+
     pub fn get_tree(&self) -> &NodeTree {
         &self.tree
     }
@@ -40,7 +45,7 @@ pub(crate) fn make_tree_context() -> NodeTreeContext {
 }
 
 pub fn initial_document_content() -> String {
-    let delta = TextOperationBuilder::new().insert("").build();
+    let delta = DeltaTextOperationBuilder::new().insert("").build();
     let node_data = NodeDataBuilder::new("text").insert_body(Body::Delta(delta)).build();
     let editor_node = NodeDataBuilder::new("editor").add_node_data(node_data).build();
     let node_operation = NodeOperation::Insert {
@@ -78,7 +83,7 @@ impl RevisionObjectDeserializer for DocumentRevisionSerde {
 
     fn deserialize_revisions(_object_id: &str, revisions: Vec<Revision>) -> FlowyResult<Self::Output> {
         let mut tree = NodeTree::new(make_tree_context());
-        let transaction = make_transaction_from_revisions(revisions)?;
+        let transaction = make_transaction_from_revisions(&revisions)?;
         let _ = tree.apply_transaction(transaction)?;
         let document = Document::new(tree);
         Result::<Document, FlowyError>::Ok(document)
@@ -87,12 +92,20 @@ impl RevisionObjectDeserializer for DocumentRevisionSerde {
 
 impl RevisionObjectSerializer for DocumentRevisionSerde {
     fn combine_revisions(revisions: Vec<Revision>) -> FlowyResult<Bytes> {
-        let transaction = make_transaction_from_revisions(revisions)?;
+        let transaction = make_transaction_from_revisions(&revisions)?;
         Ok(Bytes::from(transaction.to_bytes()?))
     }
 }
 
-fn make_transaction_from_revisions(revisions: Vec<Revision>) -> FlowyResult<Transaction> {
+pub(crate) struct DocumentRevisionCompress();
+impl RevisionCompress for DocumentRevisionCompress {
+    fn combine_revisions(&self, revisions: Vec<Revision>) -> FlowyResult<Bytes> {
+        DocumentRevisionSerde::combine_revisions(revisions)
+    }
+}
+
+#[tracing::instrument(level = "trace", skip_all, err)]
+pub fn make_transaction_from_revisions(revisions: &[Revision]) -> FlowyResult<Transaction> {
     let mut transaction = Transaction::new();
     for revision in revisions {
         let _ = transaction.compose(Transaction::from_bytes(&revision.bytes)?)?;

+ 54 - 17
frontend/rust-lib/flowy-document/src/editor/document_serde.rs

@@ -3,11 +3,12 @@ use crate::editor::document::Document;
 use bytes::Bytes;
 use flowy_error::FlowyResult;
 use lib_ot::core::{
-    AttributeHashMap, Body, Changeset, Extension, NodeData, NodeId, NodeOperation, NodeTree, Path, Selection,
-    Transaction,
+    AttributeHashMap, Body, Changeset, Extension, NodeData, NodeId, NodeOperation, NodeTree, NodeTreeContext, Path,
+    Selection, Transaction,
 };
-use lib_ot::text_delta::TextOperations;
-use serde::de::{self, MapAccess, Visitor};
+
+use lib_ot::text_delta::DeltaTextOperations;
+use serde::de::{self, MapAccess, Unexpected, Visitor};
 use serde::ser::{SerializeMap, SerializeSeq};
 use serde::{Deserialize, Deserializer, Serialize, Serializer};
 use std::fmt;
@@ -44,14 +45,14 @@ impl<'de> Deserialize<'de> for Document {
             where
                 M: MapAccess<'de>,
             {
-                let mut node_tree = None;
+                let mut document_node = None;
                 while let Some(key) = map.next_key()? {
                     match key {
                         "document" => {
-                            if node_tree.is_some() {
+                            if document_node.is_some() {
                                 return Err(de::Error::duplicate_field("document"));
                             }
-                            node_tree = Some(map.next_value::<NodeTree>()?)
+                            document_node = Some(map.next_value::<DocumentNode>()?)
                         }
                         s => {
                             return Err(de::Error::unknown_field(s, FIELDS));
@@ -59,8 +60,13 @@ impl<'de> Deserialize<'de> for Document {
                     }
                 }
 
-                match node_tree {
-                    Some(tree) => Ok(Document::new(tree)),
+                match document_node {
+                    Some(document_node) => {
+                        match NodeTree::from_node_data(document_node.into(), NodeTreeContext::default()) {
+                            Ok(tree) => Ok(Document::new(tree)),
+                            Err(err) => Err(de::Error::invalid_value(Unexpected::Other(&format!("{}", err)), &"")),
+                        }
+                    }
                     None => Err(de::Error::missing_field("document")),
                 }
             }
@@ -69,10 +75,20 @@ impl<'de> Deserialize<'de> for Document {
     }
 }
 
-#[derive(Debug)]
-struct DocumentContentSerializer<'a>(pub &'a Document);
+pub fn make_transaction_from_document_content(content: &str) -> FlowyResult<Transaction> {
+    let document_node: DocumentNode = serde_json::from_str::<DocumentContentDeserializer>(content)?.document;
+    let document_operation = DocumentOperation::Insert {
+        path: 0_usize.into(),
+        nodes: vec![document_node],
+    };
+    let mut document_transaction = DocumentTransaction::default();
+    document_transaction.operations.push(document_operation);
+    Ok(document_transaction.into())
+}
 
-#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct DocumentContentSerde {}
+
+#[derive(Debug, Clone, Default, Serialize, Deserialize)]
 pub struct DocumentTransaction {
     #[serde(default)]
     operations: Vec<DocumentOperation>,
@@ -161,8 +177,8 @@ pub enum DocumentOperation {
     #[serde(rename = "update_text")]
     UpdateText {
         path: Path,
-        delta: TextOperations,
-        inverted: TextOperations,
+        delta: DeltaTextOperations,
+        inverted: DeltaTextOperations,
     },
 }
 
@@ -230,20 +246,27 @@ pub struct DocumentNode {
     #[serde(default)]
     pub attributes: AttributeHashMap,
 
-    #[serde(skip_serializing_if = "TextOperations::is_empty")]
-    pub delta: TextOperations,
+    #[serde(skip_serializing_if = "DeltaTextOperations::is_empty")]
+    #[serde(default)]
+    pub delta: DeltaTextOperations,
 
     #[serde(skip_serializing_if = "Vec::is_empty")]
     #[serde(default)]
     pub children: Vec<DocumentNode>,
 }
 
+impl DocumentNode {
+    pub fn new() -> Self {
+        Self::default()
+    }
+}
+
 impl std::convert::From<NodeData> for DocumentNode {
     fn from(node_data: NodeData) -> Self {
         let delta = if let Body::Delta(operations) = node_data.body {
             operations
         } else {
-            TextOperations::default()
+            DeltaTextOperations::default()
         };
         DocumentNode {
             node_type: node_data.node_type,
@@ -265,6 +288,14 @@ impl std::convert::From<DocumentNode> for NodeData {
     }
 }
 
+#[derive(Debug, Deserialize)]
+struct DocumentContentDeserializer {
+    document: DocumentNode,
+}
+
+#[derive(Debug)]
+struct DocumentContentSerializer<'a>(pub &'a Document);
+
 impl<'a> Serialize for DocumentContentSerializer<'a> {
     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
     where
@@ -299,6 +330,12 @@ impl<'a> Serialize for DocumentContentSerializer<'a> {
 mod tests {
     use crate::editor::document::Document;
     use crate::editor::document_serde::DocumentTransaction;
+    use crate::editor::initial_read_me;
+
+    #[test]
+    fn load_read_me() {
+        let _ = initial_read_me();
+    }
 
     #[test]
     fn transaction_deserialize_update_text_operation_test() {

+ 28 - 10
frontend/rust-lib/flowy-document/src/editor/editor.rs

@@ -1,5 +1,6 @@
 use crate::editor::document::{Document, DocumentRevisionSerde};
 use crate::editor::document_serde::DocumentTransaction;
+use crate::editor::make_transaction_from_revisions;
 use crate::editor::queue::{Command, CommandSender, DocumentQueue};
 use crate::{DocumentEditor, DocumentUser};
 use bytes::Bytes;
@@ -17,6 +18,7 @@ pub struct AppFlowyDocumentEditor {
     #[allow(dead_code)]
     doc_id: String,
     command_sender: CommandSender,
+    rev_manager: Arc<RevisionManager>,
 }
 
 impl AppFlowyDocumentEditor {
@@ -28,9 +30,13 @@ impl AppFlowyDocumentEditor {
     ) -> FlowyResult<Arc<Self>> {
         let document = rev_manager.load::<DocumentRevisionSerde>(Some(cloud_service)).await?;
         let rev_manager = Arc::new(rev_manager);
-        let command_sender = spawn_edit_queue(user, rev_manager, document);
+        let command_sender = spawn_edit_queue(user, rev_manager.clone(), document);
         let doc_id = doc_id.to_string();
-        let editor = Arc::new(Self { doc_id, command_sender });
+        let editor = Arc::new(Self {
+            doc_id,
+            command_sender,
+            rev_manager,
+        });
         Ok(editor)
     }
 
@@ -53,6 +59,13 @@ impl AppFlowyDocumentEditor {
         let content = rx.await.map_err(internal_error)??;
         Ok(content)
     }
+
+    pub async fn duplicate_document(&self) -> FlowyResult<String> {
+        let revisions = self.rev_manager.load_revisions().await?;
+        let transaction = make_transaction_from_revisions(&revisions)?;
+        let json = transaction.to_json()?;
+        Ok(json)
+    }
 }
 
 fn spawn_edit_queue(
@@ -67,28 +80,33 @@ fn spawn_edit_queue(
 }
 
 impl DocumentEditor for Arc<AppFlowyDocumentEditor> {
+    fn close(&self) {}
+
     fn export(&self) -> FutureResult<String, FlowyError> {
         let this = self.clone();
         FutureResult::new(async move { this.get_content(false).await })
     }
 
-    fn compose_local_operations(&self, data: Bytes) -> FutureResult<(), FlowyError> {
+    fn duplicate(&self) -> FutureResult<String, FlowyError> {
         let this = self.clone();
-        FutureResult::new(async move {
-            let transaction = DocumentTransaction::from_bytes(data)?;
-            let _ = this.apply_transaction(transaction.into()).await?;
-            Ok(())
-        })
+        FutureResult::new(async move { this.duplicate_document().await })
     }
 
-    fn close(&self) {}
-
     fn receive_ws_data(&self, _data: ServerRevisionWSData) -> FutureResult<(), FlowyError> {
         FutureResult::new(async move { Ok(()) })
     }
 
     fn receive_ws_state(&self, _state: &WSConnectState) {}
 
+    fn compose_local_operations(&self, data: Bytes) -> FutureResult<(), FlowyError> {
+        let this = self.clone();
+        FutureResult::new(async move {
+            let transaction = DocumentTransaction::from_bytes(data)?;
+            let _ = this.apply_transaction(transaction.into()).await?;
+            Ok(())
+        })
+    }
+
     fn as_any(&self) -> &dyn Any {
         self
     }

+ 419 - 0
frontend/rust-lib/flowy-document/src/editor/migration/delta_migration.rs

@@ -0,0 +1,419 @@
+use crate::editor::{DocumentNode, DocumentOperation};
+use flowy_error::FlowyResult;
+
+use lib_ot::core::{AttributeHashMap, DeltaOperation, Insert, Transaction};
+use lib_ot::text_delta::{DeltaTextOperation, DeltaTextOperations};
+
+pub struct DeltaRevisionMigration();
+
+impl DeltaRevisionMigration {
+    pub fn run(delta: DeltaTextOperations) -> FlowyResult<Transaction> {
+        let migrate_background_attribute = |insert: &mut Insert<AttributeHashMap>| {
+            if let Some(Some(color)) = insert.attributes.get("background").map(|value| value.str_value()) {
+                insert.attributes.remove_key("background");
+                insert.attributes.insert("backgroundColor", color);
+            }
+        };
+        let migrate_strike_attribute = |insert: &mut Insert<AttributeHashMap>| {
+            if let Some(Some(_)) = insert.attributes.get("strike").map(|value| value.str_value()) {
+                insert.attributes.remove_key("strike");
+                insert.attributes.insert("strikethrough", true);
+            }
+        };
+
+        let migrate_link_attribute = |insert: &mut Insert<AttributeHashMap>| {
+            if let Some(Some(link)) = insert.attributes.get("link").map(|value| value.str_value()) {
+                insert.attributes.remove_key("link");
+                insert.attributes.insert("href", link);
+            }
+        };
+
+        let migrate_list_attribute =
+            |attribute_node: &mut DocumentNode, value: &str, number_list_number: &mut usize| {
+                if value == "unchecked" {
+                    *number_list_number = 0;
+                    attribute_node.attributes.insert("subtype", "checkbox");
+                    attribute_node.attributes.insert("checkbox", false);
+                }
+                if value == "checked" {
+                    *number_list_number = 0;
+                    attribute_node.attributes.insert("subtype", "checkbox");
+                    attribute_node.attributes.insert("checkbox", true);
+                }
+
+                if value == "bullet" {
+                    *number_list_number = 0;
+                    attribute_node.attributes.insert("subtype", "bulleted-list");
+                }
+
+                if value == "ordered" {
+                    *number_list_number += 1;
+                    attribute_node.attributes.insert("subtype", "number-list");
+                    attribute_node.attributes.insert("number", *number_list_number);
+                }
+            };
+
+        let generate_new_op_with_double_new_lines = |insert: &mut Insert<AttributeHashMap>| {
+            let pattern = "\n\n";
+            let mut new_ops = vec![];
+            if insert.s.as_str().contains(pattern) {
+                let insert_str = insert.s.clone();
+                let insert_strings = insert_str.split(pattern).map(|s| s.to_owned());
+                for (index, new_s) in insert_strings.enumerate() {
+                    if index == 0 {
+                        insert.s = new_s.into();
+                    } else {
+                        new_ops.push(DeltaOperation::Insert(Insert {
+                            s: new_s.into(),
+                            attributes: AttributeHashMap::default(),
+                        }));
+                    }
+                }
+            }
+            new_ops
+        };
+
+        let create_text_node = |ops: Vec<DeltaTextOperation>| {
+            let mut document_node = DocumentNode::new();
+            document_node.node_type = "text".to_owned();
+            ops.into_iter().for_each(|op| document_node.delta.add(op));
+            document_node
+        };
+
+        let transform_op = |mut insert: Insert<AttributeHashMap>| {
+            // Rename the attribute name from background to backgroundColor
+            migrate_background_attribute(&mut insert);
+            migrate_strike_attribute(&mut insert);
+            migrate_link_attribute(&mut insert);
+
+            let new_ops = generate_new_op_with_double_new_lines(&mut insert);
+            (DeltaOperation::Insert(insert), new_ops)
+        };
+        let mut index: usize = 0;
+        let mut number_list_number = 0;
+        let mut editor_node = DocumentNode::new();
+        editor_node.node_type = "editor".to_owned();
+
+        let mut transaction = Transaction::new();
+        transaction.push_operation(DocumentOperation::Insert {
+            path: 0.into(),
+            nodes: vec![editor_node],
+        });
+
+        let mut iter = delta.ops.into_iter().enumerate();
+        while let Some((_, op)) = iter.next() {
+            let mut document_node = create_text_node(vec![]);
+            let mut split_document_nodes = vec![];
+            match op {
+                DeltaOperation::Delete(_) => tracing::warn!("Should not contain delete operation"),
+                DeltaOperation::Retain(_) => tracing::warn!("Should not contain retain operation"),
+                DeltaOperation::Insert(insert) => {
+                    if insert.s.as_str() != "\n" {
+                        let (op, new_ops) = transform_op(insert);
+                        document_node.delta.add(op);
+                        if !new_ops.is_empty() {
+                            split_document_nodes.push(create_text_node(new_ops));
+                        }
+                    }
+
+                    while let Some((_, DeltaOperation::Insert(insert))) = iter.next() {
+                        if insert.s.as_str() != "\n" {
+                            let (op, new_ops) = transform_op(insert);
+                            document_node.delta.add(op);
+
+                            if !new_ops.is_empty() {
+                                split_document_nodes.push(create_text_node(new_ops));
+                            }
+                        } else {
+                            let attribute_node = match split_document_nodes.last_mut() {
+                                None => &mut document_node,
+                                Some(split_document_node) => split_document_node,
+                            };
+
+                            if let Some(value) = insert.attributes.get("header") {
+                                attribute_node.attributes.insert("subtype", "heading");
+                                if let Some(v) = value.int_value() {
+                                    number_list_number = 0;
+                                    attribute_node.attributes.insert("heading", format!("h{}", v));
+                                }
+                            }
+
+                            if insert.attributes.get("blockquote").is_some() {
+                                attribute_node.attributes.insert("subtype", "quote");
+                            }
+
+                            if let Some(value) = insert.attributes.get("list") {
+                                if let Some(s) = value.str_value() {
+                                    migrate_list_attribute(attribute_node, &s, &mut number_list_number);
+                                }
+                            }
+                            break;
+                        }
+                    }
+                }
+            }
+            let mut operations = vec![document_node];
+            operations.extend(split_document_nodes);
+            operations.into_iter().for_each(|node| {
+                // println!("{}", serde_json::to_string(&node).unwrap());
+                let operation = DocumentOperation::Insert {
+                    path: vec![0, index].into(),
+                    nodes: vec![node],
+                };
+                transaction.push_operation(operation);
+                index += 1;
+            });
+        }
+        Ok(transaction)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::editor::migration::delta_migration::DeltaRevisionMigration;
+    use crate::editor::Document;
+    use lib_ot::text_delta::DeltaTextOperations;
+
+    #[test]
+    fn transform_delta_to_transaction_test() {
+        let delta = DeltaTextOperations::from_json(DELTA_STR).unwrap();
+        let transaction = DeltaRevisionMigration::run(delta).unwrap();
+        let document = Document::from_transaction(transaction).unwrap();
+        let s = document.get_content(true).unwrap();
+        assert!(!s.is_empty());
+    }
+
+    const DELTA_STR: &str = r#"[
+    {
+        "insert": "\n👋 Welcome to AppFlowy!"
+    },
+    {
+        "insert": "\n",
+        "attributes": {
+            "header": 1
+        }
+    },
+    {
+        "insert": "\nHere are the basics"
+    },
+    {
+        "insert": "\n",
+        "attributes": {
+            "header": 2
+        }
+    },
+    {
+        "insert": "Click anywhere and just start typing"
+    },
+    {
+        "insert": "\n",
+        "attributes": {
+            "list": "unchecked"
+        }
+    },
+    {
+        "insert": "Highlight",
+        "attributes": {
+            "background": "$fff2cd"
+        }
+    },
+    {
+        "insert": " any text, and use the menu at the bottom to "
+    },
+    {
+        "insert": "style",
+        "attributes": {
+            "italic": true
+        }
+    },
+    {
+        "insert": " "
+    },
+    {
+        "insert": "your",
+        "attributes": {
+            "bold": true
+        }
+    },
+    {
+        "insert": " "
+    },
+    {
+        "insert": "writing",
+        "attributes": {
+            "underline": true
+        }
+    },
+    {
+        "insert": " "
+    },
+    {
+        "insert": "however",
+        "attributes": {
+            "code": true
+        }
+    },
+    {
+        "insert": " "
+    },
+    {
+        "insert": "you",
+        "attributes": {
+            "strike": true
+        }
+    },
+    {
+        "insert": " "
+    },
+    {
+        "insert": "like",
+        "attributes": {
+            "background": "$e8e0ff"
+        }
+    },
+    {
+        "insert": "\n",
+        "attributes": {
+            "list": "unchecked"
+        }
+    },
+    {
+        "insert": "Click "
+    },
+    {
+        "insert": "+ New Page",
+        "attributes": {
+            "background": "$defff1",
+            "bold": true
+        }
+    },
+    {
+        "insert": " button at the bottom of your sidebar to add a new page"
+    },
+    {
+        "insert": "\n",
+        "attributes": {
+            "list": "unchecked"
+        }
+    },
+    {
+        "insert": "Click the "
+    },
+    {
+        "insert": "'",
+        "attributes": {
+            "background": "$defff1"
+        }
+    },
+    {
+        "insert": "+",
+        "attributes": {
+            "background": "$defff1",
+            "bold": true
+        }
+    },
+    {
+        "insert": "'",
+        "attributes": {
+            "background": "$defff1"
+        }
+    },
+    {
+        "insert": "  next to any page title in the sidebar to quickly add a new subpage"
+    },
+    {
+        "insert": "\n",
+        "attributes": {
+            "list": "unchecked"
+        }
+    },
+    {
+        "insert": "\nHave a question? "
+    },
+    {
+        "insert": "\n",
+        "attributes": {
+            "header": 2
+        }
+    },
+    {
+        "insert": "Click the "
+    },
+    {
+        "insert": "'?'",
+        "attributes": {
+            "background": "$defff1",
+            "bold": true
+        }
+    },
+    {
+        "insert": " at the bottom right for help and support.\n\nLike AppFlowy? Follow us:"
+    },
+    {
+        "insert": "\n",
+        "attributes": {
+            "header": 2
+        }
+    },
+    {
+        "insert": "GitHub: https://github.com/AppFlowy-IO/appflowy"
+    },
+    {
+        "insert": "\n",
+        "attributes": {
+            "blockquote": true
+        }
+    },
+    {
+        "insert": "Twitter: https://twitter.com/appflowy"
+    },
+    {
+        "insert": "\n",
+        "attributes": {
+            "blockquote": true
+        }
+    },
+    {
+        "insert": "Newsletter: https://www.appflowy.io/blog"
+    },
+    {
+        "insert": "\n",
+        "attributes": {
+            "blockquote": true
+        }
+    },
+    {
+        "insert": "item 1"
+    },
+    {
+        "insert": "\n",
+        "attributes": {
+            "list": "ordered"
+        }
+    },
+    {
+        "insert": "item 2"
+    },
+    {
+        "insert": "\n",
+        "attributes": {
+            "list": "ordered"
+        }
+    },
+    {
+        "insert": "item3"
+    },
+    {
+        "insert": "\n",
+        "attributes": {
+            "list": "ordered"
+        }
+    },
+    {
+        "insert": "appflowy",
+        "attributes": {
+            "link": "https://www.appflowy.io/"
+        }
+    }
+]"#;
+}

+ 3 - 0
frontend/rust-lib/flowy-document/src/editor/migration/mod.rs

@@ -0,0 +1,3 @@
+mod delta_migration;
+
+pub use delta_migration::*;

+ 10 - 0
frontend/rust-lib/flowy-document/src/editor/mod.rs

@@ -2,7 +2,17 @@
 mod document;
 mod document_serde;
 mod editor;
+mod migration;
 mod queue;
 
 pub use document::*;
+pub use document_serde::*;
 pub use editor::*;
+pub use migration::*;
+
+#[inline]
+pub fn initial_read_me() -> String {
+    let document_content = include_str!("READ_ME.json");
+    let transaction = make_transaction_from_document_content(document_content).unwrap();
+    transaction.to_json().unwrap()
+}

+ 18 - 1
frontend/rust-lib/flowy-document/src/editor/queue.rs

@@ -1,13 +1,17 @@
 use crate::editor::document::Document;
 use crate::DocumentUser;
 use async_stream::stream;
+use bytes::Bytes;
 use flowy_error::FlowyError;
 use flowy_revision::RevisionManager;
+use flowy_sync::entities::revision::{RevId, Revision};
 use futures::stream::StreamExt;
 use lib_ot::core::Transaction;
+
 use std::sync::Arc;
 use tokio::sync::mpsc::{Receiver, Sender};
 use tokio::sync::{oneshot, RwLock};
+
 pub struct DocumentQueue {
     #[allow(dead_code)]
     user: Arc<dyn DocumentUser>,
@@ -56,7 +60,10 @@ impl DocumentQueue {
     async fn handle_command(&self, command: Command) -> Result<(), FlowyError> {
         match command {
             Command::ComposeTransaction { transaction, ret } => {
-                self.document.write().await.apply_transaction(transaction)?;
+                self.document.write().await.apply_transaction(transaction.clone())?;
+                let _ = self
+                    .save_local_operations(transaction, self.document.read().await.md5())
+                    .await?;
                 let _ = ret.send(Ok(()));
             }
             Command::GetDocumentContent { pretty, ret } => {
@@ -66,6 +73,16 @@ impl DocumentQueue {
         }
         Ok(())
     }
+
+    #[tracing::instrument(level = "trace", skip(self, transaction, md5), err)]
+    async fn save_local_operations(&self, transaction: Transaction, md5: String) -> Result<RevId, FlowyError> {
+        let bytes = Bytes::from(transaction.to_bytes()?);
+        let (base_rev_id, rev_id) = self.rev_manager.next_rev_id_pair();
+        let user_id = self.user.user_id()?;
+        let revision = Revision::new(&self.rev_manager.object_id, base_rev_id, rev_id, bytes, &user_id, md5);
+        let _ = self.rev_manager.add_local_revision(&revision).await?;
+        Ok(rev_id.into())
+    }
 }
 
 pub(crate) type CommandSender = Sender<Command>;

+ 30 - 0
frontend/rust-lib/flowy-document/src/entities.rs

@@ -74,12 +74,41 @@ pub struct ExportPayloadPB {
 
     #[pb(index = 2)]
     pub export_type: ExportType,
+
+    #[pb(index = 3)]
+    pub document_version: DocumentVersionPB,
+}
+
+#[derive(PartialEq, Debug, ProtoBuf_Enum, Clone)]
+pub enum DocumentVersionPB {
+    /// this version's content of the document is build from `Delta`. It uses
+    /// `DeltaDocumentEditor`.
+    V0 = 0,
+    /// this version's content of the document is build from `NodeTree`. It uses
+    /// `AppFlowyDocumentEditor`
+    V1 = 1,
+}
+
+impl std::default::Default for DocumentVersionPB {
+    fn default() -> Self {
+        Self::V0
+    }
+}
+
+#[derive(Default, ProtoBuf)]
+pub struct OpenDocumentContextPB {
+    #[pb(index = 1)]
+    pub document_id: String,
+
+    #[pb(index = 2)]
+    pub document_version: DocumentVersionPB,
 }
 
 #[derive(Default, Debug)]
 pub struct ExportParams {
     pub view_id: String,
     pub export_type: ExportType,
+    pub document_version: DocumentVersionPB,
 }
 
 impl TryInto<ExportParams> for ExportPayloadPB {
@@ -88,6 +117,7 @@ impl TryInto<ExportParams> for ExportPayloadPB {
         Ok(ExportParams {
             view_id: self.view_id,
             export_type: self.export_type,
+            document_version: self.document_version,
         })
     }
 }

+ 10 - 8
frontend/rust-lib/flowy-document/src/event_handler.rs

@@ -1,21 +1,23 @@
-use crate::entities::{DocumentSnapshotPB, EditParams, EditPayloadPB, ExportDataPB, ExportParams, ExportPayloadPB};
+use crate::entities::{
+    DocumentSnapshotPB, EditParams, EditPayloadPB, ExportDataPB, ExportParams, ExportPayloadPB, OpenDocumentContextPB,
+};
 use crate::DocumentManager;
 use flowy_error::FlowyError;
-use flowy_sync::entities::document::DocumentIdPB;
+
 use lib_dispatch::prelude::{data_result, AppData, Data, DataResult};
 use std::convert::TryInto;
 use std::sync::Arc;
 
 pub(crate) async fn get_document_handler(
-    data: Data<DocumentIdPB>,
+    data: Data<OpenDocumentContextPB>,
     manager: AppData<Arc<DocumentManager>>,
 ) -> DataResult<DocumentSnapshotPB, FlowyError> {
-    let document_id: DocumentIdPB = data.into_inner();
-    let editor = manager.open_document_editor(&document_id).await?;
-    let operations_str = editor.export().await?;
+    let context: OpenDocumentContextPB = data.into_inner();
+    let editor = manager.open_document_editor(&context.document_id).await?;
+    let document_data = editor.export().await?;
     data_result(DocumentSnapshotPB {
-        doc_id: document_id.into(),
-        snapshot: operations_str,
+        doc_id: context.document_id,
+        snapshot: document_data,
     })
 }
 

+ 1 - 1
frontend/rust-lib/flowy-document/src/event_map.rs

@@ -19,7 +19,7 @@ pub fn create(document_manager: Arc<DocumentManager>) -> Module {
 #[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)]
 #[event_err = "FlowyError"]
 pub enum DocumentEvent {
-    #[event(input = "DocumentIdPB", output = "DocumentSnapshotPB")]
+    #[event(input = "OpenDocumentContextPB", output = "DocumentSnapshotPB")]
     GetDocument = 0,
 
     #[event(input = "EditPayloadPB")]

+ 2 - 1
frontend/rust-lib/flowy-document/src/lib.rs

@@ -1,4 +1,4 @@
-mod entities;
+pub mod entities;
 mod event_handler;
 pub mod event_map;
 pub mod manager;
@@ -6,6 +6,7 @@ pub mod manager;
 pub mod editor;
 pub mod old_editor;
 pub mod protobuf;
+mod services;
 
 pub use manager::*;
 pub mod errors {

+ 104 - 53
frontend/rust-lib/flowy-document/src/manager.rs

@@ -1,23 +1,23 @@
-use crate::editor::{initial_document_content, AppFlowyDocumentEditor};
-use crate::entities::EditParams;
-use crate::old_editor::editor::{DeltaDocumentEditor, DocumentRevisionCompress};
+use crate::editor::{initial_document_content, AppFlowyDocumentEditor, DocumentRevisionCompress};
+use crate::entities::{DocumentVersionPB, EditParams};
+use crate::old_editor::editor::{DeltaDocumentEditor, DeltaDocumentRevisionCompress};
+use crate::services::DocumentPersistence;
 use crate::{errors::FlowyError, DocumentCloudService};
 use bytes::Bytes;
 use dashmap::DashMap;
 use flowy_database::ConnectionPool;
 use flowy_error::FlowyResult;
-use flowy_revision::disk::SQLiteDocumentRevisionPersistence;
+use flowy_revision::disk::{SQLiteDeltaDocumentRevisionPersistence, SQLiteDocumentRevisionPersistence};
 use flowy_revision::{
     RevisionCloudService, RevisionManager, RevisionPersistence, RevisionWebSocket, SQLiteRevisionSnapshotPersistence,
 };
-use flowy_sync::client_document::initial_old_document_content;
+use flowy_sync::client_document::initial_delta_document_content;
 use flowy_sync::entities::{
-    document::{DocumentIdPB, DocumentOperationsPB},
+    document::DocumentIdPB,
     revision::{md5, RepeatedRevision, Revision},
     ws_data::ServerRevisionWSData,
 };
 use lib_infra::future::FutureResult;
-
 use lib_ws::WSConnectState;
 use std::any::Any;
 use std::{convert::TryInto, sync::Arc};
@@ -26,17 +26,31 @@ pub trait DocumentUser: Send + Sync {
     fn user_dir(&self) -> Result<String, FlowyError>;
     fn user_id(&self) -> Result<String, FlowyError>;
     fn token(&self) -> Result<String, FlowyError>;
+}
+
+pub trait DocumentDatabase: Send + Sync {
     fn db_pool(&self) -> Result<Arc<ConnectionPool>, FlowyError>;
 }
 
 pub trait DocumentEditor: Send + Sync {
-    fn export(&self) -> FutureResult<String, FlowyError>;
-    fn compose_local_operations(&self, data: Bytes) -> FutureResult<(), FlowyError>;
+    /// Called when the document get closed
     fn close(&self);
 
+    /// Exports the document content. The content is encoded in the corresponding
+    /// editor data format.
+    fn export(&self) -> FutureResult<String, FlowyError>;
+
+    /// Duplicate the document inner data into String
+    fn duplicate(&self) -> FutureResult<String, FlowyError>;
+
     fn receive_ws_data(&self, data: ServerRevisionWSData) -> FutureResult<(), FlowyError>;
+
     fn receive_ws_state(&self, state: &WSConnectState);
 
+    /// Receives the local operations made by the user input. The operations are encoded
+    /// in binary format.
+    fn compose_local_operations(&self, data: Bytes) -> FutureResult<(), FlowyError>;
+
     /// Returns the `Any` reference that can be used to downcast back to the original,
     /// concrete type.
     ///
@@ -50,7 +64,15 @@ pub trait DocumentEditor: Send + Sync {
 
 #[derive(Clone, Debug)]
 pub struct DocumentConfig {
-    pub use_new_editor: bool,
+    pub version: DocumentVersionPB,
+}
+
+impl std::default::Default for DocumentConfig {
+    fn default() -> Self {
+        Self {
+            version: DocumentVersionPB::V1,
+        }
+    }
 }
 
 pub struct DocumentManager {
@@ -58,6 +80,8 @@ pub struct DocumentManager {
     rev_web_socket: Arc<dyn RevisionWebSocket>,
     editor_map: Arc<DocumentEditorMap>,
     user: Arc<dyn DocumentUser>,
+    persistence: Arc<DocumentPersistence>,
+    #[allow(dead_code)]
     config: DocumentConfig,
 }
 
@@ -65,6 +89,7 @@ impl DocumentManager {
     pub fn new(
         cloud_service: Arc<dyn DocumentCloudService>,
         document_user: Arc<dyn DocumentUser>,
+        database: Arc<dyn DocumentDatabase>,
         rev_web_socket: Arc<dyn RevisionWebSocket>,
         config: DocumentConfig,
     ) -> Self {
@@ -73,24 +98,31 @@ impl DocumentManager {
             rev_web_socket,
             editor_map: Arc::new(DocumentEditorMap::new()),
             user: document_user,
+            persistence: Arc::new(DocumentPersistence::new(database)),
             config,
         }
     }
 
-    pub fn init(&self) -> FlowyResult<()> {
+    /// Called immediately after the application launched with the user sign in/sign up.
+    #[tracing::instrument(level = "trace", skip_all, err)]
+    pub async fn initialize(&self, user_id: &str) -> FlowyResult<()> {
+        let _ = self.persistence.initialize(user_id)?;
         listen_ws_state_changed(self.rev_web_socket.clone(), self.editor_map.clone());
+        Ok(())
+    }
 
+    pub async fn initialize_with_new_user(&self, _user_id: &str, _token: &str) -> FlowyResult<()> {
         Ok(())
     }
 
-    #[tracing::instrument(level = "trace", skip(self, editor_id), fields(editor_id), err)]
+    #[tracing::instrument(level = "trace", skip_all, fields(document_id), err)]
     pub async fn open_document_editor<T: AsRef<str>>(
         &self,
-        editor_id: T,
+        document_id: T,
     ) -> Result<Arc<dyn DocumentEditor>, FlowyError> {
-        let editor_id = editor_id.as_ref();
-        tracing::Span::current().record("editor_id", &editor_id);
-        self.init_document_editor(editor_id).await
+        let document_id = document_id.as_ref();
+        tracing::Span::current().record("document_id", &document_id);
+        self.init_document_editor(document_id).await
     }
 
     #[tracing::instrument(level = "trace", skip(self, editor_id), fields(editor_id), err)]
@@ -101,22 +133,6 @@ impl DocumentManager {
         Ok(())
     }
 
-    #[tracing::instrument(level = "debug", skip(self, payload), err)]
-    pub async fn receive_local_operations(
-        &self,
-        payload: DocumentOperationsPB,
-    ) -> Result<DocumentOperationsPB, FlowyError> {
-        let editor = self.get_document_editor(&payload.doc_id).await?;
-        let _ = editor
-            .compose_local_operations(Bytes::from(payload.operations_str))
-            .await?;
-        let operations_str = editor.export().await?;
-        Ok(DocumentOperationsPB {
-            doc_id: payload.doc_id.clone(),
-            operations_str,
-        })
-    }
-
     pub async fn apply_edit(&self, params: EditParams) -> FlowyResult<()> {
         let editor = self.get_document_editor(&params.doc_id).await?;
         let _ = editor.compose_local_operations(Bytes::from(params.operations)).await?;
@@ -125,9 +141,9 @@ impl DocumentManager {
 
     pub async fn create_document<T: AsRef<str>>(&self, doc_id: T, revisions: RepeatedRevision) -> FlowyResult<()> {
         let doc_id = doc_id.as_ref().to_owned();
-        let db_pool = self.user.db_pool()?;
+        let db_pool = self.persistence.database.db_pool()?;
         // Maybe we could save the document to disk without creating the RevisionManager
-        let rev_manager = self.make_document_rev_manager(&doc_id, db_pool)?;
+        let rev_manager = self.make_rev_manager(&doc_id, db_pool)?;
         let _ = rev_manager.reset_object(revisions).await?;
         Ok(())
     }
@@ -149,10 +165,9 @@ impl DocumentManager {
     }
 
     pub fn initial_document_content(&self) -> String {
-        if self.config.use_new_editor {
-            initial_document_content()
-        } else {
-            initial_old_document_content()
+        match self.config.version {
+            DocumentVersionPB::V0 => initial_delta_document_content(),
+            DocumentVersionPB::V1 => initial_document_content(),
         }
     }
 }
@@ -168,7 +183,11 @@ impl DocumentManager {
     ///
     async fn get_document_editor(&self, doc_id: &str) -> FlowyResult<Arc<dyn DocumentEditor>> {
         match self.editor_map.get(doc_id) {
-            None => self.init_document_editor(doc_id).await,
+            None => {
+                //
+                tracing::warn!("Should call init_document_editor first");
+                self.init_document_editor(doc_id).await
+            }
             Some(editor) => Ok(editor),
         }
     }
@@ -184,25 +203,39 @@ impl DocumentManager {
     ///
     #[tracing::instrument(level = "trace", skip(self), err)]
     pub async fn init_document_editor(&self, doc_id: &str) -> Result<Arc<dyn DocumentEditor>, FlowyError> {
-        let pool = self.user.db_pool()?;
+        let pool = self.persistence.database.db_pool()?;
         let user = self.user.clone();
         let token = self.user.token()?;
-        let rev_manager = self.make_document_rev_manager(doc_id, pool.clone())?;
         let cloud_service = Arc::new(DocumentRevisionCloudService {
             token,
             server: self.cloud_service.clone(),
         });
 
-        let editor: Arc<dyn DocumentEditor> = if self.config.use_new_editor {
-            let editor = AppFlowyDocumentEditor::new(doc_id, user, rev_manager, cloud_service).await?;
-            Arc::new(editor)
-        } else {
-            let editor =
-                DeltaDocumentEditor::new(doc_id, user, rev_manager, self.rev_web_socket.clone(), cloud_service).await?;
-            Arc::new(editor)
-        };
-        self.editor_map.insert(doc_id, editor.clone());
-        Ok(editor)
+        match self.config.version {
+            DocumentVersionPB::V0 => {
+                let rev_manager = self.make_delta_document_rev_manager(doc_id, pool.clone())?;
+                let editor: Arc<dyn DocumentEditor> = Arc::new(
+                    DeltaDocumentEditor::new(doc_id, user, rev_manager, self.rev_web_socket.clone(), cloud_service)
+                        .await?,
+                );
+                self.editor_map.insert(doc_id, editor.clone());
+                Ok(editor)
+            }
+            DocumentVersionPB::V1 => {
+                let rev_manager = self.make_document_rev_manager(doc_id, pool.clone())?;
+                let editor: Arc<dyn DocumentEditor> =
+                    Arc::new(AppFlowyDocumentEditor::new(doc_id, user, rev_manager, cloud_service).await?);
+                self.editor_map.insert(doc_id, editor.clone());
+                Ok(editor)
+            }
+        }
+    }
+
+    fn make_rev_manager(&self, doc_id: &str, pool: Arc<ConnectionPool>) -> Result<RevisionManager, FlowyError> {
+        match self.config.version {
+            DocumentVersionPB::V0 => self.make_delta_document_rev_manager(doc_id, pool),
+            DocumentVersionPB::V1 => self.make_document_rev_manager(doc_id, pool),
+        }
     }
 
     fn make_document_rev_manager(
@@ -215,13 +248,31 @@ impl DocumentManager {
         let rev_persistence = RevisionPersistence::new(&user_id, doc_id, disk_cache);
         // let history_persistence = SQLiteRevisionHistoryPersistence::new(doc_id, pool.clone());
         let snapshot_persistence = SQLiteRevisionSnapshotPersistence::new(doc_id, pool);
-        let rev_compactor = DocumentRevisionCompress();
+        Ok(RevisionManager::new(
+            &user_id,
+            doc_id,
+            rev_persistence,
+            DocumentRevisionCompress(),
+            // history_persistence,
+            snapshot_persistence,
+        ))
+    }
 
+    fn make_delta_document_rev_manager(
+        &self,
+        doc_id: &str,
+        pool: Arc<ConnectionPool>,
+    ) -> Result<RevisionManager, FlowyError> {
+        let user_id = self.user.user_id()?;
+        let disk_cache = SQLiteDeltaDocumentRevisionPersistence::new(&user_id, pool.clone());
+        let rev_persistence = RevisionPersistence::new(&user_id, doc_id, disk_cache);
+        // let history_persistence = SQLiteRevisionHistoryPersistence::new(doc_id, pool.clone());
+        let snapshot_persistence = SQLiteRevisionSnapshotPersistence::new(doc_id, pool);
         Ok(RevisionManager::new(
             &user_id,
             doc_id,
             rev_persistence,
-            rev_compactor,
+            DeltaDocumentRevisionCompress(),
             // history_persistence,
             snapshot_persistence,
         ))

+ 28 - 24
frontend/rust-lib/flowy-document/src/old_editor/editor.rs

@@ -18,7 +18,7 @@ use lib_infra::future::FutureResult;
 use lib_ot::core::{AttributeEntry, AttributeHashMap};
 use lib_ot::{
     core::{DeltaOperation, Interval},
-    text_delta::TextOperations,
+    text_delta::DeltaTextOperations,
 };
 use lib_ws::WSConnectState;
 use std::any::Any;
@@ -46,7 +46,7 @@ impl DeltaDocumentEditor {
         let document = rev_manager
             .load::<DeltaDocumentRevisionSerde>(Some(cloud_service))
             .await?;
-        let operations = TextOperations::from_bytes(&document.content)?;
+        let operations = DeltaTextOperations::from_bytes(&document.content)?;
         let rev_manager = Arc::new(rev_manager);
         let doc_id = doc_id.to_string();
         let user_id = user.user_id()?;
@@ -147,6 +147,11 @@ impl DeltaDocumentEditor {
 }
 
 impl DocumentEditor for Arc<DeltaDocumentEditor> {
+    fn close(&self) {
+        #[cfg(feature = "sync")]
+        self.ws_manager.stop();
+    }
+
     fn export(&self) -> FutureResult<String, FlowyError> {
         let (ret, rx) = oneshot::channel::<CollaborateResult<String>>();
         let msg = EditorCommand::GetOperationsString { ret };
@@ -158,22 +163,8 @@ impl DocumentEditor for Arc<DeltaDocumentEditor> {
         })
     }
 
-    fn compose_local_operations(&self, data: Bytes) -> FutureResult<(), FlowyError> {
-        let edit_cmd_tx = self.edit_cmd_tx.clone();
-        FutureResult::new(async move {
-            let operations = TextOperations::from_bytes(&data)?;
-            let (ret, rx) = oneshot::channel::<CollaborateResult<()>>();
-            let msg = EditorCommand::ComposeLocalOperations { operations, ret };
-
-            let _ = edit_cmd_tx.send(msg).await;
-            let _ = rx.await.map_err(internal_error)??;
-            Ok(())
-        })
-    }
-
-    fn close(&self) {
-        #[cfg(feature = "sync")]
-        self.ws_manager.stop();
+    fn duplicate(&self) -> FutureResult<String, FlowyError> {
+        self.export()
     }
 
     #[allow(unused_variables)]
@@ -193,6 +184,19 @@ impl DocumentEditor for Arc<DeltaDocumentEditor> {
         self.ws_manager.connect_state_changed(state.clone());
     }
 
+    fn compose_local_operations(&self, data: Bytes) -> FutureResult<(), FlowyError> {
+        let edit_cmd_tx = self.edit_cmd_tx.clone();
+        FutureResult::new(async move {
+            let operations = DeltaTextOperations::from_bytes(&data)?;
+            let (ret, rx) = oneshot::channel::<CollaborateResult<()>>();
+            let msg = EditorCommand::ComposeLocalOperations { operations, ret };
+
+            let _ = edit_cmd_tx.send(msg).await;
+            let _ = rx.await.map_err(internal_error)??;
+            Ok(())
+        })
+    }
+
     fn as_any(&self) -> &dyn Any {
         self
     }
@@ -207,7 +211,7 @@ impl std::ops::Drop for DeltaDocumentEditor {
 fn spawn_edit_queue(
     user: Arc<dyn DocumentUser>,
     rev_manager: Arc<RevisionManager>,
-    delta: TextOperations,
+    delta: DeltaTextOperations,
 ) -> EditorCommandSender {
     let (sender, receiver) = mpsc::channel(1000);
     let edit_queue = EditDocumentQueue::new(user, rev_manager, delta, receiver);
@@ -226,8 +230,8 @@ fn spawn_edit_queue(
 
 #[cfg(feature = "flowy_unit_test")]
 impl DeltaDocumentEditor {
-    pub async fn document_operations(&self) -> FlowyResult<TextOperations> {
-        let (ret, rx) = oneshot::channel::<CollaborateResult<TextOperations>>();
+    pub async fn document_operations(&self) -> FlowyResult<DeltaTextOperations> {
+        let (ret, rx) = oneshot::channel::<CollaborateResult<DeltaTextOperations>>();
         let msg = EditorCommand::GetOperations { ret };
         let _ = self.edit_cmd_tx.send(msg).await;
         let delta = rx.await.map_err(internal_error)??;
@@ -264,8 +268,8 @@ impl RevisionObjectSerializer for DeltaDocumentRevisionSerde {
     }
 }
 
-pub(crate) struct DocumentRevisionCompress();
-impl RevisionCompress for DocumentRevisionCompress {
+pub(crate) struct DeltaDocumentRevisionCompress();
+impl RevisionCompress for DeltaDocumentRevisionCompress {
     fn combine_revisions(&self, revisions: Vec<Revision>) -> FlowyResult<Bytes> {
         DeltaDocumentRevisionSerde::combine_revisions(revisions)
     }
@@ -273,7 +277,7 @@ impl RevisionCompress for DocumentRevisionCompress {
 
 // quill-editor requires the delta should end with '\n' and only contains the
 // insert operation. The function, correct_delta maybe be removed in the future.
-fn correct_delta(delta: &mut TextOperations) {
+fn correct_delta(delta: &mut DeltaTextOperations) {
     if let Some(op) = delta.ops.last() {
         let op_data = op.get_data();
         if !op_data.ends_with('\n') {

+ 14 - 14
frontend/rust-lib/flowy-document/src/old_editor/queue.rs

@@ -1,4 +1,4 @@
-use crate::old_editor::web_socket::DocumentResolveOperations;
+use crate::old_editor::web_socket::DeltaDocumentResolveOperations;
 use crate::DocumentUser;
 use async_stream::stream;
 use flowy_error::FlowyError;
@@ -12,7 +12,7 @@ use futures::stream::StreamExt;
 use lib_ot::core::AttributeEntry;
 use lib_ot::{
     core::{Interval, OperationTransform},
-    text_delta::TextOperations,
+    text_delta::DeltaTextOperations,
 };
 use std::sync::Arc;
 use tokio::sync::mpsc::{Receiver, Sender};
@@ -31,7 +31,7 @@ impl EditDocumentQueue {
     pub(crate) fn new(
         user: Arc<dyn DocumentUser>,
         rev_manager: Arc<RevisionManager>,
-        operations: TextOperations,
+        operations: DeltaTextOperations,
         receiver: EditorCommandReceiver,
     ) -> Self {
         let document = Arc::new(RwLock::new(ClientDocument::from_operations(operations)));
@@ -91,8 +91,8 @@ impl EditDocumentQueue {
             EditorCommand::TransformOperations { operations, ret } => {
                 let f = || async {
                     let read_guard = self.document.read().await;
-                    let mut server_operations: Option<DocumentResolveOperations> = None;
-                    let client_operations: TextOperations;
+                    let mut server_operations: Option<DeltaDocumentResolveOperations> = None;
+                    let client_operations: DeltaTextOperations;
 
                     if read_guard.is_empty() {
                         // Do nothing
@@ -100,11 +100,11 @@ impl EditDocumentQueue {
                     } else {
                         let (s_prime, c_prime) = read_guard.get_operations().transform(&operations)?;
                         client_operations = c_prime;
-                        server_operations = Some(DocumentResolveOperations(s_prime));
+                        server_operations = Some(DeltaDocumentResolveOperations(s_prime));
                     }
                     drop(read_guard);
                     Ok::<TextTransformOperations, CollaborateError>(TransformOperations {
-                        client_operations: DocumentResolveOperations(client_operations),
+                        client_operations: DeltaDocumentResolveOperations(client_operations),
                         server_operations,
                     })
                 };
@@ -174,7 +174,7 @@ impl EditDocumentQueue {
         Ok(())
     }
 
-    async fn save_local_operations(&self, operations: TextOperations, md5: String) -> Result<RevId, FlowyError> {
+    async fn save_local_operations(&self, operations: DeltaTextOperations, md5: String) -> Result<RevId, FlowyError> {
         let bytes = operations.json_bytes();
         let (base_rev_id, rev_id) = self.rev_manager.next_rev_id_pair();
         let user_id = self.user.user_id()?;
@@ -184,26 +184,26 @@ impl EditDocumentQueue {
     }
 }
 
-pub type TextTransformOperations = TransformOperations<DocumentResolveOperations>;
+pub type TextTransformOperations = TransformOperations<DeltaDocumentResolveOperations>;
 pub(crate) type EditorCommandSender = Sender<EditorCommand>;
 pub(crate) type EditorCommandReceiver = Receiver<EditorCommand>;
 pub(crate) type Ret<T> = oneshot::Sender<Result<T, CollaborateError>>;
 
 pub(crate) enum EditorCommand {
     ComposeLocalOperations {
-        operations: TextOperations,
+        operations: DeltaTextOperations,
         ret: Ret<()>,
     },
     ComposeRemoteOperation {
-        client_operations: TextOperations,
+        client_operations: DeltaTextOperations,
         ret: Ret<OperationsMD5>,
     },
     ResetOperations {
-        operations: TextOperations,
+        operations: DeltaTextOperations,
         ret: Ret<OperationsMD5>,
     },
     TransformOperations {
-        operations: TextOperations,
+        operations: DeltaTextOperations,
         ret: Ret<TextTransformOperations>,
     },
     Insert {
@@ -242,7 +242,7 @@ pub(crate) enum EditorCommand {
     },
     #[allow(dead_code)]
     GetOperations {
-        ret: Ret<TextOperations>,
+        ret: Ret<DeltaTextOperations>,
     },
 }
 

+ 22 - 14
frontend/rust-lib/flowy-document/src/old_editor/web_socket.rs

@@ -13,33 +13,35 @@ use flowy_sync::{
     errors::CollaborateResult,
 };
 use lib_infra::future::{BoxResultFuture, FutureResult};
-use lib_ot::text_delta::TextOperations;
+use lib_ot::text_delta::DeltaTextOperations;
 use lib_ws::WSConnectState;
 use std::{sync::Arc, time::Duration};
 use tokio::sync::{broadcast, oneshot};
 
 #[derive(Clone)]
-pub struct DocumentResolveOperations(pub TextOperations);
+pub struct DeltaDocumentResolveOperations(pub DeltaTextOperations);
 
-impl OperationsDeserializer<DocumentResolveOperations> for DocumentResolveOperations {
-    fn deserialize_revisions(revisions: Vec<Revision>) -> FlowyResult<DocumentResolveOperations> {
-        Ok(DocumentResolveOperations(make_operations_from_revisions(revisions)?))
+impl OperationsDeserializer<DeltaDocumentResolveOperations> for DeltaDocumentResolveOperations {
+    fn deserialize_revisions(revisions: Vec<Revision>) -> FlowyResult<DeltaDocumentResolveOperations> {
+        Ok(DeltaDocumentResolveOperations(make_operations_from_revisions(
+            revisions,
+        )?))
     }
 }
 
-impl OperationsSerializer for DocumentResolveOperations {
+impl OperationsSerializer for DeltaDocumentResolveOperations {
     fn serialize_operations(&self) -> Bytes {
         self.0.json_bytes()
     }
 }
 
-impl DocumentResolveOperations {
-    pub fn into_inner(self) -> TextOperations {
+impl DeltaDocumentResolveOperations {
+    pub fn into_inner(self) -> DeltaTextOperations {
         self.0
     }
 }
 
-pub type DocumentConflictController = ConflictController<DocumentResolveOperations>;
+pub type DocumentConflictController = ConflictController<DeltaDocumentResolveOperations>;
 
 #[allow(dead_code)]
 pub(crate) async fn make_document_ws_manager(
@@ -129,8 +131,11 @@ struct DocumentConflictResolver {
     edit_cmd_tx: EditorCommandSender,
 }
 
-impl ConflictResolver<DocumentResolveOperations> for DocumentConflictResolver {
-    fn compose_operations(&self, operations: DocumentResolveOperations) -> BoxResultFuture<OperationsMD5, FlowyError> {
+impl ConflictResolver<DeltaDocumentResolveOperations> for DocumentConflictResolver {
+    fn compose_operations(
+        &self,
+        operations: DeltaDocumentResolveOperations,
+    ) -> BoxResultFuture<OperationsMD5, FlowyError> {
         let tx = self.edit_cmd_tx.clone();
         let operations = operations.into_inner();
         Box::pin(async move {
@@ -150,8 +155,8 @@ impl ConflictResolver<DocumentResolveOperations> for DocumentConflictResolver {
 
     fn transform_operations(
         &self,
-        operations: DocumentResolveOperations,
-    ) -> BoxResultFuture<TransformOperations<DocumentResolveOperations>, FlowyError> {
+        operations: DeltaDocumentResolveOperations,
+    ) -> BoxResultFuture<TransformOperations<DeltaDocumentResolveOperations>, FlowyError> {
         let tx = self.edit_cmd_tx.clone();
         let operations = operations.into_inner();
         Box::pin(async move {
@@ -166,7 +171,10 @@ impl ConflictResolver<DocumentResolveOperations> for DocumentConflictResolver {
         })
     }
 
-    fn reset_operations(&self, operations: DocumentResolveOperations) -> BoxResultFuture<OperationsMD5, FlowyError> {
+    fn reset_operations(
+        &self,
+        operations: DeltaDocumentResolveOperations,
+    ) -> BoxResultFuture<OperationsMD5, FlowyError> {
         let tx = self.edit_cmd_tx.clone();
         let operations = operations.into_inner();
         Box::pin(async move {

+ 75 - 0
frontend/rust-lib/flowy-document/src/services/migration.rs

@@ -0,0 +1,75 @@
+use crate::editor::DeltaRevisionMigration;
+use crate::DocumentDatabase;
+use bytes::Bytes;
+use flowy_database::kv::KV;
+use flowy_error::FlowyResult;
+use flowy_revision::disk::{DeltaRevisionSql, RevisionDiskCache, RevisionRecord, SQLiteDocumentRevisionPersistence};
+use flowy_sync::entities::revision::{md5, Revision};
+use flowy_sync::util::make_operations_from_revisions;
+use std::sync::Arc;
+
+const V1_MIGRATION: &str = "DOCUMENT_V1_MIGRATION";
+pub(crate) struct DocumentMigration {
+    user_id: String,
+    database: Arc<dyn DocumentDatabase>,
+}
+
+impl DocumentMigration {
+    pub fn new(user_id: &str, database: Arc<dyn DocumentDatabase>) -> Self {
+        let user_id = user_id.to_owned();
+        Self { user_id, database }
+    }
+
+    pub fn run_v1_migration(&self) -> FlowyResult<()> {
+        let key = migration_flag_key(&self.user_id, V1_MIGRATION);
+        if KV::get_bool(&key) {
+            return Ok(());
+        }
+
+        let pool = self.database.db_pool()?;
+        let conn = &*pool.get()?;
+        let disk_cache = SQLiteDocumentRevisionPersistence::new(&self.user_id, pool);
+        let documents = DeltaRevisionSql::read_all_documents(&self.user_id, conn)?;
+        tracing::info!("[Document Migration]: try migrate {} documents", documents.len());
+        for revisions in documents {
+            if revisions.is_empty() {
+                continue;
+            }
+
+            let document_id = revisions.first().unwrap().object_id.clone();
+            match make_operations_from_revisions(revisions) {
+                Ok(delta) => match DeltaRevisionMigration::run(delta) {
+                    Ok(transaction) => {
+                        let bytes = Bytes::from(transaction.to_bytes()?);
+                        let md5 = format!("{:x}", md5::compute(&bytes));
+                        let revision = Revision::new(&document_id, 0, 1, bytes, &self.user_id, md5);
+                        let record = RevisionRecord::new(revision);
+                        match disk_cache.create_revision_records(vec![record]) {
+                            Ok(_) => {}
+                            Err(err) => {
+                                tracing::error!("[Document Migration]: Save revisions to disk failed {:?}", err);
+                            }
+                        }
+                    }
+                    Err(err) => {
+                        tracing::error!(
+                            "[Document Migration]: Migrate revisions to transaction failed {:?}",
+                            err
+                        );
+                    }
+                },
+                Err(e) => {
+                    tracing::error!("[Document migration]: Make delta from revisions failed: {:?}", e);
+                }
+            }
+        }
+        //
+
+        KV::set_bool(&key, true);
+        tracing::info!("Run document v1 migration");
+        Ok(())
+    }
+}
+fn migration_flag_key(user_id: &str, version: &str) -> String {
+    md5(format!("{}{}", user_id, version,))
+}

+ 4 - 0
frontend/rust-lib/flowy-document/src/services/mod.rs

@@ -0,0 +1,4 @@
+mod migration;
+mod persistence;
+
+pub use persistence::*;

+ 23 - 0
frontend/rust-lib/flowy-document/src/services/persistence.rs

@@ -0,0 +1,23 @@
+use crate::services::migration::DocumentMigration;
+use crate::DocumentDatabase;
+use flowy_error::FlowyResult;
+use std::sync::Arc;
+
+pub struct DocumentPersistence {
+    pub database: Arc<dyn DocumentDatabase>,
+}
+
+impl DocumentPersistence {
+    pub fn new(database: Arc<dyn DocumentDatabase>) -> Self {
+        Self { database }
+    }
+
+    #[tracing::instrument(level = "trace", skip_all, err)]
+    pub fn initialize(&self, user_id: &str) -> FlowyResult<()> {
+        let migration = DocumentMigration::new(user_id, self.database.clone());
+        if let Err(e) = migration.run_v1_migration() {
+            tracing::error!("[Document Migration]: run v1 migration failed: {:?}", e);
+        }
+        Ok(())
+    }
+}

+ 10 - 10
frontend/rust-lib/flowy-document/tests/editor/attribute_test.rs

@@ -3,7 +3,7 @@ use crate::editor::{TestBuilder, TestOp::*};
 use flowy_sync::client_document::{NewlineDocument, EmptyDocument};
 use lib_ot::core::{Interval, OperationTransform, NEW_LINE, WHITESPACE, OTString};
 use unicode_segmentation::UnicodeSegmentation;
-use lib_ot::text_delta::TextOperations;
+use lib_ot::text_delta::DeltaTextOperations;
 
 #[test]
 fn attributes_bold_added() {
@@ -29,7 +29,7 @@ fn attributes_bold_added_and_invert_all() {
         Bold(0, Interval::new(0, 3), true),
         AssertDocJson(0, r#"[{"insert":"123","attributes":{"bold":true}}]"#),
         Bold(0, Interval::new(0, 3), false),
-        AssertDocJson(0, r#"[{"insert":"123"}]"#),
+        AssertDocJson(0, r#"[{"insert":"123","attributes":{"bold":false}}]"#),
     ];
     TestBuilder::new().run_scripts::<EmptyDocument>(ops);
 }
@@ -41,7 +41,7 @@ fn attributes_bold_added_and_invert_partial_suffix() {
         Bold(0, Interval::new(0, 4), true),
         AssertDocJson(0, r#"[{"insert":"1234","attributes":{"bold":true}}]"#),
         Bold(0, Interval::new(2, 4), false),
-        AssertDocJson(0, r#"[{"insert":"12","attributes":{"bold":true}},{"insert":"34"}]"#),
+        AssertDocJson(0, r#"[{"insert":"12","attributes":{"bold":true}},{"insert":"34","attributes":{"bold":false}}]"#),
     ];
     TestBuilder::new().run_scripts::<EmptyDocument>(ops);
 }
@@ -53,7 +53,7 @@ fn attributes_bold_added_and_invert_partial_suffix2() {
         Bold(0, Interval::new(0, 4), true),
         AssertDocJson(0, r#"[{"insert":"1234","attributes":{"bold":true}}]"#),
         Bold(0, Interval::new(2, 4), false),
-        AssertDocJson(0, r#"[{"insert":"12","attributes":{"bold":true}},{"insert":"34"}]"#),
+        AssertDocJson(0, r#"[{"insert":"12","attributes":{"bold":true}},{"insert":"34","attributes":{"bold":false}}]"#),
         Bold(0, Interval::new(2, 4), true),
         AssertDocJson(0, r#"[{"insert":"1234","attributes":{"bold":true}}]"#),
     ];
@@ -95,7 +95,7 @@ fn attributes_bold_added_and_invert_partial_prefix() {
         Bold(0, Interval::new(0, 4), true),
         AssertDocJson(0, r#"[{"insert":"1234","attributes":{"bold":true}}]"#),
         Bold(0, Interval::new(0, 2), false),
-        AssertDocJson(0, r#"[{"insert":"12"},{"insert":"34","attributes":{"bold":true}}]"#),
+        AssertDocJson(0, r#"[{"insert":"12","attributes":{"bold":false}},{"insert":"34","attributes":{"bold":true}}]"#),
     ];
     TestBuilder::new().run_scripts::<EmptyDocument>(ops);
 }
@@ -762,12 +762,12 @@ fn attributes_preserve_list_format_on_merge() {
 
 #[test]
 fn delta_compose() {
-    let mut delta = TextOperations::from_json(r#"[{"insert":"\n"}]"#).unwrap();
+    let mut delta = DeltaTextOperations::from_json(r#"[{"insert":"\n"}]"#).unwrap();
     let deltas = vec![
-        TextOperations::from_json(r#"[{"retain":1,"attributes":{"list":"unchecked"}}]"#).unwrap(),
-        TextOperations::from_json(r#"[{"insert":"a"}]"#).unwrap(),
-        TextOperations::from_json(r#"[{"retain":1},{"insert":"\n","attributes":{"list":"unchecked"}}]"#).unwrap(),
-        TextOperations::from_json(r#"[{"retain":2},{"retain":1,"attributes":{"list":""}}]"#).unwrap(),
+        DeltaTextOperations::from_json(r#"[{"retain":1,"attributes":{"list":"unchecked"}}]"#).unwrap(),
+        DeltaTextOperations::from_json(r#"[{"insert":"a"}]"#).unwrap(),
+        DeltaTextOperations::from_json(r#"[{"retain":1},{"insert":"\n","attributes":{"list":"unchecked"}}]"#).unwrap(),
+        DeltaTextOperations::from_json(r#"[{"retain":2},{"retain":1,"attributes":{"list":""}}]"#).unwrap(),
     ];
 
     for d in deltas {

+ 11 - 11
frontend/rust-lib/flowy-document/tests/editor/mod.rs

@@ -8,7 +8,7 @@ use derive_more::Display;
 use flowy_sync::client_document::{ClientDocument, InitialDocument};
 use lib_ot::{
     core::*,
-    text_delta::{BuildInTextAttribute, TextOperations},
+    text_delta::{BuildInTextAttribute, DeltaTextOperations},
 };
 use rand::{prelude::*, Rng as WrappedRng};
 use std::{sync::Once, time::Duration};
@@ -81,8 +81,8 @@ pub enum TestOp {
 
 pub struct TestBuilder {
     documents: Vec<ClientDocument>,
-    deltas: Vec<Option<TextOperations>>,
-    primes: Vec<Option<TextOperations>>,
+    deltas: Vec<Option<DeltaTextOperations>>,
+    primes: Vec<Option<DeltaTextOperations>>,
 }
 
 impl TestBuilder {
@@ -226,20 +226,20 @@ impl TestBuilder {
 
             TestOp::AssertDocJson(delta_i, expected) => {
                 let delta_json = self.documents[*delta_i].get_operations_json();
-                let expected_delta: TextOperations = serde_json::from_str(expected).unwrap();
-                let target_delta: TextOperations = serde_json::from_str(&delta_json).unwrap();
+                let expected_delta: DeltaTextOperations = serde_json::from_str(expected).unwrap();
+                let target_delta: DeltaTextOperations = serde_json::from_str(&delta_json).unwrap();
 
                 if expected_delta != target_delta {
-                    log::error!("✅ expect: {}", expected,);
-                    log::error!("❌ receive: {}", delta_json);
+                    println!("✅ expect: {}", expected,);
+                    println!("❌ receive: {}", delta_json);
                 }
                 assert_eq!(target_delta, expected_delta);
             }
 
             TestOp::AssertPrimeJson(doc_i, expected) => {
                 let prime_json = self.primes[*doc_i].as_ref().unwrap().json_str();
-                let expected_prime: TextOperations = serde_json::from_str(expected).unwrap();
-                let target_prime: TextOperations = serde_json::from_str(&prime_json).unwrap();
+                let expected_prime: DeltaTextOperations = serde_json::from_str(expected).unwrap();
+                let target_prime: DeltaTextOperations = serde_json::from_str(&prime_json).unwrap();
 
                 if expected_prime != target_prime {
                     log::error!("✅ expect prime: {}", expected,);
@@ -297,8 +297,8 @@ impl Rng {
             .collect()
     }
 
-    pub fn gen_delta(&mut self, s: &str) -> TextOperations {
-        let mut delta = TextOperations::default();
+    pub fn gen_delta(&mut self, s: &str) -> DeltaTextOperations {
+        let mut delta = DeltaTextOperations::default();
         let s = OTString::from(s);
         loop {
             let left = s.utf16_len() - delta.utf16_base_len;

+ 31 - 31
frontend/rust-lib/flowy-document/tests/editor/op_test.rs

@@ -1,8 +1,8 @@
 #![allow(clippy::all)]
 use crate::editor::{Rng, TestBuilder, TestOp::*};
 use flowy_sync::client_document::{EmptyDocument, NewlineDocument};
-use lib_ot::text_delta::TextOperationBuilder;
-use lib_ot::{core::Interval, core::*, text_delta::TextOperations};
+use lib_ot::text_delta::DeltaTextOperationBuilder;
+use lib_ot::{core::Interval, core::*, text_delta::DeltaTextOperations};
 
 #[test]
 fn attributes_insert_text() {
@@ -37,7 +37,7 @@ fn attributes_insert_text_at_middle() {
 #[test]
 fn delta_get_ops_in_interval_1() {
     let operations = OperationsBuilder::new().insert("123").insert("4").build();
-    let delta = TextOperationBuilder::from_operations(operations);
+    let delta = DeltaTextOperationBuilder::from_operations(operations);
 
     let mut iterator = OperationIterator::from_interval(&delta, Interval::new(0, 4));
     assert_eq!(iterator.ops(), delta.ops);
@@ -45,7 +45,7 @@ fn delta_get_ops_in_interval_1() {
 
 #[test]
 fn delta_get_ops_in_interval_2() {
-    let mut delta = TextOperations::default();
+    let mut delta = DeltaTextOperations::default();
     let insert_a = DeltaOperation::insert("123");
     let insert_b = DeltaOperation::insert("4");
     let insert_c = DeltaOperation::insert("5");
@@ -89,7 +89,7 @@ fn delta_get_ops_in_interval_2() {
 
 #[test]
 fn delta_get_ops_in_interval_3() {
-    let mut delta = TextOperations::default();
+    let mut delta = DeltaTextOperations::default();
     let insert_a = DeltaOperation::insert("123456");
     delta.add(insert_a.clone());
     assert_eq!(
@@ -100,7 +100,7 @@ fn delta_get_ops_in_interval_3() {
 
 #[test]
 fn delta_get_ops_in_interval_4() {
-    let mut delta = TextOperations::default();
+    let mut delta = DeltaTextOperations::default();
     let insert_a = DeltaOperation::insert("12");
     let insert_b = DeltaOperation::insert("34");
     let insert_c = DeltaOperation::insert("56");
@@ -130,7 +130,7 @@ fn delta_get_ops_in_interval_4() {
 
 #[test]
 fn delta_get_ops_in_interval_5() {
-    let mut delta = TextOperations::default();
+    let mut delta = DeltaTextOperations::default();
     let insert_a = DeltaOperation::insert("123456");
     let insert_b = DeltaOperation::insert("789");
     delta.ops.push(insert_a.clone());
@@ -148,7 +148,7 @@ fn delta_get_ops_in_interval_5() {
 
 #[test]
 fn delta_get_ops_in_interval_6() {
-    let mut delta = TextOperations::default();
+    let mut delta = DeltaTextOperations::default();
     let insert_a = DeltaOperation::insert("12345678");
     delta.add(insert_a.clone());
     assert_eq!(
@@ -159,7 +159,7 @@ fn delta_get_ops_in_interval_6() {
 
 #[test]
 fn delta_get_ops_in_interval_7() {
-    let mut delta = TextOperations::default();
+    let mut delta = DeltaTextOperations::default();
     let insert_a = DeltaOperation::insert("12345");
     let retain_a = DeltaOperation::retain(3);
 
@@ -179,7 +179,7 @@ fn delta_get_ops_in_interval_7() {
 
 #[test]
 fn delta_op_seek() {
-    let mut delta = TextOperations::default();
+    let mut delta = DeltaTextOperations::default();
     let insert_a = DeltaOperation::insert("12345");
     let retain_a = DeltaOperation::retain(3);
     delta.add(insert_a.clone());
@@ -191,7 +191,7 @@ fn delta_op_seek() {
 
 #[test]
 fn delta_utf16_code_unit_seek() {
-    let mut delta = TextOperations::default();
+    let mut delta = DeltaTextOperations::default();
     delta.add(DeltaOperation::insert("12345"));
 
     let mut iter = OperationIterator::new(&delta);
@@ -201,7 +201,7 @@ fn delta_utf16_code_unit_seek() {
 
 #[test]
 fn delta_utf16_code_unit_seek_with_attributes() {
-    let mut delta = TextOperations::default();
+    let mut delta = DeltaTextOperations::default();
     let attributes = AttributeBuilder::new()
         .insert("bold", true)
         .insert("italic", true)
@@ -221,7 +221,7 @@ fn delta_utf16_code_unit_seek_with_attributes() {
 
 #[test]
 fn delta_next_op_len() {
-    let mut delta = TextOperations::default();
+    let mut delta = DeltaTextOperations::default();
     delta.add(DeltaOperation::insert("12345"));
     let mut iter = OperationIterator::new(&delta);
     assert_eq!(iter.next_op_with_len(2).unwrap(), DeltaOperation::insert("12"));
@@ -232,7 +232,7 @@ fn delta_next_op_len() {
 
 #[test]
 fn delta_next_op_len_with_chinese() {
-    let mut delta = TextOperations::default();
+    let mut delta = DeltaTextOperations::default();
     delta.add(DeltaOperation::insert("你好"));
 
     let mut iter = OperationIterator::new(&delta);
@@ -242,7 +242,7 @@ fn delta_next_op_len_with_chinese() {
 
 #[test]
 fn delta_next_op_len_with_english() {
-    let mut delta = TextOperations::default();
+    let mut delta = DeltaTextOperations::default();
     delta.add(DeltaOperation::insert("ab"));
     let mut iter = OperationIterator::new(&delta);
     assert_eq!(iter.next_op_len().unwrap(), 2);
@@ -251,7 +251,7 @@ fn delta_next_op_len_with_english() {
 
 #[test]
 fn delta_next_op_len_after_seek() {
-    let mut delta = TextOperations::default();
+    let mut delta = DeltaTextOperations::default();
     delta.add(DeltaOperation::insert("12345"));
     let mut iter = OperationIterator::new(&delta);
     assert_eq!(iter.next_op_len().unwrap(), 5);
@@ -264,7 +264,7 @@ fn delta_next_op_len_after_seek() {
 
 #[test]
 fn delta_next_op_len_none() {
-    let mut delta = TextOperations::default();
+    let mut delta = DeltaTextOperations::default();
     delta.add(DeltaOperation::insert("12345"));
     let mut iter = OperationIterator::new(&delta);
 
@@ -275,7 +275,7 @@ fn delta_next_op_len_none() {
 
 #[test]
 fn delta_next_op_with_len_zero() {
-    let mut delta = TextOperations::default();
+    let mut delta = DeltaTextOperations::default();
     delta.add(DeltaOperation::insert("12345"));
     let mut iter = OperationIterator::new(&delta);
     assert_eq!(iter.next_op_with_len(0), None,);
@@ -284,7 +284,7 @@ fn delta_next_op_with_len_zero() {
 
 #[test]
 fn delta_next_op_with_len_cross_op_return_last() {
-    let mut delta = TextOperations::default();
+    let mut delta = DeltaTextOperations::default();
     delta.add(DeltaOperation::insert("12345"));
     delta.add(DeltaOperation::retain(1));
     delta.add(DeltaOperation::insert("678"));
@@ -297,7 +297,7 @@ fn delta_next_op_with_len_cross_op_return_last() {
 
 #[test]
 fn lengths() {
-    let mut delta = TextOperations::default();
+    let mut delta = DeltaTextOperations::default();
     assert_eq!(delta.utf16_base_len, 0);
     assert_eq!(delta.utf16_target_len, 0);
     delta.retain(5, AttributeHashMap::default());
@@ -315,7 +315,7 @@ fn lengths() {
 }
 #[test]
 fn sequence() {
-    let mut delta = TextOperations::default();
+    let mut delta = DeltaTextOperations::default();
     delta.retain(5, AttributeHashMap::default());
     delta.retain(0, AttributeHashMap::default());
     delta.insert("appflowy", AttributeHashMap::default());
@@ -348,7 +348,7 @@ fn apply_test() {
 
 #[test]
 fn base_len_test() {
-    let mut delta_a = TextOperations::default();
+    let mut delta_a = DeltaTextOperations::default();
     delta_a.insert("a", AttributeHashMap::default());
     delta_a.insert("b", AttributeHashMap::default());
     delta_a.insert("c", AttributeHashMap::default());
@@ -387,7 +387,7 @@ fn invert_test() {
 
 #[test]
 fn empty_ops() {
-    let mut delta = TextOperations::default();
+    let mut delta = DeltaTextOperations::default();
     delta.retain(0, AttributeHashMap::default());
     delta.insert("", AttributeHashMap::default());
     delta.delete(0);
@@ -395,12 +395,12 @@ fn empty_ops() {
 }
 #[test]
 fn eq() {
-    let mut delta_a = TextOperations::default();
+    let mut delta_a = DeltaTextOperations::default();
     delta_a.delete(1);
     delta_a.insert("lo", AttributeHashMap::default());
     delta_a.retain(2, AttributeHashMap::default());
     delta_a.retain(3, AttributeHashMap::default());
-    let mut delta_b = TextOperations::default();
+    let mut delta_b = DeltaTextOperations::default();
     delta_b.delete(1);
     delta_b.insert("l", AttributeHashMap::default());
     delta_b.insert("o", AttributeHashMap::default());
@@ -412,7 +412,7 @@ fn eq() {
 }
 #[test]
 fn ops_merging() {
-    let mut delta = TextOperations::default();
+    let mut delta = DeltaTextOperations::default();
     assert_eq!(delta.ops.len(), 0);
     delta.retain(2, AttributeHashMap::default());
     assert_eq!(delta.ops.len(), 1);
@@ -436,7 +436,7 @@ fn ops_merging() {
 
 #[test]
 fn is_noop() {
-    let mut delta = TextOperations::default();
+    let mut delta = DeltaTextOperations::default();
     assert!(delta.is_noop());
     delta.retain(5, AttributeHashMap::default());
     assert!(delta.is_noop());
@@ -484,13 +484,13 @@ fn transform_random_delta() {
 
 #[test]
 fn transform_with_two_delta() {
-    let mut a = TextOperations::default();
+    let mut a = DeltaTextOperations::default();
     let mut a_s = String::new();
     a.insert("123", AttributeBuilder::new().insert("bold", true).build());
     a_s = a.apply(&a_s).unwrap();
     assert_eq!(&a_s, "123");
 
-    let mut b = TextOperations::default();
+    let mut b = DeltaTextOperations::default();
     let mut b_s = String::new();
     b.insert("456", AttributeHashMap::default());
     b_s = b.apply(&b_s).unwrap();
@@ -580,10 +580,10 @@ fn transform_two_conflict_non_seq_delta() {
 
 #[test]
 fn delta_invert_no_attribute_delta() {
-    let mut delta = TextOperations::default();
+    let mut delta = DeltaTextOperations::default();
     delta.add(DeltaOperation::insert("123"));
 
-    let mut change = TextOperations::default();
+    let mut change = DeltaTextOperations::default();
     change.add(DeltaOperation::retain(3));
     change.add(DeltaOperation::insert("456"));
     let undo = change.invert(&delta);

+ 9 - 9
frontend/rust-lib/flowy-document/tests/editor/serde_test.rs

@@ -1,8 +1,8 @@
 use flowy_sync::client_document::{ClientDocument, EmptyDocument};
-use lib_ot::text_delta::TextOperation;
+use lib_ot::text_delta::DeltaTextOperation;
 use lib_ot::{
     core::*,
-    text_delta::{BuildInTextAttribute, TextOperations},
+    text_delta::{BuildInTextAttribute, DeltaTextOperations},
 };
 
 #[test]
@@ -15,7 +15,7 @@ fn operation_insert_serialize_test() {
     let json = serde_json::to_string(&operation).unwrap();
     eprintln!("{}", json);
 
-    let insert_op: TextOperation = serde_json::from_str(&json).unwrap();
+    let insert_op: DeltaTextOperation = serde_json::from_str(&json).unwrap();
     assert_eq!(insert_op, operation);
 }
 
@@ -24,15 +24,15 @@ fn operation_retain_serialize_test() {
     let operation = DeltaOperation::Retain(12.into());
     let json = serde_json::to_string(&operation).unwrap();
     eprintln!("{}", json);
-    let insert_op: TextOperation = serde_json::from_str(&json).unwrap();
+    let insert_op: DeltaTextOperation = serde_json::from_str(&json).unwrap();
     assert_eq!(insert_op, operation);
 }
 
 #[test]
 fn operation_delete_serialize_test() {
-    let operation = TextOperation::Delete(2);
+    let operation = DeltaTextOperation::Delete(2);
     let json = serde_json::to_string(&operation).unwrap();
-    let insert_op: TextOperation = serde_json::from_str(&json).unwrap();
+    let insert_op: DeltaTextOperation = serde_json::from_str(&json).unwrap();
     assert_eq!(insert_op, operation);
 }
 
@@ -77,7 +77,7 @@ fn delta_deserialize_test() {
         {"retain":2,"attributes":{"italic":true,"bold":true}},
         {"retain":2,"attributes":{"italic":true,"bold":true}}
      ]"#;
-    let delta = TextOperations::from_json(json).unwrap();
+    let delta = DeltaTextOperations::from_json(json).unwrap();
     eprintln!("{}", delta);
 }
 
@@ -86,12 +86,12 @@ fn delta_deserialize_null_test() {
     let json = r#"[
         {"retain":7,"attributes":{"bold":null}}
      ]"#;
-    let delta1 = TextOperations::from_json(json).unwrap();
+    let delta1 = DeltaTextOperations::from_json(json).unwrap();
 
     let mut attribute = BuildInTextAttribute::Bold(true);
     attribute.remove_value();
 
-    let delta2 = OperationBuilder::new()
+    let delta2 = DeltaOperationBuilder::new()
         .retain_with_attributes(7, attribute.into())
         .build();
 

+ 24 - 0
frontend/rust-lib/flowy-document/tests/new_document/document_compose_test.rs

@@ -0,0 +1,24 @@
+use crate::new_document::script::DocumentEditorTest;
+use crate::new_document::script::EditScript::*;
+
+#[tokio::test]
+async fn document_insert_h1_style_test() {
+    let scripts = vec![
+        ComposeTransactionStr {
+            transaction: r#"{"operations":[{"op":"update_text","path":[0,0],"delta":[{"insert":"/"}],"inverted":[{"delete":1}]}],"after_selection":{"start":{"path":[0,0],"offset":1},"end":{"path":[0,0],"offset":1}},"before_selection":{"start":{"path":[0,0],"offset":0},"end":{"path":[0,0],"offset":0}}}"#,
+        },
+        AssertContent {
+            expected: r#"{"document":{"type":"editor","children":[{"type":"text","delta":[{"insert":"/"}]}]}}"#,
+        },
+        ComposeTransactionStr {
+            transaction: r#"{"operations":[{"op":"update_text","path":[0,0],"delta":[{"delete":1}],"inverted":[{"insert":"/"}]}],"after_selection":{"start":{"path":[0,0],"offset":0},"end":{"path":[0,0],"offset":0}},"before_selection":{"start":{"path":[0,0],"offset":1},"end":{"path":[0,0],"offset":1}}}"#,
+        },
+        ComposeTransactionStr {
+            transaction: r#"{"operations":[{"op":"update","path":[0,0],"attributes":{"subtype":"heading","heading":"h1"},"oldAttributes":{"subtype":null,"heading":null}}],"after_selection":{"start":{"path":[0,0],"offset":0},"end":{"path":[0,0],"offset":0}},"before_selection":{"start":{"path":[0,0],"offset":0},"end":{"path":[0,0],"offset":0}}}"#,
+        },
+        AssertContent {
+            expected: r#"{"document":{"type":"editor","children":[{"type":"text","attributes":{"subtype":"heading","heading":"h1"}}]}}"#,
+        },
+    ];
+    DocumentEditorTest::new().await.run_scripts(scripts).await;
+}

+ 1 - 0
frontend/rust-lib/flowy-document/tests/new_document/mod.rs

@@ -1,2 +1,3 @@
+mod document_compose_test;
 mod script;
 mod test;

+ 40 - 8
frontend/rust-lib/flowy-document/tests/new_document/script.rs

@@ -1,17 +1,37 @@
-use flowy_document::editor::AppFlowyDocumentEditor;
+use flowy_document::editor::{AppFlowyDocumentEditor, Document, DocumentTransaction};
 
+use flowy_document::entities::DocumentVersionPB;
 use flowy_test::helper::ViewTest;
 use flowy_test::FlowySDKTest;
 use lib_ot::core::{Body, Changeset, NodeDataBuilder, NodeOperation, Path, Transaction};
-use lib_ot::text_delta::TextOperations;
+use lib_ot::text_delta::DeltaTextOperations;
 use std::sync::Arc;
 
 pub enum EditScript {
-    InsertText { path: Path, delta: TextOperations },
-    UpdateText { path: Path, delta: TextOperations },
-    Delete { path: Path },
-    AssertContent { expected: &'static str },
-    AssertPrettyContent { expected: &'static str },
+    InsertText {
+        path: Path,
+        delta: DeltaTextOperations,
+    },
+    UpdateText {
+        path: Path,
+        delta: DeltaTextOperations,
+    },
+    #[allow(dead_code)]
+    ComposeTransaction {
+        transaction: Transaction,
+    },
+    ComposeTransactionStr {
+        transaction: &'static str,
+    },
+    Delete {
+        path: Path,
+    },
+    AssertContent {
+        expected: &'static str,
+    },
+    AssertPrettyContent {
+        expected: &'static str,
+    },
 }
 
 pub struct DocumentEditorTest {
@@ -21,7 +41,8 @@ pub struct DocumentEditorTest {
 
 impl DocumentEditorTest {
     pub async fn new() -> Self {
-        let sdk = FlowySDKTest::new(true);
+        let version = DocumentVersionPB::V1;
+        let sdk = FlowySDKTest::new(version.clone());
         let _ = sdk.init_user().await;
 
         let test = ViewTest::new_document_view(&sdk).await;
@@ -62,6 +83,14 @@ impl DocumentEditorTest {
                     .await
                     .unwrap();
             }
+            EditScript::ComposeTransaction { transaction } => {
+                self.editor.apply_transaction(transaction).await.unwrap();
+            }
+            EditScript::ComposeTransactionStr { transaction } => {
+                let document_transaction = serde_json::from_str::<DocumentTransaction>(transaction).unwrap();
+                let transaction: Transaction = document_transaction.into();
+                self.editor.apply_transaction(transaction).await.unwrap();
+            }
             EditScript::Delete { path } => {
                 let operation = NodeOperation::Delete { path, nodes: vec![] };
                 self.editor
@@ -72,6 +101,9 @@ impl DocumentEditorTest {
             EditScript::AssertContent { expected } => {
                 //
                 let content = self.editor.get_content(false).await.unwrap();
+                let expected_document: Document = serde_json::from_str(expected).unwrap();
+                let expected = serde_json::to_string(&expected_document).unwrap();
+
                 assert_eq!(content, expected);
             }
             EditScript::AssertPrettyContent { expected } => {

+ 7 - 7
frontend/rust-lib/flowy-document/tests/new_document/test.rs

@@ -1,7 +1,7 @@
 use crate::new_document::script::DocumentEditorTest;
 use crate::new_document::script::EditScript::*;
 
-use lib_ot::text_delta::TextOperationBuilder;
+use lib_ot::text_delta::DeltaTextOperationBuilder;
 
 #[tokio::test]
 async fn document_initialize_test() {
@@ -13,7 +13,7 @@ async fn document_initialize_test() {
 
 #[tokio::test]
 async fn document_insert_text_test() {
-    let delta = TextOperationBuilder::new().insert("Hello world").build();
+    let delta = DeltaTextOperationBuilder::new().insert("Hello world").build();
     let expected = r#"{
   "document": {
     "type": "editor",
@@ -49,7 +49,7 @@ async fn document_update_text_test() {
     let scripts = vec![
         UpdateText {
             path: vec![0, 0].into(),
-            delta: TextOperationBuilder::new().insert(&hello_world).build(),
+            delta: DeltaTextOperationBuilder::new().insert(&hello_world).build(),
         },
         AssertPrettyContent {
             expected: r#"{
@@ -75,7 +75,7 @@ async fn document_update_text_test() {
     let scripts = vec![
         UpdateText {
             path: vec![0, 0].into(),
-            delta: TextOperationBuilder::new()
+            delta: DeltaTextOperationBuilder::new()
                 .retain(hello_world.len())
                 .insert(", AppFlowy")
                 .build(),
@@ -122,11 +122,11 @@ async fn document_delete_text_test() {
     let scripts = vec![
         UpdateText {
             path: vec![0, 0].into(),
-            delta: TextOperationBuilder::new().insert(&hello_world).build(),
+            delta: DeltaTextOperationBuilder::new().insert(&hello_world).build(),
         },
         UpdateText {
             path: vec![0, 0].into(),
-            delta: TextOperationBuilder::new().retain(5).delete(6).build(),
+            delta: DeltaTextOperationBuilder::new().retain(5).delete(6).build(),
         },
         AssertPrettyContent { expected },
     ];
@@ -139,7 +139,7 @@ async fn document_delete_node_test() {
     let scripts = vec![
         UpdateText {
             path: vec![0, 0].into(),
-            delta: TextOperationBuilder::new().insert("Hello world").build(),
+            delta: DeltaTextOperationBuilder::new().insert("Hello world").build(),
         },
         AssertContent {
             expected: r#"{"document":{"type":"editor","children":[{"type":"text","delta":[{"insert":"Hello world"}]}]}}"#,

+ 2 - 2
frontend/rust-lib/flowy-document/tests/old_document/script.rs

@@ -2,7 +2,7 @@ use flowy_document::old_editor::editor::DeltaDocumentEditor;
 use flowy_document::TEXT_BLOCK_SYNC_INTERVAL_IN_MILLIS;
 use flowy_revision::disk::RevisionState;
 use flowy_test::{helper::ViewTest, FlowySDKTest};
-use lib_ot::{core::Interval, text_delta::TextOperations};
+use lib_ot::{core::Interval, text_delta::DeltaTextOperations};
 use std::sync::Arc;
 use tokio::time::{sleep, Duration};
 
@@ -75,7 +75,7 @@ impl DeltaDocumentEditorTest {
                 assert_eq!(next_revision.rev_id, rev_id.unwrap());
             }
             EditorScript::AssertJson(expected) => {
-                let expected_delta: TextOperations = serde_json::from_str(expected).unwrap();
+                let expected_delta: DeltaTextOperations = serde_json::from_str(expected).unwrap();
                 let delta = self.editor.document_operations().await.unwrap();
                 if expected_delta != delta {
                     eprintln!("✅ expect: {}", expected,);

Some files were not shown because too many files changed in this diff