diff --git a/crates/engine/src/cmd/edit.rs b/crates/engine/src/cmd/edit.rs index 3bba84748..f2972be5a 100644 --- a/crates/engine/src/cmd/edit.rs +++ b/crates/engine/src/cmd/edit.rs @@ -129,7 +129,7 @@ pub fn specs() -> Vec { "Replace…", ["Home", "Editing", "Find & Select"], Some("Ctrl+H"), - "{what, with, matchCase?, wholeCell?, all?: true, within?: sheet|workbook}", + "{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)", has_doc, replace ), diff --git a/crates/engine/src/cmd/edit/find.rs b/crates/engine/src/cmd/edit/find.rs index bce0ba728..06a908e14 100644 --- a/crates/engine/src/cmd/edit/find.rs +++ b/crates/engine/src/cmd/edit/find.rs @@ -115,6 +115,7 @@ pub(super) fn replace(s: &mut Session, p: &Json) -> Result { let all = bool_param(p, "all").unwrap_or(true); let workbook = str_param(p, "within") == Some("workbook"); let active = s.doc()?.selection.active; + let mut skipped = 0usize; let n = edit(s, |cx| { let sheets: Vec = if workbook { (0..cx.wb.sheets.len()).collect() } else { vec![cx.wb.active_sheet()] }; let mut n = 0; @@ -135,6 +136,31 @@ pub(super) fn replace(s: &mut Session, p: &Json) -> Result { } else { replace_ci(&text, &what, &with) }; + // A Python cell's text is its code: the replacement edits the code and the cell + // stays a Python cell. Code that would no longer fit in a PY formula is left as it was. + let old_py = cx.wb.sheet(si).and_then(|sh| sh.cell(c)).and_then(|x| x.py.clone()); + if let Some(old) = old_py { + let py = gridcraft_model::PyCode { object: old.object, ..gridcraft_model::PyCode::new(&new) }; + if py.code == old.code { + continue; + } + if !py.fits() { + skipped += 1; + continue; + } + let sh = cx.sheet_mut(si)?; + if let Some(r) = sh.spill_ranges.remove(&c) { + sh.clear_spill(r); + } + let style = sh.style_id(c); + sh.set_cell( + c, + Cell { value: Value::Error(gridcraft_core::CellError::Blocked), formula: None, style, py: Some(std::sync::Arc::new(py)) }, + ); + cx.touch(si, c); + n += 1; + continue; + } let cell = input_to_cell(&new, si, c, &mut cx.wb).unwrap_or_else(|_| Some(Cell::value(Value::text(new.as_str())))); let sh = cx.sheet_mut(si)?; match cell { @@ -149,6 +175,9 @@ pub(super) fn replace(s: &mut Session, p: &Json) -> Result { } Ok(n) })?; + if skipped > 0 { + return Ok(json!({"replaced": n, "skipped": skipped})); + } Ok(json!({"replaced": n})) } diff --git a/crates/engine/src/tests/python_cells.rs b/crates/engine/src/tests/python_cells.rs index 417ed1e4a..ef1df605a 100644 --- a/crates/engine/src/tests/python_cells.rs +++ b/crates/engine/src/tests/python_cells.rs @@ -263,3 +263,36 @@ fn a_macro_counts_python_code_as_data_and_a_blocked_spill_as_no_write() { assert!(e.contains("F2") && e.contains("H2") && !e.contains("J2"), "{e}"); assert_eq!(code(&s, 0, "F2").as_deref(), Some("1"), "nothing was changed"); } + +#[test] +fn replace_edits_a_python_cells_code_and_keeps_it_a_python_cell() { + let mut s = s(); + plant(&mut s, 0, "A1", "x = 1\nx"); + { + let d = s.doc_mut().unwrap(); + let mut wb = (*d.wb).clone(); + let cell = wb.sheet_mut(0).unwrap().cells.get_mut(CellRef::parse("A1").unwrap()).unwrap(); + Arc::make_mut(cell.py.as_mut().unwrap()).object = true; + d.wb = Arc::new(wb); + } + let r = s.execute("edit.replace", json!({"what": "x", "with": "y"})).unwrap(); + assert_eq!(r["replaced"], 1); + assert_eq!(code(&s, 0, "A1").as_deref(), Some("y = 1\ny")); + let d = s.doc().unwrap(); + let cell = d.wb.sheet(0).unwrap().cell(CellRef::parse("A1").unwrap()).unwrap(); + assert!(cell.py.as_ref().unwrap().object, "the return type is kept"); + assert_eq!(cell.value, Value::Error(gridcraft_core::CellError::Blocked)); + s.execute("edit.undo", json!({})).unwrap(); + assert_eq!(code(&s, 0, "A1").as_deref(), Some("x = 1\nx")); +} + +#[test] +fn replace_leaves_python_code_that_would_no_longer_fit() { + let mut s = s(); + let long = "a".repeat(gridcraft_model::python::MAX_FORMULA_CHARS / 2); + plant(&mut s, 0, "A1", &long); + let r = s.execute("edit.replace", json!({"what": "a", "with": "bb"})).unwrap(); + assert_eq!(r["replaced"], 0); + assert_eq!(r["skipped"], 1); + assert_eq!(code(&s, 0, "A1").as_deref(), Some(long.as_str())); +}