Browse Source

chore: create redo/undo bridge (#2760)

* chore: create redo/undo bridge

* chore: update test

* chore: review update

* chore: review update

* chore: react redo/undo

* chore: review update

* chore: add test

* chore: review update

* chore: generate document id

* chore: update undo/redo

* chore: update cargo lock
Kilu.He 1 year ago
parent
commit
95f8b2e9a4
31 changed files with 857 additions and 274 deletions
  1. 160 176
      frontend/appflowy_tauri/src-tauri/Cargo.lock
  2. 2 0
      frontend/appflowy_tauri/src/appflowy_app/components/document/Overlay/index.tsx
  3. 4 2
      frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/useKeyDown.ts
  4. 1 1
      frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SlateEditor/useSlateYjs.ts
  5. 39 0
      frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/UndoHooks/useUndoRedo.ts
  6. 0 5
      frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/FolderItem.hooks.ts
  7. 2 0
      frontend/appflowy_tauri/src/appflowy_app/constants/document/keyboard.ts
  8. 26 7
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_bd_svc.ts
  9. 20 7
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_controller.ts
  10. 2 0
      frontend/rust-lib/Cargo.lock
  11. 1 1
      frontend/rust-lib/flowy-core/src/deps_resolve/folder2_deps.rs
  12. 1 0
      frontend/rust-lib/flowy-document2/Cargo.toml
  13. 114 4
      frontend/rust-lib/flowy-document2/src/entities.rs
  14. 78 16
      frontend/rust-lib/flowy-document2/src/event_handler.rs
  15. 25 3
      frontend/rust-lib/flowy-document2/src/event_map.rs
  16. 1 0
      frontend/rust-lib/flowy-document2/src/lib.rs
  17. 7 6
      frontend/rust-lib/flowy-document2/src/manager.rs
  18. 23 0
      frontend/rust-lib/flowy-document2/src/parse.rs
  19. 6 27
      frontend/rust-lib/flowy-document2/tests/document/document_insert_test.rs
  20. 59 0
      frontend/rust-lib/flowy-document2/tests/document/document_redo_undo_test.rs
  21. 17 18
      frontend/rust-lib/flowy-document2/tests/document/document_test.rs
  22. 1 0
      frontend/rust-lib/flowy-document2/tests/document/mod.rs
  23. 30 1
      frontend/rust-lib/flowy-document2/tests/document/util.rs
  24. 6 0
      frontend/rust-lib/flowy-error/src/code.rs
  25. 1 0
      frontend/rust-lib/flowy-test/Cargo.toml
  26. 107 0
      frontend/rust-lib/flowy-test/src/document_event.rs
  27. 1 0
      frontend/rust-lib/flowy-test/src/lib.rs
  28. 2 0
      frontend/rust-lib/flowy-test/tests/document/mod.rs
  29. 62 0
      frontend/rust-lib/flowy-test/tests/document/test.rs
  30. 58 0
      frontend/rust-lib/flowy-test/tests/document/utils.rs
  31. 1 0
      frontend/rust-lib/flowy-test/tests/main.rs

File diff suppressed because it is too large
+ 160 - 176
frontend/appflowy_tauri/src-tauri/Cargo.lock


+ 2 - 0
frontend/appflowy_tauri/src/appflowy_app/components/document/Overlay/index.tsx

@@ -6,10 +6,12 @@ import BlockSlash from '$app/components/document/BlockSlash';
 import { useCopy } from '$app/components/document/_shared/CopyPasteHooks/useCopy';
 import { useCopy } from '$app/components/document/_shared/CopyPasteHooks/useCopy';
 import { usePaste } from '$app/components/document/_shared/CopyPasteHooks/usePaste';
 import { usePaste } from '$app/components/document/_shared/CopyPasteHooks/usePaste';
 import LinkEditPopover from '$app/components/document/_shared/TextLink/LinkEditPopover';
 import LinkEditPopover from '$app/components/document/_shared/TextLink/LinkEditPopover';
+import { useUndoRedo } from '$app/components/document/_shared/UndoHooks/useUndoRedo';
 
 
 export default function Overlay({ container }: { container: HTMLDivElement }) {
 export default function Overlay({ container }: { container: HTMLDivElement }) {
   useCopy(container);
   useCopy(container);
   usePaste(container);
   usePaste(container);
+  useUndoRedo(container);
   return (
   return (
     <>
     <>
       <BlockSideToolbar container={container} />
       <BlockSideToolbar container={container} />

+ 4 - 2
frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/useKeyDown.ts

@@ -88,9 +88,11 @@ export function useKeyDown(id: string) {
 
 
   const onKeyDown = useCallback(
   const onKeyDown = useCallback(
     (e: React.KeyboardEvent<HTMLDivElement>) => {
     (e: React.KeyboardEvent<HTMLDivElement>) => {
-      e.stopPropagation();
       const filteredEvents = interceptEvents.filter((event) => event.canHandle(e));
       const filteredEvents = interceptEvents.filter((event) => event.canHandle(e));
-      filteredEvents.forEach((event) => event.handler(e));
+      filteredEvents.forEach((event) => {
+        e.stopPropagation();
+        event.handler(e);
+      });
     },
     },
     [interceptEvents]
     [interceptEvents]
   );
   );

+ 1 - 1
frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SlateEditor/useSlateYjs.ts

@@ -19,7 +19,7 @@ export function useSlateYjs({ delta }: { delta?: Delta }) {
     // eslint-disable-next-line react-hooks/exhaustive-deps
     // eslint-disable-next-line react-hooks/exhaustive-deps
   }, []);
   }, []);
 
 
-  const editor = useMemo(() => withYjs(withReact(createEditor()), sharedType), []);
+  const editor = useMemo(() => withReact(withYjs(createEditor(), sharedType)), []);
 
 
   // Connect editor in useEffect to comply with concurrent mode requirements.
   // Connect editor in useEffect to comply with concurrent mode requirements.
   useEffect(() => {
   useEffect(() => {

+ 39 - 0
frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/UndoHooks/useUndoRedo.ts

@@ -0,0 +1,39 @@
+import { useCallback, useContext, useEffect } from 'react';
+import { DocumentControllerContext } from '$app/stores/effects/document/document_controller';
+import isHotkey from 'is-hotkey';
+import { Keyboard } from '@/appflowy_app/constants/document/keyboard';
+
+export function useUndoRedo(container: HTMLDivElement) {
+  const controller = useContext(DocumentControllerContext);
+
+  const onUndo = useCallback(() => {
+    if (!controller) return;
+    controller.undo();
+  }, [controller]);
+
+  const onRedo = useCallback(() => {
+    if (!controller) return;
+    controller.redo();
+  }, [controller]);
+
+  const handleKeyDownCapture = useCallback(
+    (e: KeyboardEvent) => {
+      if (isHotkey(Keyboard.keys.UNDO, e)) {
+        e.stopPropagation();
+        onUndo();
+      }
+      if (isHotkey(Keyboard.keys.REDO, e)) {
+        e.stopPropagation();
+        onRedo();
+      }
+    },
+    [onRedo, onUndo]
+  );
+
+  useEffect(() => {
+    container.addEventListener('keydown', handleKeyDownCapture, true);
+    return () => {
+      container.removeEventListener('keydown', handleKeyDownCapture, true);
+    };
+  }, [container, handleKeyDownCapture]);
+}

+ 0 - 5
frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/FolderItem.hooks.ts

@@ -10,8 +10,6 @@ import { AppObserver } from '$app/stores/effects/folder/app/app_observer';
 import { useNavigate } from 'react-router-dom';
 import { useNavigate } from 'react-router-dom';
 import { INITIAL_FOLDER_HEIGHT, PAGE_ITEM_HEIGHT } from '../../_shared/constants';
 import { INITIAL_FOLDER_HEIGHT, PAGE_ITEM_HEIGHT } from '../../_shared/constants';
 
 
-import { DocumentController } from '$app/stores/effects/document/document_controller';
-
 export const useFolderEvents = (folder: IFolder, pages: IPage[]) => {
 export const useFolderEvents = (folder: IFolder, pages: IPage[]) => {
   const appDispatch = useAppDispatch();
   const appDispatch = useAppDispatch();
   const workspace = useAppSelector((state) => state.workspace);
   const workspace = useAppSelector((state) => state.workspace);
@@ -118,9 +116,6 @@ export const useFolderEvents = (folder: IFolder, pages: IPage[]) => {
       layoutType: ViewLayoutPB.Document,
       layoutType: ViewLayoutPB.Document,
     });
     });
     try {
     try {
-      const c = new DocumentController(newView.id);
-      await c.create();
-      await c.dispose();
       appDispatch(
       appDispatch(
         pagesActions.addPage({
         pagesActions.addPage({
           folderId: folder.id,
           folderId: folder.id,

+ 2 - 0
frontend/appflowy_tauri/src/appflowy_app/constants/document/keyboard.ts

@@ -38,5 +38,7 @@ export const Keyboard = {
     COPY: 'Mod+c',
     COPY: 'Mod+c',
     CUT: 'Mod+x',
     CUT: 'Mod+x',
     PASTE: 'Mod+v',
     PASTE: 'Mod+v',
+    REDO: 'Mod+Shift+z',
+    UNDO: 'Mod+z',
   },
   },
 };
 };

+ 26 - 7
frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_bd_svc.ts

@@ -6,6 +6,8 @@ import {
   ApplyActionPayloadPB,
   ApplyActionPayloadPB,
   BlockActionPB,
   BlockActionPB,
   CloseDocumentPayloadPB,
   CloseDocumentPayloadPB,
+  DocumentRedoUndoPayloadPB,
+  DocumentRedoUndoResponsePB,
 } from '@/services/backend';
 } from '@/services/backend';
 import { Result } from 'ts-results';
 import { Result } from 'ts-results';
 import {
 import {
@@ -13,18 +15,14 @@ import {
   DocumentEventCloseDocument,
   DocumentEventCloseDocument,
   DocumentEventOpenDocument,
   DocumentEventOpenDocument,
   DocumentEventCreateDocument,
   DocumentEventCreateDocument,
+  DocumentEventCanUndoRedo,
+  DocumentEventRedo,
+  DocumentEventUndo,
 } from '@/services/backend/events/flowy-document2';
 } from '@/services/backend/events/flowy-document2';
 
 
 export class DocumentBackendService {
 export class DocumentBackendService {
   constructor(public readonly viewId: string) {}
   constructor(public readonly viewId: string) {}
 
 
-  create = (): Promise<Result<void, FlowyError>> => {
-    const payload = CreateDocumentPayloadPB.fromObject({
-      document_id: this.viewId,
-    });
-    return DocumentEventCreateDocument(payload);
-  };
-
   open = (): Promise<Result<DocumentDataPB, FlowyError>> => {
   open = (): Promise<Result<DocumentDataPB, FlowyError>> => {
     const payload = OpenDocumentPayloadPB.fromObject({
     const payload = OpenDocumentPayloadPB.fromObject({
       document_id: this.viewId,
       document_id: this.viewId,
@@ -46,4 +44,25 @@ export class DocumentBackendService {
     });
     });
     return DocumentEventCloseDocument(payload);
     return DocumentEventCloseDocument(payload);
   };
   };
+
+  canUndoRedo = (): Promise<Result<DocumentRedoUndoResponsePB, FlowyError>> => {
+    const payload = DocumentRedoUndoPayloadPB.fromObject({
+      document_id: this.viewId,
+    });
+    return DocumentEventCanUndoRedo(payload);
+  };
+
+  undo = (): Promise<Result<DocumentRedoUndoResponsePB, FlowyError>> => {
+    const payload = DocumentRedoUndoPayloadPB.fromObject({
+      document_id: this.viewId,
+    });
+    return DocumentEventUndo(payload);
+  };
+
+  redo = (): Promise<Result<DocumentRedoUndoResponsePB, FlowyError>> => {
+    const payload = DocumentRedoUndoPayloadPB.fromObject({
+      document_id: this.viewId,
+    });
+    return DocumentEventRedo(payload);
+  };
 }
 }

+ 20 - 7
frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_controller.ts

@@ -31,13 +31,6 @@ export class DocumentController {
     this.observer = new DocumentObserver(documentId);
     this.observer = new DocumentObserver(documentId);
   }
   }
 
 
-  create = async (): Promise<FlowyError | void> => {
-    const result = await this.backendService.create();
-    if (result.ok) {
-      return;
-    }
-    return result.val;
-  };
   open = async (): Promise<DocumentData> => {
   open = async (): Promise<DocumentData> => {
     await this.observer.subscribe({
     await this.observer.subscribe({
       didReceiveUpdate: this.updated,
       didReceiveUpdate: this.updated,
@@ -114,6 +107,26 @@ export class DocumentController {
     };
     };
   };
   };
 
 
+  canUndo = async () => {
+    const result = await this.backendService.canUndoRedo();
+    return result.ok && result.val.can_undo;
+  };
+
+  canRedo = async () => {
+    const result = await this.backendService.canUndoRedo();
+    return result.ok && result.val.can_redo;
+  };
+
+  undo = async () => {
+    const result = await this.backendService.undo();
+    return result.ok && result.val.is_success;
+  };
+
+  redo = async () => {
+    const result = await this.backendService.redo();
+    return result.ok && result.val.is_success;
+  };
+
   dispose = async () => {
   dispose = async () => {
     this.onDocChange = undefined;
     this.onDocChange = undefined;
     await this.backendService.close();
     await this.backendService.close();

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

@@ -1710,6 +1710,7 @@ dependencies = [
  "tokio",
  "tokio",
  "tracing",
  "tracing",
  "tracing-subscriber 0.3.16",
  "tracing-subscriber 0.3.16",
+ "uuid",
 ]
 ]
 
 
 [[package]]
 [[package]]
@@ -1867,6 +1868,7 @@ dependencies = [
  "dotenv",
  "dotenv",
  "flowy-core",
  "flowy-core",
  "flowy-database2",
  "flowy-database2",
+ "flowy-document2",
  "flowy-folder2",
  "flowy-folder2",
  "flowy-net",
  "flowy-net",
  "flowy-notification",
  "flowy-notification",

+ 1 - 1
frontend/rust-lib/flowy-core/src/deps_resolve/folder2_deps.rs

@@ -143,7 +143,7 @@ impl FolderOperationHandler for DocumentFolderOperation {
     let manager = self.0.clone();
     let manager = self.0.clone();
     let view_id = view_id.to_string();
     let view_id = view_id.to_string();
     FutureResult::new(async move {
     FutureResult::new(async move {
-      let document = manager.get_document(view_id)?;
+      let document = manager.get_document_from_disk(view_id)?;
       let data: DocumentDataPB = document.lock().get_document()?.into();
       let data: DocumentDataPB = document.lock().get_document()?.into();
       let data_bytes = data.into_bytes().map_err(|_| FlowyError::invalid_data())?;
       let data_bytes = data.into_bytes().map_err(|_| FlowyError::invalid_data())?;
       Ok(data_bytes)
       Ok(data_bytes)

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

@@ -27,6 +27,7 @@ tracing = { version = "0.1", features = ["log"] }
 tokio = { version = "1.26", features = ["full"] }
 tokio = { version = "1.26", features = ["full"] }
 anyhow = "1.0"
 anyhow = "1.0"
 indexmap = {version = "1.9.2", features = ["serde"]}
 indexmap = {version = "1.9.2", features = ["serde"]}
+uuid = { version = "1.3.3", features = ["v4"] }
 
 
 [dev-dependencies]
 [dev-dependencies]
 tempfile = "3.4.0"
 tempfile = "3.4.0"

+ 114 - 4
frontend/rust-lib/flowy-document2/src/entities.rs

@@ -1,6 +1,9 @@
+use collab_document::blocks::{BlockAction, DocumentData};
 use std::collections::HashMap;
 use std::collections::HashMap;
 
 
+use crate::parse::{NotEmptyStr, NotEmptyVec};
 use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
 use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
+use flowy_error::ErrorCode;
 
 
 #[derive(Default, ProtoBuf)]
 #[derive(Default, ProtoBuf)]
 pub struct OpenDocumentPayloadPB {
 pub struct OpenDocumentPayloadPB {
@@ -8,6 +11,54 @@ pub struct OpenDocumentPayloadPB {
   pub document_id: String,
   pub document_id: String,
 }
 }
 
 
+pub struct OpenDocumentParams {
+  pub document_id: String,
+}
+
+impl TryInto<OpenDocumentParams> for OpenDocumentPayloadPB {
+  type Error = ErrorCode;
+  fn try_into(self) -> Result<OpenDocumentParams, Self::Error> {
+    let document_id =
+      NotEmptyStr::parse(self.document_id).map_err(|_| ErrorCode::DocumentIdIsEmpty)?;
+    Ok(OpenDocumentParams {
+      document_id: document_id.0,
+    })
+  }
+}
+
+#[derive(Default, ProtoBuf)]
+pub struct DocumentRedoUndoPayloadPB {
+  #[pb(index = 1)]
+  pub document_id: String,
+}
+
+pub struct DocumentRedoUndoParams {
+  pub document_id: String,
+}
+
+impl TryInto<DocumentRedoUndoParams> for DocumentRedoUndoPayloadPB {
+  type Error = ErrorCode;
+  fn try_into(self) -> Result<DocumentRedoUndoParams, Self::Error> {
+    let document_id =
+      NotEmptyStr::parse(self.document_id).map_err(|_| ErrorCode::DocumentIdIsEmpty)?;
+    Ok(DocumentRedoUndoParams {
+      document_id: document_id.0,
+    })
+  }
+}
+
+#[derive(Default, Debug, ProtoBuf)]
+pub struct DocumentRedoUndoResponsePB {
+  #[pb(index = 1)]
+  pub can_undo: bool,
+
+  #[pb(index = 2)]
+  pub can_redo: bool,
+
+  #[pb(index = 3)]
+  pub is_success: bool,
+}
+
 #[derive(Default, ProtoBuf)]
 #[derive(Default, ProtoBuf)]
 pub struct CreateDocumentPayloadPB {
 pub struct CreateDocumentPayloadPB {
   #[pb(index = 1)]
   #[pb(index = 1)]
@@ -17,12 +68,45 @@ pub struct CreateDocumentPayloadPB {
   pub initial_data: Option<DocumentDataPB>,
   pub initial_data: Option<DocumentDataPB>,
 }
 }
 
 
+pub struct CreateDocumentParams {
+  pub document_id: String,
+  pub initial_data: Option<DocumentData>,
+}
+
+impl TryInto<CreateDocumentParams> for CreateDocumentPayloadPB {
+  type Error = ErrorCode;
+  fn try_into(self) -> Result<CreateDocumentParams, Self::Error> {
+    let document_id =
+      NotEmptyStr::parse(self.document_id).map_err(|_| ErrorCode::DocumentIdIsEmpty)?;
+    let initial_data = self.initial_data.map(|data| data.into());
+    Ok(CreateDocumentParams {
+      document_id: document_id.0,
+      initial_data,
+    })
+  }
+}
+
 #[derive(Default, ProtoBuf)]
 #[derive(Default, ProtoBuf)]
 pub struct CloseDocumentPayloadPB {
 pub struct CloseDocumentPayloadPB {
   #[pb(index = 1)]
   #[pb(index = 1)]
   pub document_id: String,
   pub document_id: String,
 }
 }
 
 
+pub struct CloseDocumentParams {
+  pub document_id: String,
+}
+
+impl TryInto<CloseDocumentParams> for CloseDocumentPayloadPB {
+  type Error = ErrorCode;
+  fn try_into(self) -> Result<CloseDocumentParams, Self::Error> {
+    let document_id =
+      NotEmptyStr::parse(self.document_id).map_err(|_| ErrorCode::DocumentIdIsEmpty)?;
+    Ok(CloseDocumentParams {
+      document_id: document_id.0,
+    })
+  }
+}
+
 #[derive(Default, ProtoBuf, Debug)]
 #[derive(Default, ProtoBuf, Debug)]
 pub struct ApplyActionPayloadPB {
 pub struct ApplyActionPayloadPB {
   #[pb(index = 1)]
   #[pb(index = 1)]
@@ -32,11 +116,23 @@ pub struct ApplyActionPayloadPB {
   pub actions: Vec<BlockActionPB>,
   pub actions: Vec<BlockActionPB>,
 }
 }
 
 
-#[derive(Default, ProtoBuf)]
-pub struct GetDocumentDataPayloadPB {
-  #[pb(index = 1)]
+pub struct ApplyActionParams {
   pub document_id: String,
   pub document_id: String,
-  // Support customize initial data
+  pub actions: Vec<BlockAction>,
+}
+
+impl TryInto<ApplyActionParams> for ApplyActionPayloadPB {
+  type Error = ErrorCode;
+  fn try_into(self) -> Result<ApplyActionParams, Self::Error> {
+    let document_id =
+      NotEmptyStr::parse(self.document_id).map_err(|_| ErrorCode::DocumentIdIsEmpty)?;
+    let actions = NotEmptyVec::parse(self.actions).map_err(|_| ErrorCode::ApplyActionsIsEmpty)?;
+    let actions = actions.0.into_iter().map(BlockAction::from).collect();
+    Ok(ApplyActionParams {
+      document_id: document_id.0,
+      actions,
+    })
+  }
 }
 }
 
 
 #[derive(Default, Debug, ProtoBuf)]
 #[derive(Default, Debug, ProtoBuf)]
@@ -226,3 +322,17 @@ pub struct ConvertDataPayloadPB {
   #[pb(index = 2)]
   #[pb(index = 2)]
   pub data: Vec<u8>,
   pub data: Vec<u8>,
 }
 }
+
+pub struct ConvertDataParams {
+  pub convert_type: ConvertType,
+  pub data: Vec<u8>,
+}
+
+impl TryInto<ConvertDataParams> for ConvertDataPayloadPB {
+  type Error = ErrorCode;
+  fn try_into(self) -> Result<ConvertDataParams, Self::Error> {
+    let convert_type = self.convert_type;
+    let data = self.data;
+    Ok(ConvertDataParams { convert_type, data })
+  }
+}

+ 78 - 16
frontend/rust-lib/flowy-document2/src/event_handler.rs

@@ -14,11 +14,16 @@ use collab_document::blocks::{
 use flowy_error::{FlowyError, FlowyResult};
 use flowy_error::{FlowyError, FlowyResult};
 use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult};
 use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult};
 
 
+use crate::entities::{
+  ApplyActionParams, CloseDocumentParams, ConvertDataParams, CreateDocumentParams,
+  DocumentRedoUndoParams, OpenDocumentParams,
+};
 use crate::{
 use crate::{
   entities::{
   entities::{
     ApplyActionPayloadPB, BlockActionPB, BlockActionPayloadPB, BlockActionTypePB, BlockEventPB,
     ApplyActionPayloadPB, BlockActionPB, BlockActionPayloadPB, BlockActionTypePB, BlockEventPB,
     BlockEventPayloadPB, BlockPB, CloseDocumentPayloadPB, ConvertDataPayloadPB, ConvertType,
     BlockEventPayloadPB, BlockPB, CloseDocumentPayloadPB, ConvertDataPayloadPB, ConvertType,
-    CreateDocumentPayloadPB, DeltaTypePB, DocEventPB, DocumentDataPB, OpenDocumentPayloadPB,
+    CreateDocumentPayloadPB, DeltaTypePB, DocEventPB, DocumentDataPB, DocumentRedoUndoPayloadPB,
+    DocumentRedoUndoResponsePB, OpenDocumentPayloadPB,
   },
   },
   manager::DocumentManager,
   manager::DocumentManager,
   parser::json::parser::JsonToDocumentParser,
   parser::json::parser::JsonToDocumentParser,
@@ -29,9 +34,8 @@ pub(crate) async fn create_document_handler(
   data: AFPluginData<CreateDocumentPayloadPB>,
   data: AFPluginData<CreateDocumentPayloadPB>,
   manager: AFPluginState<Arc<DocumentManager>>,
   manager: AFPluginState<Arc<DocumentManager>>,
 ) -> FlowyResult<()> {
 ) -> FlowyResult<()> {
-  let data = data.into_inner();
-  let initial_data = data.initial_data.map(|data| data.into());
-  manager.create_document(data.document_id, initial_data)?;
+  let params: CreateDocumentParams = data.into_inner().try_into()?;
+  manager.create_document(params.document_id, params.initial_data)?;
   Ok(())
   Ok(())
 }
 }
 
 
@@ -40,8 +44,9 @@ pub(crate) async fn open_document_handler(
   data: AFPluginData<OpenDocumentPayloadPB>,
   data: AFPluginData<OpenDocumentPayloadPB>,
   manager: AFPluginState<Arc<DocumentManager>>,
   manager: AFPluginState<Arc<DocumentManager>>,
 ) -> DataResult<DocumentDataPB, FlowyError> {
 ) -> DataResult<DocumentDataPB, FlowyError> {
-  let context = data.into_inner();
-  let document = manager.open_document(context.document_id)?;
+  let params: OpenDocumentParams = data.into_inner().try_into()?;
+  let doc_id = params.document_id;
+  let document = manager.get_or_open_document(doc_id)?;
   let document_data = document.lock().get_document()?;
   let document_data = document.lock().get_document()?;
   data_result_ok(DocumentDataPB::from(document_data))
   data_result_ok(DocumentDataPB::from(document_data))
 }
 }
@@ -50,8 +55,9 @@ pub(crate) async fn close_document_handler(
   data: AFPluginData<CloseDocumentPayloadPB>,
   data: AFPluginData<CloseDocumentPayloadPB>,
   manager: AFPluginState<Arc<DocumentManager>>,
   manager: AFPluginState<Arc<DocumentManager>>,
 ) -> FlowyResult<()> {
 ) -> FlowyResult<()> {
-  let context = data.into_inner();
-  manager.close_document(&context.document_id)?;
+  let params: CloseDocumentParams = data.into_inner().try_into()?;
+  let doc_id = params.document_id;
+  manager.close_document(&doc_id)?;
   Ok(())
   Ok(())
 }
 }
 
 
@@ -61,8 +67,9 @@ pub(crate) async fn get_document_data_handler(
   data: AFPluginData<OpenDocumentPayloadPB>,
   data: AFPluginData<OpenDocumentPayloadPB>,
   manager: AFPluginState<Arc<DocumentManager>>,
   manager: AFPluginState<Arc<DocumentManager>>,
 ) -> DataResult<DocumentDataPB, FlowyError> {
 ) -> DataResult<DocumentDataPB, FlowyError> {
-  let context = data.into_inner();
-  let document = manager.get_document(context.document_id)?;
+  let params: OpenDocumentParams = data.into_inner().try_into()?;
+  let doc_id = params.document_id;
+  let document = manager.get_document_from_disk(doc_id)?;
   let document_data = document.lock().get_document()?;
   let document_data = document.lock().get_document()?;
   data_result_ok(DocumentDataPB::from(document_data))
   data_result_ok(DocumentDataPB::from(document_data))
 }
 }
@@ -72,10 +79,10 @@ pub(crate) async fn apply_action_handler(
   data: AFPluginData<ApplyActionPayloadPB>,
   data: AFPluginData<ApplyActionPayloadPB>,
   manager: AFPluginState<Arc<DocumentManager>>,
   manager: AFPluginState<Arc<DocumentManager>>,
 ) -> FlowyResult<()> {
 ) -> FlowyResult<()> {
-  let context = data.into_inner();
-  let doc_id = context.document_id;
-  let document = manager.open_document(doc_id)?;
-  let actions = context.actions.into_iter().map(BlockAction::from).collect();
+  let params: ApplyActionParams = data.into_inner().try_into()?;
+  let doc_id = params.document_id;
+  let document = manager.get_or_open_document(doc_id)?;
+  let actions = params.actions;
   document.lock().apply_action(actions);
   document.lock().apply_action(actions);
   Ok(())
   Ok(())
 }
 }
@@ -92,8 +99,9 @@ pub(crate) async fn convert_data_to_document(
 pub fn convert_data_to_document_internal(
 pub fn convert_data_to_document_internal(
   payload: ConvertDataPayloadPB,
   payload: ConvertDataPayloadPB,
 ) -> Result<DocumentDataPB, FlowyError> {
 ) -> Result<DocumentDataPB, FlowyError> {
-  let convert_type = payload.convert_type;
-  let data = payload.data;
+  let params: ConvertDataParams = payload.try_into()?;
+  let convert_type = params.convert_type;
+  let data = params.data;
   match convert_type {
   match convert_type {
     ConvertType::Json => {
     ConvertType::Json => {
       let json_str = String::from_utf8(data).map_err(|_| FlowyError::invalid_data())?;
       let json_str = String::from_utf8(data).map_err(|_| FlowyError::invalid_data())?;
@@ -103,6 +111,60 @@ pub fn convert_data_to_document_internal(
   }
   }
 }
 }
 
 
+pub(crate) async fn redo_handler(
+  data: AFPluginData<DocumentRedoUndoPayloadPB>,
+  manager: AFPluginState<Arc<DocumentManager>>,
+) -> DataResult<DocumentRedoUndoResponsePB, FlowyError> {
+  let params: DocumentRedoUndoParams = data.into_inner().try_into()?;
+  let doc_id = params.document_id;
+  let document = manager.get_or_open_document(doc_id)?;
+  let document = document.lock();
+  let redo = document.redo();
+  let can_redo = document.can_redo();
+  let can_undo = document.can_undo();
+  data_result_ok(DocumentRedoUndoResponsePB {
+    can_redo,
+    can_undo,
+    is_success: redo,
+  })
+}
+
+pub(crate) async fn undo_handler(
+  data: AFPluginData<DocumentRedoUndoPayloadPB>,
+  manager: AFPluginState<Arc<DocumentManager>>,
+) -> DataResult<DocumentRedoUndoResponsePB, FlowyError> {
+  let params: DocumentRedoUndoParams = data.into_inner().try_into()?;
+  let doc_id = params.document_id;
+  let document = manager.get_or_open_document(doc_id)?;
+  let document = document.lock();
+  let undo = document.undo();
+  let can_redo = document.can_redo();
+  let can_undo = document.can_undo();
+  data_result_ok(DocumentRedoUndoResponsePB {
+    can_redo,
+    can_undo,
+    is_success: undo,
+  })
+}
+
+pub(crate) async fn can_undo_redo_handler(
+  data: AFPluginData<DocumentRedoUndoPayloadPB>,
+  manager: AFPluginState<Arc<DocumentManager>>,
+) -> DataResult<DocumentRedoUndoResponsePB, FlowyError> {
+  let params: DocumentRedoUndoParams = data.into_inner().try_into()?;
+  let doc_id = params.document_id;
+  let document = manager.get_or_open_document(doc_id)?;
+  let document = document.lock();
+  let can_redo = document.can_redo();
+  let can_undo = document.can_undo();
+  drop(document);
+  data_result_ok(DocumentRedoUndoResponsePB {
+    can_redo,
+    can_undo,
+    is_success: true,
+  })
+}
+
 impl From<BlockActionPB> for BlockAction {
 impl From<BlockActionPB> for BlockAction {
   fn from(pb: BlockActionPB) -> Self {
   fn from(pb: BlockActionPB) -> Self {
     Self {
     Self {

+ 25 - 3
frontend/rust-lib/flowy-document2/src/event_map.rs

@@ -6,8 +6,9 @@ use lib_dispatch::prelude::AFPlugin;
 
 
 use crate::{
 use crate::{
   event_handler::{
   event_handler::{
-    apply_action_handler, close_document_handler, convert_data_to_document,
-    create_document_handler, get_document_data_handler, open_document_handler,
+    apply_action_handler, can_undo_redo_handler, close_document_handler, convert_data_to_document,
+    create_document_handler, get_document_data_handler, open_document_handler, redo_handler,
+    undo_handler,
   },
   },
   manager::DocumentManager,
   manager::DocumentManager,
 };
 };
@@ -26,6 +27,9 @@ pub fn init(document_manager: Arc<DocumentManager>) -> AFPlugin {
     DocumentEvent::ConvertDataToDocument,
     DocumentEvent::ConvertDataToDocument,
     convert_data_to_document,
     convert_data_to_document,
   );
   );
+  plugin = plugin.event(DocumentEvent::Redo, redo_handler);
+  plugin = plugin.event(DocumentEvent::Undo, undo_handler);
+  plugin = plugin.event(DocumentEvent::CanUndoRedo, can_undo_redo_handler);
 
 
   plugin
   plugin
 }
 }
@@ -45,9 +49,27 @@ pub enum DocumentEvent {
   #[event(input = "ApplyActionPayloadPB")]
   #[event(input = "ApplyActionPayloadPB")]
   ApplyAction = 3,
   ApplyAction = 3,
 
 
-  #[event(input = "GetDocumentDataPayloadPB")]
+  #[event(input = "OpenDocumentPayloadPB")]
   GetDocumentData = 4,
   GetDocumentData = 4,
 
 
   #[event(input = "ConvertDataPayloadPB", output = "DocumentDataPB")]
   #[event(input = "ConvertDataPayloadPB", output = "DocumentDataPB")]
   ConvertDataToDocument = 5,
   ConvertDataToDocument = 5,
+
+  #[event(
+    input = "DocumentRedoUndoPayloadPB",
+    output = "DocumentRedoUndoResponsePB"
+  )]
+  Redo = 6,
+
+  #[event(
+    input = "DocumentRedoUndoPayloadPB",
+    output = "DocumentRedoUndoResponsePB"
+  )]
+  Undo = 7,
+
+  #[event(
+    input = "DocumentRedoUndoPayloadPB",
+    output = "DocumentRedoUndoResponsePB"
+  )]
+  CanUndoRedo = 8,
 }
 }

+ 1 - 0
frontend/rust-lib/flowy-document2/src/lib.rs

@@ -9,3 +9,4 @@ pub mod parser;
 pub mod protobuf;
 pub mod protobuf;
 
 
 mod notification;
 mod notification;
+mod parse;

+ 7 - 6
frontend/rust-lib/flowy-document2/src/manager.rs

@@ -54,16 +54,15 @@ impl DocumentManager {
     Ok(document)
     Ok(document)
   }
   }
 
 
-  pub fn open_document(&self, doc_id: String) -> FlowyResult<Arc<Document>> {
+  /// get document
+  /// read the existing document from the map if it exists, otherwise read it from the disk and write it to the map.
+  pub fn get_or_open_document(&self, doc_id: String) -> FlowyResult<Arc<Document>> {
     if let Some(doc) = self.documents.read().get(&doc_id) {
     if let Some(doc) = self.documents.read().get(&doc_id) {
       return Ok(doc.clone());
       return Ok(doc.clone());
     }
     }
     tracing::debug!("open_document: {:?}", &doc_id);
     tracing::debug!("open_document: {:?}", &doc_id);
-    let uid = self.user.user_id()?;
-    let db = self.user.collab_db()?;
-    let collab = self.collab_builder.build(uid, &doc_id, "document", db);
     // read the existing document from the disk.
     // read the existing document from the disk.
-    let document = Arc::new(Document::new(collab)?);
+    let document = self.get_document_from_disk(doc_id.clone())?;
     // save the document to the memory and read it from the memory if we open the same document again.
     // save the document to the memory and read it from the memory if we open the same document again.
     // and we don't want to subscribe to the document changes if we open the same document again.
     // and we don't want to subscribe to the document changes if we open the same document again.
     self
     self
@@ -87,7 +86,9 @@ impl DocumentManager {
     Ok(document)
     Ok(document)
   }
   }
 
 
-  pub fn get_document(&self, doc_id: String) -> FlowyResult<Arc<Document>> {
+  /// get document
+  /// read the existing document from the disk.
+  pub fn get_document_from_disk(&self, doc_id: String) -> FlowyResult<Arc<Document>> {
     let uid = self.user.user_id()?;
     let uid = self.user.user_id()?;
     let db = self.user.collab_db()?;
     let db = self.user.collab_db()?;
     let collab = self.collab_builder.build(uid, &doc_id, "document", db);
     let collab = self.collab_builder.build(uid, &doc_id, "document", db);

+ 23 - 0
frontend/rust-lib/flowy-document2/src/parse.rs

@@ -0,0 +1,23 @@
+#[derive(Debug)]
+pub struct NotEmptyStr(pub String);
+
+impl NotEmptyStr {
+  pub fn parse(s: String) -> Result<Self, String> {
+    if s.trim().is_empty() {
+      return Err("Input string is empty".to_owned());
+    }
+    Ok(Self(s))
+  }
+}
+
+#[derive(Debug)]
+pub struct NotEmptyVec<T>(pub Vec<T>);
+
+impl<T> NotEmptyVec<T> {
+  pub fn parse(v: Vec<T>) -> Result<Self, String> {
+    if v.is_empty() {
+      return Err("Input vector is empty".to_owned());
+    }
+    Ok(Self(v))
+  }
+}

+ 6 - 27
frontend/rust-lib/flowy-document2/tests/document/document_insert_test.rs

@@ -1,25 +1,21 @@
-use std::{collections::HashMap, sync::Arc, vec};
+use std::{collections::HashMap, vec};
 
 
-use crate::document::util::default_collab_builder;
+use crate::document::util;
+use crate::document::util::gen_id;
 use collab_document::blocks::{Block, BlockAction, BlockActionPayload, BlockActionType};
 use collab_document::blocks::{Block, BlockAction, BlockActionPayload, BlockActionType};
 use flowy_document2::document_block_keys::PARAGRAPH_BLOCK_TYPE;
 use flowy_document2::document_block_keys::PARAGRAPH_BLOCK_TYPE;
-use flowy_document2::document_data::default_document_data;
-use flowy_document2::{document::Document, manager::DocumentManager};
-use nanoid::nanoid;
-
-use super::util::FakeUser;
 
 
 #[test]
 #[test]
 fn document_apply_insert_block_with_empty_parent_id() {
 fn document_apply_insert_block_with_empty_parent_id() {
-  let (_, document, page_id) = create_and_open_empty_document();
+  let (_, document, page_id) = util::create_and_open_empty_document();
 
 
   // create a text block with no parent
   // create a text block with no parent
-  let text_block_id = nanoid!(10);
+  let text_block_id = gen_id();
   let text_block = Block {
   let text_block = Block {
     id: text_block_id.clone(),
     id: text_block_id.clone(),
     ty: PARAGRAPH_BLOCK_TYPE.to_string(),
     ty: PARAGRAPH_BLOCK_TYPE.to_string(),
     parent: "".to_string(),
     parent: "".to_string(),
-    children: nanoid!(10),
+    children: gen_id(),
     external_id: None,
     external_id: None,
     external_type: None,
     external_type: None,
     data: HashMap::new(),
     data: HashMap::new(),
@@ -38,20 +34,3 @@ fn document_apply_insert_block_with_empty_parent_id() {
   let block = document.lock().get_block(&text_block_id).unwrap();
   let block = document.lock().get_block(&text_block_id).unwrap();
   assert_eq!(block.parent, page_id);
   assert_eq!(block.parent, page_id);
 }
 }
-
-fn create_and_open_empty_document() -> (DocumentManager, Arc<Document>, String) {
-  let user = FakeUser::new();
-  let manager = DocumentManager::new(Arc::new(user), default_collab_builder());
-
-  let doc_id: String = nanoid!(10);
-  let data = default_document_data();
-
-  // create a document
-  _ = manager
-    .create_document(doc_id.clone(), Some(data.clone()))
-    .unwrap();
-
-  let document = manager.open_document(doc_id).unwrap();
-
-  (manager, document, data.page_id)
-}

+ 59 - 0
frontend/rust-lib/flowy-document2/tests/document/document_redo_undo_test.rs

@@ -0,0 +1,59 @@
+use crate::document::util::{default_collab_builder, gen_document_id, gen_id, FakeUser};
+use collab_document::blocks::{Block, BlockAction, BlockActionPayload, BlockActionType};
+use flowy_document2::document_block_keys::PARAGRAPH_BLOCK_TYPE;
+use flowy_document2::document_data::default_document_data;
+use flowy_document2::manager::DocumentManager;
+use std::collections::HashMap;
+use std::sync::Arc;
+
+#[tokio::test]
+async fn undo_redo_test() {
+  let user = FakeUser::new();
+  let manager = DocumentManager::new(Arc::new(user), default_collab_builder());
+
+  let doc_id: String = gen_document_id();
+  let data = default_document_data();
+
+  // create a document
+  _ = manager.create_document(doc_id.clone(), Some(data.clone()));
+
+  // open a document
+  let document = manager.get_or_open_document(doc_id.clone()).unwrap();
+  let document = document.lock();
+  let page_block = document.get_block(&data.page_id).unwrap();
+  let page_id = page_block.id;
+  let text_block_id = gen_id();
+
+  // insert a text block
+  let text_block = Block {
+    id: text_block_id.clone(),
+    ty: PARAGRAPH_BLOCK_TYPE.to_string(),
+    parent: page_id.clone(),
+    children: gen_id(),
+    external_id: None,
+    external_type: None,
+    data: HashMap::new(),
+  };
+  let insert_text_action = BlockAction {
+    action: BlockActionType::Insert,
+    payload: BlockActionPayload {
+      block: text_block,
+      parent_id: Some(page_id.clone()),
+      prev_id: None,
+    },
+  };
+  document.apply_action(vec![insert_text_action]);
+
+  let can_undo = document.can_undo();
+  assert_eq!(can_undo, true);
+  // undo the insert
+  let undo = document.undo();
+  assert_eq!(undo, true);
+  assert_eq!(document.get_block(&text_block_id), None);
+
+  let can_redo = document.can_redo();
+  assert!(can_redo);
+  // redo the insert
+  let redo = document.redo();
+  assert_eq!(redo, true);
+}

+ 17 - 18
frontend/rust-lib/flowy-document2/tests/document/document_test.rs

@@ -1,14 +1,13 @@
 use std::{collections::HashMap, sync::Arc, vec};
 use std::{collections::HashMap, sync::Arc, vec};
 
 
 use collab_document::blocks::{Block, BlockAction, BlockActionPayload, BlockActionType};
 use collab_document::blocks::{Block, BlockAction, BlockActionPayload, BlockActionType};
-use nanoid::nanoid;
 use serde_json::{json, to_value, Value};
 use serde_json::{json, to_value, Value};
 
 
 use flowy_document2::document_block_keys::PARAGRAPH_BLOCK_TYPE;
 use flowy_document2::document_block_keys::PARAGRAPH_BLOCK_TYPE;
 use flowy_document2::document_data::default_document_data;
 use flowy_document2::document_data::default_document_data;
 use flowy_document2::manager::DocumentManager;
 use flowy_document2::manager::DocumentManager;
 
 
-use crate::document::util::default_collab_builder;
+use crate::document::util::{default_collab_builder, gen_document_id, gen_id};
 
 
 use super::util::FakeUser;
 use super::util::FakeUser;
 
 
@@ -18,7 +17,7 @@ fn restore_document() {
   let manager = DocumentManager::new(Arc::new(user), default_collab_builder());
   let manager = DocumentManager::new(Arc::new(user), default_collab_builder());
 
 
   // create a document
   // create a document
-  let doc_id: String = nanoid!(10);
+  let doc_id: String = gen_document_id();
   let data = default_document_data();
   let data = default_document_data();
   let document_a = manager
   let document_a = manager
     .create_document(doc_id.clone(), Some(data.clone()))
     .create_document(doc_id.clone(), Some(data.clone()))
@@ -28,7 +27,7 @@ fn restore_document() {
 
 
   // open a document
   // open a document
   let data_b = manager
   let data_b = manager
-    .open_document(doc_id.clone())
+    .get_or_open_document(doc_id.clone())
     .unwrap()
     .unwrap()
     .lock()
     .lock()
     .get_document()
     .get_document()
@@ -41,7 +40,7 @@ fn restore_document() {
   _ = manager.create_document(doc_id.clone(), Some(data.clone()));
   _ = manager.create_document(doc_id.clone(), Some(data.clone()));
   // open a document
   // open a document
   let data_b = manager
   let data_b = manager
-    .open_document(doc_id.clone())
+    .get_or_open_document(doc_id.clone())
     .unwrap()
     .unwrap()
     .lock()
     .lock()
     .get_document()
     .get_document()
@@ -57,22 +56,22 @@ fn document_apply_insert_action() {
   let user = FakeUser::new();
   let user = FakeUser::new();
   let manager = DocumentManager::new(Arc::new(user), default_collab_builder());
   let manager = DocumentManager::new(Arc::new(user), default_collab_builder());
 
 
-  let doc_id: String = nanoid!(10);
+  let doc_id: String = gen_document_id();
   let data = default_document_data();
   let data = default_document_data();
 
 
   // create a document
   // create a document
   _ = manager.create_document(doc_id.clone(), Some(data.clone()));
   _ = manager.create_document(doc_id.clone(), Some(data.clone()));
 
 
   // open a document
   // open a document
-  let document = manager.open_document(doc_id.clone()).unwrap();
+  let document = manager.get_or_open_document(doc_id.clone()).unwrap();
   let page_block = document.lock().get_block(&data.page_id).unwrap();
   let page_block = document.lock().get_block(&data.page_id).unwrap();
 
 
   // insert a text block
   // insert a text block
   let text_block = Block {
   let text_block = Block {
-    id: nanoid!(10),
+    id: gen_id(),
     ty: PARAGRAPH_BLOCK_TYPE.to_string(),
     ty: PARAGRAPH_BLOCK_TYPE.to_string(),
     parent: page_block.id,
     parent: page_block.id,
-    children: nanoid!(10),
+    children: gen_id(),
     external_id: None,
     external_id: None,
     external_type: None,
     external_type: None,
     data: HashMap::new(),
     data: HashMap::new(),
@@ -92,7 +91,7 @@ fn document_apply_insert_action() {
 
 
   // re-open the document
   // re-open the document
   let data_b = manager
   let data_b = manager
-    .open_document(doc_id.clone())
+    .get_or_open_document(doc_id.clone())
     .unwrap()
     .unwrap()
     .lock()
     .lock()
     .get_document()
     .get_document()
@@ -108,14 +107,14 @@ fn document_apply_update_page_action() {
   let user = FakeUser::new();
   let user = FakeUser::new();
   let manager = DocumentManager::new(Arc::new(user), default_collab_builder());
   let manager = DocumentManager::new(Arc::new(user), default_collab_builder());
 
 
-  let doc_id: String = nanoid!(10);
+  let doc_id: String = gen_document_id();
   let data = default_document_data();
   let data = default_document_data();
 
 
   // create a document
   // create a document
   _ = manager.create_document(doc_id.clone(), Some(data.clone()));
   _ = manager.create_document(doc_id.clone(), Some(data.clone()));
 
 
   // open a document
   // open a document
-  let document = manager.open_document(doc_id.clone()).unwrap();
+  let document = manager.get_or_open_document(doc_id.clone()).unwrap();
   let page_block = document.lock().get_block(&data.page_id).unwrap();
   let page_block = document.lock().get_block(&data.page_id).unwrap();
 
 
   let mut page_block_clone = page_block;
   let mut page_block_clone = page_block;
@@ -139,7 +138,7 @@ fn document_apply_update_page_action() {
   _ = manager.close_document(&doc_id);
   _ = manager.close_document(&doc_id);
 
 
   // re-open the document
   // re-open the document
-  let document = manager.open_document(doc_id).unwrap();
+  let document = manager.get_or_open_document(doc_id).unwrap();
   let page_block_new = document.lock().get_block(&data.page_id).unwrap();
   let page_block_new = document.lock().get_block(&data.page_id).unwrap();
   assert_eq!(page_block_old, page_block_new);
   assert_eq!(page_block_old, page_block_new);
   assert!(page_block_new.data.contains_key("delta"));
   assert!(page_block_new.data.contains_key("delta"));
@@ -150,23 +149,23 @@ fn document_apply_update_action() {
   let user = FakeUser::new();
   let user = FakeUser::new();
   let manager = DocumentManager::new(Arc::new(user), default_collab_builder());
   let manager = DocumentManager::new(Arc::new(user), default_collab_builder());
 
 
-  let doc_id: String = nanoid!(10);
+  let doc_id: String = gen_document_id();
   let data = default_document_data();
   let data = default_document_data();
 
 
   // create a document
   // create a document
   _ = manager.create_document(doc_id.clone(), Some(data.clone()));
   _ = manager.create_document(doc_id.clone(), Some(data.clone()));
 
 
   // open a document
   // open a document
-  let document = manager.open_document(doc_id.clone()).unwrap();
+  let document = manager.get_or_open_document(doc_id.clone()).unwrap();
   let page_block = document.lock().get_block(&data.page_id).unwrap();
   let page_block = document.lock().get_block(&data.page_id).unwrap();
 
 
   // insert a text block
   // insert a text block
-  let text_block_id = nanoid!(10);
+  let text_block_id = gen_id();
   let text_block = Block {
   let text_block = Block {
     id: text_block_id.clone(),
     id: text_block_id.clone(),
     ty: PARAGRAPH_BLOCK_TYPE.to_string(),
     ty: PARAGRAPH_BLOCK_TYPE.to_string(),
     parent: page_block.id,
     parent: page_block.id,
-    children: nanoid!(10),
+    children: gen_id(),
     external_id: None,
     external_id: None,
     external_type: None,
     external_type: None,
     data: HashMap::new(),
     data: HashMap::new(),
@@ -207,7 +206,7 @@ fn document_apply_update_action() {
   _ = manager.close_document(&doc_id);
   _ = manager.close_document(&doc_id);
 
 
   // re-open the document
   // re-open the document
-  let document = manager.open_document(doc_id.clone()).unwrap();
+  let document = manager.get_or_open_document(doc_id.clone()).unwrap();
   let block = document.lock().get_block(&text_block_id).unwrap();
   let block = document.lock().get_block(&text_block_id).unwrap();
   assert_eq!(block.data, updated_text_block_data);
   assert_eq!(block.data, updated_text_block_data);
   // close a document
   // close a document

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

@@ -1,4 +1,5 @@
 mod document_insert_test;
 mod document_insert_test;
+mod document_redo_undo_test;
 mod document_test;
 mod document_test;
 mod event_handler_test;
 mod event_handler_test;
 mod util;
 mod util;

+ 30 - 1
frontend/rust-lib/flowy-document2/tests/document/util.rs

@@ -3,11 +3,14 @@ use appflowy_integrate::collab_builder::{AppFlowyCollabBuilder, CloudStorageType
 use std::sync::Arc;
 use std::sync::Arc;
 
 
 use appflowy_integrate::RocksCollabDB;
 use appflowy_integrate::RocksCollabDB;
+use flowy_document2::document::Document;
 use parking_lot::Once;
 use parking_lot::Once;
 use tempfile::TempDir;
 use tempfile::TempDir;
 use tracing_subscriber::{fmt::Subscriber, util::SubscriberInitExt, EnvFilter};
 use tracing_subscriber::{fmt::Subscriber, util::SubscriberInitExt, EnvFilter};
 
 
-use flowy_document2::manager::DocumentUser;
+use flowy_document2::document_data::default_document_data;
+use flowy_document2::manager::{DocumentManager, DocumentUser};
+use nanoid::nanoid;
 
 
 pub struct FakeUser {
 pub struct FakeUser {
   kv: Arc<RocksCollabDB>,
   kv: Arc<RocksCollabDB>,
@@ -53,3 +56,29 @@ pub fn default_collab_builder() -> Arc<AppFlowyCollabBuilder> {
   let builder = AppFlowyCollabBuilder::new(CloudStorageType::Local, None);
   let builder = AppFlowyCollabBuilder::new(CloudStorageType::Local, None);
   Arc::new(builder)
   Arc::new(builder)
 }
 }
+
+pub fn create_and_open_empty_document() -> (DocumentManager, Arc<Document>, String) {
+  let user = FakeUser::new();
+  let manager = DocumentManager::new(Arc::new(user), default_collab_builder());
+
+  let doc_id: String = gen_document_id();
+  let data = default_document_data();
+
+  // create a document
+  _ = manager
+    .create_document(doc_id.clone(), Some(data.clone()))
+    .unwrap();
+
+  let document = manager.get_or_open_document(doc_id).unwrap();
+
+  (manager, document, data.page_id)
+}
+
+pub fn gen_document_id() -> String {
+  let uuid = uuid::Uuid::new_v4();
+  uuid.to_string()
+}
+
+pub fn gen_id() -> String {
+  nanoid!(10)
+}

+ 6 - 0
frontend/rust-lib/flowy-error/src/code.rs

@@ -202,6 +202,12 @@ pub enum ErrorCode {
 
 
   #[error("Only one application can access the database")]
   #[error("Only one application can access the database")]
   MultipleDBInstance = 66,
   MultipleDBInstance = 66,
+
+  #[error("Document id is empty")]
+  DocumentIdIsEmpty = 67,
+
+  #[error("Apply actions is empty")]
+  ApplyActionsIsEmpty = 68,
 }
 }
 
 
 impl ErrorCode {
 impl ErrorCode {

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

@@ -11,6 +11,7 @@ flowy-user = { path = "../flowy-user"}
 flowy-net = { path = "../flowy-net"}
 flowy-net = { path = "../flowy-net"}
 flowy-folder2 = { path = "../flowy-folder2", features = ["test_helper"] }
 flowy-folder2 = { path = "../flowy-folder2", features = ["test_helper"] }
 flowy-database2 = { path = "../flowy-database2" }
 flowy-database2 = { path = "../flowy-database2" }
+flowy-document2 = { path = "../flowy-document2" }
 lib-dispatch = { path = "../lib-dispatch" }
 lib-dispatch = { path = "../lib-dispatch" }
 lib-ot = { path = "../../../shared-lib/lib-ot" }
 lib-ot = { path = "../../../shared-lib/lib-ot" }
 lib-infra = { path = "../../../shared-lib/lib-infra" }
 lib-infra = { path = "../../../shared-lib/lib-infra" }

+ 107 - 0
frontend/rust-lib/flowy-test/src/document_event.rs

@@ -0,0 +1,107 @@
+use crate::event_builder::EventBuilder;
+use crate::FlowyCoreTest;
+use flowy_document2::entities::*;
+use flowy_document2::event_map::DocumentEvent;
+use flowy_folder2::entities::{CreateViewPayloadPB, ViewLayoutPB, ViewPB};
+use flowy_folder2::event_map::FolderEvent;
+
+pub struct DocumentEventTest {
+  inner: FlowyCoreTest,
+}
+
+pub struct OpenDocumentData {
+  pub id: String,
+  pub data: DocumentDataPB,
+}
+
+impl DocumentEventTest {
+  pub async fn new() -> Self {
+    let sdk = FlowyCoreTest::new_with_user().await;
+    Self { inner: sdk }
+  }
+
+  pub async fn create_document(&self) -> ViewPB {
+    let core = &self.inner;
+    let current_workspace = core.get_current_workspace().await.workspace;
+    let parent_id = current_workspace.id.clone();
+
+    let payload = CreateViewPayloadPB {
+      parent_view_id: parent_id.to_string(),
+      name: "document".to_string(),
+      desc: "".to_string(),
+      thumbnail: None,
+      layout: ViewLayoutPB::Document,
+      initial_data: vec![],
+      meta: Default::default(),
+      set_as_current: true,
+    };
+    EventBuilder::new(core.clone())
+      .event(FolderEvent::CreateView)
+      .payload(payload)
+      .async_send()
+      .await
+      .parse::<ViewPB>()
+  }
+
+  pub async fn open_document(&self, doc_id: String) -> OpenDocumentData {
+    let core = &self.inner;
+    let payload = OpenDocumentPayloadPB {
+      document_id: doc_id.clone(),
+    };
+    let data = EventBuilder::new(core.clone())
+      .event(DocumentEvent::OpenDocument)
+      .payload(payload)
+      .async_send()
+      .await
+      .parse::<DocumentDataPB>();
+    OpenDocumentData { id: doc_id, data }
+  }
+
+  pub async fn apply_actions(&self, payload: ApplyActionPayloadPB) {
+    let core = &self.inner;
+    EventBuilder::new(core.clone())
+      .event(DocumentEvent::ApplyAction)
+      .payload(payload)
+      .async_send()
+      .await;
+  }
+
+  pub async fn undo(&self, doc_id: String) -> DocumentRedoUndoResponsePB {
+    let core = &self.inner;
+    let payload = DocumentRedoUndoPayloadPB {
+      document_id: doc_id.clone(),
+    };
+    EventBuilder::new(core.clone())
+      .event(DocumentEvent::Undo)
+      .payload(payload)
+      .async_send()
+      .await
+      .parse::<DocumentRedoUndoResponsePB>()
+  }
+
+  pub async fn redo(&self, doc_id: String) -> DocumentRedoUndoResponsePB {
+    let core = &self.inner;
+    let payload = DocumentRedoUndoPayloadPB {
+      document_id: doc_id.clone(),
+    };
+    EventBuilder::new(core.clone())
+      .event(DocumentEvent::Redo)
+      .payload(payload)
+      .async_send()
+      .await
+      .parse::<DocumentRedoUndoResponsePB>()
+  }
+
+  pub async fn can_undo_redo(&self, doc_id: String) -> DocumentRedoUndoResponsePB {
+    let core = &self.inner;
+    let payload = DocumentRedoUndoPayloadPB {
+      document_id: doc_id.clone(),
+    };
+    EventBuilder::new(core.clone())
+      .event(DocumentEvent::CanUndoRedo)
+      .payload(payload)
+      .async_send()
+      .await
+      .parse::<DocumentRedoUndoResponsePB>()
+  }
+}

+ 1 - 0
frontend/rust-lib/flowy-test/src/lib.rs

@@ -15,6 +15,7 @@ use flowy_user::errors::FlowyError;
 use crate::event_builder::EventBuilder;
 use crate::event_builder::EventBuilder;
 use crate::user_event::{async_sign_up, init_user_setting, SignUpContext};
 use crate::user_event::{async_sign_up, init_user_setting, SignUpContext};
 
 
+pub mod document_event;
 pub mod event_builder;
 pub mod event_builder;
 pub mod folder_event;
 pub mod folder_event;
 pub mod user_event;
 pub mod user_event;

+ 2 - 0
frontend/rust-lib/flowy-test/tests/document/mod.rs

@@ -0,0 +1,2 @@
+mod test;
+mod utils;

+ 62 - 0
frontend/rust-lib/flowy-test/tests/document/test.rs

@@ -0,0 +1,62 @@
+use crate::document::utils::*;
+use flowy_document2::entities::*;
+use flowy_test::document_event::DocumentEventTest;
+
+#[tokio::test]
+async fn get_document_event_test() {
+  let test = DocumentEventTest::new().await;
+  let view = test.create_document().await;
+  let document = test.open_document(view.id).await;
+  let document_data = document.data;
+  assert!(!document_data.page_id.is_empty());
+  assert!(document_data.blocks.len() > 1);
+}
+
+#[tokio::test]
+async fn apply_document_event_test() {
+  let test = DocumentEventTest::new().await;
+  let view = test.create_document().await;
+  let doc_id = view.id.clone();
+  let document = test.open_document(doc_id.clone()).await;
+  let block_count = document.data.blocks.len();
+  let insert_action = gen_insert_block_action(document);
+  let payload = ApplyActionPayloadPB {
+    document_id: doc_id.clone(),
+    actions: vec![insert_action],
+  };
+  test.apply_actions(payload).await;
+  let document = test.open_document(doc_id).await;
+  let document_data = document.data;
+  let block_count_after = document_data.blocks.len();
+  assert_eq!(block_count_after, block_count + 1);
+}
+
+#[tokio::test]
+async fn undo_redo_event_test() {
+  let test = DocumentEventTest::new().await;
+  let view = test.create_document().await;
+  let doc_id = view.id.clone();
+
+  let document = test.open_document(doc_id.clone()).await;
+  let insert_action = gen_insert_block_action(document);
+  let payload = ApplyActionPayloadPB {
+    document_id: doc_id.clone(),
+    actions: vec![insert_action],
+  };
+  test.apply_actions(payload).await;
+  let block_count_after_insert = test.open_document(doc_id.clone()).await.data.blocks.len();
+
+  // undo insert action
+  let can_undo = test.can_undo_redo(doc_id.clone()).await.can_undo;
+  assert!(can_undo);
+  test.undo(doc_id.clone()).await;
+  let block_count_after_undo = test.open_document(doc_id.clone()).await.data.blocks.len();
+  assert_eq!(block_count_after_undo, block_count_after_insert - 1);
+
+  // redo insert action
+  let can_redo = test.can_undo_redo(doc_id.clone()).await.can_redo;
+  assert!(can_redo);
+  test.redo(doc_id.clone()).await;
+  let block_count_after_redo = test.open_document(doc_id.clone()).await.data.blocks.len();
+  assert_eq!(block_count_after_redo, block_count_after_insert);
+}

+ 58 - 0
frontend/rust-lib/flowy-test/tests/document/utils.rs

@@ -0,0 +1,58 @@
+use flowy_document2::entities::*;
+use flowy_test::document_event::OpenDocumentData;
+use nanoid::nanoid;
+use std::collections::HashMap;
+
+pub fn gen_id() -> String {
+  nanoid!(10)
+}
+
+pub struct ParseDocumentData {
+  pub doc_id: String,
+  pub page_id: String,
+  pub blocks: HashMap<String, BlockPB>,
+  pub children_map: HashMap<String, ChildrenPB>,
+  pub first_block_id: String,
+}
+pub fn parse_document_data(document: OpenDocumentData) -> ParseDocumentData {
+  let doc_id = document.id.clone();
+  let data = document.data;
+  let page_id = data.page_id;
+  let blocks = data.blocks;
+  let children_map = data.meta.children_map;
+  let page_block = blocks.get(&page_id).unwrap();
+  let children_id = page_block.children_id.clone();
+  let children = children_map.get(&children_id).unwrap();
+  let block_id = children.children.get(0).unwrap().to_string();
+  ParseDocumentData {
+    doc_id,
+    page_id,
+    blocks,
+    children_map,
+    first_block_id: block_id,
+  }
+}
+
+pub fn gen_insert_block_action(document: OpenDocumentData) -> BlockActionPB {
+  let parse_data = parse_document_data(document);
+  let first_block_id = parse_data.first_block_id;
+  let block = parse_data.blocks.get(&first_block_id).unwrap();
+  let page_id = parse_data.page_id;
+  let data = block.data.clone();
+  let new_block_id = gen_id();
+  let new_block = BlockPB {
+    id: new_block_id.clone(),
+    ty: block.ty.clone(),
+    data,
+    parent_id: page_id.clone(),
+    children_id: gen_id(),
+  };
+  BlockActionPB {
+    action: BlockActionTypePB::Insert,
+    payload: BlockActionPayloadPB {
+      block: new_block,
+      prev_id: Some(first_block_id),
+      parent_id: Some(page_id),
+    },
+  }
+}

+ 1 - 0
frontend/rust-lib/flowy-test/tests/main.rs

@@ -1,3 +1,4 @@
 mod database;
 mod database;
+mod document;
 mod folder;
 mod folder;
 mod user;
 mod user;

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