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