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