buffer_search.rs

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