Selecting one cell (Table Layout > Select > Select Cell, or a click along
its left edge) and dragging its left or right border moved the border in
the whole column: one selected cell wasn't told apart from text selected
inside it. `ResizeTarget::limit_to_selection` now takes the selected cells
from `Session::cell_range` (#637) when they're in the dragged table, so one
selected cell's border moves in its row only, as in Word, like the border
of cells selected across (#720). Text selected within a cell still moves
the whole column.
Tests: engine `a_selected_cells_border_moves_only_in_its_row`; kittest
`a_selected_cells_border_drags_in_its_row_only` (a click along the cell's
edge, then a real drag). Progress docs: ui-parity, ROADMAP.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
The model's paragraph walk (each_para / each_para_mut) stopped 16 tables
down, while a .docx opens with tables nested up to 24 deep and pasting can
nest them further. Text box pruning, which runs after every edit, took a
box shown only from deeper for one shown nowhere and dropped its story,
so an unrelated edit lost the box's text. The same walk decides which
boxes a copy takes along and which pasted shapes get stories of their
own, and serves reading order, the next drawing id and note conversion.
The walk now keeps its own stack instead of recursing, so it reaches
every paragraph however deep tables nest, in the same document order and
with no call stack to run out of; a paragraph on its own still needs no
allocation. The signature is unchanged (the depth argument is now
unused), so every caller builds as before.
Tests: a box 17 tables down in an opened .docx survives an unrelated
edit and a save, while a box shown nowhere still goes; boxes 17 and 40
tables down, in a box and in a header are kept, counted in reading order,
and new drawing ids go above the deep shape's; a copied deep table
pastes into another document showing its own box's text.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The Word XML Document (Flat OPC) reader counted only the parts it kept:
a repeated or nameless part, a child of the package that isn't a part
and anything nested inside one were read without being counted, nesting
had no cap, and a part's inline XML was copied before its size was
checked. Every child of the package now counts against the part limit
and every part's data against the part and package size limits, kept or
not; nesting and the elements of each child are capped as the XML tree
reader caps a part's; and data is checked before it is copied.
Base64 in pkg:binaryData was taken from plain text only, so CDATA and
character references were dropped and a picture written that way lost
bytes. Both now read as the same text, and an entity there is an
invalid base64 error.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The story tables of footnotes and endnotes (PlcffndTxt, PlcfendTxt),
headers and footers (Plcfhdd) and text boxes (PlcftxbxTxt) were read
one range at a time, and each range only had to run forwards: a table
stepping back to an earlier CP after every story pointed many notes,
header slots or text boxes at the same text, and each was read into a
part of its own (2,000 note references on one story made 1,001 copies
of it). The CPs of a well-formed PLC ascend, so a story that starts
before the end of the last one kept is now dropped (a header or footer
slot is left empty, which links it to the previous section's): every
subdocument CP becomes at most one story, and the stories together
never hold more than the file's text.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
New parts, lists and comments took "largest id + 1": after an id of
u32::MAX (a .json model or a .docx can carry one) that overflowed, a
panic in debug builds and a wrap to 0 in release, and list.define's
saturating_add reused the taken id. free_id gives one past the largest,
or the lowest free id once that would overflow; try_add_part,
try_add_list and try_add_levels report ids running out and add nothing,
and the commands that add parts and lists use them.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* #1336: Markdown looks up the backtick run closing a code span instead of rescanning the text
Code spans and link brackets searched the rest of the text for the run
of backticks closing a code span. Every run nothing closes (one per
length) cost a scan to the end, and the bracket scan repeated those
searches for each `[` over them, outside its look-ahead budget: 2 MB of
such runs took 2-18 s. The runs are now indexed by length once per text
and found with a binary search; what reads as a code span or a link is
unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #1452: Markdown brackets no longer turn later links in the paragraph into plain text
The link and image bracket scans share a look-ahead budget per text, so
hostile input stays linear, but it was charged more than they read: a
`[` nothing closes took the whole budget, and every bracket pair and
every link took the rest of the text, as if a destination had been read
to the end. An interval like `[0, 1)`, a dozen references in a long
paragraph or ten links were enough to make every later link plain text.
A scan now pays for the bytes it reads and records the pairs it passes,
and the brackets still open at the end of the text, so later brackets
are looked up. A destination is read, and paid for, only when `(`
follows the `]`; one that parses costs its own length.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The index was a u16 clamped to u16::MAX while the style was still
appended, so every distinct format past the 65,536th in a paragraph
drew with the last one that fit: its font, size, link and tracked-change
marking. It is now a u32, like the glyph ranges beside it.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The attribution check spawned `git ls-files` but never looked at its
exit status: outside a usable checkout, or on a git error, the empty
output read as "no files" and the check passed with nothing checked.
A non-zero status is now an error with git's message.
The list is read NUL-separated (`-z`): without it git quotes a name
with non-ASCII letters ("assets/\303\251.png"), which matched no file
on disk and was silently left out of the check.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Over differently formatted paragraphs the dialog showed the caret
paragraph's values and OK gave them to every selected paragraph, through
para.rtl/ltr, para.align and para.set (three undo steps). It now reads
every selected paragraph: a field they share shows as before, a field
they differ in shows blank (number boxes empty, alignment empty, check
boxes neither on nor off, no direction picked) and OK leaves it as each
paragraph has it until the user sets it. OK sends only the fields that
changed, as a single para.set.
para.set takes "onPage": alignment and indents as seen on the page,
which each paragraph turns into its own start and end once `props` is
applied, so mixed directions get the right sides in one command. The
line spacing and space boxes no longer pull an out-of-range value from
the document (line spacing 6) into their range, which counted as an
edit.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
File Explorer's New › Microsoft Word Document makes a zero-byte .docx.
file.open read it as a Word package, failed, and left the untitled
document in place, so Save asked for a name. An empty file (from disk or
as bytes) now opens as the Normal-template blank document File › New
gives (region's paper, editing language) and keeps its path.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Insert › Bookmark: Delete and Go To, laid out as Word's (#1236)
The Bookmark dialog had only Add: a bookmark could not be deleted, and a
click in its list jumped to the bookmark at once. It is now laid out as
Word's (observed in Word 2019 on Windows): the name box over the list, Add,
Delete and Go To beside them, Sort by Name or Location and Hidden bookmarks
below. A click in the list picks a name; Add (Enter) adds and closes; Delete
and Go To leave the dialog open, and after a delete Cancel reads Close. The
dialog opens on the bookmark around the caret, and Add is off for a name
the command would refuse.
insert.deleteBookmark {name} removes a bookmark's start and end wherever
they are, also across paragraphs, as one undo step, keeping the selection
on its text; from the command search without a name it opens the dialog.
Adding a bookmark under a name in use goes through the same removal: it
looked for the old end only in the start's paragraph, so a bookmark across
paragraphs left a stray end behind. edit.goto with a bookmark selects its
text, as Word's Go To does, instead of placing the caret at its start.
Closes#1236
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Insert › Bookmark: Ctrl+click on a link to a bookmark keeps a caret (#1236)
Word's Go To selects a bookmark's text, but Ctrl+click on a link to a
bookmark only puts the caret at the bookmark's start (checked in Word 2019
on Windows). Following a link runs edit.goto, which now selects, so it
collapses the selection to its start.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Resolve the doc conflict markers left by the previous merge
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
eframe shrinks the start size to the monitor's, but not by the frame, the
title bar or the taskbar, and Windows places a new window away from the
screen's corner. On a laptop screen the first window's bottom, with the end
of the scroll bar, sat under the taskbar or past the screen's edge, so a
drag of the scroll bar stopped where the pointer met the edge. The window
was saved that size and opened so again on every launch.
On the frame that first knows the monitor, a window that doesn't fit with
48 pt to spare on each side (WindowGeometry::fit_on_monitor) gets a smaller
size, never below the minimum, and is centred. A window the system placed
counts its position too; a restored one, maybe on another monitor, only its
size. Maximized, full-screen and minimized windows are left alone. Checked
on Windows with no saved settings: the window now opens wholly on screen.
Closes#1215
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Windows Magnifier keeps the text cursor in view through UI Automation: the
caret's own box has no width, so it takes the box of the character after
the caret, and when that has none either, of the two characters before it.
The canvas described a line's end mark (the paragraph mark, a line break)
with no width, so on the empty line Enter or Shift+Enter makes Magnifier
looked at the end of the line above and stayed there until two more
letters were typed.
A line's end mark now takes the room a selection shows a paragraph mark
with: PARA_MARK_WIDTH (5 pt, now shared with selection_rects) at the line's
visual end (Line::mark_box). Checked live with Windows Magnifier, its view
read with MagGetFullscreenTransform: after Enter and Shift+Enter it moves
to the new line at once.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Down from the start of the line above a table (or Up from the start of the
paragraph after it) skipped the whole table: `DocLayout::vertical` took the
nearest line covering the caret's x, and a cell's lines start a cell margin in
from the table's edge, so the paragraph beyond the table won. Lines between
the caret's paragraph and that line in document order now come first: the
caret goes into the nearest such line's table row, to the cell under it or the
horizontally nearest. Another cell of the caret's own row never counts, so Down
in a table still goes to the cell below. Pinned in `tests_typing.rs` and
`docs/typing-parity.md`.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Tables: AutoFit in a nested table fits within its cell (#971)
A double-click on a nested table's column border let the table widen up to
the page's right margin, far past the cell around it. It now widens only up
to the right edge of that cell's text (a right-to-left table's start margin
on the right); the columns right of the border still keep their widths.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Tables: a selection through a table shades its rows' whole cells (#971)
A selection from text into or across a table takes whole rows (#722), but
only the letters in the cells were highlighted. Every row it takes from start
to end now shades all its cells (an empty one too), as selected cells do, and
the letters in them aren't shaded twice (`table_mouse::selection_rects`, used
by the canvas). A selection that starts and ends in one table keeps its text
highlight, so selecting all the text of a cell doesn't look like selecting the
cell; a nested table in a shaded row shades with it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Typing: Shift+Right at a cell's end selects the cell first (#971)
Shift+Right out of the cell the selection is in selected that cell and the
next one at once. As in Word, the first step out of the cell now takes the
cell alone (its end-of-cell mark) and the next press goes on into the next
cell; in an empty cell the first press selects it. Shift+Left in
right-to-left text, the forward step there, does the same. Pinned in
`tests_typing.rs` and `docs/typing-parity.md`; progress docs for all three
table fixes of #971.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
ODT: every frame inflated its picture entry again, and the byte budget
was checked only after inflating, so frames pointing at an entry that
didn't fit (or at any entry once the budget was used up) inflated it
again each time. Each entry is now inflated at most once, and only when
the size it declares fits in what is left; the bytes inflated and every
later copy are charged, so an entry that inflates past its declared
size uses the budget up and nothing is inflated after it.
.doc: inline pictures remembered only successes, so a picture that
couldn't be read or didn't fit was located, decoded and copied again
for every anchor on it, and an inline shape's BLIP was decoded against
the whole media cap rather than what was left. Failures are remembered
too, bytes are copied and decoded only within what is left of the cap,
and nothing is read once it is used up.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>