diff --git a/crates/ui-egui/src/app/editing.rs b/crates/ui-egui/src/app/editing.rs index 903e302ac..8422e1538 100644 --- a/crates/ui-egui/src/app/editing.rs +++ b/crates/ui-egui/src/app/editing.rs @@ -220,18 +220,18 @@ impl SheetApp { /// drawn, pointed from and committed on its sheet, and the point-mode anchor, held by sheet /// index, is let go. A draft is dropped at once with a message when its sheet is gone, or when /// it holds Python code that sheet protection now hides (a draft in Python mode, or one that - /// began on a Python cell): neither editor shows hidden code, as the Python editor window - /// closes then too. + /// began on a Python cell's code): neither editor shows hidden code, as the Python editor + /// window closes then too. A draft typed over a Python cell holds only what was typed, so it + /// stays open. pub(crate) fn follow_open_draft(&mut self) { let Some(ed) = self.editor.as_ref() else { return }; let Some(d) = self.session.documents().iter().find(|d| d.uid == ed.doc) else { return }; let order = editor::sheet_order(&d.wb); let moved = ed.sheet_uid != 0 && order != ed.sheet_order; let found = if moved { ed.sheet_in(&d.wb) } else { Some(ed.sheet) }; - let hidden = found.and_then(|i| d.wb.sheet(i)).is_some_and(|sh| { - (ed.python || ed.py_cell || sh.cell(ed.cell).is_some_and(|c| c.py.is_some())) - && gridcraft_engine::display::formula_hidden(&d.wb, sh, ed.cell) - }); + let hidden = found + .and_then(|i| d.wb.sheet(i)) + .is_some_and(|sh| (ed.python || ed.py_cell) && gridcraft_engine::display::formula_hidden(&d.wb, sh, ed.cell)); let why = match found { None => Some(DRAFT_SHEET_GONE), Some(_) if hidden => Some(DRAFT_CODE_HIDDEN), diff --git a/crates/ui-egui/src/control.rs b/crates/ui-egui/src/control.rs index d7919e60a..dddce9759 100644 --- a/crates/ui-egui/src/control.rs +++ b/crates/ui-egui/src/control.rs @@ -161,16 +161,18 @@ pub fn inspect(app: &SheetApp, ctx: &egui::Context) -> Json { /// The open edit, for `ui.inspect`. The draft is left out (`text` is null, `hidden` true) while /// its cell's code is hidden on a protected sheet, checked now: a protection command earlier in -/// the same batch runs before the next frame. That holds for a draft in Python mode, and for any -/// draft of a Python cell, since leaving Python mode keeps the code as an ordinary draft. The -/// sheet is the draft's own, found by identity: a sheet inserted, deleted or moved in the same -/// batch puts another at its index. With its sheet gone, such a draft is left out too. +/// the same batch runs before the next frame. That holds for a draft in Python mode, and for a +/// draft that began on a Python cell's code, since leaving Python mode keeps the code as an +/// ordinary draft; a draft typed over a Python cell holds only what was typed. The sheet is the +/// draft's own, found by identity: a sheet inserted, deleted or moved in the same batch puts +/// another at its index. With its sheet gone, such a draft is left out too. fn editor_json(app: &SheetApp, e: &crate::editor::EditState) -> Json { - let code = |sh: &gridcraft_engine::model::Sheet| e.python || sh.cell(e.cell).is_some_and(|c| c.py.is_some()); - let hidden = app.session.documents().iter().find(|d| d.uid == e.doc).is_some_and(|d| match e.sheet_in(&d.wb).and_then(|i| d.wb.sheet(i)) { - Some(sh) => code(sh) && gridcraft_engine::display::formula_hidden(&d.wb, sh, e.cell), - None => e.python || e.py_cell, - }); + let code = e.python || e.py_cell; + let hidden = code + && app.session.documents().iter().find(|d| d.uid == e.doc).is_some_and(|d| match e.sheet_in(&d.wb).and_then(|i| d.wb.sheet(i)) { + Some(sh) => gridcraft_engine::display::formula_hidden(&d.wb, sh, e.cell), + None => true, + }); let text = if hidden { Json::Null } else { json!(e.text) }; json!({"cell": e.cell.a1(), "text": text, "hidden": hidden, "formulaBar": e.from_formula_bar, "point": e.can_point(), "python": e.python}) } diff --git a/crates/ui-egui/src/control/tests.rs b/crates/ui-egui/src/control/tests.rs index 1183535e8..de507907a 100644 --- a/crates/ui-egui/src/control/tests.rs +++ b/crates/ui-egui/src/control/tests.rs @@ -1208,3 +1208,28 @@ fn a_draft_of_python_code_that_protection_hides_is_dropped_with_a_message() { execute(&mut app, &ctx, "review.protectSheet", json!({})); assert!(app.editor.as_ref().is_some_and(|e| e.text == "=2+2"), "a formula draft is unchanged"); } + +#[test] +fn a_draft_typed_over_a_hidden_python_cell_stays_open_through_commands() { + // An unlocked, hidden Python cell on a protected sheet: typing replaces it, as in Excel. The + // draft holds only what was typed, so a command neither drops it nor hides it from ui.inspect. + let mut session = gridcraft_engine::Session::new(); + session.new_workbook(); + session.execute("python.set", json!({"cell": "B2", "code": "secret = 41"})).unwrap(); + session.execute("home.formatCells", json!({"range": "B2", "style": {"protection": {"hidden": true, "locked": false}}})).unwrap(); + session.execute("review.protectSheet", json!({})).unwrap(); + session.execute("selection.set", json!({"cell": "B2"})).unwrap(); + let mut app = SheetApp::new(session, crate::Services::default()); + let ctx = egui::Context::default(); + call(&mut app, &ctx, "ui.edit.begin", json!({"text": "7"})); + execute(&mut app, &ctx, "home.insertSheet", json!({"name": "Other", "before": 1})); + assert!(app.editor.as_ref().is_some_and(|e| e.text == "7"), "the typed entry stays open"); + assert!(app.message.is_none(), "{:?}", app.message); + let after = inspected(&mut app, &ctx); + assert_eq!(after["editor"]["text"], "7"); + assert_eq!(after["editor"]["hidden"], false); + call(&mut app, &ctx, "ui.edit.commit", json!({"move": "none"})); + let b2 = app.session.active().and_then(|d| d.wb.sheet(0)).and_then(|sh| sh.cell(gridcraft_engine::core::CellRef::new(1, 1)).cloned()); + assert_eq!(b2.map(|c| (c.input_text(), c.py.is_some())), Some(("7".to_string(), false)), "the typed entry replaces the Python cell"); +} +