buffer_search.rs

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