buffer_search.rs

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