use crate::transaction::{Committed, EditOp};
#[derive(Debug, Default)]
pub(crate) struct ChangeLog {
observing: bool,
ops: Vec<EditOp>,
}
impl ChangeLog {
pub(crate) fn set_observing(&mut self, on: bool) {
self.observing = on;
if !on {
self.ops = Vec::new();
}
}
pub(crate) fn record(&mut self, committed: &Committed) {
if !self.observing {
return; }
self.ops.extend(committed.forward_ops().iter().rev().cloned());
}
pub(crate) fn drain(&mut self) -> Vec<EditOp> {
core::mem::take(&mut self.ops)
}
}
#[cfg(test)]
mod tests {
use crate::{Document, EditOp, GroupingHint, OpClass};
fn replay(mirror: &mut String, ops: &[EditOp]) {
for op in ops {
mirror.replace_range(op.range.start as usize..op.range.end as usize, &op.text);
}
}
fn observed(text: &str) -> (Document, String) {
let mut d = Document::new(text).unwrap();
d.observe_changes(true);
let mirror = d.text().into_owned();
(d, mirror)
}
fn sync(d: &mut Document, mirror: &mut String, at: &str) {
let ops = d.drain_changes();
replay(mirror, &ops);
assert_eq!(*mirror, d.text(), "mirror drifted after {at}; drained {ops:?}");
}
#[test]
fn undo_and_redo_are_logged() {
let (mut d, mut mirror) = observed("hello\n");
d.edit(vec![EditOp::insert(5, " world")]).unwrap();
sync(&mut d, &mut mirror, "the edit");
assert!(d.undo());
let undo = d.drain_changes();
assert_eq!(undo, vec![EditOp::delete(5..11)], "undo logs the revert");
replay(&mut mirror, &undo);
assert_eq!(mirror, "hello\n");
assert!(d.redo());
let redo = d.drain_changes();
assert_eq!(redo, vec![EditOp::insert(5, " world")], "redo logs the replay");
replay(&mut mirror, &redo);
assert_eq!(mirror, "hello world\n");
assert_eq!(mirror, d.text());
}
#[test]
fn same_offset_inserts_replay_in_applied_order() {
let (mut d, mut mirror) = observed("abcd");
d.edit(vec![EditOp::insert(2, "A"), EditOp::insert(2, "B")]).unwrap();
assert_eq!(d.text(), "abABcd");
sync(&mut d, &mut mirror, "two inserts at one offset");
}
#[test]
fn insert_then_delete_at_one_offset_replays_in_applied_order() {
let (mut d, mut mirror) = observed("abcdef");
d.edit(vec![EditOp::insert(2, "X"), EditOp::delete(2..4)]).unwrap();
assert_eq!(d.text(), "abXef");
sync(&mut d, &mut mirror, "an insert and a delete sharing a start");
}
#[test]
fn unobserved_edits_never_allocate_a_log() {
let mut d = Document::new("abc\n").unwrap();
d.edit(vec![EditOp::insert(1, "x"), EditOp::delete(2..3)]).unwrap();
assert!(d.undo());
assert!(d.redo());
assert_eq!(d.drain_changes().capacity(), 0, "a never-observed document allocated a log");
d.observe_changes(true);
d.edit(vec![EditOp::insert(0, "y")]).unwrap();
d.observe_changes(false);
d.edit(vec![EditOp::insert(0, "z")]).unwrap();
assert!(d.undo());
assert!(d.redo());
assert_eq!(d.drain_changes().capacity(), 0, "an unobserved document kept or grew a log");
}
fn xorshift(mut state: u64) -> impl FnMut() -> u64 {
move || {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
state
}
}
fn random_batch(next: &mut impl FnMut() -> u64, len: u32) -> Vec<EditOp> {
const PIECES: &[&str] = &["a", "b", "{", "}", "\n", " ", "é", "\r\n"];
let pos = |next: &mut dyn FnMut() -> u64| (next() % u64::from(len + 1)) as u32;
let anchors = [pos(next), pos(next)];
let n = 1 + (next() % 4) as usize;
let mut cuts: Vec<u32> = (0..2 * n)
.map(|_| if next().is_multiple_of(3) { pos(next) } else { anchors[(next() % 2) as usize] })
.collect();
cuts.sort_unstable();
let ops: Vec<EditOp> = cuts
.chunks(2)
.map(|c| {
let text: String =
(0..next() % 3).map(|_| PIECES[(next() % PIECES.len() as u64) as usize]).collect();
EditOp::new(c[0]..c[1], text)
})
.collect();
let mut runs: Vec<Vec<EditOp>> = Vec::new();
for op in ops {
match runs.last_mut() {
Some(run) if run[0].range.start == op.range.start => run.push(op),
_ => runs.push(vec![op]),
}
}
for i in (1..runs.len()).rev() {
runs.swap(i, (next() % (i as u64 + 1)) as usize);
}
runs.into_iter().flatten().collect()
}
#[test]
fn replayed_log_equals_text_across_random_batches_and_undo_redo() {
let mut next = xorshift(0x5EED_C4A6_E10C_0001);
let (mut d, mut mirror) = observed("fn main() {\n let s = \"é\";\n}\n");
let (mut undos, mut redos, mut forks, mut batches) = (0, 0, 0, 0);
for step in 0..1_500 {
match next() % 10 {
0..=1 => {
for _ in 0..1 + next() % 3 {
undos += usize::from(d.undo());
}
}
2..=4 => {
let branches = d.redo_branch_count();
if branches > 1 {
assert!(d.select_redo_branch((next() % branches as u64) as usize));
forks += 1;
}
redos += usize::from(d.redo());
}
_ => {
let ops = random_batch(&mut next, d.buffer().len());
let hint = match next() % 3 {
0 => GroupingHint::discrete(),
1 => GroupingHint::mergeable(OpClass::Type),
_ => GroupingHint::mergeable(OpClass::Delete),
};
batches += usize::from(d.edit_grouped(ops, hint).is_ok_and(|c| !c.is_empty()));
}
}
sync(&mut d, &mut mirror, &format!("step {step}"));
}
assert!(
batches > 400 && undos > 300 && redos > 80 && forks > 20,
"walk too thin: {batches} batches / {undos} undos / {redos} redos / {forks} forks",
);
}
}