mirror of
https://github.com/storytold/gridcraft.git
synced 2026-11-03 00:41:56 -06:00
Python cells get an editing mode of their own in the formula bar, in the code editor (monospace, Python colours). It starts with Ctrl+Alt+Shift+P (Cmd+Option+Shift+P on macOS), with =PY then Tab as in Excel, or by editing a Python cell (F2, a double-click, a click on the formula bar); typing over a Python cell still replaces it. The shortcut again leaves Python mode and keeps the text as an ordinary entry. In Python mode Enter starts a new line with the indentation, Tab and Shift+Tab indent, Ctrl+Enter (Cmd+Enter on macOS) commits the code through python.set and Esc cancels. Every commit path of a Python edit goes through python.set, the formula bar's Enter button and a click on the sheet included. When the command refuses the code (longer than a cell holds), its message shows and the edit stays open with the code as typed. A locked cell on a protected sheet is refused before a draft exists, and a draft whose cell protection locks while it is open is dropped with the warning, as an ordinary edit is (#2945). A Python cell's code committed unchanged writes nothing (no undo step), as F2 then Enter on any other cell does. With all the code deleted, the commit clears a Python cell through cell.set, as an empty entry clears any other cell (code of spaces is still code); on another cell it writes nothing. The formula bar shows while a Python edit is open, also when View > Formula Bar hides it, since Python mode edits only there. A Python draft records the window it was typed in, as an ordinary draft does, so it is committed in that window when another comes forward. Python mode replaces the route that sent a changed edit of a Python cell's code through python.set (EditState.code_edit and commit_code_edit): every such edit now starts in Python mode, so that route is removed, and leaving Python mode commits the text as an ordinary entry through cell.set. The UI tests of that route commit with Ctrl+Enter, since Enter starts a new line in Python mode; the guard that keeps a column completion out of a code edit now reads Python mode. The keys are applied in input order with the typing, on main's apply_in_order: the code editor takes a set of stop keys from its caller (apply_in_order_until), so Ctrl+Enter, Esc and the shortcut act on the text as it was when they were pressed. After a key the text field acts on (select all), a stop key waits for the next pass with the editor's keys before it, so select all, Tab and Ctrl+Enter in one frame commits the indented code. Keyboard events after a stop key in the same pass are dropped. code_field is the editor without its frame, for the bar. The PY badge beside fx shows in Python mode and on a selected Python cell; clicking it switches Python mode as the shortcut does. ui.pythonMode {on?} does the same from the control channel, without a dialog, and engine.commands lists it. ui.inspect reports editor.python, and leaves a Python draft's code out (text null, hidden true) while its cell's code is hidden on a protected sheet, checked when it is called: a protection command earlier in the same batch runs before the next frame. Theme tokens badge_py, on_badge, sheet_py_tag and on_sheet_py_tag join the contrast test. A draft, Python or not, records its sheet's identity (Sheet::uid) when it starts and follows that sheet, since commands that run while it is open (sheet.move, home.insertSheet, home.deleteSheet, their undo, a macro, a tool that adds a report sheet) change which sheet sits at its index. A commit goes to the draft's own sheet; before, a draft committed after a sheet was inserted before its own went into the inserted sheet, an ordinary draft as well. A draft whose sheet is gone is dropped with a message, as one whose cell protection locks is. ui.inspect checks for hidden code on the draft's own sheet, and once that sheet is gone it leaves out a Python draft, or one that began on a Python cell.
138 lines
5.5 KiB
Rust
138 lines
5.5 KiB
Rust
//! Editing a Python cell's content (F2, the formula bar) edits its code, in Python mode; typing
|
|
//! over it replaces it, as typing over a formula does.
|
|
|
|
use egui::{Event, Key, Modifiers};
|
|
use gridcraft_engine::Session;
|
|
use gridcraft_engine::core::{CellRef, Value};
|
|
use gridcraft_ui_egui::SheetApp;
|
|
use serde_json::json;
|
|
|
|
type Harness = egui_kittest::Harness<'static, SheetApp>;
|
|
|
|
fn harness() -> Harness {
|
|
let mut session = Session::new();
|
|
session.new_workbook();
|
|
session.execute("python.set", json!({"cell": "A1", "code": "1 + 1", "output": "object"})).unwrap();
|
|
session.execute("selection.set", json!({"range": "A1"})).unwrap();
|
|
crate::common::harness_with(crate::common::builder().with_size(egui::vec2(1200.0, 800.0)), crate::common::app(session), 4)
|
|
}
|
|
|
|
fn text(h: &mut Harness, text: &str) {
|
|
h.input_mut().events.push(Event::Text(text.into()));
|
|
h.run_steps(2);
|
|
}
|
|
|
|
fn key_with(h: &mut Harness, key: Key, modifiers: Modifiers) {
|
|
for pressed in [true, false] {
|
|
h.input_mut().events.push(Event::Key { key, physical_key: None, pressed, repeat: false, modifiers });
|
|
}
|
|
h.run_steps(2);
|
|
}
|
|
|
|
fn key(h: &mut Harness, key: Key) {
|
|
key_with(h, key, Modifiers::NONE);
|
|
}
|
|
|
|
/// Ctrl+Enter (Cmd+Enter on macOS): commits Python mode, where Enter starts a new line.
|
|
fn commit(h: &mut Harness) {
|
|
key_with(h, Key::Enter, Modifiers::COMMAND);
|
|
}
|
|
|
|
/// A1's code and return type (true: a Python object), or `None` when it isn't a Python cell.
|
|
fn python(h: &Harness) -> Option<(String, bool)> {
|
|
let sh = h.state().session.active().unwrap().wb.active().unwrap();
|
|
sh.cell(CellRef::parse("A1").unwrap()).and_then(|c| c.py.as_deref()).map(|p| (p.code.clone(), p.object))
|
|
}
|
|
|
|
fn select_a1(h: &mut Harness) {
|
|
h.state_mut().session.execute("selection.set", json!({"range": "A1"})).unwrap();
|
|
h.run_steps(2);
|
|
}
|
|
|
|
#[test]
|
|
fn f2_edits_a_python_cells_code_and_typing_replaces_it() {
|
|
let mut h = harness();
|
|
let undo_depth = |h: &Harness| h.state().session.active().unwrap().undo.len();
|
|
let before = undo_depth(&h);
|
|
|
|
// F2 and a commit without a change writes nothing.
|
|
key(&mut h, Key::F2);
|
|
assert_eq!(h.state().editor.as_ref().map(|e| (e.text.as_str(), e.python)), Some(("1 + 1", true)));
|
|
commit(&mut h);
|
|
assert!(h.state().editor.is_none());
|
|
assert_eq!(undo_depth(&h), before, "an unchanged edit is no undo step");
|
|
assert_eq!(python(&h), Some(("1 + 1".into(), true)));
|
|
|
|
// A change is new code for the same Python cell, which keeps its return type.
|
|
select_a1(&mut h);
|
|
key(&mut h, Key::F2);
|
|
text(&mut h, " + 2");
|
|
commit(&mut h);
|
|
assert!(h.state().editor.is_none(), "{:?}", h.state().message);
|
|
assert_eq!(python(&h), Some(("1 + 1 + 2".into(), true)), "the cell stays a Python cell with the new code");
|
|
|
|
// Typing over the cell replaces it.
|
|
select_a1(&mut h);
|
|
text(&mut h, "5");
|
|
key(&mut h, Key::Enter);
|
|
assert_eq!(python(&h), None);
|
|
assert_eq!(h.state().session.active().unwrap().wb.active().unwrap().value(CellRef::parse("A1").unwrap()), Value::Number(5.0));
|
|
}
|
|
|
|
#[test]
|
|
fn deleting_all_the_code_clears_the_cell() {
|
|
let mut h = harness();
|
|
let a1 = |h: &Harness| h.state().session.active().unwrap().wb.active().unwrap().value(CellRef::parse("A1").unwrap());
|
|
// Code of spaces is still code, for the same Python cell.
|
|
key(&mut h, Key::F2);
|
|
h.state_mut().editor.as_mut().unwrap().text = " ".into();
|
|
assert!(h.state_mut().commit_edit(0, 0, false, false));
|
|
assert_eq!(python(&h), Some((" ".into(), true)));
|
|
// No code at all clears the cell, as an empty entry clears any other cell.
|
|
select_a1(&mut h);
|
|
key(&mut h, Key::F2);
|
|
h.state_mut().editor.as_mut().unwrap().text.clear();
|
|
assert!(h.state_mut().commit_edit(0, 0, false, false));
|
|
assert!(h.state().editor.is_none());
|
|
assert_eq!(python(&h), None, "the cell is no longer a Python cell");
|
|
assert_eq!(a1(&h), Value::Empty);
|
|
h.state_mut().run("edit.undo", json!({})).unwrap();
|
|
assert_eq!(python(&h), Some((" ".into(), true)), "one undo step brings the code back");
|
|
}
|
|
|
|
#[test]
|
|
fn deleting_all_the_code_clears_a_python_cell_with_no_cached_value() {
|
|
let mut h = harness();
|
|
// A Python cell from a file that cached no value (the value is empty, the code is not).
|
|
{
|
|
let d = h.state_mut().session.doc_mut().unwrap();
|
|
let mut wb = (*d.wb).clone();
|
|
let i = wb.active_sheet();
|
|
let sh = wb.sheet_mut(i).unwrap();
|
|
let at = CellRef::parse("A1").unwrap();
|
|
let mut cell = sh.cell(at).cloned().unwrap();
|
|
cell.value = Value::Empty;
|
|
sh.set_cell(at, cell);
|
|
d.wb = std::sync::Arc::new(wb);
|
|
}
|
|
h.run_steps(2);
|
|
key(&mut h, Key::F2);
|
|
h.state_mut().editor.as_mut().unwrap().text.clear();
|
|
assert!(h.state_mut().commit_edit(0, 0, false, false));
|
|
assert!(h.state().editor.is_none());
|
|
assert_eq!(python(&h), None, "the code is removed, not kept as an unchanged empty cell");
|
|
}
|
|
|
|
#[test]
|
|
fn a_code_edit_is_never_completed_from_the_column() {
|
|
let mut h = harness();
|
|
// A2 holds text that starts with A1's code: typing at the end of the code would offer it.
|
|
h.state_mut().session.execute("cell.set", json!({"cell": "A2", "input": "1 + 1 and more"})).unwrap();
|
|
select_a1(&mut h);
|
|
key(&mut h, Key::F2);
|
|
text(&mut h, " ");
|
|
assert_eq!(h.state().editor.as_ref().and_then(|e| e.completion.clone()), None, "no completion while code is edited");
|
|
commit(&mut h);
|
|
assert_eq!(python(&h), Some(("1 + 1 ".into(), true)), "the typed code is kept");
|
|
}
|