From c6b7de43bc4e69d8a654156bdc7c8c4c47d5508a Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Tue, 3 Nov 2026 20:05:53 -0500 Subject: [PATCH] engine: use sets for slide-range expansion, added slide selection and repeated-word offsets (#1756) --- crates/engine/src/cmd/file.rs | 13 ++++++++++++- crates/engine/src/cmd/review.rs | 2 +- crates/engine/src/cmd/slide.rs | 3 ++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/crates/engine/src/cmd/file.rs b/crates/engine/src/cmd/file.rs index 8943eb9a3..4f8580c86 100644 --- a/crates/engine/src/cmd/file.rs +++ b/crates/engine/src/cmd/file.rs @@ -397,6 +397,7 @@ pub fn pdf_options(p: &Value, count: usize) -> Result pub fn parse_slide_range(text: &str, count: usize) -> Result> { let invalid = || bad("file.export", format!("“{text}” isn't a valid slide range: use numbers and ranges such as 1-3, 5")); let mut out: Vec = vec![]; + let mut seen: std::collections::HashSet = std::collections::HashSet::new(); for part in text.split([',', ';']).map(str::trim).filter(|p| !p.is_empty()) { let (a, b) = match part.split_once('-') { Some((a, b)) => (a.trim(), b.trim()), @@ -412,7 +413,7 @@ pub fn parse_slide_range(text: &str, count: usize) -> Result> { return Err(bad("file.export", format!("the presentation has {count} slides; “{part}” is outside them"))); } for i in lo - 1..hi { - if !out.contains(&i) { + if seen.insert(i) { out.push(i); } } @@ -776,6 +777,16 @@ mod tests { use super::{parse_slide_range, write_file}; + #[test] + fn broad_and_repeated_slide_ranges_expand_once() { + // 200 copies of a 100,000-slide range: each index once, in order, without a scan per index. + let text = vec!["1-100000"; 200].join(","); + let v = parse_slide_range(&text, 100_000).unwrap(); + assert_eq!(v.len(), 100_000); + assert!(v.windows(2).all(|w| w[0] < w[1])); + assert_eq!(parse_slide_range("3, 1-3, 2", 3).unwrap(), vec![2, 0, 1]); + } + #[test] fn slide_range_rejects_plus_sign() { assert!(parse_slide_range("+2", 3).is_err()); diff --git a/crates/engine/src/cmd/review.rs b/crates/engine/src/cmd/review.rs index 92637741f..47cf1ff24 100644 --- a/crates/engine/src/cmd/review.rs +++ b/crates/engine/src/cmd/review.rs @@ -367,7 +367,7 @@ fn spelling(s: &mut Session, p: &Value) -> Result { } out.push(v); } - let rep: Vec = crate::spell::repeated(para, &default).iter().map(|w| w.start).collect(); + let rep: std::collections::HashSet = crate::spell::repeated(para, &default).iter().map(|w| w.start).collect(); for w in crate::spell::paragraph_words(para, &default) { if ignore.contains(&w.text.to_lowercase()) || rep.contains(&w.start) { continue; diff --git a/crates/engine/src/cmd/slide.rs b/crates/engine/src/cmd/slide.rs index 198f1bc6c..f15cf32fa 100644 --- a/crates/engine/src/cmd/slide.rs +++ b/crates/engine/src/cmd/slide.rs @@ -408,8 +408,9 @@ fn select_slides(s: &mut Session, p: &Value) -> Result { }; s.select(|doc, sel| { if add { + let mut have: std::collections::HashSet<_> = sel.slides.iter().copied().collect(); for i in &ids { - if !sel.slides.contains(i) { + if have.insert(*i) { sel.slides.push(*i); } }