use super::*;
#[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
pub(super) enum Base {
Current,
Behind,
Ahead,
}
impl Base {
pub(super) fn concrete(self, revision: u64) -> u64 {
match self {
Base::Current => revision,
Base::Behind => revision.saturating_sub(1),
Base::Ahead => revision + 1,
}
}
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub(super) enum BatchOp {
Batch {
base: Base,
anchors: Vec<(usize, usize)>,
snippets: Vec<usize>,
undo_open: bool,
},
Undo,
}
fn step_batch(
op: &BatchOp,
text: &mut String,
revision: &mut u64,
groups: &mut UndoGroups,
buffer: Option<&mut Buffer>,
) -> Result<(), String> {
let BatchOp::Batch {
base,
anchors,
snippets,
undo_open,
} = op
else {
let expected = groups.undo();
if let Some(buffer) = buffer {
buffer
.undo()
.map_err(|error| format!("undo errored: {error}"))?;
let undone = buffer.text().to_string();
let want = expected.clone().unwrap_or_else(|| undone.clone());
must!(
undone == want,
"undo restored {undone:?}, expected {want:?}"
);
let now = buffer.revision().get();
must!(
now >= *revision,
"revision went back on undo ({revision} -> {now})"
);
*revision = now;
*text = undone;
} else {
if let Some(start) = expected {
*text = start;
}
*revision += 1;
}
return Ok(());
};
let edits = resolve(anchors, snippets);
let before = text.clone();
let before_revision = *revision;
let expected = expect_batch(
&before,
before_revision,
base.concrete(before_revision),
&edits,
false,
);
if let Some(buffer) = buffer {
let journal_before = buffer.changes().len();
let depth_before = buffer.history().depth();
let prepared = buffer.prepare_replacements(
BufferRevision::new(base.concrete(before_revision)),
edits.clone(),
);
match (&expected, prepared) {
(Ok(prediction), Ok(prepared)) => {
let got = buffer.apply_prepared(prepared, *undo_open);
must!(
got == Ok(BufferRevision::new(before_revision + prediction.delta)),
"applied to revision {got:?}, expected {}",
before_revision + prediction.delta
);
must!(
*buffer.text() == prediction.text,
"text {:?} after batch, expected {:?}",
buffer.text().to_string(),
prediction.text
);
let appended = &buffer.changes()[journal_before..];
must!(
appended.len() as u64 == prediction.delta,
"journal appended {} entries for {} revisions",
appended.len(),
prediction.delta
);
for (entry, want) in appended.iter().zip(&prediction.entries) {
must!(
entry.origin == ChangeOrigin::User,
"journal origin is not User"
);
must!(
entry.revision.get() == want.revision,
"journal revision {} at {:?}, expected {}",
entry.revision.get(),
(want.start_byte, want.old_end_byte),
want.revision
);
let edit = entry.edit;
must!(
(edit.start_byte, edit.old_end_byte, edit.new_end_byte)
== (want.start_byte, want.old_end_byte, want.new_end_byte),
"journal bytes {:?} for revision {}, expected {:?} (pre-edit geometry)",
(edit.start_byte, edit.old_end_byte, edit.new_end_byte),
entry.revision.get(),
(want.start_byte, want.old_end_byte, want.new_end_byte)
);
must!(
(edit.start_point, edit.old_end_point, edit.new_end_point)
== (want.start_point, want.old_end_point, want.new_end_point),
"journal points {:?} for revision {}, expected {:?}",
(edit.start_point, edit.old_end_point, edit.new_end_point),
entry.revision.get(),
(want.start_point, want.old_end_point, want.new_end_point)
);
}
buffer.clear_changes(); }
(Err(want), Err(got)) => {
must!(got == *want, "rejected with {got:?}, expected {want:?}");
must!(*buffer.text() == before, "rejected batch changed the text");
must!(
buffer.revision().get() == before_revision,
"rejected batch moved the revision"
);
must!(
buffer.changes().len() == journal_before,
"rejected batch appended journal entries"
);
must!(
buffer.history().depth() == depth_before,
"rejected batch changed the history depth"
);
}
(expected, prepared) => {
return Err(match (expected.is_ok(), prepared.is_ok()) {
(true, false) => "prepare rejected a batch the model accepts".into(),
_ => "prepare accepted a batch the model rejects".into(),
});
}
}
}
if let Ok(prediction) = expected {
groups.batch_landed(&before, prediction.delta, *undo_open);
*text = prediction.text;
*revision += prediction.delta;
}
Ok(())
}
pub(super) fn gen_batch_stream(seed: u64, steps: usize) -> Vec<BatchOp> {
let mut rng = Rng(seed);
let mut text = TEXTS[(seed % TEXTS.len() as u64) as usize].to_string();
let mut revision = 0u64;
let mut groups = UndoGroups::default();
let mut ops = Vec::new();
while ops.len() < steps {
let count = rng.below(3) + 1;
let overlap = rng.below(100) < 20;
let mut anchors: Vec<(usize, usize)> = Vec::new();
let mut snippets = Vec::new();
for index in 0..count {
let (start, end) = if overlap && index > 0 {
let start = anchors[0].0;
(start, (start + 1 + rng.below(2)).min(text.len() + 1))
} else {
(rng.below(text.len() + 1), rng.below(text.len() + 1))
};
anchors.push((start, end));
snippets.push(rng.below(SNIPPETS.len()));
}
if rng.below(100) < 10 {
let index = rng.below(anchors.len());
let start = text.len() + 1 + rng.below(2);
anchors[index] = (start, start + rng.below(2));
}
let base = if rng.below(100) < 85 {
Base::Current
} else if revision > 0 {
Base::Behind
} else {
Base::Ahead
};
let undo_open = rng.below(4) == 0;
let op = BatchOp::Batch {
base,
anchors,
snippets,
undo_open,
};
step_batch(&op, &mut text, &mut revision, &mut groups, None)
.expect("generator model diverged from itself");
ops.push(op);
if rng.below(100) < 12 {
step_batch(&BatchOp::Undo, &mut text, &mut revision, &mut groups, None)
.expect("generator model diverged from itself");
ops.push(BatchOp::Undo);
}
}
ops
}
pub(super) fn run_batch_stream(ops: &[BatchOp]) -> Option<String> {
let start = TEXTS[0];
let mut text = start.to_string();
let mut revision = 0u64;
let mut groups = UndoGroups::default();
let mut buffer = Buffer::from_text(start);
for (index, op) in ops.iter().enumerate() {
step_batch(op, &mut text, &mut revision, &mut groups, Some(&mut buffer))
.err()
.map(|message| format!("op {index} {op:?}: {message}"))
.map(Some)
.unwrap_or(None)?;
}
None
}