per file history

This commit is contained in:
vcoppe committed 2026-10-16 08:42:45 +02:00
1 parent 1ba5436af3
commit 55a8b020e0
6 files changed
+676 -175

No files matched your search

@@ -1,12 +1,11 @@
use std::{
collections::{HashMap, HashSet},
rc::Rc,
};
use std::rc::Rc;
use crate::{CommandError, File, FileId};
use super::{Diff, Files, History, diff_between};
use crate::{CommandError, FileId};
/// One history for the whole workspace: each entry is a state of all the files.
#[derive(Debug, Default)]
pub struct History {
pub struct GlobalHistory {
entries: Vec<Files>,
index: Option<usize>,
/// Whether the first entry is the state the history starts from, which is not undone: files
@@ -14,37 +13,7 @@ pub struct History {
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<F>(&mut self, f: F) -> Result<Option<Diff>, 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)))
}
impl GlobalHistory {
/// Whether `entry` holds exactly the files of the current entry.
fn is_current(&self, entry: &Files) -> bool {
let current = self.current();
@@ -57,46 +26,6 @@ impl History {
})
}
pub fn undo(&mut self) -> Option<Diff> {
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<Diff> {
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)
}
@@ -120,47 +49,86 @@ impl History {
if prev == cur {
return None;
}
Some(Self::diff_between(
Some(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<FileId> = prev.map_or_default(|e| e.keys().copied().collect());
let cur_ids: HashSet<FileId> = 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<FileId, Rc<File>>;
impl History for GlobalHistory {
fn restored(entry: Files) -> Self {
Self {
entries: vec![entry],
index: Some(0),
floor: true,
}
}
/// 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<FileId>,
pub removed: Vec<FileId>,
pub modified: Vec<FileId>,
fn current(&self) -> Option<&Files> {
self.index.map_or_default(|i| self.get(i))
}
/// Files are told apart by their identity, not their content.
fn create_and_push_next<F>(&mut self, f: F) -> Result<Option<Diff>, 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)))
}
fn undo(&mut self, _files: &[FileId]) -> Option<Diff> {
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);
}
}
})
}
fn redo(&mut self, _files: &[FileId]) -> Option<Diff> {
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);
}
}
})
}
fn can_undo(&self, _files: &[FileId]) -> bool {
self.index.is_some_and(|i| i > 0 || !self.floor)
}
fn can_redo(&self, _files: &[FileId]) -> bool {
match self.index {
Some(i) => i + 1 < self.entries.len(),
None => !self.entries.is_empty(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::File;
impl History {
impl GlobalHistory {
/// A step that is expected not to fail.
fn push_ok(
&mut self,
@@ -172,10 +140,10 @@ mod tests {
#[test]
fn test_new_file() {
let mut history = History::default();
let mut history = GlobalHistory::default();
assert!(!history.can_undo());
assert!(!history.can_redo());
assert!(!history.can_undo(&[]));
assert!(!history.can_redo(&[]));
assert!(history.current().is_none());
let diff = history.push_ok(|e| {
@@ -184,8 +152,8 @@ mod tests {
Ok(())
});
assert!(history.can_undo());
assert!(!history.can_redo());
assert!(history.can_undo(&[]));
assert!(!history.can_redo(&[]));
assert!(history.current().is_some());
assert!(diff.is_some());
@@ -196,7 +164,7 @@ mod tests {
assert!(diff.modified.is_empty());
}
fn add_file(history: &mut History) -> FileId {
fn add_file(history: &mut GlobalHistory) -> FileId {
let file = Rc::new(File::default());
let id = file.id;
history
@@ -210,43 +178,43 @@ mod tests {
#[test]
fn test_failed_command_pushes_nothing() {
let mut history = History::default();
let mut history = GlobalHistory::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.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 mut history = GlobalHistory::default();
assert!(history.undo(&[]).is_none());
assert!(history.redo(&[]).is_none());
let id = add_file(&mut history);
let diff = history.undo().unwrap();
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());
assert!(!history.can_undo(&[]));
assert!(history.can_redo(&[]));
assert!(history.undo(&[]).is_none());
let diff = history.redo().unwrap();
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());
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 mut history = GlobalHistory::default();
let id = add_file(&mut history);
let diff = history
@@ -282,13 +250,13 @@ mod tests {
assert!(history.current().unwrap().is_empty());
// going back restores the renamed file
history.undo();
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 mut history = GlobalHistory::default();
let id = add_file(&mut history);
// a copy, even if it has the same content, is another file for the history: files are
@@ -316,17 +284,17 @@ mod tests {
#[test]
fn test_push_after_undo_drops_redo_branch() {
let mut history = History::default();
let mut history = GlobalHistory::default();
let first = add_file(&mut history);
let second = add_file(&mut history);
history.undo();
assert!(history.can_redo());
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());
assert!(!history.can_redo(&[]));
assert!(history.redo(&[]).is_none());
let current = history.current().unwrap();
assert!(current.contains_key(&first));
assert!(current.contains_key(&third));
@@ -342,24 +310,24 @@ mod tests {
#[test]
fn test_restored_history_starts_from_its_entry() {
let (entry, id) = restored_entry();
let mut history = History::restored(entry);
let mut history = GlobalHistory::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.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());
let mut history = GlobalHistory::restored(Files::new());
assert!(history.current().is_some_and(|files| files.is_empty()));
assert!(!history.can_undo());
assert!(history.undo().is_none());
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());
}
@@ -367,39 +335,39 @@ mod tests {
#[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 mut history = GlobalHistory::restored(entry);
let added = add_file(&mut history);
assert!(history.can_undo());
assert!(!history.can_redo());
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();
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.can_undo(&[]));
assert!(history.can_redo(&[]));
assert!(history.undo(&[]).is_none());
assert!(history.current().unwrap().contains_key(&restored));
let diff = history.redo().unwrap();
let diff = history.redo(&[]).unwrap();
assert_eq!(diff.added, vec![added]);
assert!(history.can_undo());
assert!(!history.can_redo());
assert!(history.can_undo(&[]));
assert!(!history.can_redo(&[]));
// and again
assert!(history.undo().is_some());
assert!(history.undo().is_none());
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 mut history = GlobalHistory::restored(entry);
let diff = history
.push_ok(|e| {
@@ -410,26 +378,26 @@ mod tests {
assert_eq!(diff.removed, vec![restored]);
assert!(history.current().unwrap().is_empty());
let diff = history.undo().unwrap();
let diff = history.undo(&[]).unwrap();
assert_eq!(diff.added, vec![restored]);
assert!(history.current().unwrap().contains_key(&restored));
assert!(!history.can_undo());
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);
let mut history = GlobalHistory::restored(entry);
add_file(&mut history);
history.undo().unwrap();
assert!(history.can_redo());
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!(!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());
assert!(!history.can_undo(&[]));
}
}
+88
View File
@@ -0,0 +1,88 @@
use std::{
collections::{HashMap, HashSet},
fmt::Debug,
rc::Rc,
};
use crate::{CommandError, File, FileId};
mod global;
mod per_file;
pub use global::*;
pub use per_file::*;
pub type Files = HashMap<FileId, Rc<File>>;
/// What the engine can undo and redo, and the files it holds as a result.
///
/// There are two ways to keep it, which the engine can use interchangeably:
/// - [`GlobalHistory`] has one history for the whole workspace: every step is a state of all the
/// files, and undoing goes back to the previous one, whatever the files it concerns. Opening and
/// closing files are steps too.
/// - [`PerFileHistory`] has one history for each file, which starts when the file appears and ends
/// when it disappears. Undoing only concerns the files that are asked for, and files appearing
/// or disappearing are never undone.
///
/// Files are told apart by their identity (`Rc::ptr_eq`), not their content.
pub trait History: Default + Debug {
/// A history that starts from `files`, which cannot be undone.
fn restored(files: Files) -> Self;
/// The current state of the files, `None` if there is none yet.
fn current(&self) -> Option<&Files>;
/// Applies `f` to a copy of the current files and makes the copy the new current state.
///
/// Nothing is recorded, 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.
fn create_and_push_next<F>(&mut self, f: F) -> Result<Option<Diff>, CommandError>
where
F: FnOnce(&mut Files) -> Result<(), CommandError>;
/// Goes back one step. A per-file history does it for each of the given `files` (and ignores
/// the ones it does not know), a global one ignores them.
fn undo(&mut self, files: &[FileId]) -> Option<Diff>;
/// Goes forward one step, for the same files as [`History::undo`].
fn redo(&mut self, files: &[FileId]) -> Option<Diff>;
/// Whether [`History::undo`] would do something for these `files`.
fn can_undo(&self, files: &[FileId]) -> bool;
/// Whether [`History::redo`] would do something for these `files`.
fn can_redo(&self, files: &[FileId]) -> bool;
}
/// Which files were added, removed or modified by a change of the current files.
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct Diff {
pub added: Vec<FileId>,
pub removed: Vec<FileId>,
pub modified: Vec<FileId>,
}
impl Diff {
pub fn is_empty(&self) -> bool {
self.added.is_empty() && self.removed.is_empty() && self.modified.is_empty()
}
}
/// Which files differ between two states.
pub fn diff_between(prev: Option<&Files>, cur: Option<&Files>) -> Diff {
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());
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,
}
}
+334
View File
@@ -0,0 +1,334 @@
use std::rc::Rc;
use super::{Diff, Files, History, diff_between};
use crate::{CommandError, File, FileId};
use std::collections::HashMap;
/// One history for each file, which begins with the state the file appears in (loaded, created,
/// produced by a command...) and ends when the file disappears: a file coming or going is never
/// undone, only what happens to it in between.
#[derive(Debug, Default)]
pub struct PerFileHistory {
current: Files,
stacks: HashMap<FileId, FileHistory>,
}
/// The successive states of a file, the first one being where it starts from.
#[derive(Debug)]
struct FileHistory {
entries: Vec<Rc<File>>,
index: usize,
}
impl FileHistory {
fn new(file: Rc<File>) -> Self {
Self {
entries: vec![file],
index: 0,
}
}
fn current(&self) -> &Rc<File> {
&self.entries[self.index]
}
fn push(&mut self, file: Rc<File>) {
self.entries.truncate(self.index + 1);
self.entries.push(file);
self.index = self.entries.len() - 1;
}
fn can_undo(&self) -> bool {
self.index > 0
}
fn can_redo(&self) -> bool {
self.index + 1 < self.entries.len()
}
}
impl PerFileHistory {
/// Moves each of the files one step back (`forward` false) or forward, and says which ones
/// moved.
fn step(&mut self, files: &[FileId], forward: bool) -> Option<Diff> {
let mut modified: Vec<FileId> = vec![];
for id in files {
let Some(history) = self.stacks.get_mut(id) else {
continue;
};
if modified.contains(id) {
continue;
}
if forward && history.can_redo() {
history.index += 1;
} else if !forward && history.can_undo() {
history.index -= 1;
} else {
continue;
}
self.current.insert(*id, history.current().clone());
modified.push(*id);
}
(!modified.is_empty()).then(|| Diff {
modified,
..Default::default()
})
}
fn any(&self, files: &[FileId], f: impl Fn(&FileHistory) -> bool) -> bool {
files.iter().any(|id| self.stacks.get(id).is_some_and(&f))
}
}
impl History for PerFileHistory {
fn restored(files: Files) -> Self {
let stacks = files
.iter()
.map(|(id, file)| (*id, FileHistory::new(file.clone())))
.collect();
Self {
current: files,
stacks,
}
}
fn current(&self) -> Option<&Files> {
Some(&self.current)
}
fn create_and_push_next<F>(&mut self, f: F) -> Result<Option<Diff>, CommandError>
where
F: FnOnce(&mut Files) -> Result<(), CommandError>,
{
let mut next = self.current.clone();
f(&mut next)?;
let diff = diff_between(Some(&self.current), Some(&next));
if diff.is_empty() {
return Ok(None);
}
for id in &diff.removed {
self.stacks.remove(id);
}
for id in &diff.added {
self.stacks.insert(*id, FileHistory::new(next[id].clone()));
}
for id in &diff.modified {
match self.stacks.get_mut(id) {
Some(history) => history.push(next[id].clone()),
None => {
self.stacks.insert(*id, FileHistory::new(next[id].clone()));
}
}
}
self.current = next;
Ok(Some(diff))
}
fn undo(&mut self, files: &[FileId]) -> Option<Diff> {
self.step(files, false)
}
fn redo(&mut self, files: &[FileId]) -> Option<Diff> {
self.step(files, true)
}
fn can_undo(&self, files: &[FileId]) -> bool {
self.any(files, FileHistory::can_undo)
}
fn can_redo(&self, files: &[FileId]) -> bool {
self.any(files, FileHistory::can_redo)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn push(history: &mut PerFileHistory, f: impl FnOnce(&mut Files)) -> Option<Diff> {
history
.create_and_push_next(|files| {
f(files);
Ok(())
})
.unwrap()
}
fn add_file(history: &mut PerFileHistory) -> FileId {
let file = Rc::new(File::default());
let id = file.id;
push(history, |files| {
files.insert(id, file);
})
.unwrap();
id
}
fn rename(history: &mut PerFileHistory, id: FileId, name: &str) -> Option<Diff> {
push(history, |files| {
let mut file = (**files.get(&id).unwrap()).clone();
file.info.name = name.to_string();
files.insert(id, Rc::new(file));
})
}
fn name(history: &PerFileHistory, id: FileId) -> String {
history.current().unwrap()[&id].info.name.clone()
}
#[test]
fn test_a_new_file_cannot_be_undone() {
let mut history = PerFileHistory::default();
assert!(history.current().unwrap().is_empty());
let id = add_file(&mut history);
assert!(!history.can_undo(&[id]));
assert!(!history.can_redo(&[id]));
assert!(history.undo(&[id]).is_none());
assert!(history.current().unwrap().contains_key(&id));
}
#[test]
fn test_undo_redo_of_a_file() {
let mut history = PerFileHistory::default();
let id = add_file(&mut history);
let before = name(&history, id);
let diff = rename(&mut history, id, "renamed").unwrap();
assert_eq!(diff.modified, vec![id]);
assert!(history.can_undo(&[id]));
let diff = history.undo(&[id]).unwrap();
assert_eq!(diff.modified, vec![id]);
assert!(diff.added.is_empty() && diff.removed.is_empty());
assert_eq!(name(&history, id), before);
assert!(!history.can_undo(&[id]));
assert!(history.can_redo(&[id]));
history.redo(&[id]).unwrap();
assert_eq!(name(&history, id), "renamed");
assert!(history.redo(&[id]).is_none());
}
#[test]
fn test_only_the_given_files_are_undone() {
let mut history = PerFileHistory::default();
let a = add_file(&mut history);
let b = add_file(&mut history);
rename(&mut history, a, "a");
rename(&mut history, b, "b");
assert!(history.can_undo(&[a]));
assert!(!history.can_undo(&[]));
assert!(history.undo(&[]).is_none());
let diff = history.undo(&[a]).unwrap();
assert_eq!(diff.modified, vec![a]);
assert_ne!(name(&history, a), "a");
assert_eq!(name(&history, b), "b");
// a has nothing more to undo, b still has
assert!(!history.can_undo(&[a]));
assert!(history.can_undo(&[a, b]));
assert!(history.undo(&[a]).is_none());
}
#[test]
fn test_several_files_are_undone_each_by_its_own_step() {
let mut history = PerFileHistory::default();
let a = add_file(&mut history);
let b = add_file(&mut history);
rename(&mut history, a, "a1");
rename(&mut history, a, "a2");
rename(&mut history, b, "b1");
let diff = history.undo(&[a, b, a]).unwrap();
assert_eq!(diff.modified, vec![a, b]);
// a is only moved once, even if it is asked for twice
assert_eq!(name(&history, a), "a1");
assert_ne!(name(&history, b), "b1");
}
#[test]
fn test_a_step_after_an_undo_drops_the_redo_branch_of_that_file_only() {
let mut history = PerFileHistory::default();
let a = add_file(&mut history);
let b = add_file(&mut history);
rename(&mut history, a, "a1");
rename(&mut history, b, "b1");
history.undo(&[a, b]).unwrap();
rename(&mut history, a, "a2");
assert!(!history.can_redo(&[a]));
assert!(history.can_redo(&[b]));
}
#[test]
fn test_a_step_that_changes_nothing_is_not_recorded() {
let mut history = PerFileHistory::default();
let id = add_file(&mut history);
assert!(push(&mut history, |_| {}).is_none());
assert!(!history.can_undo(&[id]));
}
#[test]
fn test_failed_command_pushes_nothing() {
let mut history = PerFileHistory::default();
let id = add_file(&mut history);
let result = history.create_and_push_next(|files| {
files.remove(&id);
Err(CommandError::InvalidData("nope".to_string()))
});
assert_eq!(result, Err(CommandError::InvalidData("nope".to_string())));
assert!(history.current().unwrap().contains_key(&id));
}
#[test]
fn test_removing_a_file_drops_its_history() {
let mut history = PerFileHistory::default();
let id = add_file(&mut history);
rename(&mut history, id, "renamed");
let diff = push(&mut history, |files| {
files.remove(&id);
})
.unwrap();
assert_eq!(diff.removed, vec![id]);
assert!(history.current().unwrap().is_empty());
assert!(!history.can_undo(&[id]));
assert!(history.undo(&[id]).is_none());
assert!(history.current().unwrap().is_empty());
}
#[test]
fn test_a_file_that_comes_back_starts_over() {
let mut history = PerFileHistory::default();
let file = Rc::new(File::default());
let id = file.id;
push(&mut history, |files| {
files.insert(id, file.clone());
});
rename(&mut history, id, "renamed");
push(&mut history, |files| {
files.remove(&id);
});
push(&mut history, |files| {
files.insert(id, file.clone());
});
assert!(!history.can_undo(&[id]));
}
#[test]
fn test_restored_files_cannot_be_undone() {
let file = Rc::new(File::default());
let id = file.id;
let mut history = PerFileHistory::restored(Files::from([(id, file)]));
assert!(history.current().unwrap().contains_key(&id));
assert!(!history.can_undo(&[id]));
assert!(history.undo(&[id]).is_none());
rename(&mut history, id, "renamed");
assert!(history.can_undo(&[id]));
history.undo(&[id]).unwrap();
assert!(!history.can_undo(&[id]));
}
}
+8
View File
@@ -312,6 +312,14 @@ fn neighbour<T: Copy + PartialEq + std::hash::Hash + Eq>(
}
impl Selection {
/// The ids of the files holding the selected elements.
pub fn file_ids(&self) -> Vec<FileId> {
match self.to_files() {
Selection::File { file_ids } => file_ids.into_iter().collect(),
_ => vec![],
}
}
/// The files holding the selected elements, as a selection of files.
pub fn to_files(&self) -> Selection {
match self {