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.
# 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"

View File

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

View File

@ -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<Op>,
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,
} 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),
});
op1 = ops1.next();
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();
}
(None, Some(Replace { old, new })) => {
if range2.0 < range2.1 {
trans.ops.push(Retain {
byte_count: range2.1 - range2.0,
});
range2.0 = range2.1;
}
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(())
}
}

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