From eb3eaa052db54ae3723569b7dcef2a0b89107846 Mon Sep 17 00:00:00 2001 From: prxnvt <53838599+prxnvt@users.noreply.github.com> Date: Thu, 22 Oct 2026 01:36:01 -0400 Subject: [PATCH] Engine: a command keeps its group's name only for a group it ended itself The record of the last group step (document, history length, epoch) outlived the Undo of that step. A later command that opened and closed its own groups at the same history length, after rolling one back, passed the epoch check (the record's epoch merely differed from the one the command started with) and took the name of the nested edit it had made instead of its own. The step must now come from a group ended during the command (an epoch past the command's starting one), and a rollback drops a record of a step it truncated away. --- crates/engine/src/lib.rs | 9 ++++++++- crates/engine/src/tests/seams.rs | 21 +++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) 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 {