use std::{ collections::{HashMap, HashSet}, rc::Rc, }; use crate::{CommandError, File, FileId}; #[derive(Debug, Default)] pub struct History { entries: Vec, index: Option, /// Whether the first entry is the state the history starts from, which is not undone: files /// restored from the storage. floor: bool, } impl History { /// A history whose first entry is `entry`, from which nothing can be undone. pub fn restored(entry: Files) -> Self { Self { entries: vec![entry], index: Some(0), floor: true, } } pub fn current(&self) -> Option<&Files> { self.index.map_or_default(|i| self.get(i)) } /// Applies `f` to a copy of the current entry and makes the copy the new current entry. /// /// Nothing is pushed, and `None` is returned, if `f` fails (the error is returned) or if it /// leaves every file as it was: a step that undoes nothing is not worth a place in the /// history. Files are told apart by their identity, not their content. pub fn create_and_push_next(&mut self, f: F) -> Result, CommandError> where F: FnOnce(&mut Files) -> Result<(), CommandError>, { let mut next = self.current().map_or_default(|c| c.clone()); f(&mut next)?; if self.is_current(&next) { return Ok(None); } Ok(self.record_diff(|history| history.push(next))) } /// Whether `entry` holds exactly the files of the current entry. fn is_current(&self, entry: &Files) -> bool { let current = self.current(); let len = current.map_or(0, |current| current.len()); entry.len() == len && current.is_none_or(|current| { entry .iter() .all(|(id, file)| current.get(id).is_some_and(|c| Rc::ptr_eq(c, file))) }) } pub fn undo(&mut self) -> Option { self.record_diff(|history| { if let Some(i) = history.index { if i == 0 { if !history.floor { history.index = None; } } else { history.index = Some(i - 1); } } }) } pub fn redo(&mut self) -> Option { self.record_diff(|history| match history.index { Some(i) => { if i + 1 < history.entries.len() { history.index = Some(i + 1); } } None => { if !history.entries.is_empty() { history.index = Some(0); } } }) } pub fn can_undo(&self) -> bool { self.index.is_some_and(|i| i > 0 || !self.floor) } pub fn can_redo(&self) -> bool { match self.index { Some(i) => i + 1 < self.entries.len(), None => !self.entries.is_empty(), } } fn get(&self, index: usize) -> Option<&Files> { self.entries.get(index) } fn push(&mut self, entry: Files) { if let Some(i) = self.index { self.entries.truncate(i + 1); } self.entries.push(entry); self.index = Some(self.entries.len() - 1); } fn record_diff(&mut self, f: F) -> Option where F: FnOnce(&mut Self), { let prev = self.index; f(self); let cur = self.index; if prev == cur { return None; } Some(Self::diff_between( prev.map_or_default(|i| self.get(i)), cur.map_or_default(|i| self.get(i)), )) } /// Which files differ between two states. pub fn diff_between(prev: Option<&Files>, cur: Option<&Files>) -> Diff { let prev_ids: HashSet = prev.map_or_default(|e| e.keys().copied().collect()); let cur_ids: HashSet = cur.map_or_default(|e| e.keys().copied().collect()); let mut modified = vec![]; for id in prev_ids.intersection(&cur_ids) { let before = prev.map_or_default(|e| e.get(id)); let after = cur.map_or_default(|e| e.get(id)); if !before.zip(after).is_some_and(|(b, a)| Rc::ptr_eq(b, a)) { modified.push(*id); } } Diff { added: cur_ids.difference(&prev_ids).copied().collect(), removed: prev_ids.difference(&cur_ids).copied().collect(), modified, } } } pub type Files = HashMap>; /// Which files were added, removed or modified by a change of the current history entry. #[derive(Debug, Default, Clone, PartialEq, Eq)] pub struct Diff { pub added: Vec, pub removed: Vec, pub modified: Vec, } #[cfg(test)] mod tests { use super::*; impl History { /// A step that is expected not to fail. fn push_ok( &mut self, f: impl FnOnce(&mut Files) -> Result<(), CommandError>, ) -> Option { self.create_and_push_next(f).unwrap() } } #[test] fn test_new_file() { let mut history = History::default(); assert!(!history.can_undo()); assert!(!history.can_redo()); assert!(history.current().is_none()); let diff = history.push_ok(|e| { let file = Rc::new(File::default()); e.insert(file.id, file); Ok(()) }); assert!(history.can_undo()); assert!(!history.can_redo()); assert!(history.current().is_some()); assert!(diff.is_some()); let diff = diff.unwrap(); assert_eq!(diff.added.len(), 1); assert!(diff.removed.is_empty()); assert!(diff.modified.is_empty()); } fn add_file(history: &mut History) -> FileId { let file = Rc::new(File::default()); let id = file.id; history .push_ok(|e| { e.insert(id, file); Ok(()) }) .unwrap(); id } #[test] fn test_failed_command_pushes_nothing() { let mut history = History::default(); let diff = history.create_and_push_next(|e| { e.insert(FileId::default(), Rc::new(File::default())); Err(CommandError::InvalidData("nope".to_string())) }); assert_eq!(diff, Err(CommandError::InvalidData("nope".to_string()))); assert!(!history.can_undo()); assert!(history.current().is_none()); } #[test] fn test_undo_redo() { let mut history = History::default(); assert!(history.undo().is_none()); assert!(history.redo().is_none()); let id = add_file(&mut history); let diff = history.undo().unwrap(); assert_eq!(diff.removed, vec![id]); assert!(diff.added.is_empty()); assert!(history.current().is_none()); assert!(!history.can_undo()); assert!(history.can_redo()); assert!(history.undo().is_none()); let diff = history.redo().unwrap(); assert_eq!(diff.added, vec![id]); assert!(history.current().unwrap().contains_key(&id)); assert!(history.can_undo()); assert!(!history.can_redo()); assert!(history.redo().is_none()); } #[test] fn test_modified_and_removed() { let mut history = History::default(); let id = add_file(&mut history); let diff = history .push_ok(|e| { let mut file = (**e.get(&id).unwrap()).clone(); file.info.name = "renamed".to_string(); e.insert(id, Rc::new(file)); Ok(()) }) .unwrap(); assert_eq!(diff.modified, vec![id]); assert!(diff.added.is_empty() && diff.removed.is_empty()); // a step that changes nothing is not a step assert!(history.push_ok(|_| Ok(())).is_none()); assert!( history .push_ok(|e| { let file = e[&id].clone(); e.insert(id, file); Ok(()) }) .is_none() ); let diff = history .push_ok(|e| { e.remove(&id); Ok(()) }) .unwrap(); assert_eq!(diff.removed, vec![id]); assert!(history.current().unwrap().is_empty()); // going back restores the renamed file history.undo(); assert_eq!(history.current().unwrap()[&id].info.name, "renamed"); } #[test] fn test_a_file_is_modified_when_it_is_not_the_same_one_anymore() { let mut history = History::default(); let id = add_file(&mut history); // a copy, even if it has the same content, is another file for the history: files are // told apart by their identity, comparing them would read all of them let diff = history .push_ok(|e| { let copy = (**e.get(&id).unwrap()).clone(); e.insert(id, Rc::new(copy)); Ok(()) }) .unwrap(); assert_eq!(diff.modified, vec![id]); // and the files that were not replaced are not let other = add_file(&mut history); let diff = history .push_ok(|e| { let copy = (**e.get(&other).unwrap()).clone(); e.insert(other, Rc::new(copy)); Ok(()) }) .unwrap(); assert_eq!(diff.modified, vec![other]); } #[test] fn test_push_after_undo_drops_redo_branch() { let mut history = History::default(); let first = add_file(&mut history); let second = add_file(&mut history); history.undo(); assert!(history.can_redo()); assert!(!history.current().unwrap().contains_key(&second)); let third = add_file(&mut history); assert!(!history.can_redo()); assert!(history.redo().is_none()); let current = history.current().unwrap(); assert!(current.contains_key(&first)); assert!(current.contains_key(&third)); assert!(!current.contains_key(&second)); } fn restored_entry() -> (Files, FileId) { let file = Rc::new(File::default()); let id = file.id; (Files::from([(id, file)]), id) } #[test] fn test_restored_history_starts_from_its_entry() { let (entry, id) = restored_entry(); let mut history = History::restored(entry); assert!(history.current().unwrap().contains_key(&id)); // there is nothing to go back to, nor forward to assert!(!history.can_undo()); assert!(!history.can_redo()); assert!(history.undo().is_none()); assert!(history.redo().is_none()); assert!(history.current().unwrap().contains_key(&id)); } #[test] fn test_restored_history_cannot_undo_an_empty_entry_either() { let mut history = History::restored(Files::new()); assert!(history.current().is_some_and(|files| files.is_empty())); assert!(!history.can_undo()); assert!(history.undo().is_none()); // the history is not the one of an engine that has done nothing assert!(history.current().is_some()); } #[test] fn test_restored_history_undoes_what_comes_after_it_and_no_further() { let (entry, restored) = restored_entry(); let mut history = History::restored(entry); let added = add_file(&mut history); assert!(history.can_undo()); assert!(!history.can_redo()); assert_eq!(history.current().unwrap().len(), 2); // back to the restored files let diff = history.undo().unwrap(); assert_eq!(diff.removed, vec![added]); assert!(diff.added.is_empty() && diff.modified.is_empty()); assert_eq!(history.current().unwrap().len(), 1); assert!(history.current().unwrap().contains_key(&restored)); assert!(!history.can_undo()); assert!(history.can_redo()); assert!(history.undo().is_none()); assert!(history.current().unwrap().contains_key(&restored)); let diff = history.redo().unwrap(); assert_eq!(diff.added, vec![added]); assert!(history.can_undo()); assert!(!history.can_redo()); // and again assert!(history.undo().is_some()); assert!(history.undo().is_none()); assert!(history.current().unwrap().contains_key(&restored)); } #[test] fn test_restored_history_removing_its_files_can_be_undone() { let (entry, restored) = restored_entry(); let mut history = History::restored(entry); let diff = history .push_ok(|e| { e.remove(&restored); Ok(()) }) .unwrap(); assert_eq!(diff.removed, vec![restored]); assert!(history.current().unwrap().is_empty()); let diff = history.undo().unwrap(); assert_eq!(diff.added, vec![restored]); assert!(history.current().unwrap().contains_key(&restored)); assert!(!history.can_undo()); } #[test] fn test_restored_history_push_after_undo_drops_redo_branch() { let (entry, restored) = restored_entry(); let mut history = History::restored(entry); add_file(&mut history); history.undo().unwrap(); assert!(history.can_redo()); let other = add_file(&mut history); assert!(!history.can_redo()); assert!(history.can_undo()); let diff = history.undo().unwrap(); assert_eq!(diff.removed, vec![other]); assert!(history.current().unwrap().contains_key(&restored)); assert!(!history.can_undo()); } }