use text_document::{MoveMode, TextDocument};
struct Rng {
state: u64,
}
impl Rng {
fn new(seed: u64) -> Self {
Self {
state: seed.wrapping_add(1),
}
}
fn next(&mut self) -> u64 {
self.state = self
.state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
self.state
}
fn range(&mut self, bound: u64) -> u64 {
self.next() % bound.max(1)
}
}
#[derive(Debug, Clone, Copy)]
enum Op {
WrapSelection,
UnwrapCurrentFrame,
Toggle,
IncreaseDepth,
DecreaseDepth,
InsertChar,
DeletePrev,
InsertBlock,
Undo,
MoveLeft,
MoveRight,
}
fn pick_op(rng: &mut Rng) -> Op {
match rng.range(11) {
0 => Op::WrapSelection,
1 => Op::UnwrapCurrentFrame,
2 => Op::Toggle,
3 => Op::IncreaseDepth,
4 => Op::DecreaseDepth,
5 => Op::InsertChar,
6 => Op::DeletePrev,
7 => Op::InsertBlock,
8 => Op::Undo,
9 => Op::MoveLeft,
_ => Op::MoveRight,
}
}
fn apply_op(doc: &TextDocument, op: Op) {
let pos = {
let c = doc.cursor_at(0);
c.position()
};
let len = doc.character_count();
let cursor = doc.cursor_at(pos.min(len));
match op {
Op::WrapSelection => {
let _ = cursor.wrap_selection_in_blockquote();
}
Op::UnwrapCurrentFrame => {
let _ = cursor.unwrap_current_frame();
}
Op::Toggle => {
let _ = cursor.toggle_blockquote();
}
Op::IncreaseDepth => {
let _ = cursor.increase_blockquote_depth();
}
Op::DecreaseDepth => {
let _ = cursor.decrease_blockquote_depth();
}
Op::InsertChar => {
let _ = cursor.insert_text("x");
}
Op::DeletePrev => {
let _ = cursor.delete_previous_char();
}
Op::InsertBlock => {
let _ = cursor.insert_block();
}
Op::Undo => {
let _ = doc.undo();
}
Op::MoveLeft => {
cursor.move_position(text_document::MoveOperation::Left, MoveMode::MoveAnchor, 1);
}
Op::MoveRight => {
cursor.move_position(text_document::MoveOperation::Right, MoveMode::MoveAnchor, 1);
}
}
}
fn check_invariants(doc: &TextDocument, step: usize, op: Op, seed: u64) {
let plain = doc
.to_plain_text()
.unwrap_or_else(|e| panic!("seed={seed} step={step} op={op:?}: to_plain_text failed: {e}"));
let md = doc
.to_markdown()
.unwrap_or_else(|e| panic!("seed={seed} step={step} op={op:?}: to_markdown failed: {e}"));
let round = TextDocument::new();
round.set_markdown(&md).unwrap().wait().unwrap_or_else(|e| {
panic!(
"seed={seed} step={step} op={op:?}: re-import of own markdown failed: {e}\n\
markdown was: {md:?}"
)
});
let cursor = doc.cursor_at(0);
let depth = cursor.blockquote_depth_at_cursor();
let in_quote = cursor.is_in_blockquote();
assert_eq!(
in_quote,
depth > 0,
"seed={seed} step={step} op={op:?}: is_in_blockquote ({in_quote}) disagrees with \
blockquote_depth_at_cursor ({depth})"
);
let _cc = doc.character_count();
let plain2 = doc.to_plain_text().expect("repeat to_plain_text");
assert_eq!(
plain, plain2,
"seed={seed} step={step} op={op:?}: to_plain_text is non-deterministic"
);
}
fn run_seed(seed: u64, steps: usize) {
let mut rng = Rng::new(seed);
let doc = TextDocument::new();
doc.set_markdown("# Title\n\nFirst paragraph.\n\n> A quoted line.\n\nLast paragraph.\n")
.unwrap()
.wait()
.unwrap();
for step in 0..steps {
let op = pick_op(&mut rng);
apply_op(&doc, op);
check_invariants(&doc, step, op, seed);
}
}
#[test]
fn random_blockquote_ops_preserve_invariants_seed_1() {
run_seed(1, 200);
}
#[test]
fn random_blockquote_ops_preserve_invariants_seed_42() {
run_seed(42, 200);
}
#[test]
fn random_blockquote_ops_preserve_invariants_seed_2026() {
run_seed(2026, 200);
}
#[test]
fn random_blockquote_ops_long_run() {
run_seed(0xC0FFEE_DEADBEEF, 1000);
}