This commit is contained in:
vcoppe
2026-10-04 16:11:44 +02:00
parent ec448b72e2
commit 7651d859c4
9 changed files with 325 additions and 341 deletions
+5 -3
View File
@@ -1,8 +1,8 @@
use crate::{Apply, CommandError, State};
use crate::{
Clean, Crop, Delete, DeleteAll, Duplicate, Elevation, Extract, Load, Merge, Metadata, Move,
MoveWaypoint, New, NewTrack, NewTrackSegment, NewWaypoint, Paste, Reverse, SpliceTrackpoints,
Split, Style, Time,
Clean, Crop, Delete, DeleteAll, Duplicate, Elevation, Extract, Load, LoadFiles, Merge,
Metadata, Move, MoveWaypoint, New, NewTrack, NewTrackSegment, NewWaypoint, Paste, Reverse,
SpliceTrackpoints, Split, Style, Time,
};
/// A user action that edits the files. Each variant wraps the command's own struct, whose
@@ -14,6 +14,7 @@ use crate::{
pub enum Command<'a> {
New(New<'a>),
Load(Load<'a>),
LoadFiles(LoadFiles<'a>),
Duplicate(Duplicate),
Delete(Delete),
Paste(Paste),
@@ -41,6 +42,7 @@ impl Apply for Command<'_> {
match self {
Command::New(c) => c.apply(state),
Command::Load(c) => c.apply(state),
Command::LoadFiles(c) => c.apply(state),
Command::Duplicate(c) => c.apply(state),
Command::Delete(c) => c.apply(state),
Command::Paste(c) => c.apply(state),
+136 -4
View File
@@ -1,4 +1,4 @@
use crate::{Apply, CommandError, State, parse, produce};
use crate::{Apply, CommandError, File, State, parse, produce};
#[derive(Debug)]
pub struct Load<'a> {
@@ -8,14 +8,49 @@ pub struct Load<'a> {
pub name: &'a str,
}
impl Apply for Load<'_> {
fn apply(self, state: &mut State) -> Result<(), CommandError> {
impl Load<'_> {
fn parse(self) -> Result<File, CommandError> {
let mut file =
parse(self.data).map_err(|err| CommandError::InvalidData(err.to_string()))?;
if file.info.name.trim().is_empty() {
file.info.name = self.name.to_owned();
}
produce(state, |_| vec![file]);
Ok(file)
}
}
impl Apply for Load<'_> {
fn apply(self, state: &mut State) -> Result<(), CommandError> {
LoadFiles { files: vec![self] }.apply(state)
}
}
/// Loads several files at once, as a single command (one undo step): the files are added at the
/// end of the list, in the given order, and the first one is selected.
///
/// The files that cannot be read are skipped, unless none of them can: then nothing is loaded
/// and the error is the one of the first file.
#[derive(Debug)]
pub struct LoadFiles<'a> {
pub files: Vec<Load<'a>>,
}
impl Apply for LoadFiles<'_> {
fn apply(self, state: &mut State) -> Result<(), CommandError> {
let mut files = Vec::with_capacity(self.files.len());
let mut first_error = None;
for load in self.files {
match load.parse() {
Ok(file) => files.push(file),
Err(error) => {
first_error.get_or_insert(error);
}
}
}
if files.is_empty() {
return Err(first_error.unwrap_or(CommandError::NothingToDo));
}
produce(state, |_| files);
Ok(())
}
}
@@ -85,4 +120,101 @@ mod tests {
let empty = br#"<gpx version="1.1"><metadata><name> </name></metadata></gpx>"#;
assert_eq!(load_with_name(empty), "from disk");
}
fn gpx(name: &str) -> Vec<u8> {
format!(r#"<gpx version="1.1"><metadata><name>{name}</name></metadata></gpx>"#).into_bytes()
}
fn load<'a>(data: &'a [u8], name: &'a str) -> Load<'a> {
Load { data, name }
}
#[test]
fn test_load_several_files() {
let mut fx = Fixture::default();
crate::New { name: "before" }
.apply(&mut fx.state())
.unwrap();
let before = fx.order.0[0];
let (a, b, c) = (gpx("a"), gpx("b"), gpx("c"));
LoadFiles {
files: vec![load(&a, "x"), load(&b, "x"), load(&c, "x")],
}
.apply(&mut fx.state())
.unwrap();
// added after the others, in the order they were given
let names: Vec<_> = fx
.order
.0
.iter()
.map(|id| fx.files[id].info.name.clone())
.collect();
assert_eq!(names, ["before", "a", "b", "c"]);
assert_eq!(fx.order.0[0], before);
// the first one is selected
assert_eq!(fx.selected_files(), [fx.order.0[1]].into());
}
#[test]
fn test_load_several_files_skips_the_invalid_ones() {
let mut fx = Fixture::default();
let (a, c) = (gpx("a"), gpx("c"));
LoadFiles {
files: vec![load(&a, "x"), load(b"<gpx><trk></gpx>", "x"), load(&c, "x")],
}
.apply(&mut fx.state())
.unwrap();
let names: Vec<_> = fx
.order
.0
.iter()
.map(|id| fx.files[id].info.name.clone())
.collect();
assert_eq!(names, ["a", "c"]);
}
#[test]
fn test_load_several_invalid_files_changes_nothing() {
let mut fx = Fixture::default();
crate::New { name: "keep" }.apply(&mut fx.state()).unwrap();
let selected = fx.selected_files();
let result = LoadFiles {
files: vec![
load(b"<gpx><trk></gpx>", "x"),
load(b"<gpx><wpt></gpx>", "y"),
],
}
.apply(&mut fx.state());
assert!(matches!(result, Err(CommandError::InvalidData(_))));
assert_eq!(fx.files.len(), 1);
assert_eq!(fx.order.0.len(), 1);
assert_eq!(fx.selected_files(), selected);
// nothing to load
assert_eq!(
LoadFiles { files: vec![] }.apply(&mut fx.state()),
Err(CommandError::NothingToDo)
);
}
#[test]
fn test_each_file_without_name_gets_its_own_name() {
let mut fx = Fixture::default();
let unnamed = br#"<gpx version="1.1"><trk><trkseg></trkseg></trk></gpx>"#;
LoadFiles {
files: vec![load(unnamed, "first"), load(unnamed, "second")],
}
.apply(&mut fx.state())
.unwrap();
let names: Vec<_> = fx
.order
.0
.iter()
.map(|id| fx.files[id].info.name.clone())
.collect();
assert_eq!(names, ["first", "second"]);
}
}
+39
View File
@@ -1071,4 +1071,43 @@ mod tests {
assert!(engine.execute(Action::Undo));
assert_eq!(track_ids(&engine, a), tracks);
}
#[test]
fn test_load_several_files_is_one_edit() {
use crate::LoadFiles;
let mut engine = Engine::default();
new(&mut engine, "before");
let gpx = |name: &str| {
format!(r#"<gpx version="1.1"><metadata><name>{name}</name></metadata></gpx>"#)
.into_bytes()
};
let (a, b) = (gpx("a"), gpx("b"));
assert!(edit(
&mut engine,
Command::LoadFiles(LoadFiles {
files: vec![
Load {
data: &a,
name: "x"
},
Load {
data: &b,
name: "x"
},
],
})
));
assert_eq!(engine.order().len(), 3);
let diff = engine.last_diff().unwrap();
assert_eq!(diff.added.len(), 2);
// the first one that was loaded is selected
assert_eq!(engine.selection(), &files(&[engine.order()[1]]));
// a single undo removes both
assert!(engine.execute(Action::Undo));
assert_eq!(engine.order().len(), 1);
assert!(engine.execute(Action::Redo));
assert_eq!(engine.order().len(), 3);
}
}