Commit graph

237 commits

Author SHA1 Message Date
06a3de6126 feat(spreadsheet): dropdown cells and button cells (#9 remainder, complete)
Some checks failed
repo hygiene / hygiene (push) Has been cancelled
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
nigig-build (CAD) / cad-widget-coverage (push) Has been cancelled
spreadsheet / engine-coverage (push) Has been cancelled
spreadsheet / ui-controller-coverage (push) Has been cancelled
Finish the widget-in-cell catalog with the two deferred controls, each a
plain value plus a render flag (same convention as checkbox/slider/Markdown).

- Dropdown: CellStyle.choices (a serialized, pipe-escaped list) makes a
  cell cycle through its choices on click. The toolbar "Drop" button parses
  the selected cell's value as a "Low|Med|High" list into choices (undoable
  SetChoices) and selects the first entry; a dropdown renders its value
  with a trailing ▾ affordance. dropdown.rs holds next_choice /
  parse_choice_list.
- Button: CellStyle.button makes a cell a button whose value is a
  TARGET[+N] action spec (A1 ref + optional signed step, default +1).
  Clicking increments the target cell's numeric value through set_cell
  (undoable, recalculates dependents) — the spreadsheet-native counterpart
  of the reference's "+10" boost. button.rs holds parse_button_spec /
  button_step / format_step_value; the cell renders as a raised box with
  the spec centred. Toolbar "Btn" toggles the flag.

Both flags ride in the render cache so cached cells stay styled, and both
commands route through WorkbookCommand (apply + apply_command + dirty
marking). Choices serialize after the markdown/slider/button flags; older
files default to off/empty.

Engine: 491 lib tests (+4) + integration. UI controllers: 142 tests (+8,
button 5 + dropdown 4). Coverage: engine 96.59%, ui-controllers 99.45%
(floors 96); button.rs and dropdown.rs at 100%.
2026-08-21 04:52:00 +00:00
328690dfbf feat(spreadsheet): 3-state header sort (asc/desc/off) with a timed report
Close the last sort gap: the spreadsheet grid's header sort now cycles
ascending → descending → off like the datagrid reference, and reports how
long each sort took.

Engine: SpreadsheetData::sort_rows remembers a RowSort { ascending,
key_col, restore } where restore[view_row] is the row the data originally
lived on, composed across sort chains. New unsort_rows applies that map to
restore the pre-sort order (no-op without a sort, clears undo, recalculates
moved formulas); sort_state() reports the active (column, direction).
Workbook gains unsort_active_sheet and active_sort_state. The sort order is
transient — reset by deserialize, never serialized.

UI: sort_state::next_sort_state replaces the 2-state next_sort_direction
with the asc→desc→off cycle, and the workspace HeaderClicked handler times
the sort with std::time::Instant, drives unsort_active_sheet on "off", and
appends the report ("sorted 1000 rows by B descending in N ms") to the
status bar via a cached sort_status field.

Engine: 487 lib tests (+6) + integration. UI controllers: 134 tests.
Coverage: engine 96.66%, ui-controllers 99.42% (floors 96).
2026-08-21 04:52:00 +00:00
4d681dfdbe feat(spreadsheet): slider cells and inline-Markdown cells (#9 remainder)
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
nigig-build (CAD) / cad-widget-coverage (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
spreadsheet / engine-coverage (push) Has been cancelled
spreadsheet / ui-controller-coverage (push) Has been cancelled
Complete the widget-in-cell gap the same value/style-convention way as the
checkbox: two more cell kinds, both plain data with a render flag.

Engine:
- CellStyle gains `markdown` and `slider` bool flags, serialized as two
  trailing columns on the CELL line (older files default them off), plus
  WorkbookCommand::{SetMarkdown, SetSlider} routed through apply and
  apply_command via mutate_cell — undoable like every style mutation.

UI:
- markdown.rs (measured): a flat inline parser splitting a cell value into
  Regular/Bold/Italic/Code runs (`**bold**`, `*italic*`, `` `code` ``);
  unclosed markers and empty spans stay literal/dropped. The grid draws
  each run with the bold or regular resource (code tinted like formulas).
- slider.rs (measured): 0-100 fraction/value mapping (rounded to whole
  steps) and track/fill/handle geometry. A slider cell draws the control
  instead of text, and a press/drag sets the value through set_cell — one
  undo step per drag (reverse-order ChangeSet application restores the
  pre-drag value).
- Toolbar "Md" and "Slider" buttons toggle the flags on the selection;
  the render cache carries the two flags so cached cells stay styled.

Dropdown and button cells are intentionally not included: a dropdown
needs a per-cell choices list and a button needs an action semantic that
a spreadsheet does not have — both would require a CellKind in the data
model rather than a render flag.

Engine: 481 lib tests (+2) + integration. UI controllers: 133 tests (+13,
markdown 10 + slider 3). Coverage: engine 96.62%, ui-controllers 99.42%
(floors 96); markdown.rs and slider.rs at 100%.
2026-08-20 21:36:21 +00:00
8c1a4ad446 feat(spreadsheet-ui): Big Data virtual tab — 1B cells, procedural, timed sort
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
nigig-build (CAD) / cad-widget-coverage (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
spreadsheet / engine-coverage (push) Has been cancelled
spreadsheet / ui-controller-coverage (push) Has been cancelled
Close the UI half of the virtualization gap. A new VirtualGrid widget
renders one billion virtual cells (1,000,000 rows × 1,000 columns) from
the engine's VirtualSheet: only the visible range is drawn, values derive
from a hashed (row, col), and a column-header click sorts the full million
rows by permuting an index — with the elapsed time reported in a status
strip, as the reference does.

- virtual_grid.rs (DSL widget, excluded from coverage like grid.rs):
  zebra-striped cells, row/column headers with the sort glyph, gridlines,
  drag-to-pan and wheel scroll, and a 3-state header sort (asc → desc →
  off) delegated to VirtualSheet::sort_rows/reset_sort. All placement and
  hit-testing reuses the measured GridMetrics; text widths reuse
  TextMeasureCache.
- workspace.rs: a "BigData" toolbar button overlays the virtual grid over
  the spreadsheet grid via two child Views toggled with View::set_visible,
  so the existing spreadsheet path is untouched.

Engine: 479 lib tests + integration (grid data-provider commit included).
UI controllers: 119 tests. Coverage: engine 96.67% (data_source.rs 100%),
ui-controllers 99.35% (floors 96).
2026-08-20 20:48:35 +00:00
f14823a6da feat(spreadsheet): grid data-provider abstraction and a virtual 1B-cell source
Close the engine half of the virtualization gap (report section A). Add a
GridDataSource trait — row/col counts, per-cell display text, sort, and a
column label — implemented by SpreadsheetData (the real store, delegating
to get_display_value/sort_rows) and by a new VirtualSheet.

VirtualSheet derives every cell procedurally from a hashed (row, col), so
1,000,000 rows × 1,000 columns (one billion cells) cost one struct and no
stored data. Its rendered text and numeric sort key come from the same
hash, so sorting by the key sorts what the user sees; sort_rows permutes a
Vec<u32> row index (O(n log n) over the index, not the data) with a
deterministic tie-break. Column labels mirror the reference (# / Name /
City / Balance / Score / Active, then ·N repeats).

Engine: 478 lib tests (+12) + integration. Coverage: data_source.rs 100%,
engine total 96.67% (floor 96).
2026-08-20 20:48:35 +00:00
7946aa889f feat(spreadsheet-ui): OS-clipboard TSV copy and toolbar zoom (#13, #7)
Some checks failed
repo hygiene / hygiene (push) Has been cancelled
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
nigig-build (CAD) / cad-widget-coverage (push) Has been cancelled
spreadsheet / engine-coverage (push) Has been cancelled
spreadsheet / ui-controller-coverage (push) Has been cancelled
Close the two remaining small UI gaps from the datagrid analysis:

- TSV copy to the OS clipboard (#13): clipboard.rs gains a tested
  tsv_escape/rows_to_tsv pair (tab/newline/quote escaping, Excel-style),
  and the grid answers Hit::TextCopy with the selection as TSV while
  Ctrl+C also writes it straight to the OS clipboard via
  Cx::copy_to_clipboard. The in-app clipboard still feeds Ctrl+V paste.
- Zoom (#7): a new measured zoom module holds the step factor (×1.15),
  50-400% clamps and the minimum/maximum cell-size clamp. The grid's
  apply_zoom/reset_zoom rescale the default cell size deterministically
  from a captured base (no floating-point drift), and the cell text's
  font_scale follows the zoom — with text widths scaled at draw time so
  right/centre alignment stays true. The toolbar gains - / 100% / +
  buttons.

Engine untouched. UI controllers: 119 tests (+7). Coverage: ui-controllers
99.35% (floor 96), clipboard.rs and zoom.rs at 100%.
2026-08-20 20:14:51 +00:00
2adec957e9 feat(spreadsheet-ui): TrendChart line + candlesticks wired to the selected row
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
nigig-build (CAD) / cad-widget-coverage (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
spreadsheet / engine-coverage (push) Has been cancelled
spreadsheet / ui-controller-coverage (push) Has been cancelled
Close gap #11 (chart integration) from the datagrid gap analysis, split as
usual into testable headless logic plus a thin DSL widget:

- chart.rs (measured): candle bucketing (OHLC from a price series), series
  bounds, line-point mapping with the reference's 8% padding, the vertical
  strips that trace a polyline with axis-aligned quads, candle body/wick
  geometry with up/down classification, and the 1/2/5×10^k axis tick step.
- market.rs (measured): a deterministic live market — the reference's
  splitmix-style `mix64`, a HISTORY-capped random walk per symbol, lazy
  per-row symbol growth (so any selected row charts), tick() with roll-off,
  and the derived stats (last/change/pct_change/day_range/candles).
- trend_chart.rs: a `TrendChart` widget (excluded from coverage like
  grid.rs) that colours and draws chart.rs output — gridlines, a polyline
  for a series, or candle bodies + wicks — via `set_series`/`set_candles`.
- workspace.rs glue: a 220px chart panel under the grid (line + candlesticks
  side by side) fed from a 0.25s `Timer` ticker; selecting a grid row
  switches the charted symbol and updates the title label.

Engine untouched. UI controllers: 112 tests (+16 chart/market). Coverage:
ui-controllers 99.32% (floor 96), chart.rs 100%, market.rs 99.36%.
2026-08-20 19:02:02 +00:00
1a36ca5538 feat(spreadsheet-ui): interactive checkbox cells (widget-in-cell)
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
nigig-build (CAD) / cad-widget-coverage (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
spreadsheet / engine-coverage (push) Has been cancelled
spreadsheet / ui-controller-coverage (push) Has been cancelled
Close gap #9 (cells hosting widgets) the spreadsheet-native way. The
datagrid reference hosts a live CheckBox widget per visible cell and
recycles instances from a per-template pool; our grid instead draws the
control from batched quads, so there is no per-cell widget object to
instantiate or recycle — the render cache and reused draw buffers already
fill the role the pool does.

- A plain value cell holding TRUE/FALSE now renders as a checkbox: a
  centred square box with a checkmark when TRUE, and its TRUE/FALSE label
  to the right. A single click toggles the value (through the normal
  set_cell path, so it is undoable and recalculates dependents), selects
  the cell, and does not open the editor — a checkbox is a button, not a
  text surface. Formula cells are never checkboxes, so a click can't
  clobber a formula.
- The predicate (is_checkbox), the value flip (toggled) and the box
  geometry (checkbox_layout) live in a new measured checkbox module with
  unit tests; grid.rs stays thin glue (draw the box/tick/label, reposition
  the label, and the click handler's toggle).

UI controllers: 96 tests (+6). Coverage: ui-controllers 99.20% (floor 96),
checkbox.rs at 100%. No engine changes.
2026-08-20 18:28:46 +00:00
1a4b479013 feat(spreadsheet): SPARKLINE(range) formula rendered as in-cell sparkline bars
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
nigig-build (CAD) / cad-widget-coverage (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
spreadsheet / engine-coverage (push) Has been cancelled
spreadsheet / ui-controller-coverage (push) Has been cancelled
Close gap #10 (sparkline cells) from the datagrid gap analysis, split as
usual into testable headless logic plus thin grid glue:

- Engine: a `SPARKLINE(range)` function resolves its single range argument
  to a flat numeric series and returns a new `Value::Sparkline(Vec<f64>)`.
  The variant is not a scalar — arithmetic on it is a `#VALUE:` error, and
  the aggregation/lookup helpers skip it — so no code path invents a number
  from a chart. `SpreadsheetData::apply_formula_result` stores the series
  on `CellData.sparkline` (derived state, like spills: never serialized,
  re-derived on recalculation) and clears it when the formula stops
  returning a sparkline. Dependency tracking is inherited from the range
  reference, so editing a cell in the source range re-derives the bars.
- UI: a new measured `sparkline` module computes the bar rectangles (bars
  rise from the series minimum, tinted up/down by last-vs-first trend,
  gap-shrunk for narrow cells) — the same geometry as the reference
  `Sparkline` widget, but testable headlessly. The grid draws the bars with
  a dedicated `draw_spark` resource (depth 0.4) when a cell carries a
  sparkline and skips the text path; the series is cached in
  `CellRenderState` alongside the display text. The in-cell editor keeps
  its text on top by suppressing the bars while editing.

Engine: 467 lib tests + integration (483 total, +6 sparkline tests).
UI controllers: 90 tests (+5). Coverage: engine 96.60%, ui-controllers
99.15% (floors 96); sparkline.rs at 100%.
2026-08-20 18:17:27 +00:00
e13aa03dca feat(spreadsheet-ui): header sort, column reorder and row/col/all selection
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
nigig-build (CAD) / cad-widget-coverage (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
spreadsheet / engine-coverage (push) Has been cancelled
spreadsheet / ui-controller-coverage (push) Has been cancelled
Wire up the three missing header interactions from the datagrid gap
analysis, split as usual into testable headless logic plus thin grid
glue:

- Selection kinds: SelectionController gains GridSelectKind
  (Cells/Rows/Cols/All). Header clicks select a whole column / row, the
  corner (or Ctrl+A) selects the sheet, and bounds() expands the
  row/column/all kinds to the sheet's full extent so the fill, copy,
  delete, format and autofill paths handle every kind without branching.
- Header-click sort: clicking a column header toggles the sort direction
  (same column flips asc/desc, a new column starts ascending) and
  re-sorts the active sheet; the sorted header shows an ▲/▼ glyph. The
  toggle lives in the new measured sort_state module; the grid holds the
  (col, asc) state and the workspace applies Workbook::sort_active_sheet.
- Column reorder: dragging a header past the tap threshold moves the
  column, with a drop-indicator line at the insertion index (computed by
  the new, tested GridMetrics::col_insert_at). Engine addition
  SpreadsheetData::move_column(from, to) remaps cells and column-width
  overrides, rebuilds the dependency graph, recalculates, and clears undo
  history; Workbook::move_active_column propagates to cross-sheet readers
  via the new WorkbookCommand::MoveColumn.

Selection overlay drawing is clipped to the cell area so a full
row/column/sheet selection border no longer paints over the headers and
the surrounding workspace.

Engine: 461 lib tests + integration (477 total). UI controllers: 85
tests. Coverage: engine 96.66% and ui-controllers 99.11% (floors 96).
2026-08-20 17:54:54 +00:00
61d666699b feat(spreadsheet): sort rows — the engine half of the datagrid sort
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
nigig-build (CAD) / cad-widget-coverage (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
spreadsheet / engine-coverage (push) Has been cancelled
spreadsheet / ui-controller-coverage (push) Has been cancelled
Tranche 3 of the gap analysis: the datagrid example sorts a million rows
on a header click. This is the engine-side sort that makes that possible
(the UI header wiring and the procedural data provider are later
tranches).

`SpreadsheetData::sort_rows(ascending, key_col)`:

- Sorts every row by the values in one column. Numbers sort before text
  (case-insensitively), and empty cells always sort last — in both
  directions, unlike a naive `reverse` which would float blanks to the
  top on a descending sort.
- Stable: equal keys keep their original order via a row-index tie-break.
- Formulas move with their rows and recalculate against the sorted
  positions, matching Excel's reference-by-position semantics.
- Undo history is cleared (a sort is a destructive bulk reorder); row
  heights, column widths and named ranges stay positional, as in Excel.

`Workbook::sort_active_sheet(ascending, key_col)`:

- Sorts the active sheet and recalculates cross-sheet dependents so
  readers on other sheets see the sorted values.

Tests: 6 (ascending/descending, numbers-vs-text-vs-empty ordering, stable
ties, formula recalculation, non-undoability, workbook-level sort with
cross-sheet propagation). Engine unit tests 447 -> 453. Engine coverage
96.61% (floor 96).
2026-08-20 17:18:33 +00:00
481a0e133c feat(spreadsheet-ui): red error cells and a visible-cell status bar
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
nigig-build (CAD) / cad-widget-coverage (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
spreadsheet / engine-coverage (push) Has been cancelled
spreadsheet / ui-controller-coverage (push) Has been cancelled
Tranche 2 of the datagrid gap analysis — the small UI polish. (Zebra
stripes were already present as `cell_alt_bg_color` alternating rows.)

Error cells render red:

- `render_cache::is_error_text` matches a display value against the
  engine's own `FormulaError::from_display` surface (plus `#SPILL!`),
  so real errors are red while a user value like `#hashtag` stays plain
  text.
- The grid's text-colour precedence now checks the error case after an
  explicit user text colour and before bold/formula/default, using a new
  `error_text_color` (default red).

Visible-cell status bar:

- `GridMetrics::visible_cell_count(viewport_w, viewport_h)` estimates the
  visible columns/rows from the viewport and default cell sizes, clamped
  to the grid extent — testable in geometry.rs.
- `SpreadsheetGrid::visible_cell_counts` hands the live viewport to it,
  and the workspace status label now shows "Ready | C × R visible = N
  cells", cached so it only re-lays-out when the numbers change (scroll
  or resize).

Tests: 2 new (error detection against real/plain values; visible-count
estimation + clamping). UI lib tests 73 -> 75; ui-controllers coverage
99.02% (floor 96).
2026-08-20 17:10:57 +00:00
0326da8dfe feat(spreadsheet): ^ power operator and General thousands separators
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
nigig-build (CAD) / cad-widget-coverage (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
spreadsheet / engine-coverage (push) Has been cancelled
spreadsheet / ui-controller-coverage (push) Has been cancelled
Tranche 1 of the gap analysis against the Makepad `work`-branch
datagrid example: the two formula-engine features it has that we
lacked.

`^` exponentiation operator:

- New `Caret` token, `BinOp::Pow` (precedence 5), and a right-associative
  `parse_pow` between multiplicative and unary in the parser.
- Unary minus binds tighter than `^`, so `-3^2` is `(-3)^2 = 9` — Excel's
  precedence, and the exact assertion the datagrid reference pins.
- `2*3^2` = 18 (power over multiply) and `2^3^2` = 512 (right-assoc).
- Evaluated in `apply_binop`, so it broadcasts element-wise over arrays
  like every other operator.

General-format thousands separators:

- `format_number` stays comma-free — computed values and criteria must
  round-trip through `parse_cell_computed_value` and `parse::<f64>`.
- New `format_number_display` (plus a `group_thousands` helper) groups
  the integer part at the display boundary only: `apply_number_format`'s
  General arm now shows `1,000,000` while the raw/edit value stays
  `1000000`. Non-numeric text passes through untouched.

Tests: `^` precedence/associativity/evaluation, display grouping, and an
end-to-end check that comma display does not break the recalc fast path.
Engine unit tests 441 -> 447; UI lib tests still pass. Engine coverage
96.58% (floor 96).
2026-08-20 17:01:46 +00:00
555c7daaa1 feat(spreadsheet): postfix LAMBDA application and LET
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
nigig-build (CAD) / cad-widget-coverage (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
spreadsheet / engine-coverage (push) Has been cancelled
spreadsheet / ui-controller-coverage (push) Has been cancelled
Completes the first-class-function story started with LAMBDA-as-an-
argument.

Postfix application:

- A new `Expr::Apply { func, args }` node lets a callable expression be
  invoked directly: `LAMBDA(x, x*2)(3)` and curried `LAMBDA(x, LAMBDA(y,
  x+y))(1)(2)`. The parser gained a `parse_postfix` loop, so `f(...)(...)`
  chains bind tightest, after the primary expression.
- Arguments are bound with `resolve_bound_arg`: a range or array binds as
  an array (so `LAMBDA(x, SUM(x))(A1:A4)` aggregates), a scalar binds as
  a scalar.
- Applying a non-callable is `#VALUE!`; an argument-count mismatch is
  reported. Postfix arguments still register their cell dependencies.

LET:

- `LET(name1, value1, [name2, value2, ...], body)` binds names to values
  sequentially — a later value may reference an earlier name — and
  evaluates the body with the names in scope, reusing the LAMBDA
  substitution machinery. A range value binds as an array, so
  `LET(s, A1:A4, SUM(s))` aggregates the whole range.
- Duplicate names are rejected in both LET and LAMBDA, matching Excel;
  an unbound name in the body is `#NAME?`.

Tests: 6 unit tests (postfix application, currying, LET binding/range/
error shapes, duplicate-parameter rejection, dependency tracking) + an
end-to-end test proving LET and postfix application recalculate through
the dependency graph. Engine unit tests 434 -> 441. Engine coverage
96.55% (floor 96).
2026-08-19 12:05:26 +00:00
47f645ab58 feat(spreadsheet): resolve the four dynamic-array limits
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
nigig-build (CAD) / cad-widget-coverage (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
spreadsheet / engine-coverage (push) Has been cancelled
spreadsheet / ui-controller-coverage (push) Has been cancelled
Closes the four limits documented when the spill model landed.

LAMBDA (was: GROUPBY/PIVOTBY only took an aggregate name):

- `LAMBDA(params..., body)` builds a `Value::Callable` without evaluating
  its body. A new `Expr::BoundValue` node splices a bound argument into
  the body AST when the callable is applied, so the ordinary evaluator
  runs the body.
- `GROUPBY` and `PIVOTBY` accept either a named aggregate (`SUM`, ...)
  or a `LAMBDA`, applied per value column / per pivot bucket. This
  unlocks arbitrary aggregations (`LAMBDA(x, MAX(x)-MIN(x))`).

FILTER include + full arithmetic broadcasting:

- `FILTER` now accepts a same-shape include: matching cells are kept and
  non-matching positions become `#N/A`, element-wise, like Excel.
- Unary operators broadcast over an array (`-A1#`, `-FILTER(...)`),
  completing the operator-level arithmetic alongside the existing binary
  broadcast.

Spill formatting:

- `SpillRange` records the anchor's `NumberFormat`, and derived cells are
  formatted at display time through a shared `apply_number_format`
  (extracted from `write_display_value`). Raw values stay numeric, so
  `SUM(A1#)` still evaluates correctly.

#SPILL! blocking:

- A spill that would overwrite an existing cell — or another spill,
  flowing or blocked — reports `#SPILL!` in the anchor instead of
  clobbering data. The would-be range is remembered in `blocked_spills`,
  and clearing the blocking cell retries the spill automatically.

Tests: 7 new unit tests (LAMBDA in GROUPBY/PIVOTBY, lambda errors,
same-shape FILTER, unary broadcast) + 2 end-to-end tests (NumberFormat
inheritance and #SPILL! block-then-retry). Engine unit tests 427 -> 434;
UI lib tests still pass. Engine coverage 96.41% (floor 96).
2026-08-19 07:58:38 +00:00
c9474e2c9f feat(spreadsheet): dynamic-array spill model — FILTER, GROUPBY, PIVOTBY, A1#
Some checks failed
repo hygiene / hygiene (push) Has been cancelled
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
nigig-build (CAD) / cad-widget-coverage (push) Has been cancelled
spreadsheet / engine-coverage (push) Has been cancelled
spreadsheet / ui-controller-coverage (push) Has been cancelled
The engine stored every formula result as one display string per cell.
This adds the dynamic-array architecture: a formula can return a grid,
which "spills" into the cells below/right of its anchor.

Value model:

- `Value::Array(Vec<Vec<Value>>)` — a grid result. Scalars coerce to
  one-cell grids where the shape matters; `to_f64`/`to_bool` refuse an
  array, `to_display_string` shows the top-left, and `resolve_arg`
  flattens an array argument so `SUM(FILTER(...))` aggregates its cells.

- Element-wise broadcasting: `resolve_array2d` maps a binary operator
  over a grid when one side is a scalar (or both grids share a shape),
  so `FILTER(A1:A4, C1:C4 > 15)` builds the boolean include the way
  Excel does. The binary-op logic was factored into `apply_binop`.

Parser and AST:

- `#` is now the spill operator: `A1#` and `Sheet2!A1#` parse as
  `SpillRef` / `SheetSpillRef`, evaluate to the anchored array (so
  `=A1#` re-spills), flatten in aggregates, and track their anchor in
  the dependency graph (intra- and cross-sheet).

Functions (dynamic arrays):

- FILTER(array, include, [if_empty]) — keep rows (column include) or
  columns (row include); `#N/A` on no match unless `if_empty`.
- GROUPBY(row_fields, values, function, [field_headers]) — group rows
  by field tuples and aggregate each value column (SUM/AVERAGE/COUNT/
  MAX/MIN/MEDIAN by name, ETA-reduced-LAMBDA form).
- PIVOTBY(row_fields, col_fields, values, function) — a 2D pivot with
  the aggregate name in the top-left corner.

Spill storage (data.rs):

- `SpillRange` + `spills` map on `SpreadsheetData`: derived cells read
  back through `get_display_value`/`get_raw`/`get_edit_value`, are not
  blank, and are read-only — `set_cell`/`put_cell`/`remove_cell`/
  `mutate_cell` refuse to touch them (the UI blocks via `is_spilled`).
- Recalc builds the spill from the `Array` result (`apply_formula_result`)
  and drops stale spills when a formula becomes scalar, is removed, or
  cycles. Spills are derived state, never serialized — the anchor
  formula persists and re-derives on load.
- Cross-sheet spills read through `get_sheet_spill_values`.

Tests: 21 new units (FILTER/GROUPBY/PIVOTBY shapes, broadcasting,
Value::Array methods, spill parsing) + 5 end-to-end tests (spill
display, read-only cells, `A1#` aggregation and re-spill, stale-spill
clearing, cross-sheet spill). Engine unit tests 405 -> 427; UI lib tests
still pass. Engine coverage 96.66% (floor 96).
2026-08-18 18:34:22 +00:00
45efc74106 feat(spreadsheet): pivot and chart aggregates — IFS family, statistics, SUMPRODUCT, LARGE/SMALL/RANK
Some checks failed
repo hygiene / hygiene (push) Has been cancelled
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
nigig-build (CAD) / cad-widget-coverage (push) Has been cancelled
spreadsheet / engine-coverage (push) Has been cancelled
spreadsheet / ui-controller-coverage (push) Has been cancelled
Full GROUPBY/PIVOTBY need a dynamic-array "spill" model (a formula
returning a grid), which this single-cell engine deliberately does not
have. These are the single-cell building blocks that do the same work.

Multi-criteria conditional aggregates (the "filter then aggregate" pivot
core), resolved positionally so criteria ranges stay aligned:

- SUMIFS(sum_range, criteria_range1, criteria1, ...)
- AVERAGEIFS(avg_range, criteria_range1, criteria1, ...)  (#DIV/0! on no match)
- COUNTIFS(criteria_range1, criteria1, ...)
- MAXIFS / MINIFS (0 on no match, like Excel)
  Mismatched range sizes are #VALUE!, not a silent misalignment.

Chart statistics (over the flattened numeric arguments):

- MEDIAN, MODE (ties keep the smallest value), and the sample/population
  STDEV / STDEVP / VAR / VARP. Sample forms divide by n-1 (#DIV/0! for a
  single value), population by n.

Pivot/ranking helpers:

- SUMPRODUCT(array1, [array2], ...) — the element-wise dot product;
  text counts as zero, errors propagate, mismatched sizes are #VALUE!.
- LARGE / SMALL(array, k) — k-th largest/smallest; k out of range is the
  new #NUM! error.
- RANK(value, array, [order]) — descending by default, ascending on any
  nonzero order, tied values share a rank (RANK.EQ).

`FormulaError` gains `NumError` (`#NUM!`) for out-of-domain numeric
arguments, rounding out the error surface after `Na` in the lookup
tranche; it round-trips through display/parse and propagates from cached
values.

Tests: 10 unit tests (multi-criteria aggregation, statistics, dot
products, top-N/ranking, argument/size errors) + an end-to-end test
proving the dependency graph tracks every range and recalculates the
pivot formulas when a source cell is edited. Engine unit tests 395 ->
405. Engine coverage 97.34% (floor 96).
2026-08-18 18:03:00 +00:00
780e323674 refactor(spreadsheet-ui): headless formula-bar state machine, wired to the workspace
Some checks failed
repo hygiene / hygiene (push) Has been cancelled
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
nigig-build (CAD) / cad-widget-coverage (push) Has been cancelled
spreadsheet / engine-coverage (push) Has been cancelled
spreadsheet / ui-controller-coverage (push) Has been cancelled
The formula bar's state — formula text, target cell, dirty flag, and
the "ignore the next change" latch — lived as five scattered fields
plus inline logic inside workspace.rs, a `script_mod!` DSL file that no
unit test can construct. That made the formula bar the one piece of the
UI whose behaviour was untestable headlessly.

Extract a `formula_bar` controller module (the same pattern as the
earlier geometry.rs extraction) and wire the workspace to it:

- `FormulaBar` owns text/target/dirty/latch with `sync_to_cell`,
  `on_user_input`, `insert_reference`, `take_commit`, and
  `sync_after_commit`. The workspace now only bridges it to the Makepad
  text input; the change/return/focus-loss/undo-redo handlers route
  through the controller unchanged in behaviour.

- Point-and-click formula building (new, Excel-style): while the formula
  bar is focused, selecting a cell appends its reference instead of
  replacing the formula being edited. `reference_for(sheet, row, col)`
  and `quote_sheet_name` build `A1`, `Sheet2!A1`, or `'My Sheet'!A1`
  with Excel's quoting rules (bare identifier unquoted; spaces,
  punctuation or a leading digit quoted; embedded quotes doubled) — the
  machinery that makes the cross-sheet reference syntax buildable from
  the UI.

- The coverage script now measures formula_bar.rs alongside the other
  controller modules.

Tests: 11 new headless unit tests (quoting, reference construction,
sync/latch/commit round trip, and insertion into empty/value/formula
bars). UI lib tests 62 -> 73, all pass; ui-controllers coverage 98.74%
(floor 96) with formula_bar.rs at ~98%.
2026-08-18 11:40:49 +00:00
d0e7ede0c1 feat(spreadsheet): cross-sheet named ranges (Data!Total)
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
nigig-build (CAD) / cad-widget-coverage (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
spreadsheet / engine-coverage (push) Has been cancelled
spreadsheet / ui-controller-coverage (push) Has been cancelled
Completes the cross-sheet reference feature with `Sheet!Name`, the one
shape the previous tranche explicitly left out.

Parser and AST:

- `Sheet2!Total` and `'My Sheet'!Sales` parse as a new
  `Expr::SheetNamedRange { sheet, name }` node. In
  `parse_sheet_qualified_ref`, an identifier after `!` that is not a
  valid cell reference is treated as a named range; a cell reference
  wins when both readings are possible (`Sheet2!A1`), matching Excel.

Evaluation:

- `EvalContext` gains `get_sheet_named_range` (default `None`).
  `evaluate` resolves a `SheetNamedRange` to its range and returns the
  first cell in expression context, while `resolve_arg` expands it to
  every cell for aggregates — so `Data!Total` reads one cell and
  `SUM(Data!Total)` sums the whole range. An unknown name on a real
  sheet is `#NAME?`, and `DataEvalContext` looks the range up on the
  sibling sheet case-insensitively (or on the sheet itself, through
  the intra-sheet path).

Dependencies:

- A named-range reference is a coarse sheet-level dependency, because
  its bounds live on the target sheet. `SpreadsheetData` tracks
  `cross_sheet_named_refs` (cell -> sheet names), rebuilt by
  `rebuild_dependency_graphs` / `update_dependency_graph` alongside the
  cell-level `cross_sheet_refs`, and the workbook folds both into its
  sheet-index dependency map. Editing a cell inside the named range
  therefore recalculates the reading sheet.

Tests: parser/evaluator units (incl. quoted names, cell-vs-named
precedence, dependency extraction) and workbook end-to-end (SUM over a
cross-sheet named range with propagation on edit, quoted names, and the
#NAME? case). Engine unit tests 390 -> 395; UI lib tests still pass.
Engine coverage 97.56% (floor 96).
2026-08-18 11:07:57 +00:00
593cd8fee8 feat(spreadsheet): cross-sheet references (Sheet2!A1)
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
nigig-build (CAD) / cad-widget-coverage (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
spreadsheet / engine-coverage (push) Has been cancelled
spreadsheet / ui-controller-coverage (push) Has been cancelled
The engine's dependency graph is per-sheet by design, so this was the
structural change: a formula can now read a cell on another sheet, and
edits to the source sheet recalculate the readers.

Parser and AST:

- New tokens: `Bang` (`!`) and `SheetName` (`'My Sheet'`, with `''` as
  an escaped quote). `!=` still tokenizes as not-equal.
- New AST nodes: `SheetCellRef { sheet, cell }` and
  `SheetRange { sheet, range }`, parsed from `Sheet2!A1`,
  `Sheet2!A1:B2`, `'My Sheet'!B3`, before the cell-ref fallback (a bare
  `Sheet2` would otherwise parse as a bogus cell reference).
- `evaluate` and `resolve_arg` resolve them through two new `EvalContext`
  hooks, `get_sheet_cell_value` / `get_sheet_range_values`, which default
  to `#REF!` in contexts without sibling sheets.

Evaluation:

- `DataEvalContext` gains a sibling-sheet view plus a shared cross-sheet
  cycle guard keyed by `(sheet, row, col)`. Reading a sibling routes to a
  child context pointed at that sibling; re-entering the same cell on the
  same sheet mid-evaluation reports `#CYCLE!`, and a reference back to the
  sheet being recalculated is caught through the guard.
- `recalculate_all_with` / `evaluate_formula_with` accept the sibling
  view; the plain per-sheet entry points are unchanged.

Recalculation and dependencies:

- `SpreadsheetData` tracks `cross_sheet_refs` per cell (rebuilt by
  `rebuild_dependency_graphs` / `update_dependency_graph`), and the
  workbook flattens it into sheet-index edges. Editing a sheet
  recalculates — with sibling access — the transitive closure of sheets
  that read it, plus the sheet itself (its own formulas may read other
  sheets). Single-sheet edits with no cross-sheet references keep the
  fast incremental path.
- `evaluate_all` clears computed values across sheets first, then runs
  one extra pass per sheet, so acyclic chains propagate and mutual
  cross-sheet cycles terminate with `#CYCLE!`.
- Cross-sheet formulas survive save/load: deserialization runs a
  sibling-aware pass, and a missing sheet renders `#REF!`.

Tests: parser/evaluator units (incl. quoted names, dependency
extraction) and workbook end-to-end (read + propagation, a three-sheet
chain, quoted names, missing sheet, save/load round trip, cycle
termination). Engine unit tests 382 -> 390; UI lib tests still pass.
Engine coverage 97.55% (floor 96).
2026-08-18 10:53:08 +00:00
2d7b48b72c feat(spreadsheet): lookup functions — MATCH, INDEX, VLOOKUP, HLOOKUP, XLOOKUP
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
The formula engine could compute over ranges but could not look a
value up in one. This adds the Excel lookup family, plus the #N/A error
they need to report "not found".

New `FormulaError::Na`:

- Renders as `#N/A`, parses back in `from_display`, and — a bonus fix —
  the recalc fast path now propagates a stored `#N/A` as an error
  instead of turning it into text.

Functions (5):

- MATCH(lookup_value, lookup_array, [match_type]) — 1-based position.
  Type 0 exact (case-insensitive text), 1 largest ≤ lookup, -1 smallest
  ≥ lookup.
- INDEX(array, row_num, [col_num]) — cell at a 1-based position; a
  single index walks the array flat in row-major order. Out of range
  is #REF!.
- VLOOKUP / HLOOKUP — exact or approximate (approximate takes the
  largest first-column/first-row value ≤ lookup, the sorted-table
  convention), col/row index out of range is #REF!.
- XLOOKUP(lookup_value, lookup_array, return_array, [if_not_found],
  [match_mode], [search_mode]) — match modes 0 exact, -1 next-smaller,
  1 next-larger, 2 wildcard (`*`/`?`, case-insensitive); search modes
  1 first-to-last and -1 last-to-first; the `if_not_found` fallback is
  evaluated lazily, only when nothing matches. Binary search modes are
  rejected with a clear error rather than silently mishandled.

Lookups index ranges by position, so they keep their arguments as AST
nodes (a new `range_from_arg` resolves Range and NamedRange expressions)
instead of flattening through the aggregate path.

Tests: 9 new unit tests (exact/approximate/wildcard/search-direction/
error arms) + an end-to-end test proving the dependency graph tracks the
lookup table and recalculates dependents when a table cell is edited.
Engine unit tests 373 -> 382.
2026-08-18 10:13:51 +00:00
26f6d431fb feat(spreadsheet): date and time functions, with a date display format
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
The formula engine had no notion of dates. This adds an Excel-style
date/time model and the functions to work with it.

Date model (new `dates` module):

- A date/time is a single f64: the integer part is a day serial, the
  fraction is the time of day. Serial 25569 = 1970-01-01, so every
  modern date agrees with Excel exactly. The calendar is the proleptic
  Gregorian (Howard Hinnant's days_from_civil/civil_from_days), so
  Excel's phantom 1900-02-29 (serial 60) reads back as 1900-02-28 and
  serial 61 = 1900-03-01 — documented rather than reproduced.
- Serial <-> calendar conversion, day-of-week, leap-year and
  days-in-month helpers, date/time string parsing, EDATE/EOMONTH,
  DATEDIF(Y/M/D), and ISO formatting.

Functions (17):

- DATE, DATEVALUE, YEAR, MONTH, DAY, HOUR, MINUTE, SECOND, TIME,
  TIMEVALUE, WEEKDAY (return types 1/2/3), DAYS, EDATE, EOMONTH,
  DATEDIF — with Excel's month/day rollover in DATE, day clamping in
  EDATE, and #VALUE! for malformed strings and unknown units.
- TODAY and NOW read a wall clock. The engine stays deterministic:
  `EvalContext` gains a default-none `now_serial` hook, and
  `SpreadsheetData` carries an optional `now_serial` (never
  serialized). Without a clock they report "requires a wall clock";
  `SpreadsheetData::set_system_now` / `Workbook::set_system_now` supply
  the system clock, and the UI workspace model wires it on creation.

Display:

- `NumberFormat::Date` and `NumberFormat::DateTime` render a serial as
  `YYYY-MM-DD` / `YYYY-MM-DD HH:MM:SS` through write_display_value,
  with codes that round-trip through the existing serialization.

Tests: 18 new unit tests (calendar round-trips across centuries,
known serials, leap-year rules, parsing, weekday schemes, EDATE/EOMONTH
clamping, DATEDIF, ISO formatting) + 6 evaluator tests + an end-to-end
test covering format rendering and recalc through the dependency graph.
Engine unit tests 352 -> 373. UI lib tests still 62 pass with the new
clock wiring.
2026-08-18 10:00:53 +00:00
b5ff5dcf40 feat(spreadsheet): text manipulation and IS* info functions
Some checks failed
repo hygiene / hygiene (push) Has been cancelled
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
The formula engine could count, sum, compare and test conditions, but
had almost no way to work with text (only CONCAT/LEN/UPPER/LOWER) and
no way to ask about a value's type. This adds both.

Text functions (operate on the display form of their arguments):

- TRIM — collapse internal space runs and drop leading/trailing spaces
- LEFT / RIGHT — first/last n characters (default 1)
- MID — n characters from a 1-based start
- SUBSTITUTE — replace all occurrences, or just the nth instance
- FIND / SEARCH — 1-based position, case-sensitive vs case-insensitive,
  #VALUE! when not found or start is out of range
- REPT — repeat n times, capped at Excel's 32767-character result
- PROPER — title-case each word

Info functions (never propagate their argument's error, like Excel —
ISERROR reports it, the others treat it as FALSE):

- ISNUMBER / ISTEXT / ISNONTEXT / ISLOGICAL / ISERROR
- ISBLANK — TRUE only for an absent cell. This needs the distinction
  between "missing" and "holds 0", so EvalContext gains a
  `cell_is_blank` method; DataEvalContext overrides it with a direct
  cells lookup (an absent cell reads as Number(0.0) through
  get_cell_value, but is blank, whereas a real 0 is not).

Dependencies flow through the existing AST walk, so a TRIM/LEFT/etc.
reference is tracked and recalculates when its source cell is edited —
pinned by an end-to-end test through SpreadsheetData.

Engine unit tests 345 -> 352.
2026-08-18 09:35:19 +00:00
b93dc485b9 test(spreadsheet): cover public workbook serialization round trip
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
2026-08-18 08:31:17 +00:00
b7eacb2a94 test(spreadsheet): cover public multi-sheet evaluation
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
2026-08-18 08:27:13 +00:00
4deefabd0a test(spreadsheet): cover public style commands
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
2026-08-18 08:24:09 +00:00
a099ab9980 test(spreadsheet): cover public workbook lifecycle API
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
2026-08-18 08:02:17 +00:00
2824b49f0e test(spreadsheet): cover public workbook deserialization API
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
2026-08-18 07:44:23 +00:00
1629994de7 test(spreadsheet): add public persistence integration test
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
2026-08-18 07:30:43 +00:00
118fbefe9a test(spreadsheet): move format detection test to integration suite
Some checks failed
repo hygiene / hygiene (push) Has been cancelled
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
2026-08-18 07:07:12 +00:00
676b47087a refactor(spreadsheet-ui): centralize dirty region state
Some checks failed
repo hygiene / hygiene (push) Has been cancelled
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
2026-08-18 05:49:55 +00:00
f75c1cc966 feat(spreadsheet-ui): model dirty render regions
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
2026-08-18 05:41:42 +00:00
ee81d292ed test(spreadsheet): cover literal and comparison formula edges
Some checks failed
email.yml / test(spreadsheet): cover literal and comparison formula edges (push) Failing after 0s
repo hygiene / hygiene (push) Has been cancelled
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
2026-08-18 05:22:48 +00:00
0fcb2d3fae test(spreadsheet): cover sum boolean text error branches
Some checks failed
email.yml / test(spreadsheet): cover sum boolean text error branches (push) Failing after 0s
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
2026-08-17 19:44:27 +00:00
b7eb271a0d feat(spreadsheet): logical and conditional formula functions
Some checks failed
email.yml / feat(spreadsheet): logical and conditional formula functions (push) Failing after 0s
repo hygiene / hygiene (push) Has been cancelled
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
The formula engine could sum, average and do single-cell math, but had
no way to combine conditions or aggregate selectively — no `=IF(AND(...))`,
no `=SUMIF(...)`. This closes that gap with seven Excel-compatible
functions.

Logical (evaluated lazily, like IF, so they short-circuit):

- `AND(a, b, ...)` — TRUE iff every argument coerces to TRUE; ranges
  flatten to their cells. Stops at the first FALSE, so `AND(FALSE, 1/0)`
  is FALSE rather than `#DIV/0!`.
- `OR(a, b, ...)` — TRUE iff any argument is TRUE, short-circuiting on
  the first TRUE.
- `NOT(x)` — logical negation of the single argument.
- `IFERROR(value, fallback)` — `value` unless it errors, in which case
  the fallback is evaluated and returned (lazily).

Conditional aggregates (criteria-matched by position):

- `COUNTIF(range, criteria)` — count of matching cells.
- `SUMIF(range, criteria[, sum_range])` — sum of `sum_range` (or
  `range`) cells whose position matches; text in the summed region is
  skipped, errors propagate.
- `AVERAGEIF(range, criteria[, average_range])` — mean of the matched
  cells, `#DIV/0!` when nothing matches.

Criteria accept numbers, comparison operators (`>5`, `>=5`, `<5`, `<=5`,
`<>5`, `=5`), case-insensitive text (`"apple"`, `=apple`, `<>apple`),
and cell references holding any of those. Wildcards are not supported.

Dependencies flow through the existing AST walk, so a SUMIF's range and
criteria cell are tracked by the dependency graph and the formula
recalculates when an input is edited — pinned by an end-to-end test
through SpreadsheetData.

Engine unit tests 331 -> 343.
2026-08-17 12:19:57 +00:00
8ca5070b26 test(spreadsheet): cover the remaining serialization and border branches
Some checks failed
email.yml / test(spreadsheet): cover the remaining serialization and border branches (push) Failing after 0s
repo hygiene / hygiene (push) Has been cancelled
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
The last reachable lines in data.rs, all in the persistence layer and
one style command:

- serialize/deserialize round-trip for named ranges (the NAME block
  line), text colour, and an unknown block tag (ignored for forward
  compatibility), plus a malformed NAME line inserting nothing.
- The legacy CSV deserializer's `|B` bold and `|F` formula metadata
  segments.
- apply_command with BorderTarget::None clearing all four edges (the
  "remove borders" action), the complement of the per-edge arm the
  existing test covers.
- A large-range self-reference reporting CycleDetected (pins the fix
  in the previous commit).

data.rs 97.91% -> 99.49%; engine total 98.50% -> 99.13%. Unit tests
325 -> 331.

Deliberately uncovered, as documented: the B17 invariant's
debug_assert/#ERROR! patch (only reachable on a dep-graph bug), the
topological-sort underflow guard, the let-else continue after a cell
disappears mid-iteration, and the `}` attribution regions after
unconditional returns.
2026-08-17 11:49:00 +00:00
e1dcbefda3 fix(spreadsheet): large-range cycle detection must report, not zero
The large-range fast path in DataEvalContext::get_range_values (used
for ranges over 64 cells) handled a re-entrant formula cell — a
reference cycle — by silently pushing 0.0. The small-range path in
get_cell_value reports FormulaError::CycleDetected for the same
situation. So a cyclic formula inside a large range contributed zero
to the sum instead of surfacing #CYCLE!.

The inconsistency is invisible in normal recalculation: the topological
sort in recalculate_incremental detects cycles before any evaluation,
so the branch only fires through the legacy recursive get_cell_value
API. The fix makes the two range paths agree.
2026-08-17 11:49:00 +00:00
691b868264 fix(spreadsheet): make an editing cell readable — no doubled border, cell's own ink and fill
Some checks failed
email.yml / fix(spreadsheet): make an editing cell readable — no doubled border, cell's own ink and fill (push) Failing after 0s
repo hygiene / hygiene (push) Has been cancelled
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
The same three symptoms were reported twice. The first fix went to
`makepad_table`, which is not in the APK — `pageflipnav` does not depend on
it. The editor actually being clicked is `SpreadsheetGrid::draw_edit_overlay`,
reached through `nigig-build -> spreadsheet-ui`, and it is a custom-drawn
overlay rather than a `TextInput`. This fixes that one.

1. **No border of its own.** `draw_walk` calls `draw_selection_overlay`
   immediately before `draw_edit_overlay`, and the selection rectangle is
   already stroked around this exact cell in `selected_border_color`. The
   overlay then drew four more 2px rects inside it, which is the doubled
   frame: an outer selection edge and an inner editor edge a pixel apart.
   The four strokes are gone.

   Checked before removing them that this cannot leave a cell unframed. All
   three paths into `request_begin_edit` are on an already-selected cell:
   the keystroke path uses `self.selected_cell()` by definition, and both
   double-tap paths record `InputIntent::Select` for the same cell on the
   first tap. The caret remains the signal that the cell is in edit mode.

2. **The cell's own ink, not a forced one.** The overlay did
   `self.draw_text.color = self.text_color;` with no branching, so a cell
   with a user text colour, a bold cell, or a formula all changed colour the
   moment the caret landed in them. It now resolves the same way the resting
   draw path does: per-cell colour, then formula green, then bold, then the
   default.

3. **The surface it rests on.** The fill was always `edit_bg_color`, which
   equals `cell_bg_color` — so an odd row, which rests on
   `cell_alt_bg_color`, visibly changed shade when editing began, and a cell
   the user had given a background lost it entirely. That last case is the
   "background goes a different colour and the text disappears" report: the
   fill came from `edit_bg_color` while the ink came from the cell's own
   style, and nothing kept the two in agreement.

   It also covers the selection fill. A cell being edited is by definition
   selected, so `draw_cell_bg` had already painted `selected_bg_color`
   (#x2d4a63, a blue-grey) underneath — text was sitting on a wash it was
   never coloured for. That is the grey.

`grid.rs` had no tests and is excluded from the coverage report, because the
widget needs a live `Cx`. The background choice does not, so it moved into
`editing_bg_source` and is tested there; the border and ink changes are
pinned by reading this file, which is blunt but is the only thing short of a
GPU that can catch "compiles, runs, wrong on screen".

6 tests, verified against all three defects: restoring the border stroke
fails 1, forcing `text_color` again fails 1, and ignoring row striping and
cell backgrounds fails 3.

The 12 clippy warnings in this crate tree are pre-existing and unchanged —
checked by running the same command on a stashed tree. None are in the hunks
here.
2026-08-17 11:07:39 +00:00
587a43864f test(spreadsheet-ui): cover the remaining controller branches
Some checks failed
email.yml / test(spreadsheet-ui): cover the remaining controller branches (push) Failing after 0s
repo hygiene / hygiene (push) Has been cancelled
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
The UI controller modules are the one part of the spreadsheet stack the
coverage report could not reach before the memory-conscious build
(-j1); measured now, their last reachable gaps are closed.

geometry.rs 98.77% -> 99.51%:

- row_resize_at: a point just inside a row's bottom edge resizes that
  row, and a point in the middle of a row is no resize target. The
  exact boundary between two rows resolves to the lower one, so the
  bottom-edge arm had no other path to it.

render_cache.rs 98.21% -> 98.68%:

- get_or_insert_with on a missing cell runs the builder (the entry
  API's insert arm, the complement of the hit arm), and a later read
  sees the cached state without rebuilding.

model.rs 90.99% -> 98.40%:

- load_saved() and save() were only exercised by a #[ignore]d test.
  The test now runs by default: it serialises on a module-local lock
  and restores whatever was in the shared generated/ directory before
  it ran, the same save/restore convention the engine's own
  persistence tests use.

ui-controllers total 96.99% -> 99.11% (floor 96). UI lib tests
52 -> 55, all pass, none ignored.

Deliberately uncovered, as before: the panic-arm canaries in
event_router's positive tests, the frozen-pane `return None` guards in
col_at_x/row_at_y (the frozen loop spans exactly the frozen interval,
so they cannot fire), the existing unreachable-builder closure in
render_cache's hit test, and the environment-dependent save/restore
cleanup branch in model's disk test.
2026-08-17 09:26:55 +00:00
6b04b07e14 test(spreadsheet): cover the last reachable engine branches
Some checks failed
email.yml / test(spreadsheet): cover the last reachable engine branches (push) Failing after 0s
repo hygiene / hygiene (push) Has been cancelled
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
Three files, each the weakest reachable logic left after the workbook_api
tranche.

autofill.rs 92.71% -> 96.26%:

- A single chrono value is still a series (delta defaults to 1).
- A chrono sequence whose delta changes mid-run is not a series.
- A repeated chrono value has delta 0 and is rejected rather than
  producing an infinite fill.
- A backward chrono sequence wraps the delta modulo the list length
  (Feb, Jan -> delta 11).
- get_series_value on Series::None returns None.
- match_case with an empty value returns an empty string.

undo.rs 98.34% -> 100%:

- Redo of a deletion (a Change::SetCell whose `new` is None, the shape
  recorded by set_cell("") and remove_cell) removes the cell and its
  dependency-graph edges; undoing it restores both. This is the mirror
  of the existing create/undo test and was the one uncovered arm in
  Change::apply_redo.

workbook.rs 99.35% -> 99.68%:

- detect_workbook_version on a legacy `#MP_SHEET_V2` payload reports
  version 1.

Engine total 98.24% -> 98.50% (floor 96). Unit tests 318 -> 325.

Deliberately uncovered, as before: panic-arm canaries in positive
tests, the `}` after an unconditional return in detect_series, and the
test-harness save/restore cleanup branches in persistence.rs (their
inverse runs depending on pre-existing state — the CI-normal path).
2026-08-17 06:31:46 +00:00
89437838b7 test(spreadsheet): cover workbook_api.rs and util.rs remaining branches
Some checks failed
email.yml / test(spreadsheet): cover workbook_api.rs and util.rs remaining branches (push) Failing after 0s
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
The two weakest files left after the formula2 tranche.

workbook_api.rs 88.80% -> 99.58%:

- with_sheets_active with an empty sheet list falls back to a fresh
  single-sheet workbook.
- set_active_sheet with an out-of-range index is a no-op.
- remove_sheet: removing a sheet before the active one shifts the index
  down; removing the active one (or one after it) clamps to the new end.
- apply(): the style arms the earlier tests did not touch — italic,
  underline, number format, background — and both SetBorders shapes
  (per-edge target writes each requested edge, BorderTarget::None clears
  all four).
- Workbook::default() is a fresh single-sheet workbook.
- save()/load() round-trip through the default path, a legacy payload
  still loads as a one-sheet workbook, and a non-workbook payload yields
  None. The disk test takes a new crate::test_disk lock so it cannot
  race the persistence module's disk test under parallel `cargo test`;
  that pre-existing test now takes the same lock.

util.rs 90.91% -> 100%:

- write_col_letters matches col_letters across one-, two- and three-
  letter columns and reuses the caller's buffer without stale tails.
- adjust_formula_refs: a digit-bearing function name (LOG10) is not a
  cell ref; an 8-letter column overflowing u32 and a 30-digit row
  overflowing i64 are copied verbatim through the overflow fallbacks.

Engine total 96.54% -> 98.24% (floor 96). Unit tests 308 -> 318;
9 integration tests unchanged.

Deliberately uncovered: the mutex-poison recovery closure in the
test_disk lock (unreachable unless a test panics while holding it), and
the disk test's restore-to-clean branch (runs only when no save file
pre-exists — the CI-normal path; its inverse is the branch that runs
otherwise). Both match the existing persistence test's save/restore
convention.
2026-08-17 05:37:38 +00:00
4a6409f37b chore(spreadsheet): drop Workbook::load's dead legacy-migrate branch
`Workbook::load()` carried a fallback — if the saved payload was not a
V2 workbook but "looked legacy", migrate it — plus the two private
helpers behind it, `is_legacy_workbook_payload` and
`migrate_legacy_workbook`.

The branch is unreachable. `deserialize_workbook` already treats both
legacy V1 formats (single-sheet `#MP_SHEET_V2` and the older CSV) as
`WorkbookFormat::V1Legacy` and returns a migrated one-sheet workbook,
so by the time `load()` sees a legacy payload the `if let` above the
fallback has already returned `Some`. The two conditions are exact
complements — any string `deserialize_workbook` rejects is also rejected
by `is_legacy_workbook_payload` — so the migrate branch and both helpers
are dead code, confirmed by direct probe against `detect_workbook_format`.

Behavior is unchanged: the legacy migration still happens, inside
`deserialize_workbook`, which the existing
`legacy_v1_migration_clears_undo_history` test already pins.
2026-08-17 05:37:38 +00:00
d7fcd4c73d refactor(spreadsheet-ui): extract grid geometry so it can be measured
The UI coverage exclusion was hiding real logic, and the exclusion note
said it was not.

`grid.rs`, `ui.rs` and `workspace.rs` are excluded from coverage on the
grounds that they carry the `script_mod!` DSL and cannot be constructed
without a `ScriptVm`. That is true of the files. It was not true of most
of their contents: `grid.rs` is 2,702 lines of which roughly the last 30
are DSL, and of its 59 functions **36 take no `cx`, no `Event` and no
`Scope`**. Hit testing, cell rectangles, frozen-pane placement, scroll
offsets, resize borders, autofill handle bounds — all arithmetic over
plain numbers, none of it reachable by the report, and it carried
**zero tests**.

Confirmed rather than assumed: a probe test constructing
`SpreadsheetGrid::default()` fails to compile, because the `Script`
derive provides `script_default(vm)` and not `Default`. So the file
genuinely cannot be unit-tested — which is exactly why the logic had to
leave it rather than stay behind the exclusion.

`geometry.rs` holds that arithmetic now as `GridMetrics`, a plain struct
with no Makepad dependency. `grid.rs` keeps no second copy: `metrics()`
snapshots the widget's live fields and `col_at_x`, `row_at_y`,
`cell_abs_rect`, `range_abs_rect` and `handle_rect` all delegate. A
parallel implementation would drift from its own tests, which is the
failure this is meant to end, not repeat.

Behaviour is unchanged and the semantics were read out of the original
before being moved — including the ones that look like bugs and are not:
a point left of the row header returns `None` rather than column 0, the
frozen pane is searched before the scrolling area, and a fractional
scroll offsets by a fraction of the *default* width rather than the
overridden one, matching the scrollbar's model.

Two review items are addressed on the way. SPREADSHEET REVIEW item 7
names `col_at_x`/`row_at_y`/`cell_abs_rect` as O(N) scans run per frame
and per pointer event; item 10 names the geometry tangled through
`handle_event`. The maths is now in one place with a stated coordinate
convention, which is the precondition for replacing the scans with
prefix sums — that is a separate change, deliberately, because this one
must not alter a single pixel.

29 tests. They assert relationships rather than constants where the
relationship is the contract: every cell origin hit-tests back to its
own cell over an 8x6 grid, cell boundaries are half-open so there is no
dead pixel between columns, frozen cells stay put under a scroll, and a
reversed selection drag normalises instead of producing a
negative-sized rect. The fixture grid uses non-uniform sizes on purpose
— with every column 100 wide, an off-by-one column index and a
100-pixel offset error are indistinguishable, and so are a width and a
height.

Verified by mutation, six injected defects, each confirmed red:

  frozen columns scroll with the grid      1 fail
  range_rect stops normalising corners     1 fail
  cell boundary becomes inclusive          1 fail
  fractional scroll ignored                1 fail
  handle touch-target floor removed        1 fail
  resize ignores the header-strip check    1 fail

UI controllers 95.70% -> 97.00%, floor 95 -> 96; geometry.rs at 98.78%.
UI tests 23 -> 52. The gain is not the percentage — it is 409 lines of
logic that were previously invisible to it.

The exclusion note now says to audit the list before widening it. An
exclusion that quietly grows to cover real logic is worse than no
exclusion, because the number stays green while the coverage goes away.
2026-08-17 05:12:59 +00:00
c301c7a48f test(spreadsheet): cover formula2.rs tokenizer/evaluator branches
Some checks failed
email.yml / test(spreadsheet): cover formula2.rs tokenizer/evaluator branches (push) Failing after 0s
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
The branches the corrected report named, now that every test binary is
measured:

- Tokenizer: a lone `!` is a parse error; string escapes (`\n`, `\t`,
  `\\`); a double-dot number (`1.2.3`) is rejected after the tokenizer
  breaks on the second dot.
- FormulaError: `InvalidRef` and `RuntimeError` display strings; the
  `Display` delegation; `from_display` round-trip for `#REF!...`.
- BinOp precedence table, pinned so a shared precedence cannot pass
  silently.
- Value conversions: `to_f64` / `to_bool` / `to_display_string` /
  `is_error` propagate and render `Value::Error`.
- Evaluator: boolean literals; unary `+` (built directly — the parser
  collapses `+x` to `x`, so the `UnaryOp::Pos` arm only runs on a
  hand-built AST); single-cell named range; single- and multi-cell
  range expressions; `SUM(Undefined)` -> UnknownName through
  `resolve_arg`; `values_equal` text (case-insensitive), boolean,
  error and mixed-type arms.
- evaluate_call: `SQRT(-4)` -> `#DIV/0!`; `LOG10`, `SIN`, `COS`, `TAN`.
- evaluate_if: wrong argument count -> `#VALUE:`.

formula2.rs 90.11% -> 99.05% lines; engine total 94.38% -> 96.54%.
Unit tests 289 -> 308; 9 integration tests unchanged.

Deliberately left uncovered: the `invalid number` map_err closure in
the tokenizer (an f64 parse of a digit/dot string cannot fail) and the
`_ => panic!(...)` arms of existing positive match tests.
2026-08-17 05:04:49 +00:00
8776a961ee chore(spreadsheet): remove dead CellRef::parse_inner
`parse_inner` is a byte-for-byte duplicate of the earlier iteration of
`CellRef::parse` that carried `#[allow(dead_code)]` since a refactor,
and no caller in the repository references it. It never ran, so its 60
lines could only ever drag the coverage report down without guarding
anything. `parse` remains the single entry point for cell-reference
grammar.
2026-08-17 05:04:49 +00:00
8325805e22 test(spreadsheet): measure every test binary, and cover what that exposed
The coverage report was reading one object file. Cargo builds each
integration test into its own executable, so measuring only the lib-test
binary discarded everything `tests/` exercised.

That is not a rounding error. `persistence.rs` reported 41.77% with 14 of
its 17 functions apparently never called, while `tests/sync_flow.rs` was
calling `save_spreadsheet_state` and `load_saved_spreadsheet_state` on
every run and passing. The functions were covered; the report was reading
the wrong object. Fixing it alone moved persistence.rs to 72.15% and the
engine total 90.09% -> 90.57% without a single new test.

This is the third defect of its kind in this script — the ignore regex
that excluded the sources being measured, the awk matcher that never
fired, and now the single-object report. All three had the same
signature: a confident number that was measuring less than it claimed.

`--all-targets` for the UI crate too, so a future `tests/` file is
measured the day it is added rather than silently skipped. Doing that
immediately surfaced `spreadsheet-ui/tests/ui.rs`, which had **never
compiled**: the crate did not enable `makepad-widgets`' `test` feature,
so `makepad_widgets::makepad_test` did not resolve. `cargo test --lib`
never built it and nothing reported the breakage. The manifest now
enables the feature, matching `pdf-makepad`, and the two tests are
`#[ignore]`d with the same documented reason as `pdf-makepad`'s — the
fork has no headless Linux backend. Compiled on every run, so they
cannot rot further while appearing to be coverage.

Then the branches the corrected report named:

- `undo.rs` 86.11% -> 98.34%. Resize undo/redo, both directions. The
  `None` arms are the substance: a column with no width override must
  have its key *removed* on undo, not have a default written into it.
  Writing a default looks identical until the default changes, at which
  point every previously-resized-then-undone column stops following it.
- `persistence.rs` -> 93.70%. The legacy `current.sheet.csv` fallback,
  including that a whitespace-only current file must not shadow a real
  legacy one; `save_spreadsheet_state_as` writing where it says it does;
  and a rejected filename writing nothing at all.
- `style.rs` 92.19% -> 100%. Format and alignment codes round-trip, and
  the codes are distinct — a shared code passes a round-trip test while
  making two formats indistinguishable on disk.
- `model.rs` 84.87% -> 90.99%. Undo/redo intents, from_parts/into_parts,
  the active-sheet accessors agreeing with each other, and the disk
  round trip (`#[ignore]`d: it writes the shared generated/ file).

Verified by mutation, seven injected defects, each confirmed red:

  undo None-arm writes a default      6 fail
  two number formats share a code     1 fail
  save_as ignores its validation      1 fail
  legacy fallback removed             2 fail
  Undo intent wired to redo()         1 fail
  from_parts drops the active index   1 fail
  active_sheet_data_mut hits sheet 0  1 fail

Two of those changed the tests rather than merely passing:

- `save_as ignores its validation` really does write `../escape.tsv`
  into the crate root, and the file survives the failing run — so every
  later run failed on the previous run's debris rather than on the
  current code. The test now removes any leftover before asserting.
- `undo_and_redo_intents_reach_the_workbook` failed on first run because
  `apply()` does not call `begin_recording` and `apply_batch()` does, so
  there was nothing to undo. That asymmetry is the trap pinned by the
  engine's `only_set_cell_records_its_own_undo_step`; it now has a test
  on the model side too, since a caller reaching for `apply` and then
  offering an undo button gets a button that does nothing.

Also fixed a pre-existing clippy **error** in `util.rs` — `approx_constant`
on a literal `3.14` in a test that has nothing to do with PI. Confirmed
pre-existing by reproducing on a stashed tree. It denies the whole crate,
so no clippy gate could be added while it stood.

Engine 269 -> 280 tests, 90.09% -> 91.58%; floor 90 -> 91.
UI 17 -> 23 tests, 94.55% -> 95.70%; floor 94 -> 95.
2026-08-17 04:42:28 +00:00
a859053bc6 test(spreadsheet): cover remaining data.rs dependency-graph branches
Some checks failed
email.yml / test(spreadsheet): cover remaining data.rs dependency-graph branches (push) Failing after 0s
repo hygiene / hygiene (push) Has been cancelled
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
Targets the uncovered branches the coverage report named, around the
formula dependency graph and incremental recalculation:

- Formula replacement/removal edge cases: set_cell("") removes the
  cell and its edges, formula->value and formula->formula rewiring
  drop stale dependency edges.
- No-op removal paths: set_cell("") on a missing cell records nothing.
- Dependency-graph cleanup after formula deletion: remove_cell on a
  formula cell, and update_dependency_graph's defensive path when a
  dependent has no dependents entry.
- Affected-cell recalculation error paths: non-formula cells inside a
  cycle-affected set keep their raw value; the ="" empty-result
  regression; the recursive eval slow path (cached AST, parse
  fallback, and CycleDetected); parse_cell_computed_value arms; parse
  errors propagating through a dependent and through a large range.

Also covers named-range unary expansion (=-Total), the >64-cell range
fast path, non-numeric number-format fallbacks, and the demo_q3 /
demo_roi constructors.

Engine line coverage: data.rs 91.17% -> 97.90%; engine total
90.09% -> 93.24%. Unit tests 260 -> 279 (19 new); 9 integration tests
unchanged.
2026-08-17 04:40:28 +00:00
a88fb68eab fix(spreadsheet): B17 recalc invariant must not panic on empty-string results
`recalculate_incremental` treated "empty computed_value on a changed
formula cell" as proof that the topological sort dropped the cell, and
debug_asserted on it. But a formula may legitimately evaluate to the
empty string (`=""`, `=IF(FALSE, "x", "")`), leaving computed_value
empty through no fault of the dep-graph walk. In a debug build that
edit panicked the engine.

The invariant now checks what it actually meant to check: whether the
topological sort *visited* every changed formula cell, using the
`visited` set built from `sorted`. Emptiness is no longer used as the
error signal, so legitimate empty-string results flow through (the
display falls back to the raw formula text, as already documented for
empty computed values).
2026-08-17 04:40:28 +00:00
5cb1bfe9f3 test(spreadsheet): cover the branches the report named
Some checks failed
email.yml / test(spreadsheet): cover the branches the report named (push) Failing after 0s
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
Payment domain, storage, platform and UI / isolated-payment-tests (push) Has been cancelled
Payment domain, storage, platform and UI / payment-ui-tests (push) Has been cancelled
Nine tests against `workbook_api.rs`, the weakest file in the engine at
77.80%. Now 88.80%; engine total 88.97% -> 90.09%, tests 259 -> 260.

One of them pins a trap rather than a bug. `set_cell` calls
`begin_recording()` before `apply()`; the other eight public mutators
(`remove_cell`, `put_cell`, `set_col_width`, `set_row_height`,
`toggle_bold`, `set_number_format`, `set_alignment`, `set_bg_color`) do
not. Calling one of those directly and then `undo()` reverts the
*previous* recorded edit, not the one just made.

Nothing ships broken: every `spreadsheet-ui` call site takes its own
`data.snapshot()` first, checked one by one in `grid.rs`. But the
asymmetry is invisible at the call site and the next caller will not know
to snapshot. `only_set_cell_records_its_own_undo_step` states the current
contract so a change to it is a deliberate decision rather than an
accident.
2026-08-17 04:05:36 +00:00
83839ea0a3 test(spreadsheet): coverage for the UI controllers, and fix a 0% report
The coverage script measured the engine only, and it cherry-picked four
source files to report on, which flattered the number: 91.15% against a
hand-picked subset versus 88.97% for the whole of `src/`.

Rewritten to cover both crates honestly, with per-crate floors and a
listing of uncovered lines. Two bugs in the script itself:

- The ignore regex contained the work-directory name, so it excluded the
  very sources being measured and reported a confident 0%. The work dir
  also cannot live inside the repo, or Cargo treats the copied crates as
  workspace members and refuses to build them.
- `llvm-cov show` filename headers carry no trailing colon, so the awk
  matcher never fired and the uncovered-line listing was always empty.

`spreadsheet-ui/src/{grid,ui,workspace}.rs` and `src/bin/` are excluded:
the first three are `script_mod!` generated DSL and the last is desktop
startup, neither of which a unit test can reach.

UI controllers now measure 94.55%: `event_router.rs` 70.59% -> 97.96%,
`selection.rs` 80.65% -> 100%. UI tests 9 -> 17.
2026-08-17 04:05:29 +00:00