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