Cargo.lock 🔗
@@ -14770,7 +14770,6 @@ dependencies = [
"language",
"lsp",
"menu",
- "multi_buffer",
"pretty_assertions",
"project",
"serde",
Kirill Bulatov created
This reverts commit 1d66bbe06f6e02008560064302c44a57ed4cff41.
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
Cargo.lock | 1
assets/settings/default.json | 2
crates/editor/src/editor_settings.rs | 3
crates/editor/src/split.rs | 47 +
crates/multi_buffer/src/multi_buffer.rs | 2
crates/multi_buffer/src/path_key.rs | 122 +----
crates/search/Cargo.toml | 1
crates/search/src/buffer_search.rs | 4
crates/search/src/project_search.rs | 553 ++++----------------------
crates/settings/src/vscode_import.rs | 3
crates/settings_content/src/editor.rs | 2
crates/settings_ui/src/page_data.rs | 25 -
docs/src/reference/all-settings.md | 8
13 files changed, 143 insertions(+), 630 deletions(-)
@@ -14770,7 +14770,6 @@ dependencies = [
"language",
"lsp",
"menu",
- "multi_buffer",
"pretty_assertions",
"project",
"serde",
@@ -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:
@@ -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(),
@@ -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(
@@ -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();
@@ -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)
- }
- }
-
- PathExcerptInsertResult {
- inserted_ranges,
- excerpt_ids,
- 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.push(range)
}
- result_ranges.push(range);
- result_counts.push(count);
}
-
- (result_ranges, result_counts)
+ (result, insert_result.added_new_excerpt)
}
- 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,
+ }
}
}
@@ -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"] }
@@ -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),
});
});
});
@@ -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 },
- ]
- );
-
- // 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 },
- ]
+ 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),
);
-
- // 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();
}
}
@@ -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()
})
}
@@ -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]
@@ -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.",
@@ -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