mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-26 03:17:28 -06:00
Merge pull request #4215 from aiongg/batch-heads
Merge the latest commits of #4100, #4133 and #4171
This commit is contained in:
+3
-1
File diff suppressed because one or more lines are too long
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,6 +152,7 @@ impl AutoScroll {
|
||||
Stroke::NONE,
|
||||
));
|
||||
}
|
||||
// No pointer position (outside the window) counts as the dead zone: `update` cancels the gesture then.
|
||||
let displacement = ui.ctx().input(|i| i.pointer.hover_pos()).map_or(0.0, |p| p.y - anchor.y);
|
||||
ui.ctx().set_cursor_icon(cursor_icon(displacement));
|
||||
}
|
||||
|
||||
+2
-1
File diff suppressed because one or more lines are too long
@@ -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 |
|
||||
|
||||
Reference in New Issue
Block a user