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