Revert "Enable type on search by default for the project search (#49374)" (#49619)

This reverts commit e477b545b7.

Needs 2 more fixes:

* enter does not move to the first excerpt anymore
* there could be situations when a narrowed search does not decrease the
excerpt enough to see the result onscreen

Release Notes:

- N/A
This commit is contained in:
Kirill Bulatov authored and GitHub committed 2026-02-19 16:24:54 +00:00
1 parent 8839d27a65
commit 761671b2ae
13 files changed
+143 -630

No files matched your search

Generated
-1
View File
@@ -14770,7 +14770,6 @@ dependencies = [
"language",
"lsp",
"menu",
"multi_buffer",
"pretty_assertions",
"project",
"serde",
-2
View File
@@ -671,8 +671,6 @@
"regex": false,
// Whether to center the cursor on each search match when navigating.
"center_on_match": false,
// Whether to search on input in project search.
"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:
-3
View File
@@ -175,8 +175,6 @@ 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 in project search.
pub search_on_input: bool,
}
impl EditorSettings {
@@ -279,7 +277,6 @@ 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(),
+30 -17
View File
@@ -2037,7 +2037,7 @@ impl LhsEditor {
base_text_buffer_snapshot = base_text_buffer.snapshot();
remote_id = base_text_buffer.remote_id();
}
let excerpt_ranges = rhs_multibuffer
let new = rhs_multibuffer
.excerpts_for_buffer(main_buffer.remote_id(), lhs_cx)
.into_iter()
.map(|(_, excerpt_range)| {
@@ -2061,26 +2061,13 @@ impl LhsEditor {
context: point_range_to_base_text_point_range(context),
}
})
.collect::<Vec<_>>();
.collect();
let (new, counts) = MultiBuffer::merge_excerpt_ranges(&excerpt_ranges);
let mut total = 0;
let rhs_merge_groups = counts
.iter()
.copied()
.map(|count| {
let group = rhs_excerpt_ids[total..total + count].to_vec();
total += count;
group
})
.collect::<Vec<_>>();
let lhs_result = lhs_multibuffer.set_merged_excerpt_ranges_for_path(
let lhs_result = lhs_multibuffer.update_path_excerpts(
path_key,
diff.read(lhs_cx).base_text_buffer().clone(),
excerpt_ranges,
&base_text_buffer_snapshot,
new,
counts,
lhs_cx,
);
if !lhs_result.excerpt_ids.is_empty()
@@ -2090,7 +2077,33 @@ impl LhsEditor {
{
lhs_multibuffer.add_inverted_diff(diff, lhs_cx);
}
Some((lhs_result.excerpt_ids, rhs_merge_groups))
let rhs_merge_groups: Vec<Vec<ExcerptId>> = {
let mut groups = Vec::new();
let mut current_group = Vec::new();
let mut last_id = None;
for (i, &lhs_id) in lhs_result.excerpt_ids.iter().enumerate() {
if last_id == Some(lhs_id) {
current_group.push(rhs_excerpt_ids[i]);
} else {
if !current_group.is_empty() {
groups.push(current_group);
}
current_group = vec![rhs_excerpt_ids[i]];
last_id = Some(lhs_id);
}
}
if !current_group.is_empty() {
groups.push(current_group);
}
groups
};
let deduplicated_lhs_ids: Vec<ExcerptId> =
lhs_result.excerpt_ids.iter().dedup().copied().collect();
Some((deduplicated_lhs_ids, rhs_merge_groups))
}
fn sync_path_excerpts(
+1 -1
View File
@@ -1734,7 +1734,7 @@ impl MultiBuffer {
}
#[instrument(skip_all)]
pub fn merge_excerpt_ranges<'a>(
fn merge_excerpt_ranges<'a>(
expanded_ranges: impl IntoIterator<Item = &'a ExcerptRange<Point>> + 'a,
) -> (Vec<ExcerptRange<Point>>, Vec<usize>) {
let mut merged_ranges: Vec<ExcerptRange<Point>> = Vec::new();
+21 -101
View File
@@ -15,7 +15,6 @@ use crate::{
#[derive(Debug, Clone)]
pub struct PathExcerptInsertResult {
pub inserted_ranges: Vec<Range<Anchor>>,
pub excerpt_ids: Vec<ExcerptId>,
pub added_new_excerpt: bool,
}
@@ -101,7 +100,7 @@ impl MultiBuffer {
let excerpt_ranges = build_excerpt_ranges(ranges, context_line_count, &buffer_snapshot);
let (new, counts) = Self::merge_excerpt_ranges(&excerpt_ranges);
let excerpt_insertion_result = self.set_merged_excerpt_ranges_for_path(
self.set_merged_excerpt_ranges_for_path(
path,
buffer,
excerpt_ranges,
@@ -109,10 +108,6 @@ impl MultiBuffer {
new,
counts,
cx,
);
(
excerpt_insertion_result.inserted_ranges,
excerpt_insertion_result.added_new_excerpt,
)
}
@@ -125,7 +120,7 @@ impl MultiBuffer {
cx: &mut Context<Self>,
) -> (Vec<Range<Anchor>>, bool) {
let (new, counts) = Self::merge_excerpt_ranges(&excerpt_ranges);
let excerpt_insertion_result = self.set_merged_excerpt_ranges_for_path(
self.set_merged_excerpt_ranges_for_path(
path,
buffer,
excerpt_ranges,
@@ -133,10 +128,6 @@ impl MultiBuffer {
new,
counts,
cx,
);
(
excerpt_insertion_result.inserted_ranges,
excerpt_insertion_result.added_new_excerpt,
)
}
@@ -165,7 +156,7 @@ impl MultiBuffer {
multi_buffer
.update(&mut app, move |multi_buffer, cx| {
let excerpt_insertion_result = multi_buffer.set_merged_excerpt_ranges_for_path(
let (ranges, _) = multi_buffer.set_merged_excerpt_ranges_for_path(
path_key,
buffer,
excerpt_ranges,
@@ -174,7 +165,7 @@ impl MultiBuffer {
counts,
cx,
);
excerpt_insertion_result.inserted_ranges
ranges
})
.ok()
.unwrap_or_default()
@@ -273,7 +264,7 @@ impl MultiBuffer {
}
/// Sets excerpts, returns `true` if at least one new excerpt was added.
pub fn set_merged_excerpt_ranges_for_path(
fn set_merged_excerpt_ranges_for_path(
&mut self,
path: PathKey,
buffer: Entity<Buffer>,
@@ -282,110 +273,36 @@ impl MultiBuffer {
new: Vec<ExcerptRange<Point>>,
counts: Vec<usize>,
cx: &mut Context<Self>,
) -> PathExcerptInsertResult {
let (new, counts) =
self.expand_new_ranges_to_existing(&path, buffer_snapshot, new, counts, cx);
let (excerpt_ids, added_new_excerpt) =
self.update_path_excerpts(path, buffer, buffer_snapshot, new, cx);
) -> (Vec<Range<Anchor>>, bool) {
let insert_result = self.update_path_excerpts(path, buffer, buffer_snapshot, new, cx);
let mut inserted_ranges = Vec::new();
let mut result = Vec::new();
let mut ranges = ranges.into_iter();
for (&excerpt_id, range_count) in excerpt_ids.iter().zip(counts.into_iter()) {
for (excerpt_id, range_count) in insert_result
.excerpt_ids
.into_iter()
.zip(counts.into_iter())
{
for range in ranges.by_ref().take(range_count) {
let range = Anchor::range_in_buffer(
excerpt_id,
buffer_snapshot.anchor_before(&range.primary.start)
..buffer_snapshot.anchor_after(&range.primary.end),
);
inserted_ranges.push(range)
result.push(range)
}
}
PathExcerptInsertResult {
inserted_ranges,
excerpt_ids,
added_new_excerpt,
}
(result, insert_result.added_new_excerpt)
}
/// Expand each new merged range to encompass any overlapping existing
/// excerpt, then re-merge. This turns "partial overlap where the union
/// equals the existing range" into an exact match, avoiding unnecessary
/// remove+insert churn that floods the wrap map with edits.
fn expand_new_ranges_to_existing(
&self,
path: &PathKey,
buffer_snapshot: &BufferSnapshot,
mut new: Vec<ExcerptRange<Point>>,
counts: Vec<usize>,
cx: &App,
) -> (Vec<ExcerptRange<Point>>, Vec<usize>) {
let existing = self.excerpts_by_path.get(path).cloned().unwrap_or_default();
if existing.is_empty() || new.is_empty() {
return (new, counts);
}
let snapshot = self.snapshot(cx);
let buffer_id = buffer_snapshot.remote_id();
let existing_ranges: Vec<Range<Point>> = existing
.iter()
.filter_map(|&id| {
let excerpt = snapshot.excerpt(id)?;
(excerpt.buffer_id == buffer_id)
.then(|| excerpt.range.context.to_point(buffer_snapshot))
})
.collect();
let mut changed = false;
for new_range in &mut new {
for existing_range in &existing_ranges {
if new_range.context.start <= existing_range.end
&& new_range.context.end >= existing_range.start
{
let expanded_start = new_range.context.start.min(existing_range.start);
let expanded_end = new_range.context.end.max(existing_range.end);
if expanded_start != new_range.context.start
|| expanded_end != new_range.context.end
{
new_range.context.start = expanded_start;
new_range.context.end = expanded_end;
changed = true;
}
}
}
}
if !changed {
return (new, counts);
}
let mut result_ranges: Vec<ExcerptRange<Point>> = Vec::new();
let mut result_counts: Vec<usize> = Vec::new();
for (range, count) in new.into_iter().zip(counts) {
if let Some(last) = result_ranges.last_mut() {
if last.context.end >= range.context.start
|| last.context.end.row + 1 == range.context.start.row
{
last.context.end = last.context.end.max(range.context.end);
*result_counts.last_mut().unwrap() += count;
continue;
}
}
result_ranges.push(range);
result_counts.push(count);
}
(result_ranges, result_counts)
}
fn update_path_excerpts(
pub fn update_path_excerpts(
&mut self,
path: PathKey,
buffer: Entity<Buffer>,
buffer_snapshot: &BufferSnapshot,
new: Vec<ExcerptRange<Point>>,
cx: &mut Context<Self>,
) -> (Vec<ExcerptId>, bool) {
) -> PathExcerptInsertResult {
let mut insert_after = self
.excerpts_by_path
.range(..path.clone())
@@ -575,6 +492,9 @@ impl MultiBuffer {
self.excerpts_by_path.insert(path, excerpt_ids);
}
(excerpt_ids, added_a_new_excerpt)
PathExcerptInsertResult {
excerpt_ids,
added_new_excerpt: added_a_new_excerpt,
}
}
}
-1
View File
@@ -52,7 +52,6 @@ editor = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] }
language = { workspace = true, features = ["test-support"] }
lsp.workspace = true
multi_buffer.workspace = true
pretty_assertions.workspace = true
unindent.workspace = true
workspace = { workspace = true, features = ["test-support"] }
-4
View File
@@ -3465,7 +3465,6 @@ mod tests {
include_ignored: false,
regex: false,
center_on_match: false,
search_on_input: false,
},
cx,
);
@@ -3529,7 +3528,6 @@ mod tests {
include_ignored: false,
regex: false,
center_on_match: false,
search_on_input: false,
},
cx,
);
@@ -3568,7 +3566,6 @@ mod tests {
include_ignored: false,
regex: false,
center_on_match: false,
search_on_input: false,
},
cx,
);
@@ -3651,7 +3648,6 @@ 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),
});
});
});
+82 -471
View File
@@ -9,7 +9,7 @@ use crate::{
},
};
use anyhow::Context as _;
use collections::{HashMap, HashSet};
use collections::HashMap;
use editor::{
Anchor, Editor, EditorEvent, EditorSettings, MAX_TAB_TITLE_LEN, MultiBuffer, PathKey,
SelectionEffects,
@@ -333,7 +333,7 @@ impl ProjectSearch {
}
}
fn search(&mut self, query: SearchQuery, incremental: bool, cx: &mut Context<Self>) {
fn search(&mut self, query: SearchQuery, cx: &mut Context<Self>) {
let search = self.project.update(cx, |project, cx| {
project
.search_history_mut(SearchInputKind::Query)
@@ -360,22 +360,18 @@ impl ProjectSearch {
let SearchResults { rx, _task_handle } = search;
let mut matches = pin!(rx.ready_chunks(1024));
if !incremental {
project_search
.update(cx, |project_search, cx| {
project_search.match_ranges.clear();
project_search
.excerpts
.update(cx, |excerpts, cx| excerpts.clear(cx));
project_search.no_results = Some(true);
project_search.limit_reached = false;
})
.ok()?;
}
project_search
.update(cx, |project_search, cx| {
project_search.match_ranges.clear();
project_search
.excerpts
.update(cx, |excerpts, cx| excerpts.clear(cx));
project_search.no_results = Some(true);
project_search.limit_reached = false;
})
.ok()?;
let mut limit_reached = false;
let mut seen_paths = HashSet::default();
while let Some(results) = matches.next().await {
let (buffers_with_ranges, has_reached_limit) = cx
.background_executor()
@@ -396,50 +392,6 @@ impl ProjectSearch {
})
.await;
limit_reached |= has_reached_limit;
if incremental {
let buffers_with_ranges: Vec<_> = buffers_with_ranges
.into_iter()
.filter(|(_, ranges)| !ranges.is_empty())
.collect();
if buffers_with_ranges.is_empty() {
continue;
}
let (mut chunk_ranges, chunk_paths) = project_search
.update(cx, |project_search, cx| {
let mut paths = Vec::new();
let futures = project_search.excerpts.update(cx, |excerpts, cx| {
buffers_with_ranges
.into_iter()
.map(|(buffer, ranges)| {
let path_key = PathKey::for_buffer(&buffer, cx);
paths.push(path_key.clone());
excerpts.set_anchored_excerpts_for_path(
path_key,
buffer,
ranges,
multibuffer_context_lines(cx),
cx,
)
})
.collect::<FuturesOrdered<_>>()
});
(futures, paths)
})
.ok()?;
seen_paths.extend(chunk_paths);
while let Some(ranges) = chunk_ranges.next().await {
smol::future::yield_now().await;
project_search
.update(cx, |project_search, cx| {
project_search.match_ranges.extend(ranges);
cx.notify();
})
.ok()?;
}
continue;
}
let mut new_ranges = project_search
.update(cx, |project_search, cx| {
project_search.excerpts.update(cx, |excerpts, cx| {
@@ -471,43 +423,16 @@ impl ProjectSearch {
}
}
if incremental {
project_search
.update(cx, |project_search, cx| {
if seen_paths.is_empty() {
project_search
.excerpts
.update(cx, |excerpts, cx| excerpts.clear(cx));
} else {
project_search.excerpts.update(cx, |excerpts, cx| {
let stale = excerpts
.paths()
.filter(|path| !seen_paths.contains(*path))
.cloned()
.collect::<Vec<_>>();
for path in stale {
excerpts.remove_excerpts_for_path(path, cx);
}
});
}
project_search.no_results = Some(project_search.match_ranges.is_empty());
project_search.limit_reached = limit_reached;
project_search.pending_search.take();
cx.notify();
})
.ok()?;
} else {
project_search
.update(cx, |project_search, cx| {
if !project_search.match_ranges.is_empty() {
project_search.no_results = Some(false);
}
project_search.limit_reached = limit_reached;
project_search.pending_search.take();
cx.notify();
})
.ok()?;
}
project_search
.update(cx, |project_search, cx| {
if !project_search.match_ranges.is_empty() {
project_search.no_results = Some(false);
}
project_search.limit_reached = limit_reached;
project_search.pending_search.take();
cx.notify();
})
.ok()?;
None
}));
@@ -814,7 +739,7 @@ impl ProjectSearchView {
&& self.query_editor.read(cx).text(cx) != *last_search_query_text
{
// search query has changed, restart search and bail
self.search(false, cx);
self.search(cx);
return;
}
if self.entity.read(cx).match_ranges.is_empty() {
@@ -842,7 +767,7 @@ impl ProjectSearchView {
&& self.query_editor.read(cx).text(cx) != *last_search_query_text
{
// search query has changed, restart search and bail
self.search(false, cx);
self.search(cx);
return;
}
if self.active_match_index.is_none() {
@@ -941,32 +866,15 @@ 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 {
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);
}
}
let search_settings = &EditorSettings::get_global(cx).search;
if search_settings.search_on_input {
if this.query_editor.read(cx).is_empty(cx) {
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;
model.last_search_query_text = None;
cx.notify();
});
} else {
this.search(true, 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);
}
}
cx.emit(ViewEvent::EditorEvent(event.clone()))
@@ -1152,7 +1060,7 @@ impl ProjectSearchView {
if let Some(new_query) = new_query {
let entity = cx.new(|cx| {
let mut entity = ProjectSearch::new(workspace.project().clone(), cx);
entity.search(new_query, false, cx);
entity.search(new_query, cx);
entity
});
let weak_workspace = cx.entity().downgrade();
@@ -1305,14 +1213,14 @@ impl ProjectSearchView {
};
if should_search {
this.update(cx, |this, cx| {
this.search(false, cx);
this.search(cx);
})?;
}
anyhow::Ok(())
})
}
fn search(&mut self, incremental: bool, cx: &mut Context<Self>) {
fn search(&mut self, cx: &mut Context<Self>) {
let open_buffers = if self.included_opened_only {
self.workspace
.update(cx, |workspace, cx| self.open_buffers(cx, workspace))
@@ -1321,8 +1229,7 @@ impl ProjectSearchView {
None
};
if let Some(query) = self.build_search_query(cx, open_buffers) {
self.entity
.update(cx, |model, cx| model.search(query, incremental, cx));
self.entity.update(cx, |model, cx| model.search(query, cx));
}
}
@@ -1584,15 +1491,10 @@ impl ProjectSearchView {
}
fn entity_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let model = self.entity.read(cx);
let match_ranges = model.match_ranges.clone();
let is_incremental_pending =
model.pending_search.is_some() && EditorSettings::get_global(cx).search.search_on_input;
let match_ranges = self.entity.read(cx).match_ranges.clone();
if match_ranges.is_empty() {
if !is_incremental_pending {
self.active_match_index = None;
}
self.active_match_index = None;
self.results_editor.update(cx, |editor, cx| {
editor.clear_background_highlights(HighlightKey::ProjectSearchView, cx);
});
@@ -1612,11 +1514,7 @@ impl ProjectSearchView {
editor.scroll(Point::default(), Some(Axis::Vertical), window, cx);
}
});
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
{
if is_new_search && self.query_editor.focus_handle(cx).is_focused(window) {
self.focus_results_editor(window, cx);
}
}
@@ -1679,13 +1577,9 @@ impl ProjectSearchView {
v_flex()
.gap_1()
.child(
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(),
Label::new("Hit enter to search. For more options:")
.color(Color::Muted)
.mb_2(),
)
.child(
Button::new("filter-paths", "Include/exclude specific paths")
@@ -2604,7 +2498,7 @@ pub fn perform_project_search(
search_view.query_editor.update(cx, |query_editor, cx| {
query_editor.set_text(text, window, cx)
});
search_view.search(false, cx);
search_view.search(cx);
});
cx.run_until_parked();
}
@@ -2612,9 +2506,7 @@ pub fn perform_project_search(
#[cfg(test)]
pub mod tests {
use std::{
cell::RefCell,
path::PathBuf,
rc::Rc,
sync::{
Arc,
atomic::{self, AtomicUsize},
@@ -2626,7 +2518,6 @@ pub mod tests {
use editor::{DisplayPoint, display_map::DisplayRow};
use gpui::{Action, TestAppContext, VisualTestContext, WindowHandle};
use language::{FakeLspAdapter, rust_lang};
use multi_buffer::Event as MultiBufferEvent;
use pretty_assertions::assert_eq;
use project::FakeFs;
use serde_json::json;
@@ -2637,155 +2528,6 @@ pub mod tests {
use util_macros::perf;
use workspace::{DeploySearch, MultiWorkspace};
#[derive(Debug, Clone, PartialEq, Eq)]
enum ExcerptEvent {
Added { excerpts: usize },
Removed { ids: usize },
Edited,
}
fn subscribe_to_excerpt_events(
search: &Entity<ProjectSearch>,
cx: &mut TestAppContext,
) -> (Rc<RefCell<Vec<ExcerptEvent>>>, Subscription) {
let events: Rc<RefCell<Vec<ExcerptEvent>>> = Rc::default();
let excerpts = cx.update(|cx| search.read(cx).excerpts.clone());
let subscription = cx.update({
let events = events.clone();
|cx| {
cx.subscribe(
&excerpts,
move |_, event: &MultiBufferEvent, _| match event {
MultiBufferEvent::ExcerptsAdded { excerpts, .. } => {
events.borrow_mut().push(ExcerptEvent::Added {
excerpts: excerpts.len(),
});
}
MultiBufferEvent::ExcerptsRemoved { ids, .. } => {
events
.borrow_mut()
.push(ExcerptEvent::Removed { ids: ids.len() });
}
MultiBufferEvent::Edited { .. } => {
events.borrow_mut().push(ExcerptEvent::Edited);
}
_ => {}
},
)
}
});
(events, subscription)
}
fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
let settings = SettingsStore::test(cx);
cx.set_global(settings);
theme::init(theme::LoadThemes::JustBase, cx);
editor::init(cx);
crate::init(cx);
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings(cx, |settings| {
settings
.editor
.search
.get_or_insert_default()
.search_on_input = Some(false);
});
});
});
}
fn perform_search(
search_view: WindowHandle<ProjectSearchView>,
text: impl Into<Arc<str>>,
cx: &mut TestAppContext,
) {
search_view
.update(cx, |search_view, window, cx| {
search_view.query_editor.update(cx, |query_editor, cx| {
query_editor.set_text(text, window, cx)
});
search_view.search(false, cx);
})
.unwrap();
// Ensure editor highlights appear after the search is done
cx.executor().advance_clock(
editor::SELECTION_HIGHLIGHT_DEBOUNCE_TIMEOUT + Duration::from_millis(100),
);
cx.background_executor.run_until_parked();
}
fn perform_incremental_search(
search_view: WindowHandle<ProjectSearchView>,
text: impl Into<Arc<str>>,
cx: &mut TestAppContext,
) {
search_view
.update(cx, |search_view, window, cx| {
search_view.query_editor.update(cx, |query_editor, cx| {
query_editor.set_text(text, window, cx)
});
search_view.search(true, cx);
})
.unwrap();
cx.executor().advance_clock(
editor::SELECTION_HIGHLIGHT_DEBOUNCE_TIMEOUT + Duration::from_millis(100),
);
cx.background_executor.run_until_parked();
}
fn read_match_count(
search_view: WindowHandle<ProjectSearchView>,
cx: &mut TestAppContext,
) -> usize {
search_view
.read_with(cx, |search_view, cx| {
search_view.entity.read(cx).match_ranges.len()
})
.unwrap()
}
fn read_match_texts(
search_view: WindowHandle<ProjectSearchView>,
cx: &mut TestAppContext,
) -> Vec<String> {
search_view
.read_with(cx, |search_view, cx| {
let search = search_view.entity.read(cx);
let snapshot = search.excerpts.read(cx).snapshot(cx);
search
.match_ranges
.iter()
.map(|range| snapshot.text_for_range(range.clone()).collect::<String>())
.collect()
})
.unwrap()
}
fn assert_all_highlights_match_query(
search_view: WindowHandle<ProjectSearchView>,
query: &str,
cx: &mut TestAppContext,
) {
let match_texts = read_match_texts(search_view, cx);
assert_eq!(
match_texts.len(),
read_match_count(search_view, cx),
"match texts count should equal match_ranges count for query {query:?}"
);
for text in &match_texts {
assert_eq!(
text.to_uppercase(),
query.to_uppercase(),
"every highlighted range should match the query {query:?}"
);
}
}
#[test]
fn test_split_glob_patterns() {
assert_eq!(split_glob_patterns("a,b,c"), vec!["a", "b", "c"]);
@@ -3331,7 +3073,7 @@ pub mod tests {
search_view.query_editor.update(cx, |query_editor, cx| {
query_editor.set_text("sOMETHINGtHATsURELYdOESnOTeXIST", window, cx)
});
search_view.search(false, cx);
search_view.search(cx);
});
})
.unwrap();
@@ -3375,7 +3117,7 @@ pub mod tests {
search_view.query_editor.update(cx, |query_editor, cx| {
query_editor.set_text("TWO", window, cx)
});
search_view.search(false, cx);
search_view.search(cx);
});
})
.unwrap();
@@ -3528,7 +3270,7 @@ pub mod tests {
.update(cx, |exclude_editor, cx| {
exclude_editor.set_text("four.rs", window, cx)
});
search_view.search(false, cx);
search_view.search(cx);
});
})
.unwrap();
@@ -3558,7 +3300,7 @@ pub mod tests {
.update(cx, |_, _, cx| {
search_view.update(cx, |search_view, cx| {
search_view.toggle_filters(cx);
search_view.search(false, cx);
search_view.search(cx);
});
})
.unwrap();
@@ -3685,7 +3427,7 @@ pub mod tests {
search_view.query_editor.update(cx, |query_editor, cx| {
query_editor.set_text("sOMETHINGtHATsURELYdOESnOTeXIST", window, cx)
});
search_view.search(false, cx);
search_view.search(cx);
});
})
.unwrap();
@@ -3730,7 +3472,7 @@ pub mod tests {
search_view.query_editor.update(cx, |query_editor, cx| {
query_editor.set_text("TWO", window, cx)
});
search_view.search(false, cx);
search_view.search(cx);
})
})
.unwrap();
@@ -3830,7 +3572,7 @@ pub mod tests {
search_view_2.query_editor.update(cx, |query_editor, cx| {
query_editor.set_text("FOUR", window, cx)
});
search_view_2.search(false, cx);
search_view_2.search(cx);
});
})
.unwrap();
@@ -3976,7 +3718,7 @@ pub mod tests {
search_view.query_editor.update(cx, |query_editor, cx| {
query_editor.set_text("const", window, cx)
});
search_view.search(false, cx);
search_view.search(cx);
});
})
.unwrap();
@@ -4051,7 +3793,7 @@ pub mod tests {
search_view.query_editor.update(cx, |query_editor, cx| {
query_editor.set_text("ONE", window, cx)
});
search_view.search(false, cx);
search_view.search(cx);
});
})
.unwrap();
@@ -4063,7 +3805,7 @@ pub mod tests {
search_view.query_editor.update(cx, |query_editor, cx| {
query_editor.set_text("TWO", window, cx)
});
search_view.search(false, cx);
search_view.search(cx);
});
})
.unwrap();
@@ -4074,7 +3816,7 @@ pub mod tests {
search_view.query_editor.update(cx, |query_editor, cx| {
query_editor.set_text("THREE", window, cx)
});
search_view.search(false, cx);
search_view.search(cx);
})
})
.unwrap();
@@ -4231,7 +3973,7 @@ pub mod tests {
search_view.query_editor.update(cx, |query_editor, cx| {
query_editor.set_text("TWO_NEW", window, cx)
});
search_view.search(false, cx);
search_view.search(cx);
});
})
.unwrap();
@@ -4445,7 +4187,7 @@ pub mod tests {
search_view.query_editor.update(cx, |query_editor, cx| {
query_editor.set_text(query, window, cx)
});
search_view.search(false, cx);
search_view.search(cx);
});
})
.unwrap();
@@ -5145,166 +4887,35 @@ pub mod tests {
.unwrap();
}
#[gpui::test]
async fn test_incremental_search_narrows_and_widens(cx: &mut TestAppContext) {
init_test(cx);
fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
let settings = SettingsStore::test(cx);
cx.set_global(settings);
// Two matches 5 lines apart: with context_line_count=2, contexts
// [3..7] and [8..12] are adjacent and merge into a single excerpt
// [3..12]. Narrowing to "targeted" produces context [3..7] ⊂ [3..12]
// — the expand_new_ranges_to_existing fix ensures zero excerpt events.
let mut lines: Vec<String> = (0..20).map(|i| format!("line {i}: filler")).collect();
lines[5] = "line 5: targeted item".into();
lines[10] = "line 10: target item".into();
let big_file = lines.join("\n");
theme::init(theme::LoadThemes::JustBase, cx);
let fs = FakeFs::new(cx.background_executor.clone());
fs.insert_tree(
path!("/dir"),
json!({
"one.rs": "const ONE: usize = 1;\nconst ONEROUS: usize = 2;",
"two.rs": "const TWO: usize = one::ONE + one::ONE;",
"three.rs": "const THREE: usize = one::ONE + two::TWO;",
"four.rs": "const FOUR: usize = one::ONE + three::THREE;",
"only_one.rs": "const ONLY_ONE: usize = 1;",
"big.txt": big_file,
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
let window =
cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = window
.read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
let search = cx.new(|cx| ProjectSearch::new(project.clone(), cx));
let search_view = cx.add_window(|window, cx| {
ProjectSearchView::new(workspace.downgrade(), search.clone(), window, cx, None)
editor::init(cx);
crate::init(cx);
});
let (events, _subscription) = subscribe_to_excerpt_events(&search, cx);
let take_excerpt_changes = || -> Vec<ExcerptEvent> {
events
.borrow_mut()
.drain(..)
.filter(|e| !matches!(e, ExcerptEvent::Edited))
.collect()
};
let expected_one_matches = vec![
"one", "ONE", "ONE", "ONE", "ONE", "one", "ONE", "one", "ONE", "one", "ONE",
];
}
// Initial non-incremental search for "ONE" — clears then inserts one excerpt per file.
perform_search(search_view, "ONE", cx);
assert_eq!(read_match_texts(search_view, cx), expected_one_matches);
assert_all_highlights_match_query(search_view, "ONE", cx);
assert_eq!(
take_excerpt_changes(),
vec![
ExcerptEvent::Removed { ids: 0 },
ExcerptEvent::Added { excerpts: 1 },
ExcerptEvent::Added { excerpts: 1 },
ExcerptEvent::Added { excerpts: 1 },
ExcerptEvent::Added { excerpts: 1 },
ExcerptEvent::Added { excerpts: 1 },
]
fn perform_search(
search_view: WindowHandle<ProjectSearchView>,
text: impl Into<Arc<str>>,
cx: &mut TestAppContext,
) {
search_view
.update(cx, |search_view, window, cx| {
search_view.query_editor.update(cx, |query_editor, cx| {
query_editor.set_text(text, window, cx)
});
search_view.search(cx);
})
.unwrap();
// Ensure editor highlights appear after the search is done
cx.executor().advance_clock(
editor::SELECTION_HIGHLIGHT_DEBOUNCE_TIMEOUT + Duration::from_millis(100),
);
// Natural narrowing: typing "R" after "ONE" -> "ONER".
// Only one.rs has ONEROUS, 4 other files removed.
perform_incremental_search(search_view, "ONER", cx);
assert_eq!(read_match_texts(search_view, cx), vec!["ONER"]);
assert_all_highlights_match_query(search_view, "ONER", cx);
assert_eq!(
take_excerpt_changes(),
vec![
ExcerptEvent::Removed { ids: 1 },
ExcerptEvent::Removed { ids: 1 },
ExcerptEvent::Removed { ids: 1 },
ExcerptEvent::Removed { ids: 1 },
]
);
// Continue typing "OUS" -> "ONEROUS". Still one.rs only, zero excerpt churn.
perform_incremental_search(search_view, "ONEROUS", cx);
assert_eq!(read_match_texts(search_view, cx), vec!["ONEROUS"]);
assert_all_highlights_match_query(search_view, "ONEROUS", cx);
assert_eq!(take_excerpt_changes(), Vec::new());
// Backspace to "ONER" — still one.rs only, zero events.
perform_incremental_search(search_view, "ONER", cx);
assert_eq!(read_match_texts(search_view, cx), vec!["ONER"]);
assert_all_highlights_match_query(search_view, "ONER", cx);
assert_eq!(take_excerpt_changes(), Vec::new());
// Backspace to "ONE" — 4 files re-added.
perform_incremental_search(search_view, "ONE", cx);
assert_eq!(read_match_texts(search_view, cx), expected_one_matches);
assert_all_highlights_match_query(search_view, "ONE", cx);
assert_eq!(
take_excerpt_changes(),
vec![
ExcerptEvent::Added { excerpts: 1 },
ExcerptEvent::Added { excerpts: 1 },
ExcerptEvent::Added { excerpts: 1 },
ExcerptEvent::Added { excerpts: 1 },
]
);
// Repeat the same "ONE" query — excerpts already match, zero events emitted.
perform_incremental_search(search_view, "ONE", cx);
assert_eq!(read_match_texts(search_view, cx), expected_one_matches);
assert_all_highlights_match_query(search_view, "ONE", cx);
assert_eq!(events.borrow().len(), 0);
// Narrow to "ONLY_ONE" — single match in only_one.rs, 4 files removed.
perform_incremental_search(search_view, "ONLY_ONE", cx);
assert_eq!(read_match_texts(search_view, cx), vec!["ONLY_ONE"]);
assert_all_highlights_match_query(search_view, "ONLY_ONE", cx);
assert_eq!(
take_excerpt_changes(),
vec![
ExcerptEvent::Removed { ids: 1 },
ExcerptEvent::Removed { ids: 1 },
ExcerptEvent::Removed { ids: 1 },
ExcerptEvent::Removed { ids: 1 },
]
);
// Widen back to "ONE" — 4 files re-added.
perform_incremental_search(search_view, "ONE", cx);
assert_eq!(read_match_texts(search_view, cx), expected_one_matches);
assert_all_highlights_match_query(search_view, "ONE", cx);
assert_eq!(
take_excerpt_changes(),
vec![
ExcerptEvent::Added { excerpts: 1 },
ExcerptEvent::Added { excerpts: 1 },
ExcerptEvent::Added { excerpts: 1 },
ExcerptEvent::Added { excerpts: 1 },
]
);
// Narrowing when all files still match — zero excerpt events.
// "usize" matches all 5 .rs files; "usize =" is narrower but still in every file.
perform_search(search_view, "usize", cx);
assert_eq!(read_match_count(search_view, cx), 6);
events.borrow_mut().clear();
perform_incremental_search(search_view, "usize =", cx);
assert_eq!(read_match_count(search_view, cx), 6);
assert_eq!(events.borrow().len(), 0);
// Merged-excerpt narrowing: "target" matches lines 5 and 10 in big.txt,
// whose context lines merge into one excerpt [3..12]. Narrowing to
// "targeted" shrinks context to [3..7] ⊂ [3..12] — the existing excerpt
// must be kept with zero events.
perform_search(search_view, "target", cx);
assert_all_highlights_match_query(search_view, "target", cx);
assert_eq!(read_match_count(search_view, cx), 2);
take_excerpt_changes();
perform_incremental_search(search_view, "targeted", cx);
assert_all_highlights_match_query(search_view, "targeted", cx);
assert_eq!(read_match_count(search_view, cx), 1);
assert_eq!(take_excerpt_changes(), Vec::new());
cx.background_executor.run_until_parked();
}
}
+1 -2
View File
@@ -357,8 +357,7 @@ impl VsCodeSettings {
fn search_content(&self) -> Option<SearchSettingsContent> {
skip_default(SearchSettingsContent {
include_ignored: self.read_bool("search.useIgnoreFiles"),
search_on_input: self.read_bool("search.searchOnType"),
..SearchSettingsContent::default()
..Default::default()
})
}
-2
View File
@@ -828,8 +828,6 @@ 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 in project search.
pub search_on_input: Option<bool>,
}
#[with_fallible_options]
+1 -24
View File
@@ -2999,7 +2999,7 @@ fn languages_and_tools_page(cx: &App) -> SettingsPage {
}
fn search_and_files_page() -> SettingsPage {
fn search_section() -> [SettingsPageItem; 10] {
fn search_section() -> [SettingsPageItem; 9] {
[
SettingsPageItem::SectionHeader("Search"),
SettingsPageItem::SettingItem(SettingItem {
@@ -3133,29 +3133,6 @@ fn search_and_files_page() -> SettingsPage {
metadata: None,
files: USER,
}),
SettingsPageItem::SettingItem(SettingItem {
title: "Search on Input",
description: "Whether to search on input in project search.",
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.",
+7 -1
View File
@@ -3279,7 +3279,13 @@ Non-negative `integer` values
- Description: Whether to search on input in project search.
- Setting: `search_on_input`
- Default: `true`
- Default: `false`
### Search On Input Debounce Ms
- Description: Debounce time in milliseconds for search on input in project search. Set to 0 to disable debouncing.
- Setting: `search_on_input_debounce_ms`
- Default: `200`
### Center On Match