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