Browse Source

chore: implement document editor trait (#1321)

Nathan.fooo 2 years ago
parent
commit
7dbd9fe8cd

+ 5 - 3
frontend/rust-lib/flowy-document/src/editor/document.rs

@@ -2,7 +2,9 @@ use bytes::Bytes;
 use flowy_error::{FlowyError, FlowyResult};
 use flowy_revision::{RevisionObjectDeserializer, RevisionObjectSerializer};
 use flowy_sync::entities::revision::Revision;
-use lib_ot::core::{Body, Extension, Interval, NodeDataBuilder, NodeOperation, NodeTree, NodeTreeContext, Transaction};
+use lib_ot::core::{
+    Body, Extension, NodeDataBuilder, NodeOperation, NodeTree, NodeTreeContext, Selection, Transaction,
+};
 use lib_ot::text_delta::TextOperationBuilder;
 
 #[derive(Debug)]
@@ -46,8 +48,8 @@ pub fn initial_document_content() -> String {
         nodes: vec![editor_node],
     };
     let extension = Extension::TextSelection {
-        before_selection: Interval::default(),
-        after_selection: Interval::default(),
+        before_selection: Selection::default(),
+        after_selection: Selection::default(),
     };
     let transaction = Transaction {
         operations: vec![node_operation].into(),

+ 206 - 7
frontend/rust-lib/flowy-document/src/editor/document_serde.rs

@@ -1,6 +1,11 @@
 use crate::editor::document::Document;
 
-use lib_ot::core::{AttributeHashMap, Body, NodeData, NodeId, NodeTree};
+use bytes::Bytes;
+use flowy_error::FlowyResult;
+use lib_ot::core::{
+    AttributeHashMap, Body, Changeset, Extension, NodeData, NodeId, NodeOperation, NodeTree, Path, Selection,
+    Transaction,
+};
 use lib_ot::text_delta::TextOperations;
 use serde::de::{self, MapAccess, Visitor};
 use serde::ser::{SerializeMap, SerializeSeq};
@@ -67,8 +72,157 @@ impl<'de> Deserialize<'de> for Document {
 #[derive(Debug)]
 struct DocumentContentSerializer<'a>(pub &'a Document);
 
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct DocumentTransaction {
+    #[serde(default)]
+    operations: Vec<DocumentOperation>,
+
+    #[serde(default)]
+    before_selection: Selection,
+
+    #[serde(default)]
+    after_selection: Selection,
+}
+
+impl DocumentTransaction {
+    pub fn to_json(&self) -> FlowyResult<String> {
+        let json = serde_json::to_string(self)?;
+        Ok(json)
+    }
+
+    pub fn to_bytes(&self) -> FlowyResult<Bytes> {
+        let data = serde_json::to_vec(&self)?;
+        Ok(Bytes::from(data))
+    }
+
+    pub fn from_bytes(bytes: Bytes) -> FlowyResult<Self> {
+        let transaction = serde_json::from_slice(&bytes)?;
+        Ok(transaction)
+    }
+}
+
+impl std::convert::From<Transaction> for DocumentTransaction {
+    fn from(transaction: Transaction) -> Self {
+        let (before_selection, after_selection) = match transaction.extension {
+            Extension::Empty => (Selection::default(), Selection::default()),
+            Extension::TextSelection {
+                before_selection,
+                after_selection,
+            } => (before_selection, after_selection),
+        };
+
+        DocumentTransaction {
+            operations: transaction
+                .operations
+                .into_inner()
+                .into_iter()
+                .map(|operation| operation.as_ref().into())
+                .collect(),
+            before_selection,
+            after_selection,
+        }
+    }
+}
+
+impl std::convert::From<DocumentTransaction> for Transaction {
+    fn from(transaction: DocumentTransaction) -> Self {
+        Transaction {
+            operations: transaction
+                .operations
+                .into_iter()
+                .map(|operation| operation.into())
+                .collect::<Vec<NodeOperation>>()
+                .into(),
+            extension: Extension::TextSelection {
+                before_selection: transaction.before_selection,
+                after_selection: transaction.after_selection,
+            },
+        }
+    }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(tag = "op")]
+pub enum DocumentOperation {
+    #[serde(rename = "insert")]
+    Insert { path: Path, nodes: Vec<DocumentNode> },
+
+    #[serde(rename = "delete")]
+    Delete { path: Path, nodes: Vec<DocumentNode> },
+
+    #[serde(rename = "update")]
+    Update {
+        path: Path,
+        attributes: AttributeHashMap,
+        #[serde(rename = "oldAttributes")]
+        old_attributes: AttributeHashMap,
+    },
+
+    #[serde(rename = "update_text")]
+    UpdateText {
+        path: Path,
+        delta: TextOperations,
+        inverted: TextOperations,
+    },
+}
+
+impl std::convert::From<DocumentOperation> for NodeOperation {
+    fn from(document_operation: DocumentOperation) -> Self {
+        match document_operation {
+            DocumentOperation::Insert { path, nodes } => NodeOperation::Insert {
+                path,
+                nodes: nodes.into_iter().map(|node| node.into()).collect(),
+            },
+            DocumentOperation::Delete { path, nodes } => NodeOperation::Delete {
+                path,
+
+                nodes: nodes.into_iter().map(|node| node.into()).collect(),
+            },
+            DocumentOperation::Update {
+                path,
+                attributes,
+                old_attributes,
+            } => NodeOperation::Update {
+                path,
+                changeset: Changeset::Attributes {
+                    new: attributes,
+                    old: old_attributes,
+                },
+            },
+            DocumentOperation::UpdateText { path, delta, inverted } => NodeOperation::Update {
+                path,
+                changeset: Changeset::Delta { delta, inverted },
+            },
+        }
+    }
+}
+
+impl std::convert::From<&NodeOperation> for DocumentOperation {
+    fn from(node_operation: &NodeOperation) -> Self {
+        let node_operation = node_operation.clone();
+        match node_operation {
+            NodeOperation::Insert { path, nodes } => DocumentOperation::Insert {
+                path,
+                nodes: nodes.into_iter().map(|node| node.into()).collect(),
+            },
+            NodeOperation::Update { path, changeset } => match changeset {
+                Changeset::Delta { delta, inverted } => DocumentOperation::UpdateText { path, delta, inverted },
+                Changeset::Attributes { new, old } => DocumentOperation::Update {
+                    path,
+                    attributes: new,
+                    old_attributes: old,
+                },
+            },
+            NodeOperation::Delete { path, nodes } => DocumentOperation::Delete {
+                path,
+                nodes: nodes.into_iter().map(|node| node.into()).collect(),
+            },
+        }
+    }
+}
+
 #[derive(Default, Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
-pub struct DocumentNodeData {
+pub struct DocumentNode {
     #[serde(rename = "type")]
     pub node_type: String,
 
@@ -81,21 +235,32 @@ pub struct DocumentNodeData {
 
     #[serde(skip_serializing_if = "Vec::is_empty")]
     #[serde(default)]
-    pub children: Vec<DocumentNodeData>,
+    pub children: Vec<DocumentNode>,
 }
 
-impl std::convert::From<NodeData> for DocumentNodeData {
+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()
         };
-        DocumentNodeData {
+        DocumentNode {
             node_type: node_data.node_type,
             attributes: node_data.attributes,
             delta,
-            children: node_data.children.into_iter().map(DocumentNodeData::from).collect(),
+            children: node_data.children.into_iter().map(DocumentNode::from).collect(),
+        }
+    }
+}
+
+impl std::convert::From<DocumentNode> for NodeData {
+    fn from(document_node: DocumentNode) -> Self {
+        NodeData {
+            node_type: document_node.node_type,
+            attributes: document_node.attributes,
+            body: Body::Delta(document_node.delta),
+            children: document_node.children.into_iter().map(|child| child.into()).collect(),
         }
     }
 }
@@ -109,7 +274,7 @@ impl<'a> Serialize for DocumentContentSerializer<'a> {
         let root_node_id = tree.root_node_id();
 
         // transform the NodeData to DocumentNodeData
-        let get_document_node_data = |node_id: NodeId| tree.get_node_data(node_id).map(DocumentNodeData::from);
+        let get_document_node_data = |node_id: NodeId| tree.get_node_data(node_id).map(DocumentNode::from);
 
         let mut children = tree.get_children_ids(root_node_id);
         if children.len() == 1 {
@@ -133,6 +298,40 @@ impl<'a> Serialize for DocumentContentSerializer<'a> {
 #[cfg(test)]
 mod tests {
     use crate::editor::document::Document;
+    use crate::editor::document_serde::DocumentTransaction;
+
+    #[test]
+    fn transaction_deserialize_update_text_operation_test() {
+        // bold
+        let json = r#"{"operations":[{"op":"update_text","path":[0],"delta":[{"retain":3,"attributes":{"bold":true}}],"inverted":[{"retain":3,"attributes":{"bold":null}}]}],"after_selection":{"start":{"path":[0],"offset":0},"end":{"path":[0],"offset":3}},"before_selection":{"start":{"path":[0],"offset":0},"end":{"path":[0],"offset":3}}}"#;
+        let _ = serde_json::from_str::<DocumentTransaction>(json).unwrap();
+
+        // delete character
+        let json = r#"{"operations":[{"op":"update_text","path":[0],"delta":[{"retain":2},{"delete":1}],"inverted":[{"retain":2},{"insert":"C","attributes":{"bold":true}}]}],"after_selection":{"start":{"path":[0],"offset":2},"end":{"path":[0],"offset":2}},"before_selection":{"start":{"path":[0],"offset":3},"end":{"path":[0],"offset":3}}}"#;
+        let _ = serde_json::from_str::<DocumentTransaction>(json).unwrap();
+    }
+
+    #[test]
+    fn transaction_deserialize_insert_operation_test() {
+        let json = r#"{"operations":[{"op":"update_text","path":[0],"delta":[{"insert":"a"}],"inverted":[{"delete":1}]}],"after_selection":{"start":{"path":[0],"offset":1},"end":{"path":[0],"offset":1}},"before_selection":{"start":{"path":[0],"offset":0},"end":{"path":[0],"offset":0}}}"#;
+        let _ = serde_json::from_str::<DocumentTransaction>(json).unwrap();
+    }
+
+    #[test]
+    fn transaction_deserialize_delete_operation_test() {
+        let json = r#"{"operations": [{"op":"delete","path":[1],"nodes":[{"type":"text","delta":[]}]}],"after_selection":{"start":{"path":[0],"offset":2},"end":{"path":[0],"offset":2}},"before_selection":{"start":{"path":[1],"offset":0},"end":{"path":[1],"offset":0}}}"#;
+        let _transaction = serde_json::from_str::<DocumentTransaction>(json).unwrap();
+    }
+
+    #[test]
+    fn transaction_deserialize_update_attribute_operation_test() {
+        // let json = r#"{"operations":[{"op":"update","path":[0],"attributes":{"retain":3,"attributes":{"bold":true}},"oldAttributes":{"retain":3,"attributes":{"bold":null}}}]}"#;
+        // let transaction = serde_json::from_str::<DocumentTransaction>(&json).unwrap();
+
+        let json =
+            r#"{"operations":[{"op":"update","path":[0],"attributes":{"retain":3},"oldAttributes":{"retain":3}}]}"#;
+        let _ = serde_json::from_str::<DocumentTransaction>(json).unwrap();
+    }
 
     #[test]
     fn document_serde_test() {

+ 14 - 11
frontend/rust-lib/flowy-document/src/editor/editor.rs

@@ -1,4 +1,5 @@
 use crate::editor::document::{Document, DocumentRevisionSerde};
+use crate::editor::document_serde::DocumentTransaction;
 use crate::editor::queue::{Command, CommandSender, DocumentQueue};
 use crate::{DocumentEditor, DocumentUser};
 use bytes::Bytes;
@@ -66,25 +67,27 @@ fn spawn_edit_queue(
 }
 
 impl DocumentEditor for Arc<AppFlowyDocumentEditor> {
-    fn get_operations_str(&self) -> FutureResult<String, FlowyError> {
-        todo!()
+    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> {
-        todo!()
+    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 close(&self) {
-        todo!()
-    }
+    fn close(&self) {}
 
     fn receive_ws_data(&self, _data: ServerRevisionWSData) -> FutureResult<(), FlowyError> {
-        todo!()
+        FutureResult::new(async move { Ok(()) })
     }
 
-    fn receive_ws_state(&self, _state: &WSConnectState) {
-        todo!()
-    }
+    fn receive_ws_state(&self, _state: &WSConnectState) {}
 
     fn as_any(&self) -> &dyn Any {
         self

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

@@ -12,7 +12,7 @@ pub(crate) async fn get_document_handler(
 ) -> DataResult<DocumentSnapshotPB, FlowyError> {
     let document_id: DocumentIdPB = data.into_inner();
     let editor = manager.open_document_editor(&document_id).await?;
-    let operations_str = editor.get_operations_str().await?;
+    let operations_str = editor.export().await?;
     data_result(DocumentSnapshotPB {
         doc_id: document_id.into(),
         snapshot: operations_str,
@@ -35,9 +35,9 @@ pub(crate) async fn export_handler(
 ) -> DataResult<ExportDataPB, FlowyError> {
     let params: ExportParams = data.into_inner().try_into()?;
     let editor = manager.open_document_editor(&params.view_id).await?;
-    let operations_str = editor.get_operations_str().await?;
+    let document_data = editor.export().await?;
     data_result(ExportDataPB {
-        data: operations_str,
+        data: document_data,
         export_type: params.export_type,
     })
 }

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

@@ -1,6 +1,6 @@
 use crate::editor::{initial_document_content, AppFlowyDocumentEditor};
 use crate::entities::EditParams;
-use crate::old_editor::editor::{DocumentRevisionCompress, OldDocumentEditor};
+use crate::old_editor::editor::{DeltaDocumentEditor, DocumentRevisionCompress};
 use crate::{errors::FlowyError, DocumentCloudService};
 use bytes::Bytes;
 use dashmap::DashMap;
@@ -30,7 +30,7 @@ pub trait DocumentUser: Send + Sync {
 }
 
 pub trait DocumentEditor: Send + Sync {
-    fn get_operations_str(&self) -> FutureResult<String, FlowyError>;
+    fn export(&self) -> FutureResult<String, FlowyError>;
     fn compose_local_operations(&self, data: Bytes) -> FutureResult<(), FlowyError>;
     fn close(&self);
 
@@ -110,7 +110,7 @@ impl DocumentManager {
         let _ = editor
             .compose_local_operations(Bytes::from(payload.operations_str))
             .await?;
-        let operations_str = editor.get_operations_str().await?;
+        let operations_str = editor.export().await?;
         Ok(DocumentOperationsPB {
             doc_id: payload.doc_id.clone(),
             operations_str,
@@ -198,7 +198,7 @@ impl DocumentManager {
             Arc::new(editor)
         } else {
             let editor =
-                OldDocumentEditor::new(doc_id, user, rev_manager, self.rev_web_socket.clone(), cloud_service).await?;
+                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());

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

@@ -25,7 +25,7 @@ use std::any::Any;
 use std::sync::Arc;
 use tokio::sync::{mpsc, oneshot};
 
-pub struct OldDocumentEditor {
+pub struct DeltaDocumentEditor {
     pub doc_id: String,
     #[allow(dead_code)]
     rev_manager: Arc<RevisionManager>,
@@ -34,7 +34,7 @@ pub struct OldDocumentEditor {
     edit_cmd_tx: EditorCommandSender,
 }
 
-impl OldDocumentEditor {
+impl DeltaDocumentEditor {
     #[allow(unused_variables)]
     pub(crate) async fn new(
         doc_id: &str,
@@ -146,8 +146,8 @@ impl OldDocumentEditor {
     }
 }
 
-impl DocumentEditor for Arc<OldDocumentEditor> {
-    fn get_operations_str(&self) -> FutureResult<String, FlowyError> {
+impl DocumentEditor for Arc<DeltaDocumentEditor> {
+    fn export(&self) -> FutureResult<String, FlowyError> {
         let (ret, rx) = oneshot::channel::<CollaborateResult<String>>();
         let msg = EditorCommand::GetOperationsString { ret };
         let edit_cmd_tx = self.edit_cmd_tx.clone();
@@ -197,7 +197,7 @@ impl DocumentEditor for Arc<OldDocumentEditor> {
         self
     }
 }
-impl std::ops::Drop for OldDocumentEditor {
+impl std::ops::Drop for DeltaDocumentEditor {
     fn drop(&mut self) {
         tracing::trace!("{} DocumentEditor was dropped", self.doc_id)
     }
@@ -225,7 +225,7 @@ fn spawn_edit_queue(
 }
 
 #[cfg(feature = "flowy_unit_test")]
-impl OldDocumentEditor {
+impl DeltaDocumentEditor {
     pub async fn document_operations(&self) -> FlowyResult<TextOperations> {
         let (ret, rx) = oneshot::channel::<CollaborateResult<TextOperations>>();
         let msg = EditorCommand::GetOperations { ret };

+ 8 - 8
frontend/rust-lib/flowy-document/tests/old_document/old_document_test.rs

@@ -14,7 +14,7 @@ async fn text_block_sync_current_rev_id_check() {
         AssertNextSyncRevId(None),
         AssertJson(r#"[{"insert":"123\n"}]"#),
     ];
-    OldDocumentEditorTest::new().await.run_scripts(scripts).await;
+    DeltaDocumentEditorTest::new().await.run_scripts(scripts).await;
 }
 
 #[tokio::test]
@@ -28,7 +28,7 @@ async fn text_block_sync_state_check() {
         AssertRevisionState(3, RevisionState::Ack),
         AssertJson(r#"[{"insert":"123\n"}]"#),
     ];
-    OldDocumentEditorTest::new().await.run_scripts(scripts).await;
+    DeltaDocumentEditorTest::new().await.run_scripts(scripts).await;
 }
 
 #[tokio::test]
@@ -40,7 +40,7 @@ async fn text_block_sync_insert_test() {
         AssertJson(r#"[{"insert":"123\n"}]"#),
         AssertNextSyncRevId(None),
     ];
-    OldDocumentEditorTest::new().await.run_scripts(scripts).await;
+    DeltaDocumentEditorTest::new().await.run_scripts(scripts).await;
 }
 
 #[tokio::test]
@@ -52,7 +52,7 @@ async fn text_block_sync_insert_in_chinese() {
         InsertText("好", offset),
         AssertJson(r#"[{"insert":"你好\n"}]"#),
     ];
-    OldDocumentEditorTest::new().await.run_scripts(scripts).await;
+    DeltaDocumentEditorTest::new().await.run_scripts(scripts).await;
 }
 
 #[tokio::test]
@@ -64,7 +64,7 @@ async fn text_block_sync_insert_with_emoji() {
         InsertText("☺️", offset),
         AssertJson(r#"[{"insert":"😁☺️\n"}]"#),
     ];
-    OldDocumentEditorTest::new().await.run_scripts(scripts).await;
+    DeltaDocumentEditorTest::new().await.run_scripts(scripts).await;
 }
 
 #[tokio::test]
@@ -76,7 +76,7 @@ async fn text_block_sync_delete_in_english() {
         Delete(Interval::new(0, 2)),
         AssertJson(r#"[{"insert":"3\n"}]"#),
     ];
-    OldDocumentEditorTest::new().await.run_scripts(scripts).await;
+    DeltaDocumentEditorTest::new().await.run_scripts(scripts).await;
 }
 
 #[tokio::test]
@@ -89,7 +89,7 @@ async fn text_block_sync_delete_in_chinese() {
         Delete(Interval::new(0, offset)),
         AssertJson(r#"[{"insert":"好\n"}]"#),
     ];
-    OldDocumentEditorTest::new().await.run_scripts(scripts).await;
+    DeltaDocumentEditorTest::new().await.run_scripts(scripts).await;
 }
 
 #[tokio::test]
@@ -101,5 +101,5 @@ async fn text_block_sync_replace_test() {
         Replace(Interval::new(0, 3), "abc"),
         AssertJson(r#"[{"insert":"abc\n"}]"#),
     ];
-    OldDocumentEditorTest::new().await.run_scripts(scripts).await;
+    DeltaDocumentEditorTest::new().await.run_scripts(scripts).await;
 }

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

@@ -1,4 +1,4 @@
-use flowy_document::old_editor::editor::OldDocumentEditor;
+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};
@@ -17,18 +17,18 @@ pub enum EditorScript {
     AssertJson(&'static str),
 }
 
-pub struct OldDocumentEditorTest {
+pub struct DeltaDocumentEditorTest {
     pub sdk: FlowySDKTest,
-    pub editor: Arc<OldDocumentEditor>,
+    pub editor: Arc<DeltaDocumentEditor>,
 }
 
-impl OldDocumentEditorTest {
+impl DeltaDocumentEditorTest {
     pub async fn new() -> Self {
         let sdk = FlowySDKTest::default();
         let _ = sdk.init_user().await;
         let test = ViewTest::new_document_view(&sdk).await;
         let document_editor = sdk.document_manager.open_document_editor(&test.view.id).await.unwrap();
-        let editor = match document_editor.as_any().downcast_ref::<Arc<OldDocumentEditor>>() {
+        let editor = match document_editor.as_any().downcast_ref::<Arc<DeltaDocumentEditor>>() {
             None => panic!(),
             Some(editor) => editor.clone(),
         };

+ 1 - 1
frontend/rust-lib/flowy-sdk/src/deps_resolve/folder_deps.rs

@@ -174,7 +174,7 @@ impl ViewDataProcessor for DocumentViewDataProcessor {
         let manager = self.0.clone();
         FutureResult::new(async move {
             let editor = manager.open_document_editor(view_id).await?;
-            let delta_bytes = Bytes::from(editor.get_operations_str().await?);
+            let delta_bytes = Bytes::from(editor.export().await?);
             Ok(delta_bytes)
         })
     }

+ 1 - 1
shared-lib/lib-ot/src/core/node_tree/path.rs

@@ -23,7 +23,7 @@ use serde::{Deserialize, Serialize};
 /// The path of  Node A-1 will be [0,0]
 /// The path of  Node A-2 will be [0,1]
 /// The path of  Node B-2 will be [1,1]
-#[derive(Clone, Serialize, Deserialize, Eq, PartialEq, Debug)]
+#[derive(Clone, Serialize, Deserialize, Eq, PartialEq, Debug, Default)]
 pub struct Path(pub Vec<usize>);
 
 impl Path {

+ 15 - 3
shared-lib/lib-ot/src/core/node_tree/transaction.rs

@@ -1,6 +1,6 @@
 use super::{Changeset, NodeOperations};
 use crate::core::attributes::AttributeHashMap;
-use crate::core::{Interval, NodeData, NodeOperation, NodeTree, Path};
+use crate::core::{NodeData, NodeOperation, NodeTree, Path};
 use crate::errors::OTError;
 
 use indextree::NodeId;
@@ -93,8 +93,8 @@ impl std::ops::DerefMut for Transaction {
 pub enum Extension {
     Empty,
     TextSelection {
-        before_selection: Interval,
-        after_selection: Interval,
+        before_selection: Selection,
+        after_selection: Selection,
     },
 }
 
@@ -110,6 +110,18 @@ impl Extension {
     }
 }
 
+#[derive(Debug, Clone, Serialize, Deserialize, Default)]
+pub struct Selection {
+    start: Position,
+    end: Position,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, Default)]
+pub struct Position {
+    path: Path,
+    offset: usize,
+}
+
 pub struct TransactionBuilder<'a> {
     node_tree: &'a NodeTree,
     operations: NodeOperations,

+ 6 - 5
shared-lib/lib-ot/tests/node/serde_test.rs

@@ -1,5 +1,5 @@
 use lib_ot::core::{
-    AttributeBuilder, Changeset, Extension, Interval, NodeData, NodeDataBuilder, NodeOperation, NodeTree, Path,
+    AttributeBuilder, Changeset, Extension, NodeData, NodeDataBuilder, NodeOperation, NodeTree, Path, Selection,
     Transaction,
 };
 use lib_ot::text_delta::TextOperationBuilder;
@@ -78,19 +78,20 @@ fn transaction_serialize_test() {
     };
     let mut transaction = Transaction::from_operations(vec![insert]);
     transaction.extension = Extension::TextSelection {
-        before_selection: Interval::new(0, 1),
-        after_selection: Interval::new(1, 2),
+        before_selection: Selection::default(),
+        after_selection: Selection::default(),
     };
     let json = serde_json::to_string(&transaction).unwrap();
     assert_eq!(
         json,
-        r#"{"operations":[{"op":"insert","path":[0,1],"nodes":[{"type":"text"}]}],"TextSelection":{"before_selection":{"start":0,"end":1},"after_selection":{"start":1,"end":2}}}"#
+        r#"{"operations":[{"op":"insert","path":[0,1],"nodes":[{"type":"text"}]}],"TextSelection":{"before_selection":{"start":{"path":[],"offset":0},"end":{"path":[],"offset":0}},"after_selection":{"start":{"path":[],"offset":0},"end":{"path":[],"offset":0}}}}"#
     );
 }
 
 #[test]
 fn transaction_deserialize_test() {
-    let json = r#"{"operations":[{"op":"insert","path":[0,1],"nodes":[{"type":"text"}]}],"TextSelection":{"before_selection":{"start":0,"end":1},"after_selection":{"start":1,"end":2}}}"#;
+    let json = r#"{"operations":[{"op":"insert","path":[0,1],"nodes":[{"type":"text"}]}],"TextSelection":{"before_selection":{"start":{"path":[],"offset":0},"end":{"path":[],"offset":0}},"after_selection":{"start":{"path":[],"offset":0},"end":{"path":[],"offset":0}}}}"#;
+
     let transaction: Transaction = serde_json::from_str(json).unwrap();
     assert_eq!(transaction.operations.len(), 1);
 }