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(TrashedEntry)
 14//!  Rename(ProjectPath, ProjectPath)  →  Renamed(ProjectPath, ProjectPath)
 15//!  Restore(TrashedEntry)             →  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(TrashedEntry(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(TrashedEntry(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(TrashedEntry(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(TrashedEntry(1))────┐ (at the cursor)
 94//!                                   │
 95//! User Operation  Redo              v
 96//! Execute         Trashed(TrashedEntry(1)) ────────> Restore(TrashedEntry(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::TrashedEntry;
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, TrashedEntry),
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_entry = undo_manager.trash(&project_path, cx).await?;
160                Change::Trashed(project_path.worktree_id, trash_entry)
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, trashed_entry) => {
167                let project_path = undo_manager.restore(worktree_id, trashed_entry, 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, TrashedEntry),
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, trashed_entry) => {
195                Operation::Restore(worktree_id, trashed_entry)
196            }
197            Change::Renamed(from, to) => Operation::Rename(to, from),
198            Change::Restored(project_path) => Operation::Trash(project_path),
199            // When inverting a batch of operations, we reverse the order of
200            // operations to handle dependencies between them. For example, if a
201            // batch contains the following order of operations:
202            //
203            // 1. Create `src/`
204            // 2. Create `src/main.rs`
205            //
206            // If we first tried to revert the directory creation, it would fail
207            // because there's still files inside the directory.
208            Change::Batched(changes) => {
209                Operation::Batch(changes.into_iter().rev().map(Change::to_inverse).collect())
210            }
211        }
212    }
213}
214
215// Imagine pressing undo 10000+ times?!
216const MAX_UNDO_OPERATIONS: usize = 10_000;
217
218struct Inner {
219    workspace: WeakEntity<Workspace>,
220    panel: WeakEntity<ProjectPanel>,
221    history: VecDeque<Change>,
222    cursor: usize,
223    /// Maximum number of operations to keep on the undo history.
224    limit: usize,
225    can_undo: Arc<AtomicBool>,
226    can_redo: Arc<AtomicBool>,
227    rx: mpsc::Receiver<UndoMessage>,
228}
229
230/// pls arc this
231#[derive(Clone)]
232pub struct UndoManager {
233    tx: mpsc::Sender<UndoMessage>,
234    can_undo: Arc<AtomicBool>,
235    can_redo: Arc<AtomicBool>,
236}
237
238impl UndoManager {
239    pub fn new(
240        workspace: WeakEntity<Workspace>,
241        panel: WeakEntity<ProjectPanel>,
242        cx: &App,
243    ) -> Self {
244        let (tx, rx) = mpsc::channel(1024);
245        let inner = Inner::new(workspace, panel, rx);
246
247        let this = Self {
248            tx,
249            can_undo: Arc::clone(&inner.can_undo),
250            can_redo: Arc::clone(&inner.can_redo),
251        };
252
253        cx.spawn(async move |cx| inner.manage_undo_and_redo(cx.clone()).await)
254            .detach();
255
256        this
257    }
258
259    pub fn undo(&mut self) -> Result<()> {
260        self.tx
261            .try_send(UndoMessage::Undo)
262            .context("Undo and redo task can not keep up")
263    }
264    pub fn redo(&mut self) -> Result<()> {
265        self.tx
266            .try_send(UndoMessage::Redo)
267            .context("Undo and redo task can not keep up")
268    }
269    pub fn record(&mut self, changes: impl IntoIterator<Item = Change>) -> Result<()> {
270        self.tx
271            .try_send(UndoMessage::Changed(changes.into_iter().collect()))
272            .context("Undo and redo task can not keep up")
273    }
274    /// just for the UI, an undo may still fail if there are concurrent file
275    /// operations happening.
276    pub fn can_undo(&self) -> bool {
277        self.can_undo.load(Ordering::Relaxed)
278    }
279    /// just for the UI, an undo may still fail if there are concurrent file
280    /// operations happening.
281    pub fn can_redo(&self) -> bool {
282        self.can_redo.load(Ordering::Relaxed)
283    }
284}
285
286#[derive(Debug)]
287enum UndoMessage {
288    Changed(Vec<Change>),
289    Undo,
290    Redo,
291}
292
293impl UndoMessage {
294    fn error_title(&self) -> &'static str {
295        match self {
296            UndoMessage::Changed(_) => {
297                "this is a bug in the manage_undo_and_redo task please report"
298            }
299            UndoMessage::Undo => "Undo failed",
300            UndoMessage::Redo => "Redo failed",
301        }
302    }
303}
304
305impl Inner {
306    async fn manage_undo_and_redo(mut self, mut cx: AsyncApp) {
307        loop {
308            let Ok(new) = self.rx.recv().await else {
309                // project panel got closed
310                return;
311            };
312
313            let error_title = new.error_title();
314            let res = match new {
315                UndoMessage::Changed(changes) => {
316                    self.record(changes);
317                    Ok(())
318                }
319                UndoMessage::Undo => {
320                    let res = self.undo(&mut cx).await;
321                    let _ = self.panel.update(&mut cx, |_, cx| cx.notify());
322                    res
323                }
324                UndoMessage::Redo => {
325                    let res = self.redo(&mut cx).await;
326                    let _ = self.panel.update(&mut cx, |_, cx| cx.notify());
327                    res
328                }
329            };
330
331            if let Err(e) = res {
332                Self::show_error(error_title, self.workspace.clone(), e.to_string(), &mut cx);
333            }
334
335            self.can_undo.store(self.can_undo(), Ordering::Relaxed);
336            self.can_redo.store(self.can_redo(), Ordering::Relaxed);
337        }
338    }
339}
340
341impl Inner {
342    pub fn new(
343        workspace: WeakEntity<Workspace>,
344        panel: WeakEntity<ProjectPanel>,
345        rx: mpsc::Receiver<UndoMessage>,
346    ) -> Self {
347        Self::new_with_limit(workspace, panel, MAX_UNDO_OPERATIONS, rx)
348    }
349
350    pub fn new_with_limit(
351        workspace: WeakEntity<Workspace>,
352        panel: WeakEntity<ProjectPanel>,
353        limit: usize,
354        rx: mpsc::Receiver<UndoMessage>,
355    ) -> Self {
356        Self {
357            workspace,
358            panel,
359            history: VecDeque::new(),
360            cursor: 0usize,
361            limit,
362            can_undo: Arc::new(AtomicBool::new(false)),
363            can_redo: Arc::new(AtomicBool::new(false)),
364            rx,
365        }
366    }
367
368    pub fn can_undo(&self) -> bool {
369        self.cursor > 0
370    }
371
372    pub fn can_redo(&self) -> bool {
373        self.cursor < self.history.len()
374    }
375
376    pub async fn undo(&mut self, cx: &mut AsyncApp) -> Result<()> {
377        if !self.can_undo() {
378            return Ok(());
379        }
380
381        // Undo failure:
382        //
383        // History
384        // 	0 Created(src/main.rs)
385        // 	1 Renamed(README.md, readme.md) ─┐
386        //     2 +++cursor+++                │(before the cursor)
387        // 	2 Trashed(TrashedEntry(1))       │
388        //                                   │
389        // User Operation  Undo              v
390        // Failed execute  Renamed(README.md, readme.md) ───> Rename(readme.md, README.md)
391        // Record nothing
392        // History
393        // 	0 Created(src/main.rs)
394        //     1 +++cursor+++
395        // 	1 Trashed(TrashedEntry(1)) -----
396        //                                  |(at the cursor)
397        // User Operation  Redo             v
398        // Execute         Trashed(TrashedEntry(1)) ────────> Restore(TrashedEntry(1))
399        // Record          Restored(ProjectPath)
400        // History
401        // 	0 Created(src/main.rs)
402        // 	1 Restored(ProjectPath)
403        //  1 +++cursor+++
404
405        // We always want to move the cursor back regardless of whether undoing
406        // succeeds or fails, otherwise the cursor could end up pointing to a
407        // position outside of the history, as we remove the change before the
408        // cursor, in case undo fails.
409        let before_cursor = self.cursor - 1; // see docs above
410        self.cursor -= 1; // take a step back into the past
411
412        // If undoing fails, the user would be in a stuck state from which
413        // manual intervention would likely be needed in order to undo. As such,
414        // we remove the change from the `history` even before attempting to
415        // execute its inversion.
416        let undo_change = self
417            .history
418            .remove(before_cursor)
419            .expect("we can undo")
420            .to_inverse()
421            .execute(self, cx)
422            .await?;
423        self.history.insert(before_cursor, undo_change);
424        Ok(())
425    }
426
427    pub async fn redo(&mut self, cx: &mut AsyncApp) -> Result<()> {
428        if !self.can_redo() {
429            return Ok(());
430        }
431
432        // If redoing fails, the user would be in a stuck state from which
433        // manual intervention would likely be needed in order to redo. As such,
434        // we remove the change from the `history` even before attempting to
435        // execute its inversion.
436        let redo_change = self
437            .history
438            .remove(self.cursor)
439            .expect("we can redo")
440            .to_inverse()
441            .execute(self, cx)
442            .await?;
443        self.history.insert(self.cursor, redo_change);
444        self.cursor += 1;
445        Ok(())
446    }
447
448    /// Passed in changes will always be performed as a single step
449    pub fn record(&mut self, mut changes: Vec<Change>) {
450        let change = match changes.len() {
451            0 => return,
452            1 => changes.remove(0),
453            _ => Change::Batched(changes),
454        };
455
456        // When recording a new change, discard any changes that could still be
457        // redone.
458        if self.cursor < self.history.len() {
459            self.history.drain(self.cursor..);
460        }
461
462        // Ensure that the number of recorded changes does not exceed the
463        // maximum amount of tracked changes.
464        if self.history.len() >= self.limit {
465            self.history.pop_front();
466        } else {
467            self.cursor += 1;
468        }
469
470        self.history.push_back(change);
471    }
472
473    async fn rename(
474        &self,
475        from: &ProjectPath,
476        to: &ProjectPath,
477        cx: &mut AsyncApp,
478    ) -> Result<CreatedEntry> {
479        let Some(workspace) = self.workspace.upgrade() else {
480            return Err(anyhow!("Failed to obtain workspace."));
481        };
482
483        let res: Result<Task<Result<CreatedEntry>>> = workspace.update(cx, |workspace, cx| {
484            workspace.project().update(cx, |project, cx| {
485                let entry_id = project
486                    .entry_for_path(from, cx)
487                    .map(|entry| entry.id)
488                    .ok_or_else(|| anyhow!("No entry for path."))?;
489
490                Ok(project.rename_entry(entry_id, to.clone(), cx))
491            })
492        });
493
494        res?.await
495    }
496
497    async fn trash(&self, project_path: &ProjectPath, cx: &mut AsyncApp) -> Result<TrashedEntry> {
498        let Some(workspace) = self.workspace.upgrade() else {
499            return Err(anyhow!("Failed to obtain workspace."));
500        };
501
502        workspace
503            .update(cx, |workspace, cx| {
504                workspace.project().update(cx, |project, cx| {
505                    let entry_id = project
506                        .entry_for_path(&project_path, cx)
507                        .map(|entry| entry.id)
508                        .ok_or_else(|| anyhow!("No entry for path."))?;
509
510                    project
511                        .delete_entry(entry_id, true, cx)
512                        .ok_or_else(|| anyhow!("Worktree entry should exist"))
513                })
514            })?
515            .await
516            .and_then(|entry| {
517                entry.ok_or_else(|| anyhow!("When trashing we should always get a trashentry"))
518            })
519    }
520
521    async fn restore(
522        &self,
523        worktree_id: WorktreeId,
524        trashed_entry: TrashedEntry,
525        cx: &mut AsyncApp,
526    ) -> Result<ProjectPath> {
527        let Some(workspace) = self.workspace.upgrade() else {
528            return Err(anyhow!("Failed to obtain workspace."));
529        };
530
531        workspace
532            .update(cx, |workspace, cx| {
533                workspace.project().update(cx, |project, cx| {
534                    project.restore_entry(worktree_id, trashed_entry, cx)
535                })
536            })
537            .await
538    }
539
540    /// Displays a notification with the provided `title` and `error`.
541    fn show_error(
542        title: impl Into<SharedString>,
543        workspace: WeakEntity<Workspace>,
544        error: String,
545        cx: &mut AsyncApp,
546    ) {
547        workspace
548            .update(cx, move |workspace, cx| {
549                let notification_id =
550                    NotificationId::Named(SharedString::new_static("project_panel_undo"));
551
552                workspace.show_notification(notification_id, cx, move |cx| {
553                    cx.new(|cx| MessageNotification::new(error, cx).with_title(title))
554                })
555            })
556            .ok();
557    }
558}