buffer_search.rs

   1use crate::{
   2    SearchOptions, SelectAllMatches, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive,
   3    ToggleRegex, ToggleWholeWord,
   4};
   5use collections::HashMap;
   6use editor::Editor;
   7use futures::channel::oneshot;
   8use gpui::{
   9    actions,
  10    elements::*,
  11    impl_actions,
  12    platform::{CursorStyle, MouseButton},
  13    Action, AnyViewHandle, AppContext, Entity, Subscription, Task, View, ViewContext, ViewHandle,
  14    WindowContext,
  15};
  16use project::search::SearchQuery;
  17use serde::Deserialize;
  18use std::{any::Any, sync::Arc};
  19use util::ResultExt;
  20use workspace::{
  21    item::ItemHandle,
  22    searchable::{Direction, SearchEvent, SearchableItemHandle, WeakSearchableItemHandle},
  23    Pane, ToolbarItemLocation, ToolbarItemView,
  24};
  25
  26#[derive(Clone, Deserialize, PartialEq)]
  27pub struct Deploy {
  28    pub focus: bool,
  29}
  30
  31actions!(buffer_search, [Dismiss, FocusEditor]);
  32impl_actions!(buffer_search, [Deploy]);
  33
  34pub enum Event {
  35    UpdateLocation,
  36}
  37
  38pub fn init(cx: &mut AppContext) {
  39    cx.add_action(BufferSearchBar::deploy);
  40    cx.add_action(BufferSearchBar::dismiss);
  41    cx.add_action(BufferSearchBar::focus_editor);
  42    cx.add_action(BufferSearchBar::select_next_match);
  43    cx.add_action(BufferSearchBar::select_prev_match);
  44    cx.add_action(BufferSearchBar::select_all_matches);
  45    cx.add_action(BufferSearchBar::select_next_match_on_pane);
  46    cx.add_action(BufferSearchBar::select_prev_match_on_pane);
  47    cx.add_action(BufferSearchBar::select_all_matches_on_pane);
  48    cx.add_action(BufferSearchBar::handle_editor_cancel);
  49    add_toggle_option_action::<ToggleCaseSensitive>(SearchOptions::CASE_SENSITIVE, cx);
  50    add_toggle_option_action::<ToggleWholeWord>(SearchOptions::WHOLE_WORD, cx);
  51    add_toggle_option_action::<ToggleRegex>(SearchOptions::REGEX, cx);
  52}
  53
  54fn add_toggle_option_action<A: Action>(option: SearchOptions, cx: &mut AppContext) {
  55    cx.add_action(move |pane: &mut Pane, _: &A, cx: &mut ViewContext<Pane>| {
  56        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
  57            search_bar.update(cx, |search_bar, cx| {
  58                if search_bar.show(cx) {
  59                    search_bar.toggle_search_option(option, cx);
  60                }
  61            });
  62        }
  63        cx.propagate_action();
  64    });
  65}
  66
  67pub struct BufferSearchBar {
  68    pub query_editor: ViewHandle<Editor>,
  69    active_searchable_item: Option<Box<dyn SearchableItemHandle>>,
  70    active_match_index: Option<usize>,
  71    active_searchable_item_subscription: Option<Subscription>,
  72    searchable_items_with_matches:
  73        HashMap<Box<dyn WeakSearchableItemHandle>, Vec<Box<dyn Any + Send>>>,
  74    pending_search: Option<Task<()>>,
  75    search_options: SearchOptions,
  76    default_options: SearchOptions,
  77    query_contains_error: bool,
  78    dismissed: bool,
  79}
  80
  81impl Entity for BufferSearchBar {
  82    type Event = Event;
  83}
  84
  85impl View for BufferSearchBar {
  86    fn ui_name() -> &'static str {
  87        "BufferSearchBar"
  88    }
  89
  90    fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
  91        if cx.is_self_focused() {
  92            cx.focus(&self.query_editor);
  93        }
  94    }
  95
  96    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
  97        let theme = theme::current(cx).clone();
  98        let editor_container = if self.query_contains_error {
  99            theme.search.invalid_editor
 100        } else {
 101            theme.search.editor.input.container
 102        };
 103        let supported_options = self
 104            .active_searchable_item
 105            .as_ref()
 106            .map(|active_searchable_item| active_searchable_item.supported_options())
 107            .unwrap_or_default();
 108
 109        Flex::row()
 110            .with_child(
 111                Flex::row()
 112                    .with_child(
 113                        Flex::row()
 114                            .with_child(
 115                                ChildView::new(&self.query_editor, cx)
 116                                    .aligned()
 117                                    .left()
 118                                    .flex(1., true),
 119                            )
 120                            .with_children(self.active_searchable_item.as_ref().and_then(
 121                                |searchable_item| {
 122                                    let matches = self
 123                                        .searchable_items_with_matches
 124                                        .get(&searchable_item.downgrade())?;
 125                                    let message = if let Some(match_ix) = self.active_match_index {
 126                                        format!("{}/{}", match_ix + 1, matches.len())
 127                                    } else {
 128                                        "No matches".to_string()
 129                                    };
 130
 131                                    Some(
 132                                        Label::new(message, theme.search.match_index.text.clone())
 133                                            .contained()
 134                                            .with_style(theme.search.match_index.container)
 135                                            .aligned(),
 136                                    )
 137                                },
 138                            ))
 139                            .contained()
 140                            .with_style(editor_container)
 141                            .aligned()
 142                            .constrained()
 143                            .with_min_width(theme.search.editor.min_width)
 144                            .with_max_width(theme.search.editor.max_width)
 145                            .flex(1., false),
 146                    )
 147                    .with_child(
 148                        Flex::row()
 149                            .with_child(self.render_nav_button("<", Direction::Prev, cx))
 150                            .with_child(self.render_nav_button(">", Direction::Next, cx))
 151                            .with_child(self.render_action_button("Select All", cx))
 152                            .aligned(),
 153                    )
 154                    .with_child(
 155                        Flex::row()
 156                            .with_children(self.render_search_option(
 157                                supported_options.case,
 158                                "Case",
 159                                SearchOptions::CASE_SENSITIVE,
 160                                cx,
 161                            ))
 162                            .with_children(self.render_search_option(
 163                                supported_options.word,
 164                                "Word",
 165                                SearchOptions::WHOLE_WORD,
 166                                cx,
 167                            ))
 168                            .with_children(self.render_search_option(
 169                                supported_options.regex,
 170                                "Regex",
 171                                SearchOptions::REGEX,
 172                                cx,
 173                            ))
 174                            .contained()
 175                            .with_style(theme.search.option_button_group)
 176                            .aligned(),
 177                    )
 178                    .flex(1., true),
 179            )
 180            .with_child(self.render_close_button(&theme.search, cx))
 181            .contained()
 182            .with_style(theme.search.container)
 183            .into_any_named("search bar")
 184    }
 185}
 186
 187impl ToolbarItemView for BufferSearchBar {
 188    fn set_active_pane_item(
 189        &mut self,
 190        item: Option<&dyn ItemHandle>,
 191        cx: &mut ViewContext<Self>,
 192    ) -> ToolbarItemLocation {
 193        cx.notify();
 194        self.active_searchable_item_subscription.take();
 195        self.active_searchable_item.take();
 196        self.pending_search.take();
 197
 198        if let Some(searchable_item_handle) =
 199            item.and_then(|item| item.to_searchable_item_handle(cx))
 200        {
 201            let this = cx.weak_handle();
 202            self.active_searchable_item_subscription =
 203                Some(searchable_item_handle.subscribe_to_search_events(
 204                    cx,
 205                    Box::new(move |search_event, cx| {
 206                        if let Some(this) = this.upgrade(cx) {
 207                            this.update(cx, |this, cx| {
 208                                this.on_active_searchable_item_event(search_event, cx)
 209                            });
 210                        }
 211                    }),
 212                ));
 213
 214            self.active_searchable_item = Some(searchable_item_handle);
 215            let _ = self.update_matches(cx);
 216            if !self.dismissed {
 217                return ToolbarItemLocation::Secondary;
 218            }
 219        }
 220
 221        ToolbarItemLocation::Hidden
 222    }
 223
 224    fn location_for_event(
 225        &self,
 226        _: &Self::Event,
 227        _: ToolbarItemLocation,
 228        _: &AppContext,
 229    ) -> ToolbarItemLocation {
 230        if self.active_searchable_item.is_some() && !self.dismissed {
 231            ToolbarItemLocation::Secondary
 232        } else {
 233            ToolbarItemLocation::Hidden
 234        }
 235    }
 236}
 237
 238impl BufferSearchBar {
 239    pub fn new(cx: &mut ViewContext<Self>) -> Self {
 240        let query_editor = cx.add_view(|cx| {
 241            Editor::auto_height(
 242                2,
 243                Some(Arc::new(|theme| theme.search.editor.input.clone())),
 244                cx,
 245            )
 246        });
 247        cx.subscribe(&query_editor, Self::on_query_editor_event)
 248            .detach();
 249
 250        Self {
 251            query_editor,
 252            active_searchable_item: None,
 253            active_searchable_item_subscription: None,
 254            active_match_index: None,
 255            searchable_items_with_matches: Default::default(),
 256            default_options: SearchOptions::NONE,
 257            search_options: SearchOptions::NONE,
 258            pending_search: None,
 259            query_contains_error: false,
 260            dismissed: true,
 261        }
 262    }
 263
 264    pub fn is_dismissed(&self) -> bool {
 265        self.dismissed
 266    }
 267
 268    pub fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext<Self>) {
 269        self.dismissed = true;
 270        for searchable_item in self.searchable_items_with_matches.keys() {
 271            if let Some(searchable_item) =
 272                WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
 273            {
 274                searchable_item.clear_matches(cx);
 275            }
 276        }
 277        if let Some(active_editor) = self.active_searchable_item.as_ref() {
 278            cx.focus(active_editor.as_any());
 279        }
 280        cx.emit(Event::UpdateLocation);
 281        cx.notify();
 282    }
 283
 284    pub fn show(&mut self, cx: &mut ViewContext<Self>) -> bool {
 285        if self.active_searchable_item.is_none() {
 286            return false;
 287        }
 288        self.dismissed = false;
 289        cx.notify();
 290        cx.emit(Event::UpdateLocation);
 291        true
 292    }
 293
 294    pub fn search_suggested(&mut self, cx: &mut ViewContext<Self>) {
 295        let search = self
 296            .query_suggestion(cx)
 297            .map(|suggestion| self.search(&suggestion, Some(self.default_options), cx));
 298
 299        if let Some(search) = search {
 300            cx.spawn(|this, mut cx| async move {
 301                search.await?;
 302                this.update(&mut cx, |this, cx| this.activate_current_match(cx))
 303            })
 304            .detach_and_log_err(cx);
 305        }
 306    }
 307
 308    pub fn activate_current_match(&mut self, cx: &mut ViewContext<Self>) {
 309        if let Some(match_ix) = self.active_match_index {
 310            if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
 311                if let Some(matches) = self
 312                    .searchable_items_with_matches
 313                    .get(&active_searchable_item.downgrade())
 314                {
 315                    active_searchable_item.activate_match(match_ix, matches, cx)
 316                }
 317            }
 318        }
 319    }
 320
 321    pub fn select_query(&mut self, cx: &mut ViewContext<Self>) {
 322        self.query_editor.update(cx, |query_editor, cx| {
 323            query_editor.select_all(&Default::default(), cx);
 324        });
 325    }
 326
 327    pub fn query(&self, cx: &WindowContext) -> String {
 328        self.query_editor.read(cx).text(cx)
 329    }
 330
 331    pub fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> Option<String> {
 332        self.active_searchable_item
 333            .as_ref()
 334            .map(|searchable_item| searchable_item.query_suggestion(cx))
 335    }
 336
 337    pub fn search(
 338        &mut self,
 339        query: &str,
 340        options: Option<SearchOptions>,
 341        cx: &mut ViewContext<Self>,
 342    ) -> oneshot::Receiver<()> {
 343        let options = options.unwrap_or(self.default_options);
 344        if query != self.query_editor.read(cx).text(cx) || self.search_options != options {
 345            self.query_editor.update(cx, |query_editor, cx| {
 346                query_editor.buffer().update(cx, |query_buffer, cx| {
 347                    let len = query_buffer.len(cx);
 348                    query_buffer.edit([(0..len, query)], None, cx);
 349                });
 350            });
 351            self.search_options = options;
 352            self.query_contains_error = false;
 353            self.clear_matches(cx);
 354            cx.notify();
 355        }
 356        self.update_matches(cx)
 357    }
 358
 359    fn render_search_option(
 360        &self,
 361        option_supported: bool,
 362        icon: &'static str,
 363        option: SearchOptions,
 364        cx: &mut ViewContext<Self>,
 365    ) -> Option<AnyElement<Self>> {
 366        if !option_supported {
 367            return None;
 368        }
 369
 370        let tooltip_style = theme::current(cx).tooltip.clone();
 371        let is_active = self.search_options.contains(option);
 372        Some(
 373            MouseEventHandler::<Self, _>::new(option.bits as usize, cx, |state, cx| {
 374                let theme = theme::current(cx);
 375                let style = theme
 376                    .search
 377                    .option_button
 378                    .in_state(is_active)
 379                    .style_for(state);
 380                Label::new(icon, style.text.clone())
 381                    .contained()
 382                    .with_style(style.container)
 383            })
 384            .on_click(MouseButton::Left, move |_, this, cx| {
 385                this.toggle_search_option(option, cx);
 386            })
 387            .with_cursor_style(CursorStyle::PointingHand)
 388            .with_tooltip::<Self>(
 389                option.bits as usize,
 390                format!("Toggle {}", option.label()),
 391                Some(option.to_toggle_action()),
 392                tooltip_style,
 393                cx,
 394            )
 395            .into_any(),
 396        )
 397    }
 398
 399    fn render_nav_button(
 400        &self,
 401        icon: &'static str,
 402        direction: Direction,
 403        cx: &mut ViewContext<Self>,
 404    ) -> AnyElement<Self> {
 405        let action: Box<dyn Action>;
 406        let tooltip;
 407        match direction {
 408            Direction::Prev => {
 409                action = Box::new(SelectPrevMatch);
 410                tooltip = "Select Previous Match";
 411            }
 412            Direction::Next => {
 413                action = Box::new(SelectNextMatch);
 414                tooltip = "Select Next Match";
 415            }
 416        };
 417        let tooltip_style = theme::current(cx).tooltip.clone();
 418
 419        enum NavButton {}
 420        MouseEventHandler::<NavButton, _>::new(direction as usize, cx, |state, cx| {
 421            let theme = theme::current(cx);
 422            let style = theme.search.option_button.inactive_state().style_for(state);
 423            Label::new(icon, style.text.clone())
 424                .contained()
 425                .with_style(style.container)
 426        })
 427        .on_click(MouseButton::Left, {
 428            move |_, this, cx| match direction {
 429                Direction::Prev => this.select_prev_match(&Default::default(), cx),
 430                Direction::Next => this.select_next_match(&Default::default(), cx),
 431            }
 432        })
 433        .with_cursor_style(CursorStyle::PointingHand)
 434        .with_tooltip::<NavButton>(
 435            direction as usize,
 436            tooltip.to_string(),
 437            Some(action),
 438            tooltip_style,
 439            cx,
 440        )
 441        .into_any()
 442    }
 443
 444    fn render_action_button(
 445        &self,
 446        icon: &'static str,
 447        cx: &mut ViewContext<Self>,
 448    ) -> AnyElement<Self> {
 449        let tooltip = "Select All Matches";
 450        let tooltip_style = theme::current(cx).tooltip.clone();
 451        let action_type_id = 0_usize;
 452
 453        enum ActionButton {}
 454        MouseEventHandler::<ActionButton, _>::new(action_type_id, cx, |state, cx| {
 455            let theme = theme::current(cx);
 456            let style = theme.search.action_button.style_for(state);
 457            Label::new(icon, style.text.clone())
 458                .contained()
 459                .with_style(style.container)
 460        })
 461        .on_click(MouseButton::Left, move |_, this, cx| {
 462            this.select_all_matches(&SelectAllMatches, cx)
 463        })
 464        .with_cursor_style(CursorStyle::PointingHand)
 465        .with_tooltip::<ActionButton>(
 466            action_type_id,
 467            tooltip.to_string(),
 468            Some(Box::new(SelectAllMatches)),
 469            tooltip_style,
 470            cx,
 471        )
 472        .into_any()
 473    }
 474
 475    fn render_close_button(
 476        &self,
 477        theme: &theme::Search,
 478        cx: &mut ViewContext<Self>,
 479    ) -> AnyElement<Self> {
 480        let tooltip = "Dismiss Buffer Search";
 481        let tooltip_style = theme::current(cx).tooltip.clone();
 482
 483        enum CloseButton {}
 484        MouseEventHandler::<CloseButton, _>::new(0, cx, |state, _| {
 485            let style = theme.dismiss_button.style_for(state);
 486            Svg::new("icons/x_mark_8.svg")
 487                .with_color(style.color)
 488                .constrained()
 489                .with_width(style.icon_width)
 490                .aligned()
 491                .constrained()
 492                .with_width(style.button_width)
 493                .contained()
 494                .with_style(style.container)
 495        })
 496        .on_click(MouseButton::Left, move |_, this, cx| {
 497            this.dismiss(&Default::default(), cx)
 498        })
 499        .with_cursor_style(CursorStyle::PointingHand)
 500        .with_tooltip::<CloseButton>(
 501            0,
 502            tooltip.to_string(),
 503            Some(Box::new(Dismiss)),
 504            tooltip_style,
 505            cx,
 506        )
 507        .into_any()
 508    }
 509
 510    fn deploy(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext<Pane>) {
 511        let mut propagate_action = true;
 512        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
 513            search_bar.update(cx, |search_bar, cx| {
 514                if search_bar.show(cx) {
 515                    search_bar.search_suggested(cx);
 516                    if action.focus {
 517                        search_bar.select_query(cx);
 518                        cx.focus_self();
 519                    }
 520                    propagate_action = false;
 521                }
 522            });
 523        }
 524
 525        if propagate_action {
 526            cx.propagate_action();
 527        }
 528    }
 529
 530    fn handle_editor_cancel(pane: &mut Pane, _: &editor::Cancel, cx: &mut ViewContext<Pane>) {
 531        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
 532            if !search_bar.read(cx).dismissed {
 533                search_bar.update(cx, |search_bar, cx| search_bar.dismiss(&Dismiss, cx));
 534                return;
 535            }
 536        }
 537        cx.propagate_action();
 538    }
 539
 540    pub fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext<Self>) {
 541        if let Some(active_editor) = self.active_searchable_item.as_ref() {
 542            cx.focus(active_editor.as_any());
 543        }
 544    }
 545
 546    fn toggle_search_option(&mut self, search_option: SearchOptions, cx: &mut ViewContext<Self>) {
 547        self.search_options.toggle(search_option);
 548        self.default_options = self.search_options;
 549        let _ = self.update_matches(cx);
 550        cx.notify();
 551    }
 552
 553    pub fn set_search_options(
 554        &mut self,
 555        search_options: SearchOptions,
 556        cx: &mut ViewContext<Self>,
 557    ) {
 558        self.search_options = search_options;
 559        cx.notify();
 560    }
 561
 562    fn select_next_match(&mut self, _: &SelectNextMatch, cx: &mut ViewContext<Self>) {
 563        self.select_match(Direction::Next, 1, cx);
 564    }
 565
 566    fn select_prev_match(&mut self, _: &SelectPrevMatch, cx: &mut ViewContext<Self>) {
 567        self.select_match(Direction::Prev, 1, cx);
 568    }
 569
 570    fn select_all_matches(&mut self, _: &SelectAllMatches, cx: &mut ViewContext<Self>) {
 571        if !self.dismissed && self.active_match_index.is_some() {
 572            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
 573                if let Some(matches) = self
 574                    .searchable_items_with_matches
 575                    .get(&searchable_item.downgrade())
 576                {
 577                    searchable_item.select_matches(matches, cx);
 578                    self.focus_editor(&FocusEditor, cx);
 579                }
 580            }
 581        }
 582    }
 583
 584    pub fn select_match(&mut self, direction: Direction, count: usize, cx: &mut ViewContext<Self>) {
 585        if let Some(index) = self.active_match_index {
 586            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
 587                if let Some(matches) = self
 588                    .searchable_items_with_matches
 589                    .get(&searchable_item.downgrade())
 590                {
 591                    let new_match_index = searchable_item
 592                        .match_index_for_direction(matches, index, direction, count, cx);
 593                    searchable_item.update_matches(matches, cx);
 594                    searchable_item.activate_match(new_match_index, matches, cx);
 595                }
 596            }
 597        }
 598    }
 599
 600    fn select_next_match_on_pane(
 601        pane: &mut Pane,
 602        action: &SelectNextMatch,
 603        cx: &mut ViewContext<Pane>,
 604    ) {
 605        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
 606            search_bar.update(cx, |bar, cx| bar.select_next_match(action, cx));
 607        }
 608    }
 609
 610    fn select_prev_match_on_pane(
 611        pane: &mut Pane,
 612        action: &SelectPrevMatch,
 613        cx: &mut ViewContext<Pane>,
 614    ) {
 615        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
 616            search_bar.update(cx, |bar, cx| bar.select_prev_match(action, cx));
 617        }
 618    }
 619
 620    fn select_all_matches_on_pane(
 621        pane: &mut Pane,
 622        action: &SelectAllMatches,
 623        cx: &mut ViewContext<Pane>,
 624    ) {
 625        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
 626            search_bar.update(cx, |bar, cx| bar.select_all_matches(action, cx));
 627        }
 628    }
 629
 630    fn on_query_editor_event(
 631        &mut self,
 632        _: ViewHandle<Editor>,
 633        event: &editor::Event,
 634        cx: &mut ViewContext<Self>,
 635    ) {
 636        if let editor::Event::Edited { .. } = event {
 637            self.query_contains_error = false;
 638            self.clear_matches(cx);
 639            let search = self.update_matches(cx);
 640            cx.spawn(|this, mut cx| async move {
 641                search.await?;
 642                this.update(&mut cx, |this, cx| this.activate_current_match(cx))
 643            })
 644            .detach_and_log_err(cx);
 645        }
 646    }
 647
 648    fn on_active_searchable_item_event(&mut self, event: SearchEvent, cx: &mut ViewContext<Self>) {
 649        match event {
 650            SearchEvent::MatchesInvalidated => {
 651                let _ = self.update_matches(cx);
 652            }
 653            SearchEvent::ActiveMatchChanged => self.update_match_index(cx),
 654        }
 655    }
 656
 657    fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
 658        let mut active_item_matches = None;
 659        for (searchable_item, matches) in self.searchable_items_with_matches.drain() {
 660            if let Some(searchable_item) =
 661                WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
 662            {
 663                if Some(&searchable_item) == self.active_searchable_item.as_ref() {
 664                    active_item_matches = Some((searchable_item.downgrade(), matches));
 665                } else {
 666                    searchable_item.clear_matches(cx);
 667                }
 668            }
 669        }
 670
 671        self.searchable_items_with_matches
 672            .extend(active_item_matches);
 673    }
 674
 675    fn update_matches(&mut self, cx: &mut ViewContext<Self>) -> oneshot::Receiver<()> {
 676        let (done_tx, done_rx) = oneshot::channel();
 677        let query = self.query_editor.read(cx).text(cx);
 678        self.pending_search.take();
 679        if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
 680            if query.is_empty() {
 681                self.active_match_index.take();
 682                active_searchable_item.clear_matches(cx);
 683                let _ = done_tx.send(());
 684            } else {
 685                let query = if self.search_options.contains(SearchOptions::REGEX) {
 686                    match SearchQuery::regex(
 687                        query,
 688                        self.search_options.contains(SearchOptions::WHOLE_WORD),
 689                        self.search_options.contains(SearchOptions::CASE_SENSITIVE),
 690                        Vec::new(),
 691                        Vec::new(),
 692                    ) {
 693                        Ok(query) => query,
 694                        Err(_) => {
 695                            self.query_contains_error = true;
 696                            cx.notify();
 697                            return done_rx;
 698                        }
 699                    }
 700                } else {
 701                    SearchQuery::text(
 702                        query,
 703                        self.search_options.contains(SearchOptions::WHOLE_WORD),
 704                        self.search_options.contains(SearchOptions::CASE_SENSITIVE),
 705                        Vec::new(),
 706                        Vec::new(),
 707                    )
 708                };
 709
 710                let matches = active_searchable_item.find_matches(query, cx);
 711
 712                let active_searchable_item = active_searchable_item.downgrade();
 713                self.pending_search = Some(cx.spawn(|this, mut cx| async move {
 714                    let matches = matches.await;
 715                    this.update(&mut cx, |this, cx| {
 716                        if let Some(active_searchable_item) =
 717                            WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx)
 718                        {
 719                            this.searchable_items_with_matches
 720                                .insert(active_searchable_item.downgrade(), matches);
 721
 722                            this.update_match_index(cx);
 723                            if !this.dismissed {
 724                                let matches = this
 725                                    .searchable_items_with_matches
 726                                    .get(&active_searchable_item.downgrade())
 727                                    .unwrap();
 728                                active_searchable_item.update_matches(matches, cx);
 729                                let _ = done_tx.send(());
 730                            }
 731                            cx.notify();
 732                        }
 733                    })
 734                    .log_err();
 735                }));
 736            }
 737        }
 738        done_rx
 739    }
 740
 741    fn update_match_index(&mut self, cx: &mut ViewContext<Self>) {
 742        let new_index = self
 743            .active_searchable_item
 744            .as_ref()
 745            .and_then(|searchable_item| {
 746                let matches = self
 747                    .searchable_items_with_matches
 748                    .get(&searchable_item.downgrade())?;
 749                searchable_item.active_match_index(matches, cx)
 750            });
 751        if new_index != self.active_match_index {
 752            self.active_match_index = new_index;
 753            cx.notify();
 754        }
 755    }
 756}
 757
 758#[cfg(test)]
 759mod tests {
 760    use super::*;
 761    use editor::{DisplayPoint, Editor};
 762    use gpui::{color::Color, test::EmptyView, TestAppContext};
 763    use language::Buffer;
 764    use unindent::Unindent as _;
 765
 766    fn init_test(cx: &mut TestAppContext) -> (ViewHandle<Editor>, ViewHandle<BufferSearchBar>) {
 767        crate::project_search::tests::init_test(cx);
 768
 769        let buffer = cx.add_model(|cx| {
 770            Buffer::new(
 771                0,
 772                r#"
 773                A regular expression (shortened as regex or regexp;[1] also referred to as
 774                rational expression[2][3]) is a sequence of characters that specifies a search
 775                pattern in text. Usually such patterns are used by string-searching algorithms
 776                for "find" or "find and replace" operations on strings, or for input validation.
 777                "#
 778                .unindent(),
 779                cx,
 780            )
 781        });
 782        let (window_id, _root_view) = cx.add_window(|_| EmptyView);
 783
 784        let editor = cx.add_view(window_id, |cx| Editor::for_buffer(buffer.clone(), None, cx));
 785
 786        let search_bar = cx.add_view(window_id, |cx| {
 787            let mut search_bar = BufferSearchBar::new(cx);
 788            search_bar.set_active_pane_item(Some(&editor), cx);
 789            search_bar.show(cx);
 790            search_bar
 791        });
 792
 793        (editor, search_bar)
 794    }
 795
 796    #[gpui::test]
 797    async fn test_search_simple(cx: &mut TestAppContext) {
 798        let (editor, search_bar) = init_test(cx);
 799
 800        // Search for a string that appears with different casing.
 801        // By default, search is case-insensitive.
 802        search_bar
 803            .update(cx, |search_bar, cx| search_bar.search("us", None, cx))
 804            .await
 805            .unwrap();
 806        editor.update(cx, |editor, cx| {
 807            assert_eq!(
 808                editor.all_background_highlights(cx),
 809                &[
 810                    (
 811                        DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19),
 812                        Color::red(),
 813                    ),
 814                    (
 815                        DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
 816                        Color::red(),
 817                    ),
 818                ]
 819            );
 820        });
 821
 822        // Switch to a case sensitive search.
 823        search_bar.update(cx, |search_bar, cx| {
 824            search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx);
 825        });
 826        editor.next_notification(cx).await;
 827        editor.update(cx, |editor, cx| {
 828            assert_eq!(
 829                editor.all_background_highlights(cx),
 830                &[(
 831                    DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
 832                    Color::red(),
 833                )]
 834            );
 835        });
 836
 837        // Search for a string that appears both as a whole word and
 838        // within other words. By default, all results are found.
 839        search_bar
 840            .update(cx, |search_bar, cx| search_bar.search("or", None, cx))
 841            .await
 842            .unwrap();
 843        editor.update(cx, |editor, cx| {
 844            assert_eq!(
 845                editor.all_background_highlights(cx),
 846                &[
 847                    (
 848                        DisplayPoint::new(0, 24)..DisplayPoint::new(0, 26),
 849                        Color::red(),
 850                    ),
 851                    (
 852                        DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
 853                        Color::red(),
 854                    ),
 855                    (
 856                        DisplayPoint::new(2, 71)..DisplayPoint::new(2, 73),
 857                        Color::red(),
 858                    ),
 859                    (
 860                        DisplayPoint::new(3, 1)..DisplayPoint::new(3, 3),
 861                        Color::red(),
 862                    ),
 863                    (
 864                        DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
 865                        Color::red(),
 866                    ),
 867                    (
 868                        DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
 869                        Color::red(),
 870                    ),
 871                    (
 872                        DisplayPoint::new(3, 60)..DisplayPoint::new(3, 62),
 873                        Color::red(),
 874                    ),
 875                ]
 876            );
 877        });
 878
 879        // Switch to a whole word search.
 880        search_bar.update(cx, |search_bar, cx| {
 881            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
 882        });
 883        editor.next_notification(cx).await;
 884        editor.update(cx, |editor, cx| {
 885            assert_eq!(
 886                editor.all_background_highlights(cx),
 887                &[
 888                    (
 889                        DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
 890                        Color::red(),
 891                    ),
 892                    (
 893                        DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
 894                        Color::red(),
 895                    ),
 896                    (
 897                        DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
 898                        Color::red(),
 899                    ),
 900                ]
 901            );
 902        });
 903
 904        editor.update(cx, |editor, cx| {
 905            editor.change_selections(None, cx, |s| {
 906                s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
 907            });
 908        });
 909        search_bar.update(cx, |search_bar, cx| {
 910            assert_eq!(search_bar.active_match_index, Some(0));
 911            search_bar.select_next_match(&SelectNextMatch, cx);
 912            assert_eq!(
 913                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
 914                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
 915            );
 916        });
 917        search_bar.read_with(cx, |search_bar, _| {
 918            assert_eq!(search_bar.active_match_index, Some(0));
 919        });
 920
 921        search_bar.update(cx, |search_bar, cx| {
 922            search_bar.select_next_match(&SelectNextMatch, cx);
 923            assert_eq!(
 924                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
 925                [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
 926            );
 927        });
 928        search_bar.read_with(cx, |search_bar, _| {
 929            assert_eq!(search_bar.active_match_index, Some(1));
 930        });
 931
 932        search_bar.update(cx, |search_bar, cx| {
 933            search_bar.select_next_match(&SelectNextMatch, cx);
 934            assert_eq!(
 935                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
 936                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
 937            );
 938        });
 939        search_bar.read_with(cx, |search_bar, _| {
 940            assert_eq!(search_bar.active_match_index, Some(2));
 941        });
 942
 943        search_bar.update(cx, |search_bar, cx| {
 944            search_bar.select_next_match(&SelectNextMatch, cx);
 945            assert_eq!(
 946                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
 947                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
 948            );
 949        });
 950        search_bar.read_with(cx, |search_bar, _| {
 951            assert_eq!(search_bar.active_match_index, Some(0));
 952        });
 953
 954        search_bar.update(cx, |search_bar, cx| {
 955            search_bar.select_prev_match(&SelectPrevMatch, cx);
 956            assert_eq!(
 957                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
 958                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
 959            );
 960        });
 961        search_bar.read_with(cx, |search_bar, _| {
 962            assert_eq!(search_bar.active_match_index, Some(2));
 963        });
 964
 965        search_bar.update(cx, |search_bar, cx| {
 966            search_bar.select_prev_match(&SelectPrevMatch, cx);
 967            assert_eq!(
 968                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
 969                [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
 970            );
 971        });
 972        search_bar.read_with(cx, |search_bar, _| {
 973            assert_eq!(search_bar.active_match_index, Some(1));
 974        });
 975
 976        search_bar.update(cx, |search_bar, cx| {
 977            search_bar.select_prev_match(&SelectPrevMatch, cx);
 978            assert_eq!(
 979                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
 980                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
 981            );
 982        });
 983        search_bar.read_with(cx, |search_bar, _| {
 984            assert_eq!(search_bar.active_match_index, Some(0));
 985        });
 986
 987        // Park the cursor in between matches and ensure that going to the previous match selects
 988        // the closest match to the left.
 989        editor.update(cx, |editor, cx| {
 990            editor.change_selections(None, cx, |s| {
 991                s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
 992            });
 993        });
 994        search_bar.update(cx, |search_bar, cx| {
 995            assert_eq!(search_bar.active_match_index, Some(1));
 996            search_bar.select_prev_match(&SelectPrevMatch, cx);
 997            assert_eq!(
 998                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
 999                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1000            );
1001        });
1002        search_bar.read_with(cx, |search_bar, _| {
1003            assert_eq!(search_bar.active_match_index, Some(0));
1004        });
1005
1006        // Park the cursor in between matches and ensure that going to the next match selects the
1007        // closest match to the right.
1008        editor.update(cx, |editor, cx| {
1009            editor.change_selections(None, cx, |s| {
1010                s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
1011            });
1012        });
1013        search_bar.update(cx, |search_bar, cx| {
1014            assert_eq!(search_bar.active_match_index, Some(1));
1015            search_bar.select_next_match(&SelectNextMatch, cx);
1016            assert_eq!(
1017                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1018                [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
1019            );
1020        });
1021        search_bar.read_with(cx, |search_bar, _| {
1022            assert_eq!(search_bar.active_match_index, Some(1));
1023        });
1024
1025        // Park the cursor after the last match and ensure that going to the previous match selects
1026        // the last match.
1027        editor.update(cx, |editor, cx| {
1028            editor.change_selections(None, cx, |s| {
1029                s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
1030            });
1031        });
1032        search_bar.update(cx, |search_bar, cx| {
1033            assert_eq!(search_bar.active_match_index, Some(2));
1034            search_bar.select_prev_match(&SelectPrevMatch, cx);
1035            assert_eq!(
1036                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1037                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1038            );
1039        });
1040        search_bar.read_with(cx, |search_bar, _| {
1041            assert_eq!(search_bar.active_match_index, Some(2));
1042        });
1043
1044        // Park the cursor after the last match and ensure that going to the next match selects the
1045        // first match.
1046        editor.update(cx, |editor, cx| {
1047            editor.change_selections(None, cx, |s| {
1048                s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
1049            });
1050        });
1051        search_bar.update(cx, |search_bar, cx| {
1052            assert_eq!(search_bar.active_match_index, Some(2));
1053            search_bar.select_next_match(&SelectNextMatch, cx);
1054            assert_eq!(
1055                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1056                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1057            );
1058        });
1059        search_bar.read_with(cx, |search_bar, _| {
1060            assert_eq!(search_bar.active_match_index, Some(0));
1061        });
1062
1063        // Park the cursor before the first match and ensure that going to the previous match
1064        // selects the last match.
1065        editor.update(cx, |editor, cx| {
1066            editor.change_selections(None, cx, |s| {
1067                s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
1068            });
1069        });
1070        search_bar.update(cx, |search_bar, cx| {
1071            assert_eq!(search_bar.active_match_index, Some(0));
1072            search_bar.select_prev_match(&SelectPrevMatch, cx);
1073            assert_eq!(
1074                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1075                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1076            );
1077        });
1078        search_bar.read_with(cx, |search_bar, _| {
1079            assert_eq!(search_bar.active_match_index, Some(2));
1080        });
1081    }
1082
1083    #[gpui::test]
1084    async fn test_search_option_handling(cx: &mut TestAppContext) {
1085        let (editor, search_bar) = init_test(cx);
1086
1087        // show with options should make current search case sensitive
1088        search_bar
1089            .update(cx, |search_bar, cx| {
1090                search_bar.show(cx);
1091                search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), cx)
1092            })
1093            .await
1094            .unwrap();
1095        editor.update(cx, |editor, cx| {
1096            assert_eq!(
1097                editor.all_background_highlights(cx),
1098                &[(
1099                    DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
1100                    Color::red(),
1101                )]
1102            );
1103        });
1104
1105        // search_suggested should restore default options
1106        search_bar.update(cx, |search_bar, cx| {
1107            search_bar.search_suggested(cx);
1108            assert_eq!(search_bar.search_options, SearchOptions::NONE)
1109        });
1110
1111        // toggling a search option should update the defaults
1112        search_bar
1113            .update(cx, |search_bar, cx| {
1114                search_bar.search("regex", Some(SearchOptions::CASE_SENSITIVE), cx)
1115            })
1116            .await
1117            .unwrap();
1118        search_bar.update(cx, |search_bar, cx| {
1119            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx)
1120        });
1121        editor.next_notification(cx).await;
1122        editor.update(cx, |editor, cx| {
1123            assert_eq!(
1124                editor.all_background_highlights(cx),
1125                &[(
1126                    DisplayPoint::new(0, 35)..DisplayPoint::new(0, 40),
1127                    Color::red(),
1128                ),]
1129            );
1130        });
1131
1132        // defaults should still include whole word
1133        search_bar.update(cx, |search_bar, cx| {
1134            search_bar.search_suggested(cx);
1135            assert_eq!(
1136                search_bar.search_options,
1137                SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD
1138            )
1139        });
1140    }
1141
1142    #[gpui::test]
1143    async fn test_search_select_all_matches(cx: &mut TestAppContext) {
1144        crate::project_search::tests::init_test(cx);
1145
1146        let buffer_text = r#"
1147        A regular expression (shortened as regex or regexp;[1] also referred to as
1148        rational expression[2][3]) is a sequence of characters that specifies a search
1149        pattern in text. Usually such patterns are used by string-searching algorithms
1150        for "find" or "find and replace" operations on strings, or for input validation.
1151        "#
1152        .unindent();
1153        let expected_query_matches_count = buffer_text
1154            .chars()
1155            .filter(|c| c.to_ascii_lowercase() == 'a')
1156            .count();
1157        assert!(
1158            expected_query_matches_count > 1,
1159            "Should pick a query with multiple results"
1160        );
1161        let buffer = cx.add_model(|cx| Buffer::new(0, buffer_text, cx));
1162        let (window_id, _root_view) = cx.add_window(|_| EmptyView);
1163
1164        let editor = cx.add_view(window_id, |cx| Editor::for_buffer(buffer.clone(), None, cx));
1165
1166        let search_bar = cx.add_view(window_id, |cx| {
1167            let mut search_bar = BufferSearchBar::new(cx);
1168            search_bar.set_active_pane_item(Some(&editor), cx);
1169            search_bar.show(cx);
1170            search_bar
1171        });
1172
1173        search_bar
1174            .update(cx, |search_bar, cx| search_bar.search("a", None, cx))
1175            .await
1176            .unwrap();
1177        search_bar.update(cx, |search_bar, cx| {
1178            cx.focus(search_bar.query_editor.as_any());
1179            search_bar.activate_current_match(cx);
1180        });
1181
1182        cx.read_window(window_id, |cx| {
1183            assert!(
1184                !editor.is_focused(cx),
1185                "Initially, the editor should not be focused"
1186            );
1187        });
1188        let initial_selections = editor.update(cx, |editor, cx| {
1189            let initial_selections = editor.selections.display_ranges(cx);
1190            assert_eq!(
1191                initial_selections.len(), 1,
1192                "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
1193            );
1194            initial_selections
1195        });
1196        search_bar.update(cx, |search_bar, _| {
1197            assert_eq!(search_bar.active_match_index, Some(0));
1198        });
1199
1200        search_bar.update(cx, |search_bar, cx| {
1201            cx.focus(search_bar.query_editor.as_any());
1202            search_bar.select_all_matches(&SelectAllMatches, cx);
1203        });
1204        cx.read_window(window_id, |cx| {
1205            assert!(
1206                editor.is_focused(cx),
1207                "Should focus editor after successful SelectAllMatches"
1208            );
1209        });
1210        search_bar.update(cx, |search_bar, cx| {
1211            let all_selections =
1212                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1213            assert_eq!(
1214                all_selections.len(),
1215                expected_query_matches_count,
1216                "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1217            );
1218            assert_eq!(
1219                search_bar.active_match_index,
1220                Some(0),
1221                "Match index should not change after selecting all matches"
1222            );
1223        });
1224
1225        search_bar.update(cx, |search_bar, cx| {
1226            search_bar.select_next_match(&SelectNextMatch, cx);
1227        });
1228        cx.read_window(window_id, |cx| {
1229            assert!(
1230                editor.is_focused(cx),
1231                "Should still have editor focused after SelectNextMatch"
1232            );
1233        });
1234        search_bar.update(cx, |search_bar, cx| {
1235            let all_selections =
1236                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1237            assert_eq!(
1238                all_selections.len(),
1239                1,
1240                "On next match, should deselect items and select the next match"
1241            );
1242            assert_ne!(
1243                all_selections, initial_selections,
1244                "Next match should be different from the first selection"
1245            );
1246            assert_eq!(
1247                search_bar.active_match_index,
1248                Some(1),
1249                "Match index should be updated to the next one"
1250            );
1251        });
1252
1253        search_bar.update(cx, |search_bar, cx| {
1254            cx.focus(search_bar.query_editor.as_any());
1255            search_bar.select_all_matches(&SelectAllMatches, cx);
1256        });
1257        cx.read_window(window_id, |cx| {
1258            assert!(
1259                editor.is_focused(cx),
1260                "Should focus editor after successful SelectAllMatches"
1261            );
1262        });
1263        search_bar.update(cx, |search_bar, cx| {
1264            let all_selections =
1265                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1266            assert_eq!(
1267                all_selections.len(),
1268                expected_query_matches_count,
1269                "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1270            );
1271            assert_eq!(
1272                search_bar.active_match_index,
1273                Some(1),
1274                "Match index should not change after selecting all matches"
1275            );
1276        });
1277
1278        search_bar.update(cx, |search_bar, cx| {
1279            search_bar.select_prev_match(&SelectPrevMatch, cx);
1280        });
1281        cx.read_window(window_id, |cx| {
1282            assert!(
1283                editor.is_focused(cx),
1284                "Should still have editor focused after SelectPrevMatch"
1285            );
1286        });
1287        let last_match_selections = search_bar.update(cx, |search_bar, cx| {
1288            let all_selections =
1289                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1290            assert_eq!(
1291                all_selections.len(),
1292                1,
1293                "On previous match, should deselect items and select the previous item"
1294            );
1295            assert_eq!(
1296                all_selections, initial_selections,
1297                "Previous match should be the same as the first selection"
1298            );
1299            assert_eq!(
1300                search_bar.active_match_index,
1301                Some(0),
1302                "Match index should be updated to the previous one"
1303            );
1304            all_selections
1305        });
1306
1307        search_bar
1308            .update(cx, |search_bar, cx| {
1309                cx.focus(search_bar.query_editor.as_any());
1310                search_bar.search("abas_nonexistent_match", None, cx)
1311            })
1312            .await
1313            .unwrap();
1314        search_bar.update(cx, |search_bar, cx| {
1315            search_bar.select_all_matches(&SelectAllMatches, cx);
1316        });
1317        cx.read_window(window_id, |cx| {
1318            assert!(
1319                !editor.is_focused(cx),
1320                "Should not switch focus to editor if SelectAllMatches does not find any matches"
1321            );
1322        });
1323        search_bar.update(cx, |search_bar, cx| {
1324            let all_selections =
1325                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1326            assert_eq!(
1327                all_selections, last_match_selections,
1328                "Should not select anything new if there are no matches"
1329            );
1330            assert!(
1331                search_bar.active_match_index.is_none(),
1332                "For no matches, there should be no active match index"
1333            );
1334        });
1335    }
1336}