From 691798432923ecd46f81c5c2c8d7cf77467da6ec Mon Sep 17 00:00:00 2001 From: bflatastic Date: Sat, 10 Oct 2026 12:48:46 +0700 Subject: [PATCH] Fix #903: Reselect repeats a Same command after Deselect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Select › Reselect repeated the last selection command, but a Select Same command needs a selected object to compare with, so after Deselect it failed with "nothing selected". Reselect now remembers the objects that were selected when the command ran and, with nothing selected, takes the reference from those still in the document. Co-Authored-By: Claude Opus 5.5 --- crates/engine/src/cmd/docmenu.rs | 3 ++- crates/engine/src/cmd/select.rs | 19 ++++++++++++++++--- crates/engine/src/lib.rs | 5 +++-- crates/engine/src/tests.rs | 12 ++++++++++++ 4 files changed, 33 insertions(+), 6 deletions(-) diff --git a/crates/engine/src/cmd/docmenu.rs b/crates/engine/src/cmd/docmenu.rs index 31413ffe..bf38e445 100644 --- a/crates/engine/src/cmd/docmenu.rs +++ b/crates/engine/src/cmd/docmenu.rs @@ -246,8 +246,9 @@ fn candidates(s: &Session, f: impl Fn(&Node) -> bool) -> Result> { fn finish_same(s: &mut Session, cmd: &str, ids: Vec) -> Result { let n = ids.len(); + let from = s.doc()?.selection.objects.clone(); s.select(|_, sel| sel.set(ids))?; - s.doc_mut()?.last_selection_cmd = Some((cmd.to_string(), json!({}))); + s.doc_mut()?.last_selection_cmd = Some((cmd.to_string(), json!({}), from)); Ok(json!({ "count": n })) } diff --git a/crates/engine/src/cmd/select.rs b/crates/engine/src/cmd/select.rs index 0dbda515..8e018e79 100644 --- a/crates/engine/src/cmd/select.rs +++ b/crates/engine/src/cmd/select.rs @@ -216,8 +216,20 @@ fn none(s: &mut Session, _: &Value) -> Result { } fn reselect(s: &mut Session, _: &Value) -> Result { - let Some((c, p)) = s.doc()?.last_selection_cmd.clone() else { return ok() }; - s.execute(&c, &p) + let st = s.doc()?; + let Some((c, p, from)) = st.last_selection_cmd.clone() else { return ok() }; + // After Deselect, a Same command takes its reference again from the objects that were selected + // when it ran, those still there (#903). + let from: Vec = from.into_iter().filter(|id| st.doc.node(*id).is_some()).collect(); + let restore = st.selection.objects.is_empty() && !from.is_empty(); + if restore { + s.select(|_, sel| sel.set(from))?; + } + let r = s.execute(&c, &p); + if restore && r.is_err() { + s.select(|_, sel| sel.clear())?; + } + r } fn inverse(s: &mut Session, _: &Value) -> Result { @@ -401,8 +413,9 @@ fn select_where(s: &mut Session, cmd: &str, p: &Value, f: impl Fn(&Document, &No for l in st.doc.layers.iter().filter(|l| l.visible && !l.locked) { visit(&st.doc, l, &f, &mut ids); } + let from = st.selection.objects.clone(); s.select(|_, sel| sel.set(ids.iter().copied()))?; - s.doc_mut()?.last_selection_cmd = Some((cmd.to_string(), p.clone())); + s.doc_mut()?.last_selection_cmd = Some((cmd.to_string(), p.clone(), from)); Ok(json!({ "count": ids.len() })) } diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 6921d818..487b26f6 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -126,8 +126,9 @@ pub struct DocState { pub undo_group: Option, /// For Object → Transform → Transform Again (⌘D). pub last_transform: Option<(Affine, bool)>, - /// Selection saved by Select → Reselect. - pub last_selection_cmd: Option<(String, Value)>, + /// What Select → Reselect repeats: the last selection command, its params and the objects + /// selected when it ran (a Same command's reference objects). + pub last_selection_cmd: Option<(String, Value, Vec)>, /// Process-unique id of this open document (tab indices shift when tabs close). pub uid: u64, /// View Opacity Mask (Alt-click the mask thumbnail): the masked object whose mask the canvas diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index ffb7e829..ad04e948 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -149,6 +149,18 @@ fn select_same_fill() { let r = s.execute("select.same.fillColor", &json!({})).unwrap(); assert_eq!(r["count"], 2); assert!(!s.doc().unwrap().selection.contains(a)); + // Reselect after Deselect repeats it from the same reference object (#903). + let blue = s.doc().unwrap().selection.objects.clone(); + s.execute("select.none", &json!({})).unwrap(); + s.execute("select.reselect", &json!({})).unwrap(); + assert_eq!(s.doc().unwrap().selection.objects, blue); + // With its reference object gone, it fails as Select Same does, and selects nothing. + s.execute("select.none", &json!({})).unwrap(); + for id in &blue { + s.execute("edit.clear", &json!({"ids": [id.0]})).unwrap(); + } + assert!(s.execute("select.reselect", &json!({})).is_err()); + assert!(s.doc().unwrap().selection.objects.is_empty()); } #[test]