mirror of
https://github.com/zed-industries/zed.git
synced 2026-05-29 19:14:13 +00:00
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 <img width="911" height="618" alt="Screenshot 2025-11-17 at 16 17 09" src="https://github.com/user-attachments/assets/8eaaab65-684e-4c5f-9a3c-9cb62cff0925" /> ### settings.json Set to false by default <img width="396" height="193" alt="Screenshot 2025-11-17 at 16 18 21" src="https://github.com/user-attachments/assets/90ebda95-c454-4bc5-8423-5da593832fd2" /> ### 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.
This commit is contained in:
parent
73853be484
commit
fee42e1d89
7 changed files with 171 additions and 21 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
]
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -828,6 +828,8 @@ pub struct SearchSettingsContent {
|
|||
pub regex: Option<bool>,
|
||||
/// Whether to center the cursor on each search match when navigating.
|
||||
pub center_on_match: Option<bool>,
|
||||
/// Whether to search on input.
|
||||
pub search_on_input: Option<bool>,
|
||||
}
|
||||
|
||||
#[with_fallible_options]
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue