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