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