diff --git a/crates/engine/src/cmd/macros.rs b/crates/engine/src/cmd/macros.rs index 1af881587..426d06492 100644 --- a/crates/engine/src/cmd/macros.rs +++ b/crates/engine/src/cmd/macros.rs @@ -185,8 +185,9 @@ fn start_recording(s: &mut Session, relative: bool) -> Result { } /// Stops recording and saves what was recorded as a macro, in the workbook where recording -/// started. When saving fails (a bad name, a full workbook) the recording goes on, so stopping -/// again keeps what was recorded; when that workbook was closed, the recording ends unsaved. +/// started. When saving fails (a bad name, a full workbook, a macro being debugged there) the +/// recording goes on, so stopping again keeps what was recorded; when that workbook was closed, +/// the recording ends unsaved. fn stop_recording(s: &mut Session, p: &Json) -> Result { let Some(rec) = s.stop_recording() else { return Ok(json!({"recording": false})) }; let start = s.macro_recording.take(); @@ -209,6 +210,10 @@ fn stop_recording(s: &mut Session, p: &Json) -> Result { /// Saves the recording into the workbook at index `at`. fn save_recording(s: &mut Session, p: &Json, rec: &crate::record::Recorder, start: Option, at: usize) -> Result { let d = s.docs.get(at).ok_or(EngineError::NoDocument)?; + // A macro being debugged there could still roll the saved macro back with its run. + if s.macro_debug.as_ref().is_some_and(|m| m.locks(d.uid)) { + return Err(EngineError::Disabled("macro.record".into(), crate::macros::debug::BUSY.into())); + } let start = start.unwrap_or_else(|| RecordingStart { doc: d.uid, sheet: d.wb.active_sheet, diff --git a/crates/engine/src/tests/macros.rs b/crates/engine/src/tests/macros.rs index 3f0901c94..df6e59606 100644 --- a/crates/engine/src/tests/macros.rs +++ b/crates/engine/src/tests/macros.rs @@ -1163,3 +1163,22 @@ fn steps_past_the_macro_limit_are_counted_as_dropped() { assert_eq!(r["steps"], gridcraft_model::MAX_MACRO_STEPS); assert_eq!(r["dropped"], 8, "{r}"); } + +#[test] +fn a_recording_waits_while_a_macro_in_its_workbook_is_debugged() { + let mut s = s(); + stepper(&mut s); + s.execute("macro.record", json!({"relative": false})).unwrap(); + set(&mut s, "Z1", "typed"); + debug(&mut s, json!({"name": "Walk", "inputs": "B2:C5"})); + debug(&mut s, json!({"action": "step"})); + // Saved now, the macro would go into the run's undo group, and stopping the run without + // keeping its changes would delete it again. + let r = s.execute("macro.record", json!({"stop": true, "name": "Typed"})); + assert!(matches!(&r, Err(EngineError::Disabled(id, why)) if id == "macro.record" && why == crate::macros::debug::BUSY), "{r:?}"); + assert!(s.is_recording()); + debug(&mut s, json!({"action": "stop"})); + let r = s.execute("macro.record", json!({"stop": true, "name": "Typed"})).unwrap(); + assert_eq!(r["saved"], "Typed"); + assert_eq!(recorded(&mut s, "Typed"), ["cell.set {\"cell\":\"Z1\",\"input\":\"typed\"}"]); +} diff --git a/docs/macros.md b/docs/macros.md index 6cc67d4e8..503068707 100644 --- a/docs/macros.md +++ b/docs/macros.md @@ -122,17 +122,17 @@ of the step that just ran, and the cells changed so far. `description` when stopping; a name that another macro has gets a suffix, such as `Tidy (2)`, so stopping never replaces a macro). The macro is saved in the workbook where recording started, even if another one is active; if that workbook was closed, stopping ends the recording without saving it. -When saving the recording fails (a name the check refuses, a workbook at its macro limit), -recording goes on, so stopping again keeps what was recorded. A recording keeps up to 100,000 -commands and a macro up to 10,000 steps; steps past either limit are left out and counted in -`dropped`. With relative references the selection when recording starts is the example input: -references in and near it on its sheet become tokens (on another sheet `C4` stays that sheet's -`C4`), and the input's shape, header and column types are inferred. Toggles such as Bold are recorded with the state they set -(`"on": true`), so playing them back sets them rather than flipping them. Commands a macro may not -run are left out and listed in the result, also from the steps of a macro or script run while -recording; a macro run while recording is replaced by its steps when it has no inputs, no output -and only plain commands (otherwise it is left out and listed), and an Automate script run by the -script's steps. +When saving the recording fails (a name the check refuses, a workbook at its macro limit, a +macro being debugged in that workbook), recording goes on, so stopping again keeps what was +recorded. A recording keeps up to 100,000 commands and a macro up to 10,000 steps; steps past +either limit are left out and counted in `dropped`. With relative references the selection when +recording starts is the example input: references in and near it on its sheet become tokens (on +another sheet `C4` stays that sheet's `C4`), and the input's shape, header and column types are +inferred. Toggles such as Bold are recorded with the state they set (`"on": true`), so playing +them back sets them rather than flipping them. Commands a macro may not run are left out and +listed in the result, also from the steps of a macro or script run while recording; a macro run +while recording is replaced by its steps when it has no inputs, no output and only plain commands +(otherwise it is left out and listed), and an Automate script run by the script's steps. ## Other commands and storage