https://github.com/loro-dev/loro/pull/361

---------

Co-authored-by: Leon Zhao <leeeon233@gmail.com>
This commit is contained in:
Zixuan Chen 2024-05-21 06:14:49 +08:00 committed by GitHub
parent 26753f0d4d
commit 321e0e72a4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
53 changed files with 4757 additions and 170 deletions

37
Cargo.lock generated
View file

@ -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
View file

@ -0,0 +1,4 @@
target
corpus
artifacts
coverage

View file

@ -0,0 +1,6 @@
{
"rust-analyzer.runnableEnv": {
"RUST_BACKTRACE": "full",
"DEBUG": "*"
},
}

1002
crates/delta/fuzz/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

View 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

View 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));

View 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);
}
}

View file

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

View file

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

View file

@ -1,5 +1,3 @@
use super::*;
struct DeltaReplace<'a, V, Attr> {

View file

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

View file

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

View file

@ -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());

View file

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

View file

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

View file

@ -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(),
],
}
}

View file

@ -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(|| {

View file

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

View file

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

View file

@ -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());

View file

@ -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
View 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,
}),
},
],
);
}

View file

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

View file

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

View file

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

View file

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

View file

@ -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());
}
}

View file

@ -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`].

View file

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

View file

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

View file

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

View file

@ -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};

View file

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

View file

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

View file

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

View file

@ -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));
});
}
}
}

View file

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

View file

@ -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();

View file

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

View file

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

View 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()
}

View file

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

View file

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

View file

@ -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;
}

View file

@ -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!(),
}
}

View file

@ -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.
///

View file

@ -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" }

View file

@ -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()
}
}

View file

@ -0,0 +1 @@
mod undo_test;

File diff suppressed because it is too large Load diff

View file

@ -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();

View file

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

View 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);
});
});