perf: use a priority-queue-based search for updating text (#544)

* perf: use a priority-queue-based search for updating text

It tends to produce diffs with more continuous edits,
which is more efficient when we need to apply them to CRDTs

* fix: use better text diff calc

* refactor: add text update options struct

* chore: update text.update comments

* chore: fix warnings

* fix: rm a dumb optimization
This commit is contained in:
Zixuan Chen 2024-11-09 16:35:15 +08:00 committed by GitHub
parent 661610165b
commit 4f0d499d4b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 1046 additions and 105 deletions

View file

@ -8,6 +8,7 @@ use bench_utils::TextAction;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use dev_utils::ByteSize;
use loro::LoroDoc;
use rand::Rng;
fn bench_text(c: &mut Criterion) {
use bench_utils::TextAction;
@ -125,6 +126,77 @@ fn bench_text(c: &mut Criterion) {
});
}
fn bench_update_text(c: &mut Criterion) {
let mut g = c.benchmark_group("update_text");
g.bench_function("Update 1024x1024 total different text", |b| {
b.iter_batched(
|| {
let from = "a".repeat(1024);
let to = "b".repeat(1024);
(from, to)
},
|(from, to)| {
let doc = LoroDoc::new();
let text = doc.get_text("text");
text.update(&from, Default::default()).unwrap();
text.update(&to, Default::default()).unwrap();
assert_eq!(&text.to_string(), &to);
},
criterion::BatchSize::SmallInput,
);
});
g.bench_function("Update 1024x1024 random diff", |b| {
b.iter_batched(
|| {
fn rand_str(len: usize, seed: u64) -> String {
let mut rng: rand::rngs::StdRng = rand::SeedableRng::seed_from_u64(seed);
let mut s = String::new();
for _ in 0..len {
s.push(rng.gen_range(b'a'..=b'z') as char);
}
s
}
let from = rand_str(1024, 42);
let mut to = from.clone();
to.insert_str(0, &rand_str(100, 43));
to.replace_range(100..200, &rand_str(100, 44)); // Make some differences
to.replace_range(500..504, &rand_str(4, 45));
to.replace_range(600..700, &rand_str(5, 46));
(from, to)
},
|(from, to)| {
let doc = LoroDoc::new();
let text = doc.get_text("text");
text.update(&from, Default::default()).unwrap();
text.update(&to, Default::default()).unwrap();
assert_eq!(&text.to_string(), &to);
},
criterion::BatchSize::SmallInput,
);
});
g.bench_function("Update 1024x1024 text with 16 inserted chars", |b| {
b.iter_batched(
|| {
let from = "a".repeat(1024);
let mut to = from.clone();
to.insert_str(504, "b".repeat(16).as_str());
(from, to)
},
|(from, to)| {
let doc = LoroDoc::new();
let text = doc.get_text("text");
text.update(&from, Default::default()).unwrap();
text.update(&to, Default::default()).unwrap();
assert_eq!(&text.to_string(), &to);
},
criterion::BatchSize::SmallInput,
);
});
}
fn apply_text_actions(actions: &[bench_utils::TextAction], n: usize) -> LoroDoc {
let loro = LoroDoc::new();
let text = loro.get_text("text");
@ -137,5 +209,5 @@ fn apply_text_actions(actions: &[bench_utils::TextAction], n: usize) -> LoroDoc
loro
}
criterion_group!(benches, bench_text);
criterion_group!(benches, bench_text, bench_update_text);
criterion_main!(benches);

View file

@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
version = 4
[[package]]
name = "ahash"
@ -275,13 +275,14 @@ dependencies = [
"arbitrary",
"bytes",
"ensure-cov",
"enum-as-inner 0.5.1",
"enum-as-inner 0.6.0",
"enum_dispatch",
"fxhash",
"itertools 0.12.1",
"loro 0.16.12",
"loro 0.16.2 (git+https://github.com/loro-dev/loro.git?tag=loro-crdt@0.16.7)",
"loro 0.16.2 (git+https://github.com/loro-dev/loro.git?tag=loro-crdt%400.16.7)",
"loro 0.16.2 (git+https://github.com/loro-dev/loro.git?rev=90470658435ec4c62b5af59ebb82fe9e1f5aa761)",
"loro 1.0.0-beta.5",
"num_cpus",
"pretty_assertions",
"rand",
@ -297,7 +298,7 @@ version = "0.0.0"
dependencies = [
"fuzz",
"libfuzzer-sys",
"loro 0.16.12",
"loro 1.0.0-beta.5",
]
[[package]]
@ -506,13 +507,13 @@ dependencies = [
[[package]]
name = "loro"
version = "0.16.2"
source = "git+https://github.com/loro-dev/loro.git?tag=loro-crdt@0.16.7#d2b0520f8633f96146a49ec205bd5e7056880f1a"
source = "git+https://github.com/loro-dev/loro.git?tag=loro-crdt%400.16.7#d2b0520f8633f96146a49ec205bd5e7056880f1a"
dependencies = [
"either",
"enum-as-inner 0.6.0",
"generic-btree",
"loro-delta 0.16.2 (git+https://github.com/loro-dev/loro.git?tag=loro-crdt@0.16.7)",
"loro-internal 0.16.2 (git+https://github.com/loro-dev/loro.git?tag=loro-crdt@0.16.7)",
"loro-delta 0.16.2 (git+https://github.com/loro-dev/loro.git?tag=loro-crdt%400.16.7)",
"loro-internal 0.16.2 (git+https://github.com/loro-dev/loro.git?tag=loro-crdt%400.16.7)",
"tracing",
]
@ -532,26 +533,39 @@ dependencies = [
[[package]]
name = "loro"
version = "0.16.12"
source = "git+https://github.com/loro-dev/loro.git?tag=loro-crdt%401.0.0-alpha.4#9bfe97bce4912c6dc8439817497d18423a0e8cb7"
dependencies = [
"either",
"enum-as-inner 0.6.0",
"generic-btree",
"loro-common 0.16.12",
"loro-delta 0.16.12",
"loro-internal 0.16.12",
"loro-kv-store",
"loro-kv-store 0.16.2",
"tracing",
]
[[package]]
name = "loro"
version = "1.0.0-beta.5"
dependencies = [
"enum-as-inner 0.6.0",
"generic-btree",
"loro-common 1.0.0-beta.5",
"loro-delta 1.0.0-beta.5",
"loro-internal 1.0.0-beta.5",
"loro-kv-store 1.0.0-beta.5",
"tracing",
]
[[package]]
name = "loro-common"
version = "0.16.2"
source = "git+https://github.com/loro-dev/loro.git?tag=loro-crdt@0.16.7#d2b0520f8633f96146a49ec205bd5e7056880f1a"
source = "git+https://github.com/loro-dev/loro.git?tag=loro-crdt%400.16.7#d2b0520f8633f96146a49ec205bd5e7056880f1a"
dependencies = [
"arbitrary",
"enum-as-inner 0.6.0",
"fxhash",
"loro-rle 0.16.2 (git+https://github.com/loro-dev/loro.git?tag=loro-crdt@0.16.7)",
"loro-rle 0.16.2 (git+https://github.com/loro-dev/loro.git?tag=loro-crdt%400.16.7)",
"nonmax",
"serde",
"serde_columnar",
@ -578,6 +592,7 @@ dependencies = [
[[package]]
name = "loro-common"
version = "0.16.12"
source = "git+https://github.com/loro-dev/loro.git?tag=loro-crdt%401.0.0-alpha.4#9bfe97bce4912c6dc8439817497d18423a0e8cb7"
dependencies = [
"arbitrary",
"enum-as-inner 0.6.0",
@ -592,10 +607,27 @@ dependencies = [
"thiserror",
]
[[package]]
name = "loro-common"
version = "1.0.0-beta.5"
dependencies = [
"arbitrary",
"enum-as-inner 0.6.0",
"fxhash",
"leb128",
"loro-rle 1.0.0-beta.5",
"nonmax",
"serde",
"serde_columnar",
"serde_json",
"string_cache",
"thiserror",
]
[[package]]
name = "loro-delta"
version = "0.16.2"
source = "git+https://github.com/loro-dev/loro.git?tag=loro-crdt@0.16.7#d2b0520f8633f96146a49ec205bd5e7056880f1a"
source = "git+https://github.com/loro-dev/loro.git?tag=loro-crdt%400.16.7#d2b0520f8633f96146a49ec205bd5e7056880f1a"
dependencies = [
"arrayvec",
"enum-as-inner 0.5.1",
@ -619,6 +651,7 @@ dependencies = [
[[package]]
name = "loro-delta"
version = "0.16.12"
source = "git+https://github.com/loro-dev/loro.git?tag=loro-crdt%401.0.0-alpha.4#9bfe97bce4912c6dc8439817497d18423a0e8cb7"
dependencies = [
"arrayvec",
"enum-as-inner 0.5.1",
@ -627,10 +660,20 @@ dependencies = [
"tracing",
]
[[package]]
name = "loro-delta"
version = "1.0.0-beta.5"
dependencies = [
"arrayvec",
"enum-as-inner 0.5.1",
"generic-btree",
"heapless 0.8.0",
]
[[package]]
name = "loro-internal"
version = "0.16.2"
source = "git+https://github.com/loro-dev/loro.git?tag=loro-crdt@0.16.7#d2b0520f8633f96146a49ec205bd5e7056880f1a"
source = "git+https://github.com/loro-dev/loro.git?tag=loro-crdt%400.16.7#d2b0520f8633f96146a49ec205bd5e7056880f1a"
dependencies = [
"append-only-bytes",
"arref",
@ -643,10 +686,10 @@ dependencies = [
"im",
"itertools 0.12.1",
"leb128",
"loro-common 0.16.2 (git+https://github.com/loro-dev/loro.git?tag=loro-crdt@0.16.7)",
"loro-delta 0.16.2 (git+https://github.com/loro-dev/loro.git?tag=loro-crdt@0.16.7)",
"loro-rle 0.16.2 (git+https://github.com/loro-dev/loro.git?tag=loro-crdt@0.16.7)",
"loro_fractional_index 0.16.2 (git+https://github.com/loro-dev/loro.git?tag=loro-crdt@0.16.7)",
"loro-common 0.16.2 (git+https://github.com/loro-dev/loro.git?tag=loro-crdt%400.16.7)",
"loro-delta 0.16.2 (git+https://github.com/loro-dev/loro.git?tag=loro-crdt%400.16.7)",
"loro-rle 0.16.2 (git+https://github.com/loro-dev/loro.git?tag=loro-crdt%400.16.7)",
"loro_fractional_index 0.16.2 (git+https://github.com/loro-dev/loro.git?tag=loro-crdt%400.16.7)",
"md5",
"num",
"num-derive",
@ -700,13 +743,14 @@ dependencies = [
[[package]]
name = "loro-internal"
version = "0.16.12"
source = "git+https://github.com/loro-dev/loro.git?tag=loro-crdt%401.0.0-alpha.4#9bfe97bce4912c6dc8439817497d18423a0e8cb7"
dependencies = [
"append-only-bytes",
"arref",
"bytes",
"either",
"ensure-cov",
"enum-as-inner 0.5.1",
"enum-as-inner 0.6.0",
"enum_dispatch",
"fxhash",
"generic-btree",
@ -716,7 +760,7 @@ dependencies = [
"leb128",
"loro-common 0.16.12",
"loro-delta 0.16.12",
"loro-kv-store",
"loro-kv-store 0.16.2",
"loro-rle 0.16.12",
"loro_fractional_index 0.16.12",
"md5",
@ -737,9 +781,49 @@ dependencies = [
"xxhash-rust",
]
[[package]]
name = "loro-internal"
version = "1.0.0-beta.5"
dependencies = [
"append-only-bytes",
"arref",
"bytes",
"either",
"ensure-cov",
"enum-as-inner 0.6.0",
"enum_dispatch",
"fxhash",
"generic-btree",
"getrandom",
"im",
"itertools 0.12.1",
"leb128",
"loro-common 1.0.0-beta.5",
"loro-delta 1.0.0-beta.5",
"loro-kv-store 1.0.0-beta.5",
"loro-rle 1.0.0-beta.5",
"loro_fractional_index 1.0.0-beta.5",
"md5",
"nonmax",
"num",
"num-traits",
"once_cell",
"postcard",
"pretty_assertions",
"rand",
"serde",
"serde_columnar",
"serde_json",
"smallvec",
"thiserror",
"tracing",
"xxhash-rust",
]
[[package]]
name = "loro-kv-store"
version = "0.16.2"
source = "git+https://github.com/loro-dev/loro.git?tag=loro-crdt%401.0.0-alpha.4#9bfe97bce4912c6dc8439817497d18423a0e8cb7"
dependencies = [
"bytes",
"ensure-cov",
@ -748,13 +832,29 @@ dependencies = [
"lz4_flex",
"once_cell",
"quick_cache",
"tracing",
"xxhash-rust",
]
[[package]]
name = "loro-kv-store"
version = "1.0.0-beta.5"
dependencies = [
"bytes",
"ensure-cov",
"fxhash",
"loro-common 1.0.0-beta.5",
"lz4_flex",
"once_cell",
"quick_cache",
"tracing",
"xxhash-rust",
]
[[package]]
name = "loro-rle"
version = "0.16.2"
source = "git+https://github.com/loro-dev/loro.git?tag=loro-crdt@0.16.7#d2b0520f8633f96146a49ec205bd5e7056880f1a"
source = "git+https://github.com/loro-dev/loro.git?tag=loro-crdt%400.16.7#d2b0520f8633f96146a49ec205bd5e7056880f1a"
dependencies = [
"append-only-bytes",
"arref",
@ -780,6 +880,7 @@ dependencies = [
[[package]]
name = "loro-rle"
version = "0.16.12"
source = "git+https://github.com/loro-dev/loro.git?tag=loro-crdt%401.0.0-alpha.4#9bfe97bce4912c6dc8439817497d18423a0e8cb7"
dependencies = [
"append-only-bytes",
"arref",
@ -789,6 +890,15 @@ dependencies = [
"smallvec",
]
[[package]]
name = "loro-rle"
version = "1.0.0-beta.5"
dependencies = [
"append-only-bytes",
"num",
"smallvec",
]
[[package]]
name = "loro-thunderdome"
version = "0.6.2"
@ -798,7 +908,7 @@ checksum = "3f3d053a135388e6b1df14e8af1212af5064746e9b87a06a345a7a779ee9695a"
[[package]]
name = "loro_fractional_index"
version = "0.16.2"
source = "git+https://github.com/loro-dev/loro.git?tag=loro-crdt@0.16.7#d2b0520f8633f96146a49ec205bd5e7056880f1a"
source = "git+https://github.com/loro-dev/loro.git?tag=loro-crdt%400.16.7#d2b0520f8633f96146a49ec205bd5e7056880f1a"
dependencies = [
"imbl",
"rand",
@ -820,6 +930,7 @@ dependencies = [
[[package]]
name = "loro_fractional_index"
version = "0.16.12"
source = "git+https://github.com/loro-dev/loro.git?tag=loro-crdt%401.0.0-alpha.4#9bfe97bce4912c6dc8439817497d18423a0e8cb7"
dependencies = [
"imbl",
"once_cell",
@ -828,6 +939,15 @@ dependencies = [
"smallvec",
]
[[package]]
name = "loro_fractional_index"
version = "1.0.0-beta.5"
dependencies = [
"once_cell",
"rand",
"serde",
]
[[package]]
name = "lz4_flex"
version = "0.11.3"

View file

@ -7,9 +7,9 @@ fuzz_target!(|data: [&str; 3]| {
let (old, new, new1) = (data[0], data[1], data[2]);
let doc = LoroDoc::new();
let text = doc.get_text("text");
text.update(old);
text.update(new);
text.update(old, Default::default()).unwrap();
text.update(new, Default::default()).unwrap();
assert_eq!(&text.to_string(), new);
text.update_by_line(new1);
text.update_by_line(new1, Default::default()).unwrap();
assert_eq!(&text.to_string(), new1);
});

View file

@ -4,7 +4,7 @@ use loro::LoroDoc;
fn update_text() {
let doc = LoroDoc::new();
let text = doc.get_text("text");
text.update("ϼCCC");
text.update("2");
text.update("ϼCCC", Default::default()).unwrap();
text.update("2", Default::default()).unwrap();
assert_eq!(&text.to_string(), "2");
}

View file

@ -1,6 +1,6 @@
use std::{fmt::Display, sync::Arc};
use loro::{cursor::Side, LoroResult, TextDelta};
use loro::{cursor::Side, LoroResult, TextDelta, UpdateOptions, UpdateTimeoutError};
use crate::{ContainerID, LoroValue, LoroValueLike};
@ -101,8 +101,8 @@ impl LoroText {
}
/// Update the current text based on the provided text.
pub fn update(&self, text: &str) {
self.text.update(text);
pub fn update(&self, text: &str, options: UpdateOptions) -> Result<(), UpdateTimeoutError> {
self.text.update(text, options)
}
/// Apply a [delta](https://quilljs.com/docs/delta/) to the text container.

View file

@ -2,7 +2,7 @@ use fxhash::FxHashMap;
use loro_common::{LoroValue, PeerID};
use serde::{Deserialize, Serialize};
use crate::change::get_sys_timestamp;
use crate::change::{get_sys_timestamp, Timestamp};
/// `Awareness` is a structure that tracks the ephemeral state of peers.
///
@ -43,7 +43,7 @@ impl Awareness {
pub fn encode(&self, peers: &[PeerID]) -> Vec<u8> {
let mut peers_info = Vec::new();
let now = get_sys_timestamp();
let now = get_sys_timestamp() as Timestamp;
for peer in peers {
if let Some(peer_info) = self.peers.get(peer) {
if now - peer_info.timestamp > self.timeout {
@ -64,7 +64,7 @@ impl Awareness {
pub fn encode_all(&self) -> Vec<u8> {
let mut peers_info = Vec::new();
let now = get_sys_timestamp();
let now = get_sys_timestamp() as Timestamp;
for (peer, peer_info) in self.peers.iter() {
if now - peer_info.timestamp > self.timeout {
continue;
@ -86,7 +86,7 @@ impl Awareness {
let peers_info: Vec<EncodedPeerInfo> = postcard::from_bytes(encoded_peers_info).unwrap();
let mut changed_peers = Vec::new();
let mut added_peers = Vec::new();
let now = get_sys_timestamp();
let now = get_sys_timestamp() as Timestamp;
for peer_info in peers_info {
match self.peers.get(&peer_info.peer) {
Some(x) if x.counter >= peer_info.counter || peer_info.peer == self.peer => {
@ -126,7 +126,7 @@ impl Awareness {
peer.state = value;
peer.counter += 1;
peer.timestamp = get_sys_timestamp();
peer.timestamp = get_sys_timestamp() as Timestamp;
}
pub fn get_local_state(&self) -> Option<LoroValue> {
@ -134,7 +134,7 @@ impl Awareness {
}
pub fn remove_outdated(&mut self) -> Vec<PeerID> {
let now = get_sys_timestamp();
let now = get_sys_timestamp() as Timestamp;
let mut removed = Vec::new();
self.peers.retain(|id, v| {
if now - v.timestamp > self.timeout {

View file

@ -263,7 +263,7 @@ impl Change {
/// [Unix time](https://en.wikipedia.org/wiki/Unix_time)
/// It is the number of milliseconds that have elapsed since 00:00:00 UTC on 1 January 1970.
#[cfg(not(all(feature = "wasm", target_arch = "wasm32")))]
pub(crate) fn get_sys_timestamp() -> Timestamp {
pub(crate) fn get_sys_timestamp() -> f64 {
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now()
.duration_since(UNIX_EPOCH)
@ -275,7 +275,7 @@ pub(crate) fn get_sys_timestamp() -> Timestamp {
/// [Unix time](https://en.wikipedia.org/wiki/Unix_time)
/// It is the number of seconds that have elapsed since 00:00:00 UTC on 1 January 1970.
#[cfg(all(feature = "wasm", target_arch = "wasm32"))]
pub fn get_sys_timestamp() -> Timestamp {
pub fn get_sys_timestamp() -> f64 {
use wasm_bindgen::prelude::wasm_bindgen;
#[wasm_bindgen]
extern "C" {
@ -285,7 +285,7 @@ pub fn get_sys_timestamp() -> Timestamp {
pub fn now() -> f64;
}
now() as Timestamp
now()
}
#[cfg(test)]

View file

@ -14,9 +14,38 @@
//!
//! The implementation of this algorithm is based on the implementation by
//! Brandon Williams.
use crate::change::get_sys_timestamp;
use fxhash::FxHashMap;
use std::cmp::Ordering;
use std::collections::BinaryHeap;
use std::iter::zip;
use std::ops::{Index, IndexMut};
/// Options for controlling the text update behavior.
///
/// - `timeout_ms`: Optional timeout in milliseconds for the diff computation
/// - `use_refined_diff`: Whether to use a more refined but slower diff algorithm. Defaults to true.
#[derive(Clone, Debug)]
pub struct UpdateOptions {
pub timeout_ms: Option<f64>,
pub use_refined_diff: bool,
}
impl Default for UpdateOptions {
fn default() -> Self {
Self {
timeout_ms: None,
use_refined_diff: true,
}
}
}
#[derive(thiserror::Error, Debug, PartialEq)]
pub enum UpdateTimeoutError {
#[error("Timeout")]
Timeout,
}
/// Utility function to check if a range is empty that works on older rust versions
#[inline(always)]
fn is_empty_range(start: usize, end: usize) -> bool {
@ -79,14 +108,33 @@ impl<D: DiffHandler> OperateProxy<D> {
pub fn insert(&mut self, old_index: usize, new_index: usize, new_len: usize) {
self.handler.insert(old_index, new_index, new_len);
}
#[allow(unused)]
fn unwrap(self) -> D {
self.handler
}
}
pub(crate) fn myers_diff<D: DiffHandler>(proxy: &mut OperateProxy<D>, old: &[u32], new: &[u32]) {
pub(crate) fn diff<D: DiffHandler>(
proxy: &mut OperateProxy<D>,
options: UpdateOptions,
old: &[u32],
new: &[u32],
) -> Result<(), UpdateTimeoutError> {
let max_d = (old.len() + new.len() + 1) / 2 + 1;
let mut vb = OffsetVec::new(max_d);
let mut vf = OffsetVec::new(max_d);
let start_time = if options.timeout_ms.is_some() {
get_sys_timestamp()
} else {
0.
};
conquer(
proxy,
options.use_refined_diff,
options.timeout_ms,
start_time,
old,
0,
old.len(),
@ -95,7 +143,7 @@ pub(crate) fn myers_diff<D: DiffHandler>(proxy: &mut OperateProxy<D>, old: &[u32
new.len(),
&mut vf,
&mut vb,
);
)
}
struct OffsetVec(isize, Vec<usize>);
@ -123,8 +171,17 @@ impl IndexMut<isize> for OffsetVec {
}
}
struct MiddleSnakeResult {
#[allow(unused)]
d: usize,
x_start: usize,
y_start: usize,
}
#[allow(clippy::too_many_arguments)]
fn find_middle_snake(
timeout_ms: Option<f64>,
start_time: f64,
old: &[u32],
old_start: usize,
old_end: usize,
@ -133,7 +190,7 @@ fn find_middle_snake(
new_end: usize,
vf: &mut OffsetVec,
vb: &mut OffsetVec,
) -> Option<(usize, usize)> {
) -> Result<Option<MiddleSnakeResult>, UpdateTimeoutError> {
let n = old_end - old_start;
let m = new_end - new_start;
let delta = n as isize - m as isize;
@ -160,9 +217,20 @@ fn find_middle_snake(
}
vf[k] = x;
if odd && (k - delta).abs() <= (d - 1) && vf[k] + vb[delta - k] >= n {
return Some((x0 + old_start, y0 + new_start));
return Ok(Some(MiddleSnakeResult {
d: d as usize,
x_start: x0 + old_start,
y_start: y0 + new_start,
}));
}
if let Some(timeout_ms) = timeout_ms {
if get_sys_timestamp() - start_time > timeout_ms {
return Err(UpdateTimeoutError::Timeout);
}
}
}
for k in (-d..=d).rev().step_by(2) {
let mut x = if k == -d || (k != d && vb[k - 1] < vb[k + 1]) {
vb[k + 1]
@ -180,16 +248,30 @@ fn find_middle_snake(
}
vb[k] = x;
if !odd && (k - delta).abs() <= d && vb[k] + vf[delta - k] >= n {
return Some((n - x + old_start, m - y + new_start));
return Ok(Some(MiddleSnakeResult {
d: d as usize,
x_start: n - x + old_start,
y_start: m - y + new_start,
}));
}
if let Some(timeout_ms) = timeout_ms {
if get_sys_timestamp() - start_time > timeout_ms {
return Err(UpdateTimeoutError::Timeout);
}
}
}
None
}
Ok(None)
}
#[allow(clippy::too_many_arguments)]
fn conquer<D: DiffHandler>(
proxy: &mut OperateProxy<D>,
should_use_dj: bool,
timeout_ms: Option<f64>,
start_time: f64,
old: &[u32],
mut old_start: usize,
mut old_end: usize,
@ -198,7 +280,7 @@ fn conquer<D: DiffHandler>(
mut new_end: usize,
vf: &mut OffsetVec,
vb: &mut OffsetVec,
) {
) -> Result<(), UpdateTimeoutError> {
let common_prefix_len = common_prefix(&old[old_start..old_end], &new[new_start..new_end]);
if common_prefix_len > 0 {
old_start += common_prefix_len;
@ -208,21 +290,566 @@ fn conquer<D: DiffHandler>(
let common_suffix_len = common_suffix_len(&old[old_start..old_end], &new[new_start..new_end]);
old_end -= common_suffix_len;
new_end -= common_suffix_len;
if is_not_empty_range(old_start, old_end) || is_not_empty_range(new_start, new_end) {
let len_old = old_end - old_start;
let len_new = new_end - new_start;
if should_use_dj && (len_old * len_new < 128 * 128) {
let ok = dj_diff(
proxy,
&old[old_start..old_end],
&new[new_start..new_end],
old_start,
new_start,
10_000,
);
if ok {
return Ok(());
}
}
if is_empty_range(new_start, new_end) {
proxy.delete(old_start, old_end - old_start);
} else if is_empty_range(old_start, old_end) {
proxy.insert(old_start, new_start, new_end - new_start);
} else if let Some((x_start, y_start)) =
find_middle_snake(old, old_start, old_end, new, new_start, new_end, vf, vb)
{
} else if let Some(MiddleSnakeResult {
d: _,
x_start,
y_start,
}) = find_middle_snake(
timeout_ms, start_time, old, old_start, old_end, new, new_start, new_end, vf, vb,
)? {
conquer(
proxy, old, old_start, x_start, new, new_start, y_start, vf, vb,
);
conquer(proxy, old, x_start, old_end, new, y_start, new_end, vf, vb);
proxy,
should_use_dj,
timeout_ms,
start_time,
old,
old_start,
x_start,
new,
new_start,
y_start,
vf,
vb,
)?;
conquer(
proxy,
should_use_dj,
timeout_ms,
start_time,
old,
x_start,
old_end,
new,
y_start,
new_end,
vf,
vb,
)?;
} else {
proxy.delete(old_start, old_end - old_start);
proxy.insert(old_start, new_start, new_end - new_start);
}
}
Ok(())
}
/// Return false if this method gives up early
#[must_use]
pub(crate) fn dj_diff<D: DiffHandler>(
proxy: &mut OperateProxy<D>,
old: &[u32],
new: &[u32],
old_offset: usize,
new_offset: usize,
max_try_count: usize,
) -> bool {
let common_prefix_len = common_prefix(old, new);
let common_suffix_len = common_suffix_len(&old[common_prefix_len..], &new[common_prefix_len..]);
let old = &old[common_prefix_len..old.len() - common_suffix_len];
let new = &new[common_prefix_len..new.len() - common_suffix_len];
assert!(old.len() <= u16::MAX as usize);
assert!(new.len() <= u16::MAX as usize);
if old.is_empty() {
if new.is_empty() {
return true;
}
proxy.insert(
old_offset + common_prefix_len,
new_offset + common_prefix_len,
new.len(),
);
return true;
}
if new.is_empty() {
proxy.delete(old_offset + common_prefix_len, old.len());
return true;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct Point {
x: u16,
y: u16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Direction {
Up,
Left,
UpLeft,
}
struct QueueItem {
point: Point,
cost: u32,
from: Direction,
}
impl PartialEq for QueueItem {
fn eq(&self, other: &Self) -> bool {
self.cost == other.cost
}
}
impl PartialOrd for QueueItem {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for QueueItem {
fn cmp(&self, other: &Self) -> Ordering {
other.cost.cmp(&self.cost)
}
}
impl Eq for QueueItem {}
let mut visited: FxHashMap<Point, Direction> = FxHashMap::default();
let mut q: BinaryHeap<QueueItem> = BinaryHeap::new();
q.push(QueueItem {
point: Point { x: 0, y: 0 },
cost: 0,
from: Direction::UpLeft,
});
while let Some(QueueItem { point, cost, from }) = q.pop() {
if visited.contains_key(&point) {
continue;
}
visited.insert(point, from);
if point.x == old.len() as u16 && point.y == new.len() as u16 {
break;
}
if visited.len() + q.len() > max_try_count {
// println!("give up on: visited len: {}", visited.len());
// println!("queue len: {}", q.len());
return false;
}
if point.x < old.len() as u16 {
let next_point = Point {
x: point.x + 1,
y: point.y,
};
if !visited.contains_key(&next_point) {
let mut next_cost = cost + 1;
if from != Direction::Left {
next_cost += 8;
}
q.push(QueueItem {
point: next_point,
cost: next_cost,
from: Direction::Left,
});
}
}
if point.y < new.len() as u16 {
let next_point = Point {
x: point.x,
y: point.y + 1,
};
if !visited.contains_key(&next_point) {
let direction = Direction::Up;
let mut next_cost = cost + 1;
if from != direction {
next_cost += 8;
}
q.push(QueueItem {
point: next_point,
cost: next_cost,
from: Direction::Up,
});
}
}
if point.x < old.len() as u16
&& point.y < new.len() as u16
&& old[point.x as usize] == new[point.y as usize]
{
let next_point = Point {
x: point.x + 1,
y: point.y + 1,
};
if !visited.contains_key(&next_point) {
let next_cost = if from == Direction::UpLeft {
cost
} else {
cost + 1
};
q.push(QueueItem {
point: next_point,
cost: next_cost,
from: Direction::UpLeft,
});
}
}
}
// println!("visited len: {}", visited.len());
// println!("queue len: {}", q.len());
// Backtrack from end point to construct diff operations
let mut current = Point {
x: old.len() as u16,
y: new.len() as u16,
};
let mut path: Vec<(Direction, usize)> = Vec::new();
while current.x > 0 || current.y > 0 {
let direction = visited.get(&current).unwrap();
if let Some((last_dir, count)) = path.last_mut() {
if last_dir == direction {
*count += 1;
} else {
path.push((*direction, 1));
}
} else {
path.push((*direction, 1));
}
match direction {
Direction::Left => {
current.x -= 1;
}
Direction::Up => {
current.y -= 1;
}
Direction::UpLeft => {
current.x -= 1;
current.y -= 1;
}
}
}
path.reverse();
let mut old_index = common_prefix_len;
let mut new_index = common_prefix_len;
for (direction, count) in path {
match direction {
Direction::Left => {
proxy.delete(old_offset + old_index, count);
old_index += count;
}
Direction::Up => {
proxy.insert(old_offset + old_index, new_index + new_offset, count);
new_index += count;
}
Direction::UpLeft => {
old_index += count;
new_index += count;
}
}
}
true
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, Default)]
struct RecordingDiffHandler {
ops: Vec<DiffOperation>,
}
#[derive(Debug, PartialEq)]
enum DiffOperation {
Insert {
old_index: usize,
new_index: usize,
length: usize,
},
Delete {
old_index: usize,
length: usize,
},
}
impl DiffHandler for RecordingDiffHandler {
fn insert(&mut self, pos: usize, start: usize, len: usize) {
self.ops.push(DiffOperation::Insert {
old_index: pos,
new_index: start,
length: len,
});
}
fn delete(&mut self, pos: usize, len: usize) {
self.ops.push(DiffOperation::Delete {
old_index: pos,
length: len,
});
}
}
#[test]
fn test_recording_diff_handler() {
let handler = RecordingDiffHandler::default();
let mut proxy = OperateProxy::new(handler);
let old = vec![1, 2, 3];
let new = vec![1, 4, 3];
let _ = dj_diff(&mut proxy, &old, &new, 0, 0, 100_000);
let handler = proxy.unwrap();
assert_eq!(
handler.ops,
vec![
DiffOperation::Delete {
old_index: 1,
length: 1
},
DiffOperation::Insert {
old_index: 2,
new_index: 1,
length: 1
},
]
);
}
#[test]
fn test_dj_diff_same() {
let handler = RecordingDiffHandler::default();
let mut proxy = OperateProxy::new(handler);
let old = vec![1, 2, 3];
let new = vec![1, 2, 3];
let _ = dj_diff(&mut proxy, &old, &new, 0, 0, 100_000);
let handler = proxy.unwrap();
assert_eq!(handler.ops, vec![]);
}
#[test]
fn test_dj_diff_1() {
let handler = RecordingDiffHandler::default();
let mut proxy = OperateProxy::new(handler);
let old = vec![1];
let new = vec![0, 1, 2];
let _ = dj_diff(&mut proxy, &old, &new, 0, 0, 100_000);
let handler = proxy.unwrap();
assert_eq!(
handler.ops,
vec![
DiffOperation::Insert {
old_index: 0,
new_index: 0,
length: 1
},
DiffOperation::Insert {
old_index: 1,
new_index: 2,
length: 1
},
]
);
}
#[test]
fn test_diff_may_scatter() {
let handler = RecordingDiffHandler::default();
let mut proxy = OperateProxy::new(handler);
let old = vec![1, 2, 3, 4, 5];
let new = vec![99, 1, 2, 3, 4, 5, 98, 97, 96, 3, 95, 4, 93, 92, 5, 91];
diff(&mut proxy, UpdateOptions::default(), &old, &new).unwrap();
let handler = proxy.unwrap();
assert_eq!(
handler.ops,
vec![
DiffOperation::Insert {
old_index: 0,
new_index: 0,
length: 1
},
DiffOperation::Insert {
old_index: 5,
new_index: 6,
length: 10
},
]
);
}
#[test]
fn test_diff_may_scatter_1() {
let handler = RecordingDiffHandler::default();
let mut proxy = OperateProxy::new(handler);
let old = vec![1, 2, 3, 4, 5];
let new = vec![99, 1, 2, 98, 97, 96, 3, 95, 4, 93, 92, 5, 1, 2, 3, 4, 5, 91];
diff(&mut proxy, UpdateOptions::default(), &old, &new).unwrap();
let handler = proxy.unwrap();
assert_eq!(
handler.ops,
vec![
DiffOperation::Insert {
old_index: 0,
new_index: 0,
length: 12
},
DiffOperation::Insert {
old_index: 5,
new_index: 17,
length: 1
},
]
);
}
#[test]
fn test_dj_diff_may_scatter() {
let handler = RecordingDiffHandler::default();
let mut proxy = OperateProxy::new(handler);
let old = vec![1, 2, 3, 4, 5];
let new = vec![99, 1, 2, 3, 4, 5, 98, 97, 96, 3, 95, 4, 93, 92, 5, 91];
let _ = dj_diff(&mut proxy, &old, &new, 0, 0, 100_000);
let handler = proxy.unwrap();
assert_eq!(
handler.ops,
vec![
DiffOperation::Insert {
old_index: 0,
new_index: 0,
length: 1
},
DiffOperation::Insert {
old_index: 5,
new_index: 6,
length: 10
},
]
);
}
#[test]
fn test_dj_diff_may_scatter_1() {
let handler = RecordingDiffHandler::default();
let mut proxy = OperateProxy::new(handler);
let old = vec![1, 2, 3, 4, 5];
let new = vec![99, 1, 2, 98, 97, 96, 3, 95, 4, 93, 92, 5, 1, 2, 3, 4, 5, 91];
let _ = dj_diff(&mut proxy, &old, &new, 0, 0, 100_000);
let handler = proxy.unwrap();
assert_eq!(
handler.ops,
vec![
DiffOperation::Insert {
old_index: 0,
new_index: 0,
length: 12
},
DiffOperation::Insert {
old_index: 5,
new_index: 17,
length: 1
},
]
);
}
#[test]
fn test_dj_diff_100_differences() {
let handler = RecordingDiffHandler::default();
let mut proxy = OperateProxy::new(handler);
let old = vec![1; 100];
let new = vec![2; 100];
let _ = dj_diff(&mut proxy, &old, &new, 0, 0, 100_000);
let handler = proxy.unwrap();
assert_eq!(
handler.ops,
vec![
DiffOperation::Delete {
old_index: 0,
length: 100
},
DiffOperation::Insert {
old_index: 100,
new_index: 0,
length: 100
},
]
);
}
#[test]
fn test_dj_diff_insert() {
let handler = RecordingDiffHandler::default();
let mut proxy = OperateProxy::new(handler);
let old = vec![1; 100];
let mut new = old.clone();
new.splice(50..50, [1, 2, 3, 4, 1, 2, 3, 4]);
new.splice(0..0, [0, 1, 2, 3, 4, 5, 6, 7]);
let _ = dj_diff(&mut proxy, &old, &new, 0, 0, 100_000);
let handler = proxy.unwrap();
assert_eq!(
handler.ops,
vec![
DiffOperation::Insert {
old_index: 0,
new_index: 0,
length: 9
},
DiffOperation::Insert {
old_index: 50,
new_index: 59,
length: 7
}
]
);
}
#[test]
fn test_diff() {
let handler = RecordingDiffHandler::default();
let mut proxy = OperateProxy::new(handler);
let old = vec![1; 100];
let new = vec![2; 100];
diff(&mut proxy, Default::default(), &old, &new).unwrap();
}
#[test]
fn test_timeout() {
let handler = RecordingDiffHandler::default();
let mut proxy = OperateProxy::new(handler);
let old = vec![1; 10000];
let new = vec![2; 10000];
let options = UpdateOptions {
timeout_ms: Some(0.1),
..Default::default()
};
let result = diff(&mut proxy, options, &old, &new);
assert!(result.is_err());
}
}

View file

@ -1,4 +1,4 @@
pub mod diff_impl;
pub(crate) use diff_impl::myers_diff;
pub(crate) use diff_impl::diff;
pub(crate) use diff_impl::DiffHandler;
pub(crate) use diff_impl::OperateProxy;

View file

@ -8,7 +8,7 @@ use crate::{
},
cursor::{Cursor, Side},
delta::{DeltaItem, Meta, StyleMeta, TreeExternalDiff},
diff::{myers_diff, OperateProxy},
diff::{diff, diff_impl::UpdateTimeoutError, OperateProxy},
event::{Diff, TextDiffItem},
op::ListSlice,
state::{IndexType, State, TreeParentId},
@ -34,6 +34,7 @@ use std::{
};
use tracing::{error, info, instrument, trace};
pub use crate::diff::diff_impl::UpdateOptions;
pub use tree::TreeHandler;
mod movable_list_apply_delta;
mod tree;
@ -2145,25 +2146,33 @@ impl TextHandler {
Ok(())
}
#[instrument(level = "trace", skip(self))]
pub fn update(&self, text: &str) {
pub fn update(&self, text: &str, options: UpdateOptions) -> Result<(), UpdateTimeoutError> {
let old_str = self.to_string();
let new = text.chars().map(|x| x as u32).collect::<Vec<u32>>();
myers_diff(
let old = old_str.chars().map(|x| x as u32).collect::<Vec<u32>>();
diff(
&mut OperateProxy::new(text_update::DiffHook::new(self, &new)),
&old_str.chars().map(|x| x as u32).collect::<Vec<u32>>(),
options,
&old,
&new,
);
)?;
Ok(())
}
#[instrument(level = "trace", skip(self))]
pub fn update_by_line(&self, text: &str) {
pub fn update_by_line(
&self,
text: &str,
options: UpdateOptions,
) -> Result<(), UpdateTimeoutError> {
let hook = text_update::DiffHookForLine::new(self, text);
let old_lines = hook.get_old_arr().to_vec();
let new_lines = hook.get_new_arr().to_vec();
trace!("old_lines: {:?}", old_lines);
trace!("new_lines: {:?}", new_lines);
myers_diff(&mut OperateProxy::new(hook), &old_lines, &new_lines);
diff(
&mut OperateProxy::new(hook),
options,
&old_lines,
&new_lines,
)
}
#[allow(clippy::inherent_to_string)]

View file

@ -499,7 +499,7 @@ impl OpLog {
pub fn get_timestamp_for_next_txn(&self) -> Timestamp {
if self.configure.record_timestamp() {
(get_sys_timestamp() + 500) / 1000
(get_sys_timestamp() as Timestamp + 500) / 1000
} else {
0
}

View file

@ -11,7 +11,7 @@ use loro_common::{
use tracing::{debug_span, info_span, instrument};
use crate::{
change::get_sys_timestamp,
change::{get_sys_timestamp, Timestamp},
cursor::{AbsolutePosition, Cursor},
delta::TreeExternalDiff,
event::{Diff, EventTriggerKind},
@ -397,7 +397,7 @@ impl UndoManagerInner {
}
assert!(self.next_counter.unwrap() < latest_counter);
let now = get_sys_timestamp();
let now = get_sys_timestamp() as Timestamp;
let span = CounterSpan::new(self.next_counter.unwrap(), latest_counter);
let meta = self
.on_push

View file

@ -1238,7 +1238,8 @@ fn test_text_update() {
let doc = LoroDoc::new_auto_commit();
let text = doc.get_text("text");
text.insert(0, "Hello 😊Bro").unwrap();
text.update("Hello World Bro😊");
text.update("Hello World Bro😊", Default::default())
.unwrap();
assert_eq!(text.to_string(), "Hello World Bro😊");
}

View file

@ -13,6 +13,7 @@ use loro_internal::{
cursor::{self, Side},
encoding::ImportBlobMetadata,
event::Index,
handler::UpdateOptions,
handler::{
Handler, ListHandler, MapHandler, TextDelta, TextHandler, TreeHandler, ValueOrHandler,
},
@ -1824,16 +1825,62 @@ impl LoroText {
/// text.update("Hello World");
/// console.log(text.toString()); // "Hello World"
/// ```
pub fn update(&self, text: &str) {
self.handler.update(text);
///
#[wasm_bindgen(skip_typescript)]
pub fn update(&self, text: &str, options: JsValue) -> JsResult<()> {
let options = if options.is_null() || options.is_undefined() {
UpdateOptions {
timeout_ms: None,
use_refined_diff: true,
}
} else {
let opts = match js_sys::Object::try_from(&options) {
Some(o) => o,
None => return Err(JsError::new("Invalid options").into()),
};
UpdateOptions {
timeout_ms: js_sys::Reflect::get(opts, &"timeoutMs".into())
.ok()
.and_then(|v| v.as_f64()),
use_refined_diff: js_sys::Reflect::get(opts, &"useRefinedDiff".into())
.ok()
.and_then(|v| v.as_bool())
.unwrap_or(true),
}
};
self.handler
.update(text, options)
.map_err(|_| JsError::new("Update timeout").into())
}
/// Update the current text to the target text, the difference is calculated line by line.
///
/// It uses Myers' diff algorithm to compute the optimal difference.
#[wasm_bindgen(js_name = "updateByLine")]
pub fn update_by_line(&self, text: &str) {
self.handler.update_by_line(text);
#[wasm_bindgen(js_name = "updateByLine", skip_typescript)]
pub fn update_by_line(&self, text: &str, options: JsValue) -> JsResult<()> {
let options = if options.is_null() || options.is_undefined() {
UpdateOptions {
timeout_ms: None,
use_refined_diff: true,
}
} else {
let opts = match js_sys::Object::try_from(&options) {
Some(o) => o,
None => return Err(JsError::new("Invalid options").into()),
};
UpdateOptions {
timeout_ms: js_sys::Reflect::get(opts, &"timeoutMs".into())
.ok()
.and_then(|v| v.as_f64()),
use_refined_diff: js_sys::Reflect::get(opts, &"useRefinedDiff".into())
.ok()
.and_then(|v| v.as_bool())
.unwrap_or(true),
}
};
self.handler
.update_by_line(text, options)
.map_err(|_| JsError::new("Update timeout").into())
}
/// Insert the string at the given index (utf-16 index).
@ -4849,6 +4896,11 @@ export type JsonChange = {
ops: JsonOp[]
}
export interface TextUpdateOptions {
timeoutMs?: number,
useRefinedDiff?: boolean,
}
export type ExportMode = {
mode: "update",
from?: VersionVector,
@ -5429,6 +5481,32 @@ interface LoroText {
insert(pos: number, text: string): void;
delete(pos: number, len: number): void;
subscribe(listener: Listener): Subscription;
/**
* Update the current text to the target text.
*
* It will calculate the minimal difference and apply it to the current text.
* It uses Myers' diff algorithm to compute the optimal difference.
*
* This could take a long time for large texts (e.g. > 50_000 characters).
* In that case, you should use `updateByLine` instead.
*
* @example
* ```ts
* import { LoroDoc } from "loro-crdt";
*
* const doc = new LoroDoc();
* const text = doc.getText("text");
* text.insert(0, "Hello");
* text.update("Hello World");
* console.log(text.toString()); // "Hello World"
* ```
*/
update(text: string, options?: TextUpdateOptions): void;
/**
* Update the current text based on the provided text.
* This update calculation is line-based, which will be more efficient but less precise.
*/
updateByLine(text: string, options?: TextUpdateOptions): void;
}
interface LoroTree<T extends Record<string, unknown> = Record<string, unknown>> {
new(): LoroTree<T>;

View file

@ -29,6 +29,8 @@ use std::ops::Range;
use std::sync::Arc;
use tracing::info;
pub use loro_internal::diff::diff_impl::UpdateOptions;
pub use loro_internal::diff::diff_impl::UpdateTimeoutError;
pub use loro_internal::subscription::LocalUpdateCallback;
pub use loro_internal::subscription::PeerIdUpdateCallback;
pub use loro_internal::ChangeMeta;
@ -1445,13 +1447,37 @@ impl LoroText {
}
/// Update the current text based on the provided text.
pub fn update(&self, text: &str) {
self.handler.update(text);
///
/// It will calculate the minimal difference and apply it to the current text.
/// It uses Myers' diff algorithm to compute the optimal difference.
///
/// This could take a long time for large texts (e.g. > 50_000 characters).
/// In that case, you should use `updateByLine` instead.
///
/// # Example
/// ```rust
/// use loro::LoroDoc;
///
/// let doc = LoroDoc::new();
/// let text = doc.get_text("text");
/// text.insert(0, "Hello").unwrap();
/// text.update("Hello World", Default::default()).unwrap();
/// assert_eq!(text.to_string(), "Hello World");
/// ```
///
pub fn update(&self, text: &str, options: UpdateOptions) -> Result<(), UpdateTimeoutError> {
self.handler.update(text, options)
}
/// Update the current text based on the provided text by line.
pub fn update_by_line(&self, text: &str) {
self.handler.update_by_line(text);
/// Update the current text based on the provided text.
///
/// This update calculation is line-based, which will be more efficient but less precise.
pub fn update_by_line(
&self,
text: &str,
options: UpdateOptions,
) -> Result<(), UpdateTimeoutError> {
self.handler.update_by_line(text, options)
}
/// Apply a [delta](https://quilljs.com/docs/delta/) to the text container.

View file

@ -5,11 +5,11 @@ fn test_text_update() -> anyhow::Result<()> {
let (old, new, new1) = (")+++", "%", "");
let doc = LoroDoc::new();
let text = doc.get_text("text");
text.update(old);
text.update(old, Default::default()).unwrap();
assert_eq!(&text.to_string(), old);
text.update(new);
text.update(new, Default::default()).unwrap();
assert_eq!(&text.to_string(), new);
text.update(new1);
text.update(new1, Default::default()).unwrap();
assert_eq!(&text.to_string(), new1);
Ok(())
}
@ -23,11 +23,11 @@ fn test_text_update_by_line() -> anyhow::Result<()> {
);
let doc = LoroDoc::new();
let text = doc.get_text("text");
text.update_by_line(old);
text.update_by_line(old, Default::default()).unwrap();
assert_eq!(&text.to_string(), old);
text.update_by_line(new);
text.update_by_line(new, Default::default()).unwrap();
assert_eq!(&text.to_string(), new);
text.update_by_line(new1);
text.update_by_line(new1, Default::default()).unwrap();
assert_eq!(&text.to_string(), new1);
Ok(())
}
@ -36,9 +36,9 @@ fn test_text_update_by_line() -> anyhow::Result<()> {
fn test_text_update_empty_to_nonempty() -> anyhow::Result<()> {
let doc = LoroDoc::new();
let text = doc.get_text("text");
text.update("");
text.update("", Default::default()).unwrap();
assert_eq!(&text.to_string(), "");
text.update("Hello, Loro!");
text.update("Hello, Loro!", Default::default()).unwrap();
assert_eq!(&text.to_string(), "Hello, Loro!");
Ok(())
}
@ -47,9 +47,9 @@ fn test_text_update_empty_to_nonempty() -> anyhow::Result<()> {
fn test_text_update_nonempty_to_empty() -> anyhow::Result<()> {
let doc = LoroDoc::new();
let text = doc.get_text("text");
text.update("Initial content");
text.update("Initial content", Default::default()).unwrap();
assert_eq!(&text.to_string(), "Initial content");
text.update("");
text.update("", Default::default()).unwrap();
assert_eq!(&text.to_string(), "");
Ok(())
}
@ -58,9 +58,11 @@ fn test_text_update_nonempty_to_empty() -> anyhow::Result<()> {
fn test_text_update_with_special_characters() -> anyhow::Result<()> {
let doc = LoroDoc::new();
let text = doc.get_text("text");
text.update("Special chars: !@#$%^&*()");
text.update("Special chars: !@#$%^&*()", Default::default())
.unwrap();
assert_eq!(&text.to_string(), "Special chars: !@#$%^&*()");
text.update("New special chars: ñáéíóú");
text.update("New special chars: ñáéíóú", Default::default())
.unwrap();
assert_eq!(&text.to_string(), "New special chars: ñáéíóú");
Ok(())
}
@ -69,9 +71,11 @@ fn test_text_update_with_special_characters() -> anyhow::Result<()> {
fn test_text_update_by_line_with_empty_lines() -> anyhow::Result<()> {
let doc = LoroDoc::new();
let text = doc.get_text("text");
text.update_by_line("Line 1\n\nLine 3\n");
text.update_by_line("Line 1\n\nLine 3\n", Default::default())
.unwrap();
assert_eq!(&text.to_string(), "Line 1\n\nLine 3\n");
text.update_by_line("Line 1\nLine 2\n\nLine 4\n");
text.update_by_line("Line 1\nLine 2\n\nLine 4\n", Default::default())
.unwrap();
assert_eq!(&text.to_string(), "Line 1\nLine 2\n\nLine 4\n");
Ok(())
}
@ -80,9 +84,11 @@ fn test_text_update_by_line_with_empty_lines() -> anyhow::Result<()> {
fn test_text_update_by_line_with_different_line_endings() -> anyhow::Result<()> {
let doc = LoroDoc::new();
let text = doc.get_text("text");
text.update_by_line("Line 1\nLine 2\r\nLine 3\n");
text.update_by_line("Line 1\nLine 2\r\nLine 3\n", Default::default())
.unwrap();
assert_eq!(&text.to_string(), "Line 1\nLine 2\r\nLine 3\n");
text.update_by_line("Line 1\r\nLine 2\nLine 3\r\n");
text.update_by_line("Line 1\r\nLine 2\nLine 3\r\n", Default::default())
.unwrap();
assert_eq!(&text.to_string(), "Line 1\r\nLine 2\nLine 3\r\n");
Ok(())
}
@ -91,9 +97,11 @@ fn test_text_update_by_line_with_different_line_endings() -> anyhow::Result<()>
fn test_text_update_by_line_with_no_trailing_newline() -> anyhow::Result<()> {
let doc = LoroDoc::new();
let text = doc.get_text("text");
text.update_by_line("Line 1\nLine 2\nLine 3");
text.update_by_line("Line 1\nLine 2\nLine 3", Default::default())
.unwrap();
assert_eq!(&text.to_string(), "Line 1\nLine 2\nLine 3");
text.update_by_line("Line 1\nLine 2\nLine 3\nLine 4");
text.update_by_line("Line 1\nLine 2\nLine 3\nLine 4", Default::default())
.unwrap();
assert_eq!(&text.to_string(), "Line 1\nLine 2\nLine 3\nLine 4");
Ok(())
}
@ -114,10 +122,10 @@ fn test_failed_case_0() -> anyhow::Result<()> {
let (old, new, new1) = (input[0], input[1], input[2]);
let doc = LoroDoc::new();
let text = doc.get_text("text");
text.update(old);
text.update(new);
text.update(old, Default::default()).unwrap();
text.update(new, Default::default()).unwrap();
assert_eq!(&text.to_string(), new);
text.update_by_line(new1);
text.update_by_line(new1, Default::default()).unwrap();
assert_eq!(&text.to_string(), new1);
Ok(())
}
@ -128,10 +136,10 @@ fn test_failed_case_1() -> anyhow::Result<()> {
let (old, new, new1) = (input[0], input[1], input[2]);
let doc = LoroDoc::new();
let text = doc.get_text("text");
text.update(old);
text.update(new);
text.update(old, Default::default()).unwrap();
text.update(new, Default::default()).unwrap();
assert_eq!(&text.to_string(), new);
text.update_by_line(new1);
text.update_by_line(new1, Default::default()).unwrap();
assert_eq!(&text.to_string(), new1);
Ok(())
}
@ -142,10 +150,10 @@ fn test_failed_case_2() -> anyhow::Result<()> {
let (old, new, new1) = (input[0], input[1], input[2]);
let doc = LoroDoc::new();
let text = doc.get_text("text");
text.update(old);
text.update(new);
text.update(old, Default::default()).unwrap();
text.update(new, Default::default()).unwrap();
assert_eq!(&text.to_string(), new);
text.update_by_line(new1);
text.update_by_line(new1, Default::default()).unwrap();
assert_eq!(&text.to_string(), new1);
Ok(())
}