diff --git a/crates/engine/src/cmd/table.rs b/crates/engine/src/cmd/table.rs index e9f6f164..780c0053 100644 --- a/crates/engine/src/cmd/table.rs +++ b/crates/engine/src/cmd/table.rs @@ -205,7 +205,11 @@ fn cell_fill(s: &mut Session, p: &Value) -> Result { with_table(s, p, "table.cellFill", move |t| { let list: Vec<(usize, usize)> = match (&cells, sel) { (Some(c), _) => c.clone(), - (None, Some((_, a, b))) => (a.0.min(b.0)..=a.0.max(b.0)).flat_map(|r| (a.1.min(b.1)..=a.1.max(b.1)).map(move |c| (r, c))).collect(), + (None, Some((_, a, b))) => { + // The selection can name cells outside the table; only walk the ones that exist. + let (r1, c1) = (a.0.max(b.0).min(t.n_rows().saturating_sub(1)), a.1.max(b.1).min(t.n_cols().saturating_sub(1))); + (a.0.min(b.0)..=r1).flat_map(|r| (a.1.min(b.1)..=c1).map(move |c| (r, c))).collect() + } _ => (0..t.n_rows()).flat_map(|r| (0..t.n_cols()).map(move |c| (r, c))).collect(), }; for (r, c) in list { diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index e83f72ef..8f0c3136 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -835,3 +835,17 @@ fn slide_ranges_parse_like_powerpoints_print_box() { let opts = cmd::file::pdf_options(&json!({"slides": [0, 7, 1]}), 2).unwrap(); assert_eq!(opts.slides, Some(vec![0, 1]), "indices past the end are dropped"); } + +#[test] +fn cell_fill_walks_only_the_cells_a_huge_selection_covers() { + // A selection may reach far past the table; filling it touches the real cells and nothing more. + let mut s = session(); + let id = s.execute("insert.table", &json!({"rows": 2, "cols": 2, "rect": [100.0, 100.0, 200.0, 40.0]})).unwrap()["id"].clone(); + s.execute("table.selectCells", &json!({"id": id, "from": [1, 1], "to": [1_000_000_000u64, 1_000_000_000u64]})).unwrap(); + s.execute("table.cellFill", &json!({"none": true})).unwrap(); + let st = s.doc().unwrap(); + let sh = st.shapes().iter().find(|sh| json!(sh.id) == id).unwrap(); + let deckcraft_model::ShapeKind::Table(t) = &sh.kind else { panic!("not a table") }; + let fills: Vec = [(0, 0), (0, 1), (1, 0), (1, 1)].iter().map(|&(r, c)| t.cell(r, c).is_some_and(|x| x.fill.is_some())).collect(); + assert_eq!(fills, [false, false, false, true]); +} diff --git a/crates/model/src/table.rs b/crates/model/src/table.rs index 2dd46e3b..91555894 100644 --- a/crates/model/src/table.rs +++ b/crates/model/src/table.rs @@ -178,8 +178,9 @@ impl Table { if gs == 1 && rs == 1 { return false; } - for rr in r..r + rs { - for cc in c..c + gs { + // Spans come from the file and may reach past the table; only walk the cells that exist. + for rr in r..r.saturating_add(rs).min(self.rows.len()) { + for cc in c..c.saturating_add(gs).min(self.cols.len()) { if let Some(x) = self.cell_mut(rr, cc) { x.h_merge = false; x.v_merge = false; @@ -349,4 +350,15 @@ mod tests { assert!(!one.delete_row(0) && !one.delete_col(0)); assert!(table_styles().len() > 30); } + + #[test] + fn split_ignores_spans_past_the_table() { + let mut t = Table::new(2, 2, 100.0, 10.0); + if let Some(c) = t.cell_mut(0, 0) { + c.grid_span = u32::MAX; + c.row_span = u32::MAX; + } + assert!(t.split(0, 0)); + assert_eq!(t.cell(0, 0).map(|c| (c.grid_span, c.row_span)), Some((1, 1))); + } }