mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-10-19 09:22:12 +00:00
clarify filename
This commit is contained in:
1 parent
11709316d2
commit
adbf89e318
16 files changed
+573
-61
No files matched your search
@@ -183,8 +183,8 @@ pub fn load_files<H: History>(
|
||||
}
|
||||
|
||||
/// Brings changes of the files from the outside in (see [`engine::SyncFiles`]): each file is
|
||||
/// `(id, data, name)`, and replaces the file of that id, or is added if there is none. The files to
|
||||
/// `remove` are deleted. The selection is left alone.
|
||||
/// `(id, data, name)`, and replaces the file of that id, or is added if there is none; it is called
|
||||
/// `name`, whatever its data says. The files to `remove` are deleted. The selection is left alone.
|
||||
pub fn sync_files<H: History>(
|
||||
engine: &mut Engine<H>,
|
||||
files: &[(Uuid, &[u8], &str)],
|
||||
@@ -232,6 +232,18 @@ pub fn reorder<H: History>(engine: &mut Engine<H>, file_ids_: &[Uuid], index: us
|
||||
)
|
||||
}
|
||||
|
||||
/// Names a file in all its states, as if it had always been called so (not undoable): for a file
|
||||
/// that is written to a file of another name, whose name it takes.
|
||||
pub fn rename_file<H: History>(engine: &mut Engine<H>, file_id: Uuid, name: &str) -> Outcome {
|
||||
execute(
|
||||
engine,
|
||||
Action::Rename {
|
||||
file_id: engine::FileId(file_id),
|
||||
name: name.to_owned(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Puts the selected elements in the clipboard. Nothing happens when nothing is selected.
|
||||
pub fn copy<H: History>(engine: &mut Engine<H>) -> Outcome {
|
||||
execute(engine, Action::Copy)
|
||||
|
||||
@@ -36,4 +36,11 @@ pub enum Action<'a> {
|
||||
file_ids: Vec<FileId>,
|
||||
index: usize,
|
||||
},
|
||||
/// Names a file in all the states of the history, as if it had always been called so (see
|
||||
/// [`crate::History::rename`]). Not undoable: for a file that is given a name from the outside,
|
||||
/// like the name of the file it is written to.
|
||||
Rename {
|
||||
file_id: FileId,
|
||||
name: String,
|
||||
},
|
||||
}
|
||||
@@ -7,7 +7,8 @@ use crate::{Apply, CommandError, FileId, Load, State};
|
||||
pub struct ExternalFile<'a> {
|
||||
pub id: FileId,
|
||||
pub data: &'a [u8],
|
||||
/// Name of the file when the data has none (see [`Load`]).
|
||||
/// Name of the file outside (the name of the file on the disk, without its extension), which
|
||||
/// is its name here whatever the data says: a file has a single name.
|
||||
pub name: &'a str,
|
||||
}
|
||||
|
||||
@@ -15,7 +16,8 @@ pub struct ExternalFile<'a> {
|
||||
/// that are given replace the ones of the same id (which stay where they are in the order, and keep
|
||||
/// their id), or are added at the end if there are none; the ones to `remove` are deleted.
|
||||
///
|
||||
/// Unlike [`crate::LoadFiles`], the selection is left alone (what it holds of a file that was
|
||||
/// Unlike [`crate::LoadFiles`], the files are named as they are outside (see [`ExternalFile`]), and
|
||||
/// the selection is left alone (what it holds of a file that was
|
||||
/// replaced or deleted is dropped by the engine afterwards, as with any edit).
|
||||
///
|
||||
/// The files that cannot be read are skipped, and the files that are here stay as they are. It is an
|
||||
@@ -38,6 +40,9 @@ impl Apply for SyncFiles<'_> {
|
||||
match load.parse(state.categories) {
|
||||
Ok(mut file) => {
|
||||
file.id = external.id;
|
||||
if !external.name.trim().is_empty() {
|
||||
file.info.name = external.name.to_owned();
|
||||
}
|
||||
if state.files.insert(external.id, Rc::new(file)).is_none() {
|
||||
state.order.0.push(external.id);
|
||||
}
|
||||
@@ -101,7 +106,8 @@ mod tests {
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
assert_eq!(fx.order.0, [first, second]);
|
||||
assert_eq!(fx.files[&first].info.name, "from disk");
|
||||
// named as it is outside, not as the data says
|
||||
assert_eq!(fx.files[&first].info.name, "name");
|
||||
assert_eq!(fx.files[&first].id, first);
|
||||
// the selection is not changed
|
||||
assert_eq!(fx.selected_files(), [second].into());
|
||||
|
||||
@@ -248,6 +248,7 @@ impl<H: History> Engine<H> {
|
||||
self.order.move_files(&file_ids, index);
|
||||
None
|
||||
}
|
||||
Action::Rename { file_id, name } => self.history.rename(file_id, &name),
|
||||
};
|
||||
let changed = diff.is_some()
|
||||
|| self.selection != selection_before
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use super::{Diff, Files, History, diff_between};
|
||||
use super::{Diff, Files, History, Renamer, diff_between};
|
||||
use crate::{CommandError, FileId};
|
||||
|
||||
/// One history for the whole workspace: each entry is a state of all the files.
|
||||
@@ -123,6 +123,22 @@ impl History for GlobalHistory {
|
||||
None => !self.entries.is_empty(),
|
||||
}
|
||||
}
|
||||
|
||||
fn rename(&mut self, id: FileId, name: &str) -> Option<Diff> {
|
||||
let before = self.current()?.get(&id)?.clone();
|
||||
let mut renamer = Renamer::new(name);
|
||||
for entry in self.entries.iter_mut() {
|
||||
if let Some(file) = entry.get(&id) {
|
||||
let renamed = renamer.rename(file);
|
||||
entry.insert(id, renamed);
|
||||
}
|
||||
}
|
||||
let after = self.current()?.get(&id)?;
|
||||
(!Rc::ptr_eq(&before, after)).then(|| Diff {
|
||||
modified: vec![id],
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -57,6 +57,41 @@ pub trait History: Default + Debug {
|
||||
|
||||
/// Whether [`History::redo`] would do something for these `files`.
|
||||
fn can_redo(&self, files: &[FileId]) -> bool;
|
||||
|
||||
/// Names a file `name` in all its states, as if it had always been called so: there is nothing
|
||||
/// to undo. Says that the file changed, `None` if there is no such file or it is called so.
|
||||
fn rename(&mut self, id: FileId, name: &str) -> Option<Diff>;
|
||||
}
|
||||
|
||||
/// Names the states of a file: the ones that were shared stay shared (a state that several
|
||||
/// entries have is renamed once), and the ones that are called so already are kept.
|
||||
pub(super) struct Renamer<'a> {
|
||||
name: &'a str,
|
||||
renamed: HashMap<*const File, Rc<File>>,
|
||||
}
|
||||
|
||||
impl<'a> Renamer<'a> {
|
||||
pub(super) fn new(name: &'a str) -> Self {
|
||||
Self {
|
||||
name,
|
||||
renamed: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn rename(&mut self, file: &Rc<File>) -> Rc<File> {
|
||||
if file.info.name == self.name {
|
||||
return file.clone();
|
||||
}
|
||||
let name = self.name;
|
||||
self.renamed
|
||||
.entry(Rc::as_ptr(file))
|
||||
.or_insert_with(|| {
|
||||
let mut renamed = (**file).clone();
|
||||
renamed.info.name = name.to_owned();
|
||||
Rc::new(renamed)
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Which files were added, removed or modified by a change of the current files.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use super::{Diff, Files, History, diff_between};
|
||||
use super::{Diff, Files, History, Renamer, diff_between};
|
||||
use crate::{CommandError, File, FileId};
|
||||
|
||||
use std::collections::HashMap;
|
||||
@@ -139,6 +139,20 @@ impl History for PerFileHistory {
|
||||
self.any(files, FileHistory::can_undo)
|
||||
}
|
||||
|
||||
fn rename(&mut self, id: FileId, name: &str) -> Option<Diff> {
|
||||
let history = self.stacks.get_mut(&id)?;
|
||||
let mut renamer = Renamer::new(name);
|
||||
for entry in history.entries.iter_mut() {
|
||||
*entry = renamer.rename(entry);
|
||||
}
|
||||
let current = history.current().clone();
|
||||
let before = self.current.insert(id, current.clone());
|
||||
(!before.is_some_and(|before| Rc::ptr_eq(&before, ¤t))).then(|| Diff {
|
||||
modified: vec![id],
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
fn can_redo(&self, files: &[FileId]) -> bool {
|
||||
self.any(files, FileHistory::can_redo)
|
||||
}
|
||||
|
||||
@@ -182,6 +182,52 @@ fn test_metadata_renames_selected_file() {
|
||||
assert_eq!(file.info.desc.as_deref(), Some("about"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rename_names_every_state_of_the_file_and_is_not_undoable() {
|
||||
let mut engine = TestEngine::default();
|
||||
new(&mut engine, "first");
|
||||
new(&mut engine, "other");
|
||||
let id = engine.order()[0];
|
||||
let other = engine.order()[1];
|
||||
let other_file = engine.history.current().unwrap()[&other].clone();
|
||||
engine.run(Action::Select {
|
||||
selection: Selection::File {
|
||||
file_ids: [id].into(),
|
||||
},
|
||||
mode: SelectMode::Replace,
|
||||
});
|
||||
assert!(edit(
|
||||
&mut engine,
|
||||
Command::Metadata(Metadata {
|
||||
name: "edited",
|
||||
desc: "",
|
||||
})
|
||||
));
|
||||
assert!(engine.run(Action::Rename {
|
||||
file_id: id,
|
||||
name: "written".to_owned(),
|
||||
}));
|
||||
assert_eq!(engine.last_diff().unwrap().modified, vec![id]);
|
||||
assert_eq!(engine.file_structure(&id).unwrap().name, "written");
|
||||
// nothing to undo for it: the edits before are undone, and the file keeps its name
|
||||
engine.run(Action::Undo);
|
||||
engine.run(Action::Undo);
|
||||
assert_eq!(engine.file_structure(&id).unwrap().name, "written");
|
||||
engine.run(Action::Redo);
|
||||
engine.run(Action::Redo);
|
||||
assert_eq!(engine.file_structure(&id).unwrap().name, "written");
|
||||
// the other file is still the same object, in every state
|
||||
assert!(Rc::ptr_eq(
|
||||
&engine.history.current().unwrap()[&other],
|
||||
&other_file
|
||||
));
|
||||
// naming it as it is already called changes nothing
|
||||
assert!(!engine.run(Action::Rename {
|
||||
file_id: id,
|
||||
name: "written".to_owned(),
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_metadata_without_selection_does_not_modify_files() {
|
||||
let mut engine = TestEngine::default();
|
||||
@@ -1469,6 +1515,31 @@ mod per_file_history {
|
||||
engine.file_structure(&id).unwrap().name.clone()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rename_names_every_state_of_the_file() {
|
||||
let mut engine = Engine::<PerFileHistory>::default();
|
||||
let id = new_file(&mut engine, "a");
|
||||
select(&mut engine, &[id]);
|
||||
rename(&mut engine, "b");
|
||||
rename(&mut engine, "c");
|
||||
let outcome = engine
|
||||
.execute(Action::Rename {
|
||||
file_id: id,
|
||||
name: "written".to_owned(),
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(outcome.diff.unwrap().modified, vec![id]);
|
||||
assert_eq!(name_of(&engine, id), "written");
|
||||
// the edits are still there to undo, the name stays
|
||||
engine.execute(Action::Undo).unwrap();
|
||||
assert!(engine.can_undo());
|
||||
engine.execute(Action::Undo).unwrap();
|
||||
assert!(!engine.can_undo());
|
||||
assert_eq!(name_of(&engine, id), "written");
|
||||
engine.execute(Action::Redo).unwrap();
|
||||
assert_eq!(name_of(&engine, id), "written");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_creating_a_file_is_not_undoable() {
|
||||
let mut engine = Engine::<PerFileHistory>::default();
|
||||
|
||||
Reference in new issue
Block a user