buffer_search.rs

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