Rework tab and escape key handling in search panel

Kirill Bulatov and Julia created

Fixes
https://linear.app/zed-industries/issue/Z-1238/focus-the-results-multibuffer-in-project-search-on-esc

and adds a tab shortcut for project search inputs

co-authored-by: Julia <julia@zed.dev>

Change summary

assets/keymaps/default.json         | 12 ++++++
crates/search/src/project_search.rs | 56 ++++++++++++++++++++++++------
2 files changed, 56 insertions(+), 12 deletions(-)

Detailed changes

assets/keymaps/default.json 🔗

@@ -199,6 +199,18 @@
       "shift-enter": "search::SelectPrevMatch"
     }
   },
+  {
+    "context": "ProjectSearchBar > Editor",
+    "bindings": {
+      "escape": "project_search::ToggleFocus"
+    }
+  },
+  {
+    "context": "ProjectSearchView > Editor",
+    "bindings": {
+      "escape": "project_search::ToggleFocus"
+    }
+  },
   {
     "context": "Pane",
     "bindings": {

crates/search/src/project_search.rs 🔗

@@ -34,7 +34,7 @@ use workspace::{
     ItemNavHistory, Pane, ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceId,
 };
 
-actions!(project_search, [SearchInNew, ToggleFocus]);
+actions!(project_search, [SearchInNew, ToggleFocus, NextField]);
 
 #[derive(Default)]
 struct ActiveSearches(HashMap<WeakModelHandle<Project>, WeakViewHandle<ProjectSearchView>>);
@@ -48,6 +48,7 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(ProjectSearchBar::select_prev_match);
     cx.add_action(ProjectSearchBar::toggle_focus);
     cx.capture_action(ProjectSearchBar::tab);
+    cx.capture_action(ProjectSearchBar::tab_previous);
     add_toggle_option_action::<ToggleCaseSensitive>(SearchOption::CaseSensitive, cx);
     add_toggle_option_action::<ToggleWholeWord>(SearchOption::WholeWord, cx);
     add_toggle_option_action::<ToggleRegex>(SearchOption::Regex, cx);
@@ -789,19 +790,50 @@ impl ProjectSearchBar {
     }
 
     fn tab(&mut self, _: &editor::Tab, cx: &mut ViewContext<Self>) {
-        if let Some(search_view) = self.active_project_search.as_ref() {
-            search_view.update(cx, |search_view, cx| {
-                if search_view.query_editor.is_focused(cx) {
-                    if !search_view.model.read(cx).match_ranges.is_empty() {
-                        search_view.focus_results_editor(cx);
-                    }
-                } else {
+        self.cycle_field(Direction::Next, cx);
+    }
+
+    fn tab_previous(&mut self, _: &editor::TabPrev, cx: &mut ViewContext<Self>) {
+        self.cycle_field(Direction::Prev, cx);
+    }
+
+    fn cycle_field(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
+        let active_project_search = match &self.active_project_search {
+            Some(active_project_search) => active_project_search,
+
+            None => {
+                cx.propagate_action();
+                return;
+            }
+        };
+
+        active_project_search.update(cx, |project_view, cx| {
+            let views = &[
+                &project_view.query_editor,
+                &project_view.included_files_editor,
+                &project_view.excluded_files_editor,
+            ];
+
+            let current_index = match views
+                .iter()
+                .enumerate()
+                .find(|(_, view)| view.is_focused(cx))
+            {
+                Some((index, _)) => index,
+
+                None => {
                     cx.propagate_action();
+                    return;
                 }
-            });
-        } else {
-            cx.propagate_action();
-        }
+            };
+
+            let new_index = match direction {
+                Direction::Next => (current_index + 1) % views.len(),
+                Direction::Prev if current_index == 0 => views.len() - 1,
+                Direction::Prev => (current_index - 1) % views.len(),
+            };
+            cx.focus(views[new_index]);
+        });
     }
 
     fn toggle_search_option(&mut self, option: SearchOption, cx: &mut ViewContext<Self>) -> bool {