Fix: richtext event (#138)

Support rich text event. Now it will emit the delta event correctly in the Quill Delta format.
This commit is contained in:
Zixuan Chen 2023-11-01 20:02:05 +08:00 committed by GitHub
parent a52549ea30
commit 95e6130d93
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 1004 additions and 545 deletions

View file

@ -20,6 +20,7 @@
"tinyvec",
"txns",
"unbold",
"unmark",
"yspan"
],
"rust-analyzer.runnableEnv": {

View file

@ -63,7 +63,7 @@ mod run {
b.iter(|| {
let loro = LoroDoc::default();
let text = loro.get_text("text");
loro.subscribe_deep(Arc::new(move |event| {
loro.subscribe_root(Arc::new(move |event| {
black_box(event);
}));
let mut txn = loro.txn().unwrap();
@ -220,7 +220,7 @@ mod run {
b.iter(|| {
let loro = LoroDoc::default();
let text = loro.get_text("text");
loro.subscribe_deep(Arc::new(move |event| {
loro.subscribe_root(Arc::new(move |event| {
black_box(event);
}));
{

View file

@ -1,6 +1,9 @@
{
"version": "3",
"redirects": {
"https://deno.land/std/fmt/printf.ts": "https://deno.land/std@0.105.0/fmt/printf.ts",
"https://deno.land/std/path/mod.ts": "https://deno.land/std@0.105.0/path/mod.ts",
"https://deno.land/std/testing/asserts.ts": "https://deno.land/std@0.105.0/testing/asserts.ts",
"https://x.nest.land/std@0.73.0/path/mod.ts": "https://lra6z45nakk5lnu3yjchp7tftsdnwwikwr65ocha5eojfnlgu4sa.arweave.net/XEHs860CldW2m8JEd_5lnIbbWQq0fdcI4OkckrVmpyQ/path/mod.ts"
},
"remote": {

View file

@ -16,13 +16,15 @@ pub(crate) mod richtext_state;
mod style_range_map;
mod tracker;
use crate::{change::Lamport, utils::string_slice::StringSlice, InternalString};
use crate::{change::Lamport, delta::StyleMeta, utils::string_slice::StringSlice, InternalString};
use fugue_span::*;
use loro_common::{Counter, LoroValue, PeerID};
use loro_common::{Counter, LoroValue, PeerID, ID};
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
pub(crate) use fugue_span::{RichtextChunk, RichtextChunkValue};
pub(crate) use richtext_state::RichtextState;
pub(crate) use style_range_map::Styles;
pub(crate) use tracker::{CrdtRopeDelta, Tracker as RichtextTracker};
/// This is the data structure that represents a span of rich text.
@ -30,7 +32,7 @@ pub(crate) use tracker::{CrdtRopeDelta, Tracker as RichtextTracker};
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct RichtextSpan {
pub text: StringSlice,
pub styles: Vec<Style>,
pub attributes: StyleMeta,
}
/// This is used to communicate with the frontend.
@ -54,26 +56,76 @@ pub struct StyleOp {
pub(crate) info: TextStyleInfoFlag,
}
#[derive(Debug, Hash, Eq, PartialEq, Clone, Serialize, Deserialize)]
pub(crate) enum StyleKey {
Key(InternalString),
KeyWithId { key: InternalString, id: ID },
}
impl StyleKey {
pub fn to_attr_key(&self) -> String {
match self {
Self::Key(key) => key.to_string(),
Self::KeyWithId { key, id } => format!("id:{}", id),
}
}
pub fn key(&self) -> &InternalString {
match self {
Self::Key(key) => key,
Self::KeyWithId { key, .. } => key,
}
}
}
impl StyleOp {
pub fn to_style(&self) -> Option<Style> {
pub fn to_style(&self) -> Style {
if self.info.is_delete() {
return None;
return Style {
key: self.key.clone(),
data: LoroValue::Bool(false),
};
}
if self.info.is_container() {
Some(Style {
Style {
key: self.key.clone(),
data: LoroValue::Container(loro_common::ContainerID::Normal {
peer: self.peer,
counter: self.cnt,
container_type: loro_common::ContainerType::Map,
}),
})
}
} else {
Some(Style {
Style {
key: self.key.clone(),
data: LoroValue::Bool(true),
}
}
}
pub fn to_value(&self) -> LoroValue {
if self.info.is_delete() {
LoroValue::Bool(false)
} else if self.info.is_container() {
LoroValue::Container(loro_common::ContainerID::Normal {
peer: self.peer,
counter: self.cnt,
container_type: loro_common::ContainerType::Map,
})
} else {
LoroValue::Bool(true)
}
}
pub(crate) fn get_style_key(&self) -> StyleKey {
if !self.info.mergeable() {
StyleKey::KeyWithId {
key: self.key.clone(),
id: self.id(),
}
} else {
StyleKey::Key(self.key.clone())
}
}
@ -87,6 +139,11 @@ impl StyleOp {
info,
}
}
#[inline(always)]
pub fn id(&self) -> ID {
ID::new(self.peer, self.cnt)
}
}
impl PartialOrd for StyleOp {

View file

@ -14,13 +14,11 @@ use std::{
};
use crate::{
container::richtext::{
query_by_len::{EntityIndexQueryWithEventIndex, IndexQueryWithEntityIndex},
style_range_map::StyleValue,
container::richtext::query_by_len::{
EntityIndexQueryWithEventIndex, IndexQueryWithEntityIndex,
},
delta::DeltaValue,
delta::{DeltaValue, Meta, StyleMeta},
utils::{string_slice::unicode_range_to_byte_range, utf16::count_utf16_chars},
InternalString,
};
// FIXME: Check splice and other things are using unicode index
@ -913,6 +911,21 @@ impl RichtextState {
entity_index
}
/// Get the insert text styles at the given entity index if we insert text at that position
///
// TODO: PERF we can avoid this calculation by getting it when inserting new text
// but that requires a lot of changes
pub(crate) fn get_styles_at_entity_index_for_insert(
&mut self,
entity_index: usize,
) -> StyleMeta {
if !self.style_ranges.has_style() {
return Default::default();
}
self.style_ranges.get_styles_for_insert(entity_index)
}
/// This is used to accept changes from DiffCalculator
pub(crate) fn insert_at_entity_index(&mut self, entity_index: usize, text: BytesSlice) {
let elem = RichtextStateChunk::try_from_bytes(text).unwrap();
@ -1531,22 +1544,12 @@ impl RichtextState {
let mut entity_index = 0;
let mut style_range_iter = self.style_ranges.iter();
let mut cur_style_range = style_range_iter.next();
fn to_styles(
(_, style_map): &(Range<usize>, &FxHashMap<InternalString, StyleValue>),
) -> Vec<Style> {
let mut styles = Vec::with_capacity(style_map.len());
for style in style_map.iter().flat_map(|(_, values)| values.to_styles()) {
styles.push(style);
}
styles
}
let mut cur_styles = cur_style_range.as_ref().map(to_styles);
let mut cur_styles: Option<StyleMeta> =
cur_style_range.as_ref().map(|x| x.1.clone().into());
self.tree.iter().filter_map(move |x| match x {
RichtextStateChunk::Text { unicode_len, text } => {
let mut styles = Vec::new();
let mut styles = Default::default();
while let Some((inner_cur_range, _)) = cur_style_range.as_ref() {
if entity_index < inner_cur_range.start {
break;
@ -1557,14 +1560,14 @@ impl RichtextState {
break;
} else {
cur_style_range = style_range_iter.next();
cur_styles = cur_style_range.as_ref().map(to_styles);
cur_styles = cur_style_range.as_ref().map(|x| x.1.clone().into());
}
}
entity_index += *unicode_len as usize;
Some(RichtextSpan {
text: text.clone().into(),
styles,
attributes: styles,
})
}
RichtextStateChunk::Style { .. } => {
@ -1574,17 +1577,18 @@ impl RichtextState {
})
}
#[inline]
pub fn iter_chunk(&self) -> impl Iterator<Item = &RichtextStateChunk> {
self.tree.iter()
}
pub fn get_richtext_value(&self) -> LoroValue {
let mut ans: Vec<LoroValue> = Vec::new();
let mut last_style_set: Option<FxHashSet<_>> = None;
let mut last_attributes: Option<LoroValue> = None;
for span in self.iter() {
let style_set: FxHashSet<Style> = span.styles.iter().cloned().collect();
if let Some(last) = last_style_set.as_ref() {
if &style_set == last {
let attributes: LoroValue = span.attributes.to_value();
if let Some(last) = last_attributes.as_ref() {
if &attributes == last {
let hash_map = ans.last_mut().unwrap().as_map_mut().unwrap();
let s = Arc::make_mut(hash_map)
.get_mut("insert")
@ -1602,17 +1606,12 @@ impl RichtextState {
LoroValue::String(Arc::new(span.text.as_str().into())),
);
if !span.styles.is_empty() {
let mut styles = FxHashMap::default();
for style in span.styles.iter() {
styles.insert(style.key.to_string(), style.data.clone());
}
value.insert("attributes".into(), LoroValue::Map(Arc::new(styles)));
if !span.attributes.is_empty() {
value.insert("attributes".into(), attributes.clone());
}
ans.push(LoroValue::Map(Arc::new(value)));
last_style_set = Some(style_set);
last_attributes = Some(attributes);
}
LoroValue::List(Arc::new(ans))
@ -1669,9 +1668,9 @@ impl RichtextState {
#[cfg(test)]
mod test {
use append_only_bytes::AppendOnlyBytes;
use loro_common::{ContainerID, ContainerType, LoroValue, ID};
use serde_json::json;
use crate::container::richtext::TextStyleInfoFlag;
use crate::{container::richtext::TextStyleInfoFlag, ToJson};
use super::*;
@ -1744,58 +1743,47 @@ mod test {
wrapper.insert(0, "Hello World!");
wrapper.mark(0..5, bold(0));
assert_eq!(
wrapper.state.to_vec(),
vec![
RichtextSpan {
text: "Hello".into(),
styles: vec![Style {
key: "bold".into(),
data: LoroValue::Bool(true)
}]
wrapper.state.get_richtext_value().to_json_value(),
json!([
{
"insert": "Hello",
"attributes": {
"bold": true
}
},
RichtextSpan {
text: " World!".into(),
styles: vec![]
{
"insert": " World!"
}
]
])
);
wrapper.mark(2..7, link(1));
dbg!(&wrapper.state);
assert_eq!(
wrapper.state.to_vec(),
vec![
RichtextSpan {
text: "He".into(),
styles: vec![Style {
key: "bold".into(),
data: LoroValue::Bool(true)
}]
wrapper.state.get_richtext_value().to_json_value(),
json!([
{
"insert": "He",
"attributes": {
"bold": true
}
},
RichtextSpan {
text: "llo".into(),
styles: vec![
Style {
key: "bold".into(),
data: LoroValue::Bool(true)
},
Style {
key: "link".into(),
data: LoroValue::Bool(true)
}
]
{
"insert": "llo",
"attributes": {
"bold": true,
"link": true
}
},
RichtextSpan {
text: " W".into(),
styles: vec![Style {
key: "link".into(),
data: LoroValue::Bool(true)
}]
{
"insert": " W",
"attributes": {
"link": true
}
},
RichtextSpan {
text: "orld!".into(),
styles: vec![]
{
"insert": "orld!"
}
]
])
);
}
@ -1805,49 +1793,44 @@ mod test {
wrapper.insert(0, "Hello World!");
wrapper.delete(0, 5);
assert_eq!(
wrapper.state.to_vec(),
vec![RichtextSpan {
text: " World!".into(),
styles: vec![]
}]
wrapper.state.get_richtext_value().to_json_value(),
json!([
{
"insert": " World!"
}
])
);
wrapper.delete(1, 1);
dbg!(&wrapper.state);
assert_eq!(
wrapper.state.to_vec(),
vec![
RichtextSpan {
text: " ".into(),
styles: vec![]
},
RichtextSpan {
text: "orld!".into(),
styles: vec![]
wrapper.state.get_richtext_value().to_json_value(),
json!([
{
"insert": " orld!"
}
]
])
);
wrapper.delete(5, 1);
assert_eq!(
wrapper.state.to_vec(),
vec![
RichtextSpan {
text: " ".into(),
styles: vec![]
},
RichtextSpan {
text: "orld".into(),
styles: vec![]
wrapper.state.get_richtext_value().to_json_value(),
json!([
{
"insert": " orld"
}
]
])
);
wrapper.delete(0, 5);
assert_eq!(wrapper.state.to_vec(), vec![]);
assert_eq!(
wrapper.state.get_richtext_value().to_json_value(),
json!([])
);
}
#[test]
#[ignore]
fn insert_cache_hit() {
let mut wrapper = SimpleWrapper::default();
wrapper.insert(0, "H");
@ -1866,27 +1849,18 @@ mod test {
wrapper.mark(0..5, bold(0));
wrapper.insert(5, " Test");
assert_eq!(
wrapper.state.to_vec(),
vec![
RichtextSpan {
text: "Hello".into(),
styles: vec![Style {
key: "bold".into(),
data: LoroValue::Bool(true)
}]
wrapper.state.get_richtext_value().to_json_value(),
json!([
{
"insert": "Hello Test",
"attributes": {
"bold": true
}
},
RichtextSpan {
text: " Test".into(),
styles: vec![Style {
key: "bold".into(),
data: LoroValue::Bool(true)
}]
},
RichtextSpan {
text: " World!".into(),
styles: vec![]
{
"insert": " World!"
}
]
])
);
}
@ -1897,24 +1871,18 @@ mod test {
wrapper.mark(0..5, link(0));
wrapper.insert(5, " Test");
assert_eq!(
wrapper.state.to_vec(),
vec![
RichtextSpan {
text: "Hello".into(),
styles: vec![Style {
key: "link".into(),
data: LoroValue::Bool(true)
}]
wrapper.state.get_richtext_value().to_json_value(),
json!([
{
"insert": "Hello",
"attributes": {
"link": true
}
},
RichtextSpan {
text: " Test".into(),
styles: vec![]
{
"insert": " Test World!"
},
RichtextSpan {
text: " World!".into(),
styles: vec![]
}
]
])
);
}
@ -1924,11 +1892,12 @@ mod test {
wrapper.insert(0, "Hello");
wrapper.insert(5, " World!");
assert_eq!(
wrapper.state.to_vec(),
vec![RichtextSpan {
text: "Hello World!".into(),
styles: vec![]
},]
wrapper.state.get_richtext_value().to_json_value(),
json!([
{
"insert": "Hello World!"
},
])
);
}
@ -1940,14 +1909,15 @@ mod test {
wrapper.insert(5, " World!");
dbg!(&wrapper.state);
assert_eq!(
wrapper.state.to_vec(),
vec![RichtextSpan {
text: "Hello World!".into(),
styles: vec![Style {
key: "bold".into(),
data: LoroValue::Bool(true)
}]
},]
wrapper.state.get_richtext_value().to_json_value(),
json!([
{
"insert": "Hello World!",
"attributes": {
"bold": true
}
},
])
);
}
@ -1958,20 +1928,19 @@ mod test {
wrapper.mark(0..5, link(0));
wrapper.insert(5, " World!");
assert_eq!(
wrapper.state.to_vec(),
vec![
RichtextSpan {
text: "Hello".into(),
styles: vec![Style {
key: "link".into(),
data: LoroValue::Bool(true)
},]
wrapper.state.get_richtext_value().to_json_value(),
json!([
{
"insert": "Hello",
"attributes": {
"link": true
}
},
RichtextSpan {
text: " World!".into(),
styles: vec![]
},
]
{
"insert": " World!",
}
])
);
}
@ -1982,73 +1951,61 @@ mod test {
wrapper.mark(0..12, bold(0));
wrapper.mark(5..12, unbold(1));
assert_eq!(
wrapper.state.to_vec(),
vec![
RichtextSpan {
text: "Hello".into(),
styles: vec![Style {
key: "bold".into(),
data: LoroValue::Bool(true)
}]
wrapper.state.get_richtext_value().to_json_value(),
json!([
{
"insert": "Hello",
"attributes": {
"bold": true
}
},
RichtextSpan {
text: " World!".into(),
styles: vec![]
{
"insert": " World!",
"attributes": {
"bold": false
}
}
]
])
);
wrapper.insert(5, "A");
assert_eq!(
wrapper.state.to_vec(),
vec![
RichtextSpan {
text: "Hello".into(),
styles: vec![Style {
key: "bold".into(),
data: LoroValue::Bool(true)
}]
wrapper.state.get_richtext_value().to_json_value(),
json!([
{
"insert": "HelloA",
"attributes": {
"bold": true
}
},
RichtextSpan {
text: "A".into(),
styles: vec![Style {
key: "bold".into(),
data: LoroValue::Bool(true)
}]
},
RichtextSpan {
text: " World!".into(),
styles: vec![]
{
"insert": " World!",
"attributes": {
"bold": false
}
}
]
])
);
wrapper.insert(0, "A");
assert_eq!(
wrapper.state.to_vec(),
vec![
RichtextSpan {
text: "A".into(),
styles: vec![]
wrapper.state.get_richtext_value().to_json_value(),
json!([
{
"insert": "A",
},
RichtextSpan {
text: "Hello".into(),
styles: vec![Style {
key: "bold".into(),
data: LoroValue::Bool(true)
}]
{
"insert": "HelloA",
"attributes": {
"bold": true
}
},
RichtextSpan {
text: "A".into(),
styles: vec![Style {
key: "bold".into(),
data: LoroValue::Bool(true)
}]
},
RichtextSpan {
text: " World!".into(),
styles: vec![]
{
"insert": " World!",
"attributes": {
"bold": false
}
}
]
])
);
}
@ -2059,31 +2016,23 @@ mod test {
wrapper.mark(0..5, link(0));
wrapper.mark(0..5, bold(1));
wrapper.insert(5, "A");
assert_eq!(
wrapper.state.to_vec(),
vec![
RichtextSpan {
text: "Hello".into(),
styles: vec![
Style {
key: "bold".into(),
data: LoroValue::Bool(true)
},
Style {
key: "link".into(),
data: LoroValue::Bool(true)
}
]
wrapper.state.get_richtext_value().to_json_value(),
json!([
{
"insert": "Hello",
"attributes": {
"bold": true,
"link": true
}
},
RichtextSpan {
text: "A".into(),
styles: vec![Style {
key: "bold".into(),
data: LoroValue::Bool(true)
}]
{
"insert": "A",
"attributes": {
"bold": true,
}
},
]
])
);
}
@ -2094,52 +2043,44 @@ mod test {
wrapper.mark(0..5, comment(0));
wrapper.mark(1..6, comment(1));
assert_eq!(
wrapper.state.to_vec(),
vec![
RichtextSpan {
text: "H".into(),
styles: vec![Style {
key: "comment".into(),
data: LoroValue::Container(ContainerID::new_normal(
ID::new(0, 0),
ContainerType::Map
))
},]
},
RichtextSpan {
text: "ello".into(),
styles: vec![
Style {
key: "comment".into(),
data: LoroValue::Container(ContainerID::new_normal(
ID::new(0, 0),
ContainerType::Map
))
wrapper.state.get_richtext_value().to_json_value(),
json!([
{
"insert": "H",
"attributes": {
"id:0@0": {
"key": "comment",
"data": null
},
Style {
key: "comment".into(),
data: LoroValue::Container(ContainerID::new_normal(
ID::new(1, 1),
ContainerType::Map
))
},
},
{
"insert": "ello",
"attributes": {
"id:0@0": {
"key": "comment",
"data": null
},
]
"id:1@1": {
"key": "comment",
"data": null
}
},
},
RichtextSpan {
text: " ".into(),
styles: vec![Style {
key: "comment".into(),
data: LoroValue::Container(ContainerID::new_normal(
ID::new(1, 1),
ContainerType::Map
))
},]
{
"insert": " ",
"attributes": {
"id:1@1": {
"key": "comment",
"data": null
}
},
},
RichtextSpan {
text: "World!".into(),
styles: vec![]
},
]
{
"insert": "World!",
}
])
);
}
@ -2157,11 +2098,10 @@ mod test {
assert_eq!(count, 2);
assert_eq!(
wrapper.state.to_vec(),
vec![RichtextSpan {
text: " World!".into(),
styles: vec![]
},]
wrapper.state.get_richtext_value().to_json_value(),
json!([{
"insert": " World!"
}])
);
}
}

View file

@ -13,11 +13,12 @@ use generic_btree::{
rle::{HasLength, Mergeable, Sliceable},
BTree, BTreeTrait, LengthFinder, UseLengthFinder,
};
use once_cell::sync::Lazy;
use crate::InternalString;
use crate::delta::StyleMeta;
use super::{Style, StyleOp};
use super::{Style, StyleKey, StyleOp};
/// This struct keep the mapping of ranges to numbers
///
@ -31,7 +32,7 @@ pub(super) struct StyleRangeMap {
#[derive(Debug, Clone)]
pub(super) struct RangeNumMapTrait;
pub(crate) type Styles = FxHashMap<InternalString, StyleValue>;
pub(crate) type Styles = FxHashMap<StyleKey, StyleValue>;
pub(super) static EMPTY_STYLES: Lazy<Styles> =
Lazy::new(|| HashMap::with_hasher(Default::default()));
@ -42,10 +43,27 @@ pub(super) struct Elem {
len: usize,
}
#[derive(Default, Clone, Debug, PartialEq, Eq)]
#[derive(Clone, Default, Debug, PartialEq, Eq)]
pub(crate) struct StyleValue {
// we need a set here because we need to calculate the intersection of styles when
// users insert new text between two style sets
set: BTreeSet<Arc<StyleOp>>,
should_merge: bool,
}
impl StyleValue {
pub fn union(&mut self, other: &Self) {
for op in other.set.iter() {
self.set.insert(op.clone());
}
}
pub fn insert(&mut self, value: Arc<StyleOp>) {
self.set.insert(value);
}
pub fn get(&self) -> Option<&Arc<StyleOp>> {
self.set.last()
}
}
impl Default for StyleRangeMap {
@ -54,24 +72,6 @@ impl Default for StyleRangeMap {
}
}
impl StyleValue {
pub fn new(mergeable: bool) -> Self {
Self {
set: Default::default(),
should_merge: mergeable,
}
}
// PERF: can we avoid this box
pub fn to_styles(&self) -> Box<dyn Iterator<Item = Style> + '_> {
if self.should_merge {
Box::new(self.set.iter().rev().take(1).filter_map(|x| x.to_style()))
} else {
Box::new(self.set.iter().filter_map(|x| x.to_style()))
}
}
}
impl StyleRangeMap {
pub fn new() -> Self {
let mut tree = BTree::new();
@ -98,19 +98,13 @@ impl StyleRangeMap {
debug_log::debug_log!("Range={:?}", &range);
self.tree
.update(range.start.cursor..range.end.cursor, &mut |x| {
// only leave one value with the greatest lamport if the style is mergeable
if let Some(set) = x.styles.get_mut(&style.key) {
if let Some(set) = x.styles.get_mut(&style.get_style_key()) {
set.set.insert(style.clone());
// TODO: Doc this, and validate it earlier
assert_eq!(
set.should_merge,
style.info.mergeable(),
"Merge behavior should be the same for the same style key"
);
} else {
let mut style_set = StyleValue::new(style.info.mergeable());
style_set.set.insert(style.clone());
x.styles.insert(style.key.clone(), style_set);
let key = style.get_style_key();
let mut value = StyleValue::default();
value.insert(style.clone());
x.styles.insert(key, value);
}
None
@ -179,19 +173,38 @@ impl StyleRangeMap {
return &self.tree.get_elem(right.leaf).unwrap().styles;
}
#[allow(unused)]
pub fn get(&mut self, index: usize) -> Option<&FxHashMap<InternalString, StyleValue>> {
if !self.has_style {
return None;
/// Return the style sets beside `index` and get the intersection of them.
pub fn get_styles_for_insert(&self, index: usize) -> StyleMeta {
if index == 0 || !self.has_style {
return StyleMeta::default();
}
let result = self.tree.query::<LengthFinder>(&index)?.cursor;
self.tree.get_elem(result.leaf).map(|x| &x.styles)
let left = self
.tree
.query::<LengthFinder>(&(index - 1))
.unwrap()
.cursor;
let right = self.tree.shift_path_by_one_offset(left).unwrap();
if left.leaf == right.leaf {
let styles = &self.tree.get_elem(left.leaf).unwrap().styles;
styles.clone().into()
} else {
let mut styles = self.tree.get_elem(left.leaf).unwrap().styles.clone();
let right_styles = &self.tree.get_elem(right.leaf).unwrap().styles;
styles.retain(|key, value| {
if let Some(right_value) = right_styles.get(key) {
value.set.retain(|x| right_value.set.contains(x));
return !value.set.is_empty();
}
false
});
styles.into()
}
}
pub fn iter(
&self,
) -> impl Iterator<Item = (Range<usize>, &FxHashMap<InternalString, StyleValue>)> + '_ {
pub fn iter(&self) -> impl Iterator<Item = (Range<usize>, &Styles)> + '_ {
let mut index = 0;
self.tree.iter().filter_map(move |elem| {
let len = elem.len;
@ -209,7 +222,7 @@ impl StyleRangeMap {
pub fn iter_from(
&self,
start_entity_index: usize,
) -> impl Iterator<Item = (Range<usize>, &FxHashMap<InternalString, StyleValue>)> + '_ {
) -> impl Iterator<Item = (Range<usize>, &Styles)> + '_ {
let start = self
.tree
.query::<LengthFinder>(&start_entity_index)
@ -331,6 +344,18 @@ impl BTreeTrait for RangeNumMapTrait {
}
}
pub(super) fn map_to_styles(style_map: &Styles) -> Vec<Style> {
let mut styles = Vec::with_capacity(style_map.len());
for style in style_map
.iter()
.filter_map(|(_, values)| values.get().map(|x| x.to_style()))
{
styles.push(style);
}
styles
}
#[cfg(test)]
mod test {
use loro_common::PeerID;

View file

@ -5,6 +5,6 @@ pub use map::{MapDiff, ValuePair};
mod map_delta;
pub use map_delta::{MapDelta, MapValue};
mod text;
pub use text::StyleMeta;
pub use text::{StyleMeta, StyleMetaItem};
mod tree;
pub use tree::{TreeDelta, TreeDiff, TreeDiffItem};

View file

@ -526,9 +526,11 @@ impl<Value: DeltaValue, M: Meta> Delta<Value, M> {
if concat_rest {
let vec = this_iter.rest();
if vec.is_empty() {
debug_log::debug_dbg!(&delta);
return delta.chop();
}
let rest = Delta { vec };
debug_log::debug_dbg!(&delta, &rest);
return delta.concat(rest).chop();
}
} else if other_op.is_delete() && this_op.is_retain() {
@ -542,6 +544,7 @@ impl<Value: DeltaValue, M: Meta> Delta<Value, M> {
}
}
}
debug_log::debug_dbg!(&delta);
delta.chop()
}

View file

@ -1,24 +1,153 @@
use crate::container::richtext::Style;
use std::sync::Arc;
use fxhash::FxHashMap;
use loro_common::{LoroValue, PeerID};
use serde::{Deserialize, Serialize};
use crate::change::Lamport;
use crate::container::richtext::{Style, StyleKey, Styles};
use crate::ToJson;
use super::Meta;
#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize)]
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StyleMeta {
pub vec: Vec<Style>,
map: FxHashMap<StyleKey, StyleMetaItem>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StyleMetaItem {
// We need lamport and peer to compose the event
pub lamport: Lamport,
pub peer: PeerID,
pub value: LoroValue,
}
impl StyleMetaItem {
pub fn try_replace(&mut self, other: &StyleMetaItem) {
if (self.lamport, self.peer) < (other.lamport, other.peer) {
self.lamport = other.lamport;
self.peer = other.peer;
self.value = other.value.clone();
}
}
}
impl From<Styles> for StyleMeta {
fn from(styles: Styles) -> Self {
let mut map = FxHashMap::with_capacity_and_hasher(styles.len(), Default::default());
for (key, value) in styles.iter() {
if let Some(value) = value.get() {
map.insert(
key.clone(),
StyleMetaItem {
value: value.to_value(),
lamport: value.lamport,
peer: value.peer,
},
);
}
}
Self { map }
}
}
impl Meta for StyleMeta {
fn is_empty(&self) -> bool {
self.vec.is_empty()
self.map.is_empty()
}
fn compose(&mut self, _other: &Self, _type_pair: (super::DeltaType, super::DeltaType)) {}
fn compose(&mut self, other: &Self, _type_pair: (super::DeltaType, super::DeltaType)) {
for (key, value) in other.map.iter() {
match self.map.get_mut(key) {
Some(old_value) => {
old_value.try_replace(value);
}
None => {
self.map.insert(key.clone(), value.clone());
}
}
}
}
fn is_mergeable(&self, other: &Self) -> bool {
self.vec == other.vec
self.map == other.map
}
fn merge(&mut self, other: &Self) {
self.vec.extend_from_slice(&other.vec)
fn merge(&mut self, other: &Self) {}
}
impl StyleMeta {
pub(crate) fn iter(&self) -> impl Iterator<Item = (StyleKey, Style)> + '_ {
self.map.iter().map(|(key, style)| {
(
key.clone(),
Style {
key: key.key().clone(),
data: style.value.clone(),
},
)
})
}
pub(crate) fn insert(&mut self, key: StyleKey, value: StyleMetaItem) {
self.map.insert(key, value);
}
pub(crate) fn clear(&mut self) {
self.map.clear()
}
pub(crate) fn to_value(&self) -> LoroValue {
LoroValue::Map(Arc::new(
self.map
.iter()
.map(|(key, value)| {
(
key.to_attr_key(),
match &value.value {
LoroValue::Null | LoroValue::Bool(_) => value.value.clone(),
LoroValue::Map(_) => {
let mut map: FxHashMap<String, LoroValue> = Default::default();
map.insert("key".into(), key.key().to_string().into());
map.insert("data".into(), value.value.clone());
LoroValue::Map(Arc::new(map))
}
LoroValue::Container(_) => {
let mut map: FxHashMap<String, LoroValue> = Default::default();
map.insert("key".into(), key.key().to_string().into());
map.insert("data".into(), LoroValue::Null);
LoroValue::Map(Arc::new(map))
}
_ => unreachable!(),
},
)
})
.collect(),
))
}
}
impl ToJson for StyleMeta {
fn to_json_value(&self) -> serde_json::Value {
let mut map = serde_json::Map::new();
for (key, style) in self.iter() {
let value = if matches!(style.data, LoroValue::Null | LoroValue::Bool(_)) {
serde_json::to_value(&style.data).unwrap()
} else {
let mut value = serde_json::Map::new();
value.insert("key".to_string(), style.key.to_string().into());
let data = serde_json::to_value(&style.data).unwrap();
value.insert("data".to_string(), data);
value.into()
};
map.insert(key.to_attr_key(), value);
}
serde_json::Value::Object(map)
}
fn from_json(s: &str) -> Self {
unreachable!()
}
}

View file

@ -100,7 +100,7 @@ mod test {
#[test]
fn test_text_event() {
let loro = LoroDoc::new();
loro.subscribe_deep(Arc::new(|event| {
loro.subscribe_root(Arc::new(|event| {
let mut value = LoroValue::String(Default::default());
value.apply_diff(&[event.container.diff.clone()]);
assert_eq!(value, "h223ello".into());

View file

@ -87,7 +87,7 @@ impl Actor {
};
let root_value = Arc::clone(&actor.value_tracker);
actor.loro.subscribe_deep(Arc::new(move |event| {
actor.loro.subscribe_root(Arc::new(move |event| {
let mut root_value = root_value.lock().unwrap();
root_value.apply(
&event.container.path.iter().map(|x| x.1.clone()).collect(),

View file

@ -93,7 +93,7 @@ impl Actor {
};
let root_value = Arc::clone(&actor.value_tracker);
actor.loro.subscribe_deep(Arc::new(move |event| {
actor.loro.subscribe_root(Arc::new(move |event| {
let mut root_value = root_value.lock().unwrap();
debug_dbg!(&event);
root_value.apply(

View file

@ -252,17 +252,17 @@ impl TextHandler {
return Ok(());
}
let entity_index = self
let (entity_index, styles) = self
.state
.upgrade()
.unwrap()
.lock()
.unwrap()
.with_state_mut(self.container_idx, |state| {
state
.as_richtext_state_mut()
.unwrap()
.get_entity_index_for_text_insert_event_index(pos)
let richtext_state = &mut state.as_richtext_state_mut().unwrap();
let pos = richtext_state.get_entity_index_for_text_insert(pos);
let styles = richtext_state.get_styles_at_entity_index(pos);
(pos, styles)
});
let unicode_len = s.chars().count();
@ -278,7 +278,7 @@ impl TextHandler {
EventHint::InsertText {
pos: pos as u32,
// FIXME: this is wrong
styles: vec![],
styles,
len: unicode_len as u32,
},
&self.state,
@ -393,11 +393,11 @@ impl TextHandler {
state
.as_richtext_state_mut()
.unwrap()
.get_entity_index_for_text_insert_event_index(start),
.get_entity_index_for_text_insert(start),
state
.as_richtext_state_mut()
.unwrap()
.get_entity_index_for_text_insert_event_index(end),
.get_entity_index_for_text_insert(end),
)
});
@ -412,10 +412,17 @@ impl TextHandler {
EventHint::Mark {
start: start as u32,
end: end as u32,
info: flag,
style: crate::container::richtext::Style {
key: key.into(),
// FIXME: style meta is incorrect
data: LoroValue::Bool(true),
data: if flag.is_delete() {
LoroValue::Bool(false)
} else if flag.mergeable() {
LoroValue::Bool(true)
} else {
// for non-mergeable type like comment.
LoroValue::Null
},
},
},
&self.state,
@ -1284,7 +1291,7 @@ mod test {
.unwrap();
let loro2 = LoroDoc::new();
loro2.subscribe_deep(Arc::new(|e| println!("{} {:?} ", e.doc.local, e.doc.diff)));
loro2.subscribe_root(Arc::new(|e| println!("{} {:?} ", e.doc.local, e.doc.diff)));
loro2.import(&loro.export_from(&loro2.oplog_vv())).unwrap();
assert_eq!(loro.get_deep_value(), loro2.get_deep_value());
}

View file

@ -214,11 +214,20 @@ impl LoroDoc {
/// Commit the cumulative auto commit transaction.
/// This method only has effect when `auto_commit` is true.
///
/// Afterwards, the users need to call `self.renew_txn_after_commit()` to resume the continuous transaction.
#[inline]
pub fn commit(&self) {
pub fn commit_then_stop(&self) {
self.commit_with(None, None, false)
}
/// Commit the cumulative auto commit transaction.
/// It will start the next one immediately
#[inline]
pub fn commit_then_renew(&self) {
self.commit_with(None, None, true)
}
/// Commit the cumulative auto commit transaction.
/// This method only has effect when `auto_commit` is true.
/// If `immediate_renew` is true, a new transaction will be created after the old one is commited
@ -323,7 +332,7 @@ impl LoroDoc {
}
pub fn export_from(&self, vv: &VersionVector) -> Vec<u8> {
self.commit();
self.commit_then_stop();
let ans = self.oplog.lock().unwrap().export_from(vv);
self.renew_txn_if_auto_commit();
ans
@ -336,14 +345,14 @@ impl LoroDoc {
#[inline]
pub fn import_without_state(&mut self, bytes: &[u8]) -> Result<(), LoroError> {
self.commit();
self.commit_then_stop();
self.detach();
self.import(bytes)
}
#[inline]
pub fn import_with(&self, bytes: &[u8], origin: InternalString) -> Result<(), LoroError> {
self.commit();
self.commit_then_stop();
let ans = self._import_with(bytes, origin);
self.renew_txn_if_auto_commit();
ans
@ -412,7 +421,7 @@ impl LoroDoc {
}
pub fn export_snapshot(&self) -> Vec<u8> {
self.commit();
self.commit_then_stop();
debug_log::group!("export snapshot");
let version = ENCODE_SCHEMA_VERSION;
let mut ans = Vec::from(MAGIC_BYTES);
@ -500,13 +509,13 @@ impl LoroDoc {
self.oplog().lock().unwrap().cmp_frontiers(other)
}
pub fn subscribe_deep(&self, callback: Subscriber) -> SubID {
pub fn subscribe_root(&self, callback: Subscriber) -> SubID {
let mut state = self.state.lock().unwrap();
if !state.is_recording() {
state.start_recording();
}
self.observer.subscribe_deep(callback)
self.observer.subscribe_root(callback)
}
pub fn subscribe(&self, container_id: &ContainerID, callback: Subscriber) -> SubID {
@ -525,7 +534,7 @@ impl LoroDoc {
// PERF: opt
pub fn import_batch(&mut self, bytes: &[Vec<u8>]) -> LoroResult<()> {
self.commit();
self.commit_then_stop();
let is_detached = self.is_detached();
self.detach();
self.oplog.lock().unwrap().batch_importing = true;
@ -580,7 +589,7 @@ impl LoroDoc {
/// This will make the current [DocState] detached from the latest version of [OpLog].
/// Any further import will not be reflected on the [DocState], until user call [LoroDoc::attach()]
pub fn checkout(&mut self, frontiers: &Frontiers) -> LoroResult<()> {
self.commit();
self.commit_then_stop();
let oplog = self.oplog.lock().unwrap();
let mut state = self.state.lock().unwrap();
self.detached = true;

View file

@ -69,7 +69,7 @@ impl Observer {
sub_id
}
pub fn subscribe_deep(&self, callback: Subscriber) -> SubID {
pub fn subscribe_root(&self, callback: Subscriber) -> SubID {
let sub_id = self.fetch_add_next_id();
let mut inner = self.inner.lock().unwrap();
inner.subscribers.insert(sub_id, callback);
@ -226,7 +226,7 @@ mod test {
let loro_cp = loro.clone();
let count = Arc::new(AtomicUsize::new(0));
let count_cp = Arc::clone(&count);
loro_cp.subscribe_deep(Arc::new(move |_| {
loro_cp.subscribe_root(Arc::new(move |_| {
count_cp.fetch_add(1, Ordering::SeqCst);
let mut txn = loro.txn().unwrap();
let text = loro.get_text("id");
@ -251,7 +251,7 @@ mod test {
let loro = Arc::new(LoroDoc::new());
let count = Arc::new(AtomicUsize::new(0));
let count_cp = Arc::clone(&count);
let sub = loro.subscribe_deep(Arc::new(move |_| {
let sub = loro.subscribe_root(Arc::new(move |_| {
count_cp.fetch_add(1, Ordering::SeqCst);
}));

View file

@ -10,7 +10,7 @@ use crate::{
container::{
idx::ContainerIdx,
richtext::{
richtext_state::PosType, AnchorType, RichtextState as InnerState, StyleOp,
richtext_state::PosType, AnchorType, RichtextState as InnerState, Style, StyleOp,
TextStyleInfoFlag,
},
},
@ -137,11 +137,17 @@ impl ContainerState for RichtextState {
unreachable!()
};
// PERF: compose delta
let mut ans: Delta<StringSlice, StyleMeta> = Delta::new();
let mut style_delta: Delta<StringSlice, StyleMeta> = Delta::new();
struct Pos {
entity_index: usize,
event_index: usize,
}
let mut style_starts: FxHashMap<Arc<StyleOp>, Pos> = FxHashMap::default();
let mut entity_index = 0;
let mut event_index = 0;
let mut last_style_index = 0;
let mut style_starts: FxHashMap<Arc<StyleOp>, usize> = FxHashMap::default();
for span in richtext.vec.iter() {
match span {
crate::delta::DeltaItem::Retain { len, .. } => {
@ -157,12 +163,7 @@ impl ContainerState for RichtextState {
text: text.clone(),
},
);
let insert_styles = StyleMeta {
vec: styles
.iter()
.flat_map(|(_, value)| value.to_styles())
.collect(),
};
let insert_styles = styles.clone().into();
if pos > event_index {
let mut new_len = 0;
@ -172,15 +173,7 @@ impl ContainerState for RichtextState {
.iter_styles_in_event_index_range(event_index..pos)
{
new_len += len;
ans = ans.retain_with_meta(
len,
StyleMeta {
vec: styles
.iter()
.flat_map(|(_, value)| value.to_styles())
.collect(),
},
);
ans = ans.retain_with_meta(len, styles.clone().into());
}
assert_eq!(new_len, pos - event_index);
@ -194,31 +187,52 @@ impl ContainerState for RichtextState {
ans = ans
.insert_with_meta(StringSlice::from(text.clone()), insert_styles);
}
RichtextStateChunk::Style { style, anchor_type } => {
match anchor_type {
AnchorType::Start => {}
AnchorType::End => {
last_style_index = event_index;
}
}
self.state.get_mut().insert_elem_at_entity_index(
entity_index,
RichtextStateChunk::Style {
style: style.clone(),
anchor_type: *anchor_type,
},
);
RichtextStateChunk::Style { anchor_type, style } => {
let (event_index, _) =
self.state.get_mut().insert_elem_at_entity_index(
entity_index,
RichtextStateChunk::Style {
style: style.clone(),
anchor_type: *anchor_type,
},
);
if *anchor_type == AnchorType::Start {
style_starts.insert(style.clone(), entity_index);
style_starts.insert(
style.clone(),
Pos {
entity_index,
event_index,
},
);
} else {
let start_pos =
style_starts.get(style).expect("Style start not found");
// get the pair of style anchor. now we can annotate the range
let Pos {
entity_index: start_entity_index,
event_index: start_event_index,
} = style_starts.remove(style).unwrap();
// we need to + 1 because we also need to annotate the end anchor
self.state.get_mut().annotate_style_range(
*start_pos..entity_index + 1,
start_entity_index..entity_index + 1,
style.clone(),
);
let mut meta = StyleMeta::default();
meta.insert(
style.get_style_key(),
crate::delta::StyleMetaItem {
lamport: style.lamport,
peer: style.peer,
value: style.to_value(),
},
);
let delta: Delta<StringSlice, _> = Delta::new()
.retain(start_event_index)
.retain_with_meta(event_index - start_event_index, meta);
dbg!(&delta);
style_delta = style_delta.compose(delta);
}
}
}
@ -235,15 +249,7 @@ impl ContainerState for RichtextState {
.get_mut()
.iter_styles_in_event_index_range(event_index..start)
{
ans = ans.retain_with_meta(
len,
StyleMeta {
vec: styles
.iter()
.flat_map(|(_, value)| value.to_styles())
.collect(),
},
);
ans = ans.retain_with_meta(len, styles.clone().into());
}
event_index = start;
@ -254,25 +260,8 @@ impl ContainerState for RichtextState {
}
}
if last_style_index > event_index {
for (len, styles) in self
.state
.get_mut()
.iter_styles_in_event_index_range(event_index..last_style_index)
{
ans = ans.retain_with_meta(
len,
StyleMeta {
vec: styles
.iter()
.flat_map(|(_, value)| value.to_styles())
.collect(),
},
);
}
}
Diff::Text(ans)
debug_assert!(style_starts.is_empty(), "Styles should always be paired");
Diff::Text(ans.compose(style_delta))
}
fn apply_diff(&mut self, diff: InternalDiff, _arena: &SharedArena) {
@ -405,7 +394,7 @@ impl ContainerState for RichtextState {
for span in self.state.get_mut().iter() {
delta.vec.push(DeltaItem::Insert {
value: span.text,
meta: StyleMeta { vec: span.styles },
meta: span.attributes,
})
}
@ -494,10 +483,17 @@ impl RichtextState {
}
#[inline(always)]
pub(crate) fn get_entity_index_for_text_insert_event_index(&mut self, pos: usize) -> usize {
pub(crate) fn get_entity_index_for_text_insert(&mut self, event_index: usize) -> usize {
self.state
.get_mut()
.get_entity_index_for_text_insert(pos, PosType::Event)
.get_entity_index_for_text_insert(event_index, PosType::Event)
}
#[inline(always)]
pub(crate) fn get_styles_at_entity_index(&mut self, entity_index: usize) -> StyleMeta {
self.state
.get_mut()
.get_styles_at_entity_index_for_insert(entity_index)
}
#[inline(always)]

View file

@ -11,8 +11,13 @@ use rle::{HasLength, Mergable, RleVec, Sliceable};
use crate::{
change::{get_sys_timestamp, Change, Lamport, Timestamp},
container::{idx::ContainerIdx, list::list_op::DeleteSpan, richtext::Style, IntoContainerId},
delta::{Delta, MapValue, TreeDelta, TreeDiff},
container::{
idx::ContainerIdx,
list::list_op::DeleteSpan,
richtext::{Style, StyleKey, TextStyleInfoFlag},
IntoContainerId,
},
delta::{Delta, MapValue, StyleMeta, StyleMetaItem, TreeDelta, TreeDiff},
event::Diff,
id::{Counter, PeerID, ID},
op::{Op, RawOp, RawOpContent},
@ -56,12 +61,13 @@ pub(super) enum EventHint {
start: u32,
end: u32,
style: Style,
info: TextStyleInfoFlag,
},
InsertText {
/// pos is a Unicode index. If wasm, it's a UTF-16 index.
pos: u32,
len: u32,
styles: Vec<Style>,
styles: StyleMeta,
},
/// pos is a Unicode index. If wasm, it's a UTF-16 index.
DeleteText(DeleteSpan),
@ -463,48 +469,80 @@ fn change_to_diff(
};
'outer: {
let diff: Diff =
match hint {
EventHint::Mark { start, end, style } => {
Diff::Text(Delta::new().retain(start as usize).retain_with_meta(
(end - start) as usize,
crate::delta::StyleMeta { vec: vec![style] },
))
}
EventHint::InsertText { pos, styles, .. } => {
let slice = op.content.as_list().unwrap().as_insert_text().unwrap().0;
Diff::Text(Delta::new().retain(pos as usize).insert_with_meta(
slice.clone(),
crate::delta::StyleMeta { vec: styles },
))
}
EventHint::DeleteText(s) => {
Diff::Text(Delta::new().retain(s.start() as usize).delete(s.len()))
}
EventHint::InsertList { .. } => {
let (range, pos) = op.content.as_list().unwrap().as_insert().unwrap();
let values = arena.get_values(range.to_range());
Diff::List(Delta::new().retain(*pos).insert(values))
}
EventHint::DeleteList(s) => {
Diff::List(Delta::new().retain(s.start() as usize).delete(s.len()))
}
EventHint::Map { key, value } => {
Diff::NewMap(crate::delta::MapDelta::new().with_entry(
key,
MapValue {
counter: op.counter,
value,
lamport: (lamport, peer),
let diff: Diff = match hint {
EventHint::Mark {
start,
end,
style,
info,
} => {
let mut meta = StyleMeta::default();
if info.mergeable() {
meta.insert(
StyleKey::Key(style.key.clone()),
StyleMetaItem {
lamport,
peer: change.id.peer,
value: style.data,
},
))
)
} else {
meta.insert(
StyleKey::KeyWithId {
key: style.key.clone(),
id: ID {
peer: change.id.peer,
counter: op.counter,
},
},
StyleMetaItem {
lamport,
peer: change.id.peer,
value: style.data,
},
)
}
EventHint::Tree(tree_diff) => Diff::Tree(TreeDelta::default().push(tree_diff)),
EventHint::MarkEnd => {
// do nothing
break 'outer;
}
};
Diff::Text(
Delta::new()
.retain(start as usize)
.retain_with_meta((end - start) as usize, meta),
)
}
EventHint::InsertText { pos, styles, .. } => {
let slice = op.content.as_list().unwrap().as_insert_text().unwrap().0;
Diff::Text(
Delta::new()
.retain(pos as usize)
.insert_with_meta(slice.clone(), styles),
)
}
EventHint::DeleteText(s) => {
Diff::Text(Delta::new().retain(s.start() as usize).delete(s.len()))
}
EventHint::InsertList { .. } => {
let (range, pos) = op.content.as_list().unwrap().as_insert().unwrap();
let values = arena.get_values(range.to_range());
Diff::List(Delta::new().retain(*pos).insert(values))
}
EventHint::DeleteList(s) => {
Diff::List(Delta::new().retain(s.start() as usize).delete(s.len()))
}
EventHint::Map { key, value } => {
Diff::NewMap(crate::delta::MapDelta::new().with_entry(
key,
MapValue {
counter: op.counter,
value,
lamport: (lamport, peer),
},
))
}
EventHint::Tree(tree_diff) => Diff::Tree(TreeDelta::default().push(tree_diff)),
EventHint::MarkEnd => {
// do nothing
break 'outer;
}
};
ans.push(TxnContainerDiff {
idx: op.container,

View file

@ -1,19 +1,25 @@
use std::sync::Arc;
use crate::{
delta::DeltaItem,
delta::{Delta, DeltaItem, Meta, StyleMeta},
event::{Diff, Index, Path},
state::Forest,
utils::string_slice::StringSlice,
};
use fxhash::FxHashMap;
pub use loro_common::LoroValue;
use loro_common::{ContainerType, TreeID};
// TODO: rename this trait
pub trait ToJson {
fn to_json_value(&self) -> serde_json::Value;
fn to_json(&self) -> String;
fn to_json_pretty(&self) -> String;
fn to_json(&self) -> String {
self.to_json_value().to_string()
}
fn to_json_pretty(&self) -> String {
serde_json::to_string_pretty(&self.to_json_value()).unwrap()
}
fn from_json(s: &str) -> Self;
}
@ -40,6 +46,85 @@ impl ToJson for LoroValue {
}
}
impl ToJson for DeltaItem<StringSlice, StyleMeta> {
fn to_json_value(&self) -> serde_json::Value {
match self {
DeltaItem::Retain { len, meta } => {
let mut map = serde_json::Map::new();
map.insert("retain".into(), serde_json::to_value(len).unwrap());
if !meta.is_empty() {
map.insert("attributes".into(), meta.to_json_value());
}
serde_json::Value::Object(map)
}
DeltaItem::Insert { value, meta } => {
let mut map = serde_json::Map::new();
map.insert("insert".into(), serde_json::to_value(value).unwrap());
if !meta.is_empty() {
map.insert("attributes".into(), meta.to_json_value());
}
serde_json::Value::Object(map)
}
DeltaItem::Delete { len, meta: _ } => {
let mut map = serde_json::Map::new();
map.insert("delete".into(), serde_json::to_value(len).unwrap());
serde_json::Value::Object(map)
}
}
}
fn from_json(s: &str) -> Self {
let map: serde_json::Map<String, serde_json::Value> = serde_json::from_str(s).unwrap();
if map.contains_key("retain") {
let len = map["retain"].as_u64().unwrap();
let meta = if let Some(meta) = map.get("attributes") {
StyleMeta::from_json(meta.to_string().as_str())
} else {
StyleMeta::default()
};
DeltaItem::Retain {
len: len as usize,
meta,
}
} else if map.contains_key("insert") {
let value = map["insert"].as_str().unwrap().to_string().into();
let meta = if let Some(meta) = map.get("attributes") {
StyleMeta::from_json(meta.to_string().as_str())
} else {
StyleMeta::default()
};
DeltaItem::Insert { value, meta }
} else if map.contains_key("delete") {
let len = map["delete"].as_u64().unwrap();
DeltaItem::Delete {
len: len as usize,
meta: Default::default(),
}
} else {
panic!("Invalid delta item: {}", s);
}
}
}
impl ToJson for Delta<StringSlice, StyleMeta> {
fn to_json_value(&self) -> serde_json::Value {
let mut vec = Vec::new();
for item in self.iter() {
vec.push(item.to_json_value());
}
serde_json::Value::Array(vec)
}
fn from_json(s: &str) -> Self {
let vec: Vec<serde_json::Value> = serde_json::from_str(s).unwrap();
let mut ans = Delta::new();
for item in vec.into_iter() {
ans.push(DeltaItem::from_json(item.to_string().as_str()));
}
ans
}
}
#[derive(Debug, PartialEq, Eq)]
enum TypeHint {
Map,
@ -105,33 +190,34 @@ impl ApplyDiff for LoroValue {
LoroValue::Map(map) => {
let is_tree = matches!(diff.first(), Some(Diff::Tree(_)));
if !is_tree {
for item in diff.iter() {
match item {
Diff::NewMap(diff) => {
let map = Arc::make_mut(map);
for (key, value) in diff.updated.iter() {
match &value.value {
Some(value) => {
map.insert(
key.to_string(),
unresolved_to_collection(value),
);
}
None => {
map.remove(&key.to_string());
for item in diff.iter() {
match item {
Diff::NewMap(diff) => {
let map = Arc::make_mut(map);
for (key, value) in diff.updated.iter() {
match &value.value {
Some(value) => {
map.insert(
key.to_string(),
unresolved_to_collection(value),
);
}
None => {
map.remove(&key.to_string());
}
}
}
}
_ => unreachable!(),
}
_ => unreachable!(),
}
}
}else {
} else {
// TODO: perf
let forest = Forest::from_value(map.as_ref().clone().into()).unwrap();
let diff_forest = forest.apply_diffs(diff);
*map = diff_forest.to_value().into_map().unwrap()
}}
}
}
_ => unreachable!(),
}
}
@ -235,7 +321,7 @@ pub mod wasm {
use wasm_bindgen::{JsValue, __rt::IntoJsResult};
use crate::{
delta::{Delta, DeltaItem, MapDelta, MapDiff,StyleMeta, TreeDelta, TreeDiffItem},
delta::{Delta, DeltaItem, MapDelta, MapDiff, Meta, StyleMeta, TreeDelta, TreeDiffItem},
event::{Diff, Index},
utils::string_slice::StringSlice,
LoroValue,
@ -499,7 +585,7 @@ pub mod wasm {
&JsValue::from_f64(len as f64),
)
.unwrap();
if !meta.vec.is_empty() {
if !meta.is_empty() {
js_sys::Reflect::set(
&obj,
&JsValue::from_str("attributes"),
@ -515,7 +601,7 @@ pub mod wasm {
&JsValue::from_str(value.as_str()),
)
.unwrap();
if !meta.vec.is_empty() {
if !meta.is_empty() {
js_sys::Reflect::set(
&obj,
&JsValue::from_str("attributes"),
@ -541,13 +627,18 @@ pub mod wasm {
impl From<StyleMeta> for JsValue {
fn from(value: StyleMeta) -> Self {
let obj = Object::new();
for style in value.vec {
js_sys::Reflect::set(
&obj,
&JsValue::from_str(&style.key),
&JsValue::from(style.data),
)
.unwrap();
for (key, style) in value.iter() {
let value = if matches!(style.data, LoroValue::Null | LoroValue::Bool(_)) {
JsValue::from(style.data)
} else {
let value = Object::new();
js_sys::Reflect::set(&value, &"key".into(), &JsValue::from_str(&style.key))
.unwrap();
let data = JsValue::from(style.data);
js_sys::Reflect::set(&value, &"data".into(), &data).unwrap();
value.into()
};
js_sys::Reflect::set(&obj, &JsValue::from_str(&key.to_attr_key()), &value).unwrap();
}
obj.into_js_result().unwrap()

View file

@ -3,6 +3,7 @@
use std::ops::Range;
use loro_internal::{container::richtext::TextStyleInfoFlag, LoroDoc, ToJson};
use serde_json::json;
fn init(s: &str) -> LoroDoc {
let doc = LoroDoc::default();
@ -79,16 +80,16 @@ fn merge(a: &LoroDoc, b: &LoroDoc) {
b.import(&a.export_from(&b.oplog_vv())).unwrap();
}
fn expect_result(doc: &LoroDoc, json: &str) {
let richtext = doc.get_text("r");
let s = richtext.get_richtext_value().to_json();
assert_eq!(&s, json);
}
fn expect_result_value(doc: &LoroDoc, json: serde_json::Value) {
fn expect_result(doc: &LoroDoc, json: serde_json::Value) {
let richtext = doc.get_text("r");
let s = richtext.get_richtext_value().to_json_value();
assert_eq!(s, json);
assert_eq!(
&s,
&json,
"expect: {}, got: {}",
serde_json::to_string_pretty(&json).unwrap(),
serde_json::to_string_pretty(&s).unwrap()
);
}
#[test]
@ -100,7 +101,7 @@ fn case0() {
merge(&doc_a, &doc_b);
expect_result(
&doc_a,
r#"[{"insert":"Hello New World","attributes":{"bold":true}}]"#,
json!([{"insert":"Hello New World","attributes":{"bold":true}}]),
);
}
@ -113,7 +114,7 @@ fn case1() {
merge(&doc_a, &doc_b);
expect_result(
&doc_a,
r#"[{"insert":"Hello World","attributes":{"bold":true}}]"#,
json!([{"insert":"Hello World","attributes":{"bold":true}}]),
);
}
@ -127,7 +128,7 @@ fn case2() {
merge(&doc_a, &doc_b);
expect_result(
&doc_a,
r#"[{"insert":"Hello a "},{"insert":"World","attributes":{"bold":true}}]"#,
json!([{"insert":"Hello a ","attributes":{"bold":false}},{"insert":"World","attributes":{"bold":true}}]),
);
}
@ -147,7 +148,7 @@ fn case3() {
merge(&doc_a, &doc_b);
expect_result(
&doc_a,
r#"[{"insert":"Hello a "},{"insert":"World","attributes":{"bold":true}}]"#,
json!([{"insert":"Hello a "},{"insert":"World","attributes":{"bold":true}}]),
);
}
@ -163,17 +164,17 @@ fn case5() {
let doc_b = clone(&doc_a, 2);
mark(&doc_a, 0..5, Kind::Link);
delete(&doc_b, 2, 3);
expect_result(&doc_b, r#"[{"insert":"He World"}]"#);
expect_result(&doc_b, json!([{"insert":"He World"}]));
insert(&doc_b, 2, "y");
expect_result(&doc_b, r#"[{"insert":"Hey World"}]"#);
expect_result(&doc_b, json!([{"insert":"Hey World"}]));
merge(&doc_a, &doc_b);
expect_result(
&doc_b,
r#"[{"insert":"Hey","attributes":{"link":true}},{"insert":" World"}]"#,
json!([{"insert":"Hey","attributes":{"link":true}},{"insert":" World"}]),
);
expect_result(
&doc_a,
r#"[{"insert":"Hey","attributes":{"link":true}},{"insert":" World"}]"#,
json!([{"insert":"Hey","attributes":{"link":true}},{"insert":" World"}]),
);
}
@ -187,7 +188,7 @@ fn case5() {
fn case6() {
let doc = init("Hello World");
mark(&doc, 0..5, Kind::Bold);
expect_result_value(
expect_result(
&doc,
serde_json::json!([
{"insert": "Hello", "attributes": {"bold": true}},
@ -195,7 +196,7 @@ fn case6() {
]),
);
mark(&doc, 0..5, Kind::Link);
expect_result_value(
expect_result(
&doc,
serde_json::json!([
{"insert": "Hello", "attributes": {"bold": true, "link": true}},
@ -203,7 +204,7 @@ fn case6() {
]),
);
insert(&doc, 5, "t");
expect_result_value(
expect_result(
&doc,
serde_json::json!([
{"insert": "Hello", "attributes": {"bold": true, "link": true}},
@ -228,7 +229,15 @@ fn case7() {
unmark(&doc_a, 0..3, Kind::Bold);
unmark(&doc_b, 4..7, Kind::Bold);
merge(&doc_a, &doc_b);
expect_result(&doc_a, r#"[{"insert":"The fox jumped over the dog."}]"#);
expect_result(
&doc_a,
json!([
{"insert":"The", "attributes": {"bold": false}},
{"insert":" ",},
{"insert":"fox", "attributes": {"bold": false}},
{"insert":" jumped over the dog."}
]),
);
}
/// | Name | Text |
@ -246,11 +255,12 @@ fn case8() {
unmark(&doc_b, 3..14, Kind::Bold);
mark(&doc_b, 24..27, Kind::Bold);
merge(&doc_a, &doc_b);
expect_result_value(
expect_result(
&doc_a,
serde_json::json!([
{"insert": "The", "attributes": {"bold": true}},
{"insert": " fox jumped over the "},
{"insert": " fox jumped", "attributes": {"bold": false}},
{"insert": " over the "},
{"insert": "dog", "attributes": {"bold": true}},
{"insert": "."}
]),
@ -270,7 +280,7 @@ fn case9() {
mark(&doc_a, 0..7, Kind::Bold);
mark(&doc_a, 4..14, Kind::Italic);
merge(&doc_a, &doc_b);
expect_result_value(
expect_result(
&doc_a,
serde_json::json!([
{"insert": "The ", "attributes": {"bold": true}},

View file

@ -1,9 +1,159 @@
use std::sync::{Arc, Mutex};
use loro_common::{ContainerID, ContainerType, LoroValue, ID};
use loro_internal::{version::Frontiers, ApplyDiff, LoroDoc, ToJson};
use loro_internal::{
container::richtext::TextStyleInfoFlag, version::Frontiers, ApplyDiff, LoroDoc, ToJson,
};
use serde_json::json;
#[test]
fn richtext_mark_event() {
let a = LoroDoc::new_auto_commit();
a.subscribe(
&a.get_text("text").id(),
Arc::new(|e| {
let delta = e.container.diff.as_text().unwrap();
assert_eq!(
delta.to_json_value(),
json!([
{"insert": "He", "attributes": {"bold": true}},
{"insert": "ll", "attributes": {"bold": false}},
{"insert": "o", "attributes": {"bold": true}}
])
)
}),
);
a.get_text("text").insert_(0, "Hello").unwrap();
a.get_text("text")
.mark_(0, 5, "bold", TextStyleInfoFlag::BOLD)
.unwrap();
a.get_text("text")
.mark_(2, 4, "bold", TextStyleInfoFlag::BOLD.to_delete())
.unwrap();
a.commit_then_stop();
let b = LoroDoc::new_auto_commit();
b.subscribe(
&a.get_text("text").id(),
Arc::new(|e| {
let delta = e.container.diff.as_text().unwrap();
assert_eq!(
delta.to_json_value(),
json!([
{"insert": "He", "attributes": {"bold": true}},
{"insert": "ll", "attributes": {"bold": false}},
{"insert": "o", "attributes": {"bold": true}}
])
)
}),
);
b.merge(&a).unwrap();
}
#[test]
fn concurrent_richtext_mark_event() {
let a = LoroDoc::new_auto_commit();
let b = LoroDoc::new_auto_commit();
let c = LoroDoc::new_auto_commit();
a.get_text("text").insert_(0, "Hello").unwrap();
b.merge(&a).unwrap();
c.merge(&a).unwrap();
b.get_text("text")
.mark_(0, 3, "bold", TextStyleInfoFlag::BOLD)
.unwrap();
c.get_text("text")
.mark_(1, 4, "link", TextStyleInfoFlag::LINK)
.unwrap();
b.merge(&c).unwrap();
let sub_id = a.subscribe(
&a.get_text("text").id(),
Arc::new(|e| {
let delta = e.container.diff.as_text().unwrap();
assert_eq!(
delta.to_json_value(),
json!([
{"retain": 1, "attributes": {"bold": true, }},
{"retain": 2, "attributes": {"bold": true, "link": true}},
{"retain": 1, "attributes": {"link": true}},
])
)
}),
);
a.merge(&b).unwrap();
a.unsubscribe(sub_id);
let sub_id = a.subscribe(
&a.get_text("text").id(),
Arc::new(|e| {
let delta = e.container.diff.as_text().unwrap();
assert_eq!(
delta.to_json_value(),
json!([
{
"retain": 2,
},
{
"retain": 1,
"attributes": {"bold": false}
}
])
)
}),
);
b.get_text("text")
.mark_(2, 3, "bold", TextStyleInfoFlag::BOLD.to_delete())
.unwrap();
a.merge(&b).unwrap();
a.unsubscribe(sub_id);
a.subscribe(
&a.get_text("text").id(),
Arc::new(|e| {
let delta = e.container.diff.as_text().unwrap();
assert_eq!(
delta.to_json_value(),
json!([
{
"retain": 2,
},
{
"insert": "A",
"attributes": {"bold": true, "link": true}
}
])
)
}),
);
a.get_text("text").insert_(2, "A").unwrap();
a.commit_then_stop();
}
#[test]
fn insert_richtext_event() {
let a = LoroDoc::new_auto_commit();
a.get_text("text").insert_(0, "Hello").unwrap();
a.get_text("text")
.mark_(0, 5, "bold", TextStyleInfoFlag::BOLD)
.unwrap();
a.commit_then_renew();
let text = a.get_text("text");
a.subscribe(
&text.id(),
Arc::new(|e| {
let delta = e.container.diff.as_text().unwrap();
assert_eq!(
delta.to_json_value(),
json!([
{"retain": 5,},
{"insert": " World!", "attributes": {"bold": true}}
])
)
}),
);
text.insert_(5, " World!").unwrap();
}
#[test]
fn import_after_init_handlers() {
let a = LoroDoc::new_auto_commit();
@ -41,7 +191,7 @@ fn import_after_init_handlers() {
b.get_text("text").insert_(0, "text").unwrap();
b.get_map("map").insert_("m", "map".into()).unwrap();
a.import(&b.export_snapshot()).unwrap();
a.commit();
a.commit_then_renew();
}
#[test]
@ -90,7 +240,7 @@ fn test_checkout() {
let value: Arc<Mutex<LoroValue>> = Arc::new(Mutex::new(LoroValue::Map(Default::default())));
let root_value = value.clone();
doc_0.subscribe_deep(Arc::new(move |event| {
doc_0.subscribe_root(Arc::new(move |event| {
let mut root_value = root_value.lock().unwrap();
root_value.apply(
&event.container.path.iter().map(|x| x.1.clone()).collect(),
@ -384,7 +534,7 @@ fn map_concurrent_checkout() {
#[test]
fn tree_checkout() {
let mut doc_a = LoroDoc::new();
doc_a.subscribe_deep(Arc::new(|_e| {}));
doc_a.subscribe_root(Arc::new(|_e| {}));
doc_a.set_peer_id(1).unwrap();
let tree = doc_a.get_tree("root");
let id1 = doc_a.with_txn(|txn| tree.create(txn)).unwrap();

View file

@ -300,7 +300,7 @@ impl Loro {
pub fn subscribe(&self, f: js_sys::Function) -> u32 {
let observer = observer::Observer::new(f);
self.0
.subscribe_deep(Arc::new(move |e| {
.subscribe_root(Arc::new(move |e| {
// call_after_micro_task(observer.clone(), e)
call_subscriber(observer.clone(), e);
}))