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