mirror of
https://github.com/loro-dev/loro.git
synced 2025-02-05 20:17:13 +00:00
Undo (#361)
https://github.com/loro-dev/loro/pull/361 --------- Co-authored-by: Leon Zhao <leeeon233@gmail.com>
This commit is contained in:
parent
26753f0d4d
commit
321e0e72a4
53 changed files with 4757 additions and 170 deletions
37
Cargo.lock
generated
37
Cargo.lock
generated
|
@ -44,6 +44,12 @@ version = "1.0.6"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc"
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.83"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3"
|
||||
|
||||
[[package]]
|
||||
name = "append-only-bytes"
|
||||
version = "0.1.12"
|
||||
|
@ -647,7 +653,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "fractional_index"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/loro-dev/loro.git?branch=leon/feat-encode-forward#c93a80dbff7ead8afb466884642dbef49f9e16b4"
|
||||
source = "git+https://github.com/loro-dev/loro.git?rev=0dade6bc0fb574a8190db2aa80c83a479f62e125#0dade6bc0fb574a8190db2aa80c83a479f62e125"
|
||||
dependencies = [
|
||||
"imbl",
|
||||
"rand",
|
||||
|
@ -678,8 +684,8 @@ dependencies = [
|
|||
"fxhash",
|
||||
"itertools 0.12.1",
|
||||
"loro 0.5.1",
|
||||
"loro 0.5.1 (git+https://github.com/loro-dev/loro.git?branch=leon/feat-encode-forward)",
|
||||
"loro-internal 0.5.1",
|
||||
"loro 0.5.1 (git+https://github.com/loro-dev/loro.git?rev=0dade6bc0fb574a8190db2aa80c83a479f62e125)",
|
||||
"loro-common 0.5.1 (git+https://github.com/loro-dev/loro.git?rev=0dade6bc0fb574a8190db2aa80c83a479f62e125)",
|
||||
"rand",
|
||||
"tabled 0.10.0",
|
||||
"tracing",
|
||||
|
@ -1000,6 +1006,7 @@ dependencies = [
|
|||
name = "loro"
|
||||
version = "0.5.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"ctor 0.2.6",
|
||||
"dev-utils",
|
||||
"either",
|
||||
|
@ -1014,13 +1021,13 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "loro"
|
||||
version = "0.5.1"
|
||||
source = "git+https://github.com/loro-dev/loro.git?branch=leon/feat-encode-forward#c93a80dbff7ead8afb466884642dbef49f9e16b4"
|
||||
source = "git+https://github.com/loro-dev/loro.git?rev=0dade6bc0fb574a8190db2aa80c83a479f62e125#0dade6bc0fb574a8190db2aa80c83a479f62e125"
|
||||
dependencies = [
|
||||
"either",
|
||||
"enum-as-inner 0.6.0",
|
||||
"generic-btree",
|
||||
"loro-delta 0.5.1 (git+https://github.com/loro-dev/loro.git?branch=leon/feat-encode-forward)",
|
||||
"loro-internal 0.5.1 (git+https://github.com/loro-dev/loro.git?branch=leon/feat-encode-forward)",
|
||||
"loro-delta 0.5.1 (git+https://github.com/loro-dev/loro.git?rev=0dade6bc0fb574a8190db2aa80c83a479f62e125)",
|
||||
"loro-internal 0.5.1 (git+https://github.com/loro-dev/loro.git?rev=0dade6bc0fb574a8190db2aa80c83a479f62e125)",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
|
@ -1044,12 +1051,12 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "loro-common"
|
||||
version = "0.5.1"
|
||||
source = "git+https://github.com/loro-dev/loro.git?branch=leon/feat-encode-forward#c93a80dbff7ead8afb466884642dbef49f9e16b4"
|
||||
source = "git+https://github.com/loro-dev/loro.git?rev=0dade6bc0fb574a8190db2aa80c83a479f62e125#0dade6bc0fb574a8190db2aa80c83a479f62e125"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"enum-as-inner 0.6.0",
|
||||
"fxhash",
|
||||
"loro-rle 0.5.1 (git+https://github.com/loro-dev/loro.git?branch=leon/feat-encode-forward)",
|
||||
"loro-rle 0.5.1 (git+https://github.com/loro-dev/loro.git?rev=0dade6bc0fb574a8190db2aa80c83a479f62e125)",
|
||||
"nonmax",
|
||||
"serde",
|
||||
"serde_columnar",
|
||||
|
@ -1076,7 +1083,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "loro-delta"
|
||||
version = "0.5.1"
|
||||
source = "git+https://github.com/loro-dev/loro.git?branch=leon/feat-encode-forward#c93a80dbff7ead8afb466884642dbef49f9e16b4"
|
||||
source = "git+https://github.com/loro-dev/loro.git?rev=0dade6bc0fb574a8190db2aa80c83a479f62e125#0dade6bc0fb574a8190db2aa80c83a479f62e125"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"enum-as-inner 0.5.1",
|
||||
|
@ -1139,23 +1146,23 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "loro-internal"
|
||||
version = "0.5.1"
|
||||
source = "git+https://github.com/loro-dev/loro.git?branch=leon/feat-encode-forward#c93a80dbff7ead8afb466884642dbef49f9e16b4"
|
||||
source = "git+https://github.com/loro-dev/loro.git?rev=0dade6bc0fb574a8190db2aa80c83a479f62e125#0dade6bc0fb574a8190db2aa80c83a479f62e125"
|
||||
dependencies = [
|
||||
"append-only-bytes",
|
||||
"arref",
|
||||
"either",
|
||||
"enum-as-inner 0.5.1",
|
||||
"enum_dispatch",
|
||||
"fractional_index 0.1.0 (git+https://github.com/loro-dev/loro.git?branch=leon/feat-encode-forward)",
|
||||
"fractional_index 0.1.0 (git+https://github.com/loro-dev/loro.git?rev=0dade6bc0fb574a8190db2aa80c83a479f62e125)",
|
||||
"fxhash",
|
||||
"generic-btree",
|
||||
"getrandom",
|
||||
"im",
|
||||
"itertools 0.12.1",
|
||||
"leb128",
|
||||
"loro-common 0.5.1 (git+https://github.com/loro-dev/loro.git?branch=leon/feat-encode-forward)",
|
||||
"loro-delta 0.5.1 (git+https://github.com/loro-dev/loro.git?branch=leon/feat-encode-forward)",
|
||||
"loro-rle 0.5.1 (git+https://github.com/loro-dev/loro.git?branch=leon/feat-encode-forward)",
|
||||
"loro-common 0.5.1 (git+https://github.com/loro-dev/loro.git?rev=0dade6bc0fb574a8190db2aa80c83a479f62e125)",
|
||||
"loro-delta 0.5.1 (git+https://github.com/loro-dev/loro.git?rev=0dade6bc0fb574a8190db2aa80c83a479f62e125)",
|
||||
"loro-rle 0.5.1 (git+https://github.com/loro-dev/loro.git?rev=0dade6bc0fb574a8190db2aa80c83a479f62e125)",
|
||||
"md5",
|
||||
"num",
|
||||
"num-derive",
|
||||
|
@ -1191,7 +1198,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "loro-rle"
|
||||
version = "0.5.1"
|
||||
source = "git+https://github.com/loro-dev/loro.git?branch=leon/feat-encode-forward#c93a80dbff7ead8afb466884642dbef49f9e16b4"
|
||||
source = "git+https://github.com/loro-dev/loro.git?rev=0dade6bc0fb574a8190db2aa80c83a479f62e125#0dade6bc0fb574a8190db2aa80c83a479f62e125"
|
||||
dependencies = [
|
||||
"append-only-bytes",
|
||||
"arref",
|
||||
|
|
4
crates/delta/fuzz/.gitignore
vendored
Normal file
4
crates/delta/fuzz/.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
target
|
||||
corpus
|
||||
artifacts
|
||||
coverage
|
6
crates/delta/fuzz/.vscode/settings.json
vendored
Normal file
6
crates/delta/fuzz/.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"rust-analyzer.runnableEnv": {
|
||||
"RUST_BACKTRACE": "full",
|
||||
"DEBUG": "*"
|
||||
},
|
||||
}
|
1002
crates/delta/fuzz/Cargo.lock
generated
Normal file
1002
crates/delta/fuzz/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
31
crates/delta/fuzz/Cargo.toml
Normal file
31
crates/delta/fuzz/Cargo.toml
Normal file
|
@ -0,0 +1,31 @@
|
|||
[package]
|
||||
name = "loro-delta-fuzz"
|
||||
version = "0.0.0"
|
||||
publish = false
|
||||
edition = "2021"
|
||||
|
||||
[package.metadata]
|
||||
cargo-fuzz = true
|
||||
|
||||
[dependencies]
|
||||
arbitrary = { version = "1.3.2", features = ["derive"] }
|
||||
libfuzzer-sys = "0.4"
|
||||
dev-utils = { path = "../../dev-utils" }
|
||||
tracing = "0.1.40"
|
||||
ctor = "0.2.8"
|
||||
|
||||
[dependencies.loro-delta]
|
||||
path = ".."
|
||||
|
||||
# Prevent this from interfering with workspaces
|
||||
[workspace]
|
||||
members = ["."]
|
||||
|
||||
[profile.release]
|
||||
debug = 1
|
||||
|
||||
[[bin]]
|
||||
name = "ot"
|
||||
path = "fuzz_targets/ot.rs"
|
||||
test = false
|
||||
doc = false
|
6
crates/delta/fuzz/fuzz_targets/ot.rs
Normal file
6
crates/delta/fuzz/fuzz_targets/ot.rs
Normal file
|
@ -0,0 +1,6 @@
|
|||
#![no_main]
|
||||
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
use loro_delta_fuzz::{run, Op};
|
||||
|
||||
fuzz_target!(|ops: Vec<Op>| run(ops, 5));
|
159
crates/delta/fuzz/src/lib.rs
Normal file
159
crates/delta/fuzz/src/lib.rs
Normal file
|
@ -0,0 +1,159 @@
|
|||
use arbitrary::Arbitrary;
|
||||
use loro_delta::{
|
||||
text_delta::{TextChunk, TextDelta},
|
||||
DeltaItem,
|
||||
};
|
||||
use tracing::{debug_span, instrument, trace};
|
||||
|
||||
#[derive(Debug, Arbitrary)]
|
||||
pub enum Op {
|
||||
Insert { site: u8, pos: u16, text: u16 },
|
||||
Delete { site: u8, pos: u16, len: u16 },
|
||||
Sync { site: u8 },
|
||||
}
|
||||
|
||||
pub struct Actor {
|
||||
rope: TextDelta,
|
||||
last_sync_version: usize,
|
||||
pending: TextDelta,
|
||||
}
|
||||
|
||||
pub struct Manager {
|
||||
server: TextDelta,
|
||||
versions: Vec<TextDelta>,
|
||||
actors: Vec<Actor>,
|
||||
}
|
||||
|
||||
#[instrument(skip(m))]
|
||||
fn sync(m: &mut Manager, site: usize) {
|
||||
let actor = &mut m.actors[site];
|
||||
let mut server_ops = TextDelta::new();
|
||||
for t in &m.versions[actor.last_sync_version..] {
|
||||
server_ops.compose(t);
|
||||
}
|
||||
|
||||
let client_to_apply = server_ops.transform(&actor.pending, true);
|
||||
|
||||
let client_ops = std::mem::take(&mut actor.pending);
|
||||
|
||||
let server_to_apply = client_ops.transform(&server_ops, false);
|
||||
|
||||
actor.rope.compose(&client_to_apply);
|
||||
m.server.compose(&server_to_apply);
|
||||
m.versions.push(server_to_apply);
|
||||
actor.last_sync_version = m.versions.len();
|
||||
}
|
||||
|
||||
pub fn run(mut ops: Vec<Op>, site_num: usize) {
|
||||
let mut m = Manager {
|
||||
server: TextDelta::new(),
|
||||
versions: vec![],
|
||||
actors: vec![],
|
||||
};
|
||||
for _ in 0..site_num {
|
||||
m.actors.push(Actor {
|
||||
rope: TextDelta::new(),
|
||||
last_sync_version: 0,
|
||||
pending: TextDelta::new(),
|
||||
})
|
||||
}
|
||||
|
||||
for op in &mut ops {
|
||||
match op {
|
||||
Op::Insert { site, pos, text } => {
|
||||
*site = ((*site as usize) % site_num) as u8;
|
||||
let actor = &mut m.actors[*site as usize];
|
||||
let len = actor.rope.len();
|
||||
*pos = ((*pos as usize) % (len + 1)) as u16;
|
||||
let pos = *pos as usize;
|
||||
|
||||
actor.rope.insert_str(pos, text.to_string().as_str());
|
||||
if actor.pending.len() < pos {
|
||||
actor.pending.push_retain(pos, ());
|
||||
}
|
||||
actor.pending.insert_values(
|
||||
pos,
|
||||
TextChunk::from_long_str(text.to_string().as_str()).map(|chunk| {
|
||||
DeltaItem::Replace {
|
||||
value: chunk,
|
||||
attr: Default::default(),
|
||||
delete: 0,
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
Op::Delete {
|
||||
site,
|
||||
pos,
|
||||
len: del_len,
|
||||
} => {
|
||||
*site = ((*site as usize) % site_num) as u8;
|
||||
let actor = &mut m.actors[*site as usize];
|
||||
let len = actor.rope.len();
|
||||
if len == 0 {
|
||||
continue;
|
||||
}
|
||||
*pos = ((*pos as usize) % len) as u16;
|
||||
let pos = *pos as usize;
|
||||
*del_len = ((*del_len as usize) % len) as u16;
|
||||
let del_len = *del_len as usize;
|
||||
let mut del = TextDelta::new();
|
||||
del.push_retain(pos, ()).push_delete(del_len);
|
||||
actor.rope.compose(&del);
|
||||
actor.pending.compose(&del);
|
||||
}
|
||||
Op::Sync { site } => {
|
||||
*site = ((*site as usize) % site_num) as u8;
|
||||
let site = *site as usize;
|
||||
sync(&mut m, site);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
debug_span!("Round 1").in_scope(|| {
|
||||
for i in 0..site_num {
|
||||
sync(&mut m, i);
|
||||
}
|
||||
});
|
||||
debug_span!("Round 2").in_scope(|| {
|
||||
for i in 0..site_num {
|
||||
sync(&mut m, i);
|
||||
}
|
||||
});
|
||||
|
||||
let server_str = m.server.try_to_string().unwrap();
|
||||
for i in 0..site_num {
|
||||
let actor = &m.actors[i];
|
||||
let rope_str = actor.rope.try_to_string().unwrap();
|
||||
assert_eq!(rope_str, server_str, "site {} ops={:#?}", i, &ops);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Op::*;
|
||||
use super::*;
|
||||
|
||||
#[ctor::ctor]
|
||||
fn init() {
|
||||
dev_utils::setup_test_log();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_run() {
|
||||
let ops = vec![
|
||||
Insert {
|
||||
site: 1,
|
||||
pos: 0,
|
||||
text: 65535,
|
||||
},
|
||||
Sync { site: 1 },
|
||||
Insert {
|
||||
site: 1,
|
||||
pos: 5,
|
||||
text: 0,
|
||||
},
|
||||
];
|
||||
run(ops, 2);
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@ use super::*;
|
|||
use generic_btree::rle::{CanRemove, TryInsert};
|
||||
|
||||
impl<V: DeltaValue, Attr> DeltaItem<V, Attr> {
|
||||
/// The real length of the item in the delta
|
||||
/// Including the delete length
|
||||
pub fn delta_len(&self) -> usize {
|
||||
match self {
|
||||
DeltaItem::Retain { len, .. } => *len,
|
||||
|
@ -16,6 +16,7 @@ impl<V: DeltaValue, Attr> DeltaItem<V, Attr> {
|
|||
}
|
||||
}
|
||||
|
||||
/// The real length of the item in the delta, excluding the delete length
|
||||
pub fn data_len(&self) -> usize {
|
||||
match self {
|
||||
DeltaItem::Retain { len, .. } => *len,
|
||||
|
|
|
@ -233,6 +233,64 @@ impl<V: DeltaValue, Attr: DeltaAttr> DeltaRope<V, Attr> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn transform(&self, other: &Self, left_prior: bool) -> Self {
|
||||
let mut this_iter = self.iter_with_len();
|
||||
let mut other_iter = other.iter_with_len();
|
||||
let mut transformed_delta = DeltaRope::new();
|
||||
|
||||
while this_iter.peek().is_some() || other_iter.peek().is_some() {
|
||||
if this_iter.peek_is_insert() && (left_prior || !other_iter.peek_is_insert()) {
|
||||
let insert_length;
|
||||
match this_iter.peek().unwrap() {
|
||||
DeltaItem::Replace { value, attr, .. } => {
|
||||
insert_length = value.rle_len();
|
||||
transformed_delta.push_insert(value.clone(), attr.clone());
|
||||
}
|
||||
DeltaItem::Retain { .. } => unreachable!(),
|
||||
}
|
||||
this_iter.next_with(insert_length).unwrap();
|
||||
} else if other_iter.peek_is_insert() {
|
||||
let insert_length = other_iter.peek_insert_length();
|
||||
transformed_delta.push_retain(insert_length, Default::default());
|
||||
other_iter.next_with(insert_length).unwrap();
|
||||
} else {
|
||||
// It's now either retains or deletes
|
||||
let length = this_iter.peek_length().min(other_iter.peek_length());
|
||||
let this_op_peek = this_iter.peek().cloned();
|
||||
let other_op_peek = other_iter.peek().cloned();
|
||||
let _ = this_iter.next_with(length);
|
||||
let _ = other_iter.next_with(length);
|
||||
if other_op_peek.map(|x| x.is_delete()).unwrap_or(false) {
|
||||
// It makes our deletes or retains redundant
|
||||
continue;
|
||||
} else if this_op_peek
|
||||
.as_ref()
|
||||
.map(|x| x.is_delete())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
transformed_delta.push_delete(length);
|
||||
} else {
|
||||
transformed_delta.push_retain(
|
||||
length,
|
||||
this_op_peek
|
||||
.map(|x| x.into_retain().unwrap().1)
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
// FIXME: transform the attributes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
transformed_delta.chop();
|
||||
transformed_delta
|
||||
}
|
||||
|
||||
/// Transforms operation `self` against another operation `other` in such a way that the
|
||||
/// impact of `other` is effectively included in `self`.
|
||||
pub fn transform_(&mut self, other: &Self, left_prior: bool) {
|
||||
*self = self.transform(other, left_prior);
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: DeltaValue + PartialEq, Attr: DeltaAttr + PartialEq> PartialEq for DeltaRope<V, Attr> {
|
||||
|
@ -308,7 +366,7 @@ impl<V: DeltaValue + Debug, Attr: DeltaAttr + Debug> Default for DeltaRope<V, At
|
|||
}
|
||||
|
||||
impl<V: DeltaValue, Attr: DeltaAttr> DeltaRope<V, Attr> {
|
||||
pub(crate) fn insert_values(
|
||||
pub fn insert_values(
|
||||
&mut self,
|
||||
pos: usize,
|
||||
values: impl IntoIterator<Item = DeltaItem<V, Attr>>,
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
|
||||
|
||||
use super::*;
|
||||
|
||||
struct DeltaReplace<'a, V, Attr> {
|
||||
|
|
|
@ -30,6 +30,35 @@ impl<'a, V: DeltaValue, Attr: DeltaAttr> Iter<'a, V, Attr> {
|
|||
self.current.as_ref()
|
||||
}
|
||||
|
||||
pub fn peek_is_replace(&self) -> bool {
|
||||
self.peek().map(|x| x.is_replace()).unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn peek_is_insert(&self) -> bool {
|
||||
self.peek().map(|x| x.is_insert()).unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn peek_is_delete(&self) -> bool {
|
||||
self.peek().map(|x| x.is_delete()).unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn peek_is_retain(&self) -> bool {
|
||||
self.peek().map(|x| x.is_retain()).unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn peek_length(&self) -> usize {
|
||||
self.peek().map(|x| x.delta_len()).unwrap_or(usize::MAX)
|
||||
}
|
||||
|
||||
pub fn peek_insert_length(&self) -> usize {
|
||||
self.peek()
|
||||
.map(|x| match x {
|
||||
DeltaItem::Retain { .. } => 0,
|
||||
DeltaItem::Replace { value, .. } => value.rle_len(),
|
||||
})
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
pub fn next_with(&mut self, mut len: usize) -> Result<(), usize> {
|
||||
while len > 0 {
|
||||
let Some(current) = self.current.as_mut() else {
|
||||
|
|
|
@ -46,3 +46,33 @@ pub enum DeltaItem<V, Attr> {
|
|||
delete: usize,
|
||||
},
|
||||
}
|
||||
|
||||
impl<V: DeltaValue, Attr: DeltaAttr> DeltaItem<V, Attr> {
|
||||
fn is_insert(&self) -> bool {
|
||||
match self {
|
||||
DeltaItem::Retain { .. } => false,
|
||||
DeltaItem::Replace { value, .. } => value.rle_len() > 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_delete(&self) -> bool {
|
||||
match self {
|
||||
DeltaItem::Retain { .. } => false,
|
||||
DeltaItem::Replace { value, delete, .. } => value.rle_len() == 0 && *delete > 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_replace(&self) -> bool {
|
||||
match self {
|
||||
DeltaItem::Retain { .. } => false,
|
||||
DeltaItem::Replace { .. } => true,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_retain(&self) -> bool {
|
||||
match self {
|
||||
DeltaItem::Retain { .. } => true,
|
||||
DeltaItem::Replace { .. } => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -201,7 +201,7 @@ pub fn minify_failed_tests_in_async_mode<T: ActorTrait>(
|
|||
actions.drain(num..);
|
||||
if let Some(min_actions) = min_actions.as_mut() {
|
||||
if actions.len() < min_actions.len() {
|
||||
*min_actions = actions.clone();
|
||||
min_actions.clone_from(&actions);
|
||||
}
|
||||
} else {
|
||||
min_actions = Some(actions.clone());
|
||||
|
|
|
@ -10,8 +10,13 @@ publish = false
|
|||
loro-without-counter = { path = "../loro", package = "loro" }
|
||||
loro = { git = "https://github.com/loro-dev/loro.git", features = [
|
||||
"counter",
|
||||
], branch = "leon/feat-encode-forward" }
|
||||
loro-internal = { path = "../loro-internal", features = ["test_utils"] }
|
||||
], rev = "0dade6bc0fb574a8190db2aa80c83a479f62e125" }
|
||||
loro-common = { git = "https://github.com/loro-dev/loro.git", features = [
|
||||
"counter",
|
||||
], rev = "0dade6bc0fb574a8190db2aa80c83a479f62e125" }
|
||||
# loro = { path = "../loro", package = "loro", features = ["counter"] }
|
||||
# loro-common = { path = "../loro-common", features = ["counter"] }
|
||||
# loro-without-counter = { git = "https://github.com/loro-dev/loro.git", branch = "zxch3n/loro-560-undoredo", package = "loro" }
|
||||
fxhash = { workspace = true }
|
||||
enum_dispatch = { workspace = true }
|
||||
enum-as-inner = { workspace = true }
|
||||
|
|
32
crates/fuzz/fuzz/Cargo.lock
generated
32
crates/fuzz/fuzz/Cargo.lock
generated
|
@ -219,7 +219,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "fractional_index"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/loro-dev/loro.git?branch=leon/feat-encode-forward#0ad15cd3c3c8e6f59d113bc948a7838bbdb5300a"
|
||||
source = "git+https://github.com/loro-dev/loro.git?rev=0dade6bc0fb574a8190db2aa80c83a479f62e125#0dade6bc0fb574a8190db2aa80c83a479f62e125"
|
||||
dependencies = [
|
||||
"imbl",
|
||||
"rand",
|
||||
|
@ -237,8 +237,8 @@ dependencies = [
|
|||
"fxhash",
|
||||
"itertools 0.12.1",
|
||||
"loro 0.5.1",
|
||||
"loro 0.5.1 (git+https://github.com/loro-dev/loro.git?branch=leon/feat-encode-forward)",
|
||||
"loro-internal 0.5.1",
|
||||
"loro 0.5.1 (git+https://github.com/loro-dev/loro.git?rev=0dade6bc0fb574a8190db2aa80c83a479f62e125)",
|
||||
"loro-common 0.5.1 (git+https://github.com/loro-dev/loro.git?rev=0dade6bc0fb574a8190db2aa80c83a479f62e125)",
|
||||
"rand",
|
||||
"tabled",
|
||||
"tracing",
|
||||
|
@ -457,13 +457,13 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "loro"
|
||||
version = "0.5.1"
|
||||
source = "git+https://github.com/loro-dev/loro.git?branch=leon/feat-encode-forward#0ad15cd3c3c8e6f59d113bc948a7838bbdb5300a"
|
||||
source = "git+https://github.com/loro-dev/loro.git?rev=0dade6bc0fb574a8190db2aa80c83a479f62e125#0dade6bc0fb574a8190db2aa80c83a479f62e125"
|
||||
dependencies = [
|
||||
"either",
|
||||
"enum-as-inner 0.6.0",
|
||||
"generic-btree",
|
||||
"loro-delta 0.5.1 (git+https://github.com/loro-dev/loro.git?branch=leon/feat-encode-forward)",
|
||||
"loro-internal 0.5.1 (git+https://github.com/loro-dev/loro.git?branch=leon/feat-encode-forward)",
|
||||
"loro-delta 0.5.1 (git+https://github.com/loro-dev/loro.git?rev=0dade6bc0fb574a8190db2aa80c83a479f62e125)",
|
||||
"loro-internal 0.5.1 (git+https://github.com/loro-dev/loro.git?rev=0dade6bc0fb574a8190db2aa80c83a479f62e125)",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
|
@ -485,12 +485,12 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "loro-common"
|
||||
version = "0.5.1"
|
||||
source = "git+https://github.com/loro-dev/loro.git?branch=leon/feat-encode-forward#0ad15cd3c3c8e6f59d113bc948a7838bbdb5300a"
|
||||
source = "git+https://github.com/loro-dev/loro.git?rev=0dade6bc0fb574a8190db2aa80c83a479f62e125#0dade6bc0fb574a8190db2aa80c83a479f62e125"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"enum-as-inner 0.6.0",
|
||||
"fxhash",
|
||||
"loro-rle 0.5.1 (git+https://github.com/loro-dev/loro.git?branch=leon/feat-encode-forward)",
|
||||
"loro-rle 0.5.1 (git+https://github.com/loro-dev/loro.git?rev=0dade6bc0fb574a8190db2aa80c83a479f62e125)",
|
||||
"nonmax",
|
||||
"serde",
|
||||
"serde_columnar",
|
||||
|
@ -512,7 +512,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "loro-delta"
|
||||
version = "0.5.1"
|
||||
source = "git+https://github.com/loro-dev/loro.git?branch=leon/feat-encode-forward#0ad15cd3c3c8e6f59d113bc948a7838bbdb5300a"
|
||||
source = "git+https://github.com/loro-dev/loro.git?rev=0dade6bc0fb574a8190db2aa80c83a479f62e125#0dade6bc0fb574a8190db2aa80c83a479f62e125"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"enum-as-inner 0.5.1",
|
||||
|
@ -526,7 +526,6 @@ name = "loro-internal"
|
|||
version = "0.5.1"
|
||||
dependencies = [
|
||||
"append-only-bytes",
|
||||
"arbitrary",
|
||||
"arref",
|
||||
"either",
|
||||
"enum-as-inner 0.5.1",
|
||||
|
@ -552,7 +551,6 @@ dependencies = [
|
|||
"serde_columnar",
|
||||
"serde_json",
|
||||
"smallvec",
|
||||
"tabled",
|
||||
"thiserror",
|
||||
"tracing",
|
||||
]
|
||||
|
@ -560,23 +558,23 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "loro-internal"
|
||||
version = "0.5.1"
|
||||
source = "git+https://github.com/loro-dev/loro.git?branch=leon/feat-encode-forward#0ad15cd3c3c8e6f59d113bc948a7838bbdb5300a"
|
||||
source = "git+https://github.com/loro-dev/loro.git?rev=0dade6bc0fb574a8190db2aa80c83a479f62e125#0dade6bc0fb574a8190db2aa80c83a479f62e125"
|
||||
dependencies = [
|
||||
"append-only-bytes",
|
||||
"arref",
|
||||
"either",
|
||||
"enum-as-inner 0.5.1",
|
||||
"enum_dispatch",
|
||||
"fractional_index 0.1.0 (git+https://github.com/loro-dev/loro.git?branch=leon/feat-encode-forward)",
|
||||
"fractional_index 0.1.0 (git+https://github.com/loro-dev/loro.git?rev=0dade6bc0fb574a8190db2aa80c83a479f62e125)",
|
||||
"fxhash",
|
||||
"generic-btree",
|
||||
"getrandom",
|
||||
"im",
|
||||
"itertools 0.12.1",
|
||||
"leb128",
|
||||
"loro-common 0.5.1 (git+https://github.com/loro-dev/loro.git?branch=leon/feat-encode-forward)",
|
||||
"loro-delta 0.5.1 (git+https://github.com/loro-dev/loro.git?branch=leon/feat-encode-forward)",
|
||||
"loro-rle 0.5.1 (git+https://github.com/loro-dev/loro.git?branch=leon/feat-encode-forward)",
|
||||
"loro-common 0.5.1 (git+https://github.com/loro-dev/loro.git?rev=0dade6bc0fb574a8190db2aa80c83a479f62e125)",
|
||||
"loro-delta 0.5.1 (git+https://github.com/loro-dev/loro.git?rev=0dade6bc0fb574a8190db2aa80c83a479f62e125)",
|
||||
"loro-rle 0.5.1 (git+https://github.com/loro-dev/loro.git?rev=0dade6bc0fb574a8190db2aa80c83a479f62e125)",
|
||||
"md5",
|
||||
"num",
|
||||
"num-derive",
|
||||
|
@ -607,7 +605,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "loro-rle"
|
||||
version = "0.5.1"
|
||||
source = "git+https://github.com/loro-dev/loro.git?branch=leon/feat-encode-forward#0ad15cd3c3c8e6f59d113bc948a7838bbdb5300a"
|
||||
source = "git+https://github.com/loro-dev/loro.git?rev=0dade6bc0fb574a8190db2aa80c83a479f62e125#0dade6bc0fb574a8190db2aa80c83a479f62e125"
|
||||
dependencies = [
|
||||
"append-only-bytes",
|
||||
"arref",
|
||||
|
|
|
@ -66,6 +66,15 @@ pub enum Action {
|
|||
site: u8,
|
||||
to: u32,
|
||||
},
|
||||
Undo {
|
||||
site: u8,
|
||||
op_len: u32,
|
||||
},
|
||||
// For concurrent undo
|
||||
SyncAllUndo {
|
||||
site: u8,
|
||||
op_len: u32,
|
||||
},
|
||||
Sync {
|
||||
from: u8,
|
||||
to: u8,
|
||||
|
@ -151,6 +160,18 @@ impl Tabled for Action {
|
|||
fields.extend(action.as_action().unwrap().table_fields());
|
||||
fields
|
||||
}
|
||||
Action::Undo { site, op_len } => vec![
|
||||
"undo".into(),
|
||||
format!("{}", site).into(),
|
||||
format!("{} op len", op_len).into(),
|
||||
"".into(),
|
||||
],
|
||||
Action::SyncAllUndo { site, op_len } => vec![
|
||||
"sync all undo".into(),
|
||||
format!("{}", site).into(),
|
||||
format!("{} op len", op_len).into(),
|
||||
"".into(),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,9 +6,10 @@ use std::{
|
|||
use enum_as_inner::EnumAsInner;
|
||||
use enum_dispatch::enum_dispatch;
|
||||
use fxhash::FxHashMap;
|
||||
use loro::{Container, ContainerID, ContainerType, Frontiers, LoroDoc, LoroValue, PeerID, ID};
|
||||
use rand::SeedableRng;
|
||||
use rand::{rngs::StdRng, Rng};
|
||||
use loro::{
|
||||
Container, ContainerID, ContainerType, Frontiers, LoroDoc, LoroValue, PeerID, UndoManager, ID,
|
||||
};
|
||||
use rand::{rngs::StdRng, Rng, SeedableRng};
|
||||
use tracing::info_span;
|
||||
|
||||
use crate::{
|
||||
|
@ -21,12 +22,20 @@ use super::{
|
|||
container::MapActor,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Undo {
|
||||
pub undo: UndoManager,
|
||||
pub last_container: u8,
|
||||
pub can_undo_length: u8,
|
||||
}
|
||||
|
||||
pub struct Actor {
|
||||
pub peer: PeerID,
|
||||
pub loro: Arc<LoroDoc>,
|
||||
pub targets: FxHashMap<ContainerType, ActionExecutor>,
|
||||
pub tracker: Arc<Mutex<ContainerTracker>>,
|
||||
pub history: FxHashMap<Vec<ID>, LoroValue>,
|
||||
pub undo_manager: Undo,
|
||||
pub rng: StdRng,
|
||||
}
|
||||
|
||||
|
@ -34,6 +43,7 @@ impl Actor {
|
|||
pub fn new(id: PeerID) -> Self {
|
||||
let loro = LoroDoc::new();
|
||||
loro.set_peer_id(id).unwrap();
|
||||
let undo = UndoManager::new(&loro);
|
||||
let tracker = Arc::new(Mutex::new(ContainerTracker::Map(MapTracker::empty(
|
||||
ContainerID::new_root("sys:root", ContainerType::Map),
|
||||
))));
|
||||
|
@ -52,6 +62,11 @@ impl Actor {
|
|||
tracker,
|
||||
targets: FxHashMap::default(),
|
||||
history: default_history,
|
||||
undo_manager: Undo {
|
||||
undo,
|
||||
last_container: 255,
|
||||
can_undo_length: 0,
|
||||
},
|
||||
rng: StdRng::from_seed({
|
||||
let mut seed = [0u8; 32];
|
||||
let bytes = id.to_be_bytes(); // Convert u64 to [u8; 8]
|
||||
|
@ -95,11 +110,43 @@ impl Actor {
|
|||
let actor = self.targets.get_mut(&ty).unwrap();
|
||||
self.loro.attach();
|
||||
let idx = action.apply(actor, container as usize);
|
||||
|
||||
if self.undo_manager.last_container != container {
|
||||
self.undo_manager.last_container = container;
|
||||
self.undo_manager.can_undo_length += 1;
|
||||
}
|
||||
|
||||
if let Some(idx) = idx {
|
||||
self.add_new_container(idx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn undo(&mut self, undo_length: u32) {
|
||||
self.loro.attach();
|
||||
let mut before_undo = self.loro.get_deep_value();
|
||||
for _ in 0..undo_length {
|
||||
self.undo_manager.undo.undo(&self.loro).unwrap();
|
||||
}
|
||||
|
||||
for _ in 0..undo_length {
|
||||
self.undo_manager.undo.redo(&self.loro).unwrap();
|
||||
}
|
||||
let mut after_undo = self.loro.get_deep_value();
|
||||
Self::patch_tree_undo_position(&mut before_undo);
|
||||
Self::patch_tree_undo_position(&mut after_undo);
|
||||
assert_value_eq(&before_undo, &after_undo);
|
||||
}
|
||||
|
||||
fn patch_tree_undo_position(a: &mut LoroValue) {
|
||||
let root = Arc::make_mut(a.as_map_mut().unwrap());
|
||||
let tree = root.get_mut("tree").unwrap();
|
||||
let nodes = Arc::make_mut(tree.as_list_mut().unwrap());
|
||||
for node in nodes.iter_mut() {
|
||||
let node = Arc::make_mut(node.as_map_mut().unwrap());
|
||||
node.remove("position");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_tracker(&self) {
|
||||
let loro = &self.loro;
|
||||
info_span!("Check tracker", "peer = {}", loro.peer_id()).in_scope(|| {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use loro::{Container, ContainerID, ContainerType, LoroDoc, LoroList};
|
||||
use tracing::{debug_span};
|
||||
use tracing::debug_span;
|
||||
|
||||
use crate::{
|
||||
actions::{Actionable, FromGenericAction, GenericAction},
|
||||
|
|
|
@ -409,7 +409,7 @@ impl ApplyDiff for TreeTracker {
|
|||
self.insert(*index, node);
|
||||
};
|
||||
}
|
||||
TreeExternalDiff::Delete => {
|
||||
TreeExternalDiff::Delete { .. } => {
|
||||
let node = self.find_node_by_id(target).unwrap();
|
||||
if let Some(parent) = node.parent {
|
||||
let parent = self.find_node_by_id_mut(parent).unwrap();
|
||||
|
@ -423,6 +423,7 @@ impl ApplyDiff for TreeTracker {
|
|||
parent,
|
||||
index,
|
||||
position,
|
||||
..
|
||||
} => {
|
||||
let node = self.find_node_by_id(target).unwrap();
|
||||
let mut node = if let Some(p) = node.parent {
|
||||
|
|
|
@ -85,6 +85,16 @@ impl CRDTFuzzer {
|
|||
action.convert_to_inner(target);
|
||||
actor.pre_process(action.as_action_mut().unwrap(), container);
|
||||
}
|
||||
Action::Undo { site, op_len } => {
|
||||
*site %= max_users;
|
||||
let actor = &mut self.actors[*site as usize];
|
||||
*op_len %= actor.undo_manager.can_undo_length as u32 + 1;
|
||||
}
|
||||
Action::SyncAllUndo { site, op_len } => {
|
||||
*site %= max_users;
|
||||
let actor = &mut self.actors[*site as usize];
|
||||
*op_len %= actor.undo_manager.can_undo_length as u32 + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -139,7 +149,36 @@ impl CRDTFuzzer {
|
|||
let actor = &mut self.actors[*site as usize];
|
||||
let action = action.as_action().unwrap();
|
||||
actor.apply(action, *container);
|
||||
// actor.loro.commit();
|
||||
}
|
||||
Action::Undo { site, op_len } => {
|
||||
let actor = &mut self.actors[*site as usize];
|
||||
if *op_len != 0 {
|
||||
actor.undo(*op_len);
|
||||
}
|
||||
}
|
||||
Action::SyncAllUndo { site, op_len } => {
|
||||
for i in 1..self.site_num() {
|
||||
info_span!("Importing", "importing to 0 from {}", i).in_scope(|| {
|
||||
let (a, b) = array_mut_ref!(&mut self.actors, [0, i]);
|
||||
a.loro
|
||||
.import(&b.loro.export_from(&a.loro.oplog_vv()))
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
for i in 1..self.site_num() {
|
||||
info_span!("Importing", "importing to {} from {}", i, 0).in_scope(|| {
|
||||
let (a, b) = array_mut_ref!(&mut self.actors, [0, i]);
|
||||
b.loro
|
||||
.import(&a.loro.export_from(&b.loro.oplog_vv()))
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
self.actors.iter_mut().for_each(|a| a.record_history());
|
||||
let actor = &mut self.actors[*site as usize];
|
||||
if *op_len != 0 {
|
||||
actor.undo(*op_len);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -248,6 +287,7 @@ pub fn test_multi_sites(site_num: u8, fuzz_targets: Vec<FuzzTarget>, actions: &m
|
|||
let mut applied = Vec::new();
|
||||
for action in actions.iter_mut() {
|
||||
fuzzer.pre_process(action);
|
||||
|
||||
info_span!("ApplyAction", ?action).in_scope(|| {
|
||||
applied.push(action.clone());
|
||||
info!("OptionsTable \n{}", (&applied).table());
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use fuzz::{
|
||||
actions::{ActionWrapper::*, GenericAction},
|
||||
actions::{
|
||||
ActionWrapper::{self, *},
|
||||
GenericAction,
|
||||
},
|
||||
container::{TreeAction, TreeActionInner},
|
||||
crdt_fuzzer::{test_multi_sites, Action::*, FuzzTarget, FuzzValue::*},
|
||||
};
|
||||
use loro::{ContainerType::*, LoroCounter, LoroDoc};
|
||||
|
@ -5587,3 +5591,57 @@ fn unknown_container() {
|
|||
|
||||
doc.import(&doc2.export_snapshot()).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn undo_tree() {
|
||||
test_multi_sites(
|
||||
5,
|
||||
vec![FuzzTarget::Tree],
|
||||
&mut [
|
||||
Handle {
|
||||
site: 0,
|
||||
target: 0,
|
||||
container: 0,
|
||||
action: ActionWrapper::Action(fuzz::actions::ActionInner::Tree(TreeAction {
|
||||
target: (0, 0),
|
||||
action: TreeActionInner::Create { index: 0 },
|
||||
})),
|
||||
},
|
||||
Handle {
|
||||
site: 0,
|
||||
target: 0,
|
||||
container: 0,
|
||||
action: ActionWrapper::Action(fuzz::actions::ActionInner::Tree(TreeAction {
|
||||
target: (0, 1),
|
||||
action: TreeActionInner::Create { index: 1 },
|
||||
})),
|
||||
},
|
||||
SyncAll,
|
||||
Handle {
|
||||
site: 0,
|
||||
target: 0,
|
||||
container: 0,
|
||||
action: ActionWrapper::Action(fuzz::actions::ActionInner::Tree(TreeAction {
|
||||
target: (0, 0),
|
||||
action: TreeActionInner::Move {
|
||||
parent: (0, 1),
|
||||
index: 0,
|
||||
},
|
||||
})),
|
||||
},
|
||||
Handle {
|
||||
site: 1,
|
||||
target: 0,
|
||||
container: 0,
|
||||
action: ActionWrapper::Action(fuzz::actions::ActionInner::Tree(TreeAction {
|
||||
target: (0, 1),
|
||||
action: TreeActionInner::Move {
|
||||
parent: (0, 0),
|
||||
index: 0,
|
||||
},
|
||||
})),
|
||||
},
|
||||
SyncAllUndo { site: 0, op_len: 1 },
|
||||
],
|
||||
)
|
||||
}
|
||||
|
|
122
crates/fuzz/tests/undo.rs
Normal file
122
crates/fuzz/tests/undo.rs
Normal file
|
@ -0,0 +1,122 @@
|
|||
use fuzz::{
|
||||
actions::{ActionWrapper::*, GenericAction},
|
||||
crdt_fuzzer::{test_multi_sites, Action::*, FuzzTarget, FuzzValue::*},
|
||||
};
|
||||
use loro_common::ContainerType::*;
|
||||
|
||||
// #[ctor::ctor]
|
||||
// fn init() {
|
||||
// dev_utils::setup_test_log();
|
||||
// }
|
||||
|
||||
#[test]
|
||||
fn undo_tree_with_map() {
|
||||
test_multi_sites(
|
||||
5,
|
||||
vec![FuzzTarget::Tree],
|
||||
&mut [
|
||||
Handle {
|
||||
site: 174,
|
||||
target: 0,
|
||||
container: 0,
|
||||
action: Generic(GenericAction {
|
||||
value: I32(117440512),
|
||||
bool: true,
|
||||
key: 1275068415,
|
||||
pos: 18446743068687204667,
|
||||
length: 46161896180416511,
|
||||
prop: 18446463698227691775,
|
||||
}),
|
||||
},
|
||||
SyncAll,
|
||||
Handle {
|
||||
site: 0,
|
||||
target: 0,
|
||||
container: 0,
|
||||
action: Generic(GenericAction {
|
||||
value: I32(-12976128),
|
||||
bool: true,
|
||||
key: 131071,
|
||||
pos: 3399988123389597184,
|
||||
length: 3400000218017509167,
|
||||
prop: 3399988123389603631,
|
||||
}),
|
||||
},
|
||||
Handle {
|
||||
site: 0,
|
||||
target: 0,
|
||||
container: 0,
|
||||
action: Generic(GenericAction {
|
||||
value: I32(791621423),
|
||||
bool: true,
|
||||
key: 791621423,
|
||||
pos: 18372433783001394991,
|
||||
length: 13281205459693609,
|
||||
prop: 18446744069425331619,
|
||||
}),
|
||||
},
|
||||
SyncAll,
|
||||
SyncAllUndo {
|
||||
site: 149,
|
||||
op_len: 65533,
|
||||
},
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn redo_tree_id_diff() {
|
||||
test_multi_sites(
|
||||
2,
|
||||
vec![FuzzTarget::All],
|
||||
&mut [
|
||||
Handle {
|
||||
site: 51,
|
||||
target: 60,
|
||||
container: 197,
|
||||
action: Generic(GenericAction {
|
||||
value: I32(-296905323),
|
||||
bool: false,
|
||||
key: 2395151462,
|
||||
pos: 6335698875578771752,
|
||||
length: 1716855125946684615,
|
||||
prop: 2807457672376879961,
|
||||
}),
|
||||
},
|
||||
Handle {
|
||||
site: 162,
|
||||
target: 167,
|
||||
container: 90,
|
||||
action: Generic(GenericAction {
|
||||
value: Container(Tree),
|
||||
bool: true,
|
||||
key: 929442508,
|
||||
pos: 4887648083275096983,
|
||||
length: 8237173174339417107,
|
||||
prop: 1571041097810100079,
|
||||
}),
|
||||
},
|
||||
Checkout {
|
||||
site: 56,
|
||||
to: 1826343396,
|
||||
},
|
||||
SyncAllUndo {
|
||||
site: 10,
|
||||
op_len: 998370061,
|
||||
},
|
||||
Handle {
|
||||
site: 112,
|
||||
target: 78,
|
||||
container: 159,
|
||||
action: Generic(GenericAction {
|
||||
value: Container(MovableList),
|
||||
bool: false,
|
||||
key: 1978700208,
|
||||
pos: 15377364763518525973,
|
||||
length: 13205966979381542996,
|
||||
prop: 5155832222345785212,
|
||||
}),
|
||||
},
|
||||
],
|
||||
);
|
||||
}
|
|
@ -47,15 +47,21 @@ pub enum LoroError {
|
|||
#[error("Unknown Error ({0})")]
|
||||
Unknown(Box<str>),
|
||||
#[error("The given ID ({0}) is not contained by the doc")]
|
||||
InvalidFrontierIdNotFound(ID),
|
||||
FrontiersNotFound(ID),
|
||||
#[error("Cannot import when the doc is in a transaction")]
|
||||
ImportWhenInTxn,
|
||||
#[error("The given method ({method}) is not allowed when the container is detached. You should insert the container to the doc first.")]
|
||||
MisuseDettachedContainer { method: &'static str },
|
||||
MisuseDetachedContainer { method: &'static str },
|
||||
#[error("Not implemented: {0}")]
|
||||
NotImplemented(&'static str),
|
||||
#[error("Reattach a container that is already attached")]
|
||||
ReattachAttachedContainer,
|
||||
#[error("Edit is not allowed when the doc is in the detached mode.")]
|
||||
EditWhenDetached,
|
||||
#[error("The given ID ({0}) is not contained by the doc")]
|
||||
UndoInvalidIdSpan(ID),
|
||||
#[error("PeerID cannot be changed. Expected: {expected:?}, Actual: {actual:?}")]
|
||||
UndoWithDifferentPeerId { expected: PeerID, actual: PeerID },
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
|
|
|
@ -2,7 +2,7 @@ use criterion::{criterion_group, criterion_main, Criterion};
|
|||
#[cfg(feature = "test_utils")]
|
||||
mod event {
|
||||
use super::*;
|
||||
|
||||
|
||||
use loro_internal::{ListHandler, LoroDoc};
|
||||
use std::sync::Arc;
|
||||
|
||||
|
@ -24,7 +24,7 @@ mod event {
|
|||
let children_num = 80;
|
||||
let deep = 3;
|
||||
b.iter(|| {
|
||||
let mut loro = LoroDoc::default();
|
||||
let loro = LoroDoc::default();
|
||||
loro.start_auto_commit();
|
||||
loro.subscribe_root(Arc::new(|_e| {}));
|
||||
let mut handlers = vec![loro.get_list("list")];
|
||||
|
|
|
@ -7,7 +7,7 @@ use loro_internal::{
|
|||
};
|
||||
|
||||
fn main() {
|
||||
let mut doc = LoroDoc::new();
|
||||
let doc = LoroDoc::new();
|
||||
doc.start_auto_commit();
|
||||
let list = doc.get_list("list");
|
||||
doc.subscribe_root(Arc::new(|e| {
|
||||
|
|
|
@ -134,6 +134,14 @@ impl ResolvedMapDelta {
|
|||
self.updated.insert(key, map_value);
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn transform(&mut self, b: &ResolvedMapDelta, left_prior: bool) {
|
||||
for (k, _) in b.updated.iter() {
|
||||
if !left_prior {
|
||||
self.updated.remove(k);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for MapValue {
|
||||
|
|
|
@ -158,14 +158,7 @@ impl ToJson for StyleMeta {
|
|||
impl DeltaAttr for StyleMeta {
|
||||
fn compose(&mut self, other: &Self) {
|
||||
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());
|
||||
}
|
||||
}
|
||||
self.map.insert(key.clone(), value.clone());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
use fractional_index::FractionalIndex;
|
||||
use fxhash::FxHashMap;
|
||||
use itertools::Itertools;
|
||||
use loro_common::{IdFull, TreeID};
|
||||
use std::fmt::Debug;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
@ -27,8 +29,13 @@ pub enum TreeExternalDiff {
|
|||
parent: Option<TreeID>,
|
||||
index: usize,
|
||||
position: FractionalIndex,
|
||||
old_parent: TreeParentId,
|
||||
old_index: usize,
|
||||
},
|
||||
Delete {
|
||||
old_parent: TreeParentId,
|
||||
old_index: usize,
|
||||
},
|
||||
Delete,
|
||||
}
|
||||
|
||||
impl TreeDiff {
|
||||
|
@ -42,6 +49,123 @@ impl TreeDiff {
|
|||
self.diff.extend(other);
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn transform(&mut self, b: &TreeDiff, left_prior: bool) {
|
||||
if b.is_empty() || self.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let b_update: FxHashMap<_, _> = b.diff.iter().map(|d| (d.target, &d.action)).collect();
|
||||
let mut self_update: FxHashMap<_, _> = self
|
||||
.diff
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, d)| (d.target, (&d.action, i)))
|
||||
.collect();
|
||||
if !left_prior {
|
||||
let mut removes = Vec::new();
|
||||
for (target, _) in b_update {
|
||||
if let Some((_, i)) = self_update.remove(&target) {
|
||||
removes.push(i);
|
||||
}
|
||||
}
|
||||
for i in removes.into_iter().sorted().rev() {
|
||||
self.diff.remove(i);
|
||||
}
|
||||
}
|
||||
let mut b_parent = FxHashMap::default();
|
||||
|
||||
fn reset_index(
|
||||
b_parent: &FxHashMap<TreeParentId, Vec<i32>>,
|
||||
index: &mut usize,
|
||||
parent: &TreeParentId,
|
||||
left_priority: bool,
|
||||
) {
|
||||
if let Some(b_indices) = b_parent.get(parent) {
|
||||
for i in b_indices.iter() {
|
||||
if (i.unsigned_abs() as usize) < *index
|
||||
|| (i.unsigned_abs() as usize == *index && !left_priority)
|
||||
{
|
||||
if i > &0 {
|
||||
*index += 1;
|
||||
} else if *index > (i.unsigned_abs() as usize) {
|
||||
*index = index.saturating_sub(1);
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for diff in b.diff.iter() {
|
||||
match &diff.action {
|
||||
TreeExternalDiff::Create {
|
||||
parent,
|
||||
index,
|
||||
position: _,
|
||||
} => {
|
||||
b_parent
|
||||
.entry(TreeParentId::from(*parent))
|
||||
.or_insert_with(Vec::new)
|
||||
.push(*index as i32);
|
||||
}
|
||||
TreeExternalDiff::Move {
|
||||
parent,
|
||||
index,
|
||||
position: _,
|
||||
old_parent,
|
||||
old_index,
|
||||
} => {
|
||||
b_parent
|
||||
.entry(*old_parent)
|
||||
.or_insert_with(Vec::new)
|
||||
.push(-(*old_index as i32));
|
||||
b_parent
|
||||
.entry(TreeParentId::from(*parent))
|
||||
.or_insert_with(Vec::new)
|
||||
.push(*index as i32);
|
||||
}
|
||||
TreeExternalDiff::Delete {
|
||||
old_index,
|
||||
old_parent,
|
||||
} => {
|
||||
b_parent
|
||||
.entry(*old_parent)
|
||||
.or_insert_with(Vec::new)
|
||||
.push(-(*old_index as i32));
|
||||
}
|
||||
}
|
||||
}
|
||||
b_parent
|
||||
.iter_mut()
|
||||
.for_each(|(_, v)| v.sort_unstable_by_key(|i| i.abs()));
|
||||
for diff in self.iter_mut() {
|
||||
match &mut diff.action {
|
||||
TreeExternalDiff::Create {
|
||||
parent,
|
||||
index,
|
||||
position: _,
|
||||
} => reset_index(&b_parent, index, &TreeParentId::from(*parent), left_prior),
|
||||
TreeExternalDiff::Move {
|
||||
parent,
|
||||
index,
|
||||
position: _,
|
||||
old_parent,
|
||||
old_index,
|
||||
} => {
|
||||
reset_index(&b_parent, index, &TreeParentId::from(*parent), left_prior);
|
||||
reset_index(&b_parent, old_index, old_parent, left_prior);
|
||||
}
|
||||
TreeExternalDiff::Delete {
|
||||
old_index,
|
||||
old_parent,
|
||||
} => {
|
||||
reset_index(&b_parent, old_index, old_parent, left_prior);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Representation of differences in movable tree. It's an ordered list of [`TreeDiff`].
|
||||
|
|
|
@ -355,6 +355,27 @@ impl InternalDiff {
|
|||
}
|
||||
|
||||
impl Diff {
|
||||
pub(crate) fn compose_ref(&mut self, diff: &Diff) {
|
||||
// PERF: avoid clone
|
||||
match (self, diff) {
|
||||
(Diff::List(a), Diff::List(b)) => {
|
||||
a.compose(b);
|
||||
}
|
||||
(Diff::Text(a), Diff::Text(b)) => {
|
||||
a.compose(b);
|
||||
}
|
||||
(Diff::Map(a), Diff::Map(b)) => {
|
||||
*a = a.clone().compose(b.clone());
|
||||
}
|
||||
(Diff::Tree(a), Diff::Tree(b)) => {
|
||||
*a = a.clone().compose(b.clone());
|
||||
}
|
||||
#[cfg(feature = "counter")]
|
||||
(Diff::Counter(a), Diff::Counter(b)) => *a += b,
|
||||
(_, _) => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn compose(self, diff: Diff) -> Result<Self, Self> {
|
||||
// PERF: avoid clone
|
||||
match (self, diff) {
|
||||
|
@ -375,6 +396,26 @@ impl Diff {
|
|||
}
|
||||
}
|
||||
|
||||
// Transform this diff based on the other diff
|
||||
pub(crate) fn transform(&mut self, other: &Self, left_prior: bool) {
|
||||
match (self, other) {
|
||||
(Diff::List(a), Diff::List(b)) => a.transform_(b, left_prior),
|
||||
(Diff::Text(a), Diff::Text(b)) => a.transform_(b, left_prior),
|
||||
(Diff::Map(a), Diff::Map(b)) => a.transform(b, left_prior),
|
||||
(Diff::Tree(a), Diff::Tree(b)) => a.transform(b, left_prior),
|
||||
#[cfg(feature = "counter")]
|
||||
(Diff::Counter(a), Diff::Counter(b)) => {
|
||||
if left_prior {
|
||||
*a += b;
|
||||
} else {
|
||||
*a -= b;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub(crate) fn is_empty(&self) -> bool {
|
||||
match self {
|
||||
Diff::List(s) => s.is_empty(),
|
||||
|
|
|
@ -7,8 +7,8 @@ use crate::{
|
|||
richtext::{richtext_state::PosType, RichtextState, StyleOp, TextStyleInfoFlag},
|
||||
},
|
||||
cursor::{Cursor, Side},
|
||||
delta::{DeltaItem, StyleMeta},
|
||||
event::TextDiffItem,
|
||||
delta::{DeltaItem, StyleMeta, TreeExternalDiff},
|
||||
event::{Diff, TextDiffItem},
|
||||
op::ListSlice,
|
||||
state::{ContainerState, IndexType, State},
|
||||
txn::EventHint,
|
||||
|
@ -28,7 +28,7 @@ use std::{
|
|||
ops::Deref,
|
||||
sync::{Arc, Mutex, Weak},
|
||||
};
|
||||
use tracing::{info, instrument};
|
||||
use tracing::{error, info, instrument};
|
||||
|
||||
mod tree;
|
||||
pub use tree::TreeHandler;
|
||||
|
@ -73,7 +73,7 @@ pub trait HandlerTrait: Clone + Sized {
|
|||
fn with_state<R>(&self, f: impl FnOnce(&mut State) -> LoroResult<R>) -> LoroResult<R> {
|
||||
let inner = self
|
||||
.attached_handler()
|
||||
.ok_or(LoroError::MisuseDettachedContainer {
|
||||
.ok_or(LoroError::MisuseDetachedContainer {
|
||||
method: "with_state",
|
||||
})?;
|
||||
let state = inner.state.upgrade().unwrap();
|
||||
|
@ -151,7 +151,7 @@ impl<T> MaybeDetached<T> {
|
|||
|
||||
fn try_attached_state(&self) -> LoroResult<&BasicHandler> {
|
||||
match self {
|
||||
MaybeDetached::Detached(_) => Err(LoroError::MisuseDettachedContainer {
|
||||
MaybeDetached::Detached(_) => Err(LoroError::MisuseDetachedContainer {
|
||||
method: "inner_state",
|
||||
}),
|
||||
MaybeDetached::Attached(a) => Ok(a),
|
||||
|
@ -1056,6 +1056,77 @@ impl Handler {
|
|||
Self::Unknown(x) => x.get_deep_value(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn apply_diff(
|
||||
&self,
|
||||
diff: Diff,
|
||||
on_container_remap: &mut dyn FnMut(ContainerID, ContainerID),
|
||||
) -> LoroResult<()> {
|
||||
match self {
|
||||
Self::Map(x) => {
|
||||
let diff = diff.into_map().unwrap();
|
||||
for (key, value) in diff.updated.into_iter() {
|
||||
match value.value {
|
||||
Some(ValueOrHandler::Handler(h)) => {
|
||||
let old_id = h.id();
|
||||
let new_h = x.insert_container(
|
||||
&key,
|
||||
Handler::new_unattached(old_id.container_type()),
|
||||
)?;
|
||||
let new_id = new_h.id();
|
||||
on_container_remap(old_id, new_id);
|
||||
}
|
||||
Some(ValueOrHandler::Value(v)) => {
|
||||
x.insert_without_skipping(&key, v)?;
|
||||
}
|
||||
None => {
|
||||
x.delete(&key)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Self::Text(x) => {
|
||||
let delta = diff.into_text().unwrap();
|
||||
x.apply_delta(&TextDelta::from_text_diff(delta.iter()))?;
|
||||
}
|
||||
Self::List(x) => {
|
||||
let delta = diff.into_list().unwrap();
|
||||
x.apply_delta(delta, on_container_remap)?;
|
||||
}
|
||||
Self::MovableList(x) => {
|
||||
let delta = diff.into_list().unwrap();
|
||||
x.apply_delta(delta, on_container_remap)?;
|
||||
}
|
||||
Self::Tree(x) => {
|
||||
for diff in diff.into_tree().unwrap().diff {
|
||||
let target = diff.target;
|
||||
match diff.action {
|
||||
TreeExternalDiff::Create {
|
||||
parent,
|
||||
index,
|
||||
position: _,
|
||||
} => {
|
||||
x.create_at_with_target(parent, index, target)?;
|
||||
// create map event
|
||||
}
|
||||
TreeExternalDiff::Delete { .. } => x.delete(target)?,
|
||||
TreeExternalDiff::Move { parent, index, .. } => {
|
||||
x.move_to(target, parent, index)?
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "counter")]
|
||||
Self::Counter(x) => {
|
||||
let delta = diff.into_counter().unwrap();
|
||||
x.increment(delta)?;
|
||||
}
|
||||
Self::Unknown(_) => {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, EnumAsInner, Debug)]
|
||||
|
@ -1337,6 +1408,7 @@ impl TextHandler {
|
|||
}
|
||||
|
||||
if pos + len > self.len_event() {
|
||||
error!("pos={} len={} len_event={}", pos, len, self.len_event());
|
||||
return Err(LoroError::OutOfBound {
|
||||
pos: pos + len,
|
||||
len: self.len_event(),
|
||||
|
@ -1344,7 +1416,7 @@ impl TextHandler {
|
|||
}
|
||||
|
||||
let inner = self.inner.try_attached_state()?;
|
||||
let s = tracing::span!(tracing::Level::INFO, "delete pos={} len={}", pos, len);
|
||||
let s = tracing::span!(tracing::Level::INFO, "delete", "pos={} len={}", pos, len);
|
||||
let _e = s.enter();
|
||||
let ranges = inner.with_state(|state| {
|
||||
let richtext_state = state.as_richtext_state_mut().unwrap();
|
||||
|
@ -2077,6 +2149,55 @@ impl ListHandler {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_delta(
|
||||
&self,
|
||||
delta: loro_delta::DeltaRope<
|
||||
loro_delta::array_vec::ArrayVec<ValueOrHandler, 8>,
|
||||
crate::event::ListDeltaMeta,
|
||||
>,
|
||||
on_container_remap: &mut dyn FnMut(ContainerID, ContainerID),
|
||||
) -> LoroResult<()> {
|
||||
match &self.inner {
|
||||
MaybeDetached::Detached(_) => unimplemented!(),
|
||||
MaybeDetached::Attached(_) => {
|
||||
let mut index = 0;
|
||||
for item in delta.iter() {
|
||||
match item {
|
||||
loro_delta::DeltaItem::Retain { len, .. } => {
|
||||
index += len;
|
||||
}
|
||||
loro_delta::DeltaItem::Replace { value, delete, .. } => {
|
||||
if *delete > 0 {
|
||||
self.delete(index, *delete)?;
|
||||
}
|
||||
|
||||
for v in value.iter() {
|
||||
match v {
|
||||
ValueOrHandler::Value(v) => {
|
||||
self.insert(index, v.clone())?;
|
||||
}
|
||||
ValueOrHandler::Handler(h) => {
|
||||
let old_id = h.id();
|
||||
let new_h = self.insert_container(
|
||||
index,
|
||||
Handler::new_unattached(old_id.container_type()),
|
||||
)?;
|
||||
let new_id = new_h.id();
|
||||
on_container_remap(old_id, new_id);
|
||||
}
|
||||
}
|
||||
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl MovableListHandler {
|
||||
|
@ -2693,6 +2814,59 @@ impl MovableListHandler {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_delta(
|
||||
&self,
|
||||
delta: loro_delta::DeltaRope<
|
||||
loro_delta::array_vec::ArrayVec<ValueOrHandler, 8>,
|
||||
crate::event::ListDeltaMeta,
|
||||
>,
|
||||
on_container_remap: &mut dyn FnMut(ContainerID, ContainerID),
|
||||
) -> LoroResult<()> {
|
||||
match &self.inner {
|
||||
MaybeDetached::Detached(_) => {
|
||||
unimplemented!();
|
||||
}
|
||||
MaybeDetached::Attached(_) => {
|
||||
let mut index = 0;
|
||||
for d in delta.iter() {
|
||||
match d {
|
||||
loro_delta::DeltaItem::Retain { len, .. } => {
|
||||
index += len;
|
||||
}
|
||||
loro_delta::DeltaItem::Replace {
|
||||
value,
|
||||
delete,
|
||||
attr: _attr,
|
||||
} => {
|
||||
// TODO: handle move error
|
||||
self.delete(index, *delete)?;
|
||||
for v in value.iter() {
|
||||
match v {
|
||||
ValueOrHandler::Value(v) => {
|
||||
self.insert(index, v.clone())?;
|
||||
}
|
||||
ValueOrHandler::Handler(h) => {
|
||||
let old_id = h.id();
|
||||
let new_h = self.insert_container(
|
||||
index,
|
||||
Handler::new_unattached(old_id.container_type()),
|
||||
)?;
|
||||
let new_id = new_h.id();
|
||||
on_container_remap(old_id, new_id);
|
||||
}
|
||||
}
|
||||
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MapHandler {
|
||||
|
@ -2719,6 +2893,43 @@ impl MapHandler {
|
|||
}
|
||||
}
|
||||
|
||||
/// This method will insert the value even if the same value is already in the given entry.
|
||||
fn insert_without_skipping(&self, key: &str, value: impl Into<LoroValue>) -> LoroResult<()> {
|
||||
match &self.inner {
|
||||
MaybeDetached::Detached(m) => {
|
||||
let mut m = m.try_lock().unwrap();
|
||||
m.value
|
||||
.insert(key.into(), ValueOrHandler::Value(value.into()));
|
||||
Ok(())
|
||||
}
|
||||
MaybeDetached::Attached(a) => a.with_txn(|txn| {
|
||||
let this = &self;
|
||||
let value = value.into();
|
||||
if let Some(_value) = value.as_container() {
|
||||
return Err(LoroError::ArgErr(
|
||||
INSERT_CONTAINER_VALUE_ARG_ERROR
|
||||
.to_string()
|
||||
.into_boxed_str(),
|
||||
));
|
||||
}
|
||||
|
||||
let inner = this.inner.try_attached_state()?;
|
||||
txn.apply_local_op(
|
||||
inner.container_idx,
|
||||
crate::op::RawOpContent::Map(crate::container::map::MapSet {
|
||||
key: key.into(),
|
||||
value: Some(value.clone()),
|
||||
}),
|
||||
EventHint::Map {
|
||||
key: key.into(),
|
||||
value: Some(value.clone()),
|
||||
},
|
||||
&inner.state,
|
||||
)
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_with_txn(
|
||||
&self,
|
||||
txn: &mut Transaction,
|
||||
|
@ -2754,10 +2965,6 @@ impl MapHandler {
|
|||
}
|
||||
|
||||
pub fn insert_container<T: HandlerTrait>(&self, key: &str, handler: T) -> LoroResult<T> {
|
||||
if handler.is_attached() {
|
||||
return Err(LoroError::ReattachAttachedContainer);
|
||||
}
|
||||
|
||||
match &self.inner {
|
||||
MaybeDetached::Detached(m) => {
|
||||
let mut m = m.try_lock().unwrap();
|
||||
|
@ -2885,7 +3092,7 @@ impl MapHandler {
|
|||
|
||||
pub fn get_deep_value_with_id(&self) -> LoroResult<LoroValue> {
|
||||
match &self.inner {
|
||||
MaybeDetached::Detached(_) => Err(LoroError::MisuseDettachedContainer {
|
||||
MaybeDetached::Detached(_) => Err(LoroError::MisuseDetachedContainer {
|
||||
method: "get_deep_value_with_id",
|
||||
}),
|
||||
MaybeDetached::Attached(inner) => Ok(inner.with_doc_state(|state| {
|
||||
|
|
|
@ -49,6 +49,19 @@ impl TreeInner {
|
|||
id
|
||||
}
|
||||
|
||||
fn create_with_target(
|
||||
&mut self,
|
||||
parent: Option<TreeID>,
|
||||
index: usize,
|
||||
target: TreeID,
|
||||
) -> TreeID {
|
||||
self.map.insert(target, MapHandler::new_detached());
|
||||
self.parent_links.insert(target, parent);
|
||||
let children = self.children_links.entry(parent).or_default();
|
||||
children.insert(index, target);
|
||||
target
|
||||
}
|
||||
|
||||
fn mov(&mut self, target: TreeID, new_parent: Option<TreeID>, index: usize) -> LoroResult<()> {
|
||||
let old_parent = self
|
||||
.parent_links
|
||||
|
@ -294,7 +307,13 @@ impl TreeHandler {
|
|||
}),
|
||||
EventHint::Tree(TreeDiffItem {
|
||||
target,
|
||||
action: TreeExternalDiff::Delete,
|
||||
action: TreeExternalDiff::Delete {
|
||||
old_parent: self
|
||||
.get_node_parent(&target)
|
||||
.map(TreeParentId::from)
|
||||
.unwrap_or(TreeParentId::Unexist),
|
||||
old_index: self.get_index_by_tree_id(&target).unwrap_or(0),
|
||||
},
|
||||
}),
|
||||
&inner.state,
|
||||
)
|
||||
|
@ -322,6 +341,45 @@ impl TreeHandler {
|
|||
}
|
||||
}
|
||||
|
||||
/// For undo/redo, Specify the TreeID of the created node
|
||||
pub(crate) fn create_at_with_target(
|
||||
&self,
|
||||
parent: Option<TreeID>,
|
||||
index: usize,
|
||||
target: TreeID,
|
||||
) -> LoroResult<()> {
|
||||
if let Some(p) = parent {
|
||||
if !self.contains(p) {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
match &self.inner {
|
||||
MaybeDetached::Detached(t) => {
|
||||
let t = &mut t.try_lock().unwrap().value;
|
||||
t.create_with_target(parent, index, target);
|
||||
Ok(())
|
||||
}
|
||||
MaybeDetached::Attached(a) => a.with_txn(|txn| {
|
||||
let inner = self.inner.try_attached_state()?;
|
||||
match self.generate_position_at(&target, parent, index) {
|
||||
FractionalIndexGenResult::Ok(position) => {
|
||||
self.create_with_position(inner, txn, target, parent, index, position)?;
|
||||
}
|
||||
FractionalIndexGenResult::Rearrange(ids) => {
|
||||
for (i, (id, position)) in ids.into_iter().enumerate() {
|
||||
if i == 0 {
|
||||
self.create_with_position(inner, txn, id, parent, index, position)?;
|
||||
continue;
|
||||
}
|
||||
self.mov_with_position(inner, txn, id, parent, index + i, position)?;
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_with_txn<T: Into<Option<TreeID>>>(
|
||||
&self,
|
||||
txn: &mut Transaction,
|
||||
|
@ -510,6 +568,11 @@ impl TreeHandler {
|
|||
parent,
|
||||
index,
|
||||
position,
|
||||
old_parent: self
|
||||
.get_node_parent(&target)
|
||||
.map(TreeParentId::from)
|
||||
.unwrap_or(TreeParentId::Unexist),
|
||||
old_index: self.get_index_by_tree_id(&target).unwrap_or(0),
|
||||
},
|
||||
}),
|
||||
&inner.state,
|
||||
|
|
|
@ -17,8 +17,10 @@ pub use handler::{
|
|||
TreeHandler, UnknownHandler,
|
||||
};
|
||||
pub use loro::LoroDoc;
|
||||
pub use loro_common;
|
||||
pub use oplog::OpLog;
|
||||
pub use state::DocState;
|
||||
pub use undo::UndoManager;
|
||||
pub mod awareness;
|
||||
pub mod cursor;
|
||||
pub mod loro;
|
||||
|
@ -53,6 +55,7 @@ pub use error::{LoroError, LoroResult};
|
|||
pub(crate) mod group;
|
||||
pub(crate) mod macros;
|
||||
pub(crate) mod state;
|
||||
pub(crate) mod undo;
|
||||
pub(crate) mod value;
|
||||
pub(crate) use id::{PeerID, ID};
|
||||
|
||||
|
|
|
@ -10,9 +10,12 @@ use std::{
|
|||
},
|
||||
};
|
||||
|
||||
use loro_common::{ContainerID, ContainerType, LoroResult, LoroValue, ID};
|
||||
use either::Either;
|
||||
use fxhash::FxHashMap;
|
||||
use itertools::Itertools;
|
||||
use loro_common::{ContainerID, ContainerType, HasIdSpan, IdSpan, LoroResult, LoroValue, ID};
|
||||
use rle::HasLength;
|
||||
use tracing::{info_span, instrument, trace_span};
|
||||
use tracing::{debug, info_span, instrument};
|
||||
|
||||
use crate::{
|
||||
arena::SharedArena,
|
||||
|
@ -32,6 +35,7 @@ use crate::{
|
|||
id::PeerID,
|
||||
op::InnerContent,
|
||||
oplog::dag::FrontiersNotIncluded,
|
||||
undo::DiffBatch,
|
||||
version::Frontiers,
|
||||
HandlerTrait, InternalString, LoroError, VersionVector,
|
||||
};
|
||||
|
@ -155,7 +159,7 @@ impl LoroDoc {
|
|||
/// Create a doc with auto commit enabled.
|
||||
#[inline]
|
||||
pub fn new_auto_commit() -> Self {
|
||||
let mut doc = Self::new();
|
||||
let doc = Self::new();
|
||||
doc.start_auto_commit();
|
||||
doc
|
||||
}
|
||||
|
@ -274,7 +278,7 @@ impl LoroDoc {
|
|||
Ok(v)
|
||||
}
|
||||
|
||||
pub fn start_auto_commit(&mut self) {
|
||||
pub fn start_auto_commit(&self) {
|
||||
self.auto_commit.store(true, Release);
|
||||
let mut self_txn = self.txn.try_lock().unwrap();
|
||||
if self_txn.is_some() || self.detached.load(Acquire) {
|
||||
|
@ -604,6 +608,16 @@ impl LoroDoc {
|
|||
self.get_by_path(&path)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_handler(&self, id: ContainerID) -> Handler {
|
||||
Handler::new_attached(
|
||||
id,
|
||||
self.arena.clone(),
|
||||
self.get_global_txn(),
|
||||
Arc::downgrade(&self.state),
|
||||
)
|
||||
}
|
||||
|
||||
/// id can be a str, ContainerID, or ContainerIdRaw.
|
||||
/// if it's str it will use Root container, which will not be None
|
||||
#[inline]
|
||||
|
@ -695,6 +709,163 @@ impl LoroDoc {
|
|||
.unwrap()
|
||||
}
|
||||
|
||||
/// Undo the operations between the given id_span. It can be used even in a collaborative environment.
|
||||
///
|
||||
/// This is an internal API. You should NOT use it directly.
|
||||
///
|
||||
/// # Internal
|
||||
///
|
||||
/// This method will use the diff calculator to calculate the diff required to time travel
|
||||
/// from the end of id_span to the beginning of the id_span. Then it will convert the diff to
|
||||
/// operations and apply them to the OpLog with a dep on the last id of the given id_span.
|
||||
///
|
||||
/// This implementation is kinda slow, but it's simple and maintainable. We can optimize it
|
||||
/// further when it's needed. The time complexity is O(n + m), n is the ops in the id_span, m is the
|
||||
/// distance from id_span to the current latest version.
|
||||
#[instrument(level = "info", skip_all)]
|
||||
pub fn undo_internal(
|
||||
&self,
|
||||
id_span: IdSpan,
|
||||
container_remap: &mut FxHashMap<ContainerID, ContainerID>,
|
||||
post_transform_base: Option<&DiffBatch>,
|
||||
before_diff: &mut dyn FnMut(&DiffBatch),
|
||||
) -> LoroResult<CommitWhenDrop> {
|
||||
if self.is_detached() {
|
||||
return Err(LoroError::EditWhenDetached);
|
||||
}
|
||||
|
||||
self.commit_then_stop();
|
||||
if !self
|
||||
.oplog()
|
||||
.lock()
|
||||
.unwrap()
|
||||
.vv()
|
||||
.includes_id(id_span.id_last())
|
||||
{
|
||||
self.renew_txn_if_auto_commit();
|
||||
return Err(LoroError::UndoInvalidIdSpan(id_span.id_last()));
|
||||
}
|
||||
|
||||
let (was_recording, latest_frontiers) = {
|
||||
let mut state = self.state.lock().unwrap();
|
||||
let was_recording = state.is_recording();
|
||||
state.stop_and_clear_recording();
|
||||
(was_recording, state.frontiers.clone())
|
||||
};
|
||||
|
||||
let spans = self.oplog.lock().unwrap().split_span_based_on_deps(id_span);
|
||||
let diff = crate::undo::undo(
|
||||
spans,
|
||||
match post_transform_base {
|
||||
Some(d) => Either::Right(d),
|
||||
None => Either::Left(&latest_frontiers),
|
||||
},
|
||||
|from, to| {
|
||||
self.checkout_without_emitting(from).unwrap();
|
||||
self.state.lock().unwrap().start_recording();
|
||||
self.checkout_without_emitting(to).unwrap();
|
||||
let mut state = self.state.lock().unwrap();
|
||||
let e = state.take_events();
|
||||
state.stop_and_clear_recording();
|
||||
DiffBatch::new(e)
|
||||
},
|
||||
before_diff,
|
||||
);
|
||||
|
||||
self.checkout_without_emitting(&latest_frontiers)?;
|
||||
self.detached.store(false, Release);
|
||||
if was_recording {
|
||||
self.state.lock().unwrap().start_recording();
|
||||
}
|
||||
self.start_auto_commit();
|
||||
|
||||
self.apply_diff(diff, container_remap).unwrap();
|
||||
Ok(CommitWhenDrop { doc: self })
|
||||
}
|
||||
|
||||
/// Calculate the diff between the current state and the target state, and apply the diff to the current state.
|
||||
pub fn diff_and_apply(&self, target: &Frontiers) -> LoroResult<()> {
|
||||
let f = self.state_frontiers();
|
||||
let diff = self.diff(&f, target)?;
|
||||
self.apply_diff(diff, &mut Default::default())
|
||||
}
|
||||
|
||||
/// Calculate the diff between two versions so that apply diff on a will make the state same as b.
|
||||
///
|
||||
/// NOTE: This method will make the doc enter the **detached mode**.
|
||||
pub fn diff(&self, a: &Frontiers, b: &Frontiers) -> LoroResult<DiffBatch> {
|
||||
{
|
||||
// check whether a and b are valid
|
||||
let oplog = self.oplog.lock().unwrap();
|
||||
for &id in a.iter() {
|
||||
if !oplog.dag.contains(id) {
|
||||
return Err(LoroError::FrontiersNotFound(id));
|
||||
}
|
||||
}
|
||||
for &id in b.iter() {
|
||||
if !oplog.dag.contains(id) {
|
||||
return Err(LoroError::FrontiersNotFound(id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.commit_then_stop();
|
||||
|
||||
let ans = {
|
||||
self.state.lock().unwrap().stop_and_clear_recording();
|
||||
self.checkout_without_emitting(a).unwrap();
|
||||
self.state.lock().unwrap().start_recording();
|
||||
self.checkout_without_emitting(b).unwrap();
|
||||
let mut state = self.state.lock().unwrap();
|
||||
let e = state.take_events();
|
||||
state.stop_and_clear_recording();
|
||||
DiffBatch::new(e)
|
||||
};
|
||||
|
||||
Ok(ans)
|
||||
}
|
||||
|
||||
/// Apply a diff to the current state.
|
||||
///
|
||||
/// This method will not recreate containers with the same [ContainerID]s.
|
||||
/// While this can be convenient in certain cases, it can break several internal invariants:
|
||||
///
|
||||
/// 1. Each container should appear only once in the document. Allowing containers with the same ID
|
||||
/// would result in multiple instances of the same container in the document.
|
||||
/// 2. Unreachable containers should be removable from the state when necessary.
|
||||
///
|
||||
/// However, the diff may contain operations that depend on container IDs.
|
||||
/// Therefore, users need to provide a `container_remap` to record and retrieve the container ID remapping.
|
||||
pub fn apply_diff(
|
||||
&self,
|
||||
mut diff: DiffBatch,
|
||||
container_remap: &mut FxHashMap<ContainerID, ContainerID>,
|
||||
) -> LoroResult<()> {
|
||||
if self.is_detached() {
|
||||
return Err(LoroError::EditWhenDetached);
|
||||
}
|
||||
|
||||
// Sort container from the top to the bottom, so that we can have correct container remap
|
||||
let containers = diff.0.keys().cloned().sorted_by_cached_key(|cid| {
|
||||
let idx = self.arena.id_to_idx(cid).unwrap();
|
||||
self.arena.get_depth(idx).unwrap().get()
|
||||
});
|
||||
for mut id in containers {
|
||||
let diff = diff.0.remove(&id).unwrap();
|
||||
|
||||
while let Some(rid) = container_remap.get(&id) {
|
||||
id = rid.clone();
|
||||
}
|
||||
let h = self.get_handler(id);
|
||||
h.apply_diff(diff, &mut |old_id, new_id| {
|
||||
container_remap.insert(old_id, new_id);
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// This is for debugging purpose. It will travel the whole oplog
|
||||
#[inline]
|
||||
pub fn diagnose_size(&self) {
|
||||
|
@ -818,25 +989,31 @@ 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(&self, frontiers: &Frontiers) -> LoroResult<()> {
|
||||
let from = self.state_frontiers();
|
||||
let span = info_span!("checkout", to=?frontiers, ?from);
|
||||
let _g = span.enter();
|
||||
self.checkout_without_emitting(frontiers)?;
|
||||
self.emit_events();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip(self))]
|
||||
fn checkout_without_emitting(&self, frontiers: &Frontiers) -> Result<(), LoroError> {
|
||||
self.commit_then_stop();
|
||||
let oplog = self.oplog.lock().unwrap();
|
||||
let mut state = self.state.lock().unwrap();
|
||||
self.detached.store(true, Release);
|
||||
let mut calc = self.diff_calculator.lock().unwrap();
|
||||
for &f in frontiers.iter() {
|
||||
if !oplog.dag.contains(f) {
|
||||
return Err(LoroError::InvalidFrontierIdNotFound(f));
|
||||
for &i in frontiers.iter() {
|
||||
if !oplog.dag.contains(i) {
|
||||
return Err(LoroError::FrontiersNotFound(i));
|
||||
}
|
||||
}
|
||||
|
||||
let before = &oplog.dag.frontiers_to_vv(&state.frontiers).unwrap();
|
||||
let Some(after) = &oplog.dag.frontiers_to_vv(frontiers) else {
|
||||
return Err(LoroError::NotFoundError(
|
||||
format!("Cannot find the specified version {:?}", frontiers).into_boxed_str(),
|
||||
));
|
||||
};
|
||||
|
||||
let diff = calc.calc_diff_internal(
|
||||
&oplog,
|
||||
before,
|
||||
|
@ -851,8 +1028,6 @@ impl LoroDoc {
|
|||
by: EventTriggerKind::Checkout,
|
||||
new_version: Cow::Owned(frontiers.clone()),
|
||||
});
|
||||
drop(state);
|
||||
self.emit_events();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -909,7 +1084,7 @@ impl LoroDoc {
|
|||
|
||||
IS_CHECKING.store(true, std::sync::atomic::Ordering::Release);
|
||||
let peer_id = self.peer_id();
|
||||
let s = trace_span!("CheckStateDiffCalcConsistencySlow", ?peer_id);
|
||||
let s = info_span!("CheckStateDiffCalcConsistencySlow", ?peer_id);
|
||||
let _g = s.enter();
|
||||
self.commit_then_stop();
|
||||
let bytes = self.export_from(&Default::default());
|
||||
|
@ -1098,6 +1273,17 @@ fn find_last_delete_op(oplog: &OpLog, id: ID, idx: ContainerIdx) -> Option<ID> {
|
|||
None
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CommitWhenDrop<'a> {
|
||||
doc: &'a LoroDoc,
|
||||
}
|
||||
|
||||
impl<'a> Drop for CommitWhenDrop<'a> {
|
||||
fn drop(&mut self) {
|
||||
self.doc.commit_then_renew()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use loro_common::ID;
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
use enum_as_inner::EnumAsInner;
|
||||
use loro_common::{ContainerID, LoroValue};
|
||||
use rle::{HasLength, Mergable, Sliceable};
|
||||
#[cfg(feature = "wasm")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
arena::SharedArena,
|
||||
container::{
|
||||
list::list_op::{InnerListOp, ListOp},
|
||||
map::MapSet,
|
||||
|
@ -21,6 +23,47 @@ pub enum InnerContent {
|
|||
Future(FutureInnerContent),
|
||||
}
|
||||
|
||||
impl InnerContent {
|
||||
pub fn visit_created_children(&self, arena: &SharedArena, f: &mut dyn FnMut(&ContainerID)) {
|
||||
match self {
|
||||
InnerContent::List(l) => match l {
|
||||
InnerListOp::Insert { slice, .. } => {
|
||||
for v in arena.iter_value_slice(slice.to_range()) {
|
||||
if let LoroValue::Container(c) = v {
|
||||
f(&c);
|
||||
}
|
||||
}
|
||||
}
|
||||
InnerListOp::Set { value, .. } => {
|
||||
if let LoroValue::Container(c) = value {
|
||||
f(c);
|
||||
}
|
||||
}
|
||||
|
||||
InnerListOp::Move { .. } => {}
|
||||
InnerListOp::InsertText { .. } => {}
|
||||
InnerListOp::Delete(_) => {}
|
||||
InnerListOp::StyleStart { .. } => {}
|
||||
InnerListOp::StyleEnd => {}
|
||||
},
|
||||
crate::op::InnerContent::Map(m) => {
|
||||
if let Some(LoroValue::Container(c)) = &m.value {
|
||||
f(c);
|
||||
}
|
||||
}
|
||||
crate::op::InnerContent::Tree(t) => {
|
||||
let id = t.target.associated_meta_container();
|
||||
f(&id);
|
||||
}
|
||||
crate::op::InnerContent::Future(f) => match &f {
|
||||
#[cfg(feature = "counter")]
|
||||
crate::op::FutureInnerContent::Counter(_) => {}
|
||||
crate::op::FutureInnerContent::Unknown { .. } => {}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(EnumAsInner, Debug, Clone)]
|
||||
pub enum FutureInnerContent {
|
||||
#[cfg(feature = "counter")]
|
||||
|
|
|
@ -7,6 +7,7 @@ use std::cell::RefCell;
|
|||
use std::cmp::Ordering;
|
||||
use std::mem::take;
|
||||
use std::rc::Rc;
|
||||
use tracing::debug;
|
||||
|
||||
use crate::change::{get_sys_timestamp, Change, Lamport, Timestamp};
|
||||
use crate::configure::Configure;
|
||||
|
@ -528,6 +529,16 @@ impl OpLog {
|
|||
None
|
||||
}
|
||||
|
||||
pub fn get_deps_of(&self, id: ID) -> Option<Frontiers> {
|
||||
self.get_change_at(id).map(|c| {
|
||||
if c.id.counter == id.counter {
|
||||
c.deps.clone()
|
||||
} else {
|
||||
Frontiers::from_id(id.inc(-1))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_remote_change_at(&self, id: ID) -> Option<Change<RemoteOp>> {
|
||||
let change = self.get_change_at(id)?;
|
||||
Some(self.convert_change_to_remote(change))
|
||||
|
@ -932,6 +943,33 @@ impl OpLog {
|
|||
let change = self.get_change_at(id)?;
|
||||
change.ops.get_by_atom_index(id.counter).map(|x| x.element)
|
||||
}
|
||||
|
||||
pub(crate) fn split_span_based_on_deps(&self, id_span: IdSpan) -> Vec<(IdSpan, Frontiers)> {
|
||||
let peer = id_span.peer;
|
||||
let mut counter = id_span.counter.min();
|
||||
let span_end = id_span.counter.norm_end();
|
||||
let mut ans = Vec::new();
|
||||
|
||||
while counter < span_end {
|
||||
let id = ID::new(peer, counter);
|
||||
let node = self.dag.get(id).unwrap();
|
||||
|
||||
let f = if node.cnt == counter {
|
||||
node.deps.clone()
|
||||
} else if counter > 0 {
|
||||
id.inc(-1).into()
|
||||
} else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
let cur_end = node.cnt + node.len as Counter;
|
||||
let len = cur_end.min(span_end) - counter;
|
||||
ans.push((id.to_span(len as usize), f));
|
||||
counter += len;
|
||||
}
|
||||
|
||||
ans
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
|
@ -9,7 +9,7 @@ use loro_common::LoroValue;
|
|||
use crate::{
|
||||
change::Change,
|
||||
container::{
|
||||
list::list_op::{self, ListOp},
|
||||
list::list_op::{ListOp},
|
||||
map::MapSet,
|
||||
tree::tree_op::TreeOp,
|
||||
},
|
||||
|
@ -25,46 +25,10 @@ impl OpLog {
|
|||
pub(super) fn register_container_and_parent_link(&self, change: &Change) {
|
||||
let arena = &self.arena;
|
||||
for op in change.ops.iter() {
|
||||
match &op.content {
|
||||
crate::op::InnerContent::List(l) => match l {
|
||||
list_op::InnerListOp::Insert { slice, .. } => {
|
||||
for v in arena.iter_value_slice(slice.to_range()) {
|
||||
if let LoroValue::Container(c) = v {
|
||||
let idx = arena.register_container(&c);
|
||||
arena.set_parent(idx, Some(op.container));
|
||||
}
|
||||
}
|
||||
}
|
||||
list_op::InnerListOp::Set { value, .. } => {
|
||||
if let LoroValue::Container(c) = value {
|
||||
let idx = arena.register_container(c);
|
||||
arena.set_parent(idx, Some(op.container));
|
||||
}
|
||||
}
|
||||
|
||||
list_op::InnerListOp::Move { .. } => {}
|
||||
list_op::InnerListOp::InsertText { .. } => {}
|
||||
list_op::InnerListOp::Delete(_) => {}
|
||||
list_op::InnerListOp::StyleStart { .. } => {}
|
||||
list_op::InnerListOp::StyleEnd => {}
|
||||
},
|
||||
crate::op::InnerContent::Map(m) => {
|
||||
if let Some(LoroValue::Container(c)) = &m.value {
|
||||
let idx = arena.register_container(c);
|
||||
arena.set_parent(idx, Some(op.container));
|
||||
}
|
||||
}
|
||||
crate::op::InnerContent::Tree(t) => {
|
||||
let id = t.target.associated_meta_container();
|
||||
let idx = arena.register_container(&id);
|
||||
arena.set_parent(idx, Some(op.container));
|
||||
}
|
||||
crate::op::InnerContent::Future(f) => match &f {
|
||||
#[cfg(feature = "counter")]
|
||||
crate::op::FutureInnerContent::Counter(_) => {}
|
||||
crate::op::FutureInnerContent::Unknown { .. } => {}
|
||||
},
|
||||
}
|
||||
op.content.visit_created_children(arena, &mut |c| {
|
||||
let idx = arena.register_container(c);
|
||||
arena.set_parent(idx, Some(op.container));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ use enum_dispatch::enum_dispatch;
|
|||
use fxhash::{FxHashMap, FxHashSet};
|
||||
use loro_common::{ContainerID, LoroError, LoroResult};
|
||||
use loro_delta::DeltaItem;
|
||||
use tracing::{info, instrument, trace_span};
|
||||
use tracing::{info, instrument};
|
||||
|
||||
use crate::{
|
||||
configure::{Configure, DefaultRandom, SecureRandomGenerator},
|
||||
|
@ -478,8 +478,6 @@ impl DocState {
|
|||
let state = get_or_create!(self, idx);
|
||||
if is_recording {
|
||||
// process bring_back before apply
|
||||
let span = trace_span!("handle internal recording");
|
||||
let _g = span.enter();
|
||||
let external_diff =
|
||||
if diff.bring_back || to_revive_in_this_layer.contains(&idx) {
|
||||
state.apply_diff(
|
||||
|
@ -549,7 +547,7 @@ impl DocState {
|
|||
}
|
||||
|
||||
diff.diff = diffs.into();
|
||||
self.frontiers = (*diff.new_version).to_owned();
|
||||
(*diff.new_version).clone_into(&mut self.frontiers);
|
||||
if self.is_recording() {
|
||||
self.record_diff(diff)
|
||||
}
|
||||
|
@ -990,13 +988,8 @@ impl DocState {
|
|||
fn get_path(&self, idx: ContainerIdx) -> Option<Vec<(ContainerID, Index)>> {
|
||||
let mut ans = Vec::new();
|
||||
let mut idx = idx;
|
||||
let id = self.arena.idx_to_id(idx).unwrap();
|
||||
let s = tracing::span!(tracing::Level::INFO, "GET PATH ", ?id);
|
||||
let _e = s.enter();
|
||||
loop {
|
||||
let id = self.arena.idx_to_id(idx).unwrap();
|
||||
let s = tracing::span!(tracing::Level::INFO, "GET PATH ", ?id);
|
||||
let _e = s.enter();
|
||||
if let Some(parent_idx) = self.arena.get_parent(idx) {
|
||||
let parent_state = self.states.get(&parent_idx)?;
|
||||
let Some(prop) = parent_state.get_child_index(&id) else {
|
||||
|
|
|
@ -2,7 +2,7 @@ use itertools::Itertools;
|
|||
use loro_delta::{array_vec::ArrayVec, DeltaRope, DeltaRopeBuilder};
|
||||
use serde_columnar::columnar;
|
||||
use std::sync::{Arc, Mutex, Weak};
|
||||
use tracing::{instrument, trace_span, warn};
|
||||
use tracing::{instrument, warn};
|
||||
|
||||
use fxhash::FxHashMap;
|
||||
use generic_btree::BTree;
|
||||
|
@ -962,10 +962,6 @@ impl ContainerState for MovableListState {
|
|||
None
|
||||
};
|
||||
|
||||
let id = arena.idx_to_id(self.idx).unwrap();
|
||||
let s = trace_span!("ListState", "ListState.id = {:?}", id);
|
||||
let _e = s.enter();
|
||||
|
||||
let mut event: ListDiff = DeltaRope::new();
|
||||
let mut maybe_moved: FxHashMap<CompactIdLp, (usize, LoroValue)> = FxHashMap::default();
|
||||
|
||||
|
|
|
@ -245,6 +245,7 @@ impl ContainerState for RichtextState {
|
|||
let mut style_starts: FxHashMap<Arc<StyleOp>, Pos> = FxHashMap::default();
|
||||
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() {
|
||||
match span {
|
||||
crate::delta::DeltaItem::Retain { retain: len, .. } => {
|
||||
|
@ -311,7 +312,7 @@ impl ContainerState for RichtextState {
|
|||
}
|
||||
|
||||
delta.chop();
|
||||
style_delta.compose(&delta);
|
||||
new_style_deltas.push(delta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -399,6 +400,9 @@ impl ContainerState for RichtextState {
|
|||
}
|
||||
}
|
||||
|
||||
for s in new_style_deltas {
|
||||
style_delta.compose(&s);
|
||||
}
|
||||
// self.check_consistency_between_content_and_style_ranges();
|
||||
ans.compose(&style_delta);
|
||||
Diff::Text(ans)
|
||||
|
|
|
@ -440,7 +440,7 @@ mod btree {
|
|||
target: &Self::QueryArg,
|
||||
caches: &[generic_btree::Child<ChildTreeTrait>],
|
||||
) -> FindResult {
|
||||
match caches.binary_search_by(|x| {
|
||||
let result = caches.binary_search_by(|x| {
|
||||
let range = x.cache.range.as_ref().unwrap();
|
||||
if target < &range.start {
|
||||
core::cmp::Ordering::Greater
|
||||
|
@ -449,7 +449,9 @@ mod btree {
|
|||
} else {
|
||||
core::cmp::Ordering::Equal
|
||||
}
|
||||
}) {
|
||||
});
|
||||
|
||||
match result {
|
||||
Ok(i) => FindResult::new_found(i, 0),
|
||||
Err(i) => FindResult::new_missing(
|
||||
i.min(caches.len() - 1),
|
||||
|
@ -832,6 +834,8 @@ impl ContainerState for TreeState {
|
|||
});
|
||||
}
|
||||
TreeInternalDiff::Move { parent, position } => {
|
||||
let old_parent = self.trees.get(&target).unwrap().parent;
|
||||
let old_index = self.get_index_by_tree_id(&target).unwrap();
|
||||
self.mov(target, *parent, last_move_op, Some(position.clone()), false)
|
||||
.unwrap();
|
||||
let index = self.get_index_by_tree_id(&target).unwrap();
|
||||
|
@ -841,15 +845,22 @@ impl ContainerState for TreeState {
|
|||
parent: parent.into_node().ok(),
|
||||
index,
|
||||
position: position.clone(),
|
||||
old_parent,
|
||||
old_index,
|
||||
},
|
||||
});
|
||||
}
|
||||
TreeInternalDiff::Delete { parent, position } => {
|
||||
let old_parent = self.trees.get(&target).unwrap().parent;
|
||||
let old_index = self.get_index_by_tree_id(&target).unwrap();
|
||||
self.mov(target, *parent, last_move_op, position.clone(), false)
|
||||
.unwrap();
|
||||
ans.push(TreeDiffItem {
|
||||
target,
|
||||
action: TreeExternalDiff::Delete,
|
||||
action: TreeExternalDiff::Delete {
|
||||
old_parent,
|
||||
old_index,
|
||||
},
|
||||
});
|
||||
}
|
||||
TreeInternalDiff::MoveInDelete { parent, position } => {
|
||||
|
@ -857,9 +868,14 @@ impl ContainerState for TreeState {
|
|||
.unwrap();
|
||||
}
|
||||
TreeInternalDiff::UnCreate => {
|
||||
let old_parent = self.trees.get(&target).unwrap().parent;
|
||||
let old_index = self.get_index_by_tree_id(&target).unwrap();
|
||||
ans.push(TreeDiffItem {
|
||||
target,
|
||||
action: TreeExternalDiff::Delete,
|
||||
action: TreeExternalDiff::Delete {
|
||||
old_parent,
|
||||
old_index,
|
||||
},
|
||||
});
|
||||
// delete it from state
|
||||
let parent = self.trees.remove(&target);
|
||||
|
|
490
crates/loro-internal/src/undo.rs
Normal file
490
crates/loro-internal/src/undo.rs
Normal file
|
@ -0,0 +1,490 @@
|
|||
use std::{
|
||||
collections::VecDeque,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use either::Either;
|
||||
use fxhash::FxHashMap;
|
||||
use loro_common::{
|
||||
ContainerID, Counter, CounterSpan, HasCounterSpan, HasIdSpan, IdSpan, LoroError, LoroResult,
|
||||
PeerID,
|
||||
};
|
||||
use tracing::{debug_span, info_span, instrument};
|
||||
|
||||
use crate::{
|
||||
change::get_sys_timestamp,
|
||||
event::{Diff, EventTriggerKind},
|
||||
version::Frontiers,
|
||||
ContainerDiff, DocDiff, LoroDoc,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct DiffBatch(pub(crate) FxHashMap<ContainerID, Diff>);
|
||||
|
||||
impl DiffBatch {
|
||||
pub fn new(diff: Vec<DocDiff>) -> Self {
|
||||
let mut map: FxHashMap<ContainerID, Diff> = Default::default();
|
||||
for d in diff.into_iter() {
|
||||
for item in d.diff.into_iter() {
|
||||
let old = map.insert(item.id.clone(), item.diff);
|
||||
assert!(old.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
Self(map)
|
||||
}
|
||||
|
||||
pub fn compose(&mut self, other: &Self) {
|
||||
if other.0.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
for (idx, diff) in self.0.iter_mut() {
|
||||
if let Some(b_diff) = other.0.get(idx) {
|
||||
diff.compose_ref(b_diff);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn transform(&mut self, other: &Self, left_priority: bool) {
|
||||
if other.0.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
for (idx, diff) in self.0.iter_mut() {
|
||||
if let Some(b_diff) = other.0.get(idx) {
|
||||
diff.transform(b_diff, left_priority);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.0.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// UndoManager is responsible for managing undo/redo from the current peer's perspective.
|
||||
///
|
||||
/// Undo/local is local: it cannot be used to undone the changes made by other peers.
|
||||
/// If you want to undo changes made by other peers, you may need to use the time travel feature.
|
||||
///
|
||||
/// PeerID cannot be changed during the lifetime of the UndoManager
|
||||
#[derive(Debug)]
|
||||
pub struct UndoManager {
|
||||
peer: PeerID,
|
||||
container_remap: FxHashMap<ContainerID, ContainerID>,
|
||||
inner: Arc<Mutex<UndoManagerInner>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct UndoManagerInner {
|
||||
latest_counter: Counter,
|
||||
undo_stack: Stack,
|
||||
redo_stack: Stack,
|
||||
processing_undo: bool,
|
||||
last_undo_time: i64,
|
||||
merge_interval: i64,
|
||||
max_stack_size: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Stack {
|
||||
stack: VecDeque<(VecDeque<CounterSpan>, Arc<Mutex<DiffBatch>>)>,
|
||||
size: usize,
|
||||
}
|
||||
|
||||
impl Stack {
|
||||
pub fn new() -> Self {
|
||||
let mut stack = VecDeque::new();
|
||||
stack.push_back((VecDeque::new(), Arc::new(Mutex::new(Default::default()))));
|
||||
Stack { stack, size: 0 }
|
||||
}
|
||||
|
||||
pub fn pop(&mut self) -> Option<(CounterSpan, Arc<Mutex<DiffBatch>>)> {
|
||||
while self.stack.back().unwrap().0.is_empty() && self.stack.len() > 1 {
|
||||
let (_, diff) = self.stack.pop_back().unwrap();
|
||||
let diff = diff.try_lock().unwrap();
|
||||
if !diff.0.is_empty() {
|
||||
self.stack
|
||||
.back_mut()
|
||||
.unwrap()
|
||||
.1
|
||||
.try_lock()
|
||||
.unwrap()
|
||||
.compose(&diff);
|
||||
}
|
||||
}
|
||||
|
||||
if self.stack.len() == 1 && self.stack.back().unwrap().0.is_empty() {
|
||||
self.stack.back_mut().unwrap().1.try_lock().unwrap().clear();
|
||||
return None;
|
||||
}
|
||||
|
||||
self.size -= 1;
|
||||
let last = self.stack.back_mut().unwrap();
|
||||
last.0.pop_back().map(|x| (x, last.1.clone()))
|
||||
}
|
||||
|
||||
pub fn push(&mut self, span: CounterSpan) {
|
||||
self.push_with_merge(span, false)
|
||||
}
|
||||
|
||||
pub fn push_with_merge(&mut self, span: CounterSpan, can_merge: bool) {
|
||||
let last = self.stack.back_mut().unwrap();
|
||||
let mut last_remote_diff = last.1.try_lock().unwrap();
|
||||
if !last_remote_diff.0.is_empty() {
|
||||
// If the remote diff is not empty, we cannot merge
|
||||
if last.0.is_empty() {
|
||||
last.0.push_back(span);
|
||||
last_remote_diff.clear();
|
||||
} else {
|
||||
drop(last_remote_diff);
|
||||
let mut v = VecDeque::new();
|
||||
v.push_back(span);
|
||||
self.stack
|
||||
.push_back((v, Arc::new(Mutex::new(DiffBatch::default()))));
|
||||
}
|
||||
|
||||
self.size += 1;
|
||||
} else {
|
||||
if can_merge {
|
||||
if let Some(last_span) = last.0.back_mut() {
|
||||
if last_span.end == span.start {
|
||||
// merge the span
|
||||
last_span.end = span.end;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.size += 1;
|
||||
last.0.push_back(span);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compose_remote_event(&mut self, diff: &[&ContainerDiff]) {
|
||||
if self.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let remote_diff = &mut self.stack.back_mut().unwrap().1;
|
||||
let mut remote_diff = remote_diff.try_lock().unwrap();
|
||||
for e in diff {
|
||||
if let Some(d) = remote_diff.0.get_mut(&e.id) {
|
||||
d.compose_ref(&e.diff);
|
||||
} else {
|
||||
remote_diff.0.insert(e.id.clone(), e.diff.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn transform_based_on_this_delta(&mut self, diff: &DiffBatch) {
|
||||
if self.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let remote_diff = &mut self.stack.back_mut().unwrap().1;
|
||||
remote_diff.try_lock().unwrap().transform(diff, false);
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.stack = VecDeque::new();
|
||||
self.stack.push_back((VecDeque::new(), Default::default()));
|
||||
self.size = 0;
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.size == 0
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.size
|
||||
}
|
||||
|
||||
fn pop_front(&mut self) {
|
||||
if self.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.size -= 1;
|
||||
let first = self.stack.front_mut().unwrap();
|
||||
let f = first.0.pop_front();
|
||||
assert!(f.is_some());
|
||||
if first.0.is_empty() {
|
||||
self.stack.pop_front();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Stack {
|
||||
fn default() -> Self {
|
||||
Stack::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl UndoManagerInner {
|
||||
fn new(last_counter: Counter) -> Self {
|
||||
UndoManagerInner {
|
||||
latest_counter: last_counter,
|
||||
undo_stack: Default::default(),
|
||||
redo_stack: Default::default(),
|
||||
processing_undo: false,
|
||||
merge_interval: 0,
|
||||
last_undo_time: 0,
|
||||
max_stack_size: usize::MAX,
|
||||
}
|
||||
}
|
||||
|
||||
fn record_checkpoint(&mut self, latest_counter: Counter) {
|
||||
if latest_counter == self.latest_counter {
|
||||
return;
|
||||
}
|
||||
|
||||
assert!(self.latest_counter < latest_counter);
|
||||
let now = get_sys_timestamp();
|
||||
let span = CounterSpan::new(self.latest_counter, latest_counter);
|
||||
if !self.undo_stack.is_empty() && now - self.last_undo_time < self.merge_interval {
|
||||
self.undo_stack.push_with_merge(span, true);
|
||||
} else {
|
||||
self.last_undo_time = now;
|
||||
self.undo_stack.push(span);
|
||||
}
|
||||
|
||||
self.latest_counter = latest_counter;
|
||||
self.redo_stack.clear();
|
||||
while self.undo_stack.len() > self.max_stack_size {
|
||||
self.undo_stack.pop_front();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_counter_end(doc: &LoroDoc, peer: PeerID) -> Counter {
|
||||
doc.oplog()
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get_peer_changes(peer)
|
||||
.and_then(|x| x.last().map(|x| x.ctr_end()))
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
impl UndoManager {
|
||||
pub fn new(doc: &LoroDoc) -> Self {
|
||||
let peer = doc.peer_id();
|
||||
let inner = Arc::new(Mutex::new(UndoManagerInner::new(get_counter_end(
|
||||
doc, peer,
|
||||
))));
|
||||
let inner_clone = inner.clone();
|
||||
doc.subscribe_root(Arc::new(move |event| match event.event_meta.by {
|
||||
EventTriggerKind::Local => {
|
||||
// TODO: PERF undo can be significantly faster if we can get
|
||||
// the DiffBatch for undo here
|
||||
let Ok(mut inner) = inner_clone.try_lock() else {
|
||||
return;
|
||||
};
|
||||
if !inner.processing_undo {
|
||||
if let Some(id) = event.event_meta.to.iter().find(|x| x.peer == peer) {
|
||||
inner.record_checkpoint(id.counter + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
EventTriggerKind::Import => {
|
||||
let mut inner = inner_clone.try_lock().unwrap();
|
||||
inner.undo_stack.compose_remote_event(event.events);
|
||||
inner.redo_stack.compose_remote_event(event.events);
|
||||
}
|
||||
EventTriggerKind::Checkout => {}
|
||||
}));
|
||||
|
||||
UndoManager {
|
||||
peer,
|
||||
container_remap: Default::default(),
|
||||
inner,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_merge_interval(&mut self, interval: i64) {
|
||||
self.inner.try_lock().unwrap().merge_interval = interval;
|
||||
}
|
||||
|
||||
pub fn set_max_undo_steps(&mut self, size: usize) {
|
||||
self.inner.try_lock().unwrap().max_stack_size = size;
|
||||
}
|
||||
|
||||
pub fn record_new_checkpoint(&mut self, doc: &LoroDoc) -> LoroResult<()> {
|
||||
if doc.peer_id() != self.peer {
|
||||
return Err(LoroError::UndoWithDifferentPeerId {
|
||||
expected: self.peer,
|
||||
actual: doc.peer_id(),
|
||||
});
|
||||
}
|
||||
|
||||
doc.commit_then_renew();
|
||||
let counter = get_counter_end(doc, self.peer);
|
||||
self.inner.try_lock().unwrap().record_checkpoint(counter);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub fn undo(&mut self, doc: &LoroDoc) -> LoroResult<()> {
|
||||
self.perform(doc, |x| &mut x.undo_stack, |x| &mut x.redo_stack)
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub fn redo(&mut self, doc: &LoroDoc) -> LoroResult<()> {
|
||||
self.perform(doc, |x| &mut x.redo_stack, |x| &mut x.undo_stack)
|
||||
}
|
||||
|
||||
fn perform(
|
||||
&mut self,
|
||||
doc: &LoroDoc,
|
||||
get_stack: impl Fn(&mut UndoManagerInner) -> &mut Stack,
|
||||
get_opposite: impl Fn(&mut UndoManagerInner) -> &mut Stack,
|
||||
) -> LoroResult<()> {
|
||||
self.record_new_checkpoint(doc)?;
|
||||
let end_counter = get_counter_end(doc, self.peer);
|
||||
let mut top = {
|
||||
let mut inner = self.inner.try_lock().unwrap();
|
||||
inner.processing_undo = true;
|
||||
get_stack(&mut inner).pop()
|
||||
};
|
||||
|
||||
while let Some((span, e)) = top {
|
||||
{
|
||||
let inner = self.inner.clone();
|
||||
// TODO: Perf we can try to avoid this clone
|
||||
let e = e.try_lock().unwrap().clone();
|
||||
doc.undo_internal(
|
||||
IdSpan {
|
||||
peer: self.peer,
|
||||
counter: span,
|
||||
},
|
||||
&mut self.container_remap,
|
||||
Some(&e),
|
||||
&mut |diff| {
|
||||
info_span!("transform remote diff").in_scope(|| {
|
||||
let mut inner = inner.try_lock().unwrap();
|
||||
get_stack(&mut inner).transform_based_on_this_delta(diff);
|
||||
});
|
||||
},
|
||||
)?;
|
||||
}
|
||||
let new_counter = get_counter_end(doc, self.peer);
|
||||
if end_counter != new_counter {
|
||||
let mut inner = self.inner.try_lock().unwrap();
|
||||
get_opposite(&mut inner).push(CounterSpan::new(end_counter, new_counter));
|
||||
inner.latest_counter = new_counter;
|
||||
break;
|
||||
} else {
|
||||
// continue to pop the undo item as this undo is a no-op
|
||||
top = get_stack(&mut self.inner.try_lock().unwrap()).pop();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
self.inner.try_lock().unwrap().processing_undo = false;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn can_undo(&self) -> bool {
|
||||
!self.inner.try_lock().unwrap().undo_stack.is_empty()
|
||||
}
|
||||
|
||||
pub fn can_redo(&self) -> bool {
|
||||
!self.inner.try_lock().unwrap().redo_stack.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// Undo the given spans of operations.
|
||||
///
|
||||
/// # Parameters
|
||||
///
|
||||
/// - `spans`: A vector of tuples where each tuple contains an `IdSpan` and its associated `Frontiers`.
|
||||
/// - `IdSpan`: Represents a span of operations identified by an ID.
|
||||
/// - `Frontiers`: Represents the deps of the given id_span
|
||||
/// - `latest_frontiers`: The latest frontiers of the document
|
||||
/// - `calc_diff`: A closure that takes two `Frontiers` and calculates the difference between them, returning a `DiffBatch`.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// - `DiffBatch`: Applying this batch on the `latest_frontiers` will undo the ops in the given spans.
|
||||
pub(crate) fn undo(
|
||||
spans: Vec<(IdSpan, Frontiers)>,
|
||||
last_frontiers_or_last_bi: Either<&Frontiers, &DiffBatch>,
|
||||
calc_diff: impl Fn(&Frontiers, &Frontiers) -> DiffBatch,
|
||||
on_last_event_a: &mut dyn FnMut(&DiffBatch),
|
||||
) -> DiffBatch {
|
||||
// The process of performing undo is:
|
||||
//
|
||||
// 0. Split the span into a series of continuous spans. There is no external dep within each continuous span.
|
||||
//
|
||||
// For each continuous span_i:
|
||||
//
|
||||
// 1. a. Calculate the event of checkout from id_span.last to id_span.deps, call it Ai. It undo the ops in the current span.
|
||||
// b. Calculate A'i = Ai + T(Ci-1, Ai) if i > 0, otherwise A'i = Ai.
|
||||
// NOTE: A'i can undo the ops in the current span and the previous spans, if it's applied on the id_span.last version.
|
||||
// 2. Calculate the event of checkout from id_span.last to [the next span's last id] or [the latest version], call it Bi.
|
||||
// 3. Transform event A'i based on Bi, call it Ci
|
||||
// 4. If span_i is the last span, apply Ci to the current state.
|
||||
|
||||
// -------------------------------------------------------
|
||||
// 0. Split the span into a series of continuous spans
|
||||
// -------------------------------------------------------
|
||||
|
||||
let mut last_ci: Option<DiffBatch> = None;
|
||||
for i in 0..spans.len() {
|
||||
debug_span!("Undo", ?i, "Undo span {:?}", &spans[i]).in_scope(|| {
|
||||
let (this_id_span, this_deps) = &spans[i];
|
||||
// ---------------------------------------
|
||||
// 1.a Calc event A_i
|
||||
// ---------------------------------------
|
||||
let mut event_a_i = debug_span!("1. Calc event A_i").in_scope(|| {
|
||||
// Checkout to the last id of the id_span
|
||||
calc_diff(&this_id_span.id_last().into(), this_deps)
|
||||
});
|
||||
|
||||
// ---------------------------------------
|
||||
// 2. Calc event B_i
|
||||
// ---------------------------------------
|
||||
let mut stack_diff_batch = None;
|
||||
let event_b_i = debug_span!("2. Calc event B_i").in_scope(|| {
|
||||
let next = if i + 1 < spans.len() {
|
||||
spans[i + 1].0.id_last().into()
|
||||
} else {
|
||||
match last_frontiers_or_last_bi {
|
||||
Either::Left(last_frontiers) => last_frontiers.clone(),
|
||||
Either::Right(right) => return right,
|
||||
}
|
||||
};
|
||||
stack_diff_batch = Some(calc_diff(&this_id_span.id_last().into(), &next));
|
||||
stack_diff_batch.as_ref().unwrap()
|
||||
});
|
||||
|
||||
// event_a_prime can undo the ops in the current span and the previous spans
|
||||
let mut event_a_prime = if let Some(mut last_ci) = last_ci.take() {
|
||||
// ------------------------------------------------------------------------------
|
||||
// 1.b Transform and apply Ci-1 based on Ai, call it A'i
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
last_ci.transform(&event_a_i, true);
|
||||
|
||||
event_a_i.compose(&last_ci);
|
||||
event_a_i
|
||||
} else {
|
||||
event_a_i
|
||||
};
|
||||
if i == spans.len() - 1 {
|
||||
on_last_event_a(&event_a_prime);
|
||||
}
|
||||
|
||||
// --------------------------------------------------
|
||||
// 3. Transform event A'_i based on B_i, call it C_i
|
||||
// --------------------------------------------------
|
||||
event_a_prime.transform(event_b_i, true);
|
||||
let c_i = event_a_prime;
|
||||
|
||||
last_ci = Some(c_i);
|
||||
});
|
||||
}
|
||||
|
||||
last_ci.unwrap()
|
||||
}
|
|
@ -568,13 +568,14 @@ pub mod wasm {
|
|||
)
|
||||
.unwrap();
|
||||
}
|
||||
TreeExternalDiff::Delete => {
|
||||
TreeExternalDiff::Delete { .. } => {
|
||||
js_sys::Reflect::set(&obj, &"action".into(), &"delete".into()).unwrap();
|
||||
}
|
||||
TreeExternalDiff::Move {
|
||||
parent,
|
||||
index,
|
||||
position,
|
||||
..
|
||||
} => {
|
||||
js_sys::Reflect::set(&obj, &"action".into(), &"move".into()).unwrap();
|
||||
js_sys::Reflect::set(&obj, &"parent".into(), &JsValue::from(*parent))
|
||||
|
|
|
@ -4,7 +4,7 @@ use serde_json::json;
|
|||
|
||||
#[test]
|
||||
fn auto_commit() {
|
||||
let mut doc_a = LoroDoc::default();
|
||||
let doc_a = LoroDoc::default();
|
||||
doc_a.set_peer_id(1).unwrap();
|
||||
doc_a.start_auto_commit();
|
||||
let text_a = doc_a.get_text("text");
|
||||
|
@ -13,7 +13,7 @@ fn auto_commit() {
|
|||
assert_eq!(&**text_a.get_value().as_string().unwrap(), "heo");
|
||||
let bytes = doc_a.export_from(&Default::default());
|
||||
|
||||
let mut doc_b = LoroDoc::default();
|
||||
let doc_b = LoroDoc::default();
|
||||
doc_b.start_auto_commit();
|
||||
doc_b.set_peer_id(2).unwrap();
|
||||
let text_b = doc_b.get_text("text");
|
||||
|
@ -26,7 +26,7 @@ fn auto_commit() {
|
|||
|
||||
#[test]
|
||||
fn auto_commit_list() {
|
||||
let mut doc_a = LoroDoc::default();
|
||||
let doc_a = LoroDoc::default();
|
||||
doc_a.start_auto_commit();
|
||||
let list_a = doc_a.get_list("list");
|
||||
list_a.insert(0, "hello").unwrap();
|
||||
|
@ -42,7 +42,7 @@ fn auto_commit_list() {
|
|||
|
||||
#[test]
|
||||
fn auto_commit_with_checkout() {
|
||||
let mut doc = LoroDoc::default();
|
||||
let doc = LoroDoc::default();
|
||||
doc.set_peer_id(1).unwrap();
|
||||
doc.start_auto_commit();
|
||||
let map = doc.get_map("a");
|
||||
|
|
|
@ -17,8 +17,10 @@ pub struct AwarenessWasm {
|
|||
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
/// Awareness states
|
||||
#[wasm_bindgen(typescript_type = "{[peer in PeerID]: unknown}")]
|
||||
pub type JsAwarenessStates;
|
||||
/// Awareness apply result
|
||||
#[wasm_bindgen(typescript_type = "{ updated: PeerID[], added: PeerID[] }")]
|
||||
pub type JsAwarenessApplyResult;
|
||||
}
|
||||
|
|
|
@ -289,6 +289,8 @@ pub(crate) fn handler_to_js_value(handler: Handler, doc: Option<Arc<LoroDoc>>) -
|
|||
Handler::List(l) => LoroList { handler: l, doc }.into(),
|
||||
Handler::Tree(t) => LoroTree { handler: t, doc }.into(),
|
||||
Handler::MovableList(m) => LoroMovableList { handler: m, doc }.into(),
|
||||
#[cfg(feature = "counter")]
|
||||
Handler::Counter(c) => unimplemented!(),
|
||||
Handler::Unknown(_) => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
//! Loro WASM bindings.
|
||||
#![allow(non_snake_case)]
|
||||
#![allow(clippy::empty_docs)]
|
||||
#![warn(missing_docs)]
|
||||
|
||||
use convert::resolved_diff_to_js;
|
||||
use js_sys::{Array, Object, Promise, Reflect, Uint8Array};
|
||||
use loro_internal::{
|
||||
|
@ -17,7 +19,7 @@ use loro_internal::{
|
|||
obs::SubID,
|
||||
version::Frontiers,
|
||||
ContainerType, DiffEvent, HandlerTrait, LoroDoc, LoroValue, MovableListHandler,
|
||||
VersionVector as InternalVersionVector,
|
||||
UndoManager as InnerUndoManager, VersionVector as InternalVersionVector,
|
||||
};
|
||||
use rle::HasLength;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
@ -153,6 +155,10 @@ extern "C" {
|
|||
pub type JsSide;
|
||||
#[wasm_bindgen(typescript_type = "{ update?: Cursor, offset: number, side: Side }")]
|
||||
pub type JsCursorQueryAns;
|
||||
#[wasm_bindgen(
|
||||
typescript_type = "{ mergeInterval?: number, maxUndoSteps?: number } | undefined"
|
||||
)]
|
||||
pub type JsUndoConfig;
|
||||
}
|
||||
|
||||
mod observer {
|
||||
|
@ -274,7 +280,7 @@ impl Loro {
|
|||
/// New document will have random peer id.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new() -> Self {
|
||||
let mut doc = LoroDoc::new();
|
||||
let doc = LoroDoc::new();
|
||||
doc.start_auto_commit();
|
||||
Self(Arc::new(doc))
|
||||
}
|
||||
|
@ -391,7 +397,7 @@ impl Loro {
|
|||
///
|
||||
#[wasm_bindgen(js_name = "fromSnapshot")]
|
||||
pub fn from_snapshot(snapshot: &[u8]) -> JsResult<Loro> {
|
||||
let mut doc = LoroDoc::from_snapshot(snapshot)?;
|
||||
let doc = LoroDoc::from_snapshot(snapshot)?;
|
||||
doc.start_auto_commit();
|
||||
Ok(Self(Arc::new(doc)))
|
||||
}
|
||||
|
@ -3273,6 +3279,84 @@ fn loro_value_to_js_value_or_container(
|
|||
}
|
||||
}
|
||||
|
||||
/// `UndoManager` is responsible for handling undo and redo operations.
|
||||
///
|
||||
/// By default, the maxUndoSteps is set to 100, mergeInterval is set to 1000 ms.
|
||||
///
|
||||
/// Each commit made by the current peer is recorded as an undo step in the `UndoManager`.
|
||||
/// Undo steps can be merged if they occur within a specified merge interval.
|
||||
///
|
||||
/// Note that undo operations are local and cannot revert changes made by other peers.
|
||||
/// To undo changes made by other peers, consider using the time travel feature.
|
||||
///
|
||||
/// Once the `peerId` is bound to the `UndoManager` in the document, it cannot be changed.
|
||||
/// Otherwise, the `UndoManager` may not function correctly.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug)]
|
||||
pub struct UndoManager {
|
||||
undo: InnerUndoManager,
|
||||
doc: Arc<LoroDoc>,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl UndoManager {
|
||||
/// Create a new undo manager. It will bind on the current PeerID.
|
||||
/// PeerID cannot be changed during the lifetime of the UndoManager.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(doc: &Loro, config: JsUndoConfig) -> Self {
|
||||
let max_undo_steps = Reflect::get(&config, &JsValue::from_str("maxUndoSteps"))
|
||||
.unwrap_or(JsValue::from_f64(100.0))
|
||||
.as_f64()
|
||||
.unwrap_or(100.0) as usize;
|
||||
let merge_interval = Reflect::get(&config, &JsValue::from_str("mergeInterval"))
|
||||
.unwrap_or(JsValue::from_f64(1000.0))
|
||||
.as_f64()
|
||||
.unwrap_or(1000.0) as i64;
|
||||
let mut undo = InnerUndoManager::new(&doc.0);
|
||||
undo.set_max_undo_steps(max_undo_steps);
|
||||
undo.set_merge_interval(merge_interval);
|
||||
UndoManager {
|
||||
undo,
|
||||
doc: doc.0.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Undo the last operation.
|
||||
pub fn undo(&mut self) -> JsResult<()> {
|
||||
self.undo.undo(&self.doc)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Redo the last undone operation.
|
||||
pub fn redo(&mut self) -> JsResult<()> {
|
||||
self.undo.redo(&self.doc)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Can undo the last operation.
|
||||
pub fn canUndo(&self) -> bool {
|
||||
self.undo.can_undo()
|
||||
}
|
||||
|
||||
/// Can redo the last operation.
|
||||
pub fn canRedo(&self) -> bool {
|
||||
self.undo.can_redo()
|
||||
}
|
||||
|
||||
/// The number of max undo steps.
|
||||
/// If the number of undo steps exceeds this number, the oldest undo step will be removed.
|
||||
pub fn setMaxUndoSteps(&mut self, steps: usize) {
|
||||
self.undo.set_max_undo_steps(steps);
|
||||
}
|
||||
|
||||
/// Set the merge interval (in ms).
|
||||
/// If the interval is set to 0, the undo steps will not be merged.
|
||||
/// Otherwise, the undo steps will be merged if the interval between the two steps is less than the given interval.
|
||||
pub fn setMergeInterval(&mut self, interval: f64) {
|
||||
self.undo.set_merge_interval(interval as i64);
|
||||
}
|
||||
}
|
||||
|
||||
/// [VersionVector](https://en.wikipedia.org/wiki/Version_vector)
|
||||
/// is a map from [PeerID] to [Counter]. Its a right-open interval.
|
||||
///
|
||||
|
|
|
@ -23,6 +23,7 @@ tracing = "0.1"
|
|||
|
||||
[dev-dependencies]
|
||||
serde_json = "1.0.87"
|
||||
anyhow = "1.0.83"
|
||||
ctor = "0.2"
|
||||
dev-utils = { path = "../dev-utils" }
|
||||
|
||||
|
|
|
@ -12,6 +12,8 @@ use loro_internal::cursor::Side;
|
|||
use loro_internal::encoding::ImportBlobMetadata;
|
||||
use loro_internal::handler::HandlerTrait;
|
||||
use loro_internal::handler::ValueOrHandler;
|
||||
use loro_internal::loro::CommitWhenDrop;
|
||||
use loro_internal::loro_common::IdSpan;
|
||||
use loro_internal::LoroDoc as InnerLoroDoc;
|
||||
use loro_internal::OpLog;
|
||||
|
||||
|
@ -41,6 +43,7 @@ pub use loro_internal::id::{PeerID, TreeID, ID};
|
|||
pub use loro_internal::obs::SubID;
|
||||
pub use loro_internal::oplog::FrontiersNotIncluded;
|
||||
pub use loro_internal::version::{Frontiers, VersionVector};
|
||||
pub use loro_internal::UndoManager as InnerUndoManager;
|
||||
pub use loro_internal::{loro_value, to_value};
|
||||
pub use loro_internal::{LoroError, LoroResult, LoroValue, ToJson};
|
||||
|
||||
|
@ -52,6 +55,7 @@ pub use counter::LoroCounter;
|
|||
/// `LoroDoc` is the entry for the whole document.
|
||||
/// When it's dropped, all the associated [`Handler`]s will be invalidated.
|
||||
#[derive(Debug)]
|
||||
#[repr(transparent)]
|
||||
pub struct LoroDoc {
|
||||
doc: InnerLoroDoc,
|
||||
}
|
||||
|
@ -65,7 +69,7 @@ impl Default for LoroDoc {
|
|||
impl LoroDoc {
|
||||
/// Create a new `LoroDoc` instance.
|
||||
pub fn new() -> Self {
|
||||
let mut doc = InnerLoroDoc::default();
|
||||
let doc = InnerLoroDoc::default();
|
||||
doc.start_auto_commit();
|
||||
|
||||
LoroDoc { doc }
|
||||
|
@ -240,7 +244,7 @@ impl LoroDoc {
|
|||
|
||||
#[cfg(feature = "counter")]
|
||||
/// Get a [LoroCounter] by container id.
|
||||
///
|
||||
///
|
||||
/// If the provided id is string, it will be converted into a root container id with the name of the string.
|
||||
pub fn get_counter<I: IntoContainerId>(&self, id: I) -> LoroCounter {
|
||||
LoroCounter {
|
||||
|
@ -471,6 +475,12 @@ impl LoroDoc {
|
|||
) -> Result<PosQueryResult, CannotFindRelativePosition> {
|
||||
self.doc.query_pos(cursor)
|
||||
}
|
||||
|
||||
/// Undo the operations between the given id_span. It can be used even in a collaborative environment.
|
||||
pub fn undo(&self, id_span: IdSpan) -> LoroResult<CommitWhenDrop> {
|
||||
self.doc
|
||||
.undo_internal(id_span, &mut Default::default(), None, &mut |_| {})
|
||||
}
|
||||
}
|
||||
|
||||
/// It's used to prevent the user from implementing the trait directly.
|
||||
|
@ -1810,3 +1820,40 @@ pub enum ValueOrContainer {
|
|||
/// A container.
|
||||
Container(Container),
|
||||
}
|
||||
|
||||
/// UndoManager can be used to undo and redo the changes made to the document with a certain peer.
|
||||
#[derive(Debug)]
|
||||
#[repr(transparent)]
|
||||
pub struct UndoManager(InnerUndoManager);
|
||||
|
||||
impl UndoManager {
|
||||
/// Create a new UndoManager.
|
||||
pub fn new(doc: &LoroDoc) -> Self {
|
||||
Self(InnerUndoManager::new(&doc.doc))
|
||||
}
|
||||
|
||||
/// Undo the last change made by the peer.
|
||||
pub fn undo(&mut self, doc: &LoroDoc) -> LoroResult<()> {
|
||||
self.0.undo(&doc.doc)
|
||||
}
|
||||
|
||||
/// Redo the last change made by the peer.
|
||||
pub fn redo(&mut self, doc: &LoroDoc) -> LoroResult<()> {
|
||||
self.0.redo(&doc.doc)
|
||||
}
|
||||
|
||||
/// Record a new checkpoint.
|
||||
pub fn record_new_checkpoint(&mut self, doc: &LoroDoc) -> LoroResult<()> {
|
||||
self.0.record_new_checkpoint(&doc.doc)
|
||||
}
|
||||
|
||||
/// Whether the undo manager can undo.
|
||||
pub fn can_undo(&self) -> bool {
|
||||
self.0.can_undo()
|
||||
}
|
||||
|
||||
/// Whether the undo manager can redo.
|
||||
pub fn can_redo(&self) -> bool {
|
||||
self.0.can_redo()
|
||||
}
|
||||
}
|
||||
|
|
1
crates/loro/tests/integration_test/mod.rs
Normal file
1
crates/loro/tests/integration_test/mod.rs
Normal file
|
@ -0,0 +1 @@
|
|||
mod undo_test;
|
1554
crates/loro/tests/integration_test/undo_test.rs
Normal file
1554
crates/loro/tests/integration_test/undo_test.rs
Normal file
File diff suppressed because it is too large
Load diff
|
@ -8,6 +8,8 @@ use loro_internal::{handler::TextDelta, id::ID, vv, LoroResult};
|
|||
use serde_json::json;
|
||||
use tracing::trace_span;
|
||||
|
||||
mod integration_test;
|
||||
|
||||
#[ctor::ctor]
|
||||
fn init() {
|
||||
dev_utils::setup_test_log();
|
||||
|
|
|
@ -82,9 +82,21 @@ export type MapDiff = {
|
|||
};
|
||||
|
||||
export type TreeDiffItem =
|
||||
| { target: TreeID; action: "create"; parent: TreeID | undefined; index: number; position: string }
|
||||
| {
|
||||
target: TreeID;
|
||||
action: "create";
|
||||
parent: TreeID | undefined;
|
||||
index: number;
|
||||
position: string;
|
||||
}
|
||||
| { target: TreeID; action: "delete" }
|
||||
| { target: TreeID; action: "move"; parent: TreeID | undefined; index: number; position: string };
|
||||
| {
|
||||
target: TreeID;
|
||||
action: "move";
|
||||
parent: TreeID | undefined;
|
||||
index: number;
|
||||
position: string;
|
||||
};
|
||||
|
||||
export type TreeDiff = {
|
||||
type: "tree";
|
||||
|
@ -153,11 +165,15 @@ export function isContainer(value: any): value is Container {
|
|||
*/
|
||||
export function getType<T>(
|
||||
value: T,
|
||||
): T extends LoroText ? "Text"
|
||||
: T extends LoroMap<any> ? "Map"
|
||||
: T extends LoroTree<any> ? "Tree"
|
||||
: T extends LoroList<any> ? "List"
|
||||
: "Json" {
|
||||
): T extends LoroText
|
||||
? "Text"
|
||||
: T extends LoroMap<any>
|
||||
? "Map"
|
||||
: T extends LoroTree<any>
|
||||
? "Tree"
|
||||
: T extends LoroList<any>
|
||||
? "List"
|
||||
: "Json" {
|
||||
if (isContainer(value)) {
|
||||
return value.kind() as unknown as any;
|
||||
}
|
||||
|
@ -187,7 +203,7 @@ declare module "loro-wasm" {
|
|||
* const map = doc.getMap("map");
|
||||
* ```
|
||||
*/
|
||||
getMap<Key extends (keyof T) | ContainerID>(
|
||||
getMap<Key extends keyof T | ContainerID>(
|
||||
name: Key,
|
||||
): T[Key] extends LoroMap ? T[Key] : LoroMap;
|
||||
/**
|
||||
|
@ -204,7 +220,7 @@ declare module "loro-wasm" {
|
|||
* const list = doc.getList("list");
|
||||
* ```
|
||||
*/
|
||||
getList<Key extends (keyof T) | ContainerID>(
|
||||
getList<Key extends keyof T | ContainerID>(
|
||||
name: Key,
|
||||
): T[Key] extends LoroList ? T[Key] : LoroList;
|
||||
/**
|
||||
|
@ -221,7 +237,7 @@ declare module "loro-wasm" {
|
|||
* const list = doc.getList("list");
|
||||
* ```
|
||||
*/
|
||||
getMovableList<Key extends (keyof T) | ContainerID>(
|
||||
getMovableList<Key extends keyof T | ContainerID>(
|
||||
name: Key,
|
||||
): T[Key] extends LoroMovableList ? T[Key] : LoroMovableList;
|
||||
/**
|
||||
|
@ -238,7 +254,7 @@ declare module "loro-wasm" {
|
|||
* const tree = doc.getTree("tree");
|
||||
* ```
|
||||
*/
|
||||
getTree<Key extends (keyof T) | ContainerID>(
|
||||
getTree<Key extends keyof T | ContainerID>(
|
||||
name: Key,
|
||||
): T[Key] extends LoroTree ? T[Key] : LoroTree;
|
||||
getText(key: string | ContainerID): LoroText;
|
||||
|
@ -531,7 +547,7 @@ declare module "loro-wasm" {
|
|||
T extends Record<string, unknown> = Record<string, unknown>,
|
||||
> {
|
||||
new (): LoroTree<T>;
|
||||
createNode(parent?: TreeID, index?: number ): LoroTreeNode<T>;
|
||||
createNode(parent?: TreeID, index?: number): LoroTreeNode<T>;
|
||||
move(target: TreeID, parent?: TreeID, index?: number): void;
|
||||
delete(target: TreeID): void;
|
||||
has(target: TreeID): boolean;
|
||||
|
@ -552,9 +568,7 @@ declare module "loro-wasm" {
|
|||
children(): Array<LoroTreeNode<T>>;
|
||||
}
|
||||
|
||||
interface AwarenessWasm<
|
||||
T extends Value = Value,
|
||||
> {
|
||||
interface AwarenessWasm<T extends Value = Value> {
|
||||
getState(peer: PeerID): T | undefined;
|
||||
getTimestamp(peer: PeerID): number | undefined;
|
||||
getAllStates(): Record<PeerID, T>;
|
||||
|
|
81
loro-js/tests/undo.test.ts
Normal file
81
loro-js/tests/undo.test.ts
Normal file
|
@ -0,0 +1,81 @@
|
|||
import { Loro, UndoManager } from "../src";
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
describe("undo", () => {
|
||||
test("basic text undo", () => {
|
||||
const doc = new Loro();
|
||||
doc.setPeerId(1);
|
||||
const undo = new UndoManager(doc, { maxUndoSteps: 100, mergeInterval: 0 });
|
||||
expect(undo.canRedo()).toBeFalsy();
|
||||
expect(undo.canUndo()).toBeFalsy();
|
||||
doc.getText("text").insert(0, "hello");
|
||||
doc.commit();
|
||||
doc.getText("text").insert(5, " world!");
|
||||
doc.commit();
|
||||
expect(undo.canRedo()).toBeFalsy();
|
||||
expect(undo.canUndo()).toBeTruthy();
|
||||
undo.undo();
|
||||
expect(undo.canRedo()).toBeTruthy();
|
||||
expect(undo.canUndo()).toBeTruthy();
|
||||
expect(doc.toJSON()).toStrictEqual({
|
||||
text: "hello",
|
||||
});
|
||||
undo.undo();
|
||||
expect(undo.canRedo()).toBeTruthy();
|
||||
expect(undo.canUndo()).toBeFalsy();
|
||||
expect(doc.toJSON()).toStrictEqual({
|
||||
text: "",
|
||||
});
|
||||
undo.redo();
|
||||
expect(undo.canRedo()).toBeTruthy();
|
||||
expect(undo.canUndo()).toBeTruthy();
|
||||
expect(doc.toJSON()).toStrictEqual({
|
||||
text: "hello",
|
||||
});
|
||||
undo.redo();
|
||||
expect(undo.canRedo()).toBeFalsy();
|
||||
expect(undo.canUndo()).toBeTruthy();
|
||||
expect(doc.toJSON()).toStrictEqual({
|
||||
text: "hello world!",
|
||||
});
|
||||
});
|
||||
|
||||
test("merge", async () => {
|
||||
const doc = new Loro();
|
||||
const undo = new UndoManager(doc, { maxUndoSteps: 100, mergeInterval: 5 });
|
||||
for (let i = 0; i < 10; i++) {
|
||||
doc.getText("text").insert(i, i.toString());
|
||||
doc.commit();
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
for (let i = 0; i < 10; i++) {
|
||||
doc.getText("text").insert(i, i.toString());
|
||||
doc.commit();
|
||||
}
|
||||
expect(doc.toJSON()).toStrictEqual({
|
||||
text: "01234567890123456789",
|
||||
});
|
||||
undo.undo();
|
||||
expect(doc.toJSON()).toStrictEqual({
|
||||
text: "0123456789",
|
||||
});
|
||||
undo.undo();
|
||||
expect(doc.toJSON()).toStrictEqual({
|
||||
text: "",
|
||||
});
|
||||
});
|
||||
|
||||
test("max undo steps", () => {
|
||||
const doc = new Loro();
|
||||
const undo = new UndoManager(doc, { maxUndoSteps: 100, mergeInterval: 0 });
|
||||
for (let i = 0; i < 200; i++) {
|
||||
doc.getText("text").insert(0, "0");
|
||||
doc.commit();
|
||||
}
|
||||
expect(doc.getText("text").length).toBe(200);
|
||||
while (undo.canUndo()) {
|
||||
undo.undo();
|
||||
}
|
||||
expect(doc.getText("text").length).toBe(100);
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue