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}