Track Changes: resolving away an empty paragraph with a section break keeps the next paragraph's properties (#5667) (#5734)

Rejecting an inserted paragraph (or accepting a deleted one) whose mark carries a section break
left it empty; remove_empty_para refused it for the break, so the next paragraph was joined into
it and took its properties (style, numbering, indents). The empty paragraph now goes whole and
its section joins the following one first, as deleting the break does (remove_section_break).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
This commit is contained in:
authored and GitHub committed 2026-11-08 04:51:36 +01:00
1 parent efd476cbc7
commit 33fa77a892
4 files changed
+63 -5

No files matched your search

+2
View File
@@ -351,6 +351,7 @@ Imported Word 97–2003 `.doc`/`.dot` files now ask for a supported DOCX target
| Date | What landed |
| 2026-11-04 | The CLI opens a non-Unicode input filename even after options such as `--page 2` or `--format flatopc`; text option values still require Unicode; `run --file` and `run --script` retain native paths. |
| 2026-11-06 | Accept All and Reject All no longer restyle the paragraph after an empty one carrying a section break (#5667): a Compare that added a section, then Reject All, left `Body` in the removed paragraph's Normal style and indents. The empty paragraph now goes whole and its section joins the following one, as deleting the break does. |
| 2026-11-04 | Pictures keep the brightness, contrast and transparency their file gives them (#1711, #1766): a "Washout" picture watermark (VML `gain`/`blacklevel`) and a washed-out letterhead picture behind the text (`a:lum`) are drawn faded on screen and in PDF instead of at full colour, with the pixel curve measured on Word's own PDF export; `a:alphaModFix` transparency too. Saved back as DrawingML (and as VML `gain`/`blacklevel` in Word 2003 XML, without the transparency). Picture Format › Corrections, Transparency and Color › Washout replace the file's values (other Color modes leave them alone), and Reset Picture discards them all, as in Word; the PDF embeds the corrected pixels once per picture |
|---|---|
| 2026-10-17 | Copies keep their formatting in programs that read RTF rather than HTML: Copy and Cut put RTF beside the text and HTML on the system clipboard on Windows, macOS and Wayland (an X11 copy stays text and HTML, since arboard owns the selection there; a copy of only pictures or shapes carries no HTML or RTF, so pasting it back keeps every detail), and Paste reads another program's RTF on macOS and X11 too, not only on Windows and Wayland |
@@ -869,6 +870,7 @@ MATH glyphs that reference the same construction now share its immutable variant
| 2026-11-06 | trivial | Windows print submissions isolated per helper process; cancelled driver work cannot block a retry (#5710) |
| 2026-11-06 | trivial | Progress log: print cancellation before sending, including without a document, and the Windows repeated-conversion limitation (#5710) |
| 2026-11-06 | trivial | Progress log: the printer list no longer waits on printer drivers (#5710) |
| 2026-11-06 | trivial | Progress log: resolving away an empty paragraph with a section break keeps the next paragraph's properties, #5667; readiness estimates unchanged |
| 2026-11-05 | trivial | PR #4545: eligible cell floats retain in-bounds offsets and touch the outer cell edge only on overflow, ignoring wrap distances for object placement. Front-of-text, turned-cell and outside-cell opt-outs remain. Synthetic geometry controls; native front/behind-text parity remains unqualified. |
| 2026-11-04 | trivial | Progress log: CLI convert format options; readiness estimates unchanged |
| 2026-11-04 | trivial | Progress log: MHT quoted MIME parameters; readiness estimates unchanged |
+14 -5
View File
@@ -54,11 +54,20 @@ fn resolve_para(
// An inserted paragraph mark that is rejected, or a deleted one that is accepted, goes away:
// the next paragraph joins this one (the split is undone). A paragraph left empty goes away
// whole, so the next one keeps its own style and numbering, as in Word. The mark at the end
// of a story or cell can't go, so it just stops being a revision.
if ((del.is_some() && accept) || (ins.is_some() && !accept))
&& (crate::cmd::remove_empty_para(s, story, path)? || crate::cmd::join_next_para(s, story, path)?)
{
return Ok(());
// of a story or cell can't go, so it just stops being a revision. An empty paragraph whose
// mark carries a section break goes whole too: its section joins the following one, as when
// the break is deleted (#5667). Joining instead gave the next paragraph its properties.
if (del.is_some() && accept) || (ins.is_some() && !accept) {
let sectioned = story == StoryRef::Body
&& path.0.len() == 1
&& s.doc.para(story, path).is_some_and(|p| p.is_empty() && p.section.is_some())
&& crate::cmd::has_next_para(s, story, path);
if sectioned {
s.doc.remove_section_break(path.last());
}
if crate::cmd::remove_empty_para(s, story, path)? || crate::cmd::join_next_para(s, story, path)? {
return Ok(());
}
}
let para = s.doc.para_mut(story, path)?;
para.mark.ins = None;
+45
View File
@@ -12787,3 +12787,48 @@ fn page_down_extends_a_selection_that_ends_in_collapsed_outline_text() {
assert!(s.sel.focus.path.0[0] > 6, "Page Down extends the selection a page on: {:?}", s.sel.focus);
assert!(s.layout().caret(&s.sel.focus).is_some(), "to a place the outline shows");
}
/// Issue #5667's document: `Intro`, then an inserted `Appendix` paragraph whose mark carries a
/// section break (Normal, indented), then `Body` in Heading 1. The whole of `Appendix` is
/// tracked as an insertion by `rev`, or as a deletion when `delete`.
fn sectioned_revision(delete: bool) -> Session {
use wordcraft_doc::{Revision, RevisionKind};
let mut s = Session::new(wordcraft_doc::Document::from_text("Intro\nAppendix\nBody"));
run(&mut s, "caret.set", json!({"pos": {"block": 2, "off": 0}}));
run(&mut s, "para.style", json!({"style": "Heading1"}));
let kind = if delete { RevisionKind::Delete } else { RevisionKind::Insert };
s.doc.revisions.push(Revision { kind, author: "Ada".into(), date: String::new(), move_name: None });
s.doc.last_section.margin_left = 72.0;
let mut sect = s.doc.last_section.clone();
sect.margin_left = 144.0;
let p = s.doc.para_mut(StoryRef::Body, &wordcraft_doc::Path(vec![1])).unwrap();
let len = p.len();
p.format(0, len, &|c| if delete { c.del = Some(0) } else { c.ins = Some(0) }).unwrap();
if delete {
p.mark.del = Some(0)
} else {
p.mark.ins = Some(0)
}
p.props.indent_left = Some(36.0);
p.section = Some(Box::new(sect));
s
}
#[test]
fn resolving_away_an_empty_paragraph_with_a_section_break_keeps_the_next_paragraphs_properties() {
// #5667: the resolved paragraph was left empty but kept for its section break, so the next
// paragraph joined it and took its properties (Normal, its indent). It goes whole instead,
// as an empty paragraph without a break does, and its section joins the following one.
for (delete, cmd) in [(false, "review.rejectAll"), (true, "review.acceptAll")] {
let mut s = sectioned_revision(delete);
assert_eq!(s.doc.sections().len(), 2);
run(&mut s, cmd, json!({}));
assert_eq!(body_texts(&s), ["Intro", "Body"], "{cmd}");
let body = s.doc.para(StoryRef::Body, &wordcraft_doc::Path(vec![1])).unwrap();
assert_eq!(body.props.style.as_deref(), Some("Heading1"), "{cmd}: Body keeps its style");
assert_eq!(body.props.indent_left, None, "{cmd}: and not the removed paragraph's indent");
assert!(s.doc.body.iter().filter_map(|b| b.as_para()).all(|p| p.section.is_none()), "{cmd}: the break went with it");
assert_eq!(s.doc.sections().len(), 1, "{cmd}");
assert_eq!(s.doc.last_section.margin_left, 72.0, "{cmd}: the following section's setup stays");
}
}
+2
View File
@@ -114,6 +114,7 @@ formatting of the text before the cursor. Not yet compared side by side in Word.
| Backspace at a paragraph start / Delete at a paragraph end (tracking on) | the paragraph mark is marked deleted (still shown); the caret moves before it (Backspace) or past it (Delete). Accept joins the paragraphs, Reject keeps them. A mark you inserted yourself is just removed | `tracked_enter_and_backspace_track_the_paragraph_mark`, `tests::tracked_backspace_and_delete_mark_a_paragraph_break_deleted` |
| Delete a range from the end of one paragraph into a later one (tracking on) | the paragraph marks and text in the range are tracked deletions. Accept All removes exactly those, so the paragraph after the range stays a paragraph of its own; Reject All restores everything. Typing into an empty paragraph and rejecting leaves that paragraph | `tests::accept_all_after_a_tracked_delete_from_a_paragraph_end_keeps_the_next_paragraph`, `tests::reject_all_after_typing_into_an_empty_paragraph_keeps_the_paragraph` |
| Backspace just after a section break / Delete just before it | the break goes and the two paragraphs join; the text before it takes the following section's page setup (Word's documented behaviour: a break holds the setup of the section before it); headers and footers the following section showed by linking to the previous one stay. Delete at the end of the last paragraph removes a break on its mark too, with no paragraph to join (with Track Changes on it still does nothing there). Undo brings the break back. Breaks read from a .docx and breaks inserted here alike (#2127) | `backspace_and_delete_remove_a_section_break`, `delete_removes_a_section_break_on_the_last_paragraph` |
| Accept All / Reject All of a paragraph left empty, whose mark carries a section break (tracking on, or a Compare result) | the paragraph goes whole and its section joins the following one, as when the break is deleted; the next paragraph keeps its own style and numbering, as for an empty paragraph without a break (#5667) | `resolving_away_an_empty_paragraph_with_a_section_break_keeps_the_next_paragraphs_properties` |
| Select All, then Delete or Backspace | the whole document goes, tables included, also when it starts with a table; one empty paragraph is left | `select_all_then_delete_removes_tables` |
| Backspace / Delete with a whole table selected (Select Table, or every cell dragged over) | Backspace removes the table, like Delete Table (one undo step; with tracking on its text is marked deleted and the structure stays); the caret goes to the paragraph after it. Delete clears the cells and keeps the table (each cell as below). A nested table goes on its own and leaves the cell's paragraph; a table that was the only block leaves one empty paragraph | `backspace_with_the_whole_table_selected_removes_the_table`, `delete_with_the_whole_table_selected_clears_the_cells_and_keeps_the_table`, `backspace_on_a_selected_nested_table_removes_only_it`, `backspace_on_a_selected_table_that_is_the_only_block_leaves_an_empty_paragraph`, `tracked_backspace_on_a_selected_table_marks_its_text_deleted_like_delete_table` |
| Backspace / Delete with a table and the paragraph before or after it selected | both remove the table, whatever end the selection starts in; the text around joins. Any table the selection covers whole goes, at any nesting level | `delete_or_backspace_with_the_table_and_the_paragraph_after_selected_removes_the_table`, `delete_or_backspace_with_the_paragraph_before_and_the_table_selected_removes_the_table` |
@@ -589,6 +590,7 @@ this correction; native Windows interaction and Microsoft Word remain untested.
|---|---|---|
| 2026-11-04 | trivial | Ctrl+K on a selection: a changed Text to display replaces the selected text before it is linked |
| 2026-10-30 | trivial | Equations: `\int` + Space waits for its limits; `_x^y` then Space builds them, Space alone builds the operator without, typing goes into its operand, and Enter or an arrow key builds it without going into the operand (#4917). Review: any character that can't continue `\name` corrects it (`\int_0^1`, `\alpha+`), and `∫∫` takes the operand into the inner operator; the Word rows are now observed in Word for Windows on #4938, Enter, arrows and `∫∫` are not |
| 2026-11-06 | trivial | Track Changes: resolving away an empty paragraph with a section break removes it whole; the next paragraph keeps its style (#5667) |
| 2026-11-03 | trivial | Alt+numpad codes type their character in text boxes too (Find what, dialog fields), not the digits or nothing (#4403) |
| 2026-11-03 | trivial | Find smart-quoted text with a keyboard apostrophe (#5388); exact search modes and replacement spelling preserved. |
| 2026-11-01 | minor | English spelling by dialect from SCOWL 2026: typed British, Canadian and Australian text, modern words and ordinals pinned (#40, #520, #1281, #1045; CP2) |