Files
gpx.studio/gpx-rs/src/engine/state/history.rs
T

435 lines
14 KiB
Rust
Raw Normal View History

2026-10-01 18:42:31 +02:00
use std::{
collections::{HashMap, HashSet},
rc::Rc,
};
2026-07-14 18:36:30 +02:00
2026-10-08 21:04:29 +02:00
use crate::{CommandError, File, FileId};
2026-09-18 09:03:51 +02:00
2026-10-02 18:06:45 +02:00
#[derive(Debug, Default)]
2026-10-14 08:17:51 +02:00
pub struct History {
entries: Vec<Files>,
2026-07-14 18:36:30 +02:00
index: Option<usize>,
2026-10-14 08:17:51 +02:00
/// Whether the first entry is the state the history starts from, which is not undone: files
2026-10-08 21:04:29 +02:00
/// restored from the storage.
floor: bool,
2026-07-14 18:36:30 +02:00
}
2026-10-14 08:17:51 +02:00
impl History {
/// A history whose first entry is `entry`, from which nothing can be undone.
pub fn restored(entry: Files) -> Self {
2026-10-08 18:34:19 +02:00
Self {
entries: vec![entry],
index: Some(0),
2026-10-08 21:04:29 +02:00
floor: true,
2026-10-08 18:34:19 +02:00
}
}
2026-10-14 08:17:51 +02:00
pub fn current(&self) -> Option<&Files> {
2026-10-01 18:42:31 +02:00
self.index.map_or_default(|i| self.get(i))
2026-07-14 18:36:30 +02:00
}
2026-10-08 21:04:29 +02:00
/// 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<F>(&mut self, f: F) -> Result<Option<Diff>, CommandError>
2026-09-23 19:53:39 +02:00
where
2026-10-14 08:17:51 +02:00
F: FnOnce(&mut Files) -> Result<(), CommandError>,
2026-09-23 19:53:39 +02:00
{
2026-10-08 21:04:29 +02:00
let mut next = self.current().map_or_default(|c| c.clone());
f(&mut next)?;
if self.is_current(&next) {
return Ok(None);
}
2026-10-14 08:17:51 +02:00
Ok(self.record_diff(|history| history.push(next)))
2026-10-08 21:04:29 +02:00
}
/// Whether `entry` holds exactly the files of the current entry.
2026-10-14 08:17:51 +02:00
fn is_current(&self, entry: &Files) -> bool {
2026-10-08 21:04:29 +02:00
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)))
})
2026-07-14 18:36:30 +02:00
}
2026-10-01 18:42:31 +02:00
pub fn undo(&mut self) -> Option<Diff> {
2026-10-14 08:17:51 +02:00
self.record_diff(|history| {
if let Some(i) = history.index {
2026-10-01 18:42:31 +02:00
if i == 0 {
2026-10-14 08:17:51 +02:00
if !history.floor {
history.index = None;
2026-10-08 18:34:19 +02:00
}
2026-10-01 18:42:31 +02:00
} else {
2026-10-14 08:17:51 +02:00
history.index = Some(i - 1);
2026-10-01 18:42:31 +02:00
}
}
})
}
2026-09-23 19:53:39 +02:00
2026-10-01 18:42:31 +02:00
pub fn redo(&mut self) -> Option<Diff> {
2026-10-14 08:17:51 +02:00
self.record_diff(|history| match history.index {
2026-10-01 18:42:31 +02:00
Some(i) => {
2026-10-14 08:17:51 +02:00
if i + 1 < history.entries.len() {
history.index = Some(i + 1);
2026-10-01 18:42:31 +02:00
}
}
None => {
2026-10-14 08:17:51 +02:00
if !history.entries.is_empty() {
history.index = Some(0);
2026-10-01 18:42:31 +02:00
}
}
})
}
2026-09-23 19:53:39 +02:00
2026-07-14 18:36:30 +02:00
pub fn can_undo(&self) -> bool {
2026-10-08 21:04:29 +02:00
self.index.is_some_and(|i| i > 0 || !self.floor)
2026-07-14 18:36:30 +02:00
}
pub fn can_redo(&self) -> bool {
match self.index {
Some(i) => i + 1 < self.entries.len(),
None => !self.entries.is_empty(),
}
}
2026-10-14 08:17:51 +02:00
fn get(&self, index: usize) -> Option<&Files> {
2026-10-03 20:54:05 +02:00
self.entries.get(index)
2026-07-14 18:36:30 +02:00
}
2026-10-14 08:17:51 +02:00
fn push(&mut self, entry: Files) {
2026-07-14 18:36:30 +02:00
if let Some(i) = self.index {
2026-10-03 20:54:05 +02:00
self.entries.truncate(i + 1);
2026-07-14 18:36:30 +02:00
}
self.entries.push(entry);
self.index = Some(self.entries.len() - 1);
}
2026-10-01 18:42:31 +02:00
fn record_diff<F>(&mut self, f: F) -> Option<Diff>
where
F: FnOnce(&mut Self),
{
let prev = self.index;
f(self);
let cur = self.index;
if prev == cur {
return None;
}
2026-10-10 11:07:07 +02:00
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.
2026-10-14 08:17:51 +02:00
pub fn diff_between(prev: Option<&Files>, cur: Option<&Files>) -> Diff {
2026-10-02 18:06:45 +02:00
let prev_ids: HashSet<FileId> = prev.map_or_default(|e| e.keys().copied().collect());
let cur_ids: HashSet<FileId> = cur.map_or_default(|e| e.keys().copied().collect());
2026-10-01 18:42:31 +02:00
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));
2026-10-08 18:34:19 +02:00
if !before.zip(after).is_some_and(|(b, a)| Rc::ptr_eq(b, a)) {
2026-10-01 18:42:31 +02:00
modified.push(*id);
}
}
2026-10-10 11:07:07 +02:00
Diff {
2026-10-01 18:42:31 +02:00
added: cur_ids.difference(&prev_ids).copied().collect(),
removed: prev_ids.difference(&cur_ids).copied().collect(),
modified,
2026-10-10 11:07:07 +02:00
}
2026-10-01 18:42:31 +02:00
}
2026-07-14 18:36:30 +02:00
}
2026-10-14 08:17:51 +02:00
pub type Files = HashMap<FileId, Rc<File>>;
2026-10-01 18:42:31 +02:00
2026-10-14 08:17:51 +02:00
/// Which files were added, removed or modified by a change of the current history entry.
2026-10-03 19:08:56 +02:00
#[derive(Debug, Default, Clone, PartialEq, Eq)]
2026-10-01 18:42:31 +02:00
pub struct Diff {
2026-10-02 18:06:45 +02:00
pub added: Vec<FileId>,
pub removed: Vec<FileId>,
pub modified: Vec<FileId>,
2026-10-01 18:42:31 +02:00
}
#[cfg(test)]
mod tests {
use super::*;
2026-10-14 08:17:51 +02:00
impl History {
2026-10-08 21:04:29 +02:00
/// A step that is expected not to fail.
fn push_ok(
&mut self,
2026-10-14 08:17:51 +02:00
f: impl FnOnce(&mut Files) -> Result<(), CommandError>,
2026-10-08 21:04:29 +02:00
) -> Option<Diff> {
self.create_and_push_next(f).unwrap()
}
}
2026-10-01 18:42:31 +02:00
#[test]
fn test_new_file() {
2026-10-14 08:17:51 +02:00
let mut history = History::default();
2026-10-01 18:42:31 +02:00
2026-10-14 08:17:51 +02:00
assert!(!history.can_undo());
assert!(!history.can_redo());
assert!(history.current().is_none());
2026-10-01 18:42:31 +02:00
2026-10-14 08:17:51 +02:00
let diff = history.push_ok(|e| {
2026-10-02 18:06:45 +02:00
let file = Rc::new(File::default());
2026-10-01 18:42:31 +02:00
e.insert(file.id, file);
2026-10-03 15:33:44 +02:00
Ok(())
2026-10-01 18:42:31 +02:00
});
2026-10-14 08:17:51 +02:00
assert!(history.can_undo());
assert!(!history.can_redo());
assert!(history.current().is_some());
2026-10-01 18:42:31 +02:00
assert!(diff.is_some());
let diff = diff.unwrap();
assert_eq!(diff.added.len(), 1);
assert!(diff.removed.is_empty());
assert!(diff.modified.is_empty());
}
2026-10-14 08:17:51 +02:00
fn add_file(history: &mut History) -> FileId {
2026-10-03 16:16:47 +02:00
let file = Rc::new(File::default());
let id = file.id;
2026-10-14 08:17:51 +02:00
history
2026-10-08 21:04:29 +02:00
.push_ok(|e| {
2026-10-03 16:16:47 +02:00
e.insert(id, file);
Ok(())
})
.unwrap();
id
}
#[test]
fn test_failed_command_pushes_nothing() {
2026-10-14 08:17:51 +02:00
let mut history = History::default();
let diff = history.create_and_push_next(|e| {
2026-10-03 16:16:47 +02:00
e.insert(FileId::default(), Rc::new(File::default()));
2026-10-08 21:04:29 +02:00
Err(CommandError::InvalidData("nope".to_string()))
2026-10-03 16:16:47 +02:00
});
2026-10-08 21:04:29 +02:00
assert_eq!(diff, Err(CommandError::InvalidData("nope".to_string())));
2026-10-14 08:17:51 +02:00
assert!(!history.can_undo());
assert!(history.current().is_none());
2026-10-03 16:16:47 +02:00
}
#[test]
fn test_undo_redo() {
2026-10-14 08:17:51 +02:00
let mut history = History::default();
assert!(history.undo().is_none());
assert!(history.redo().is_none());
2026-10-03 16:16:47 +02:00
2026-10-14 08:17:51 +02:00
let id = add_file(&mut history);
2026-10-03 16:16:47 +02:00
2026-10-14 08:17:51 +02:00
let diff = history.undo().unwrap();
2026-10-03 16:16:47 +02:00
assert_eq!(diff.removed, vec![id]);
assert!(diff.added.is_empty());
2026-10-14 08:17:51 +02:00
assert!(history.current().is_none());
assert!(!history.can_undo());
assert!(history.can_redo());
assert!(history.undo().is_none());
2026-10-03 16:16:47 +02:00
2026-10-14 08:17:51 +02:00
let diff = history.redo().unwrap();
2026-10-03 16:16:47 +02:00
assert_eq!(diff.added, vec![id]);
2026-10-14 08:17:51 +02:00
assert!(history.current().unwrap().contains_key(&id));
assert!(history.can_undo());
assert!(!history.can_redo());
assert!(history.redo().is_none());
2026-10-03 16:16:47 +02:00
}
#[test]
fn test_modified_and_removed() {
2026-10-14 08:17:51 +02:00
let mut history = History::default();
let id = add_file(&mut history);
2026-10-03 16:16:47 +02:00
2026-10-14 08:17:51 +02:00
let diff = history
2026-10-08 21:04:29 +02:00
.push_ok(|e| {
2026-10-03 16:16:47 +02:00
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());
2026-10-08 21:04:29 +02:00
// a step that changes nothing is not a step
2026-10-14 08:17:51 +02:00
assert!(history.push_ok(|_| Ok(())).is_none());
2026-10-08 21:04:29 +02:00
assert!(
2026-10-14 08:17:51 +02:00
history
2026-10-08 21:04:29 +02:00
.push_ok(|e| {
let file = e[&id].clone();
e.insert(id, file);
Ok(())
})
.is_none()
);
2026-10-03 16:16:47 +02:00
2026-10-14 08:17:51 +02:00
let diff = history
2026-10-08 21:04:29 +02:00
.push_ok(|e| {
2026-10-03 16:16:47 +02:00
e.remove(&id);
Ok(())
})
.unwrap();
assert_eq!(diff.removed, vec![id]);
2026-10-14 08:17:51 +02:00
assert!(history.current().unwrap().is_empty());
2026-10-03 16:16:47 +02:00
// going back restores the renamed file
2026-10-14 08:17:51 +02:00
history.undo();
assert_eq!(history.current().unwrap()[&id].info.name, "renamed");
2026-10-03 16:16:47 +02:00
}
2026-10-08 18:34:19 +02:00
#[test]
fn test_a_file_is_modified_when_it_is_not_the_same_one_anymore() {
2026-10-14 08:17:51 +02:00
let mut history = History::default();
let id = add_file(&mut history);
2026-10-08 18:34:19 +02:00
2026-10-14 08:17:51 +02:00
// a copy, even if it has the same content, is another file for the history: files are
2026-10-08 18:34:19 +02:00
// told apart by their identity, comparing them would read all of them
2026-10-14 08:17:51 +02:00
let diff = history
2026-10-08 21:04:29 +02:00
.push_ok(|e| {
2026-10-08 18:34:19 +02:00
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
2026-10-14 08:17:51 +02:00
let other = add_file(&mut history);
let diff = history
2026-10-08 21:04:29 +02:00
.push_ok(|e| {
2026-10-08 18:34:19 +02:00
let copy = (**e.get(&other).unwrap()).clone();
e.insert(other, Rc::new(copy));
Ok(())
})
.unwrap();
assert_eq!(diff.modified, vec![other]);
}
2026-10-03 16:16:47 +02:00
#[test]
fn test_push_after_undo_drops_redo_branch() {
2026-10-14 08:17:51 +02:00
let mut history = History::default();
let first = add_file(&mut history);
let second = add_file(&mut history);
2026-10-03 16:16:47 +02:00
2026-10-14 08:17:51 +02:00
history.undo();
assert!(history.can_redo());
assert!(!history.current().unwrap().contains_key(&second));
2026-10-03 16:16:47 +02:00
2026-10-14 08:17:51 +02:00
let third = add_file(&mut history);
assert!(!history.can_redo());
assert!(history.redo().is_none());
let current = history.current().unwrap();
2026-10-03 16:16:47 +02:00
assert!(current.contains_key(&first));
assert!(current.contains_key(&third));
assert!(!current.contains_key(&second));
}
2026-10-08 18:34:19 +02:00
2026-10-14 08:17:51 +02:00
fn restored_entry() -> (Files, FileId) {
2026-10-08 18:34:19 +02:00
let file = Rc::new(File::default());
let id = file.id;
2026-10-14 08:17:51 +02:00
(Files::from([(id, file)]), id)
2026-10-08 18:34:19 +02:00
}
#[test]
2026-10-14 08:17:51 +02:00
fn test_restored_history_starts_from_its_entry() {
2026-10-08 18:34:19 +02:00
let (entry, id) = restored_entry();
2026-10-14 08:17:51 +02:00
let mut history = History::restored(entry);
2026-10-08 18:34:19 +02:00
2026-10-14 08:17:51 +02:00
assert!(history.current().unwrap().contains_key(&id));
2026-10-08 18:34:19 +02:00
// there is nothing to go back to, nor forward to
2026-10-14 08:17:51 +02:00
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));
2026-10-08 18:34:19 +02:00
}
#[test]
2026-10-14 08:17:51 +02:00
fn test_restored_history_cannot_undo_an_empty_entry_either() {
let mut history = History::restored(Files::new());
2026-10-08 18:34:19 +02:00
2026-10-14 08:17:51 +02:00
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());
2026-10-08 18:34:19 +02:00
}
#[test]
2026-10-14 08:17:51 +02:00
fn test_restored_history_undoes_what_comes_after_it_and_no_further() {
2026-10-08 18:34:19 +02:00
let (entry, restored) = restored_entry();
2026-10-14 08:17:51 +02:00
let mut history = History::restored(entry);
2026-10-08 18:34:19 +02:00
2026-10-14 08:17:51 +02:00
let added = add_file(&mut history);
assert!(history.can_undo());
assert!(!history.can_redo());
assert_eq!(history.current().unwrap().len(), 2);
2026-10-08 18:34:19 +02:00
// back to the restored files
2026-10-14 08:17:51 +02:00
let diff = history.undo().unwrap();
2026-10-08 18:34:19 +02:00
assert_eq!(diff.removed, vec![added]);
assert!(diff.added.is_empty() && diff.modified.is_empty());
2026-10-14 08:17:51 +02:00
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));
2026-10-08 18:34:19 +02:00
2026-10-14 08:17:51 +02:00
let diff = history.redo().unwrap();
2026-10-08 18:34:19 +02:00
assert_eq!(diff.added, vec![added]);
2026-10-14 08:17:51 +02:00
assert!(history.can_undo());
assert!(!history.can_redo());
2026-10-08 18:34:19 +02:00
// and again
2026-10-14 08:17:51 +02:00
assert!(history.undo().is_some());
assert!(history.undo().is_none());
assert!(history.current().unwrap().contains_key(&restored));
2026-10-08 18:34:19 +02:00
}
#[test]
2026-10-14 08:17:51 +02:00
fn test_restored_history_removing_its_files_can_be_undone() {
2026-10-08 18:34:19 +02:00
let (entry, restored) = restored_entry();
2026-10-14 08:17:51 +02:00
let mut history = History::restored(entry);
2026-10-08 18:34:19 +02:00
2026-10-14 08:17:51 +02:00
let diff = history
2026-10-08 21:04:29 +02:00
.push_ok(|e| {
2026-10-08 18:34:19 +02:00
e.remove(&restored);
Ok(())
})
.unwrap();
assert_eq!(diff.removed, vec![restored]);
2026-10-14 08:17:51 +02:00
assert!(history.current().unwrap().is_empty());
2026-10-08 18:34:19 +02:00
2026-10-14 08:17:51 +02:00
let diff = history.undo().unwrap();
2026-10-08 18:34:19 +02:00
assert_eq!(diff.added, vec![restored]);
2026-10-14 08:17:51 +02:00
assert!(history.current().unwrap().contains_key(&restored));
assert!(!history.can_undo());
2026-10-08 18:34:19 +02:00
}
#[test]
2026-10-14 08:17:51 +02:00
fn test_restored_history_push_after_undo_drops_redo_branch() {
2026-10-08 18:34:19 +02:00
let (entry, restored) = restored_entry();
2026-10-14 08:17:51 +02:00
let mut history = History::restored(entry);
add_file(&mut history);
history.undo().unwrap();
assert!(history.can_redo());
2026-10-08 18:34:19 +02:00
2026-10-14 08:17:51 +02:00
let other = add_file(&mut history);
assert!(!history.can_redo());
assert!(history.can_undo());
let diff = history.undo().unwrap();
2026-10-08 18:34:19 +02:00
assert_eq!(diff.removed, vec![other]);
2026-10-14 08:17:51 +02:00
assert!(history.current().unwrap().contains_key(&restored));
assert!(!history.can_undo());
2026-10-08 18:34:19 +02:00
}
2026-10-01 18:42:31 +02:00
}