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