buffer_search.rs

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