Skip to content

Commit

Permalink
fix: time travel back should be able to nullify rich text span (#254)
Browse files Browse the repository at this point in the history
  • Loading branch information
zxch3n authored Jan 19, 2024
1 parent f2d9152 commit 77eb685
Show file tree
Hide file tree
Showing 11 changed files with 256 additions and 72 deletions.
6 changes: 0 additions & 6 deletions crates/loro-internal/src/container/richtext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,6 @@ pub(crate) enum StyleKey {
}

impl StyleKey {
pub fn to_attr_key(&self) -> String {
match self {
Self::Key(key) => key.to_string(),
}
}

pub fn key(&self) -> &InternalString {
match self {
Self::Key(key) => key,
Expand Down
70 changes: 54 additions & 16 deletions crates/loro-internal/src/container/richtext/richtext_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ use generic_btree::{
};
use loro_common::{IdSpan, LoroValue, ID};
use serde::{ser::SerializeStruct, Serialize};
use std::fmt::{Display, Formatter};
use std::{
fmt::{Display, Formatter},
ops::RangeBounds,
};
use std::{
ops::{Add, AddAssign, Range, Sub},
str::Utf8Error,
Expand All @@ -32,7 +35,7 @@ use self::{

use super::{
query_by_len::{IndexQuery, QueryByLen},
style_range_map::{IterAnchorItem, StyleRangeMap, Styles},
style_range_map::{self, IterAnchorItem, StyleRangeMap, Styles},
AnchorType, RichtextSpan, StyleOp,
};

Expand Down Expand Up @@ -276,18 +279,28 @@ mod text_chunk {
let mut start = 0;
let mut end = 0;
let mut started = false;
let mut last_unicode_index = 0;
for (unicode_index, (i, c)) in self.as_str().char_indices().enumerate() {
if unicode_index == range.start {
start = i;
started = true;
}

if unicode_index == range.end {
end = i;
break;
}
if started {
utf16_len += c.len_utf16();
}

last_unicode_index = unicode_index;
}

assert!(started);
if end == 0 {
assert_eq!(last_unicode_index + 1, range.end);
end = self.bytes.len();
}

let ans = Self {
Expand Down Expand Up @@ -1662,7 +1675,7 @@ impl RichtextState {
&mut self,
pos: usize,
len: usize,
mut f: impl FnMut(RichtextStateChunk),
mut f: Option<&mut dyn FnMut(RichtextStateChunk)>,
) -> (usize, usize) {
assert!(
pos + len <= self.len_entity(),
Expand Down Expand Up @@ -1721,13 +1734,25 @@ impl RichtextState {
updater.update(&*elem);
match elem {
RichtextStateChunk::Text(text) => {
if let Some(f) = f {
let span = text.slice(start_cursor.offset..start_cursor.offset + len);
f(RichtextStateChunk::Text(span));
}
let (next, event_len_) =
text.delete_by_entity_index(start_cursor.offset, len);
event_len = event_len_;
(true, next.map(RichtextStateChunk::Text), None)
}
RichtextStateChunk::Style { .. } => {
*elem = RichtextStateChunk::Text(TextChunk::new_empty());
if let Some(f) = f {
let v = std::mem::replace(
elem,
RichtextStateChunk::Text(TextChunk::new_empty()),
);
f(v);
} else {
*elem = RichtextStateChunk::Text(TextChunk::new_empty());
}
(true, None, None)
}
}
Expand All @@ -1744,7 +1769,9 @@ impl RichtextState {
let mut updater = StyleRangeUpdater::new(self.style_ranges.as_mut(), pos);
for iter in generic_btree::iter::Drain::new(&mut self.tree, start, end) {
updater.update(&iter);
f(iter)
if let Some(f) = f.as_mut() {
f(iter)
}
}

if let Some(s) = self.style_ranges.as_mut() {
Expand Down Expand Up @@ -1828,7 +1855,6 @@ impl RichtextState {
}

pub fn get_richtext_value(&self) -> LoroValue {
self.check_consistency_between_content_and_style_ranges();
let mut ans: Vec<LoroValue> = Vec::new();
let mut last_attributes: Option<LoroValue> = None;
for span in self.iter() {
Expand Down Expand Up @@ -2003,6 +2029,14 @@ impl RichtextState {
.map(|x| x.estimate_size())
.unwrap_or(0)
}

/// Iter style ranges in the given range in entity index
pub(crate) fn iter_style_range(
&self,
range: impl RangeBounds<usize>,
) -> Option<impl Iterator<Item = &style_range_map::Elem>> {
self.style_ranges.as_ref().map(|x| x.iter_range(range))
}
}

use converter::ContinuousIndexConverter;
Expand Down Expand Up @@ -2103,7 +2137,7 @@ mod test {
self.state.drain_by_entity_index(
range.entity_start,
range.entity_end - range.entity_start,
|_| {},
None,
);
}
}
Expand Down Expand Up @@ -2487,11 +2521,15 @@ mod test {
wrapper.insert(0, "Hello World!");
wrapper.mark(0..5, bold(0));
let mut count = 0;
wrapper.state.drain_by_entity_index(0, 7, |span| {
if matches!(span, RichtextStateChunk::Style { .. }) {
count += 1;
}
});
wrapper.state.drain_by_entity_index(
0,
7,
Some(&mut |span| {
if matches!(span, RichtextStateChunk::Style { .. }) {
count += 1;
}
}),
);

assert_eq!(count, 2);
assert_eq!(
Expand All @@ -2507,8 +2545,8 @@ mod test {
let mut wrapper = SimpleWrapper::default();
wrapper.insert(0, "Hello World!");
wrapper.mark(0..5, bold(0));
wrapper.state.drain_by_entity_index(6, 1, |_| {});
wrapper.state.drain_by_entity_index(0, 1, |_| {});
wrapper.state.drain_by_entity_index(6, 1, None);
wrapper.state.drain_by_entity_index(0, 1, None);
assert_eq!(
wrapper.state.get_richtext_value().to_json_value(),
json!([{
Expand All @@ -2522,8 +2560,8 @@ mod test {
let mut wrapper = SimpleWrapper::default();
wrapper.insert(0, "Hello World!");
wrapper.mark(2..5, bold(0));
wrapper.state.drain_by_entity_index(6, 1, |_| {});
wrapper.state.drain_by_entity_index(1, 2, |_| {});
wrapper.state.drain_by_entity_index(6, 1, None);
wrapper.state.drain_by_entity_index(1, 2, None);
assert_eq!(
wrapper.state.get_richtext_value().to_json_value(),
json!([{
Expand Down
31 changes: 27 additions & 4 deletions crates/loro-internal/src/container/richtext/style_range_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
use std::{
collections::BTreeSet,
ops::{ControlFlow, Deref, DerefMut, Range},
ops::{ControlFlow, Deref, DerefMut, Range, RangeBounds},
sync::Arc,
usize,
};
Expand Down Expand Up @@ -91,9 +91,9 @@ impl DerefMut for Styles {
pub(super) static EMPTY_STYLES: Lazy<Styles> = Lazy::new(Default::default);

#[derive(Debug, Clone)]
pub(super) struct Elem {
styles: Styles,
len: usize,
pub(crate) struct Elem {
pub(crate) styles: Styles,
pub(crate) len: usize,
}

#[derive(Clone, Default, Debug, PartialEq, Eq)]
Expand Down Expand Up @@ -313,6 +313,29 @@ impl StyleRangeMap {
}
}

pub(crate) fn iter_range(
&self,
range: impl RangeBounds<usize>,
) -> impl Iterator<Item = &Elem> + '_ {
let start = match range.start_bound() {
std::ops::Bound::Included(x) => *x,
std::ops::Bound::Excluded(x) => *x + 1,
std::ops::Bound::Unbounded => 0,
};

let end = match range.end_bound() {
std::ops::Bound::Included(x) => *x + 1,
std::ops::Bound::Excluded(x) => *x,
std::ops::Bound::Unbounded => usize::MAX,
};

let start = self.tree.query::<LengthFinder>(&start).unwrap();
let end = self.tree.query::<LengthFinder>(&end).unwrap();
self.tree
.iter_range(start.cursor..end.cursor)
.map(|x| x.elem)
}

/// Return the expected style anchors with their indexes.
pub(super) fn iter_anchors(&self) -> impl Iterator<Item = IterAnchorItem> + '_ {
let mut index = 0;
Expand Down
61 changes: 41 additions & 20 deletions crates/loro-internal/src/delta/text.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
use std::sync::Arc;

use fxhash::FxHashMap;
use loro_common::{LoroValue, PeerID};
use loro_common::{InternalString, LoroValue, PeerID};
use serde::{Deserialize, Serialize};

use crate::change::Lamport;
use crate::container::richtext::{Style, StyleKey, Styles};
use crate::container::richtext::{Style, Styles};
use crate::ToJson;

use super::Meta;

#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StyleMeta {
map: FxHashMap<StyleKey, StyleMetaItem>,
map: FxHashMap<InternalString, StyleMetaItem>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
Expand All @@ -39,7 +39,7 @@ impl From<&Styles> for StyleMeta {
for (key, value) in styles.iter() {
if let Some(value) = value.get() {
map.insert(
key.clone(),
key.key().clone(),
StyleMetaItem {
value: value.to_value(),
lamport: value.lamport,
Expand Down Expand Up @@ -85,35 +85,56 @@ impl Meta for StyleMeta {
}

impl StyleMeta {
pub(crate) fn iter(&self) -> impl Iterator<Item = (StyleKey, Style)> + '_ {
pub(crate) fn iter(&self) -> impl Iterator<Item = (InternalString, Style)> + '_ {
self.map.iter().map(|(key, style)| {
(
key.clone(),
Style {
key: key.key().clone(),
key: key.clone(),
data: style.value.clone(),
},
)
})
}

pub(crate) fn insert(&mut self, key: StyleKey, value: StyleMetaItem) {
pub(crate) fn insert(&mut self, key: InternalString, value: StyleMetaItem) {
self.map.insert(key, value);
}

pub(crate) fn contains_key(&self, key: &InternalString) -> bool {
self.map.contains_key(key)
}

pub(crate) fn to_value(&self) -> LoroValue {
LoroValue::Map(Arc::new(
self.map
.iter()
.filter_map(|(key, value)| {
if value.value.is_null() {
return None;
}

Some((key.to_attr_key(), value.value.clone()))
})
.collect(),
))
LoroValue::Map(Arc::new(self.to_map_without_null_value()))
}

fn to_map_without_null_value(&self) -> FxHashMap<String, LoroValue> {
self.map
.iter()
.filter_map(|(key, value)| {
if value.value.is_null() {
None
} else {
Some((key.to_string(), value.value.clone()))
}
})
.collect()
}

pub(crate) fn to_map(&self) -> FxHashMap<String, LoroValue> {
self.map
.iter()
.map(|(key, value)| (key.to_string(), value.value.clone()))
.collect()
}

pub(crate) fn to_option_map(&self) -> Option<FxHashMap<String, LoroValue>> {
if self.is_empty() {
return None;
}

Some(self.to_map())
}
}

Expand All @@ -122,7 +143,7 @@ impl ToJson for StyleMeta {
let mut map = serde_json::Map::new();
for (key, style) in self.iter() {
let value = serde_json::to_value(&style.data).unwrap();
map.insert(key.to_attr_key(), value);
map.insert(key.to_string(), value);
}

serde_json::Value::Object(map)
Expand Down
2 changes: 2 additions & 0 deletions crates/loro-internal/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,8 @@ impl From<InternalDiff> for DiffVariant {
#[derive(Clone, Debug, EnumAsInner)]
pub enum Diff {
List(Delta<Vec<ValueOrContainer>>),
// TODO: refactor, doesn't make much sense to use `StyleMeta` here, because sometime style
// don't have peer and lamport info
/// - When feature `wasm` is enabled, it should use utf16 indexes.
/// - When feature `wasm` is disabled, it should use unicode indexes.
Text(Delta<StringSlice, StyleMeta>),
Expand Down
11 changes: 3 additions & 8 deletions crates/loro-internal/src/fuzz/richtext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ use crate::{
array_mut_ref, container::ContainerID, delta::DeltaItem, id::PeerID, ContainerType, LoroValue,
};
use crate::{
container::richtext::StyleKey, event::Diff, handler::TextDelta, loro::LoroDoc, value::ToJson,
version::Frontiers, TextHandler,
event::Diff, handler::TextDelta, loro::LoroDoc, value::ToJson, version::Frontiers, TextHandler,
};

const STYLES_NAME: [&str; 4] = ["bold", "comment", "link", "highlight"];
Expand Down Expand Up @@ -96,9 +95,7 @@ impl Actor {
let attributes: FxHashMap<_, _> = attributes
.iter()
.filter(|(_, v)| !v.data.is_null())
.map(|(k, v)| match k {
StyleKey::Key(k) => (k.to_string(), v.data),
})
.map(|(k, v)| (k.to_string(), v.data))
.collect();
let attributes = if attributes.is_empty() {
None
Expand All @@ -118,9 +115,7 @@ impl Actor {
let attributes: FxHashMap<_, _> = attributes
.iter()
.filter(|(_, v)| !v.data.is_null())
.map(|(k, v)| match k {
StyleKey::Key(k) => (k.to_string(), v.data),
})
.map(|(k, v)| (k.to_string(), v.data))
.collect();
let attributes = if attributes.is_empty() {
None
Expand Down
Loading

0 comments on commit 77eb685

Please sign in to comment.