Align project panel and git panel deletion behavior (#27525)

Mikayla Maki created

This change makes the git panel and project panel behave the same, on
Linux and macOS, and adds prompts.

Release Notes:

- Changed the git panel to prompt before restoring a file.

Change summary

assets/keymaps/default-linux.json |  7 +++-
assets/keymaps/default-macos.json |  5 +++
crates/git/src/git.rs             | 10 ++++++
crates/git_ui/src/git_panel.rs    | 43 +++++++++++++++++++++++++++++---
crates/gpui/src/action.rs         | 36 ++++++++++++++++++++++++++
5 files changed, 92 insertions(+), 9 deletions(-)

Detailed changes

assets/keymaps/default-linux.json 🔗

@@ -754,8 +754,11 @@
       "escape": "git_panel::ToggleFocus",
       "ctrl-enter": "git::Commit",
       "alt-enter": "menu::SecondaryConfirm",
-      "shift-delete": "git::RestoreFile",
-      "ctrl-delete": "git::RestoreFile"
+      "delete": ["git::RestoreFile", { "skip_prompt": false }],
+      "backspace": ["git::RestoreFile", { "skip_prompt": false }],
+      "shift-delete": ["git::RestoreFile", { "skip_prompt": false }],
+      "ctrl-backspace": ["git::RestoreFile", { "skip_prompt": false }],
+      "ctrl-delete": ["git::RestoreFile", { "skip_prompt": false }]
     }
   },
   {

assets/keymaps/default-macos.json 🔗

@@ -803,7 +803,10 @@
       "shift-tab": "git_panel::FocusEditor",
       "escape": "git_panel::ToggleFocus",
       "cmd-enter": "git::Commit",
-      "cmd-backspace": "git::RestoreFile"
+      "backspace": ["git::RestoreFile", { "skip_prompt": false }],
+      "delete": ["git::RestoreFile", { "skip_prompt": false }],
+      "cmd-backspace": ["git::RestoreFile", { "skip_prompt": true }],
+      "cmd-delete": ["git::RestoreFile", { "skip_prompt": true }]
     }
   },
   {

crates/git/src/git.rs 🔗

@@ -11,7 +11,9 @@ use anyhow::{anyhow, Context as _, Result};
 pub use git2 as libgit;
 use gpui::action_with_deprecated_aliases;
 use gpui::actions;
+use gpui::impl_action_with_deprecated_aliases;
 pub use repository::WORK_DIRECTORY_REPO_PATH;
+use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use std::ffi::OsStr;
 use std::fmt;
@@ -54,7 +56,13 @@ actions!(
     ]
 );
 
-action_with_deprecated_aliases!(git, RestoreFile, ["editor::RevertFile"]);
+#[derive(Clone, Debug, Default, PartialEq, Deserialize, JsonSchema)]
+pub struct RestoreFile {
+    #[serde(default)]
+    pub skip_prompt: bool,
+}
+
+impl_action_with_deprecated_aliases!(git, RestoreFile, ["editor::RevertFile"]);
 action_with_deprecated_aliases!(git, Restore, ["editor::RevertSelectedHunks"]);
 action_with_deprecated_aliases!(git, Blame, ["editor::ToggleGitBlame"]);
 

crates/git_ui/src/git_panel.rs 🔗

@@ -935,14 +935,49 @@ impl GitPanel {
 
     fn revert_selected(
         &mut self,
-        _: &git::RestoreFile,
+        action: &git::RestoreFile,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
         maybe!({
+            let skip_prompt = action.skip_prompt;
             let list_entry = self.entries.get(self.selected_entry?)?.clone();
-            let entry = list_entry.status_entry()?;
-            self.revert_entry(&entry, window, cx);
+            let entry = list_entry.status_entry()?.to_owned();
+
+            let prompt = if skip_prompt {
+                Task::ready(Ok(0))
+            } else {
+                let prompt = window.prompt(
+                    PromptLevel::Warning,
+                    &format!(
+                        "Are you sure you want to restore {}?",
+                        entry
+                            .worktree_path
+                            .file_name()
+                            .unwrap_or(entry.worktree_path.as_os_str())
+                            .to_string_lossy()
+                    ),
+                    None,
+                    &["Restore", "Cancel"],
+                    cx,
+                );
+                cx.background_spawn(prompt)
+            };
+
+            let this = cx.weak_entity();
+            window
+                .spawn(cx, async move |cx| {
+                    if prompt.await? != 0 {
+                        return anyhow::Ok(());
+                    }
+
+                    this.update_in(cx, |this, window, cx| {
+                        this.revert_entry(&entry, window, cx);
+                    })?;
+
+                    Ok(())
+                })
+                .detach();
             Some(())
         });
     }
@@ -3460,7 +3495,7 @@ impl GitPanel {
             context_menu
                 .context(self.focus_handle.clone())
                 .action(stage_title, ToggleStaged.boxed_clone())
-                .action(restore_title, git::RestoreFile.boxed_clone())
+                .action(restore_title, git::RestoreFile::default().boxed_clone())
                 .separator()
                 .action("Open Diff", Confirm.boxed_clone())
                 .action("Open File", SecondaryConfirm.boxed_clone())

crates/gpui/src/action.rs 🔗

@@ -375,16 +375,50 @@ macro_rules! action_with_deprecated_aliases {
             $name,
             $name,
             fn build(
-                _: gpui::private::serde_json::Value,
+                value: gpui::private::serde_json::Value,
             ) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>> {
                 Ok(Box::new(Self))
             },
+
             fn action_json_schema(
                 generator: &mut gpui::private::schemars::gen::SchemaGenerator,
             ) -> Option<gpui::private::schemars::schema::Schema> {
                 None
+            },
+
+            fn deprecated_aliases() -> &'static [&'static str] {
+                &[
+                    $($alias),*
+                ]
+            }
+        );
+
+        gpui::register_action!($name);
+    };
+}
+
+/// Defines and registers a unit struct that can be used as an action, with some deprecated aliases.
+#[macro_export]
+macro_rules! impl_action_with_deprecated_aliases {
+    ($namespace:path, $name:ident, [$($alias:literal),* $(,)?]) => {
+        gpui::__impl_action!(
+            $namespace,
+            $name,
+            $name,
+            fn build(
+                value: gpui::private::serde_json::Value,
+            ) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>> {
+                Ok(std::boxed::Box::new(gpui::private::serde_json::from_value::<Self>(value)?))
+            },
 
+            fn action_json_schema(
+                generator: &mut gpui::private::schemars::gen::SchemaGenerator,
+            ) -> Option<gpui::private::schemars::schema::Schema> {
+                Some(<Self as gpui::private::schemars::JsonSchema>::json_schema(
+                    generator,
+                ))
             },
+
             fn deprecated_aliases() -> &'static [&'static str] {
                 &[
                     $($alias),*