Feat/op compose (#1392)

This commit is contained in:
Nathan.fooo 2022-10-29 20:54:11 +08:00 committed by GitHub
parent 95fdfd7da2
commit 783fd40f63
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 1340 additions and 690 deletions

View File

@ -2,9 +2,7 @@ use bytes::Bytes;
use flowy_error::{FlowyError, FlowyResult};
use flowy_revision::{RevisionCompress, RevisionObjectDeserializer, RevisionObjectSerializer};
use flowy_sync::entities::revision::Revision;
use lib_ot::core::{
Body, Extension, NodeDataBuilder, NodeOperation, NodeTree, NodeTreeContext, Selection, Transaction,
};
use lib_ot::core::{Extension, NodeDataBuilder, NodeOperation, NodeTree, NodeTreeContext, Selection, Transaction};
use lib_ot::text_delta::DeltaTextOperationBuilder;
#[derive(Debug)]
@ -46,7 +44,7 @@ pub(crate) fn make_tree_context() -> NodeTreeContext {
pub fn initial_document_content() -> String {
let delta = DeltaTextOperationBuilder::new().insert("").build();
let node_data = NodeDataBuilder::new("text").insert_body(Body::Delta(delta)).build();
let node_data = NodeDataBuilder::new("text").insert_delta(delta).build();
let editor_node = NodeDataBuilder::new("editor").add_node_data(node_data).build();
let node_operation = NodeOperation::Insert {
path: vec![0].into(),

View File

@ -117,7 +117,8 @@ impl DocumentTransaction {
impl std::convert::From<Transaction> for DocumentTransaction {
fn from(transaction: Transaction) -> Self {
let (before_selection, after_selection) = match transaction.extension {
let (operations, extension) = transaction.split();
let (before_selection, after_selection) = match extension {
Extension::Empty => (Selection::default(), Selection::default()),
Extension::TextSelection {
before_selection,
@ -126,9 +127,7 @@ impl std::convert::From<Transaction> for DocumentTransaction {
};
DocumentTransaction {
operations: transaction
.operations
.into_inner()
operations: operations
.into_iter()
.map(|operation| operation.as_ref().into())
.collect(),
@ -139,19 +138,16 @@ impl std::convert::From<Transaction> for DocumentTransaction {
}
impl std::convert::From<DocumentTransaction> for Transaction {
fn from(transaction: DocumentTransaction) -> Self {
Transaction {
operations: transaction
.operations
.into_iter()
.map(|operation| operation.into())
.collect::<Vec<NodeOperation>>()
.into(),
extension: Extension::TextSelection {
before_selection: transaction.before_selection,
after_selection: transaction.after_selection,
},
fn from(document_transaction: DocumentTransaction) -> Self {
let mut transaction = Transaction::new();
for document_operation in document_transaction.operations {
transaction.push_operation(document_operation);
}
transaction.extension = Extension::TextSelection {
before_selection: document_transaction.before_selection,
after_selection: document_transaction.after_selection,
};
transaction
}
}
@ -374,6 +370,17 @@ mod tests {
let _ = serde_json::to_string_pretty(&document).unwrap();
}
// #[test]
// fn document_operation_compose_test() {
// let json = include_str!("./test.json");
// let transaction: Transaction = Transaction::from_json(json).unwrap();
// let json = transaction.to_json().unwrap();
// // let transaction: Transaction = Transaction::from_json(&json).unwrap();
// let document = Document::from_transaction(transaction).unwrap();
// let content = document.get_content(false).unwrap();
// println!("{}", json);
// }
const EXAMPLE_DOCUMENT: &str = r#"{
"document": {
"type": "editor",

View File

@ -89,7 +89,7 @@ fn delta_deserialize_null_test() {
let delta1 = DeltaTextOperations::from_json(json).unwrap();
let mut attribute = BuildInTextAttribute::Bold(true);
attribute.remove_value();
attribute.clear();
let delta2 = DeltaOperationBuilder::new()
.retain_with_attributes(7, attribute.into())

View File

@ -3,7 +3,7 @@ use flowy_document::editor::{AppFlowyDocumentEditor, Document, DocumentTransacti
use flowy_document::entities::DocumentVersionPB;
use flowy_test::helper::ViewTest;
use flowy_test::FlowySDKTest;
use lib_ot::core::{Body, Changeset, NodeDataBuilder, NodeOperation, Path, Transaction};
use lib_ot::core::{Changeset, NodeDataBuilder, NodeOperation, Path, Transaction};
use lib_ot::text_delta::DeltaTextOperations;
use std::sync::Arc;
@ -64,7 +64,7 @@ impl DocumentEditorTest {
async fn run_script(&self, script: EditScript) {
match script {
EditScript::InsertText { path, delta } => {
let node_data = NodeDataBuilder::new("text").insert_body(Body::Delta(delta)).build();
let node_data = NodeDataBuilder::new("text").insert_delta(delta).build();
let operation = NodeOperation::Insert {
path,
nodes: vec![node_data],

View File

@ -119,7 +119,6 @@ fn read_workspaces_on_server(
let workspace_revs = server.read_workspace(&token, params).await?;
let _ = persistence
.begin_transaction(|transaction| {
tracing::trace!("Save {} workspace", workspace_revs.len());
for workspace_rev in &workspace_revs {
let m_workspace = workspace_rev.clone();
let app_revs = m_workspace.apps.clone();

View File

@ -12,7 +12,14 @@ pub struct AttributeEntry {
}
impl AttributeEntry {
pub fn remove_value(&mut self) {
pub fn new<K: Into<AttributeKey>, V: Into<AttributeValue>>(key: K, value: V) -> Self {
Self {
key: key.into(),
value: value.into(),
}
}
pub fn clear(&mut self) {
self.value.ty = None;
self.value.value = None;
}

View File

@ -3,6 +3,7 @@
mod node;
mod node_serde;
mod operation;
mod operation_serde;
mod path;
mod transaction;
mod transaction_serde;

View File

@ -1,7 +1,7 @@
use super::node_serde::*;
use crate::core::attributes::{AttributeHashMap, AttributeKey, AttributeValue};
use crate::core::Body::Delta;
use crate::core::OperationTransform;
use crate::core::{AttributeEntry, OperationTransform};
use crate::errors::OTError;
use crate::text_delta::DeltaTextOperations;
use serde::{Deserialize, Serialize};
@ -69,14 +69,18 @@ impl NodeDataBuilder {
/// Inserts attributes to the builder's node.
///
/// The attributes will be replace if they shared the same key
pub fn insert_attribute(mut self, key: AttributeKey, value: AttributeValue) -> Self {
self.node.attributes.insert(key, value);
pub fn insert_attribute<K: Into<AttributeKey>, V: Into<AttributeValue>>(mut self, key: K, value: V) -> Self {
self.node.attributes.insert(key.into(), value);
self
}
/// Inserts a body to the builder's node
pub fn insert_body(mut self, body: Body) -> Self {
self.node.body = body;
pub fn insert_attribute_entry(mut self, entry: AttributeEntry) -> Self {
self.node.attributes.insert_entry(entry);
self
}
pub fn insert_delta(mut self, delta: DeltaTextOperations) -> Self {
self.node.body = Body::Delta(delta);
self
}
@ -174,6 +178,18 @@ pub enum Changeset {
}
impl Changeset {
pub fn is_delta(&self) -> bool {
match self {
Changeset::Delta { .. } => true,
Changeset::Attributes { .. } => false,
}
}
pub fn is_attribute(&self) -> bool {
match self {
Changeset::Delta { .. } => false,
Changeset::Attributes { .. } => true,
}
}
pub fn inverted(&self) -> Changeset {
match self {
Changeset::Delta { delta, inverted } => Changeset::Delta {
@ -186,6 +202,41 @@ impl Changeset {
},
}
}
pub fn compose(&mut self, other: &Changeset) -> Result<(), OTError> {
match (self, other) {
(
Changeset::Delta { delta, inverted },
Changeset::Delta {
delta: other_delta,
inverted: _,
},
) => {
let original = delta.invert(inverted);
let new_delta = delta.compose(other_delta)?;
let new_inverted = new_delta.invert(&original);
*delta = new_delta;
*inverted = new_inverted;
Ok(())
}
(
Changeset::Attributes { new, old },
Changeset::Attributes {
new: other_new,
old: other_old,
},
) => {
*new = other_new.clone();
*old = other_old.clone();
Ok(())
}
(left, right) => {
let err = format!("Compose changeset failed. {:?} can't compose {:?}", left, right);
Err(OTError::compose().context(err))
}
}
}
}
/// [`Node`] represents as a leaf in the [`NodeTree`].

View File

@ -1,5 +1,6 @@
use crate::core::{Changeset, NodeData, Path};
use crate::core::{Body, Changeset, NodeData, OperationTransform, Path};
use crate::errors::OTError;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
@ -33,7 +34,80 @@ impl NodeOperation {
}
}
pub fn invert(&self) -> NodeOperation {
pub fn is_update_delta(&self) -> bool {
match self {
NodeOperation::Insert { .. } => false,
NodeOperation::Update { path: _, changeset } => changeset.is_delta(),
NodeOperation::Delete { .. } => false,
}
}
pub fn is_update_attribute(&self) -> bool {
match self {
NodeOperation::Insert { .. } => false,
NodeOperation::Update { path: _, changeset } => changeset.is_attribute(),
NodeOperation::Delete { .. } => false,
}
}
pub fn is_insert(&self) -> bool {
match self {
NodeOperation::Insert { .. } => true,
NodeOperation::Update { .. } => false,
NodeOperation::Delete { .. } => false,
}
}
pub fn can_compose(&self, other: &NodeOperation) -> bool {
if self.get_path() != other.get_path() {
return false;
}
if self.is_update_delta() && other.is_update_delta() {
return true;
}
if self.is_update_attribute() && other.is_update_attribute() {
return true;
}
if self.is_insert() && other.is_update_delta() {
return true;
}
false
}
pub fn compose(&mut self, other: &NodeOperation) -> Result<(), OTError> {
match (self, other) {
(
NodeOperation::Insert { path: _, nodes },
NodeOperation::Update {
path: _other_path,
changeset,
},
) => {
match changeset {
Changeset::Delta { delta, inverted: _ } => {
if let Body::Delta(old_delta) = &mut nodes.last_mut().unwrap().body {
let new_delta = old_delta.compose(delta)?;
*old_delta = new_delta;
}
}
Changeset::Attributes { new: _, old: _ } => {
return Err(OTError::compose().context("Can't compose the attributes changeset"));
}
}
Ok(())
}
(
NodeOperation::Update { path: _, changeset },
NodeOperation::Update {
path: _,
changeset: other_changeset,
},
) => changeset.compose(other_changeset),
(_left, _right) => Err(OTError::compose().context("Can't compose the operation")),
}
}
pub fn inverted(&self) -> NodeOperation {
match self {
NodeOperation::Insert { path, nodes } => NodeOperation::Delete {
path: path.clone(),
@ -99,52 +173,24 @@ impl NodeOperation {
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
type OperationIndexMap = Vec<Arc<NodeOperation>>;
#[derive(Debug, Clone, Default)]
pub struct NodeOperations {
operations: Vec<Arc<NodeOperation>>,
inner: OperationIndexMap,
}
impl NodeOperations {
pub fn into_inner(self) -> Vec<Arc<NodeOperation>> {
self.operations
pub fn new() -> Self {
Self::default()
}
pub fn push_op(&mut self, operation: NodeOperation) {
self.operations.push(Arc::new(operation));
}
pub fn extend(&mut self, other: NodeOperations) {
for operation in other.operations {
self.operations.push(operation);
}
}
}
impl std::ops::Deref for NodeOperations {
type Target = Vec<Arc<NodeOperation>>;
fn deref(&self) -> &Self::Target {
&self.operations
}
}
impl std::ops::DerefMut for NodeOperations {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.operations
}
}
impl std::convert::From<Vec<NodeOperation>> for NodeOperations {
fn from(operations: Vec<NodeOperation>) -> Self {
Self::new(operations)
}
}
impl NodeOperations {
pub fn new(operations: Vec<NodeOperation>) -> Self {
Self {
operations: operations.into_iter().map(Arc::new).collect(),
pub fn from_operations(operations: Vec<NodeOperation>) -> Self {
let mut ops = Self::new();
for op in operations {
ops.push_op(op)
}
ops
}
pub fn from_bytes(bytes: Vec<u8>) -> Result<Self, OTError> {
@ -156,4 +202,69 @@ impl NodeOperations {
let bytes = serde_json::to_vec(self).map_err(|err| OTError::serde().context(err))?;
Ok(bytes)
}
pub fn values(&self) -> &Vec<Arc<NodeOperation>> {
&self.inner
}
pub fn values_mut(&mut self) -> &mut Vec<Arc<NodeOperation>> {
&mut self.inner
}
pub fn len(&self) -> usize {
self.values().len()
}
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
pub fn into_inner(self) -> Vec<Arc<NodeOperation>> {
self.inner
}
pub fn push_op<T: Into<Arc<NodeOperation>>>(&mut self, other: T) {
let other = other.into();
if let Some(last_operation) = self.inner.last_mut() {
if last_operation.can_compose(&other) {
let mut_operation = Arc::make_mut(last_operation);
if mut_operation.compose(&other).is_ok() {
return;
}
}
}
// if let Some(operations) = self.inner.get_mut(other.get_path()) {
// if let Some(last_operation) = operations.last_mut() {
// if last_operation.can_compose(&other) {
// let mut_operation = Arc::make_mut(last_operation);
// if mut_operation.compose(&other).is_ok() {
// return;
// }
// }
// }
// }
// If the passed-in operation can't be composed, then append it to the end.
self.inner.push(other);
}
pub fn compose(&mut self, other: NodeOperations) {
for operation in other.values() {
self.push_op(operation.clone());
}
}
pub fn inverted(&self) -> Self {
let mut operations = Self::new();
for operation in self.values() {
operations.push_op(operation.inverted());
}
operations
}
}
impl std::convert::From<Vec<NodeOperation>> for NodeOperations {
fn from(operations: Vec<NodeOperation>) -> Self {
Self::from_operations(operations)
}
}

View File

@ -1,196 +1,49 @@
use crate::core::{AttributeHashMap, Changeset, Path};
use crate::text_delta::TextOperations;
use serde::de::{self, MapAccess, Visitor};
use serde::ser::SerializeMap;
use serde::{Deserializer, Serializer};
use std::convert::TryInto;
use crate::core::{NodeOperation, NodeOperations};
use serde::de::{SeqAccess, Visitor};
use serde::ser::SerializeSeq;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::fmt;
use std::marker::PhantomData;
#[allow(dead_code)]
pub fn serialize_changeset<S>(path: &Path, changeset: &Changeset, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut map = serializer.serialize_map(Some(3))?;
map.serialize_key("path")?;
map.serialize_value(path)?;
match changeset {
Changeset::Delta { delta, inverted } => {
map.serialize_key("delta")?;
map.serialize_value(delta)?;
map.serialize_key("inverted")?;
map.serialize_value(inverted)?;
map.end()
}
Changeset::Attributes { new, old } => {
map.serialize_key("new")?;
map.serialize_value(new)?;
map.serialize_key("old")?;
map.serialize_value(old)?;
map.end()
impl Serialize for NodeOperations {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let operations = self.values();
let mut seq = serializer.serialize_seq(Some(operations.len()))?;
for operation in operations {
let _ = seq.serialize_element(&operation)?;
}
seq.end()
}
}
#[allow(dead_code)]
pub fn deserialize_changeset<'de, D>(deserializer: D) -> Result<(Path, Changeset), D::Error>
where
D: Deserializer<'de>,
{
struct ChangesetVisitor();
impl<'de> Deserialize<'de> for NodeOperations {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct NodeOperationsVisitor();
impl<'de> Visitor<'de> for ChangesetVisitor {
type Value = (Path, Changeset);
impl<'de> Visitor<'de> for NodeOperationsVisitor {
type Value = NodeOperations;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("Expect Path and Changeset")
}
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("Expected node operation")
}
#[inline]
fn visit_map<V>(self, mut map: V) -> Result<Self::Value, V::Error>
where
V: MapAccess<'de>,
{
let mut path: Option<Path> = None;
let mut delta_changeset = DeltaChangeset::<V::Error>::new();
let mut attribute_changeset = AttributeChangeset::new();
while let Some(key) = map.next_key()? {
match key {
"delta" => {
if delta_changeset.delta.is_some() {
return Err(de::Error::duplicate_field("delta"));
}
delta_changeset.delta = Some(map.next_value()?);
}
"inverted" => {
if delta_changeset.inverted.is_some() {
return Err(de::Error::duplicate_field("inverted"));
}
delta_changeset.inverted = Some(map.next_value()?);
}
"path" => {
if path.is_some() {
return Err(de::Error::duplicate_field("path"));
}
path = Some(map.next_value::<Path>()?)
}
"new" => {
if attribute_changeset.new.is_some() {
return Err(de::Error::duplicate_field("new"));
}
attribute_changeset.new = Some(map.next_value()?);
}
"old" => {
if attribute_changeset.old.is_some() {
return Err(de::Error::duplicate_field("old"));
}
attribute_changeset.old = Some(map.next_value()?);
}
other => {
tracing::warn!("Unexpected key: {}", other);
panic!()
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: SeqAccess<'de>,
{
let mut operations = NodeOperations::new();
while let Some(operation) = seq.next_element::<NodeOperation>()? {
operations.push_op(operation);
}
Ok(operations)
}
if path.is_none() {
return Err(de::Error::missing_field("path"));
}
let mut changeset: Changeset;
if !delta_changeset.is_empty() {
changeset = delta_changeset.try_into()?
} else {
changeset = attribute_changeset.try_into()?;
}
Ok((path.unwrap(), changeset))
}
}
deserializer.deserialize_any(ChangesetVisitor())
}
struct DeltaChangeset<E> {
delta: Option<TextOperations>,
inverted: Option<TextOperations>,
error: PhantomData<E>,
}
impl<E> DeltaChangeset<E> {
fn new() -> Self {
Self {
delta: None,
inverted: None,
error: PhantomData,
}
}
fn is_empty(&self) -> bool {
self.delta.is_none() && self.inverted.is_none()
}
}
impl<E> std::convert::TryInto<Changeset> for DeltaChangeset<E>
where
E: de::Error,
{
type Error = E;
fn try_into(self) -> Result<Changeset, Self::Error> {
if self.delta.is_none() {
return Err(de::Error::missing_field("delta"));
}
if self.inverted.is_none() {
return Err(de::Error::missing_field("inverted"));
}
let changeset = Changeset::Delta {
delta: self.delta.unwrap(),
inverted: self.inverted.unwrap(),
};
Ok(changeset)
}
}
struct AttributeChangeset<E> {
new: Option<AttributeHashMap>,
old: Option<AttributeHashMap>,
error: PhantomData<E>,
}
impl<E> AttributeChangeset<E> {
fn new() -> Self {
Self {
new: Default::default(),
old: Default::default(),
error: PhantomData,
}
}
fn is_empty(&self) -> bool {
self.new.is_none() && self.old.is_none()
}
}
impl<E> std::convert::TryInto<Changeset> for AttributeChangeset<E>
where
E: de::Error,
{
type Error = E;
fn try_into(self) -> Result<Changeset, Self::Error> {
if self.new.is_none() {
return Err(de::Error::missing_field("new"));
}
if self.old.is_none() {
return Err(de::Error::missing_field("old"));
}
Ok(Changeset::Attributes {
new: self.new.unwrap(),
old: self.old.unwrap(),
})
deserializer.deserialize_any(NodeOperationsVisitor())
}
}

View File

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

View File

@ -1,13 +1,12 @@
use super::{Changeset, NodeOperations};
use crate::core::attributes::AttributeHashMap;
use crate::core::{NodeData, NodeOperation, NodeTree, Path};
use crate::errors::OTError;
use indextree::NodeId;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Transaction {
#[serde(flatten)]
pub operations: NodeOperations,
#[serde(default)]
@ -37,6 +36,16 @@ impl Transaction {
Ok(bytes)
}
pub fn from_json(s: &str) -> Result<Self, OTError> {
let serde_transaction: Transaction = serde_json::from_str(s).map_err(|err| OTError::serde().context(err))?;
let mut transaction = Self::new();
transaction.extension = serde_transaction.extension;
for operation in serde_transaction.operations.into_inner() {
transaction.operations.push_op(operation);
}
Ok(transaction)
}
pub fn to_json(&self) -> Result<String, OTError> {
serde_json::to_string(&self).map_err(|err| OTError::serde().context(err))
}
@ -45,6 +54,10 @@ impl Transaction {
self.operations.into_inner()
}
pub fn split(self) -> (Vec<Arc<NodeOperation>>, Extension) {
(self.operations.into_inner(), self.extension)
}
pub fn push_operation<T: Into<NodeOperation>>(&mut self, operation: T) {
let operation = operation.into();
self.operations.push_op(operation);
@ -57,38 +70,26 @@ impl Transaction {
pub fn transform(&self, other: &Transaction) -> Result<Transaction, OTError> {
let mut other = other.clone();
other.extension = self.extension.clone();
for other_operation in other.iter_mut() {
for other_operation in other.operations.values_mut() {
let other_operation = Arc::make_mut(other_operation);
for operation in self.operations.iter() {
for operation in self.operations.values() {
operation.transform(other_operation);
}
}
Ok(other)
}
pub fn compose(&mut self, other: Transaction) -> Result<(), OTError> {
// For the moment, just append `other` operations to the end of `self`.
let Transaction { operations, extension } = other;
self.operations.extend(operations);
self.operations.compose(operations);
self.extension = extension;
Ok(())
}
}
impl std::ops::Deref for Transaction {
type Target = Vec<Arc<NodeOperation>>;
fn deref(&self) -> &Self::Target {
&self.operations
}
}
impl std::ops::DerefMut for Transaction {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.operations
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Extension {
Empty,
@ -121,18 +122,14 @@ pub struct Position {
path: Path,
offset: usize,
}
pub struct TransactionBuilder<'a> {
node_tree: &'a NodeTree,
#[derive(Default)]
pub struct TransactionBuilder {
operations: NodeOperations,
}
impl<'a> TransactionBuilder<'a> {
pub fn new(node_tree: &'a NodeTree) -> TransactionBuilder {
TransactionBuilder {
node_tree,
operations: NodeOperations::default(),
}
impl TransactionBuilder {
pub fn new() -> TransactionBuilder {
Self::default()
}
///
@ -148,9 +145,9 @@ impl<'a> TransactionBuilder<'a> {
/// // 0 -- text_1
/// use lib_ot::core::{NodeTree, NodeData, TransactionBuilder};
/// let mut node_tree = NodeTree::default();
/// let transaction = TransactionBuilder::new(&node_tree)
/// let transaction = TransactionBuilder::new()
/// .insert_nodes_at_path(0,vec![ NodeData::new("text_1")])
/// .finalize();
/// .build();
/// node_tree.apply_transaction(transaction).unwrap();
///
/// node_tree.node_id_at_path(vec![0]).unwrap();
@ -178,9 +175,9 @@ impl<'a> TransactionBuilder<'a> {
/// // |-- text
/// use lib_ot::core::{NodeTree, NodeData, TransactionBuilder};
/// let mut node_tree = NodeTree::default();
/// let transaction = TransactionBuilder::new(&node_tree)
/// let transaction = TransactionBuilder::new()
/// .insert_node_at_path(0, NodeData::new("text"))
/// .finalize();
/// .build();
/// node_tree.apply_transaction(transaction).unwrap();
/// ```
///
@ -188,49 +185,50 @@ impl<'a> TransactionBuilder<'a> {
self.insert_nodes_at_path(path, vec![node])
}
pub fn update_attributes_at_path(mut self, path: &Path, attributes: AttributeHashMap) -> Self {
match self.node_tree.get_node_at_path(path) {
Some(node) => {
let mut old_attributes = AttributeHashMap::new();
for key in attributes.keys() {
let old_attrs = &node.attributes;
if let Some(value) = old_attrs.get(key.as_str()) {
old_attributes.insert(key.clone(), value.clone());
}
}
self.operations.push_op(NodeOperation::Update {
path: path.clone(),
changeset: Changeset::Attributes {
new: attributes,
old: old_attributes,
},
});
}
None => tracing::warn!("Update attributes at path: {:?} failed. Node is not exist", path),
}
pub fn update_node_at_path<T: Into<Path>>(mut self, path: T, changeset: Changeset) -> Self {
self.operations.push_op(NodeOperation::Update {
path: path.into(),
changeset,
});
self
}
//
// pub fn update_delta_at_path<T: Into<Path>>(
// mut self,
// path: T,
// new_delta: DeltaTextOperations,
// ) -> Result<Self, OTError> {
// let path = path.into();
// let operation: NodeOperation = self
// .operations
// .get(&path)
// .ok_or(Err(OTError::record_not_found().context("Can't found the node")))?;
//
// match operation {
// NodeOperation::Insert { path, nodes } => {}
// NodeOperation::Update { path, changeset } => {}
// NodeOperation::Delete { .. } => {}
// }
//
// match node.body {
// Body::Empty => Ok(self),
// Body::Delta(delta) => {
// let inverted = new_delta.invert(&delta);
// let changeset = Changeset::Delta {
// delta: new_delta,
// inverted,
// };
// Ok(self.update_node_at_path(path, changeset))
// }
// }
// }
pub fn update_body_at_path(mut self, path: &Path, changeset: Changeset) -> Self {
match self.node_tree.node_id_at_path(path) {
Some(_) => {
self.operations.push_op(NodeOperation::Update {
path: path.clone(),
changeset,
});
}
None => tracing::warn!("Update attributes at path: {:?} failed. Node is not exist", path),
}
self
pub fn delete_node_at_path(self, node_tree: &NodeTree, path: &Path) -> Self {
self.delete_nodes_at_path(node_tree, path, 1)
}
pub fn delete_node_at_path(self, path: &Path) -> Self {
self.delete_nodes_at_path(path, 1)
}
pub fn delete_nodes_at_path(mut self, path: &Path, length: usize) -> Self {
let node_id = self.node_tree.node_id_at_path(path);
pub fn delete_nodes_at_path(mut self, node_tree: &NodeTree, path: &Path, length: usize) -> Self {
let node_id = node_tree.node_id_at_path(path);
if node_id.is_none() {
tracing::warn!("Path: {:?} doesn't contains any nodes", path);
return self;
@ -239,8 +237,8 @@ impl<'a> TransactionBuilder<'a> {
let mut node_id = node_id.unwrap();
let mut deleted_nodes = vec![];
for _ in 0..length {
deleted_nodes.push(self.get_deleted_node_data(node_id));
node_id = self.node_tree.following_siblings(node_id).next().unwrap();
deleted_nodes.push(self.get_deleted_node_data(node_tree, node_id));
node_id = node_tree.following_siblings(node_id).next().unwrap();
}
self.operations.push_op(NodeOperation::Delete {
@ -250,16 +248,12 @@ impl<'a> TransactionBuilder<'a> {
self
}
fn get_deleted_node_data(&self, node_id: NodeId) -> NodeData {
let node_data = self.node_tree.get_node(node_id).unwrap();
fn get_deleted_node_data(&self, node_tree: &NodeTree, node_id: NodeId) -> NodeData {
let node_data = node_tree.get_node(node_id).unwrap();
let mut children = vec![];
self.node_tree
.get_children_ids(node_id)
.into_iter()
.for_each(|child_id| {
children.push(self.get_deleted_node_data(child_id));
});
node_tree.get_children_ids(node_id).into_iter().for_each(|child_id| {
children.push(self.get_deleted_node_data(node_tree, child_id));
});
NodeData {
node_type: node_data.node_type.clone(),
@ -274,7 +268,7 @@ impl<'a> TransactionBuilder<'a> {
self
}
pub fn finalize(self) -> Transaction {
pub fn build(self) -> Transaction {
Transaction::from_operations(self.operations)
}
}

View File

@ -1,6 +1,6 @@
use super::NodeOperations;
use crate::core::{Changeset, Node, NodeData, NodeOperation, Path, Transaction};
use crate::errors::{ErrorBuilder, OTError, OTErrorCode};
use crate::errors::{OTError, OTErrorCode};
use indextree::{Arena, FollowingSiblings, NodeId};
use std::sync::Arc;
@ -20,6 +20,8 @@ impl Default for NodeTree {
}
}
pub const PLACEHOLDER_NODE_TYPE: &str = "";
impl NodeTree {
pub fn new(context: NodeTreeContext) -> NodeTree {
let mut arena = Arena::new();
@ -41,7 +43,7 @@ impl NodeTree {
pub fn from_operations<T: Into<NodeOperations>>(operations: T, context: NodeTreeContext) -> Result<Self, OTError> {
let operations = operations.into();
let mut node_tree = NodeTree::new(context);
for operation in operations.into_inner().into_iter() {
for (_, operation) in operations.into_inner().into_iter().enumerate() {
let _ = node_tree.apply_op(operation)?;
}
Ok(node_tree)
@ -149,15 +151,15 @@ impl NodeTree {
return None;
}
let mut iterate_node = self.root;
let mut node_id = self.root;
for id in path.iter() {
iterate_node = self.child_from_node_at_index(iterate_node, *id)?;
node_id = self.node_id_from_parent_at_index(node_id, *id)?;
}
if iterate_node.is_removed(&self.arena) {
if node_id.is_removed(&self.arena) {
return None;
}
Some(iterate_node)
Some(node_id)
}
pub fn path_from_node_id(&self, node_id: NodeId) -> Path {
@ -210,7 +212,7 @@ impl NodeTree {
/// let node_2 = node_tree.get_node_at_path(&inserted_path).unwrap();
/// assert_eq!(node_2.node_type, node_1.node_type);
/// ```
pub fn child_from_node_at_index(&self, node_id: NodeId, index: usize) -> Option<NodeId> {
pub fn node_id_from_parent_at_index(&self, node_id: NodeId, index: usize) -> Option<NodeId> {
let children = node_id.children(&self.arena);
for (counter, child) in children.enumerate() {
if counter == index {
@ -240,10 +242,11 @@ impl NodeTree {
}
pub fn apply_transaction(&mut self, transaction: Transaction) -> Result<(), OTError> {
let operations = transaction.into_operations();
let operations = transaction.split().0;
for operation in operations {
self.apply_op(operation)?;
}
Ok(())
}
@ -294,25 +297,68 @@ impl NodeTree {
if parent_path.is_empty() {
self.insert_nodes_at_index(self.root, last_index, nodes)
} else {
let parent_node = self
.node_id_at_path(parent_path)
.ok_or_else(|| ErrorBuilder::new(OTErrorCode::PathNotFound).build())?;
let parent_node = match self.node_id_at_path(parent_path) {
None => self.create_adjacent_nodes_for_path(parent_path),
Some(parent_node) => parent_node,
};
self.insert_nodes_at_index(parent_node, last_index, nodes)
}
}
/// Create the adjacent nodes for the path
///
/// It will create a corresponding node for each node on the path if it's not existing.
/// If the path is not start from zero, it will create its siblings.
///
/// Check out the operation_insert_test.rs for more examples.
/// * operation_insert_node_when_its_parent_is_not_exist
/// * operation_insert_node_when_multiple_parent_is_not_exist_test
///
/// # Arguments
///
/// * `path`: creates nodes for this path
///
/// returns: NodeId
///
fn create_adjacent_nodes_for_path<T: Into<Path>>(&mut self, path: T) -> NodeId {
let path = path.into();
let mut node_id = self.root;
for id in path.iter() {
match self.node_id_from_parent_at_index(node_id, *id) {
None => {
let num_of_children = node_id.children(&self.arena).count();
if *id > num_of_children {
for _ in 0..(*id - num_of_children) {
let node: Node = placeholder_node().into();
let sibling_node = self.arena.new_node(node);
node_id.append(sibling_node, &mut self.arena);
}
}
let node: Node = placeholder_node().into();
let new_node_id = self.arena.new_node(node);
node_id.append(new_node_id, &mut self.arena);
node_id = new_node_id;
}
Some(next_node_id) => {
node_id = next_node_id;
}
}
}
node_id
}
/// Inserts nodes before the node with node_id
///
fn insert_nodes_before(&mut self, node_id: &NodeId, nodes: Vec<NodeData>) {
if node_id.is_removed(&self.arena) {
tracing::warn!("Node:{:?} is remove before insert", node_id);
return;
}
for node in nodes {
let (node, children) = node.split();
let new_node_id = self.arena.new_node(node);
if node_id.is_removed(&self.arena) {
tracing::warn!("Node:{:?} is remove before insert", node_id);
return;
}
node_id.insert_before(new_node_id, &mut self.arena);
self.append_nodes(&new_node_id, children);
}
@ -326,14 +372,21 @@ impl NodeTree {
// Append the node to the end of the children list if index greater or equal to the
// length of the children.
if index >= parent.children(&self.arena).count() {
let num_of_children = parent.children(&self.arena).count();
if index >= num_of_children {
let mut num_of_nodes_to_insert = index - num_of_children;
while num_of_nodes_to_insert > 0 {
self.append_nodes(&parent, vec![placeholder_node()]);
num_of_nodes_to_insert -= 1;
}
self.append_nodes(&parent, nodes);
return Ok(());
}
let node_to_insert = self
.child_from_node_at_index(parent, index)
.ok_or_else(|| ErrorBuilder::new(OTErrorCode::PathNotFound).build())?;
.node_id_from_parent_at_index(parent, index)
.ok_or_else(|| OTError::internal().context(format!("Can't find the node at {}", index)))?;
self.insert_nodes_before(&node_to_insert, nodes);
Ok(())
@ -364,11 +417,22 @@ impl NodeTree {
Ok(())
}
/// Update the node at path with the `changeset`
///
/// Do nothing if there is no node at the path.
///
/// # Arguments
///
/// * `path`: references to the node that will be applied with the changeset
/// * `changeset`: the change that will be applied to the node
///
/// returns: Result<(), OTError>
fn update(&mut self, path: &Path, changeset: Changeset) -> Result<(), OTError> {
self.mut_node_at_path(path, |node| {
let _ = node.apply_changeset(changeset)?;
Ok(())
})
match self.mut_node_at_path(path, |node| node.apply_changeset(changeset)) {
Ok(_) => {}
Err(err) => tracing::error!("{}", err),
}
Ok(())
}
fn mut_node_at_path<F>(&mut self, path: &Path, f: F) -> Result<(), OTError>
@ -378,9 +442,9 @@ impl NodeTree {
if !path.is_valid() {
return Err(OTErrorCode::InvalidPath.into());
}
let node_id = self
.node_id_at_path(path)
.ok_or_else(|| ErrorBuilder::new(OTErrorCode::PathNotFound).build())?;
let node_id = self.node_id_at_path(path).ok_or_else(|| {
OTError::path_not_found().context(format!("Can't find the mutated node at path: {:?}", path))
})?;
match self.arena.get_mut(node_id) {
None => tracing::warn!("The path: {:?} does not contain any nodes", path),
Some(node) => {
@ -391,3 +455,7 @@ impl NodeTree {
Ok(())
}
}
pub fn placeholder_node() -> NodeData {
NodeData::new(PLACEHOLDER_NODE_TYPE)
}

View File

@ -38,6 +38,9 @@ impl OTError {
static_ot_error!(revision_id_conflict, OTErrorCode::RevisionIDConflict);
static_ot_error!(internal, OTErrorCode::Internal);
static_ot_error!(serde, OTErrorCode::SerdeError);
static_ot_error!(path_not_found, OTErrorCode::PathNotFound);
static_ot_error!(compose, OTErrorCode::ComposeOperationFail);
static_ot_error!(record_not_found, OTErrorCode::RecordNotFound);
}
impl fmt::Display for OTError {
@ -75,7 +78,7 @@ pub enum OTErrorCode {
PathNotFound,
PathIsEmpty,
InvalidPath,
UnexpectedEmpty,
RecordNotFound,
}
pub struct ErrorBuilder {

View File

@ -1,4 +1,7 @@
mod operation_test;
mod operation_delete_test;
mod operation_delta_test;
mod operation_insert_test;
mod script;
mod serde_test;
mod transaction_compose_test;
mod tree_test;

View File

@ -0,0 +1,178 @@
use crate::node::script::NodeScript::*;
use crate::node::script::NodeTest;
use lib_ot::core::{Changeset, NodeData, NodeDataBuilder};
#[test]
fn operation_delete_nested_node_test() {
let mut test = NodeTest::new();
let image_a = NodeData::new("image_a");
let image_b = NodeData::new("image_b");
let video_a = NodeData::new("video_a");
let video_b = NodeData::new("video_b");
let image_1 = NodeDataBuilder::new("image_1")
.add_node_data(image_a.clone())
.add_node_data(image_b.clone())
.build();
let video_1 = NodeDataBuilder::new("video_1")
.add_node_data(video_a.clone())
.add_node_data(video_b)
.build();
let text_node_1 = NodeDataBuilder::new("text_1")
.add_node_data(image_1)
.add_node_data(video_1.clone())
.build();
let image_2 = NodeDataBuilder::new("image_2")
.add_node_data(image_a)
.add_node_data(image_b.clone())
.build();
let text_node_2 = NodeDataBuilder::new("text_2").add_node_data(image_2).build();
let scripts = vec![
InsertNode {
path: 0.into(),
node_data: text_node_1,
rev_id: 1,
},
InsertNode {
path: 1.into(),
node_data: text_node_2,
rev_id: 2,
},
// 0:text_1
// 0:image_1
// 0:image_a
// 1:image_b
// 1:video_1
// 0:video_a
// 1:video_b
// 1:text_2
// 0:image_2
// 0:image_a
// 1:image_b
DeleteNode {
path: vec![0, 0, 0].into(),
rev_id: 3,
},
AssertNode {
path: vec![0, 0, 0].into(),
expected: Some(image_b),
},
AssertNode {
path: vec![0, 1].into(),
expected: Some(video_1),
},
DeleteNode {
path: vec![0, 1, 1].into(),
rev_id: 4,
},
AssertNode {
path: vec![0, 1, 0].into(),
expected: Some(video_a),
},
];
test.run_scripts(scripts);
}
#[test]
fn operation_delete_node_with_revision_conflict_test() {
let mut test = NodeTest::new();
let text_1 = NodeDataBuilder::new("text_1").build();
let text_2 = NodeDataBuilder::new("text_2").build();
let text_3 = NodeDataBuilder::new("text_3").build();
let scripts = vec![
InsertNode {
path: 0.into(),
node_data: text_1.clone(),
rev_id: 1,
},
InsertNode {
path: 1.into(),
node_data: text_2,
rev_id: 2,
},
// The node's in the tree will be:
// 0: text_1
// 2: text_2
//
// The insert action is happened concurrently with the delete action, because they
// share the same rev_id. aka, 3. The delete action is want to delete the node at index 1,
// but it was moved to index 2.
InsertNode {
path: 1.into(),
node_data: text_3.clone(),
rev_id: 3,
},
// 0: text_1
// 1: text_3
// 2: text_2
//
// The path of the delete action will be transformed to a new path that point to the text_2.
// 1 -> 2
DeleteNode {
path: 1.into(),
rev_id: 3,
},
// After perform the delete action, the tree will be:
// 0: text_1
// 1: text_3
AssertNumberOfChildrenAtPath {
path: None,
expected: 2,
},
AssertNode {
path: 0.into(),
expected: Some(text_1),
},
AssertNode {
path: 1.into(),
expected: Some(text_3),
},
AssertNode {
path: 2.into(),
expected: None,
},
];
test.run_scripts(scripts);
}
#[test]
fn operation_update_node_after_delete_test() {
let mut test = NodeTest::new();
let text_1 = NodeDataBuilder::new("text_1").build();
let text_2 = NodeDataBuilder::new("text_2").build();
let scripts = vec![
InsertNode {
path: 0.into(),
node_data: text_1,
rev_id: 1,
},
InsertNode {
path: 1.into(),
node_data: text_2,
rev_id: 2,
},
DeleteNode {
path: 0.into(),
rev_id: 3,
},
// The node at path 1 is not exist. The following UpdateBody script will do nothing
AssertNode {
path: 1.into(),
expected: None,
},
UpdateBody {
path: 1.into(),
changeset: Changeset::Delta {
delta: Default::default(),
inverted: Default::default(),
},
},
];
test.run_scripts(scripts);
}

View File

@ -0,0 +1,41 @@
use crate::node::script::NodeScript::{AssertNodeDelta, InsertNode, UpdateBody};
use crate::node::script::{edit_node_delta, NodeTest};
use lib_ot::core::NodeDataBuilder;
use lib_ot::text_delta::DeltaTextOperationBuilder;
#[test]
fn operation_update_delta_test() {
let mut test = NodeTest::new();
let initial_delta = DeltaTextOperationBuilder::new().build();
let new_delta = DeltaTextOperationBuilder::new()
.retain(initial_delta.utf16_base_len)
.insert("Hello, world")
.build();
let (changeset, expected) = edit_node_delta(&initial_delta, new_delta);
let node = NodeDataBuilder::new("text").insert_delta(initial_delta.clone()).build();
let scripts = vec![
InsertNode {
path: 0.into(),
node_data: node,
rev_id: 1,
},
UpdateBody {
path: 0.into(),
changeset: changeset.clone(),
},
AssertNodeDelta {
path: 0.into(),
expected,
},
UpdateBody {
path: 0.into(),
changeset: changeset.inverted(),
},
AssertNodeDelta {
path: 0.into(),
expected: initial_delta,
},
];
test.run_scripts(scripts);
}

View File

@ -0,0 +1,460 @@
use crate::node::script::NodeScript::*;
use crate::node::script::NodeTest;
use lib_ot::core::{placeholder_node, NodeData, NodeDataBuilder, NodeOperation, Path};
#[test]
fn operation_insert_op_transform_test() {
let node_1 = NodeDataBuilder::new("text_1").build();
let node_2 = NodeDataBuilder::new("text_2").build();
let op_1 = NodeOperation::Insert {
path: Path(vec![0, 1]),
nodes: vec![node_1],
};
let mut insert_2 = NodeOperation::Insert {
path: Path(vec![0, 1]),
nodes: vec![node_2],
};
// let mut node_tree = NodeTree::new("root");
// node_tree.apply_op(insert_1.clone()).unwrap();
op_1.transform(&mut insert_2);
let json = serde_json::to_string(&insert_2).unwrap();
assert_eq!(json, r#"{"op":"insert","path":[0,2],"nodes":[{"type":"text_2"}]}"#);
}
#[test]
fn operation_insert_one_level_path_test() {
let node_data_1 = NodeDataBuilder::new("text_1").build();
let node_data_2 = NodeDataBuilder::new("text_2").build();
let node_data_3 = NodeDataBuilder::new("text_3").build();
let node_3 = node_data_3.clone();
// 0: text_1
// 1: text_2
//
// Insert a new operation with rev_id 2 to index 1,but the index was already taken, so
// it needs to be transformed.
//
// 0: text_1
// 1: text_2
// 2: text_3
let scripts = vec![
InsertNode {
path: 0.into(),
node_data: node_data_1.clone(),
rev_id: 1,
},
InsertNode {
path: 1.into(),
node_data: node_data_2.clone(),
rev_id: 2,
},
InsertNode {
path: 1.into(),
node_data: node_data_3.clone(),
rev_id: 2,
},
AssertNode {
path: 2.into(),
expected: Some(node_3.clone()),
},
];
NodeTest::new().run_scripts(scripts);
// If the rev_id of the node_data_3 is 3. then the tree will be:
// 0: text_1
// 1: text_3
// 2: text_2
let scripts = vec![
InsertNode {
path: 0.into(),
node_data: node_data_1,
rev_id: 1,
},
InsertNode {
path: 1.into(),
node_data: node_data_2,
rev_id: 2,
},
InsertNode {
path: 1.into(),
node_data: node_data_3,
rev_id: 3,
},
AssertNode {
path: 1.into(),
expected: Some(node_3),
},
];
NodeTest::new().run_scripts(scripts);
}
#[test]
fn operation_insert_with_multiple_level_path_test() {
let mut test = NodeTest::new();
let node_data_1 = NodeDataBuilder::new("text_1")
.add_node_data(NodeDataBuilder::new("text_1_1").build())
.add_node_data(NodeDataBuilder::new("text_1_2").build())
.build();
let node_data_2 = NodeDataBuilder::new("text_2")
.add_node_data(NodeDataBuilder::new("text_2_1").build())
.add_node_data(NodeDataBuilder::new("text_2_2").build())
.build();
let node_data_3 = NodeDataBuilder::new("text_3").build();
let scripts = vec![
InsertNode {
path: 0.into(),
node_data: node_data_1,
rev_id: 1,
},
InsertNode {
path: 1.into(),
node_data: node_data_2,
rev_id: 2,
},
InsertNode {
path: 1.into(),
node_data: node_data_3.clone(),
rev_id: 2,
},
AssertNode {
path: 2.into(),
expected: Some(node_data_3),
},
];
test.run_scripts(scripts);
}
#[test]
fn operation_insert_node_out_of_bound_test() {
let mut test = NodeTest::new();
let image_a = NodeData::new("image_a");
let image_b = NodeData::new("image_b");
let image = NodeDataBuilder::new("image_1")
.add_node_data(image_a)
.add_node_data(image_b)
.build();
let text_node = NodeDataBuilder::new("text_1").add_node_data(image).build();
let image_c = NodeData::new("image_c");
let scripts = vec![
InsertNode {
path: 0.into(),
node_data: text_node,
rev_id: 1,
},
// 0:text_1
// 0:image_1
// 0:image_a
// 1:image_b
InsertNode {
path: vec![0, 0, 3].into(),
node_data: image_c.clone(),
rev_id: 2,
},
// 0:text_1
// 0:image_1
// 0:image_a
// 1:image_b
// 2:placeholder node
// 3:image_c
AssertNode {
path: vec![0, 0, 2].into(),
expected: Some(placeholder_node()),
},
AssertNode {
path: vec![0, 0, 3].into(),
expected: Some(image_c),
},
AssertNode {
path: vec![0, 0, 10].into(),
expected: None,
},
];
test.run_scripts(scripts);
}
#[test]
fn operation_insert_node_when_parent_is_not_exist_test1() {
let mut test = NodeTest::new();
let text_1 = NodeDataBuilder::new("text_1").build();
let text_2 = NodeDataBuilder::new("text_2").build();
let scripts = vec![
InsertNode {
path: 0.into(),
node_data: text_1,
rev_id: 1,
},
// The node at path 1 is not existing when inserting the text_2 to path 2.
InsertNode {
path: 2.into(),
node_data: text_2.clone(),
rev_id: 2,
},
AssertNode {
path: 1.into(),
expected: Some(placeholder_node()),
},
AssertNode {
path: 2.into(),
expected: Some(text_2),
},
];
test.run_scripts(scripts);
}
#[test]
fn operation_insert_node_when_parent_is_not_exist_test2() {
let mut test = NodeTest::new();
let text_1 = NodeDataBuilder::new("text_1").build();
let text_2 = NodeDataBuilder::new("text_2").build();
let scripts = vec![
InsertNode {
path: 0.into(),
node_data: text_1,
rev_id: 1,
},
// The node at path 1 is not existing when inserting the text_2 to path 2.
InsertNode {
path: 3.into(),
node_data: text_2.clone(),
rev_id: 2,
},
AssertNode {
path: 1.into(),
expected: Some(placeholder_node()),
},
AssertNode {
path: 2.into(),
expected: Some(placeholder_node()),
},
AssertNode {
path: 3.into(),
expected: Some(text_2),
},
];
test.run_scripts(scripts);
}
#[test]
fn operation_insert_node_when_its_parent_is_not_exist_test3() {
let mut test = NodeTest::new();
let text_1 = NodeDataBuilder::new("text_1").build();
let text_2 = NodeDataBuilder::new("text_2").build();
let mut placeholder_node = placeholder_node();
placeholder_node.children.push(text_2.clone());
let scripts = vec![
InsertNode {
path: 0.into(),
node_data: text_1,
rev_id: 1,
},
// The node at path 1 is not existing when inserting the text_2 to path 2.
InsertNode {
path: vec![1, 0].into(),
node_data: text_2.clone(),
rev_id: 2,
},
AssertNode {
path: 1.into(),
expected: Some(placeholder_node),
},
AssertNode {
path: vec![1, 0].into(),
expected: Some(text_2),
},
];
test.run_scripts(scripts);
}
#[test]
fn operation_insert_node_to_the_end_when_parent_is_not_exist_test() {
let mut test = NodeTest::new();
let node_0 = NodeData::new("0");
let node_1 = NodeData::new("1");
let node_1_1 = NodeData::new("1_1");
let text_node = NodeData::new("text");
let mut ghost = placeholder_node();
ghost.children.push(text_node.clone());
// 0:0
// 1:1
// 0:1_1
// 1:ghost
// 0:text
let scripts = vec![
InsertNode {
path: 0.into(),
node_data: node_0,
rev_id: 1,
},
InsertNode {
path: 1.into(),
node_data: node_1,
rev_id: 2,
},
InsertNode {
path: vec![1, 0].into(),
node_data: node_1_1.clone(),
rev_id: 3,
},
InsertNode {
path: vec![1, 1, 0].into(),
node_data: text_node.clone(),
rev_id: 4,
},
AssertNode {
path: vec![1, 0].into(),
expected: Some(node_1_1),
},
AssertNode {
path: vec![1, 1].into(),
expected: Some(ghost),
},
AssertNode {
path: vec![1, 1, 0].into(),
expected: Some(text_node),
},
];
test.run_scripts(scripts);
}
#[test]
fn operation_insert_node_when_multiple_parent_is_not_exist_test() {
let mut test = NodeTest::new();
let text_1 = NodeDataBuilder::new("text_1").build();
let text_2 = NodeDataBuilder::new("text_2").build();
let path = vec![1, 0, 0, 0, 0, 0];
let mut auto_fill_node = placeholder_node();
let mut iter_node: &mut NodeData = &mut auto_fill_node;
let insert_path = path.split_at(1).1;
for (index, _) in insert_path.iter().enumerate() {
if index == insert_path.len() - 1 {
iter_node.children.push(text_2.clone());
} else {
iter_node.children.push(placeholder_node());
iter_node = iter_node.children.last_mut().unwrap();
}
}
let scripts = vec![
InsertNode {
path: 0.into(),
node_data: text_1,
rev_id: 1,
},
InsertNode {
path: path.clone().into(),
node_data: text_2.clone(),
rev_id: 2,
},
AssertNode {
path: vec![1].into(),
expected: Some(auto_fill_node),
},
AssertNode {
path: path.into(),
expected: Some(text_2),
},
];
test.run_scripts(scripts);
}
#[test]
fn operation_insert_node_when_multiple_parent_is_not_exist_test2() {
let mut test = NodeTest::new();
// 0:ghost
// 0:ghost
// 1:ghost
// 0:text
let mut text_node_parent = placeholder_node();
let text_node = NodeDataBuilder::new("text").build();
text_node_parent.children.push(text_node.clone());
let mut ghost = placeholder_node();
ghost.children.push(placeholder_node());
ghost.children.push(text_node_parent.clone());
let path = vec![1, 1, 0];
let scripts = vec![
InsertNode {
path: path.into(),
node_data: text_node.clone(),
rev_id: 1,
},
// 0:ghost
// 1:ghost
// 0:ghost
// 1:ghost
// 0:text
AssertNode {
path: 0.into(),
expected: Some(placeholder_node()),
},
AssertNode {
path: 1.into(),
expected: Some(ghost),
},
AssertNumberOfChildrenAtPath {
path: Some(1.into()),
expected: 2,
},
AssertNode {
path: vec![1, 1].into(),
expected: Some(text_node_parent),
},
AssertNode {
path: vec![1, 1, 0].into(),
expected: Some(text_node),
},
];
test.run_scripts(scripts);
}
#[test]
fn operation_insert_node_when_multiple_parent_is_not_exist_test3() {
let mut test = NodeTest::new();
let text_node = NodeDataBuilder::new("text").build();
let path = vec![3, 3, 0];
let scripts = vec![
InsertNode {
path: path.clone().into(),
node_data: text_node.clone(),
rev_id: 1,
},
// 0:ghost
// 1:ghost
// 2:ghost
// 3:ghost
// 0:ghost
// 1:ghost
// 2:ghost
// 3:ghost
// 0:text
AssertNode {
path: 0.into(),
expected: Some(placeholder_node()),
},
AssertNode {
path: 1.into(),
expected: Some(placeholder_node()),
},
AssertNode {
path: 2.into(),
expected: Some(placeholder_node()),
},
AssertNumberOfChildrenAtPath {
path: Some(3.into()),
expected: 4,
},
AssertNode {
path: path.into(),
expected: Some(text_node),
},
];
test.run_scripts(scripts);
}

View File

@ -1,170 +0,0 @@
use crate::node::script::NodeScript::*;
use crate::node::script::NodeTest;
use lib_ot::core::{NodeDataBuilder, NodeOperation, Path};
#[test]
fn operation_insert_op_transform_test() {
let node_1 = NodeDataBuilder::new("text_1").build();
let node_2 = NodeDataBuilder::new("text_2").build();
let op_1 = NodeOperation::Insert {
path: Path(vec![0, 1]),
nodes: vec![node_1],
};
let mut insert_2 = NodeOperation::Insert {
path: Path(vec![0, 1]),
nodes: vec![node_2],
};
// let mut node_tree = NodeTree::new("root");
// node_tree.apply_op(insert_1.clone()).unwrap();
op_1.transform(&mut insert_2);
let json = serde_json::to_string(&insert_2).unwrap();
assert_eq!(json, r#"{"op":"insert","path":[0,2],"nodes":[{"type":"text_2"}]}"#);
}
#[test]
fn operation_insert_one_level_path_test() {
let mut test = NodeTest::new();
let node_data_1 = NodeDataBuilder::new("text_1").build();
let node_data_2 = NodeDataBuilder::new("text_2").build();
let node_data_3 = NodeDataBuilder::new("text_3").build();
let node_3 = node_data_3.clone();
// 0: text_1
// 1: text_2
//
// Insert a new operation with rev_id 1,but the rev_id:1 is already exist, so
// it needs to be transformed.
// 1:text_3 => 2:text_3
//
// 0: text_1
// 1: text_2
// 2: text_3
//
// If the rev_id of the insert operation is 3. then the tree will be:
// 0: text_1
// 1: text_3
// 2: text_2
let scripts = vec![
InsertNode {
path: 0.into(),
node_data: node_data_1,
rev_id: 1,
},
InsertNode {
path: 1.into(),
node_data: node_data_2,
rev_id: 2,
},
InsertNode {
path: 1.into(),
node_data: node_data_3,
rev_id: 1,
},
AssertNode {
path: 2.into(),
expected: Some(node_3),
},
];
test.run_scripts(scripts);
}
#[test]
fn operation_insert_with_multiple_level_path_test() {
let mut test = NodeTest::new();
let node_data_1 = NodeDataBuilder::new("text_1")
.add_node_data(NodeDataBuilder::new("text_1_1").build())
.add_node_data(NodeDataBuilder::new("text_1_2").build())
.build();
let node_data_2 = NodeDataBuilder::new("text_2")
.add_node_data(NodeDataBuilder::new("text_2_1").build())
.add_node_data(NodeDataBuilder::new("text_2_2").build())
.build();
let node_data_3 = NodeDataBuilder::new("text_3").build();
let scripts = vec![
InsertNode {
path: 0.into(),
node_data: node_data_1,
rev_id: 1,
},
InsertNode {
path: 1.into(),
node_data: node_data_2,
rev_id: 2,
},
InsertNode {
path: 1.into(),
node_data: node_data_3.clone(),
rev_id: 1,
},
AssertNode {
path: 2.into(),
expected: Some(node_data_3),
},
];
test.run_scripts(scripts);
}
#[test]
fn operation_delete_test() {
let mut test = NodeTest::new();
let node_data_1 = NodeDataBuilder::new("text_1").build();
let node_data_2 = NodeDataBuilder::new("text_2").build();
let node_data_3 = NodeDataBuilder::new("text_3").build();
let node_3 = node_data_3.clone();
let scripts = vec![
InsertNode {
path: 0.into(),
node_data: node_data_1,
rev_id: 1,
},
InsertNode {
path: 1.into(),
node_data: node_data_2,
rev_id: 2,
},
// The node's in the tree will be:
// 0: text_1
// 2: text_2
//
// The insert action is happened concurrently with the delete action, because they
// share the same rev_id. aka, 3. The delete action is want to delete the node at index 1,
// but it was moved to index 2.
InsertNode {
path: 1.into(),
node_data: node_data_3,
rev_id: 3,
},
// 0: text_1
// 1: text_3
// 2: text_2
//
// The path of the delete action will be transformed to a new path that point to the text_2.
// 1 -> 2
DeleteNode {
path: 1.into(),
rev_id: 3,
},
// After perform the delete action, the tree will be:
// 0: text_1
// 1: text_3
AssertNumberOfChildrenAtPath {
path: None,
expected: 2,
},
AssertNode {
path: 1.into(),
expected: Some(node_3),
},
AssertNode {
path: 2.into(),
expected: None,
},
];
test.run_scripts(scripts);
}

View File

@ -1,5 +1,6 @@
#![allow(clippy::all)]
use lib_ot::core::{NodeTreeContext, Transaction};
use lib_ot::core::{NodeTreeContext, OperationTransform, Transaction};
use lib_ot::text_delta::DeltaTextOperationBuilder;
use lib_ot::{
core::attributes::AttributeHashMap,
core::{Body, Changeset, NodeData, NodeTree, Path, TransactionBuilder},
@ -84,9 +85,7 @@ impl NodeTest {
node_data: node,
rev_id,
} => {
let mut transaction = TransactionBuilder::new(&self.node_tree)
.insert_node_at_path(path, node)
.finalize();
let mut transaction = TransactionBuilder::new().insert_node_at_path(path, node).build();
self.transform_transaction_if_need(&mut transaction, rev_id);
self.apply_transaction(transaction);
}
@ -95,29 +94,34 @@ impl NodeTest {
node_data_list,
rev_id,
} => {
let mut transaction = TransactionBuilder::new(&self.node_tree)
let mut transaction = TransactionBuilder::new()
.insert_nodes_at_path(path, node_data_list)
.finalize();
.build();
self.transform_transaction_if_need(&mut transaction, rev_id);
self.apply_transaction(transaction);
}
NodeScript::UpdateAttributes { path, attributes } => {
let transaction = TransactionBuilder::new(&self.node_tree)
.update_attributes_at_path(&path, attributes)
.finalize();
let node = self.node_tree.get_node_data_at_path(&path).unwrap();
let transaction = TransactionBuilder::new()
.update_node_at_path(
&path,
Changeset::Attributes {
new: attributes,
old: node.attributes,
},
)
.build();
self.apply_transaction(transaction);
}
NodeScript::UpdateBody { path, changeset } => {
//
let transaction = TransactionBuilder::new(&self.node_tree)
.update_body_at_path(&path, changeset)
.finalize();
let transaction = TransactionBuilder::new().update_node_at_path(&path, changeset).build();
self.apply_transaction(transaction);
}
NodeScript::DeleteNode { path, rev_id } => {
let mut transaction = TransactionBuilder::new(&self.node_tree)
.delete_node_at_path(&path)
.finalize();
let mut transaction = TransactionBuilder::new()
.delete_node_at_path(&self.node_tree, &path)
.build();
self.transform_transaction_if_need(&mut transaction, rev_id);
self.apply_transaction(transaction);
}
@ -175,3 +179,35 @@ impl NodeTest {
}
}
}
pub fn edit_node_delta(
delta: &DeltaTextOperations,
new_delta: DeltaTextOperations,
) -> (Changeset, DeltaTextOperations) {
let inverted = new_delta.invert(&delta);
let expected = delta.compose(&new_delta).unwrap();
let changeset = Changeset::Delta {
delta: new_delta.clone(),
inverted: inverted.clone(),
};
(changeset, expected)
}
pub fn make_node_delta_changeset(
initial_content: &str,
insert_str: &str,
) -> (DeltaTextOperations, Changeset, DeltaTextOperations) {
let initial_content = initial_content.to_owned();
let initial_delta = DeltaTextOperationBuilder::new().insert(&initial_content).build();
let delta = DeltaTextOperationBuilder::new()
.retain(initial_content.len())
.insert(insert_str)
.build();
let inverted = delta.invert(&initial_delta);
let expected = initial_delta.compose(&delta).unwrap();
let changeset = Changeset::Delta {
delta: delta.clone(),
inverted: inverted.clone(),
};
(initial_delta, changeset, expected)
}

View File

@ -1,6 +1,4 @@
use lib_ot::core::{
AttributeBuilder, Changeset, NodeData, NodeDataBuilder, NodeOperation, NodeTree, Path, Transaction,
};
use lib_ot::core::{AttributeBuilder, Changeset, NodeData, NodeDataBuilder, NodeOperation, NodeTree, Path};
use lib_ot::text_delta::DeltaTextOperationBuilder;
#[test]
@ -69,33 +67,33 @@ fn operation_update_node_body_deserialize_test() {
assert_eq!(json_1, json_2);
}
#[test]
fn transaction_serialize_test() {
let insert = NodeOperation::Insert {
path: Path(vec![0, 1]),
nodes: vec![NodeData::new("text".to_owned())],
};
let transaction = Transaction::from_operations(vec![insert]);
let json = serde_json::to_string(&transaction).unwrap();
assert_eq!(
json,
r#"{"operations":[{"op":"insert","path":[0,1],"nodes":[{"type":"text"}]}]}"#
);
}
#[test]
fn transaction_deserialize_test() {
let json = r#"{"operations":[{"op":"insert","path":[0,1],"nodes":[{"type":"text"}]}],"TextSelection":{"before_selection":{"start":{"path":[],"offset":0},"end":{"path":[],"offset":0}},"after_selection":{"start":{"path":[],"offset":0},"end":{"path":[],"offset":0}}}}"#;
let transaction: Transaction = serde_json::from_str(json).unwrap();
assert_eq!(transaction.operations.len(), 1);
}
#[test]
fn node_tree_deserialize_test() {
let tree: NodeTree = serde_json::from_str(TREE_JSON).unwrap();
assert_eq!(tree.number_of_children(None), 1);
}
// #[test]
// fn transaction_serialize_test() {
// let insert = NodeOperation::Insert {
// path: Path(vec![0, 1]),
// nodes: vec![NodeData::new("text".to_owned())],
// };
// let transaction = Transaction::from_operations(vec![insert]);
// let json = serde_json::to_string(&transaction).unwrap();
// assert_eq!(
// json,
// r#"{"operations":[{"op":"insert","path":[0,1],"nodes":[{"type":"text"}]}]}"#
// );
// }
//
// #[test]
// fn transaction_deserialize_test() {
// let json = r#"{"operations":[{"op":"insert","path":[0,1],"nodes":[{"type":"text"}]}],"TextSelection":{"before_selection":{"start":{"path":[],"offset":0},"end":{"path":[],"offset":0}},"after_selection":{"start":{"path":[],"offset":0},"end":{"path":[],"offset":0}}}}"#;
//
// let transaction: Transaction = serde_json::from_str(json).unwrap();
// assert_eq!(transaction.operations.len(), 1);
// }
//
// #[test]
// fn node_tree_deserialize_test() {
// let tree: NodeTree = serde_json::from_str(TREE_JSON).unwrap();
// assert_eq!(tree.number_of_children(None), 1);
// }
#[test]
fn node_tree_serialize_test() {

View File

@ -0,0 +1,104 @@
use crate::node::script::{edit_node_delta, make_node_delta_changeset};
use lib_ot::core::{AttributeEntry, Changeset, NodeDataBuilder, NodeOperation, Transaction, TransactionBuilder};
use lib_ot::text_delta::DeltaTextOperationBuilder;
#[test]
fn transaction_compose_update_after_insert_test() {
let (initial_delta, changeset, _) = make_node_delta_changeset("Hello", " world");
let node_data = NodeDataBuilder::new("text").insert_delta(initial_delta).build();
// Modify the same path, the operations will be merged after composing if possible.
let mut transaction_a = TransactionBuilder::new().insert_node_at_path(0, node_data).build();
let transaction_b = TransactionBuilder::new().update_node_at_path(0, changeset).build();
let _ = transaction_a.compose(transaction_b).unwrap();
// The operations are merged into one operation
assert_eq!(transaction_a.operations.len(), 1);
assert_eq!(
transaction_a.to_json().unwrap(),
r#"{"operations":[{"op":"insert","path":[0],"nodes":[{"type":"text","body":{"delta":[{"insert":"Hello world"}]}}]}]}"#
);
}
#[test]
fn transaction_compose_multiple_update_test() {
let (initial_delta, changeset_1, final_delta) = make_node_delta_changeset("Hello", " world");
let mut transaction = TransactionBuilder::new()
.insert_node_at_path(0, NodeDataBuilder::new("text").insert_delta(initial_delta).build())
.build();
let (changeset_2, _) = edit_node_delta(
&final_delta,
DeltaTextOperationBuilder::new()
.retain(final_delta.utf16_target_len)
.insert("😁")
.build(),
);
let mut other_transaction = Transaction::new();
// the following two update operations will be merged into one
let update_1 = TransactionBuilder::new().update_node_at_path(0, changeset_1).build();
other_transaction.compose(update_1).unwrap();
let update_2 = TransactionBuilder::new().update_node_at_path(0, changeset_2).build();
other_transaction.compose(update_2).unwrap();
let inverted = Transaction::from_operations(other_transaction.operations.inverted());
// the update operation will be merged into insert operation
let _ = transaction.compose(other_transaction).unwrap();
assert_eq!(transaction.operations.len(), 1);
assert_eq!(
transaction.to_json().unwrap(),
r#"{"operations":[{"op":"insert","path":[0],"nodes":[{"type":"text","body":{"delta":[{"insert":"Hello world😁"}]}}]}]}"#
);
let _ = transaction.compose(inverted).unwrap();
assert_eq!(
transaction.to_json().unwrap(),
r#"{"operations":[{"op":"insert","path":[0],"nodes":[{"type":"text","body":{"delta":[{"insert":"Hello"}]}}]}]}"#
);
}
#[test]
fn transaction_compose_multiple_attribute_test() {
let delta = DeltaTextOperationBuilder::new().insert("Hello").build();
let node = NodeDataBuilder::new("text").insert_delta(delta).build();
let insert_operation = NodeOperation::Insert {
path: 0.into(),
nodes: vec![node],
};
let mut transaction = Transaction::new();
transaction.push_operation(insert_operation);
let new_attribute = AttributeEntry::new("subtype", "bulleted-list");
let update_operation = NodeOperation::Update {
path: 0.into(),
changeset: Changeset::Attributes {
new: new_attribute.clone().into(),
old: Default::default(),
},
};
transaction.push_operation(update_operation);
assert_eq!(
transaction.to_json().unwrap(),
r#"{"operations":[{"op":"insert","path":[0],"nodes":[{"type":"text","body":{"delta":[{"insert":"Hello"}]}}]},{"op":"update","path":[0],"changeset":{"attributes":{"new":{"subtype":"bulleted-list"},"old":{}}}}]}"#
);
let old_attribute = new_attribute;
let new_attribute = AttributeEntry::new("subtype", "number-list");
transaction.push_operation(NodeOperation::Update {
path: 0.into(),
changeset: Changeset::Attributes {
new: new_attribute.into(),
old: old_attribute.into(),
},
});
assert_eq!(
transaction.to_json().unwrap(),
r#"{"operations":[{"op":"insert","path":[0],"nodes":[{"type":"text","body":{"delta":[{"insert":"Hello"}]}}]},{"op":"update","path":[0],"changeset":{"attributes":{"new":{"subtype":"number-list"},"old":{"subtype":"bulleted-list"}}}}]}"#
);
}

View File

@ -1,10 +1,7 @@
use crate::node::script::NodeScript::*;
use crate::node::script::NodeTest;
use lib_ot::core::Body;
use lib_ot::core::Changeset;
use lib_ot::core::OperationTransform;
use crate::node::script::{make_node_delta_changeset, NodeTest};
use lib_ot::core::{NodeData, NodeDataBuilder, Path};
use lib_ot::text_delta::{DeltaTextOperationBuilder, DeltaTextOperations};
#[test]
fn node_insert_test() {
@ -37,71 +34,6 @@ fn node_insert_with_empty_path_test() {
test.run_scripts(scripts);
}
#[test]
#[should_panic]
fn node_insert_with_not_exist_path_test() {
let mut test = NodeTest::new();
let node_data = NodeData::new("text");
let path: Path = vec![0, 0, 9].into();
let scripts = vec![
InsertNode {
path: path.clone(),
node_data: node_data.clone(),
rev_id: 1,
},
AssertNode {
path,
expected: Some(node_data),
},
];
test.run_scripts(scripts);
}
#[test]
// Append the node to the end of the list if the insert path is out of bounds.
fn node_insert_out_of_bound_test() {
let mut test = NodeTest::new();
let image_a = NodeData::new("image_a");
let image_b = NodeData::new("image_b");
let image = NodeDataBuilder::new("image_1")
.add_node_data(image_a)
.add_node_data(image_b)
.build();
let text_node = NodeDataBuilder::new("text_1").add_node_data(image).build();
let image_c = NodeData::new("image_c");
let scripts = vec![
InsertNode {
path: 0.into(),
node_data: text_node,
rev_id: 1,
},
// 0:text_1
// 0:image_1
// 0:image_a
// 1:image_b
InsertNode {
path: vec![0, 0, 10].into(),
node_data: image_c.clone(),
rev_id: 2,
},
// 0:text_1
// 0:image_1
// 0:image_a
// 1:image_b
// 2:image_b
AssertNode {
path: vec![0, 0, 2].into(),
expected: Some(image_c),
},
AssertNode {
path: vec![0, 0, 10].into(),
expected: None,
},
];
test.run_scripts(scripts);
}
#[test]
fn tree_insert_multiple_nodes_at_root_path_test() {
let mut test = NodeTest::new();
@ -438,11 +370,11 @@ fn node_delete_node_from_list_test() {
InsertNode {
path: 1.into(),
node_data: text_node_2.clone(),
rev_id: 1,
rev_id: 2,
},
DeleteNode {
path: 0.into(),
rev_id: 2,
rev_id: 3,
},
AssertNode {
path: 1.into(),
@ -487,7 +419,7 @@ fn node_delete_nested_node_test() {
InsertNode {
path: 1.into(),
node_data: text_node_2,
rev_id: 1,
rev_id: 2,
},
// 0:text_1
// 0:image_1
@ -499,7 +431,7 @@ fn node_delete_nested_node_test() {
// 1:image_b
DeleteNode {
path: vec![0, 0, 0].into(),
rev_id: 2,
rev_id: 3,
},
// 0:text_1
// 0:image_1
@ -514,7 +446,7 @@ fn node_delete_nested_node_test() {
},
DeleteNode {
path: vec![0, 0].into(),
rev_id: 3,
rev_id: 4,
},
// 0:text_1
// 1:text_2
@ -676,12 +608,16 @@ fn node_reorder_nodes_test() {
// 1:image_b
DeleteNode {
path: vec![0].into(),
rev_id: 2,
rev_id: 3,
},
AssertNode {
path: vec![0].into(),
expected: Some(text_node_2.clone()),
},
InsertNode {
path: vec![1].into(),
node_data: text_node_1.clone(),
rev_id: 3,
rev_id: 4,
},
// 0:text_2
// 0:image_2
@ -722,10 +658,8 @@ fn node_reorder_nodes_test() {
#[test]
fn node_update_body_test() {
let mut test = NodeTest::new();
let (initial_delta, changeset, _, expected) = make_node_delta_changeset("Hello", "AppFlowy");
let node = NodeDataBuilder::new("text")
.insert_body(Body::Delta(initial_delta))
.build();
let (initial_delta, changeset, expected) = make_node_delta_changeset("Hello", "AppFlowy");
let node = NodeDataBuilder::new("text").insert_delta(initial_delta).build();
let scripts = vec![
InsertNode {
@ -748,10 +682,8 @@ fn node_update_body_test() {
#[test]
fn node_inverted_body_changeset_test() {
let mut test = NodeTest::new();
let (initial_delta, changeset, inverted_changeset, _expected) = make_node_delta_changeset("Hello", "AppFlowy");
let node = NodeDataBuilder::new("text")
.insert_body(Body::Delta(initial_delta.clone()))
.build();
let (initial_delta, changeset, _expected) = make_node_delta_changeset("Hello", "AppFlowy");
let node = NodeDataBuilder::new("text").insert_delta(initial_delta.clone()).build();
let scripts = vec![
InsertNode {
@ -761,11 +693,11 @@ fn node_inverted_body_changeset_test() {
},
UpdateBody {
path: 0.into(),
changeset,
changeset: changeset.clone(),
},
UpdateBody {
path: 0.into(),
changeset: inverted_changeset,
changeset: changeset.inverted(),
},
AssertNodeDelta {
path: 0.into(),
@ -774,27 +706,3 @@ fn node_inverted_body_changeset_test() {
];
test.run_scripts(scripts);
}
fn make_node_delta_changeset(
initial_content: &str,
insert_str: &str,
) -> (DeltaTextOperations, Changeset, Changeset, DeltaTextOperations) {
let initial_content = initial_content.to_owned();
let initial_delta = DeltaTextOperationBuilder::new().insert(&initial_content).build();
let delta = DeltaTextOperationBuilder::new()
.retain(initial_content.len())
.insert(insert_str)
.build();
let inverted = delta.invert(&initial_delta);
let expected = initial_delta.compose(&delta).unwrap();
let changeset = Changeset::Delta {
delta: delta.clone(),
inverted: inverted.clone(),
};
let inverted_changeset = Changeset::Delta {
delta: inverted,
inverted: delta,
};
(initial_delta, changeset, inverted_changeset, expected)
}