Search in selections (#10831)

Release Notes:

- Adding [#8617 ](https://github.com/zed-industries/zed/issues/8617)

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
This commit is contained in:
kshokhin
2024-06-05 13:42:51 -06:00
committed by GitHub
co-authored by Conrad Irwin
parent 428c143fbb
commit c7c19609b3
13 changed files with 818 additions and 69 deletions
+212 -17
View File
@@ -3,7 +3,7 @@ mod registrar;
use crate::{
search_bar::render_nav_button, FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll,
ReplaceNext, SearchOptions, SelectAllMatches, SelectNextMatch, SelectPrevMatch,
ToggleCaseSensitive, ToggleRegex, ToggleReplace, ToggleWholeWord,
ToggleCaseSensitive, ToggleRegex, ToggleReplace, ToggleSelection, ToggleWholeWord,
};
use any_vec::AnyVec;
use collections::HashMap;
@@ -48,6 +48,8 @@ pub struct Deploy {
pub focus: bool,
#[serde(default)]
pub replace_enabled: bool,
#[serde(default)]
pub selection_search_enabled: bool,
}
impl_actions!(buffer_search, [Deploy]);
@@ -59,6 +61,7 @@ impl Deploy {
Self {
focus: true,
replace_enabled: false,
selection_search_enabled: false,
}
}
}
@@ -90,6 +93,7 @@ pub struct BufferSearchBar {
search_history: SearchHistory,
search_history_cursor: SearchHistoryCursor,
replace_enabled: bool,
selection_search_enabled: bool,
scroll_handle: ScrollHandle,
editor_scroll_handle: ScrollHandle,
editor_needed_width: Pixels,
@@ -228,7 +232,7 @@ impl Render for BufferSearchBar {
}),
)
}))
.children(supported_options.word.then(|| {
.children(supported_options.regex.then(|| {
self.render_search_option_button(
SearchOptions::REGEX,
cx.listener(|this, _, cx| this.toggle_regex(&ToggleRegex, cx)),
@@ -251,6 +255,26 @@ impl Render for BufferSearchBar {
.tooltip(|cx| Tooltip::for_action("Toggle replace", &ToggleReplace, cx)),
)
})
.when(supported_options.selection, |this| {
this.child(
IconButton::new(
"buffer-search-bar-toggle-search-selection-button",
IconName::SearchSelection,
)
.style(ButtonStyle::Subtle)
.when(self.selection_search_enabled, |button| {
button.style(ButtonStyle::Filled)
})
.on_click(cx.listener(|this, _: &ClickEvent, cx| {
this.toggle_selection(&ToggleSelection, cx);
}))
.selected(self.selection_search_enabled)
.size(ButtonSize::Compact)
.tooltip(|cx| {
Tooltip::for_action("Toggle search selection", &ToggleSelection, cx)
}),
)
})
.child(
h_flex()
.flex_none()
@@ -359,6 +383,9 @@ impl Render for BufferSearchBar {
.when(self.supported_options().regex, |this| {
this.on_action(cx.listener(Self::toggle_regex))
})
.when(self.supported_options().selection, |this| {
this.on_action(cx.listener(Self::toggle_selection))
})
.gap_2()
.child(
h_flex()
@@ -440,6 +467,11 @@ impl BufferSearchBar {
this.toggle_whole_word(action, cx);
}
}));
registrar.register_handler(ForDeployed(|this, action: &ToggleSelection, cx| {
if this.supported_options().selection {
this.toggle_selection(action, cx);
}
}));
registrar.register_handler(ForDeployed(|this, action: &ToggleReplace, cx| {
if this.supported_options().replacement {
this.toggle_replace(action, cx);
@@ -497,6 +529,7 @@ impl BufferSearchBar {
search_history_cursor: Default::default(),
active_search: None,
replace_enabled: false,
selection_search_enabled: false,
scroll_handle: ScrollHandle::new(),
editor_scroll_handle: ScrollHandle::new(),
editor_needed_width: px(0.),
@@ -516,8 +549,11 @@ impl BufferSearchBar {
searchable_item.clear_matches(cx);
}
}
if let Some(active_editor) = self.active_searchable_item.as_ref() {
if let Some(active_editor) = self.active_searchable_item.as_mut() {
self.selection_search_enabled = false;
self.replace_enabled = false;
active_editor.search_bar_visibility_changed(false, cx);
active_editor.toggle_filtered_search_ranges(false, cx);
let handle = active_editor.focus_handle(cx);
cx.focus(&handle);
}
@@ -530,8 +566,12 @@ impl BufferSearchBar {
pub fn deploy(&mut self, deploy: &Deploy, cx: &mut ViewContext<Self>) -> bool {
if self.show(cx) {
if let Some(active_item) = self.active_searchable_item.as_mut() {
active_item.toggle_filtered_search_ranges(deploy.selection_search_enabled, cx);
}
self.search_suggested(cx);
self.replace_enabled = deploy.replace_enabled;
self.selection_search_enabled = deploy.selection_search_enabled;
if deploy.focus {
let mut handle = self.query_editor.focus_handle(cx).clone();
let mut select_query = true;
@@ -539,9 +579,11 @@ impl BufferSearchBar {
handle = self.replacement_editor.focus_handle(cx).clone();
select_query = false;
};
if select_query {
self.select_query(cx);
}
cx.focus(&handle);
}
return true;
@@ -823,6 +865,15 @@ impl BufferSearchBar {
self.toggle_search_option(SearchOptions::WHOLE_WORD, cx)
}
fn toggle_selection(&mut self, _: &ToggleSelection, cx: &mut ViewContext<Self>) {
if let Some(active_item) = self.active_searchable_item.as_mut() {
self.selection_search_enabled = !self.selection_search_enabled;
active_item.toggle_filtered_search_ranges(self.selection_search_enabled, cx);
let _ = self.update_matches(cx);
cx.notify();
}
}
fn toggle_regex(&mut self, _: &ToggleRegex, cx: &mut ViewContext<Self>) {
self.toggle_search_option(SearchOptions::REGEX, cx)
}
@@ -1090,9 +1141,9 @@ mod tests {
use std::ops::Range;
use super::*;
use editor::{display_map::DisplayRow, DisplayPoint, Editor};
use editor::{display_map::DisplayRow, DisplayPoint, Editor, MultiBuffer};
use gpui::{Context, Hsla, TestAppContext, VisualTestContext};
use language::Buffer;
use language::{Buffer, Point};
use project::Project;
use smol::stream::StreamExt as _;
use unindent::Unindent as _;
@@ -1405,6 +1456,15 @@ mod tests {
});
}
fn display_points_of(
background_highlights: Vec<(Range<DisplayPoint>, Hsla)>,
) -> Vec<Range<DisplayPoint>> {
background_highlights
.into_iter()
.map(|(range, _)| range)
.collect::<Vec<_>>()
}
#[gpui::test]
async fn test_search_option_handling(cx: &mut TestAppContext) {
let (editor, search_bar, cx) = init_test(cx);
@@ -1417,12 +1477,6 @@ mod tests {
})
.await
.unwrap();
let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
background_highlights
.into_iter()
.map(|(range, _)| range)
.collect::<Vec<_>>()
};
editor.update(cx, |editor, cx| {
assert_eq!(
display_points_of(editor.all_text_background_highlights(cx)),
@@ -2032,15 +2086,156 @@ mod tests {
.await;
}
#[gpui::test]
async fn test_find_matches_in_selections_singleton_buffer_multiple_selections(
cx: &mut TestAppContext,
) {
init_globals(cx);
let buffer = cx.new_model(|cx| {
Buffer::local(
r#"
aaa bbb aaa ccc
aaa bbb aaa ccc
aaa bbb aaa ccc
aaa bbb aaa ccc
aaa bbb aaa ccc
aaa bbb aaa ccc
"#
.unindent(),
cx,
)
});
let cx = cx.add_empty_window();
let editor = cx.new_view(|cx| Editor::for_buffer(buffer.clone(), None, cx));
let search_bar = cx.new_view(|cx| {
let mut search_bar = BufferSearchBar::new(cx);
search_bar.set_active_pane_item(Some(&editor), cx);
search_bar.show(cx);
search_bar
});
editor.update(cx, |editor, cx| {
editor.change_selections(None, cx, |s| {
s.select_ranges(vec![Point::new(1, 0)..Point::new(2, 4)])
})
});
search_bar.update(cx, |search_bar, cx| {
let deploy = Deploy {
focus: true,
replace_enabled: false,
selection_search_enabled: true,
};
search_bar.deploy(&deploy, cx);
});
cx.run_until_parked();
search_bar
.update(cx, |search_bar, cx| search_bar.search("aaa", None, cx))
.await
.unwrap();
editor.update(cx, |editor, cx| {
assert_eq!(
editor.search_background_highlights(cx),
&[
Point::new(1, 0)..Point::new(1, 3),
Point::new(1, 8)..Point::new(1, 11),
Point::new(2, 0)..Point::new(2, 3),
]
);
});
}
#[gpui::test]
async fn test_find_matches_in_selections_multiple_excerpts_buffer_multiple_selections(
cx: &mut TestAppContext,
) {
init_globals(cx);
let text = r#"
aaa bbb aaa ccc
aaa bbb aaa ccc
aaa bbb aaa ccc
aaa bbb aaa ccc
aaa bbb aaa ccc
aaa bbb aaa ccc
aaa bbb aaa ccc
aaa bbb aaa ccc
aaa bbb aaa ccc
aaa bbb aaa ccc
aaa bbb aaa ccc
aaa bbb aaa ccc
"#
.unindent();
let cx = cx.add_empty_window();
let editor = cx.new_view(|cx| {
let multibuffer = MultiBuffer::build_multi(
[
(
&text,
vec![
Point::new(0, 0)..Point::new(2, 0),
Point::new(4, 0)..Point::new(5, 0),
],
),
(&text, vec![Point::new(9, 0)..Point::new(11, 0)]),
],
cx,
);
Editor::for_multibuffer(multibuffer, None, false, cx)
});
let search_bar = cx.new_view(|cx| {
let mut search_bar = BufferSearchBar::new(cx);
search_bar.set_active_pane_item(Some(&editor), cx);
search_bar.show(cx);
search_bar
});
editor.update(cx, |editor, cx| {
editor.change_selections(None, cx, |s| {
s.select_ranges(vec![
Point::new(1, 0)..Point::new(1, 4),
Point::new(5, 3)..Point::new(6, 4),
])
})
});
search_bar.update(cx, |search_bar, cx| {
let deploy = Deploy {
focus: true,
replace_enabled: false,
selection_search_enabled: true,
};
search_bar.deploy(&deploy, cx);
});
cx.run_until_parked();
search_bar
.update(cx, |search_bar, cx| search_bar.search("aaa", None, cx))
.await
.unwrap();
editor.update(cx, |editor, cx| {
assert_eq!(
editor.search_background_highlights(cx),
&[
Point::new(1, 0)..Point::new(1, 3),
Point::new(5, 8)..Point::new(5, 11),
Point::new(6, 0)..Point::new(6, 3),
]
);
});
}
#[gpui::test]
async fn test_invalid_regexp_search_after_valid(cx: &mut TestAppContext) {
let (editor, search_bar, cx) = init_test(cx);
let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
background_highlights
.into_iter()
.map(|(range, _)| range)
.collect::<Vec<_>>()
};
// Search using valid regexp
search_bar
.update(cx, |search_bar, cx| {