buffer_search.rs

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