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