buffer_search.rs

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