Add a way to configure default search options (#17179)

thataboy and Kirill Bulatov created

Closes https://github.com/zed-industries/zed/issues/4646

```json
// Search options to enable by default when opening new project and buffer searches.
"search": {
  "whole_word": false,
  "case_sensitive": false,
  "include_ignored": false,
  "regex": false
}
```

Release Notes:

- Added `search` settings section to configure default options enabled
in buffer and project searches
([#4646](https://github.com/zed-industries/zed/issues/4646))

---------

Co-authored-by: Kirill Bulatov <mail4score@gmail.com>

Change summary

assets/settings/default.json            |  7 ++++
crates/editor/src/editor.rs             |  4 +
crates/editor/src/editor_settings.rs    | 41 +++++++++++++++++++++++++++
crates/search/src/buffer_search.rs      | 20 ++++++++++--
crates/search/src/project_search.rs     |  6 ++-
crates/search/src/search.rs             | 12 +++++++
crates/vim/src/test/vim_test_context.rs |  2 
crates/zed/src/zed.rs                   |  1 
8 files changed, 85 insertions(+), 8 deletions(-)

Detailed changes

assets/settings/default.json 🔗

@@ -279,6 +279,13 @@
   "relative_line_numbers": false,
   // If 'search_wrap' is disabled, search result do not wrap around the end of the file.
   "search_wrap": true,
+  // Search options to enable by default when opening new project and buffer searches.
+  "search": {
+    "whole_word": false,
+    "case_sensitive": false,
+    "include_ignored": false,
+    "regex": 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.rs 🔗

@@ -59,7 +59,9 @@ use convert_case::{Case, Casing};
 use debounced_delay::DebouncedDelay;
 use display_map::*;
 pub use display_map::{DisplayPoint, FoldPlaceholder};
-pub use editor_settings::{CurrentLineHighlight, EditorSettings, ScrollBeyondLastLine};
+pub use editor_settings::{
+    CurrentLineHighlight, EditorSettings, ScrollBeyondLastLine, SearchSettings,
+};
 pub use editor_settings_controls::*;
 use element::LineWithInvisibles;
 pub use element::{

crates/editor/src/editor_settings.rs 🔗

@@ -28,6 +28,8 @@ pub struct EditorSettings {
     #[serde(default)]
     pub double_click_in_multibuffer: DoubleClickInMultibuffer,
     pub search_wrap: bool,
+    #[serde(default)]
+    pub search: SearchSettings,
     pub auto_signature_help: bool,
     pub show_signature_help_after_edits: bool,
     pub jupyter: Jupyter,
@@ -156,6 +158,40 @@ pub enum ScrollBeyondLastLine {
     VerticalScrollMargin,
 }
 
+/// Default options for buffer and project search items.
+#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+pub struct SearchSettings {
+    #[serde(default)]
+    pub whole_word: bool,
+    #[serde(default)]
+    pub case_sensitive: bool,
+    #[serde(default)]
+    pub include_ignored: bool,
+    #[serde(default)]
+    pub regex: bool,
+}
+
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
+pub struct SearchSettingsContent {
+    pub whole_word: Option<bool>,
+    pub case_sensitive: Option<bool>,
+    pub include_ignored: Option<bool>,
+    pub regex: Option<bool>,
+}
+
+impl Settings for SearchSettings {
+    const KEY: Option<&'static str> = Some("search");
+
+    type FileContent = SearchSettingsContent;
+
+    fn load(
+        sources: SettingsSources<Self::FileContent>,
+        _: &mut gpui::AppContext,
+    ) -> anyhow::Result<Self> {
+        sources.json_merge()
+    }
+}
+
 #[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
 pub struct EditorSettingsContent {
     /// Whether the cursor blinks in the editor.
@@ -251,6 +287,11 @@ pub struct EditorSettingsContent {
     /// Default: true
     pub search_wrap: Option<bool>,
 
+    /// Defaults to use when opening a new buffer and project search items.
+    ///
+    /// Default: nothing is enabled
+    pub search: Option<SearchSettings>,
+
     /// Whether to automatically show a signature help pop-up or not.
     ///
     /// Default: false

crates/search/src/buffer_search.rs 🔗

@@ -9,7 +9,7 @@ use any_vec::AnyVec;
 use collections::HashMap;
 use editor::{
     actions::{Tab, TabPrev},
-    DisplayPoint, Editor, EditorElement, EditorSettings, EditorStyle,
+    DisplayPoint, Editor, EditorElement, EditorSettings, EditorStyle, SearchSettings,
 };
 use futures::channel::oneshot;
 use gpui::{
@@ -22,7 +22,7 @@ use project::{
     search_history::{SearchHistory, SearchHistoryCursor},
 };
 use serde::Deserialize;
-use settings::Settings;
+use settings::{Settings, SettingsStore};
 use std::sync::Arc;
 use theme::ThemeSettings;
 
@@ -96,6 +96,7 @@ pub struct BufferSearchBar {
     scroll_handle: ScrollHandle,
     editor_scroll_handle: ScrollHandle,
     editor_needed_width: Pixels,
+    _subscriptions: Vec<Subscription>,
 }
 
 impl BufferSearchBar {
@@ -505,6 +506,12 @@ impl BufferSearchBar {
         cx.subscribe(&replacement_editor, Self::on_replacement_editor_event)
             .detach();
 
+        let search_options = SearchOptions::from_settings(&SearchSettings::get_global(cx));
+
+        let settings_subscription = cx.observe_global::<SettingsStore>(move |this, cx| {
+            this.default_options = SearchOptions::from_settings(&SearchSettings::get_global(cx));
+        });
+
         Self {
             query_editor,
             query_editor_focused: false,
@@ -514,8 +521,8 @@ impl BufferSearchBar {
             active_searchable_item_subscription: None,
             active_match_index: None,
             searchable_items_with_matches: Default::default(),
-            default_options: SearchOptions::NONE,
-            search_options: SearchOptions::NONE,
+            default_options: search_options,
+            search_options,
             pending_search: None,
             query_contains_error: false,
             dismissed: true,
@@ -530,6 +537,7 @@ impl BufferSearchBar {
             scroll_handle: ScrollHandle::new(),
             editor_scroll_handle: ScrollHandle::new(),
             editor_needed_width: px(0.),
+            _subscriptions: vec![settings_subscription],
         }
     }
 
@@ -602,6 +610,9 @@ impl BufferSearchBar {
         let Some(handle) = self.active_searchable_item.as_ref() else {
             return false;
         };
+        if self.default_options != self.search_options {
+            self.search_options = self.default_options;
+        }
 
         self.dismissed = false;
         handle.search_bar_visibility_changed(true, cx);
@@ -1203,6 +1214,7 @@ mod tests {
             language::init(cx);
             Project::init_settings(cx);
             theme::init(theme::LoadThemes::JustBase, cx);
+            crate::init(cx);
         });
     }
 

crates/search/src/project_search.rs 🔗

@@ -668,7 +668,9 @@ impl ProjectSearchView {
         let (mut options, filters_enabled) = if let Some(settings) = settings {
             (settings.search_options, settings.filters_enabled)
         } else {
-            (SearchOptions::NONE, false)
+            let search_options =
+                SearchOptions::from_settings(&EditorSettings::get_global(cx).search);
+            (search_options, false)
         };
 
         {
@@ -3537,7 +3539,7 @@ pub mod tests {
             editor::init(cx);
             workspace::init_settings(cx);
             Project::init_settings(cx);
-            super::init(cx);
+            crate::init(cx);
         });
     }
 

crates/search/src/search.rs 🔗

@@ -1,8 +1,10 @@
 use bitflags::bitflags;
 pub use buffer_search::BufferSearchBar;
+use editor::SearchSettings;
 use gpui::{actions, Action, AppContext, IntoElement};
 use project::search::SearchQuery;
 pub use project_search::ProjectSearchView;
+use settings::Settings;
 use ui::{prelude::*, Tooltip};
 use ui::{ButtonStyle, IconButton};
 use workspace::notifications::NotificationId;
@@ -13,6 +15,7 @@ pub mod project_search;
 pub(crate) mod search_bar;
 
 pub fn init(cx: &mut AppContext) {
+    SearchSettings::register(cx);
     menu::init();
     buffer_search::init(cx);
     project_search::init(cx);
@@ -93,6 +96,15 @@ impl SearchOptions {
         options
     }
 
+    pub fn from_settings(settings: &SearchSettings) -> SearchOptions {
+        let mut options = SearchOptions::NONE;
+        options.set(SearchOptions::WHOLE_WORD, settings.whole_word);
+        options.set(SearchOptions::CASE_SENSITIVE, settings.case_sensitive);
+        options.set(SearchOptions::INCLUDE_IGNORED, settings.include_ignored);
+        options.set(SearchOptions::REGEX, settings.regex);
+        options
+    }
+
     pub fn as_button(
         &self,
         active: bool,

crates/vim/src/test/vim_test_context.rs 🔗

@@ -16,12 +16,12 @@ impl VimTestContext {
             return;
         }
         cx.update(|cx| {
-            search::init(cx);
             let settings = SettingsStore::test(cx);
             cx.set_global(settings);
             release_channel::init(SemanticVersion::default(), cx);
             command_palette::init(cx);
             crate::init(cx);
+            search::init(cx);
         });
     }
 

crates/zed/src/zed.rs 🔗

@@ -3442,6 +3442,7 @@ mod tests {
             );
             tasks_ui::init(cx);
             initialize_workspace(app_state.clone(), prompt_builder, cx);
+            search::init(cx);
             app_state
         })
     }