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