Files
oak-gpui/crates/search/src/buffer_search.rs
T
K SimmonsandMikayla Maki 31ecb2f7bc Introduced ItemEvent and to_item_events function to Item trait which converts the Item's events into a standard ItemEvent similar to how SearchableItems work.
Add breadcrumb_location and breadcrumbs functions to item trait which handles rendering of the breadcrumb elements
Change breadcrumb toolbar to use these new functions rather than having hard coded breadcrumb logic
Add breadcrumb support to the terminal tabs

Co-Authored-By: Mikayla Maki <mikayla@zed.dev>
2022-09-06 16:05:36 -07:00

928 lines
34 KiB
Rust

use crate::{
SearchOption, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleRegex,
ToggleWholeWord,
};
use collections::HashMap;
use editor::Editor;
use gpui::{
actions, elements::*, impl_actions, platform::CursorStyle, Action, AnyViewHandle, AppContext,
Entity, MouseButton, MutableAppContext, RenderContext, Subscription, Task, View, ViewContext,
ViewHandle,
};
use project::search::SearchQuery;
use serde::Deserialize;
use settings::Settings;
use std::any::Any;
use workspace::{
searchable::{Direction, SearchEvent, SearchableItemHandle, WeakSearchableItemHandle},
ItemHandle, Pane, ToolbarItemLocation, ToolbarItemView,
};
#[derive(Clone, Deserialize, PartialEq)]
pub struct Deploy {
pub focus: bool,
}
actions!(buffer_search, [Dismiss, FocusEditor]);
impl_actions!(buffer_search, [Deploy]);
pub enum Event {
UpdateLocation,
}
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(BufferSearchBar::deploy);
cx.add_action(BufferSearchBar::dismiss);
cx.add_action(BufferSearchBar::focus_editor);
cx.add_action(BufferSearchBar::select_next_match);
cx.add_action(BufferSearchBar::select_prev_match);
cx.add_action(BufferSearchBar::select_next_match_on_pane);
cx.add_action(BufferSearchBar::select_prev_match_on_pane);
cx.add_action(BufferSearchBar::handle_editor_cancel);
add_toggle_option_action::<ToggleCaseSensitive>(SearchOption::CaseSensitive, cx);
add_toggle_option_action::<ToggleWholeWord>(SearchOption::WholeWord, cx);
add_toggle_option_action::<ToggleRegex>(SearchOption::Regex, cx);
}
fn add_toggle_option_action<A: Action>(option: SearchOption, cx: &mut MutableAppContext) {
cx.add_action(move |pane: &mut Pane, _: &A, cx: &mut ViewContext<Pane>| {
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
if search_bar.update(cx, |search_bar, cx| search_bar.show(false, false, cx)) {
search_bar.update(cx, |search_bar, cx| {
search_bar.toggle_search_option(option, cx);
});
return;
}
}
cx.propagate_action();
});
}
pub struct BufferSearchBar {
pub query_editor: ViewHandle<Editor>,
active_searchable_item: Option<Box<dyn SearchableItemHandle>>,
active_match_index: Option<usize>,
active_searchable_item_subscription: Option<Subscription>,
seachable_items_with_matches:
HashMap<Box<dyn WeakSearchableItemHandle>, Vec<Box<dyn Any + Send>>>,
pending_search: Option<Task<()>>,
case_sensitive: bool,
whole_word: bool,
regex: bool,
query_contains_error: bool,
dismissed: bool,
}
impl Entity for BufferSearchBar {
type Event = Event;
}
impl View for BufferSearchBar {
fn ui_name() -> &'static str {
"BufferSearchBar"
}
fn on_focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
if cx.is_self_focused() {
cx.focus(&self.query_editor);
}
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let theme = cx.global::<Settings>().theme.clone();
let editor_container = if self.query_contains_error {
theme.search.invalid_editor
} else {
theme.search.editor.input.container
};
let supported_options = self
.active_searchable_item
.as_ref()
.map(|active_searchable_item| active_searchable_item.supported_options())
.unwrap_or_default();
Flex::row()
.with_child(
Flex::row()
.with_child(
ChildView::new(&self.query_editor)
.aligned()
.left()
.flex(1., true)
.boxed(),
)
.with_children(self.active_searchable_item.as_ref().and_then(
|searchable_item| {
let matches = self
.seachable_items_with_matches
.get(&searchable_item.downgrade())?;
let message = if let Some(match_ix) = self.active_match_index {
format!("{}/{}", match_ix + 1, matches.len())
} else {
"No matches".to_string()
};
Some(
Label::new(message, theme.search.match_index.text.clone())
.contained()
.with_style(theme.search.match_index.container)
.aligned()
.boxed(),
)
},
))
.contained()
.with_style(editor_container)
.aligned()
.constrained()
.with_min_width(theme.search.editor.min_width)
.with_max_width(theme.search.editor.max_width)
.flex(1., false)
.boxed(),
)
.with_child(
Flex::row()
.with_child(self.render_nav_button("<", Direction::Prev, cx))
.with_child(self.render_nav_button(">", Direction::Next, cx))
.aligned()
.boxed(),
)
.with_child(
Flex::row()
.with_children(self.render_search_option(
supported_options.case,
"Case",
SearchOption::CaseSensitive,
cx,
))
.with_children(self.render_search_option(
supported_options.word,
"Word",
SearchOption::WholeWord,
cx,
))
.with_children(self.render_search_option(
supported_options.regex,
"Regex",
SearchOption::Regex,
cx,
))
.contained()
.with_style(theme.search.option_button_group)
.aligned()
.boxed(),
)
.contained()
.with_style(theme.search.container)
.named("search bar")
}
}
impl ToolbarItemView for BufferSearchBar {
fn set_active_pane_item(
&mut self,
item: Option<&dyn ItemHandle>,
cx: &mut ViewContext<Self>,
) -> ToolbarItemLocation {
cx.notify();
self.active_searchable_item_subscription.take();
self.active_searchable_item.take();
self.pending_search.take();
if let Some(searchable_item_handle) =
item.and_then(|item| item.to_searchable_item_handle(cx))
{
let handle = cx.weak_handle();
self.active_searchable_item_subscription =
Some(searchable_item_handle.subscribe_to_search_events(
cx,
Box::new(move |search_event, cx| {
if let Some(this) = handle.upgrade(cx) {
this.update(cx, |this, cx| {
this.on_active_searchable_item_event(search_event, cx)
});
}
}),
));
self.active_searchable_item = Some(searchable_item_handle);
self.update_matches(false, cx);
if !self.dismissed {
return ToolbarItemLocation::Secondary;
}
}
ToolbarItemLocation::Hidden
}
fn location_for_event(
&self,
_: &Self::Event,
_: ToolbarItemLocation,
_: &AppContext,
) -> ToolbarItemLocation {
if self.active_searchable_item.is_some() && !self.dismissed {
ToolbarItemLocation::Secondary
} else {
ToolbarItemLocation::Hidden
}
}
}
impl BufferSearchBar {
pub fn new(cx: &mut ViewContext<Self>) -> Self {
let query_editor = cx.add_view(|cx| {
Editor::auto_height(2, Some(|theme| theme.search.editor.input.clone()), cx)
});
cx.subscribe(&query_editor, Self::on_query_editor_event)
.detach();
Self {
query_editor,
active_searchable_item: None,
active_searchable_item_subscription: None,
active_match_index: None,
seachable_items_with_matches: Default::default(),
case_sensitive: false,
whole_word: false,
regex: false,
pending_search: None,
query_contains_error: false,
dismissed: true,
}
}
fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext<Self>) {
self.dismissed = true;
for searchable_item in self.seachable_items_with_matches.keys() {
if let Some(searchable_item) =
WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
{
searchable_item.clear_matches(cx);
}
}
if let Some(active_editor) = self.active_searchable_item.as_ref() {
cx.focus(active_editor);
}
cx.emit(Event::UpdateLocation);
cx.notify();
}
fn show(&mut self, focus: bool, suggest_query: bool, cx: &mut ViewContext<Self>) -> bool {
let searchable_item = if let Some(searchable_item) = &self.active_searchable_item {
SearchableItemHandle::boxed_clone(searchable_item.as_ref())
} else {
return false;
};
if suggest_query {
let text = searchable_item.query_suggestion(cx);
if !text.is_empty() {
self.set_query(&text, cx);
}
}
if focus {
let query_editor = self.query_editor.clone();
query_editor.update(cx, |query_editor, cx| {
query_editor.select_all(&editor::SelectAll, cx);
});
cx.focus_self();
}
self.dismissed = false;
cx.notify();
cx.emit(Event::UpdateLocation);
true
}
fn set_query(&mut self, query: &str, cx: &mut ViewContext<Self>) {
self.query_editor.update(cx, |query_editor, cx| {
query_editor.buffer().update(cx, |query_buffer, cx| {
let len = query_buffer.len(cx);
query_buffer.edit([(0..len, query)], None, cx);
});
});
}
fn render_search_option(
&self,
option_supported: bool,
icon: &str,
option: SearchOption,
cx: &mut RenderContext<Self>,
) -> Option<ElementBox> {
if !option_supported {
return None;
}
let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
let is_active = self.is_search_option_enabled(option);
Some(
MouseEventHandler::new::<Self, _, _>(option as usize, cx, |state, cx| {
let style = &cx
.global::<Settings>()
.theme
.search
.option_button
.style_for(state, is_active);
Label::new(icon.to_string(), style.text.clone())
.contained()
.with_style(style.container)
.boxed()
})
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_any_action(option.to_toggle_action())
})
.with_cursor_style(CursorStyle::PointingHand)
.with_tooltip::<Self, _>(
option as usize,
format!("Toggle {}", option.label()),
Some(option.to_toggle_action()),
tooltip_style,
cx,
)
.boxed(),
)
}
fn render_nav_button(
&self,
icon: &str,
direction: Direction,
cx: &mut RenderContext<Self>,
) -> ElementBox {
let action: Box<dyn Action>;
let tooltip;
match direction {
Direction::Prev => {
action = Box::new(SelectPrevMatch);
tooltip = "Select Previous Match";
}
Direction::Next => {
action = Box::new(SelectNextMatch);
tooltip = "Select Next Match";
}
};
let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
enum NavButton {}
MouseEventHandler::new::<NavButton, _, _>(direction as usize, cx, |state, cx| {
let style = &cx
.global::<Settings>()
.theme
.search
.option_button
.style_for(state, false);
Label::new(icon.to_string(), style.text.clone())
.contained()
.with_style(style.container)
.boxed()
})
.on_click(MouseButton::Left, {
let action = action.boxed_clone();
move |_, cx| cx.dispatch_any_action(action.boxed_clone())
})
.with_cursor_style(CursorStyle::PointingHand)
.with_tooltip::<NavButton, _>(
direction as usize,
tooltip.to_string(),
Some(action),
tooltip_style,
cx,
)
.boxed()
}
fn deploy(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext<Pane>) {
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
if search_bar.update(cx, |search_bar, cx| search_bar.show(action.focus, true, cx)) {
return;
}
}
cx.propagate_action();
}
fn handle_editor_cancel(pane: &mut Pane, _: &editor::Cancel, cx: &mut ViewContext<Pane>) {
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
if !search_bar.read(cx).dismissed {
search_bar.update(cx, |search_bar, cx| search_bar.dismiss(&Dismiss, cx));
return;
}
}
cx.propagate_action();
}
fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext<Self>) {
if let Some(active_editor) = self.active_searchable_item.as_ref() {
cx.focus(active_editor);
}
}
fn is_search_option_enabled(&self, search_option: SearchOption) -> bool {
match search_option {
SearchOption::WholeWord => self.whole_word,
SearchOption::CaseSensitive => self.case_sensitive,
SearchOption::Regex => self.regex,
}
}
fn toggle_search_option(&mut self, search_option: SearchOption, cx: &mut ViewContext<Self>) {
let value = match search_option {
SearchOption::WholeWord => &mut self.whole_word,
SearchOption::CaseSensitive => &mut self.case_sensitive,
SearchOption::Regex => &mut self.regex,
};
*value = !*value;
self.update_matches(false, cx);
cx.notify();
}
fn select_next_match(&mut self, _: &SelectNextMatch, cx: &mut ViewContext<Self>) {
self.select_match(Direction::Next, cx);
}
fn select_prev_match(&mut self, _: &SelectPrevMatch, cx: &mut ViewContext<Self>) {
self.select_match(Direction::Prev, cx);
}
fn select_match(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
if let Some(index) = self.active_match_index {
if let Some(searchable_item) = self.active_searchable_item.as_ref() {
if let Some(matches) = self
.seachable_items_with_matches
.get(&searchable_item.downgrade())
{
let new_match_index =
searchable_item.match_index_for_direction(matches, index, direction, cx);
searchable_item.update_matches(matches, cx);
searchable_item.activate_match(new_match_index, matches, cx);
}
}
}
}
fn select_next_match_on_pane(
pane: &mut Pane,
action: &SelectNextMatch,
cx: &mut ViewContext<Pane>,
) {
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
search_bar.update(cx, |bar, cx| bar.select_next_match(action, cx));
}
}
fn select_prev_match_on_pane(
pane: &mut Pane,
action: &SelectPrevMatch,
cx: &mut ViewContext<Pane>,
) {
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
search_bar.update(cx, |bar, cx| bar.select_prev_match(action, cx));
}
}
fn on_query_editor_event(
&mut self,
_: ViewHandle<Editor>,
event: &editor::Event,
cx: &mut ViewContext<Self>,
) {
if let editor::Event::BufferEdited { .. } = event {
self.query_contains_error = false;
self.clear_matches(cx);
self.update_matches(true, cx);
cx.notify();
}
}
fn on_active_searchable_item_event(&mut self, event: SearchEvent, cx: &mut ViewContext<Self>) {
match event {
SearchEvent::MatchesInvalidated => self.update_matches(false, cx),
SearchEvent::ActiveMatchChanged => self.update_match_index(cx),
}
}
fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
let mut active_item_matches = None;
for (searchable_item, matches) in self.seachable_items_with_matches.drain() {
if let Some(searchable_item) =
WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
{
if Some(&searchable_item) == self.active_searchable_item.as_ref() {
active_item_matches = Some((searchable_item.downgrade(), matches));
} else {
searchable_item.clear_matches(cx);
}
}
}
self.seachable_items_with_matches
.extend(active_item_matches);
}
fn update_matches(&mut self, select_closest_match: bool, cx: &mut ViewContext<Self>) {
let query = self.query_editor.read(cx).text(cx);
self.pending_search.take();
if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
if query.is_empty() {
self.active_match_index.take();
active_searchable_item.clear_matches(cx);
} else {
let query = if self.regex {
match SearchQuery::regex(query, self.whole_word, self.case_sensitive) {
Ok(query) => query,
Err(_) => {
self.query_contains_error = true;
cx.notify();
return;
}
}
} else {
SearchQuery::text(query, self.whole_word, self.case_sensitive)
};
let matches = active_searchable_item.find_matches(query, cx);
let active_searchable_item = active_searchable_item.downgrade();
self.pending_search = Some(cx.spawn_weak(|this, mut cx| async move {
let matches = matches.await;
if let Some(this) = this.upgrade(&cx) {
this.update(&mut cx, |this, cx| {
if let Some(active_searchable_item) = WeakSearchableItemHandle::upgrade(
active_searchable_item.as_ref(),
cx,
) {
this.seachable_items_with_matches
.insert(active_searchable_item.downgrade(), matches);
this.update_match_index(cx);
if !this.dismissed {
let matches = this
.seachable_items_with_matches
.get(&active_searchable_item.downgrade())
.unwrap();
active_searchable_item.update_matches(matches, cx);
if select_closest_match {
if let Some(match_ix) = this.active_match_index {
active_searchable_item
.activate_match(match_ix, matches, cx);
}
}
}
cx.notify();
}
});
}
}));
}
}
}
fn update_match_index(&mut self, cx: &mut ViewContext<Self>) {
let new_index = self
.active_searchable_item
.as_ref()
.and_then(|searchable_item| {
let matches = self
.seachable_items_with_matches
.get(&searchable_item.downgrade())?;
searchable_item.active_match_index(matches, cx)
});
if new_index != self.active_match_index {
self.active_match_index = new_index;
cx.notify();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use editor::{DisplayPoint, Editor};
use gpui::{color::Color, test::EmptyView, TestAppContext};
use language::Buffer;
use std::sync::Arc;
use unindent::Unindent as _;
#[gpui::test]
async fn test_search_simple(cx: &mut TestAppContext) {
let fonts = cx.font_cache();
let mut theme = gpui::fonts::with_font_cache(fonts.clone(), theme::Theme::default);
theme.search.match_background = Color::red();
cx.update(|cx| {
let mut settings = Settings::test(cx);
settings.theme = Arc::new(theme);
cx.set_global(settings)
});
let buffer = cx.add_model(|cx| {
Buffer::new(
0,
r#"
A regular expression (shortened as regex or regexp;[1] also referred to as
rational expression[2][3]) is a sequence of characters that specifies a search
pattern in text. Usually such patterns are used by string-searching algorithms
for "find" or "find and replace" operations on strings, or for input validation.
"#
.unindent(),
cx,
)
});
let (_, root_view) = cx.add_window(|_| EmptyView);
let editor = cx.add_view(&root_view, |cx| {
Editor::for_buffer(buffer.clone(), None, cx)
});
let search_bar = cx.add_view(&root_view, |cx| {
let mut search_bar = BufferSearchBar::new(cx);
search_bar.set_active_pane_item(Some(&editor), cx);
search_bar.show(false, true, cx);
search_bar
});
// Search for a string that appears with different casing.
// By default, search is case-insensitive.
search_bar.update(cx, |search_bar, cx| {
search_bar.set_query("us", cx);
});
editor.next_notification(cx).await;
editor.update(cx, |editor, cx| {
assert_eq!(
editor.all_background_highlights(cx),
&[
(
DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19),
Color::red(),
),
(
DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
Color::red(),
),
]
);
});
// Switch to a case sensitive search.
search_bar.update(cx, |search_bar, cx| {
search_bar.toggle_search_option(SearchOption::CaseSensitive, cx);
});
editor.next_notification(cx).await;
editor.update(cx, |editor, cx| {
assert_eq!(
editor.all_background_highlights(cx),
&[(
DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
Color::red(),
)]
);
});
// Search for a string that appears both as a whole word and
// within other words. By default, all results are found.
search_bar.update(cx, |search_bar, cx| {
search_bar.set_query("or", cx);
});
editor.next_notification(cx).await;
editor.update(cx, |editor, cx| {
assert_eq!(
editor.all_background_highlights(cx),
&[
(
DisplayPoint::new(0, 24)..DisplayPoint::new(0, 26),
Color::red(),
),
(
DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
Color::red(),
),
(
DisplayPoint::new(2, 71)..DisplayPoint::new(2, 73),
Color::red(),
),
(
DisplayPoint::new(3, 1)..DisplayPoint::new(3, 3),
Color::red(),
),
(
DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
Color::red(),
),
(
DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
Color::red(),
),
(
DisplayPoint::new(3, 60)..DisplayPoint::new(3, 62),
Color::red(),
),
]
);
});
// Switch to a whole word search.
search_bar.update(cx, |search_bar, cx| {
search_bar.toggle_search_option(SearchOption::WholeWord, cx);
});
editor.next_notification(cx).await;
editor.update(cx, |editor, cx| {
assert_eq!(
editor.all_background_highlights(cx),
&[
(
DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
Color::red(),
),
(
DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
Color::red(),
),
(
DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
Color::red(),
),
]
);
});
editor.update(cx, |editor, cx| {
editor.change_selections(None, cx, |s| {
s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
});
});
search_bar.update(cx, |search_bar, cx| {
assert_eq!(search_bar.active_match_index, Some(0));
search_bar.select_next_match(&SelectNextMatch, cx);
assert_eq!(
editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
[DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
);
});
search_bar.read_with(cx, |search_bar, _| {
assert_eq!(search_bar.active_match_index, Some(0));
});
search_bar.update(cx, |search_bar, cx| {
search_bar.select_next_match(&SelectNextMatch, cx);
assert_eq!(
editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
[DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
);
});
search_bar.read_with(cx, |search_bar, _| {
assert_eq!(search_bar.active_match_index, Some(1));
});
search_bar.update(cx, |search_bar, cx| {
search_bar.select_next_match(&SelectNextMatch, cx);
assert_eq!(
editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
[DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
);
});
search_bar.read_with(cx, |search_bar, _| {
assert_eq!(search_bar.active_match_index, Some(2));
});
search_bar.update(cx, |search_bar, cx| {
search_bar.select_next_match(&SelectNextMatch, cx);
assert_eq!(
editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
[DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
);
});
search_bar.read_with(cx, |search_bar, _| {
assert_eq!(search_bar.active_match_index, Some(0));
});
search_bar.update(cx, |search_bar, cx| {
search_bar.select_prev_match(&SelectPrevMatch, cx);
assert_eq!(
editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
[DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
);
});
search_bar.read_with(cx, |search_bar, _| {
assert_eq!(search_bar.active_match_index, Some(2));
});
search_bar.update(cx, |search_bar, cx| {
search_bar.select_prev_match(&SelectPrevMatch, cx);
assert_eq!(
editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
[DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
);
});
search_bar.read_with(cx, |search_bar, _| {
assert_eq!(search_bar.active_match_index, Some(1));
});
search_bar.update(cx, |search_bar, cx| {
search_bar.select_prev_match(&SelectPrevMatch, cx);
assert_eq!(
editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
[DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
);
});
search_bar.read_with(cx, |search_bar, _| {
assert_eq!(search_bar.active_match_index, Some(0));
});
// Park the cursor in between matches and ensure that going to the previous match selects
// the closest match to the left.
editor.update(cx, |editor, cx| {
editor.change_selections(None, cx, |s| {
s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
});
});
search_bar.update(cx, |search_bar, cx| {
assert_eq!(search_bar.active_match_index, Some(1));
search_bar.select_prev_match(&SelectPrevMatch, cx);
assert_eq!(
editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
[DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
);
});
search_bar.read_with(cx, |search_bar, _| {
assert_eq!(search_bar.active_match_index, Some(0));
});
// Park the cursor in between matches and ensure that going to the next match selects the
// closest match to the right.
editor.update(cx, |editor, cx| {
editor.change_selections(None, cx, |s| {
s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
});
});
search_bar.update(cx, |search_bar, cx| {
assert_eq!(search_bar.active_match_index, Some(1));
search_bar.select_next_match(&SelectNextMatch, cx);
assert_eq!(
editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
[DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
);
});
search_bar.read_with(cx, |search_bar, _| {
assert_eq!(search_bar.active_match_index, Some(1));
});
// Park the cursor after the last match and ensure that going to the previous match selects
// the last match.
editor.update(cx, |editor, cx| {
editor.change_selections(None, cx, |s| {
s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
});
});
search_bar.update(cx, |search_bar, cx| {
assert_eq!(search_bar.active_match_index, Some(2));
search_bar.select_prev_match(&SelectPrevMatch, cx);
assert_eq!(
editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
[DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
);
});
search_bar.read_with(cx, |search_bar, _| {
assert_eq!(search_bar.active_match_index, Some(2));
});
// Park the cursor after the last match and ensure that going to the next match selects the
// first match.
editor.update(cx, |editor, cx| {
editor.change_selections(None, cx, |s| {
s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
});
});
search_bar.update(cx, |search_bar, cx| {
assert_eq!(search_bar.active_match_index, Some(2));
search_bar.select_next_match(&SelectNextMatch, cx);
assert_eq!(
editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
[DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
);
});
search_bar.read_with(cx, |search_bar, _| {
assert_eq!(search_bar.active_match_index, Some(0));
});
// Park the cursor before the first match and ensure that going to the previous match
// selects the last match.
editor.update(cx, |editor, cx| {
editor.change_selections(None, cx, |s| {
s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
});
});
search_bar.update(cx, |search_bar, cx| {
assert_eq!(search_bar.active_match_index, Some(0));
search_bar.select_prev_match(&SelectPrevMatch, cx);
assert_eq!(
editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
[DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
);
});
search_bar.read_with(cx, |search_bar, _| {
assert_eq!(search_bar.active_match_index, Some(2));
});
}
}