diff --git a/crates/engine/src/guard.rs b/crates/engine/src/guard.rs index 57cc575fa..41b028f08 100644 --- a/crates/engine/src/guard.rs +++ b/crates/engine/src/guard.rs @@ -36,6 +36,7 @@ impl Session { /// Run command `id` (`f`) inside the guard. pub(crate) fn guarded(&mut self, id: &str, f: impl FnOnce(&mut Session) -> Result) -> Result { let before = self.active().map(|d| (d.uid, d.doc.clone(), d.selection.clone(), d.revision)); + let in_undo_step = self.in_undo_step; match catch_unwind(AssertUnwindSafe(|| f(self))) { Ok(r) => r, Err(payload) => { @@ -54,8 +55,8 @@ impl Session { { log::error!("crash recovery save after `{id}` failed: {e}"); } - // The command never got to say that it is done. - self.in_undo_step = false; + // The command never got to say that it is done; a command that ran it goes on. + self.in_undo_step = in_undo_step; log::error!("command `{id}` panicked: {msg}"); Err(EngineError::Internal(id.to_string(), msg)) } @@ -103,4 +104,24 @@ mod tests { s.execute("frame.create", &json!({"rect": [36, 36, 200, 200]})).unwrap(); assert_eq!(s.active().unwrap().history.undo.len(), 1); } + + /// A command that panics inside an undoable one leaves that one running: what it runs next + /// is still part of its undo step. + #[test] + fn a_panic_in_a_command_run_by_another_leaves_the_outer_undo_step() { + let mut s = Session::new(); + s.execute("file.new", &json!({})).unwrap(); + // An undoable command that runs one that panics, carries on and runs another. + s.guarded("test.outer", |s| { + s.in_undo_step = true; + assert!(s.guarded("test.panic", |_| panic!("boom")).is_err()); + let made = s.execute("frame.create", &json!({"rect": [36, 36, 200, 200]})); + s.in_undo_step = false; + made + }) + .unwrap(); + let st = s.active().unwrap(); + assert_eq!(st.doc.spreads[0].items.len(), 1); + assert_eq!(st.history.undo.len(), 0, "the frame is left to the outer command's step"); + } }