Implemented Transaction composition.

This commit is contained in:
Nathan Vegdahl 2022-08-22 21:10:28 -07:00
parent 8a9388e816
commit 696ecb732d
5 changed files with 459 additions and 91 deletions

121
Cargo.lock generated
View File

@ -1,5 +1,7 @@
# This file is automatically @generated by Cargo. # This file is automatically @generated by Cargo.
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3
[[package]] [[package]]
name = "Led" name = "Led"
version = "0.0.2" version = "0.0.2"
@ -35,10 +37,17 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]] [[package]]
name = "backend" name = "backend"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"proptest",
"ropey", "ropey",
"unicode-segmentation", "unicode-segmentation",
] ]
@ -49,6 +58,21 @@ version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4521f3e3d031370679b3b140beb36dfe4801b09ac77e30c61941f97df3ef28b" checksum = "a4521f3e3d031370679b3b140beb36dfe4801b09ac77e30c61941f97df3ef28b"
[[package]]
name = "bit-set"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1"
dependencies = [
"bit-vec",
]
[[package]]
name = "bit-vec"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "1.2.1" version = "1.2.1"
@ -61,6 +85,12 @@ version = "3.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c59e7af012c713f529e7a3ee57ce9b31ddd858d4b512923602f74608b009631" checksum = "9c59e7af012c713f529e7a3ee57ce9b31ddd858d4b512923602f74608b009631"
[[package]]
name = "byteorder"
version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.0" version = "1.0.0"
@ -130,6 +160,12 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]] [[package]]
name = "gag" name = "gag"
version = "1.0.0" version = "1.0.0"
@ -153,9 +189,9 @@ dependencies = [
[[package]] [[package]]
name = "hermit-abi" name = "hermit-abi"
version = "0.1.18" version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c" checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
dependencies = [ dependencies = [
"libc", "libc",
] ]
@ -236,6 +272,15 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "num-traits"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
dependencies = [
"autocfg",
]
[[package]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.11.1" version = "0.11.1"
@ -282,6 +327,38 @@ dependencies = [
"unicode-xid", "unicode-xid",
] ]
[[package]]
name = "proptest"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e0d9cc07f18492d879586c92b485def06bc850da3118075cd45d50e9c95b0e5"
dependencies = [
"bit-set",
"bitflags",
"byteorder",
"lazy_static",
"num-traits",
"quick-error 2.0.1",
"rand",
"rand_chacha",
"rand_xorshift",
"regex-syntax",
"rusty-fork",
"tempfile",
]
[[package]]
name = "quick-error"
version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
[[package]]
name = "quick-error"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.9" version = "1.0.9"
@ -331,6 +408,15 @@ dependencies = [
"rand_core", "rand_core",
] ]
[[package]]
name = "rand_xorshift"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f"
dependencies = [
"rand_core",
]
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.2.9" version = "0.2.9"
@ -340,6 +426,12 @@ dependencies = [
"bitflags", "bitflags",
] ]
[[package]]
name = "regex-syntax"
version = "0.6.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244"
[[package]] [[package]]
name = "remove_dir_all" name = "remove_dir_all"
version = "0.5.3" version = "0.5.3"
@ -351,9 +443,9 @@ dependencies = [
[[package]] [[package]]
name = "ropey" name = "ropey"
version = "1.3.0" version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4b1996ad82fb6b8ac0711329432d80aa1ad5d70a0fe6edd1a9cf6fd2375fbb2" checksum = "9150aff6deb25b20ed110889f070a678bcd1033e46e5e9d6fb1abeab17947f28"
dependencies = [ dependencies = [
"smallvec", "smallvec",
] ]
@ -367,6 +459,18 @@ dependencies = [
"semver", "semver",
] ]
[[package]]
name = "rusty-fork"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f"
dependencies = [
"fnv",
"quick-error 1.2.3",
"tempfile",
"wait-timeout",
]
[[package]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.5" version = "1.0.5"
@ -650,6 +754,15 @@ version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
[[package]]
name = "wait-timeout"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "wasi" name = "wasi"
version = "0.10.2+wasi-snapshot-preview1" version = "0.10.2+wasi-snapshot-preview1"

View File

@ -12,4 +12,7 @@ path = "src/lib.rs"
[dependencies] [dependencies]
ropey = "1" ropey = "1"
# ropey = { git = "https://github.com/cessen/ropey", branch = "master" } # ropey = { git = "https://github.com/cessen/ropey", branch = "master" }
unicode-segmentation = "1.7" unicode-segmentation = "1.7"
[dev-dependencies]
proptest = "1.0"

View File

@ -4,9 +4,7 @@ use crate::marks::MarkSet;
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
enum Op { enum Op {
Retain { Retain(usize), // In bytes.
byte_count: usize,
},
Replace { Replace {
// These both represent strings, and are byte-index ranges into // These both represent strings, and are byte-index ranges into
// the Transaction's `buffer` where their actual string data is // the Transaction's `buffer` where their actual string data is
@ -16,8 +14,28 @@ enum Op {
}, },
} }
impl Op {
/// The length of the string segment this Op represents *before*
/// its application. In bytes.
fn len_pre(&self) -> usize {
match self {
Op::Retain(byte_count) => *byte_count,
Op::Replace { old, .. } => old.1 - old.0,
}
}
/// The length of the string segment this Op represents *after*
/// its application. In bytes.
fn len_post(&self) -> usize {
match self {
Op::Retain(byte_count) => *byte_count,
Op::Replace { new, .. } => new.1 - new.0,
}
}
}
/// A reversable set of edits treated as an atomic unit. /// A reversable set of edits treated as an atomic unit.
#[derive(Debug, Clone)] #[derive(Clone)]
pub struct Transaction { pub struct Transaction {
ops: Vec<Op>, ops: Vec<Op>,
buffer: String, buffer: String,
@ -38,9 +56,7 @@ impl Transaction {
buffer.push_str(new); buffer.push_str(new);
let ops = vec![ let ops = vec![
Op::Retain { Op::Retain(byte_idx),
byte_count: byte_idx,
},
Op::Replace { Op::Replace {
old: (0, old.len()), old: (0, old.len()),
new: (old.len(), old.len() + new.len()), new: (old.len(), old.len() + new.len()),
@ -79,9 +95,7 @@ impl Transaction {
trans.buffer.push_str(new); trans.buffer.push_str(new);
if retained > 0 { if retained > 0 {
trans.ops.push(Op::Retain { trans.ops.push(Op::Retain(retained));
byte_count: retained,
});
} }
trans.ops.push(Op::Replace { trans.ops.push(Op::Replace {
old: old_range, old: old_range,
@ -101,30 +115,119 @@ impl Transaction {
use Op::*; use Op::*;
let mut trans = Transaction::new(); let mut trans = Transaction::new();
let mut ops1 = self.ops.iter();
let mut ops2 = other.ops.iter();
let mut op1 = ops1.next();
let mut op2 = ops2.next();
let mut range1 = (0, 0);
let mut range2 = (0, 0);
let mut ops1 = self.ops.iter().copied();
let mut ops2 = other.ops.iter().copied();
let mut next_op1 = ops1.next();
let mut next_op2 = ops2.next();
// We progress through both transaction's ops such that their
// heads stay in sync. To do this, we have to split ops
// sometimes, fragmenting the transaction.
loop { loop {
match (op1, op2) { match (next_op1, next_op2) {
//------------------------------- // Done!
// Move past the unchanged bits. (None, None) => break,
(Some(Retain { byte_count: bc }), _) => {
range1.1 += bc; // One of the transactions is empty, so just append the
op1 = ops1.next(); // remaining ops from the other one.
(Some(op), None) => {
trans.push_op(op, &self.buffer);
next_op1 = ops1.next();
}
(None, Some(op)) => {
trans.push_op(op, &other.buffer);
next_op2 = ops2.next();
} }
(_, Some(Retain { byte_count: bc })) => { //----------------
range2.1 += bc; // Retain, Retain
op2 = ops2.next(); (Some(Retain(bc1)), Some(Retain(bc2))) => {
if bc1 < bc2 {
trans.retain(bc1);
next_op1 = ops1.next();
next_op2 = Some(Retain(bc2 - bc1));
} else if bc2 < bc1 {
trans.retain(bc2);
next_op1 = Some(Retain(bc1 - bc2));
next_op2 = ops2.next();
} else {
trans.retain(bc1);
next_op1 = ops1.next();
next_op2 = ops2.next();
}
} }
//----------------------------------------------------- //-----------------
// Handle changes when there are still changes in both // Replace, Retain
// source transactions. (Some(Replace { .. }), Some(Retain(bc2))) => {
let op1 = next_op1.unwrap();
if op1.len_post() < bc2 {
trans.push_op(op1, &self.buffer);
next_op1 = ops1.next();
next_op2 = Some(Retain(bc2 - op1.len_post()));
} else if op1.len_post() > bc2 {
let (op1a, op1b) = match op1 {
Op::Retain(bc1) => (Op::Retain(bc2), Op::Retain(bc1 - bc2)),
Op::Replace { old, new } => (
Op::Replace {
old: old,
new: (new.0, new.0 + bc2),
},
Op::Replace {
old: (0, 0),
new: (new.0 + bc2, new.1),
},
),
};
trans.push_op(op1a, &self.buffer);
next_op1 = Some(op1b);
next_op2 = ops2.next();
} else {
trans.push_op(op1, &self.buffer);
next_op1 = ops1.next();
next_op2 = ops2.next();
}
}
//-----------------
// Retain, Replace
(
Some(Retain(bc1)),
Some(Replace {
old: old2,
new: new2,
}),
) => {
let op2 = next_op2.unwrap();
if bc1 < op2.len_pre() {
trans.push_op(
Replace {
old: (old2.0, old2.0 + bc1),
new: (0, 0),
},
&other.buffer,
);
next_op1 = ops1.next();
next_op2 = Some(Replace {
old: (old2.0 + bc1, old2.1),
new: new2,
});
} else if bc1 > op2.len_pre() {
trans.push_op(op2, &other.buffer);
next_op1 = Some(Retain(bc1 - op2.len_pre()));
next_op2 = ops2.next();
} else {
trans.push_op(op2, &other.buffer);
next_op1 = ops1.next();
next_op2 = ops2.next();
}
}
//------------------
// Replace, Replace
( (
Some(Replace { Some(Replace {
old: old1, old: old1,
@ -135,65 +238,54 @@ impl Transaction {
new: new2, new: new2,
}), }),
) => { ) => {
// This is the complex case. let op1 = next_op1.unwrap();
todo!() let op2 = next_op2.unwrap();
}
//------------------------------------------------------ if op1.len_post() < op2.len_pre() {
// Handle changes when there are only changes remaining trans.push_op(
// in one of the source transactions. Replace {
(Some(Replace { old, new }), None) => { old: old1,
if range1.0 < range1.1 { new: (0, 0),
trans.ops.push(Retain { },
byte_count: range1.1 - range1.0, &self.buffer,
);
next_op1 = ops1.next();
next_op2 = Some(Replace {
old: (old2.0 + op1.len_post(), old2.1),
new: new2,
}); });
range1.0 = range1.1; } else if op1.len_post() > op2.len_pre() {
} trans.push_op(
Replace {
let ti = trans.buffer.len(); old: old1,
trans.buffer.push_str(&self.buffer[old.0..old.1]); new: (0, 0),
let old_range = (ti, ti + (old.1 - old.0)); },
&self.buffer,
let ti = trans.buffer.len(); );
trans.buffer.push_str(&self.buffer[new.0..new.1]); trans.push_op(
let new_range = (ti, ti + (new.1 - new.0)); Replace {
old: (0, 0),
trans.ops.push(Replace { new: new2,
old: old_range, },
new: new_range, &other.buffer,
}); );
next_op1 = Some(Replace {
op1 = ops1.next(); old: (0, 0),
} new: (new1.0 + op2.len_pre(), new1.1),
(None, Some(Replace { old, new })) => {
if range2.0 < range2.1 {
trans.ops.push(Retain {
byte_count: range2.1 - range2.0,
}); });
range2.0 = range2.1; next_op2 = ops2.next();
} else {
trans.push_op_separate_buf(
Replace {
old: old1,
new: new2,
},
&self.buffer,
&other.buffer,
);
next_op1 = ops1.next();
next_op2 = ops2.next();
} }
let ti = trans.buffer.len();
trans.buffer.push_str(&other.buffer[old.0..old.1]);
let old_range = (ti, ti + (old.1 - old.0));
let ti = trans.buffer.len();
trans.buffer.push_str(&other.buffer[new.0..new.1]);
let new_range = (ti, ti + (new.1 - new.0));
trans.ops.push(Replace {
old: old_range,
new: new_range,
});
op2 = ops2.next();
}
//-------
// Done.
(None, None) => {
break;
} }
} }
} }
@ -212,7 +304,7 @@ impl Transaction {
let mut inverted = self.clone(); let mut inverted = self.clone();
for op in inverted.ops.iter_mut() { for op in inverted.ops.iter_mut() {
match *op { match *op {
Op::Retain { .. } => {} // Do nothing. Op::Retain(_) => {} // Do nothing.
Op::Replace { Op::Replace {
ref mut old, ref mut old,
ref mut new, ref mut new,
@ -225,11 +317,11 @@ impl Transaction {
} }
/// Applies the Transaction to a Rope. /// Applies the Transaction to a Rope.
pub fn apply_to_text(&self, text: &mut Rope) { pub fn apply(&self, text: &mut Rope) {
let mut i = 0; let mut i = 0;
for op in self.ops.iter() { for op in self.ops.iter() {
match op { match op {
Op::Retain { byte_count } => { Op::Retain(byte_count) => {
i += byte_count; i += byte_count;
} }
Op::Replace { old, new } => { Op::Replace { old, new } => {
@ -257,4 +349,87 @@ impl Transaction {
pub fn apply_to_marks(&self, _marks: &mut MarkSet) { pub fn apply_to_marks(&self, _marks: &mut MarkSet) {
todo!() todo!()
} }
//---------------------------------------------------------
/// Pushes a retain op onto the transaction operations.
#[inline(always)]
fn retain(&mut self, byte_count: usize) {
if let Some(Op::Retain(ref mut last_byte_count)) = self.ops.last_mut() {
*last_byte_count += byte_count;
} else if byte_count > 0 {
self.ops.push(Op::Retain(byte_count));
}
}
/// Pushes a replace op onto the transaction operations.
#[inline(always)]
fn replace(&mut self, old: &str, new: &str) {
if !old.is_empty() || !new.is_empty() {
let ti = self.buffer.len();
self.buffer.push_str(old);
let old_range = (ti, ti + old.len());
let ti = self.buffer.len();
self.buffer.push_str(new);
let new_range = (ti, ti + new.len());
self.ops.push(Op::Replace {
old: old_range,
new: new_range,
});
}
}
/// Pushes an op onto the transaction operations.
///
/// - op: the operation.
/// - buffer: the string buffer the op points into if it's a `Replace`.
#[inline(always)]
fn push_op(&mut self, op: Op, buffer: &str) {
self.push_op_separate_buf(op, buffer, buffer);
}
/// Pushes an op onto the transaction operations.
///
/// - op: the operation.
/// - buf_old: the string buffer that `Replace { old }` points into.
/// - buf_new: the string buffer that `Replace { new }` points into.
#[inline(always)]
fn push_op_separate_buf(&mut self, op: Op, buf_old: &str, buf_new: &str) {
match op {
Op::Retain(byte_count) => self.retain(byte_count),
Op::Replace { old, new } => {
self.replace(&buf_old[old.0..old.1], &buf_new[new.0..new.1])
}
}
}
}
impl std::fmt::Debug for Transaction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
f.write_str("Transaction {\n")?;
f.write_str(" ops: [\n")?;
for op in self.ops.iter() {
match op {
Op::Retain(byte_count) => f.write_str(&format!(" Retain({}),\n", byte_count))?,
Op::Replace { old, new } => f.write_str(&format!(
" Replace(\n old({}): \"{}\",\n new({}): \"{}\"\n ),\n",
old.1 - old.0,
&self.buffer[old.0..old.1],
new.1 - new.0,
&self.buffer[new.0..new.1],
))?,
}
}
f.write_str(" ],\n")?;
f.write_str(&format!(
" buffer({}): \"{}\",\n",
self.buffer.len(),
&self.buffer
))?;
f.write_str("}\n")?;
Ok(())
}
} }

View File

@ -0,0 +1,12 @@
# Seeds for failure cases proptest has generated in the past. It is
# automatically read and these particular cases re-run before any
# novel cases are generated.
#
# It is recommended to check this file in to source control so that
# everyone who runs the test benefits from these saved cases.
cc 9a4e25ad28ca0e4c6282ebe1e7de4d2f021a698e7c36691e99aad7e62af967f7 # shrinks to ref start_text = "🄀𐀨", mut edits1 = [], mut edits2 = [(0, 0, "®")]
cc cc0cfd1ee5c07fa5555bc31a889474ee30c835b17df1a72deb6392ba59ac567c # shrinks to ref start_text = "ਓ A🉠A𐊠០a\u{3099}𖩮 aA\u{1e130}0aA¡🌀﷏a00 \u{b3c}aa®A \u{1da9b} 𝑖 ㇰ\u{1e130}0Σ ", mut edits1 = [(0, 0, "A")], mut edits2 = []
cc f3e118eb2c12051b9272965ede18bedae8d80c8b20deb204ea2ddcac0167ea7c # shrinks to ref start_text = " a¡a®\u{abc}ﹶAa ὐAA®🌀ⶠ¡0a\u{c3c}a0aa0𖿠0AA 𞹂 ዀ𐼀𖭛", mut edits1 = [(0, 0, "𐖗")], mut edits2 = [(0, 1, "")]
cc 91517cacb2a3f87865f9c3cc2679187e1c766c87c3bbf677fb45ce1a336f2156 # shrinks to ref start_text = "᭐𞥞 𝕒𞹛\u{20d0}A 0𝋠0𐐀AaA𐕼 𞅎\u{10eab}Aኊ࡞𑌵יִA0𞺡00𛅤מּ 𐝀", mut edits1 = [(1, 1, ""), (1, 6, "¡𑊏a"), (69, 0, " 0\u{1b80}0A\u{16f8f}𞸻\u{11366}")], mut edits2 = [(71, 5, "")]
cc 4aeb2caebb202397370ec1021309a8f10e05336b2841a3d357d6164081038219 # shrinks to ref start_text = "㆐વ\u{3000}𐔀0ஒஜ 𞹝 𐒰0\u{ec8}ዘഒa𞺀𑰊a0AaaA 𐧒0A", mut edits1 = [(0, 0, "🌀"), (52, 0, "®0ก𑵧 A a")], mut edits2 = [(60, 9, "")]
cc 8955ab9f00a8484dceb51acb72d612c8720c227a5f57fd87a18acfe1958dfd66 # shrinks to ref start_text = "ΣὙ®aAΣ 0 ኲ 0 𘠀ኸ𑊟0ਓ AAﬓ𐴰𑶠A🂱¡ \u{2de0}A𝟎𝕊", mut edits1 = [(25, 1, "¡Aa ")], mut edits2 = [(25, 1, ""), (25, 4, "")]

View File

@ -0,0 +1,65 @@
#[macro_use]
extern crate proptest;
use proptest::collection::vec;
use proptest::test_runner::Config;
use ropey::Rope;
use backend::transaction::Transaction;
fn make_edit_list_valid(edits: &mut Vec<(usize, usize, String)>, start_text: &str) {
edits.sort_by_key(|e| e.0);
let mut tail = 0;
for e in edits.iter_mut() {
e.0 = e.0.max(tail);
while e.0 < start_text.len() && !start_text.is_char_boundary(e.0) {
e.0 += 1;
}
while (e.0 + e.1) < start_text.len() && !start_text.is_char_boundary(e.0 + e.1) {
e.1 += 1;
}
tail = e.0 + e.1;
}
for i in (0..edits.len()).rev() {
if edits[i].0 > start_text.len() {
edits.remove(i);
} else if (edits[i].0 + edits[i].1) > start_text.len() {
edits[i].1 = start_text.len() - edits[i].0;
}
}
}
proptest! {
#![proptest_config(Config::with_cases(512))]
#[test]
fn compose(
ref start_text in "\\PC{0, 200}",
mut edits1 in vec((0usize..100, 0usize..10, "\\PC{0, 10}"), 0..5),
mut edits2 in vec((0usize..100, 0usize..10, "\\PC{0, 10}"), 0..5),
) {
// Make edits1 valid and compute the post-edits1 string.
make_edit_list_valid(&mut edits1, start_text);
let trans1 = Transaction::from_ordered_edit_set(edits1.iter().map(|e| {
(e.0, &start_text[e.0..(e.0+e.1)], &e.2[..])
}));
let mut r1 = Rope::from_str(start_text);
trans1.apply(&mut r1);
let text1: String = r1.clone().into();
// Make edits2 valid and compute the post-edits2 string.
make_edit_list_valid(&mut edits2, &text1);
let trans2 = Transaction::from_ordered_edit_set(edits2.iter().map(|e| {
(e.0, &text1[e.0..(e.0+e.1)], &e.2[..])
}));
trans2.apply(&mut r1);
// Do the test!
let trans3 = trans1.compose(&trans2);
let mut r2 = Rope::from_str(start_text);
trans3.apply(&mut r2);
assert_eq!(r1, r2);
}
}