diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 6cc1c90a..03d173dd 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -369,7 +369,15 @@ impl Engine { let key = self.merge_hold.clone().unwrap_or_else(|| key.to_string()); let continuing = self.merge_key.as_ref() == Some(&key); let before_len = self.undo.len(); - let r = self.execute(id, params)?; + // The step pushed for a continuing tick is dropped again below, so it must not trim the + // oldest entry first when the history is full. + let limit = self.history_limit; + if continuing && before_len > 0 { + self.history_limit = limit.max(before_len.saturating_add(1)); + } + let r = self.execute(id, params); + self.history_limit = limit; + let r = r?; if continuing && self.undo.len() == before_len + 1 && before_len > 0 { // Drop the step just pushed; the earlier one still holds the pre-gesture document. self.undo.pop(); diff --git a/crates/engine/tests/fader_merge_full_history.rs b/crates/engine/tests/fader_merge_full_history.rs new file mode 100644 index 00000000..c4656535 --- /dev/null +++ b/crates/engine/tests/fader_merge_full_history.rs @@ -0,0 +1,42 @@ +use serde_json::json; +use soundcraft_engine::Engine; +use soundcraft_model::{ChannelFormat, TrackKind}; + +fn engine_with_full_history(limit: usize) -> (Engine, u64) { + let mut e = Engine::default(); + let t = e.session_mut().add_track(TrackKind::Audio, ChannelFormat::Mono, None).0; + e.history_limit = limit; + for i in 0..limit + 1 { + e.execute("mix.pan", &json!({"track": t, "pan": 0.1 * (i as f64 + 1.0)})).unwrap(); + } + assert_eq!(e.undo_history().len(), limit); + (e, t) +} + +#[test] +fn a_fader_gesture_is_one_undo_step_when_the_history_is_full() { + let (mut e, t) = engine_with_full_history(3); + for db in [-1.0, -2.0, -3.0, -4.0, -5.0] { + e.execute_merged("mix.volume", &json!({"track": t, "db": db}), "fader").unwrap(); + } + e.end_merge(); + let volume = |e: &Engine| e.session().tracks.iter().find(|x| x.id.0 == t).map(|x| x.mixer.volume_db); + assert_eq!(volume(&e), Some(-5.0)); + // The gesture costs one step; it must not push further real edits out of the history. + assert_eq!(e.undo_history().len(), 3, "history: {:?}", e.undo_history()); + assert_eq!(e.undo_history().iter().filter(|l| l.as_str() == "Set Volume").count(), 1, "history: {:?}", e.undo_history()); + assert!(e.undo()); + assert_eq!(volume(&e), Some(0.0), "one undo should undo the whole fader gesture; history: {:?}", e.undo_history()); +} + +#[test] +fn a_zero_history_limit_keeps_no_undo_entry_for_a_gesture() { + let mut e = Engine::default(); + let t = e.session_mut().add_track(TrackKind::Audio, ChannelFormat::Mono, None).0; + e.history_limit = 0; + for db in [-1.0, -2.0, -3.0] { + e.execute_merged("mix.volume", &json!({"track": t, "db": db}), "fader").unwrap(); + } + e.end_merge(); + assert!(e.undo_history().is_empty(), "history: {:?}", e.undo_history()); +}