buffer_search.rs

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