Add keyboard shortcut to suppress prompt while deleting files/dirs (#9452)

Mayfield created

Completes #7228.
Adds back Backspace as the main delete key binding and makes Linux
bindings consistent with macOS

Release Notes:
- ⌘-Delete/⌘-Backspace will now suppress deletion confirmation prompts
in project panel
([#7228](https://github.com/zed-industries/zed/issues/7228)).

Change summary

assets/keymaps/default-linux.json         |  3 +
assets/keymaps/default-macos.json         |  4 +
crates/gpui/src/app/entity_map.rs         |  2 
crates/project_panel/src/project_panel.rs | 62 +++++++++++++++++-------
docs/src/configuring_zed__key_bindings.md |  1 
5 files changed, 52 insertions(+), 20 deletions(-)

Detailed changes

assets/keymaps/default-linux.json 🔗

@@ -514,6 +514,9 @@
       "f2": "project_panel::Rename",
       "enter": "project_panel::Rename",
       "backspace": "project_panel::Delete",
+      "delete": "project_panel::Delete",
+      "ctrl-backspace": ["project_panel::Delete", { "skip_prompt": true }],
+      "ctrl-delete": ["project_panel::Delete", { "skip_prompt": true }],
       "ctrl-alt-r": "project_panel::RevealInFinder",
       "alt-shift-f": "project_panel::NewSearchInDirectory"
     }

assets/keymaps/default-macos.json 🔗

@@ -561,8 +561,10 @@
       "alt-cmd-shift-c": "project_panel::CopyRelativePath",
       "f2": "project_panel::Rename",
       "enter": "project_panel::Rename",
+      "backspace": "project_panel::Delete",
       "delete": "project_panel::Delete",
-      "cmd-backspace": "project_panel::Delete",
+      "cmd-backspace": ["project_panel::Delete", { "skip_prompt": true }],
+      "cmd-delete": ["project_panel::Delete", { "skip_prompt": true }],
       "alt-cmd-r": "project_panel::RevealInFinder",
       "alt-shift-f": "project_panel::NewSearchInDirectory"
     }

crates/gpui/src/app/entity_map.rs 🔗

@@ -394,7 +394,7 @@ impl<T: 'static> Model<T> {
     ///
     /// The update function receives a context appropriate for its environment.
     /// When updating in an `AppContext`, it receives a `ModelContext`.
-    /// When updating an a `WindowContext`, it receives a `ViewContext`.
+    /// When updating in a `WindowContext`, it receives a `ViewContext`.
     pub fn update<C, R>(
         &self,
         cx: &mut C,

crates/project_panel/src/project_panel.rs 🔗

@@ -10,11 +10,11 @@ use file_associations::FileAssociations;
 use anyhow::{anyhow, Result};
 use collections::{hash_map, HashMap};
 use gpui::{
-    actions, div, overlay, px, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext,
-    ClipboardItem, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, InteractiveElement,
-    KeyContext, Model, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel,
-    Render, Stateful, Styled, Subscription, Task, UniformListScrollHandle, View, ViewContext,
-    VisualContext as _, WeakView, WindowContext,
+    actions, div, impl_actions, overlay, px, uniform_list, Action, AppContext, AssetSource,
+    AsyncWindowContext, ClipboardItem, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView,
+    InteractiveElement, KeyContext, Model, MouseButton, MouseDownEvent, ParentElement, Pixels,
+    Point, PromptLevel, Render, Stateful, Styled, Subscription, Task, UniformListScrollHandle,
+    View, ViewContext, VisualContext as _, WeakView, WindowContext,
 };
 use menu::{Confirm, SelectNext, SelectPrev};
 use project::{
@@ -106,6 +106,13 @@ pub struct EntryDetails {
     is_dotenv: bool,
 }
 
+#[derive(PartialEq, Clone, Default, Debug, Deserialize)]
+pub struct Delete {
+    pub skip_prompt: bool,
+}
+
+impl_actions!(project_panel, [Delete]);
+
 actions!(
     project_panel,
     [
@@ -121,7 +128,6 @@ actions!(
         OpenInTerminal,
         Cut,
         Paste,
-        Delete,
         Rename,
         Open,
         ToggleFocus,
@@ -448,7 +454,9 @@ impl ProjectPanel {
                         })
                         .separator()
                         .action("Rename", Box::new(Rename))
-                        .when(!is_root, |menu| menu.action("Delete", Box::new(Delete)))
+                        .when(!is_root, |menu| {
+                            menu.action("Delete", Box::new(Delete { skip_prompt: false }))
+                        })
                     },
                 )
             });
@@ -793,22 +801,26 @@ impl ProjectPanel {
         }
     }
 
-    fn delete(&mut self, _: &Delete, cx: &mut ViewContext<Self>) {
+    fn delete(&mut self, action: &Delete, cx: &mut ViewContext<Self>) {
         maybe!({
             let Selection { entry_id, .. } = self.selection?;
             let path = self.project.read(cx).path_for_entry(entry_id, cx)?.path;
             let file_name = path.file_name()?;
 
-            let answer = cx.prompt(
-                PromptLevel::Info,
-                &format!("Delete {file_name:?}?"),
-                None,
-                &["Delete", "Cancel"],
-            );
+            let answer = (!action.skip_prompt).then(|| {
+                cx.prompt(
+                    PromptLevel::Info,
+                    &format!("Delete {file_name:?}?"),
+                    None,
+                    &["Delete", "Cancel"],
+                )
+            });
 
             cx.spawn(|this, mut cx| async move {
-                if answer.await != Ok(0) {
-                    return Ok(());
+                if let Some(answer) = answer {
+                    if answer.await != Ok(0) {
+                        return Ok(());
+                    }
                 }
                 this.update(&mut cx, |this, cx| {
                     this.project
@@ -2691,7 +2703,7 @@ mod tests {
                 open_editor.update(cx, |editor, cx| editor.set_text("Another text!", cx));
             })
             .unwrap();
-        submit_deletion(&panel, cx);
+        submit_deletion_skipping_prompt(&panel, cx);
         assert_eq!(
             visible_entries_as_strings(&panel, 0..10, cx),
             &["v src", "    v test", "          third.rs"],
@@ -3652,7 +3664,9 @@ mod tests {
             !cx.has_pending_prompt(),
             "Should have no prompts before the deletion"
         );
-        panel.update(cx, |panel, cx| panel.delete(&Delete, cx));
+        panel.update(cx, |panel, cx| {
+            panel.delete(&Delete { skip_prompt: false }, cx)
+        });
         assert!(
             cx.has_pending_prompt(),
             "Should have a prompt after the deletion"
@@ -3665,6 +3679,18 @@ mod tests {
         cx.executor().run_until_parked();
     }
 
+    fn submit_deletion_skipping_prompt(panel: &View<ProjectPanel>, cx: &mut VisualTestContext) {
+        assert!(
+            !cx.has_pending_prompt(),
+            "Should have no prompts before the deletion"
+        );
+        panel.update(cx, |panel, cx| {
+            panel.delete(&Delete { skip_prompt: true }, cx)
+        });
+        assert!(!cx.has_pending_prompt(), "Should have received no prompts");
+        cx.executor().run_until_parked();
+    }
+
     fn ensure_no_open_items_and_panes(
         workspace: &WindowHandle<Workspace>,
         cx: &mut VisualTestContext,

docs/src/configuring_zed__key_bindings.md 🔗

@@ -378,6 +378,7 @@ But, it was impossible to take into account the `{` and `}` when he was typing s
 | Copy relative path      | Project Panel | `Alt` + `⌘` + `Shift` + `C` |
 | Cut                     | Project Panel | `⌘` + `X`                   |
 | Delete                  | Project Panel | `Backspace`                 |
+| Delete (no prompt)      | Project Panel | `⌘` + `Backspace`           |
 | Expand selected entry   | Project Panel | `Right`                     |
 | New directory           | Project Panel | `Alt` + `⌘` + `N`           |
 | New file                | Project Panel | `Command + N`               |