Skip to content

Commit

Permalink
feat: support incremental to update textblock's delta
Browse files Browse the repository at this point in the history
  • Loading branch information
qinluhe committed Aug 16, 2023
1 parent 7f26d56 commit 9547947
Show file tree
Hide file tree
Showing 18 changed files with 820 additions and 99 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions collab-document/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ collab-derive = { path = "../collab-derive" }
collab-persistence = { path = "../collab-persistence" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.94"
lib0 = { version = "0.16.3", features = ["lib0-serde"] }
nanoid = "0.4.0"
thiserror = "1.0.30"
anyhow = "1.0"
Expand Down
15 changes: 14 additions & 1 deletion collab-document/src/blocks/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ impl BlockOperation {
map.insert_with_txn(txn, PARENT, block.parent);
map.insert_with_txn(txn, CHILDREN, block.children);
map.insert_with_txn(txn, DATA, json_str);
map.insert_with_txn(txn, EXTERNAL_ID, block.external_id);
map.insert_with_txn(txn, EXTERNAL_TYPE, block.external_type);

// Create the children for each block.
self
Expand Down Expand Up @@ -106,14 +108,16 @@ impl BlockOperation {
}

/// Update the block with the given id.
/// Except \`data\` and \`parent\`, other fields can not be updated.
/// Except \`data\` and \`parent\` and \'external_id\' and \'external_type\' field, other fields can be updated.
/// If you want to turn into other block, you should delete the block and create a new block.
pub fn set_block_with_txn(
&self,
txn: &mut TransactionMut,
id: &str,
data: Option<HashMap<String, Value>>,
parent_id: Option<&str>,
external_id: Option<String>,
external_type: Option<String>,
) -> Result<(), DocumentError> {
let map = self
.root
Expand All @@ -128,6 +132,15 @@ impl BlockOperation {
if let Some(data) = data {
map.insert_with_txn(txn, DATA, hashmap_to_json_str(data)?);
}

// Update external id and external type.
if let Some(external_id) = external_id {
map.insert_with_txn(txn, EXTERNAL_ID, external_id);
}

if let Some(external_type) = external_type {
map.insert_with_txn(txn, EXTERNAL_TYPE, external_type);
}
Ok(())
}
}
Expand Down
6 changes: 6 additions & 0 deletions collab-document/src/blocks/entities.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use serde::Serialize;
use serde_json;
use serde_json::Value;
use std::collections::HashMap;
use std::hash::Hash;
use std::ops::Deref;

/// [Block] Struct.
Expand All @@ -26,6 +28,10 @@ pub struct Block {
pub struct DocumentMeta {
/// Meta has a children map.
pub children_map: HashMap<String, Vec<String>>,
/// Meta has a text map.
/// - @key: [Block]'s `external_id`
/// - @value: text delta json string - "\[ { "insert": "Hello World!", "attributes": { "bold": true } } \]"
pub text_map: Option<HashMap<String, String>>,
}

#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
Expand Down
4 changes: 4 additions & 0 deletions collab-document/src/blocks/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ mod block;
mod children;
mod entities;
mod subscribe;
mod text;
mod text_entities;
mod utils;

pub use block::*;
pub use children::*;
pub use entities::*;
pub use subscribe::*;
pub use text::*;
pub use text_entities::*;
pub use utils::*;
66 changes: 66 additions & 0 deletions collab-document/src/blocks/text.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
use crate::blocks::text_entities::TextDelta;
use collab::preclude::*;
use std::collections::HashMap;

pub struct TextOperation {
root: MapRefWrapper,
}

impl TextOperation {
pub fn new(root: MapRefWrapper) -> Self {
Self { root }
}

pub fn get_text_with_txn(&self, txn: &mut TransactionMut, text_id: &str) -> TextRefWrapper {
self
.root
.get_text_ref_with_txn(txn, text_id)
.unwrap_or_else(|| self.create_text_with_txn(txn, text_id))
}

pub fn create_text_with_txn(&self, txn: &mut TransactionMut, text_id: &str) -> TextRefWrapper {
self.root.insert_text_with_txn(txn, text_id)
}

pub fn delete_text_with_txn(&self, txn: &mut TransactionMut, text_id: &str) {
self.root.delete_with_txn(txn, text_id);
}

pub fn get_delta_with_txn<T: ReadTxn>(&self, txn: &T, text_id: &str) -> Option<Vec<TextDelta>> {
let text_ref = self.root.get_text_ref_with_txn(txn, text_id)?;
Some(
text_ref
.get_delta_with_txn(txn)
.iter()
.map(|d| TextDelta::from(txn, d.to_owned()))
.collect(),
)
}

pub fn apply_delta_with_txn(
&self,
txn: &mut TransactionMut,
text_id: &str,
delta: Vec<TextDelta>,
) {
let text_ref = self.get_text_with_txn(txn, text_id);
let delta = delta.iter().map(|d| d.to_owned().to_delta()).collect();
text_ref.apply_delta_with_txn(txn, delta);
}

pub fn serialize_all_text_delta(&self) -> HashMap<String, String> {
let txn = self.root.transact();
self
.root
.iter(&txn)
.filter_map(|(k, _)| {
self.get_delta_with_txn(&txn, k).map(|delta| {
(
k.to_string(),
serde_json::to_string(&delta).unwrap_or_default(),
)
})
})
.collect()
}
}
216 changes: 216 additions & 0 deletions collab-document/src/blocks/text_entities.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
use collab::preclude::{Attrs, Delta, ReadTxn, Value as YrsValue};
use lib0::any::Any;
use serde::de::{self, Deserialize, Deserializer, MapAccess, Visitor};
use serde::ser::SerializeMap;
use serde::{Serialize, Serializer};
use std::collections::HashMap;
use std::fmt;
use std::rc::Rc;

const FIELD_INSERT: &str = "insert";
const FIELD_DELETE: &str = "delete";
const FIELD_RETAIN: &str = "retain";
const FIELD_ATTRIBUTES: &str = "attributes";
const FIELDS: &[&str] = &[FIELD_INSERT, FIELD_DELETE, FIELD_RETAIN, FIELD_ATTRIBUTES];

#[derive(Debug, Clone)]
pub enum TextDelta {
/// Determines a change that resulted in insertion of a piece of text, which optionally could have been
/// formatted with provided set of attributes.
Inserted(String, Option<Attrs>),

/// Determines a change that resulted in removing a consecutive range of characters.
Deleted(u32),

/// Determines a number of consecutive unchanged characters. Used to recognize non-edited spaces
/// between [Delta::Inserted] and/or [Delta::Deleted] chunks. Can contain an optional set of
/// attributes, which have been used to format an existing piece of text.
Retain(u32, Option<Attrs>),
}

impl PartialEq for TextDelta {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Inserted(content1, attrs1), Self::Inserted(content2, attrs2)) => {
content1 == content2 && attrs1 == attrs2
},
(Self::Deleted(len1), Self::Deleted(len2)) => len1 == len2,
(Self::Retain(len1, attrs1), Self::Retain(len2, attrs2)) => len1 == len2 && attrs1 == attrs2,
_ => false,

Check warning on line 39 in collab-document/src/blocks/text_entities.rs

View check run for this annotation

Codecov / codecov/patch

collab-document/src/blocks/text_entities.rs#L39

Added line #L39 was not covered by tests
}
}
}

impl Eq for TextDelta {}

impl TextDelta {
pub fn from<T: ReadTxn>(txn: &T, value: Delta) -> Self {
match value {
Delta::Inserted(content, attrs) => {
let content = content.to_string(txn);
Self::Inserted(content, attrs.map(|attrs| *attrs))
},
Delta::Deleted(len) => Self::Deleted(len),
Delta::Retain(len, attrs) => Self::Retain(len, attrs.map(|attrs| *attrs)),
}
}

pub fn to_delta(self) -> Delta {
match self {
Self::Inserted(content, attrs) => {
let content = YrsValue::from(content);
Delta::Inserted(content, attrs.map(Box::new))
},
Self::Deleted(len) => Delta::Deleted(len),
Self::Retain(len, attrs) => Delta::Retain(len, attrs.map(Box::new)),
}
}
}

impl Serialize for TextDelta {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Self::Inserted(content, attrs) => {
let mut map = serializer.serialize_map(Some(2))?;
map.serialize_entry(FIELD_INSERT, content)?;
if let Some(attrs) = attrs {
let attrs_hash = attrs
.iter()
.map(|(k, v)| (k.to_string(), v.to_owned()))
.collect::<HashMap<String, Any>>();
map.serialize_entry(FIELD_ATTRIBUTES, &attrs_hash)?;
}
map.end()
},
Self::Deleted(len) => {
let mut map = serializer.serialize_map(Some(1))?;
map.serialize_entry(FIELD_DELETE, len)?;
map.end()
},
Self::Retain(len, attrs) => {
let mut map = serializer.serialize_map(Some(1))?;
map.serialize_entry(FIELD_RETAIN, len)?;
if let Some(attrs) = attrs {
let attrs_hash = attrs
.iter()
.map(|(k, v)| (k.to_string(), v.to_owned()))
.collect::<HashMap<String, Any>>();
map.serialize_entry(FIELD_ATTRIBUTES, &attrs_hash)?;
}
map.end()
},
}
}
}

impl<'de> Deserialize<'de> for TextDelta {
fn deserialize<D>(deserializer: D) -> Result<TextDelta, D::Error>
where
D: Deserializer<'de>,
{
struct TextDeltaVisitor;

impl<'de> Visitor<'de> for TextDeltaVisitor {
type Value = TextDelta;

fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a valid TextDelta")
}

Check warning on line 121 in collab-document/src/blocks/text_entities.rs

View check run for this annotation

Codecov / codecov/patch

collab-document/src/blocks/text_entities.rs#L120-L121

Added lines #L120 - L121 were not covered by tests

fn visit_map<A>(self, mut map: A) -> Result<TextDelta, A::Error>
where
A: MapAccess<'de>,
{
let mut delta_type: Option<String> = None;
let mut content: Option<String> = None;
let mut len: Option<usize> = None;
let mut attrs: Option<HashMap<Rc<str>, Any>> = None;

while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
FIELD_INSERT => {
if delta_type.is_some() {
return Err(de::Error::duplicate_field(FIELD_INSERT));
}
content = Some(map.next_value()?);
delta_type = Some(key);
},
FIELD_DELETE => {
if delta_type.is_some() {
return Err(de::Error::duplicate_field(FIELD_DELETE));
}
len = Some(map.next_value()?);
delta_type = Some(key);
},
FIELD_RETAIN => {
if delta_type.is_some() {
return Err(de::Error::duplicate_field(FIELD_RETAIN));
}
len = Some(map.next_value()?);
delta_type = Some(key);
},
FIELD_ATTRIBUTES => {
if attrs.is_none() {
attrs = Some(HashMap::new());
}
let attrs_val = map.next_value::<HashMap<String, Any>>()?;
attrs_val.iter().for_each(|(key, val)| {
attrs
.as_mut()
.unwrap()
.insert(Rc::from(key.to_string()), val.clone());
});
},
_ => {
return Err(de::Error::unknown_field(key.as_str(), FIELDS));
},
}
}

match delta_type {
Some(delta_type) => match delta_type.as_str() {
FIELD_INSERT => {
if let Some(attrs) = attrs {
Ok(TextDelta::Inserted(
content.ok_or_else(|| de::Error::missing_field(FIELD_INSERT))?,
Some(attrs),
))
} else {
Ok(TextDelta::Inserted(
content.ok_or_else(|| de::Error::missing_field(FIELD_INSERT))?,
None,
))
}
},
FIELD_DELETE => Ok(TextDelta::Deleted(
len.ok_or_else(|| de::Error::missing_field(FIELD_DELETE))? as u32,
)),
FIELD_RETAIN => {
if let Some(attrs) = attrs {
Ok(TextDelta::Retain(
len.ok_or_else(|| de::Error::missing_field(FIELD_RETAIN))? as u32,
Some(attrs),
))
} else {
Ok(TextDelta::Retain(
len.ok_or_else(|| de::Error::missing_field(FIELD_RETAIN))? as u32,
None,
))
}
},
_ => Err(de::Error::unknown_variant(
&delta_type,
&[FIELD_INSERT, FIELD_DELETE, FIELD_RETAIN],
)),

Check warning on line 207 in collab-document/src/blocks/text_entities.rs

View check run for this annotation

Codecov / codecov/patch

collab-document/src/blocks/text_entities.rs#L205-L207

Added lines #L205 - L207 were not covered by tests
},
None => Err(de::Error::missing_field("delta type")),
}
}
}

deserializer.deserialize_map(TextDeltaVisitor)
}
}
Loading

0 comments on commit 9547947

Please sign in to comment.