Advanced Find: every match, Reading Highlight, search within a selection (#234)
Home › Editing › Find › Advanced Find (edit.advancedFind) opens the Find
and Replace dialog. With params it returns every match in the body, or
only inside the selection ("in": "selection"), and "highlight": true
turns on Reading Highlight, which marks all matches on the page and
follows edits until it's turned off. The ribbon's Find button becomes a
menu with Find, Advanced Find and Go To; the Find dialog gets a Reading
Highlight toggle. Parity 355/405.
Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
parent
d8f71959d3
commit
9c3ad84d33
|
|
@ -9,7 +9,7 @@ and agent control. This file tracks where we are honestly. Generated numbers com
|
|||
| Measure | Value |
|
||||
|---|---|
|
||||
| Commands (every action, scriptable by CLI/MCP/control channel) | **422** |
|
||||
| Feature catalog coverage (Word ribbon/menu features with a command) | **380 / 430 (88%)** |
|
||||
| Feature catalog coverage (Word ribbon/menu features with a command) | **381 / 430 (89%)** |
|
||||
| **Estimated real feature parity** (depth and fidelity, not just a command) | **~62%** |
|
||||
| **Distance to alpha** | **~85% of the way; ≈20–25 h of Claude wall-clock work** |
|
||||
| **Distance to 100% parity** | **≈120–150 h of Claude Opus 5.5 wall-clock work** (with parallel agents) |
|
||||
|
|
|
|||
|
|
@ -36,6 +36,9 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
.key("Mod+F")
|
||||
.params(r#"{"text": string, "matchCase"?: bool, "wholeWord"?: bool, "regex"?: bool}"#)
|
||||
.pure(),
|
||||
CommandSpec::new("edit.advancedFind", "Advanced Find", "Home › Editing › Find", advanced_find)
|
||||
.params(r#"{"text"?: string, "matchCase"?: bool, "wholeWord"?: bool, "regex"?: bool, "in"?: "main|selection", "highlight"?: bool}"#)
|
||||
.pure(),
|
||||
CommandSpec::new("edit.findNext", "Find Next", "Home › Editing › Find", |s, _| step(s, 1)).key("Mod+G / F3").pure(),
|
||||
CommandSpec::new("edit.findPrevious", "Find Previous", "Home › Editing › Find", |s, _| step(s, -1)).key("Mod+Shift+G").pure(),
|
||||
CommandSpec::new("edit.replace", "Replace", "Home › Editing", replace).key("Mod+H").params(r#"{"text": string, "with": string}"#),
|
||||
|
|
@ -115,7 +118,7 @@ fn paste_text(s: &mut Session, v: &Value) -> CmdResult {
|
|||
/// paragraph without it: a match lies within one stretch of live text, and word
|
||||
/// boundaries and anchors see the neighbouring live text, not the deleted text.
|
||||
/// Otherwise Replace would find its own deleted text again.
|
||||
fn search(s: &Session, story: StoryRef) -> Result<Vec<(Pos, Pos)>, CmdError> {
|
||||
pub(crate) fn search(s: &Session, story: StoryRef) -> Result<Vec<(Pos, Pos)>, CmdError> {
|
||||
use regex_automata::{Input, meta, util::syntax};
|
||||
let f = &s.find;
|
||||
if f.query.is_empty() {
|
||||
|
|
@ -160,6 +163,8 @@ fn search(s: &Session, story: StoryRef) -> Result<Vec<(Pos, Pos)>, CmdError> {
|
|||
}
|
||||
|
||||
fn read_opts(s: &mut Session, v: &Value) {
|
||||
// A new search changes what Reading Highlight marks.
|
||||
s.find.highlights = None;
|
||||
if let Some(t) = p::str(v, "text") {
|
||||
s.find.query = t.to_string();
|
||||
}
|
||||
|
|
@ -199,6 +204,35 @@ fn find(s: &mut Session, v: &Value) -> CmdResult {
|
|||
}))
|
||||
}
|
||||
|
||||
/// Find in the classic dialog: every match in the body or the selection, optionally highlighted while you read.
|
||||
fn advanced_find(s: &mut Session, v: &Value) -> CmdResult {
|
||||
if p::str(v, "text").is_none() && v.get("highlight").is_none() {
|
||||
s.ui_requests.push(json!({"open": "find"}));
|
||||
return Ok(json!({"count": s.find.results.len()}));
|
||||
}
|
||||
read_opts(s, v);
|
||||
let within = match p::str(v, "in").unwrap_or("main") {
|
||||
"main" => None,
|
||||
"selection" if !s.sel.is_collapsed() => Some(s.sel.ordered()),
|
||||
"selection" => return Err(CmdError::Params("`in: selection` needs a selection".into())),
|
||||
other => return Err(CmdError::Params(format!("`in` is main or selection, not `{other}`"))),
|
||||
};
|
||||
let story = within.as_ref().map(|(a, _)| a.story).unwrap_or(StoryRef::Body);
|
||||
let mut found = search(s, story)?;
|
||||
if let Some((a, b)) = &within {
|
||||
found.retain(|(x, y)| x >= a && y <= b);
|
||||
}
|
||||
if let Some(h) = p::bool(v, "highlight") {
|
||||
s.find.highlight = h;
|
||||
s.find.highlights = None;
|
||||
}
|
||||
let n = found.len();
|
||||
let matches: Vec<Value> = found.iter().take(200).map(|(a, b)| json!({"start": pos_json(a), "end": pos_json(b)})).collect();
|
||||
s.find.current = 0;
|
||||
s.find.results = found;
|
||||
Ok(json!({"count": n, "matches": matches, "highlight": s.find.highlight}))
|
||||
}
|
||||
|
||||
fn step(s: &mut Session, dir: i64) -> CmdResult {
|
||||
let story = s.sel.focus.story;
|
||||
s.find.results = search(s, story)?;
|
||||
|
|
|
|||
|
|
@ -98,6 +98,11 @@ pub struct FindState {
|
|||
#[serde(skip)]
|
||||
pub results: Vec<(Pos, Pos)>,
|
||||
pub current: usize,
|
||||
/// Reading Highlight: mark every match of `query` in the body.
|
||||
pub highlight: bool,
|
||||
/// Highlighted matches, for the document revision they were found in.
|
||||
#[serde(skip)]
|
||||
pub highlights: Option<(u64, Vec<(Pos, Pos)>)>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
|
|
@ -304,6 +309,18 @@ impl Session {
|
|||
self.dirty = true;
|
||||
}
|
||||
|
||||
/// Reading Highlight ranges for the current document (searched again after edits); empty when it's off.
|
||||
pub fn find_highlights(&mut self) -> &[(Pos, Pos)] {
|
||||
if !self.find.highlight {
|
||||
return &[];
|
||||
}
|
||||
if self.find.highlights.as_ref().is_none_or(|(r, _)| *r != self.rev) {
|
||||
let found = crate::cmd::edit::search(self, StoryRef::Body).unwrap_or_default();
|
||||
self.find.highlights = Some((self.rev, found));
|
||||
}
|
||||
self.find.highlights.as_ref().map(|(_, v)| v.as_slice()).unwrap_or(&[])
|
||||
}
|
||||
|
||||
/// The current layout (recomputed when the document or view changed).
|
||||
pub fn layout(&mut self) -> Arc<DocLayout> {
|
||||
let ww = self.view.web_width;
|
||||
|
|
|
|||
|
|
@ -1701,6 +1701,37 @@ fn timestamps_come_from_the_clock() {
|
|||
assert!(date >= cmd::iso_from_unix_secs(before) && date <= cmd::iso_from_unix_secs(cmd::now_unix()), "{date}");
|
||||
}
|
||||
|
||||
/// Home › Editing › Find › Advanced Find: every match, in the body or a selection, with Reading Highlight.
|
||||
#[test]
|
||||
fn advanced_find_lists_and_highlights_matches() {
|
||||
let mut s = s();
|
||||
run(&mut s, "text.insert", json!({"text": "cat dog cat"}));
|
||||
run(&mut s, "text.newParagraph", json!({}));
|
||||
run(&mut s, "text.insert", json!({"text": "Cat"}));
|
||||
// From the UI it opens the dialog.
|
||||
run(&mut s, "edit.advancedFind", json!({}));
|
||||
assert_eq!(s.ui_requests.last(), Some(&json!({"open": "find"})));
|
||||
let r = run(&mut s, "edit.advancedFind", json!({"text": "cat", "highlight": true}));
|
||||
assert_eq!(r["count"], 3);
|
||||
assert_eq!(s.find_highlights().len(), 3);
|
||||
assert_eq!(run(&mut s, "edit.advancedFind", json!({"text": "cat", "matchCase": true}))["count"], 2);
|
||||
// Highlights follow edits and the latest search.
|
||||
assert_eq!(s.find_highlights().len(), 2);
|
||||
run(&mut s, "text.insert", json!({"text": " cat"}));
|
||||
assert_eq!(s.find_highlights().len(), 3);
|
||||
// Only inside the selection.
|
||||
run(&mut s, "select.text", json!({"text": "cat dog"}));
|
||||
assert_eq!(run(&mut s, "edit.advancedFind", json!({"text": "cat", "in": "selection"}))["count"], 1);
|
||||
run(&mut s, "edit.advancedFind", json!({"highlight": false}));
|
||||
assert!(s.find_highlights().is_empty());
|
||||
// Hostile params.
|
||||
assert!(s.run("edit.advancedFind", &json!({"text": "x", "in": "elsewhere"})).is_err());
|
||||
assert!(s.run("edit.advancedFind", &json!({"text": "(", "regex": true})).is_err());
|
||||
run(&mut s, "select.all", json!({}));
|
||||
run(&mut s, "text.delete", json!({}));
|
||||
assert!(s.run("edit.advancedFind", &json!({"text": "x", "in": "selection"})).is_err());
|
||||
}
|
||||
|
||||
/// #146: a table style made by command styles the table at the caret (header row fill and bold
|
||||
/// text, banded rows), can be modified, and saves as a table style the table refers to.
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -480,6 +480,17 @@ pub fn show(app: &mut WordApp, ui: &mut Ui) {
|
|||
app.canvas.textures.retain(|k, _| *k < n);
|
||||
app.canvas.page_rects = rects.clone();
|
||||
balloons(app, ui, &painter, &rects, &layout, geo.scale);
|
||||
// Find › Reading Highlight.
|
||||
let marks: Vec<(Pos, Pos)> = app.session.find_highlights().iter().take(5000).cloned().collect();
|
||||
for (a, b) in &marks {
|
||||
for (pi, r) in layout.selection_rects(&app.session.doc, a, b, app.session.page_hint) {
|
||||
if let Some(pr) = rects.get(pi) {
|
||||
let scale = layout.pages.get(pi).map_or(geo.scale, |page| page_screen_scale(*pr, page, geo.scale));
|
||||
let hr = Rect::from_min_size(pos2(pr.min.x + r.x * scale, pr.min.y + r.y * scale), vec2(r.w * scale, r.h * scale));
|
||||
painter.rect_filled(hr, 0.0, t.find_highlight);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Selection (a selected object shows its frame instead).
|
||||
if !app.session.sel.is_collapsed() && crate::objects::selected(app).is_none() {
|
||||
let (a, b) = app.session.sel.ordered();
|
||||
|
|
|
|||
|
|
@ -767,10 +767,24 @@ fn body(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bool {
|
|||
ui.checkbox(whole_word, tl!("Whole words"));
|
||||
ui.checkbox(regex, tl!("Wildcards (regex)"));
|
||||
});
|
||||
let opts = json!({"text": query, "with": replace, "matchCase": *match_case, "wholeWord": *whole_word, "regex": *regex});
|
||||
if !*replace_mode {
|
||||
ui.horizontal(|ui| {
|
||||
let lit = app.session.find.highlight;
|
||||
if ui.selectable_label(lit, tl!("Reading Highlight")).clicked() {
|
||||
let mut v = opts.clone();
|
||||
v["highlight"] = json!(!lit);
|
||||
match app.run("edit.advancedFind", v) {
|
||||
Ok(r) if !lit => *message = crate::i18n::fmt(tl!("{count} items highlighted"), &[("count", &r["count"].to_string())]),
|
||||
Ok(_) => message.clear(),
|
||||
Err(e) => *message = e,
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
if !message.is_empty() {
|
||||
ui.label(egui::RichText::new(message.as_str()).weak());
|
||||
}
|
||||
let opts = json!({"text": query, "with": replace, "matchCase": *match_case, "wholeWord": *whole_word, "regex": *regex});
|
||||
let mut close = false;
|
||||
ui.horizontal(|ui| {
|
||||
if *replace_mode {
|
||||
|
|
|
|||
|
|
@ -35,6 +35,8 @@
|
|||
Address\nBlock Bloque de\ndirección
|
||||
Adjust Ajustar
|
||||
Advanced Avanzado
|
||||
Advanced Find Búsqueda avanzada
|
||||
Advanced Find… Búsqueda avanzada…
|
||||
After: Después:
|
||||
Agents Agentes
|
||||
Align Alinear
|
||||
|
|
@ -274,6 +276,7 @@
|
|||
General General
|
||||
Go To Ir a
|
||||
Go To (⌘⌥G) Ir a (⌘⌥G)
|
||||
Go To… Ir a…
|
||||
Go to Ir a
|
||||
Grab bag Bolsa de datos adicionales
|
||||
Greeting Line Línea de saludo
|
||||
|
|
@ -527,6 +530,7 @@
|
|||
Read Mode Modo lectura
|
||||
Read\nAloud Leer en\nvoz alta
|
||||
Read\nMode Modo\nlectura
|
||||
Reading Highlight Resaltado de lectura
|
||||
Recent Recientes
|
||||
Record Macro Grabar macro
|
||||
Recover Unsaved Documents Recuperar documentos sin guardar
|
||||
|
|
@ -955,3 +959,4 @@
|
|||
Apply formatting to: Aplicar formato a:
|
||||
Whole Table Toda la tabla
|
||||
Fill color: Color de relleno:
|
||||
{count} items highlighted {count} elementos resaltados
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 25.
|
|
|
@ -931,6 +931,10 @@
|
|||
Dark ダーク
|
||||
Use system setting システム設定を使用
|
||||
A file dialog is already open. ファイル ダイアログが既に開いています。
|
||||
Advanced Find 高度な検索
|
||||
Advanced Find… 高度な検索…
|
||||
Go To… ジャンプ…
|
||||
Reading Highlight 検索結果を強調表示
|
||||
New Table Style 新しい表スタイル
|
||||
Modify Table Style 表スタイルの変更
|
||||
New Table Style… 新しい表スタイル…
|
||||
|
|
@ -938,3 +942,4 @@
|
|||
Apply formatting to: 書式の適用先:
|
||||
Whole Table 表全体
|
||||
Fill color: 塗りつぶしの色:
|
||||
{count} items highlighted {count} 件を強調表示しました
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 3.
|
|
|
@ -943,6 +943,10 @@
|
|||
Dark Escuro
|
||||
Use system setting Usar a configuração do sistema
|
||||
A file dialog is already open. Já há uma caixa de diálogo de arquivo aberta.
|
||||
Advanced Find Localização avançada
|
||||
Advanced Find… Localização avançada…
|
||||
Go To… Ir para…
|
||||
Reading Highlight Realçar resultados
|
||||
New Table Style Novo estilo de tabela
|
||||
Modify Table Style Modificar estilo de tabela
|
||||
New Table Style… Novo estilo de tabela…
|
||||
|
|
@ -950,3 +954,4 @@
|
|||
Apply formatting to: Aplicar formatação a:
|
||||
Whole Table Tabela inteira
|
||||
Fill color: Cor de preenchimento:
|
||||
{count} items highlighted {count} itens realçados
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 25.
|
|
|
@ -32,6 +32,8 @@
|
|||
Address\nBlock Блок\nадреси
|
||||
Adjust Налаштувати
|
||||
Advanced Додатково
|
||||
Advanced Find Розширений пошук
|
||||
Advanced Find… Розширений пошук…
|
||||
After: Після:
|
||||
Agents Агенти
|
||||
Align Вирівняти
|
||||
|
|
@ -277,6 +279,7 @@
|
|||
Go To Перейти
|
||||
Go to Перейти
|
||||
Go To (⌘⌥G) Перейти (⌘⌥G)
|
||||
Go To… Перейти…
|
||||
Grab bag Різне
|
||||
Greeting Line Рядок привітання
|
||||
Greeting\nLine Рядок\nпривітання
|
||||
|
|
@ -528,6 +531,7 @@
|
|||
Read Mode Режим читання
|
||||
Read\nAloud Читати\nвголос
|
||||
Read\nMode Режим\nчитання
|
||||
Reading Highlight Виділення під час читання
|
||||
Recent Останні
|
||||
Record Macro Записати макрос
|
||||
Recover Unsaved Documents Відновити незбережені документи
|
||||
|
|
@ -950,3 +954,4 @@
|
|||
Apply formatting to: Застосувати форматування до:
|
||||
Whole Table Уся таблиця
|
||||
Fill color: Колір заливки:
|
||||
{count} items highlighted Виділено елементів: {count}
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 31.
|
|
|
@ -942,6 +942,10 @@
|
|||
Dark 深色
|
||||
Use system setting 使用系统设置
|
||||
A file dialog is already open. 已有一个文件对话框处于打开状态。
|
||||
Advanced Find 高级查找
|
||||
Advanced Find… 高级查找…
|
||||
Go To… 定位…
|
||||
Reading Highlight 突出显示查找结果
|
||||
New Table Style 新建表格样式
|
||||
Modify Table Style 修改表格样式
|
||||
New Table Style… 新建表格样式…
|
||||
|
|
@ -949,3 +953,4 @@
|
|||
Apply formatting to: 将格式应用于:
|
||||
Whole Table 整个表格
|
||||
Fill color: 填充颜色:
|
||||
{count} items highlighted 已突出显示 {count} 项
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 25.
|
|
|
@ -932,6 +932,10 @@
|
|||
Dark 深色
|
||||
Use system setting 使用系統設定
|
||||
A file dialog is already open. 已有一個檔案對話方塊開啟中。
|
||||
Advanced Find 進階尋找
|
||||
Advanced Find… 進階尋找…
|
||||
Go To… 移至…
|
||||
Reading Highlight 醒目提示尋找結果
|
||||
New Table Style 新增表格樣式
|
||||
Modify Table Style 修改表格樣式
|
||||
New Table Style… 新增表格樣式…
|
||||
|
|
@ -939,3 +943,4 @@
|
|||
Apply formatting to: 將格式套用至:
|
||||
Whole Table 整個表格
|
||||
Fill color: 填滿色彩:
|
||||
{count} items highlighted 已醒目提示 {count} 個項目
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 4.
|
|
|
@ -457,7 +457,11 @@ fn home(app: &mut WordApp, ui: &mut Ui) {
|
|||
});
|
||||
group(ui, "Editing", None, app, |ui, app| {
|
||||
stack(ui, |ui| {
|
||||
small(ui, app, "find", Some("Find"), "Find", "ui.dialog", json!({"name": "find"}), false).clicked();
|
||||
menu_button(ui, app, "find", Some("Find"), "Find", false, |ui, app| {
|
||||
mi(ui, app, "Find", "ui.dialog", json!({"name": "find"}));
|
||||
mi(ui, app, "Advanced Find…", "edit.advancedFind", json!({}));
|
||||
mi(ui, app, "Go To…", "ui.dialog", json!({"name": "goto"}));
|
||||
});
|
||||
small(ui, app, "replace", Some("Replace"), "Replace", "ui.dialog", json!({"name": "replace"}), false);
|
||||
menu_button(ui, app, "select", Some("Select"), "Select", false, |ui, app| {
|
||||
mi(ui, app, "Select All", "select.all", json!({}));
|
||||
|
|
|
|||
|
|
@ -99,6 +99,8 @@ pub struct Tokens {
|
|||
pub ruler_tick: Color32,
|
||||
pub status_bar: Color32,
|
||||
pub selection: Color32,
|
||||
/// Find › Reading Highlight.
|
||||
pub find_highlight: Color32,
|
||||
pub caret: Color32,
|
||||
pub page_shadow: Color32,
|
||||
/// Selection-frame handles (pictures, shapes, text boxes) and their shadow.
|
||||
|
|
@ -140,6 +142,7 @@ impl Tokens {
|
|||
ruler_tick: Color32::from_rgb(0x70, 0x70, 0x70),
|
||||
status_bar: Color32::from_rgb(0xF0, 0xF0, 0xF0),
|
||||
selection: Color32::from_rgba_unmultiplied(0x3B, 0x5B, 0xDB, 0x48),
|
||||
find_highlight: Color32::from_rgba_unmultiplied(0xFF, 0xE0, 0x3D, 0x90),
|
||||
caret: Color32::BLACK,
|
||||
page_shadow: Color32::from_rgba_unmultiplied(0, 0, 0, 0x22),
|
||||
handle: Color32::WHITE,
|
||||
|
|
@ -179,6 +182,7 @@ impl Tokens {
|
|||
ruler_tick: Color32::from_rgb(0xA0, 0xA0, 0xA0),
|
||||
status_bar: Color32::from_rgb(0x1F, 0x1F, 0x1F),
|
||||
selection: Color32::from_rgba_unmultiplied(0x7A, 0x93, 0xF0, 0x55),
|
||||
find_highlight: Color32::from_rgba_unmultiplied(0xC8, 0xA8, 0x00, 0x70),
|
||||
caret: Color32::BLACK,
|
||||
page_shadow: Color32::from_rgba_unmultiplied(0, 0, 0, 0x60),
|
||||
handle: Color32::from_rgb(0xF2, 0xF2, 0xF2),
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@
|
|||
|
||||
Generated by `cargo xtask parity` (`wordcraft-cli parity --markdown`): the word-processor feature catalog (`crates/engine/src/catalog.rs`) compared with the live command registry.
|
||||
|
||||
**380 of 430 catalog features have commands (88.4%).**
|
||||
**381 of 430 catalog features have commands (88.6%).**
|
||||
|
||||
| Tab | Live | Total |
|
||||
|---|---|---|
|
||||
| File | 20 | 20 |
|
||||
| Quick Access | 4 | 4 |
|
||||
| Home | 92 | 101 |
|
||||
| Home | 93 | 101 |
|
||||
| Insert | 42 | 53 |
|
||||
| Draw | 0 | 11 |
|
||||
| Design | 10 | 10 |
|
||||
|
|
@ -37,7 +37,6 @@ Generated by `cargo xtask parity` (`wordcraft-cli parity --markdown`): the word-
|
|||
- Paragraph › Asian Typography (`para.asianTypography`)
|
||||
- Styles › Style Inspector (`styles.inspector`)
|
||||
- Styles › Manage Styles (`styles.manage`)
|
||||
- Editing › Advanced Find (`edit.advancedFind`)
|
||||
- Voice › Dictate (`tools.dictate`)
|
||||
|
||||
### Insert
|
||||
|
|
|
|||
Loading…
Reference in New Issue