buffer_search.rs

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