use crate::cursor::CursorPosition;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum EditOperation {
Insert {
pos: CursorPosition,
text: String,
},
Delete {
pos: CursorPosition,
text: String,
},
Replace {
pos: CursorPosition,
old_text: String,
new_text: String,
},
}
impl EditOperation {
pub fn inverse(&self) -> Self {
match self {
Self::Insert { pos, text } => Self::Delete {
pos: *pos,
text: text.clone(),
},
Self::Delete { pos, text } => Self::Insert {
pos: *pos,
text: text.clone(),
},
Self::Replace {
pos,
old_text,
new_text,
} => Self::Replace {
pos: *pos,
old_text: new_text.clone(),
new_text: old_text.clone(),
},
}
}
}
#[derive(Clone, Debug)]
pub struct UndoStack {
undo_stack: Vec<EditOperation>,
redo_stack: Vec<EditOperation>,
max_history: usize,
}
impl UndoStack {
pub fn new(max_history: usize) -> Self {
Self {
undo_stack: Vec::new(),
redo_stack: Vec::new(),
max_history,
}
}
pub fn push(&mut self, op: EditOperation) {
self.redo_stack.clear();
self.undo_stack.push(op);
if self.undo_stack.len() > self.max_history {
self.undo_stack.remove(0);
}
}
pub fn undo(&mut self) -> Option<EditOperation> {
let op = self.undo_stack.pop()?;
let inverse = op.inverse();
self.redo_stack.push(op);
Some(inverse)
}
pub fn redo(&mut self) -> Option<EditOperation> {
let op = self.redo_stack.pop()?;
let result = op.clone();
self.undo_stack.push(op);
Some(result)
}
pub fn can_undo(&self) -> bool {
!self.undo_stack.is_empty()
}
pub fn can_redo(&self) -> bool {
!self.redo_stack.is_empty()
}
pub fn clear(&mut self) {
self.undo_stack.clear();
self.redo_stack.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
fn insert_op(line: usize, col: usize, text: &str) -> EditOperation {
EditOperation::Insert {
pos: CursorPosition::new(line, col),
text: text.to_string(),
}
}
fn delete_op(line: usize, col: usize, text: &str) -> EditOperation {
EditOperation::Delete {
pos: CursorPosition::new(line, col),
text: text.to_string(),
}
}
#[test]
fn push_and_undo() {
let mut stack = UndoStack::new(100);
stack.push(insert_op(0, 0, "hello"));
assert!(stack.can_undo());
let inv = stack.undo();
match inv {
Some(EditOperation::Delete { ref text, .. }) if text == "hello" => {}
other => unreachable!("expected Delete('hello'), got {other:?}"),
}
}
#[test]
fn undo_then_redo() {
let mut stack = UndoStack::new(100);
stack.push(insert_op(0, 0, "a"));
let _inv = stack.undo();
assert!(stack.can_redo());
let redo = stack.redo();
match redo {
Some(EditOperation::Insert { ref text, .. }) if text == "a" => {}
other => unreachable!("expected Insert('a'), got {other:?}"),
}
}
#[test]
fn push_clears_redo() {
let mut stack = UndoStack::new(100);
stack.push(insert_op(0, 0, "a"));
let _inv = stack.undo();
assert!(stack.can_redo());
stack.push(insert_op(0, 0, "b"));
assert!(!stack.can_redo());
}
#[test]
fn undo_multiple() {
let mut stack = UndoStack::new(100);
stack.push(insert_op(0, 0, "a"));
stack.push(insert_op(0, 1, "b"));
stack.push(insert_op(0, 2, "c"));
match stack.undo() {
Some(EditOperation::Delete { ref text, .. }) if text == "c" => {}
other => unreachable!("expected Delete('c'), got {other:?}"),
}
match stack.undo() {
Some(EditOperation::Delete { ref text, .. }) if text == "b" => {}
other => unreachable!("expected Delete('b'), got {other:?}"),
}
match stack.undo() {
Some(EditOperation::Delete { ref text, .. }) if text == "a" => {}
other => unreachable!("expected Delete('a'), got {other:?}"),
}
assert!(!stack.can_undo());
}
#[test]
fn max_history_limit() {
let mut stack = UndoStack::new(3);
stack.push(insert_op(0, 0, "a"));
stack.push(insert_op(0, 1, "b"));
stack.push(insert_op(0, 2, "c"));
stack.push(insert_op(0, 3, "d"));
let _d = stack.undo();
let _c = stack.undo();
match stack.undo() {
Some(EditOperation::Delete { ref text, .. }) if text == "b" => {}
other => unreachable!("expected Delete('b'), got {other:?}"),
}
assert!(!stack.can_undo()); }
#[test]
fn inverse_insert() {
let op = insert_op(1, 5, "hello");
match op.inverse() {
EditOperation::Delete { pos, ref text } => {
assert!(pos == CursorPosition::new(1, 5));
assert!(text == "hello");
}
other => unreachable!("expected Delete, got {other:?}"),
}
}
#[test]
fn inverse_delete() {
let op = delete_op(2, 3, "world");
match op.inverse() {
EditOperation::Insert { pos, ref text } => {
assert!(pos == CursorPosition::new(2, 3));
assert!(text == "world");
}
other => unreachable!("expected Insert, got {other:?}"),
}
}
#[test]
fn inverse_replace() {
let op = EditOperation::Replace {
pos: CursorPosition::new(0, 0),
old_text: "foo".to_string(),
new_text: "bar".to_string(),
};
match op.inverse() {
EditOperation::Replace {
ref old_text,
ref new_text,
..
} => {
assert!(old_text == "bar");
assert!(new_text == "foo");
}
other => unreachable!("expected Replace, got {other:?}"),
}
}
#[test]
fn clear_resets_both_stacks() {
let mut stack = UndoStack::new(100);
stack.push(insert_op(0, 0, "a"));
let _inv = stack.undo();
assert!(stack.can_redo());
stack.clear();
assert!(!stack.can_undo());
assert!(!stack.can_redo());
}
#[test]
fn empty_stack_undo_redo_returns_none() {
let mut stack = UndoStack::new(100);
assert!(stack.undo().is_none());
assert!(stack.redo().is_none());
}
}