Add missing shortcuts in tooltips (#18282)

Bennet Bo Fenner created

Fixes some missing shortcuts from Tooltips like the project search,
buffer search, quick action bar, ....


https://github.com/user-attachments/assets/d3a0160a-8d6e-4ddc-bf82-1fabeca42d59

This should hopefully help new users learn and discover some nice
keyboard shortcuts

Release Notes:

- Display keyboard shortcuts inside tooltips in the project search,
buffer search etc.

Change summary

assets/keymaps/default-linux.json               |  2 
assets/keymaps/default-macos.json               |  2 
crates/breadcrumbs/src/breadcrumbs.rs           | 35 +++++--
crates/quick_action_bar/src/quick_action_bar.rs | 15 ++
crates/search/src/buffer_search.rs              | 80 +++++++++++++++---
crates/search/src/project_search.rs             | 78 ++++++++++++++++-
crates/search/src/search.rs                     |  5 
crates/search/src/search_bar.rs                 |  5 
crates/terminal_view/src/terminal_panel.rs      | 20 ++++
9 files changed, 199 insertions(+), 43 deletions(-)

Detailed changes

assets/keymaps/default-linux.json 🔗

@@ -196,7 +196,7 @@
     }
   },
   {
-    "context": "BufferSearchBar && in_replace",
+    "context": "BufferSearchBar && in_replace > Editor",
     "bindings": {
       "enter": "search::ReplaceNext",
       "ctrl-enter": "search::ReplaceAll"

assets/keymaps/default-macos.json 🔗

@@ -232,7 +232,7 @@
     }
   },
   {
-    "context": "BufferSearchBar && in_replace",
+    "context": "BufferSearchBar && in_replace > Editor",
     "bindings": {
       "enter": "search::ReplaceNext",
       "cmd-enter": "search::ReplaceAll"

crates/breadcrumbs/src/breadcrumbs.rs 🔗

@@ -1,7 +1,7 @@
 use editor::Editor;
 use gpui::{
-    Element, EventEmitter, IntoElement, ParentElement, Render, StyledText, Subscription,
-    ViewContext,
+    Element, EventEmitter, FocusableView, IntoElement, ParentElement, Render, StyledText,
+    Subscription, ViewContext,
 };
 use itertools::Itertools;
 use std::cmp;
@@ -90,17 +90,30 @@ impl Render for Breadcrumbs {
                 ButtonLike::new("toggle outline view")
                     .child(breadcrumbs_stack)
                     .style(ButtonStyle::Transparent)
-                    .on_click(move |_, cx| {
-                        if let Some(editor) = editor.upgrade() {
-                            outline::toggle(editor, &editor::actions::ToggleOutline, cx)
+                    .on_click({
+                        let editor = editor.clone();
+                        move |_, cx| {
+                            if let Some(editor) = editor.upgrade() {
+                                outline::toggle(editor, &editor::actions::ToggleOutline, cx)
+                            }
                         }
                     })
-                    .tooltip(|cx| {
-                        Tooltip::for_action(
-                            "Show symbol outline",
-                            &editor::actions::ToggleOutline,
-                            cx,
-                        )
+                    .tooltip(move |cx| {
+                        if let Some(editor) = editor.upgrade() {
+                            let focus_handle = editor.read(cx).focus_handle(cx);
+                            Tooltip::for_action_in(
+                                "Show symbol outline",
+                                &editor::actions::ToggleOutline,
+                                &focus_handle,
+                                cx,
+                            )
+                        } else {
+                            Tooltip::for_action(
+                                "Show symbol outline",
+                                &editor::actions::ToggleOutline,
+                                cx,
+                            )
+                        }
                     }),
             ),
             None => element

crates/quick_action_bar/src/quick_action_bar.rs 🔗

@@ -8,8 +8,8 @@ use editor::actions::{
 use editor::{Editor, EditorSettings};
 
 use gpui::{
-    Action, AnchorCorner, ClickEvent, ElementId, EventEmitter, InteractiveElement, ParentElement,
-    Render, Styled, Subscription, View, ViewContext, WeakView,
+    Action, AnchorCorner, ClickEvent, ElementId, EventEmitter, FocusHandle, FocusableView,
+    InteractiveElement, ParentElement, Render, Styled, Subscription, View, ViewContext, WeakView,
 };
 use search::{buffer_search, BufferSearchBar};
 use settings::{Settings, SettingsStore};
@@ -110,12 +110,15 @@ impl Render for QuickActionBar {
             )
         };
 
+        let focus_handle = editor.read(cx).focus_handle(cx);
+
         let search_button = editor.is_singleton(cx).then(|| {
             QuickActionBarButton::new(
                 "toggle buffer search",
                 IconName::MagnifyingGlass,
                 !self.buffer_search_bar.read(cx).is_dismissed(),
                 Box::new(buffer_search::Deploy::find()),
+                focus_handle.clone(),
                 "Buffer Search",
                 {
                     let buffer_search_bar = self.buffer_search_bar.clone();
@@ -133,6 +136,7 @@ impl Render for QuickActionBar {
             IconName::ZedAssistant,
             false,
             Box::new(InlineAssist::default()),
+            focus_handle.clone(),
             "Inline Assist",
             {
                 let workspace = self.workspace.clone();
@@ -321,6 +325,7 @@ struct QuickActionBarButton {
     icon: IconName,
     toggled: bool,
     action: Box<dyn Action>,
+    focus_handle: FocusHandle,
     tooltip: SharedString,
     on_click: Box<dyn Fn(&ClickEvent, &mut WindowContext)>,
 }
@@ -331,6 +336,7 @@ impl QuickActionBarButton {
         icon: IconName,
         toggled: bool,
         action: Box<dyn Action>,
+        focus_handle: FocusHandle,
         tooltip: impl Into<SharedString>,
         on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
     ) -> Self {
@@ -339,6 +345,7 @@ impl QuickActionBarButton {
             icon,
             toggled,
             action,
+            focus_handle,
             tooltip: tooltip.into(),
             on_click: Box::new(on_click),
         }
@@ -355,7 +362,9 @@ impl RenderOnce for QuickActionBarButton {
             .icon_size(IconSize::Small)
             .style(ButtonStyle::Subtle)
             .selected(self.toggled)
-            .tooltip(move |cx| Tooltip::for_action(tooltip.clone(), &*action, cx))
+            .tooltip(move |cx| {
+                Tooltip::for_action_in(tooltip.clone(), &*action, &self.focus_handle, cx)
+            })
             .on_click(move |event, cx| (self.on_click)(event, cx))
     }
 }

crates/search/src/buffer_search.rs 🔗

@@ -13,9 +13,10 @@ use editor::{
 };
 use futures::channel::oneshot;
 use gpui::{
-    actions, div, impl_actions, Action, AppContext, ClickEvent, EventEmitter, FocusableView, Hsla,
-    InteractiveElement as _, IntoElement, KeyContext, ParentElement as _, Render, ScrollHandle,
-    Styled, Subscription, Task, TextStyle, View, ViewContext, VisualContext as _, WindowContext,
+    actions, div, impl_actions, Action, AppContext, ClickEvent, EventEmitter, FocusHandle,
+    FocusableView, Hsla, InteractiveElement as _, IntoElement, KeyContext, ParentElement as _,
+    Render, ScrollHandle, Styled, Subscription, Task, TextStyle, View, ViewContext,
+    VisualContext as _, WindowContext,
 };
 use project::{
     search::SearchQuery,
@@ -142,6 +143,8 @@ impl Render for BufferSearchBar {
             return div().id("search_bar");
         }
 
+        let focus_handle = self.focus_handle(cx);
+
         let narrow_mode =
             self.scroll_handle.bounds().size.width / cx.rem_size() < 340. / BASE_REM_SIZE_IN_PX;
         let hide_inline_icons = self.editor_needed_width
@@ -217,6 +220,7 @@ impl Render for BufferSearchBar {
                         div.children(supported_options.case.then(|| {
                             self.render_search_option_button(
                                 SearchOptions::CASE_SENSITIVE,
+                                focus_handle.clone(),
                                 cx.listener(|this, _, cx| {
                                     this.toggle_case_sensitive(&ToggleCaseSensitive, cx)
                                 }),
@@ -225,6 +229,7 @@ impl Render for BufferSearchBar {
                         .children(supported_options.word.then(|| {
                             self.render_search_option_button(
                                 SearchOptions::WHOLE_WORD,
+                                focus_handle.clone(),
                                 cx.listener(|this, _, cx| {
                                     this.toggle_whole_word(&ToggleWholeWord, cx)
                                 }),
@@ -233,6 +238,7 @@ impl Render for BufferSearchBar {
                         .children(supported_options.regex.then(|| {
                             self.render_search_option_button(
                                 SearchOptions::REGEX,
+                                focus_handle.clone(),
                                 cx.listener(|this, _, cx| this.toggle_regex(&ToggleRegex, cx)),
                             )
                         }))
@@ -250,7 +256,17 @@ impl Render for BufferSearchBar {
                         }))
                         .selected(self.replace_enabled)
                         .size(ButtonSize::Compact)
-                        .tooltip(|cx| Tooltip::for_action("Toggle replace", &ToggleReplace, cx)),
+                        .tooltip({
+                            let focus_handle = focus_handle.clone();
+                            move |cx| {
+                                Tooltip::for_action_in(
+                                    "Toggle replace",
+                                    &ToggleReplace,
+                                    &focus_handle,
+                                    cx,
+                                )
+                            }
+                        }),
                 )
             })
             .when(supported_options.selection, |this| {
@@ -268,8 +284,16 @@ impl Render for BufferSearchBar {
                     }))
                     .selected(self.selection_search_enabled)
                     .size(ButtonSize::Compact)
-                    .tooltip(|cx| {
-                        Tooltip::for_action("Toggle search selection", &ToggleSelection, cx)
+                    .tooltip({
+                        let focus_handle = focus_handle.clone();
+                        move |cx| {
+                            Tooltip::for_action_in(
+                                "Toggle search selection",
+                                &ToggleSelection,
+                                &focus_handle,
+                                cx,
+                            )
+                        }
                     }),
                 )
             })
@@ -280,8 +304,16 @@ impl Render for BufferSearchBar {
                         IconButton::new("select-all", ui::IconName::SelectAll)
                             .on_click(|_, cx| cx.dispatch_action(SelectAllMatches.boxed_clone()))
                             .size(ButtonSize::Compact)
-                            .tooltip(|cx| {
-                                Tooltip::for_action("Select all matches", &SelectAllMatches, cx)
+                            .tooltip({
+                                let focus_handle = focus_handle.clone();
+                                move |cx| {
+                                    Tooltip::for_action_in(
+                                        "Select all matches",
+                                        &SelectAllMatches,
+                                        &focus_handle,
+                                        cx,
+                                    )
+                                }
                             }),
                     )
                     .child(render_nav_button(
@@ -289,12 +321,14 @@ impl Render for BufferSearchBar {
                         self.active_match_index.is_some(),
                         "Select previous match",
                         &SelectPrevMatch,
+                        focus_handle.clone(),
                     ))
                     .child(render_nav_button(
                         ui::IconName::ChevronRight,
                         self.active_match_index.is_some(),
                         "Select next match",
                         &SelectNextMatch,
+                        focus_handle.clone(),
                     ))
                     .when(!narrow_mode, |this| {
                         this.child(h_flex().ml_2().min_w(rems_from_px(40.)).child(
@@ -335,8 +369,16 @@ impl Render for BufferSearchBar {
                         .flex_none()
                         .child(
                             IconButton::new("search-replace-next", ui::IconName::ReplaceNext)
-                                .tooltip(move |cx| {
-                                    Tooltip::for_action("Replace next", &ReplaceNext, cx)
+                                .tooltip({
+                                    let focus_handle = focus_handle.clone();
+                                    move |cx| {
+                                        Tooltip::for_action_in(
+                                            "Replace next match",
+                                            &ReplaceNext,
+                                            &focus_handle,
+                                            cx,
+                                        )
+                                    }
                                 })
                                 .on_click(
                                     cx.listener(|this, _, cx| this.replace_next(&ReplaceNext, cx)),
@@ -344,8 +386,16 @@ impl Render for BufferSearchBar {
                         )
                         .child(
                             IconButton::new("search-replace-all", ui::IconName::ReplaceAll)
-                                .tooltip(move |cx| {
-                                    Tooltip::for_action("Replace all", &ReplaceAll, cx)
+                                .tooltip({
+                                    let focus_handle = focus_handle.clone();
+                                    move |cx| {
+                                        Tooltip::for_action_in(
+                                            "Replace all matches",
+                                            &ReplaceAll,
+                                            &focus_handle,
+                                            cx,
+                                        )
+                                    }
                                 })
                                 .on_click(
                                     cx.listener(|this, _, cx| this.replace_all(&ReplaceAll, cx)),
@@ -719,10 +769,11 @@ impl BufferSearchBar {
     fn render_search_option_button(
         &self,
         option: SearchOptions,
+        focus_handle: FocusHandle,
         action: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
     ) -> impl IntoElement {
         let is_active = self.search_options.contains(option);
-        option.as_button(is_active, action)
+        option.as_button(is_active, focus_handle, action)
     }
 
     pub fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext<Self>) {
@@ -1122,6 +1173,7 @@ impl BufferSearchBar {
         });
         cx.focus(handle);
     }
+
     fn toggle_replace(&mut self, _: &ToggleReplace, cx: &mut ViewContext<Self>) {
         if self.active_searchable_item.is_some() {
             self.replace_enabled = !self.replace_enabled;
@@ -1134,6 +1186,7 @@ impl BufferSearchBar {
             cx.notify();
         }
     }
+
     fn replace_next(&mut self, _: &ReplaceNext, cx: &mut ViewContext<Self>) {
         let mut should_propagate = true;
         if !self.dismissed && self.active_search.is_some() {
@@ -1161,6 +1214,7 @@ impl BufferSearchBar {
             cx.stop_propagation();
         }
     }
+
     pub fn replace_all(&mut self, _: &ReplaceAll, cx: &mut ViewContext<Self>) {
         if !self.dismissed && self.active_search.is_some() {
             if let Some(searchable_item) = self.active_searchable_item.as_ref() {

crates/search/src/project_search.rs 🔗

@@ -1551,6 +1551,7 @@ impl Render for ProjectSearchBar {
             return div();
         };
         let search = search.read(cx);
+        let focus_handle = search.focus_handle(cx);
 
         let query_column = h_flex()
             .flex_1()
@@ -1571,18 +1572,21 @@ impl Render for ProjectSearchBar {
                 h_flex()
                     .child(SearchOptions::CASE_SENSITIVE.as_button(
                         self.is_option_enabled(SearchOptions::CASE_SENSITIVE, cx),
+                        focus_handle.clone(),
                         cx.listener(|this, _, cx| {
                             this.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx);
                         }),
                     ))
                     .child(SearchOptions::WHOLE_WORD.as_button(
                         self.is_option_enabled(SearchOptions::WHOLE_WORD, cx),
+                        focus_handle.clone(),
                         cx.listener(|this, _, cx| {
                             this.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
                         }),
                     ))
                     .child(SearchOptions::REGEX.as_button(
                         self.is_option_enabled(SearchOptions::REGEX, cx),
+                        focus_handle.clone(),
                         cx.listener(|this, _, cx| {
                             this.toggle_search_option(SearchOptions::REGEX, cx);
                         }),
@@ -1603,7 +1607,17 @@ impl Render for ProjectSearchBar {
                                 .map(|search| search.read(cx).filters_enabled)
                                 .unwrap_or_default(),
                         )
-                        .tooltip(|cx| Tooltip::for_action("Toggle filters", &ToggleFilters, cx)),
+                        .tooltip({
+                            let focus_handle = focus_handle.clone();
+                            move |cx| {
+                                Tooltip::for_action_in(
+                                    "Toggle filters",
+                                    &ToggleFilters,
+                                    &focus_handle,
+                                    cx,
+                                )
+                            }
+                        }),
                 )
                 .child(
                     IconButton::new("project-search-toggle-replace", IconName::Replace)
@@ -1616,7 +1630,17 @@ impl Render for ProjectSearchBar {
                                 .map(|search| search.read(cx).replace_enabled)
                                 .unwrap_or_default(),
                         )
-                        .tooltip(|cx| Tooltip::for_action("Toggle replace", &ToggleReplace, cx)),
+                        .tooltip({
+                            let focus_handle = focus_handle.clone();
+                            move |cx| {
+                                Tooltip::for_action_in(
+                                    "Toggle replace",
+                                    &ToggleReplace,
+                                    &focus_handle,
+                                    cx,
+                                )
+                            }
+                        }),
                 ),
         );
 
@@ -1650,8 +1674,16 @@ impl Render for ProjectSearchBar {
                             })
                         }
                     }))
-                    .tooltip(|cx| {
-                        Tooltip::for_action("Go to previous match", &SelectPrevMatch, cx)
+                    .tooltip({
+                        let focus_handle = focus_handle.clone();
+                        move |cx| {
+                            Tooltip::for_action_in(
+                                "Go to previous match",
+                                &SelectPrevMatch,
+                                &focus_handle,
+                                cx,
+                            )
+                        }
                     }),
             )
             .child(
@@ -1664,7 +1696,17 @@ impl Render for ProjectSearchBar {
                             })
                         }
                     }))
-                    .tooltip(|cx| Tooltip::for_action("Go to next match", &SelectNextMatch, cx)),
+                    .tooltip({
+                        let focus_handle = focus_handle.clone();
+                        move |cx| {
+                            Tooltip::for_action_in(
+                                "Go to next match",
+                                &SelectNextMatch,
+                                &focus_handle,
+                                cx,
+                            )
+                        }
+                    }),
             )
             .child(
                 h_flex()
@@ -1702,6 +1744,7 @@ impl Render for ProjectSearchBar {
                 .border_color(cx.theme().colors().border)
                 .rounded_lg()
                 .child(self.render_text_input(&search.replacement_editor, cx));
+            let focus_handle = search.replacement_editor.read(cx).focus_handle(cx);
             let replace_actions = h_flex().when(search.replace_enabled, |this| {
                 this.child(
                     IconButton::new("project-search-replace-next", IconName::ReplaceNext)
@@ -1712,7 +1755,17 @@ impl Render for ProjectSearchBar {
                                 })
                             }
                         }))
-                        .tooltip(|cx| Tooltip::for_action("Replace next match", &ReplaceNext, cx)),
+                        .tooltip({
+                            let focus_handle = focus_handle.clone();
+                            move |cx| {
+                                Tooltip::for_action_in(
+                                    "Replace next match",
+                                    &ReplaceNext,
+                                    &focus_handle,
+                                    cx,
+                                )
+                            }
+                        }),
                 )
                 .child(
                     IconButton::new("project-search-replace-all", IconName::ReplaceAll)
@@ -1723,7 +1776,17 @@ impl Render for ProjectSearchBar {
                                 })
                             }
                         }))
-                        .tooltip(|cx| Tooltip::for_action("Replace all matches", &ReplaceAll, cx)),
+                        .tooltip({
+                            let focus_handle = focus_handle.clone();
+                            move |cx| {
+                                Tooltip::for_action_in(
+                                    "Replace all matches",
+                                    &ReplaceAll,
+                                    &focus_handle,
+                                    cx,
+                                )
+                            }
+                        }),
                 )
             });
             h_flex()
@@ -1790,6 +1853,7 @@ impl Render for ProjectSearchBar {
                         search
                             .search_options
                             .contains(SearchOptions::INCLUDE_IGNORED),
+                        focus_handle.clone(),
                         cx.listener(|this, _, cx| {
                             this.toggle_search_option(SearchOptions::INCLUDE_IGNORED, cx);
                         }),

crates/search/src/search.rs 🔗

@@ -1,7 +1,7 @@
 use bitflags::bitflags;
 pub use buffer_search::BufferSearchBar;
 use editor::SearchSettings;
-use gpui::{actions, Action, AppContext, IntoElement};
+use gpui::{actions, Action, AppContext, FocusHandle, IntoElement};
 use project::search::SearchQuery;
 pub use project_search::ProjectSearchView;
 use ui::{prelude::*, Tooltip};
@@ -106,6 +106,7 @@ impl SearchOptions {
     pub fn as_button(
         &self,
         active: bool,
+        focus_handle: FocusHandle,
         action: impl Fn(&gpui::ClickEvent, &mut WindowContext) + 'static,
     ) -> impl IntoElement {
         IconButton::new(self.label(), self.icon())
@@ -115,7 +116,7 @@ impl SearchOptions {
             .tooltip({
                 let action = self.to_toggle_action();
                 let label = self.label();
-                move |cx| Tooltip::for_action(label, &*action, cx)
+                move |cx| Tooltip::for_action_in(label, &*action, &focus_handle, cx)
             })
     }
 }

crates/search/src/search_bar.rs 🔗

@@ -1,4 +1,4 @@
-use gpui::{Action, IntoElement};
+use gpui::{Action, FocusHandle, IntoElement};
 use ui::IconButton;
 use ui::{prelude::*, Tooltip};
 
@@ -7,12 +7,13 @@ pub(super) fn render_nav_button(
     active: bool,
     tooltip: &'static str,
     action: &'static dyn Action,
+    focus_handle: FocusHandle,
 ) -> impl IntoElement {
     IconButton::new(
         SharedString::from(format!("search-nav-button-{}", action.name())),
         icon,
     )
     .on_click(|_, cx| cx.dispatch_action(action.boxed_clone()))
-    .tooltip(move |cx| Tooltip::for_action(tooltip, action, cx))
+    .tooltip(move |cx| Tooltip::for_action_in(tooltip, action, &focus_handle, cx))
     .disabled(!active)
 }

crates/terminal_view/src/terminal_panel.rs 🔗

@@ -166,7 +166,16 @@ impl TerminalPanel {
     pub fn asssistant_enabled(&mut self, enabled: bool, cx: &mut ViewContext<Self>) {
         self.assistant_enabled = enabled;
         if enabled {
-            self.assistant_tab_bar_button = Some(cx.new_view(|_| InlineAssistTabBarButton).into());
+            let focus_handle = self
+                .pane
+                .read(cx)
+                .active_item()
+                .map(|item| item.focus_handle(cx))
+                .unwrap_or(self.focus_handle(cx));
+            self.assistant_tab_bar_button = Some(
+                cx.new_view(move |_| InlineAssistTabBarButton { focus_handle })
+                    .into(),
+            );
         } else {
             self.assistant_tab_bar_button = None;
         }
@@ -859,16 +868,21 @@ impl Panel for TerminalPanel {
     }
 }
 
-struct InlineAssistTabBarButton;
+struct InlineAssistTabBarButton {
+    focus_handle: FocusHandle,
+}
 
 impl Render for InlineAssistTabBarButton {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+        let focus_handle = self.focus_handle.clone();
         IconButton::new("terminal_inline_assistant", IconName::ZedAssistant)
             .icon_size(IconSize::Small)
             .on_click(cx.listener(|_, _, cx| {
                 cx.dispatch_action(InlineAssist::default().boxed_clone());
             }))
-            .tooltip(move |cx| Tooltip::for_action("Inline Assist", &InlineAssist::default(), cx))
+            .tooltip(move |cx| {
+                Tooltip::for_action_in("Inline Assist", &InlineAssist::default(), &focus_handle, cx)
+            })
     }
 }