Merge pull request #1198 from zed-industries/keyboard-toggle-search-options

Antonio Scandurra created

Toggle search options via the keyboard

Change summary

assets/keymaps/default.json         | 21 +++--
crates/search/src/buffer_search.rs  | 98 +++++++++++++++++++++---------
crates/search/src/project_search.rs | 74 ++++++++++++++++++----
crates/search/src/search.rs         | 35 +++++++++-
4 files changed, 169 insertions(+), 59 deletions(-)

Detailed changes

assets/keymaps/default.json 🔗

@@ -15,7 +15,7 @@
             "shift-cmd-}": "pane::ActivateNextItem",
             "cmd-w": "pane::CloseActiveItem",
             "cmd-shift-W": "workspace::CloseWindow",
-            "alt-cmd-w": "pane::CloseInactiveItems",
+            "alt-cmd-t": "pane::CloseInactiveItems",
             "cmd-s": "workspace::Save",
             "cmd-shift-S": "workspace::SaveAs",
             "cmd-=": "zed::IncreaseBufferFontSize",
@@ -141,14 +141,6 @@
             ]
         }
     },
-    {
-        "context": "Pane",
-        "bindings": {
-            "cmd-f": "project_search::ToggleFocus",
-            "cmd-g": "search::SelectNextMatch",
-            "cmd-shift-G": "search::SelectPrevMatch"
-        }
-    },
     {
         "context": "BufferSearchBar",
         "bindings": {
@@ -158,6 +150,17 @@
             "shift-enter": "search::SelectPrevMatch"
         }
     },
+    {
+        "context": "Pane",
+        "bindings": {
+            "cmd-f": "project_search::ToggleFocus",
+            "cmd-g": "search::SelectNextMatch",
+            "cmd-shift-G": "search::SelectPrevMatch",
+            "alt-cmd-c": "search::ToggleCaseSensitive",
+            "alt-cmd-w": "search::ToggleWholeWord",
+            "alt-cmd-r": "search::ToggleRegex"
+        }
+    },
     // Bindings from VS Code
     {
         "context": "Editor",

crates/search/src/buffer_search.rs 🔗

@@ -1,12 +1,13 @@
 use crate::{
     active_match_index, match_index_for_direction, query_suggestion_for_editor, Direction,
-    SearchOption, SelectNextMatch, SelectPrevMatch,
+    SearchOption, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleRegex,
+    ToggleWholeWord,
 };
 use collections::HashMap;
 use editor::{Anchor, Autoscroll, Editor};
 use gpui::{
-    actions, elements::*, impl_actions, impl_internal_actions, platform::CursorStyle, AppContext,
-    Entity, MutableAppContext, RenderContext, Subscription, Task, View, ViewContext, ViewHandle,
+    actions, elements::*, impl_actions, platform::CursorStyle, Action, AppContext, Entity,
+    MutableAppContext, RenderContext, Subscription, Task, View, ViewContext, ViewHandle,
     WeakViewHandle,
 };
 use language::OffsetRangeExt;
@@ -21,12 +22,8 @@ pub struct Deploy {
     pub focus: bool,
 }
 
-#[derive(Clone, PartialEq)]
-pub struct ToggleSearchOption(pub SearchOption);
-
 actions!(buffer_search, [Dismiss, FocusEditor]);
 impl_actions!(buffer_search, [Deploy]);
-impl_internal_actions!(buffer_search, [ToggleSearchOption]);
 
 pub enum Event {
     UpdateLocation,
@@ -36,12 +33,28 @@ pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(BufferSearchBar::deploy);
     cx.add_action(BufferSearchBar::dismiss);
     cx.add_action(BufferSearchBar::focus_editor);
-    cx.add_action(BufferSearchBar::toggle_search_option);
     cx.add_action(BufferSearchBar::select_next_match);
     cx.add_action(BufferSearchBar::select_prev_match);
     cx.add_action(BufferSearchBar::select_next_match_on_pane);
     cx.add_action(BufferSearchBar::select_prev_match_on_pane);
     cx.add_action(BufferSearchBar::handle_editor_cancel);
+    add_toggle_option_action::<ToggleCaseSensitive>(SearchOption::CaseSensitive, cx);
+    add_toggle_option_action::<ToggleWholeWord>(SearchOption::WholeWord, cx);
+    add_toggle_option_action::<ToggleRegex>(SearchOption::Regex, cx);
+}
+
+fn add_toggle_option_action<A: Action>(option: SearchOption, cx: &mut MutableAppContext) {
+    cx.add_action(move |pane: &mut Pane, _: &A, cx: &mut ViewContext<Pane>| {
+        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
+            if search_bar.update(cx, |search_bar, cx| search_bar.show(false, false, cx)) {
+                search_bar.update(cx, |search_bar, cx| {
+                    search_bar.toggle_search_option(option, cx);
+                });
+                return;
+            }
+        }
+        cx.propagate_action();
+    });
 }
 
 pub struct BufferSearchBar {
@@ -215,16 +228,18 @@ impl BufferSearchBar {
         cx.notify();
     }
 
-    fn show(&mut self, focus: bool, cx: &mut ViewContext<Self>) -> bool {
+    fn show(&mut self, focus: bool, suggest_query: bool, cx: &mut ViewContext<Self>) -> bool {
         let editor = if let Some(editor) = self.active_editor.clone() {
             editor
         } else {
             return false;
         };
 
-        let text = query_suggestion_for_editor(&editor, cx);
-        if !text.is_empty() {
-            self.set_query(&text, cx);
+        if suggest_query {
+            let text = query_suggestion_for_editor(&editor, cx);
+            if !text.is_empty() {
+                self.set_query(&text, cx);
+            }
         }
 
         if focus {
@@ -253,11 +268,12 @@ impl BufferSearchBar {
     fn render_search_option(
         &self,
         icon: &str,
-        search_option: SearchOption,
+        option: SearchOption,
         cx: &mut RenderContext<Self>,
     ) -> ElementBox {
-        let is_active = self.is_search_option_enabled(search_option);
-        MouseEventHandler::new::<Self, _, _>(search_option as usize, cx, |state, cx| {
+        let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
+        let is_active = self.is_search_option_enabled(option);
+        MouseEventHandler::new::<Self, _, _>(option as usize, cx, |state, cx| {
             let style = &cx
                 .global::<Settings>()
                 .theme
@@ -269,8 +285,15 @@ impl BufferSearchBar {
                 .with_style(style.container)
                 .boxed()
         })
-        .on_click(move |_, _, cx| cx.dispatch_action(ToggleSearchOption(search_option)))
+        .on_click(move |_, _, cx| cx.dispatch_any_action(option.to_toggle_action()))
         .with_cursor_style(CursorStyle::PointingHand)
+        .with_tooltip::<Self, _>(
+            option as usize,
+            format!("Toggle {}", option.label()),
+            Some(option.to_toggle_action()),
+            tooltip_style,
+            cx,
+        )
         .boxed()
     }
 
@@ -280,6 +303,20 @@ impl BufferSearchBar {
         direction: Direction,
         cx: &mut RenderContext<Self>,
     ) -> ElementBox {
+        let action: Box<dyn Action>;
+        let tooltip;
+        match direction {
+            Direction::Prev => {
+                action = Box::new(SelectPrevMatch);
+                tooltip = "Select Previous Match";
+            }
+            Direction::Next => {
+                action = Box::new(SelectNextMatch);
+                tooltip = "Select Next Match";
+            }
+        };
+        let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
+
         enum NavButton {}
         MouseEventHandler::new::<NavButton, _, _>(direction as usize, cx, |state, cx| {
             let style = &cx
@@ -293,17 +330,24 @@ impl BufferSearchBar {
                 .with_style(style.container)
                 .boxed()
         })
-        .on_click(move |_, _, cx| match direction {
-            Direction::Prev => cx.dispatch_action(SelectPrevMatch),
-            Direction::Next => cx.dispatch_action(SelectNextMatch),
+        .on_click({
+            let action = action.boxed_clone();
+            move |_, _, cx| cx.dispatch_any_action(action.boxed_clone())
         })
         .with_cursor_style(CursorStyle::PointingHand)
+        .with_tooltip::<NavButton, _>(
+            direction as usize,
+            tooltip.to_string(),
+            Some(action),
+            tooltip_style,
+            cx,
+        )
         .boxed()
     }
 
     fn deploy(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext<Pane>) {
         if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
-            if search_bar.update(cx, |search_bar, cx| search_bar.show(action.focus, cx)) {
+            if search_bar.update(cx, |search_bar, cx| search_bar.show(action.focus, true, cx)) {
                 return;
             }
         }
@@ -334,18 +378,14 @@ impl BufferSearchBar {
         }
     }
 
-    fn toggle_search_option(
-        &mut self,
-        ToggleSearchOption(search_option): &ToggleSearchOption,
-        cx: &mut ViewContext<Self>,
-    ) {
+    fn toggle_search_option(&mut self, search_option: SearchOption, cx: &mut ViewContext<Self>) {
         let value = match search_option {
             SearchOption::WholeWord => &mut self.whole_word,
             SearchOption::CaseSensitive => &mut self.case_sensitive,
             SearchOption::Regex => &mut self.regex,
         };
         *value = !*value;
-        self.update_matches(true, cx);
+        self.update_matches(false, cx);
         cx.notify();
     }
 
@@ -591,7 +631,7 @@ mod tests {
         let search_bar = cx.add_view(Default::default(), |cx| {
             let mut search_bar = BufferSearchBar::new(cx);
             search_bar.set_active_pane_item(Some(&editor), cx);
-            search_bar.show(false, cx);
+            search_bar.show(false, true, cx);
             search_bar
         });
 
@@ -619,7 +659,7 @@ mod tests {
 
         // Switch to a case sensitive search.
         search_bar.update(cx, |search_bar, cx| {
-            search_bar.toggle_search_option(&ToggleSearchOption(SearchOption::CaseSensitive), cx);
+            search_bar.toggle_search_option(SearchOption::CaseSensitive, cx);
         });
         editor.next_notification(&cx).await;
         editor.update(cx, |editor, cx| {
@@ -676,7 +716,7 @@ mod tests {
 
         // Switch to a whole word search.
         search_bar.update(cx, |search_bar, cx| {
-            search_bar.toggle_search_option(&ToggleSearchOption(SearchOption::WholeWord), cx);
+            search_bar.toggle_search_option(SearchOption::WholeWord, cx);
         });
         editor.next_notification(&cx).await;
         editor.update(cx, |editor, cx| {

crates/search/src/project_search.rs 🔗

@@ -1,13 +1,14 @@
 use crate::{
     active_match_index, match_index_for_direction, query_suggestion_for_editor, Direction,
-    SearchOption, SelectNextMatch, SelectPrevMatch, ToggleSearchOption,
+    SearchOption, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleRegex,
+    ToggleWholeWord,
 };
 use collections::HashMap;
 use editor::{Anchor, Autoscroll, Editor, MultiBuffer, SelectAll};
 use gpui::{
-    actions, elements::*, platform::CursorStyle, AppContext, ElementBox, Entity, ModelContext,
-    ModelHandle, MutableAppContext, RenderContext, Subscription, Task, View, ViewContext,
-    ViewHandle, WeakModelHandle, WeakViewHandle,
+    actions, elements::*, platform::CursorStyle, Action, AppContext, ElementBox, Entity,
+    ModelContext, ModelHandle, MutableAppContext, RenderContext, Subscription, Task, View,
+    ViewContext, ViewHandle, WeakModelHandle, WeakViewHandle,
 };
 use menu::Confirm;
 use project::{search::SearchQuery, Project};
@@ -35,11 +36,26 @@ pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(ProjectSearchView::deploy);
     cx.add_action(ProjectSearchBar::search);
     cx.add_action(ProjectSearchBar::search_in_new);
-    cx.add_action(ProjectSearchBar::toggle_search_option);
     cx.add_action(ProjectSearchBar::select_next_match);
     cx.add_action(ProjectSearchBar::select_prev_match);
     cx.add_action(ProjectSearchBar::toggle_focus);
     cx.capture_action(ProjectSearchBar::tab);
+    add_toggle_option_action::<ToggleCaseSensitive>(SearchOption::CaseSensitive, cx);
+    add_toggle_option_action::<ToggleWholeWord>(SearchOption::WholeWord, cx);
+    add_toggle_option_action::<ToggleRegex>(SearchOption::Regex, cx);
+}
+
+fn add_toggle_option_action<A: Action>(option: SearchOption, cx: &mut MutableAppContext) {
+    cx.add_action(move |pane: &mut Pane, _: &A, cx: &mut ViewContext<Pane>| {
+        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<ProjectSearchBar>() {
+            if search_bar.update(cx, |search_bar, cx| {
+                search_bar.toggle_search_option(option, cx)
+            }) {
+                return;
+            }
+        }
+        cx.propagate_action();
+    });
 }
 
 struct ProjectSearch {
@@ -653,11 +669,7 @@ impl ProjectSearchBar {
         }
     }
 
-    fn toggle_search_option(
-        &mut self,
-        ToggleSearchOption(option): &ToggleSearchOption,
-        cx: &mut ViewContext<Self>,
-    ) {
+    fn toggle_search_option(&mut self, option: SearchOption, cx: &mut ViewContext<Self>) -> bool {
         if let Some(search_view) = self.active_project_search.as_ref() {
             search_view.update(cx, |search_view, cx| {
                 let value = match option {
@@ -669,6 +681,9 @@ impl ProjectSearchBar {
                 search_view.search(cx);
             });
             cx.notify();
+            true
+        } else {
+            false
         }
     }
 
@@ -678,6 +693,20 @@ impl ProjectSearchBar {
         direction: Direction,
         cx: &mut RenderContext<Self>,
     ) -> ElementBox {
+        let action: Box<dyn Action>;
+        let tooltip;
+        match direction {
+            Direction::Prev => {
+                action = Box::new(SelectPrevMatch);
+                tooltip = "Select Previous Match";
+            }
+            Direction::Next => {
+                action = Box::new(SelectNextMatch);
+                tooltip = "Select Next Match";
+            }
+        };
+        let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
+
         enum NavButton {}
         MouseEventHandler::new::<NavButton, _, _>(direction as usize, cx, |state, cx| {
             let style = &cx
@@ -691,11 +720,18 @@ impl ProjectSearchBar {
                 .with_style(style.container)
                 .boxed()
         })
-        .on_click(move |_, _, cx| match direction {
-            Direction::Prev => cx.dispatch_action(SelectPrevMatch),
-            Direction::Next => cx.dispatch_action(SelectNextMatch),
+        .on_click({
+            let action = action.boxed_clone();
+            move |_, _, cx| cx.dispatch_any_action(action.boxed_clone())
         })
         .with_cursor_style(CursorStyle::PointingHand)
+        .with_tooltip::<NavButton, _>(
+            direction as usize,
+            tooltip.to_string(),
+            Some(action),
+            tooltip_style,
+            cx,
+        )
         .boxed()
     }
 
@@ -705,8 +741,9 @@ impl ProjectSearchBar {
         option: SearchOption,
         cx: &mut RenderContext<Self>,
     ) -> ElementBox {
+        let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
         let is_active = self.is_option_enabled(option, cx);
-        MouseEventHandler::new::<ProjectSearchBar, _, _>(option as usize, cx, |state, cx| {
+        MouseEventHandler::new::<Self, _, _>(option as usize, cx, |state, cx| {
             let style = &cx
                 .global::<Settings>()
                 .theme
@@ -718,8 +755,15 @@ impl ProjectSearchBar {
                 .with_style(style.container)
                 .boxed()
         })
-        .on_click(move |_, _, cx| cx.dispatch_action(ToggleSearchOption(option)))
+        .on_click(move |_, _, cx| cx.dispatch_any_action(option.to_toggle_action()))
         .with_cursor_style(CursorStyle::PointingHand)
+        .with_tooltip::<Self, _>(
+            option as usize,
+            format!("Toggle {}", option.label()),
+            Some(option.to_toggle_action()),
+            tooltip_style,
+            cx,
+        )
         .boxed()
     }
 

crates/search/src/search.rs 🔗

@@ -1,6 +1,6 @@
 pub use buffer_search::BufferSearchBar;
 use editor::{display_map::ToDisplayPoint, Anchor, Bias, Editor, MultiBufferSnapshot};
-use gpui::{actions, impl_internal_actions, MutableAppContext, ViewHandle};
+use gpui::{actions, Action, MutableAppContext, ViewHandle};
 pub use project_search::{ProjectSearchBar, ProjectSearchView};
 use std::{
     cmp::{self, Ordering},
@@ -15,11 +15,16 @@ pub fn init(cx: &mut MutableAppContext) {
     project_search::init(cx);
 }
 
-#[derive(Clone, PartialEq)]
-pub struct ToggleSearchOption(pub SearchOption);
-
-actions!(search, [SelectNextMatch, SelectPrevMatch]);
-impl_internal_actions!(search, [ToggleSearchOption]);
+actions!(
+    search,
+    [
+        ToggleWholeWord,
+        ToggleCaseSensitive,
+        ToggleRegex,
+        SelectNextMatch,
+        SelectPrevMatch
+    ]
+);
 
 #[derive(Clone, Copy, PartialEq)]
 pub enum SearchOption {
@@ -28,6 +33,24 @@ pub enum SearchOption {
     Regex,
 }
 
+impl SearchOption {
+    pub fn label(&self) -> &'static str {
+        match self {
+            SearchOption::WholeWord => "Match Whole Word",
+            SearchOption::CaseSensitive => "Match Case",
+            SearchOption::Regex => "Use Regular Expression",
+        }
+    }
+
+    pub fn to_toggle_action(&self) -> Box<dyn Action> {
+        match self {
+            SearchOption::WholeWord => Box::new(ToggleWholeWord),
+            SearchOption::CaseSensitive => Box::new(ToggleCaseSensitive),
+            SearchOption::Regex => Box::new(ToggleRegex),
+        }
+    }
+}
+
 #[derive(Clone, Copy, PartialEq, Eq)]
 pub enum Direction {
     Prev,