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}