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