diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 4c848ecbc..423f69f13 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -1998,7 +1998,8 @@ impl Session { deselect_guides(&st.doc, &mut st.selection); } st.revision += 1; - journal = Some(g.journal); + // The journal goes back to where the first of the canceled groups began, whatever the order of the documents. + journal = Some(journal.map_or(g.journal, |len: usize| len.min(g.journal))); } } if let Some(len) = journal { diff --git a/crates/engine/src/tests_journal.rs b/crates/engine/src/tests_journal.rs index 2de23ea0c..a32cf801b 100644 --- a/crates/engine/src/tests_journal.rs +++ b/crates/engine/src/tests_journal.rs @@ -343,6 +343,47 @@ fn a_failed_batch_gives_back_the_highlighted_rows_and_the_current_layer() { assert_eq!(documents(&r), documents(&s)); } +/// Canceling undo groups open in two documents undoes the edits of both and cuts the journal back +/// to where the first of them began, whichever document comes first in the list. +#[test] +fn canceling_undo_groups_in_two_documents_drops_the_edits_of_both_from_the_journal() { + let mut s = Session::new(); + s.execute("file.new", &json!({"created": null})).unwrap(); + s.execute("file.new", &json!({"created": null})).unwrap(); + let objects = |s: &Session| s.documents().iter().map(|d| d.doc.node_count()).collect::>(); + let before = objects(&s); + // The first document's group begins first, and the second document's group begins after it. + let journal = s.journal.len(); + assert!(s.set_active(0)); + s.begin_undo_group(); + s.execute("shape.rectangle", &rect_step(10)["params"]).unwrap(); + assert!(s.set_active(1)); + s.begin_undo_group(); + s.execute("shape.rectangle", &rect_step(50)["params"]).unwrap(); + s.end_undo_group(true); + assert_eq!(objects(&s), before, "both documents' edits are undone"); + assert_eq!(s.journal.len(), journal, "the journal holds neither group's edits"); + assert_eq!(documents(&replayed(&s)), documents(&s)); + + // The commands do the same: neither group's begin nor its edit stays in the journal. + let journal = s.journal.len(); + for (id, p) in [ + ("document.activate", json!({"index": 0})), + ("edit.undoGroup.begin", json!({})), + ("shape.rectangle", rect_step(10)["params"].clone()), + ("document.activate", json!({"index": 1})), + ("edit.undoGroup.begin", json!({})), + ("shape.rectangle", rect_step(50)["params"].clone()), + ("edit.undoGroup.end", json!({"cancel": true})), + ] { + s.execute(id, &p).unwrap(); + } + assert_eq!(objects(&s), before); + let ids = s.journal.since(journal).iter().map(|(id, _)| id.as_str()).collect::>(); + assert!(!ids.iter().any(|id| matches!(*id, "edit.undoGroup.begin" | "shape.rectangle")), "{ids:?}"); + assert_eq!(ids.last(), Some(&"edit.undoGroup.end")); +} + /// The edits made in an undo group (a scrubbed numeric field, #400) are one undo step, journaled /// one by one; a cancelled group undoes them and leaves the journal as it was. #[test]