buffer_search.rs

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