buffer_search.rs

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