* #147: wire Picture Format UI for inserted pictures
* #147: fix Picture Format state bugs from review (alt/crop buffers, undo coalescing, pending target, tab fallback)
* #147: review round 2 (cancel pending change on next action, alt media check, drag-only undo join)
---------
Co-authored-by: Tony <gittonyp@gmail.com>
* Find/Replace: skip text deleted under Track Changes
With Track Changes on, Replace marks the match deleted and leaves it in
the paragraph text, then inserts the replacement in front of it. The
search helper matched the raw paragraph text, so the next step found that
same deleted "cat" right after the caret and replaced it again: four
Replaces on "one cat two cat" gave "one dogdogdogdog two cat" and
`remaining` never dropped.
`search` (used by Find, Find Next/Previous, Replace, its `remaining`
count and Replace All) now skips any match that overlaps a run marked
deleted, so deleted text is never found, as in Word. A match that would
run across deleted text is skipped too: that string is not in the
document as it now reads.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Undo: a failed command keeps the redo stack and the oldest undo step
`run` checkpoints a mutating command before running it, and the
checkpoint clears the redo stack (and, once the history is at its limit,
drops the oldest step). When the command then failed, the error path put
back the document, selection and typing group and truncated the history,
but the cleared redo stack stayed cleared. Insert, Undo, then any
command that fails on its parameters (`text.insert {}`,
`para.align {value:"bogus"}`) made the undone work unrecoverable. At the
limit, truncating to the old length also kept the failed command's empty
step and lost the oldest real one.
The checkpoint now hands back what it removed, and the error path
restores both. join_next is deliberately left consumed: the drag that
set it re-arms it every frame, and restoring it would merge the next
unrelated command into the drag's undo step.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Find/Replace: find live text next to a tracked deletion
The previous fix rejected any regex match that overlapped tracked-deleted
text, but `find_iter` had already consumed those positions, so a valid
match overlapping the rejected one was never tried. "aaa" with the first
"a" deleted reads "aa", yet Find "aa" found nothing (raw 0..2 was
rejected and the scan resumed at 2) and Replace All replaced nothing.
Searching the raw text also let deleted characters decide word
boundaries: deleting the space in "cat fish" still matched whole-word
"fish".
`search` now builds each paragraph as it reads, without deleted runs,
and searches each stretch of live text separately with
`regex_automata::Input::range`. A match stays within one stretch, as
before, and `\b`, `^` and `$` see the neighbouring live text, so a
deletion neither creates nor hides a word boundary. `regex::Regex` cannot
bound a search's end without slicing the haystack, which turns the slice
end into a word boundary; regex-automata is the engine `regex` already
uses (0.4.18, already in the lockfile), so no new code is built.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Undo: a failed command restores history changed by nested commands
The previous fix had the checkpoint report what it evicted and put that
back on failure, but it only saw the outer command's own checkpoint.
Commands run nested commands, and a nested one that succeeds checkpoints
too. `file.inspect {"remove":["comments","revisions"]}` in a
comments-only document runs `review.deleteComment` (allowed) and then
`review.acceptAll` (refused). With 499 undo steps, the nested checkpoint
evicted the oldest step and the rollback, truncating to 499, kept the
outer command's empty step instead. At 500, only one of the two
evictions came back. Nested undo, redo and version restore change the
stacks in ways a length cannot describe either.
Undo steps are now `Arc<Undo>`, and `run` snapshots both stacks whole
before a mutating command and puts them back on failure, so the
history is exact whatever ran inside. The snapshot copies one pointer
per step, under 1,000 refcount bumps and an 8 KB allocation at
MAX_UNDO, beside the two document clones every mutating command
already makes. Undo and Redo are not mutating commands, so a top-level
Undo still moves its document out of the step without cloning it. Only
an undo nested inside a mutating command (a macro) clones, because the
snapshot still holds the step. This replaces the eviction-return
plumbing, so `checkpoint` is back to its original form.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Tests: compare undo timelines without comment timestamps
failed_command_restores_history_changed_by_nested_commands builds the
same session twice and compares their documents. Comments are stamped
with the time to the second, so when the clock crossed a second between
the two builds the documents differed and the test failed (about 1 run
in 12). Clear the stamp in the compared copies; the test still checks
every other part of each document in the timeline.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Undo: wrap the typing-parity undo step in Arc after merging main
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>
* M6: close the TOC field in its last entry, not in a text box inside it
`para_inner` took `toc_end_here` only after writing the paragraph's content,
so a nested story in the last TOC entry (a text box) reached `para_inner`
first, took the flag, and wrote the field's end inside `w:txbxContent`.
Take it before the content.
Review follow-up on #52.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M6: Update Table reads a heading above the TOC at its own index
Pass 1 shifted every heading by `entries.len()` to account for the entries
now in the body, but entries go in below the TOC heading, so a heading above
it never moves. Its lookup landed on a TOC entry and took that page (a
Preface on page 1 above a TOC on page 2 showed 2). Shift only headings below
the TOC.
Review follow-up on #52.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M6: start the TOC field in the first entry, as Word does
The writer opened the TOC field at the end of the "Contents" heading.
Word reads that fine, but LibreOffice splits the heading: "Content" as
body text, then "s" as the TOC title (on the next page when the TOC
follows a page break). Word itself writes the heading before the field
and begins the field in the first entry; written that way, LibreOffice
shows the heading whole.
Hold the heading's field back and write its begin/instruction/separate
at the start of the first entry, taken before the paragraph content so
a text box in the heading or an entry can't claim it (as with the end).
The reader moves a TOC field that starts a paragraph right after a TOC
Heading back to the end of the heading, so WordCraft's own files read
back as before and a TOC saved by Word now updates in WordCraft too
(before, Update Table left Word's first entry behind as a duplicate).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
`hostile_params_never_panic` runs every command four times, Read Aloud
included, and on macOS that spawns `say` with the whole sample document:
four overlapping voices for minutes after `cargo test` returns. Skip it
with the other commands that reach outside the session; it ignores its
params, so junk payloads had nothing to exercise there.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Typing parity with Word: lists, AutoFormat and AutoCorrect as you type
Typed side by side with Word (same keystrokes, results read back) and matched it:
- Enter after a list no longer gets stuck: the paragraph that leaves a list goes back to
Normal, and later Enters make blank paragraphs (the `num: 0` marker counted as a list item,
so every Enter re-ran "leave the list" and did nothing; the list indent also stuck to every
following paragraph).
- Enter on an empty nested item moves it up a level before leaving the list.
- Second Backspace at the start of an un-numbered list paragraph returns it to Normal
instead of joining it to the paragraph above.
- "1. " always starts numbering at 1; "a. " and "I. " start lettered/roman lists; "- " makes
a dash bullet.
- Undo right after an AutoFormat/AutoCorrect change undoes only that change.
- AutoCorrect: sentence capitals (with abbreviation/initial exceptions), runs on Enter too,
"word -- word" en dash, "wait..." ellipsis, 1/2 1/4 3/4, ordinal superscripts, web
addresses become links.
- Border lines: Enter after ---, ===, ***, ___, ~~~, ### draws a bottom border.
- Layout: no line break right after a slash ("and/or", URLs), as in Word.
- ui_shot: UI_SHOT_FULL=1 prints whole replies for scripted checks.
Regression tests in crates/engine/src/tests_typing.rs and the layout tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Typing parity: record Word's observed typing rules, two more tests
docs/typing-parity.md lists what Word does for each key sequence (lists, Backspace, Tab,
AutoCorrect, AutoFormat, slash line breaks) with the test that pins it, the known gaps, and how
to compare without taking over the shared machine. CLAUDE.md/AGENTS.md point at it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* M6: Update Table puts each heading on its real page
The second pass removed the entries and laid the document out again before
looking headings up at `path + entries.len()`, so every lookup landed past
its heading: later entries showed the next heading's page and the last fell
back to page 1. Lay out with the entries still in place, then replace them.
A heading typed after a manual page break also starts with the break, so its
page is now read after any leading breaks rather than before them.
The existing test only had a heading on page 1; the new one puts headings on
pages 2, 3 and 4 (before: 4, 1, 1).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M6: save the table of contents as a TOC field Word can update
The writer saved the TOC heading's field empty (begin, instruction, separate
and end all at the end of "Contents"), with the entries as plain paragraphs
after it. Word showed them as ordinary text: no TOC frame, and no Update
Table or Update Field on the entries.
Write the field so its result spans the entries (closed at the end of the
last entry) and wrap heading and entries in the Table of Contents content
control (w:sdt with docPartGallery "Table of Contents", ECMA-376 17.5.2).
The reader already keeps a multi-paragraph field's start as an empty field
in its first paragraph and the rest as text, so WordCraft reads the TOC back
exactly as before; the new round-trip test checks both the file and that.
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 <shared2@storyteller.ai>
apply_style's guard ended with `&& id == "Normal"`, making the
`id.starts_with("Heading") || id == "Title"` terms dead: only Normal
cleared a numbered paragraph's direct numbering, so applying Heading 1/2/3
or Title to a list item left the number behind — contradicting the
comment right above it. Drop the redundant term and cover it with a
regression test.
Also clears the clippy overly_complex_bool_expr error this expression
raised, which was failing `cargo xtask ci`.
Co-authored-by: justinjohn0306 <babyjjbabyjj549@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>