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