hydrate_model/editor/
undo.rs

1use hydrate_data::DataSetResult;
2use slotmap::DenseSlotMap;
3use std::sync::mpsc;
4use std::sync::mpsc::{Receiver, Sender};
5
6use crate::edit_context::EditContext;
7use crate::{AssetId, DataSet, DataSetDiffSet, EditContextKey, HashSet};
8
9//TODO: Delete unused property data when path ancestor is null or in replace mode
10
11//TODO: Should we make a struct that refs the schema/data? We could have transactions and databases
12// return the temp struct with refs and move all the functions to that
13
14//TODO: Read-only sources? For things like network cache. Could only sync files we edit and overlay
15// files source over net cache source, etc.
16
17#[derive(PartialEq)]
18pub enum EndContextBehavior {
19    Finish,
20    AllowResume,
21}
22
23pub struct CompletedUndoContextMessage {
24    edit_context_key: EditContextKey,
25    diff_set: DataSetDiffSet,
26}
27
28pub struct UndoStack {
29    undo_chain: Vec<CompletedUndoContextMessage>,
30    // Undo/Redo will decrease/increase this value, using apply/revert diffs to move backward and
31    // forward. Appending new diffs will truncate the chain at current position and push a new
32    // step on the chain. Zero means we have undone everything or there are no steps to undo.
33    current_undo_index: usize,
34    completed_undo_context_tx: Sender<CompletedUndoContextMessage>,
35    completed_undo_context_rx: Receiver<CompletedUndoContextMessage>,
36}
37
38impl Default for UndoStack {
39    fn default() -> Self {
40        let (tx, rx) = mpsc::channel();
41        UndoStack {
42            undo_chain: Default::default(),
43            current_undo_index: 0,
44            completed_undo_context_tx: tx,
45            completed_undo_context_rx: rx,
46        }
47    }
48}
49
50impl UndoStack {
51    // This pulls incoming steps off the receive queue. These diffs have already been applied, so
52    // we mainly just use this to drop undone steps that can no longer be used, and to place them
53    // on the end of the chain
54    fn drain_rx(&mut self) {
55        while let Ok(diff) = self.completed_undo_context_rx.try_recv() {
56            self.undo_chain.truncate(self.current_undo_index);
57            self.undo_chain.push(diff);
58            self.current_undo_index += 1;
59        }
60    }
61
62    pub fn undo(
63        &mut self,
64        edit_contexts: &mut DenseSlotMap<EditContextKey, EditContext>,
65    ) -> DataSetResult<()> {
66        //Commit any undo context that might be open
67        for (_, edit_context) in edit_contexts.iter_mut() {
68            edit_context.commit_pending_undo_context();
69        }
70
71        // Flush any pending incoming steps
72        self.drain_rx();
73
74        // Now undo one step, if there is a step to undo
75        // if we undo the first step in the chain (i.e. our undo index is currently 1), we want to
76        // use the revert diff in the 0th index of the chain
77        if self.current_undo_index > 0 {
78            if let Some(current_step) = self.undo_chain.get(self.current_undo_index - 1) {
79                let edit_context = edit_contexts
80                    .get_mut(current_step.edit_context_key)
81                    .unwrap();
82
83                let result = edit_context.apply_diff(&current_step.diff_set.revert_diff);
84                self.current_undo_index -= 1;
85                return result;
86            }
87        }
88
89        Ok(())
90    }
91
92    pub fn redo(
93        &mut self,
94        edit_contexts: &mut DenseSlotMap<EditContextKey, EditContext>,
95    ) -> DataSetResult<()> {
96        // If we have any incoming steps, consume them now
97        self.drain_rx();
98
99        // if we redo the first step in the chain (i.e. our undo index is currently 0), we want to
100        // use the apply diff in the 0th index of the chain. If our current step is == length of
101        // chain, we have no more steps available to redo
102        if let Some(current_step) = self.undo_chain.get(self.current_undo_index) {
103            let edit_context = edit_contexts
104                .get_mut(current_step.edit_context_key)
105                .unwrap();
106            // We don't want anything being written to the undo context at this point, since we're using it
107            edit_context.cancel_pending_undo_context()?;
108            let result = edit_context.apply_diff(&current_step.diff_set.apply_diff);
109            self.current_undo_index += 1;
110            return result;
111        }
112
113        Ok(())
114    }
115}
116
117// Transaction that holds exclusive access for the data and will directly commit changes. It can
118// compare directly against the original dataset for changes
119pub struct UndoContext {
120    edit_context_key: EditContextKey,
121    before_state: DataSet,
122    tracked_assets: HashSet<AssetId>,
123    context_name: Option<&'static str>,
124    completed_undo_context_tx: Sender<CompletedUndoContextMessage>,
125}
126
127impl UndoContext {
128    pub(crate) fn new(
129        undo_stack: &UndoStack,
130        edit_context_key: EditContextKey,
131    ) -> Self {
132        UndoContext {
133            edit_context_key,
134            before_state: Default::default(),
135            tracked_assets: Default::default(),
136            context_name: Default::default(),
137            completed_undo_context_tx: undo_stack.completed_undo_context_tx.clone(),
138        }
139    }
140
141    // Call after adding a new asset
142    pub(crate) fn track_new_asset(
143        &mut self,
144        asset_id: AssetId,
145    ) {
146        if self.context_name.is_some() {
147            self.tracked_assets.insert(asset_id);
148        }
149    }
150
151    // Call before editing or deleting an asset
152    pub(crate) fn track_existing_asset(
153        &mut self,
154        before_state: &DataSet,
155        asset_id: AssetId,
156    ) -> DataSetResult<()> {
157        if self.context_name.is_some() {
158            //TODO: Preserve sub-assets?
159            if !self.tracked_assets.contains(&asset_id) {
160                self.tracked_assets.insert(asset_id);
161                self.before_state.copy_from(&before_state, asset_id)?;
162            }
163        }
164
165        Ok(())
166    }
167
168    pub(crate) fn has_open_context(&self) -> bool {
169        self.context_name.is_some()
170    }
171
172    pub(crate) fn begin_context(
173        &mut self,
174        after_state: &DataSet,
175        name: &'static str,
176    ) {
177        if self.context_name == Some(name) {
178            // don't need to do anything, we can append to the current context
179        } else {
180            // commit the context that's in flight, if one exists
181            if self.context_name.is_some() {
182                // This won't do anything if there's nothing to send
183                self.commit_context(after_state);
184            }
185
186            self.context_name = Some(name);
187        }
188    }
189
190    pub(crate) fn end_context(
191        &mut self,
192        after_state: &DataSet,
193        end_context_behavior: EndContextBehavior,
194    ) {
195        if end_context_behavior != EndContextBehavior::AllowResume {
196            // This won't do anything if there's nothing to send
197            self.commit_context(after_state);
198        }
199    }
200
201    pub(crate) fn cancel_context(
202        &mut self,
203        after_state: &mut DataSet,
204    ) -> DataSetResult<()> {
205        let mut first_error = None;
206
207        if !self.tracked_assets.is_empty() {
208            // Delete newly created assets
209            let keys_to_delete: Vec<_> = after_state
210                .assets()
211                .keys()
212                .filter(|x| {
213                    self.tracked_assets.contains(x) && !self.before_state.assets().contains_key(x)
214                })
215                .copied()
216                .collect();
217
218            for key_to_delete in keys_to_delete {
219                if let Err(e) = after_state.delete_asset(key_to_delete) {
220                    if first_error.is_none() {
221                        first_error = Some(Err(e));
222                    }
223                }
224            }
225
226            // Overwrite pre-existing assets back to the previous state (before_state only contains
227            // assets that were tracked and were pre-existing)
228            for (asset_id, _asset) in self.before_state.assets() {
229                if let Err(e) = after_state.copy_from(&self.before_state, *asset_id) {
230                    if first_error.is_none() {
231                        first_error = Some(Err(e));
232                    }
233                }
234            }
235
236            // before state will be cleared
237            self.tracked_assets.clear();
238        }
239
240        self.before_state = Default::default();
241        self.context_name = None;
242
243        first_error.unwrap_or(Ok(()))
244    }
245
246    pub(crate) fn commit_context(
247        &mut self,
248        after_state: &DataSet,
249    ) {
250        if !self.tracked_assets.is_empty() {
251            // Make a diff and send it if it has changes
252            let diff_set = DataSetDiffSet::diff_data_set(
253                &self.before_state,
254                &after_state,
255                &self.tracked_assets,
256            );
257            if diff_set.has_changes() {
258                //
259                // Send the undo command
260                //
261                self.completed_undo_context_tx
262                    .send(CompletedUndoContextMessage {
263                        edit_context_key: self.edit_context_key,
264                        diff_set,
265                    })
266                    .unwrap();
267            }
268
269            self.tracked_assets.clear();
270        }
271
272        self.before_state = Default::default();
273        self.context_name = None;
274    }
275}