fix: time travel back should be able to nullify rich text span (#254)

This commit is contained in:
Zixuan Chen 2024-01-19 22:20:27 +08:00 committed by GitHub
parent f2d9152f2d
commit 77eb6853b8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 256 additions and 72 deletions

View file

@ -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,

View file

@ -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,
@ -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,
};
@ -276,11 +279,13 @@ 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;
@ -288,6 +293,14 @@ mod text_chunk {
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 {
@ -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(),
@ -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)
}
}
@ -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() {
@ -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() {
@ -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;
@ -2103,7 +2137,7 @@ mod test {
self.state.drain_by_entity_index(
range.entity_start,
range.entity_end - range.entity_start,
|_| {},
None,
);
}
}
@ -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!(
@ -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!([{
@ -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!([{

View file

@ -3,7 +3,7 @@
use std::{
collections::BTreeSet,
ops::{ControlFlow, Deref, DerefMut, Range},
ops::{ControlFlow, Deref, DerefMut, Range, RangeBounds},
sync::Arc,
usize,
};
@ -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)]
@ -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;

View file

@ -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)]
@ -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,
@ -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 to_value(&self) -> LoroValue {
LoroValue::Map(Arc::new(
self.map
.iter()
.filter_map(|(key, value)| {
if value.value.is_null() {
return None;
}
pub(crate) fn contains_key(&self, key: &InternalString) -> bool {
self.map.contains_key(key)
}
Some((key.to_attr_key(), value.value.clone()))
})
.collect(),
))
pub(crate) fn to_value(&self) -> LoroValue {
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())
}
}
@ -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)

View file

@ -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>),

View file

@ -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"];
@ -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
@ -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

View file

@ -7,11 +7,11 @@ use crate::{
richtext::richtext_state::PosType,
tree::tree_op::TreeOp,
},
delta::{TreeDiffItem, TreeExternalDiff},
delta::{DeltaItem, StyleMeta, TreeDiffItem, TreeExternalDiff},
op::ListSlice,
state::RichtextState,
txn::EventHint,
utils::utf16::count_utf16_len,
utils::{string_slice::StringSlice, utf16::count_utf16_len},
};
use enum_as_inner::EnumAsInner;
use fxhash::FxHashMap;
@ -43,6 +43,25 @@ pub enum TextDelta {
},
}
impl From<&DeltaItem<StringSlice, StyleMeta>> for TextDelta {
fn from(value: &DeltaItem<StringSlice, StyleMeta>) -> Self {
match value {
crate::delta::DeltaItem::Retain { retain, attributes } => TextDelta::Retain {
retain: *retain,
attributes: attributes.to_option_map(),
},
crate::delta::DeltaItem::Insert { insert, attributes } => TextDelta::Insert {
insert: insert.to_string(),
attributes: attributes.to_option_map(),
},
crate::delta::DeltaItem::Delete {
delete,
attributes: _,
} => TextDelta::Delete { delete: *delete },
}
}
}
/// Flatten attributes that allow overlap
#[derive(Clone)]
pub struct TextHandler {
@ -346,10 +365,7 @@ impl TextHandler {
let mut override_styles = Vec::new();
if let Some(attr) = attr {
// current styles
let map: FxHashMap<_, _> = styles
.iter()
.map(|x| (x.0.key().clone(), x.1.data))
.collect();
let map: FxHashMap<_, _> = styles.iter().map(|x| (x.0.clone(), x.1.data)).collect();
debug_log::debug_dbg!(&map);
for (key, style) in map.iter() {
match attr.get(key.deref()) {

View file

@ -18,7 +18,7 @@ use crate::{
},
},
container::{list::list_op, richtext::richtext_state::RichtextStateChunk},
delta::{Delta, DeltaItem, StyleMeta},
delta::{Delta, DeltaItem, StyleMeta, StyleMetaItem},
encoding::{EncodeMode, StateSnapshotDecodeContext, StateSnapshotEncoder},
event::{Diff, Index, InternalDiff},
op::{Op, RawOp},
@ -180,7 +180,7 @@ impl ContainerState for RichtextState {
event_index: start_event_index,
} = style_starts.remove(style).unwrap();
let mut delta: Delta<StringSlice, _> =
let mut delta: Delta<StringSlice, StyleMeta> =
Delta::new().retain(start_event_index);
// we need to + 1 because we also need to annotate the end anchor
let event = self.state.get_mut().annotate_style_range_with_event(
@ -202,15 +202,78 @@ impl ContainerState for RichtextState {
delete: len,
attributes: _,
} => {
let (start, end) =
self.state
.get_mut()
.drain_by_entity_index(entity_index, *len, |_| {});
let mut deleted_style_chunks = Vec::new();
let (start, end) = self.state.get_mut().drain_by_entity_index(
entity_index,
*len,
Some(&mut |c| {
if matches!(c, RichtextStateChunk::Style { .. }) {
deleted_style_chunks.push(c);
}
}),
);
if start > event_index {
ans = ans.retain(start - event_index);
event_index = start;
}
for chunk in deleted_style_chunks {
if let RichtextStateChunk::Style { style, anchor_type } = chunk {
match anchor_type {
AnchorType::Start => {
style_starts.insert(
style,
Pos {
entity_index,
event_index,
},
);
}
AnchorType::End => {
let Pos {
entity_index: start_entity_index,
event_index: start_event_index,
} = style_starts.remove(&style).unwrap();
if event_index == start_event_index {
debug_assert_eq!(start_entity_index, entity_index);
// deleted by this batch, can be ignored
continue;
}
// Otherwise, we need to calculate the new styles with the key between the ranges
let mut delta: Delta<StringSlice, StyleMeta> =
Delta::new().retain(start_event_index);
if let Some(iter) = self
.state
.get_mut()
.iter_style_range(start_entity_index..entity_index)
{
for style_range in iter {
let mut style_meta: StyleMeta =
(&style_range.styles).into();
if !style_meta.contains_key(&style.key) {
style_meta.insert(
style.key.clone(),
StyleMetaItem {
lamport: 0,
peer: 0,
value: LoroValue::Null,
},
)
}
delta =
delta.retain_with_meta(style_range.len, style_meta);
}
}
delta = delta.chop();
style_delta = style_delta.compose(delta);
}
}
}
}
ans = ans.delete(end - start);
}
}
@ -284,7 +347,7 @@ impl ContainerState for RichtextState {
} => {
self.state
.get_mut()
.drain_by_entity_index(entity_index, *len, |_| {});
.drain_by_entity_index(entity_index, *len, None);
}
}
}
@ -314,7 +377,7 @@ impl ContainerState for RichtextState {
self.state.get_mut().drain_by_entity_index(
del.start() as usize,
rle::HasLength::atom_len(&del),
|_| {},
None,
);
}
list_op::InnerListOp::StyleStart {

View file

@ -16,7 +16,7 @@ use crate::{
container::{
idx::ContainerIdx,
list::list_op::{DeleteSpan, InnerListOp},
richtext::{Style, StyleKey},
richtext::Style,
IntoContainerId,
},
delta::{
@ -503,7 +503,7 @@ fn change_to_diff(
EventHint::Mark { start, end, style } => {
let mut meta = StyleMeta::default();
meta.insert(
StyleKey::Key(style.key.clone()),
style.key.clone(),
StyleMetaItem {
lamport,
peer: change.id.peer,

View file

@ -457,7 +457,7 @@ pub mod wasm {
let obj = Object::new();
for (key, style) in value.iter() {
let value = JsValue::from(style.data);
js_sys::Reflect::set(&obj, &JsValue::from_str(&key.to_attr_key()), &value).unwrap();
js_sys::Reflect::set(&obj, &JsValue::from_str(&key), &value).unwrap();
}
obj.into_js_result().unwrap()

View file

@ -1,4 +1,36 @@
use loro_internal::{delta::DeltaItem, DiffEvent, LoroResult};
use std::sync::Arc;
use loro::LoroDoc;
use loro_internal::{delta::DeltaItem, handler::TextDelta, DiffEvent, LoroResult};
#[test]
fn travel_back_should_remove_styles() {
let mut doc = LoroDoc::new();
let doc2 = LoroDoc::new();
let text = doc.get_text("text");
let text2 = doc2.get_text("text");
doc.subscribe(
&text.id(),
Arc::new(move |x| {
let Some(text) = x.container.diff.as_text() else {
return;
};
let delta: Vec<TextDelta> = text.iter().map(|x| x.into()).collect();
text2.apply_delta(&delta).unwrap();
}),
);
let text2 = doc2.get_text("text");
text.insert(0, "Hello world!").unwrap();
doc.commit();
let f = doc.state_frontiers();
text.mark(0..5, "bold", true).unwrap();
doc.commit();
assert_eq!(text.to_delta(), text2.to_delta());
doc.checkout(&f).unwrap();
assert_eq!(text.to_delta(), text2.to_delta());
}
#[test]
fn list() -> LoroResult<()> {