From 1f7a4ab3e4d47e36a63f1e54278c1e63687c83cb Mon Sep 17 00:00:00 2001 From: Marco Arnulfo Date: Mon, 12 Oct 2026 16:42:19 +0200 Subject: [PATCH 1/2] Restore Last Selection brings back the previous selection (fixes #231) --- crates/engine/src/cmd/edit.rs | 55 +++++++++++++++++++++++++++++++++-- crates/engine/src/cmd/mod.rs | 2 ++ crates/engine/src/lib.rs | 1 + 3 files changed, 56 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/cmd/edit.rs b/crates/engine/src/cmd/edit.rs index 776fe023..7e8bd9eb 100644 --- a/crates/engine/src/cmd/edit.rs +++ b/crates/engine/src/cmd/edit.rs @@ -10,7 +10,7 @@ pub fn specs() -> Vec { vec![ cmd!(noundo "edit.undo", "Undo", ["Edit"], Some("Cmd+Z"), "{}", can_undo, |e, _| Ok(json!({"undone": e.undo()}))), cmd!(noundo "edit.redo", "Redo", ["Edit"], Some("Cmd+Shift+Z"), "{}", can_redo, |e, _| Ok(json!({"redone": e.redo()}))), - cmd!("edit.restore_last_selection", "Restore Last Selection", ["Edit"], Some("Cmd+Alt+Z"), "{}", always, restore_selection), + cmd!(noundo "edit.restore_last_selection", "Restore Last Selection", ["Edit"], Some("Cmd+Alt+Z"), "{}", has_last_selection, restore_selection), cmd!("edit.cut", "Cut", ["Edit"], Some("Cmd+X"), "{tracks?, start?, end?}", has_range, |e, p| cut_copy(e, p, true, "edit.cut")), cmd!(noundo "edit.copy", "Copy", ["Edit"], Some("Cmd+C"), "{tracks?, start?, end?}", has_range, |e, p| cut_copy(e, p, false, "edit.copy")), cmd!("edit.paste", "Paste", ["Edit"], Some("Cmd+V"), "{tracks?, at?}", has_clipboard, paste), @@ -153,8 +153,43 @@ fn shuffle(s: &Session) -> bool { s.edit.edit_mode == EditMode::Shuffle } +const LAST: &str = "selection.last"; + +fn last_selection(s: &Session) -> Option<(Range, Vec)> { + let v = |k: &str| s.edit.values.get(&format!("{LAST}.{k}")).copied().filter(|v| v.is_finite() && *v >= 0.0); + let range = Range::new(v("start")? as Samples, v("end")? as Samples); + let tracks = (0..).map_while(|i| v(&format!("track.{i}"))).map(|t| TrackId(t as u64)).filter(|t| s.track(*t).is_some()).collect(); + Some((range, tracks)) +} + +fn has_last_selection(e: &Engine) -> std::result::Result<(), String> { + if last_selection(e.session()).is_some() { Ok(()) } else { Err("no previous selection to restore".into()) } +} + +/// Remember the selection `before` had when a command changed it, for Restore Last Selection +/// (kept in `edit.values` like the recent playlist, so it is saved with the session). +pub(crate) fn remember_selection(e: &mut Engine, before: &Session) { + let (old, new) = (&before.edit, &e.session().edit); + if old.selection == new.selection && old.selected_tracks == new.selected_tracks { + return; + } + let values = &mut e.session_mut().edit.values; + values.retain(|k, _| !k.starts_with(LAST)); + values.insert(format!("{LAST}.start"), old.selection.start as f64); + values.insert(format!("{LAST}.end"), old.selection.end as f64); + for (i, t) in old.selected_tracks.iter().enumerate() { + values.insert(format!("{LAST}.track.{i}"), t.0 as f64); + } +} + +/// Swaps in the previous selection; `remember_selection` then keeps the one replaced, so +/// invoking it again goes back. fn restore_selection(e: &mut Engine, _: &Value) -> Result { - Ok(json!({"selection": e.session().edit.selection})) + let (range, tracks) = last_selection(e.session()).ok_or_else(|| bad("edit.restore_last_selection", "no previous selection to restore"))?; + let ed = &mut e.session_mut().edit; + ed.selection = range; + ed.selected_tracks = tracks; + Ok(json!({"selection": range})) } fn cut_copy(e: &mut Engine, p: &Value, cut: bool, id: &str) -> Result { @@ -782,4 +817,20 @@ mod tests { e.execute("edit.select", &json!({"tracks": [ids[0]], "start": 100, "end": 300})).unwrap(); assert_ne!(e.revision, rev); } + + #[test] + fn restore_last_selection_swaps_back_to_the_previous_range_and_tracks() { + let mut e = Engine::default(); + assert!(e.execute("edit.restore_last_selection", &json!({})).is_err(), "nothing to restore yet"); + let ids = e.execute("track.new", &json!({"count": 2})).unwrap()["tracks"].clone(); + e.execute("edit.select", &json!({"tracks": [ids[0]], "start": 1_000, "end": 4_000})).unwrap(); + e.execute("edit.select", &json!({"tracks": [ids[1]], "start": 7_000, "end": 9_000})).unwrap(); + let tracks = |e: &Engine| e.session().edit.selected_tracks.iter().map(|t| t.0).collect::>(); + e.execute("edit.restore_last_selection", &json!({})).unwrap(); + assert_eq!(e.session().edit.selection, soundcraft_time::Range::new(1_000, 4_000)); + assert_eq!(json!(tracks(&e)), json!([ids[0]])); + e.execute("edit.restore_last_selection", &json!({})).unwrap(); + assert_eq!(e.session().edit.selection, soundcraft_time::Range::new(7_000, 9_000)); + assert_eq!(json!(tracks(&e)), json!([ids[1]])); + } } diff --git a/crates/engine/src/cmd/mod.rs b/crates/engine/src/cmd/mod.rs index d0a453c7..96ba4a23 100644 --- a/crates/engine/src/cmd/mod.rs +++ b/crates/engine/src/cmd/mod.rs @@ -36,6 +36,8 @@ mod view; mod view_more; mod vst3; +pub(crate) use edit::remember_selection; + pub type Run = fn(&mut Engine, &Value) -> Result; /// `Err(reason)` = disabled. pub type Enabled = fn(&Engine) -> std::result::Result<(), String>; diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index b607534d..f928b295 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -282,6 +282,7 @@ impl Engine { }; match result { Ok(v) => { + cmd::remember_selection(self, &before); if spec.undoable && !Arc::ptr_eq(&before, &self.doc) { self.push_undo(spec.label, before); } From d5f069417e5c8940e0ebc08d5b118b31f9c8870e Mon Sep 17 00:00:00 2001 From: Marco Arnulfo Date: Mon, 12 Oct 2026 16:47:56 +0200 Subject: [PATCH 2/2] Keep the last selection out of the document so selecting leaves the session clean --- crates/engine/src/cmd/edit.rs | 25 +++++-------------------- crates/engine/src/lib.rs | 5 +++++ 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/crates/engine/src/cmd/edit.rs b/crates/engine/src/cmd/edit.rs index 7e8bd9eb..0738341b 100644 --- a/crates/engine/src/cmd/edit.rs +++ b/crates/engine/src/cmd/edit.rs @@ -153,39 +153,24 @@ fn shuffle(s: &Session) -> bool { s.edit.edit_mode == EditMode::Shuffle } -const LAST: &str = "selection.last"; - -fn last_selection(s: &Session) -> Option<(Range, Vec)> { - let v = |k: &str| s.edit.values.get(&format!("{LAST}.{k}")).copied().filter(|v| v.is_finite() && *v >= 0.0); - let range = Range::new(v("start")? as Samples, v("end")? as Samples); - let tracks = (0..).map_while(|i| v(&format!("track.{i}"))).map(|t| TrackId(t as u64)).filter(|t| s.track(*t).is_some()).collect(); - Some((range, tracks)) -} - fn has_last_selection(e: &Engine) -> std::result::Result<(), String> { - if last_selection(e.session()).is_some() { Ok(()) } else { Err("no previous selection to restore".into()) } + if e.last_selection.is_some() { Ok(()) } else { Err("no previous selection to restore".into()) } } -/// Remember the selection `before` had when a command changed it, for Restore Last Selection -/// (kept in `edit.values` like the recent playlist, so it is saved with the session). +/// Remember the selection `before` had when a command changed it, for Restore Last Selection. pub(crate) fn remember_selection(e: &mut Engine, before: &Session) { let (old, new) = (&before.edit, &e.session().edit); if old.selection == new.selection && old.selected_tracks == new.selected_tracks { return; } - let values = &mut e.session_mut().edit.values; - values.retain(|k, _| !k.starts_with(LAST)); - values.insert(format!("{LAST}.start"), old.selection.start as f64); - values.insert(format!("{LAST}.end"), old.selection.end as f64); - for (i, t) in old.selected_tracks.iter().enumerate() { - values.insert(format!("{LAST}.track.{i}"), t.0 as f64); - } + e.last_selection = Some((old.selection, old.selected_tracks.clone())); } /// Swaps in the previous selection; `remember_selection` then keeps the one replaced, so /// invoking it again goes back. fn restore_selection(e: &mut Engine, _: &Value) -> Result { - let (range, tracks) = last_selection(e.session()).ok_or_else(|| bad("edit.restore_last_selection", "no previous selection to restore"))?; + let (range, mut tracks) = e.last_selection.clone().ok_or_else(|| bad("edit.restore_last_selection", "no previous selection to restore"))?; + tracks.retain(|t| e.session().track(*t).is_some()); let ed = &mut e.session_mut().edit; ed.selection = range; ed.selected_tracks = tracks; diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index f928b295..ba9f67e5 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -120,6 +120,9 @@ pub struct Engine { merge_key: Option, /// Messages for the UI status line / agents. pub messages: Vec, + /// The edit selection (range, tracks) before the last change, for Restore Last Selection. + /// Kept out of the document so selecting never marks the session modified. + pub last_selection: Option<(soundcraft_time::Range, Vec)>, } impl Default for Engine { @@ -148,6 +151,7 @@ impl Engine { dirty_at: Cell::new((0, false)), merge_key: None, messages: Vec::new(), + last_selection: None, } } @@ -181,6 +185,7 @@ impl Engine { self.saved = Arc::clone(&self.doc); self.undo.clear(); self.redo.clear(); + self.last_selection = None; self.revision = self.revision.wrapping_add(1); self.dirty = false; }