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.
This commit is contained in:
prxnvt
2026-10-22 01:36:01 -04:00
parent 0457f102a0
commit eb3eaa052d
2 changed files with 29 additions and 1 deletions
+8 -1
View File
@@ -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(())
}
+21
View File
@@ -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<std::sync::Arc<gridcraft_model::Workbook>> {
for i in 0..crate::UNDO_LIMIT {