mirror of
https://github.com/loro-dev/loro.git
synced 2025-01-22 12:57:20 +00:00
refactor: add linear mode for text
This commit is contained in:
parent
a9c6c32b3e
commit
af274eac79
7 changed files with 604 additions and 353 deletions
|
@ -433,6 +433,31 @@ impl<V: DeltaValue, Attr: DeltaAttr> DeltaRope<V, Attr> {
|
|||
.insert_many_by_cursor(Some(pos.cursor), values.into_iter());
|
||||
}
|
||||
|
||||
pub fn insert_value(&mut self, pos: usize, value: V, attr: Attr) {
|
||||
if self.len() < pos {
|
||||
self.push_retain(pos - self.len(), Default::default());
|
||||
}
|
||||
|
||||
if pos == self.len() {
|
||||
self.tree.push(DeltaItem::Replace {
|
||||
value,
|
||||
attr,
|
||||
delete: 0,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let pos = self.tree.query::<LengthFinder>(&pos).unwrap();
|
||||
self.tree.insert_by_path(
|
||||
pos.cursor,
|
||||
DeltaItem::Replace {
|
||||
value,
|
||||
attr,
|
||||
delete: 0,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn update_attr_in_range(&mut self, range: Range<usize>, attr: &Attr) {
|
||||
if range.start == range.end || self.is_empty() {
|
||||
return;
|
||||
|
|
|
@ -59,6 +59,17 @@ impl<V: DeltaValue, Attr: DeltaAttr> DeltaRope<V, Attr> {
|
|||
// trace!("Composed {:#?}", &self);
|
||||
}
|
||||
|
||||
pub fn delete(&mut self, mut index: usize, len: usize) {
|
||||
self._compose_replace(
|
||||
DeltaReplace {
|
||||
value: &Default::default(),
|
||||
attr: &Default::default(),
|
||||
delete: len,
|
||||
},
|
||||
&mut index,
|
||||
);
|
||||
}
|
||||
|
||||
fn _compose_replace(
|
||||
&mut self,
|
||||
delta_replace_item @ DeltaReplace {
|
||||
|
@ -70,8 +81,11 @@ impl<V: DeltaValue, Attr: DeltaAttr> DeltaRope<V, Attr> {
|
|||
) {
|
||||
let mut should_insert = this_value.rle_len() > 0;
|
||||
let mut left_del_len = delete;
|
||||
if delete > 0 {
|
||||
assert!(*index < self.len());
|
||||
if *index > self.len() {
|
||||
self.push_retain(*index - self.len(), Attr::default());
|
||||
}
|
||||
|
||||
if delete > 0 && *index != self.len() {
|
||||
let range = *index..(*index + left_del_len).min(self.len());
|
||||
let from = self.tree.query::<LengthFinder>(&range.start).unwrap();
|
||||
let to = self.tree.query::<LengthFinder>(&range.end).unwrap();
|
||||
|
|
|
@ -397,11 +397,21 @@ pub(crate) enum RichtextStateChunk {
|
|||
},
|
||||
}
|
||||
|
||||
impl Default for RichtextStateChunk {
|
||||
fn default() -> Self {
|
||||
Self::new_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl RichtextStateChunk {
|
||||
pub fn new_text(s: BytesSlice, id: IdFull) -> Self {
|
||||
Self::Text(TextChunk::new(s, id))
|
||||
}
|
||||
|
||||
pub fn new_empty() -> Self {
|
||||
Self::Text(TextChunk::new_empty())
|
||||
}
|
||||
|
||||
pub fn new_style(style: Arc<StyleOp>, anchor_type: AnchorType) -> Self {
|
||||
Self::Style { style, anchor_type }
|
||||
}
|
||||
|
@ -450,6 +460,8 @@ impl RichtextStateChunk {
|
|||
}
|
||||
}
|
||||
|
||||
impl loro_delta::delta_trait::DeltaValue for RichtextStateChunk {}
|
||||
|
||||
impl DeltaValue for RichtextStateChunk {
|
||||
fn value_extend(&mut self, other: Self) -> Result<(), Self> {
|
||||
Err(other)
|
||||
|
@ -1065,7 +1077,6 @@ mod cursor_cache {
|
|||
RichtextTreeTrait,
|
||||
};
|
||||
use generic_btree::{rle::HasLength, BTree, Cursor, LeafIndex};
|
||||
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct CursorCacheItem {
|
||||
|
|
|
@ -598,7 +598,7 @@ impl Tracker {
|
|||
self._checkout(from, false);
|
||||
self._checkout(to, true);
|
||||
// self.id_to_cursor.diagnose();
|
||||
// tracing::trace!("Trace::diff {:#?}, ", &self);
|
||||
tracing::trace!("Trace::diff {:#?}, ", &self);
|
||||
|
||||
self.rope.get_diff()
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ use loro_common::{
|
|||
CompactIdLp, ContainerID, Counter, HasCounterSpan, HasIdSpan, IdFull, IdLp, IdSpan, LoroValue,
|
||||
PeerID, ID,
|
||||
};
|
||||
use loro_delta::DeltaRope;
|
||||
use smallvec::SmallVec;
|
||||
use tracing::{instrument, trace, warn};
|
||||
|
||||
|
@ -161,6 +162,7 @@ impl DiffCalculator {
|
|||
|
||||
if *has_all {
|
||||
use_persisted_shortcut = true;
|
||||
trace!("use persisted shortcut");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -184,6 +186,7 @@ impl DiffCalculator {
|
|||
tracing::debug!("LCA: {:?} mode={:?}", &lca, diff_mode);
|
||||
let mut started_set = FxHashSet::default();
|
||||
for (change, start_counter, vv) in iter {
|
||||
let end_counter = *merged.get(&change.id.peer).unwrap();
|
||||
if let DiffCalculatorRetainMode::Persist { has_all, last_vv } =
|
||||
&mut self.retain_mode
|
||||
{
|
||||
|
@ -197,7 +200,7 @@ impl DiffCalculator {
|
|||
);
|
||||
}
|
||||
|
||||
last_vv.extend_to_include_end_id(change.id_end());
|
||||
last_vv.extend_to_include_end_id(ID::new(change.id.peer, end_counter));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -206,9 +209,8 @@ impl DiffCalculator {
|
|||
.binary_search_by(|op| op.ctr_last().cmp(&start_counter))
|
||||
.unwrap_or_else(|e| e);
|
||||
let mut visited = FxHashSet::default();
|
||||
let end_counter = merged.get(&change.id.peer).unwrap();
|
||||
for mut op in &change.ops.vec()[iter_start..] {
|
||||
if op.counter >= *end_counter {
|
||||
if op.counter >= end_counter {
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -226,7 +228,7 @@ impl DiffCalculator {
|
|||
continue;
|
||||
}
|
||||
|
||||
if op.counter < start_counter || op.ctr_end() > *end_counter {
|
||||
if op.counter < start_counter || op.ctr_end() > end_counter {
|
||||
stack_sliced_op = Some(op.slice(
|
||||
(start_counter as usize).saturating_sub(op.counter as usize),
|
||||
op.atom_len().min((end_counter - op.counter) as usize),
|
||||
|
@ -391,7 +393,7 @@ impl DiffCalculator {
|
|||
.or_insert_with(|| match idx.get_type() {
|
||||
crate::ContainerType::Text => (
|
||||
depth,
|
||||
ContainerDiffCalculator::Richtext(RichtextDiffCalculator::default()),
|
||||
ContainerDiffCalculator::Richtext(RichtextDiffCalculator::new()),
|
||||
),
|
||||
crate::ContainerType::Map => (
|
||||
depth,
|
||||
|
@ -764,19 +766,45 @@ impl DiffCalculatorTrait for ListDiffCalculator {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct RichtextDiffCalculator {
|
||||
start_vv: VersionVector,
|
||||
tracker: Box<RichtextTracker>,
|
||||
styles: Vec<StyleOp>,
|
||||
mode: Box<RichtextCalcMode>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum RichtextCalcMode {
|
||||
Crdt {
|
||||
tracker: Box<RichtextTracker>,
|
||||
styles: Vec<StyleOp>,
|
||||
start_vv: VersionVector,
|
||||
},
|
||||
Linear {
|
||||
diff: DeltaRope<RichtextStateChunk, ()>,
|
||||
last_style_start: Option<(Arc<StyleOp>, u32)>,
|
||||
},
|
||||
}
|
||||
|
||||
impl RichtextDiffCalculator {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
mode: Box::new(RichtextCalcMode::Crdt {
|
||||
tracker: Box::new(RichtextTracker::new_with_unknown()),
|
||||
styles: Vec::new(),
|
||||
start_vv: VersionVector::new(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// This should be called after calc_diff
|
||||
///
|
||||
/// TODO: Refactor, this can be simplified
|
||||
pub fn get_id_latest_pos(&self, id: ID) -> Option<AbsolutePosition> {
|
||||
self.tracker.get_target_id_latest_index_at_new_version(id)
|
||||
match &*self.mode {
|
||||
RichtextCalcMode::Crdt { tracker, .. } => {
|
||||
tracker.get_target_id_latest_index_at_new_version(id)
|
||||
}
|
||||
RichtextCalcMode::Linear { .. } => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -787,13 +815,42 @@ impl DiffCalculatorTrait for RichtextDiffCalculator {
|
|||
vv: &crate::VersionVector,
|
||||
mode: DiffMode,
|
||||
) {
|
||||
if !vv.includes_vv(&self.start_vv) || !self.tracker.all_vv().includes_vv(vv) {
|
||||
self.tracker = Box::new(RichtextTracker::new_with_unknown());
|
||||
self.styles.clear();
|
||||
self.start_vv = vv.clone();
|
||||
match mode {
|
||||
DiffMode::Linear => {
|
||||
self.mode = Box::new(RichtextCalcMode::Linear {
|
||||
diff: DeltaRope::new(),
|
||||
last_style_start: None,
|
||||
});
|
||||
}
|
||||
_ => {
|
||||
if !matches!(&*self.mode, RichtextCalcMode::Crdt { .. }) {
|
||||
self.mode = Box::new(RichtextCalcMode::Crdt {
|
||||
tracker: Box::new(RichtextTracker::new_with_unknown()),
|
||||
styles: Vec::new(),
|
||||
start_vv: vv.clone(),
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.tracker.checkout(vv);
|
||||
match &mut *self.mode {
|
||||
RichtextCalcMode::Crdt {
|
||||
tracker,
|
||||
styles,
|
||||
start_vv,
|
||||
} => {
|
||||
if !vv.includes_vv(start_vv) || tracker.all_vv().includes_vv(vv) {
|
||||
*tracker = Box::new(RichtextTracker::new_with_unknown());
|
||||
styles.clear();
|
||||
*start_vv = vv.clone();
|
||||
}
|
||||
|
||||
tracker.checkout(vv);
|
||||
}
|
||||
RichtextCalcMode::Linear { .. } => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_change(
|
||||
|
@ -802,102 +859,178 @@ impl DiffCalculatorTrait for RichtextDiffCalculator {
|
|||
op: crate::op::RichOp,
|
||||
vv: Option<&crate::VersionVector>,
|
||||
) {
|
||||
if let Some(vv) = vv {
|
||||
self.tracker.checkout(vv);
|
||||
}
|
||||
match &op.raw_op().content {
|
||||
crate::op::InnerContent::List(l) => match l {
|
||||
InnerListOp::Insert { .. } | InnerListOp::Move { .. } | InnerListOp::Set { .. } => {
|
||||
unreachable!()
|
||||
}
|
||||
crate::container::list::list_op::InnerListOp::InsertText {
|
||||
slice: _,
|
||||
unicode_start,
|
||||
unicode_len: len,
|
||||
pos,
|
||||
} => {
|
||||
self.tracker.insert(
|
||||
op.id_full(),
|
||||
*pos as usize,
|
||||
RichtextChunk::new_text(*unicode_start..*unicode_start + *len),
|
||||
);
|
||||
}
|
||||
crate::container::list::list_op::InnerListOp::Delete(del) => {
|
||||
self.tracker.delete(
|
||||
op.id_start(),
|
||||
del.id_start,
|
||||
del.start() as usize,
|
||||
del.atom_len(),
|
||||
del.is_reversed(),
|
||||
);
|
||||
}
|
||||
crate::container::list::list_op::InnerListOp::StyleStart {
|
||||
start,
|
||||
end,
|
||||
key,
|
||||
info,
|
||||
value,
|
||||
} => {
|
||||
debug_assert!(start < end, "start: {}, end: {}", start, end);
|
||||
let style_id = self.styles.len();
|
||||
self.styles.push(StyleOp {
|
||||
lamport: op.lamport(),
|
||||
peer: op.peer,
|
||||
cnt: op.id_start().counter,
|
||||
key: key.clone(),
|
||||
value: value.clone(),
|
||||
info: *info,
|
||||
});
|
||||
self.tracker.insert(
|
||||
op.id_full(),
|
||||
*start as usize,
|
||||
RichtextChunk::new_style_anchor(style_id as u32, AnchorType::Start),
|
||||
);
|
||||
}
|
||||
crate::container::list::list_op::InnerListOp::StyleEnd => {
|
||||
let id = op.id();
|
||||
// PERF: this can be sped up by caching the last style op
|
||||
let start_op = oplog.get_op(op.id().inc(-1)).unwrap();
|
||||
let InnerListOp::StyleStart {
|
||||
start: _,
|
||||
trace!("apply_change: {:?}", op.id());
|
||||
match &mut *self.mode {
|
||||
RichtextCalcMode::Linear {
|
||||
diff,
|
||||
last_style_start,
|
||||
} => match &op.raw_op().content {
|
||||
crate::op::InnerContent::List(l) => match l {
|
||||
InnerListOp::Insert { .. }
|
||||
| InnerListOp::Move { .. }
|
||||
| InnerListOp::Set { .. } => {
|
||||
unreachable!()
|
||||
}
|
||||
crate::container::list::list_op::InnerListOp::InsertText {
|
||||
slice: _,
|
||||
unicode_start,
|
||||
unicode_len: len,
|
||||
pos,
|
||||
} => {
|
||||
let s = oplog.arena.slice_by_unicode(
|
||||
*unicode_start as usize..(*unicode_start + *len) as usize,
|
||||
);
|
||||
diff.insert_value(
|
||||
*pos as usize,
|
||||
RichtextStateChunk::new_text(s, op.id_full()),
|
||||
(),
|
||||
);
|
||||
}
|
||||
crate::container::list::list_op::InnerListOp::Delete(del) => {
|
||||
diff.delete(del.start() as usize, del.atom_len());
|
||||
}
|
||||
crate::container::list::list_op::InnerListOp::StyleStart {
|
||||
start,
|
||||
end,
|
||||
key,
|
||||
value,
|
||||
info,
|
||||
} = start_op.content.as_list().unwrap()
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
let style_id = match self.styles.last() {
|
||||
Some(last) if last.peer == id.peer && last.cnt == id.counter - 1 => {
|
||||
self.styles.len() - 1
|
||||
value,
|
||||
} => {
|
||||
debug_assert!(start < end, "start: {}, end: {}", start, end);
|
||||
let style_op = Arc::new(StyleOp {
|
||||
lamport: op.lamport(),
|
||||
peer: op.peer,
|
||||
cnt: op.id_start().counter,
|
||||
key: key.clone(),
|
||||
value: value.clone(),
|
||||
info: *info,
|
||||
});
|
||||
|
||||
*last_style_start = Some((style_op.clone(), *end));
|
||||
diff.insert_value(
|
||||
*start as usize,
|
||||
RichtextStateChunk::new_style(style_op, AnchorType::Start),
|
||||
(),
|
||||
);
|
||||
}
|
||||
crate::container::list::list_op::InnerListOp::StyleEnd => {
|
||||
let (style_op, pos) = last_style_start.take().unwrap();
|
||||
assert_eq!(style_op.peer, op.peer);
|
||||
assert_eq!(style_op.cnt, op.id_start().counter - 1);
|
||||
diff.insert_value(
|
||||
pos as usize + 1,
|
||||
RichtextStateChunk::new_style(style_op, AnchorType::End),
|
||||
(),
|
||||
);
|
||||
}
|
||||
},
|
||||
_ => unreachable!(),
|
||||
},
|
||||
RichtextCalcMode::Crdt {
|
||||
tracker,
|
||||
styles,
|
||||
start_vv,
|
||||
} => {
|
||||
if let Some(vv) = vv {
|
||||
tracker.checkout(vv);
|
||||
}
|
||||
match &op.raw_op().content {
|
||||
crate::op::InnerContent::List(l) => match l {
|
||||
InnerListOp::Insert { .. }
|
||||
| InnerListOp::Move { .. }
|
||||
| InnerListOp::Set { .. } => {
|
||||
unreachable!()
|
||||
}
|
||||
_ => {
|
||||
self.styles.push(StyleOp {
|
||||
lamport: op.lamport() - 1,
|
||||
peer: id.peer,
|
||||
cnt: id.counter - 1,
|
||||
crate::container::list::list_op::InnerListOp::InsertText {
|
||||
slice: _,
|
||||
unicode_start,
|
||||
unicode_len: len,
|
||||
pos,
|
||||
} => {
|
||||
tracker.insert(
|
||||
op.id_full(),
|
||||
*pos as usize,
|
||||
RichtextChunk::new_text(*unicode_start..*unicode_start + *len),
|
||||
);
|
||||
}
|
||||
crate::container::list::list_op::InnerListOp::Delete(del) => {
|
||||
tracker.delete(
|
||||
op.id_start(),
|
||||
del.id_start,
|
||||
del.start() as usize,
|
||||
del.atom_len(),
|
||||
del.is_reversed(),
|
||||
);
|
||||
}
|
||||
crate::container::list::list_op::InnerListOp::StyleStart {
|
||||
start,
|
||||
end,
|
||||
key,
|
||||
info,
|
||||
value,
|
||||
} => {
|
||||
debug_assert!(start < end, "start: {}, end: {}", start, end);
|
||||
let style_id = styles.len();
|
||||
styles.push(StyleOp {
|
||||
lamport: op.lamport(),
|
||||
peer: op.peer,
|
||||
cnt: op.id_start().counter,
|
||||
key: key.clone(),
|
||||
value: value.clone(),
|
||||
info: *info,
|
||||
});
|
||||
self.styles.len() - 1
|
||||
tracker.insert(
|
||||
op.id_full(),
|
||||
*start as usize,
|
||||
RichtextChunk::new_style_anchor(style_id as u32, AnchorType::Start),
|
||||
);
|
||||
}
|
||||
};
|
||||
self.tracker.insert(
|
||||
op.id_full(),
|
||||
// need to shift 1 because we insert the start style anchor before this pos
|
||||
*end as usize + 1,
|
||||
RichtextChunk::new_style_anchor(style_id as u32, AnchorType::End),
|
||||
);
|
||||
crate::container::list::list_op::InnerListOp::StyleEnd => {
|
||||
let id = op.id();
|
||||
// PERF: this can be sped up by caching the last style op
|
||||
let start_op = oplog.get_op(op.id().inc(-1)).unwrap();
|
||||
let InnerListOp::StyleStart {
|
||||
start: _,
|
||||
end,
|
||||
key,
|
||||
value,
|
||||
info,
|
||||
} = start_op.content.as_list().unwrap()
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
let style_id = match styles.last() {
|
||||
Some(last)
|
||||
if last.peer == id.peer && last.cnt == id.counter - 1 =>
|
||||
{
|
||||
styles.len() - 1
|
||||
}
|
||||
_ => {
|
||||
styles.push(StyleOp {
|
||||
lamport: op.lamport() - 1,
|
||||
peer: id.peer,
|
||||
cnt: id.counter - 1,
|
||||
key: key.clone(),
|
||||
value: value.clone(),
|
||||
info: *info,
|
||||
});
|
||||
styles.len() - 1
|
||||
}
|
||||
};
|
||||
tracker.insert(
|
||||
op.id_full(),
|
||||
// need to shift 1 because we insert the start style anchor before this pos
|
||||
*end as usize + 1,
|
||||
RichtextChunk::new_style_anchor(style_id as u32, AnchorType::End),
|
||||
);
|
||||
}
|
||||
},
|
||||
_ => unreachable!(),
|
||||
}
|
||||
},
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn finish_this_round(&mut self) {}
|
||||
|
||||
fn calculate_diff(
|
||||
&mut self,
|
||||
oplog: &OpLog,
|
||||
|
@ -905,81 +1038,117 @@ impl DiffCalculatorTrait for RichtextDiffCalculator {
|
|||
to: &crate::VersionVector,
|
||||
_: impl FnMut(&ContainerID),
|
||||
) -> (InternalDiff, DiffMode) {
|
||||
// TODO: PERF: It can be linearized in certain cases
|
||||
tracing::debug!("CalcDiff {:?} {:?}", from, to);
|
||||
let mut delta = Delta::new();
|
||||
for item in self.tracker.diff(from, to) {
|
||||
match item {
|
||||
CrdtRopeDelta::Retain(len) => {
|
||||
delta = delta.retain(len);
|
||||
}
|
||||
CrdtRopeDelta::Insert {
|
||||
chunk: value,
|
||||
id,
|
||||
lamport,
|
||||
} => match value.value() {
|
||||
RichtextChunkValue::Text(text) => {
|
||||
delta = delta.insert(RichtextStateChunk::Text(
|
||||
// PERF: can be speedup by acquiring lock on arena
|
||||
TextChunk::new(
|
||||
oplog
|
||||
.arena
|
||||
.slice_by_unicode(text.start as usize..text.end as usize),
|
||||
IdFull::new(id.peer, id.counter, lamport.unwrap()),
|
||||
),
|
||||
));
|
||||
}
|
||||
RichtextChunkValue::StyleAnchor { id, anchor_type } => {
|
||||
delta = delta.insert(RichtextStateChunk::Style {
|
||||
style: Arc::new(self.styles[id as usize].clone()),
|
||||
anchor_type,
|
||||
});
|
||||
}
|
||||
RichtextChunkValue::Unknown(len) => {
|
||||
// assert not unknown id
|
||||
assert_ne!(id.peer, PeerID::MAX);
|
||||
let mut acc_len = 0;
|
||||
for rich_op in oplog.iter_ops(IdSpan::new(
|
||||
id.peer,
|
||||
id.counter,
|
||||
id.counter + len as Counter,
|
||||
)) {
|
||||
acc_len += rich_op.content_len();
|
||||
let op = rich_op.op();
|
||||
let lamport = rich_op.lamport();
|
||||
let content = op.content.as_list().unwrap();
|
||||
match content {
|
||||
match &mut *self.mode {
|
||||
RichtextCalcMode::Linear { diff, .. } => {
|
||||
trace!("end with linear mode");
|
||||
(
|
||||
InternalDiff::RichtextRaw(std::mem::take(diff)),
|
||||
DiffMode::Linear,
|
||||
)
|
||||
}
|
||||
RichtextCalcMode::Crdt {
|
||||
tracker, styles, ..
|
||||
} => {
|
||||
trace!("end with crdt mode");
|
||||
tracing::debug!("CalcDiff {:?} {:?}", from, to);
|
||||
trace!("tracker version vector = {:?}", tracker.all_vv());
|
||||
let mut delta = DeltaRope::new();
|
||||
for item in tracker.diff(from, to) {
|
||||
match item {
|
||||
CrdtRopeDelta::Retain(len) => {
|
||||
delta.push_retain(len, ());
|
||||
}
|
||||
CrdtRopeDelta::Insert {
|
||||
chunk: value,
|
||||
id,
|
||||
lamport,
|
||||
} => match value.value() {
|
||||
RichtextChunkValue::Text(text) => {
|
||||
delta.push_insert(
|
||||
RichtextStateChunk::Text(
|
||||
// PERF: can be speedup by acquiring lock on arena
|
||||
TextChunk::new(
|
||||
oplog.arena.slice_by_unicode(
|
||||
text.start as usize..text.end as usize,
|
||||
),
|
||||
IdFull::new(id.peer, id.counter, lamport.unwrap()),
|
||||
),
|
||||
),
|
||||
(),
|
||||
);
|
||||
}
|
||||
RichtextChunkValue::StyleAnchor { id, anchor_type } => {
|
||||
delta.push_insert(
|
||||
RichtextStateChunk::Style {
|
||||
style: Arc::new(styles[id as usize].clone()),
|
||||
anchor_type,
|
||||
},
|
||||
(),
|
||||
);
|
||||
}
|
||||
RichtextChunkValue::Unknown(len) => {
|
||||
// assert not unknown id
|
||||
assert_ne!(id.peer, PeerID::MAX);
|
||||
let mut acc_len = 0;
|
||||
for rich_op in oplog.iter_ops(IdSpan::new(
|
||||
id.peer,
|
||||
id.counter,
|
||||
id.counter + len as Counter,
|
||||
)) {
|
||||
acc_len += rich_op.content_len();
|
||||
let op = rich_op.op();
|
||||
let lamport = rich_op.lamport();
|
||||
let content = op.content.as_list().unwrap();
|
||||
match content {
|
||||
crate::container::list::list_op::InnerListOp::InsertText {
|
||||
slice,
|
||||
..
|
||||
} => {
|
||||
delta = delta.insert(RichtextStateChunk::Text(TextChunk::new(
|
||||
slice.clone(),
|
||||
IdFull::new(id.peer, op.counter, lamport),
|
||||
)));
|
||||
delta.push_insert(
|
||||
RichtextStateChunk::Text(TextChunk::new(
|
||||
slice.clone(),
|
||||
IdFull::new(id.peer, op.counter, lamport),
|
||||
)),
|
||||
(),
|
||||
);
|
||||
}
|
||||
_ => unreachable!("{:?}", content),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
debug_assert_eq!(acc_len, len as usize);
|
||||
debug_assert_eq!(acc_len, len as usize);
|
||||
}
|
||||
RichtextChunkValue::MoveAnchor => unreachable!(),
|
||||
},
|
||||
CrdtRopeDelta::Delete(len) => {
|
||||
delta.push_delete(len);
|
||||
}
|
||||
}
|
||||
RichtextChunkValue::MoveAnchor => unreachable!(),
|
||||
},
|
||||
CrdtRopeDelta::Delete(len) => {
|
||||
delta = delta.delete(len);
|
||||
}
|
||||
|
||||
(InternalDiff::RichtextRaw(delta), DiffMode::Checkout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(InternalDiff::RichtextRaw(delta), DiffMode::Checkout)
|
||||
fn finish_this_round(&mut self) {
|
||||
match &mut *self.mode {
|
||||
RichtextCalcMode::Crdt { .. } => {}
|
||||
RichtextCalcMode::Linear {
|
||||
diff,
|
||||
last_style_start,
|
||||
} => {
|
||||
*diff = DeltaRope::new();
|
||||
last_style_start.take();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct MovableListDiffCalculator {
|
||||
container_idx: ContainerIdx,
|
||||
list: ListDiffCalculator,
|
||||
list: Box<ListDiffCalculator>,
|
||||
changed_elements: FxHashMap<CompactIdLp, ElementDelta>,
|
||||
current_mode: DiffMode,
|
||||
}
|
||||
|
@ -1284,3 +1453,19 @@ impl MovableListDiffCalculator {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_size() {
|
||||
let text = RichtextDiffCalculator::new();
|
||||
let size = std::mem::size_of_val(&text);
|
||||
assert!(size < 50, "RichtextDiffCalculator size: {}", size);
|
||||
let list = MovableListDiffCalculator::new(ContainerIdx::from_index_and_type(
|
||||
0,
|
||||
loro_common::ContainerType::MovableList,
|
||||
));
|
||||
let size = std::mem::size_of_val(&list);
|
||||
assert!(size < 50, "MovableListDiffCalculator size: {}", size);
|
||||
let calc = ContainerDiffCalculator::Richtext(text);
|
||||
let size = std::mem::size_of_val(&calc);
|
||||
assert!(size < 50, "ContainerDiffCalculator size: {}", size);
|
||||
}
|
||||
|
|
|
@ -234,7 +234,7 @@ impl DiffVariant {
|
|||
pub(crate) enum InternalDiff {
|
||||
ListRaw(Delta<SliceRanges>),
|
||||
/// This always uses entity indexes.
|
||||
RichtextRaw(Delta<RichtextStateChunk>),
|
||||
RichtextRaw(DeltaRope<RichtextStateChunk, ()>),
|
||||
Map(MapDelta),
|
||||
Tree(TreeDelta),
|
||||
MovableList(MovableListInnerDelta),
|
||||
|
@ -356,7 +356,9 @@ impl InternalDiff {
|
|||
Ok(InternalDiff::ListRaw(a.compose(b)))
|
||||
}
|
||||
(InternalDiff::RichtextRaw(a), InternalDiff::RichtextRaw(b)) => {
|
||||
Ok(InternalDiff::RichtextRaw(a.compose(b)))
|
||||
let mut ans = a.clone();
|
||||
ans.compose(&b);
|
||||
Ok(InternalDiff::RichtextRaw(ans))
|
||||
}
|
||||
(InternalDiff::Map(a), InternalDiff::Map(b)) => Ok(InternalDiff::Map(a.compose(b))),
|
||||
(InternalDiff::Tree(a), InternalDiff::Tree(b)) => Ok(InternalDiff::Tree(a.compose(b))),
|
||||
|
|
|
@ -7,6 +7,7 @@ use fxhash::{FxHashMap, FxHashSet};
|
|||
use generic_btree::rle::HasLength;
|
||||
use loro_common::{ContainerID, InternalString, LoroError, LoroResult, LoroValue, ID};
|
||||
use loro_delta::DeltaRopeBuilder;
|
||||
use tracing::trace;
|
||||
|
||||
use crate::{
|
||||
arena::SharedArena,
|
||||
|
@ -282,6 +283,7 @@ impl ContainerState for RichtextState {
|
|||
unreachable!()
|
||||
};
|
||||
|
||||
trace!("diff = {:#?}, mode = {:?}", richtext, _ctx.mode);
|
||||
// tracing::info!("Self state = {:#?}", &self);
|
||||
// PERF: compose delta
|
||||
let mut ans: TextDiff = TextDiff::new();
|
||||
|
@ -290,156 +292,167 @@ impl ContainerState for RichtextState {
|
|||
let mut entity_index = 0;
|
||||
let mut event_index = 0;
|
||||
let mut new_style_deltas: Vec<TextDiff> = Vec::new();
|
||||
for span in richtext.vec.iter() {
|
||||
for span in richtext.iter() {
|
||||
match span {
|
||||
crate::delta::DeltaItem::Retain { retain: len, .. } => {
|
||||
loro_delta::DeltaItem::Retain { len, .. } => {
|
||||
entity_index += len;
|
||||
}
|
||||
crate::delta::DeltaItem::Insert { insert: value, .. } => {
|
||||
match value {
|
||||
RichtextStateChunk::Text(s) => {
|
||||
let (pos, styles) = self.state.get_mut().insert_elem_at_entity_index(
|
||||
entity_index,
|
||||
RichtextStateChunk::Text(s.clone()),
|
||||
);
|
||||
let insert_styles = styles.clone().into();
|
||||
loro_delta::DeltaItem::Replace { value, delete, .. } => {
|
||||
if *delete > 0 {
|
||||
// Deletions
|
||||
let mut deleted_style_keys: FxHashSet<InternalString> =
|
||||
FxHashSet::default();
|
||||
let DrainInfo {
|
||||
start_event_index: start,
|
||||
end_event_index: end,
|
||||
affected_style_range,
|
||||
} = self.state.get_mut().drain_by_entity_index(
|
||||
entity_index,
|
||||
*delete,
|
||||
Some(&mut |c| match c {
|
||||
RichtextStateChunk::Style {
|
||||
style,
|
||||
anchor_type: AnchorType::Start,
|
||||
} => {
|
||||
deleted_style_keys.insert(style.key.clone());
|
||||
}
|
||||
RichtextStateChunk::Style {
|
||||
style,
|
||||
anchor_type: AnchorType::End,
|
||||
} => {
|
||||
deleted_style_keys.insert(style.key.clone());
|
||||
}
|
||||
_ => {}
|
||||
}),
|
||||
);
|
||||
|
||||
if pos > event_index {
|
||||
ans.push_retain(pos - event_index, Default::default());
|
||||
}
|
||||
event_index = pos + s.event_len() as usize;
|
||||
ans.push_insert(StringSlice::from(s.bytes().clone()), insert_styles);
|
||||
if start > event_index {
|
||||
ans.push_retain(start - event_index, Default::default());
|
||||
event_index = start;
|
||||
}
|
||||
RichtextStateChunk::Style { anchor_type, style } => {
|
||||
let (new_event_index, _) =
|
||||
self.state.get_mut().insert_elem_at_entity_index(
|
||||
entity_index,
|
||||
RichtextStateChunk::Style {
|
||||
style: style.clone(),
|
||||
anchor_type: *anchor_type,
|
||||
},
|
||||
);
|
||||
|
||||
if new_event_index > event_index {
|
||||
ans.push_retain(new_event_index - event_index, Default::default());
|
||||
// inserting style anchor will not affect event_index's positions
|
||||
event_index = new_event_index;
|
||||
if let Some((entity_range, event_range)) = affected_style_range {
|
||||
let mut delta: TextDiff = DeltaRopeBuilder::new()
|
||||
.retain(event_range.start, Default::default())
|
||||
.build();
|
||||
let mut entity_len_sum = 0;
|
||||
let expected_sum = entity_range.len();
|
||||
|
||||
for IterRangeItem {
|
||||
event_len,
|
||||
chunk,
|
||||
styles,
|
||||
entity_len,
|
||||
..
|
||||
} in self.state.get_mut().iter_range(entity_range)
|
||||
{
|
||||
entity_len_sum += entity_len;
|
||||
match chunk {
|
||||
RichtextStateChunk::Text(_) => {
|
||||
let mut style_meta: StyleMeta = styles.into();
|
||||
for key in deleted_style_keys.iter() {
|
||||
if !style_meta.contains_key(key) {
|
||||
style_meta.insert(
|
||||
key.clone(),
|
||||
StyleMetaItem {
|
||||
lamport: 0,
|
||||
peer: 0,
|
||||
value: LoroValue::Null,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
delta.push_retain(event_len, style_meta);
|
||||
}
|
||||
RichtextStateChunk::Style { .. } => {}
|
||||
}
|
||||
}
|
||||
|
||||
match anchor_type {
|
||||
AnchorType::Start => {
|
||||
style_starts.insert(
|
||||
style.clone(),
|
||||
Pos {
|
||||
entity_index,
|
||||
event_index: new_event_index,
|
||||
debug_assert_eq!(entity_len_sum, expected_sum);
|
||||
delta.chop();
|
||||
style_delta.compose(&delta);
|
||||
}
|
||||
|
||||
ans.push_delete(end - start);
|
||||
}
|
||||
|
||||
if value.rle_len() > 0 {
|
||||
// Insertions
|
||||
match value {
|
||||
RichtextStateChunk::Text(s) => {
|
||||
let (pos, styles) =
|
||||
self.state.get_mut().insert_elem_at_entity_index(
|
||||
entity_index,
|
||||
RichtextStateChunk::Text(s.clone()),
|
||||
);
|
||||
let insert_styles = styles.clone().into();
|
||||
|
||||
if pos > event_index {
|
||||
ans.push_retain(pos - event_index, Default::default());
|
||||
}
|
||||
event_index = pos + s.event_len() as usize;
|
||||
ans.push_insert(
|
||||
StringSlice::from(s.bytes().clone()),
|
||||
insert_styles,
|
||||
);
|
||||
}
|
||||
RichtextStateChunk::Style { anchor_type, style } => {
|
||||
let (new_event_index, _) =
|
||||
self.state.get_mut().insert_elem_at_entity_index(
|
||||
entity_index,
|
||||
RichtextStateChunk::Style {
|
||||
style: style.clone(),
|
||||
anchor_type: *anchor_type,
|
||||
},
|
||||
);
|
||||
|
||||
if new_event_index > event_index {
|
||||
ans.push_retain(
|
||||
new_event_index - event_index,
|
||||
Default::default(),
|
||||
);
|
||||
// inserting style anchor will not affect event_index's positions
|
||||
event_index = new_event_index;
|
||||
}
|
||||
AnchorType::End => {
|
||||
// get the pair of style anchor. now we can annotate the range
|
||||
let Pos {
|
||||
entity_index: start_entity_index,
|
||||
event_index: start_event_index,
|
||||
} = self.get_style_start(&mut style_starts, style);
|
||||
let mut delta: TextDiff = DeltaRopeBuilder::new()
|
||||
.retain(start_event_index, Default::default())
|
||||
.build();
|
||||
// we need to + 1 because we also need to annotate the end anchor
|
||||
let event =
|
||||
self.state.get_mut().annotate_style_range_with_event(
|
||||
start_entity_index..entity_index + 1,
|
||||
|
||||
match anchor_type {
|
||||
AnchorType::Start => {
|
||||
style_starts.insert(
|
||||
style.clone(),
|
||||
Pos {
|
||||
entity_index,
|
||||
event_index: new_event_index,
|
||||
},
|
||||
);
|
||||
for (s, l) in event {
|
||||
delta.push_retain(l, s);
|
||||
}
|
||||
|
||||
delta.chop();
|
||||
new_style_deltas.push(delta);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
entity_index += value.rle_len();
|
||||
}
|
||||
crate::delta::DeltaItem::Delete {
|
||||
delete: len,
|
||||
attributes: _,
|
||||
} => {
|
||||
let mut deleted_style_keys: FxHashSet<InternalString> = FxHashSet::default();
|
||||
let DrainInfo {
|
||||
start_event_index: start,
|
||||
end_event_index: end,
|
||||
affected_style_range,
|
||||
} = self.state.get_mut().drain_by_entity_index(
|
||||
entity_index,
|
||||
*len,
|
||||
Some(&mut |c| match c {
|
||||
RichtextStateChunk::Style {
|
||||
style,
|
||||
anchor_type: AnchorType::Start,
|
||||
} => {
|
||||
deleted_style_keys.insert(style.key.clone());
|
||||
}
|
||||
RichtextStateChunk::Style {
|
||||
style,
|
||||
anchor_type: AnchorType::End,
|
||||
} => {
|
||||
deleted_style_keys.insert(style.key.clone());
|
||||
}
|
||||
_ => {}
|
||||
}),
|
||||
);
|
||||
|
||||
if start > event_index {
|
||||
ans.push_retain(start - event_index, Default::default());
|
||||
event_index = start;
|
||||
}
|
||||
|
||||
if let Some((entity_range, event_range)) = affected_style_range {
|
||||
let mut delta: TextDiff = DeltaRopeBuilder::new()
|
||||
.retain(event_range.start, Default::default())
|
||||
.build();
|
||||
let mut entity_len_sum = 0;
|
||||
let expected_sum = entity_range.len();
|
||||
|
||||
for IterRangeItem {
|
||||
event_len,
|
||||
chunk,
|
||||
styles,
|
||||
entity_len,
|
||||
..
|
||||
} in self.state.get_mut().iter_range(entity_range)
|
||||
{
|
||||
entity_len_sum += entity_len;
|
||||
match chunk {
|
||||
RichtextStateChunk::Text(_) => {
|
||||
let mut style_meta: StyleMeta = styles.into();
|
||||
for key in deleted_style_keys.iter() {
|
||||
if !style_meta.contains_key(key) {
|
||||
style_meta.insert(
|
||||
key.clone(),
|
||||
StyleMetaItem {
|
||||
lamport: 0,
|
||||
peer: 0,
|
||||
value: LoroValue::Null,
|
||||
},
|
||||
)
|
||||
AnchorType::End => {
|
||||
// get the pair of style anchor. now we can annotate the range
|
||||
let Pos {
|
||||
entity_index: start_entity_index,
|
||||
event_index: start_event_index,
|
||||
} = self.get_style_start(&mut style_starts, style);
|
||||
let mut delta: TextDiff = DeltaRopeBuilder::new()
|
||||
.retain(start_event_index, Default::default())
|
||||
.build();
|
||||
// we need to + 1 because we also need to annotate the end anchor
|
||||
let event =
|
||||
self.state.get_mut().annotate_style_range_with_event(
|
||||
start_entity_index..entity_index + 1,
|
||||
style.clone(),
|
||||
);
|
||||
for (s, l) in event {
|
||||
delta.push_retain(l, s);
|
||||
}
|
||||
|
||||
delta.chop();
|
||||
new_style_deltas.push(delta);
|
||||
}
|
||||
delta.push_retain(event_len, style_meta);
|
||||
}
|
||||
RichtextStateChunk::Style { .. } => {}
|
||||
}
|
||||
}
|
||||
|
||||
debug_assert_eq!(entity_len_sum, expected_sum);
|
||||
delta.chop();
|
||||
style_delta.compose(&delta);
|
||||
entity_index += value.rle_len();
|
||||
}
|
||||
|
||||
ans.push_delete(end - start);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -458,80 +471,81 @@ impl ContainerState for RichtextState {
|
|||
unreachable!()
|
||||
};
|
||||
|
||||
trace!("diff = {:#?}", richtext);
|
||||
let mut style_starts: FxHashMap<Arc<StyleOp>, usize> = FxHashMap::default();
|
||||
let mut entity_index = 0;
|
||||
for span in richtext.vec.iter() {
|
||||
for span in richtext.iter() {
|
||||
match span {
|
||||
crate::delta::DeltaItem::Retain {
|
||||
retain: len,
|
||||
attributes: _,
|
||||
} => {
|
||||
loro_delta::DeltaItem::Retain { len, .. } => {
|
||||
entity_index += len;
|
||||
}
|
||||
crate::delta::DeltaItem::Insert {
|
||||
insert: value,
|
||||
attributes: _,
|
||||
loro_delta::DeltaItem::Replace {
|
||||
value,
|
||||
attr,
|
||||
delete,
|
||||
} => {
|
||||
match value {
|
||||
RichtextStateChunk::Text(s) => {
|
||||
self.state.get_mut().insert_elem_at_entity_index(
|
||||
entity_index,
|
||||
RichtextStateChunk::Text(s.clone()),
|
||||
);
|
||||
}
|
||||
RichtextStateChunk::Style { style, anchor_type } => {
|
||||
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);
|
||||
} else {
|
||||
let start_pos = match style_starts.get(style) {
|
||||
Some(x) => *x,
|
||||
None => {
|
||||
// This should be rare, so it should be fine to scan
|
||||
let mut start_entity_index = 0;
|
||||
for c in self.state.get_mut().iter_chunk() {
|
||||
match c {
|
||||
RichtextStateChunk::Style {
|
||||
style: s,
|
||||
anchor_type: AnchorType::Start,
|
||||
} if style == s => {
|
||||
break;
|
||||
}
|
||||
RichtextStateChunk::Text(t) => {
|
||||
start_entity_index += t.unicode_len() as usize;
|
||||
}
|
||||
RichtextStateChunk::Style { .. } => {
|
||||
start_entity_index += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
start_entity_index
|
||||
}
|
||||
};
|
||||
// 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,
|
||||
style.clone(),
|
||||
if *delete > 0 {
|
||||
// Deletions
|
||||
self.state
|
||||
.get_mut()
|
||||
.drain_by_entity_index(entity_index, *delete, None);
|
||||
}
|
||||
if value.rle_len() > 0 {
|
||||
// Insertions
|
||||
match value {
|
||||
RichtextStateChunk::Text(s) => {
|
||||
self.state.get_mut().insert_elem_at_entity_index(
|
||||
entity_index,
|
||||
RichtextStateChunk::Text(s.clone()),
|
||||
);
|
||||
}
|
||||
RichtextStateChunk::Style { style, anchor_type } => {
|
||||
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);
|
||||
} else {
|
||||
let start_pos = match style_starts.get(style) {
|
||||
Some(x) => *x,
|
||||
None => {
|
||||
// This should be rare, so it should be fine to scan
|
||||
let mut start_entity_index = 0;
|
||||
for c in self.state.get_mut().iter_chunk() {
|
||||
match c {
|
||||
RichtextStateChunk::Style {
|
||||
style: s,
|
||||
anchor_type: AnchorType::Start,
|
||||
} if style == s => {
|
||||
break;
|
||||
}
|
||||
RichtextStateChunk::Text(t) => {
|
||||
start_entity_index +=
|
||||
t.unicode_len() as usize;
|
||||
}
|
||||
RichtextStateChunk::Style { .. } => {
|
||||
start_entity_index += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
start_entity_index
|
||||
}
|
||||
};
|
||||
// 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,
|
||||
style.clone(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
entity_index += value.rle_len();
|
||||
}
|
||||
entity_index += value.rle_len();
|
||||
}
|
||||
crate::delta::DeltaItem::Delete {
|
||||
delete: len,
|
||||
attributes: _,
|
||||
} => {
|
||||
self.state
|
||||
.get_mut()
|
||||
.drain_by_entity_index(entity_index, *len, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue