From 104d9dd6e89b7a37758775a7aa9831fdbb2161fc Mon Sep 17 00:00:00 2001 From: dino Date: Thu, 19 Mar 2026 19:01:13 +0000 Subject: [PATCH] feat(project_panel): add redo support for rename operations * Update `project_panel::undo::UndoManager::redo` to support redoing rename operations. Support for other opeations will come in future commits * Extract rename logic into `project_panel::undo::UndoManager::rename` as it can be used both when undoing and redoing rename operations * Refactor `project_panel::undo::UndoManager::show_errors` to expect the title as an argument, so that it can be used for both undo and redo errors * Update `project_panel::project_panel_tests::test_undo_rename` to `project_panel::project_panel_tests::test_undo_redo_rename` as it now also tests redoing rename operations --- .../project_panel/src/project_panel_tests.rs | 17 ++- crates/project_panel/src/undo.rs | 116 +++++++++++++----- 2 files changed, 100 insertions(+), 33 deletions(-) diff --git a/crates/project_panel/src/project_panel_tests.rs b/crates/project_panel/src/project_panel_tests.rs index afcc6db8d1600ed7df438d2e3e5546ba13fe4dd0..843e627ae39d1f59d3574168a2d4f36f180cf30d 100644 --- a/crates/project_panel/src/project_panel_tests.rs +++ b/crates/project_panel/src/project_panel_tests.rs @@ -1995,7 +1995,7 @@ async fn test_copy_paste_nested_and_root_entries(cx: &mut gpui::TestAppContext) } #[gpui::test] -async fn test_undo_rename(cx: &mut gpui::TestAppContext) { +async fn test_undo_redo_rename(cx: &mut gpui::TestAppContext) { init_test(cx); let fs = FakeFs::new(cx.executor()); @@ -2054,6 +2054,21 @@ async fn test_undo_rename(cx: &mut gpui::TestAppContext) { None, "Renamed file should no longer exist after undo" ); + + panel.update_in(cx, |panel, window, cx| { + panel.redo(&Redo, window, cx); + }); + cx.run_until_parked(); + + assert!( + find_project_entry(&panel, "root/renamed.txt", cx).is_some(), + "File should be renamed to renamed.txt after redo" + ); + assert_eq!( + find_project_entry(&panel, "root/a.txt", cx), + None, + "Original file should no longer exist after redo" + ); } #[gpui::test] diff --git a/crates/project_panel/src/undo.rs b/crates/project_panel/src/undo.rs index c9384072e302d22fe2cd5d6ffc3358ad39f61d23..624caa68d2244f4359f88898ee8966c644a2740c 100644 --- a/crates/project_panel/src/undo.rs +++ b/crates/project_panel/src/undo.rs @@ -80,7 +80,12 @@ impl UndoManager { .map(|err| SharedString::from(err.to_string())) .collect(); - Self::show_errors(workspace, messages, cx) + Self::show_errors( + "Failed to undo Project Panel Operation(s)", + workspace, + messages, + cx, + ) }) } }) @@ -88,18 +93,52 @@ impl UndoManager { } } - pub fn redo(&mut self, _cx: &mut App) { + pub fn redo(&mut self, cx: &mut App) { if self.cursor >= self.history.len() { return; } - if let Some(_operation) = self.history.get(self.cursor) { - // TODO!: Implement actual operation redo. + if let Some(operation) = self.history.get(self.cursor) { + let task = self.redo_operation(operation, cx); + let workspace = self.workspace.clone(); + + cx.spawn(async move |cx| { + let errors = task.await; + if !errors.is_empty() { + cx.update(|cx| { + let messages = errors + .iter() + .map(|err| SharedString::from(err.to_string())) + .collect(); + + Self::show_errors( + "Failed to redo Project Panel Operation(s)", + workspace, + messages, + cx, + ) + }) + } + }) + .detach(); } self.cursor += 1; } + fn redo_operation( + &self, + operation: &ProjectPanelOperation, + cx: &mut App, + ) -> Task> { + match operation { + ProjectPanelOperation::Rename { old_path, new_path } => { + self.rename(old_path, new_path, cx) + } + _ => Task::ready(vec![anyhow!("Not implemented.")]), + } + } + pub fn record(&mut self, operation: ProjectPanelOperation) { // Recording a new operation while the cursor is not at the end of the // undo history should remove all operations from the cursor position to @@ -172,30 +211,7 @@ impl UndoManager { }) } ProjectPanelOperation::Rename { old_path, new_path } => { - let Some(workspace) = self.workspace.upgrade() else { - return Task::ready(vec![anyhow!("Failed to obtain workspace.")]); - }; - - let result = workspace.update(cx, |workspace, cx| { - workspace.project().update(cx, |project, cx| { - let entry_id = project - .entry_for_path(&new_path, cx) - .map(|entry| entry.id) - .ok_or_else(|| anyhow!("No entry for path."))?; - - Ok(project.rename_entry(entry_id, old_path.clone(), cx)) - }) - }); - - let task = match result { - Ok(task) => task, - Err(err) => return Task::ready(vec![err]), - }; - - cx.spawn(async move |_| match task.await { - Ok(_) => vec![], - Err(err) => vec![err], - }) + self.rename(new_path, old_path, cx) } ProjectPanelOperation::Batch(operations) => { // When reverting operations in a batch, we reverse the order of @@ -226,11 +242,48 @@ impl UndoManager { } } + fn rename( + &self, + from: &ProjectPath, + to: &ProjectPath, + cx: &mut App, + ) -> Task> { + let Some(workspace) = self.workspace.upgrade() else { + return Task::ready(vec![anyhow!("Failed to obtain workspace.")]); + }; + + let result = workspace.update(cx, |workspace, cx| { + workspace.project().update(cx, |project, cx| { + let entry_id = project + .entry_for_path(from, cx) + .map(|entry| entry.id) + .ok_or_else(|| anyhow!("No entry for path."))?; + + Ok(project.rename_entry(entry_id, to.clone(), cx)) + }) + }); + + let task = match result { + Ok(task) => task, + Err(err) => return Task::ready(vec![err]), + }; + + cx.spawn(async move |_| match task.await { + Ok(_) => vec![], + Err(err) => vec![err], + }) + } + /// Displays a notification with the list of provided errors ensuring that, /// when more than one error is provided, which can be the case when dealing /// with undoing a [`crate::undo::ProjectPanelOperation::Batch`], a list is /// displayed with each of the errors, instead of a single message. - fn show_errors(workspace: WeakEntity, messages: Vec, cx: &mut App) { + fn show_errors( + title: impl Into, + workspace: WeakEntity, + messages: Vec, + cx: &mut App, + ) { workspace .update(cx, move |workspace, cx| { let notification_id = @@ -239,8 +292,7 @@ impl UndoManager { workspace.show_notification(notification_id, cx, move |cx| { cx.new(|cx| { if let [err] = messages.as_slice() { - MessageNotification::new(err.to_string(), cx) - .with_title("Failed to undo Project Panel Operation") + MessageNotification::new(err.to_string(), cx).with_title(title) } else { MessageNotification::new_from_builder(cx, move |_, _| { v_flex() @@ -252,7 +304,7 @@ impl UndoManager { ) .into_any_element() }) - .with_title("Failed to undo Project Panel Operations") + .with_title(title) } }) })