mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-30 13:46:47 -06:00
@@ -104,7 +104,7 @@ struct OpenGroup {
|
|||||||
redo: Vec<HistoryEntry>,
|
redo: Vec<HistoryEntry>,
|
||||||
/// Where the recording was when the group began (`None` when nothing was recording): a
|
/// Where the recording was when the group began (`None` when nothing was recording): a
|
||||||
/// rollback takes back the steps recorded since, as it takes back their edits.
|
/// rollback takes back the steps recorded since, as it takes back their edits.
|
||||||
recorded: Option<(usize, usize)>,
|
recorded: Option<(u64, u64)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Where the last [`Session::group_end`] or [`Session::group_rollback`] left its document: the
|
/// Where the last [`Session::group_end`] or [`Session::group_rollback`] left its document: the
|
||||||
|
|||||||
+39
-14
@@ -38,8 +38,11 @@ pub struct RecordedStep {
|
|||||||
pub params: Json,
|
pub params: Json,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Numbers each recording, so an undo group's mark from an earlier one isn't read as this one's.
|
||||||
|
static NEXT_RECORDING: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
|
||||||
|
|
||||||
/// The steps recorded since recording started.
|
/// The steps recorded since recording started.
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Recorder {
|
pub struct Recorder {
|
||||||
pub steps: Vec<RecordedStep>,
|
pub steps: Vec<RecordedStep>,
|
||||||
/// Steps not kept because the recording reached [`RECORD_LIMIT`] or [`RECORD_MAX_BYTES`].
|
/// Steps not kept because the recording reached [`RECORD_LIMIT`] or [`RECORD_MAX_BYTES`].
|
||||||
@@ -48,6 +51,18 @@ pub struct Recorder {
|
|||||||
pub dropped: usize,
|
pub dropped: usize,
|
||||||
/// The size of `steps` serialized as JSON.
|
/// The size of `steps` serialized as JSON.
|
||||||
bytes: usize,
|
bytes: usize,
|
||||||
|
/// Which recording this is, and the number the next kept step gets: each step's number is in
|
||||||
|
/// `seqs`, beside it, so a mark stays right however many steps rollbacks take out.
|
||||||
|
generation: u64,
|
||||||
|
next_seq: u64,
|
||||||
|
seqs: Vec<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Recorder {
|
||||||
|
fn default() -> Self {
|
||||||
|
let generation = NEXT_RECORDING.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
Recorder { steps: Vec::new(), dropped: 0, bytes: 0, generation, next_seq: 0, seqs: Vec::new() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Recorder {
|
impl Recorder {
|
||||||
@@ -63,28 +78,38 @@ impl Recorder {
|
|||||||
if self.bytes.saturating_add(size) <= RECORD_MAX_BYTES {
|
if self.bytes.saturating_add(size) <= RECORD_MAX_BYTES {
|
||||||
self.bytes = self.bytes.saturating_add(size);
|
self.bytes = self.bytes.saturating_add(size);
|
||||||
self.steps.push(step);
|
self.steps.push(step);
|
||||||
|
self.seqs.push(self.next_seq);
|
||||||
|
self.next_seq = self.next_seq.saturating_add(1);
|
||||||
} else {
|
} else {
|
||||||
self.dropped = self.dropped.saturating_add(1);
|
self.dropped = self.dropped.saturating_add(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Where the recording is now, for [`Recorder::cut_back`].
|
/// Where the recording is now, for [`Recorder::cut_back`]: this recording and the number its
|
||||||
pub(crate) fn mark(&self) -> (usize, usize) {
|
/// next step gets.
|
||||||
(self.steps.len(), self.dropped)
|
pub(crate) fn mark(&self) -> (u64, u64) {
|
||||||
|
(self.generation, self.next_seq)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Takes back the steps recorded in the document with uid `doc` since `mark` (an undo group
|
/// 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
|
/// 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.
|
/// other documents stay: their edits do too. A mark from another recording (the recording
|
||||||
pub(crate) fn cut_back(&mut self, (len, _): (usize, usize), doc: u64) {
|
/// started again inside the group) means every step of this one came after it. Steps the
|
||||||
let before = self.steps.len();
|
/// buffer dropped stay counted.
|
||||||
let mut i = 0;
|
pub(crate) fn cut_back(&mut self, (generation, seq): (u64, u64), doc: u64) {
|
||||||
self.steps.retain(|s| {
|
let from = if generation == self.generation { seq } else { 0 };
|
||||||
i += 1;
|
let gone = |i: usize, step: &RecordedStep| step.doc == doc && self.seqs.get(i).is_some_and(|&n| n >= from);
|
||||||
i <= len || s.doc != doc
|
if !self.steps.iter().enumerate().any(|(i, s)| gone(i, s)) {
|
||||||
});
|
return;
|
||||||
if self.steps.len() != before {
|
|
||||||
self.bytes = self.steps.iter().map(|s| serde_json::to_vec(s).map_or(0, |v| v.len())).sum();
|
|
||||||
}
|
}
|
||||||
|
let (mut steps, mut seqs) = (Vec::with_capacity(self.steps.len()), Vec::with_capacity(self.seqs.len()));
|
||||||
|
for (i, step) in self.steps.iter().enumerate() {
|
||||||
|
if !gone(i, step) {
|
||||||
|
steps.push(step.clone());
|
||||||
|
seqs.push(self.seqs.get(i).copied().unwrap_or(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(self.steps, self.seqs) = (steps, seqs);
|
||||||
|
self.bytes = self.steps.iter().map(|s| serde_json::to_vec(s).map_or(0, |v| v.len())).sum();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -857,6 +857,34 @@ fn stop_action_runs_only_on_its_own_and_is_not_recorded() {
|
|||||||
assert_eq!(s.stop_recording().unwrap().dropped, 0);
|
assert_eq!(s.stop_recording().unwrap().dropped, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rollbacks_take_back_their_own_recorded_steps_in_any_order() {
|
||||||
|
// Groups open on two workbooks, each with a recorded edit: rolling back the first takes out
|
||||||
|
// only its edit, and the second's mark still finds its own after that.
|
||||||
|
let mut s = s();
|
||||||
|
s.start_recording();
|
||||||
|
let a = s.active_index();
|
||||||
|
let _in_a = s.group_begin("A").unwrap();
|
||||||
|
set(&mut s, "A1", "a");
|
||||||
|
s.new_workbook();
|
||||||
|
let b = s.active_index();
|
||||||
|
let _in_b = s.group_begin("B").unwrap();
|
||||||
|
set(&mut s, "B1", "b");
|
||||||
|
s.set_active(a);
|
||||||
|
s.execute("automate.stopAction", json!({})).unwrap();
|
||||||
|
let cells = |s: &Session| s.recorder.as_ref().unwrap().steps.iter().map(|st| st.params["cell"].clone()).collect::<Vec<_>>();
|
||||||
|
assert_eq!(cells(&s), [json!("B1")]);
|
||||||
|
s.set_active(b);
|
||||||
|
s.execute("automate.stopAction", json!({})).unwrap();
|
||||||
|
assert!(cells(&s).is_empty());
|
||||||
|
// A recording started again inside a group: every step of it came after the group began.
|
||||||
|
let _again = s.group_begin("Again").unwrap();
|
||||||
|
s.start_recording();
|
||||||
|
set(&mut s, "C1", "c");
|
||||||
|
s.execute("automate.stopAction", json!({})).unwrap();
|
||||||
|
assert!(cells(&s).is_empty(), "{:?}", cells(&s));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_run_that_edits_and_opens_a_dialog_is_recorded() {
|
fn a_run_that_edits_and_opens_a_dialog_is_recorded() {
|
||||||
// A script with an edit before a menu-style step: the edit happened, so the run is a step of
|
// A script with an edit before a menu-style step: the edit happened, so the run is a step of
|
||||||
|
|||||||
Reference in New Issue
Block a user