diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index fc859e16a..1dc9196d4 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -457,8 +457,11 @@ impl Session { let mut label = spec.label.trim_end_matches('…').to_string(); if own_groups && let Some((_, len)) = start { // A command whose only step is its group's keeps that name ("Run Macro: Totals"). + // The step must have been made by a group this command ended (its epoch is + // past the one the command started with): an older record at the same length + // belongs to an earlier command, undone since. if d.undo.len() == len + 1 - && self.group_step.is_some_and(|(doc, n, epoch)| doc == uid && n == len + 1 && epoch != group_epoch) + && self.group_step.is_some_and(|(doc, n, epoch)| doc == uid && n == len + 1 && epoch > group_epoch) && let Some(last) = d.undo.last() { label = last.label.clone(); @@ -567,6 +570,10 @@ impl Session { d.undo.truncate(g.start); d.redo = g.redo; d.revision += 1; + // A step the rollback dropped is no longer anyone's to name. + if self.group_step.is_some_and(|(doc, n, _)| doc == g.doc && n > g.start) { + self.group_step = None; + } Ok(()) } diff --git a/crates/engine/src/tests/seams.rs b/crates/engine/src/tests/seams.rs index 16b2617d7..e1aaaba67 100644 --- a/crates/engine/src/tests/seams.rs +++ b/crates/engine/src/tests/seams.rs @@ -257,6 +257,27 @@ fn a_command_that_runs_its_own_group_makes_one_step() { assert!(v(&s, "B2").is_empty()); } +#[test] +fn a_stale_group_step_does_not_name_a_later_command() { + // The record of the last group step (document, length, epoch) survives the Undo of that step. + // A later command that ends a group of its own at the same history length, after rolling + // another back, must not take the name of the nested edit it made. + register_group_commands(); + let mut s = s(); + s.execute("test.onlyGroup", json!({})).unwrap(); + assert_eq!(undo_labels(&s), ["Named Step"]); + s.execute("edit.undo", json!({})).unwrap(); + s.execute("test.failThenEdit", json!({})).unwrap(); + assert_eq!(undo_labels(&s), ["test.failThenEdit"]); + s.execute("edit.undo", json!({})).unwrap(); + s.execute("test.failThenEdit", json!({})).unwrap(); + assert_eq!(undo_labels(&s), ["test.failThenEdit"]); + assert!(v(&s, "B1").is_empty()); + assert_eq!(v(&s, "B2").display(), "kept"); + s.execute("edit.undo", json!({})).unwrap(); + assert!(v(&s, "B2").is_empty() && v(&s, "D1").is_empty()); +} + /// Fills the undo history to the limit with edits of Z1 ("v0" to "v99"), and returns its snapshots. fn fill_history(s: &mut Session) -> Vec> { for i in 0..crate::UNDO_LIMIT {