From 2287f63a95139db4cb4d0d42e8c238e6f34319a5 Mon Sep 17 00:00:00 2001 From: prxnvt <53838599+prxnvt@users.noreply.github.com> Date: Thu, 22 Oct 2026 02:00:54 -0400 Subject: [PATCH] Engine: a group rolled back inside a command keeps the redo history A command that changed the workbook cleared the redo history even when an open group owned its step. When the group was then rolled back inside the same command (the debugger's Stop with Undo Changes), the rollback put the redo history back and the command's bookkeeping cleared it again. Now only a command that makes a step of its own clears the redo history; a group clears it when it ends as a step, and a rollback restores it. --- crates/engine/src/lib.rs | 4 +++- crates/engine/src/tests/seams.rs | 26 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 1dc9196d4..80bcfd5ae 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -469,8 +469,10 @@ impl Session { d.undo.truncate(len); } d.undo.push(HistoryEntry { label, wb, selection: sel }); + d.redo.clear(); } - d.redo.clear(); + // Inside a group the redo history waits for the group: `group_end` clears it when the + // group becomes a step, and `group_rollback` puts it back as it was. d.revision += 1; } // The history limit applies once the outermost command has collapsed its steps, so the diff --git a/crates/engine/src/tests/seams.rs b/crates/engine/src/tests/seams.rs index e1aaaba67..b3397c724 100644 --- a/crates/engine/src/tests/seams.rs +++ b/crates/engine/src/tests/seams.rs @@ -226,6 +226,13 @@ fn register_group_commands() { }); // A command that is one group keeps the group's name. register("test.onlyGroup", |s, _| s.grouped("Named Step", |s| put(s, "D1", "x")).map(|_| json!({}))); + // An edit, then the rollback of the innermost group that was open before the command. + register("test.rollbackOpenGroup", |s, _| { + put(s, "E1", "gone")?; + let token = s.groups.last().map(|g| crate::GroupToken { id: g.id, doc: g.doc }); + s.group_rollback(token.ok_or_else(|| EngineError::Other("no group".into()))?)?; + Ok(json!({})) + }); // An edit, then the end of the innermost group that was open before the command. register("test.endOpenGroup", |s, _| { put(s, "E1", "end")?; @@ -278,6 +285,25 @@ fn a_stale_group_step_does_not_name_a_later_command() { assert!(v(&s, "B2").is_empty() && v(&s, "D1").is_empty()); } +#[test] +fn a_group_rolled_back_inside_a_command_keeps_the_redo_history() { + // The debugger's Stop with Undo Changes rolls its group back inside a command. The rollback + // put the redo history back, and the command's own bookkeeping then cleared it again. + register_group_commands(); + let mut s = s(); + set(&mut s, "A1", "first"); + s.execute("edit.undo", json!({})).unwrap(); + assert_eq!(s.doc().unwrap().redo.len(), 1); + let token = s.group_begin("Debug").unwrap(); + set(&mut s, "B1", "inside"); + s.execute("test.rollbackOpenGroup", json!({})).unwrap(); + assert!(!s.in_group(token.doc)); + assert!(v(&s, "B1").is_empty() && v(&s, "E1").is_empty()); + assert_eq!(s.doc().unwrap().redo.len(), 1, "the redo history survives the rolled-back group"); + s.execute("edit.redo", json!({})).unwrap(); + assert_eq!(v(&s, "A1").display(), "first"); +} + /// 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 {