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    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}