undo.rs

  1//! # Undo Manager
  2//!
  3//! ## Operations and Results
  4//!
  5//! Undo and Redo actions execute an operation against the filesystem, producing
  6//! a result that is recorded back into the history in place of the original
  7//! entry. Each result is the semantic inverse of its paired operation, so the
  8//! cycle can repeat for continued undo and redo.
  9//!
 10//!  Operations                            Results
 11//!  ─────────────────────────────────  ──────────────────────────────────────
 12//!  Create(ProjectPath)               →  Created(ProjectPath)
 13//!  Trash(ProjectPath)                →  Trashed(TrashId)
 14//!  Rename(ProjectPath, ProjectPath)  →  Renamed(ProjectPath, ProjectPath)
 15//!  Restore(TrashId)                  →  Restored(ProjectPath)
 16//!  Batch(Vec<Operation>)             →  Batch(Vec<Result>)
 17//!
 18//!
 19//! ## History and Cursor
 20//!
 21//! The undo manager maintains an operation history with a cursor position (↑).
 22//! Recording an operation appends it to the history and advances the cursor to
 23//! the end. The cursor separates past entries (left of ↑) from future entries
 24//! (right of ↑).
 25//!
 26//! ─ **Undo**: Takes the history entry just *before* ↑, executes its inverse,
 27//!   records the result back in its place, and moves ↑ one step to the left.
 28//! ─ **Redo**: Takes the history entry just *at* ↑, executes its inverse,
 29//!   records the result back in its place, and advances ↑ one step to the right.
 30//!
 31//!
 32//! ## Example
 33//!
 34//! User Operation  Create(src/main.rs)
 35//! History
 36//! 	0 Created(src/main.rs)
 37//!     1 +++cursor+++
 38//!
 39//! User Operation  Rename(README.md, readme.md)
 40//! History
 41//! 	0 Created(src/main.rs)
 42//! 	1 Renamed(README.md, readme.md)
 43//!     2 +++cursor+++
 44//!
 45//! User Operation  Create(CONTRIBUTING.md)
 46//! History
 47//! 	0 Created(src/main.rs)
 48//!     1 Renamed(README.md, readme.md)
 49//! 	2 Created(CONTRIBUTING.md) ──┐
 50//!     3 +++cursor+++               │(before the cursor)
 51//!                                  │
 52//!   ┌──────────────────────────────┴─────────────────────────────────────────────┐
 53//!     Redoing will take the result at the cursor position, convert that into the
 54//!     operation that can revert that result, execute that operation and replace
 55//!     the result in the history with the new result, obtained from running the
 56//!     inverse operation, advancing the cursor position.
 57//!   └──────────────────────────────┬─────────────────────────────────────────────┘
 58//!                                  │
 59//!                                  │
 60//! User Operation  Undo             v
 61//! Execute         Created(CONTRIBUTING.md) ────────> Trash(CONTRIBUTING.md)
 62//! Record          Trashed(TrashId(1))
 63//! History
 64//! 	0 Created(src/main.rs)
 65//! 	1 Renamed(README.md, readme.md) ─┐
 66//!     2 +++cursor+++                   │(before the cursor)
 67//! 	2 Trashed(TrashId(1))            │
 68//!                                      │
 69//! User Operation  Undo                 v
 70//! Execute         Renamed(README.md, readme.md) ───> Rename(readme.md, README.md)
 71//! Record          Renamed(readme.md, README.md)
 72//! History
 73//! 	0 Created(src/main.rs)
 74//!     1 +++cursor+++
 75//! 	1 Renamed(readme.md, README.md) ─┐ (at the cursor)
 76//! 	2 Trashed(TrashId(1))            │
 77//!                                      │
 78//!   ┌──────────────────────────────────┴─────────────────────────────────────────┐
 79//!     Redoing will take the result at the cursor position, convert that into the
 80//!     operation that can revert that result, execute that operation and replace
 81//!     the result in the history with the new result, obtained from running the
 82//!     inverse operation, advancing the cursor position.
 83//!   └──────────────────────────────────┬─────────────────────────────────────────┘
 84//!                                      │
 85//!                                      │
 86//! User Operation  Redo                 v
 87//! Execute         Renamed(readme.md, README.md) ───> Rename(README.md, readme.md)
 88//! Record          Renamed(README.md, readme.md)
 89//! History
 90//! 	0 Created(src/main.rs)
 91//! 	1 Renamed(README.md, readme.md)
 92//!     2 +++cursor+++
 93//! 	2 Trashed(TrashId(1))───────┐ (at the cursor)
 94//!                                 │
 95//! User Operation  Redo            v
 96//! Execute         Trashed(TrashId(1)) ────────> Restore(TrashId(1))
 97//! Record          Restored(ProjectPath)
 98//! History
 99//! 	0 Created(src/main.rs)
100//! 	1 Renamed(README.md, readme.md)
101//! 	2 Restored(ProjectPath)
102//!     2 +++cursor+++
103
104//!
105//! create A;                                                      A
106//! rename A -> B;                                                 B
107//! undo (rename B -> A)       (takes 10s for some reason)         B (still b cause it's hanging for 10s)
108//! remove B                                                       _
109//! create B                                                       B
110//! put important content in B                                     B
111//! undo manger renames (does not hang)                            A
112//! remove A                                                       _
113//! user sad
114
115//!
116//! create A;                                                      A
117//! rename A -> B;                                                 B
118//! undo (rename B -> A)       (takes 10s for some reason)         B (still b cause it's hanging for 10s)
119//! create C                                                       B
120//! -- src/c.rs
121//!    --
122
123//!
124//! create docs/files/ directory                                   docs/files/
125//! create docs/files/a.txt                                        docs/files/
126//! undo (rename B -> A)       (takes 10s for some reason)         B (still b cause it's hanging for 10s)
127//! create C                                                       B
128//! -- src/c.rs
129//!    --
130
131//! List of "tainted files" that the user may not operate on
132
133use crate::ProjectPanel;
134use anyhow::{Context, Result, anyhow};
135use fs::TrashId;
136use futures::channel::mpsc;
137use gpui::{AppContext, AsyncApp, SharedString, Task, WeakEntity};
138use project::{ProjectPath, WorktreeId};
139use std::sync::atomic::{AtomicBool, Ordering};
140use std::{collections::VecDeque, sync::Arc};
141use ui::App;
142use workspace::{
143    Workspace,
144    notifications::{NotificationId, simple_message_notification::MessageNotification},
145};
146use worktree::CreatedEntry;
147
148enum Operation {
149    Trash(ProjectPath),
150    Rename(ProjectPath, ProjectPath),
151    Restore(WorktreeId, TrashId),
152    Batch(Vec<Operation>),
153}
154
155impl Operation {
156    async fn execute(self, undo_manager: &Inner, cx: &mut AsyncApp) -> Result<Change> {
157        Ok(match self {
158            Operation::Trash(project_path) => {
159                let trash_id = undo_manager.trash(&project_path, cx).await?;
160                Change::Trashed(project_path.worktree_id, trash_id)
161            }
162            Operation::Rename(from, to) => {
163                undo_manager.rename(&from, &to, cx).await?;
164                Change::Renamed(from, to)
165            }
166            Operation::Restore(worktree_id, trash_id) => {
167                let project_path = undo_manager.restore(worktree_id, trash_id, cx).await?;
168                Change::Restored(project_path)
169            }
170            Operation::Batch(operations) => {
171                let mut res = Vec::new();
172                for op in operations {
173                    res.push(Box::pin(op.execute(undo_manager, cx)).await?);
174                }
175                Change::Batched(res)
176            }
177        })
178    }
179}
180
181#[derive(Clone, Debug)]
182pub(crate) enum Change {
183    Created(ProjectPath),
184    Trashed(WorktreeId, TrashId),
185    Renamed(ProjectPath, ProjectPath),
186    Restored(ProjectPath),
187    Batched(Vec<Change>),
188}
189
190impl Change {
191    fn to_inverse(self) -> Operation {
192        match self {
193            Change::Created(project_path) => Operation::Trash(project_path),
194            Change::Trashed(worktree_id, trash_id) => Operation::Restore(worktree_id, trash_id),
195            Change::Renamed(from, to) => Operation::Rename(to, from),
196            Change::Restored(project_path) => Operation::Trash(project_path),
197            // When inverting a batch of operations, we reverse the order of
198            // operations to handle dependencies between them. For example, if a
199            // batch contains the following order of operations:
200            //
201            // 1. Create `src/`
202            // 2. Create `src/main.rs`
203            //
204            // If we first tried to revert the directory creation, it would fail
205            // because there's still files inside the directory.
206            Change::Batched(changes) => {
207                Operation::Batch(changes.into_iter().rev().map(Change::to_inverse).collect())
208            }
209        }
210    }
211}
212
213// Imagine pressing undo 10000+ times?!
214const MAX_UNDO_OPERATIONS: usize = 10_000;
215
216struct Inner {
217    workspace: WeakEntity<Workspace>,
218    panel: WeakEntity<ProjectPanel>,
219    history: VecDeque<Change>,
220    cursor: usize,
221    /// Maximum number of operations to keep on the undo history.
222    limit: usize,
223    can_undo: Arc<AtomicBool>,
224    can_redo: Arc<AtomicBool>,
225    rx: mpsc::Receiver<UndoMessage>,
226}
227
228/// pls arc this
229#[derive(Clone)]
230pub struct UndoManager {
231    tx: mpsc::Sender<UndoMessage>,
232    can_undo: Arc<AtomicBool>,
233    can_redo: Arc<AtomicBool>,
234}
235
236impl UndoManager {
237    pub fn new(
238        workspace: WeakEntity<Workspace>,
239        panel: WeakEntity<ProjectPanel>,
240        cx: &App,
241    ) -> Self {
242        let (tx, rx) = mpsc::channel(1024);
243        let inner = Inner::new(workspace, panel, rx);
244
245        let this = Self {
246            tx,
247            can_undo: Arc::clone(&inner.can_undo),
248            can_redo: Arc::clone(&inner.can_redo),
249        };
250
251        cx.spawn(async move |cx| inner.manage_undo_and_redo(cx.clone()).await)
252            .detach();
253
254        this
255    }
256
257    pub fn undo(&mut self) -> Result<()> {
258        self.tx
259            .try_send(UndoMessage::Undo)
260            .context("Undo and redo task can not keep up")
261    }
262    pub fn redo(&mut self) -> Result<()> {
263        self.tx
264            .try_send(UndoMessage::Redo)
265            .context("Undo and redo task can not keep up")
266    }
267    pub fn record(&mut self, changes: impl IntoIterator<Item = Change>) -> Result<()> {
268        self.tx
269            .try_send(UndoMessage::Changed(changes.into_iter().collect()))
270            .context("Undo and redo task can not keep up")
271    }
272    /// just for the UI, an undo may still fail if there are concurrent file
273    /// operations happening.
274    pub fn can_undo(&self) -> bool {
275        self.can_undo.load(Ordering::Relaxed)
276    }
277    /// just for the UI, an undo may still fail if there are concurrent file
278    /// operations happening.
279    pub fn can_redo(&self) -> bool {
280        self.can_redo.load(Ordering::Relaxed)
281    }
282}
283
284#[derive(Debug)]
285enum UndoMessage {
286    Changed(Vec<Change>),
287    Undo,
288    Redo,
289}
290
291impl UndoMessage {
292    fn error_title(&self) -> &'static str {
293        match self {
294            UndoMessage::Changed(_) => {
295                "this is a bug in the manage_undo_and_redo task please report"
296            }
297            UndoMessage::Undo => "Undo failed",
298            UndoMessage::Redo => "Redo failed",
299        }
300    }
301}
302
303impl Inner {
304    async fn manage_undo_and_redo(mut self, mut cx: AsyncApp) {
305        loop {
306            let Ok(new) = self.rx.recv().await else {
307                // project panel got closed
308                return;
309            };
310
311            let error_title = new.error_title();
312            let res = match new {
313                UndoMessage::Changed(changes) => {
314                    self.record(changes);
315                    Ok(())
316                }
317                UndoMessage::Undo => {
318                    let res = self.undo(&mut cx).await;
319                    let _ = self.panel.update(&mut cx, |_, cx| cx.notify());
320                    res
321                }
322                UndoMessage::Redo => {
323                    let res = self.redo(&mut cx).await;
324                    let _ = self.panel.update(&mut cx, |_, cx| cx.notify());
325                    res
326                }
327            };
328
329            if let Err(e) = res {
330                Self::show_error(error_title, self.workspace.clone(), e.to_string(), &mut cx);
331            }
332
333            self.can_undo.store(self.can_undo(), Ordering::Relaxed);
334            self.can_redo.store(self.can_redo(), Ordering::Relaxed);
335        }
336    }
337}
338
339impl Inner {
340    pub fn new(
341        workspace: WeakEntity<Workspace>,
342        panel: WeakEntity<ProjectPanel>,
343        rx: mpsc::Receiver<UndoMessage>,
344    ) -> Self {
345        Self::new_with_limit(workspace, panel, MAX_UNDO_OPERATIONS, rx)
346    }
347
348    pub fn new_with_limit(
349        workspace: WeakEntity<Workspace>,
350        panel: WeakEntity<ProjectPanel>,
351        limit: usize,
352        rx: mpsc::Receiver<UndoMessage>,
353    ) -> Self {
354        Self {
355            workspace,
356            panel,
357            history: VecDeque::new(),
358            cursor: 0usize,
359            limit,
360            can_undo: Arc::new(AtomicBool::new(false)),
361            can_redo: Arc::new(AtomicBool::new(false)),
362            rx,
363        }
364    }
365
366    pub fn can_undo(&self) -> bool {
367        self.cursor > 0
368    }
369
370    pub fn can_redo(&self) -> bool {
371        self.cursor < self.history.len()
372    }
373
374    pub async fn undo(&mut self, cx: &mut AsyncApp) -> Result<()> {
375        if !self.can_undo() {
376            return Ok(());
377        }
378
379        // Undo failure:
380        //
381        // History
382        // 	0 Created(src/main.rs)
383        // 	1 Renamed(README.md, readme.md) ─┐
384        //     2 +++cursor+++                │(before the cursor)
385        // 	2 Trashed(TrashId(1))            │
386        //                                   │
387        // User Operation  Undo              v
388        // Failed execute  Renamed(README.md, readme.md) ───> Rename(readme.md, README.md)
389        // Record nothing
390        // History
391        // 	0 Created(src/main.rs)
392        //     1 +++cursor+++
393        // 	1 Trashed(TrashId(1)) ---------
394        //                                |(at the cursor)
395        // User Operation  Redo           v
396        // Execute         Trashed(TrashId(1)) ────────> Restore(TrashId(1))
397        // Record          Restored(ProjectPath)
398        // History
399        // 	0 Created(src/main.rs)
400        // 	1 Restored(ProjectPath)
401        //  1 +++cursor+++
402
403        // We always want to move the cursor back regardless of whether undoing
404        // succeeds or fails, otherwise the cursor could end up pointing to a
405        // position outside of the history, as we remove the change before the
406        // cursor, in case undo fails.
407        let before_cursor = self.cursor - 1; // see docs above
408        self.cursor -= 1; // take a step back into the past
409
410        // If undoing fails, the user would be in a stuck state from which
411        // manual intervention would likely be needed in order to undo. As such,
412        // we remove the change from the `history` even before attempting to
413        // execute its inversion.
414        let undo_change = self
415            .history
416            .remove(before_cursor)
417            .expect("we can undo")
418            .to_inverse()
419            .execute(self, cx)
420            .await?;
421        self.history.insert(before_cursor, undo_change);
422        Ok(())
423    }
424
425    pub async fn redo(&mut self, cx: &mut AsyncApp) -> Result<()> {
426        if !self.can_redo() {
427            return Ok(());
428        }
429
430        // If redoing fails, the user would be in a stuck state from which
431        // manual intervention would likely be needed in order to redo. As such,
432        // we remove the change from the `history` even before attempting to
433        // execute its inversion.
434        let redo_change = self
435            .history
436            .remove(self.cursor)
437            .expect("we can redo")
438            .to_inverse()
439            .execute(self, cx)
440            .await?;
441        self.history.insert(self.cursor, redo_change);
442        self.cursor += 1;
443        Ok(())
444    }
445
446    /// Passed in changes will always be performed as a single step
447    pub fn record(&mut self, mut changes: Vec<Change>) {
448        let change = match changes.len() {
449            0 => return,
450            1 => changes.remove(0),
451            _ => Change::Batched(changes),
452        };
453
454        // When recording a new change, discard any changes that could still be
455        // redone.
456        if self.cursor < self.history.len() {
457            self.history.drain(self.cursor..);
458        }
459
460        // Ensure that the number of recorded changes does not exceed the
461        // maximum amount of tracked changes.
462        if self.history.len() >= self.limit {
463            self.history.pop_front();
464        } else {
465            self.cursor += 1;
466        }
467
468        self.history.push_back(change);
469    }
470
471    async fn rename(
472        &self,
473        from: &ProjectPath,
474        to: &ProjectPath,
475        cx: &mut AsyncApp,
476    ) -> Result<CreatedEntry> {
477        let Some(workspace) = self.workspace.upgrade() else {
478            return Err(anyhow!("Failed to obtain workspace."));
479        };
480
481        let res: Result<Task<Result<CreatedEntry>>> = workspace.update(cx, |workspace, cx| {
482            workspace.project().update(cx, |project, cx| {
483                let entry_id = project
484                    .entry_for_path(from, cx)
485                    .map(|entry| entry.id)
486                    .ok_or_else(|| anyhow!("No entry for path."))?;
487
488                Ok(project.rename_entry(entry_id, to.clone(), cx))
489            })
490        });
491
492        res?.await
493    }
494
495    async fn trash(&self, project_path: &ProjectPath, cx: &mut AsyncApp) -> Result<TrashId> {
496        let Some(workspace) = self.workspace.upgrade() else {
497            return Err(anyhow!("Failed to obtain workspace."));
498        };
499
500        workspace
501            .update(cx, |workspace, cx| {
502                workspace.project().update(cx, |project, cx| {
503                    let entry_id = project
504                        .entry_for_path(&project_path, cx)
505                        .map(|entry| entry.id)
506                        .ok_or_else(|| anyhow!("No entry for path."))?;
507
508                    project
509                        .trash_entry(entry_id, cx)
510                        .ok_or_else(|| anyhow!("Worktree entry should exist"))
511                })
512            })?
513            .await
514    }
515
516    async fn restore(
517        &self,
518        worktree_id: WorktreeId,
519        trash_id: TrashId,
520        cx: &mut AsyncApp,
521    ) -> Result<ProjectPath> {
522        let Some(workspace) = self.workspace.upgrade() else {
523            return Err(anyhow!("Failed to obtain workspace."));
524        };
525
526        workspace
527            .update(cx, |workspace, cx| {
528                workspace.project().update(cx, |project, cx| {
529                    project.restore_entry(worktree_id, trash_id, cx)
530                })
531            })
532            .await
533    }
534
535    /// Displays a notification with the provided `title` and `error`.
536    fn show_error(
537        title: impl Into<SharedString>,
538        workspace: WeakEntity<Workspace>,
539        error: String,
540        cx: &mut AsyncApp,
541    ) {
542        workspace
543            .update(cx, move |workspace, cx| {
544                let notification_id =
545                    NotificationId::Named(SharedString::new_static("project_panel_undo"));
546
547                workspace.show_notification(notification_id, cx, move |cx| {
548                    cx.new(|cx| MessageNotification::new(error, cx).with_title(title))
549                })
550            })
551            .ok();
552    }
553}