From 121441a079eae4502c87dea7a483aff75f0aba08 Mon Sep 17 00:00:00 2001 From: Faabyy <68284158+Faabyy@users.noreply.github.com> Date: Sun, 25 Oct 2026 13:47:18 +0000 Subject: [PATCH] Slides: the range anchor is held by slide id, so moving, inserting or deleting slides can't shift it (#1105) Co-Authored-By: Claude --- crates/engine/src/cmd/slide.rs | 18 ++++++++++++++++-- crates/engine/src/lib.rs | 5 +++-- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/crates/engine/src/cmd/slide.rs b/crates/engine/src/cmd/slide.rs index 8773b5b5..65f4296e 100644 --- a/crates/engine/src/cmd/slide.rs +++ b/crates/engine/src/cmd/slide.rs @@ -386,7 +386,8 @@ fn select_range(s: &mut Session, p: &Value) -> Result { let (from, count) = { let st = s.doc()?; let sel = &st.selection; - let anchor = sel.range_anchor.filter(|(_, end)| *end == sel.slide).map(|(a, _)| a); + let current = st.doc.slides.get(sel.slide).map(|x| x.id); + let anchor = sel.range_anchor.filter(|(_, end)| Some(*end) == current).and_then(|(a, _)| st.doc.slide_index(a)); (given.or(anchor).unwrap_or(sel.slide), st.doc.slides.len()) }; if to >= count || from >= count { @@ -405,7 +406,7 @@ fn select_range(s: &mut Session, p: &Value) -> Result { sel.slides = range; } sel.slide = to; - sel.range_anchor = Some((from, to)); + sel.range_anchor = doc.slides.get(from).zip(doc.slides.get(to)).map(|(a, b)| (a.id, b.id)); sel.shapes.clear(); sel.text = None; sel.cells = None; @@ -815,6 +816,19 @@ mod tests { assert_eq!(selected_indices(&s), vec![4, 5, 6]); } + #[test] + fn moving_a_slide_drops_the_range_anchor() { + let mut s = deck_of(6); + s.execute("slide.go", &json!({"index": 0})).expect("go"); + s.execute("slide.selectRange", &json!({"to": 2})).expect("range"); + // Slide 0 moves to index 2 and stays current; index 0 now holds another slide, so the + // next Shift-click starts at the moved slide instead of at index 0. + s.execute("slide.move", &json!({"from": 0, "to": 2})).expect("move"); + assert_eq!(s.doc().expect("doc").selection.slide, 2); + s.execute("slide.selectRange", &json!({"to": 4})).expect("range"); + assert_eq!(selected_indices(&s), vec![2, 3, 4]); + } + #[test] fn select_range_clears_a_table_cell_selection_and_returns_to_the_slides() { let mut s = deck_of(3); diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 5455f0c3..f6e67205 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -156,9 +156,10 @@ pub struct Selection { /// Selected table cells: (table, (r0, c0), (r1, c1)). pub cells: Option<(ShapeId, (usize, usize), (usize, usize))>, /// Slide-list range last made with Shift-click: `(anchor, end)`. A further Shift-click extends from - /// the anchor while the current slide is still `end`; moving to another slide drops it. + /// the anchor while the current slide is still `end`; moving to another slide drops it. Held as + /// slide ids, so moving, inserting or deleting slides can't make it name other slides. #[serde(default, skip_serializing_if = "Option::is_none")] - pub range_anchor: Option<(usize, usize)>, + pub range_anchor: Option<(SlideId, SlideId)>, pub target: Target, }