`formulas.defineName {"name": "_gc_macro_1_0"}` replaced the hidden
chunk of a stored item with a visible name: the item vanished from
`stash::get` and `stash::keys`, and its other chunks stayed behind as
orphan hidden names. `formulas.deleteName` could remove chunks the
same way, and `formulas.createFromSelection` with `replace` could
replace one from a cell label. `stash::set` refused a clash with a
visible name, but nothing guarded the other direction.
Defining, renaming or deleting a name that `stash::item_key`
recognises (`_gc_<key>_<n>` and legacy `_sc_<key>`) is now refused
with an error that names `force: true`, which lets the call go ahead.
Create from Selection lists such labels in `skipped` unless `force`
is true. The params docs of the three commands say so.
Each `stash::get` looks at every defined name, so listing the items
with `keys` and then reading each with `get` took time quadratic in
the number of names. A file with 20,000 hidden `_gc_macro_<i>_0`
names took about ten seconds in a debug build, and the macro list
reads its items this way from data that comes from the file.
`stash::items(wb, prefix)` now groups the names by item in one pass
and returns each item's text by key, and `keys` lists from the same
pass. Both return at most `MAX_ITEMS` (10,000) items, the first keys
in sorted order, so a file with any number of hidden names can't make
a listing unbounded. A test counts the names each listing looks at.
The xlsx writer drops characters XML 1.0 can't carry: controls other
than tab, line feed and carriage return, and U+FFFE and U+FFFF. A
stash item holding one lost it on save without a warning, and so did
a JSON item, since serde_json leaves U+FFFE and U+FFFF unescaped: a
stored macro parameter changed silently.
`chunk_formulas` now writes each such character, and the marker
itself, as the noncharacter U+FDD0 followed by four hex digits, and
`get` turns them back for `_gc_` items. A marker that isn't followed
by four hex digits naming a character is read as it is. An escape
counts as five units against the 255-unit literal limit and is never
split across literals, and the guard on the stored size allows the
longest item even when every character is escaped. Legacy `_sc_`
items are read unchanged.
A menu-style call with no parameters, such as `file.open`,
`data.fromTextCsv` or `home.formatCells` with `{}`, only queues its
dialog and succeeds. The recorder kept it as a step with empty
parameters, and then kept the dialog's confirming call as well, so
playing the recording back ran the command twice, or failed on the
empty step when played back headless. Only `execute_headless` took
the step back, and only for the open and save-as dialogs.
`execute` now compares the queued UI requests before and after a
top-level command and does not record one that queued a dialog. The
special case in `execute_headless` goes (it keeps turning a missing
file into an error), and with it `Recorder::truncate`, which had no
other caller.
A command that opened an undo group and then returned an error (a `?`
between `group_begin` and `group_end`) left the group open for good:
Undo and Redo refused with "finish or stop the running action first",
later edits made no undo steps, and nothing could stop the group.
`execute` now rolls back every group a failed command opened, as
`group_rollback` does, restoring the workbook, selection, undo and
redo history to where each group began. Only a command that succeeds
keeps a group it opened, which is what a debugger's start needs.
`Session::abort_groups(doc)` and the menu-less command
`automate.stopAction` roll back the groups left open on a document,
for a debugger the person abandons or a caller that dropped its
token. The command is not undoable and works while a group is open,
and the Undo refusal now names it.
The architecture note named formulas and validation only; it now lists the
Paste Special kinds and linked pastes that write formulas, and the kinds that
carry nothing.
The panic guard restored the workbook and selection but not the history, so
a nested edit made before the panic left a no-op undo step and the redo
history it had cleared stayed lost. The guard now truncates the steps made
during the command and puts the redo history back. Regression test, and the
architecture note says what the guard restores.
The header and the revision row said 2026-10-11, older than the row above them. The document trust paragraph now says how DocState::trust (provenance) differs from DocState::scripts_trusted (per-session consent for stored scripts, #2178).
* CI: dev builds keep line tables only
* CI: one integration-test binary per crate
* CI: xtask runs tests with nextest, doctests apart, --keep-going on clippy and wasm
cargo xtask test runs the workspace tests (--lib --bins --tests) with cargo-nextest when it is installed, else cargo test, then the doctests. cargo xtask ci uses the same two steps.
* CI: docs name cargo xtask test
* CI: agent guides name the moved fuzz test
* CI: tests added on main join their crate's test binary
With row 1 frozen, Page Up could land on A1, which is always on screen, while the scrolled rows
stayed a page down (row 10, Page Down, Page Up, Page Up ends on row 1 with rows 2-9 hidden). The
same held for Ctrl+Up into frozen rows and Ctrl+Left / Alt+Page Up into frozen columns.
Arrow and paging keys now record the cell they moved from (`GridState::reveal_from`). When such a
move goes from the scrolled rows (columns) into the frozen ones, the scrolled area goes back to
its top (left). Other reveals (clicks, Find, Go To, Name Box, Undo, the control channel, formula
pointing) leave it unset and keep the view where the user scrolled it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- automate.trustScripts: only a missing `trusted` defaults to true; a
non-boolean value is an error and leaves the workbook untrusted.
- The refusal message points at Trust Scripts in View > Macros (Alt+F8)
and at automate.trustScripts.
- Catalog row for automate.trustScripts; parity checklist regenerated.
- Script steps run inside an io::ScriptSandbox (thread-local) so
read_file/write_file refuse whatever the parameter is called, and a
step that raises OpenUrl is dropped and stops the script. The id/param
deny list stays as the up-front refusal.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With headings hidden too, a level button sat across the empty column band
at the grid's top edge, so a click there still ran it. Outline hit testing
now stops when both bands are empty. The parity doc no longer lists Ctrl+8
as missing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A sheet can hide its outline's level buttons and brackets while keeping
the outline (Excel's Ctrl+8, saved as showOutlineSymbols="0"). It wasn't
modelled, so such sheets showed a wide outline band Excel doesn't.
- model: Sheet::show_outline_symbols (default on, also for older JSON).
- xlsx: read and written on the sheet view.
- engine: view.showOutlineSymbols {on?} (Ctrl+8), undoable.
- ui: no outline bands while it's off.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A number too wide for its column is drawn as ####. With a repeat fill
(`*` in the format, as in the Accounting format) the fill step then
rebuilt the text from the formatted value, so it overflowed and, being
right-aligned, lost its leading digits: 2,509.00 CHF in a narrow column
was drawn as "509.00 CHF". The fill now leaves #### alone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts:
- crates/ui-egui/src/i18n.rs: language registration lines kept from both sides (Language::ALL has 18 entries).
- crates/ui-egui/tests/file_menu.rs: both language rows kept.
- docs/localization-parity.md: one Last updated line naming all changes; both revision rows kept, newest first.
Conflicts:
- crates/ui-egui/src/i18n.rs: language registration lines kept from both sides (Language::ALL has 17 entries).
- ROADMAP.md: one Last updated line naming both changes; both progress-log rows kept.
- docs/localization-parity.md: one Last updated line naming both changes; both revision rows kept.
Conflicts:
- crates/ui-egui/src/i18n.rs: language registration lines kept from both sides (Language::ALL has 16 entries).
- docs/localization-parity.md: both revision rows kept.
crates/ui-egui/src/grid.rs: keep main's touch-pan guard around interact, add 1095's update_cursor call after it.
docs/ui-parity.md: one Last updated line, change notes combined; both history rows kept.
Conflicts:
- crates/model/src/lib.rs, crates/xlsx/src/read.rs, crates/xlsx/src/write.rs: use lists unioned (ShowObjects and the phonetic types)
- docs/file-format-parity.md: main's header and rows kept (#1692 collapsed the three duplicate .xlsx rows; they stay), phonetic-runs text added to each; Last updated 2026-10-20
Conflicts:
- crates/ui-egui/src/i18n.rs: mod it and mod ja both kept; Language::Ja dispatches to ja::translate; the shape test (PR #831) iterates ja::CORE, which replaces JAPANESE.
- docs/localization-parity.md: both revision rows kept, newest first.
ROADMAP.md: all progress-log rows kept.
docs/ui-parity.md: one Last updated line, change notes combined; both history rows kept; ribbon row wording tidied.
crates/ui-egui/src/dialogs.rs: window width match keeps both zoom and pasteSpecial arms
docs/ui-parity.md: status line combined, change-log rows both kept