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