From fee42e1d89cc5e50df3579e873ce5280650285e8 Mon Sep 17 00:00:00 2001 From: holoflash <114405740+holoflash@users.noreply.github.com> Date: Fri, 13 Feb 2026 16:54:17 +0100 Subject: [PATCH] Add search_on_input setting to Project Search (#42889) I was really missing the ability to instantly see search results while typing in the Project Search and decided to try and implement it. As this may not be a feature that _everyone_ wants, I made it toggle-able via the settings (or the settings.json) ### Settings Set to false by default Screenshot 2025-11-17 at 16 17 09 ### settings.json Set to false by default Screenshot 2025-11-17 at 16 18 21 ### Video demo: https://github.com/user-attachments/assets/715d6b77-3a61-45f8-8e1a-9bd880c697c3 - Search input is debounced with 250ms in this mode (cool?) The desire for this feature has been expressed here too: https://github.com/zed-industries/zed/discussions/30843 Release Notes: - Enabled project search on input. Can be turned off with `search.search_on_input` settings toggle. --- assets/settings/default.json | 2 + crates/editor/src/editor_settings.rs | 3 + crates/search/src/buffer_search.rs | 85 +++++++++++++++++++++++++++ crates/search/src/project_search.rs | 63 +++++++++++++++----- crates/settings_content/src/editor.rs | 2 + crates/settings_ui/src/page_data.rs | 25 +++++++- docs/src/reference/all-settings.md | 12 ++-- 7 files changed, 171 insertions(+), 21 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 3e6282e14eaebfb5a1d091a90c1883ed84da3d92..8e76ada7e9a193f666451b63743e0ad89b29e5c0 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -667,6 +667,8 @@ "regex": false, // Whether to center the cursor on each search match when navigating. "center_on_match": false, + // Whether to search on input. + "search_on_input": true, }, // When to populate a new search's query based on the text under the cursor. // This setting can take the following three values: diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index 47210a7561f4a3ebbddb994f00afcaff9158254d..654a541a699b62fc7fb9d88859ac8845a1dbff61 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -175,6 +175,8 @@ pub struct SearchSettings { pub regex: bool, /// Whether to center the cursor on each search match when navigating. pub center_on_match: bool, + /// Whether to search on input. + pub search_on_input: bool, } impl EditorSettings { @@ -271,6 +273,7 @@ impl Settings for EditorSettings { include_ignored: search.include_ignored.unwrap(), regex: search.regex.unwrap(), center_on_match: search.center_on_match.unwrap(), + search_on_input: search.search_on_input.unwrap(), }, auto_signature_help: editor.auto_signature_help.unwrap(), show_signature_help_after_edits: editor.show_signature_help_after_edits.unwrap(), diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 25cbb4ef4cdb65128adee25b93d33409b9df57f9..a8639b70cd75ca38bcafd81e6d4aee9acf6dbc98 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -3421,6 +3421,7 @@ mod tests { include_ignored: false, regex: false, center_on_match: false, + search_on_input: false, }, cx, ); @@ -3484,6 +3485,7 @@ mod tests { include_ignored: false, regex: false, center_on_match: false, + search_on_input: false, }, cx, ); @@ -3522,6 +3524,7 @@ mod tests { include_ignored: false, regex: false, center_on_match: false, + search_on_input: false, }, cx, ); @@ -3604,9 +3607,91 @@ mod tests { include_ignored: Some(search_settings.include_ignored), regex: Some(search_settings.regex), center_on_match: Some(search_settings.center_on_match), + search_on_input: Some(search_settings.search_on_input), }); }); }); }); } + #[gpui::test] + async fn test_search_on_input_setting(cx: &mut TestAppContext) { + let (editor, search_bar, cx) = init_test(cx); + + update_search_settings( + SearchSettings { + button: true, + whole_word: false, + case_sensitive: false, + include_ignored: false, + regex: false, + center_on_match: false, + search_on_input: false, + }, + cx, + ); + + search_bar.update_in(cx, |search_bar, window, cx| { + search_bar.show(window, cx); + search_bar.query_editor.update(cx, |query_editor, cx| { + query_editor.buffer().update(cx, |buffer, cx| { + buffer.edit( + [(MultiBufferOffset(0)..MultiBufferOffset(0), "expression")], + None, + cx, + ); + }); + }); + }); + + cx.background_executor.run_until_parked(); + + editor.update_in(cx, |editor, window, cx| { + let highlights = editor.all_text_background_highlights(window, cx); + assert!( + highlights.is_empty(), + "No highlights should appear when search_on_input is false" + ); + }); + + update_search_settings( + SearchSettings { + button: true, + whole_word: false, + case_sensitive: false, + include_ignored: false, + regex: false, + center_on_match: false, + search_on_input: true, + }, + cx, + ); + + search_bar.update_in(cx, |search_bar, window, cx| { + search_bar.dismiss(&Dismiss, window, cx); + search_bar.show(window, cx); + }); + + search_bar + .update_in(cx, |search_bar, window, cx| { + search_bar.search("expression", None, true, window, cx) + }) + .await + .unwrap(); + + editor.update_in(cx, |editor, window, cx| { + let highlights = display_points_of(editor.all_text_background_highlights(window, cx)); + assert_eq!( + highlights.len(), + 2, + "Should find 2 matches for 'expression' when search_on_input is true" + ); + assert_eq!( + highlights, + &[ + DisplayPoint::new(DisplayRow(0), 10)..DisplayPoint::new(DisplayRow(0), 20), + DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 19), + ] + ); + }); + } } diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 7c85077c488371e611fdadf0baf7ee94f49fe511..acd6576ce298ef86282cd0e6a6f98cde1d62bfba 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -869,15 +869,32 @@ impl ProjectSearchView { // Subscribe to query_editor in order to reraise editor events for workspace item activation purposes subscriptions.push( cx.subscribe(&query_editor, |this, _, event: &EditorEvent, cx| { - if let EditorEvent::Edited { .. } = event - && EditorSettings::get_global(cx).use_smartcase_search - { - let query = this.search_query_text(cx); - if !query.is_empty() - && this.search_options.contains(SearchOptions::CASE_SENSITIVE) - != contains_uppercase(&query) - { - this.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx); + if let EditorEvent::Edited { .. } = event { + if EditorSettings::get_global(cx).use_smartcase_search { + let query = this.search_query_text(cx); + if !query.is_empty() + && this.search_options.contains(SearchOptions::CASE_SENSITIVE) + != contains_uppercase(&query) + { + this.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx); + } + } + // Trigger search on input: + if EditorSettings::get_global(cx).search.search_on_input { + let query = this.search_query_text(cx); + if query.is_empty() { + // Clear results immediately when query is empty and abort ongoing search + this.entity.update(cx, |model, cx| { + model.pending_search = None; + model.match_ranges.clear(); + model.excerpts.update(cx, |excerpts, cx| excerpts.clear(cx)); + model.no_results = None; + model.limit_reached = false; + cx.notify(); + }); + } else { + this.search(cx); + } } } cx.emit(ViewEvent::EditorEvent(event.clone())) @@ -1529,7 +1546,11 @@ impl ProjectSearchView { editor.scroll(Point::default(), Some(Axis::Vertical), window, cx); } }); - if is_new_search && self.query_editor.focus_handle(cx).is_focused(window) { + let should_auto_focus = !EditorSettings::get_global(cx).search.search_on_input; + if is_new_search + && self.query_editor.focus_handle(cx).is_focused(window) + && should_auto_focus + { self.focus_results_editor(window, cx); } } @@ -1592,9 +1613,13 @@ impl ProjectSearchView { v_flex() .gap_1() .child( - Label::new("Hit enter to search. For more options:") - .color(Color::Muted) - .mb_2(), + Label::new(if EditorSettings::get_global(cx).search.search_on_input { + "Start typing to search. For more options:" + } else { + "Hit enter to search. For more options:" + }) + .color(Color::Muted) + .mb_2(), ) .child( Button::new("filter-paths", "Include/exclude specific paths") @@ -2537,7 +2562,8 @@ pub mod tests { use project::FakeFs; use serde_json::json; use settings::{ - InlayHintSettingsContent, SettingsStore, ThemeColorsContent, ThemeStyleContent, + InlayHintSettingsContent, SearchSettingsContent, SettingsStore, ThemeColorsContent, + ThemeStyleContent, }; use util::{path, paths::PathStyle, rel_path::rel_path}; use util_macros::perf; @@ -4756,6 +4782,15 @@ pub mod tests { let settings = SettingsStore::test(cx); cx.set_global(settings); + SettingsStore::update_global(cx, |store, cx| { + store.update_user_settings(cx, |settings| { + settings.editor.search = Some(SearchSettingsContent { + search_on_input: Some(false), + ..Default::default() + }); + }); + }); + theme::init(theme::LoadThemes::JustBase, cx); editor::init(cx); diff --git a/crates/settings_content/src/editor.rs b/crates/settings_content/src/editor.rs index 4d824e85e0e2ee020f48cdddb530bf494b2ce800..a2f53e1b9e309e5b07eaef7f7730773abfac70a9 100644 --- a/crates/settings_content/src/editor.rs +++ b/crates/settings_content/src/editor.rs @@ -828,6 +828,8 @@ pub struct SearchSettingsContent { pub regex: Option, /// Whether to center the cursor on each search match when navigating. pub center_on_match: Option, + /// Whether to search on input. + pub search_on_input: Option, } #[with_fallible_options] diff --git a/crates/settings_ui/src/page_data.rs b/crates/settings_ui/src/page_data.rs index 81500ce1730868e8090df6d97b5c84a25dd965fb..32fe90a9a39700d993d61051497a7c2ef1edddc9 100644 --- a/crates/settings_ui/src/page_data.rs +++ b/crates/settings_ui/src/page_data.rs @@ -2999,7 +2999,7 @@ fn languages_and_tools_page(cx: &App) -> SettingsPage { } fn search_and_files_page() -> SettingsPage { - fn search_section() -> [SettingsPageItem; 9] { + fn search_section() -> [SettingsPageItem; 10] { [ SettingsPageItem::SectionHeader("Search"), SettingsPageItem::SettingItem(SettingItem { @@ -3133,6 +3133,29 @@ fn search_and_files_page() -> SettingsPage { metadata: None, files: USER, }), + SettingsPageItem::SettingItem(SettingItem { + title: "Search on Input", + description: "Whether to search on input.", + field: Box::new(SettingField { + json_path: Some("editor.search.search_on_input"), + pick: |settings_content| { + settings_content + .editor + .search + .as_ref() + .and_then(|search| search.search_on_input.as_ref()) + }, + write: |settings_content, value| { + settings_content + .editor + .search + .get_or_insert_default() + .search_on_input = 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.", diff --git a/docs/src/reference/all-settings.md b/docs/src/reference/all-settings.md index 4aaf979993802da20d0b1a0f43d1a1323008611c..56f5b234aef463304874efc16177727196a320fb 100644 --- a/docs/src/reference/all-settings.md +++ b/docs/src/reference/all-settings.md @@ -3270,6 +3270,12 @@ Non-negative `integer` values - Setting: `regex` - Default: `false` +### Search On Input + +- Description: Whether to search on input. +- Setting: `search_on_input +- Default: `true` + ### Center On Match - Description: Whether to center the cursor on each search match when navigating. @@ -3282,12 +3288,6 @@ 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.