mirror of
https://github.com/aphrody-labs/wordcraft.git
synced 2026-10-13 19:26:53 +00:00
Track Changes: Accept All no longer joins an extra paragraph after a tracked delete from a paragraph end (#864) (#914)
* Track Changes: emptying a paragraph no longer turns its mark into the text's revision (#864) Paragraph::delete copies the formatting of what it deletes onto the paragraph mark when it empties the paragraph. That copy included the revision ids, so accepting a tracked deletion of a whole paragraph text made the mark a deleted mark too, and Accept All joined the following paragraph. Rejecting typed text in an empty paragraph likewise rejected the paragraph mark. The mark now keeps its own ins, del and fmt_change. * Credit Silvertree2010 in the About window * Docs: update the progress docs for this change * Docs: Last reviewed stays as it was --------- Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
1 parent
fbacffd282
commit
159b84f54c
5 files changed
+82
-4
No files matched your search
@@ -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 |
|
||||
|
||||
@@ -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]
|
||||
+24
-1
@@ -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());
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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) |
|
||||
|
||||
Reference in new issue
Block a user