Add `center_on_match` option for search (#40523)

Bob Mannino and Smit Barmase created

[Closes discussion
#28943](https://github.com/zed-industries/zed/discussions/28943)

Release Notes:

- Added `center_on_match` option to center matched text in view during buffer or project search.

---------

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>

Change summary

assets/settings/default.json                   |  4 ++
crates/editor/src/editor_settings.rs           |  2 +
crates/editor/src/items.rs                     |  7 +++++
crates/search/src/buffer_search.rs             |  4 +++
crates/search/src/project_search.rs            | 11 +++++++-
crates/settings/src/settings_content/editor.rs |  2 +
crates/settings_ui/src/page_data.rs            | 23 ++++++++++++++++++++
docs/src/configuring-zed.md                    |  6 +++++
8 files changed, 55 insertions(+), 4 deletions(-)

Detailed changes

assets/settings/default.json 🔗

@@ -602,7 +602,9 @@
     "whole_word": false,
     "case_sensitive": false,
     "include_ignored": false,
-    "regex": false
+    "regex": false,
+    // Whether to center the cursor on each search match when navigating.
+    "center_on_match": false
   },
   // When to populate a new search's query based on the text under the cursor.
   // This setting can take the following three values:

crates/editor/src/editor_settings.rs 🔗

@@ -159,6 +159,7 @@ pub struct SearchSettings {
     pub case_sensitive: bool,
     pub include_ignored: bool,
     pub regex: bool,
+    pub center_on_match: bool,
 }
 
 impl EditorSettings {
@@ -249,6 +250,7 @@ impl Settings for EditorSettings {
                 case_sensitive: search.case_sensitive.unwrap(),
                 include_ignored: search.include_ignored.unwrap(),
                 regex: search.regex.unwrap(),
+                center_on_match: search.center_on_match.unwrap(),
             },
             auto_signature_help: editor.auto_signature_help.unwrap(),
             show_signature_help_after_edits: editor.show_signature_help_after_edits.unwrap(),

crates/editor/src/items.rs 🔗

@@ -1593,7 +1593,12 @@ impl SearchableItem for Editor {
     ) {
         self.unfold_ranges(&[matches[index].clone()], false, true, cx);
         let range = self.range_for_match(&matches[index], collapse);
-        self.change_selections(Default::default(), window, cx, |s| {
+        let autoscroll = if EditorSettings::get_global(cx).search.center_on_match {
+            Autoscroll::center()
+        } else {
+            Autoscroll::fit()
+        };
+        self.change_selections(SelectionEffects::scroll(autoscroll), window, cx, |s| {
             s.select_ranges([range]);
         })
     }

crates/search/src/buffer_search.rs 🔗

@@ -2813,6 +2813,7 @@ mod tests {
                 case_sensitive: false,
                 include_ignored: false,
                 regex: false,
+                center_on_match: false,
             },
             cx,
         );
@@ -2875,6 +2876,7 @@ mod tests {
                 case_sensitive: true,
                 include_ignored: false,
                 regex: false,
+                center_on_match: false,
             },
             cx,
         );
@@ -2912,6 +2914,7 @@ mod tests {
                 case_sensitive: true,
                 include_ignored: false,
                 regex: false,
+                center_on_match: false,
             },
             cx,
         );
@@ -2938,6 +2941,7 @@ mod tests {
                         case_sensitive: Some(search_settings.case_sensitive),
                         include_ignored: Some(search_settings.include_ignored),
                         regex: Some(search_settings.regex),
+                        center_on_match: Some(search_settings.center_on_match),
                     });
                 });
             });

crates/search/src/project_search.rs 🔗

@@ -12,7 +12,9 @@ use editor::{
     SelectionEffects, VimFlavor,
     actions::{Backtab, SelectAll, Tab},
     items::active_match_index,
-    multibuffer_context_lines, vim_flavor,
+    multibuffer_context_lines,
+    scroll::Autoscroll,
+    vim_flavor,
 };
 use futures::{StreamExt, stream::FuturesOrdered};
 use gpui::{
@@ -1346,8 +1348,13 @@ impl ProjectSearchView {
             self.results_editor.update(cx, |editor, cx| {
                 let collapse = vim_flavor(cx) == Some(VimFlavor::Vim);
                 let range_to_select = editor.range_for_match(&range_to_select, collapse);
+                let autoscroll = if EditorSettings::get_global(cx).search.center_on_match {
+                    Autoscroll::center()
+                } else {
+                    Autoscroll::fit()
+                };
                 editor.unfold_ranges(std::slice::from_ref(&range_to_select), false, true, cx);
-                editor.change_selections(Default::default(), window, cx, |s| {
+                editor.change_selections(SelectionEffects::scroll(autoscroll), window, cx, |s| {
                     s.select_ranges([range_to_select])
                 });
             });

crates/settings/src/settings_content/editor.rs 🔗

@@ -699,6 +699,8 @@ pub struct SearchSettingsContent {
     pub case_sensitive: Option<bool>,
     pub include_ignored: Option<bool>,
     pub regex: Option<bool>,
+    /// Whether to center the cursor on each search match when navigating.
+    pub center_on_match: Option<bool>,
 }
 
 #[skip_serializing_none]

crates/settings_ui/src/page_data.rs 🔗

@@ -2450,6 +2450,29 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     metadata: None,
                     files: USER,
                 }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Center on Match",
+                    description: "Whether to center the current match in the editor",
+                    field: Box::new(SettingField {
+                        json_path: Some("editor.search.center_on_match"),
+                        pick: |settings_content| {
+                            settings_content
+                                .editor
+                                .search
+                                .as_ref()
+                                .and_then(|search| search.center_on_match.as_ref())
+                        },
+                        write: |settings_content, value| {
+                            settings_content
+                                .editor
+                                .search
+                                .get_or_insert_default()
+                                .center_on_match = value;
+                        },
+                    }),
+                    metadata: None,
+                    files: USER,
+                }),
                 SettingsPageItem::SettingItem(SettingItem {
                     title: "Seed Search Query From Cursor",
                     description: "When to populate a new search's query based on the text under the cursor.",

docs/src/configuring-zed.md 🔗

@@ -3163,6 +3163,12 @@ Non-negative `integer` values
 - Setting: `search_wrap`
 - Default: `true`
 
+## Center on Match
+
+- Description: If `center_on_match` is enabled, the editor will center the cursor on the current match when searching.
+- Setting: `center_on_match`
+- Default: `false`
+
 ## Seed Search Query From Cursor
 
 - Description: When to populate a new search's query based on the text under the cursor.