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