From ca2be34e8a637d9d32939388b018f6d8a02f3c0b Mon Sep 17 00:00:00 2001 From: prxnvt <53838599+prxnvt@users.noreply.github.com> Date: Sun, 25 Oct 2026 06:29:23 -0400 Subject: [PATCH] Engine: a rolled-back group takes what its script runs ran out of the recording too Taking back the steps a rolled-back group recorded left what a taken-back script or macro run ran (`Recorder::ran`) under its old index, where a later run could pick it up, and left it out of the size count. `cut_back` now drops it with its step, moves a kept run's entry to the step's new index and counts both again. - Test: `a_rolled_back_script_run_takes_what_it_ran_out_of_the_recording`. --- crates/engine/src/record.rs | 27 +++++++++++++++++++-------- crates/engine/src/tests/macros.rs | 20 ++++++++++++++++++++ 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/crates/engine/src/record.rs b/crates/engine/src/record.rs index 413a8f62d..4dfbe82d0 100644 --- a/crates/engine/src/record.rs +++ b/crates/engine/src/record.rs @@ -112,15 +112,26 @@ impl Recorder { /// Takes back the steps recorded in the document with uid `doc` since `mark` (an undo group /// on it was rolled back, so playing them back would make edits it no longer has). Steps in /// other documents stay: their edits do too. Steps the buffer dropped stay counted. + /// What a step ran ([`Recorder::ran`]) goes with its step, and stays with a kept one under its + /// new index. pub(crate) fn cut_back(&mut self, (len, _): (usize, usize), doc: u64) { - let before = self.steps.len(); - let mut i = 0; - self.steps.retain(|s| { - i += 1; - i <= len || s.doc != doc - }); - if self.steps.len() != before { - self.bytes = self.steps.iter().map(|s| serde_json::to_vec(s).map_or(0, |v| v.len())).sum(); + if !self.steps.iter().skip(len).any(|s| s.doc == doc) { + return; } + let mut ran = std::mem::take(&mut self.ran); + let mut steps = Vec::with_capacity(self.steps.len()); + for (i, step) in std::mem::take(&mut self.steps).into_iter().enumerate() { + if i >= len && step.doc == doc { + continue; + } + if let Some(r) = ran.remove(&i) { + self.ran.insert(steps.len(), r); + } + steps.push(step); + } + self.steps = steps; + let size = |v: Result, serde_json::Error>| v.map_or(0, |v| v.len()); + self.bytes = self.steps.iter().map(|s| size(serde_json::to_vec(s))).sum::() + + self.ran.values().map(|r| size(serde_json::to_vec(r))).sum::(); } } diff --git a/crates/engine/src/tests/macros.rs b/crates/engine/src/tests/macros.rs index b4ee076e2..459faa406 100644 --- a/crates/engine/src/tests/macros.rs +++ b/crates/engine/src/tests/macros.rs @@ -2164,6 +2164,26 @@ fn a_script_step_that_only_opened_a_dialog_is_left_out_of_the_recording() { assert!(recorded(&mut s, "Without").is_empty()); } +#[test] +fn a_rolled_back_script_run_takes_what_it_ran_out_of_the_recording() { + // What a recorded script run ran goes with its step when a group rollback takes the step + // back, and a kept run keeps what it ran under its new place. + let mut s = s(); + s.execute("automate.newScript", json!({"name": "Mark", "commands": [{"command": "cell.set", "params": {"cell": "A1", "input": "m"}}]})).unwrap(); + s.execute("macro.record", json!({"relative": false})).unwrap(); + s.execute("automate.runScript", json!({"name": "Mark"})).unwrap(); + let _left = s.group_begin("Left open").unwrap(); + set(&mut s, "B1", "gone"); + s.execute("automate.runScript", json!({"name": "Mark"})).unwrap(); + s.execute("automate.stopAction", json!({})).unwrap(); + let rec = s.recorder.as_ref().unwrap(); + assert_eq!((rec.steps.len(), rec.ran.keys().copied().collect::>()), (1, vec![0])); + s.execute("automate.runScript", json!({"name": "Mark"})).unwrap(); + s.execute("macro.record", json!({"stop": true, "name": "Rec"})).unwrap(); + let a1 = "cell.set {\"cell\":\"A1\",\"input\":\"m\"}"; + assert_eq!(recorded(&mut s, "Rec"), [a1, a1]); +} + #[test] fn a_script_run_records_the_state_its_toggles_set() { let mut s = s();