buffer_search.rs

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