Merge PR #4133: #4085: toggle the last highlight colour with the keyboard shortcut

This commit is contained in:
aiongg
2026-10-24 22:35:22 +00:00
5 changed files with 145 additions and 6 deletions
+3 -1
View File
File diff suppressed because one or more lines are too long
+8 -1
View File
@@ -401,7 +401,7 @@ fn target_has_highlight(s: &Session, h: Highlight) -> bool {
if let Some(rows) = column_rows(s) {
for (a, b) in rows {
let Some(p) = s.doc.para_at(&a) else { continue };
if a == b {
if a == b || (a.off == 0 && b.off == p.len()) {
seen = true;
if !matches(p.props.style.as_deref(), &p.mark) {
return false;
@@ -425,6 +425,13 @@ fn target_has_highlight(s: &Session, h: Highlight) -> bool {
let Some(p) = s.doc.para(a.story, &path) else { continue };
let from = if path == a.path { a.off } else { 0 };
let to = if path == b.path { b.off } else { p.len() };
// Match format_range: fully selected paragraphs include their mark.
if to == p.len() && (from == 0 || path != a.path) {
seen = true;
if !matches(p.props.style.as_deref(), &p.mark) {
return false;
}
}
for (r, c) in p.run_ranges() {
if r.end > from && r.start < to {
seen = true;
+124
View File
@@ -2951,6 +2951,7 @@ fn highlight_toggle_checks_past_the_resolved_run_limit() {
} else {
doc.para_mut(StoryRef::Body, &Pos::body(1, 0).path).unwrap().runs[0].props.highlight = Some(Highlight::Yellow);
}
doc.para_mut(StoryRef::Body, &Pos::body(0, 0).path).unwrap().mark.highlight = Some(Highlight::Blue);
let original = doc.clone();
let mut s = Session::new(doc);
s.last_highlight = Highlight::Blue;
@@ -2988,3 +2989,126 @@ fn highlight_preview_preserves_the_session_colour_and_document() {
assert_eq!(s.last_highlight, Highlight::Blue);
assert_eq!(s.undo_depth(), depth);
}
/// Whole-cell column selections include paragraph marks, including empty cells and
/// inherited mark colours; adjacent unselected cells and undo/redo keep their formatting.
#[test]
fn highlight_toggle_checks_whole_cell_marks() {
use wordcraft_doc::props::Highlight;
use wordcraft_doc::{CharProps, Paragraph};
for inherited in [false, true] {
for empty in [false, true] {
let mut s = s();
run(&mut s, "insert.table", json!({"rows":2,"cols":2}));
let (table, _, _) = s.sel.focus.path.cell().unwrap();
let pos = |row, col| {
let mut path = table.clone();
path.0.extend([row, col, 0]);
Pos::new(StoryRef::Body, path, 0)
};
if inherited {
s.doc.styles.default_chr.highlight = Some(Highlight::Yellow);
}
for row in 0..2 {
for col in 0..2 {
let p = s.doc.para_mut(StoryRef::Body, &pos(row, col).path).unwrap();
let colour = if col == 0 { Highlight::Blue } else { Highlight::Red };
*p = Paragraph::with_text(
if empty && row == 1 && col == 0 { "" } else { "AB" },
CharProps { highlight: Some(colour), ..Default::default() },
);
p.mark.highlight = Some(colour);
if row == 1 && col == 0 {
p.mark.highlight = if inherited { None } else { Some(Highlight::Yellow) };
}
}
}
s.last_highlight = Highlight::Blue;
run(&mut s, "caret.set", json!({"pos":pos(0,0)}));
run(&mut s, "table.selectColumn", json!({}));
assert!(!s.cell_range().unwrap().stream);
let original = serde_json::to_value(&s.doc).unwrap();
run(&mut s, "format.highlight", json!({}));
for row in 0..2 {
let p = s.doc.para_at(&pos(row, 0)).unwrap();
assert_eq!(p.mark.highlight, Some(Highlight::Blue), "inherited={inherited}, empty={empty}");
assert!(p.runs.iter().all(|r| r.props.highlight == Some(Highlight::Blue)));
}
run(&mut s, "format.highlight", json!({}));
for row in 0..2 {
let p = s.doc.para_at(&pos(row, 0)).unwrap();
assert_eq!(p.mark.highlight, Some(Highlight::None));
assert!(p.runs.iter().all(|r| r.props.highlight == Some(Highlight::None)));
let other = s.doc.para_at(&pos(row, 1)).unwrap();
assert_eq!(other.mark.highlight, Some(Highlight::Red));
assert!(other.runs.iter().all(|r| r.props.highlight == Some(Highlight::Red)));
}
run(&mut s, "edit.undo", json!({}));
run(&mut s, "edit.undo", json!({}));
assert_eq!(serde_json::to_value(&s.doc).unwrap(), original);
run(&mut s, "edit.redo", json!({}));
assert_eq!(s.doc.para_at(&pos(1, 0)).unwrap().mark.highlight, Some(Highlight::Blue));
}
}
}
/// Fully selected marks participate even when the selection has no text runs or an
/// empty paragraph between blue runs; repeated toggles and undo preserve these targets.
#[test]
fn highlight_toggle_checks_ordinary_paragraph_marks() {
use wordcraft_doc::props::Highlight;
for text in ["AB", "AB\n\nAB", "\n"] {
let mut doc = wordcraft_doc::Document::from_text(text);
doc.styles.default_chr.highlight = Some(Highlight::Blue);
let paths = doc.para_paths(StoryRef::Body);
for path in &paths {
let p = doc.para_mut(StoryRef::Body, path).unwrap();
p.mark.highlight = Some(Highlight::Blue);
for r in &mut p.runs {
r.props.highlight = Some(Highlight::Blue);
}
}
let mismatch = if text == "AB\n\nAB" { 1 } else { 0 };
doc.para_mut(StoryRef::Body, &paths[mismatch]).unwrap().mark.highlight = Some(Highlight::Yellow);
let original = serde_json::to_value(&doc).unwrap();
let mut s = Session::new(doc);
s.last_highlight = Highlight::Blue;
run(&mut s, "select.all", json!({}));
run(&mut s, "format.highlight", json!({}));
for path in &paths {
let p = s.doc.para(StoryRef::Body, path).unwrap();
assert_eq!(p.mark.highlight, Some(Highlight::Blue), "text={text:?}");
assert!(p.runs.iter().all(|r| r.props.highlight == Some(Highlight::Blue)));
}
run(&mut s, "format.highlight", json!({}));
assert!(paths.iter().all(|p| s.doc.para(StoryRef::Body, p).unwrap().mark.highlight == Some(Highlight::None)));
run(&mut s, "edit.undo", json!({}));
run(&mut s, "edit.undo", json!({}));
assert_eq!(serde_json::to_value(&s.doc).unwrap(), original);
}
}
/// A partial text column excludes paragraph marks and text beyond its selected offsets,
/// so a different mark colour must not prevent clearing the selected blue characters.
#[test]
fn highlight_partial_column_ignores_unselected_marks() {
use wordcraft_doc::props::Highlight;
let mut doc = wordcraft_doc::Document::from_text("AB\nAB");
for path in doc.para_paths(StoryRef::Body) {
let p = doc.para_mut(StoryRef::Body, &path).unwrap();
p.mark.highlight = Some(Highlight::Yellow);
for r in &mut p.runs {
r.props.highlight = Some(Highlight::Blue);
}
}
let mut s = Session::new(doc);
s.last_highlight = Highlight::Blue;
run(&mut s, "select.column", json!({"anchor":Pos::body(0,0),"focus":Pos::body(1,1)}));
run(&mut s, "format.highlight", json!({}));
for row in 0..2 {
let p = s.doc.para_at(&Pos::body(row, 0)).unwrap();
assert_eq!(p.props_of_char(0).highlight, Some(Highlight::None));
assert_eq!(p.props_of_char(1).highlight, Some(Highlight::Blue));
assert_eq!(p.mark.highlight, Some(Highlight::Yellow));
}
}
+2 -1
View File
File diff suppressed because one or more lines are too long
+8 -3
View File
@@ -1,6 +1,6 @@
# Typing parity with Word
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-25 · **Change:** trivial (full-target highlight check beyond 10,000 runs, #4085; previously trivial: AutoCorrect finds the word after a no-break space or an ideographic space; previously trivial: a command that changes nothing is not an edit: no undo step, no unsaved changes, no relayout, and the redo stack survives it; previously trivial: while an input method composes, the caret and its candidate window sit at the input method's cursor inside the composition; previously trivial: Ctrl+Tab types a tab in a table cell, #3137; previously trivial: List AutoFormat triggers in front of an already-written paragraph, not only a fresh empty one, checked in Word for Windows, #3352; previously trivial: AltGr types its characters without opening KeyTips on Linux, #2388; previously trivial: Delete on whole selected cells leaves one empty paragraph in each, with the cell's last paragraph's formatting; nested tables go too, #2802; previously trivial: Bullets and Numbering toggle off style-inherited lists at their effective level, including basedOn chains and numbered ListParagraph styles; other paragraphs and list definitions stay unchanged (related to #3459); previously trivial: a text box that keeps the keyboard no longer resets the input method every frame, #2986; previously trivial: a slash opening a word travels whole to the next line, #2963; previously trivial: typing over a selection takes the first selected character's formatting, also when the selection was made right to left, #2879; previously trivial: Turkish and Azerbaijani case mapping in AutoCorrect, #1923; previously trivial: smart quotes in Hebrew pinned: logical order, ” in acronyms as in Word, #1670; previously trivial: Backspace on a selected table, rows or columns deletes them, Delete clears them, #1844; previously trivial: typed text takes its direction from its own letters, #1543; previously trivial: an input method's request to erase text around the caret is honoured, so web typing in Cyrillic and other non-Latin text no longer repeats, #1480; previously trivial: `-->` AutoCorrect replacement, #2223; previously trivial: a heading-list preset keeps Heading styles and makes other paragraphs Heading 1, as in Word, #1793; previously trivial: numbered headings: a heading-linked Multilevel List numbers new headings by their style; numbering keeps a heading a heading, #1793; previously trivial: reversing an automatic capital adds an exception, #1857; previously trivial: smart quotes open after guillemets and low quotes and nest; previously trivial: Enter on an empty list item numbered by its paragraph style moves it up a level, #2143; previously trivial: Backspace after a section break and Delete before it remove the break, also on the last paragraph, #2127; previously trivial: a table inserted in right-to-left text is right to left, so Tab moves left, #2634; previously trivial: Shift+Delete cuts the selection on every platform, #1669; previously trivial: Select All then Delete removes a table the document starts with, #1198; previously trivial: Alt+numpad character codes, #961; previously trivial: Down and Up from a line start beside a table go into it, #1007; previously trivial: sample-text formulas on Enter, #1067; previously 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
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-25 · **Change:** trivial (highlight target includes selected paragraph marks, #4085; previously trivial: full-target highlight check beyond 10,000 runs, #4085; previously trivial: AutoCorrect finds the word after a no-break space or an ideographic space; previously trivial: a command that changes nothing is not an edit: no undo step, no unsaved changes, no relayout, and the redo stack survives it; previously trivial: while an input method composes, the caret and its candidate window sit at the input method's cursor inside the composition; previously trivial: Ctrl+Tab types a tab in a table cell, #3137; previously trivial: List AutoFormat triggers in front of an already-written paragraph, not only a fresh empty one, checked in Word for Windows, #3352; previously trivial: AltGr types its characters without opening KeyTips on Linux, #2388; previously trivial: Delete on whole selected cells leaves one empty paragraph in each, with the cell's last paragraph's formatting; nested tables go too, #2802; previously trivial: Bullets and Numbering toggle off style-inherited lists at their effective level, including basedOn chains and numbered ListParagraph styles; other paragraphs and list definitions stay unchanged (related to #3459); previously trivial: a text box that keeps the keyboard no longer resets the input method every frame, #2986; previously trivial: a slash opening a word travels whole to the next line, #2963; previously trivial: typing over a selection takes the first selected character's formatting, also when the selection was made right to left, #2879; previously trivial: Turkish and Azerbaijani case mapping in AutoCorrect, #1923; previously trivial: smart quotes in Hebrew pinned: logical order, ” in acronyms as in Word, #1670; previously trivial: Backspace on a selected table, rows or columns deletes them, Delete clears them, #1844; previously trivial: typed text takes its direction from its own letters, #1543; previously trivial: an input method's request to erase text around the caret is honoured, so web typing in Cyrillic and other non-Latin text no longer repeats, #1480; previously trivial: `-->` AutoCorrect replacement, #2223; previously trivial: a heading-list preset keeps Heading styles and makes other paragraphs Heading 1, as in Word, #1793; previously trivial: numbered headings: a heading-linked Multilevel List numbers new headings by their style; numbering keeps a heading a heading, #1793; previously trivial: reversing an automatic capital adds an exception, #1857; previously trivial: smart quotes open after guillemets and low quotes and nest; previously trivial: Enter on an empty list item numbered by its paragraph style moves it up a level, #2143; previously trivial: Backspace after a section break and Delete before it remove the break, also on the last paragraph, #2127; previously trivial: a table inserted in right-to-left text is right to left, so Tab moves left, #2634; previously trivial: Shift+Delete cuts the selection on every platform, #1669; previously trivial: Select All then Delete removes a table the document starts with, #1198; previously trivial: Alt+numpad character codes, #961; previously trivial: Down and Up from a line start beside a table go into it, #1007; previously trivial: sample-text formulas on Enter, #1067; previously 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
@@ -17,13 +17,17 @@ Choosing another colour applies it, and choosing No Color makes that the current
Explicit `format.highlight {"color": ...}` remains an idempotent setter; omitting `color`
uses the toggle. Ribbon and mini-toolbar colour buttons remain setters, not toggles.
The highlight decision checks the complete target, including mixed colours after 10,000 runs;
formatting-state queries retain their existing sampling limit. Palette state is outside document undo; the formatting itself is undoable.
formatting-state queries retain their existing sampling limit. Fully selected paragraph marks
(including whole cells and empty paragraphs) participate in that decision; partial text
columns ignore marks outside the selected range. Palette state is outside document undo; the formatting itself is undoable.
The existing selection/word/pending-caret targeting is unchanged (the separate request #4086
is not addressed).
Regressions: `highlight_shortcut_toggles_the_last_colour` and
`highlight_toggle_handles_mixed_inherited_and_pending_formatting`. Synthetic command/key
qualification; native Word comparison has not been performed for this change.
qualification, with additional large-selection, non-mutating preview, whole-cell mark,
empty-paragraph mark and partial-column controls. Native Word comparison has not been
performed for this change.
## Sample text (#1067)
@@ -396,6 +400,7 @@ again.
| Date | Change | Summary |
|---|---|---|
| 2026-10-24 | trivial | AutoCorrect: a word typed after a no-break space or an ideographic space is corrected as after a plain space |
| 2026-10-25 | trivial | Highlight decisions include fully selected paragraph marks in whole cells and ordinary ranges, including empty paragraphs; partial text columns exclude unselected marks (#4085). |
| 2026-10-25 | trivial | Highlight toggle checks the full target beyond 10,000 runs without collecting resolved formatting; mixed and inherited tails and Undo/Redo covered (#4085). |
| 2026-10-24 | trivial | Highlight shortcut toggles the last chosen colour off when the whole selection has it; explicit colour commands remain setters (#4085). |
| 2026-10-22 | trivial | Input methods: while composing, the caret and the candidate window follow the input method's cursor inside the marked text |