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    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 display_map = editor
226            .update(cx, |editor, cx| editor.snapshot(cx))
227            .display_snapshot;
228        let selection = editor
229            .read(cx)
230            .newest_selection_with_snapshot::<usize>(&display_map.buffer_snapshot);
231
232        let mut text: String;
233        if selection.start == selection.end {
234            let point = selection.start.to_display_point(&display_map);
235            let range = editor::movement::surrounding_word(&display_map, point);
236            let range = range.start.to_offset(&display_map, Bias::Left)
237                ..range.end.to_offset(&display_map, Bias::Right);
238            text = display_map.buffer_snapshot.text_for_range(range).collect();
239            if text.trim().is_empty() {
240                text = String::new();
241            }
242        } else {
243            text = display_map
244                .buffer_snapshot
245                .text_for_range(selection.start..selection.end)
246                .collect();
247        }
248
249        if !text.is_empty() {
250            self.set_query(&text, cx);
251        }
252
253        if focus {
254            let query_editor = self.query_editor.clone();
255            query_editor.update(cx, |query_editor, cx| {
256                query_editor.select_all(&editor::SelectAll, cx);
257            });
258            cx.focus_self();
259        }
260
261        self.dismissed = false;
262        cx.notify();
263        cx.emit(Event::UpdateLocation);
264        true
265    }
266
267    fn set_query(&mut self, query: &str, cx: &mut ViewContext<Self>) {
268        self.query_editor.update(cx, |query_editor, cx| {
269            query_editor.buffer().update(cx, |query_buffer, cx| {
270                let len = query_buffer.read(cx).len();
271                query_buffer.edit([0..len], query, cx);
272            });
273        });
274    }
275
276    fn render_search_option(
277        &self,
278        icon: &str,
279        search_option: SearchOption,
280        cx: &mut RenderContext<Self>,
281    ) -> ElementBox {
282        let is_active = self.is_search_option_enabled(search_option);
283        MouseEventHandler::new::<Self, _, _>(search_option as usize, cx, |state, cx| {
284            let theme = &cx.global::<Settings>().theme.search;
285            let style = match (is_active, state.hovered) {
286                (false, false) => &theme.option_button,
287                (false, true) => &theme.hovered_option_button,
288                (true, false) => &theme.active_option_button,
289                (true, true) => &theme.active_hovered_option_button,
290            };
291            Label::new(icon.to_string(), style.text.clone())
292                .contained()
293                .with_style(style.container)
294                .boxed()
295        })
296        .on_click(move |cx| cx.dispatch_action(ToggleSearchOption(search_option)))
297        .with_cursor_style(CursorStyle::PointingHand)
298        .boxed()
299    }
300
301    fn render_nav_button(
302        &self,
303        icon: &str,
304        direction: Direction,
305        cx: &mut RenderContext<Self>,
306    ) -> ElementBox {
307        enum NavButton {}
308        MouseEventHandler::new::<NavButton, _, _>(direction as usize, cx, |state, cx| {
309            let theme = &cx.global::<Settings>().theme.search;
310            let style = if state.hovered {
311                &theme.hovered_option_button
312            } else {
313                &theme.option_button
314            };
315            Label::new(icon.to_string(), style.text.clone())
316                .contained()
317                .with_style(style.container)
318                .boxed()
319        })
320        .on_click(move |cx| match direction {
321            Direction::Prev => cx.dispatch_action(SelectPrevMatch),
322            Direction::Next => cx.dispatch_action(SelectNextMatch),
323        })
324        .with_cursor_style(CursorStyle::PointingHand)
325        .boxed()
326    }
327
328    fn deploy(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext<Pane>) {
329        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
330            if search_bar.update(cx, |search_bar, cx| search_bar.show(action.focus, cx)) {
331                return;
332            }
333        }
334        cx.propagate_action();
335    }
336
337    fn handle_editor_cancel(pane: &mut Pane, _: &editor::Cancel, cx: &mut ViewContext<Pane>) {
338        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
339            if !search_bar.read(cx).dismissed {
340                search_bar.update(cx, |search_bar, cx| search_bar.dismiss(&Dismiss, cx));
341                return;
342            }
343        }
344        cx.propagate_action();
345    }
346
347    fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext<Self>) {
348        if let Some(active_editor) = self.active_editor.as_ref() {
349            cx.focus(active_editor);
350        }
351    }
352
353    fn is_search_option_enabled(&self, search_option: SearchOption) -> bool {
354        match search_option {
355            SearchOption::WholeWord => self.whole_word,
356            SearchOption::CaseSensitive => self.case_sensitive,
357            SearchOption::Regex => self.regex,
358        }
359    }
360
361    fn toggle_search_option(
362        &mut self,
363        ToggleSearchOption(search_option): &ToggleSearchOption,
364        cx: &mut ViewContext<Self>,
365    ) {
366        let value = match search_option {
367            SearchOption::WholeWord => &mut self.whole_word,
368            SearchOption::CaseSensitive => &mut self.case_sensitive,
369            SearchOption::Regex => &mut self.regex,
370        };
371        *value = !*value;
372        self.update_matches(true, cx);
373        cx.notify();
374    }
375
376    fn select_next_match(&mut self, _: &SelectNextMatch, cx: &mut ViewContext<Self>) {
377        self.select_match(Direction::Next, cx);
378    }
379
380    fn select_prev_match(&mut self, _: &SelectPrevMatch, cx: &mut ViewContext<Self>) {
381        self.select_match(Direction::Prev, cx);
382    }
383
384    fn select_match(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
385        if let Some(index) = self.active_match_index {
386            if let Some(editor) = self.active_editor.as_ref() {
387                editor.update(cx, |editor, cx| {
388                    if let Some(ranges) = self.editors_with_matches.get(&cx.weak_handle()) {
389                        let new_index = match_index_for_direction(
390                            ranges,
391                            &editor.newest_anchor_selection().head(),
392                            index,
393                            direction,
394                            &editor.buffer().read(cx).read(cx),
395                        );
396                        let range_to_select = ranges[new_index].clone();
397                        editor.unfold_ranges([range_to_select.clone()], false, cx);
398                        editor.select_ranges([range_to_select], Some(Autoscroll::Fit), cx);
399                    }
400                });
401            }
402        }
403    }
404
405    fn select_next_match_on_pane(
406        pane: &mut Pane,
407        action: &SelectNextMatch,
408        cx: &mut ViewContext<Pane>,
409    ) {
410        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
411            search_bar.update(cx, |bar, cx| bar.select_next_match(action, cx));
412        }
413    }
414
415    fn select_prev_match_on_pane(
416        pane: &mut Pane,
417        action: &SelectPrevMatch,
418        cx: &mut ViewContext<Pane>,
419    ) {
420        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
421            search_bar.update(cx, |bar, cx| bar.select_prev_match(action, cx));
422        }
423    }
424
425    fn on_query_editor_event(
426        &mut self,
427        _: ViewHandle<Editor>,
428        event: &editor::Event,
429        cx: &mut ViewContext<Self>,
430    ) {
431        match event {
432            editor::Event::BufferEdited { .. } => {
433                self.query_contains_error = false;
434                self.clear_matches(cx);
435                self.update_matches(true, cx);
436                cx.notify();
437            }
438            _ => {}
439        }
440    }
441
442    fn on_active_editor_event(
443        &mut self,
444        _: ViewHandle<Editor>,
445        event: &editor::Event,
446        cx: &mut ViewContext<Self>,
447    ) {
448        match event {
449            editor::Event::BufferEdited { .. } => self.update_matches(false, cx),
450            editor::Event::SelectionsChanged { .. } => self.update_match_index(cx),
451            _ => {}
452        }
453    }
454
455    fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
456        let mut active_editor_matches = None;
457        for (editor, ranges) in self.editors_with_matches.drain() {
458            if let Some(editor) = editor.upgrade(cx) {
459                if Some(&editor) == self.active_editor.as_ref() {
460                    active_editor_matches = Some((editor.downgrade(), ranges));
461                } else {
462                    editor.update(cx, |editor, cx| {
463                        editor.clear_background_highlights::<Self>(cx)
464                    });
465                }
466            }
467        }
468        self.editors_with_matches.extend(active_editor_matches);
469    }
470
471    fn update_matches(&mut self, select_closest_match: bool, cx: &mut ViewContext<Self>) {
472        let query = self.query_editor.read(cx).text(cx);
473        self.pending_search.take();
474        if let Some(editor) = self.active_editor.as_ref() {
475            if query.is_empty() {
476                self.active_match_index.take();
477                editor.update(cx, |editor, cx| {
478                    editor.clear_background_highlights::<Self>(cx)
479                });
480            } else {
481                let buffer = editor.read(cx).buffer().read(cx).snapshot(cx);
482                let query = if self.regex {
483                    match SearchQuery::regex(query, self.whole_word, self.case_sensitive) {
484                        Ok(query) => query,
485                        Err(_) => {
486                            self.query_contains_error = true;
487                            cx.notify();
488                            return;
489                        }
490                    }
491                } else {
492                    SearchQuery::text(query, self.whole_word, self.case_sensitive)
493                };
494
495                let ranges = cx.background().spawn(async move {
496                    let mut ranges = Vec::new();
497                    if let Some((_, _, excerpt_buffer)) = buffer.as_singleton() {
498                        ranges.extend(
499                            query
500                                .search(excerpt_buffer.as_rope())
501                                .await
502                                .into_iter()
503                                .map(|range| {
504                                    buffer.anchor_after(range.start)
505                                        ..buffer.anchor_before(range.end)
506                                }),
507                        );
508                    } else {
509                        for excerpt in buffer.excerpt_boundaries_in_range(0..buffer.len()) {
510                            let excerpt_range = excerpt.range.to_offset(&excerpt.buffer);
511                            let rope = excerpt.buffer.as_rope().slice(excerpt_range.clone());
512                            ranges.extend(query.search(&rope).await.into_iter().map(|range| {
513                                let start = excerpt
514                                    .buffer
515                                    .anchor_after(excerpt_range.start + range.start);
516                                let end = excerpt
517                                    .buffer
518                                    .anchor_before(excerpt_range.start + range.end);
519                                buffer.anchor_in_excerpt(excerpt.id.clone(), start)
520                                    ..buffer.anchor_in_excerpt(excerpt.id.clone(), end)
521                            }));
522                        }
523                    }
524                    ranges
525                });
526
527                let editor = editor.downgrade();
528                self.pending_search = Some(cx.spawn_weak(|this, mut cx| async move {
529                    let ranges = ranges.await;
530                    if let Some((this, editor)) = this.upgrade(&cx).zip(editor.upgrade(&cx)) {
531                        this.update(&mut cx, |this, cx| {
532                            this.editors_with_matches
533                                .insert(editor.downgrade(), ranges.clone());
534                            this.update_match_index(cx);
535                            if !this.dismissed {
536                                editor.update(cx, |editor, cx| {
537                                    if select_closest_match {
538                                        if let Some(match_ix) = this.active_match_index {
539                                            editor.select_ranges(
540                                                [ranges[match_ix].clone()],
541                                                Some(Autoscroll::Fit),
542                                                cx,
543                                            );
544                                        }
545                                    }
546
547                                    editor.highlight_background::<Self>(
548                                        ranges,
549                                        |theme| theme.search.match_background,
550                                        cx,
551                                    );
552                                });
553                            }
554                            cx.notify();
555                        });
556                    }
557                }));
558            }
559        }
560    }
561
562    fn update_match_index(&mut self, cx: &mut ViewContext<Self>) {
563        let new_index = self.active_editor.as_ref().and_then(|editor| {
564            let ranges = self.editors_with_matches.get(&editor.downgrade())?;
565            let editor = editor.read(cx);
566            active_match_index(
567                &ranges,
568                &editor.newest_anchor_selection().head(),
569                &editor.buffer().read(cx).read(cx),
570            )
571        });
572        if new_index != self.active_match_index {
573            self.active_match_index = new_index;
574            cx.notify();
575        }
576    }
577}
578
579#[cfg(test)]
580mod tests {
581    use super::*;
582    use editor::{DisplayPoint, Editor};
583    use gpui::{color::Color, TestAppContext};
584    use language::Buffer;
585    use std::sync::Arc;
586    use unindent::Unindent as _;
587
588    #[gpui::test]
589    async fn test_search_simple(cx: &mut TestAppContext) {
590        let fonts = cx.font_cache();
591        let mut theme = gpui::fonts::with_font_cache(fonts.clone(), || theme::Theme::default());
592        theme.search.match_background = Color::red();
593        let settings = Settings::new("Courier", &fonts, Arc::new(theme)).unwrap();
594        cx.update(|cx| cx.set_global(settings));
595
596        let buffer = cx.add_model(|cx| {
597            Buffer::new(
598                0,
599                r#"
600                A regular expression (shortened as regex or regexp;[1] also referred to as
601                rational expression[2][3]) is a sequence of characters that specifies a search
602                pattern in text. Usually such patterns are used by string-searching algorithms
603                for "find" or "find and replace" operations on strings, or for input validation.
604                "#
605                .unindent(),
606                cx,
607            )
608        });
609        let editor = cx.add_view(Default::default(), |cx| {
610            Editor::for_buffer(buffer.clone(), None, cx)
611        });
612
613        let search_bar = cx.add_view(Default::default(), |cx| {
614            let mut search_bar = BufferSearchBar::new(cx);
615            search_bar.set_active_pane_item(Some(&editor), cx);
616            search_bar.show(false, cx);
617            search_bar
618        });
619
620        // Search for a string that appears with different casing.
621        // By default, search is case-insensitive.
622        search_bar.update(cx, |search_bar, cx| {
623            search_bar.set_query("us", cx);
624        });
625        editor.next_notification(&cx).await;
626        editor.update(cx, |editor, cx| {
627            assert_eq!(
628                editor.all_background_highlights(cx),
629                &[
630                    (
631                        DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19),
632                        Color::red(),
633                    ),
634                    (
635                        DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
636                        Color::red(),
637                    ),
638                ]
639            );
640        });
641
642        // Switch to a case sensitive search.
643        search_bar.update(cx, |search_bar, cx| {
644            search_bar.toggle_search_option(&ToggleSearchOption(SearchOption::CaseSensitive), cx);
645        });
646        editor.next_notification(&cx).await;
647        editor.update(cx, |editor, cx| {
648            assert_eq!(
649                editor.all_background_highlights(cx),
650                &[(
651                    DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
652                    Color::red(),
653                )]
654            );
655        });
656
657        // Search for a string that appears both as a whole word and
658        // within other words. By default, all results are found.
659        search_bar.update(cx, |search_bar, cx| {
660            search_bar.set_query("or", cx);
661        });
662        editor.next_notification(&cx).await;
663        editor.update(cx, |editor, cx| {
664            assert_eq!(
665                editor.all_background_highlights(cx),
666                &[
667                    (
668                        DisplayPoint::new(0, 24)..DisplayPoint::new(0, 26),
669                        Color::red(),
670                    ),
671                    (
672                        DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
673                        Color::red(),
674                    ),
675                    (
676                        DisplayPoint::new(2, 71)..DisplayPoint::new(2, 73),
677                        Color::red(),
678                    ),
679                    (
680                        DisplayPoint::new(3, 1)..DisplayPoint::new(3, 3),
681                        Color::red(),
682                    ),
683                    (
684                        DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
685                        Color::red(),
686                    ),
687                    (
688                        DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
689                        Color::red(),
690                    ),
691                    (
692                        DisplayPoint::new(3, 60)..DisplayPoint::new(3, 62),
693                        Color::red(),
694                    ),
695                ]
696            );
697        });
698
699        // Switch to a whole word search.
700        search_bar.update(cx, |search_bar, cx| {
701            search_bar.toggle_search_option(&ToggleSearchOption(SearchOption::WholeWord), cx);
702        });
703        editor.next_notification(&cx).await;
704        editor.update(cx, |editor, cx| {
705            assert_eq!(
706                editor.all_background_highlights(cx),
707                &[
708                    (
709                        DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
710                        Color::red(),
711                    ),
712                    (
713                        DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
714                        Color::red(),
715                    ),
716                    (
717                        DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
718                        Color::red(),
719                    ),
720                ]
721            );
722        });
723
724        editor.update(cx, |editor, cx| {
725            editor.select_display_ranges(&[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)], cx);
726        });
727        search_bar.update(cx, |search_bar, cx| {
728            assert_eq!(search_bar.active_match_index, Some(0));
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(0, 41)..DisplayPoint::new(0, 43)]
733            );
734        });
735        search_bar.read_with(cx, |search_bar, _| {
736            assert_eq!(search_bar.active_match_index, Some(0));
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, 11)..DisplayPoint::new(3, 13)]
744            );
745        });
746        search_bar.read_with(cx, |search_bar, _| {
747            assert_eq!(search_bar.active_match_index, Some(1));
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(3, 56)..DisplayPoint::new(3, 58)]
755            );
756        });
757        search_bar.read_with(cx, |search_bar, _| {
758            assert_eq!(search_bar.active_match_index, Some(2));
759        });
760
761        search_bar.update(cx, |search_bar, cx| {
762            search_bar.select_next_match(&SelectNextMatch, cx);
763            assert_eq!(
764                editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
765                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
766            );
767        });
768        search_bar.read_with(cx, |search_bar, _| {
769            assert_eq!(search_bar.active_match_index, Some(0));
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, 56)..DisplayPoint::new(3, 58)]
777            );
778        });
779        search_bar.read_with(cx, |search_bar, _| {
780            assert_eq!(search_bar.active_match_index, Some(2));
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(3, 11)..DisplayPoint::new(3, 13)]
788            );
789        });
790        search_bar.read_with(cx, |search_bar, _| {
791            assert_eq!(search_bar.active_match_index, Some(1));
792        });
793
794        search_bar.update(cx, |search_bar, cx| {
795            search_bar.select_prev_match(&SelectPrevMatch, cx);
796            assert_eq!(
797                editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
798                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
799            );
800        });
801        search_bar.read_with(cx, |search_bar, _| {
802            assert_eq!(search_bar.active_match_index, Some(0));
803        });
804
805        // Park the cursor in between matches and ensure that going to the previous match selects
806        // the closest match to the left.
807        editor.update(cx, |editor, cx| {
808            editor.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx);
809        });
810        search_bar.update(cx, |search_bar, cx| {
811            assert_eq!(search_bar.active_match_index, Some(1));
812            search_bar.select_prev_match(&SelectPrevMatch, cx);
813            assert_eq!(
814                editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
815                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
816            );
817        });
818        search_bar.read_with(cx, |search_bar, _| {
819            assert_eq!(search_bar.active_match_index, Some(0));
820        });
821
822        // Park the cursor in between matches and ensure that going to the next match selects the
823        // closest match to the right.
824        editor.update(cx, |editor, cx| {
825            editor.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx);
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.selected_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.select_display_ranges(&[DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)], cx);
843        });
844        search_bar.update(cx, |search_bar, cx| {
845            assert_eq!(search_bar.active_match_index, Some(2));
846            search_bar.select_prev_match(&SelectPrevMatch, cx);
847            assert_eq!(
848                editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
849                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
850            );
851        });
852        search_bar.read_with(cx, |search_bar, _| {
853            assert_eq!(search_bar.active_match_index, Some(2));
854        });
855
856        // Park the cursor after the last match and ensure that going to the next match selects the
857        // first match.
858        editor.update(cx, |editor, cx| {
859            editor.select_display_ranges(&[DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)], cx);
860        });
861        search_bar.update(cx, |search_bar, cx| {
862            assert_eq!(search_bar.active_match_index, Some(2));
863            search_bar.select_next_match(&SelectNextMatch, cx);
864            assert_eq!(
865                editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
866                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
867            );
868        });
869        search_bar.read_with(cx, |search_bar, _| {
870            assert_eq!(search_bar.active_match_index, Some(0));
871        });
872
873        // Park the cursor before the first match and ensure that going to the previous match
874        // selects the last match.
875        editor.update(cx, |editor, cx| {
876            editor.select_display_ranges(&[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)], cx);
877        });
878        search_bar.update(cx, |search_bar, cx| {
879            assert_eq!(search_bar.active_match_index, Some(0));
880            search_bar.select_prev_match(&SelectPrevMatch, cx);
881            assert_eq!(
882                editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
883                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
884            );
885        });
886        search_bar.read_with(cx, |search_bar, _| {
887            assert_eq!(search_bar.active_match_index, Some(2));
888        });
889    }
890}