From bbf1bc441ba1c382187e5854cb4617c27b3004c0 Mon Sep 17 00:00:00 2001 From: prxnvt <53838599+prxnvt@users.noreply.github.com> Date: Wed, 21 Oct 2026 17:28:03 -0400 Subject: [PATCH] Engine: stopping a recording waits while a macro in its workbook is being debugged Stopping a recording saved the macro with a nested macro.save while a debugged run's undo group could be open on the same workbook. Stopping the run without keeping its changes then deleted the macro that macro.record had reported as saved. Stopping now fails with "Finish or stop the macro first" under macro.record before anything changes, and the recording goes on, as when a save fails for another reason, so stopping again after the run ends saves it. --- crates/engine/src/cmd/macros.rs | 9 +++++++-- crates/engine/src/tests/macros.rs | 19 +++++++++++++++++++ docs/macros.md | 22 +++++++++++----------- 3 files changed, 37 insertions(+), 13 deletions(-) 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