ravenrs_sheet/engine/cmd/
python.rs1use std::sync::Arc;
6
7use crate::base::{CellRef, Value};
8use crate::model::{Cell, PyCode, python::MAX_FORMULA_CHARS};
9use serde_json::{Value as Json, json};
10
11use super::{CommandSpec, bad, cell_param_on, cmd, edit, has_doc, str_param, target_sheet};
12use crate::engine::{EngineError, Result, Session};
13
14pub fn specs() -> Vec<CommandSpec> {
15 vec![
16 cmd!(
17 "python.set",
18 "Set Python Code",
19 [],
20 None,
21 "{cell?, code, output?: object|value} makes the cell (default: the active cell) a Python cell with this code; the code must fit in Excel's 8,192-character formula once written as PY(…). `output` is the return type: `value` (an Excel value, the default) or `object` (a Python object)",
22 has_doc,
23 set
24 ),
25 cmd!(
26 "python.output",
27 "Python Output",
28 [],
29 Some("Ctrl+Alt+Shift+M"),
30 "{cell?, mode?: object|value} sets the return type of a Python cell (default: the active cell): `object` (a Python object) or `value` (an Excel value). Without `mode` it switches to the other one. The cell keeps its code; a cached result of the old return type goes, as with `python.set`, so the cell shows #BLOCKED! until it runs",
31 has_doc,
32 output
33 ),
34 ]
35}
36
37fn target_cell(s: &Session, p: &Json) -> Result<(usize, CellRef)> {
38 let sheet = target_sheet(s, p)?;
39 let at = match cell_param_on(s, p, "cell", sheet)? {
40 Some(c) => c,
41 None => s.doc()?.selection.active,
42 };
43 Ok((sheet, at))
44}
45
46fn existing(s: &Session, sheet: usize, at: CellRef) -> Result<Option<PyCode>> {
47 Ok(s.doc()?.wb.sheet(sheet).and_then(|sh| sh.cell(at)).and_then(|c| c.py.as_deref()).cloned())
48}
49
50fn output_mode(cmd: &str, p: &Json, key: &str) -> Result<Option<bool>> {
51 match p.get(key) {
52 None | Some(Json::Null) => return Ok(None),
53 Some(Json::String(_)) => {}
54 Some(_) => return Err(bad(cmd, format!("`{key}` is `object` or `value`"))),
55 }
56 match str_param(p, key) {
57 None => Ok(None),
58 Some(m) if m.eq_ignore_ascii_case("object") => Ok(Some(true)),
59 Some(m) if m.eq_ignore_ascii_case("value") => Ok(Some(false)),
60 Some(m) => Err(bad(cmd, format!("`{key}` is `object` or `value`, not `{m}`"))),
61 }
62}
63
64fn set(s: &mut Session, p: &Json) -> Result<Json> {
65 let (sheet, at) = target_cell(s, p)?;
66 let code = match p.get("code") {
67 Some(Json::String(c)) => c.as_str(),
68 Some(_) => return Err(bad("python.set", "`code` must be text")),
69 None => return Err(bad("python.set", "missing `code`")),
70 };
71 let mode = output_mode("python.set", p, "output")?;
72 let old = existing(s, sheet, at)?;
73 let object = mode.or(old.as_ref().map(|o| o.object)).unwrap_or(false);
74 store(s, "python.set", sheet, at, PyCode { object, ..PyCode::new(code) }, old)?;
75 let d = s.doc()?;
76 let shown = d.wb.sheet(sheet).map(|sh| sh.value(at)).unwrap_or_default();
77 Ok(json!({"cell": at.a1(), "value": crate::engine::cmd::inspect::value_json(&shown)}))
78}
79
80fn store(s: &mut Session, cmd: &str, sheet: usize, at: CellRef, mut py: PyCode, old: Option<PyCode>) -> Result<()> {
84 if !py.fits() {
85 return Err(bad(
86 cmd,
87 format!("the code doesn't fit in one cell: written as PY(…) it would be longer than Excel's {MAX_FORMULA_CHARS}-character formula limit"),
88 ));
89 }
90 let header =
92 s.doc()?.wb.sheet(sheet).is_some_and(|sh| sh.tables.iter().any(|t| t.header_row && t.range.start.row == at.row && t.range.contains(at)));
93 if header {
94 return Err(EngineError::Other(format!("{} is a table header; Python cells can't go in a table's header row", at.a1())));
95 }
96 let keep = old.filter(|o| o.code == py.code && o.object == py.object);
100 let value = match &keep {
101 Some(o) => {
102 py.output = o.output.clone();
103 py.stale = o.stale;
104 None
105 }
106 None => Some(crate::model::python::BLOCKED),
107 };
108 edit(s, |cx| {
109 let sh = cx.sheet_mut(sheet)?;
110 let style = sh.style_id(at);
111 let value = value.unwrap_or_else(|| sh.cell(at).map(|c| c.value.clone()).unwrap_or(Value::Empty));
112 sh.set_cell(at, Cell { value, formula: None, style, py: Some(Arc::new(py)) });
115 cx.touch(sheet, at);
116 Ok(())
117 })
118}
119
120fn output(s: &mut Session, p: &Json) -> Result<Json> {
124 let (sheet, at) = target_cell(s, p)?;
125 let Some(old) = existing(s, sheet, at)? else {
126 return Err(EngineError::Other(format!("{} isn't a Python cell, so it has no Python output to change", at.a1())));
127 };
128 let object = output_mode("python.output", p, "mode")?.unwrap_or(!old.object);
129 let mode = |object: bool| if object { "object" } else { "value" };
130 if object != old.object {
131 let py = PyCode { code: old.code.clone(), object, output: None, stale: false };
132 store(s, "python.output", sheet, at, py, Some(old))?;
133 }
134 let d = s.doc()?;
135 let shown = d.wb.sheet(sheet).map(|sh| sh.value(at)).unwrap_or_default();
136 Ok(json!({"cell": at.a1(), "output": mode(object), "value": crate::engine::cmd::inspect::value_json(&shown)}))
137}