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..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(), @@ -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/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/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/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..52b33dce08 --- /dev/null +++ b/shared-lib/lib-ot/src/core/document/attributes.rs @@ -0,0 +1,22 @@ +use std::collections::HashMap; + +#[derive(Clone, serde::Serialize, serde::Deserialize)] +pub struct NodeAttributes(pub HashMap>); + +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 new file mode 100644 index 0000000000..73ff03fe64 --- /dev/null +++ b/shared-lib/lib-ot/src/core/document/document.rs @@ -0,0 +1,211 @@ +use crate::core::document::position::Position; +use crate::core::{ + DocumentOperation, NodeAttributes, NodeData, NodeSubTree, OperationTransform, TextDelta, Transaction, +}; +use crate::errors::{ErrorBuilder, OTError, OTErrorCode}; +use indextree::{Arena, NodeId}; + +pub struct DocumentTree { + 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, root } + } + + pub fn node_at_path(&self, position: &Position) -> Option { + if position.is_empty() { + return Some(self.root); + } + + 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) + } + + 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); + + let mut counter = 0; + for child in children { + if counter == index { + return Some(child); + } + + counter += 1; + } + + None + } + + pub fn apply(&mut self, transaction: Transaction) -> Result<(), OTError> { + for op in &transaction.operations { + self.apply_op(op)?; + } + Ok(()) + } + + fn apply_op(&mut self, op: &DocumentOperation) -> Result<(), OTError> { + match 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, 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 + .node_at_path(&Position(parent_path.to_vec())) + .ok_or(ErrorBuilder::new(OTErrorCode::PathNotFound).build())?; + + 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: &[Box], + ) -> Result<(), OTError> { + if index == 0 && parent.children(&self.arena).next().is_none() { + self.append_subtree(&parent, insert_children); + return Ok(()); + } + + let children_length = parent.children(&self.arena).fold(0, |counter, _| counter + 1); + + if index == children_length { + self.append_subtree(&parent, insert_children); + return Ok(()); + } + + let node_to_insert = self + .child_at_index_of_path(parent, index) + .ok_or(ErrorBuilder::new(OTErrorCode::PathNotFound).build())?; + + self.insert_subtree_before(&node_to_insert, insert_children); + Ok(()) + } + + // 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: &[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()); + } + } + + 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 = { + 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_mut() = new_node; + Ok(()) + } + + 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(&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 new file mode 100644 index 0000000000..4d9d3617eb --- /dev/null +++ b/shared-lib/lib-ot/src/core/document/document_operation.rs @@ -0,0 +1,215 @@ +use crate::core::document::position::Position; +use crate::core::{NodeAttributes, NodeSubTree, TextDelta}; + +#[derive(Clone, serde::Serialize, serde::Deserialize)] +#[serde(tag = "op")] +pub enum DocumentOperation { + #[serde(rename = "insert")] + Insert { + path: Position, + nodes: Vec>, + }, + #[serde(rename = "update")] + Update { + path: Position, + attributes: NodeAttributes, + #[serde(rename = "oldAttributes")] + old_attributes: NodeAttributes, + }, + #[serde(rename = "delete")] + Delete { + path: Position, + nodes: Vec>, + }, + #[serde(rename = "text-edit")] + TextEdit { + path: Position, + delta: TextDelta, + inverted: TextDelta, + }, +} + +impl DocumentOperation { + pub fn path(&self) -> &Position { + match self { + 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 { 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 { nodes, .. } => DocumentOperation::Insert { + path, + nodes: nodes.clone(), + }, + DocumentOperation::Update { + attributes, + old_attributes, + .. + } => DocumentOperation::Update { + path, + attributes: attributes.clone(), + old_attributes: old_attributes.clone(), + }, + DocumentOperation::Delete { nodes, .. } => DocumentOperation::Delete { + path, + nodes: nodes.clone(), + }, + DocumentOperation::TextEdit { delta, inverted, .. } => DocumentOperation::TextEdit { + path, + delta: delta.clone(), + inverted: inverted.clone(), + }, + } + } + pub fn transform(a: &DocumentOperation, b: &DocumentOperation) -> DocumentOperation { + match a { + 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 { 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(), + } + } +} + +#[cfg(test)] +mod tests { + use crate::core::{Delta, DocumentOperation, NodeAttributes, NodeSubTree, 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] + ); + } + + #[test] + fn test_serialize_insert_operation() { + let insert = DocumentOperation::Insert { + path: Position(vec![0, 1]), + nodes: vec![Box::new(NodeSubTree::new("text"))], + }; + let result = serde_json::to_string(&insert).unwrap(); + assert_eq!( + result, + r#"{"op":"insert","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#"{"op":"insert","path":[0,1],"nodes":[{"type":"text","attributes":{},"children":[{"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#"{"op":"update","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#"{"op":"text-edit","path":[0,1],"delta":[],"inverted":[]}"#); + } +} 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..b019cb0f71 --- /dev/null +++ b/shared-lib/lib-ot/src/core/document/mod.rs @@ -0,0 +1,13 @@ +mod attributes; +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 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 new file mode 100644 index 0000000000..e74c7d4918 --- /dev/null +++ b/shared-lib/lib-ot/src/core/document/node.rs @@ -0,0 +1,48 @@ +use crate::core::{NodeAttributes, TextDelta}; + +#[derive(Clone)] +pub struct NodeData { + pub node_type: String, + pub attributes: NodeAttributes, + pub delta: Option, +} + +impl NodeData { + pub fn new(node_type: &str) -> NodeData { + NodeData { + node_type: node_type.into(), + attributes: NodeAttributes::new(), + delta: None, + } + } +} + +#[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")] + 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/position.rs b/shared-lib/lib-ot/src/core/document/position.rs new file mode 100644 index 0000000000..b98edd97f4 --- /dev/null +++ b/shared-lib/lib-ot/src/core/document/position.rs @@ -0,0 +1,46 @@ +#[derive(Clone, serde::Serialize, serde::Deserialize)] +pub struct Position(pub Vec); + +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, offset: i64) -> 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() - 1) { + 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 as i64) + offset) as usize); + } else { + prefix.push(b_at_index); + } + prefix.append(&mut suffix); + return Position(prefix); + } +} + +impl From> for Position { + fn from(v: Vec) -> Self { + Position(v) + } +} 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..73fce7d8ad --- /dev/null +++ b/shared-lib/lib-ot/src/core/document/transaction.rs @@ -0,0 +1,106 @@ +use crate::core::document::position::Position; +use crate::core::{DocumentOperation, DocumentTree, NodeAttributes, NodeSubTree}; +use indextree::NodeId; +use std::collections::HashMap; + +pub struct Transaction { + pub operations: Vec, +} + +impl Transaction { + fn new(operations: Vec) -> Transaction { + Transaction { operations } + } +} + +pub struct TransactionBuilder<'a> { + document: &'a DocumentTree, + operations: Vec, +} + +impl<'a> TransactionBuilder<'a> { + pub fn new(document: &'a DocumentTree) -> TransactionBuilder { + TransactionBuilder { + document, + operations: Vec::new(), + } + } + + pub fn insert_nodes_at_path(&mut self, path: &Position, nodes: &[Box]) { + self.push(DocumentOperation::Insert { + path: path.clone(), + nodes: nodes.to_vec(), + }); + } + + 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(); + + for key in attributes.keys() { + let old_attrs = &node_data.attributes; + 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 { + path: path.clone(), + attributes: NodeAttributes(attributes), + old_attributes: NodeAttributes(old_attributes), + }) + } + + pub fn delete_node_at_path(&mut self, path: &Position) { + self.delete_nodes_at_path(path, 1); + } + + 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(); + + for _ in 0..length { + deleted_nodes.push(self.get_deleted_nodes(node.clone())); + node = node.following_siblings(&self.document.arena).next().unwrap(); + } + + self.operations.push(DocumentOperation::Delete { + path: path.clone(), + nodes: deleted_nodes, + }) + } + + 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); + } + + pub fn finalize(self) -> Transaction { + Transaction::new(self.operations) + } +} 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/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 8b13789179..31e7748b3a 100644 --- a/shared-lib/lib-ot/tests/main.rs +++ b/shared-lib/lib-ot/tests/main.rs @@ -1 +1,147 @@ +use lib_ot::core::{DocumentTree, NodeAttributes, NodeSubTree, Position, TransactionBuilder}; +use lib_ot::errors::OTErrorCode; +use std::collections::HashMap; +#[test] +fn main() { + // Create a new arena + let _document = DocumentTree::new(); +} + +#[test] +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![Box::new(NodeSubTree::new("text"))]); + tb.finalize() + }; + document.apply(transaction).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 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).unwrap(); + + let transaction = { + let mut tb = TransactionBuilder::new(&document); + tb.delete_node_at_path(&vec![0].into()); + tb.finalize() + }; + document.apply(transaction).unwrap(); + assert!(document.node_at_path(&vec![0].into()).is_none()); +} + +#[test] +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![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![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![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.update_attributes_at_path(&vec![1].into(), HashMap::from([("bolded".into(), Some("true".into()))])); + tb.finalize() + }; + 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.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![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.delete_node_at_path(&Position(vec![1])); + tb.finalize() + }; + 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![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); + assert_eq!(result.err().unwrap().code, OTErrorCode::PathNotFound); +}