use proptest::prelude::*;
use std::ops::Range;
use crate::editor::buffer::Buffer;
use crate::editor::position::Position;
use crate::editor::selection::Selection;
#[derive(Debug, Clone)]
enum Op {
Insert(String),
DeleteBackward,
DeleteForward,
InsertNewline,
Undo,
Redo,
MoveCursorLeft,
MoveCursorRight,
SetSelection(Option<Range<Position>>),
}
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(),
Op::Undo => {
buf.undo();
}
Op::Redo => {
buf.redo();
}
Op::MoveCursorLeft => {
buf.cursor_left();
}
Op::MoveCursorRight => {
buf.cursor_right();
}
Op::SetSelection(range) => {
if let Some(r) = range {
buf.selection = Some(Selection::new(r.start, r.end));
} else {
buf.selection = None;
}
}
}
}
}
fn strategy_for_text() -> impl Strategy<Value = String> {
prop_oneof![
Just("a".to_string()),
Just("hello".to_string()),
Just(" ".to_string()),
Just("\n".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![
strategy_for_text().prop_map(Op::Insert),
Just(Op::DeleteBackward),
Just(Op::DeleteForward),
Just(Op::InsertNewline),
Just(Op::Undo),
Just(Op::Redo),
],
0..max_len,
)
}
proptest! {
#[test]
fn test_undo_restores_previous_state(ref ops in gen_ops(50)) {
let mut buf = Buffer::from_text("");
let mut snapshots = Vec::new();
for op in ops.iter() {
if !matches!(op, Op::Undo | Op::Redo) {
snapshots.push((buf.text(), buf.cursor(), buf.selection.clone()));
}
op.apply(&mut buf);
}
prop_assert!(buf.can_undo() || ops.iter().all(|op| matches!(op, Op::Undo | Op::Redo)));
let mut undo_count = 0;
while buf.can_undo() && undo_count < ops.len() {
prop_assert!(buf.undo());
undo_count += 1;
if undo_count <= snapshots.len() {
let (expected_text, expected_cursor, expected_selection) = &snapshots[snapshots.len() - undo_count];
prop_assert_eq!(buf.text(), *expected_text, "Text mismatch after {} undos", undo_count);
}
}
}
#[test]
fn test_undo_redo_roundtrip(ref ops in gen_ops(30)) {
let mut buf = Buffer::from_text("initial");
let initial_text = buf.text();
let initial_cursor = buf.cursor();
for op in ops.iter() {
if !matches!(op, Op::Undo | Op::Redo) {
op.apply(&mut buf);
}
}
let text_before_undo = buf.text();
let cursor_before_undo = buf.cursor();
buf.undo();
buf.redo();
prop_assert_eq!(buf.text(), text_before_undo, "Redo should restore text");
prop_assert_eq!(buf.cursor(), cursor_before_undo, "Redo should restore cursor");
}
#[test]
fn test_random_operations_undo_redo(ref ops in gen_ops(20)) {
let mut buf = Buffer::from_text("test");
let initial = (buf.text(), buf.cursor());
for op in ops {
op.apply(&mut buf);
}
let ops_count = ops.iter().filter(|o| !matches!(o, Op::Undo | Op::Redo)).count();
for _ in 0..ops_count {
if buf.can_undo() {
buf.undo();
}
}
prop_assert_eq!(buf.text(), initial.0, "Full undo should restore initial text");
prop_assert_eq!(buf.cursor(), initial.1, "Full undo should restore initial cursor");
}
}
#[test]
fn test_cursor_at_start() {
let mut buf = Buffer::from_text("hello");
buf.set_cursor(Position::new(0, 0));
buf.delete_backward();
assert_eq!(buf.text(), "hello");
buf.undo();
assert_eq!(buf.text(), "hello");
}
#[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_empty_buffer_operations() {
let mut buf = Buffer::from_text("");
buf.insert("a");
assert_eq!(buf.text(), "a");
buf.undo();
assert_eq!(buf.text(), "");
buf.redo();
assert_eq!(buf.text(), "a");
}
#[test]
fn test_empty_buffer_delete() {
let mut buf = Buffer::from_text("");
buf.delete_backward();
assert_eq!(buf.text(), "");
buf.delete_forward();
assert_eq!(buf.text(), "");
}
#[test]
fn test_utf8_multibyte_characters() {
let mut buf = Buffer::from_text("");
buf.insert("héllo");
assert_eq!(buf.text(), "héllo");
assert_eq!(buf.cursor().column, 6);
buf.insert("世界");
assert_eq!(buf.text(), "héllo世界");
assert_eq!(buf.cursor().column, 12);
buf.undo();
assert_eq!(buf.text(), "héllo");
buf.undo();
assert_eq!(buf.text(), "");
buf.redo();
assert_eq!(buf.text(), "héllo");
buf.redo();
assert_eq!(buf.text(), "héllo世界");
}
#[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.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_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\n");
buf.undo();
assert_eq!(buf.text(), "a");
buf.undo();
assert_eq!(buf.text(), "");
}
#[test]
fn test_delete_word_and_undo() {
let mut buf = Buffer::from_text("hello world test");
buf.set_cursor(Position::new(0, 16));
buf.delete_word_backward();
assert_eq!(buf.text(), "hello world ");
buf.undo();
assert_eq!(buf.text(), "hello world test");
}
#[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("");
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();
assert_eq!(buf.text(), "AB");
buf.undo();
assert_eq!(buf.text(), "A");
buf.undo();
assert_eq!(buf.text(), "");
buf.redo();
buf.redo();
buf.redo();
buf.redo();
assert_eq!(buf.text(), "ABC");
}
#[test]
fn test_utf8_edit_at_line_end_lf() {
let mut buf = Buffer::from_text("# ∞\nline2");
buf.set_cursor(Position::new(0, 3));
buf.insert("test");
assert_eq!(buf.text(), "# ∞test\nline2");
assert_eq!(buf.cursor(), Position::new(0, 7)); }
#[test]
fn test_utf8_edit_at_line_end_crlf() {
let mut buf = Buffer::from_text("# ∞\r\nline2");
buf.set_cursor(Position::new(0, 3));
buf.insert("test");
assert_eq!(buf.text(), "# ∞test\r\nline2");
assert_eq!(buf.cursor(), Position::new(0, 7)); }
#[test]
fn test_utf8_backspace_at_line_end_lf() {
let mut buf = Buffer::from_text("# ∞x\nline2");
buf.set_cursor(Position::new(0, 4));
buf.delete_backward();
assert_eq!(buf.text(), "# ∞\nline2");
assert_eq!(buf.cursor(), Position::new(0, 3));
}
#[test]
fn test_utf8_backspace_at_line_end_crlf() {
let mut buf = Buffer::from_text("# ∞x\r\nline2");
buf.set_cursor(Position::new(0, 4));
buf.delete_backward();
assert_eq!(buf.text(), "# ∞\r\nline2");
assert_eq!(buf.cursor(), Position::new(0, 3));
}
#[test]
fn test_multiple_utf8_chars_at_line_end() {
let mut buf = Buffer::from_text("emoji: 🚀✨\nmore");
let line_len = "emoji: 🚀✨".chars().count();
buf.set_cursor(Position::new(0, line_len));
buf.insert("!");
assert_eq!(buf.text(), "emoji: 🚀✨!\nmore");
assert_eq!(buf.cursor().column, line_len + 1);
}