mirror of
https://github.com/storytold/gridcraft.git
synced 2026-11-07 02:51:33 +00:00
Objects: every kind follows edits; chart series follow cut-moves and sheet deletes
Structural edits and sorts kept three hand-indexed lists (charts, images,
shapes) in step in two places, and slicers were left out of both, so a
slicer stayed put when rows were inserted above it. Chart series were
rewritten for inserted and deleted lines and for sheet renames, but not for
cut-moves or sheet deletes, which left them pointing at the old cells.
- Inserting and deleting rows and columns and sorting walk
`Sheet::object_anchors` and `object_anchors_mut`, one fixed order over
every kind, so slicers now follow their cells by their anchor mode like
the other objects, and a new kind cannot be forgotten in one of the
passes.
- `rewrite_cell_links` is the one place that rewrites the references
floating objects hold (chart series values, categories and names,
and the source block a chart was made from).
`rewrite_all_formulas` calls it, which covers inserting and deleting
lines and cells and cut-moves on one sheet or across sheets; renaming and
deleting a sheet call it too, so a series into a deleted sheet becomes
#REF!, as a formula does.
- Copying a sheet gives its charts, pictures, shapes and slicers fresh ids,
keeping ids unique across the workbook, which object selection and the
`{kind, id}` commands rely on.
Tests: every kind through row and column inserts and deletes, an absolute
shape, a sort and undo; a slicer through inserted rows; chart series
through a cut-move, a sheet rename, a sheet delete and undo; ids after a
sheet copy.
This commit is contained in:
1 parent
e7591de2ec
commit
2fce1a9b42
7 files changed
+155
-71
No files matched your search
+1
-1
@@ -135,7 +135,7 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in
|
||||
|
||||
| Date | What landed |
|
||||
|---|---|
|
||||
| 2026-10-31 | Floating objects: charts, pictures, shapes and slicers share one selection with a frame of eight resize handles (each moves only its own edges, also on right-to-left sheets); Delete or Backspace deletes the selected object and Escape drops the selection; `ui.inspect` reports `selectedObject` and `ui.object.select` selects any object (#190, #213) |
|
||||
| 2026-10-31 | Floating objects: charts, pictures, shapes and slicers share one selection with a frame of eight resize handles (each moves only its own edges, also on right-to-left sheets); Delete or Backspace deletes the selected object and Escape drops the selection; slicers follow row and column edits and sorts like the other objects; chart series follow cut-moves and sheet renames and deletes; a copied sheet gives its objects fresh ids; `ui.inspect` reports `selectedObject` and `ui.object.select` selects any object (#190, #213) |
|
||||
| 2026-10-25 | AutoSum over a selection does what Excel does: a selected row gets one total to its right (in its last cell if that is empty), not a one-cell total under each cell; a block whose last column is empty gets each row's total there, one whose last row is empty each column's, and with both the corner gets the total of the block; otherwise totals go under each column as before |
|
||||
| 2026-10-24 | Macro interface ([macros.md](docs/macros.md#in-the-window)): an Automate › Macros group (Record Macro, which turns into Stop Recording, Use Relative References, Run Macro with recent macros); "Recording · N steps" with Stop in the status bar; a Record Macro dialog (checked name, free Ctrl+Shift shortcut, description); the Macros dialog lists macros beside Automate scripts and a VBA project, with Run, Step Into, Edit, Create, Delete and Options; Ctrl+Shift+letter macro shortcuts; questions that offer Run, Overwrite, Shift Down or Shift Right; a step editor with a breakpoint gutter, drag to reorder, a command picker, reference boxes that switch between relative tokens and absolute references, Inputs and Output tabs, a debug bar (Check, Run, Step, Continue, Run to Here, Stop with keep or undo) and grid overlays for inputs, writes, conflicts and the current step |
|
||||
| 2026-10-24 | Go To lists the named ranges of every sheet (`Sheet2!Name` for a name scoped to another sheet; hidden names, constants and formulas left out) and the references entered before, a double-click goes to an entry, and the Reference box takes several names or references separated by commas (`Price, Type`, `B14:C22,F19:G30`) |
|
||||
|
||||
@@ -195,8 +195,7 @@ fn do_sort(s: &mut Session, sheet: usize, r: RangeRef, header: bool, keys: Vec<S
|
||||
// The object's own geometry (its top-left anchor cell, offset and size) is read from the
|
||||
// pre-sort `sh`; a two-cell object's bottom-right cell is derived from its size here so it
|
||||
// can be re-fitted to the lines it spans after the sort.
|
||||
let objs: Vec<Anchor> =
|
||||
sh.images.iter().map(|o| o.anchor).chain(sh.charts.iter().map(|o| o.anchor)).chain(sh.shapes.iter().map(|o| o.anchor)).collect();
|
||||
let objs: Vec<Anchor> = sh.object_anchors().copied().collect();
|
||||
let descs: Vec<(u32, u32, f32, f32)> = objs
|
||||
.iter()
|
||||
.map(|a| {
|
||||
@@ -242,33 +241,22 @@ fn do_sort(s: &mut Session, sheet: usize, r: RangeRef, header: bool, keys: Vec<S
|
||||
}
|
||||
}
|
||||
}
|
||||
// Re-anchor floating objects now that the row heights are final. `objs` was collected as
|
||||
// images ++ charts ++ shapes, so the same index maps straight onto each vec.
|
||||
// Re-anchor floating objects now that the row heights are final. `objs` was collected in
|
||||
// `object_anchors` order, which `object_anchors_mut` repeats.
|
||||
// Only objects anchored inside the sorted block move: within its lines on the sort axis
|
||||
// and within its span on the other axis (a picture beside the block stays put).
|
||||
let (b0, b1) = if by_cols { (body.start.col, body.end.col) } else { (body.start.row, body.end.row) };
|
||||
let (o0, o1) = if by_cols { (body.start.row, body.end.row) } else { (body.start.col, body.end.col) };
|
||||
let ni = shm.images.len();
|
||||
let nch = shm.charts.len();
|
||||
for (i, (a, desc)) in objs.into_iter().zip(descs).enumerate() {
|
||||
let other = if by_cols { a.cell.row } else { a.cell.col };
|
||||
if other < o0 || other > o1 {
|
||||
continue;
|
||||
}
|
||||
let na = sorted_anchor(shm, a, desc, by_cols, b0, b1, &dst_of);
|
||||
if na != a {
|
||||
if i < ni {
|
||||
if let Some(o) = shm.images.get_mut(i) {
|
||||
o.anchor = na;
|
||||
}
|
||||
} else if i < ni + nch {
|
||||
if let Some(o) = shm.charts.get_mut(i - ni) {
|
||||
o.anchor = na;
|
||||
}
|
||||
} else if let Some(o) = shm.shapes.get_mut(i - ni - nch) {
|
||||
o.anchor = na;
|
||||
}
|
||||
}
|
||||
let new_anchors: Vec<Anchor> = objs
|
||||
.into_iter()
|
||||
.zip(descs)
|
||||
.map(|(a, desc)| {
|
||||
let other = if by_cols { a.cell.row } else { a.cell.col };
|
||||
if other < o0 || other > o1 { a } else { sorted_anchor(shm, a, desc, by_cols, b0, b1, &dst_of) }
|
||||
})
|
||||
.collect();
|
||||
for (a, n) in shm.object_anchors_mut().zip(new_anchors) {
|
||||
*a = n;
|
||||
}
|
||||
for (c, _) in moved {
|
||||
cx.changed.push((sheet, c));
|
||||
|
||||
@@ -35,7 +35,7 @@ use go_to::go_to_special;
|
||||
#[cfg(test)]
|
||||
use go_to::{GO_TO_MAX_AREAS, GO_TO_RECENT, reference_parts};
|
||||
use paste_special::paste_special;
|
||||
pub(crate) use paste_special::rewrite_all_formulas;
|
||||
pub(crate) use paste_special::{rewrite_all_formulas, rewrite_cell_links};
|
||||
use selection::{select_all, select_col, select_region, select_row, selection_move, selection_next, selection_set};
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
|
||||
@@ -355,4 +355,32 @@ pub(crate) fn rewrite_all_formulas(wb: &mut gridcraft_model::Workbook, target: &
|
||||
n.formula = gridcraft_formula::print(&new);
|
||||
}
|
||||
}
|
||||
rewrite_cell_links(wb, |host, expr| gridcraft_formula::adjust::adjust(expr, host, target, e));
|
||||
}
|
||||
|
||||
/// Runs `f` over every reference a floating object holds (a chart's series values, categories
|
||||
/// and name, and the source block it was made from). Every edit that moves references calls it:
|
||||
/// inserting and deleting lines and cells and cut-moves (through [`rewrite_all_formulas`]), and
|
||||
/// renaming and deleting sheets. `f` maps a reference held by an object on the sheet named `host`
|
||||
/// to its new form; text that does not parse is kept. A source block that no longer exists is
|
||||
/// dropped.
|
||||
pub(crate) fn rewrite_cell_links(wb: &mut gridcraft_model::Workbook, f: impl Fn(&str, gridcraft_formula::Expr) -> gridcraft_formula::Expr) {
|
||||
for si in 0..wb.sheets.len() {
|
||||
let Some(host) = wb.sheets.get(si).filter(|s| !s.charts.is_empty()).map(|s| s.name.clone()) else { continue };
|
||||
let Some(sh) = wb.sheet_mut(si) else { continue };
|
||||
let fix = |text: &str| -> String {
|
||||
match gridcraft_formula::parse(text) {
|
||||
Ok(expr) => gridcraft_formula::print(&f(&host, expr)),
|
||||
Err(_) => text.to_string(),
|
||||
}
|
||||
};
|
||||
for ch in sh.charts.iter_mut() {
|
||||
ch.source = ch.source.as_deref().map(fix).filter(|s| !s.contains("#REF!"));
|
||||
for se in ch.series.iter_mut() {
|
||||
se.values = fix(&se.values);
|
||||
se.categories = se.categories.as_deref().map(fix);
|
||||
se.name = se.name.as_deref().map(fix);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -141,14 +141,7 @@ fn shift_sheet_features(sh: &mut Sheet, axis: Axis, at: u32, count: u32, insert:
|
||||
};
|
||||
// Floating objects: snapshot their bottom-right corners against the pre-shift geometry, so a
|
||||
// re-fit after the shift measures against the grid the object actually lands on.
|
||||
let obj_corners: Vec<(u32, u32, f32, f32)> = sh
|
||||
.charts
|
||||
.iter()
|
||||
.map(|o| &o.anchor)
|
||||
.chain(sh.images.iter().map(|o| &o.anchor))
|
||||
.chain(sh.shapes.iter().map(|o| &o.anchor))
|
||||
.map(|a| anchor_corner(sh, a))
|
||||
.collect();
|
||||
let obj_corners: Vec<(u32, u32, f32, f32)> = sh.object_anchors().map(|a| anchor_corner(sh, a)).collect();
|
||||
// Line info.
|
||||
let lines = if axis == Axis::Rows { &mut sh.rows } else { &mut sh.cols };
|
||||
let old = std::mem::take(lines);
|
||||
@@ -193,22 +186,13 @@ fn shift_sheet_features(sh: &mut Sheet, axis: Axis, at: u32, count: u32, insert:
|
||||
std::mem::take(&mut sh.cell_pictures).into_iter().filter_map(|(c, v)| map_cell(c).filter(CellRef::is_valid).map(|c| (c, v))).collect();
|
||||
sh.comments = std::mem::take(&mut sh.comments).into_iter().filter_map(|(c, v)| map_cell(c).map(|c| (c, v))).collect();
|
||||
sh.hyperlinks = std::mem::take(&mut sh.hyperlinks).into_iter().filter_map(|(c, v)| map_cell(c).map(|c| (c, v))).collect();
|
||||
// Floating objects follow their anchor lines per their [`AnchorMode`]. Corners were snapshotted
|
||||
// as charts ++ images ++ shapes, matching the three slices below. New anchors are computed
|
||||
// against the post-shift grid first (the re-fit needs its sizes), then written back.
|
||||
let anchored = |ui: usize, a: Anchor| -> Anchor { obj_corners.get(ui).map_or(a, |&corner| shift_anchor(sh, a, corner, axis, at, &map_line)) };
|
||||
let new_charts: Vec<Anchor> = sh.charts.iter().enumerate().map(|(i, o)| anchored(i, o.anchor)).collect();
|
||||
let new_images: Vec<Anchor> = sh.images.iter().enumerate().map(|(i, o)| anchored(new_charts.len() + i, o.anchor)).collect();
|
||||
let base = new_charts.len() + new_images.len();
|
||||
let new_shapes: Vec<Anchor> = sh.shapes.iter().enumerate().map(|(i, o)| anchored(base + i, o.anchor)).collect();
|
||||
for (o, a) in sh.charts.iter_mut().zip(new_charts) {
|
||||
o.anchor = a;
|
||||
}
|
||||
for (o, a) in sh.images.iter_mut().zip(new_images) {
|
||||
o.anchor = a;
|
||||
}
|
||||
for (o, a) in sh.shapes.iter_mut().zip(new_shapes) {
|
||||
o.anchor = a;
|
||||
// Floating objects follow their anchor lines per their [`AnchorMode`]. New anchors are
|
||||
// computed against the post-shift grid first (the re-fit needs its sizes), then written back
|
||||
// in the same `object_anchors` order the corners were snapshotted in.
|
||||
let new_anchors: Vec<Anchor> =
|
||||
sh.object_anchors().zip(&obj_corners).map(|(a, &corner)| shift_anchor(sh, *a, corner, axis, at, &map_line)).collect();
|
||||
for (a, n) in sh.object_anchors_mut().zip(new_anchors) {
|
||||
*a = n;
|
||||
}
|
||||
sh.sparklines.retain_mut(|sp| match map_cell(sp.cell) {
|
||||
Some(c) => {
|
||||
@@ -231,7 +215,8 @@ fn shift_sheet_features(sh: &mut Sheet, axis: Axis, at: u32, count: u32, insert:
|
||||
}
|
||||
}
|
||||
|
||||
/// Rewrites every formula reference (and chart series) for a structural edit on `target`.
|
||||
/// Rewrites every formula reference (chart series included), sparkline source and PivotTable
|
||||
/// source for a structural edit on `target`.
|
||||
pub(super) fn rewrite(cx: &mut Ctx, target: &str, e: &Edit) {
|
||||
cx.edits.push((target.to_string(), e.clone()));
|
||||
let wb = &mut cx.wb;
|
||||
@@ -245,15 +230,6 @@ pub(super) fn rewrite(cx: &mut Ctx, target: &str, e: &Edit) {
|
||||
Err(_) => text.to_string(),
|
||||
}
|
||||
};
|
||||
for ch in sh.charts.iter_mut() {
|
||||
// The source block moves with its cells; one that was deleted is dropped.
|
||||
ch.source = ch.source.as_deref().map(fix).filter(|s| !s.contains("#REF!"));
|
||||
for se in ch.series.iter_mut() {
|
||||
se.values = fix(&se.values);
|
||||
se.categories = se.categories.as_deref().map(fix);
|
||||
se.name = se.name.as_deref().map(fix);
|
||||
}
|
||||
}
|
||||
for sp in sh.sparklines.iter_mut() {
|
||||
sp.source = fix(&sp.source);
|
||||
}
|
||||
|
||||
@@ -63,6 +63,8 @@ pub(super) fn delete_sheet(s: &mut Session, p: &Json) -> Result<Json> {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Chart series into the deleted sheet become #REF!, like formulas.
|
||||
crate::cmd::edit::rewrite_cell_links(&mut cx.wb, |_, e| gridcraft_formula::adjust::delete_sheet(e, &name, &order));
|
||||
cx.wb.names.retain(|n| n.scope != Some(i));
|
||||
for n in cx.wb.names.iter_mut() {
|
||||
if let Some(sc) = n.scope.as_mut()
|
||||
@@ -116,17 +118,7 @@ pub(super) fn rename_sheet(s: &mut Session, p: &Json) -> Result<Json> {
|
||||
}
|
||||
}
|
||||
}
|
||||
let fix = |t: &str| {
|
||||
gridcraft_formula::parse(t)
|
||||
.map(|e| gridcraft_formula::print(&gridcraft_formula::adjust::rename_sheet(e, &old, &name)))
|
||||
.unwrap_or_else(|_| t.to_string())
|
||||
};
|
||||
for ch in sh.charts.iter_mut() {
|
||||
for se in ch.series.iter_mut() {
|
||||
se.values = fix(&se.values);
|
||||
se.categories = se.categories.as_deref().map(fix);
|
||||
se.name = se.name.as_deref().map(fix);
|
||||
}
|
||||
// A PivotChart follows its PivotTable's sheet.
|
||||
if let Some((sheet, pivot)) = ch.pivot.as_deref().and_then(crate::pivot::parse_chart_link)
|
||||
&& sheet.eq_ignore_ascii_case(&old)
|
||||
@@ -135,6 +127,7 @@ pub(super) fn rename_sheet(s: &mut Session, p: &Json) -> Result<Json> {
|
||||
}
|
||||
}
|
||||
}
|
||||
crate::cmd::edit::rewrite_cell_links(&mut cx.wb, |_, e| gridcraft_formula::adjust::rename_sheet(e, &old, &name));
|
||||
for n in cx.wb.names.iter_mut() {
|
||||
if let Ok(e) = gridcraft_formula::parse_name(&n.formula) {
|
||||
n.formula = gridcraft_formula::print(&gridcraft_formula::adjust::rename_sheet(e, &old, &name));
|
||||
@@ -170,6 +163,18 @@ pub(super) fn move_sheet(s: &mut Session, p: &Json) -> Result<Json> {
|
||||
sh.name = name;
|
||||
sh.tables.clear(); // table names must be unique
|
||||
sh.preserved = None; // kept file content (form controls, OLE objects…) stays with the original
|
||||
// Object ids are unique across the workbook (selection and the `{kind, id}` commands
|
||||
// rely on it), so the copied objects get fresh ones.
|
||||
let mut next = cx.wb.next_object_id();
|
||||
let mut fresh = || {
|
||||
let id = next;
|
||||
next = next.saturating_add(1);
|
||||
id
|
||||
};
|
||||
sh.charts.iter_mut().for_each(|o| o.id = fresh());
|
||||
sh.images.iter_mut().for_each(|o| o.id = fresh());
|
||||
sh.shapes.iter_mut().for_each(|o| o.id = fresh());
|
||||
sh.slicers.iter_mut().for_each(|o| o.id = fresh());
|
||||
let at = to.min(n);
|
||||
// The copy gets its own copies of the sheet's names, referring to the copy.
|
||||
let copies: Vec<gridcraft_model::DefinedName> = cx
|
||||
|
||||
@@ -103,3 +103,90 @@ fn hostile_object_params_are_refused() {
|
||||
let a = sheet(&s, 0).object_anchor(o).unwrap();
|
||||
assert!(a.dx.is_finite() && a.dy.is_finite() && a.width <= 10000.0 && a.height >= 0.0, "{a:?}");
|
||||
}
|
||||
|
||||
fn cell_of(s: &Session, o: ObjRef) -> String {
|
||||
sheet(s, 0).object_anchor(o).unwrap().cell.a1()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_kind_follows_row_and_column_edits_and_sorts() {
|
||||
let mut s = s();
|
||||
let objs = every_kind(&mut s);
|
||||
// Anchors: chart D2, picture D18, shape H18, slicer L2.
|
||||
s.execute("object.setAnchorMode", json!({"kind": "shape", "id": objs[2].id, "mode": "absolute"})).unwrap();
|
||||
let cells = |s: &Session| objs.map(|o| cell_of(s, o));
|
||||
assert_eq!(cells(&s), ["D2", "D18", "H18", "L2"]);
|
||||
s.execute("home.insertRows", json!({"rows": "10:11"})).unwrap();
|
||||
assert_eq!(cells(&s), ["D2", "D20", "H18", "L2"], "rows inserted above move the objects below them; an absolute shape stays");
|
||||
s.execute("home.insertColumns", json!({"cols": "C:C"})).unwrap();
|
||||
assert_eq!(cells(&s), ["E2", "E20", "H18", "M2"]);
|
||||
s.execute("home.deleteColumns", json!({"cols": "C:C"})).unwrap();
|
||||
s.execute("home.deleteRows", json!({"rows": "10:11"})).unwrap();
|
||||
assert_eq!(cells(&s), ["D2", "D18", "H18", "L2"]);
|
||||
// Sorting moves a picture anchored inside the sorted block with its row.
|
||||
s.execute("range.setValues", json!({"range": "T1", "values": [["k"], [3], [1], [2]]})).unwrap();
|
||||
let pic = ObjRef::new(ObjKind::Image, super::add_image(&mut s, "T2", AnchorMode::MoveAndSize));
|
||||
s.execute("selection.set", json!({"range": "T1:T4"})).unwrap();
|
||||
s.execute("data.sortAscending", json!({"header": true, "column": "T"})).unwrap();
|
||||
assert_eq!(cell_of(&s, pic), "T4", "the row holding 3 sorts last");
|
||||
s.execute("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(cell_of(&s, pic), "T2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slicers_follow_inserted_rows_like_other_objects() {
|
||||
let mut s = s();
|
||||
let objs = every_kind(&mut s);
|
||||
s.execute("home.insertRows", json!({"rows": "1:2"})).unwrap();
|
||||
assert_eq!(cell_of(&s, objs[3]), "L4");
|
||||
s.execute("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(cell_of(&s, objs[3]), "L2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chart_series_follow_cut_moves_renames_and_sheet_deletes() {
|
||||
let mut s = s();
|
||||
s.execute("range.setValues", json!({"range": "A1", "values": [[3], [5], [2], [8]]})).unwrap();
|
||||
let chart = id_of(&s.execute("insert.chart", json!({"range": "A1:A4", "type": "column"})).unwrap(), "chart");
|
||||
let values = |s: &Session| sheet(s, 0).charts.iter().find(|c| c.id == chart).map(|c| c.series[0].values.clone()).unwrap_or_default();
|
||||
s.execute("edit.cut", json!({"range": "A1:A4"})).unwrap();
|
||||
s.execute("edit.paste", json!({"at": "F2"})).unwrap();
|
||||
assert!(values(&s).contains("$F$2:$F$5"), "a cut-move takes the series with the cells: {}", values(&s));
|
||||
s.execute("home.insertSheet", json!({"name": "Other"})).unwrap();
|
||||
s.execute("range.setValues", json!({"range": "A1", "values": [[1], [2], [3]]})).unwrap();
|
||||
s.execute("sheet.activate", json!({"sheet": 0})).unwrap();
|
||||
std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().wb).sheet_mut(0).unwrap().charts[0].series[0].values = "Other!$A$1:$A$3".into();
|
||||
s.execute("sheet.rename", json!({"sheet": 1, "name": "Inputs"})).unwrap();
|
||||
assert_eq!(values(&s), "Inputs!$A$1:$A$3");
|
||||
s.execute("home.deleteSheet", json!({"sheet": 1})).unwrap();
|
||||
assert!(values(&s).contains("#REF!"), "a series into a deleted sheet becomes #REF!: {}", values(&s));
|
||||
s.execute("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(values(&s), "Inputs!$A$1:$A$3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_copied_sheet_gives_its_objects_fresh_ids() {
|
||||
let mut s = s();
|
||||
let objs = every_kind(&mut s);
|
||||
s.execute("sheet.move", json!({"sheet": 0, "to": 1, "copy": true})).unwrap();
|
||||
let wb = &s.doc().unwrap().wb;
|
||||
let ids_of = |sh: &Sheet| -> Vec<u32> {
|
||||
sh.charts
|
||||
.iter()
|
||||
.map(|c| c.id)
|
||||
.chain(sh.images.iter().map(|c| c.id))
|
||||
.chain(sh.shapes.iter().map(|c| c.id))
|
||||
.chain(sh.slicers.iter().map(|c| c.id))
|
||||
.collect()
|
||||
};
|
||||
let mut ids: Vec<u32> = wb.sheets.iter().flat_map(|sh| ids_of(sh)).collect();
|
||||
assert_eq!(ids.len(), 2 * objs.len());
|
||||
ids.sort_unstable();
|
||||
ids.dedup();
|
||||
assert_eq!(ids.len(), 2 * objs.len(), "every object id stays unique across the workbook");
|
||||
let copy = sheet(&s, 1);
|
||||
for (o, id) in objs.iter().zip(ids_of(copy)) {
|
||||
assert_eq!(copy.object_ref(id).map(|c| c.kind), Some(o.kind));
|
||||
}
|
||||
assert!(s.doc().unwrap().wb.next_object_id() > *ids.last().unwrap_or(&0));
|
||||
}
|
||||
Reference in new issue
Block a user