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