Add keyboard shortcuts for the prompts on Linux (#13915)

Aleksei Gusev and Thorsten Ball created

This change adds ability to choose any action from prompts, not just the
default one and cancel as Zed has right now. For example, when a user
tries to close a file with edits in it the prompt offers "Don't save"
option that can be selected only with mouse. Now you can use arrows,
tab/shift-tab to pick action and enter/space to confirm it.

Fixes [#13906](https://github.com/zed-industries/zed/issues/13906)


Release Notes:

- Added keyboard navigation in the prompts on Linux
([#13906](https://github.com/zed-industries/zed/issues/13906)).


Co-authored-by: Thorsten Ball <mrnugget@gmail.com>

Change summary

assets/keymaps/default-linux.json   |  4 +++
assets/keymaps/default-macos.json   |  4 +++
crates/zed/src/zed/linux_prompts.rs | 38 ++++++++++++++++++++++++++++--
3 files changed, 43 insertions(+), 3 deletions(-)

Detailed changes

assets/keymaps/default-linux.json 🔗

@@ -3,10 +3,14 @@
   {
     "bindings": {
       "up": "menu::SelectPrev",
+      "shift-tab": "menu::SelectPrev",
+      "home": "menu::SelectFirst",
       "pageup": "menu::SelectFirst",
       "shift-pageup": "menu::SelectFirst",
       "ctrl-p": "menu::SelectPrev",
       "down": "menu::SelectNext",
+      "tab": "menu::SelectNext",
+      "end": "menu::SelectLast",
       "pagedown": "menu::SelectLast",
       "shift-pagedown": "menu::SelectFirst",
       "ctrl-n": "menu::SelectNext",

assets/keymaps/default-macos.json 🔗

@@ -3,10 +3,14 @@
   {
     "bindings": {
       "up": "menu::SelectPrev",
+      "shift-tab": "menu::SelectPrev",
+      "home": "menu::SelectFirst",
       "pageup": "menu::SelectFirst",
       "shift-pageup": "menu::SelectFirst",
       "ctrl-p": "menu::SelectPrev",
       "down": "menu::SelectNext",
+      "tab": "menu::SelectNext",
+      "end": "menu::SelectLast",
       "pagedown": "menu::SelectLast",
       "shift-pagedown": "menu::SelectFirst",
       "ctrl-n": "menu::SelectNext",

crates/zed/src/zed/linux_prompts.rs 🔗

@@ -31,6 +31,7 @@ pub fn fallback_prompt_renderer(
             detail: detail.map(ToString::to_string),
             actions: actions.iter().map(ToString::to_string).collect(),
             focus: cx.focus_handle(),
+            active_action_id: actions.len() - 1,
         }
     });
 
@@ -44,10 +45,12 @@ pub struct FallbackPromptRenderer {
     detail: Option<String>,
     actions: Vec<String>,
     focus: FocusHandle,
+    active_action_id: usize,
 }
+
 impl FallbackPromptRenderer {
     fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
-        cx.emit(PromptResponse(0));
+        cx.emit(PromptResponse(self.active_action_id));
     }
 
     fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
@@ -55,7 +58,32 @@ impl FallbackPromptRenderer {
             cx.emit(PromptResponse(ix));
         }
     }
+
+    fn select_first(&mut self, _: &menu::SelectFirst, cx: &mut ViewContext<Self>) {
+        self.active_action_id = 0;
+        cx.notify();
+    }
+
+    fn select_last(&mut self, _: &menu::SelectLast, cx: &mut ViewContext<Self>) {
+        self.active_action_id = self.actions.len().saturating_sub(1);
+        cx.notify();
+    }
+
+    fn select_next(&mut self, _: &menu::SelectNext, cx: &mut ViewContext<Self>) {
+        self.active_action_id = (self.active_action_id + 1) % self.actions.len();
+        cx.notify();
+    }
+
+    fn select_prev(&mut self, _: &menu::SelectPrev, cx: &mut ViewContext<Self>) {
+        if self.active_action_id > 0 {
+            self.active_action_id -= 1;
+        } else {
+            self.active_action_id = self.actions.len().saturating_sub(1);
+        }
+        cx.notify();
+    }
 }
+
 impl Render for FallbackPromptRenderer {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
         let settings = ThemeSettings::get_global(cx);
@@ -66,6 +94,10 @@ impl Render for FallbackPromptRenderer {
             .track_focus(&self.focus)
             .on_action(cx.listener(Self::confirm))
             .on_action(cx.listener(Self::cancel))
+            .on_action(cx.listener(Self::select_next))
+            .on_action(cx.listener(Self::select_prev))
+            .on_action(cx.listener(Self::select_first))
+            .on_action(cx.listener(Self::select_last))
             .elevation_3(cx)
             .w_72()
             .overflow_hidden()
@@ -87,11 +119,11 @@ impl Render for FallbackPromptRenderer {
                     .child(detail)
             }))
             .child(h_flex().justify_end().gap_2().children(
-                self.actions.iter().enumerate().rev().map(|(ix, action)| {
+                self.actions.iter().rev().enumerate().map(|(ix, action)| {
                     ui::Button::new(ix, action.clone())
                         .label_size(LabelSize::Large)
                         .style(ButtonStyle::Filled)
-                        .when(ix == 0, |el| {
+                        .when(ix == self.active_action_id, |el| {
                             el.style(ButtonStyle::Tinted(TintColor::Accent))
                         })
                         .layer(ElevationIndex::ModalSurface)