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