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