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