From 5119fb5f5190d54fa6f23c2926d287bd43801d55 Mon Sep 17 00:00:00 2001 From: prxnvt <53838599+prxnvt@users.noreply.github.com> Date: Mon, 2 Nov 2026 01:33:10 -0500 Subject: [PATCH] UI: Python mode starts on no draft typed inside a spill Python mode refuses a cell inside another cell's spill, but a draft typed on a spill child could still switch to it with Ctrl+Alt+Shift+P, the PY badge, ui.pythonMode or =PY then Tab, and Ctrl+Enter then wrote code into the spill. toggle_python_mode and the formula bar's switch now check the draft's own cell (draft_in_spill) and keep such a draft an ordinary entry. Tests: python_mode_does_not_start_on_a_draft_typed_inside_a_spill (control) and a_draft_typed_inside_a_spill_does_not_enter_python_mode (keys); both fail without the fix. --- crates/ui-egui/src/app/editing.rs | 11 +++++++++++ crates/ui-egui/src/control/tests.rs | 18 ++++++++++++++++++ crates/ui-egui/src/formula_bar.rs | 2 +- crates/ui-egui/tests/it/python_mode.rs | 19 +++++++++++++++++++ 4 files changed, 49 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/app/editing.rs b/crates/ui-egui/src/app/editing.rs index 8422e1538..bd7a6ab5e 100644 --- a/crates/ui-egui/src/app/editing.rs +++ b/crates/ui-egui/src/app/editing.rs @@ -321,6 +321,9 @@ impl SheetApp { /// Ctrl+Alt+Shift+P (and the PY badge): Python mode on or off. An edit in progress keeps its /// text; with no edit, Python mode starts on the active cell. pub fn toggle_python_mode(&mut self) { + if self.editor.as_ref().is_some_and(|ed| !ed.python && self.draft_in_spill(ed)) { + return; + } match self.editor.as_mut() { Some(ed) if ed.python => ed.leave_python(), Some(ed) => ed.enter_python(), @@ -329,6 +332,14 @@ impl SheetApp { self.session.mode = gridcraft_engine::Mode::Edit; } + /// Whether a draft's cell lies inside another cell's spill. A draft typed there stays an + /// ordinary entry: Python mode starts on no cell inside a spill, as the formula bar's entry + /// paths don't. + pub(crate) fn draft_in_spill(&self, ed: &editor::EditState) -> bool { + let doc = self.session.documents().iter().find(|d| d.uid == ed.doc); + doc.and_then(|d| d.wb.sheet(ed.sheet)).is_some_and(|sh| sh.spill_anchor(ed.cell).is_some()) + } + /// Commits a Python mode edit through `python.set`, on the sheet the edit started on; with no /// code left, a Python cell is cleared through `cell.set`. When the command refuses (code /// longer than a cell holds), its message shows and the edit stays open with the code as diff --git a/crates/ui-egui/src/control/tests.rs b/crates/ui-egui/src/control/tests.rs index de507907a..7f876d35c 100644 --- a/crates/ui-egui/src/control/tests.rs +++ b/crates/ui-egui/src/control/tests.rs @@ -1233,3 +1233,21 @@ fn a_draft_typed_over_a_hidden_python_cell_stays_open_through_commands() { assert_eq!(b2.map(|c| (c.input_text(), c.py.is_some())), Some(("7".to_string(), false)), "the typed entry replaces the Python cell"); } +#[test] +fn python_mode_does_not_start_on_a_draft_typed_inside_a_spill() { + // Typing on a spill child is an ordinary entry; switching that draft to Python mode would let + // Ctrl+Enter write code into a cell another cell's result spills over, which every other way + // into Python mode refuses. + let mut session = gridcraft_engine::Session::new(); + session.new_workbook(); + session.execute("cell.set", json!({"cell": "A1", "input": "=SEQUENCE(3)"})).unwrap(); + session.execute("selection.set", json!({"cell": "A2"})).unwrap(); + let mut app = SheetApp::new(session, crate::Services::default()); + let ctx = egui::Context::default(); + call(&mut app, &ctx, "ui.edit.begin", json!({"text": "5"})); + execute(&mut app, &ctx, "ui.pythonMode", json!({"on": true})); + assert!(app.editor.as_ref().is_some_and(|e| !e.python && e.text == "5"), "the draft stays an ordinary entry"); + call(&mut app, &ctx, "ui.edit.commit", json!({"move": "none"})); + let a2 = app.session.active().and_then(|d| d.wb.sheet(0)).and_then(|sh| sh.cell(gridcraft_engine::core::CellRef::new(1, 0)).cloned()); + assert_eq!(a2.map(|c| (c.input_text(), c.py.is_some())), Some(("5".to_string(), false))); +} diff --git a/crates/ui-egui/src/formula_bar.rs b/crates/ui-egui/src/formula_bar.rs index 3f2d08a60..25178ce50 100644 --- a/crates/ui-egui/src/formula_bar.rs +++ b/crates/ui-egui/src/formula_bar.rs @@ -518,7 +518,7 @@ pub fn editor_widget(app: &mut SheetApp, ui: &mut egui::Ui, id: egui::Id, font: show_editor_suggestions(ui, EditorSuggestion { app, id, resp: &resp, ed: &mut ed, galley: &output.galley, tokens: &t, mine }); let lines = output.galley.rows.len().max(1); let caret = Some(output.galley.pos_from_cursor(egui::text::CCursor::new(ed.caret)).translate(output.galley_pos.to_vec2())); - if to_python { + if to_python && !app.draft_in_spill(&ed) { ed.enter_python(); app.editor = Some(ed); app.session.mode = gridcraft_engine::Mode::Edit; diff --git a/crates/ui-egui/tests/it/python_mode.rs b/crates/ui-egui/tests/it/python_mode.rs index 6e88e39d4..79c0b079c 100644 --- a/crates/ui-egui/tests/it/python_mode.rs +++ b/crates/ui-egui/tests/it/python_mode.rs @@ -126,6 +126,25 @@ fn typing_py_then_tab_enters_python_mode_as_in_excel() { assert_eq!(mode(&h), None); } +/// A cell inside a spill: typing there is an ordinary entry, as on any spill child, and neither +/// `=PY` then Tab nor the shortcut turns that draft into code for the cell, as Python mode starts +/// on no cell inside a spill. +#[test] +fn a_draft_typed_inside_a_spill_does_not_enter_python_mode() { + let mut s = blank(); + s.execute("cell.set", json!({"cell": "A1", "input": "=SEQUENCE(3)"})).unwrap(); + s.execute("selection.set", json!({"cell": "A2"})).unwrap(); + let mut h = harness(s); + type_text(&mut h, "=PY"); + assert_eq!(mode(&h), Some(false), "typing starts an ordinary entry"); + send(&mut h, press(Key::Tab, Modifiers::NONE)); + assert_eq!(mode(&h), Some(false), "=PY then Tab"); + shortcut(&mut h); + assert_eq!(mode(&h), Some(false), "the shortcut"); + ctrl_enter(&mut h); + assert_eq!(code(&h, "A2"), None, "no code is written into the spill"); +} + #[test] fn enter_and_tab_edit_the_code_and_ctrl_enter_commits_through_python_set() { let mut h = harness(blank());