diff --git a/ROADMAP.md b/ROADMAP.md index 1dce7f8..c331514 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -176,6 +176,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm | 2026-10-11 | Hide White Space (#645): `view.hideWhiteSpace` (View › Show checkbox, or double-click the gap between pages) collapses margins, headers and footers between pages to a thin line in Print Layout; screen only, page count unchanged | | 2026-10-11 | Fonts: Palatino Linotype and Book Antiqua fall back to Palatino (macOS) or TeX Gyre Pagella / P052 / URW Palladio (Linux) before generic serif (#670) | |---|---| +| 2026-10-12 | Track Changes: Accept All after a tracked deletion from the end of one paragraph into a later one no longer joins the paragraph that follows the range, and Reject All after typing into an empty paragraph keeps that paragraph (#864) | | 2026-10-11 | Table border drags like Word's (#711): Ctrl shares a column border's change among the columns right of it (the table keeps its width), the table's left edge moves a left-aligned table's indent (a centred or right-aligned table keeps its alignment, as in Word), a border of cells selected across moves only in their rows, and Alt shows the column widths on the ruler | | 2026-10-11 | Tables on the rulers (#700): with the caret in a table the horizontal ruler marks its row's column borders and the vertical ruler its rows' bottoms; dragging a marker resizes the column (the columns right of it move along; with Shift the neighbour makes up the difference) or the row, one undo step, Escape cancels; in a cell the indents and tabs are measured from the cell | | 2026-10-12 | Save Changes prompt answers from the keyboard: Save has focus when it opens; arrow keys and Tab move between Save, Don't Save and Cancel; Enter answers (#861) | @@ -272,6 +273,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm | Date | Change | Summary | |---|---|---| +| 2026-10-12 | trivial | Progress log: Accept All and Reject All keep the right paragraph marks (#864) | | 2026-10-11 | trivial | Progress log: Ctrl, Alt, left-edge and selected-cells table border drags (#711) | | 2026-10-11 | trivial | Progress log: table markers on the rulers (#700) | | 2026-10-11 | trivial | Progress log: the table context menu | diff --git a/contributors/people.toml b/contributors/people.toml index bd0aa39..072e5a8 100644 --- a/contributors/people.toml +++ b/contributors/people.toml @@ -64,9 +64,11 @@ sync_github_name = false [people.alexanderalber] real_name = "Alexander Alber" [people.edurbs] + +[people.Silvertree2010] +display_name = "Netter" +sync_github_name = false [people.manucarcor] real_name = "Manuel Carmona" display_name = "manucarcor" -sync_github_name = false - [people.odedigo] diff --git a/crates/doc/src/para.rs b/crates/doc/src/para.rs index b1b0692..5f936b9 100644 --- a/crates/doc/src/para.rs +++ b/crates/doc/src/para.rs @@ -1064,7 +1064,12 @@ impl Paragraph { if self.runs.is_empty() || a == 0 { // Keep the formatting of what's deleted for the mark when emptying. if b == self.text.len() { - self.mark = self.props_of_char(a).clone(); + // Formatting only: the mark is a tracked change of its own or it is not, whatever + // revision the deleted text was part of. + let revisions = (self.mark.ins, self.mark.del, self.mark.fmt_change.clone()); + let mut mark = self.props_of_char(a).clone(); + (mark.ins, mark.del, mark.fmt_change) = revisions; + self.mark = mark; } } let k0 = self.objects_before(a); @@ -1436,6 +1441,24 @@ mod tests { assert_eq!(t.mark.bold, Some(true)); } + #[test] + fn emptying_a_paragraph_keeps_the_formatting_but_not_the_revisions_of_its_text() { + let mut props = bold(); + props.ins = Some(3); + props.del = Some(4); + let mut p = Paragraph::with_text("Hello", props); + p.mark = bold(); + p.delete(0, 5).unwrap(); + assert!(p.is_empty()); + assert_eq!(p.mark.bold, Some(true)); + assert_eq!((p.mark.ins, p.mark.del), (None, None)); + // A mark that is itself a tracked change keeps its own revision. + let mut p = Paragraph::with_text("Hello", bold()); + p.mark.del = Some(7); + p.delete(0, 5).unwrap(); + assert_eq!((p.mark.ins, p.mark.del), (None, Some(7))); + } + #[test] fn words() { let p = Paragraph::with_text("The quick brown fox.", CharProps::default()); diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 3ce8a19..6ed7b0d 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -1449,6 +1449,55 @@ fn accept_all_keeps_a_tracked_paragraph_break() { assert_eq!(run(&mut s, "review.changes", json!({})), json!([])); } +/// Issue #864: five paragraphs, a tracked deletion from the end of the first to the end of the +/// third. The two breaks inside the range and the text of the second and third paragraphs are +/// deleted; the third paragraph's own break is not. +fn tracked_delete_from_paragraph_end() -> Session { + let mut s = s(); + for (i, word) in ["Alpha", "Bravo", "Charlie", "Delta", "Echo"].iter().enumerate() { + if i > 0 { + run(&mut s, "text.newParagraph", json!({})); + } + run(&mut s, "text.insert", json!({"text": word})); + } + run(&mut s, "review.trackChanges", json!({"value": true})); + let range = json!({"anchor": {"path": [0], "off": 5, "story": "body"}, "focus": {"path": [2], "off": 7, "story": "body"}}); + run(&mut s, "select.range", range); + run(&mut s, "text.delete", json!({})); + s +} + +#[test] +fn accept_all_after_a_tracked_delete_from_a_paragraph_end_keeps_the_next_paragraph() { + let mut s = tracked_delete_from_paragraph_end(); + assert_eq!(body_texts(&s), ["Alpha", "Bravo", "Charlie", "Delta", "Echo"], "tracked deletions stay in the text"); + run(&mut s, "review.acceptAll", json!({})); + assert_eq!(body_texts(&s), ["Alpha", "Delta", "Echo"]); + assert!(s.doc.revisions.is_empty()); + assert!(s.doc.body.iter().filter_map(|b| b.as_para()).all(|p| p.mark.ins.is_none() && p.mark.del.is_none())); + // Rejecting instead restores everything, as it already did. + let mut s = tracked_delete_from_paragraph_end(); + run(&mut s, "review.rejectAll", json!({})); + assert_eq!(body_texts(&s), ["Alpha", "Bravo", "Charlie", "Delta", "Echo"]); +} + +#[test] +fn reject_all_after_typing_into_an_empty_paragraph_keeps_the_paragraph() { + // Same cause as #864 seen from the other side: rejecting the typed text emptied the + // paragraph, and its mark picked up the text's insertion, so it was rejected as well. + let mut s = s(); + run(&mut s, "text.insert", json!({"text": "One"})); + run(&mut s, "text.newParagraph", json!({})); + run(&mut s, "text.newParagraph", json!({})); + run(&mut s, "text.insert", json!({"text": "Three"})); + run(&mut s, "review.trackChanges", json!({"value": true})); + run(&mut s, "caret.set", json!({"pos": {"block": 1, "off": 0}})); + run(&mut s, "text.insert", json!({"text": "Two"})); + assert_eq!(body_texts(&s), ["One", "Two", "Three"]); + run(&mut s, "review.rejectAll", json!({})); + assert_eq!(body_texts(&s), ["One", "", "Three"]); +} + #[test] fn reject_one_paragraph_break_and_rejecting_a_typed_paragraph() { // Reject with the caret on the break (Next Change selects it), or at the end of its paragraph. diff --git a/docs/typing-parity.md b/docs/typing-parity.md index edced93..07b3e77 100644 --- a/docs/typing-parity.md +++ b/docs/typing-parity.md @@ -1,6 +1,6 @@ # Typing parity with Word -> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-12 · **Change:** minor (Correct TWo INitial CApitals, cAPS LOCK and table-cell capitals, #544; previously minor: right-to-left letters typed into an empty paragraph make it right-to-left, #713; previously trivial: grammar proofing: ellipses are not sentence boundaries; previously minor: AutoCorrect's English replacement list applies only to English text, #835; previously trivial: complex-script caret and Backspace, #604; Move Paragraph Up/Down, Promote/Demote, Go Back and Alt+X keys, #343; previously trivial: outline keys in Outline view, #493) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 +> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-12 · **Change:** trivial (Track Changes: Accept All after a tracked delete from a paragraph end keeps the next paragraph; Reject All after typing into an empty paragraph keeps it (#864); previously minor: Correct TWo INitial CApitals, cAPS LOCK and table-cell capitals, #544; previously trivial: complex-script caret and Backspace, #604; Move Paragraph Up/Down, Promote/Demote, Go Back and Alt+X keys, #343) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 What Word does when you type, observed black-box (same keystrokes typed into Word and WordCraft, results read back paragraph by paragraph) and pinned by tests in @@ -40,6 +40,7 @@ overall picture is in [`target-app-parity.md`](target-app-parity.md). | Enter (tracking on) | the new paragraph mark is a tracked insertion; Reject All joins the paragraphs again, Accept All keeps them | `tracked_enter_and_backspace_track_the_paragraph_mark`, `tests::reject_all_removes_a_tracked_paragraph_break` | | Paste of several paragraphs (tracking on) | every paragraph mark the paste adds is a tracked insertion; Reject All restores the one paragraph, Accept All keeps the pasted ones | `reject_all_takes_out_the_paragraph_breaks_a_tracked_paste_inserted` | | 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` | ## Editing keys @@ -167,6 +168,7 @@ Word. | Date | Change | Summary | |---|---|---| +| 2026-10-12 | trivial | Track Changes: Accept All after a tracked delete from a paragraph end keeps the next paragraph; Reject All after typing into an empty paragraph keeps it (#864) | | 2026-10-12 | minor | AutoCorrect: Correct TWo INitial CApitals, accidental cAPS LOCK, capitalize first letter of table cells (#544) | | 2026-10-11 | minor | Text direction: right-to-left letters typed into an empty paragraph make it right-to-left; existing paragraphs keep theirs (#713) | | 2026-10-12 | trivial | AutoCorrect: untagged text follows the editing language; English decided as proofing does (#835 review) |