#![doc = include_str!("../README.md")]
#![warn(missing_docs)]
#![forbid(unsafe_code)]
#![deny(clippy::all, clippy::pedantic, clippy::cargo, clippy::nursery)]
use std::ops::Deref;
pub struct Undo<'state, TState> {
initial_state: TState,
current_state: TState,
updates: Vec<Box<dyn Fn(&mut TState) + 'state>>,
nb_updates: usize,
}
impl<'state, TState: Clone> Undo<'state, TState> {
pub fn new(state: TState) -> Self {
Self {
current_state: state.clone(),
initial_state: state,
updates: Vec::new(),
nb_updates: 0,
}
}
pub fn unwrap(self) -> TState {
self.current_state
}
pub fn update(&mut self, update_fn: impl Fn(&mut TState) + 'state) {
if self.nb_updates != self.updates.len() {
self.updates.truncate(self.nb_updates);
}
update_fn(&mut self.current_state);
self.updates.push(Box::new(update_fn));
self.nb_updates += 1;
}
pub fn undo(&mut self) {
if self.nb_updates == 0 {
return;
}
self.nb_updates -= 1;
self.current_state = self.initial_state.clone();
for update_fn in self.updates[..self.nb_updates].iter() {
update_fn(&mut self.current_state);
}
}
pub fn redo(&mut self) {
if self.nb_updates == self.updates.len() {
return;
}
self.updates[self.nb_updates](&mut self.current_state);
self.nb_updates += 1;
}
}
impl<'state, TState: Clone> Deref for Undo<'state, TState> {
type Target = TState;
fn deref(&self) -> &Self::Target {
&self.current_state
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Clone)]
struct Counter {
count: u64,
}
#[test]
fn it_can_undo_and_redo_updates() {
let mut counter = Undo::new(Counter { count: 0 });
assert_eq!(counter.count, 0);
counter.update(|c| c.count = 5);
assert_eq!(counter.count, 5);
counter.update(|c| c.count += 3);
assert_eq!(counter.count, 8);
counter.undo();
assert_eq!(counter.count, 5);
counter.undo();
assert_eq!(counter.count, 0);
counter.redo();
assert_eq!(counter.count, 5);
counter.redo();
assert_eq!(counter.count, 8);
}
#[test]
fn it_does_nothing_on_too_many_undo_or_redo() {
let mut counter = Undo::new(Counter { count: 3 });
counter.undo();
assert_eq!(counter.count, 3);
counter.redo();
assert_eq!(counter.count, 3);
counter.update(|c| c.count = 8);
assert_eq!(counter.count, 8);
counter.undo();
counter.undo();
counter.undo();
assert_eq!(counter.count, 3);
counter.redo();
counter.redo();
counter.redo();
counter.redo();
assert_eq!(counter.count, 8);
}
#[test]
fn it_discards_previous_updates_when_updating_after_an_undo() {
let mut counter = Undo::new(Counter { count: 0 });
counter.update(|c| c.count += 2);
counter.update(|c| c.count += 2);
counter.update(|c| c.count += 2);
counter.update(|c| c.count += 2);
counter.update(|c| c.count += 2);
assert_eq!(counter.count, 10);
counter.undo(); counter.undo(); counter.undo(); counter.redo(); assert_eq!(counter.count, 6);
counter.update(|c| c.count += 10); assert_eq!(counter.count, 16);
counter.redo(); counter.redo(); counter.undo(); counter.undo(); assert_eq!(counter.count, 4);
counter.redo(); counter.redo(); counter.redo(); assert_eq!(counter.count, 16);
}
#[test]
fn it_unwraps_the_inner_value() {
let mut counter = Undo::new(Counter { count: 0 });
counter.update(|c| c.count = 5);
let counter: Counter = counter.unwrap();
assert_eq!(counter.count, 5);
}
#[test]
fn it_works_with_string() {
let mut input_text = Undo::new(String::new());
input_text.update(|text| text.push('H')); input_text.update(|text| text.push('e')); input_text.update(|text| text.push('l')); input_text.update(|text| text.push('k')); input_text.update(|text| text.push('o')); input_text.undo(); input_text.undo(); input_text.undo(); input_text.redo(); input_text.update(|text| text.push('l')); input_text.update(|text| text.push('o'));
let result: String = input_text.unwrap();
assert_eq!(result, "Hello");
}
#[test]
fn it_works_with_capturing_closures() {
let to_add = String::from(" world !");
let mut message = Undo::new(String::from("Hello"));
message.update(|text| text.push_str(&to_add));
message.undo();
assert_eq!(*message, "Hello");
message.redo();
assert_eq!(*message, "Hello world !");
}
}