use proptest::prelude::*;
use std::time::Duration;
use crate::editor::buffer::Buffer;
use crate::editor::history::ChangeKind;
use crate::editor::position::Position;
use crate::editor::selection::Selection;
#[derive(Debug, Clone)]
enum Op {
Insert(String),
DeleteBackward,
DeleteForward,
InsertNewline,
}
impl Op {
fn apply(&self, buf: &mut Buffer) {
match self {
Op::Insert(text) => buf.insert(text),
Op::DeleteBackward => buf.delete_backward(),
Op::DeleteForward => buf.delete_forward(),
Op::InsertNewline => buf.insert_newline(),
}
}
}
fn gen_insert_text() -> impl Strategy<Value = String> {
prop_oneof![
Just("a".to_string()),
Just("hello".to_string()),
Just(" ".to_string()),
Just("AB".to_string()),
any::<char>().prop_map(|c| c.to_string()),
]
}
fn gen_ops(max_len: usize) -> impl Strategy<Value = Vec<Op>> {
prop::collection::vec(
prop_oneof![
gen_insert_text().prop_map(Op::Insert),
Just(Op::DeleteBackward),
Just(Op::DeleteForward),
Just(Op::InsertNewline),
],
0..max_len,
)
}
proptest! {
#[test]
fn test_undo_restores_state(ref ops in gen_ops(20)) {
let mut buf = Buffer::from_text("");
let mut snapshots: Vec<String> = Vec::new();
for op in ops.iter() {
snapshots.push(buf.text());
op.apply(&mut buf);
}
while buf.can_undo() {
prop_assert!(buf.undo());
}
prop_assert_eq!(buf.text(), String::new());
}
#[test]
fn test_undo_redo_roundtrip(ref ops in gen_ops(15)) {
let mut buf = Buffer::from_text("initial");
for op in ops.iter() {
op.apply(&mut buf);
}
let text_before_undo = buf.text();
if buf.can_undo() {
buf.undo();
buf.redo();
prop_assert_eq!(buf.text(), text_before_undo);
}
}
#[test]
fn test_random_operations_deterministic(ref ops in gen_ops(10)) {
let mut buf = Buffer::from_text("");
let initial = buf.text();
for op in ops {
op.apply(&mut buf);
}
let ops_count = ops.len();
for _ in 0..ops_count {
if buf.can_undo() {
buf.undo();
}
}
prop_assert_eq!(buf.text(), initial);
}
}
#[test]
fn test_cursor_at_end() {
let mut buf = Buffer::from_text("hello");
buf.set_cursor(Position::new(0, 5));
buf.insert("!");
assert_eq!(buf.text(), "hello!");
buf.undo();
assert_eq!(buf.text(), "hello");
}
#[test]
fn test_merge_across_lines() {
let mut buf = Buffer::from_text("");
buf.insert("a");
buf.insert_newline();
buf.insert("b");
buf.insert_newline();
buf.insert("c");
assert_eq!(buf.text(), "a\nb\nc");
buf.undo();
assert_eq!(buf.text(), "a\nb\n");
buf.undo();
assert_eq!(buf.text(), "a\nb");
buf.undo();
assert_eq!(buf.text(), "a\n");
buf.undo();
assert_eq!(buf.text(), "a");
buf.undo();
assert_eq!(buf.text(), "");
}
#[test]
fn test_merge_across_lines_debug() {
let mut buf = Buffer::from_text("");
buf.insert("a");
eprintln!(
"After 'a': text='{}', cursor={:?}",
buf.text(),
buf.cursor()
);
buf.insert_newline();
eprintln!(
"After newline: text='{}', cursor={:?}",
buf.text(),
buf.cursor()
);
buf.insert("b");
eprintln!(
"After 'b': text='{}', cursor={:?}",
buf.text(),
buf.cursor()
);
buf.insert_newline();
eprintln!(
"After newline: text='{}', cursor={:?}",
buf.text(),
buf.cursor()
);
buf.insert("c");
eprintln!(
"After 'c': text='{}', cursor={:?}",
buf.text(),
buf.cursor()
);
assert_eq!(buf.text(), "a\nb\nc");
buf.undo();
eprintln!(
"After undo 1: text='{}', cursor={:?} (expected 'a\\nb\\n')",
buf.text(),
buf.cursor()
);
assert_eq!(buf.text(), "a\nb\n");
buf.undo();
eprintln!(
"After undo 2: text='{}', cursor={:?} (expected 'a\\nb')",
buf.text(),
buf.cursor()
);
assert_eq!(buf.text(), "a\nb");
buf.undo();
eprintln!(
"After undo 3: text='{}', cursor={:?} (expected 'a\\n')",
buf.text(),
buf.cursor()
);
assert_eq!(buf.text(), "a\n");
buf.undo();
eprintln!(
"After undo 4: text='{}', cursor={:?} (expected 'a')",
buf.text(),
buf.cursor()
);
assert_eq!(buf.text(), "a");
buf.undo();
eprintln!(
"After undo 5: text='{}', cursor={:?} (expected '')",
buf.text(),
buf.cursor()
);
assert_eq!(buf.text(), "");
}
#[test]
fn test_utf8_delete_multibyte() {
let mut buf = Buffer::from_text("日本");
buf.set_cursor(Position::new(0, 2));
buf.delete_backward();
assert_eq!(buf.text(), "日");
buf.undo();
assert_eq!(buf.text(), "日本");
}
#[test]
fn test_utf8_emoji_in_middle() {
let mut buf = Buffer::from_text("a🚀b");
buf.set_cursor(Position::new(0, 2));
buf.insert("X");
assert_eq!(buf.text(), "a🚀Xb");
buf.undo();
assert_eq!(buf.text(), "a🚀b");
}
#[test]
fn test_mixed_operations_with_newline() {
let mut buf = Buffer::from_text("");
buf.insert("line1");
buf.insert_newline();
buf.insert("line2");
assert_eq!(buf.text(), "line1\nline2");
buf.undo();
assert_eq!(buf.text(), "line1\n");
buf.undo();
assert_eq!(buf.text(), "line1");
buf.undo();
assert_eq!(buf.text(), "");
buf.redo();
assert_eq!(buf.text(), "line1");
buf.redo();
assert_eq!(buf.text(), "line1\n");
buf.redo();
assert_eq!(buf.text(), "line1\nline2");
}
#[test]
fn test_selection_undo_restores() {
let mut buf = Buffer::from_text("hello world");
buf.set_selection(Some(Selection::new(
Position::new(0, 0),
Position::new(0, 6),
)));
buf.insert("hi ");
assert_eq!(buf.text(), "hi world");
buf.undo();
assert_eq!(buf.text(), "hello world");
}
#[test]
fn test_replace_and_undo() {
let mut buf = Buffer::from_text("hello world");
buf.replace_range(Position::new(0, 0), Position::new(0, 5), "goodbye");
assert_eq!(buf.text(), "goodbye world");
buf.undo();
assert_eq!(buf.text(), "hello world");
}
#[test]
fn test_multiple_undo_redo_cycles() {
let mut buf = Buffer::from_text_no_merge("");
buf.insert("A");
buf.insert("B");
buf.insert("C");
assert_eq!(buf.text(), "ABC");
buf.undo();
assert_eq!(buf.text(), "AB");
buf.redo();
assert_eq!(buf.text(), "ABC");
buf.undo();
buf.undo();
buf.undo();
assert_eq!(buf.text(), "");
buf.redo();
buf.redo();
buf.redo();
assert_eq!(buf.text(), "ABC");
}
#[test]
fn test_undo_after_new_edit_truncates_redo() {
let mut buf = Buffer::from_text_no_merge("");
buf.insert("a");
buf.insert("b");
buf.insert("c");
buf.undo();
buf.undo();
assert_eq!(buf.text(), "a");
buf.insert("X");
assert_eq!(buf.text(), "aX");
buf.undo();
assert_eq!(buf.text(), "a");
buf.undo();
assert_eq!(buf.text(), "");
}
#[test]
fn test_large_text_undo() {
let mut buf = Buffer::from_text("");
let large_text = "x".repeat(1000);
buf.insert(&large_text);
assert_eq!(buf.text(), large_text);
buf.undo();
assert_eq!(buf.text(), "");
buf.redo();
assert_eq!(buf.text(), large_text);
}
#[test]
fn test_consecutive_newlines() {
let mut buf = Buffer::from_text("");
buf.insert_newline();
buf.insert_newline();
buf.insert_newline();
assert_eq!(buf.text(), "\n\n\n");
buf.undo();
assert_eq!(buf.text(), "\n\n");
buf.undo();
assert_eq!(buf.text(), "\n");
buf.undo();
assert_eq!(buf.text(), "");
}
#[test]
fn test_backspace_at_line_start() {
let mut buf = Buffer::from_text("a\nb");
buf.set_cursor(Position::new(1, 0));
buf.delete_backward();
assert_eq!(buf.text(), "ab");
buf.undo();
assert_eq!(buf.text(), "a\nb");
}
#[test]
fn test_forward_delete_at_line_end() {
let mut buf = Buffer::from_text("ab\ncd");
buf.set_cursor(Position::new(0, 2));
buf.delete_forward();
assert_eq!(buf.text(), "abcd");
buf.undo();
assert_eq!(buf.text(), "ab\ncd");
}
#[test]
fn test_transaction_single_undo_step() {
let mut buf = Buffer::from_text("");
buf.begin_transaction(ChangeKind::InsertText);
buf.insert("a");
buf.insert("b");
buf.insert("c");
buf.end_transaction();
assert_eq!(buf.text(), "abc");
assert_eq!(buf.history_position(), 1);
buf.undo();
assert_eq!(buf.text(), "");
assert!(!buf.can_undo());
}
#[test]
fn test_transaction_redo_roundtrip() {
let mut buf = Buffer::from_text("");
buf.begin_transaction(ChangeKind::Paste);
buf.insert("hello");
buf.insert(" ");
buf.insert("world");
buf.end_transaction();
assert_eq!(buf.text(), "hello world");
buf.undo();
assert_eq!(buf.text(), "");
buf.redo();
assert_eq!(buf.text(), "hello world");
assert!(!buf.can_redo());
}
#[test]
fn test_transaction_cursor_restored_on_undo() {
let mut buf = Buffer::from_text("hello");
buf.set_cursor(Position::new(0, 2));
let cursor_before = buf.cursor();
buf.begin_transaction(ChangeKind::InsertText);
buf.insert("X");
buf.insert("Y");
buf.end_transaction();
assert_eq!(buf.text(), "heXYllo");
buf.undo();
assert_eq!(buf.text(), "hello");
assert_eq!(buf.cursor(), cursor_before);
}
#[test]
fn test_transaction_empty_is_noop() {
let mut buf = Buffer::from_text("hello");
let pos_before = buf.history_position();
buf.begin_transaction(ChangeKind::InsertText);
buf.end_transaction();
assert_eq!(buf.text(), "hello");
assert_eq!(buf.history_position(), pos_before);
}
#[test]
#[should_panic(expected = "nested transactions are not supported")]
fn test_nested_begin_transaction_panics_in_debug() {
let mut buf = Buffer::from_text("");
buf.begin_transaction(ChangeKind::InsertText);
buf.begin_transaction(ChangeKind::DeleteText);
}
#[test]
fn test_transaction_is_not_merged_with_adjacent_edits() {
let mut buf = Buffer::from_text("");
buf.insert("x");
buf.begin_transaction(ChangeKind::Paste);
buf.insert("a");
buf.insert("b");
buf.end_transaction();
buf.insert("y");
assert_eq!(buf.text(), "xaby");
assert_eq!(buf.history_position(), 3);
buf.undo();
assert_eq!(buf.text(), "xab");
buf.undo();
assert_eq!(buf.text(), "x");
}
#[test]
fn test_abort_transaction_does_not_record_history() {
let mut buf = Buffer::from_text("hello");
let history_pos_before = buf.history_position();
buf.begin_transaction(ChangeKind::InsertText);
buf.insert(" world");
buf.abort_transaction();
assert_eq!(buf.text(), "hello world"); assert_eq!(buf.history_position(), history_pos_before); assert!(!buf.can_undo()); }
#[test]
fn test_transaction_with_heterogeneous_ops() {
let mut buf = Buffer::from_text("hello world");
buf.set_cursor(Position::new(0, 5));
buf.begin_transaction(ChangeKind::Structural);
buf.delete_backward(); buf.insert("O"); buf.insert("!");
buf.end_transaction();
assert_eq!(buf.text(), "hellO! world");
buf.undo();
assert_eq!(buf.text(), "hello world");
}
#[test]
fn test_in_transaction_flag() {
let mut buf = Buffer::from_text("");
assert!(!buf.in_transaction());
buf.begin_transaction(ChangeKind::Paste);
assert!(buf.in_transaction());
buf.end_transaction();
assert!(!buf.in_transaction());
}
#[test]
fn test_merge_respects_time_gap() {
let mut buf = Buffer::from_text("");
buf.insert("a");
std::thread::sleep(Duration::from_millis(600));
buf.insert("b");
assert_eq!(buf.text(), "ab");
assert_eq!(buf.history_position(), 2);
buf.undo();
assert_eq!(buf.text(), "a");
buf.undo();
assert_eq!(buf.text(), "");
}