1use crate::{
2 SearchOption, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleRegex,
3 ToggleWholeWord,
4};
5use collections::HashMap;
6use editor::Editor;
7use gpui::{
8 actions,
9 elements::*,
10 impl_actions,
11 platform::{CursorStyle, MouseButton},
12 Action, AnyViewHandle, AppContext, Entity, Subscription, Task, View, ViewContext, ViewHandle,
13};
14use project::search::SearchQuery;
15use serde::Deserialize;
16use std::{any::Any, sync::Arc};
17use util::ResultExt;
18use workspace::{
19 item::ItemHandle,
20 searchable::{Direction, SearchEvent, SearchableItemHandle, WeakSearchableItemHandle},
21 Pane, ToolbarItemLocation, ToolbarItemView,
22};
23
24#[derive(Clone, Deserialize, PartialEq)]
25pub struct Deploy {
26 pub focus: bool,
27}
28
29actions!(buffer_search, [Dismiss, FocusEditor]);
30impl_actions!(buffer_search, [Deploy]);
31
32pub enum Event {
33 UpdateLocation,
34}
35
36pub fn init(cx: &mut AppContext) {
37 cx.add_action(BufferSearchBar::deploy);
38 cx.add_action(BufferSearchBar::dismiss);
39 cx.add_action(BufferSearchBar::focus_editor);
40 cx.add_action(BufferSearchBar::select_next_match);
41 cx.add_action(BufferSearchBar::select_prev_match);
42 cx.add_action(BufferSearchBar::select_next_match_on_pane);
43 cx.add_action(BufferSearchBar::select_prev_match_on_pane);
44 cx.add_action(BufferSearchBar::handle_editor_cancel);
45 add_toggle_option_action::<ToggleCaseSensitive>(SearchOption::CaseSensitive, cx);
46 add_toggle_option_action::<ToggleWholeWord>(SearchOption::WholeWord, cx);
47 add_toggle_option_action::<ToggleRegex>(SearchOption::Regex, cx);
48}
49
50fn add_toggle_option_action<A: Action>(option: SearchOption, cx: &mut AppContext) {
51 cx.add_action(move |pane: &mut Pane, _: &A, cx: &mut ViewContext<Pane>| {
52 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
53 if search_bar.update(cx, |search_bar, cx| search_bar.show(false, false, cx)) {
54 search_bar.update(cx, |search_bar, cx| {
55 search_bar.toggle_search_option(option, cx);
56 });
57 return;
58 }
59 }
60 cx.propagate_action();
61 });
62}
63
64pub struct BufferSearchBar {
65 pub query_editor: ViewHandle<Editor>,
66 active_searchable_item: Option<Box<dyn SearchableItemHandle>>,
67 active_match_index: Option<usize>,
68 active_searchable_item_subscription: Option<Subscription>,
69 seachable_items_with_matches:
70 HashMap<Box<dyn WeakSearchableItemHandle>, Vec<Box<dyn Any + Send>>>,
71 pending_search: Option<Task<()>>,
72 case_sensitive: bool,
73 whole_word: bool,
74 regex: bool,
75 query_contains_error: bool,
76 dismissed: bool,
77}
78
79impl Entity for BufferSearchBar {
80 type Event = Event;
81}
82
83impl View for BufferSearchBar {
84 fn ui_name() -> &'static str {
85 "BufferSearchBar"
86 }
87
88 fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
89 if cx.is_self_focused() {
90 cx.focus(&self.query_editor);
91 }
92 }
93
94 fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
95 let theme = theme::current(cx).clone();
96 let editor_container = if self.query_contains_error {
97 theme.search.invalid_editor
98 } else {
99 theme.search.editor.input.container
100 };
101 let supported_options = self
102 .active_searchable_item
103 .as_ref()
104 .map(|active_searchable_item| active_searchable_item.supported_options())
105 .unwrap_or_default();
106
107 Flex::row()
108 .with_child(
109 Flex::row()
110 .with_child(
111 Flex::row()
112 .with_child(
113 ChildView::new(&self.query_editor, cx)
114 .aligned()
115 .left()
116 .flex(1., true),
117 )
118 .with_children(self.active_searchable_item.as_ref().and_then(
119 |searchable_item| {
120 let matches = self
121 .seachable_items_with_matches
122 .get(&searchable_item.downgrade())?;
123 let message = if let Some(match_ix) = self.active_match_index {
124 format!("{}/{}", match_ix + 1, matches.len())
125 } else {
126 "No matches".to_string()
127 };
128
129 Some(
130 Label::new(message, theme.search.match_index.text.clone())
131 .contained()
132 .with_style(theme.search.match_index.container)
133 .aligned(),
134 )
135 },
136 ))
137 .contained()
138 .with_style(editor_container)
139 .aligned()
140 .constrained()
141 .with_min_width(theme.search.editor.min_width)
142 .with_max_width(theme.search.editor.max_width)
143 .flex(1., false),
144 )
145 .with_child(
146 Flex::row()
147 .with_child(self.render_nav_button("<", Direction::Prev, cx))
148 .with_child(self.render_nav_button(">", Direction::Next, cx))
149 .aligned(),
150 )
151 .with_child(
152 Flex::row()
153 .with_children(self.render_search_option(
154 supported_options.case,
155 "Case",
156 SearchOption::CaseSensitive,
157 cx,
158 ))
159 .with_children(self.render_search_option(
160 supported_options.word,
161 "Word",
162 SearchOption::WholeWord,
163 cx,
164 ))
165 .with_children(self.render_search_option(
166 supported_options.regex,
167 "Regex",
168 SearchOption::Regex,
169 cx,
170 ))
171 .contained()
172 .with_style(theme.search.option_button_group)
173 .aligned(),
174 )
175 .flex(1., true),
176 )
177 .with_child(self.render_close_button(&theme.search, cx))
178 .contained()
179 .with_style(theme.search.container)
180 .into_any_named("search bar")
181 }
182}
183
184impl ToolbarItemView for BufferSearchBar {
185 fn set_active_pane_item(
186 &mut self,
187 item: Option<&dyn ItemHandle>,
188 cx: &mut ViewContext<Self>,
189 ) -> ToolbarItemLocation {
190 cx.notify();
191 self.active_searchable_item_subscription.take();
192 self.active_searchable_item.take();
193 self.pending_search.take();
194
195 if let Some(searchable_item_handle) =
196 item.and_then(|item| item.to_searchable_item_handle(cx))
197 {
198 let this = cx.weak_handle();
199 self.active_searchable_item_subscription =
200 Some(searchable_item_handle.subscribe_to_search_events(
201 cx,
202 Box::new(move |search_event, cx| {
203 if let Some(this) = this.upgrade(cx) {
204 this.update(cx, |this, cx| {
205 this.on_active_searchable_item_event(search_event, cx)
206 });
207 }
208 }),
209 ));
210
211 self.active_searchable_item = Some(searchable_item_handle);
212 self.update_matches(false, cx);
213 if !self.dismissed {
214 return ToolbarItemLocation::Secondary;
215 }
216 }
217
218 ToolbarItemLocation::Hidden
219 }
220
221 fn location_for_event(
222 &self,
223 _: &Self::Event,
224 _: ToolbarItemLocation,
225 _: &AppContext,
226 ) -> ToolbarItemLocation {
227 if self.active_searchable_item.is_some() && !self.dismissed {
228 ToolbarItemLocation::Secondary
229 } else {
230 ToolbarItemLocation::Hidden
231 }
232 }
233}
234
235impl BufferSearchBar {
236 pub fn new(cx: &mut ViewContext<Self>) -> Self {
237 let query_editor = cx.add_view(|cx| {
238 Editor::auto_height(
239 2,
240 Some(Arc::new(|theme| theme.search.editor.input.clone())),
241 cx,
242 )
243 });
244 cx.subscribe(&query_editor, Self::on_query_editor_event)
245 .detach();
246
247 Self {
248 query_editor,
249 active_searchable_item: None,
250 active_searchable_item_subscription: None,
251 active_match_index: None,
252 seachable_items_with_matches: Default::default(),
253 case_sensitive: false,
254 whole_word: false,
255 regex: false,
256 pending_search: None,
257 query_contains_error: false,
258 dismissed: true,
259 }
260 }
261
262 pub fn is_dismissed(&self) -> bool {
263 self.dismissed
264 }
265
266 pub fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext<Self>) {
267 self.dismissed = true;
268 for searchable_item in self.seachable_items_with_matches.keys() {
269 if let Some(searchable_item) =
270 WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
271 {
272 searchable_item.clear_matches(cx);
273 }
274 }
275 if let Some(active_editor) = self.active_searchable_item.as_ref() {
276 cx.focus(active_editor.as_any());
277 }
278 cx.emit(Event::UpdateLocation);
279 cx.notify();
280 }
281
282 pub fn show(&mut self, focus: bool, suggest_query: bool, cx: &mut ViewContext<Self>) -> bool {
283 let searchable_item = if let Some(searchable_item) = &self.active_searchable_item {
284 SearchableItemHandle::boxed_clone(searchable_item.as_ref())
285 } else {
286 return false;
287 };
288
289 if suggest_query {
290 let text = searchable_item.query_suggestion(cx);
291 if !text.is_empty() {
292 self.set_query(&text, cx);
293 }
294 }
295
296 if focus {
297 let query_editor = self.query_editor.clone();
298 query_editor.update(cx, |query_editor, cx| {
299 query_editor.select_all(&editor::SelectAll, cx);
300 });
301 cx.focus_self();
302 }
303
304 self.dismissed = false;
305 cx.notify();
306 cx.emit(Event::UpdateLocation);
307 true
308 }
309
310 fn set_query(&mut self, query: &str, cx: &mut ViewContext<Self>) {
311 self.query_editor.update(cx, |query_editor, cx| {
312 query_editor.buffer().update(cx, |query_buffer, cx| {
313 let len = query_buffer.len(cx);
314 query_buffer.edit([(0..len, query)], None, cx);
315 });
316 });
317 }
318
319 fn render_search_option(
320 &self,
321 option_supported: bool,
322 icon: &'static str,
323 option: SearchOption,
324 cx: &mut ViewContext<Self>,
325 ) -> Option<AnyElement<Self>> {
326 if !option_supported {
327 return None;
328 }
329
330 let tooltip_style = theme::current(cx).tooltip.clone();
331 let is_active = self.is_search_option_enabled(option);
332 Some(
333 MouseEventHandler::<Self, _>::new(option as usize, cx, |state, cx| {
334 let theme = theme::current(cx);
335 let style = theme
336 .search
337 .option_button
338 .in_state(is_active)
339 .style_for(state);
340 Label::new(icon, style.text.clone())
341 .contained()
342 .with_style(style.container)
343 })
344 .on_click(MouseButton::Left, move |_, this, cx| {
345 this.toggle_search_option(option, cx);
346 })
347 .with_cursor_style(CursorStyle::PointingHand)
348 .with_tooltip::<Self>(
349 option as usize,
350 format!("Toggle {}", option.label()),
351 Some(option.to_toggle_action()),
352 tooltip_style,
353 cx,
354 )
355 .into_any(),
356 )
357 }
358
359 fn render_nav_button(
360 &self,
361 icon: &'static str,
362 direction: Direction,
363 cx: &mut ViewContext<Self>,
364 ) -> AnyElement<Self> {
365 let action: Box<dyn Action>;
366 let tooltip;
367 match direction {
368 Direction::Prev => {
369 action = Box::new(SelectPrevMatch);
370 tooltip = "Select Previous Match";
371 }
372 Direction::Next => {
373 action = Box::new(SelectNextMatch);
374 tooltip = "Select Next Match";
375 }
376 };
377 let tooltip_style = theme::current(cx).tooltip.clone();
378
379 enum NavButton {}
380 MouseEventHandler::<NavButton, _>::new(direction as usize, cx, |state, cx| {
381 let theme = theme::current(cx);
382 let style = theme.search.option_button.inactive_state().style_for(state);
383 Label::new(icon, style.text.clone())
384 .contained()
385 .with_style(style.container)
386 })
387 .on_click(MouseButton::Left, {
388 move |_, this, cx| match direction {
389 Direction::Prev => this.select_prev_match(&Default::default(), cx),
390 Direction::Next => this.select_next_match(&Default::default(), cx),
391 }
392 })
393 .with_cursor_style(CursorStyle::PointingHand)
394 .with_tooltip::<NavButton>(
395 direction as usize,
396 tooltip.to_string(),
397 Some(action),
398 tooltip_style,
399 cx,
400 )
401 .into_any()
402 }
403
404 fn render_close_button(
405 &self,
406 theme: &theme::Search,
407 cx: &mut ViewContext<Self>,
408 ) -> AnyElement<Self> {
409 let tooltip = "Dismiss Buffer Search";
410 let tooltip_style = theme::current(cx).tooltip.clone();
411
412 enum CloseButton {}
413 MouseEventHandler::<CloseButton, _>::new(0, cx, |state, _| {
414 let style = theme.dismiss_button.style_for(state);
415 Svg::new("icons/x_mark_8.svg")
416 .with_color(style.color)
417 .constrained()
418 .with_width(style.icon_width)
419 .aligned()
420 .constrained()
421 .with_width(style.button_width)
422 .contained()
423 .with_style(style.container)
424 })
425 .on_click(MouseButton::Left, move |_, this, cx| {
426 this.dismiss(&Default::default(), cx)
427 })
428 .with_cursor_style(CursorStyle::PointingHand)
429 .with_tooltip::<CloseButton>(
430 0,
431 tooltip.to_string(),
432 Some(Box::new(Dismiss)),
433 tooltip_style,
434 cx,
435 )
436 .into_any()
437 }
438
439 fn deploy(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext<Pane>) {
440 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
441 if search_bar.update(cx, |search_bar, cx| search_bar.show(action.focus, true, cx)) {
442 return;
443 }
444 }
445 cx.propagate_action();
446 }
447
448 fn handle_editor_cancel(pane: &mut Pane, _: &editor::Cancel, cx: &mut ViewContext<Pane>) {
449 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
450 if !search_bar.read(cx).dismissed {
451 search_bar.update(cx, |search_bar, cx| search_bar.dismiss(&Dismiss, cx));
452 return;
453 }
454 }
455 cx.propagate_action();
456 }
457
458 fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext<Self>) {
459 if let Some(active_editor) = self.active_searchable_item.as_ref() {
460 cx.focus(active_editor.as_any());
461 }
462 }
463
464 fn is_search_option_enabled(&self, search_option: SearchOption) -> bool {
465 match search_option {
466 SearchOption::WholeWord => self.whole_word,
467 SearchOption::CaseSensitive => self.case_sensitive,
468 SearchOption::Regex => self.regex,
469 }
470 }
471
472 fn toggle_search_option(&mut self, search_option: SearchOption, cx: &mut ViewContext<Self>) {
473 let value = match search_option {
474 SearchOption::WholeWord => &mut self.whole_word,
475 SearchOption::CaseSensitive => &mut self.case_sensitive,
476 SearchOption::Regex => &mut self.regex,
477 };
478 *value = !*value;
479 self.update_matches(false, cx);
480 cx.notify();
481 }
482
483 fn select_next_match(&mut self, _: &SelectNextMatch, cx: &mut ViewContext<Self>) {
484 self.select_match(Direction::Next, cx);
485 }
486
487 fn select_prev_match(&mut self, _: &SelectPrevMatch, cx: &mut ViewContext<Self>) {
488 self.select_match(Direction::Prev, cx);
489 }
490
491 pub fn select_match(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
492 if let Some(index) = self.active_match_index {
493 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
494 if let Some(matches) = self
495 .seachable_items_with_matches
496 .get(&searchable_item.downgrade())
497 {
498 let new_match_index =
499 searchable_item.match_index_for_direction(matches, index, direction, cx);
500 searchable_item.update_matches(matches, cx);
501 searchable_item.activate_match(new_match_index, matches, cx);
502 }
503 }
504 }
505 }
506
507 fn select_next_match_on_pane(
508 pane: &mut Pane,
509 action: &SelectNextMatch,
510 cx: &mut ViewContext<Pane>,
511 ) {
512 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
513 search_bar.update(cx, |bar, cx| bar.select_next_match(action, cx));
514 }
515 }
516
517 fn select_prev_match_on_pane(
518 pane: &mut Pane,
519 action: &SelectPrevMatch,
520 cx: &mut ViewContext<Pane>,
521 ) {
522 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
523 search_bar.update(cx, |bar, cx| bar.select_prev_match(action, cx));
524 }
525 }
526
527 fn on_query_editor_event(
528 &mut self,
529 _: ViewHandle<Editor>,
530 event: &editor::Event,
531 cx: &mut ViewContext<Self>,
532 ) {
533 if let editor::Event::BufferEdited { .. } = event {
534 self.query_contains_error = false;
535 self.clear_matches(cx);
536 self.update_matches(true, cx);
537 cx.notify();
538 }
539 }
540
541 fn on_active_searchable_item_event(&mut self, event: SearchEvent, cx: &mut ViewContext<Self>) {
542 match event {
543 SearchEvent::MatchesInvalidated => self.update_matches(false, cx),
544 SearchEvent::ActiveMatchChanged => self.update_match_index(cx),
545 }
546 }
547
548 fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
549 let mut active_item_matches = None;
550 for (searchable_item, matches) in self.seachable_items_with_matches.drain() {
551 if let Some(searchable_item) =
552 WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
553 {
554 if Some(&searchable_item) == self.active_searchable_item.as_ref() {
555 active_item_matches = Some((searchable_item.downgrade(), matches));
556 } else {
557 searchable_item.clear_matches(cx);
558 }
559 }
560 }
561
562 self.seachable_items_with_matches
563 .extend(active_item_matches);
564 }
565
566 fn update_matches(&mut self, select_closest_match: bool, cx: &mut ViewContext<Self>) {
567 let query = self.query_editor.read(cx).text(cx);
568 self.pending_search.take();
569 if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
570 if query.is_empty() {
571 self.active_match_index.take();
572 active_searchable_item.clear_matches(cx);
573 } else {
574 let query = if self.regex {
575 match SearchQuery::regex(
576 query,
577 self.whole_word,
578 self.case_sensitive,
579 Vec::new(),
580 Vec::new(),
581 ) {
582 Ok(query) => query,
583 Err(_) => {
584 self.query_contains_error = true;
585 cx.notify();
586 return;
587 }
588 }
589 } else {
590 SearchQuery::text(
591 query,
592 self.whole_word,
593 self.case_sensitive,
594 Vec::new(),
595 Vec::new(),
596 )
597 };
598
599 let matches = active_searchable_item.find_matches(query, cx);
600
601 let active_searchable_item = active_searchable_item.downgrade();
602 self.pending_search = Some(cx.spawn(|this, mut cx| async move {
603 let matches = matches.await;
604 this.update(&mut cx, |this, cx| {
605 if let Some(active_searchable_item) =
606 WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx)
607 {
608 this.seachable_items_with_matches
609 .insert(active_searchable_item.downgrade(), matches);
610
611 this.update_match_index(cx);
612 if !this.dismissed {
613 let matches = this
614 .seachable_items_with_matches
615 .get(&active_searchable_item.downgrade())
616 .unwrap();
617 active_searchable_item.update_matches(matches, cx);
618 if select_closest_match {
619 if let Some(match_ix) = this.active_match_index {
620 active_searchable_item
621 .activate_match(match_ix, matches, cx);
622 }
623 }
624 }
625 cx.notify();
626 }
627 })
628 .log_err();
629 }));
630 }
631 }
632 }
633
634 fn update_match_index(&mut self, cx: &mut ViewContext<Self>) {
635 let new_index = self
636 .active_searchable_item
637 .as_ref()
638 .and_then(|searchable_item| {
639 let matches = self
640 .seachable_items_with_matches
641 .get(&searchable_item.downgrade())?;
642 searchable_item.active_match_index(matches, cx)
643 });
644 if new_index != self.active_match_index {
645 self.active_match_index = new_index;
646 cx.notify();
647 }
648 }
649}
650
651#[cfg(test)]
652mod tests {
653 use super::*;
654 use editor::{DisplayPoint, Editor};
655 use gpui::{color::Color, test::EmptyView, TestAppContext};
656 use language::Buffer;
657 use unindent::Unindent as _;
658
659 #[gpui::test]
660 async fn test_search_simple(cx: &mut TestAppContext) {
661 crate::project_search::tests::init_test(cx);
662
663 let buffer = cx.add_model(|cx| {
664 Buffer::new(
665 0,
666 r#"
667 A regular expression (shortened as regex or regexp;[1] also referred to as
668 rational expression[2][3]) is a sequence of characters that specifies a search
669 pattern in text. Usually such patterns are used by string-searching algorithms
670 for "find" or "find and replace" operations on strings, or for input validation.
671 "#
672 .unindent(),
673 cx,
674 )
675 });
676 let (window_id, _root_view) = cx.add_window(|_| EmptyView);
677
678 let editor = cx.add_view(window_id, |cx| Editor::for_buffer(buffer.clone(), None, cx));
679
680 let search_bar = cx.add_view(window_id, |cx| {
681 let mut search_bar = BufferSearchBar::new(cx);
682 search_bar.set_active_pane_item(Some(&editor), cx);
683 search_bar.show(false, true, cx);
684 search_bar
685 });
686
687 // Search for a string that appears with different casing.
688 // By default, search is case-insensitive.
689 search_bar.update(cx, |search_bar, cx| {
690 search_bar.set_query("us", cx);
691 });
692 editor.next_notification(cx).await;
693 editor.update(cx, |editor, cx| {
694 assert_eq!(
695 editor.all_background_highlights(cx),
696 &[
697 (
698 DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19),
699 Color::red(),
700 ),
701 (
702 DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
703 Color::red(),
704 ),
705 ]
706 );
707 });
708
709 // Switch to a case sensitive search.
710 search_bar.update(cx, |search_bar, cx| {
711 search_bar.toggle_search_option(SearchOption::CaseSensitive, cx);
712 });
713 editor.next_notification(cx).await;
714 editor.update(cx, |editor, cx| {
715 assert_eq!(
716 editor.all_background_highlights(cx),
717 &[(
718 DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
719 Color::red(),
720 )]
721 );
722 });
723
724 // Search for a string that appears both as a whole word and
725 // within other words. By default, all results are found.
726 search_bar.update(cx, |search_bar, cx| {
727 search_bar.set_query("or", cx);
728 });
729 editor.next_notification(cx).await;
730 editor.update(cx, |editor, cx| {
731 assert_eq!(
732 editor.all_background_highlights(cx),
733 &[
734 (
735 DisplayPoint::new(0, 24)..DisplayPoint::new(0, 26),
736 Color::red(),
737 ),
738 (
739 DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
740 Color::red(),
741 ),
742 (
743 DisplayPoint::new(2, 71)..DisplayPoint::new(2, 73),
744 Color::red(),
745 ),
746 (
747 DisplayPoint::new(3, 1)..DisplayPoint::new(3, 3),
748 Color::red(),
749 ),
750 (
751 DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
752 Color::red(),
753 ),
754 (
755 DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
756 Color::red(),
757 ),
758 (
759 DisplayPoint::new(3, 60)..DisplayPoint::new(3, 62),
760 Color::red(),
761 ),
762 ]
763 );
764 });
765
766 // Switch to a whole word search.
767 search_bar.update(cx, |search_bar, cx| {
768 search_bar.toggle_search_option(SearchOption::WholeWord, cx);
769 });
770 editor.next_notification(cx).await;
771 editor.update(cx, |editor, cx| {
772 assert_eq!(
773 editor.all_background_highlights(cx),
774 &[
775 (
776 DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
777 Color::red(),
778 ),
779 (
780 DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
781 Color::red(),
782 ),
783 (
784 DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
785 Color::red(),
786 ),
787 ]
788 );
789 });
790
791 editor.update(cx, |editor, cx| {
792 editor.change_selections(None, cx, |s| {
793 s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
794 });
795 });
796 search_bar.update(cx, |search_bar, cx| {
797 assert_eq!(search_bar.active_match_index, Some(0));
798 search_bar.select_next_match(&SelectNextMatch, cx);
799 assert_eq!(
800 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
801 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
802 );
803 });
804 search_bar.read_with(cx, |search_bar, _| {
805 assert_eq!(search_bar.active_match_index, Some(0));
806 });
807
808 search_bar.update(cx, |search_bar, cx| {
809 search_bar.select_next_match(&SelectNextMatch, cx);
810 assert_eq!(
811 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
812 [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
813 );
814 });
815 search_bar.read_with(cx, |search_bar, _| {
816 assert_eq!(search_bar.active_match_index, Some(1));
817 });
818
819 search_bar.update(cx, |search_bar, cx| {
820 search_bar.select_next_match(&SelectNextMatch, cx);
821 assert_eq!(
822 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
823 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
824 );
825 });
826 search_bar.read_with(cx, |search_bar, _| {
827 assert_eq!(search_bar.active_match_index, Some(2));
828 });
829
830 search_bar.update(cx, |search_bar, cx| {
831 search_bar.select_next_match(&SelectNextMatch, cx);
832 assert_eq!(
833 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
834 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
835 );
836 });
837 search_bar.read_with(cx, |search_bar, _| {
838 assert_eq!(search_bar.active_match_index, Some(0));
839 });
840
841 search_bar.update(cx, |search_bar, cx| {
842 search_bar.select_prev_match(&SelectPrevMatch, cx);
843 assert_eq!(
844 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
845 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
846 );
847 });
848 search_bar.read_with(cx, |search_bar, _| {
849 assert_eq!(search_bar.active_match_index, Some(2));
850 });
851
852 search_bar.update(cx, |search_bar, cx| {
853 search_bar.select_prev_match(&SelectPrevMatch, cx);
854 assert_eq!(
855 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
856 [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
857 );
858 });
859 search_bar.read_with(cx, |search_bar, _| {
860 assert_eq!(search_bar.active_match_index, Some(1));
861 });
862
863 search_bar.update(cx, |search_bar, cx| {
864 search_bar.select_prev_match(&SelectPrevMatch, cx);
865 assert_eq!(
866 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
867 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
868 );
869 });
870 search_bar.read_with(cx, |search_bar, _| {
871 assert_eq!(search_bar.active_match_index, Some(0));
872 });
873
874 // Park the cursor in between matches and ensure that going to the previous match selects
875 // the closest match to the left.
876 editor.update(cx, |editor, cx| {
877 editor.change_selections(None, cx, |s| {
878 s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
879 });
880 });
881 search_bar.update(cx, |search_bar, cx| {
882 assert_eq!(search_bar.active_match_index, Some(1));
883 search_bar.select_prev_match(&SelectPrevMatch, cx);
884 assert_eq!(
885 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
886 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
887 );
888 });
889 search_bar.read_with(cx, |search_bar, _| {
890 assert_eq!(search_bar.active_match_index, Some(0));
891 });
892
893 // Park the cursor in between matches and ensure that going to the next match selects the
894 // closest match to the right.
895 editor.update(cx, |editor, cx| {
896 editor.change_selections(None, cx, |s| {
897 s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
898 });
899 });
900 search_bar.update(cx, |search_bar, cx| {
901 assert_eq!(search_bar.active_match_index, Some(1));
902 search_bar.select_next_match(&SelectNextMatch, cx);
903 assert_eq!(
904 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
905 [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
906 );
907 });
908 search_bar.read_with(cx, |search_bar, _| {
909 assert_eq!(search_bar.active_match_index, Some(1));
910 });
911
912 // Park the cursor after the last match and ensure that going to the previous match selects
913 // the last match.
914 editor.update(cx, |editor, cx| {
915 editor.change_selections(None, cx, |s| {
916 s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
917 });
918 });
919 search_bar.update(cx, |search_bar, cx| {
920 assert_eq!(search_bar.active_match_index, Some(2));
921 search_bar.select_prev_match(&SelectPrevMatch, cx);
922 assert_eq!(
923 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
924 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
925 );
926 });
927 search_bar.read_with(cx, |search_bar, _| {
928 assert_eq!(search_bar.active_match_index, Some(2));
929 });
930
931 // Park the cursor after the last match and ensure that going to the next match selects the
932 // first match.
933 editor.update(cx, |editor, cx| {
934 editor.change_selections(None, cx, |s| {
935 s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
936 });
937 });
938 search_bar.update(cx, |search_bar, cx| {
939 assert_eq!(search_bar.active_match_index, Some(2));
940 search_bar.select_next_match(&SelectNextMatch, cx);
941 assert_eq!(
942 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
943 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
944 );
945 });
946 search_bar.read_with(cx, |search_bar, _| {
947 assert_eq!(search_bar.active_match_index, Some(0));
948 });
949
950 // Park the cursor before the first match and ensure that going to the previous match
951 // selects the last match.
952 editor.update(cx, |editor, cx| {
953 editor.change_selections(None, cx, |s| {
954 s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
955 });
956 });
957 search_bar.update(cx, |search_bar, cx| {
958 assert_eq!(search_bar.active_match_index, Some(0));
959 search_bar.select_prev_match(&SelectPrevMatch, cx);
960 assert_eq!(
961 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
962 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
963 );
964 });
965 search_bar.read_with(cx, |search_bar, _| {
966 assert_eq!(search_bar.active_match_index, Some(2));
967 });
968 }
969}