mirror of
https://github.com/storytold/gridcraft.git
synced 2026-11-03 00:41:56 -06:00
doesn't run it
python.set {cell?, code, output?: object|value} makes the cell (the
active cell by default) a Python cell, keeping its style and taking a
formula and its spill away. `output` is the return type Microsoft
documents for PY(): an Excel value (the default) or a Python object;
without it a Python cell keeps the one it had. The code must fit in
Excel's 8,192-character formula once written as PY(...), quotes doubled
and literals joined, so the command checks the encoded formula rather
than a character count. A table's header row refuses Python (it holds
the column's name, as Excel keeps formulas out of headers), and a locked
cell on a protected sheet refuses it through the usual protection check.
Nothing runs Python here. Unchanged code keeps its cached result and
value; new or changed code shows #BLOCKED!. One undo step takes the cell
back.
Sheet::clear_spill removes the spilled values inside a range with work
bounded by the values stored, not by the range's size.
In the interface, F2, a double click or the formula bar on a Python cell
edits its code, and a changed text is committed through python.set,
which keeps the return type; through cell.set it would have replaced the
Python cell with an ordinary cell holding the code as text. Typing over
a Python cell replaces it, as typing over a formula does, and committing
the code unchanged writes nothing. Deleting all the code clears the
cell through cell.set, as an empty entry clears any other cell; code
of spaces is still code.
Tests: the code and return type in cell.get, #BLOCKED! for new code,
cached results kept for unchanged code, the active-cell default, undo,
refused params (missing or non-text code, a bad cell, a bad return
type), the encoded limit at 8,079 and 8,080 characters and with doubled
quotes, a table header, a protected sheet, and a formula's spill taken
away. A UI test edits a Python cell's code with F2 and Enter, commits it
unchanged, and types over it; another clears a Python cell by deleting
all its code, keeps code of spaces, and undoes the clearing.
116 lines
4.6 KiB
Rust
116 lines
4.6 KiB
Rust
//! Editing a Python cell's content (F2, the formula bar) edits its code; 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(h: &mut Harness, key: Key) {
|
|
for pressed in [true, false] {
|
|
h.input_mut().events.push(Event::Key { key, physical_key: None, pressed, repeat: false, modifiers: Modifiers::NONE });
|
|
}
|
|
h.run_steps(2);
|
|
}
|
|
|
|
/// 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 Enter without a change writes nothing.
|
|
key(&mut h, Key::F2);
|
|
assert_eq!(h.state().editor.as_ref().map(|e| e.text.as_str()), Some("1 + 1"));
|
|
key(&mut h, Key::Enter);
|
|
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");
|
|
key(&mut h, Key::Enter);
|
|
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");
|
|
}
|