diff --git a/crates/engine/src/cmd/edit.rs b/crates/engine/src/cmd/edit.rs index aaf44a7..4b2dc2d 100644 --- a/crates/engine/src/cmd/edit.rs +++ b/crates/engine/src/cmd/edit.rs @@ -33,7 +33,17 @@ pub fn specs() -> Vec { ] } +/// A programmatic undo/redo (`engine.execute`, MCP, control channel, scripts) first ends a running +/// command the way Esc does, exactly as `Session::start` does for Cmd+Z, the menu or the command +/// line. Otherwise it would swap the drawing under the command and pop the previous command's step. +fn end_running_command(s: &mut Session) { + if s.running.is_some() { + s.cancel(); + } +} + fn run_undo(s: &mut Session, p: &Value) -> Result { + end_running_command(s); let n = p.get("count").and_then(Value::as_u64).unwrap_or(1).clamp(1, 1000); let mut labels = Vec::new(); for _ in 0..n { @@ -49,6 +59,7 @@ fn run_undo(s: &mut Session, p: &Value) -> Result { } fn run_redo(s: &mut Session, p: &Value) -> Result { + end_running_command(s); let n = p.get("count").and_then(Value::as_u64).unwrap_or(1).clamp(1, 1000); let mut labels = Vec::new(); for _ in 0..n { diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 8296141..934954e 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -34,6 +34,28 @@ fn line_undo_option_and_aliases() { assert!(s.running.is_none()); } +#[test] +fn programmatic_undo_during_a_command_ends_it_first() { + let mut s = Session::new(); + s.execute("line", &json!({"points": [[0, 0], [10, 0]]})).unwrap(); + // TEXT stays active, waiting for the next line. + s.script("TEXT 0,0 5 0 HELLO\n").unwrap(); + assert!(s.running.is_some()); + assert_eq!(kinds(&s), vec!["Line", "Text"]); + // Like Ctrl+Z during a command: end it (as Esc does), then undo it, not the line. + s.execute("undo", &json!({})).unwrap(); + assert!(s.running.is_none()); + assert_eq!(kinds(&s), vec!["Line"]); + s.execute("redo", &json!({})).unwrap(); + assert_eq!(kinds(&s), vec!["Line", "Text"]); + // Redo while a command runs ends it the same way and leaves the drawing alone. + s.script("TEXT 0,-10 5 0\n").unwrap(); + assert!(s.running.is_some()); + s.execute("redo", &json!({})).unwrap(); + assert!(s.running.is_none()); + assert_eq!(kinds(&s), vec!["Line", "Text"]); +} + #[test] fn polar_and_direct_distance() { let mut s = Session::new();