* engine: bound Advanced Filter, Remove Duplicates, AutoFilter, Circle Invalid Data; one-pass AutoFit; saturating range dimensions
* address review: AutoFit scans each run of adjacent selected columns separately
* address review: report a truncated circle-invalid scan, no spill scan for plain cells in Replace, one pass for many AutoFit runs
* address review: warn in the app when Circle Invalid Data stopped early; Replace test requires success
* address review: the scan note joins an open-time notice; say cell visits
* M12.AR4: preserve Arabic logical text in bidi display and editing
* M12.AR4: drop the stale "no bidi" note from the RTL sheet status
* M12.AR4: open the Arabic editors and place the caret through key and pointer events in tests
* M12.AR4: skip the conditional-format lookup in the formula bar unless a rule sets reading order
* M12.AR4: test that the caret after typed Arabic is drawn at the text's left end (#1070)
* M12.AR4: pass the theme body font in the reading-order style test
* M12.AR4: adapt the Arabic UI tests to egui 0.37 (row direction, editor ids, Ctrl+End to the logical end)
* M12.AR4: read the active sheet through Workbook::active_sheet() in the formula bar
main made `Workbook::active_sheet` an `ActiveSheet` (shared between windows on one
workbook); the formula bar's reading-order lookup takes the index from the accessor.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M12.AR4: keep explicit Left on wrapped right-to-left cells and trailing spaces in the editor
Wrapped cell text lays out every row from the left when the painted alignment is Left, instead of aligning right-to-left rows to their start. The cell and formula bar editor keep trailing whitespace in their layout so a right-to-left entry ending in a space keeps the caret inside the text.
* M12.AR4: keep Right and General on right-to-left sheets aligned right in wrapped cells
* M12.AR4: keep right-aligned Arabic with a marked first letter inside its cell
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
* M12.AR12: estimate Arabic text width from measured glyph widths in AutoFit
* M12.AR12: treat U+0897–089F as marks and U+08E2 as visible; size the honorific ligatures (review)
* M12.AR12: size the remaining honorific ligatures like their siblings (review)
U+FD4C-FD4F (Unicode 14) and U+FDC8-FDCF (FDCF Unicode 14, FDC8-FDCE
Unicode 17) are honorific word ligatures, not letters, but fell into
the 0.48 em letter arm. Give them the 1.6 em of the measured
U+FD40-FD4B honorifics and assert each range's endpoints.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M12.AR12: size Arabic-Indic digits like ASCII digits and the remaining honorific ligatures (review)
Arabic-Indic (U+0660-0669) and Extended Arabic-Indic (U+06F0-06F9)
digits measure 0.53-0.57 em in Noto Sans Arabic, Arial, Tahoma and
Times New Roman, the same as each font's ASCII digits, so they get
the ASCII digit estimate (0.56) instead of the letter estimate.
Arabic punctuation measures 0.17-0.50 em, so it keeps 0.48 rather
than falling back to 1 em.
U+FBC3-FBD2 and U+FD90-FD91 are honorific word ligatures like
U+FD40-FD4F and get the same 1.6 em.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
Main gained #471 (equations) and #2729 (command palette shortcuts). Conflicts in
ROADMAP.md, docs/architecture.md and docs/file-format-parity.md only: header lines
combined, both sides' sections and rows kept.
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.
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.
- scripts: refuse `window.*` steps like file and link steps — activating
another window repointed every later step at a different open document
(finding 17).
- format: on a protected sheet, formatting may not change a cell's
protection flags: the format-cells permission can't drop `locked` and
then authorize content edits the password gates (finding 20).
- sheet.moveColumns: the insert/delete protection checks now carry the
command's own `sheet` parameter, so the sheet modified is the sheet
authorized, not whichever is active (finding 24).
Tests: unlock-via-format refused, cross-sheet move refused, window switch
refused, plus the positive controls. Engine: 462 tests pass; clippy clean.
- The workbook's active sheet is an atomic set in place (Workbook::active_sheet /
set_active_sheet), so a window swap or sheet switch never clones the
Arc<Workbook> or marks it dirty. Selections already live in DocState and the
window views. Save records the sheet of the most recently active window.
- A dialog taken out of its window while it runs goes back to that window when
a command run from it switched windows (dialogs::show, ui.dialog.confirm).
- view.arrangeAll {arrange?: tiled|horizontal|vertical|cascade, activeWorkbook?}
returns the visible windows, active first; the desktop moves and sizes the
native windows over the main window's monitor. New Window and Arrange All
buttons on the View ribbon; Arrange Windows dialog.
- Docs: ui-parity, hardware-parity, gaps #20, target-app-parity, ROADMAP.
The symbol was interpolated with Rust's Debug formatting, which escapes a
control character into literal `\n`-style text. It is now written into the
format's quoted literal directly, and a symbol holding a control character is
refused along with a quote or a backslash, so nothing is escaped behind the
user's back.
Test: `accounting_format_takes_the_symbol_it_is_given` covers `\n` and a tab.
`cargo xtask ci`: 8/8 steps, 0 failures.
`home.accounting` documents `{range?, symbol?}` but ignored the symbol: the
command always applied the `$` format, so a script or MCP client asking for
lempiras (#1893) or any other currency silently got dollars. The symbol is now
part of the code it writes, and a symbol that would break the format string (a
quote or a backslash) is refused instead of being pasted in. The default is
unchanged, character for character.
Tests: `accounting_format_takes_the_symbol_it_is_given` (displayed text for `L`
and the default, and the two refused symbols) and
`the_default_symbol_reproduces_the_accounting_format` (the built code still
equals the named `Accounting` format). `cargo xtask ci`: 8/8 steps, 0 failures.
home.insertRows {"rows":"3"} and home.insertColumns {"cols":"B"}
failed with "not a range": only spans such as 3:3 or B:B parsed. The
same went for deleting, hiding, sizing and grouping rows and columns.
A single row number or column letter now means that one line.
home.formatCells documents {style: {numFmt?, ...}}, but the style patch
was merged with the style's snake_case field names, so numFmt (and
borders.diagUp/diagDown) were silently ignored. camelCase keys in the
patch are now renamed to the style's own; snake_case keys still work.
`{"sheet": null, "cols": "Data!B:B"}` was refused as naming a sheet other than `sheet`,
while `target_sheet` and the cell parameters read a null `sheet` as absent. The prefix now
decides, as it does when `sheet` is left out.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
home.rowHeight and home.columnWidth take pixels at 96 dpi, the model's unit
(header drags and the Row Height dialog send pixels), but their parameter docs
said points. An agent asking for a 30 pt row got `ht="22.5"` in the file, and
the range check accepted 500 while its message said "between 0 and 409 points".
- The params docs say pixels: `height: 20 (pixels) | points: 15`,
`width: 64 (pixels) | chars: 8.43`, and `{width: 64 (pixels)}` for
home.defaultWidth. Pixel values behave as before.
- home.rowHeight takes `points`, Excel's unit (Row Height dialog, `ht` in the
file), as home.columnWidth already takes `chars`.
- Range errors give the limit in the unit passed: 546 pixels (409.5 points),
409.5 points, 1800 pixels, 255 characters. `chars` is checked against 255,
Excel's limit and the widest column a file holds (it went to 256.4 through
the pixel limit).
- The model's size comments say pixels.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
view.freezePanes, home.rowHeight, home.columnWidth, the AutoFit, Hide and
Unhide row/column commands and home.defaultWidth always changed the active
sheet: a `sheet` parameter was silently ignored, so a programmatic caller
laying out a sheet that isn't active changed the wrong one, and `Sheet!`
prefixes on `rows`/`cols` were refused as "not a range".
They now take `sheet` (name or index) like the commands that already use
target_sheet, accept a `Sheet!` prefix on `rows`/`cols` (and on Freeze
Panes' `cell`, which was refused), and refuse a prefix that contradicts
`sheet`. The selection belongs to the active sheet, so another sheet needs
explicit lines or a cell. Freeze Top Row, Freeze First Column and Unfreeze
Panes take `sheet` too. Without `sheet`, nothing changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merge & Center and Merge Cells kept only the upper-left value, but
Merge Across left the other values of each row in place, hidden under
the merge and still counted: with 1 2 / 3 4 in A1:B2, SUM(A1:B2) stayed
10 after Merge Across, where Excel gives 4. Each row's merged area now
keeps only its first value, as the other merges do.
home.rowHeight, home.autofitRowHeight and data.ungroup change row hidden
state without marking the edit structural, so SUBTOTAL(101-111) kept a stale
value. Found by the command-sequence fuzz (seed 3).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Dragging a header boundary inside a selection of whole columns (rows)
sized only the current area, so with B, D and F Ctrl-selected only the
last-selected one changed. The drag now targets every whole-column
(whole-row) area of the selection, as Excel does; lines outside the
selection, and partial-cell areas, keep their size.
home.columnWidth / home.rowHeight accept a comma-separated list of
areas ("B:B,D:F") so the drag stays one command and one undo step.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Audited the grid, ribbon and editor key handling against Microsoft's
published Windows and Mac shortcut lists. New keymap.rs binds the ones
whose feature already exists: Ctrl+Shift+~!@#$%^&_ number formats and
borders, Ctrl+2-5, Ctrl+Shift+: time, Ctrl+' / Ctrl+Shift+" from the cell
above, Ctrl+L, Ctrl+Shift+L, Ctrl+P, Ctrl+Alt+V, Alt+=, Alt+Down,
Alt+Page Down/Up, Ctrl+Shift+Home/End, Shift+Home, the F-key family
(Shift+F2/F3/F9/F10/F11, F5, F7, F12, Ctrl+F1/F3/F4, Alt+F1, Ctrl+Alt+F9)
and the Excel for Mac variants (Cmd+L/E alignment, Control+E Flash Fill,
Cmd+Option borders, Cmd+Shift+K/J/H/R). Number-format keys match the
physical key; AltGr text never triggers a Ctrl+Alt shortcut. Fixes the
command catalog's wrong shortcut labels and documents the audit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>