buffer_search.rs

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