mirror of
https://github.com/storytold/gridcraft.git
synced 2026-11-05 01:58:58 +00:00
Merge PR #3254
This commit is contained in:
commit
ebf762a03a
6 files changed
+76
-5
No files matched your search
@@ -144,13 +144,13 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
fill_series
|
||||
),
|
||||
cmd!("edit.flashFill", "Flash Fill", ["Home", "Editing", "Fill"], Some("Ctrl+E"), "{range?}", has_doc, flash_fill),
|
||||
cmd!(noundo "edit.find", "Find…", ["Home", "Editing", "Find & Select"], Some("Cmd+F"), "{what, matchCase?, wholeCell?, lookIn?: formulas|values, byColumns?, all?: bool, within?: sheet|workbook}", has_doc, find),
|
||||
cmd!(noundo "edit.find", "Find…", ["Home", "Editing", "Find & Select"], Some("Cmd+F"), "{what, matchCase?, wholeCell?, lookIn?: formulas|values, byColumns?, all?: bool, within?: sheet|workbook|selection} (selection: only the selected cells; Find Next keeps them selected and moves the active cell)", has_doc, find),
|
||||
cmd!(
|
||||
"edit.replace",
|
||||
"Replace…",
|
||||
["Home", "Editing", "Find & Select"],
|
||||
Some("Ctrl+H"),
|
||||
"{what, with, matchCase?, wholeCell?, all?: true, within?: sheet|workbook} → {replaced, skipped?} (in a Python cell the code is edited; skipped counts Python cells whose code would no longer fit)",
|
||||
"{what, with, matchCase?, wholeCell?, all?: true, within?: sheet|workbook|selection} → {replaced, skipped?} (in a Python cell the code is edited; skipped counts Python cells whose code would no longer fit)",
|
||||
has_doc,
|
||||
replace
|
||||
),
|
||||
|
||||
@@ -62,6 +62,8 @@ pub(super) fn find(s: &mut Session, p: &Json) -> Result<Json> {
|
||||
let by_cols = bool_param(p, "byColumns").unwrap_or(false);
|
||||
let all = bool_param(p, "all").unwrap_or(false);
|
||||
let workbook = str_param(p, "within") == Some("workbook");
|
||||
// `selection`: only the selected cells of the active sheet, as Excel searches a selected range.
|
||||
let in_selection = str_param(p, "within") == Some("selection");
|
||||
s.last_find = Some(p.clone());
|
||||
let d = s.doc()?;
|
||||
let sheets: Vec<usize> = if workbook { (0..d.wb.sheets.len()).collect() } else { vec![d.wb.active_sheet()] };
|
||||
@@ -71,6 +73,7 @@ pub(super) fn find(s: &mut Session, p: &Json) -> Result<Json> {
|
||||
let mut cells: Vec<(CellRef, String)> = sh
|
||||
.cells
|
||||
.iter()
|
||||
.filter(|(c, _)| !in_selection || d.selection.contains(*c))
|
||||
.filter_map(|(c, cell)| {
|
||||
let text = if values || cell.formula.is_none() || crate::display::formula_hidden(&d.wb, sh, c) {
|
||||
crate::display::cell_text(&d.wb, sh, c)
|
||||
@@ -100,7 +103,12 @@ pub(super) fn find(s: &mut Session, p: &Json) -> Result<Json> {
|
||||
let Some((si, c, t)) = next else { return Err(EngineError::Other("not found".into())) };
|
||||
let dd = s.doc_mut()?;
|
||||
dd.wb_switch_sheet(si);
|
||||
dd.selection = crate::Selection::at(c);
|
||||
if in_selection {
|
||||
// The range stays selected and the active cell moves through it, as in Excel.
|
||||
dd.selection.active = c;
|
||||
} else {
|
||||
dd.selection = crate::Selection::at(c);
|
||||
}
|
||||
Ok(json!({"cell": c.a1(), "sheet": si, "value": t, "count": hits.len()}))
|
||||
}
|
||||
|
||||
@@ -114,6 +122,7 @@ pub(super) fn replace(s: &mut Session, p: &Json) -> Result<Json> {
|
||||
let whole = bool_param(p, "wholeCell").unwrap_or(false);
|
||||
let all = bool_param(p, "all").unwrap_or(true);
|
||||
let workbook = str_param(p, "within") == Some("workbook");
|
||||
let selection = (str_param(p, "within") == Some("selection")).then(|| s.doc().map(|d| d.selection.clone())).transpose()?;
|
||||
let active = s.doc()?.selection.active;
|
||||
let mut skipped = 0usize;
|
||||
let n = edit(s, |cx| {
|
||||
@@ -123,7 +132,7 @@ pub(super) fn replace(s: &mut Session, p: &Json) -> Result<Json> {
|
||||
let cells: Vec<(CellRef, String)> =
|
||||
cx.wb.sheet(si).map(|sh| sh.cells.iter().map(|(c, _)| (c, sh.input_text(c))).collect()).unwrap_or_default();
|
||||
for (c, text) in cells {
|
||||
if !all && c != active {
|
||||
if (!all && c != active) || selection.as_ref().is_some_and(|sel| !sel.contains(c)) {
|
||||
continue;
|
||||
}
|
||||
if !matches(&text, &what, case, whole) {
|
||||
|
||||
@@ -552,3 +552,31 @@ fn a_field_that_is_not_really_quoted_keeps_its_separators() {
|
||||
let hostile = "\"x\t".repeat(50_000);
|
||||
assert_eq!(parse_tsv(&hostile).first().map(Vec::len), Some(50_001));
|
||||
}
|
||||
|
||||
/// `within: selection` searches only the selected cells, as Excel does with a range selected
|
||||
/// (#3224): Find All lists them, Find Next moves the active cell through them and keeps the range
|
||||
/// selected, and Replace changes only them.
|
||||
#[test]
|
||||
fn find_and_replace_within_the_selection() {
|
||||
let mut s = Session::new();
|
||||
s.new_workbook();
|
||||
s.execute("range.setValues", json!({"range": "A1:B4", "values": [["pear", "pear"], ["plum", "pear"], ["pear", "fig"], ["pear", "pear"]]}))
|
||||
.unwrap();
|
||||
s.execute("selection.set", json!({"range": "B1:B4"})).unwrap();
|
||||
let all = s.execute("edit.find", json!({"what": "pear", "all": true, "within": "selection"})).unwrap();
|
||||
let cells: Vec<&str> = all["results"].as_array().unwrap().iter().filter_map(|r| r["cell"].as_str()).collect();
|
||||
assert_eq!(cells, ["B1", "B2", "B4"], "only column B");
|
||||
assert_eq!(s.execute("edit.find", json!({"what": "pear", "within": "selection"})).unwrap()["cell"], "B2");
|
||||
assert_eq!(s.execute("edit.find", json!({"what": "pear", "within": "selection"})).unwrap()["cell"], "B4");
|
||||
assert_eq!(s.execute("edit.find", json!({"what": "pear", "within": "selection"})).unwrap()["cell"], "B1", "wraps inside the range");
|
||||
let sel = &s.doc().unwrap().selection;
|
||||
assert_eq!((sel.a1(), sel.active.a1()), ("B1:B4".to_string(), "B1".to_string()), "the range stays selected");
|
||||
assert!(s.execute("edit.find", json!({"what": "plum", "within": "selection"})).is_err(), "A2 is outside the range");
|
||||
assert_eq!(s.execute("edit.replace", json!({"what": "pear", "with": "kiwi", "within": "selection"})).unwrap()["replaced"], 3);
|
||||
let sh = s.doc().unwrap().wb.active().unwrap();
|
||||
let at = |a: &str| sh.value(CellRef::parse(a).unwrap());
|
||||
assert_eq!((at("A1"), at("B1"), at("B4"), at("A4")), (Value::text("pear"), Value::text("kiwi"), Value::text("kiwi"), Value::text("pear")));
|
||||
// The sheet scope is unchanged.
|
||||
let all = s.execute("edit.find", json!({"what": "pear", "all": true})).unwrap();
|
||||
assert_eq!(all["count"], 3, "A1, A3 and A4 on the whole sheet");
|
||||
}
|
||||
@@ -40,7 +40,16 @@ pub(super) fn find(app: &mut SheetApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
d.values.insert("matchCase".into(), json!(case));
|
||||
d.values.insert("wholeCell".into(), json!(whole));
|
||||
d.values.insert("within".into(), json!(if wb { "workbook" } else { "sheet" }));
|
||||
let base = json!({"what": what, "matchCase": case, "wholeCell": whole, "within": if wb { "workbook" } else { "sheet" }});
|
||||
// As in Excel, a selected range (more than one cell) limits the search on the sheet to it.
|
||||
let range_selected = app.session.active().is_some_and(|doc| !doc.selection.is_single_cell());
|
||||
let within = if wb {
|
||||
"workbook"
|
||||
} else if range_selected {
|
||||
"selection"
|
||||
} else {
|
||||
"sheet"
|
||||
};
|
||||
let base = json!({"what": what, "matchCase": case, "wholeCell": whole, "within": within});
|
||||
ui.horizontal(|ui| {
|
||||
if d.tab == "Replace" {
|
||||
if ui.button("Replace All").clicked() {
|
||||
|
||||
@@ -34,3 +34,27 @@ fn typing_right_after_ctrl_f_fills_find_what() {
|
||||
fn typing_right_after_ctrl_h_fills_find_what() {
|
||||
assert_eq!(type_after_shortcut(Key::H), Some(json!("abc")));
|
||||
}
|
||||
|
||||
/// With a range selected, Find All in the dialog lists only the matches inside it, as in Excel
|
||||
/// (#3224); with one cell selected it searches the whole sheet.
|
||||
#[test]
|
||||
fn find_all_with_a_range_selected_searches_only_the_range() {
|
||||
use egui_kittest::kittest::Queryable;
|
||||
for (range, expected) in [("B1:B3", 2), ("C5", 4)] {
|
||||
let mut h = harness();
|
||||
let app = h.state_mut();
|
||||
app.session.execute("range.setValues", json!({"range": "A1:B3", "values": [["pear", "pear"], ["fig", "fig"], ["pear", "pear"]]})).unwrap();
|
||||
app.session.execute("selection.set", json!({"range": range})).unwrap();
|
||||
h.run_steps(2);
|
||||
h.input_mut().events.push(Event::Key { key: Key::F, physical_key: Some(Key::F), pressed: true, repeat: false, modifiers: CTRL });
|
||||
h.input_mut().events.push(Event::Key { key: Key::F, physical_key: Some(Key::F), pressed: false, repeat: false, modifiers: CTRL });
|
||||
h.run_steps(6);
|
||||
h.input_mut().events.push(Event::Text("pear".into()));
|
||||
h.run_steps(4);
|
||||
h.get_by_label("Find All").click();
|
||||
h.run_steps(4);
|
||||
let d = h.state().dialog.as_ref().expect("find dialog is open");
|
||||
let count = d.result.as_ref().map(|r| r["count"].clone());
|
||||
assert_eq!(count, Some(json!(expected)), "{range} selected");
|
||||
}
|
||||
}
|
||||
@@ -238,6 +238,7 @@ scrollbar bugs 2–4 · pixel/feel pass against observed Excel 7–12.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-11-03 | minor | Find and Replace with a range selected searches only that range, as in Excel: Find Next moves the active cell through it and keeps it selected, Find All lists its matches, Replace and Replace All change only them; one selected cell still searches the sheet. `edit.find` / `edit.replace` take `within: selection` (#3224) |
|
||||
| 2026-11-03 | minor | Point mode: dragging over row or column headers extends the whole-row or whole-column reference, as it does over cells |
|
||||
| 2026-11-03 | minor | Frozen panes: text in frozen columns overflows only into its own pane and keeps its whole width while the sheet scrolls; frozen gridlines and merged-cell borders reach the freeze line, and the freeze handle measures the last frozen column itself (#2544). Page Layout view gridlines stop at each page's last column |
|
||||
| 2026-11-03 | minor | Excel 2003 menu access keys: 43 legacy sequences (Insert, Edit, Format, Data, View, Tools and File ▸ Page Setup, e.g. Alt+I, R inserts rows, Alt+E, A, A clears all, Alt+D, S sorts, Alt+T, O opens Options) run the existing commands and dialogs; Alt+I, D, V and T paint no badges, as in Excel (#941, #2311) |
|
||||
|
||||
Reference in new issue
Block a user