//! # Undo Manager//!//! ## Operations and Results//!//! Undo and Redo actions execute an operation against the filesystem, producing//! a result that is recorded back into the history in place of the original//! entry. Each result is the semantic inverse of its paired operation, so the//! cycle can repeat for continued undo and redo.//!//! Operations Results//! ───────────────────────────────── ──────────────────────────────────────//! Create(ProjectPath) → Created(ProjectPath)//! Trash(ProjectPath) → Trashed(WorktreeId, TrashId)//! Rename(ProjectPath, ProjectPath) → Renamed(ProjectPath, ProjectPath)//! Restore(WorktreeId, TrashId) → Restored(ProjectPath)//! Batch(Vec<Operation>) → Batch(Vec<Result>)//!//!//! ## History and Cursor//!//! The undo manager maintains an operation history with a cursor position (↑).//! Recording an operation appends it to the history and advances the cursor to//! the end. The cursor separates past entries (left of ↑) from future entries//! (right of ↑).//!//! ─ **Undo**: Takes the history entry just *before* ↑, executes its inverse,//! records the result back in its place, and moves ↑ one step to the left.//! ─ **Redo**: Takes the history entry just *at* ↑, executes its inverse,//! records the result back in its place, and advances ↑ one step to the right.//!//!//! ## Example//!//! User Operation Create(src/main.rs)//! History//! 0 Created(src/main.rs)//! 1 +++cursor+++//!//! User Operation Rename(README.md, readme.md)//! History//! 0 Created(src/main.rs)//! 1 Renamed(README.md, readme.md)//! 2 +++cursor+++//!//! User Operation Create(CONTRIBUTING.md)//! History//! 0 Created(src/main.rs)//! 1 Renamed(README.md, readme.md)//! 2 Created(CONTRIBUTING.md) ──┐//! 3 +++cursor+++ │(before the cursor)//! │//! ┌──────────────────────────────┴─────────────────────────────────────────────┐//! Redoing will take the result at the cursor position, convert that into the//! operation that can revert that result, execute that operation and replace//! the result in the history with the new result, obtained from running the//! inverse operation, advancing the cursor position.//! └──────────────────────────────┬─────────────────────────────────────────────┘//! │//! │//! User Operation Undo v//! Execute Created(CONTRIBUTING.md) ────────> Trash(CONTRIBUTING.md)//! Record Trashed(WorktreeId(1), TrashId(1))//! History//! 0 Created(src/main.rs)//! 1 Renamed(README.md, readme.md) ─────┐//! 2 +++cursor+++ │(before the cursor)//! 2 Trashed(WorktreeId(1), TrashId(1)) │//! │//! User Operation Undo v//! Execute Renamed(README.md, readme.md) ───> Rename(readme.md, README.md)//! Record Renamed(readme.md, README.md)//! History//! 0 Created(src/main.rs)//! 1 +++cursor+++//! 1 Renamed(readme.md, README.md) ─────┐ (at the cursor)//! 2 Trashed(WorktreeId(1), TrashId(1)) │//! │//! ┌──────────────────────────────────────┴─────────────────────────────────────┐//! Redoing will take the result at the cursor position, convert that into the//! operation that can revert that result, execute that operation and replace//! the result in the history with the new result, obtained from running the//! inverse operation, advancing the cursor position.//! └─────────────────────────────────────┬──────────────────────────────────────┘//! │//! │//! User Operation Redo v//! Execute Renamed(readme.md, README.md) ───> Rename(README.md, readme.md)//! Record Renamed(README.md, readme.md)//! History//! 0 Created(src/main.rs)//! 1 Renamed(README.md, readme.md)//! 2 +++cursor+++//! 2 Trashed(WorktreeId(1), TrashId(1)) ─┐ (at the cursor)//! │//! User Operation Redo v//! Execute Trashed(WorktreeId(1), TrashId(1)) ─> Restore(WorktreeId(1), TrashId(1))//! Record Restored(ProjectPath)//! History//! 0 Created(src/main.rs)//! 1 Renamed(README.md, readme.md)//! 2 Restored(ProjectPath)//! 2 +++cursor+++ //!//! create A; A//! rename A -> B; B//! undo (rename B -> A) (takes 10s for some reason) B (still b cause it's hanging for 10s)//! remove B _//! create B B//! put important content in B B//! undo manger renames (does not hang) A//! remove A _//! user sad //!//! create A; A//! rename A -> B; B//! undo (rename B -> A) (takes 10s for some reason) B (still b cause it's hanging for 10s)//! create C B//! -- src/c.rs//! -- //!//! create docs/files/ directory docs/files///! create docs/files/a.txt docs/files///! undo (rename B -> A) (takes 10s for some reason) B (still b cause it's hanging for 10s)//! create C B//! -- src/c.rs//! -- //! List of "tainted files" that the user may not operate on use crate::ProjectPanel;use anyhow::{Context, Result, anyhow};use fs::{TrashId, TrashRestoreError};use futures::channel::mpsc;use gpui::{AppContext, AsyncApp, IntoElement, SharedString, Styled, Task, WeakEntity};use markdown::{Markdown, MarkdownElement};use project::Project;use project::{ProjectPath, WorktreeId};use std::sync::atomic::{AtomicBool, Ordering};use std::{collections::VecDeque, sync::Arc};use ui::{App, TextSize};use util::{paths::PathStyle, rel_path::RelPath};use workspace::{ Workspace, notifications::{ NotificationId, markdown_style, simple_message_notification::MessageNotification, },};use worktree::CreatedEntry; enum Operation { Trash(ProjectPath), Rename(ProjectPath, ProjectPath), Restore(WorktreeId, TrashId), Batch(Vec<Operation>),} impl Operation { async fn execute(self, undo_manager: &Inner, cx: &mut AsyncApp) -> Result<Change> { Ok(match self { Operation::Trash(project_path) => { let trash_id = undo_manager.trash(&project_path, cx).await?; Change::Trashed(project_path.worktree_id, trash_id) } Operation::Rename(from, to) => { undo_manager.rename(&from, &to, cx).await?; Change::Renamed(from, to) } Operation::Restore(worktree_id, trash_id) => { let project_path = undo_manager.restore(worktree_id, trash_id, cx).await?; Change::Restored(project_path) } Operation::Batch(operations) => { let mut res = Vec::new(); for op in operations { res.push(Box::pin(op.execute(undo_manager, cx)).await?); } Change::Batched(res) } }) }} #[derive(Clone, Debug)]pub(crate) enum Change { Created(ProjectPath), Trashed(WorktreeId, TrashId), Renamed(ProjectPath, ProjectPath), Restored(ProjectPath), Batched(Vec<Change>),} impl Change { fn to_inverse(self) -> Operation { match self { Change::Created(project_path) => Operation::Trash(project_path), Change::Trashed(worktree_id, trash_id) => Operation::Restore(worktree_id, trash_id), Change::Renamed(from, to) => Operation::Rename(to, from), Change::Restored(project_path) => Operation::Trash(project_path), // When inverting a batch of operations, we reverse the order of // operations to handle dependencies between them. For example, if a // batch contains the following order of operations: // // 1. Create `src/` // 2. Create `src/main.rs` // // If we first tried to revert the directory creation, it would fail // because there's still files inside the directory. Change::Batched(changes) => { Operation::Batch(changes.into_iter().rev().map(Change::to_inverse).collect()) } } }} // Imagine pressing undo 10000+ times?!const MAX_UNDO_OPERATIONS: usize = 10_000; struct Inner { workspace: WeakEntity<Workspace>, panel: WeakEntity<ProjectPanel>, history: VecDeque<Change>, cursor: usize, /// Maximum number of operations to keep on the undo history. limit: usize, can_undo: Arc<AtomicBool>, can_redo: Arc<AtomicBool>, rx: mpsc::Receiver<UndoMessage>,} /// pls arc this#[derive(Clone)]pub struct UndoManager { tx: mpsc::Sender<UndoMessage>, is_via_collab: bool, can_undo: Arc<AtomicBool>, can_redo: Arc<AtomicBool>,} impl UndoManager { pub fn new( workspace: WeakEntity<Workspace>, panel: WeakEntity<ProjectPanel>, is_via_collab: bool, cx: &App, ) -> Self { let (tx, rx) = mpsc::channel(1024); let inner = Inner::new(workspace, panel, rx); let this = Self { tx, is_via_collab, can_undo: Arc::clone(&inner.can_undo), can_redo: Arc::clone(&inner.can_redo), }; cx.spawn(async move |cx| inner.manage_undo_and_redo(cx.clone()).await) .detach(); this } pub fn undo(&mut self) -> Result<()> { self.tx .try_send(UndoMessage::Undo) .context("Undo and redo task can not keep up") } pub fn redo(&mut self) -> Result<()> { self.tx .try_send(UndoMessage::Redo) .context("Undo and redo task can not keep up") } pub fn record(&mut self, changes: impl IntoIterator<Item = Change>) -> Result<()> { // In a collab session, undoing or redoing can send `TrashProjectEntry` // or `RestoreProjectEntry`, for example, undoing a create or undoing a // trash. // Since older hosts can't decode those messages, which would // silently never complete, besides disabling the `Undo`/`Redo` actions // for collab, we also avoid recording history here so there's nothing // that could later trigger those messages. if self.is_via_collab { return Ok(()); } self.tx .try_send(UndoMessage::Changed(changes.into_iter().collect())) .context("Undo and redo task can not keep up") } /// just for the UI, an undo may still fail if there are concurrent file /// operations happening. pub fn can_undo(&self) -> bool { self.can_undo.load(Ordering::Relaxed) } /// just for the UI, an undo may still fail if there are concurrent file /// operations happening. pub fn can_redo(&self) -> bool { self.can_redo.load(Ordering::Relaxed) } #[cfg(test)] pub(crate) fn set_is_via_collab(&mut self, is_via_collab: bool) { self.is_via_collab = is_via_collab; }} #[derive(Debug)]enum UndoMessage { Changed(Vec<Change>), Undo, Redo,} impl UndoMessage { fn error_title(&self) -> &'static str { match self { UndoMessage::Changed(_) => { "this is a bug in the manage_undo_and_redo task please report" } UndoMessage::Undo => "Undo Failed", UndoMessage::Redo => "Redo Failed", } }} fn project_path_display( project: &Project, project_path: &ProjectPath, path_style: PathStyle, cx: &App,) -> String { project .short_full_path_for_project_path(project_path, cx) .unwrap_or_else(|| project_path.path.display(path_style).to_string())} impl Inner { async fn manage_undo_and_redo(mut self, mut cx: AsyncApp) { loop { let Ok(new) = self.rx.recv().await else { // project panel got closed return; }; let error_title = new.error_title(); let res = match new { UndoMessage::Changed(changes) => { self.record(changes); Ok(()) } UndoMessage::Undo => { let res = self.undo(&mut cx).await; let _ = self.panel.update(&mut cx, |_, cx| cx.notify()); res } UndoMessage::Redo => { let res = self.redo(&mut cx).await; let _ = self.panel.update(&mut cx, |_, cx| cx.notify()); res } }; if let Err(e) = res { Self::show_error( error_title, self.workspace.clone(), format!("{e:#}"), &mut cx, ); } self.can_undo.store(self.can_undo(), Ordering::Relaxed); self.can_redo.store(self.can_redo(), Ordering::Relaxed); } } pub fn new( workspace: WeakEntity<Workspace>, panel: WeakEntity<ProjectPanel>, rx: mpsc::Receiver<UndoMessage>, ) -> Self { Self::new_with_limit(workspace, panel, MAX_UNDO_OPERATIONS, rx) } pub fn new_with_limit( workspace: WeakEntity<Workspace>, panel: WeakEntity<ProjectPanel>, limit: usize, rx: mpsc::Receiver<UndoMessage>, ) -> Self { Self { workspace, panel, history: VecDeque::new(), cursor: 0usize, limit, can_undo: Arc::new(AtomicBool::new(false)), can_redo: Arc::new(AtomicBool::new(false)), rx, } } pub fn can_undo(&self) -> bool { self.cursor > 0 } pub fn can_redo(&self) -> bool { self.cursor < self.history.len() } pub async fn undo(&mut self, cx: &mut AsyncApp) -> Result<()> { if !self.can_undo() { return Ok(()); } // Undo failure: // // History // 0 Created(src/main.rs) // 1 Renamed(README.md, readme.md) ─┐ // 2 +++cursor+++ │(before the cursor) // 2 Trashed(WorktreeId(1), TrashId(1)) │ // │ // User Operation Undo v // Failed execute Renamed(README.md, readme.md) ───> Rename(readme.md, README.md) // Record nothing // History // 0 Created(src/main.rs) // 1 +++cursor+++ // 1 Trashed(WorktreeId(1), TrashId(1)) --------- // |(at the cursor) // User Operation Redo v // Execute Trashed(WorktreeId(1), TrashId(1)) ─> Restore(WorktreeId(1), TrashId(1)) // Record Restored(ProjectPath) // History // 0 Created(src/main.rs) // 1 Restored(ProjectPath) // 1 +++cursor+++ // We always want to move the cursor back regardless of whether undoing // succeeds or fails, otherwise the cursor could end up pointing to a // position outside of the history, as we remove the change before the // cursor, in case undo fails. let before_cursor = self.cursor - 1; // see docs above self.cursor -= 1; // take a step back into the past // If undoing fails, the user would be in a stuck state from which // manual intervention would likely be needed in order to undo. As such, // we remove the change from the `history` even before attempting to // execute its inversion. let undo_change = self .history .remove(before_cursor) .expect("we can undo") .to_inverse() .execute(self, cx) .await?; self.history.insert(before_cursor, undo_change); Ok(()) } pub async fn redo(&mut self, cx: &mut AsyncApp) -> Result<()> { if !self.can_redo() { return Ok(()); } // If redoing fails, the user would be in a stuck state from which // manual intervention would likely be needed in order to redo. As such, // we remove the change from the `history` even before attempting to // execute its inversion. let redo_change = self .history .remove(self.cursor) .expect("we can redo") .to_inverse() .execute(self, cx) .await?; self.history.insert(self.cursor, redo_change); self.cursor += 1; Ok(()) } /// Passed in changes will always be performed as a single step pub fn record(&mut self, mut changes: Vec<Change>) { let change = match changes.len() { 0 => return, 1 => changes.remove(0), _ => Change::Batched(changes), }; // When recording a new change, discard any changes that could still be // redone. if self.cursor < self.history.len() { self.history.drain(self.cursor..); } // Ensure that the number of recorded changes does not exceed the // maximum amount of tracked changes. if self.history.len() >= self.limit {Lines 1–500 of 686. Load more
//! # Undo Manager //! //! ## Operations and Results //! //! Undo and Redo actions execute an operation against the filesystem, producing //! a result that is recorded back into the history in place of the original //! entry. Each result is the semantic inverse of its paired operation, so the //! cycle can repeat for continued undo and redo. //! //! Operations Results //! ───────────────────────────────── ────────────────────────────────────── //! Create(ProjectPath) → Created(ProjectPath) //! Trash(ProjectPath) → Trashed(WorktreeId, TrashId) //! Rename(ProjectPath, ProjectPath) → Renamed(ProjectPath, ProjectPath) //! Restore(WorktreeId, TrashId) → Restored(ProjectPath) //! Batch(Vec<Operation>) → Batch(Vec<Result>) //! //! //! ## History and Cursor //! //! The undo manager maintains an operation history with a cursor position (↑). //! Recording an operation appends it to the history and advances the cursor to //! the end. The cursor separates past entries (left of ↑) from future entries //! (right of ↑). //! //! ─ **Undo**: Takes the history entry just *before* ↑, executes its inverse, //! records the result back in its place, and moves ↑ one step to the left. //! ─ **Redo**: Takes the history entry just *at* ↑, executes its inverse, //! records the result back in its place, and advances ↑ one step to the right. //! //! //! ## Example //! //! User Operation Create(src/main.rs) //! History //! 0 Created(src/main.rs) //! 1 +++cursor+++ //! //! User Operation Rename(README.md, readme.md) //! History //! 0 Created(src/main.rs) //! 1 Renamed(README.md, readme.md) //! 2 +++cursor+++ //! //! User Operation Create(CONTRIBUTING.md) //! History //! 0 Created(src/main.rs) //! 1 Renamed(README.md, readme.md) //! 2 Created(CONTRIBUTING.md) ──┐ //! 3 +++cursor+++ │(before the cursor) //! │ //! ┌──────────────────────────────┴─────────────────────────────────────────────┐ //! Redoing will take the result at the cursor position, convert that into the //! operation that can revert that result, execute that operation and replace //! the result in the history with the new result, obtained from running the //! inverse operation, advancing the cursor position. //! └──────────────────────────────┬─────────────────────────────────────────────┘ //! │ //! │ //! User Operation Undo v //! Execute Created(CONTRIBUTING.md) ────────> Trash(CONTRIBUTING.md) //! Record Trashed(WorktreeId(1), TrashId(1)) //! History //! 0 Created(src/main.rs) //! 1 Renamed(README.md, readme.md) ─────┐ //! 2 +++cursor+++ │(before the cursor) //! 2 Trashed(WorktreeId(1), TrashId(1)) │ //! │ //! User Operation Undo v //! Execute Renamed(README.md, readme.md) ───> Rename(readme.md, README.md) //! Record Renamed(readme.md, README.md) //! History //! 0 Created(src/main.rs) //! 1 +++cursor+++ //! 1 Renamed(readme.md, README.md) ─────┐ (at the cursor) //! 2 Trashed(WorktreeId(1), TrashId(1)) │ //! │ //! ┌──────────────────────────────────────┴─────────────────────────────────────┐ //! Redoing will take the result at the cursor position, convert that into the //! operation that can revert that result, execute that operation and replace //! the result in the history with the new result, obtained from running the //! inverse operation, advancing the cursor position. //! └─────────────────────────────────────┬──────────────────────────────────────┘ //! │ //! │ //! User Operation Redo v //! Execute Renamed(readme.md, README.md) ───> Rename(README.md, readme.md) //! Record Renamed(README.md, readme.md) //! History //! 0 Created(src/main.rs) //! 1 Renamed(README.md, readme.md) //! 2 +++cursor+++ //! 2 Trashed(WorktreeId(1), TrashId(1)) ─┐ (at the cursor) //! │ //! User Operation Redo v //! Execute Trashed(WorktreeId(1), TrashId(1)) ─> Restore(WorktreeId(1), TrashId(1)) //! Record Restored(ProjectPath) //! History //! 0 Created(src/main.rs) //! 1 Renamed(README.md, readme.md) //! 2 Restored(ProjectPath) //! 2 +++cursor+++ //! //! create A; A //! rename A -> B; B //! undo (rename B -> A) (takes 10s for some reason) B (still b cause it's hanging for 10s) //! remove B _ //! create B B //! put important content in B B //! undo manger renames (does not hang) A //! remove A _ //! user sad //! //! create A; A //! rename A -> B; B //! undo (rename B -> A) (takes 10s for some reason) B (still b cause it's hanging for 10s) //! create C B //! -- src/c.rs //! -- //! //! create docs/files/ directory docs/files/ //! create docs/files/a.txt docs/files/ //! undo (rename B -> A) (takes 10s for some reason) B (still b cause it's hanging for 10s) //! create C B //! -- src/c.rs //! -- //! List of "tainted files" that the user may not operate on use crate::ProjectPanel; use anyhow::{Context, Result, anyhow}; use fs::{TrashId, TrashRestoreError}; use futures::channel::mpsc; use gpui::{AppContext, AsyncApp, IntoElement, SharedString, Styled, Task, WeakEntity}; use markdown::{Markdown, MarkdownElement}; use project::Project; use project::{ProjectPath, WorktreeId}; use std::sync::atomic::{AtomicBool, Ordering}; use std::{collections::VecDeque, sync::Arc}; use ui::{App, TextSize}; use util::{paths::PathStyle, rel_path::RelPath}; use workspace::{ Workspace, notifications::{ NotificationId, markdown_style, simple_message_notification::MessageNotification, }, }; use worktree::CreatedEntry; enum Operation { Trash(ProjectPath), Rename(ProjectPath, ProjectPath), Restore(WorktreeId, TrashId), Batch(Vec<Operation>), } impl Operation { async fn execute(self, undo_manager: &Inner, cx: &mut AsyncApp) -> Result<Change> { Ok(match self { Operation::Trash(project_path) => { let trash_id = undo_manager.trash(&project_path, cx).await?; Change::Trashed(project_path.worktree_id, trash_id) } Operation::Rename(from, to) => { undo_manager.rename(&from, &to, cx).await?; Change::Renamed(from, to) } Operation::Restore(worktree_id, trash_id) => { let project_path = undo_manager.restore(worktree_id, trash_id, cx).await?; Change::Restored(project_path) } Operation::Batch(operations) => { let mut res = Vec::new(); for op in operations { res.push(Box::pin(op.execute(undo_manager, cx)).await?); } Change::Batched(res) } }) } } #[derive(Clone, Debug)] pub(crate) enum Change { Created(ProjectPath), Trashed(WorktreeId, TrashId), Renamed(ProjectPath, ProjectPath), Restored(ProjectPath), Batched(Vec<Change>), } impl Change { fn to_inverse(self) -> Operation { match self { Change::Created(project_path) => Operation::Trash(project_path), Change::Trashed(worktree_id, trash_id) => Operation::Restore(worktree_id, trash_id), Change::Renamed(from, to) => Operation::Rename(to, from), Change::Restored(project_path) => Operation::Trash(project_path), // When inverting a batch of operations, we reverse the order of // operations to handle dependencies between them. For example, if a // batch contains the following order of operations: // // 1. Create `src/` // 2. Create `src/main.rs` // // If we first tried to revert the directory creation, it would fail // because there's still files inside the directory. Change::Batched(changes) => { Operation::Batch(changes.into_iter().rev().map(Change::to_inverse).collect()) } } } } // Imagine pressing undo 10000+ times?! const MAX_UNDO_OPERATIONS: usize = 10_000; struct Inner { workspace: WeakEntity<Workspace>, panel: WeakEntity<ProjectPanel>, history: VecDeque<Change>, cursor: usize, /// Maximum number of operations to keep on the undo history. limit: usize, can_undo: Arc<AtomicBool>, can_redo: Arc<AtomicBool>, rx: mpsc::Receiver<UndoMessage>, } /// pls arc this #[derive(Clone)] pub struct UndoManager { tx: mpsc::Sender<UndoMessage>, is_via_collab: bool, can_undo: Arc<AtomicBool>, can_redo: Arc<AtomicBool>, } impl UndoManager { pub fn new( workspace: WeakEntity<Workspace>, panel: WeakEntity<ProjectPanel>, is_via_collab: bool, cx: &App, ) -> Self { let (tx, rx) = mpsc::channel(1024); let inner = Inner::new(workspace, panel, rx); let this = Self { tx, is_via_collab, can_undo: Arc::clone(&inner.can_undo), can_redo: Arc::clone(&inner.can_redo), }; cx.spawn(async move |cx| inner.manage_undo_and_redo(cx.clone()).await) .detach(); this } pub fn undo(&mut self) -> Result<()> { self.tx .try_send(UndoMessage::Undo) .context("Undo and redo task can not keep up") } pub fn redo(&mut self) -> Result<()> { self.tx .try_send(UndoMessage::Redo) .context("Undo and redo task can not keep up") } pub fn record(&mut self, changes: impl IntoIterator<Item = Change>) -> Result<()> { // In a collab session, undoing or redoing can send `TrashProjectEntry` // or `RestoreProjectEntry`, for example, undoing a create or undoing a // trash. // Since older hosts can't decode those messages, which would // silently never complete, besides disabling the `Undo`/`Redo` actions // for collab, we also avoid recording history here so there's nothing // that could later trigger those messages. if self.is_via_collab { return Ok(()); } self.tx .try_send(UndoMessage::Changed(changes.into_iter().collect())) .context("Undo and redo task can not keep up") } /// just for the UI, an undo may still fail if there are concurrent file /// operations happening. pub fn can_undo(&self) -> bool { self.can_undo.load(Ordering::Relaxed) } /// just for the UI, an undo may still fail if there are concurrent file /// operations happening. pub fn can_redo(&self) -> bool { self.can_redo.load(Ordering::Relaxed) } #[cfg(test)] pub(crate) fn set_is_via_collab(&mut self, is_via_collab: bool) { self.is_via_collab = is_via_collab; } } #[derive(Debug)] enum UndoMessage { Changed(Vec<Change>), Undo, Redo, } impl UndoMessage { fn error_title(&self) -> &'static str { match self { UndoMessage::Changed(_) => { "this is a bug in the manage_undo_and_redo task please report" } UndoMessage::Undo => "Undo Failed", UndoMessage::Redo => "Redo Failed", } } } fn project_path_display( project: &Project, project_path: &ProjectPath, path_style: PathStyle, cx: &App, ) -> String { project .short_full_path_for_project_path(project_path, cx) .unwrap_or_else(|| project_path.path.display(path_style).to_string()) } impl Inner { async fn manage_undo_and_redo(mut self, mut cx: AsyncApp) { loop { let Ok(new) = self.rx.recv().await else { // project panel got closed return; }; let error_title = new.error_title(); let res = match new { UndoMessage::Changed(changes) => { self.record(changes); Ok(()) } UndoMessage::Undo => { let res = self.undo(&mut cx).await; let _ = self.panel.update(&mut cx, |_, cx| cx.notify()); res } UndoMessage::Redo => { let res = self.redo(&mut cx).await; let _ = self.panel.update(&mut cx, |_, cx| cx.notify()); res } }; if let Err(e) = res { Self::show_error( error_title, self.workspace.clone(), format!("{e:#}"), &mut cx, ); } self.can_undo.store(self.can_undo(), Ordering::Relaxed); self.can_redo.store(self.can_redo(), Ordering::Relaxed); } } pub fn new( workspace: WeakEntity<Workspace>, panel: WeakEntity<ProjectPanel>, rx: mpsc::Receiver<UndoMessage>, ) -> Self { Self::new_with_limit(workspace, panel, MAX_UNDO_OPERATIONS, rx) } pub fn new_with_limit( workspace: WeakEntity<Workspace>, panel: WeakEntity<ProjectPanel>, limit: usize, rx: mpsc::Receiver<UndoMessage>, ) -> Self { Self { workspace, panel, history: VecDeque::new(), cursor: 0usize, limit, can_undo: Arc::new(AtomicBool::new(false)), can_redo: Arc::new(AtomicBool::new(false)), rx, } } pub fn can_undo(&self) -> bool { self.cursor > 0 } pub fn can_redo(&self) -> bool { self.cursor < self.history.len() } pub async fn undo(&mut self, cx: &mut AsyncApp) -> Result<()> { if !self.can_undo() { return Ok(()); } // Undo failure: // // History // 0 Created(src/main.rs) // 1 Renamed(README.md, readme.md) ─┐ // 2 +++cursor+++ │(before the cursor) // 2 Trashed(WorktreeId(1), TrashId(1)) │ // │ // User Operation Undo v // Failed execute Renamed(README.md, readme.md) ───> Rename(readme.md, README.md) // Record nothing // History // 0 Created(src/main.rs) // 1 +++cursor+++ // 1 Trashed(WorktreeId(1), TrashId(1)) --------- // |(at the cursor) // User Operation Redo v // Execute Trashed(WorktreeId(1), TrashId(1)) ─> Restore(WorktreeId(1), TrashId(1)) // Record Restored(ProjectPath) // History // 0 Created(src/main.rs) // 1 Restored(ProjectPath) // 1 +++cursor+++ // We always want to move the cursor back regardless of whether undoing // succeeds or fails, otherwise the cursor could end up pointing to a // position outside of the history, as we remove the change before the // cursor, in case undo fails. let before_cursor = self.cursor - 1; // see docs above self.cursor -= 1; // take a step back into the past // If undoing fails, the user would be in a stuck state from which // manual intervention would likely be needed in order to undo. As such, // we remove the change from the `history` even before attempting to // execute its inversion. let undo_change = self .history .remove(before_cursor) .expect("we can undo") .to_inverse() .execute(self, cx) .await?; self.history.insert(before_cursor, undo_change); Ok(()) } pub async fn redo(&mut self, cx: &mut AsyncApp) -> Result<()> { if !self.can_redo() { return Ok(()); } // If redoing fails, the user would be in a stuck state from which // manual intervention would likely be needed in order to redo. As such, // we remove the change from the `history` even before attempting to // execute its inversion. let redo_change = self .history .remove(self.cursor) .expect("we can redo") .to_inverse() .execute(self, cx) .await?; self.history.insert(self.cursor, redo_change); self.cursor += 1; Ok(()) } /// Passed in changes will always be performed as a single step pub fn record(&mut self, mut changes: Vec<Change>) { let change = match changes.len() { 0 => return, 1 => changes.remove(0), _ => Change::Batched(changes), }; // When recording a new change, discard any changes that could still be // redone. if self.cursor < self.history.len() { self.history.drain(self.cursor..); } // Ensure that the number of recorded changes does not exceed the // maximum amount of tracked changes. if self.history.len() >= self.limit {