Commit Graph
130 Commits
Author SHA1 Message Date
prxnvt c1cc8264fe Model: sheets carry an identity while the app runs
Each sheet gets an identity (gridcraft_model::SheetUid, Sheet::uid) for
what must follow one sheet while sheets are inserted, deleted, moved or
renamed, which a sheet index or a name can't do. It lives only while the
app runs: it isn't saved to JSON or a file, and a new, inserted, copied
or opened sheet gets a fresh one. A rename, a move and the copies undo
keeps don't change it, so undo of a deletion brings back the same sheet.
Sheets compare by content, whatever their identity: any two identities
compare equal, and SheetUid::get compares them.

sheet.move with copy gives the copy a fresh identity. Tests cover new
sheets, clones, JSON, renames, moves, deletion and undo, copies and a
workbook read from a file.
2026-11-01 20:13:05 -05:00
prxnvt 52b00fe8f8 Engine: an edit that would make Python code too long is refused
Structural edits, sheet renames and deletes, and a cut to another sheet
rewrite the xl() references in Python code, and could make a cell's code
longer than the 8,192-character limit that python.set enforces. Opening the
saved file then dropped that code to its value. Such an edit is now refused
with a message naming the cell, and nothing changes; keeping the old text
would have left a reference the edit had moved.
2026-11-01 15:35:54 -05:00
prxnvt 0151f43af8 Engine: python.set keeps a cell's result only for the same code and return type
python.set kept the stored result whenever the code was unchanged, also when
the call switched the return type between an Excel value and a Python object.
The cell then showed a value and preview of the other type, and a save wrote
the new type with that old cached value. The result is now kept only when both
the code and the return type match; otherwise the cell is new code, which
shows #BLOCKED! until it runs, and its old spill goes with its readers
recalculated.
2026-11-01 15:11:59 -05:00
prxnvt 11b55e0c71 Calc: new Python code recalculates the readers of the cell's old spill
python.set, and Replace in a Python cell's code, removed the cell's spill
registration and its spilled values before the recalculation ran, so the
recalculation never saw the freed range and formulas reading those cells kept
their old values until a full recalculation. The commands now leave the spill
to the recalculation. When a Python cell's content changes and it has no array
result to lay out, cells_changed removes the spill and queues the freed range,
as it does for a formula replaced by a constant, so its readers are
recalculated; a cell that keeps its array result (the same code again) keeps
its spill and lays it out again. Clearing, pasting over and blocking a Python
spill already recalculated its readers, and undo and redo restore the values
along with the cells; a test covers them.
2026-11-01 14:41:48 -05:00
prxnvt 8e816dff1e Engine: Save Model asks before it writes over a Python cell
Save Model (new on main) asks before it replaces data in the column it writes
to, but it looked for a value or a formula only, so a Python cell whose cached
value is empty was taken for an empty cell and its code was replaced without
the question. Python code now counts as content there, as it does elsewhere.
2026-11-01 14:40:50 -05:00
prxnvt 0524c04756 Engine: Merge Across keeps only the upper-left Python code, and unmerging unblocks a Python spill
Merge Across emptied the value and formula of the other cells in each row but
left their Python code, so a hidden cell stayed a Python cell with an empty
value and was saved as a PY formula. It now clears their whole content, as
Merge Cells and Merge & Center already did. In the same command, a merge
change evaluates again the blocked arrays that could reach the area, but only
formulas were picked, so a Python result blocked by a merged cell stayed
#SPILL! after the cells were unmerged; Python cells are picked too.
2026-11-01 14:08:08 -05:00
prxnvt ec7ee8d4ea Engine: a Python cell that keeps only its value keeps its whole cached result
When a file is opened, a Python cell falls back to keeping only its value in
four cases: its code is missing from the Python part or the part is damaged
(the xlsx reader), its code is too long for a cell, its kept result is
malformed, or its result is past the adoption budget. The first kept the
values the cell spilled as plain values; the others dropped them, so values
the file showed were lost. All of them now follow one rule, through
Sheet::settle_spill: the anchor and every spilled cell keep their cached
values as plain values, with no live spill range; a cell's own content and
format are never overwritten; and the work is bounded by the spilled values
stored, not by the range. The values move from the spill into the cells, so
nothing is copied. The cut-by-budget test is renamed for what it now checks,
the dropped-result test now expects the values kept, and the adoption warning
and the file-format docs say so.
2026-11-01 13:28:19 -05:00
prxnvt 84d4f6aa84 XLSX: a Python script's length is measured after its escapes are decoded
A script's code was cut at the character limit before its ST_Xstring escapes
were decoded, so a script whose escapes (`_x0009_`, `_x000D_`) made the file's
text longer than the limit lost its end without a warning, although its code
fits. The code is now decoded first and then cut one character past the limit,
so a script that is too long once decoded still fails the check and its cell
keeps only its value, as before. Decoding is linear and reads at most a bounded
number of encoded characters, enough for any code that fits.
2026-11-01 12:58:13 -05:00
prxnvt e82e91b451 XLSX: a Python cell without its code keeps its whole cached result as values
A PY formula whose script the file doesn't hold (the Python part missing or
damaged, or the script over the code budget) keeps only its cached value, but
its array range stayed recorded, so the cached values over the rest of its
spill were dropped as a formula's spill would be, and nothing filled them
again. The range of such a cell is now forgotten when it is read: the anchor
and the cells of its spill keep their cached values as plain values, as the
file shows them, with no live spill. An ordinary formula's dynamic-array spill
is handled as before.
2026-11-01 12:56:55 -05:00
prxnvt 113aeff07c XLSX: a damaged Python part is a warning, and the workbook opens with its values
A Python part whose zip entry failed to read (a bad checksum or compressed
stream) made the whole workbook fail to open: the error was passed up, and the
failed read had also ended the package's read budget, so even a warning would
have left the sheets unread. The part is now read with
Package::read_optional_limited, which charges a failed read only that part's
allowance, and every error other than TooLarge is a warning. The Python cells
then keep only their cached values, as when the part is missing. Malformed XML
in the part was already a warning; the test covers it too.
2026-11-01 12:36:04 -05:00
prxnvt 44e9ddad45 Engine: a Python result cut by the adoption budget takes its cached spill with it
When a file's cached Python spills were larger than the adoption budget, the
cells past it kept only their own value, but their spill ranges and spilled
values stayed on the sheet, which went on showing values no result held. The
spill of such a cell is now cleared, as for a dropped result; the warning is
unchanged.
2026-11-01 12:33:22 -05:00
prxnvt 4fbd1dc337 Engine: a Hidden cell on a protected sheet hides its Python code
A cell formatted Hidden on a protected sheet hid its formula but not its
Python code: display::formula_hidden only looked for a formula at the cell (a
Python cell was covered only while its result spilled), and the formula bar
had its own check for formulas alone. Both now treat Python code as they treat
a formula, so the formula bar shows nothing for a hidden Python cell, as it
does for a hidden formula. Editing the cell is refused as before, because it
is locked.
2026-11-01 12:16:05 -05:00
prxnvt 14ab0f8ee3 Engine: Replace edits a Python cell's code and keeps it a Python cell
Replace matched a Python cell on its code and then rebuilt the cell from the
replaced text as if it had been typed, so the cell became a text cell holding
the edited code and the Python cell was lost. The replacement now edits the
code: the cell stays a Python cell with its return type, its result is dropped
(#BLOCKED!, as for any changed code) and a spill it had is cleared. Code that
would no longer fit in a PY formula is left unchanged and counted in a new
skipped field of the result.
2026-11-01 12:10:31 -05:00
prxnvt 2d56f1b936 Engine: a macro's check for writes over data keeps a Python cell
The footprint dry run and the per-step guard of macro runs (new on main)
compared formulas and values only. A step that replaced a Python cell with
its own cached value, or with other code that also shows #BLOCKED!, was not
counted as a write over data, so the code could be lost without the
question. A step that only blocked a Python cell's spill, which leaves its
code as it was, was counted as one. Python code is now content as a formula
is: a change of code or return type is a write, and a change of the cached
value alone is a recalculation.
2026-11-01 12:09:50 -05:00
prxnvt c03c61fd05 Merge main into Python cells as data 2026-11-01 12:00:09 -05:00
prxnvt fcbbfa5142 XLSX: Python cells as _xlfn._xlws.PY() formulas, code in the Python part, cached values; opening limits
A Python cell is written to xlsx as [MS-XLSX] 2.3.10 specifies: its
formula is _xlfn._xlws.PY(i,0|1), where i is the zero-based index of the
cell's script in the workbook's Python part and 0|1 the return type, 0
for an Excel value and 1 for a Python object. The part (2.1.23) is
xl/python.xml, content type application/vnd.ms-excel.python+xml,
related from the workbook part with
http://schemas.microsoft.com/office/2023/09/relationships/Python; its
root python holds pythonScripts, one pythonScript per Python cell in the
order the cells are written, with the code in code. The code keeps its
xl() references as text, so the formula has no further arguments. The
cell keeps its cached value. A spilling result is written like a dynamic
array (cm="1", t="array" ref=...) with the spilled values cached, so
other applications show the values (and #NAME? if they recalculate).

The reader turns a PY( formula with or without the _xlfn. and _xlws.
prefixes into a Python cell. An index names a script of the Python part,
and each %Px% in the script becomes the text of the formula's argument x
(2.3.10.4). The string-literal form, PY("chunk"&"chunk",0|1), still
reads, from the formula's own text so line breaks in the code survive. A
formula naming a script the part doesn't hold keeps only its cached
value, with a warning: no calculation can evaluate it. The reader keeps
the cached spilled values as the cell's result, so a workbook from Excel
shows its arrays without running anything. Python errors are cached as
#VALUE!, as #SPILL! and #CALC! already are. Native JSON keeps Cell.py
whole: code, return type, the last result and the exact error.

Calc lays out a Python cell's kept result: Python cells are nodes of the
dependency graph that read nothing, and a pass takes a dirty Python
cell's result instead of evaluating it, so its array spills, is blocked
(#SPILL!) and unblocked, and feeds its dependents exactly like a
formula's. An array that is malformed or has more than 1,000,000 cells
(only a crafted file holds one) shows #SPILL!. A dirty Python cell is
pending in the pass, as a dirty formula is, so a formula, a range read
or a cell-by-cell range walk that reaches it before its turn lays it out
first rather than reading its old value.

Opening a file can't make the app loop over a claimed size: the reader
keeps a Python cell's spill range only when it has at most 1,000,000
cells (a range claiming the whole sheet is dropped with its values, as a
formula's spilled values are), and io::adopt_python rebuilds results for
at most 4,000,000 cells over the whole workbook, with a warning for the
cells it skips. The Python part is read up to 64 MB and 1,000,000
scripts, a script is kept up to one character past what a cell may
hold, and the cells take at most 64 MB of code from the part in all,
since several formulas can name one script. io::enforce_python_limits
drops the code of a cell whose code doesn't fit in 8,192 characters as
PY(...) (the cell keeps its cached value, with a warning) and a kept
result that isn't a well-formed array within the cap, each with its
spill range and spilled values, so the sheet and a save no longer show
the old spill. Formats that keep PY() as an ordinary formula (xls, xlsb,
ods) are adopted the same way. Opening a file runs nothing.

The command-sequence fuzzer sets Python cells (code with xl()
references, quotes, line breaks, and code at and over the size limit),
and its xlsx round-trip check allows the Python errors becoming
#VALUE!, which is what the file format stores.

Tests: the written formula naming its script, the Python part with its
content type and relationship, return types, a spilling table saved as a
dynamic array, Python errors as #VALUE!, the round trip of values, code,
long code with quotes and line breaks and the error, and no Python part
without Python cells; a workbook laid out as the format specifies (an
index, a parameter named as %P2%) read as Python cells with their
spilled values and saved back with its code in a Python part; the
string-literal form; a formula naming a missing script, with and without
a Python part; 20,000 cells naming one long script against the code
budget; the part's reader and writer (escaping, odd %P tokens, a long
script, a repeated parameter); a blocker in the spill area giving #SPILL!
and clearing it spilling again; the native JSON round trip; a spill
range claiming the whole sheet; the workbook-wide budget; hostile JSON
(code over the limit, a result claiming far more cells than it holds); a
malformed result that never spills; PY() formulas at 8,079 and 8,080
characters of code. Calc: formulas, MAX, SUM, COUNTA and SUMPRODUCT read
a dirty Python cell whose array is blocked before its turn, in a full
and an incremental recalculation, and see the array once it is
unblocked; enforce_python_limits removes the spill of dropped code and
of a dropped result, and keeps a sound one.
2026-10-26 10:18:38 -04:00
prxnvt c6c54c57e6 Engine: python.set makes a Python cell; this build stores code and
doesn't run it

python.set {cell?, code, output?: object|value} makes the cell (the
active cell by default) a Python cell, keeping its style and taking a
formula and its spill away. `output` is the return type Microsoft
documents for PY(): an Excel value (the default) or a Python object;
without it a Python cell keeps the one it had. The code must fit in
Excel's 8,192-character formula once written as PY(...), quotes doubled
and literals joined, so the command checks the encoded formula rather
than a character count. A table's header row refuses Python (it holds
the column's name, as Excel keeps formulas out of headers), and a locked
cell on a protected sheet refuses it through the usual protection check.

Nothing runs Python here. Unchanged code keeps its cached result and
value; new or changed code shows #BLOCKED!. One undo step takes the cell
back.

Sheet::clear_spill removes the spilled values inside a range with work
bounded by the values stored, not by the range's size.

In the interface, F2, a double click or the formula bar on a Python cell
edits its code, and a changed text is committed through python.set,
which keeps the return type; through cell.set it would have replaced the
Python cell with an ordinary cell holding the code as text. Typing over
a Python cell replaces it, as typing over a formula does, and committing
the code unchanged writes nothing. Deleting all the code clears the
cell through cell.set, as an empty entry clears any other cell; code
of spaces is still code.

Tests: the code and return type in cell.get, #BLOCKED! for new code,
cached results kept for unchanged code, the active-cell default, undo,
refused params (missing or non-text code, a bad cell, a bad return
type), the encoded limit at 8,079 and 8,080 characters and with doubled
quotes, a table header, a protected sheet, and a formula's spill taken
away. A UI test edits a Python cell's code with F2 and Enter, commits it
unchanged, and types over it; another clears a Python cell by deleting
all its code, keeps code of spaces, and undoes the clearing.
2026-10-26 10:18:38 -04:00
prxnvt 44c663aea2 Model: Python cells, and the #PYTHON!, #BUSY!, #TIMEOUT! and #BLOCKED! errors
Cell gains `py: Option<Arc<PyCode>>`, read with a serde default and not
written for ordinary cells, so older native files load and ordinary
cells serialize as before. PyCode holds the code, the return type (an
Excel value or a Python object), the last result (value, type name,
preview, whether it converted to cells) and a stale flag for manual
calculation. The cell's `value` stays the cached result, so formulas,
display and saving read a Python cell like any other. is_blank,
CellStore::has and input_text (which returns the code) take the code
into account; Cell::has_content and clear_content cover value, formula
and code together.

The model reads and writes the PY function's syntax as Microsoft
documents it, PY("chunk"&"chunk",0|1), with string literals of at most
255 characters and the return type 0 for an Excel value, 1 for a Python
object. The reader takes the `_xlfn._xlws.` prefix (and `_xlfn.` or
`_xlws.` alone, in any letter case); to_file_formula writes it. (An xlsx
file keeps the code in its Python part instead; the xlsx commit adds
it.) The size limit is Excel's 8,192 characters for that formula, not a
count of code characters: doubled quotes and the "&" between literals
count, so code with many quotes fits in fewer characters (8,079
characters of plain code fit exactly). The model also finds the
xl("...") literals in code, skipping comments and other strings; a plain
literal may escape a quote or a backslash. A rewritten reference that
holds the literal's quote (a sheet name that needs quotes, 'Q4
2026'!B2) takes the other quote as the literal's delimiter, or an
escaped quote when it holds both, so it never ends the string early.

Edits: cell.set over a Python cell, Clear Contents, Clear All and the
cells a merge empties remove the code; Clear Formats keeps it. Copy and
paste (All, Formulas), Fill, sort, cut-move, sheet copy and undo carry
it; a cut to another sheet first qualifies its xl() references with the
source sheet, as a moved formula's references are, so they keep
reading the same cells. Paste Values and Paste Link do not carry it,
and paste operations do not combine numbers with a Python cell. Fill
copies Python cells instead of extending them as a series. cell.set
with =PY(...) is refused with a message that points to python.set, and
Text to Columns keeps a piece that reads as =PY(...) as text instead of
clearing it. Goal Seek refuses a Python cell as its changing cell, and
Solver as a variable cell, as both refuse formulas: the number written
there would replace the code. The sheet-protection check compares the
code, so a locked Python cell cannot be changed. cell.get reports
{python: code, output, typeName, preview, stale}.

The xl() literals follow reference-moving edits: rewrite_all_formulas
(row and column insert and delete, cell insert and delete, cut-move),
sheet rename and sheet delete adjust each literal like a formula
reference, and replace it only when it changed.

Errors: CellError gains Python, Busy, Timeout and Blocked, and ALL has
14 entries, so the lexer reads them as literals. ERROR.TYPE gives 11 for
#BLOCKED!, the number Excel returns. Microsoft documents no ERROR.TYPE
number for #PYTHON!, #BUSY! or #TIMEOUT!; they take 19, 20 and 21, after
the numbers Excel is known to use. The xlsx writer caches the four as
#VALUE!, as it already does #SPILL! and #CALC!, because they are not
error codes of the file format.

Tests cover the =PY refusal, removal by entry and clearing, undo, copy,
paste values, fill and sort, xl() rewrites through row insert, sheet
rename (also to names that need quotes, with one quote and with both),
cut-move (also to another sheet) and column delete, Goal Seek and
Solver refusing Python cells, Text to Columns keeping a =PY piece,
native JSON round trips, the new errors, the prefixes, escaped quotes in
literals, and the encoded size limit at 8,192 characters exactly and
with doubled quotes.
2026-10-26 10:18:38 -04:00
prxnvt c801e50e40 Merge #2152 (f8e1f8df, with main at f6b0cec6) into #478
# Conflicts:
#	ROADMAP.md
#	crates/engine/src/io.rs
#	crates/ui-egui/src/dialogs.rs
#	crates/ui-egui/src/lib.rs
#	docs/target-app-parity.md
#	docs/ui-parity.md
2026-10-26 00:32:55 -04:00
prxnvt f8e1f8dfc0 Merge #2145 (4d26e395, with main at f6b0cec6) into #2152
# Conflicts:
#	crates/engine/src/io.rs
#	crates/engine/src/lib.rs
#	docs/target-app-parity.md
2026-10-26 00:29:13 -04:00
prxnvt 4d26e395fc Merge #2550 (d4096eee, with main at f6b0cec6) into #2145
# Conflicts:
#	crates/engine/src/lib.rs
2026-10-26 00:26:35 -04:00
prxnvt d4096eee5a Merge #2144 (34eee860, with main at f6b0cec6) into #2550 2026-10-26 00:24:57 -04:00
prxnvt 34eee86053 Merge #2151 (6781aa49, with main at f6b0cec6) into #2144
# Conflicts:
#	crates/engine/src/io.rs
#	crates/engine/src/lib.rs
2026-10-26 00:24:11 -04:00
prxnvt 6781aa4927 Merge main (f6b0cec6) into #2151 2026-10-26 00:21:54 -04:00
aiongg f102fe29a1 Merge g3 into g4
# Conflicts:
#	ROADMAP.md
#	crates/engine/src/cmd/edit.rs
#	crates/engine/src/cmd/formulas.rs
#	crates/engine/src/cmd/inspect.rs
#	crates/engine/src/cmd/pivot.rs
#	crates/ui-egui/src/lib.rs
#	crates/ui-egui/src/tabs.rs
#	docs/gaps.md
#	docs/ui-parity.md
2026-10-26 02:45:41 +00:00
prxnvt 8fd6ec124b Engine: a macro shortcut is one letter, alone or after Ctrl+Shift
`parse_shortcut` took the last letter of any spelling, so `"Ctrl+Alt+M"` was saved as
Ctrl+Shift+M. It now takes a bare letter or one after Ctrl+Shift, Cmd+Shift or ⌘⇧, and refuses
other modifiers.

- Test: `shortcuts_parse_to_one_letter_and_avoid_reserved_and_taken_ones`.
2026-10-25 06:48:57 -04:00
prxnvt 299beb5085 Merge #2145 (bf6a8520) into #2152
# Conflicts:
#	crates/engine/src/record.rs
2026-10-25 06:48:57 -04:00
prxnvt c442da9221 Engine: stopping a recording says when it left the chosen shortcut off
Record Macro checks the Ctrl+Shift letter when recording starts, but
another macro can take it before recording stops; the save then leaves
the shortcut off, and nothing said so. The result now names it in
shortcutDropped, the toast after Stop says the macro was saved without
it, and the macro page describes it.
2026-10-25 06:48:57 -04:00
prxnvt 7c66aaa2d3 Engine: Record Macro takes a name, shortcut and description
macro.record checks name, shortcut and description when recording
starts and keeps them for the save, so a name that is taken, too long
or holds line breaks, or a Ctrl+Shift letter that GridCraft or another
macro uses, is refused before anything is recorded. With empty
parameters it records under the next MacroN, as before. A name or
description given when stopping still wins, and a shortcut that another
macro took while recording is dropped.

The shortcut rules (macros::parse_shortcut, shortcut_text,
shortcut_conflict, RESERVED_SHORTCUTS and check_new_name) live in the
engine, so the Record Macro dialog, Macro Options and agents agree.
2026-10-25 06:48:57 -04:00
prxnvt bf6a8520b8 Merge the footprint pull request (65634c2b, with #2144 and #2151) into #2145 2026-10-25 06:48:03 -04:00
prxnvt 65634c2bb7 Merge #2144 (b9aa4a6a) into the footprint pull request 2026-10-25 06:48:03 -04:00
prxnvt 0533480c97 Engine: recorded steps carry a number, so a group's rollback finds its own after others
A group remembered the recording's length when it began, which another workbook's rollback (or a
recording started again inside the group) made point at the wrong steps. Each kept step now has
a number, and each recording one of its own; a group remembers both, and its rollback takes back
the steps of its workbook numbered from its mark, or all of them when the recording started
inside the group.

- Test: `rollbacks_take_back_their_own_recorded_steps_in_any_order` (groups on two workbooks
  rolled back in turn, and a recording started again inside a group).
2026-10-25 06:48:03 -04:00
prxnvt b9aa4a6a9c Merge #2151 (0533480c) into #2144 2026-10-25 06:48:03 -04:00
prxnvt ca2be34e8a Engine: a rolled-back group takes what its script runs ran out of the recording too
Taking back the steps a rolled-back group recorded left what a taken-back script or macro run
ran (`Recorder::ran`) under its old index, where a later run could pick it up, and left it out of
the size count. `cut_back` now drops it with its step, moves a kept run's entry to the step's new
index and counts both again.

- Test: `a_rolled_back_script_run_takes_what_it_ran_out_of_the_recording`.
2026-10-25 06:29:23 -04:00
prxnvt 9a22c4b2f2 Merge the footprint pull request (24f0c3df, with #2144 and #2151) into #2145 2026-10-25 06:28:06 -04:00
prxnvt 24f0c3df92 Merge #2144 (1a00ce19) into the footprint pull request 2026-10-25 06:28:06 -04:00
prxnvt 1a00ce19fb Merge #2151 (0a929eaf) into #2144 2026-10-25 06:28:06 -04:00
prxnvt 0a9305f784 Merge #2145 (9a22c4b2) into #2152 2026-10-25 06:28:06 -04:00
prxnvt 0a929eaf68 Engine: a rolled-back group takes back only the steps recorded in its workbook
A rollback took back every step recorded since its group began, so an edit recorded in another
workbook while the group was open was lost from the recording although the edit stayed. It now
takes back only the steps recorded in the group's own workbook.

- Test: the other workbook's edit in `stop_action_runs_only_on_its_own_and_is_not_recorded`.
2026-10-25 06:27:53 -04:00
prxnvt 2e8284c191 Engine: a panic during a dry run puts the journal pause and the touch log back at any depth
The pause of a dry run was cleared only by a panic at the top level. A command that started a
dry run, nested in another, and panicked left journaling paused for the rest of the session (and
the touch log open) once the outer command finished. Each command now keeps the pause count it
started with, and its panic guard puts it back, closing the touch logs, whatever its depth.

- Test: `a_dry_run_cut_short_by_a_panic_leaves_journaling_on`.
2026-10-25 06:26:51 -04:00
prxnvt f0b667aa02 Merge the footprint pull request (361fff98, with #2144 and #2151) into #2145 2026-10-25 06:02:18 -04:00
prxnvt ec58ec5f4b Merge #2145 (f0b667aa) into #2152 2026-10-25 06:02:18 -04:00
prxnvt 361fff982a Engine: a dry run journals nothing, so the journal and Record Actions' start stay as they were
Every nested command a dry run executed was journaled. Past the journal's limits that evicted
the person's own oldest entries, and the restore could only cut back what was left, so one
`macro.check` of a long macro could empty the journal and move a Record Actions recording's
start. While `Saved` holds the session's state, commands are not journaled
(`Session::journal_paused`), and the restore puts the journal and the recording start back
exactly; the count of dropped entries it relied on is gone. The fallible calls of a dry run come
before the pause, and a panic at the top level turns journaling back on.

- Test: `a_dry_run_leaves_the_journal_and_the_recording_start_as_they_were` (a full journal, a
  Record Actions start, and a check of 12,000 nested steps; replaces the test that accepted the
  loss).
2026-10-25 06:02:18 -04:00
prxnvt fe14c0d86e Merge #2151 (0aae1ffe) into #2144 2026-10-25 06:00:19 -04:00
prxnvt 891986bf66 Merge #2144 (fe14c0d8) into the footprint pull request 2026-10-25 06:00:19 -04:00
prxnvt bfcf50aad3 Engine: rolling back an undo group takes back the steps it recorded
Stop Action (or any rollback) put a group's edits back but left the steps recorded while the
group was open in the recording, so playing the recording back made edits the workbook no longer
had. A group remembers where the recording was when it began, and its rollback cuts the
recording back to there.

- Test: `stop_action_runs_only_on_its_own_and_is_not_recorded`, which kept the rolled-back edit
  before.
2026-10-25 06:00:13 -04:00
prxnvt f225cd1026 Engine: Check Macro waits while a macro is being debugged
`macro.check` is a query, so the debugger's lock let it through, but its dry run runs the macro's
steps, which the lock refuses: the check reported the lock as the failure of a step. It now waits
with "Finish or stop the macro first", as other commands that change the workbook do.

- Test: `between_steps_only_looking_around_runs_and_the_run_continues_where_it_was`.
2026-10-25 05:16:37 -04:00
prxnvt 9fe25835d4 Merge #2145 (f225cd10) into #2152 2026-10-25 05:16:37 -04:00
prxnvt f6f4585c93 Engine: a dry run puts the document's revision back
The dry run's rollback counts as a change of the document and moves its revision on, which front
ends key their caches on, so a `macro.check` that changed nothing (and a shift it tried and took
back) looked like a change. `Saved` keeps the revision and puts it back with the rest.

- Test: the revision in `shifting_makes_room_and_is_checked_again`.
2026-10-25 04:14:12 -04:00
prxnvt 91bb5eded8 Merge the footprint pull request (f6f4585c, with #2144) into #2145 2026-10-25 04:14:12 -04:00