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    MouseButton, MutableAppContext, RenderContext, Subscription, Task, View, ViewContext,
 11    ViewHandle, 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(MouseButton::Left, move |_, cx| {
289            cx.dispatch_any_action(option.to_toggle_action())
290        })
291        .with_cursor_style(CursorStyle::PointingHand)
292        .with_tooltip::<Self, _>(
293            option as usize,
294            format!("Toggle {}", option.label()),
295            Some(option.to_toggle_action()),
296            tooltip_style,
297            cx,
298        )
299        .boxed()
300    }
301
302    fn render_nav_button(
303        &self,
304        icon: &str,
305        direction: Direction,
306        cx: &mut RenderContext<Self>,
307    ) -> ElementBox {
308        let action: Box<dyn Action>;
309        let tooltip;
310        match direction {
311            Direction::Prev => {
312                action = Box::new(SelectPrevMatch);
313                tooltip = "Select Previous Match";
314            }
315            Direction::Next => {
316                action = Box::new(SelectNextMatch);
317                tooltip = "Select Next Match";
318            }
319        };
320        let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
321
322        enum NavButton {}
323        MouseEventHandler::new::<NavButton, _, _>(direction as usize, cx, |state, cx| {
324            let style = &cx
325                .global::<Settings>()
326                .theme
327                .search
328                .option_button
329                .style_for(state, false);
330            Label::new(icon.to_string(), style.text.clone())
331                .contained()
332                .with_style(style.container)
333                .boxed()
334        })
335        .on_click(MouseButton::Left, {
336            let action = action.boxed_clone();
337            move |_, cx| cx.dispatch_any_action(action.boxed_clone())
338        })
339        .with_cursor_style(CursorStyle::PointingHand)
340        .with_tooltip::<NavButton, _>(
341            direction as usize,
342            tooltip.to_string(),
343            Some(action),
344            tooltip_style,
345            cx,
346        )
347        .boxed()
348    }
349
350    fn deploy(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext<Pane>) {
351        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
352            if search_bar.update(cx, |search_bar, cx| search_bar.show(action.focus, true, cx)) {
353                return;
354            }
355        }
356        cx.propagate_action();
357    }
358
359    fn handle_editor_cancel(pane: &mut Pane, _: &editor::Cancel, cx: &mut ViewContext<Pane>) {
360        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
361            if !search_bar.read(cx).dismissed {
362                search_bar.update(cx, |search_bar, cx| search_bar.dismiss(&Dismiss, cx));
363                return;
364            }
365        }
366        cx.propagate_action();
367    }
368
369    fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext<Self>) {
370        if let Some(active_editor) = self.active_editor.as_ref() {
371            cx.focus(active_editor);
372        }
373    }
374
375    fn is_search_option_enabled(&self, search_option: SearchOption) -> bool {
376        match search_option {
377            SearchOption::WholeWord => self.whole_word,
378            SearchOption::CaseSensitive => self.case_sensitive,
379            SearchOption::Regex => self.regex,
380        }
381    }
382
383    fn toggle_search_option(&mut self, search_option: SearchOption, cx: &mut ViewContext<Self>) {
384        let value = match search_option {
385            SearchOption::WholeWord => &mut self.whole_word,
386            SearchOption::CaseSensitive => &mut self.case_sensitive,
387            SearchOption::Regex => &mut self.regex,
388        };
389        *value = !*value;
390        self.update_matches(false, cx);
391        cx.notify();
392    }
393
394    fn select_next_match(&mut self, _: &SelectNextMatch, cx: &mut ViewContext<Self>) {
395        self.select_match(Direction::Next, cx);
396    }
397
398    fn select_prev_match(&mut self, _: &SelectPrevMatch, cx: &mut ViewContext<Self>) {
399        self.select_match(Direction::Prev, cx);
400    }
401
402    fn select_match(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
403        if let Some(index) = self.active_match_index {
404            if let Some(editor) = self.active_editor.as_ref() {
405                editor.update(cx, |editor, cx| {
406                    if let Some(ranges) = self.editors_with_matches.get(&cx.weak_handle()) {
407                        let new_index = match_index_for_direction(
408                            ranges,
409                            &editor.selections.newest_anchor().head(),
410                            index,
411                            direction,
412                            &editor.buffer().read(cx).snapshot(cx),
413                        );
414                        let range_to_select = ranges[new_index].clone();
415                        editor.unfold_ranges([range_to_select.clone()], false, cx);
416                        editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
417                            s.select_ranges([range_to_select])
418                        });
419                    }
420                });
421            }
422        }
423    }
424
425    fn select_next_match_on_pane(
426        pane: &mut Pane,
427        action: &SelectNextMatch,
428        cx: &mut ViewContext<Pane>,
429    ) {
430        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
431            search_bar.update(cx, |bar, cx| bar.select_next_match(action, cx));
432        }
433    }
434
435    fn select_prev_match_on_pane(
436        pane: &mut Pane,
437        action: &SelectPrevMatch,
438        cx: &mut ViewContext<Pane>,
439    ) {
440        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
441            search_bar.update(cx, |bar, cx| bar.select_prev_match(action, cx));
442        }
443    }
444
445    fn on_query_editor_event(
446        &mut self,
447        _: ViewHandle<Editor>,
448        event: &editor::Event,
449        cx: &mut ViewContext<Self>,
450    ) {
451        match event {
452            editor::Event::BufferEdited { .. } => {
453                self.query_contains_error = false;
454                self.clear_matches(cx);
455                self.update_matches(true, cx);
456                cx.notify();
457            }
458            _ => {}
459        }
460    }
461
462    fn on_active_editor_event(
463        &mut self,
464        _: ViewHandle<Editor>,
465        event: &editor::Event,
466        cx: &mut ViewContext<Self>,
467    ) {
468        match event {
469            editor::Event::BufferEdited { .. } => self.update_matches(false, cx),
470            editor::Event::SelectionsChanged { .. } => self.update_match_index(cx),
471            _ => {}
472        }
473    }
474
475    fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
476        let mut active_editor_matches = None;
477        for (editor, ranges) in self.editors_with_matches.drain() {
478            if let Some(editor) = editor.upgrade(cx) {
479                if Some(&editor) == self.active_editor.as_ref() {
480                    active_editor_matches = Some((editor.downgrade(), ranges));
481                } else {
482                    editor.update(cx, |editor, cx| {
483                        editor.clear_background_highlights::<Self>(cx)
484                    });
485                }
486            }
487        }
488        self.editors_with_matches.extend(active_editor_matches);
489    }
490
491    fn update_matches(&mut self, select_closest_match: bool, cx: &mut ViewContext<Self>) {
492        let query = self.query_editor.read(cx).text(cx);
493        self.pending_search.take();
494        if let Some(editor) = self.active_editor.as_ref() {
495            if query.is_empty() {
496                self.active_match_index.take();
497                editor.update(cx, |editor, cx| {
498                    editor.clear_background_highlights::<Self>(cx)
499                });
500            } else {
501                let buffer = editor.read(cx).buffer().read(cx).snapshot(cx);
502                let query = if self.regex {
503                    match SearchQuery::regex(query, self.whole_word, self.case_sensitive) {
504                        Ok(query) => query,
505                        Err(_) => {
506                            self.query_contains_error = true;
507                            cx.notify();
508                            return;
509                        }
510                    }
511                } else {
512                    SearchQuery::text(query, self.whole_word, self.case_sensitive)
513                };
514
515                let ranges = cx.background().spawn(async move {
516                    let mut ranges = Vec::new();
517                    if let Some((_, _, excerpt_buffer)) = buffer.as_singleton() {
518                        ranges.extend(
519                            query
520                                .search(excerpt_buffer.as_rope())
521                                .await
522                                .into_iter()
523                                .map(|range| {
524                                    buffer.anchor_after(range.start)
525                                        ..buffer.anchor_before(range.end)
526                                }),
527                        );
528                    } else {
529                        for excerpt in buffer.excerpt_boundaries_in_range(0..buffer.len()) {
530                            let excerpt_range = excerpt.range.context.to_offset(&excerpt.buffer);
531                            let rope = excerpt.buffer.as_rope().slice(excerpt_range.clone());
532                            ranges.extend(query.search(&rope).await.into_iter().map(|range| {
533                                let start = excerpt
534                                    .buffer
535                                    .anchor_after(excerpt_range.start + range.start);
536                                let end = excerpt
537                                    .buffer
538                                    .anchor_before(excerpt_range.start + range.end);
539                                buffer.anchor_in_excerpt(excerpt.id.clone(), start)
540                                    ..buffer.anchor_in_excerpt(excerpt.id.clone(), end)
541                            }));
542                        }
543                    }
544                    ranges
545                });
546
547                let editor = editor.downgrade();
548                self.pending_search = Some(cx.spawn_weak(|this, mut cx| async move {
549                    let ranges = ranges.await;
550                    if let Some((this, editor)) = this.upgrade(&cx).zip(editor.upgrade(&cx)) {
551                        this.update(&mut cx, |this, cx| {
552                            this.editors_with_matches
553                                .insert(editor.downgrade(), ranges.clone());
554                            this.update_match_index(cx);
555                            if !this.dismissed {
556                                editor.update(cx, |editor, cx| {
557                                    if select_closest_match {
558                                        if let Some(match_ix) = this.active_match_index {
559                                            editor.change_selections(
560                                                Some(Autoscroll::Fit),
561                                                cx,
562                                                |s| s.select_ranges([ranges[match_ix].clone()]),
563                                            );
564                                        }
565                                    }
566
567                                    editor.highlight_background::<Self>(
568                                        ranges,
569                                        |theme| theme.search.match_background,
570                                        cx,
571                                    );
572                                });
573                            }
574                            cx.notify();
575                        });
576                    }
577                }));
578            }
579        }
580    }
581
582    fn update_match_index(&mut self, cx: &mut ViewContext<Self>) {
583        let new_index = self.active_editor.as_ref().and_then(|editor| {
584            let ranges = self.editors_with_matches.get(&editor.downgrade())?;
585            let editor = editor.read(cx);
586            active_match_index(
587                &ranges,
588                &editor.selections.newest_anchor().head(),
589                &editor.buffer().read(cx).snapshot(cx),
590            )
591        });
592        if new_index != self.active_match_index {
593            self.active_match_index = new_index;
594            cx.notify();
595        }
596    }
597}
598
599#[cfg(test)]
600mod tests {
601    use super::*;
602    use editor::{DisplayPoint, Editor};
603    use gpui::{color::Color, TestAppContext};
604    use language::Buffer;
605    use std::sync::Arc;
606    use unindent::Unindent as _;
607
608    #[gpui::test]
609    async fn test_search_simple(cx: &mut TestAppContext) {
610        let fonts = cx.font_cache();
611        let mut theme = gpui::fonts::with_font_cache(fonts.clone(), || theme::Theme::default());
612        theme.search.match_background = Color::red();
613        cx.update(|cx| {
614            let mut settings = Settings::test(cx);
615            settings.theme = Arc::new(theme);
616            cx.set_global(settings)
617        });
618
619        let buffer = cx.add_model(|cx| {
620            Buffer::new(
621                0,
622                r#"
623                A regular expression (shortened as regex or regexp;[1] also referred to as
624                rational expression[2][3]) is a sequence of characters that specifies a search
625                pattern in text. Usually such patterns are used by string-searching algorithms
626                for "find" or "find and replace" operations on strings, or for input validation.
627                "#
628                .unindent(),
629                cx,
630            )
631        });
632        let editor = cx.add_view(Default::default(), |cx| {
633            Editor::for_buffer(buffer.clone(), None, cx)
634        });
635
636        let search_bar = cx.add_view(Default::default(), |cx| {
637            let mut search_bar = BufferSearchBar::new(cx);
638            search_bar.set_active_pane_item(Some(&editor), cx);
639            search_bar.show(false, true, cx);
640            search_bar
641        });
642
643        // Search for a string that appears with different casing.
644        // By default, search is case-insensitive.
645        search_bar.update(cx, |search_bar, cx| {
646            search_bar.set_query("us", cx);
647        });
648        editor.next_notification(&cx).await;
649        editor.update(cx, |editor, cx| {
650            assert_eq!(
651                editor.all_background_highlights(cx),
652                &[
653                    (
654                        DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19),
655                        Color::red(),
656                    ),
657                    (
658                        DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
659                        Color::red(),
660                    ),
661                ]
662            );
663        });
664
665        // Switch to a case sensitive search.
666        search_bar.update(cx, |search_bar, cx| {
667            search_bar.toggle_search_option(SearchOption::CaseSensitive, cx);
668        });
669        editor.next_notification(&cx).await;
670        editor.update(cx, |editor, cx| {
671            assert_eq!(
672                editor.all_background_highlights(cx),
673                &[(
674                    DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
675                    Color::red(),
676                )]
677            );
678        });
679
680        // Search for a string that appears both as a whole word and
681        // within other words. By default, all results are found.
682        search_bar.update(cx, |search_bar, cx| {
683            search_bar.set_query("or", cx);
684        });
685        editor.next_notification(&cx).await;
686        editor.update(cx, |editor, cx| {
687            assert_eq!(
688                editor.all_background_highlights(cx),
689                &[
690                    (
691                        DisplayPoint::new(0, 24)..DisplayPoint::new(0, 26),
692                        Color::red(),
693                    ),
694                    (
695                        DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
696                        Color::red(),
697                    ),
698                    (
699                        DisplayPoint::new(2, 71)..DisplayPoint::new(2, 73),
700                        Color::red(),
701                    ),
702                    (
703                        DisplayPoint::new(3, 1)..DisplayPoint::new(3, 3),
704                        Color::red(),
705                    ),
706                    (
707                        DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
708                        Color::red(),
709                    ),
710                    (
711                        DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
712                        Color::red(),
713                    ),
714                    (
715                        DisplayPoint::new(3, 60)..DisplayPoint::new(3, 62),
716                        Color::red(),
717                    ),
718                ]
719            );
720        });
721
722        // Switch to a whole word search.
723        search_bar.update(cx, |search_bar, cx| {
724            search_bar.toggle_search_option(SearchOption::WholeWord, cx);
725        });
726        editor.next_notification(&cx).await;
727        editor.update(cx, |editor, cx| {
728            assert_eq!(
729                editor.all_background_highlights(cx),
730                &[
731                    (
732                        DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
733                        Color::red(),
734                    ),
735                    (
736                        DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
737                        Color::red(),
738                    ),
739                    (
740                        DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
741                        Color::red(),
742                    ),
743                ]
744            );
745        });
746
747        editor.update(cx, |editor, cx| {
748            editor.change_selections(None, cx, |s| {
749                s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
750            });
751        });
752        search_bar.update(cx, |search_bar, cx| {
753            assert_eq!(search_bar.active_match_index, Some(0));
754            search_bar.select_next_match(&SelectNextMatch, cx);
755            assert_eq!(
756                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
757                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
758            );
759        });
760        search_bar.read_with(cx, |search_bar, _| {
761            assert_eq!(search_bar.active_match_index, Some(0));
762        });
763
764        search_bar.update(cx, |search_bar, cx| {
765            search_bar.select_next_match(&SelectNextMatch, cx);
766            assert_eq!(
767                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
768                [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
769            );
770        });
771        search_bar.read_with(cx, |search_bar, _| {
772            assert_eq!(search_bar.active_match_index, Some(1));
773        });
774
775        search_bar.update(cx, |search_bar, cx| {
776            search_bar.select_next_match(&SelectNextMatch, cx);
777            assert_eq!(
778                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
779                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
780            );
781        });
782        search_bar.read_with(cx, |search_bar, _| {
783            assert_eq!(search_bar.active_match_index, Some(2));
784        });
785
786        search_bar.update(cx, |search_bar, cx| {
787            search_bar.select_next_match(&SelectNextMatch, cx);
788            assert_eq!(
789                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
790                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
791            );
792        });
793        search_bar.read_with(cx, |search_bar, _| {
794            assert_eq!(search_bar.active_match_index, Some(0));
795        });
796
797        search_bar.update(cx, |search_bar, cx| {
798            search_bar.select_prev_match(&SelectPrevMatch, cx);
799            assert_eq!(
800                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
801                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
802            );
803        });
804        search_bar.read_with(cx, |search_bar, _| {
805            assert_eq!(search_bar.active_match_index, Some(2));
806        });
807
808        search_bar.update(cx, |search_bar, cx| {
809            search_bar.select_prev_match(&SelectPrevMatch, 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_prev_match(&SelectPrevMatch, cx);
821            assert_eq!(
822                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
823                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
824            );
825        });
826        search_bar.read_with(cx, |search_bar, _| {
827            assert_eq!(search_bar.active_match_index, Some(0));
828        });
829
830        // Park the cursor in between matches and ensure that going to the previous match selects
831        // the closest match to the left.
832        editor.update(cx, |editor, cx| {
833            editor.change_selections(None, cx, |s| {
834                s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
835            });
836        });
837        search_bar.update(cx, |search_bar, cx| {
838            assert_eq!(search_bar.active_match_index, Some(1));
839            search_bar.select_prev_match(&SelectPrevMatch, cx);
840            assert_eq!(
841                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
842                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
843            );
844        });
845        search_bar.read_with(cx, |search_bar, _| {
846            assert_eq!(search_bar.active_match_index, Some(0));
847        });
848
849        // Park the cursor in between matches and ensure that going to the next match selects the
850        // closest match to the right.
851        editor.update(cx, |editor, cx| {
852            editor.change_selections(None, cx, |s| {
853                s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
854            });
855        });
856        search_bar.update(cx, |search_bar, cx| {
857            assert_eq!(search_bar.active_match_index, Some(1));
858            search_bar.select_next_match(&SelectNextMatch, cx);
859            assert_eq!(
860                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
861                [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
862            );
863        });
864        search_bar.read_with(cx, |search_bar, _| {
865            assert_eq!(search_bar.active_match_index, Some(1));
866        });
867
868        // Park the cursor after the last match and ensure that going to the previous match selects
869        // the last match.
870        editor.update(cx, |editor, cx| {
871            editor.change_selections(None, cx, |s| {
872                s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
873            });
874        });
875        search_bar.update(cx, |search_bar, cx| {
876            assert_eq!(search_bar.active_match_index, Some(2));
877            search_bar.select_prev_match(&SelectPrevMatch, cx);
878            assert_eq!(
879                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
880                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
881            );
882        });
883        search_bar.read_with(cx, |search_bar, _| {
884            assert_eq!(search_bar.active_match_index, Some(2));
885        });
886
887        // Park the cursor after the last match and ensure that going to the next match selects the
888        // first match.
889        editor.update(cx, |editor, cx| {
890            editor.change_selections(None, cx, |s| {
891                s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
892            });
893        });
894        search_bar.update(cx, |search_bar, cx| {
895            assert_eq!(search_bar.active_match_index, Some(2));
896            search_bar.select_next_match(&SelectNextMatch, cx);
897            assert_eq!(
898                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
899                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
900            );
901        });
902        search_bar.read_with(cx, |search_bar, _| {
903            assert_eq!(search_bar.active_match_index, Some(0));
904        });
905
906        // Park the cursor before the first match and ensure that going to the previous match
907        // selects the last match.
908        editor.update(cx, |editor, cx| {
909            editor.change_selections(None, cx, |s| {
910                s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
911            });
912        });
913        search_bar.update(cx, |search_bar, cx| {
914            assert_eq!(search_bar.active_match_index, Some(0));
915            search_bar.select_prev_match(&SelectPrevMatch, cx);
916            assert_eq!(
917                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
918                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
919            );
920        });
921        search_bar.read_with(cx, |search_bar, _| {
922            assert_eq!(search_bar.active_match_index, Some(2));
923        });
924    }
925}