buffer_search.rs

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