buffer_search.rs

  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    fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext<Self>) {
263        self.dismissed = true;
264        for searchable_item in self.seachable_items_with_matches.keys() {
265            if let Some(searchable_item) =
266                WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
267            {
268                searchable_item.clear_matches(cx);
269            }
270        }
271        if let Some(active_editor) = self.active_searchable_item.as_ref() {
272            cx.focus(active_editor.as_any());
273        }
274        cx.emit(Event::UpdateLocation);
275        cx.notify();
276    }
277
278    fn show(&mut self, focus: bool, suggest_query: bool, cx: &mut ViewContext<Self>) -> bool {
279        let searchable_item = if let Some(searchable_item) = &self.active_searchable_item {
280            SearchableItemHandle::boxed_clone(searchable_item.as_ref())
281        } else {
282            return false;
283        };
284
285        if suggest_query {
286            let text = searchable_item.query_suggestion(cx);
287            if !text.is_empty() {
288                self.set_query(&text, cx);
289            }
290        }
291
292        if focus {
293            let query_editor = self.query_editor.clone();
294            query_editor.update(cx, |query_editor, cx| {
295                query_editor.select_all(&editor::SelectAll, cx);
296            });
297            cx.focus_self();
298        }
299
300        self.dismissed = false;
301        cx.notify();
302        cx.emit(Event::UpdateLocation);
303        true
304    }
305
306    fn set_query(&mut self, query: &str, cx: &mut ViewContext<Self>) {
307        self.query_editor.update(cx, |query_editor, cx| {
308            query_editor.buffer().update(cx, |query_buffer, cx| {
309                let len = query_buffer.len(cx);
310                query_buffer.edit([(0..len, query)], None, cx);
311            });
312        });
313    }
314
315    fn render_search_option(
316        &self,
317        option_supported: bool,
318        icon: &'static str,
319        option: SearchOption,
320        cx: &mut ViewContext<Self>,
321    ) -> Option<AnyElement<Self>> {
322        if !option_supported {
323            return None;
324        }
325
326        let tooltip_style = theme::current(cx).tooltip.clone();
327        let is_active = self.is_search_option_enabled(option);
328        Some(
329            MouseEventHandler::<Self, _>::new(option as usize, cx, |state, cx| {
330                let theme = theme::current(cx);
331                let style = theme
332                    .search
333                    .option_button
334                    .in_state(is_active)
335                    .style_for(state);
336                Label::new(icon, style.text.clone())
337                    .contained()
338                    .with_style(style.container)
339            })
340            .on_click(MouseButton::Left, move |_, this, cx| {
341                this.toggle_search_option(option, cx);
342            })
343            .with_cursor_style(CursorStyle::PointingHand)
344            .with_tooltip::<Self>(
345                option as usize,
346                format!("Toggle {}", option.label()),
347                Some(option.to_toggle_action()),
348                tooltip_style,
349                cx,
350            )
351            .into_any(),
352        )
353    }
354
355    fn render_nav_button(
356        &self,
357        icon: &'static str,
358        direction: Direction,
359        cx: &mut ViewContext<Self>,
360    ) -> AnyElement<Self> {
361        let action: Box<dyn Action>;
362        let tooltip;
363        match direction {
364            Direction::Prev => {
365                action = Box::new(SelectPrevMatch);
366                tooltip = "Select Previous Match";
367            }
368            Direction::Next => {
369                action = Box::new(SelectNextMatch);
370                tooltip = "Select Next Match";
371            }
372        };
373        let tooltip_style = theme::current(cx).tooltip.clone();
374
375        enum NavButton {}
376        MouseEventHandler::<NavButton, _>::new(direction as usize, cx, |state, cx| {
377            let theme = theme::current(cx);
378            let style = theme.search.option_button.inactive_state().style_for(state);
379            Label::new(icon, style.text.clone())
380                .contained()
381                .with_style(style.container)
382        })
383        .on_click(MouseButton::Left, {
384            move |_, this, cx| match direction {
385                Direction::Prev => this.select_prev_match(&Default::default(), cx),
386                Direction::Next => this.select_next_match(&Default::default(), cx),
387            }
388        })
389        .with_cursor_style(CursorStyle::PointingHand)
390        .with_tooltip::<NavButton>(
391            direction as usize,
392            tooltip.to_string(),
393            Some(action),
394            tooltip_style,
395            cx,
396        )
397        .into_any()
398    }
399
400    fn render_close_button(
401        &self,
402        theme: &theme::Search,
403        cx: &mut ViewContext<Self>,
404    ) -> AnyElement<Self> {
405        let tooltip = "Dismiss Buffer Search";
406        let tooltip_style = theme::current(cx).tooltip.clone();
407
408        enum CloseButton {}
409        MouseEventHandler::<CloseButton, _>::new(0, cx, |state, _| {
410            let style = theme.dismiss_button.style_for(state);
411            Svg::new("icons/x_mark_8.svg")
412                .with_color(style.color)
413                .constrained()
414                .with_width(style.icon_width)
415                .aligned()
416                .constrained()
417                .with_width(style.button_width)
418                .contained()
419                .with_style(style.container)
420        })
421        .on_click(MouseButton::Left, move |_, this, cx| {
422            this.dismiss(&Default::default(), cx)
423        })
424        .with_cursor_style(CursorStyle::PointingHand)
425        .with_tooltip::<CloseButton>(
426            0,
427            tooltip.to_string(),
428            Some(Box::new(Dismiss)),
429            tooltip_style,
430            cx,
431        )
432        .into_any()
433    }
434
435    fn deploy(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext<Pane>) {
436        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
437            if search_bar.update(cx, |search_bar, cx| search_bar.show(action.focus, true, cx)) {
438                return;
439            }
440        }
441        cx.propagate_action();
442    }
443
444    fn handle_editor_cancel(pane: &mut Pane, _: &editor::Cancel, cx: &mut ViewContext<Pane>) {
445        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
446            if !search_bar.read(cx).dismissed {
447                search_bar.update(cx, |search_bar, cx| search_bar.dismiss(&Dismiss, cx));
448                return;
449            }
450        }
451        cx.propagate_action();
452    }
453
454    fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext<Self>) {
455        if let Some(active_editor) = self.active_searchable_item.as_ref() {
456            cx.focus(active_editor.as_any());
457        }
458    }
459
460    fn is_search_option_enabled(&self, search_option: SearchOption) -> bool {
461        match search_option {
462            SearchOption::WholeWord => self.whole_word,
463            SearchOption::CaseSensitive => self.case_sensitive,
464            SearchOption::Regex => self.regex,
465        }
466    }
467
468    fn toggle_search_option(&mut self, search_option: SearchOption, cx: &mut ViewContext<Self>) {
469        let value = match search_option {
470            SearchOption::WholeWord => &mut self.whole_word,
471            SearchOption::CaseSensitive => &mut self.case_sensitive,
472            SearchOption::Regex => &mut self.regex,
473        };
474        *value = !*value;
475        self.update_matches(false, cx);
476        cx.notify();
477    }
478
479    fn select_next_match(&mut self, _: &SelectNextMatch, cx: &mut ViewContext<Self>) {
480        self.select_match(Direction::Next, cx);
481    }
482
483    fn select_prev_match(&mut self, _: &SelectPrevMatch, cx: &mut ViewContext<Self>) {
484        self.select_match(Direction::Prev, cx);
485    }
486
487    fn select_match(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
488        if let Some(index) = self.active_match_index {
489            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
490                if let Some(matches) = self
491                    .seachable_items_with_matches
492                    .get(&searchable_item.downgrade())
493                {
494                    let new_match_index =
495                        searchable_item.match_index_for_direction(matches, index, direction, cx);
496                    searchable_item.update_matches(matches, cx);
497                    searchable_item.activate_match(new_match_index, matches, cx);
498                }
499            }
500        }
501    }
502
503    fn select_next_match_on_pane(
504        pane: &mut Pane,
505        action: &SelectNextMatch,
506        cx: &mut ViewContext<Pane>,
507    ) {
508        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
509            search_bar.update(cx, |bar, cx| bar.select_next_match(action, cx));
510        }
511    }
512
513    fn select_prev_match_on_pane(
514        pane: &mut Pane,
515        action: &SelectPrevMatch,
516        cx: &mut ViewContext<Pane>,
517    ) {
518        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
519            search_bar.update(cx, |bar, cx| bar.select_prev_match(action, cx));
520        }
521    }
522
523    fn on_query_editor_event(
524        &mut self,
525        _: ViewHandle<Editor>,
526        event: &editor::Event,
527        cx: &mut ViewContext<Self>,
528    ) {
529        if let editor::Event::BufferEdited { .. } = event {
530            self.query_contains_error = false;
531            self.clear_matches(cx);
532            self.update_matches(true, cx);
533            cx.notify();
534        }
535    }
536
537    fn on_active_searchable_item_event(&mut self, event: SearchEvent, cx: &mut ViewContext<Self>) {
538        match event {
539            SearchEvent::MatchesInvalidated => self.update_matches(false, cx),
540            SearchEvent::ActiveMatchChanged => self.update_match_index(cx),
541        }
542    }
543
544    fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
545        let mut active_item_matches = None;
546        for (searchable_item, matches) in self.seachable_items_with_matches.drain() {
547            if let Some(searchable_item) =
548                WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
549            {
550                if Some(&searchable_item) == self.active_searchable_item.as_ref() {
551                    active_item_matches = Some((searchable_item.downgrade(), matches));
552                } else {
553                    searchable_item.clear_matches(cx);
554                }
555            }
556        }
557
558        self.seachable_items_with_matches
559            .extend(active_item_matches);
560    }
561
562    fn update_matches(&mut self, select_closest_match: bool, cx: &mut ViewContext<Self>) {
563        let query = self.query_editor.read(cx).text(cx);
564        self.pending_search.take();
565        if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
566            if query.is_empty() {
567                self.active_match_index.take();
568                active_searchable_item.clear_matches(cx);
569            } else {
570                let query = if self.regex {
571                    match SearchQuery::regex(
572                        query,
573                        self.whole_word,
574                        self.case_sensitive,
575                        Vec::new(),
576                        Vec::new(),
577                    ) {
578                        Ok(query) => query,
579                        Err(_) => {
580                            self.query_contains_error = true;
581                            cx.notify();
582                            return;
583                        }
584                    }
585                } else {
586                    SearchQuery::text(
587                        query,
588                        self.whole_word,
589                        self.case_sensitive,
590                        Vec::new(),
591                        Vec::new(),
592                    )
593                };
594
595                let matches = active_searchable_item.find_matches(query, cx);
596
597                let active_searchable_item = active_searchable_item.downgrade();
598                self.pending_search = Some(cx.spawn(|this, mut cx| async move {
599                    let matches = matches.await;
600                    this.update(&mut cx, |this, cx| {
601                        if let Some(active_searchable_item) =
602                            WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx)
603                        {
604                            this.seachable_items_with_matches
605                                .insert(active_searchable_item.downgrade(), matches);
606
607                            this.update_match_index(cx);
608                            if !this.dismissed {
609                                let matches = this
610                                    .seachable_items_with_matches
611                                    .get(&active_searchable_item.downgrade())
612                                    .unwrap();
613                                active_searchable_item.update_matches(matches, cx);
614                                if select_closest_match {
615                                    if let Some(match_ix) = this.active_match_index {
616                                        active_searchable_item
617                                            .activate_match(match_ix, matches, cx);
618                                    }
619                                }
620                            }
621                            cx.notify();
622                        }
623                    })
624                    .log_err();
625                }));
626            }
627        }
628    }
629
630    fn update_match_index(&mut self, cx: &mut ViewContext<Self>) {
631        let new_index = self
632            .active_searchable_item
633            .as_ref()
634            .and_then(|searchable_item| {
635                let matches = self
636                    .seachable_items_with_matches
637                    .get(&searchable_item.downgrade())?;
638                searchable_item.active_match_index(matches, cx)
639            });
640        if new_index != self.active_match_index {
641            self.active_match_index = new_index;
642            cx.notify();
643        }
644    }
645}
646
647#[cfg(test)]
648mod tests {
649    use super::*;
650    use editor::{DisplayPoint, Editor};
651    use gpui::{color::Color, test::EmptyView, TestAppContext};
652    use language::Buffer;
653    use unindent::Unindent as _;
654
655    #[gpui::test]
656    async fn test_search_simple(cx: &mut TestAppContext) {
657        crate::project_search::tests::init_test(cx);
658
659        let buffer = cx.add_model(|cx| {
660            Buffer::new(
661                0,
662                r#"
663                A regular expression (shortened as regex or regexp;[1] also referred to as
664                rational expression[2][3]) is a sequence of characters that specifies a search
665                pattern in text. Usually such patterns are used by string-searching algorithms
666                for "find" or "find and replace" operations on strings, or for input validation.
667                "#
668                .unindent(),
669                cx,
670            )
671        });
672        let (window_id, _root_view) = cx.add_window(|_| EmptyView);
673
674        let editor = cx.add_view(window_id, |cx| Editor::for_buffer(buffer.clone(), None, cx));
675
676        let search_bar = cx.add_view(window_id, |cx| {
677            let mut search_bar = BufferSearchBar::new(cx);
678            search_bar.set_active_pane_item(Some(&editor), cx);
679            search_bar.show(false, true, cx);
680            search_bar
681        });
682
683        // Search for a string that appears with different casing.
684        // By default, search is case-insensitive.
685        search_bar.update(cx, |search_bar, cx| {
686            search_bar.set_query("us", cx);
687        });
688        editor.next_notification(cx).await;
689        editor.update(cx, |editor, cx| {
690            assert_eq!(
691                editor.all_background_highlights(cx),
692                &[
693                    (
694                        DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19),
695                        Color::red(),
696                    ),
697                    (
698                        DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
699                        Color::red(),
700                    ),
701                ]
702            );
703        });
704
705        // Switch to a case sensitive search.
706        search_bar.update(cx, |search_bar, cx| {
707            search_bar.toggle_search_option(SearchOption::CaseSensitive, cx);
708        });
709        editor.next_notification(cx).await;
710        editor.update(cx, |editor, cx| {
711            assert_eq!(
712                editor.all_background_highlights(cx),
713                &[(
714                    DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
715                    Color::red(),
716                )]
717            );
718        });
719
720        // Search for a string that appears both as a whole word and
721        // within other words. By default, all results are found.
722        search_bar.update(cx, |search_bar, cx| {
723            search_bar.set_query("or", cx);
724        });
725        editor.next_notification(cx).await;
726        editor.update(cx, |editor, cx| {
727            assert_eq!(
728                editor.all_background_highlights(cx),
729                &[
730                    (
731                        DisplayPoint::new(0, 24)..DisplayPoint::new(0, 26),
732                        Color::red(),
733                    ),
734                    (
735                        DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
736                        Color::red(),
737                    ),
738                    (
739                        DisplayPoint::new(2, 71)..DisplayPoint::new(2, 73),
740                        Color::red(),
741                    ),
742                    (
743                        DisplayPoint::new(3, 1)..DisplayPoint::new(3, 3),
744                        Color::red(),
745                    ),
746                    (
747                        DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
748                        Color::red(),
749                    ),
750                    (
751                        DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
752                        Color::red(),
753                    ),
754                    (
755                        DisplayPoint::new(3, 60)..DisplayPoint::new(3, 62),
756                        Color::red(),
757                    ),
758                ]
759            );
760        });
761
762        // Switch to a whole word search.
763        search_bar.update(cx, |search_bar, cx| {
764            search_bar.toggle_search_option(SearchOption::WholeWord, cx);
765        });
766        editor.next_notification(cx).await;
767        editor.update(cx, |editor, cx| {
768            assert_eq!(
769                editor.all_background_highlights(cx),
770                &[
771                    (
772                        DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
773                        Color::red(),
774                    ),
775                    (
776                        DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
777                        Color::red(),
778                    ),
779                    (
780                        DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
781                        Color::red(),
782                    ),
783                ]
784            );
785        });
786
787        editor.update(cx, |editor, cx| {
788            editor.change_selections(None, cx, |s| {
789                s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
790            });
791        });
792        search_bar.update(cx, |search_bar, cx| {
793            assert_eq!(search_bar.active_match_index, Some(0));
794            search_bar.select_next_match(&SelectNextMatch, cx);
795            assert_eq!(
796                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
797                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
798            );
799        });
800        search_bar.read_with(cx, |search_bar, _| {
801            assert_eq!(search_bar.active_match_index, Some(0));
802        });
803
804        search_bar.update(cx, |search_bar, cx| {
805            search_bar.select_next_match(&SelectNextMatch, cx);
806            assert_eq!(
807                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
808                [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
809            );
810        });
811        search_bar.read_with(cx, |search_bar, _| {
812            assert_eq!(search_bar.active_match_index, Some(1));
813        });
814
815        search_bar.update(cx, |search_bar, cx| {
816            search_bar.select_next_match(&SelectNextMatch, cx);
817            assert_eq!(
818                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
819                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
820            );
821        });
822        search_bar.read_with(cx, |search_bar, _| {
823            assert_eq!(search_bar.active_match_index, Some(2));
824        });
825
826        search_bar.update(cx, |search_bar, cx| {
827            search_bar.select_next_match(&SelectNextMatch, cx);
828            assert_eq!(
829                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
830                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
831            );
832        });
833        search_bar.read_with(cx, |search_bar, _| {
834            assert_eq!(search_bar.active_match_index, Some(0));
835        });
836
837        search_bar.update(cx, |search_bar, cx| {
838            search_bar.select_prev_match(&SelectPrevMatch, cx);
839            assert_eq!(
840                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
841                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
842            );
843        });
844        search_bar.read_with(cx, |search_bar, _| {
845            assert_eq!(search_bar.active_match_index, Some(2));
846        });
847
848        search_bar.update(cx, |search_bar, cx| {
849            search_bar.select_prev_match(&SelectPrevMatch, cx);
850            assert_eq!(
851                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
852                [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
853            );
854        });
855        search_bar.read_with(cx, |search_bar, _| {
856            assert_eq!(search_bar.active_match_index, Some(1));
857        });
858
859        search_bar.update(cx, |search_bar, cx| {
860            search_bar.select_prev_match(&SelectPrevMatch, cx);
861            assert_eq!(
862                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
863                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
864            );
865        });
866        search_bar.read_with(cx, |search_bar, _| {
867            assert_eq!(search_bar.active_match_index, Some(0));
868        });
869
870        // Park the cursor in between matches and ensure that going to the previous match selects
871        // the closest match to the left.
872        editor.update(cx, |editor, cx| {
873            editor.change_selections(None, cx, |s| {
874                s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
875            });
876        });
877        search_bar.update(cx, |search_bar, cx| {
878            assert_eq!(search_bar.active_match_index, Some(1));
879            search_bar.select_prev_match(&SelectPrevMatch, cx);
880            assert_eq!(
881                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
882                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
883            );
884        });
885        search_bar.read_with(cx, |search_bar, _| {
886            assert_eq!(search_bar.active_match_index, Some(0));
887        });
888
889        // Park the cursor in between matches and ensure that going to the next match selects the
890        // closest match to the right.
891        editor.update(cx, |editor, cx| {
892            editor.change_selections(None, cx, |s| {
893                s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
894            });
895        });
896        search_bar.update(cx, |search_bar, cx| {
897            assert_eq!(search_bar.active_match_index, Some(1));
898            search_bar.select_next_match(&SelectNextMatch, cx);
899            assert_eq!(
900                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
901                [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
902            );
903        });
904        search_bar.read_with(cx, |search_bar, _| {
905            assert_eq!(search_bar.active_match_index, Some(1));
906        });
907
908        // Park the cursor after the last match and ensure that going to the previous match selects
909        // the last match.
910        editor.update(cx, |editor, cx| {
911            editor.change_selections(None, cx, |s| {
912                s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
913            });
914        });
915        search_bar.update(cx, |search_bar, cx| {
916            assert_eq!(search_bar.active_match_index, Some(2));
917            search_bar.select_prev_match(&SelectPrevMatch, cx);
918            assert_eq!(
919                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
920                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
921            );
922        });
923        search_bar.read_with(cx, |search_bar, _| {
924            assert_eq!(search_bar.active_match_index, Some(2));
925        });
926
927        // Park the cursor after the last match and ensure that going to the next match selects the
928        // first match.
929        editor.update(cx, |editor, cx| {
930            editor.change_selections(None, cx, |s| {
931                s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
932            });
933        });
934        search_bar.update(cx, |search_bar, cx| {
935            assert_eq!(search_bar.active_match_index, Some(2));
936            search_bar.select_next_match(&SelectNextMatch, cx);
937            assert_eq!(
938                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
939                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
940            );
941        });
942        search_bar.read_with(cx, |search_bar, _| {
943            assert_eq!(search_bar.active_match_index, Some(0));
944        });
945
946        // Park the cursor before the first match and ensure that going to the previous match
947        // selects the last match.
948        editor.update(cx, |editor, cx| {
949            editor.change_selections(None, cx, |s| {
950                s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
951            });
952        });
953        search_bar.update(cx, |search_bar, cx| {
954            assert_eq!(search_bar.active_match_index, Some(0));
955            search_bar.select_prev_match(&SelectPrevMatch, cx);
956            assert_eq!(
957                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
958                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
959            );
960        });
961        search_bar.read_with(cx, |search_bar, _| {
962            assert_eq!(search_bar.active_match_index, Some(2));
963        });
964    }
965}