Repository navigation
Conversation
…contrast test Tokens gains the colours the chrome was hard-coding, each with a light and a dark value: on_accent (text on an accent fill), an eight-colour category palette (theme::Cat), the paper colour inside icons, reference colours for the formula bar and for the sheet, the message box warning disc, the tab bar scroll thumb, and sheet_control_* colours that follow the sheet and so stay light in both themes. What now comes from the tokens: - icons: paper (white pages glared in dark mode) and the accent constants; icons placed on the sheet use icons::paint_on_sheet, which keeps the light colours; - reference colours (editor::REF_COLORS), which were hard to read on the dark formula field; - white text on the accent in OK, Solve, slicer tiles and the AutoSave switch, 2:1 on the dark accent; - the comments pane (a white card with light text in dark mode), gallery hover and swatch outlines, the message box disc, the tab bar thumb, chart selection frames, and the sheet's filter and validation buttons. A unit test (theme::contrast_tests) checks every token pair used by the ribbon, popups, dialogs, panes, formula bar, status bar and sheet in both themes against WCAG 2: 4.5:1 for text and 3:1 for glyphs. It caught the dark danger red (3.5:1 on hover) and the selected header text (4.1:1 light), which are adjusted.
- widgets::big_split_button: the icon runs the default action, the
label and its arrow open the menu. It returns both responses, with
accessible names ("AutoSum" and "AutoSum options"). big_button shares
its label layout.
- Formulas › AutoSum uses it: the icon inserts SUM as before, and the
arrow, which used to do the same, opens the Sum / Average / Count /
Max / Min / More Functions menu that Home › AutoSum already had
(shared as autosum_menu). Its keytip is unchanged.
- When a tab's groups are wider than the window, arrows at the card's
edges scroll them; before, only the mouse wheel did and nothing showed
that the tab went on. ribbon::RibbonOverflow records the groups' and
the card's sizes each frame.
- A test checks every tab fits the card's three rows. It found View ›
Show stacking four checkboxes, with Headings cut off; Show is now two
columns (Ruler, Gridlines / Formula Bar, Headings).
Tests: tests/ribbon_layout.rs (the split button's two parts, AutoSum's
icon and menu, the scroll arrows, the card height).
The formula bar is a top-aligned row: the Name Box and the cancel, enter and fx buttons stay on the first line while the field grows. Its height is the content's laid-out line count (Alt+Enter or wrapped text) times the font's row height, from one line (30 px, as before) to five, then the field scrolls with the caret in view. This holds while editing and while viewing a cell. The height eases over 120 ms when the line count changes, and the chevron pins it at five lines (it used to give a fixed 72 px). The in-cell editor likewise stops at five lines and scrolls. formula_bar::BarLayout records where the parts went, for tests. widgets::code_editor(ui, id, &mut text, Lang, max_lines) is a small code editor for features that edit code: the monospace font, syntax colours from Tokens::syntax (new, with light and dark values in the contrast test) for Python (keywords, strings including triple-quoted ones, numbers, comments and builtins) and LaTeX (commands, scripts and comments), Tab inserting four spaces (or indenting every selected line, Shift+Tab dedenting), Enter keeping the line's indentation (one level more after a Python colon), and a scroll area that follows the caret. The tokenizer and the edits are pure functions with tests, including hostile input that must keep spans on char boundaries. kittest (tests/formula_bar.rs) covers Alt+Enter up to eight lines with the Name Box and buttons fixed and the caret visible, the commit, viewing the committed cell, the chevron, the ease, the in-cell cap and the code editor's Tab and Enter.
Session::grouped(label, f) runs f as one undo step on the active document. On success the group leaves exactly one undo entry holding the workbook from before the group; on error it restores the workbook, selection, undo and redo stacks as they were and returns the error. group_begin, group_end and group_rollback do the same across separate calls, for an action that runs one step per call. While a group is open on a document, undoable commands on it add no undo steps of their own, so a long group neither grows the history nor pushes older entries past the undo limit, and Undo and Redo are disabled there. Groups nest: the outermost one makes the step, and a failing inner group rolls back only its own changes. Closing a document drops its groups, and a command that panics drops the groups it opened. history.list reports whether a group is open. Insert Copied Cells folds its nested steps into one undo step by counting revisions; inside a group the nested steps push no entries, so it now leaves the history alone there instead of removing entries from before the group.
A macro recorder needs more than a journal index: the journal is trimmed past 10,000 entries, it also holds the commands that other commands run (so playback would run them twice), and it keeps no selection, which relative references need. Session now tracks how deeply execute calls are nested. While recording, a buffer (record.rs) keeps each successful, journaled top-level call with the document uid, the active sheet (index and name) and the selection it ran in, then its id and parameters without secrets, as the journal stores them. Queries, failed commands and the command that starts recording are not kept. The buffer holds up to 100,000 steps and counts any it drops. Session::start_recording, stop_recording, is_recording and depth are the interface. Automate › Record Actions keeps recording through the journal as before.
Active content such as macros must not run only because a workbook was opened. DocState gets a trust state: Local for workbooks made in the session (new workbooks and samples) and FromFile for anything opened with file.open. Active content in a FromFile workbook asks before it runs. A second window on a workbook shares its trust. The clipboard carries the trust of its source document. Pasting formulas, formats (conditional formats hold formulas) or validation from a FromFile workbook into another workbook makes that workbook FromFile too; pasting values, column widths or notes carries nothing active and changes nothing. Sheet moves and copies stay inside one workbook, so its trust is unchanged. document.inspect reports the trust.
GridCraft keeps workbook data in hidden defined names so it travels inside the xlsx file. The existing items write one name holding one string literal, which past Excel's 255-character literal and 8,192-character formula limits gives a file Excel does not accept. cmd/stash.rs stores an item `key` as hidden workbook-scope names _gc_<key>_0, _gc_<key>_1, ..., each a formula of string literals of at most 255 characters (UTF-16 units, as Excel counts them) joined with &, at most 8,192 characters per name. Reading concatenates the chunks in order and stops at a gap; it also accepts the older single _sc_<key> name. Names are matched case-insensitively and only at workbook scope. Keys are limited to letters, digits, _ and ., items to 4,194,304 characters, and junk around or inside the literals, duplicate or out-of-order chunks, non-JSON and over-deep JSON all read safely. keys(prefix) lists the stored items that start with a prefix. Custom views, scripts, the watch list and scenarios keep their current storage; nothing reads or writes the new names yet. docs/architecture.md describes the engine building blocks added in this series: undo groups, the recording buffer, document trust and this storage.
A macro is plain data in the model (model/src/macros.rs): a name, a description, an optional shortcut, declared inputs (shape, header, column types, non-empty, default reference), steps, an output rule (anchor and what to do on a conflict) and the commands it may run that read or write files. A step runs a command (id and params) or is one of three bounded control steps: Repeat over the rows or columns of a range, If on one cell's value, and Stop with a message. Every step carries enabled, breakpoint and note flags. These are GridCraft macros, not VBA; a VBA project in a file stays preserved and is never run. Workbook::macros holds them. The JSON format keeps the field. In xlsx, io::save_bytes writes each macro as one chunked hidden-name item, _gc_macro_<n>, and io::open_bytes takes the items back out of the names, so the names never linger in the open document. Limits are checked whenever a macro is read from a file or a command: at most 1,000 macros, 10,000 steps each, Repeat and If nested 4 deep, names and notes of bounded length, and at most 4,194,304 characters of JSON, so that saving a workbook never fails because of a macro; nesting is checked before the steps are deserialized. What is over or damaged is dropped with one warning, and clashing names get a suffix. Automate scripts keep their own storage (_sc_scripts) and are neither read nor changed by this.
Step parameters can name where they act through tokens that are worked
out when the step runs, so a macro made on one block of data works on
another: a declared input (Input, or any other input's name), Output
(the output rule's anchor), Row or Column (the current item of the
Repeat step around the step) and Name:Total (a defined name or a
table's data). Methods narrow or move the range: col(n) or
col("Header"), row(n), cols(a..b), rows(2..), cell(r,c) relative to the
top-left cell, offset(r,c), resize(h,w), data (without the header row),
below(n) and right(n). Methods chain.
A token is written as {"$ref": "..."} anywhere, as the whole string of
a reference parameter (cell, range, target, keys ending in Cell or Range
and similar), or as {{...}} inside any string, which is how a formula
refers to its input. Other text parameters are never read as tokens, so
a cell can still hold the word Row and a chart can still be of type
column.
Every token becomes A1 text, sheet-qualified when it is on another
sheet, before the command runs, so no existing command changes. Plain A1
references in a step stay absolute. A reference that runs past the edge
of the sheet is reported apart from other mistakes, so the footprint
check can tell the two apart.
A height of 0 hides rows and any other height shows them, but home.rowHeight changed the rows without a full recalculation, so SUBTOTAL(10x) kept counting the rows as they were before. It now recalculates when a row's hidden state changes, as Hide and Unhide do. Found by the command-sequence fuzz once the macro commands changed the commands it draws.
…olicy
macro.run runs a macro as one undo step ("Run Macro: <name>"); an error
at any step rolls everything back and names the step and its path (3,
2.1 inside a Repeat, 4.then.1 inside an If). It works in four parts:
- Policy (macro_policy). File, app, window, automate and macro commands
and Undo and Redo never run in a macro. Reading or writing files
(data.getData, data.fromTextCsv, insert.picture, Page Layout
Background, or an export with a path) needs the command in the
macro's permissions and the person's approval. A macro in a workbook
opened from a file asks before it runs. Approval holds for the
session until the macro's content changes; it is asked for with the
new EngineError::Approve and given with approve: true. A step that
would open a dialog or a web page fails instead.
- Inputs. Each declared input is bound (given, its default, or the
selection for the first one), whole columns shrink to the used range,
and the shape and column types are checked with messages that name
the cells: "Input column 2 (Amount) must hold numbers; C4 holds text".
- Footprint. Unless the macro may overwrite, it first runs in dry-run
mode on the real workbook inside an undo group that is rolled back,
with the session's clipboard, journal, windows and the document's
trust put back. CellStore::diff on every sheet, plus the cells
commands wrote (DocState::touched), sheets and objects, gives what it
would write outside its inputs; cells that only recalculated do not
count. Writing over data, deleting a sheet or deleting an object is a
conflict: ask (the new EngineError::Confirm, so programmatic runs fail
unless they pass onConflict or overwrite), fail, overwrite, or shift
the data down or right with Insert Cells and check again. A step that
would fail, for example past the edge of the sheet, fails the run
before anything changes. File steps and Solver stop the dry run early
and are reported.
- Steps run on a machine with an explicit frame stack: Repeat over the
rows or columns of a range with Row or Column bound, If on one cell
with Excel's comparisons, and Stop, which keeps the changes or rolls
them back. A run executes at most 100,000 steps within a time limit
(60 s by default, timeLimit to change; the web build has no clock, so
only the step limit applies there).
macro.check reports the bound inputs, the output anchor, the approval
needed, the footprint and the conflicts without changing anything;
nothing from an unapproved macro runs, not even as a dry run.
macro.debug runs a macro one step at a time. start checks the macro as macro.run does (policy, approval, inputs, footprint and output rule) and pauses before the first step; step runs one step; continue runs to the next breakpoint; runTo runs to a chosen step path, stopping at breakpoints on the way; stop ends the run and keeps its changes or rolls them back; state reports where it is. Breakpoints are the steps' own flags plus a list of paths given with any action (breakpointsOnly makes the list replace the flags), and they stop in every pass of a Repeat. The run lives on the session between calls, inside an undo group that spans them (group_begin and group_end), so a debugged run is one undo step "Run Macro: <name>", as a normal run is. A failing step ends the run and rolls it back; so does a Stop step that asks for a rollback. While a macro is being debugged, edits of the workbooks the run touched (its own, and any other a step changed) are refused at the top level with "Finish or stop the macro first"; other workbooks stay editable, and selecting and queries still work. Every answer gives the next step's path, kind, command, parameters as they would run now (tokens resolved), the Repeat pass, the parameters of the step that just ran, the steps executed and the cells changed so far. Each action gets its own time limit. If a command panics while a step runs, the session's panic guard rolls the debugged run back as well, so its undo group never stays open.
Commands: macro.list, macro.get, macro.save (a macro object or its
fields; replace defaults to true), macro.delete, macro.rename and
macro.record, beside macro.run, macro.check and macro.debug. Saving,
renaming and deleting are undoable. The catalog gains View › Macros ›
Record Macro (parity 275 / 291).
The recorder builds on the recording buffer of top-level commands, so
nested commands are never recorded twice and trimming the journal loses
nothing. When it stops, commands a macro may not run are left out and
listed; a macro run while recording is replaced by its steps when it
has no inputs, and an Automate script run by the script's steps;
sheet.activate and selection.set steps are added where the sheet or
selection changed between commands without one, and runs of them
collapse to the last. With relative references (the default) the
selection when recording started is the example input: references in
and near it become tokens (Input.col(2), Input.rows(2..),
Input.col(2).below, Input.row(4).right, Input.cell(r,c)), exact
references in formulas become {{...}}, and the input's width, header
and column types are inferred from the example. A recording replayed on
a longer block of data writes in the matching places.
A toggle such as Bold is recorded with the state it set ("on": true),
as Excel records Bold = True, so a replay sets it instead of flipping
it again.
Automate scripts are unchanged: automate.* keep their commands,
parameters, storage and results, and View › Macros still lists the
scripts and a preserved VBA project.
docs/macros.md describes the macro format, tokens, running, the footprint check, the policy, debugging, recording and storage, and how macros sit beside Automate scripts and a preserved VBA project. ROADMAP.md gets a progress log row and a revision row (catalog 274 to 275 of 291); docs/architecture.md and docs/target-app-parity.md mention the macro engine.
# Conflicts: # ROADMAP.md # docs/architecture.md
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.
…ng status, macro shortcuts The Automate tab gains a Macros group: Macros opens the Macros dialog, Record Macro opens the Record Macro dialog (name, Ctrl+Shift shortcut letter, description, Use Relative References) and turns into Stop Recording while recording, Use Relative References sets how the next recording starts, and Run Macro runs the macro run last; its menu lists the recent macros, then every macro. While recording, the status bar shows a red dot, "Recording · N steps" and Stop. The Macros dialog (Automate › Macros, View › Macros, Alt+F8) lists the workbook's macros with their shortcuts, then its Automate scripts, and says when the file carries a VBA project, which is kept and not run. Run, Delete (which asks first) and Options (shortcut letter and description) act on the selected row. The dialog keeps the result of view.macros, so the scripts and the VBA entry are listed as before. A macro that needs approval asks with Run, and one whose output would land on data asks with Overwrite, Shift Down and Shift Right; each button runs the command again with its answer. Ctrl+Shift+letter (Cmd+Shift on a Mac) runs the macro with that shortcut; the letters GridCraft uses keep their meaning. The View › Macros keytips (WMR, WMV) now open the Record Macro and Macros dialogs. Icons for macros, recording and relative references are drawn in code, the recording dot is a theme token covered by the contrast test, and large and small ribbon buttons and icon buttons carry accessible names.
New Macro (Automate › Macros) and Step Into, Edit and Create in the Macros dialog open a resizable step editor. It works on a draft of the macro and saves it with macro.save. The step list shows each step's number, a breakpoint dot in the gutter (click to set or clear), a category icon, a summary, an error marker and an enabled checkbox; Repeat bodies and If branches are indented under their step, and rows drag to reorder within their list. Add Step opens a command picker (control steps first; commands a macro may never run are left out) and fills in the command's required parameters; Duplicate and Delete act on the selected step. The inspector's Details tab edits the selected step. Reference parameters are reference boxes that take a typed reference or a selection on the grid, with a Rel/Abs switch between a token relative to the example input and an absolute reference, and token suggestions. The Inputs tab sets each input's shape, header and column types (Use current selection as example infers them); the Output tab sets the anchor and the conflict rule. The debug bar has Check (a dry run that colours the grid: inputs blue, writes green, writes over data red), Run, Step, Continue, Run to Here and Stop, which asks whether to keep the changes or undo them. The debugger receives the editor's breakpoints with breakpointsOnly, so a dot cleared while paused takes effect at once; the current step is selected, its parameters are shown as they will run, and its target is outlined on the grid. While a macro is debugged, the status bar says so and Show reopens the editor. List drag and drop lives in a small dnd module. The debugger icons are drawn in code, and the breakpoint and sheet overlay colours are theme tokens covered by the contrast test.
…kbook closes Closing a workbook (File > Close, Cmd+W, the Backstage Close) with the macro editor open now asks Save Changes? when the editor's draft has unsaved changes, even if the workbook itself is saved. Save saves the macro into the workbook, then saves and closes the workbook; Don't Save discards the draft; Cancel, or dismissing the prompt, brings the editor back with its draft. A new macro that has not been edited does not ask. A workbook that closes now closes the macro editor too, so a draft can no longer be saved into whichever workbook becomes active next.
Contributor
Author
|
Related: #496 adds a Save / Don't Save / Cancel prompt when the window closes or the application quits. It reuses the per-workbook Save Changes? prompt that this pull request extends, so once both are merged an unsaved macro-editor draft is also covered when the window closes. |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Depends on #478 (macro UI), which in turn depends on #456 and #465. Only the last commit belongs to this pull request.
Summary
The macro step editor works on a draft that is saved into the workbook with
macro.save. Before this change, closing the workbook (File > Close, Cmd+W, or Close in the Backstage view) took no account of that draft. A clean workbook closed at once and the draft was lost. A dirty workbook showed Save Changes?, and Save wrote the file without the macro. In both cases the editor could remain open after its workbook had closed, so a later Save would have written the draft into whichever workbook became active next.With this change, a draft with unsaved changes counts as unsaved work in the existing Save Changes? prompt, in the same way as the cell that is still being edited:
A workbook that closes now also closes the macro editor.
Changes
macro_editor:Editor::has_unsaved_changes. A new macro counts only once it differs from the empty macro the editor started with. The file also gainshas_unsaved_draft,save_draft,reopenanddiscard, which the close prompt uses.SheetApp::run:file.closewithoutforceopens Save Changes? when the editor holds an unsaved draft. It returns{"closed": false, "dirty": true}, which is the result the engine gives for a dirty workbook. When the workbook does close, the editor closes with it.dialogs: in Save Changes?, Save commits the macro draft after the cell draft and before saving. Whenever the prompt closes without closing the workbook, the macro editor comes back.docs/macros.md: describes the behaviour under Step editor.No new UI strings are added, because the existing Save Changes? prompt is reused.
Testing
crates/ui-egui/tests/macro_editor.rs:closing_the_workbook_asks_about_an_unsaved_draft_and_cancel_brings_the_editor_back: a dirty draft makes closing a clean workbook ask; Cancel restores the editor with its draft; Don't Save closes the editor and leaves no macro behind.save_in_the_prompt_saves_the_draft_into_the_workbook_file: Save puts the macro into the saved file, which is then reopened and checked.an_untouched_new_macro_does_not_make_closing_ask: the workbook closes without a prompt, and the editor closes with it.cargo fmt --check,cargo clippy --workspace --all-targets -- -D warnings, andcargo xtaskassets, layers, parity, keytips and wasm all pass.cargo test --workspace --no-fail-fastpasses except for three tests that already fail onmain: the French translation uniqueness test and two CSV separator tests. Open pull requests CSV/text export: General numbers without exponents (integers below 1e16 exact);&switches to scientific past 21 characters #449 and fix(i18n): correct scattered wrong French translations and add 2 missing keys #454 fix them.Generative AI use: The code and descriptions in this pull request were written with the assistance of Claude Opus 5.5 (Claude Code). I directed the work, chose its scope, and reviewed and tested the changes.