diff --git a/Cargo.lock b/Cargo.lock index 055ae34..bfcaaa2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,7 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +version = 3 + [[package]] name = "Led" version = "0.0.2" @@ -35,10 +37,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + [[package]] name = "backend" version = "0.1.0" dependencies = [ + "proptest", "ropey", "unicode-segmentation", ] @@ -49,6 +58,21 @@ version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "bitflags" version = "1.2.1" @@ -61,6 +85,12 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c59e7af012c713f529e7a3ee57ce9b31ddd858d4b512923602f74608b009631" +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + [[package]] name = "cfg-if" version = "1.0.0" @@ -130,6 +160,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "gag" version = "1.0.0" @@ -153,9 +189,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" dependencies = [ "libc", ] @@ -236,6 +272,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + [[package]] name = "parking_lot" version = "0.11.1" @@ -282,6 +327,38 @@ dependencies = [ "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]] name = "quote" version = "1.0.9" @@ -331,6 +408,15 @@ dependencies = [ "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]] name = "redox_syscall" version = "0.2.9" @@ -340,6 +426,12 @@ dependencies = [ "bitflags", ] +[[package]] +name = "regex-syntax" +version = "0.6.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" + [[package]] name = "remove_dir_all" version = "0.5.3" @@ -351,9 +443,9 @@ dependencies = [ [[package]] name = "ropey" -version = "1.3.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4b1996ad82fb6b8ac0711329432d80aa1ad5d70a0fe6edd1a9cf6fd2375fbb2" +checksum = "9150aff6deb25b20ed110889f070a678bcd1033e46e5e9d6fb1abeab17947f28" dependencies = [ "smallvec", ] @@ -367,6 +459,18 @@ dependencies = [ "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]] name = "ryu" version = "1.0.5" @@ -650,6 +754,15 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "wasi" version = "0.10.2+wasi-snapshot-preview1" diff --git a/sub_crates/backend/Cargo.toml b/sub_crates/backend/Cargo.toml index a9d6fb9..f5dab41 100644 --- a/sub_crates/backend/Cargo.toml +++ b/sub_crates/backend/Cargo.toml @@ -12,4 +12,7 @@ path = "src/lib.rs" [dependencies] ropey = "1" # ropey = { git = "https://github.com/cessen/ropey", branch = "master" } -unicode-segmentation = "1.7" \ No newline at end of file +unicode-segmentation = "1.7" + +[dev-dependencies] +proptest = "1.0" diff --git a/sub_crates/backend/src/transaction.rs b/sub_crates/backend/src/transaction.rs index 2ff6efd..1bc64ed 100644 --- a/sub_crates/backend/src/transaction.rs +++ b/sub_crates/backend/src/transaction.rs @@ -4,9 +4,7 @@ use crate::marks::MarkSet; #[derive(Debug, Clone, Copy)] enum Op { - Retain { - byte_count: usize, - }, + Retain(usize), // In bytes. Replace { // These both represent strings, and are byte-index ranges into // 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. -#[derive(Debug, Clone)] +#[derive(Clone)] pub struct Transaction { ops: Vec, buffer: String, @@ -38,9 +56,7 @@ impl Transaction { buffer.push_str(new); let ops = vec![ - Op::Retain { - byte_count: byte_idx, - }, + Op::Retain(byte_idx), Op::Replace { old: (0, old.len()), new: (old.len(), old.len() + new.len()), @@ -79,9 +95,7 @@ impl Transaction { trans.buffer.push_str(new); if retained > 0 { - trans.ops.push(Op::Retain { - byte_count: retained, - }); + trans.ops.push(Op::Retain(retained)); } trans.ops.push(Op::Replace { old: old_range, @@ -101,30 +115,119 @@ impl Transaction { use Op::*; 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 { - match (op1, op2) { - //------------------------------- - // Move past the unchanged bits. - (Some(Retain { byte_count: bc }), _) => { - range1.1 += bc; - op1 = ops1.next(); + match (next_op1, next_op2) { + // Done! + (None, None) => break, + + // One of the transactions is empty, so just append the + // 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; - op2 = ops2.next(); + //---------------- + // Retain, Retain + (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 - // source transactions. + //----------------- + // Replace, Retain + (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 { old: old1, @@ -135,65 +238,54 @@ impl Transaction { new: new2, }), ) => { - // This is the complex case. - todo!() - } + let op1 = next_op1.unwrap(); + let op2 = next_op2.unwrap(); - //------------------------------------------------------ - // Handle changes when there are only changes remaining - // in one of the source transactions. - (Some(Replace { old, new }), None) => { - if range1.0 < range1.1 { - trans.ops.push(Retain { - byte_count: range1.1 - range1.0, + if op1.len_post() < op2.len_pre() { + trans.push_op( + Replace { + old: old1, + new: (0, 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; - } - - let ti = trans.buffer.len(); - trans.buffer.push_str(&self.buffer[old.0..old.1]); - let old_range = (ti, ti + (old.1 - old.0)); - - let ti = trans.buffer.len(); - trans.buffer.push_str(&self.buffer[new.0..new.1]); - let new_range = (ti, ti + (new.1 - new.0)); - - trans.ops.push(Replace { - old: old_range, - new: new_range, - }); - - op1 = ops1.next(); - } - - (None, Some(Replace { old, new })) => { - if range2.0 < range2.1 { - trans.ops.push(Retain { - byte_count: range2.1 - range2.0, + } else if op1.len_post() > op2.len_pre() { + trans.push_op( + Replace { + old: old1, + new: (0, 0), + }, + &self.buffer, + ); + trans.push_op( + Replace { + old: (0, 0), + new: new2, + }, + &other.buffer, + ); + next_op1 = Some(Replace { + old: (0, 0), + new: (new1.0 + op2.len_pre(), new1.1), }); - 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(); for op in inverted.ops.iter_mut() { match *op { - Op::Retain { .. } => {} // Do nothing. + Op::Retain(_) => {} // Do nothing. Op::Replace { ref mut old, ref mut new, @@ -225,11 +317,11 @@ impl Transaction { } /// 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; for op in self.ops.iter() { match op { - Op::Retain { byte_count } => { + Op::Retain(byte_count) => { i += byte_count; } Op::Replace { old, new } => { @@ -257,4 +349,87 @@ impl Transaction { pub fn apply_to_marks(&self, _marks: &mut MarkSet) { 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(()) + } } diff --git a/sub_crates/backend/tests/transaction.proptest-regressions b/sub_crates/backend/tests/transaction.proptest-regressions new file mode 100644 index 0000000..6c400b2 --- /dev/null +++ b/sub_crates/backend/tests/transaction.proptest-regressions @@ -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, "")] diff --git a/sub_crates/backend/tests/transaction.rs b/sub_crates/backend/tests/transaction.rs new file mode 100644 index 0000000..fef3a81 --- /dev/null +++ b/sub_crates/backend/tests/transaction.rs @@ -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); + } +}