mirror of
https://github.com/loro-dev/loro.git
synced 2024-11-24 20:20:36 +00:00
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:
parent
a52549ea30
commit
95e6130d93
21 changed files with 1004 additions and 545 deletions
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
|
@ -20,6 +20,7 @@
|
|||
"tinyvec",
|
||||
"txns",
|
||||
"unbold",
|
||||
"unmark",
|
||||
"yspan"
|
||||
],
|
||||
"rust-analyzer.runnableEnv": {
|
||||
|
|
|
@ -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);
|
||||
}));
|
||||
{
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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!"
|
||||
}])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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!()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}));
|
||||
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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}},
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}))
|
||||
|
|
Loading…
Reference in a new issue