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
Unified · Split
| @@ -28,7 +28,7 @@ · 27 unmodified lines | ||
| 28 | 28 | "ctrl-0": "zed::ResetBufferFontSize", |
| 29 | 29 | "ctrl-,": "zed::OpenSettings", |
| 30 | 30 | "ctrl-q": "zed::Quit", |
| 31 | − "ctrl-h": "zed::Hide", | |
| 31 | + "alt-f9": "zed::Hide", | |
| 32 | 32 | "f11": "zed::ToggleFullScreen" |
| 33 | 33 | } |
| 34 | 34 | }, |
| @@ -38,7 +38,6 @@ · 3 unmodified lines | ||
| 38 | 38 | "escape": "editor::Cancel", |
| 39 | 39 | "backspace": "editor::Backspace", |
| 40 | 40 | "shift-backspace": "editor::Backspace", |
| 41 | − "ctrl-h": "editor::Backspace", | |
| 42 | 41 | "delete": "editor::Delete", |
| 43 | 42 | "ctrl-d": "editor::Delete", |
| 44 | 43 | "tab": "editor::Tab", |
| @@ -150,10 +149,11 @@ · 105 unmodified lines | ||
| 150 | 149 | "ctrl-shift-enter": "editor::NewlineBelow", |
| 151 | 150 | "ctrl-enter": "editor::NewlineAbove", |
| 152 | 151 | "alt-z": "editor::ToggleSoftWrap", |
| 153 | − "ctrl-f": [ | |
| 152 | + "ctrl-f": "buffer_search::Deploy", | |
| 153 | + "ctrl-h": [ | |
| 154 | 154 | "buffer_search::Deploy", |
| 155 | 155 | { |
| 156 | − "focus": true | |
| 156 | + "replace_enabled": true | |
| 157 | 157 | } |
| 158 | 158 | ], |
| 159 | 159 | // "cmd-e": [ |
| @@ -212,7 +212,9 @@ · 52 unmodified lines | ||
| 212 | 212 | "enter": "search::SelectNextMatch", |
| 213 | 213 | "shift-enter": "search::SelectPrevMatch", |
| 214 | 214 | "alt-enter": "search::SelectAllMatches", |
| 215 | − "alt-tab": "search::CycleMode" | |
| 215 | + "alt-tab": "search::CycleMode", | |
| 216 | + "ctrl-f": "search::FocusSearch", | |
| 217 | + "ctrl-h": "search::ToggleReplace" | |
| 216 | 218 | } |
| 217 | 219 | }, |
| 218 | 220 | { |
| @@ -234,6 +236,7 @@ · 15 unmodified lines | ||
| 234 | 236 | "bindings": { |
| 235 | 237 | "escape": "project_search::ToggleFocus", |
| 236 | 238 | "alt-tab": "search::CycleMode", |
| 239 | + "ctrl-shift-f": "search::FocusSearch", | |
| 237 | 240 | "ctrl-shift-h": "search::ToggleReplace", |
| 238 | 241 | "alt-ctrl-g": "search::ActivateRegexMode", |
| 239 | 242 | "alt-ctrl-x": "search::ActivateTextMode" |
| @@ -419,6 +422,12 @@ · 179 unmodified lines | ||
| 419 | 422 | "ctrl-j": "workspace::ToggleBottomDock", |
| 420 | 423 | "ctrl-alt-y": "workspace::CloseAllDocks", |
| 421 | 424 | "ctrl-shift-f": "pane::DeploySearch", |
| 425 | + "ctrl-shift-h": [ | |
| 426 | + "pane::DeploySearch", | |
| 427 | + { | |
| 428 | + "replace_enabled": true | |
| 429 | + } | |
| 430 | + ], | |
| 422 | 431 | "ctrl-k ctrl-s": "zed::OpenKeymap", |
| 423 | 432 | "ctrl-k ctrl-t": "theme_selector::Toggle", |
| 424 | 433 | "ctrl-shift-t": "project_symbols::Toggle", |
| @@ -170,10 +170,11 @@ · 169 unmodified lines | ||
| 170 | 170 | "cmd-shift-enter": "editor::NewlineAbove", |
| 171 | 171 | "cmd-enter": "editor::NewlineBelow", |
| 172 | 172 | "alt-z": "editor::ToggleSoftWrap", |
| 173 | − "cmd-f": [ | |
| 173 | + "cmd-f": "buffer_search::Deploy", | |
| 174 | + "cmd-alt-f": [ | |
| 174 | 175 | "buffer_search::Deploy", |
| 175 | 176 | { |
| 176 | − "focus": true | |
| 177 | + "replace_enabled": true | |
| 177 | 178 | } |
| 178 | 179 | ], |
| 179 | 180 | "cmd-e": [ |
| @@ -232,7 +233,9 @@ · 52 unmodified lines | ||
| 232 | 233 | "enter": "search::SelectNextMatch", |
| 233 | 234 | "shift-enter": "search::SelectPrevMatch", |
| 234 | 235 | "alt-enter": "search::SelectAllMatches", |
| 235 | − "alt-tab": "search::CycleMode" | |
| 236 | + "alt-tab": "search::CycleMode", | |
| 237 | + "cmd-f": "search::FocusSearch", | |
| 238 | + "cmd-alt-f": "search::ToggleReplace" | |
| 236 | 239 | } |
| 237 | 240 | }, |
| 238 | 241 | { |
| @@ -254,6 +257,7 @@ · 15 unmodified lines | ||
| 254 | 257 | "bindings": { |
| 255 | 258 | "escape": "project_search::ToggleFocus", |
| 256 | 259 | "alt-tab": "search::CycleMode", |
| 260 | + "cmd-shift-f": "search::FocusSearch", | |
| 257 | 261 | "cmd-shift-h": "search::ToggleReplace", |
| 258 | 262 | "alt-cmd-g": "search::ActivateRegexMode", |
| 259 | 263 | "alt-cmd-x": "search::ActivateTextMode" |
| @@ -436,6 +440,12 @@ · 176 unmodified lines | ||
| 436 | 440 | "cmd-j": "workspace::ToggleBottomDock", |
| 437 | 441 | "alt-cmd-y": "workspace::CloseAllDocks", |
| 438 | 442 | "cmd-shift-f": "pane::DeploySearch", |
| 443 | + "cmd-shift-h": [ | |
| 444 | + "pane::DeploySearch", | |
| 445 | + { | |
| 446 | + "replace_enabled": true | |
| 447 | + } | |
| 448 | + ], | |
| 439 | 449 | "cmd-k cmd-s": "zed::OpenKeymap", |
| 440 | 450 | "cmd-k cmd-t": "theme_selector::Toggle", |
| 441 | 451 | "cmd-t": "project_symbols::Toggle", |
| @@ -91,13 +91,13 @@ impl Render for QuickActionBar { · 90 unmodified lines | ||
| 91 | 91 | "toggle buffer search", |
| 92 | 92 | IconName::MagnifyingGlass, |
| 93 | 93 | !self.buffer_search_bar.read(cx).is_dismissed(), |
| 94 | − Box::new(buffer_search::Deploy { focus: false }), | |
| 94 | + Box::new(buffer_search::Deploy::find()), | |
| 95 | 95 | "Buffer Search", |
| 96 | 96 | { |
| 97 | 97 | let buffer_search_bar = self.buffer_search_bar.clone(); |
| 98 | 98 | move |_, cx| { |
| 99 | 99 | buffer_search_bar.update(cx, |search_bar, cx| { |
| 100 | − search_bar.toggle(&buffer_search::Deploy { focus: true }, cx) | |
| 100 | + search_bar.toggle(&buffer_search::Deploy::find(), cx) | |
| 101 | 101 | }); |
| 102 | 102 | } |
| 103 | 103 | }, |
| @@ -3,9 +3,9 @@ mod registrar; · 2 unmodified lines | ||
| 3 | 3 | use crate::{ |
| 4 | 4 | mode::{next_mode, SearchMode}, |
| 5 | 5 | search_bar::render_nav_button, |
| 6 | − ActivateRegexMode, ActivateTextMode, CycleMode, NextHistoryQuery, PreviousHistoryQuery, | |
| 7 | − ReplaceAll, ReplaceNext, SearchOptions, SelectAllMatches, SelectNextMatch, SelectPrevMatch, | |
| 8 | − ToggleCaseSensitive, ToggleReplace, ToggleWholeWord, | |
| 6 | + ActivateRegexMode, ActivateTextMode, CycleMode, FocusSearch, NextHistoryQuery, | |
| 7 | + PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions, SelectAllMatches, | |
| 8 | + SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleReplace, ToggleWholeWord, | |
| 9 | 9 | }; |
| 10 | 10 | use any_vec::AnyVec; |
| 11 | 11 | use collections::HashMap; |
| @@ -44,15 +44,31 @@ const MIN_INPUT_WIDTH_REMS: f32 = 15.; · 32 unmodified lines | ||
| 44 | 44 | const MAX_INPUT_WIDTH_REMS: f32 = 30.; |
| 45 | 45 | const MAX_BUFFER_SEARCH_HISTORY_SIZE: usize = 50; |
| 46 | 46 | |
| 47 | +const fn true_value() -> bool { | |
| 48 | + true | |
| 49 | +} | |
| 50 | + | |
| 47 | 51 | #[derive(PartialEq, Clone, Deserialize)] |
| 48 | 52 | pub struct Deploy { |
| 53 | + #[serde(default = "true_value")] | |
| 49 | 54 | pub focus: bool, |
| 55 | + #[serde(default)] | |
| 56 | + pub replace_enabled: bool, | |
| 50 | 57 | } |
| 51 | 58 | |
| 52 | 59 | impl_actions!(buffer_search, [Deploy]); |
| 53 | 60 | |
| 54 | 61 | actions!(buffer_search, [Dismiss, FocusEditor]); |
| 55 | 62 | |
| 63 | +impl Deploy { | |
| 64 | + pub fn find() -> Self { | |
| 65 | + Self { | |
| 66 | + focus: true, | |
| 67 | + replace_enabled: false, | |
| 68 | + } | |
| 69 | + } | |
| 70 | +} | |
| 71 | + | |
| 56 | 72 | pub enum Event { |
| 57 | 73 | UpdateLocation, |
| 58 | 74 | } |
| @@ -470,6 +486,9 @@ impl ToolbarItemView for BufferSearchBar { · 411 unmodified lines | ||
| 470 | 486 | |
| 471 | 487 | impl BufferSearchBar { |
| 472 | 488 | pub fn register(registrar: &mut impl SearchActionsRegistrar) { |
| 489 | + registrar.register_handler(ForDeployed(|this, _: &FocusSearch, cx| { | |
| 490 | + this.query_editor.focus_handle(cx).focus(cx); | |
| 491 | + })); | |
| 473 | 492 | registrar.register_handler(ForDeployed(|this, action: &ToggleCaseSensitive, cx| { |
| 474 | 493 | if this.supported_options().case { |
| 475 | 494 | this.toggle_case_sensitive(action, cx); |
| @@ -583,9 +602,17 @@ impl BufferSearchBar { · 107 unmodified lines | ||
| 583 | 602 | pub fn deploy(&mut self, deploy: &Deploy, cx: &mut ViewContext<Self>) -> bool { |
| 584 | 603 | if self.show(cx) { |
| 585 | 604 | self.search_suggested(cx); |
| 605 | + self.replace_enabled = deploy.replace_enabled; | |
| 586 | 606 | if deploy.focus { |
| 587 | − self.select_query(cx); | |
| 588 | − let handle = self.query_editor.focus_handle(cx); | |
| 607 | + let mut handle = self.query_editor.focus_handle(cx).clone(); | |
| 608 | + let mut select_query = true; | |
| 609 | + if deploy.replace_enabled && handle.is_focused(cx) { | |
| 610 | + handle = self.replacement_editor.focus_handle(cx).clone(); | |
| 611 | + select_query = false; | |
| 612 | + }; | |
| 613 | + if select_query { | |
| 614 | + self.select_query(cx); | |
| 615 | + } | |
| 589 | 616 | cx.focus(&handle); |
| 590 | 617 | } |
| 591 | 618 | return true; |
| @@ -1,7 +1,8 @@ | ||
| 1 | 1 | use crate::{ |
| 2 | − mode::SearchMode, ActivateRegexMode, ActivateTextMode, CycleMode, NextHistoryQuery, | |
| 3 | − PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions, SelectNextMatch, SelectPrevMatch, | |
| 4 | − ToggleCaseSensitive, ToggleIncludeIgnored, ToggleReplace, ToggleWholeWord, | |
| 2 | + mode::SearchMode, ActivateRegexMode, ActivateTextMode, CycleMode, FocusSearch, | |
| 3 | + NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions, | |
| 4 | + SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleIncludeIgnored, ToggleReplace, | |
| 5 | + ToggleWholeWord, | |
| 5 | 6 | }; |
| 6 | 7 | use anyhow::Context as _; |
| 7 | 8 | use collections::{HashMap, HashSet}; |
| @@ -60,6 +61,9 @@ const SEARCH_CONTEXT: u32 = 2; · 52 unmodified lines | ||
| 60 | 61 | pub fn init(cx: &mut AppContext) { |
| 61 | 62 | cx.set_global(ActiveSettings::default()); |
| 62 | 63 | cx.observe_new_views(|workspace: &mut Workspace, _cx| { |
| 64 | + register_workspace_action(workspace, move |search_bar, _: &FocusSearch, cx| { | |
| 65 | + search_bar.focus_search(cx); | |
| 66 | + }); | |
| 63 | 67 | register_workspace_action(workspace, move |search_bar, _: &ToggleFilters, cx| { |
| 64 | 68 | search_bar.toggle_filters(cx); |
| 65 | 69 | }); |
| @@ -797,7 +801,7 @@ impl ProjectSearchView { · 731 unmodified lines | ||
| 797 | 801 | // If no search exists in the workspace, create a new one. |
| 798 | 802 | fn deploy_search( |
| 799 | 803 | workspace: &mut Workspace, |
| 800 | − _: &workspace::DeploySearch, | |
| 804 | + action: &workspace::DeploySearch, | |
| 801 | 805 | cx: &mut ViewContext<Workspace>, |
| 802 | 806 | ) { |
| 803 | 807 | let existing = workspace |
| @@ -806,7 +810,7 @@ impl ProjectSearchView { · 2 unmodified lines | ||
| 806 | 810 | .items() |
| 807 | 811 | .find_map(|item| item.downcast::<ProjectSearchView>()); |
| 808 | 812 | |
| 809 | − Self::existing_or_new_search(workspace, existing, cx) | |
| 813 | + Self::existing_or_new_search(workspace, existing, action, cx); | |
| 810 | 814 | } |
| 811 | 815 | |
| 812 | 816 | fn search_in_new(workspace: &mut Workspace, _: &SearchInNew, cx: &mut ViewContext<Workspace>) { |
| @@ -846,12 +850,13 @@ impl ProjectSearchView { · 33 unmodified lines | ||
| 846 | 850 | _: &workspace::NewSearch, |
| 847 | 851 | cx: &mut ViewContext<Workspace>, |
| 848 | 852 | ) { |
| 849 | − Self::existing_or_new_search(workspace, None, cx) | |
| 853 | + Self::existing_or_new_search(workspace, None, &DeploySearch::find(), cx) | |
| 850 | 854 | } |
| 851 | 855 | |
| 852 | 856 | fn existing_or_new_search( |
| 853 | 857 | workspace: &mut Workspace, |
| 854 | 858 | existing: Option<View<ProjectSearchView>>, |
| 859 | + action: &workspace::DeploySearch, | |
| 855 | 860 | cx: &mut ViewContext<Workspace>, |
| 856 | 861 | ) { |
| 857 | 862 | let query = workspace.active_item(cx).and_then(|item| { |
| @@ -887,6 +892,7 @@ impl ProjectSearchView { · 29 unmodified lines | ||
| 887 | 892 | }; |
| 888 | 893 | |
| 889 | 894 | search.update(cx, |search, cx| { |
| 895 | + search.replace_enabled = action.replace_enabled; | |
| 890 | 896 | if let Some(query) = query { |
| 891 | 897 | search.set_query(&query, cx); |
| 892 | 898 | } |
| @@ -1172,6 +1178,14 @@ impl ProjectSearchBar { · 279 unmodified lines | ||
| 1172 | 1178 | self.cycle_field(Direction::Prev, cx); |
| 1173 | 1179 | } |
| 1174 | 1180 | |
| 1181 | + fn focus_search(&mut self, cx: &mut ViewContext<Self>) { | |
| 1182 | + if let Some(search_view) = self.active_project_search.as_ref() { | |
| 1183 | + search_view.update(cx, |search_view, cx| { | |
| 1184 | + search_view.query_editor.focus_handle(cx).focus(cx); | |
| 1185 | + }); | |
| 1186 | + } | |
| 1187 | + } | |
| 1188 | + | |
| 1175 | 1189 | fn cycle_field(&mut self, direction: Direction, cx: &mut ViewContext<Self>) { |
| 1176 | 1190 | let active_project_search = match &self.active_project_search { |
| 1177 | 1191 | Some(active_project_search) => active_project_search, |
| @@ -2011,7 +2025,7 @@ pub mod tests { · 833 unmodified lines | ||
| 2011 | 2025 | .update(cx, |toolbar, cx| toolbar.add_item(search_bar, cx)) |
| 2012 | 2026 | }); |
| 2013 | 2027 | |
| 2014 | − ProjectSearchView::deploy_search(workspace, &workspace::DeploySearch, cx) | |
| 2028 | + ProjectSearchView::deploy_search(workspace, &workspace::DeploySearch::find(), cx) | |
| 2015 | 2029 | }) |
| 2016 | 2030 | .unwrap(); |
| 2017 | 2031 | |
| @@ -2160,7 +2174,7 @@ pub mod tests { · 142 unmodified lines | ||
| 2160 | 2174 | |
| 2161 | 2175 | workspace |
| 2162 | 2176 | .update(cx, |workspace, cx| { |
| 2163 | − ProjectSearchView::deploy_search(workspace, &workspace::DeploySearch, cx) | |
| 2177 | + ProjectSearchView::deploy_search(workspace, &workspace::DeploySearch::find(), cx) | |
| 2164 | 2178 | }) |
| 2165 | 2179 | .unwrap(); |
| 2166 | 2180 | window.update(cx, |_, cx| { |
| @@ -3259,7 +3273,7 @@ pub mod tests { · 1092 unmodified lines | ||
| 3259 | 3273 | .unwrap(); |
| 3260 | 3274 | |
| 3261 | 3275 | // Deploy a new search |
| 3262 | − cx.dispatch_action(window.into(), DeploySearch); | |
| 3276 | + cx.dispatch_action(window.into(), DeploySearch::find()); | |
| 3263 | 3277 | |
| 3264 | 3278 | // Both panes should now have a project search in them |
| 3265 | 3279 | window |
| @@ -3284,7 +3298,7 @@ pub mod tests { · 18 unmodified lines | ||
| 3284 | 3298 | .unwrap(); |
| 3285 | 3299 | |
| 3286 | 3300 | // Deploy a new search |
| 3287 | − cx.dispatch_action(window.into(), DeploySearch); | |
| 3301 | + cx.dispatch_action(window.into(), DeploySearch::find()); | |
| 3288 | 3302 | |
| 3289 | 3303 | // The project search view should now be focused in the second pane |
| 3290 | 3304 | // And the number of items should be unchanged. |
| @@ -22,6 +22,7 @@ actions!( · 21 unmodified lines | ||
| 22 | 22 | search, |
| 23 | 23 | [ |
| 24 | 24 | CycleMode, |
| 25 | + FocusSearch, | |
| 25 | 26 | ToggleWholeWord, |
| 26 | 27 | ToggleCaseSensitive, |
| 27 | 28 | ToggleIncludeIgnored, |
| @@ -86,6 +86,12 @@ pub struct RevealInProjectPanel { · 85 unmodified lines | ||
| 86 | 86 | pub entry_id: Option<u64>, |
| 87 | 87 | } |
| 88 | 88 | |
| 89 | +#[derive(PartialEq, Clone, Deserialize)] | |
| 90 | +pub struct DeploySearch { | |
| 91 | + #[serde(default)] | |
| 92 | + pub replace_enabled: bool, | |
| 93 | +} | |
| 94 | + | |
| 89 | 95 | impl_actions!( |
| 90 | 96 | pane, |
| 91 | 97 | [ |
| @@ -93,7 +99,8 @@ impl_actions!( · 1 unmodified line | ||
| 93 | 99 | CloseActiveItem, |
| 94 | 100 | CloseInactiveItems, |
| 95 | 101 | ActivateItem, |
| 96 | − RevealInProjectPanel | |
| 102 | + RevealInProjectPanel, | |
| 103 | + DeploySearch, | |
| 97 | 104 | ] |
| 98 | 105 | ); |
| 99 | 106 | |
| @@ -107,7 +114,6 @@ actions!( · 7 unmodified lines | ||
| 107 | 114 | CloseItemsToTheLeft, |
| 108 | 115 | CloseItemsToTheRight, |
| 109 | 116 | GoBack, |
| 110 | − DeploySearch, | |
| 111 | 117 | GoForward, |
| 112 | 118 | ReopenClosedItem, |
| 113 | 119 | SplitLeft, |
| @@ -117,6 +123,14 @@ actions!( · 3 unmodified lines | ||
| 117 | 123 | ] |
| 118 | 124 | ); |
| 119 | 125 | |
| 126 | +impl DeploySearch { | |
| 127 | + pub fn find() -> Self { | |
| 128 | + Self { | |
| 129 | + replace_enabled: false, | |
| 130 | + } | |
| 131 | + } | |
| 132 | +} | |
| 133 | + | |
| 120 | 134 | const MAX_NAVIGATION_HISTORY_LEN: usize = 1024; |
| 121 | 135 | |
| 122 | 136 | pub enum Event { |
| @@ -67,7 +67,7 @@ pub fn app_menus() -> Vec<Menu<'static>> { · 66 unmodified lines | ||
| 67 | 67 | MenuItem::os_action("Copy", editor::actions::Copy, OsAction::Copy), |
| 68 | 68 | MenuItem::os_action("Paste", editor::actions::Paste, OsAction::Paste), |
| 69 | 69 | MenuItem::separator(), |
| 70 | − MenuItem::action("Find", search::buffer_search::Deploy { focus: true }), | |
| 70 | + MenuItem::action("Find", search::buffer_search::Deploy::find()), | |
| 71 | 71 | MenuItem::action("Find In Project", workspace::NewSearch), |
| 72 | 72 | MenuItem::separator(), |
| 73 | 73 | MenuItem::action( |