From 935e0d547e115ff43fffa65eb77586ed030d9411 Mon Sep 17 00:00:00 2001 From: Andrew Lygin Date: Tue, 9 Apr 2024 08:07:59 +0300 Subject: [PATCH] Improve Find/Replace shortcuts (#10297) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR changes ways the Find/Replace functionality in the Buffer/Project Search is accessible via shortcuts. It makes those panels work the same way as in VS Code and Sublime Text. The details are described in the issue: [Make Find/Replace easier to use](https://github.com/zed-industries/zed/issues/9142) There's a difficulty with the Linux keybindings: VS Code uses on MacOS (this PR replicates it): | Action | Buffer Search | Project Search | | --- | --- | --- | | Find | `cmd-f` | `cmd-shift-f` | | Replace | `cmd-alt-f` | `cmd-shift-h` | VS Code uses on Linux (this PR replicates all but one): | Action | Buffer Search | Project Search | | --- | --- | --- | | Find | `ctrl-f` | `ctrl-shift-f` | | Replace | `ctrl-h` ❗ | `ctrl-shift-h` | The problem is that `ctrl-h` is already taken by the `editor::Backspace` action in Zed on Linux. There's two options here: 1. Change keybinding for `editor::Backspace` on Linux to something else, and use `ctrl-h` for the "replace in buffer" action. 2. Use some other keybinding on Linux in Zed. This PR introduces `ctrl-r` for this purpose, though I'm not sure it's the best choice. What do you think? fixes #9142 Release Notes: - Improved access to "Find/Replace in Buffer" and "Find/Replace in Files" via shortcuts (#9142). Optionally, include screenshots / media showcasing your addition that can be included in the release notes. - N/A --- assets/keymaps/default-linux.json | 19 +++++++--- assets/keymaps/default-macos.json | 16 ++++++-- .../quick_action_bar/src/quick_action_bar.rs | 4 +- crates/search/src/buffer_search.rs | 37 ++++++++++++++++--- crates/search/src/project_search.rs | 34 ++++++++++++----- crates/search/src/search.rs | 1 + crates/workspace/src/pane.rs | 18 ++++++++- crates/zed/src/zed/app_menus.rs | 2 +- 8 files changed, 103 insertions(+), 28 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index b1a00641d9..32a489afa8 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -28,7 +28,7 @@ "ctrl-0": "zed::ResetBufferFontSize", "ctrl-,": "zed::OpenSettings", "ctrl-q": "zed::Quit", - "ctrl-h": "zed::Hide", + "alt-f9": "zed::Hide", "f11": "zed::ToggleFullScreen" } }, @@ -38,7 +38,6 @@ "escape": "editor::Cancel", "backspace": "editor::Backspace", "shift-backspace": "editor::Backspace", - "ctrl-h": "editor::Backspace", "delete": "editor::Delete", "ctrl-d": "editor::Delete", "tab": "editor::Tab", @@ -150,10 +149,11 @@ "ctrl-shift-enter": "editor::NewlineBelow", "ctrl-enter": "editor::NewlineAbove", "alt-z": "editor::ToggleSoftWrap", - "ctrl-f": [ + "ctrl-f": "buffer_search::Deploy", + "ctrl-h": [ "buffer_search::Deploy", { - "focus": true + "replace_enabled": true } ], // "cmd-e": [ @@ -212,7 +212,9 @@ "enter": "search::SelectNextMatch", "shift-enter": "search::SelectPrevMatch", "alt-enter": "search::SelectAllMatches", - "alt-tab": "search::CycleMode" + "alt-tab": "search::CycleMode", + "ctrl-f": "search::FocusSearch", + "ctrl-h": "search::ToggleReplace" } }, { @@ -234,6 +236,7 @@ "bindings": { "escape": "project_search::ToggleFocus", "alt-tab": "search::CycleMode", + "ctrl-shift-f": "search::FocusSearch", "ctrl-shift-h": "search::ToggleReplace", "alt-ctrl-g": "search::ActivateRegexMode", "alt-ctrl-x": "search::ActivateTextMode" @@ -419,6 +422,12 @@ "ctrl-j": "workspace::ToggleBottomDock", "ctrl-alt-y": "workspace::CloseAllDocks", "ctrl-shift-f": "pane::DeploySearch", + "ctrl-shift-h": [ + "pane::DeploySearch", + { + "replace_enabled": true + } + ], "ctrl-k ctrl-s": "zed::OpenKeymap", "ctrl-k ctrl-t": "theme_selector::Toggle", "ctrl-shift-t": "project_symbols::Toggle", diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 0d252bc986..0c91b53ed2 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -170,10 +170,11 @@ "cmd-shift-enter": "editor::NewlineAbove", "cmd-enter": "editor::NewlineBelow", "alt-z": "editor::ToggleSoftWrap", - "cmd-f": [ + "cmd-f": "buffer_search::Deploy", + "cmd-alt-f": [ "buffer_search::Deploy", { - "focus": true + "replace_enabled": true } ], "cmd-e": [ @@ -232,7 +233,9 @@ "enter": "search::SelectNextMatch", "shift-enter": "search::SelectPrevMatch", "alt-enter": "search::SelectAllMatches", - "alt-tab": "search::CycleMode" + "alt-tab": "search::CycleMode", + "cmd-f": "search::FocusSearch", + "cmd-alt-f": "search::ToggleReplace" } }, { @@ -254,6 +257,7 @@ "bindings": { "escape": "project_search::ToggleFocus", "alt-tab": "search::CycleMode", + "cmd-shift-f": "search::FocusSearch", "cmd-shift-h": "search::ToggleReplace", "alt-cmd-g": "search::ActivateRegexMode", "alt-cmd-x": "search::ActivateTextMode" @@ -436,6 +440,12 @@ "cmd-j": "workspace::ToggleBottomDock", "alt-cmd-y": "workspace::CloseAllDocks", "cmd-shift-f": "pane::DeploySearch", + "cmd-shift-h": [ + "pane::DeploySearch", + { + "replace_enabled": true + } + ], "cmd-k cmd-s": "zed::OpenKeymap", "cmd-k cmd-t": "theme_selector::Toggle", "cmd-t": "project_symbols::Toggle", diff --git a/crates/quick_action_bar/src/quick_action_bar.rs b/crates/quick_action_bar/src/quick_action_bar.rs index 75d35fd4f8..a6fe46ec66 100644 --- a/crates/quick_action_bar/src/quick_action_bar.rs +++ b/crates/quick_action_bar/src/quick_action_bar.rs @@ -91,13 +91,13 @@ impl Render for QuickActionBar { "toggle buffer search", IconName::MagnifyingGlass, !self.buffer_search_bar.read(cx).is_dismissed(), - Box::new(buffer_search::Deploy { focus: false }), + Box::new(buffer_search::Deploy::find()), "Buffer Search", { let buffer_search_bar = self.buffer_search_bar.clone(); move |_, cx| { buffer_search_bar.update(cx, |search_bar, cx| { - search_bar.toggle(&buffer_search::Deploy { focus: true }, cx) + search_bar.toggle(&buffer_search::Deploy::find(), cx) }); } }, diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index d6602469e0..0d3d199ce1 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -3,9 +3,9 @@ mod registrar; use crate::{ mode::{next_mode, SearchMode}, search_bar::render_nav_button, - ActivateRegexMode, ActivateTextMode, CycleMode, NextHistoryQuery, PreviousHistoryQuery, - ReplaceAll, ReplaceNext, SearchOptions, SelectAllMatches, SelectNextMatch, SelectPrevMatch, - ToggleCaseSensitive, ToggleReplace, ToggleWholeWord, + ActivateRegexMode, ActivateTextMode, CycleMode, FocusSearch, NextHistoryQuery, + PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions, SelectAllMatches, + SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleReplace, ToggleWholeWord, }; use any_vec::AnyVec; use collections::HashMap; @@ -44,15 +44,31 @@ const MIN_INPUT_WIDTH_REMS: f32 = 15.; const MAX_INPUT_WIDTH_REMS: f32 = 30.; const MAX_BUFFER_SEARCH_HISTORY_SIZE: usize = 50; +const fn true_value() -> bool { + true +} + #[derive(PartialEq, Clone, Deserialize)] pub struct Deploy { + #[serde(default = "true_value")] pub focus: bool, + #[serde(default)] + pub replace_enabled: bool, } impl_actions!(buffer_search, [Deploy]); actions!(buffer_search, [Dismiss, FocusEditor]); +impl Deploy { + pub fn find() -> Self { + Self { + focus: true, + replace_enabled: false, + } + } +} + pub enum Event { UpdateLocation, } @@ -470,6 +486,9 @@ impl ToolbarItemView for BufferSearchBar { impl BufferSearchBar { pub fn register(registrar: &mut impl SearchActionsRegistrar) { + registrar.register_handler(ForDeployed(|this, _: &FocusSearch, cx| { + this.query_editor.focus_handle(cx).focus(cx); + })); registrar.register_handler(ForDeployed(|this, action: &ToggleCaseSensitive, cx| { if this.supported_options().case { this.toggle_case_sensitive(action, cx); @@ -583,9 +602,17 @@ impl BufferSearchBar { pub fn deploy(&mut self, deploy: &Deploy, cx: &mut ViewContext) -> bool { if self.show(cx) { self.search_suggested(cx); + self.replace_enabled = deploy.replace_enabled; if deploy.focus { - self.select_query(cx); - let handle = self.query_editor.focus_handle(cx); + let mut handle = self.query_editor.focus_handle(cx).clone(); + let mut select_query = true; + if deploy.replace_enabled && handle.is_focused(cx) { + handle = self.replacement_editor.focus_handle(cx).clone(); + select_query = false; + }; + if select_query { + self.select_query(cx); + } cx.focus(&handle); } return true; diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 030c56b7c7..c6a9a83ede 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1,7 +1,8 @@ use crate::{ - mode::SearchMode, ActivateRegexMode, ActivateTextMode, CycleMode, NextHistoryQuery, - PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions, SelectNextMatch, SelectPrevMatch, - ToggleCaseSensitive, ToggleIncludeIgnored, ToggleReplace, ToggleWholeWord, + mode::SearchMode, ActivateRegexMode, ActivateTextMode, CycleMode, FocusSearch, + NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions, + SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleIncludeIgnored, ToggleReplace, + ToggleWholeWord, }; use anyhow::Context as _; use collections::{HashMap, HashSet}; @@ -60,6 +61,9 @@ const SEARCH_CONTEXT: u32 = 2; pub fn init(cx: &mut AppContext) { cx.set_global(ActiveSettings::default()); cx.observe_new_views(|workspace: &mut Workspace, _cx| { + register_workspace_action(workspace, move |search_bar, _: &FocusSearch, cx| { + search_bar.focus_search(cx); + }); register_workspace_action(workspace, move |search_bar, _: &ToggleFilters, cx| { search_bar.toggle_filters(cx); }); @@ -797,7 +801,7 @@ impl ProjectSearchView { // If no search exists in the workspace, create a new one. fn deploy_search( workspace: &mut Workspace, - _: &workspace::DeploySearch, + action: &workspace::DeploySearch, cx: &mut ViewContext, ) { let existing = workspace @@ -806,7 +810,7 @@ impl ProjectSearchView { .items() .find_map(|item| item.downcast::()); - Self::existing_or_new_search(workspace, existing, cx) + Self::existing_or_new_search(workspace, existing, action, cx); } fn search_in_new(workspace: &mut Workspace, _: &SearchInNew, cx: &mut ViewContext) { @@ -846,12 +850,13 @@ impl ProjectSearchView { _: &workspace::NewSearch, cx: &mut ViewContext, ) { - Self::existing_or_new_search(workspace, None, cx) + Self::existing_or_new_search(workspace, None, &DeploySearch::find(), cx) } fn existing_or_new_search( workspace: &mut Workspace, existing: Option>, + action: &workspace::DeploySearch, cx: &mut ViewContext, ) { let query = workspace.active_item(cx).and_then(|item| { @@ -887,6 +892,7 @@ impl ProjectSearchView { }; search.update(cx, |search, cx| { + search.replace_enabled = action.replace_enabled; if let Some(query) = query { search.set_query(&query, cx); } @@ -1172,6 +1178,14 @@ impl ProjectSearchBar { self.cycle_field(Direction::Prev, cx); } + fn focus_search(&mut self, cx: &mut ViewContext) { + if let Some(search_view) = self.active_project_search.as_ref() { + search_view.update(cx, |search_view, cx| { + search_view.query_editor.focus_handle(cx).focus(cx); + }); + } + } + fn cycle_field(&mut self, direction: Direction, cx: &mut ViewContext) { let active_project_search = match &self.active_project_search { Some(active_project_search) => active_project_search, @@ -2011,7 +2025,7 @@ pub mod tests { .update(cx, |toolbar, cx| toolbar.add_item(search_bar, cx)) }); - ProjectSearchView::deploy_search(workspace, &workspace::DeploySearch, cx) + ProjectSearchView::deploy_search(workspace, &workspace::DeploySearch::find(), cx) }) .unwrap(); @@ -2160,7 +2174,7 @@ pub mod tests { workspace .update(cx, |workspace, cx| { - ProjectSearchView::deploy_search(workspace, &workspace::DeploySearch, cx) + ProjectSearchView::deploy_search(workspace, &workspace::DeploySearch::find(), cx) }) .unwrap(); window.update(cx, |_, cx| { @@ -3259,7 +3273,7 @@ pub mod tests { .unwrap(); // Deploy a new search - cx.dispatch_action(window.into(), DeploySearch); + cx.dispatch_action(window.into(), DeploySearch::find()); // Both panes should now have a project search in them window @@ -3284,7 +3298,7 @@ pub mod tests { .unwrap(); // Deploy a new search - cx.dispatch_action(window.into(), DeploySearch); + cx.dispatch_action(window.into(), DeploySearch::find()); // The project search view should now be focused in the second pane // And the number of items should be unchanged. diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index a071f0fd70..3b710226d4 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -22,6 +22,7 @@ actions!( search, [ CycleMode, + FocusSearch, ToggleWholeWord, ToggleCaseSensitive, ToggleIncludeIgnored, diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index e657d1cecd..57320256e6 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -86,6 +86,12 @@ pub struct RevealInProjectPanel { pub entry_id: Option, } +#[derive(PartialEq, Clone, Deserialize)] +pub struct DeploySearch { + #[serde(default)] + pub replace_enabled: bool, +} + impl_actions!( pane, [ @@ -93,7 +99,8 @@ impl_actions!( CloseActiveItem, CloseInactiveItems, ActivateItem, - RevealInProjectPanel + RevealInProjectPanel, + DeploySearch, ] ); @@ -107,7 +114,6 @@ actions!( CloseItemsToTheLeft, CloseItemsToTheRight, GoBack, - DeploySearch, GoForward, ReopenClosedItem, SplitLeft, @@ -117,6 +123,14 @@ actions!( ] ); +impl DeploySearch { + pub fn find() -> Self { + Self { + replace_enabled: false, + } + } +} + const MAX_NAVIGATION_HISTORY_LEN: usize = 1024; pub enum Event { diff --git a/crates/zed/src/zed/app_menus.rs b/crates/zed/src/zed/app_menus.rs index 62a59f337f..7d9f344612 100644 --- a/crates/zed/src/zed/app_menus.rs +++ b/crates/zed/src/zed/app_menus.rs @@ -67,7 +67,7 @@ pub fn app_menus() -> Vec> { MenuItem::os_action("Copy", editor::actions::Copy, OsAction::Copy), MenuItem::os_action("Paste", editor::actions::Paste, OsAction::Paste), MenuItem::separator(), - MenuItem::action("Find", search::buffer_search::Deploy { focus: true }), + MenuItem::action("Find", search::buffer_search::Deploy::find()), MenuItem::action("Find In Project", workspace::NewSearch), MenuItem::separator(), MenuItem::action(