buffer_search.rs

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