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