Merge pull request #766 from vangel76:fix/multi-row-delete-758
This commit is contained in:
commit
61266958ff
|
|
@ -132,6 +132,7 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in
|
|||
|
||||
| Date | What landed |
|
||||
|---|---|
|
||||
| 2026-10-12 | Deleting several Ctrl-selected rows or columns removes them all in one undo step (#758) |
|
||||
| 2026-10-12 | XLSX chart sheets are kept on open and save: the sheet, its drawing and chart (with style and colours) are written back unchanged in their place among the worksheets, and left out when the sheet they plot is deleted or renamed or a worksheet took their name (#727) |
|
||||
| 2026-10-12 | CSV and tab-separated export is capped at 512 MiB: a sheet whose used range would expand to more (two cells far apart, such as A1 and XFD1048576, are 16 GiB of commas) is refused with the rows, columns and limit, before anything is allocated or written (#528) |
|
||||
| 2026-10-12 | PDF writer hardening: text width cannot overflow, all-zero dash arrays and negative or NaN stroke widths are sanitised, non-finite sizes and positions draw nothing (#812) |
|
||||
|
|
@ -188,6 +189,7 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in
|
|||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-12 | minor | Progress log: multi-area row/column delete (#758) |
|
||||
| 2026-10-12 | trivial | Progress log: XLSX chart sheets kept on save (#727) |
|
||||
| 2026-10-12 | trivial | Progress log: CSV export size cap (#528) |
|
||||
| 2026-10-12 | minor | Progress log: margins dragged on the Page Layout rulers (#792) |
|
||||
|
|
|
|||
|
|
@ -292,30 +292,65 @@ fn insert_lines(s: &mut Session, p: &Json, axis: Axis) -> Result<Json> {
|
|||
Ok(out)
|
||||
}
|
||||
|
||||
/// The areas a line delete/insert targets as `(first line, count)`, merged and sorted from the
|
||||
/// last line to the first so deleting one never shifts the next. A comma-separated list
|
||||
/// (`"3:3,7:8"`) or a multi-area selection yields several; `count` forces the single-area form.
|
||||
fn delete_areas(s: &Session, p: &Json, axis: Axis) -> Result<Vec<(u32, u32)>> {
|
||||
let key = if axis == Axis::Rows { "rows" } else { "cols" };
|
||||
let spans: Vec<(u32, u32)> = if u32_param(p, "count").is_some() {
|
||||
let (at, count) = lines(s, p, axis)?;
|
||||
let limit = if axis == Axis::Rows { MAX_ROWS } else { MAX_COLS };
|
||||
// Deleting past the last line only removes up to the end of the worksheet.
|
||||
let count = count.min(limit.saturating_sub(at)).max(1);
|
||||
vec![(at, at.saturating_add(count - 1))]
|
||||
} else if let Some(t) = str_param(p, key).or_else(|| str_param(p, "range")) {
|
||||
t.split(',')
|
||||
.map(|part| {
|
||||
let r = RangeRef::parse(part).ok_or_else(|| bad(key, format!("not a range: {part}")))?;
|
||||
Ok(if axis == Axis::Rows { (r.start.row, r.end.row) } else { (r.start.col, r.end.col) })
|
||||
})
|
||||
.collect::<Result<_>>()?
|
||||
} else {
|
||||
s.doc()?.selection.ranges.iter().map(|r| if axis == Axis::Rows { (r.start.row, r.end.row) } else { (r.start.col, r.end.col) }).collect()
|
||||
};
|
||||
let mut spans: Vec<(u32, u32)> = spans.into_iter().map(|(a, b)| (a.min(b), a.max(b))).collect();
|
||||
spans.sort_unstable();
|
||||
let mut merged: Vec<(u32, u32)> = Vec::new();
|
||||
for (a, b) in spans {
|
||||
match merged.last_mut() {
|
||||
Some(last) if a <= last.1.saturating_add(1) => last.1 = last.1.max(b),
|
||||
_ => merged.push((a, b)),
|
||||
}
|
||||
}
|
||||
Ok(merged.into_iter().rev().map(|(a, b)| (a, b - a + 1)).collect())
|
||||
}
|
||||
|
||||
fn delete_lines(s: &mut Session, p: &Json, axis: Axis) -> Result<Json> {
|
||||
let (at, count) = lines(s, p, axis)?;
|
||||
let limit = if axis == Axis::Rows { MAX_ROWS } else { MAX_COLS };
|
||||
// Deleting past the last line only removes up to the end of the worksheet.
|
||||
let count = count.min(limit.saturating_sub(at)).max(1);
|
||||
check_lines_protection(s, p, axis, at, count, false)?;
|
||||
let areas = delete_areas(s, p, axis)?;
|
||||
for &(at, count) in &areas {
|
||||
check_lines_protection(s, p, axis, at, count, false)?;
|
||||
}
|
||||
let sheet = target_sheet(s, p)?;
|
||||
let out = edit(s, |cx| {
|
||||
let name = cx.wb.sheet(sheet).map(|s| s.name.clone()).unwrap_or_default();
|
||||
cx.protection_checked = true;
|
||||
{
|
||||
let sh = cx.sheet_mut(sheet)?;
|
||||
let block = match axis {
|
||||
Axis::Rows => RangeRef::rows(at, (at + count - 1).min(MAX_ROWS - 1)),
|
||||
Axis::Cols => RangeRef::cols(at, (at + count - 1).min(MAX_COLS - 1)),
|
||||
};
|
||||
sh.take_cells(block);
|
||||
match axis {
|
||||
Axis::Rows => sh.cells.shift_rows(at + count, -(count as i64)),
|
||||
Axis::Cols => sh.cells.shift_cols(at + count, -(count as i64)),
|
||||
// Last area first, so the earlier ones keep their positions.
|
||||
for (at, count) in areas {
|
||||
{
|
||||
let sh = cx.sheet_mut(sheet)?;
|
||||
let block = match axis {
|
||||
Axis::Rows => RangeRef::rows(at, (at + count - 1).min(MAX_ROWS - 1)),
|
||||
Axis::Cols => RangeRef::cols(at, (at + count - 1).min(MAX_COLS - 1)),
|
||||
};
|
||||
sh.take_cells(block);
|
||||
match axis {
|
||||
Axis::Rows => sh.cells.shift_rows(at + count, -(count as i64)),
|
||||
Axis::Cols => sh.cells.shift_cols(at + count, -(count as i64)),
|
||||
}
|
||||
shift_sheet_features(sh, axis, at, count, false);
|
||||
}
|
||||
shift_sheet_features(sh, axis, at, count, false);
|
||||
rewrite(&mut cx.wb, &name, &Edit::Delete { axis, at, count });
|
||||
}
|
||||
rewrite(&mut cx.wb, &name, &Edit::Delete { axis, at, count });
|
||||
cx.structural = true;
|
||||
Ok(Json::Null)
|
||||
})?;
|
||||
|
|
|
|||
|
|
@ -560,6 +560,28 @@ fn insert_delete_rows_adjust() {
|
|||
assert_eq!(v(&s, "A1"), Value::Number(10.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deleting_a_multi_area_row_selection_removes_every_area_and_undoes_once() {
|
||||
let mut s = s();
|
||||
s.execute("range.setValues", json!({"range": "A1", "values": [[1], [2], [3], [4], [5], [6]]})).unwrap();
|
||||
s.execute("cell.set", json!({"cell": "B1", "input": "=A6"})).unwrap();
|
||||
// Ctrl-select rows 2, 4 and 5:6 (A1 refs are 1-based).
|
||||
let sel = &mut s.doc_mut().unwrap().selection;
|
||||
sel.ranges = vec![gridcraft_core::RangeRef::rows(1, 1), gridcraft_core::RangeRef::rows(3, 3), gridcraft_core::RangeRef::rows(4, 5)];
|
||||
sel.active = CellRef::new(4, 0);
|
||||
s.doc_mut().unwrap().undo.clear();
|
||||
s.execute("home.deleteRows", json!({})).unwrap();
|
||||
assert_eq!((v(&s, "A1"), v(&s, "A2"), v(&s, "A3")), (Value::Number(1.0), Value::Number(3.0), Value::Empty));
|
||||
assert_eq!(s.execute("cell.get", json!({"cell": "B1"})).unwrap()["formula"], "=#REF!");
|
||||
s.execute("edit.undo", json!({})).unwrap();
|
||||
assert_eq!((1..=6).map(|r| v(&s, &format!("A{r}"))).collect::<Vec<_>>(), (1..=6).map(|n| Value::Number(n as f64)).collect::<Vec<_>>());
|
||||
assert!(s.doc().unwrap().undo.is_empty(), "one undo step restored all rows");
|
||||
// The explicit list form behaves the same, for columns too.
|
||||
s.execute("range.setValues", json!({"range": "A1", "values": [[1, 2, 3, 4]]})).unwrap();
|
||||
s.execute("home.deleteColumns", json!({"cols": "A:A,C:C"})).unwrap();
|
||||
assert_eq!((v(&s, "A1"), v(&s, "B1"), v(&s, "C1")), (Value::Number(2.0), Value::Number(4.0), Value::Empty));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn move_columns_reorders_without_overwriting_and_undoes_once() {
|
||||
let mut s = s();
|
||||
|
|
|
|||
Loading…
Reference in New Issue