From 7d404ff0dadfec5ba96302c8f853aea0951f11e7 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Tue, 16 Aug 2022 16:25:52 +0800 Subject: [PATCH 01/19] feat: document model in rust --- shared-lib/Cargo.lock | 7 +++ shared-lib/lib-ot/Cargo.toml | 1 + .../lib-ot/src/core/document/attributes.rs | 9 ++++ .../lib-ot/src/core/document/document.rs | 52 +++++++++++++++++++ shared-lib/lib-ot/src/core/document/mod.rs | 8 +++ shared-lib/lib-ot/src/core/document/node.rs | 15 ++++++ .../lib-ot/src/core/document/position.rs | 7 +++ shared-lib/lib-ot/src/core/mod.rs | 2 + shared-lib/lib-ot/tests/main.rs | 6 +++ 9 files changed, 107 insertions(+) create mode 100644 shared-lib/lib-ot/src/core/document/attributes.rs create mode 100644 shared-lib/lib-ot/src/core/document/document.rs create mode 100644 shared-lib/lib-ot/src/core/document/mod.rs create mode 100644 shared-lib/lib-ot/src/core/document/node.rs create mode 100644 shared-lib/lib-ot/src/core/document/position.rs diff --git a/shared-lib/Cargo.lock b/shared-lib/Cargo.lock index da8bc0ff1c..596f1fe694 100644 --- a/shared-lib/Cargo.lock +++ b/shared-lib/Cargo.lock @@ -741,6 +741,12 @@ dependencies = [ "serde", ] +[[package]] +name = "indextree" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42b4b46b3311ebd8e5cd44f6b03b36e0f48a70552cf6b036afcebc5626794066" + [[package]] name = "instant" version = "0.1.12" @@ -809,6 +815,7 @@ dependencies = [ "bytes", "dashmap", "derive_more", + "indextree", "lazy_static", "log", "md5", diff --git a/shared-lib/lib-ot/Cargo.toml b/shared-lib/lib-ot/Cargo.toml index a1a577132e..d47968dcfe 100644 --- a/shared-lib/lib-ot/Cargo.toml +++ b/shared-lib/lib-ot/Cargo.toml @@ -24,6 +24,7 @@ lazy_static = "1.4.0" strum = "0.21" strum_macros = "0.21" bytes = "1.0" +indextree = "4.4.0" [features] diff --git a/shared-lib/lib-ot/src/core/document/attributes.rs b/shared-lib/lib-ot/src/core/document/attributes.rs new file mode 100644 index 0000000000..ae9d1fbffb --- /dev/null +++ b/shared-lib/lib-ot/src/core/document/attributes.rs @@ -0,0 +1,9 @@ +use std::collections::HashMap; + +pub struct NodeAttributes(HashMap>); + +impl NodeAttributes { + pub fn new() -> NodeAttributes { + NodeAttributes(HashMap::new()) + } +} diff --git a/shared-lib/lib-ot/src/core/document/document.rs b/shared-lib/lib-ot/src/core/document/document.rs new file mode 100644 index 0000000000..fa8e2aea6a --- /dev/null +++ b/shared-lib/lib-ot/src/core/document/document.rs @@ -0,0 +1,52 @@ +use crate::core::document::position::Position; +use crate::core::NodeData; +use indextree::{Arena, NodeId}; + +pub struct DocumentTree { + arena: Arena, + root: NodeId, +} + +impl DocumentTree { + pub fn new() -> DocumentTree { + let mut arena = Arena::new(); + let root = arena.new_node(NodeData::new("root".into())); + DocumentTree { + arena: Arena::new(), + root, + } + } + + pub fn node_at_path(&self, position: &Position) -> Option { + if position.is_empty() { + return None; + } + + let mut iterate_node = self.root; + + for id in &position.0 { + let child = self.child_at_index_of_path(iterate_node, id.clone()); + iterate_node = match child { + Some(node) => node, + None => return None, + }; + } + + Some(iterate_node) + } + + fn child_at_index_of_path(&self, at_node: NodeId, index: usize) -> Option { + let children = at_node.children(&self.arena); + + let mut counter = 0; + for child in children { + if counter == index { + return Some(child); + } + + counter += 1; + } + + None + } +} diff --git a/shared-lib/lib-ot/src/core/document/mod.rs b/shared-lib/lib-ot/src/core/document/mod.rs new file mode 100644 index 0000000000..efbf9f362c --- /dev/null +++ b/shared-lib/lib-ot/src/core/document/mod.rs @@ -0,0 +1,8 @@ +mod attributes; +mod document; +mod node; +mod position; + +pub use attributes::*; +pub use document::*; +pub use node::*; diff --git a/shared-lib/lib-ot/src/core/document/node.rs b/shared-lib/lib-ot/src/core/document/node.rs new file mode 100644 index 0000000000..7f0f172cd6 --- /dev/null +++ b/shared-lib/lib-ot/src/core/document/node.rs @@ -0,0 +1,15 @@ +use crate::core::NodeAttributes; + +pub struct NodeData { + pub node_type: String, + pub attributes: NodeAttributes, +} + +impl NodeData { + pub fn new(node_type: &str) -> NodeData { + NodeData { + node_type: node_type.into(), + attributes: NodeAttributes::new(), + } + } +} diff --git a/shared-lib/lib-ot/src/core/document/position.rs b/shared-lib/lib-ot/src/core/document/position.rs new file mode 100644 index 0000000000..6e1983b440 --- /dev/null +++ b/shared-lib/lib-ot/src/core/document/position.rs @@ -0,0 +1,7 @@ +pub struct Position(pub Vec); + +impl Position { + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} diff --git a/shared-lib/lib-ot/src/core/mod.rs b/shared-lib/lib-ot/src/core/mod.rs index 7c1ed3f2ef..262233c85a 100644 --- a/shared-lib/lib-ot/src/core/mod.rs +++ b/shared-lib/lib-ot/src/core/mod.rs @@ -1,9 +1,11 @@ mod delta; +mod document; mod interval; mod operation; mod ot_str; pub use delta::*; +pub use document::*; pub use interval::*; pub use operation::*; pub use ot_str::*; diff --git a/shared-lib/lib-ot/tests/main.rs b/shared-lib/lib-ot/tests/main.rs index 8b13789179..2c766017e4 100644 --- a/shared-lib/lib-ot/tests/main.rs +++ b/shared-lib/lib-ot/tests/main.rs @@ -1 +1,7 @@ +use lib_ot::core::DocumentTree; +#[test] +fn main() { + // Create a new arena + let _document = DocumentTree::new(); +} From 2466b3eebcd51d927e63e239961a282fe2fcfbcd Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Wed, 17 Aug 2022 16:20:56 +0800 Subject: [PATCH 02/19] feat: add operations of the document --- .../lib-ot/src/core/document/attributes.rs | 1 + .../src/core/document/document_operation.rs | 57 +++++++++++++++++++ shared-lib/lib-ot/src/core/document/mod.rs | 2 + .../lib-ot/src/core/document/position.rs | 1 + 4 files changed, 61 insertions(+) create mode 100644 shared-lib/lib-ot/src/core/document/document_operation.rs diff --git a/shared-lib/lib-ot/src/core/document/attributes.rs b/shared-lib/lib-ot/src/core/document/attributes.rs index ae9d1fbffb..1b898ccbb3 100644 --- a/shared-lib/lib-ot/src/core/document/attributes.rs +++ b/shared-lib/lib-ot/src/core/document/attributes.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; +#[derive(Clone)] pub struct NodeAttributes(HashMap>); impl NodeAttributes { diff --git a/shared-lib/lib-ot/src/core/document/document_operation.rs b/shared-lib/lib-ot/src/core/document/document_operation.rs new file mode 100644 index 0000000000..b1e797a146 --- /dev/null +++ b/shared-lib/lib-ot/src/core/document/document_operation.rs @@ -0,0 +1,57 @@ +use crate::core::document::position::Position; +use crate::core::{NodeAttributes, TextDelta}; +use indextree::NodeId; + +pub enum DocumentOperation { + Insert(InsertOperation), + Update(UpdateOperation), + Delete(DeleteOperation), + TextEdit(TextEditOperation), +} + +impl DocumentOperation { + pub fn invert(&self) -> DocumentOperation { + match self { + DocumentOperation::Insert(insert_operation) => DocumentOperation::Delete(DeleteOperation { + path: insert_operation.path.clone(), + nodes: insert_operation.nodes.clone(), + }), + DocumentOperation::Update(update_operation) => DocumentOperation::Update(UpdateOperation { + path: update_operation.path.clone(), + attributes: update_operation.old_attributes.clone(), + old_attributes: update_operation.attributes.clone(), + }), + DocumentOperation::Delete(delete_operation) => DocumentOperation::Insert(InsertOperation { + path: delete_operation.path.clone(), + nodes: delete_operation.nodes.clone(), + }), + DocumentOperation::TextEdit(text_edit_operation) => DocumentOperation::TextEdit(TextEditOperation { + path: text_edit_operation.path.clone(), + delta: text_edit_operation.inverted.clone(), + inverted: text_edit_operation.delta.clone(), + }), + } + } +} + +pub struct InsertOperation { + path: Position, + nodes: Vec, +} + +pub struct UpdateOperation { + path: Position, + attributes: NodeAttributes, + old_attributes: NodeAttributes, +} + +pub struct DeleteOperation { + path: Position, + nodes: Vec, +} + +pub struct TextEditOperation { + path: Position, + delta: TextDelta, + inverted: TextDelta, +} diff --git a/shared-lib/lib-ot/src/core/document/mod.rs b/shared-lib/lib-ot/src/core/document/mod.rs index efbf9f362c..81008fa50e 100644 --- a/shared-lib/lib-ot/src/core/document/mod.rs +++ b/shared-lib/lib-ot/src/core/document/mod.rs @@ -1,8 +1,10 @@ mod attributes; mod document; +mod document_operation; mod node; mod position; pub use attributes::*; pub use document::*; +pub use document_operation::*; pub use node::*; diff --git a/shared-lib/lib-ot/src/core/document/position.rs b/shared-lib/lib-ot/src/core/document/position.rs index 6e1983b440..60a69ce8be 100644 --- a/shared-lib/lib-ot/src/core/document/position.rs +++ b/shared-lib/lib-ot/src/core/document/position.rs @@ -1,3 +1,4 @@ +#[derive(Clone)] pub struct Position(pub Vec); impl Position { From aa90613bf65a61691d64d8ba7b25c1d3c9e3a61c Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Wed, 17 Aug 2022 16:48:45 +0800 Subject: [PATCH 03/19] feat: transaction built der --- .../lib-ot/src/core/document/document.rs | 42 ++++++++++++++++++- shared-lib/lib-ot/src/core/document/mod.rs | 2 + .../lib-ot/src/core/document/transaction.rs | 25 +++++++++++ shared-lib/lib-ot/tests/main.rs | 9 +++- 4 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 shared-lib/lib-ot/src/core/document/transaction.rs diff --git a/shared-lib/lib-ot/src/core/document/document.rs b/shared-lib/lib-ot/src/core/document/document.rs index fa8e2aea6a..7acd357159 100644 --- a/shared-lib/lib-ot/src/core/document/document.rs +++ b/shared-lib/lib-ot/src/core/document/document.rs @@ -1,5 +1,5 @@ use crate::core::document::position::Position; -use crate::core::NodeData; +use crate::core::{NodeData, Transaction}; use indextree::{Arena, NodeId}; pub struct DocumentTree { @@ -35,6 +35,42 @@ impl DocumentTree { Some(iterate_node) } + pub fn path_of_node(&self, node_id: NodeId) -> Position { + let mut path: Vec = Vec::new(); + + let mut ancestors = node_id.ancestors(&self.arena); + let mut current_node = node_id; + let mut parent = ancestors.next(); + + while parent.is_some() { + let parent_node = parent.unwrap(); + let counter = self.index_of_node(parent_node, current_node); + path.push(counter); + current_node = parent_node; + parent = ancestors.next(); + } + + Position(path) + } + + fn index_of_node(&self, parent_node: NodeId, child_node: NodeId) -> usize { + let mut counter: usize = 0; + + let mut children_iterator = parent_node.children(&self.arena); + let mut node = children_iterator.next(); + + while node.is_some() { + if node.unwrap() == child_node { + return counter; + } + + node = children_iterator.next(); + counter += 1; + } + + counter + } + fn child_at_index_of_path(&self, at_node: NodeId, index: usize) -> Option { let children = at_node.children(&self.arena); @@ -49,4 +85,8 @@ impl DocumentTree { None } + + pub fn apply(&self, _transaction: Transaction) { + unimplemented!() + } } diff --git a/shared-lib/lib-ot/src/core/document/mod.rs b/shared-lib/lib-ot/src/core/document/mod.rs index 81008fa50e..968c78bc2b 100644 --- a/shared-lib/lib-ot/src/core/document/mod.rs +++ b/shared-lib/lib-ot/src/core/document/mod.rs @@ -3,8 +3,10 @@ mod document; mod document_operation; mod node; mod position; +mod transaction; pub use attributes::*; pub use document::*; pub use document_operation::*; pub use node::*; +pub use transaction::*; diff --git a/shared-lib/lib-ot/src/core/document/transaction.rs b/shared-lib/lib-ot/src/core/document/transaction.rs new file mode 100644 index 0000000000..7e48a4c544 --- /dev/null +++ b/shared-lib/lib-ot/src/core/document/transaction.rs @@ -0,0 +1,25 @@ +use crate::core::DocumentOperation; + +pub struct Transaction { + pub operations: Vec, +} + +pub struct TransactionBuilder { + operations: Vec, +} + +impl TransactionBuilder { + pub fn new() -> TransactionBuilder { + TransactionBuilder { operations: Vec::new() } + } + + pub fn push(&mut self, op: DocumentOperation) { + self.operations.push(op); + } + + pub fn finalize(self) -> Transaction { + Transaction { + operations: self.operations, + } + } +} diff --git a/shared-lib/lib-ot/tests/main.rs b/shared-lib/lib-ot/tests/main.rs index 2c766017e4..cf2a6a94f3 100644 --- a/shared-lib/lib-ot/tests/main.rs +++ b/shared-lib/lib-ot/tests/main.rs @@ -1,7 +1,14 @@ -use lib_ot::core::DocumentTree; +use lib_ot::core::{DocumentTree, TransactionBuilder}; #[test] fn main() { // Create a new arena let _document = DocumentTree::new(); } + +#[test] +fn test_documents() { + let document = DocumentTree::new(); + let tb = TransactionBuilder::new(); + document.apply(tb.finalize()); +} From bb7c7e4f41fe48c806fb2710cda3e7599adef460 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Wed, 17 Aug 2022 17:43:58 +0800 Subject: [PATCH 04/19] feat: apply operation --- .../lib-ot/src/core/document/document.rs | 51 ++++++++++++++++++- .../src/core/document/document_operation.rs | 20 ++++---- shared-lib/lib-ot/tests/main.rs | 2 +- 3 files changed, 60 insertions(+), 13 deletions(-) diff --git a/shared-lib/lib-ot/src/core/document/document.rs b/shared-lib/lib-ot/src/core/document/document.rs index 7acd357159..bc62491c82 100644 --- a/shared-lib/lib-ot/src/core/document/document.rs +++ b/shared-lib/lib-ot/src/core/document/document.rs @@ -1,5 +1,5 @@ use crate::core::document::position::Position; -use crate::core::{NodeData, Transaction}; +use crate::core::{DeleteOperation, DocumentOperation, InsertOperation, NodeData, TextEditOperation, Transaction, UpdateOperation}; use indextree::{Arena, NodeId}; pub struct DocumentTree { @@ -86,7 +86,54 @@ impl DocumentTree { None } - pub fn apply(&self, _transaction: Transaction) { + pub fn apply(&mut self, transaction: Transaction) { + for op in &transaction.operations { + self.apply_op(op); + } + } + + fn apply_op(&mut self, op: &DocumentOperation) { + match op { + DocumentOperation::Insert(op) => self.apply_insert(op), + DocumentOperation::Update(op) => self.apply_update(op), + DocumentOperation::Delete(op) => self.apply_delete(op), + DocumentOperation::TextEdit(op) => self.apply_text_edit(op), + } + } + + fn apply_insert(&mut self, op: &InsertOperation) { + let parent_path = &op.path.0[0..(op.path.0.len() - 1)]; + let last_index = op.path.0[op.path.0.len() - 1]; + let parent_node = self.node_at_path(&Position(parent_path.to_vec())); + if let Some(parent_node) = parent_node { + self.insert_child_at_index(parent_node, last_index, &op.nodes); + } + } + + fn insert_child_at_index(&mut self, parent: NodeId, index: usize, insert_children: &[NodeId]) { + if index == 0 && insert_children.len() == 0 { + for id in insert_children { + parent.append(*id, &mut self.arena); + } + return; + } + + let node_to_insert = self.child_at_index_of_path(parent, index).unwrap(); + + for id in insert_children { + node_to_insert.insert_before(*id, &mut self.arena); + } + } + + fn apply_update(&self, _op: &UpdateOperation) { + unimplemented!() + } + + fn apply_delete(&self, _op: &DeleteOperation) { + unimplemented!() + } + + fn apply_text_edit(&self, _op: &TextEditOperation) { unimplemented!() } } diff --git a/shared-lib/lib-ot/src/core/document/document_operation.rs b/shared-lib/lib-ot/src/core/document/document_operation.rs index b1e797a146..d5c60cc45d 100644 --- a/shared-lib/lib-ot/src/core/document/document_operation.rs +++ b/shared-lib/lib-ot/src/core/document/document_operation.rs @@ -35,23 +35,23 @@ impl DocumentOperation { } pub struct InsertOperation { - path: Position, - nodes: Vec, + pub path: Position, + pub nodes: Vec, } pub struct UpdateOperation { - path: Position, - attributes: NodeAttributes, - old_attributes: NodeAttributes, + pub path: Position, + pub attributes: NodeAttributes, + pub old_attributes: NodeAttributes, } pub struct DeleteOperation { - path: Position, - nodes: Vec, + pub path: Position, + pub nodes: Vec, } pub struct TextEditOperation { - path: Position, - delta: TextDelta, - inverted: TextDelta, + pub path: Position, + pub delta: TextDelta, + pub inverted: TextDelta, } diff --git a/shared-lib/lib-ot/tests/main.rs b/shared-lib/lib-ot/tests/main.rs index cf2a6a94f3..d554c12cfb 100644 --- a/shared-lib/lib-ot/tests/main.rs +++ b/shared-lib/lib-ot/tests/main.rs @@ -8,7 +8,7 @@ fn main() { #[test] fn test_documents() { - let document = DocumentTree::new(); + let mut document = DocumentTree::new(); let tb = TransactionBuilder::new(); document.apply(tb.finalize()); } From ba160c8026dec010f3ca1fb6c38a4e9dfe3728e7 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Thu, 18 Aug 2022 16:19:50 +0800 Subject: [PATCH 05/19] feat: compose attributes --- .../lib-ot/src/core/document/attributes.rs | 12 ++++++++++ .../lib-ot/src/core/document/document.rs | 17 +++++++++---- shared-lib/lib-ot/src/core/document/node.rs | 9 ++++--- .../lib-ot/src/core/document/transaction.rs | 24 +++++++++++++++---- shared-lib/lib-ot/tests/main.rs | 5 ++-- 5 files changed, 52 insertions(+), 15 deletions(-) diff --git a/shared-lib/lib-ot/src/core/document/attributes.rs b/shared-lib/lib-ot/src/core/document/attributes.rs index 1b898ccbb3..011c4cec43 100644 --- a/shared-lib/lib-ot/src/core/document/attributes.rs +++ b/shared-lib/lib-ot/src/core/document/attributes.rs @@ -7,4 +7,16 @@ impl NodeAttributes { pub fn new() -> NodeAttributes { NodeAttributes(HashMap::new()) } + + pub fn compose(a: &NodeAttributes, b: &NodeAttributes) -> NodeAttributes { + let mut new_map: HashMap> = b.0.clone(); + + for (key, value) in &a.0 { + if b.0.contains_key(key.as_str()) { + new_map.insert(key.into(), value.clone()); + } + } + + NodeAttributes(new_map) + } } diff --git a/shared-lib/lib-ot/src/core/document/document.rs b/shared-lib/lib-ot/src/core/document/document.rs index bc62491c82..4200d4e398 100644 --- a/shared-lib/lib-ot/src/core/document/document.rs +++ b/shared-lib/lib-ot/src/core/document/document.rs @@ -1,5 +1,5 @@ use crate::core::document::position::Position; -use crate::core::{DeleteOperation, DocumentOperation, InsertOperation, NodeData, TextEditOperation, Transaction, UpdateOperation}; +use crate::core::{DeleteOperation, DocumentOperation, InsertOperation, NodeAttributes, NodeData, TextEditOperation, Transaction, UpdateOperation}; use indextree::{Arena, NodeId}; pub struct DocumentTree { @@ -125,12 +125,19 @@ impl DocumentTree { } } - fn apply_update(&self, _op: &UpdateOperation) { - unimplemented!() + fn apply_update(&self, op: &UpdateOperation) { + let update_node = self.node_at_path(&op.path).unwrap(); + let node_data = self.arena.get(update_node).unwrap(); + let new_attributes = { + let old_attributes = node_data.get().attributes.borrow(); + NodeAttributes::compose(&old_attributes, &op.attributes) + }; + node_data.get().attributes.replace(new_attributes); } - fn apply_delete(&self, _op: &DeleteOperation) { - unimplemented!() + fn apply_delete(&mut self, op: &DeleteOperation) { + let update_node = self.node_at_path(&op.path).unwrap(); + update_node.remove_subtree(&mut self.arena); } fn apply_text_edit(&self, _op: &TextEditOperation) { diff --git a/shared-lib/lib-ot/src/core/document/node.rs b/shared-lib/lib-ot/src/core/document/node.rs index 7f0f172cd6..2fa9706ce3 100644 --- a/shared-lib/lib-ot/src/core/document/node.rs +++ b/shared-lib/lib-ot/src/core/document/node.rs @@ -1,15 +1,18 @@ -use crate::core::NodeAttributes; +use std::cell::RefCell; +use crate::core::{TextDelta, NodeAttributes}; pub struct NodeData { pub node_type: String, - pub attributes: NodeAttributes, + pub attributes: RefCell, + pub delta: RefCell>, } impl NodeData { pub fn new(node_type: &str) -> NodeData { NodeData { node_type: node_type.into(), - attributes: NodeAttributes::new(), + attributes: RefCell::new(NodeAttributes::new()), + delta: RefCell::new(None), } } } diff --git a/shared-lib/lib-ot/src/core/document/transaction.rs b/shared-lib/lib-ot/src/core/document/transaction.rs index 7e48a4c544..c75cb16bea 100644 --- a/shared-lib/lib-ot/src/core/document/transaction.rs +++ b/shared-lib/lib-ot/src/core/document/transaction.rs @@ -1,16 +1,30 @@ -use crate::core::DocumentOperation; +use crate::core::{DocumentOperation, DocumentTree}; pub struct Transaction { pub operations: Vec, } -pub struct TransactionBuilder { +impl Transaction { + + fn new(operations: Vec) -> Transaction { + Transaction { + operations, + } + } + +} + +pub struct TransactionBuilder<'a> { + document: &'a DocumentTree, operations: Vec, } -impl TransactionBuilder { - pub fn new() -> TransactionBuilder { - TransactionBuilder { operations: Vec::new() } +impl<'a> TransactionBuilder<'a> { + pub fn new(document: &'a DocumentTree) -> TransactionBuilder { + TransactionBuilder { + document, + operations: Vec::new() + } } pub fn push(&mut self, op: DocumentOperation) { diff --git a/shared-lib/lib-ot/tests/main.rs b/shared-lib/lib-ot/tests/main.rs index d554c12cfb..ecfdfb4da2 100644 --- a/shared-lib/lib-ot/tests/main.rs +++ b/shared-lib/lib-ot/tests/main.rs @@ -9,6 +9,7 @@ fn main() { #[test] fn test_documents() { let mut document = DocumentTree::new(); - let tb = TransactionBuilder::new(); - document.apply(tb.finalize()); + let tb = TransactionBuilder::new(&document); + let transaction = tb.finalize(); + document.apply(transaction); } From c207bf36794ffb95449b92de26687d6c7c83767a Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Thu, 18 Aug 2022 17:49:20 +0800 Subject: [PATCH 06/19] feat: apply transactions --- .../lib-ot/src/core/document/document.rs | 28 ++++++++----- .../src/core/document/document_operation.rs | 7 ++-- shared-lib/lib-ot/src/core/document/mod.rs | 1 + shared-lib/lib-ot/src/core/document/node.rs | 3 +- .../lib-ot/src/core/document/transaction.rs | 42 ++++++++++++++----- shared-lib/lib-ot/tests/main.rs | 5 ++- 6 files changed, 58 insertions(+), 28 deletions(-) diff --git a/shared-lib/lib-ot/src/core/document/document.rs b/shared-lib/lib-ot/src/core/document/document.rs index 4200d4e398..9d1d071f95 100644 --- a/shared-lib/lib-ot/src/core/document/document.rs +++ b/shared-lib/lib-ot/src/core/document/document.rs @@ -1,25 +1,25 @@ use crate::core::document::position::Position; -use crate::core::{DeleteOperation, DocumentOperation, InsertOperation, NodeAttributes, NodeData, TextEditOperation, Transaction, UpdateOperation}; +use crate::core::{ + DeleteOperation, DocumentOperation, InsertOperation, NodeAttributes, NodeData, TextEditOperation, Transaction, + UpdateOperation, +}; use indextree::{Arena, NodeId}; pub struct DocumentTree { - arena: Arena, - root: NodeId, + pub arena: Arena, + pub root: NodeId, } impl DocumentTree { pub fn new() -> DocumentTree { let mut arena = Arena::new(); let root = arena.new_node(NodeData::new("root".into())); - DocumentTree { - arena: Arena::new(), - root, - } + DocumentTree { arena, root } } pub fn node_at_path(&self, position: &Position) -> Option { if position.is_empty() { - return None; + return Some(self.root); } let mut iterate_node = self.root; @@ -93,7 +93,7 @@ impl DocumentTree { } fn apply_op(&mut self, op: &DocumentOperation) { - match op { + match op { DocumentOperation::Insert(op) => self.apply_insert(op), DocumentOperation::Update(op) => self.apply_update(op), DocumentOperation::Delete(op) => self.apply_delete(op), @@ -106,12 +106,18 @@ impl DocumentTree { let last_index = op.path.0[op.path.0.len() - 1]; let parent_node = self.node_at_path(&Position(parent_path.to_vec())); if let Some(parent_node) = parent_node { - self.insert_child_at_index(parent_node, last_index, &op.nodes); + let mut inserted_nodes = Vec::new(); + + for node in &op.nodes { + inserted_nodes.push(self.arena.new_node(node.clone())); + } + + self.insert_child_at_index(parent_node, last_index, &inserted_nodes); } } fn insert_child_at_index(&mut self, parent: NodeId, index: usize, insert_children: &[NodeId]) { - if index == 0 && insert_children.len() == 0 { + if index == 0 && parent.children(&self.arena).next().is_none() { for id in insert_children { parent.append(*id, &mut self.arena); } diff --git a/shared-lib/lib-ot/src/core/document/document_operation.rs b/shared-lib/lib-ot/src/core/document/document_operation.rs index d5c60cc45d..7b7b0c4a62 100644 --- a/shared-lib/lib-ot/src/core/document/document_operation.rs +++ b/shared-lib/lib-ot/src/core/document/document_operation.rs @@ -1,6 +1,5 @@ use crate::core::document::position::Position; -use crate::core::{NodeAttributes, TextDelta}; -use indextree::NodeId; +use crate::core::{NodeAttributes, NodeData, TextDelta}; pub enum DocumentOperation { Insert(InsertOperation), @@ -36,7 +35,7 @@ impl DocumentOperation { pub struct InsertOperation { pub path: Position, - pub nodes: Vec, + pub nodes: Vec, } pub struct UpdateOperation { @@ -47,7 +46,7 @@ pub struct UpdateOperation { pub struct DeleteOperation { pub path: Position, - pub nodes: Vec, + pub nodes: Vec, } pub struct TextEditOperation { diff --git a/shared-lib/lib-ot/src/core/document/mod.rs b/shared-lib/lib-ot/src/core/document/mod.rs index 968c78bc2b..b019cb0f71 100644 --- a/shared-lib/lib-ot/src/core/document/mod.rs +++ b/shared-lib/lib-ot/src/core/document/mod.rs @@ -9,4 +9,5 @@ pub use attributes::*; pub use document::*; pub use document_operation::*; pub use node::*; +pub use position::*; pub use transaction::*; diff --git a/shared-lib/lib-ot/src/core/document/node.rs b/shared-lib/lib-ot/src/core/document/node.rs index 2fa9706ce3..a3ec5e729a 100644 --- a/shared-lib/lib-ot/src/core/document/node.rs +++ b/shared-lib/lib-ot/src/core/document/node.rs @@ -1,6 +1,7 @@ +use crate::core::{NodeAttributes, TextDelta}; use std::cell::RefCell; -use crate::core::{TextDelta, NodeAttributes}; +#[derive(Clone)] pub struct NodeData { pub node_type: String, pub attributes: RefCell, diff --git a/shared-lib/lib-ot/src/core/document/transaction.rs b/shared-lib/lib-ot/src/core/document/transaction.rs index c75cb16bea..875b1bebdf 100644 --- a/shared-lib/lib-ot/src/core/document/transaction.rs +++ b/shared-lib/lib-ot/src/core/document/transaction.rs @@ -1,17 +1,14 @@ -use crate::core::{DocumentOperation, DocumentTree}; +use crate::core::document::position::Position; +use crate::core::{DeleteOperation, DocumentOperation, DocumentTree, InsertOperation, NodeData}; pub struct Transaction { pub operations: Vec, } impl Transaction { - fn new(operations: Vec) -> Transaction { - Transaction { - operations, - } + Transaction { operations } } - } pub struct TransactionBuilder<'a> { @@ -23,17 +20,42 @@ impl<'a> TransactionBuilder<'a> { pub fn new(document: &'a DocumentTree) -> TransactionBuilder { TransactionBuilder { document, - operations: Vec::new() + operations: Vec::new(), } } + pub fn insert_nodes(&mut self, path: &Position, nodes: &[NodeData]) { + self.push(DocumentOperation::Insert(InsertOperation { + path: path.clone(), + nodes: nodes.to_vec(), + })); + } + + pub fn delete_node(&mut self, path: &Position) { + self.delete_nodes(path, 1); + } + + pub fn delete_nodes(&mut self, path: &Position, length: usize) { + let mut node = self.document.node_at_path(path).unwrap(); + let mut deleted_nodes: Vec = Vec::new(); + + for _ in 0..length { + let data = self.document.arena.get(node).unwrap(); + deleted_nodes.push(data.get().clone()); + node = node.following_siblings(&self.document.arena).next().unwrap(); + } + + self.operations.push(DocumentOperation::Delete(DeleteOperation { + path: path.clone(), + nodes: deleted_nodes, + })) + } + pub fn push(&mut self, op: DocumentOperation) { self.operations.push(op); } pub fn finalize(self) -> Transaction { - Transaction { - operations: self.operations, - } + Transaction::new(self.operations) } } diff --git a/shared-lib/lib-ot/tests/main.rs b/shared-lib/lib-ot/tests/main.rs index ecfdfb4da2..8ed7c8a74e 100644 --- a/shared-lib/lib-ot/tests/main.rs +++ b/shared-lib/lib-ot/tests/main.rs @@ -1,4 +1,4 @@ -use lib_ot::core::{DocumentTree, TransactionBuilder}; +use lib_ot::core::{DocumentTree, NodeData, Position, TransactionBuilder}; #[test] fn main() { @@ -9,7 +9,8 @@ fn main() { #[test] fn test_documents() { let mut document = DocumentTree::new(); - let tb = TransactionBuilder::new(&document); + let mut tb = TransactionBuilder::new(&document); + tb.insert_nodes(&Position(vec![0]), &vec![NodeData::new("type")]); let transaction = tb.finalize(); document.apply(transaction); } From 8401fa0983cd49dc4acb5ae22c5b27c2530ab8dc Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Thu, 18 Aug 2022 20:15:34 +0800 Subject: [PATCH 07/19] feat: update attributes --- .../lib-ot/src/core/document/attributes.rs | 2 +- .../lib-ot/src/core/document/transaction.rs | 24 ++++++++++++++++++- shared-lib/lib-ot/tests/main.rs | 15 +++++++++++- 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/shared-lib/lib-ot/src/core/document/attributes.rs b/shared-lib/lib-ot/src/core/document/attributes.rs index 011c4cec43..b682fd8404 100644 --- a/shared-lib/lib-ot/src/core/document/attributes.rs +++ b/shared-lib/lib-ot/src/core/document/attributes.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; #[derive(Clone)] -pub struct NodeAttributes(HashMap>); +pub struct NodeAttributes(pub HashMap>); impl NodeAttributes { pub fn new() -> NodeAttributes { diff --git a/shared-lib/lib-ot/src/core/document/transaction.rs b/shared-lib/lib-ot/src/core/document/transaction.rs index 875b1bebdf..f240b8ea89 100644 --- a/shared-lib/lib-ot/src/core/document/transaction.rs +++ b/shared-lib/lib-ot/src/core/document/transaction.rs @@ -1,5 +1,6 @@ +use std::collections::HashMap; use crate::core::document::position::Position; -use crate::core::{DeleteOperation, DocumentOperation, DocumentTree, InsertOperation, NodeData}; +use crate::core::{DeleteOperation, DocumentOperation, DocumentTree, InsertOperation, NodeAttributes, NodeData, UpdateOperation}; pub struct Transaction { pub operations: Vec, @@ -31,6 +32,27 @@ impl<'a> TransactionBuilder<'a> { })); } + pub fn update_attributes(&mut self, path: &Position, attributes: HashMap>) { + let mut old_attributes: HashMap> = HashMap::new(); + let node = self.document.node_at_path(path).unwrap(); + let node_data = self.document.arena.get(node).unwrap().get(); + + for key in attributes.keys() { + let old_attrs = node_data.attributes.borrow(); + let old_value = match old_attrs.0.get(key.as_str()) { + Some(value) => value.clone(), + None => None, + }; + old_attributes.insert(key.clone(), old_value); + } + + self.push(DocumentOperation::Update(UpdateOperation { + path: path.clone(), + attributes: NodeAttributes(attributes), + old_attributes: NodeAttributes(old_attributes), + })) + } + pub fn delete_node(&mut self, path: &Position) { self.delete_nodes(path, 1); } diff --git a/shared-lib/lib-ot/tests/main.rs b/shared-lib/lib-ot/tests/main.rs index 8ed7c8a74e..2d1813f2c9 100644 --- a/shared-lib/lib-ot/tests/main.rs +++ b/shared-lib/lib-ot/tests/main.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use lib_ot::core::{DocumentTree, NodeData, Position, TransactionBuilder}; #[test] @@ -10,7 +11,19 @@ fn main() { fn test_documents() { let mut document = DocumentTree::new(); let mut tb = TransactionBuilder::new(&document); - tb.insert_nodes(&Position(vec![0]), &vec![NodeData::new("type")]); + tb.insert_nodes(&Position(vec![0]), &vec![NodeData::new("text")]); + let transaction = tb.finalize(); + document.apply(transaction); + + assert!(document.node_at_path(&Position(vec![0])).is_some()); + let node = document.node_at_path(&Position(vec![0])).unwrap(); + let node_data = document.arena.get(node).unwrap().get(); + assert_eq!(node_data.node_type, "text"); + + let mut tb = TransactionBuilder::new(&document); + tb.update_attributes(&Position(vec![0]), HashMap::from([ + ("subtype".into(), Some("bullet-list".into())), + ])); let transaction = tb.finalize(); document.apply(transaction); } From 61d181b22864849aa939f9152dc956a187eb3652 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Fri, 19 Aug 2022 12:04:43 +0800 Subject: [PATCH 08/19] feat: vec to position convertion --- shared-lib/lib-ot/src/core/document/position.rs | 6 ++++++ shared-lib/lib-ot/tests/main.rs | 16 +++++++++++----- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/shared-lib/lib-ot/src/core/document/position.rs b/shared-lib/lib-ot/src/core/document/position.rs index 60a69ce8be..34e0c784d5 100644 --- a/shared-lib/lib-ot/src/core/document/position.rs +++ b/shared-lib/lib-ot/src/core/document/position.rs @@ -6,3 +6,9 @@ impl Position { self.0.is_empty() } } + +impl From> for Position { + fn from(v: Vec) -> Self { + Position(v) + } +} diff --git a/shared-lib/lib-ot/tests/main.rs b/shared-lib/lib-ot/tests/main.rs index 2d1813f2c9..c2e0226eb6 100644 --- a/shared-lib/lib-ot/tests/main.rs +++ b/shared-lib/lib-ot/tests/main.rs @@ -1,5 +1,5 @@ use std::collections::HashMap; -use lib_ot::core::{DocumentTree, NodeData, Position, TransactionBuilder}; +use lib_ot::core::{DocumentTree, NodeData, TransactionBuilder}; #[test] fn main() { @@ -11,19 +11,25 @@ fn main() { fn test_documents() { let mut document = DocumentTree::new(); let mut tb = TransactionBuilder::new(&document); - tb.insert_nodes(&Position(vec![0]), &vec![NodeData::new("text")]); + tb.insert_nodes(&vec![0].into(), &vec![NodeData::new("text")]); let transaction = tb.finalize(); document.apply(transaction); - assert!(document.node_at_path(&Position(vec![0])).is_some()); - let node = document.node_at_path(&Position(vec![0])).unwrap(); + assert!(document.node_at_path(&vec![0].into()).is_some()); + let node = document.node_at_path(&vec![0].into()).unwrap(); let node_data = document.arena.get(node).unwrap().get(); assert_eq!(node_data.node_type, "text"); let mut tb = TransactionBuilder::new(&document); - tb.update_attributes(&Position(vec![0]), HashMap::from([ + tb.update_attributes(&vec![0].into(), HashMap::from([ ("subtype".into(), Some("bullet-list".into())), ])); let transaction = tb.finalize(); document.apply(transaction); + + let mut tb = TransactionBuilder::new(&document); + tb.delete_node(&vec![0].into()); + let transaction = tb.finalize(); + document.apply(transaction); + assert!(document.node_at_path(&vec![0].into()).is_none()); } From c61b4d08656696c3f6401833fe264e6d429e59bc Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Fri, 19 Aug 2022 14:43:11 +0800 Subject: [PATCH 09/19] fix: CI format error --- .../src/core/document/document_operation.rs | 8 +++++ .../lib-ot/src/core/document/position.rs | 32 +++++++++++++++++++ .../lib-ot/src/core/document/transaction.rs | 6 ++-- shared-lib/lib-ot/tests/main.rs | 9 +++--- 4 files changed, 49 insertions(+), 6 deletions(-) diff --git a/shared-lib/lib-ot/src/core/document/document_operation.rs b/shared-lib/lib-ot/src/core/document/document_operation.rs index 7b7b0c4a62..ff1a968af5 100644 --- a/shared-lib/lib-ot/src/core/document/document_operation.rs +++ b/shared-lib/lib-ot/src/core/document/document_operation.rs @@ -1,6 +1,7 @@ use crate::core::document::position::Position; use crate::core::{NodeAttributes, NodeData, TextDelta}; +#[derive(Clone)] pub enum DocumentOperation { Insert(InsertOperation), Update(UpdateOperation), @@ -31,24 +32,31 @@ impl DocumentOperation { }), } } + pub fn transform(_a: &DocumentOperation, b: &DocumentOperation) -> DocumentOperation { + b.clone() + } } +#[derive(Clone)] pub struct InsertOperation { pub path: Position, pub nodes: Vec, } +#[derive(Clone)] pub struct UpdateOperation { pub path: Position, pub attributes: NodeAttributes, pub old_attributes: NodeAttributes, } +#[derive(Clone)] pub struct DeleteOperation { pub path: Position, pub nodes: Vec, } +#[derive(Clone)] pub struct TextEditOperation { pub path: Position, pub delta: TextDelta, diff --git a/shared-lib/lib-ot/src/core/document/position.rs b/shared-lib/lib-ot/src/core/document/position.rs index 34e0c784d5..b463f207da 100644 --- a/shared-lib/lib-ot/src/core/document/position.rs +++ b/shared-lib/lib-ot/src/core/document/position.rs @@ -5,6 +5,38 @@ impl Position { pub fn is_empty(&self) -> bool { self.0.is_empty() } + pub fn len(&self) -> usize { + self.0.len() + } +} + +impl Position { + // delta is default to be 1 + pub fn transform(pre_insert_path: &Position, b: &Position, delta: usize) -> Position { + if pre_insert_path.len() > b.len() { + return b.clone(); + } + if pre_insert_path.is_empty() || b.is_empty() { + return b.clone(); + } + // check the prefix + for i in 0..(pre_insert_path.len()) { + if pre_insert_path.0[i] != b.0[i] { + return b.clone(); + } + } + let mut prefix: Vec = pre_insert_path.0[0..(pre_insert_path.len() - 1)].into(); + let mut suffix: Vec = b.0[pre_insert_path.0.len()..].into(); + let prev_insert_last: usize = *pre_insert_path.0.last().unwrap(); + let b_at_index = b.0[pre_insert_path.0.len() - 1]; + if prev_insert_last <= b_at_index { + prefix.push(b_at_index + delta); + } else { + prefix.push(b_at_index); + } + prefix.append(&mut suffix); + return Position(prefix); + } } impl From> for Position { diff --git a/shared-lib/lib-ot/src/core/document/transaction.rs b/shared-lib/lib-ot/src/core/document/transaction.rs index f240b8ea89..c0e7cc3243 100644 --- a/shared-lib/lib-ot/src/core/document/transaction.rs +++ b/shared-lib/lib-ot/src/core/document/transaction.rs @@ -1,6 +1,8 @@ -use std::collections::HashMap; use crate::core::document::position::Position; -use crate::core::{DeleteOperation, DocumentOperation, DocumentTree, InsertOperation, NodeAttributes, NodeData, UpdateOperation}; +use crate::core::{ + DeleteOperation, DocumentOperation, DocumentTree, InsertOperation, NodeAttributes, NodeData, UpdateOperation, +}; +use std::collections::HashMap; pub struct Transaction { pub operations: Vec, diff --git a/shared-lib/lib-ot/tests/main.rs b/shared-lib/lib-ot/tests/main.rs index c2e0226eb6..f6d5aa6ace 100644 --- a/shared-lib/lib-ot/tests/main.rs +++ b/shared-lib/lib-ot/tests/main.rs @@ -1,5 +1,5 @@ -use std::collections::HashMap; use lib_ot::core::{DocumentTree, NodeData, TransactionBuilder}; +use std::collections::HashMap; #[test] fn main() { @@ -21,9 +21,10 @@ fn test_documents() { assert_eq!(node_data.node_type, "text"); let mut tb = TransactionBuilder::new(&document); - tb.update_attributes(&vec![0].into(), HashMap::from([ - ("subtype".into(), Some("bullet-list".into())), - ])); + tb.update_attributes( + &vec![0].into(), + HashMap::from([("subtype".into(), Some("bullet-list".into()))]), + ); let transaction = tb.finalize(); document.apply(transaction); From 0def48d0ca917f3194d2aa563127e6a4726be727 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Mon, 22 Aug 2022 11:10:06 +0800 Subject: [PATCH 10/19] feat: transform patht --- .../lib/src/operation/operation.dart | 4 +- .../lib-ot/src/core/document/document.rs | 9 ++++ .../src/core/document/document_operation.rs | 44 ++++++++++++++++++- .../lib-ot/src/core/document/position.rs | 4 +- shared-lib/lib-ot/tests/main.rs | 20 +++++++++ 5 files changed, 75 insertions(+), 6 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/operation.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/operation.dart index d1a0024a98..456be06766 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/operation.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/operation.dart @@ -207,10 +207,10 @@ Path transformPath(Path preInsertPath, Path b, [int delta = 1]) { Operation transformOperation(Operation a, Operation b) { if (a is InsertOperation) { - final newPath = transformPath(a.path, b.path); + final newPath = transformPath(a.path, b.path, a.nodes.length); return b.copyWithPath(newPath); } else if (a is DeleteOperation) { - final newPath = transformPath(a.path, b.path, -1); + final newPath = transformPath(a.path, b.path, -1 * a.nodes.length); return b.copyWithPath(newPath); } // TODO: transform update and textedit diff --git a/shared-lib/lib-ot/src/core/document/document.rs b/shared-lib/lib-ot/src/core/document/document.rs index 9d1d071f95..07626e1d75 100644 --- a/shared-lib/lib-ot/src/core/document/document.rs +++ b/shared-lib/lib-ot/src/core/document/document.rs @@ -124,6 +124,15 @@ impl DocumentTree { return; } + let children_length = parent.children(&self.arena).fold(0, |counter, _| counter + 1); + + if index == children_length { + for id in insert_children { + parent.append(*id, &mut self.arena); + } + return; + } + let node_to_insert = self.child_at_index_of_path(parent, index).unwrap(); for id in insert_children { diff --git a/shared-lib/lib-ot/src/core/document/document_operation.rs b/shared-lib/lib-ot/src/core/document/document_operation.rs index ff1a968af5..5a19f45a75 100644 --- a/shared-lib/lib-ot/src/core/document/document_operation.rs +++ b/shared-lib/lib-ot/src/core/document/document_operation.rs @@ -10,6 +10,14 @@ pub enum DocumentOperation { } impl DocumentOperation { + pub fn path(&self) -> &Position { + match self { + DocumentOperation::Insert(insert) => &insert.path, + DocumentOperation::Update(update) => &update.path, + DocumentOperation::Delete(delete) => &delete.path, + DocumentOperation::TextEdit(text_edit) => &text_edit.path, + } + } pub fn invert(&self) -> DocumentOperation { match self { DocumentOperation::Insert(insert_operation) => DocumentOperation::Delete(DeleteOperation { @@ -32,8 +40,40 @@ impl DocumentOperation { }), } } - pub fn transform(_a: &DocumentOperation, b: &DocumentOperation) -> DocumentOperation { - b.clone() + pub fn clone_with_new_path(&self, path: Position) -> DocumentOperation { + match self { + DocumentOperation::Insert(insert) => DocumentOperation::Insert(InsertOperation { + path, + nodes: insert.nodes.clone(), + }), + DocumentOperation::Update(update) => DocumentOperation::Update(UpdateOperation { + path, + attributes: update.attributes.clone(), + old_attributes: update.old_attributes.clone(), + }), + DocumentOperation::Delete(delete) => DocumentOperation::Delete(DeleteOperation { + path, + nodes: delete.nodes.clone(), + }), + DocumentOperation::TextEdit(text_edit) => DocumentOperation::TextEdit(TextEditOperation { + path, + delta: text_edit.delta.clone(), + inverted: text_edit.inverted.clone(), + }), + } + } + pub fn transform(a: &DocumentOperation, b: &DocumentOperation) -> DocumentOperation { + match a { + DocumentOperation::Insert(insert_op) => { + let new_path = Position::transform(a.path(), b.path(), insert_op.nodes.len() as i64); + b.clone_with_new_path(new_path) + } + DocumentOperation::Delete(delete_op) => { + let new_path = Position::transform(a.path(), b.path(), delete_op.nodes.len() as i64); + b.clone_with_new_path(new_path) + } + _ => b.clone(), + } } } diff --git a/shared-lib/lib-ot/src/core/document/position.rs b/shared-lib/lib-ot/src/core/document/position.rs index b463f207da..76f6a0eec4 100644 --- a/shared-lib/lib-ot/src/core/document/position.rs +++ b/shared-lib/lib-ot/src/core/document/position.rs @@ -12,7 +12,7 @@ impl Position { impl Position { // delta is default to be 1 - pub fn transform(pre_insert_path: &Position, b: &Position, delta: usize) -> Position { + pub fn transform(pre_insert_path: &Position, b: &Position, delta: i64) -> Position { if pre_insert_path.len() > b.len() { return b.clone(); } @@ -30,7 +30,7 @@ impl Position { let prev_insert_last: usize = *pre_insert_path.0.last().unwrap(); let b_at_index = b.0[pre_insert_path.0.len() - 1]; if prev_insert_last <= b_at_index { - prefix.push(b_at_index + delta); + prefix.push(((b_at_index as i64) + delta) as usize); } else { prefix.push(b_at_index); } diff --git a/shared-lib/lib-ot/tests/main.rs b/shared-lib/lib-ot/tests/main.rs index f6d5aa6ace..bfaf470dfa 100644 --- a/shared-lib/lib-ot/tests/main.rs +++ b/shared-lib/lib-ot/tests/main.rs @@ -34,3 +34,23 @@ fn test_documents() { document.apply(transaction); assert!(document.node_at_path(&vec![0].into()).is_none()); } + +#[test] +fn test_transform_paths() { + let mut document = DocumentTree::new(); + let transaction = { + let mut tb = TransactionBuilder::new(&document); + tb.insert_nodes(&vec![0].into(), &vec![NodeData::new("text")]); + tb.insert_nodes(&vec![1].into(), &vec![NodeData::new("text")]); + tb.insert_nodes(&vec![2].into(), &vec![NodeData::new("text")]); + tb.finalize() + }; + document.apply(transaction); + + let transaction = { + let mut tb = TransactionBuilder::new(&document); + tb.insert_nodes(&vec![1].into(), &vec![NodeData::new("text")]); + tb.finalize() + }; + document.apply(transaction); +} From 1801a47b1d7c384e8332e3104b99178bbd10390a Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Mon, 22 Aug 2022 14:44:59 +0800 Subject: [PATCH 11/19] feat: test update nodes --- shared-lib/lib-ot/tests/main.rs | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/shared-lib/lib-ot/tests/main.rs b/shared-lib/lib-ot/tests/main.rs index bfaf470dfa..becc91a31f 100644 --- a/shared-lib/lib-ot/tests/main.rs +++ b/shared-lib/lib-ot/tests/main.rs @@ -1,4 +1,4 @@ -use lib_ot::core::{DocumentTree, NodeData, TransactionBuilder}; +use lib_ot::core::{DocumentTree, NodeData, Position, TransactionBuilder}; use std::collections::HashMap; #[test] @@ -36,7 +36,7 @@ fn test_documents() { } #[test] -fn test_transform_paths() { +fn test_inserts_nodes() { let mut document = DocumentTree::new(); let transaction = { let mut tb = TransactionBuilder::new(&document); @@ -54,3 +54,30 @@ fn test_transform_paths() { }; document.apply(transaction); } + +#[test] +fn test_update_nodes() { + let mut document = DocumentTree::new(); + let transaction = { + let mut tb = TransactionBuilder::new(&document); + tb.insert_nodes(&vec![0].into(), &vec![NodeData::new("text")]); + tb.insert_nodes(&vec![1].into(), &vec![NodeData::new("text")]); + tb.insert_nodes(&vec![2].into(), &vec![NodeData::new("text")]); + tb.finalize() + }; + document.apply(transaction); + + let transaction = { + let mut tb = TransactionBuilder::new(&document); + tb.update_attributes(&vec![1].into(), HashMap::from([ + ("bolded".into(), Some("true".into())), + ])); + tb.finalize() + }; + document.apply(transaction); + + let node = document.node_at_path(&Position(vec![1])).unwrap(); + let node_data = document.arena.get(node).unwrap().get(); + let is_bold = node_data.attributes.borrow().0.get("bolded").unwrap().clone(); + assert_eq!(is_bold.unwrap(), "true"); +} From ef185cd5d5097e26b8908e7b19a609300662c14e Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Mon, 22 Aug 2022 14:58:44 +0800 Subject: [PATCH 12/19] refactor: add at_path suffix to transaction builder --- .../lib-ot/src/core/document/transaction.rs | 10 +-- shared-lib/lib-ot/tests/main.rs | 71 +++++++++++++------ 2 files changed, 54 insertions(+), 27 deletions(-) diff --git a/shared-lib/lib-ot/src/core/document/transaction.rs b/shared-lib/lib-ot/src/core/document/transaction.rs index c0e7cc3243..99d5ee8169 100644 --- a/shared-lib/lib-ot/src/core/document/transaction.rs +++ b/shared-lib/lib-ot/src/core/document/transaction.rs @@ -27,14 +27,14 @@ impl<'a> TransactionBuilder<'a> { } } - pub fn insert_nodes(&mut self, path: &Position, nodes: &[NodeData]) { + pub fn insert_nodes_at_path(&mut self, path: &Position, nodes: &[NodeData]) { self.push(DocumentOperation::Insert(InsertOperation { path: path.clone(), nodes: nodes.to_vec(), })); } - pub fn update_attributes(&mut self, path: &Position, attributes: HashMap>) { + pub fn update_attributes_at_path(&mut self, path: &Position, attributes: HashMap>) { let mut old_attributes: HashMap> = HashMap::new(); let node = self.document.node_at_path(path).unwrap(); let node_data = self.document.arena.get(node).unwrap().get(); @@ -55,11 +55,11 @@ impl<'a> TransactionBuilder<'a> { })) } - pub fn delete_node(&mut self, path: &Position) { - self.delete_nodes(path, 1); + pub fn delete_node_at_path(&mut self, path: &Position) { + self.delete_nodes_at_path(path, 1); } - pub fn delete_nodes(&mut self, path: &Position, length: usize) { + pub fn delete_nodes_at_path(&mut self, path: &Position, length: usize) { let mut node = self.document.node_at_path(path).unwrap(); let mut deleted_nodes: Vec = Vec::new(); diff --git a/shared-lib/lib-ot/tests/main.rs b/shared-lib/lib-ot/tests/main.rs index becc91a31f..1595c20f29 100644 --- a/shared-lib/lib-ot/tests/main.rs +++ b/shared-lib/lib-ot/tests/main.rs @@ -10,9 +10,11 @@ fn main() { #[test] fn test_documents() { let mut document = DocumentTree::new(); - let mut tb = TransactionBuilder::new(&document); - tb.insert_nodes(&vec![0].into(), &vec![NodeData::new("text")]); - let transaction = tb.finalize(); + let transaction = { + let mut tb = TransactionBuilder::new(&document); + tb.insert_nodes_at_path(&vec![0].into(), &vec![NodeData::new("text")]); + tb.finalize() + }; document.apply(transaction); assert!(document.node_at_path(&vec![0].into()).is_some()); @@ -20,17 +22,21 @@ fn test_documents() { let node_data = document.arena.get(node).unwrap().get(); assert_eq!(node_data.node_type, "text"); - let mut tb = TransactionBuilder::new(&document); - tb.update_attributes( - &vec![0].into(), - HashMap::from([("subtype".into(), Some("bullet-list".into()))]), - ); - let transaction = tb.finalize(); + let transaction = { + let mut tb = TransactionBuilder::new(&document); + tb.update_attributes_at_path( + &vec![0].into(), + HashMap::from([("subtype".into(), Some("bullet-list".into()))]), + ); + tb.finalize() + }; document.apply(transaction); - let mut tb = TransactionBuilder::new(&document); - tb.delete_node(&vec![0].into()); - let transaction = tb.finalize(); + let transaction = { + let mut tb = TransactionBuilder::new(&document); + tb.delete_node_at_path(&vec![0].into()); + tb.finalize() + }; document.apply(transaction); assert!(document.node_at_path(&vec![0].into()).is_none()); } @@ -40,16 +46,16 @@ fn test_inserts_nodes() { let mut document = DocumentTree::new(); let transaction = { let mut tb = TransactionBuilder::new(&document); - tb.insert_nodes(&vec![0].into(), &vec![NodeData::new("text")]); - tb.insert_nodes(&vec![1].into(), &vec![NodeData::new("text")]); - tb.insert_nodes(&vec![2].into(), &vec![NodeData::new("text")]); + tb.insert_nodes_at_path(&vec![0].into(), &vec![NodeData::new("text")]); + tb.insert_nodes_at_path(&vec![1].into(), &vec![NodeData::new("text")]); + tb.insert_nodes_at_path(&vec![2].into(), &vec![NodeData::new("text")]); tb.finalize() }; document.apply(transaction); let transaction = { let mut tb = TransactionBuilder::new(&document); - tb.insert_nodes(&vec![1].into(), &vec![NodeData::new("text")]); + tb.insert_nodes_at_path(&vec![1].into(), &vec![NodeData::new("text")]); tb.finalize() }; document.apply(transaction); @@ -60,18 +66,16 @@ fn test_update_nodes() { let mut document = DocumentTree::new(); let transaction = { let mut tb = TransactionBuilder::new(&document); - tb.insert_nodes(&vec![0].into(), &vec![NodeData::new("text")]); - tb.insert_nodes(&vec![1].into(), &vec![NodeData::new("text")]); - tb.insert_nodes(&vec![2].into(), &vec![NodeData::new("text")]); + tb.insert_nodes_at_path(&vec![0].into(), &vec![NodeData::new("text")]); + tb.insert_nodes_at_path(&vec![1].into(), &vec![NodeData::new("text")]); + tb.insert_nodes_at_path(&vec![2].into(), &vec![NodeData::new("text")]); tb.finalize() }; document.apply(transaction); let transaction = { let mut tb = TransactionBuilder::new(&document); - tb.update_attributes(&vec![1].into(), HashMap::from([ - ("bolded".into(), Some("true".into())), - ])); + tb.update_attributes_at_path(&vec![1].into(), HashMap::from([("bolded".into(), Some("true".into()))])); tb.finalize() }; document.apply(transaction); @@ -81,3 +85,26 @@ fn test_update_nodes() { let is_bold = node_data.attributes.borrow().0.get("bolded").unwrap().clone(); assert_eq!(is_bold.unwrap(), "true"); } + +#[test] +fn test_delete_nodes() { + let mut document = DocumentTree::new(); + let transaction = { + let mut tb = TransactionBuilder::new(&document); + tb.insert_nodes_at_path(&vec![0].into(), &vec![NodeData::new("text")]); + tb.insert_nodes_at_path(&vec![1].into(), &vec![NodeData::new("text")]); + tb.insert_nodes_at_path(&vec![2].into(), &vec![NodeData::new("text")]); + tb.finalize() + }; + document.apply(transaction); + + let transaction = { + let mut tb = TransactionBuilder::new(&document); + tb.delete_node_at_path(&Position(vec![1])); + tb.finalize() + }; + document.apply(transaction); + + let len = document.root.children(&document.arena).fold(0, |count, _| count + 1); + assert_eq!(len, 2); +} From b0bafff22c92c5016e99f90cef32826dd7b04c89 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Mon, 22 Aug 2022 16:46:24 +0800 Subject: [PATCH 13/19] feat: introduce error to apply method --- frontend/rust-lib/Cargo.lock | 7 + .../lib-ot/src/core/document/document.rs | 120 +++++++++++----- .../src/core/document/document_operation.rs | 136 +++++++++--------- shared-lib/lib-ot/src/core/document/node.rs | 9 +- .../lib-ot/src/core/document/transaction.rs | 18 ++- shared-lib/lib-ot/src/errors.rs | 3 +- shared-lib/lib-ot/tests/main.rs | 34 +++-- 7 files changed, 193 insertions(+), 134 deletions(-) diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 7534492ab0..4119cc1edf 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -1618,6 +1618,12 @@ dependencies = [ "serde", ] +[[package]] +name = "indextree" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42b4b46b3311ebd8e5cd44f6b03b36e0f48a70552cf6b036afcebc5626794066" + [[package]] name = "instant" version = "0.1.12" @@ -1766,6 +1772,7 @@ dependencies = [ "bytes", "dashmap", "derive_more", + "indextree", "lazy_static", "log", "md5", diff --git a/shared-lib/lib-ot/src/core/document/document.rs b/shared-lib/lib-ot/src/core/document/document.rs index 07626e1d75..063fe408b0 100644 --- a/shared-lib/lib-ot/src/core/document/document.rs +++ b/shared-lib/lib-ot/src/core/document/document.rs @@ -1,8 +1,6 @@ use crate::core::document::position::Position; -use crate::core::{ - DeleteOperation, DocumentOperation, InsertOperation, NodeAttributes, NodeData, TextEditOperation, Transaction, - UpdateOperation, -}; +use crate::core::{DocumentOperation, NodeAttributes, NodeData, OperationTransform, TextDelta, Transaction}; +use crate::errors::{ErrorBuilder, OTError, OTErrorCode}; use indextree::{Arena, NodeId}; pub struct DocumentTree { @@ -86,42 +84,48 @@ impl DocumentTree { None } - pub fn apply(&mut self, transaction: Transaction) { + pub fn apply(&mut self, transaction: Transaction) -> Result<(), OTError> { for op in &transaction.operations { - self.apply_op(op); + self.apply_op(op)?; } + Ok(()) } - fn apply_op(&mut self, op: &DocumentOperation) { + fn apply_op(&mut self, op: &DocumentOperation) -> Result<(), OTError> { match op { - DocumentOperation::Insert(op) => self.apply_insert(op), - DocumentOperation::Update(op) => self.apply_update(op), - DocumentOperation::Delete(op) => self.apply_delete(op), - DocumentOperation::TextEdit(op) => self.apply_text_edit(op), + DocumentOperation::Insert { path, nodes } => self.apply_insert(path, nodes), + DocumentOperation::Update { path, attributes, .. } => self.apply_update(path, attributes), + DocumentOperation::Delete { path, nodes } => self.apply_delete(path, nodes.len()), + DocumentOperation::TextEdit { path, delta, .. } => self.apply_text_edit(path, delta), } } - fn apply_insert(&mut self, op: &InsertOperation) { - let parent_path = &op.path.0[0..(op.path.0.len() - 1)]; - let last_index = op.path.0[op.path.0.len() - 1]; - let parent_node = self.node_at_path(&Position(parent_path.to_vec())); - if let Some(parent_node) = parent_node { - let mut inserted_nodes = Vec::new(); + fn apply_insert(&mut self, path: &Position, nodes: &Vec) -> Result<(), OTError> { + let parent_path = &path.0[0..(path.0.len() - 1)]; + let last_index = path.0[path.0.len() - 1]; + let parent_node = self + .node_at_path(&Position(parent_path.to_vec())) + .ok_or(ErrorBuilder::new(OTErrorCode::PathNotFound).build())?; + let mut inserted_nodes = Vec::new(); - for node in &op.nodes { - inserted_nodes.push(self.arena.new_node(node.clone())); - } - - self.insert_child_at_index(parent_node, last_index, &inserted_nodes); + for node in nodes { + inserted_nodes.push(self.arena.new_node(node.clone())); } + + self.insert_child_at_index(parent_node, last_index, &inserted_nodes) } - fn insert_child_at_index(&mut self, parent: NodeId, index: usize, insert_children: &[NodeId]) { + fn insert_child_at_index( + &mut self, + parent: NodeId, + index: usize, + insert_children: &[NodeId], + ) -> Result<(), OTError> { if index == 0 && parent.children(&self.arena).next().is_none() { for id in insert_children { parent.append(*id, &mut self.arena); } - return; + return Ok(()); } let children_length = parent.children(&self.arena).fold(0, |counter, _| counter + 1); @@ -130,32 +134,72 @@ impl DocumentTree { for id in insert_children { parent.append(*id, &mut self.arena); } - return; + return Ok(()); } - let node_to_insert = self.child_at_index_of_path(parent, index).unwrap(); + let node_to_insert = self + .child_at_index_of_path(parent, index) + .ok_or(ErrorBuilder::new(OTErrorCode::PathNotFound).build())?; for id in insert_children { node_to_insert.insert_before(*id, &mut self.arena); } + Ok(()) } - fn apply_update(&self, op: &UpdateOperation) { - let update_node = self.node_at_path(&op.path).unwrap(); - let node_data = self.arena.get(update_node).unwrap(); - let new_attributes = { - let old_attributes = node_data.get().attributes.borrow(); - NodeAttributes::compose(&old_attributes, &op.attributes) + fn apply_update(&mut self, path: &Position, attributes: &NodeAttributes) -> Result<(), OTError> { + let update_node = self + .node_at_path(path) + .ok_or(ErrorBuilder::new(OTErrorCode::PathNotFound).build())?; + let node_data = self.arena.get_mut(update_node).unwrap(); + // let new_node = NodeData { + // ..node_data.get().clone() + // attributes: + // }; + let new_node = { + let old_attributes = &node_data.get().attributes; + let new_attributes = NodeAttributes::compose(&old_attributes, attributes); + NodeData { + attributes: new_attributes, + ..node_data.get().clone() + } }; - node_data.get().attributes.replace(new_attributes); + *node_data.get_mut() = new_node; + Ok(()) } - fn apply_delete(&mut self, op: &DeleteOperation) { - let update_node = self.node_at_path(&op.path).unwrap(); - update_node.remove_subtree(&mut self.arena); + fn apply_delete(&mut self, path: &Position, len: usize) -> Result<(), OTError> { + let mut update_node = self + .node_at_path(path) + .ok_or(ErrorBuilder::new(OTErrorCode::PathNotFound).build())?; + for _ in 0..len { + let next = update_node.following_siblings(&self.arena).next(); + update_node.remove_subtree(&mut self.arena); + if let Some(next_id) = next { + update_node = next_id; + } else { + break; + } + } + Ok(()) } - fn apply_text_edit(&self, _op: &TextEditOperation) { - unimplemented!() + fn apply_text_edit(&mut self, path: &Position, delta: &TextDelta) -> Result<(), OTError> { + let edit_node = self + .node_at_path(path) + .ok_or(ErrorBuilder::new(OTErrorCode::PathNotFound).build())?; + let node_data = self.arena.get_mut(edit_node).unwrap(); + let new_delta = if let Some(old_delta) = &node_data.get().delta { + Some(old_delta.compose(delta)?) + } else { + None + }; + if let Some(new_delta) = new_delta { + *node_data.get_mut() = NodeData { + delta: Some(new_delta), + ..node_data.get().clone() + }; + }; + Ok(()) } } diff --git a/shared-lib/lib-ot/src/core/document/document_operation.rs b/shared-lib/lib-ot/src/core/document/document_operation.rs index 5a19f45a75..0ce16ae89c 100644 --- a/shared-lib/lib-ot/src/core/document/document_operation.rs +++ b/shared-lib/lib-ot/src/core/document/document_operation.rs @@ -3,102 +3,98 @@ use crate::core::{NodeAttributes, NodeData, TextDelta}; #[derive(Clone)] pub enum DocumentOperation { - Insert(InsertOperation), - Update(UpdateOperation), - Delete(DeleteOperation), - TextEdit(TextEditOperation), + Insert { + path: Position, + nodes: Vec, + }, + Update { + path: Position, + attributes: NodeAttributes, + old_attributes: NodeAttributes, + }, + Delete { + path: Position, + nodes: Vec, + }, + TextEdit { + path: Position, + delta: TextDelta, + inverted: TextDelta, + }, } impl DocumentOperation { pub fn path(&self) -> &Position { match self { - DocumentOperation::Insert(insert) => &insert.path, - DocumentOperation::Update(update) => &update.path, - DocumentOperation::Delete(delete) => &delete.path, - DocumentOperation::TextEdit(text_edit) => &text_edit.path, + DocumentOperation::Insert { path, .. } => path, + DocumentOperation::Update { path, .. } => path, + DocumentOperation::Delete { path, .. } => path, + DocumentOperation::TextEdit { path, .. } => path, } } pub fn invert(&self) -> DocumentOperation { match self { - DocumentOperation::Insert(insert_operation) => DocumentOperation::Delete(DeleteOperation { - path: insert_operation.path.clone(), - nodes: insert_operation.nodes.clone(), - }), - DocumentOperation::Update(update_operation) => DocumentOperation::Update(UpdateOperation { - path: update_operation.path.clone(), - attributes: update_operation.old_attributes.clone(), - old_attributes: update_operation.attributes.clone(), - }), - DocumentOperation::Delete(delete_operation) => DocumentOperation::Insert(InsertOperation { - path: delete_operation.path.clone(), - nodes: delete_operation.nodes.clone(), - }), - DocumentOperation::TextEdit(text_edit_operation) => DocumentOperation::TextEdit(TextEditOperation { - path: text_edit_operation.path.clone(), - delta: text_edit_operation.inverted.clone(), - inverted: text_edit_operation.delta.clone(), - }), + DocumentOperation::Insert { path, nodes } => DocumentOperation::Delete { + path: path.clone(), + nodes: nodes.clone(), + }, + DocumentOperation::Update { + path, + attributes, + old_attributes, + } => DocumentOperation::Update { + path: path.clone(), + attributes: old_attributes.clone(), + old_attributes: attributes.clone(), + }, + DocumentOperation::Delete { path, nodes } => DocumentOperation::Insert { + path: path.clone(), + nodes: nodes.clone(), + }, + DocumentOperation::TextEdit { path, delta, inverted } => DocumentOperation::TextEdit { + path: path.clone(), + delta: inverted.clone(), + inverted: delta.clone(), + }, } } pub fn clone_with_new_path(&self, path: Position) -> DocumentOperation { match self { - DocumentOperation::Insert(insert) => DocumentOperation::Insert(InsertOperation { + DocumentOperation::Insert { nodes, .. } => DocumentOperation::Insert { path, - nodes: insert.nodes.clone(), - }), - DocumentOperation::Update(update) => DocumentOperation::Update(UpdateOperation { + nodes: nodes.clone(), + }, + DocumentOperation::Update { + attributes, + old_attributes, + .. + } => DocumentOperation::Update { path, - attributes: update.attributes.clone(), - old_attributes: update.old_attributes.clone(), - }), - DocumentOperation::Delete(delete) => DocumentOperation::Delete(DeleteOperation { + attributes: attributes.clone(), + old_attributes: old_attributes.clone(), + }, + DocumentOperation::Delete { nodes, .. } => DocumentOperation::Delete { path, - nodes: delete.nodes.clone(), - }), - DocumentOperation::TextEdit(text_edit) => DocumentOperation::TextEdit(TextEditOperation { + nodes: nodes.clone(), + }, + DocumentOperation::TextEdit { delta, inverted, .. } => DocumentOperation::TextEdit { path, - delta: text_edit.delta.clone(), - inverted: text_edit.inverted.clone(), - }), + delta: delta.clone(), + inverted: inverted.clone(), + }, } } pub fn transform(a: &DocumentOperation, b: &DocumentOperation) -> DocumentOperation { match a { - DocumentOperation::Insert(insert_op) => { - let new_path = Position::transform(a.path(), b.path(), insert_op.nodes.len() as i64); + DocumentOperation::Insert { path: a_path, nodes } => { + let new_path = Position::transform(a_path, b.path(), nodes.len() as i64); b.clone_with_new_path(new_path) } - DocumentOperation::Delete(delete_op) => { - let new_path = Position::transform(a.path(), b.path(), delete_op.nodes.len() as i64); + DocumentOperation::Delete { path: a_path, nodes } => { + let new_path = Position::transform(a_path, b.path(), nodes.len() as i64); b.clone_with_new_path(new_path) } _ => b.clone(), } } } - -#[derive(Clone)] -pub struct InsertOperation { - pub path: Position, - pub nodes: Vec, -} - -#[derive(Clone)] -pub struct UpdateOperation { - pub path: Position, - pub attributes: NodeAttributes, - pub old_attributes: NodeAttributes, -} - -#[derive(Clone)] -pub struct DeleteOperation { - pub path: Position, - pub nodes: Vec, -} - -#[derive(Clone)] -pub struct TextEditOperation { - pub path: Position, - pub delta: TextDelta, - pub inverted: TextDelta, -} diff --git a/shared-lib/lib-ot/src/core/document/node.rs b/shared-lib/lib-ot/src/core/document/node.rs index a3ec5e729a..e7c5e0e68f 100644 --- a/shared-lib/lib-ot/src/core/document/node.rs +++ b/shared-lib/lib-ot/src/core/document/node.rs @@ -1,19 +1,18 @@ use crate::core::{NodeAttributes, TextDelta}; -use std::cell::RefCell; #[derive(Clone)] pub struct NodeData { pub node_type: String, - pub attributes: RefCell, - pub delta: RefCell>, + pub attributes: NodeAttributes, + pub delta: Option, } impl NodeData { pub fn new(node_type: &str) -> NodeData { NodeData { node_type: node_type.into(), - attributes: RefCell::new(NodeAttributes::new()), - delta: RefCell::new(None), + attributes: NodeAttributes::new(), + delta: None, } } } diff --git a/shared-lib/lib-ot/src/core/document/transaction.rs b/shared-lib/lib-ot/src/core/document/transaction.rs index 99d5ee8169..00697b7da1 100644 --- a/shared-lib/lib-ot/src/core/document/transaction.rs +++ b/shared-lib/lib-ot/src/core/document/transaction.rs @@ -1,7 +1,5 @@ use crate::core::document::position::Position; -use crate::core::{ - DeleteOperation, DocumentOperation, DocumentTree, InsertOperation, NodeAttributes, NodeData, UpdateOperation, -}; +use crate::core::{DocumentOperation, DocumentTree, NodeAttributes, NodeData}; use std::collections::HashMap; pub struct Transaction { @@ -28,10 +26,10 @@ impl<'a> TransactionBuilder<'a> { } pub fn insert_nodes_at_path(&mut self, path: &Position, nodes: &[NodeData]) { - self.push(DocumentOperation::Insert(InsertOperation { + self.push(DocumentOperation::Insert { path: path.clone(), nodes: nodes.to_vec(), - })); + }); } pub fn update_attributes_at_path(&mut self, path: &Position, attributes: HashMap>) { @@ -40,7 +38,7 @@ impl<'a> TransactionBuilder<'a> { let node_data = self.document.arena.get(node).unwrap().get(); for key in attributes.keys() { - let old_attrs = node_data.attributes.borrow(); + let old_attrs = &node_data.attributes; let old_value = match old_attrs.0.get(key.as_str()) { Some(value) => value.clone(), None => None, @@ -48,11 +46,11 @@ impl<'a> TransactionBuilder<'a> { old_attributes.insert(key.clone(), old_value); } - self.push(DocumentOperation::Update(UpdateOperation { + self.push(DocumentOperation::Update { path: path.clone(), attributes: NodeAttributes(attributes), old_attributes: NodeAttributes(old_attributes), - })) + }) } pub fn delete_node_at_path(&mut self, path: &Position) { @@ -69,10 +67,10 @@ impl<'a> TransactionBuilder<'a> { node = node.following_siblings(&self.document.arena).next().unwrap(); } - self.operations.push(DocumentOperation::Delete(DeleteOperation { + self.operations.push(DocumentOperation::Delete { path: path.clone(), nodes: deleted_nodes, - })) + }) } pub fn push(&mut self, op: DocumentOperation) { diff --git a/shared-lib/lib-ot/src/errors.rs b/shared-lib/lib-ot/src/errors.rs index e7aea28bcf..eb313c784f 100644 --- a/shared-lib/lib-ot/src/errors.rs +++ b/shared-lib/lib-ot/src/errors.rs @@ -60,7 +60,7 @@ impl std::convert::From for OTError { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Eq, PartialEq)] pub enum OTErrorCode { IncompatibleLength, ApplyInsertFail, @@ -74,6 +74,7 @@ pub enum OTErrorCode { DuplicatedRevision, RevisionIDConflict, Internal, + PathNotFound, } pub struct ErrorBuilder { diff --git a/shared-lib/lib-ot/tests/main.rs b/shared-lib/lib-ot/tests/main.rs index 1595c20f29..7daf74e957 100644 --- a/shared-lib/lib-ot/tests/main.rs +++ b/shared-lib/lib-ot/tests/main.rs @@ -1,4 +1,5 @@ use lib_ot::core::{DocumentTree, NodeData, Position, TransactionBuilder}; +use lib_ot::errors::OTErrorCode; use std::collections::HashMap; #[test] @@ -15,7 +16,7 @@ fn test_documents() { tb.insert_nodes_at_path(&vec![0].into(), &vec![NodeData::new("text")]); tb.finalize() }; - document.apply(transaction); + document.apply(transaction).unwrap(); assert!(document.node_at_path(&vec![0].into()).is_some()); let node = document.node_at_path(&vec![0].into()).unwrap(); @@ -30,14 +31,14 @@ fn test_documents() { ); tb.finalize() }; - document.apply(transaction); + document.apply(transaction).unwrap(); let transaction = { let mut tb = TransactionBuilder::new(&document); tb.delete_node_at_path(&vec![0].into()); tb.finalize() }; - document.apply(transaction); + document.apply(transaction).unwrap(); assert!(document.node_at_path(&vec![0].into()).is_none()); } @@ -51,14 +52,14 @@ fn test_inserts_nodes() { tb.insert_nodes_at_path(&vec![2].into(), &vec![NodeData::new("text")]); tb.finalize() }; - document.apply(transaction); + document.apply(transaction).unwrap(); let transaction = { let mut tb = TransactionBuilder::new(&document); tb.insert_nodes_at_path(&vec![1].into(), &vec![NodeData::new("text")]); tb.finalize() }; - document.apply(transaction); + document.apply(transaction).unwrap(); } #[test] @@ -71,18 +72,18 @@ fn test_update_nodes() { tb.insert_nodes_at_path(&vec![2].into(), &vec![NodeData::new("text")]); tb.finalize() }; - document.apply(transaction); + document.apply(transaction).unwrap(); let transaction = { let mut tb = TransactionBuilder::new(&document); tb.update_attributes_at_path(&vec![1].into(), HashMap::from([("bolded".into(), Some("true".into()))])); tb.finalize() }; - document.apply(transaction); + document.apply(transaction).unwrap(); let node = document.node_at_path(&Position(vec![1])).unwrap(); let node_data = document.arena.get(node).unwrap().get(); - let is_bold = node_data.attributes.borrow().0.get("bolded").unwrap().clone(); + let is_bold = node_data.attributes.0.get("bolded").unwrap().clone(); assert_eq!(is_bold.unwrap(), "true"); } @@ -96,15 +97,28 @@ fn test_delete_nodes() { tb.insert_nodes_at_path(&vec![2].into(), &vec![NodeData::new("text")]); tb.finalize() }; - document.apply(transaction); + document.apply(transaction).unwrap(); let transaction = { let mut tb = TransactionBuilder::new(&document); tb.delete_node_at_path(&Position(vec![1])); tb.finalize() }; - document.apply(transaction); + document.apply(transaction).unwrap(); let len = document.root.children(&document.arena).fold(0, |count, _| count + 1); assert_eq!(len, 2); } + +#[test] +fn test_errors() { + let mut document = DocumentTree::new(); + let transaction = { + let mut tb = TransactionBuilder::new(&document); + tb.insert_nodes_at_path(&vec![0].into(), &vec![NodeData::new("text")]); + tb.insert_nodes_at_path(&vec![100].into(), &vec![NodeData::new("text")]); + tb.finalize() + }; + let result = document.apply(transaction); + assert_eq!(result.err().unwrap().code, OTErrorCode::PathNotFound); +} From ef65551340a77f1504a8f542566e29eeed81f45a Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Mon, 22 Aug 2022 18:33:02 +0800 Subject: [PATCH 14/19] feat: add serialize/deserialize --- .../lib-ot/src/core/document/attributes.rs | 2 +- .../src/core/document/document_operation.rs | 55 ++++++++++++++++++- shared-lib/lib-ot/src/core/document/node.rs | 2 +- .../lib-ot/src/core/document/position.rs | 4 +- 4 files changed, 58 insertions(+), 5 deletions(-) diff --git a/shared-lib/lib-ot/src/core/document/attributes.rs b/shared-lib/lib-ot/src/core/document/attributes.rs index b682fd8404..52b33dce08 100644 --- a/shared-lib/lib-ot/src/core/document/attributes.rs +++ b/shared-lib/lib-ot/src/core/document/attributes.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -#[derive(Clone)] +#[derive(Clone, serde::Serialize, serde::Deserialize)] pub struct NodeAttributes(pub HashMap>); impl NodeAttributes { diff --git a/shared-lib/lib-ot/src/core/document/document_operation.rs b/shared-lib/lib-ot/src/core/document/document_operation.rs index 0ce16ae89c..46cb79be4a 100644 --- a/shared-lib/lib-ot/src/core/document/document_operation.rs +++ b/shared-lib/lib-ot/src/core/document/document_operation.rs @@ -1,7 +1,7 @@ use crate::core::document::position::Position; use crate::core::{NodeAttributes, NodeData, TextDelta}; -#[derive(Clone)] +#[derive(Clone, serde::Serialize, serde::Deserialize)] pub enum DocumentOperation { Insert { path: Position, @@ -98,3 +98,56 @@ impl DocumentOperation { } } } + +#[cfg(test)] +mod tests { + use crate::core::Position; + + #[test] + fn test_transform_path_1() { + assert_eq!( + { Position::transform(&Position(vec![0, 1]), &Position(vec![0, 1]), 1) }.0, + vec![0, 2] + ); + } + + #[test] + fn test_transform_path_2() { + assert_eq!( + { Position::transform(&Position(vec![0, 1]), &Position(vec![0, 2]), 1) }.0, + vec![0, 3] + ); + } + + #[test] + fn test_transform_path_3() { + assert_eq!( + { Position::transform(&Position(vec![0, 1]), &Position(vec![0, 2, 7, 8, 9]), 1) }.0, + vec![0, 3, 7, 8, 9] + ); + } + + #[test] + fn test_transform_path_not_changed() { + assert_eq!( + { Position::transform(&Position(vec![0, 1, 2]), &Position(vec![0, 0, 7, 8, 9]), 1) }.0, + vec![0, 0, 7, 8, 9] + ); + assert_eq!( + { Position::transform(&Position(vec![0, 1, 2]), &Position(vec![0, 1]), 1) }.0, + vec![0, 1] + ); + assert_eq!( + { Position::transform(&Position(vec![1, 1]), &Position(vec![1, 0]), 1) }.0, + vec![1, 0] + ); + } + + #[test] + fn test_transform_delta() { + assert_eq!( + { Position::transform(&Position(vec![0, 1]), &Position(vec![0, 1]), 5) }.0, + vec![0, 6] + ); + } +} diff --git a/shared-lib/lib-ot/src/core/document/node.rs b/shared-lib/lib-ot/src/core/document/node.rs index e7c5e0e68f..96747b0fd8 100644 --- a/shared-lib/lib-ot/src/core/document/node.rs +++ b/shared-lib/lib-ot/src/core/document/node.rs @@ -1,6 +1,6 @@ use crate::core::{NodeAttributes, TextDelta}; -#[derive(Clone)] +#[derive(Clone, serde::Serialize, serde::Deserialize)] pub struct NodeData { pub node_type: String, pub attributes: NodeAttributes, diff --git a/shared-lib/lib-ot/src/core/document/position.rs b/shared-lib/lib-ot/src/core/document/position.rs index 76f6a0eec4..521d0e9a60 100644 --- a/shared-lib/lib-ot/src/core/document/position.rs +++ b/shared-lib/lib-ot/src/core/document/position.rs @@ -1,4 +1,4 @@ -#[derive(Clone)] +#[derive(Clone, serde::Serialize, serde::Deserialize)] pub struct Position(pub Vec); impl Position { @@ -20,7 +20,7 @@ impl Position { return b.clone(); } // check the prefix - for i in 0..(pre_insert_path.len()) { + for i in 0..(pre_insert_path.len() - 1) { if pre_insert_path.0[i] != b.0[i] { return b.clone(); } From bb8e0485cdd9869900b7358bd050ee58007dd68b Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Tue, 23 Aug 2022 11:15:11 +0800 Subject: [PATCH 15/19] feat: serialize --- .../lib-ot/src/core/document/document.rs | 46 ++++++++------ .../src/core/document/document_operation.rs | 61 ++++++++++++++++--- shared-lib/lib-ot/src/core/document/node.rs | 31 +++++++++- .../lib-ot/src/core/document/transaction.rs | 16 +++-- 4 files changed, 120 insertions(+), 34 deletions(-) diff --git a/shared-lib/lib-ot/src/core/document/document.rs b/shared-lib/lib-ot/src/core/document/document.rs index 063fe408b0..17cd1d7989 100644 --- a/shared-lib/lib-ot/src/core/document/document.rs +++ b/shared-lib/lib-ot/src/core/document/document.rs @@ -1,5 +1,7 @@ use crate::core::document::position::Position; -use crate::core::{DocumentOperation, NodeAttributes, NodeData, OperationTransform, TextDelta, Transaction}; +use crate::core::{ + DocumentOperation, NodeAttributes, NodeData, NodeSubTree, OperationTransform, TextDelta, Transaction, +}; use crate::errors::{ErrorBuilder, OTError, OTErrorCode}; use indextree::{Arena, NodeId}; @@ -100,40 +102,36 @@ impl DocumentTree { } } - fn apply_insert(&mut self, path: &Position, nodes: &Vec) -> Result<(), OTError> { + fn apply_insert(&mut self, path: &Position, nodes: &Vec) -> Result<(), OTError> { let parent_path = &path.0[0..(path.0.len() - 1)]; let last_index = path.0[path.0.len() - 1]; let parent_node = self .node_at_path(&Position(parent_path.to_vec())) .ok_or(ErrorBuilder::new(OTErrorCode::PathNotFound).build())?; - let mut inserted_nodes = Vec::new(); + // let mut inserted_nodes = Vec::new(); + // + // for node in nodes { + // inserted_nodes.push(self.arena.new_node(node.to_node_data())); + // } - for node in nodes { - inserted_nodes.push(self.arena.new_node(node.clone())); - } - - self.insert_child_at_index(parent_node, last_index, &inserted_nodes) + self.insert_child_at_index(parent_node, last_index, nodes.as_ref()) } fn insert_child_at_index( &mut self, parent: NodeId, index: usize, - insert_children: &[NodeId], + insert_children: &[NodeSubTree], ) -> Result<(), OTError> { if index == 0 && parent.children(&self.arena).next().is_none() { - for id in insert_children { - parent.append(*id, &mut self.arena); - } + self.append_subtree(&parent, insert_children); return Ok(()); } let children_length = parent.children(&self.arena).fold(0, |counter, _| counter + 1); if index == children_length { - for id in insert_children { - parent.append(*id, &mut self.arena); - } + self.append_subtree(&parent, insert_children); return Ok(()); } @@ -141,12 +139,24 @@ impl DocumentTree { .child_at_index_of_path(parent, index) .ok_or(ErrorBuilder::new(OTErrorCode::PathNotFound).build())?; - for id in insert_children { - node_to_insert.insert_before(*id, &mut self.arena); - } + self.insert_subtree_before(&node_to_insert, insert_children); Ok(()) } + fn append_subtree(&mut self, parent: &NodeId, insert_children: &[NodeSubTree]) { + for child in insert_children { + let child_id = self.arena.new_node(child.to_node_data()); + parent.append(child_id, &mut self.arena); + } + } + + fn insert_subtree_before(&mut self, before: &NodeId, insert_children: &[NodeSubTree]) { + for id in insert_children { + let child_id = self.arena.new_node(id.to_node_data()); + before.insert_before(child_id, &mut self.arena); + } + } + fn apply_update(&mut self, path: &Position, attributes: &NodeAttributes) -> Result<(), OTError> { let update_node = self .node_at_path(path) diff --git a/shared-lib/lib-ot/src/core/document/document_operation.rs b/shared-lib/lib-ot/src/core/document/document_operation.rs index 46cb79be4a..6a2e97e976 100644 --- a/shared-lib/lib-ot/src/core/document/document_operation.rs +++ b/shared-lib/lib-ot/src/core/document/document_operation.rs @@ -1,21 +1,21 @@ use crate::core::document::position::Position; -use crate::core::{NodeAttributes, NodeData, TextDelta}; +use crate::core::{NodeAttributes, NodeSubTree, TextDelta}; #[derive(Clone, serde::Serialize, serde::Deserialize)] +#[serde(tag = "type")] pub enum DocumentOperation { - Insert { - path: Position, - nodes: Vec, - }, + #[serde(rename = "insert-operation")] + Insert { path: Position, nodes: Vec }, + #[serde(rename = "update-operation")] Update { path: Position, attributes: NodeAttributes, + #[serde(rename = "oldAttributes")] old_attributes: NodeAttributes, }, - Delete { - path: Position, - nodes: Vec, - }, + #[serde(rename = "delete-operation")] + Delete { path: Position, nodes: Vec }, + #[serde(rename = "text-edit-operation")] TextEdit { path: Position, delta: TextDelta, @@ -101,7 +101,7 @@ impl DocumentOperation { #[cfg(test)] mod tests { - use crate::core::Position; + use crate::core::{Delta, DocumentOperation, NodeAttributes, NodeSubTree, Position}; #[test] fn test_transform_path_1() { @@ -150,4 +150,45 @@ mod tests { vec![0, 6] ); } + + #[test] + fn test_serialize_insert_operation() { + let insert = DocumentOperation::Insert { + path: Position(vec![0, 1]), + nodes: vec![NodeSubTree::new("text")], + }; + let result = serde_json::to_string(&insert).unwrap(); + assert_eq!( + result, + r#"{"type":"insert-operation","path":[0,1],"nodes":[{"node_type":"text","attributes":{}}]}"# + ); + } + + #[test] + fn test_serialize_update_operation() { + let insert = DocumentOperation::Update { + path: Position(vec![0, 1]), + attributes: NodeAttributes::new(), + old_attributes: NodeAttributes::new(), + }; + let result = serde_json::to_string(&insert).unwrap(); + assert_eq!( + result, + r#"{"type":"update-operation","path":[0,1],"attributes":{},"oldAttributes":{}}"# + ); + } + + #[test] + fn test_serialize_text_edit_operation() { + let insert = DocumentOperation::TextEdit { + path: Position(vec![0, 1]), + delta: Delta::new(), + inverted: Delta::new(), + }; + let result = serde_json::to_string(&insert).unwrap(); + assert_eq!( + result, + r#"{"type":"text-edit-operation","path":[0,1],"delta":[],"inverted":[]}"# + ); + } } diff --git a/shared-lib/lib-ot/src/core/document/node.rs b/shared-lib/lib-ot/src/core/document/node.rs index 96747b0fd8..1e298bc72e 100644 --- a/shared-lib/lib-ot/src/core/document/node.rs +++ b/shared-lib/lib-ot/src/core/document/node.rs @@ -1,6 +1,6 @@ use crate::core::{NodeAttributes, TextDelta}; -#[derive(Clone, serde::Serialize, serde::Deserialize)] +#[derive(Clone)] pub struct NodeData { pub node_type: String, pub attributes: NodeAttributes, @@ -16,3 +16,32 @@ impl NodeData { } } } + +#[derive(Clone, serde::Serialize, serde::Deserialize)] +pub struct NodeSubTree { + pub node_type: String, + pub attributes: NodeAttributes, + #[serde(skip_serializing_if = "Option::is_none")] + pub delta: Option, + #[serde(skip_serializing_if = "Vec::is_empty")] + pub children: Vec>, +} + +impl NodeSubTree { + pub fn new(node_type: &str) -> NodeSubTree { + NodeSubTree { + node_type: node_type.into(), + attributes: NodeAttributes::new(), + delta: None, + children: Vec::new(), + } + } + + pub fn to_node_data(&self) -> NodeData { + NodeData { + node_type: self.node_type.clone(), + attributes: self.attributes.clone(), + delta: self.delta.clone(), + } + } +} diff --git a/shared-lib/lib-ot/src/core/document/transaction.rs b/shared-lib/lib-ot/src/core/document/transaction.rs index 00697b7da1..b24a085eae 100644 --- a/shared-lib/lib-ot/src/core/document/transaction.rs +++ b/shared-lib/lib-ot/src/core/document/transaction.rs @@ -1,5 +1,5 @@ use crate::core::document::position::Position; -use crate::core::{DocumentOperation, DocumentTree, NodeAttributes, NodeData}; +use crate::core::{DocumentOperation, DocumentTree, NodeAttributes, NodeSubTree}; use std::collections::HashMap; pub struct Transaction { @@ -25,7 +25,7 @@ impl<'a> TransactionBuilder<'a> { } } - pub fn insert_nodes_at_path(&mut self, path: &Position, nodes: &[NodeData]) { + pub fn insert_nodes_at_path(&mut self, path: &Position, nodes: &[NodeSubTree]) { self.push(DocumentOperation::Insert { path: path.clone(), nodes: nodes.to_vec(), @@ -59,11 +59,17 @@ impl<'a> TransactionBuilder<'a> { pub fn delete_nodes_at_path(&mut self, path: &Position, length: usize) { let mut node = self.document.node_at_path(path).unwrap(); - let mut deleted_nodes: Vec = Vec::new(); + let mut deleted_nodes: Vec = Vec::new(); for _ in 0..length { - let data = self.document.arena.get(node).unwrap(); - deleted_nodes.push(data.get().clone()); + let node_data = self.document.arena.get(node).unwrap(); + let data = node_data.get(); + deleted_nodes.push(NodeSubTree { + node_type: data.node_type.clone(), + attributes: data.attributes.clone(), + delta: data.delta.clone(), + children: vec![], + }); node = node.following_siblings(&self.document.arena).next().unwrap(); } From 9d1475df2bfd9caf3e0de74415248a938438726c Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Tue, 23 Aug 2022 17:12:11 +0800 Subject: [PATCH 16/19] feat: recursive append children --- shared-lib/lib-ot/src/core/document/document.rs | 17 +++++++++++------ .../src/core/document/document_operation.rs | 6 +++--- .../lib-ot/src/core/document/transaction.rs | 8 ++++---- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/shared-lib/lib-ot/src/core/document/document.rs b/shared-lib/lib-ot/src/core/document/document.rs index 17cd1d7989..340f4fdc57 100644 --- a/shared-lib/lib-ot/src/core/document/document.rs +++ b/shared-lib/lib-ot/src/core/document/document.rs @@ -102,7 +102,7 @@ impl DocumentTree { } } - fn apply_insert(&mut self, path: &Position, nodes: &Vec) -> Result<(), OTError> { + fn apply_insert(&mut self, path: &Position, nodes: &[Box]) -> Result<(), OTError> { let parent_path = &path.0[0..(path.0.len() - 1)]; let last_index = path.0[path.0.len() - 1]; let parent_node = self @@ -121,7 +121,7 @@ impl DocumentTree { &mut self, parent: NodeId, index: usize, - insert_children: &[NodeSubTree], + insert_children: &[Box], ) -> Result<(), OTError> { if index == 0 && parent.children(&self.arena).next().is_none() { self.append_subtree(&parent, insert_children); @@ -143,17 +143,22 @@ impl DocumentTree { Ok(()) } - fn append_subtree(&mut self, parent: &NodeId, insert_children: &[NodeSubTree]) { + // recursive append the subtrees to the node + fn append_subtree(&mut self, parent: &NodeId, insert_children: &[Box]) { for child in insert_children { let child_id = self.arena.new_node(child.to_node_data()); parent.append(child_id, &mut self.arena); + + self.append_subtree(&child_id, child.children.as_ref()); } } - fn insert_subtree_before(&mut self, before: &NodeId, insert_children: &[NodeSubTree]) { - for id in insert_children { - let child_id = self.arena.new_node(id.to_node_data()); + fn insert_subtree_before(&mut self, before: &NodeId, insert_children: &[Box]) { + for child in insert_children { + let child_id = self.arena.new_node(child.to_node_data()); before.insert_before(child_id, &mut self.arena); + + self.append_subtree(&child_id, child.children.as_ref()); } } diff --git a/shared-lib/lib-ot/src/core/document/document_operation.rs b/shared-lib/lib-ot/src/core/document/document_operation.rs index 6a2e97e976..77c74127bd 100644 --- a/shared-lib/lib-ot/src/core/document/document_operation.rs +++ b/shared-lib/lib-ot/src/core/document/document_operation.rs @@ -5,7 +5,7 @@ use crate::core::{NodeAttributes, NodeSubTree, TextDelta}; #[serde(tag = "type")] pub enum DocumentOperation { #[serde(rename = "insert-operation")] - Insert { path: Position, nodes: Vec }, + Insert { path: Position, nodes: Vec> }, #[serde(rename = "update-operation")] Update { path: Position, @@ -14,7 +14,7 @@ pub enum DocumentOperation { old_attributes: NodeAttributes, }, #[serde(rename = "delete-operation")] - Delete { path: Position, nodes: Vec }, + Delete { path: Position, nodes: Vec> }, #[serde(rename = "text-edit-operation")] TextEdit { path: Position, @@ -155,7 +155,7 @@ mod tests { fn test_serialize_insert_operation() { let insert = DocumentOperation::Insert { path: Position(vec![0, 1]), - nodes: vec![NodeSubTree::new("text")], + nodes: vec![Box::new(NodeSubTree::new("text"))], }; let result = serde_json::to_string(&insert).unwrap(); assert_eq!( diff --git a/shared-lib/lib-ot/src/core/document/transaction.rs b/shared-lib/lib-ot/src/core/document/transaction.rs index b24a085eae..86448820bc 100644 --- a/shared-lib/lib-ot/src/core/document/transaction.rs +++ b/shared-lib/lib-ot/src/core/document/transaction.rs @@ -25,7 +25,7 @@ impl<'a> TransactionBuilder<'a> { } } - pub fn insert_nodes_at_path(&mut self, path: &Position, nodes: &[NodeSubTree]) { + pub fn insert_nodes_at_path(&mut self, path: &Position, nodes: &[Box]) { self.push(DocumentOperation::Insert { path: path.clone(), nodes: nodes.to_vec(), @@ -59,17 +59,17 @@ impl<'a> TransactionBuilder<'a> { pub fn delete_nodes_at_path(&mut self, path: &Position, length: usize) { let mut node = self.document.node_at_path(path).unwrap(); - let mut deleted_nodes: Vec = Vec::new(); + let mut deleted_nodes: Vec> = Vec::new(); for _ in 0..length { let node_data = self.document.arena.get(node).unwrap(); let data = node_data.get(); - deleted_nodes.push(NodeSubTree { + deleted_nodes.push(Box::new(NodeSubTree { node_type: data.node_type.clone(), attributes: data.attributes.clone(), delta: data.delta.clone(), children: vec![], - }); + })); node = node.following_siblings(&self.document.arena).next().unwrap(); } From d6ef13adaeb5400a0c7c3b4ea6618a6a350336f5 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Tue, 23 Aug 2022 17:58:54 +0800 Subject: [PATCH 17/19] feat: get deleted subtrees from the document --- .../lib-ot/src/core/document/document.rs | 9 ----- .../lib-ot/src/core/document/transaction.rs | 33 ++++++++++++++----- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/shared-lib/lib-ot/src/core/document/document.rs b/shared-lib/lib-ot/src/core/document/document.rs index 340f4fdc57..73ff03fe64 100644 --- a/shared-lib/lib-ot/src/core/document/document.rs +++ b/shared-lib/lib-ot/src/core/document/document.rs @@ -108,11 +108,6 @@ impl DocumentTree { let parent_node = self .node_at_path(&Position(parent_path.to_vec())) .ok_or(ErrorBuilder::new(OTErrorCode::PathNotFound).build())?; - // let mut inserted_nodes = Vec::new(); - // - // for node in nodes { - // inserted_nodes.push(self.arena.new_node(node.to_node_data())); - // } self.insert_child_at_index(parent_node, last_index, nodes.as_ref()) } @@ -167,10 +162,6 @@ impl DocumentTree { .node_at_path(path) .ok_or(ErrorBuilder::new(OTErrorCode::PathNotFound).build())?; let node_data = self.arena.get_mut(update_node).unwrap(); - // let new_node = NodeData { - // ..node_data.get().clone() - // attributes: - // }; let new_node = { let old_attributes = &node_data.get().attributes; let new_attributes = NodeAttributes::compose(&old_attributes, attributes); diff --git a/shared-lib/lib-ot/src/core/document/transaction.rs b/shared-lib/lib-ot/src/core/document/transaction.rs index 86448820bc..e22ba21c3a 100644 --- a/shared-lib/lib-ot/src/core/document/transaction.rs +++ b/shared-lib/lib-ot/src/core/document/transaction.rs @@ -1,6 +1,7 @@ use crate::core::document::position::Position; use crate::core::{DocumentOperation, DocumentTree, NodeAttributes, NodeSubTree}; use std::collections::HashMap; +use indextree::NodeId; pub struct Transaction { pub operations: Vec, @@ -62,14 +63,7 @@ impl<'a> TransactionBuilder<'a> { let mut deleted_nodes: Vec> = Vec::new(); for _ in 0..length { - let node_data = self.document.arena.get(node).unwrap(); - let data = node_data.get(); - deleted_nodes.push(Box::new(NodeSubTree { - node_type: data.node_type.clone(), - attributes: data.attributes.clone(), - delta: data.delta.clone(), - children: vec![], - })); + deleted_nodes.push(self.get_deleted_nodes(node.clone())); node = node.following_siblings(&self.document.arena).next().unwrap(); } @@ -79,6 +73,29 @@ impl<'a> TransactionBuilder<'a> { }) } + fn get_deleted_nodes(&self, node_id: NodeId) -> Box { + let node = self.document.arena.get(node_id.clone()).unwrap(); + let node_data = node.get(); + let mut children: Vec> = vec![]; + + let mut children_iterators = node_id.children(&self.document.arena); + loop { + let next_child = children_iterators.next(); + if let Some(child_id) = next_child { + children.push(self.get_deleted_nodes(child_id)); + } else { + break; + } + }; + + Box::new(NodeSubTree { + node_type: node_data.node_type.clone(), + attributes: node_data.attributes.clone(), + delta: node_data.delta.clone(), + children, + }) + } + pub fn push(&mut self, op: DocumentOperation) { self.operations.push(op); } From 096544d6a314e08e10606935d8d260d1e6fc03ba Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Tue, 23 Aug 2022 19:39:00 +0800 Subject: [PATCH 18/19] feat: test insert sub trees --- .../src/core/document/document_operation.rs | 30 +++++++++-- shared-lib/lib-ot/src/core/document/node.rs | 1 + .../lib-ot/src/core/document/transaction.rs | 4 +- shared-lib/lib-ot/tests/main.rs | 51 ++++++++++++++----- 4 files changed, 67 insertions(+), 19 deletions(-) diff --git a/shared-lib/lib-ot/src/core/document/document_operation.rs b/shared-lib/lib-ot/src/core/document/document_operation.rs index 77c74127bd..caca29e110 100644 --- a/shared-lib/lib-ot/src/core/document/document_operation.rs +++ b/shared-lib/lib-ot/src/core/document/document_operation.rs @@ -5,7 +5,10 @@ use crate::core::{NodeAttributes, NodeSubTree, TextDelta}; #[serde(tag = "type")] pub enum DocumentOperation { #[serde(rename = "insert-operation")] - Insert { path: Position, nodes: Vec> }, + Insert { + path: Position, + nodes: Vec>, + }, #[serde(rename = "update-operation")] Update { path: Position, @@ -14,7 +17,10 @@ pub enum DocumentOperation { old_attributes: NodeAttributes, }, #[serde(rename = "delete-operation")] - Delete { path: Position, nodes: Vec> }, + Delete { + path: Position, + nodes: Vec>, + }, #[serde(rename = "text-edit-operation")] TextEdit { path: Position, @@ -160,7 +166,25 @@ mod tests { let result = serde_json::to_string(&insert).unwrap(); assert_eq!( result, - r#"{"type":"insert-operation","path":[0,1],"nodes":[{"node_type":"text","attributes":{}}]}"# + r#"{"type":"insert-operation","path":[0,1],"nodes":[{"type":"text","attributes":{}}]}"# + ); + } + + #[test] + fn test_serialize_insert_sub_trees() { + let insert = DocumentOperation::Insert { + path: Position(vec![0, 1]), + nodes: vec![Box::new(NodeSubTree { + node_type: "text".into(), + attributes: NodeAttributes::new(), + delta: None, + children: vec![Box::new(NodeSubTree::new("text".into()))], + })], + }; + let result = serde_json::to_string(&insert).unwrap(); + assert_eq!( + result, + r#"{"type":"insert-operation","path":[0,1],"nodes":[{"type":"text","attributes":{},"children":[{"type":"text","attributes":{}}]}]}"# ); } diff --git a/shared-lib/lib-ot/src/core/document/node.rs b/shared-lib/lib-ot/src/core/document/node.rs index 1e298bc72e..e74c7d4918 100644 --- a/shared-lib/lib-ot/src/core/document/node.rs +++ b/shared-lib/lib-ot/src/core/document/node.rs @@ -19,6 +19,7 @@ impl NodeData { #[derive(Clone, serde::Serialize, serde::Deserialize)] pub struct NodeSubTree { + #[serde(rename = "type")] pub node_type: String, pub attributes: NodeAttributes, #[serde(skip_serializing_if = "Option::is_none")] diff --git a/shared-lib/lib-ot/src/core/document/transaction.rs b/shared-lib/lib-ot/src/core/document/transaction.rs index e22ba21c3a..73fce7d8ad 100644 --- a/shared-lib/lib-ot/src/core/document/transaction.rs +++ b/shared-lib/lib-ot/src/core/document/transaction.rs @@ -1,7 +1,7 @@ use crate::core::document::position::Position; use crate::core::{DocumentOperation, DocumentTree, NodeAttributes, NodeSubTree}; -use std::collections::HashMap; use indextree::NodeId; +use std::collections::HashMap; pub struct Transaction { pub operations: Vec, @@ -86,7 +86,7 @@ impl<'a> TransactionBuilder<'a> { } else { break; } - }; + } Box::new(NodeSubTree { node_type: node_data.node_type.clone(), diff --git a/shared-lib/lib-ot/tests/main.rs b/shared-lib/lib-ot/tests/main.rs index 7daf74e957..31e7748b3a 100644 --- a/shared-lib/lib-ot/tests/main.rs +++ b/shared-lib/lib-ot/tests/main.rs @@ -1,4 +1,4 @@ -use lib_ot::core::{DocumentTree, NodeData, Position, TransactionBuilder}; +use lib_ot::core::{DocumentTree, NodeAttributes, NodeSubTree, Position, TransactionBuilder}; use lib_ot::errors::OTErrorCode; use std::collections::HashMap; @@ -13,7 +13,7 @@ fn test_documents() { let mut document = DocumentTree::new(); let transaction = { let mut tb = TransactionBuilder::new(&document); - tb.insert_nodes_at_path(&vec![0].into(), &vec![NodeData::new("text")]); + tb.insert_nodes_at_path(&vec![0].into(), &vec![Box::new(NodeSubTree::new("text"))]); tb.finalize() }; document.apply(transaction).unwrap(); @@ -47,29 +47,52 @@ fn test_inserts_nodes() { let mut document = DocumentTree::new(); let transaction = { let mut tb = TransactionBuilder::new(&document); - tb.insert_nodes_at_path(&vec![0].into(), &vec![NodeData::new("text")]); - tb.insert_nodes_at_path(&vec![1].into(), &vec![NodeData::new("text")]); - tb.insert_nodes_at_path(&vec![2].into(), &vec![NodeData::new("text")]); + tb.insert_nodes_at_path(&vec![0].into(), &vec![Box::new(NodeSubTree::new("text"))]); + tb.insert_nodes_at_path(&vec![1].into(), &vec![Box::new(NodeSubTree::new("text"))]); + tb.insert_nodes_at_path(&vec![2].into(), &vec![Box::new(NodeSubTree::new("text"))]); tb.finalize() }; document.apply(transaction).unwrap(); let transaction = { let mut tb = TransactionBuilder::new(&document); - tb.insert_nodes_at_path(&vec![1].into(), &vec![NodeData::new("text")]); + tb.insert_nodes_at_path(&vec![1].into(), &vec![Box::new(NodeSubTree::new("text"))]); tb.finalize() }; document.apply(transaction).unwrap(); } +#[test] +fn test_inserts_subtrees() { + let mut document = DocumentTree::new(); + let transaction = { + let mut tb = TransactionBuilder::new(&document); + tb.insert_nodes_at_path( + &vec![0].into(), + &vec![Box::new(NodeSubTree { + node_type: "text".into(), + attributes: NodeAttributes::new(), + delta: None, + children: vec![Box::new(NodeSubTree::new("image".into()))], + })], + ); + tb.finalize() + }; + document.apply(transaction).unwrap(); + + let node = document.node_at_path(&Position(vec![0, 0])).unwrap(); + let data = document.arena.get(node).unwrap().get(); + assert_eq!(data.node_type, "image"); +} + #[test] fn test_update_nodes() { let mut document = DocumentTree::new(); let transaction = { let mut tb = TransactionBuilder::new(&document); - tb.insert_nodes_at_path(&vec![0].into(), &vec![NodeData::new("text")]); - tb.insert_nodes_at_path(&vec![1].into(), &vec![NodeData::new("text")]); - tb.insert_nodes_at_path(&vec![2].into(), &vec![NodeData::new("text")]); + tb.insert_nodes_at_path(&vec![0].into(), &vec![Box::new(NodeSubTree::new("text"))]); + tb.insert_nodes_at_path(&vec![1].into(), &vec![Box::new(NodeSubTree::new("text"))]); + tb.insert_nodes_at_path(&vec![2].into(), &vec![Box::new(NodeSubTree::new("text"))]); tb.finalize() }; document.apply(transaction).unwrap(); @@ -92,9 +115,9 @@ fn test_delete_nodes() { let mut document = DocumentTree::new(); let transaction = { let mut tb = TransactionBuilder::new(&document); - tb.insert_nodes_at_path(&vec![0].into(), &vec![NodeData::new("text")]); - tb.insert_nodes_at_path(&vec![1].into(), &vec![NodeData::new("text")]); - tb.insert_nodes_at_path(&vec![2].into(), &vec![NodeData::new("text")]); + tb.insert_nodes_at_path(&vec![0].into(), &vec![Box::new(NodeSubTree::new("text"))]); + tb.insert_nodes_at_path(&vec![1].into(), &vec![Box::new(NodeSubTree::new("text"))]); + tb.insert_nodes_at_path(&vec![2].into(), &vec![Box::new(NodeSubTree::new("text"))]); tb.finalize() }; document.apply(transaction).unwrap(); @@ -115,8 +138,8 @@ fn test_errors() { let mut document = DocumentTree::new(); let transaction = { let mut tb = TransactionBuilder::new(&document); - tb.insert_nodes_at_path(&vec![0].into(), &vec![NodeData::new("text")]); - tb.insert_nodes_at_path(&vec![100].into(), &vec![NodeData::new("text")]); + tb.insert_nodes_at_path(&vec![0].into(), &vec![Box::new(NodeSubTree::new("text"))]); + tb.insert_nodes_at_path(&vec![100].into(), &vec![Box::new(NodeSubTree::new("text"))]); tb.finalize() }; let result = document.apply(transaction); From a309a9c82cc2ed04059071edd6f65b710d9a1157 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Wed, 24 Aug 2022 11:50:06 +0800 Subject: [PATCH 19/19] fix: minor issues --- .../lib/src/operation/operation.dart | 18 ++++++++-------- .../test/legacy/operation_test.dart | 4 ++-- .../src/core/document/document_operation.rs | 21 ++++++++----------- .../lib-ot/src/core/document/position.rs | 4 ++-- 4 files changed, 22 insertions(+), 25 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/operation.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/operation.dart index 456be06766..af2ec831d4 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/operation.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/operation.dart @@ -2,14 +2,14 @@ import 'package:appflowy_editor/appflowy_editor.dart'; abstract class Operation { factory Operation.fromJson(Map map) { - String t = map["type"] as String; - if (t == "insert-operation") { + String t = map["op"] as String; + if (t == "insert") { return InsertOperation.fromJson(map); - } else if (t == "update-operation") { + } else if (t == "update") { return UpdateOperation.fromJson(map); - } else if (t == "delete-operation") { + } else if (t == "delete") { return DeleteOperation.fromJson(map); - } else if (t == "text-edit-operation") { + } else if (t == "text-edit") { return TextEditOperation.fromJson(map); } @@ -51,7 +51,7 @@ class InsertOperation extends Operation { @override Map toJson() { return { - "type": "insert-operation", + "op": "insert", "path": path.toList(), "nodes": nodes.map((n) => n.toJson()), }; @@ -95,7 +95,7 @@ class UpdateOperation extends Operation { @override Map toJson() { return { - "type": "update-operation", + "op": "update", "path": path.toList(), "attributes": {...attributes}, "oldAttributes": {...oldAttributes}, @@ -132,7 +132,7 @@ class DeleteOperation extends Operation { @override Map toJson() { return { - "type": "delete-operation", + "op": "delete", "path": path.toList(), "nodes": nodes.map((n) => n.toJson()), }; @@ -171,7 +171,7 @@ class TextEditOperation extends Operation { @override Map toJson() { return { - "type": "text-edit-operation", + "op": "text-edit", "path": path.toList(), "delta": delta.toJson(), "invert": inverted.toJson(), diff --git a/frontend/app_flowy/packages/appflowy_editor/test/legacy/operation_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/legacy/operation_test.dart index 1f44ebfd3c..6c20ebd134 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/legacy/operation_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/legacy/operation_test.dart @@ -84,7 +84,7 @@ void main() { expect(transaction.toJson(), { "operations": [ { - "type": "insert-operation", + "op": "insert", "path": [0], "nodes": [item1.toJson()], } @@ -107,7 +107,7 @@ void main() { expect(transaction.toJson(), { "operations": [ { - "type": "delete-operation", + "op": "delete", "path": [0], "nodes": [item1.toJson()], } diff --git a/shared-lib/lib-ot/src/core/document/document_operation.rs b/shared-lib/lib-ot/src/core/document/document_operation.rs index caca29e110..4d9d3617eb 100644 --- a/shared-lib/lib-ot/src/core/document/document_operation.rs +++ b/shared-lib/lib-ot/src/core/document/document_operation.rs @@ -2,26 +2,26 @@ use crate::core::document::position::Position; use crate::core::{NodeAttributes, NodeSubTree, TextDelta}; #[derive(Clone, serde::Serialize, serde::Deserialize)] -#[serde(tag = "type")] +#[serde(tag = "op")] pub enum DocumentOperation { - #[serde(rename = "insert-operation")] + #[serde(rename = "insert")] Insert { path: Position, nodes: Vec>, }, - #[serde(rename = "update-operation")] + #[serde(rename = "update")] Update { path: Position, attributes: NodeAttributes, #[serde(rename = "oldAttributes")] old_attributes: NodeAttributes, }, - #[serde(rename = "delete-operation")] + #[serde(rename = "delete")] Delete { path: Position, nodes: Vec>, }, - #[serde(rename = "text-edit-operation")] + #[serde(rename = "text-edit")] TextEdit { path: Position, delta: TextDelta, @@ -166,7 +166,7 @@ mod tests { let result = serde_json::to_string(&insert).unwrap(); assert_eq!( result, - r#"{"type":"insert-operation","path":[0,1],"nodes":[{"type":"text","attributes":{}}]}"# + r#"{"op":"insert","path":[0,1],"nodes":[{"type":"text","attributes":{}}]}"# ); } @@ -184,7 +184,7 @@ mod tests { let result = serde_json::to_string(&insert).unwrap(); assert_eq!( result, - r#"{"type":"insert-operation","path":[0,1],"nodes":[{"type":"text","attributes":{},"children":[{"type":"text","attributes":{}}]}]}"# + r#"{"op":"insert","path":[0,1],"nodes":[{"type":"text","attributes":{},"children":[{"type":"text","attributes":{}}]}]}"# ); } @@ -198,7 +198,7 @@ mod tests { let result = serde_json::to_string(&insert).unwrap(); assert_eq!( result, - r#"{"type":"update-operation","path":[0,1],"attributes":{},"oldAttributes":{}}"# + r#"{"op":"update","path":[0,1],"attributes":{},"oldAttributes":{}}"# ); } @@ -210,9 +210,6 @@ mod tests { inverted: Delta::new(), }; let result = serde_json::to_string(&insert).unwrap(); - assert_eq!( - result, - r#"{"type":"text-edit-operation","path":[0,1],"delta":[],"inverted":[]}"# - ); + assert_eq!(result, r#"{"op":"text-edit","path":[0,1],"delta":[],"inverted":[]}"#); } } diff --git a/shared-lib/lib-ot/src/core/document/position.rs b/shared-lib/lib-ot/src/core/document/position.rs index 521d0e9a60..b98edd97f4 100644 --- a/shared-lib/lib-ot/src/core/document/position.rs +++ b/shared-lib/lib-ot/src/core/document/position.rs @@ -12,7 +12,7 @@ impl Position { impl Position { // delta is default to be 1 - pub fn transform(pre_insert_path: &Position, b: &Position, delta: i64) -> Position { + pub fn transform(pre_insert_path: &Position, b: &Position, offset: i64) -> Position { if pre_insert_path.len() > b.len() { return b.clone(); } @@ -30,7 +30,7 @@ impl Position { let prev_insert_last: usize = *pre_insert_path.0.last().unwrap(); let b_at_index = b.0[pre_insert_path.0.len() - 1]; if prev_insert_last <= b_at_index { - prefix.push(((b_at_index as i64) + delta) as usize); + prefix.push(((b_at_index as i64) + offset) as usize); } else { prefix.push(b_at_index); }