Improve Find/Replace shortcuts (#10297)

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
This commit is contained in:
Andrew Lygin 2024-04-09 08:07:59 +03:00 committed by GitHub
parent cc367d43d6
commit 935e0d547e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 103 additions and 28 deletions

View file

@ -28,7 +28,7 @@
"ctrl-0": "zed::ResetBufferFontSize", "ctrl-0": "zed::ResetBufferFontSize",
"ctrl-,": "zed::OpenSettings", "ctrl-,": "zed::OpenSettings",
"ctrl-q": "zed::Quit", "ctrl-q": "zed::Quit",
"ctrl-h": "zed::Hide", "alt-f9": "zed::Hide",
"f11": "zed::ToggleFullScreen" "f11": "zed::ToggleFullScreen"
} }
}, },
@ -38,7 +38,6 @@
"escape": "editor::Cancel", "escape": "editor::Cancel",
"backspace": "editor::Backspace", "backspace": "editor::Backspace",
"shift-backspace": "editor::Backspace", "shift-backspace": "editor::Backspace",
"ctrl-h": "editor::Backspace",
"delete": "editor::Delete", "delete": "editor::Delete",
"ctrl-d": "editor::Delete", "ctrl-d": "editor::Delete",
"tab": "editor::Tab", "tab": "editor::Tab",
@ -150,10 +149,11 @@
"ctrl-shift-enter": "editor::NewlineBelow", "ctrl-shift-enter": "editor::NewlineBelow",
"ctrl-enter": "editor::NewlineAbove", "ctrl-enter": "editor::NewlineAbove",
"alt-z": "editor::ToggleSoftWrap", "alt-z": "editor::ToggleSoftWrap",
"ctrl-f": [ "ctrl-f": "buffer_search::Deploy",
"ctrl-h": [
"buffer_search::Deploy", "buffer_search::Deploy",
{ {
"focus": true "replace_enabled": true
} }
], ],
// "cmd-e": [ // "cmd-e": [
@ -212,7 +212,9 @@
"enter": "search::SelectNextMatch", "enter": "search::SelectNextMatch",
"shift-enter": "search::SelectPrevMatch", "shift-enter": "search::SelectPrevMatch",
"alt-enter": "search::SelectAllMatches", "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": { "bindings": {
"escape": "project_search::ToggleFocus", "escape": "project_search::ToggleFocus",
"alt-tab": "search::CycleMode", "alt-tab": "search::CycleMode",
"ctrl-shift-f": "search::FocusSearch",
"ctrl-shift-h": "search::ToggleReplace", "ctrl-shift-h": "search::ToggleReplace",
"alt-ctrl-g": "search::ActivateRegexMode", "alt-ctrl-g": "search::ActivateRegexMode",
"alt-ctrl-x": "search::ActivateTextMode" "alt-ctrl-x": "search::ActivateTextMode"
@ -419,6 +422,12 @@
"ctrl-j": "workspace::ToggleBottomDock", "ctrl-j": "workspace::ToggleBottomDock",
"ctrl-alt-y": "workspace::CloseAllDocks", "ctrl-alt-y": "workspace::CloseAllDocks",
"ctrl-shift-f": "pane::DeploySearch", "ctrl-shift-f": "pane::DeploySearch",
"ctrl-shift-h": [
"pane::DeploySearch",
{
"replace_enabled": true
}
],
"ctrl-k ctrl-s": "zed::OpenKeymap", "ctrl-k ctrl-s": "zed::OpenKeymap",
"ctrl-k ctrl-t": "theme_selector::Toggle", "ctrl-k ctrl-t": "theme_selector::Toggle",
"ctrl-shift-t": "project_symbols::Toggle", "ctrl-shift-t": "project_symbols::Toggle",

View file

@ -170,10 +170,11 @@
"cmd-shift-enter": "editor::NewlineAbove", "cmd-shift-enter": "editor::NewlineAbove",
"cmd-enter": "editor::NewlineBelow", "cmd-enter": "editor::NewlineBelow",
"alt-z": "editor::ToggleSoftWrap", "alt-z": "editor::ToggleSoftWrap",
"cmd-f": [ "cmd-f": "buffer_search::Deploy",
"cmd-alt-f": [
"buffer_search::Deploy", "buffer_search::Deploy",
{ {
"focus": true "replace_enabled": true
} }
], ],
"cmd-e": [ "cmd-e": [
@ -232,7 +233,9 @@
"enter": "search::SelectNextMatch", "enter": "search::SelectNextMatch",
"shift-enter": "search::SelectPrevMatch", "shift-enter": "search::SelectPrevMatch",
"alt-enter": "search::SelectAllMatches", "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": { "bindings": {
"escape": "project_search::ToggleFocus", "escape": "project_search::ToggleFocus",
"alt-tab": "search::CycleMode", "alt-tab": "search::CycleMode",
"cmd-shift-f": "search::FocusSearch",
"cmd-shift-h": "search::ToggleReplace", "cmd-shift-h": "search::ToggleReplace",
"alt-cmd-g": "search::ActivateRegexMode", "alt-cmd-g": "search::ActivateRegexMode",
"alt-cmd-x": "search::ActivateTextMode" "alt-cmd-x": "search::ActivateTextMode"
@ -436,6 +440,12 @@
"cmd-j": "workspace::ToggleBottomDock", "cmd-j": "workspace::ToggleBottomDock",
"alt-cmd-y": "workspace::CloseAllDocks", "alt-cmd-y": "workspace::CloseAllDocks",
"cmd-shift-f": "pane::DeploySearch", "cmd-shift-f": "pane::DeploySearch",
"cmd-shift-h": [
"pane::DeploySearch",
{
"replace_enabled": true
}
],
"cmd-k cmd-s": "zed::OpenKeymap", "cmd-k cmd-s": "zed::OpenKeymap",
"cmd-k cmd-t": "theme_selector::Toggle", "cmd-k cmd-t": "theme_selector::Toggle",
"cmd-t": "project_symbols::Toggle", "cmd-t": "project_symbols::Toggle",

View file

@ -91,13 +91,13 @@ impl Render for QuickActionBar {
"toggle buffer search", "toggle buffer search",
IconName::MagnifyingGlass, IconName::MagnifyingGlass,
!self.buffer_search_bar.read(cx).is_dismissed(), !self.buffer_search_bar.read(cx).is_dismissed(),
Box::new(buffer_search::Deploy { focus: false }), Box::new(buffer_search::Deploy::find()),
"Buffer Search", "Buffer Search",
{ {
let buffer_search_bar = self.buffer_search_bar.clone(); let buffer_search_bar = self.buffer_search_bar.clone();
move |_, cx| { move |_, cx| {
buffer_search_bar.update(cx, |search_bar, 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)
}); });
} }
}, },

View file

@ -3,9 +3,9 @@ mod registrar;
use crate::{ use crate::{
mode::{next_mode, SearchMode}, mode::{next_mode, SearchMode},
search_bar::render_nav_button, search_bar::render_nav_button,
ActivateRegexMode, ActivateTextMode, CycleMode, NextHistoryQuery, PreviousHistoryQuery, ActivateRegexMode, ActivateTextMode, CycleMode, FocusSearch, NextHistoryQuery,
ReplaceAll, ReplaceNext, SearchOptions, SelectAllMatches, SelectNextMatch, SelectPrevMatch, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions, SelectAllMatches,
ToggleCaseSensitive, ToggleReplace, ToggleWholeWord, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleReplace, ToggleWholeWord,
}; };
use any_vec::AnyVec; use any_vec::AnyVec;
use collections::HashMap; use collections::HashMap;
@ -44,15 +44,31 @@ const MIN_INPUT_WIDTH_REMS: f32 = 15.;
const MAX_INPUT_WIDTH_REMS: f32 = 30.; const MAX_INPUT_WIDTH_REMS: f32 = 30.;
const MAX_BUFFER_SEARCH_HISTORY_SIZE: usize = 50; const MAX_BUFFER_SEARCH_HISTORY_SIZE: usize = 50;
const fn true_value() -> bool {
true
}
#[derive(PartialEq, Clone, Deserialize)] #[derive(PartialEq, Clone, Deserialize)]
pub struct Deploy { pub struct Deploy {
#[serde(default = "true_value")]
pub focus: bool, pub focus: bool,
#[serde(default)]
pub replace_enabled: bool,
} }
impl_actions!(buffer_search, [Deploy]); impl_actions!(buffer_search, [Deploy]);
actions!(buffer_search, [Dismiss, FocusEditor]); actions!(buffer_search, [Dismiss, FocusEditor]);
impl Deploy {
pub fn find() -> Self {
Self {
focus: true,
replace_enabled: false,
}
}
}
pub enum Event { pub enum Event {
UpdateLocation, UpdateLocation,
} }
@ -470,6 +486,9 @@ impl ToolbarItemView for BufferSearchBar {
impl BufferSearchBar { impl BufferSearchBar {
pub fn register(registrar: &mut impl SearchActionsRegistrar) { 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| { registrar.register_handler(ForDeployed(|this, action: &ToggleCaseSensitive, cx| {
if this.supported_options().case { if this.supported_options().case {
this.toggle_case_sensitive(action, cx); this.toggle_case_sensitive(action, cx);
@ -583,9 +602,17 @@ impl BufferSearchBar {
pub fn deploy(&mut self, deploy: &Deploy, cx: &mut ViewContext<Self>) -> bool { pub fn deploy(&mut self, deploy: &Deploy, cx: &mut ViewContext<Self>) -> bool {
if self.show(cx) { if self.show(cx) {
self.search_suggested(cx); self.search_suggested(cx);
self.replace_enabled = deploy.replace_enabled;
if deploy.focus { if deploy.focus {
self.select_query(cx); let mut handle = self.query_editor.focus_handle(cx).clone();
let handle = self.query_editor.focus_handle(cx); 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); cx.focus(&handle);
} }
return true; return true;

View file

@ -1,7 +1,8 @@
use crate::{ use crate::{
mode::SearchMode, ActivateRegexMode, ActivateTextMode, CycleMode, NextHistoryQuery, mode::SearchMode, ActivateRegexMode, ActivateTextMode, CycleMode, FocusSearch,
PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions, SelectNextMatch, SelectPrevMatch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions,
ToggleCaseSensitive, ToggleIncludeIgnored, ToggleReplace, ToggleWholeWord, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleIncludeIgnored, ToggleReplace,
ToggleWholeWord,
}; };
use anyhow::Context as _; use anyhow::Context as _;
use collections::{HashMap, HashSet}; use collections::{HashMap, HashSet};
@ -60,6 +61,9 @@ const SEARCH_CONTEXT: u32 = 2;
pub fn init(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {
cx.set_global(ActiveSettings::default()); cx.set_global(ActiveSettings::default());
cx.observe_new_views(|workspace: &mut Workspace, _cx| { 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| { register_workspace_action(workspace, move |search_bar, _: &ToggleFilters, cx| {
search_bar.toggle_filters(cx); search_bar.toggle_filters(cx);
}); });
@ -797,7 +801,7 @@ impl ProjectSearchView {
// If no search exists in the workspace, create a new one. // If no search exists in the workspace, create a new one.
fn deploy_search( fn deploy_search(
workspace: &mut Workspace, workspace: &mut Workspace,
_: &workspace::DeploySearch, action: &workspace::DeploySearch,
cx: &mut ViewContext<Workspace>, cx: &mut ViewContext<Workspace>,
) { ) {
let existing = workspace let existing = workspace
@ -806,7 +810,7 @@ impl ProjectSearchView {
.items() .items()
.find_map(|item| item.downcast::<ProjectSearchView>()); .find_map(|item| item.downcast::<ProjectSearchView>());
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<Workspace>) { fn search_in_new(workspace: &mut Workspace, _: &SearchInNew, cx: &mut ViewContext<Workspace>) {
@ -846,12 +850,13 @@ impl ProjectSearchView {
_: &workspace::NewSearch, _: &workspace::NewSearch,
cx: &mut ViewContext<Workspace>, cx: &mut ViewContext<Workspace>,
) { ) {
Self::existing_or_new_search(workspace, None, cx) Self::existing_or_new_search(workspace, None, &DeploySearch::find(), cx)
} }
fn existing_or_new_search( fn existing_or_new_search(
workspace: &mut Workspace, workspace: &mut Workspace,
existing: Option<View<ProjectSearchView>>, existing: Option<View<ProjectSearchView>>,
action: &workspace::DeploySearch,
cx: &mut ViewContext<Workspace>, cx: &mut ViewContext<Workspace>,
) { ) {
let query = workspace.active_item(cx).and_then(|item| { let query = workspace.active_item(cx).and_then(|item| {
@ -887,6 +892,7 @@ impl ProjectSearchView {
}; };
search.update(cx, |search, cx| { search.update(cx, |search, cx| {
search.replace_enabled = action.replace_enabled;
if let Some(query) = query { if let Some(query) = query {
search.set_query(&query, cx); search.set_query(&query, cx);
} }
@ -1172,6 +1178,14 @@ impl ProjectSearchBar {
self.cycle_field(Direction::Prev, cx); self.cycle_field(Direction::Prev, cx);
} }
fn focus_search(&mut self, cx: &mut ViewContext<Self>) {
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<Self>) { fn cycle_field(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
let active_project_search = match &self.active_project_search { let active_project_search = match &self.active_project_search {
Some(active_project_search) => 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)) .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(); .unwrap();
@ -2160,7 +2174,7 @@ pub mod tests {
workspace workspace
.update(cx, |workspace, cx| { .update(cx, |workspace, cx| {
ProjectSearchView::deploy_search(workspace, &workspace::DeploySearch, cx) ProjectSearchView::deploy_search(workspace, &workspace::DeploySearch::find(), cx)
}) })
.unwrap(); .unwrap();
window.update(cx, |_, cx| { window.update(cx, |_, cx| {
@ -3259,7 +3273,7 @@ pub mod tests {
.unwrap(); .unwrap();
// Deploy a new search // 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 // Both panes should now have a project search in them
window window
@ -3284,7 +3298,7 @@ pub mod tests {
.unwrap(); .unwrap();
// Deploy a new search // 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 // The project search view should now be focused in the second pane
// And the number of items should be unchanged. // And the number of items should be unchanged.

View file

@ -22,6 +22,7 @@ actions!(
search, search,
[ [
CycleMode, CycleMode,
FocusSearch,
ToggleWholeWord, ToggleWholeWord,
ToggleCaseSensitive, ToggleCaseSensitive,
ToggleIncludeIgnored, ToggleIncludeIgnored,

View file

@ -86,6 +86,12 @@ pub struct RevealInProjectPanel {
pub entry_id: Option<u64>, pub entry_id: Option<u64>,
} }
#[derive(PartialEq, Clone, Deserialize)]
pub struct DeploySearch {
#[serde(default)]
pub replace_enabled: bool,
}
impl_actions!( impl_actions!(
pane, pane,
[ [
@ -93,7 +99,8 @@ impl_actions!(
CloseActiveItem, CloseActiveItem,
CloseInactiveItems, CloseInactiveItems,
ActivateItem, ActivateItem,
RevealInProjectPanel RevealInProjectPanel,
DeploySearch,
] ]
); );
@ -107,7 +114,6 @@ actions!(
CloseItemsToTheLeft, CloseItemsToTheLeft,
CloseItemsToTheRight, CloseItemsToTheRight,
GoBack, GoBack,
DeploySearch,
GoForward, GoForward,
ReopenClosedItem, ReopenClosedItem,
SplitLeft, SplitLeft,
@ -117,6 +123,14 @@ actions!(
] ]
); );
impl DeploySearch {
pub fn find() -> Self {
Self {
replace_enabled: false,
}
}
}
const MAX_NAVIGATION_HISTORY_LEN: usize = 1024; const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
pub enum Event { pub enum Event {

View file

@ -67,7 +67,7 @@ pub fn app_menus() -> Vec<Menu<'static>> {
MenuItem::os_action("Copy", editor::actions::Copy, OsAction::Copy), MenuItem::os_action("Copy", editor::actions::Copy, OsAction::Copy),
MenuItem::os_action("Paste", editor::actions::Paste, OsAction::Paste), MenuItem::os_action("Paste", editor::actions::Paste, OsAction::Paste),
MenuItem::separator(), 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::action("Find In Project", workspace::NewSearch),
MenuItem::separator(), MenuItem::separator(),
MenuItem::action( MenuItem::action(