buffer_search.rs

   1use crate::{
   2    SearchOption, SelectAllMatches, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive,
   3    ToggleRegex, ToggleWholeWord,
   4};
   5use collections::HashMap;
   6use editor::Editor;
   7use gpui::{
   8    actions,
   9    elements::*,
  10    impl_actions,
  11    platform::{CursorStyle, MouseButton},
  12    Action, AnyViewHandle, AppContext, Entity, Subscription, Task, View, ViewContext, ViewHandle,
  13};
  14use project::search::SearchQuery;
  15use serde::Deserialize;
  16use std::{any::Any, sync::Arc};
  17use util::ResultExt;
  18use workspace::{
  19    item::ItemHandle,
  20    searchable::{Direction, SearchEvent, SearchableItemHandle, WeakSearchableItemHandle},
  21    Pane, ToolbarItemLocation, ToolbarItemView,
  22};
  23
  24#[derive(Clone, Deserialize, PartialEq)]
  25pub struct Deploy {
  26    pub focus: bool,
  27}
  28
  29actions!(buffer_search, [Dismiss, FocusEditor]);
  30impl_actions!(buffer_search, [Deploy]);
  31
  32pub enum Event {
  33    UpdateLocation,
  34}
  35
  36pub fn init(cx: &mut AppContext) {
  37    cx.add_action(BufferSearchBar::deploy);
  38    cx.add_action(BufferSearchBar::dismiss);
  39    cx.add_action(BufferSearchBar::focus_editor);
  40    cx.add_action(BufferSearchBar::select_next_match);
  41    cx.add_action(BufferSearchBar::select_prev_match);
  42    cx.add_action(BufferSearchBar::select_all_matches);
  43    cx.add_action(BufferSearchBar::select_next_match_on_pane);
  44    cx.add_action(BufferSearchBar::select_prev_match_on_pane);
  45    cx.add_action(BufferSearchBar::select_all_matches_on_pane);
  46    cx.add_action(BufferSearchBar::handle_editor_cancel);
  47    add_toggle_option_action::<ToggleCaseSensitive>(SearchOption::CaseSensitive, cx);
  48    add_toggle_option_action::<ToggleWholeWord>(SearchOption::WholeWord, cx);
  49    add_toggle_option_action::<ToggleRegex>(SearchOption::Regex, cx);
  50}
  51
  52fn add_toggle_option_action<A: Action>(option: SearchOption, cx: &mut AppContext) {
  53    cx.add_action(move |pane: &mut Pane, _: &A, cx: &mut ViewContext<Pane>| {
  54        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
  55            if search_bar.update(cx, |search_bar, cx| search_bar.show(false, false, cx)) {
  56                search_bar.update(cx, |search_bar, cx| {
  57                    search_bar.toggle_search_option(option, cx);
  58                });
  59                return;
  60            }
  61        }
  62        cx.propagate_action();
  63    });
  64}
  65
  66pub struct BufferSearchBar {
  67    pub query_editor: ViewHandle<Editor>,
  68    active_searchable_item: Option<Box<dyn SearchableItemHandle>>,
  69    active_match_index: Option<usize>,
  70    active_searchable_item_subscription: Option<Subscription>,
  71    searchable_items_with_matches:
  72        HashMap<Box<dyn WeakSearchableItemHandle>, Vec<Box<dyn Any + Send>>>,
  73    pending_search: Option<Task<()>>,
  74    case_sensitive: bool,
  75    whole_word: bool,
  76    regex: bool,
  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                            .aligned(),
 152                    )
 153                    .with_child(
 154                        Flex::row()
 155                            .with_children(self.render_search_option(
 156                                supported_options.case,
 157                                "Case",
 158                                SearchOption::CaseSensitive,
 159                                cx,
 160                            ))
 161                            .with_children(self.render_search_option(
 162                                supported_options.word,
 163                                "Word",
 164                                SearchOption::WholeWord,
 165                                cx,
 166                            ))
 167                            .with_children(self.render_search_option(
 168                                supported_options.regex,
 169                                "Regex",
 170                                SearchOption::Regex,
 171                                cx,
 172                            ))
 173                            .contained()
 174                            .with_style(theme.search.option_button_group)
 175                            .aligned(),
 176                    )
 177                    .flex(1., true),
 178            )
 179            .with_child(self.render_close_button(&theme.search, cx))
 180            .contained()
 181            .with_style(theme.search.container)
 182            .into_any_named("search bar")
 183    }
 184}
 185
 186impl ToolbarItemView for BufferSearchBar {
 187    fn set_active_pane_item(
 188        &mut self,
 189        item: Option<&dyn ItemHandle>,
 190        cx: &mut ViewContext<Self>,
 191    ) -> ToolbarItemLocation {
 192        cx.notify();
 193        self.active_searchable_item_subscription.take();
 194        self.active_searchable_item.take();
 195        self.pending_search.take();
 196
 197        if let Some(searchable_item_handle) =
 198            item.and_then(|item| item.to_searchable_item_handle(cx))
 199        {
 200            let this = cx.weak_handle();
 201            self.active_searchable_item_subscription =
 202                Some(searchable_item_handle.subscribe_to_search_events(
 203                    cx,
 204                    Box::new(move |search_event, cx| {
 205                        if let Some(this) = this.upgrade(cx) {
 206                            this.update(cx, |this, cx| {
 207                                this.on_active_searchable_item_event(search_event, cx)
 208                            });
 209                        }
 210                    }),
 211                ));
 212
 213            self.active_searchable_item = Some(searchable_item_handle);
 214            self.update_matches(false, cx);
 215            if !self.dismissed {
 216                return ToolbarItemLocation::Secondary;
 217            }
 218        }
 219
 220        ToolbarItemLocation::Hidden
 221    }
 222
 223    fn location_for_event(
 224        &self,
 225        _: &Self::Event,
 226        _: ToolbarItemLocation,
 227        _: &AppContext,
 228    ) -> ToolbarItemLocation {
 229        if self.active_searchable_item.is_some() && !self.dismissed {
 230            ToolbarItemLocation::Secondary
 231        } else {
 232            ToolbarItemLocation::Hidden
 233        }
 234    }
 235}
 236
 237impl BufferSearchBar {
 238    pub fn new(cx: &mut ViewContext<Self>) -> Self {
 239        let query_editor = cx.add_view(|cx| {
 240            Editor::auto_height(
 241                2,
 242                Some(Arc::new(|theme| theme.search.editor.input.clone())),
 243                cx,
 244            )
 245        });
 246        cx.subscribe(&query_editor, Self::on_query_editor_event)
 247            .detach();
 248
 249        Self {
 250            query_editor,
 251            active_searchable_item: None,
 252            active_searchable_item_subscription: None,
 253            active_match_index: None,
 254            searchable_items_with_matches: Default::default(),
 255            case_sensitive: false,
 256            whole_word: false,
 257            regex: false,
 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, focus: bool, suggest_query: bool, cx: &mut ViewContext<Self>) -> bool {
 285        let searchable_item = if let Some(searchable_item) = &self.active_searchable_item {
 286            SearchableItemHandle::boxed_clone(searchable_item.as_ref())
 287        } else {
 288            return false;
 289        };
 290
 291        if suggest_query {
 292            let text = searchable_item.query_suggestion(cx);
 293            if !text.is_empty() {
 294                self.set_query(&text, cx);
 295            }
 296        }
 297
 298        if focus {
 299            let query_editor = self.query_editor.clone();
 300            query_editor.update(cx, |query_editor, cx| {
 301                query_editor.select_all(&editor::SelectAll, cx);
 302            });
 303            cx.focus_self();
 304        }
 305
 306        self.dismissed = false;
 307        cx.notify();
 308        cx.emit(Event::UpdateLocation);
 309        true
 310    }
 311
 312    fn set_query(&mut self, query: &str, cx: &mut ViewContext<Self>) {
 313        self.query_editor.update(cx, |query_editor, cx| {
 314            query_editor.buffer().update(cx, |query_buffer, cx| {
 315                let len = query_buffer.len(cx);
 316                query_buffer.edit([(0..len, query)], None, cx);
 317            });
 318        });
 319    }
 320
 321    fn render_search_option(
 322        &self,
 323        option_supported: bool,
 324        icon: &'static str,
 325        option: SearchOption,
 326        cx: &mut ViewContext<Self>,
 327    ) -> Option<AnyElement<Self>> {
 328        if !option_supported {
 329            return None;
 330        }
 331
 332        let tooltip_style = theme::current(cx).tooltip.clone();
 333        let is_active = self.is_search_option_enabled(option);
 334        Some(
 335            MouseEventHandler::<Self, _>::new(option as usize, cx, |state, cx| {
 336                let theme = theme::current(cx);
 337                let style = theme
 338                    .search
 339                    .option_button
 340                    .in_state(is_active)
 341                    .style_for(state);
 342                Label::new(icon, style.text.clone())
 343                    .contained()
 344                    .with_style(style.container)
 345            })
 346            .on_click(MouseButton::Left, move |_, this, cx| {
 347                this.toggle_search_option(option, cx);
 348            })
 349            .with_cursor_style(CursorStyle::PointingHand)
 350            .with_tooltip::<Self>(
 351                option as usize,
 352                format!("Toggle {}", option.label()),
 353                Some(option.to_toggle_action()),
 354                tooltip_style,
 355                cx,
 356            )
 357            .into_any(),
 358        )
 359    }
 360
 361    fn render_nav_button(
 362        &self,
 363        icon: &'static str,
 364        direction: Direction,
 365        cx: &mut ViewContext<Self>,
 366    ) -> AnyElement<Self> {
 367        let action: Box<dyn Action>;
 368        let tooltip;
 369        match direction {
 370            Direction::Prev => {
 371                action = Box::new(SelectPrevMatch);
 372                tooltip = "Select Previous Match";
 373            }
 374            Direction::Next => {
 375                action = Box::new(SelectNextMatch);
 376                tooltip = "Select Next Match";
 377            }
 378        };
 379        let tooltip_style = theme::current(cx).tooltip.clone();
 380
 381        enum NavButton {}
 382        MouseEventHandler::<NavButton, _>::new(direction as usize, cx, |state, cx| {
 383            let theme = theme::current(cx);
 384            let style = theme.search.option_button.inactive_state().style_for(state);
 385            Label::new(icon, style.text.clone())
 386                .contained()
 387                .with_style(style.container)
 388        })
 389        .on_click(MouseButton::Left, {
 390            move |_, this, cx| match direction {
 391                Direction::Prev => this.select_prev_match(&Default::default(), cx),
 392                Direction::Next => this.select_next_match(&Default::default(), cx),
 393            }
 394        })
 395        .with_cursor_style(CursorStyle::PointingHand)
 396        .with_tooltip::<NavButton>(
 397            direction as usize,
 398            tooltip.to_string(),
 399            Some(action),
 400            tooltip_style,
 401            cx,
 402        )
 403        .into_any()
 404    }
 405
 406    fn render_close_button(
 407        &self,
 408        theme: &theme::Search,
 409        cx: &mut ViewContext<Self>,
 410    ) -> AnyElement<Self> {
 411        let tooltip = "Dismiss Buffer Search";
 412        let tooltip_style = theme::current(cx).tooltip.clone();
 413
 414        enum CloseButton {}
 415        MouseEventHandler::<CloseButton, _>::new(0, cx, |state, _| {
 416            let style = theme.dismiss_button.style_for(state);
 417            Svg::new("icons/x_mark_8.svg")
 418                .with_color(style.color)
 419                .constrained()
 420                .with_width(style.icon_width)
 421                .aligned()
 422                .constrained()
 423                .with_width(style.button_width)
 424                .contained()
 425                .with_style(style.container)
 426        })
 427        .on_click(MouseButton::Left, move |_, this, cx| {
 428            this.dismiss(&Default::default(), cx)
 429        })
 430        .with_cursor_style(CursorStyle::PointingHand)
 431        .with_tooltip::<CloseButton>(
 432            0,
 433            tooltip.to_string(),
 434            Some(Box::new(Dismiss)),
 435            tooltip_style,
 436            cx,
 437        )
 438        .into_any()
 439    }
 440
 441    fn deploy(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext<Pane>) {
 442        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
 443            if search_bar.update(cx, |search_bar, cx| search_bar.show(action.focus, true, cx)) {
 444                return;
 445            }
 446        }
 447        cx.propagate_action();
 448    }
 449
 450    fn handle_editor_cancel(pane: &mut Pane, _: &editor::Cancel, cx: &mut ViewContext<Pane>) {
 451        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
 452            if !search_bar.read(cx).dismissed {
 453                search_bar.update(cx, |search_bar, cx| search_bar.dismiss(&Dismiss, cx));
 454                return;
 455            }
 456        }
 457        cx.propagate_action();
 458    }
 459
 460    fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext<Self>) {
 461        if let Some(active_editor) = self.active_searchable_item.as_ref() {
 462            cx.focus(active_editor.as_any());
 463        }
 464    }
 465
 466    fn is_search_option_enabled(&self, search_option: SearchOption) -> bool {
 467        match search_option {
 468            SearchOption::WholeWord => self.whole_word,
 469            SearchOption::CaseSensitive => self.case_sensitive,
 470            SearchOption::Regex => self.regex,
 471        }
 472    }
 473
 474    fn toggle_search_option(&mut self, search_option: SearchOption, cx: &mut ViewContext<Self>) {
 475        let value = match search_option {
 476            SearchOption::WholeWord => &mut self.whole_word,
 477            SearchOption::CaseSensitive => &mut self.case_sensitive,
 478            SearchOption::Regex => &mut self.regex,
 479        };
 480        *value = !*value;
 481        self.update_matches(false, cx);
 482        cx.notify();
 483    }
 484
 485    fn select_next_match(&mut self, _: &SelectNextMatch, cx: &mut ViewContext<Self>) {
 486        self.select_match(Direction::Next, cx);
 487    }
 488
 489    fn select_prev_match(&mut self, _: &SelectPrevMatch, cx: &mut ViewContext<Self>) {
 490        self.select_match(Direction::Prev, cx);
 491    }
 492
 493    fn select_all_matches(&mut self, _: &SelectAllMatches, cx: &mut ViewContext<Self>) {
 494        if !self.dismissed {
 495            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
 496                if let Some(matches) = self
 497                    .searchable_items_with_matches
 498                    .get(&searchable_item.downgrade())
 499                {
 500                    searchable_item.select_matches(matches, cx);
 501                    self.focus_editor(&FocusEditor, cx);
 502                }
 503            }
 504        }
 505    }
 506
 507    pub fn select_match(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
 508        if let Some(index) = self.active_match_index {
 509            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
 510                if let Some(matches) = self
 511                    .searchable_items_with_matches
 512                    .get(&searchable_item.downgrade())
 513                {
 514                    let new_match_index =
 515                        searchable_item.match_index_for_direction(matches, index, direction, cx);
 516                    searchable_item.update_matches(matches, cx);
 517                    searchable_item.activate_match(new_match_index, matches, cx);
 518                }
 519            }
 520        }
 521    }
 522
 523    fn select_next_match_on_pane(
 524        pane: &mut Pane,
 525        action: &SelectNextMatch,
 526        cx: &mut ViewContext<Pane>,
 527    ) {
 528        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
 529            search_bar.update(cx, |bar, cx| bar.select_next_match(action, cx));
 530        }
 531    }
 532
 533    fn select_prev_match_on_pane(
 534        pane: &mut Pane,
 535        action: &SelectPrevMatch,
 536        cx: &mut ViewContext<Pane>,
 537    ) {
 538        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
 539            search_bar.update(cx, |bar, cx| bar.select_prev_match(action, cx));
 540        }
 541    }
 542
 543    fn select_all_matches_on_pane(
 544        pane: &mut Pane,
 545        action: &SelectAllMatches,
 546        cx: &mut ViewContext<Pane>,
 547    ) {
 548        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
 549            search_bar.update(cx, |bar, cx| bar.select_all_matches(action, cx));
 550        }
 551    }
 552
 553    fn on_query_editor_event(
 554        &mut self,
 555        _: ViewHandle<Editor>,
 556        event: &editor::Event,
 557        cx: &mut ViewContext<Self>,
 558    ) {
 559        if let editor::Event::BufferEdited { .. } = event {
 560            self.query_contains_error = false;
 561            self.clear_matches(cx);
 562            self.update_matches(true, cx);
 563            cx.notify();
 564        }
 565    }
 566
 567    fn on_active_searchable_item_event(&mut self, event: SearchEvent, cx: &mut ViewContext<Self>) {
 568        match event {
 569            SearchEvent::MatchesInvalidated => self.update_matches(false, cx),
 570            SearchEvent::ActiveMatchChanged => self.update_match_index(cx),
 571        }
 572    }
 573
 574    fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
 575        let mut active_item_matches = None;
 576        for (searchable_item, matches) in self.searchable_items_with_matches.drain() {
 577            if let Some(searchable_item) =
 578                WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
 579            {
 580                if Some(&searchable_item) == self.active_searchable_item.as_ref() {
 581                    active_item_matches = Some((searchable_item.downgrade(), matches));
 582                } else {
 583                    searchable_item.clear_matches(cx);
 584                }
 585            }
 586        }
 587
 588        self.searchable_items_with_matches
 589            .extend(active_item_matches);
 590    }
 591
 592    fn update_matches(&mut self, select_closest_match: bool, cx: &mut ViewContext<Self>) {
 593        let query = self.query_editor.read(cx).text(cx);
 594        self.pending_search.take();
 595        if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
 596            if query.is_empty() {
 597                self.active_match_index.take();
 598                active_searchable_item.clear_matches(cx);
 599            } else {
 600                let query = if self.regex {
 601                    match SearchQuery::regex(
 602                        query,
 603                        self.whole_word,
 604                        self.case_sensitive,
 605                        Vec::new(),
 606                        Vec::new(),
 607                    ) {
 608                        Ok(query) => query,
 609                        Err(_) => {
 610                            self.query_contains_error = true;
 611                            cx.notify();
 612                            return;
 613                        }
 614                    }
 615                } else {
 616                    SearchQuery::text(
 617                        query,
 618                        self.whole_word,
 619                        self.case_sensitive,
 620                        Vec::new(),
 621                        Vec::new(),
 622                    )
 623                };
 624
 625                let matches = active_searchable_item.find_matches(query, cx);
 626
 627                let active_searchable_item = active_searchable_item.downgrade();
 628                self.pending_search = Some(cx.spawn(|this, mut cx| async move {
 629                    let matches = matches.await;
 630                    this.update(&mut cx, |this, cx| {
 631                        if let Some(active_searchable_item) =
 632                            WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx)
 633                        {
 634                            this.searchable_items_with_matches
 635                                .insert(active_searchable_item.downgrade(), matches);
 636
 637                            this.update_match_index(cx);
 638                            if !this.dismissed {
 639                                let matches = this
 640                                    .searchable_items_with_matches
 641                                    .get(&active_searchable_item.downgrade())
 642                                    .unwrap();
 643                                active_searchable_item.update_matches(matches, cx);
 644                                if select_closest_match {
 645                                    if let Some(match_ix) = this.active_match_index {
 646                                        active_searchable_item
 647                                            .activate_match(match_ix, matches, cx);
 648                                    }
 649                                }
 650                            }
 651                            cx.notify();
 652                        }
 653                    })
 654                    .log_err();
 655                }));
 656            }
 657        }
 658    }
 659
 660    fn update_match_index(&mut self, cx: &mut ViewContext<Self>) {
 661        let new_index = self
 662            .active_searchable_item
 663            .as_ref()
 664            .and_then(|searchable_item| {
 665                let matches = self
 666                    .searchable_items_with_matches
 667                    .get(&searchable_item.downgrade())?;
 668                searchable_item.active_match_index(matches, cx)
 669            });
 670        if new_index != self.active_match_index {
 671            self.active_match_index = new_index;
 672            cx.notify();
 673        }
 674    }
 675}
 676
 677#[cfg(test)]
 678mod tests {
 679    use super::*;
 680    use editor::{DisplayPoint, Editor};
 681    use gpui::{color::Color, test::EmptyView, TestAppContext};
 682    use language::Buffer;
 683    use unindent::Unindent as _;
 684
 685    #[gpui::test]
 686    async fn test_search_simple(cx: &mut TestAppContext) {
 687        crate::project_search::tests::init_test(cx);
 688
 689        let buffer = cx.add_model(|cx| {
 690            Buffer::new(
 691                0,
 692                r#"
 693                A regular expression (shortened as regex or regexp;[1] also referred to as
 694                rational expression[2][3]) is a sequence of characters that specifies a search
 695                pattern in text. Usually such patterns are used by string-searching algorithms
 696                for "find" or "find and replace" operations on strings, or for input validation.
 697                "#
 698                .unindent(),
 699                cx,
 700            )
 701        });
 702        let (window_id, _root_view) = cx.add_window(|_| EmptyView);
 703
 704        let editor = cx.add_view(window_id, |cx| Editor::for_buffer(buffer.clone(), None, cx));
 705
 706        let search_bar = cx.add_view(window_id, |cx| {
 707            let mut search_bar = BufferSearchBar::new(cx);
 708            search_bar.set_active_pane_item(Some(&editor), cx);
 709            search_bar.show(false, true, cx);
 710            search_bar
 711        });
 712
 713        // Search for a string that appears with different casing.
 714        // By default, search is case-insensitive.
 715        search_bar.update(cx, |search_bar, cx| {
 716            search_bar.set_query("us", cx);
 717        });
 718        editor.next_notification(cx).await;
 719        editor.update(cx, |editor, cx| {
 720            assert_eq!(
 721                editor.all_background_highlights(cx),
 722                &[
 723                    (
 724                        DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19),
 725                        Color::red(),
 726                    ),
 727                    (
 728                        DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
 729                        Color::red(),
 730                    ),
 731                ]
 732            );
 733        });
 734
 735        // Switch to a case sensitive search.
 736        search_bar.update(cx, |search_bar, cx| {
 737            search_bar.toggle_search_option(SearchOption::CaseSensitive, cx);
 738        });
 739        editor.next_notification(cx).await;
 740        editor.update(cx, |editor, cx| {
 741            assert_eq!(
 742                editor.all_background_highlights(cx),
 743                &[(
 744                    DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
 745                    Color::red(),
 746                )]
 747            );
 748        });
 749
 750        // Search for a string that appears both as a whole word and
 751        // within other words. By default, all results are found.
 752        search_bar.update(cx, |search_bar, cx| {
 753            search_bar.set_query("or", cx);
 754        });
 755        editor.next_notification(cx).await;
 756        editor.update(cx, |editor, cx| {
 757            assert_eq!(
 758                editor.all_background_highlights(cx),
 759                &[
 760                    (
 761                        DisplayPoint::new(0, 24)..DisplayPoint::new(0, 26),
 762                        Color::red(),
 763                    ),
 764                    (
 765                        DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
 766                        Color::red(),
 767                    ),
 768                    (
 769                        DisplayPoint::new(2, 71)..DisplayPoint::new(2, 73),
 770                        Color::red(),
 771                    ),
 772                    (
 773                        DisplayPoint::new(3, 1)..DisplayPoint::new(3, 3),
 774                        Color::red(),
 775                    ),
 776                    (
 777                        DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
 778                        Color::red(),
 779                    ),
 780                    (
 781                        DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
 782                        Color::red(),
 783                    ),
 784                    (
 785                        DisplayPoint::new(3, 60)..DisplayPoint::new(3, 62),
 786                        Color::red(),
 787                    ),
 788                ]
 789            );
 790        });
 791
 792        // Switch to a whole word search.
 793        search_bar.update(cx, |search_bar, cx| {
 794            search_bar.toggle_search_option(SearchOption::WholeWord, cx);
 795        });
 796        editor.next_notification(cx).await;
 797        editor.update(cx, |editor, cx| {
 798            assert_eq!(
 799                editor.all_background_highlights(cx),
 800                &[
 801                    (
 802                        DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
 803                        Color::red(),
 804                    ),
 805                    (
 806                        DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
 807                        Color::red(),
 808                    ),
 809                    (
 810                        DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
 811                        Color::red(),
 812                    ),
 813                ]
 814            );
 815        });
 816
 817        editor.update(cx, |editor, cx| {
 818            editor.change_selections(None, cx, |s| {
 819                s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
 820            });
 821        });
 822        search_bar.update(cx, |search_bar, cx| {
 823            assert_eq!(search_bar.active_match_index, Some(0));
 824            search_bar.select_next_match(&SelectNextMatch, cx);
 825            assert_eq!(
 826                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
 827                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
 828            );
 829        });
 830        search_bar.read_with(cx, |search_bar, _| {
 831            assert_eq!(search_bar.active_match_index, Some(0));
 832        });
 833
 834        search_bar.update(cx, |search_bar, cx| {
 835            search_bar.select_next_match(&SelectNextMatch, cx);
 836            assert_eq!(
 837                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
 838                [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
 839            );
 840        });
 841        search_bar.read_with(cx, |search_bar, _| {
 842            assert_eq!(search_bar.active_match_index, Some(1));
 843        });
 844
 845        search_bar.update(cx, |search_bar, cx| {
 846            search_bar.select_next_match(&SelectNextMatch, cx);
 847            assert_eq!(
 848                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
 849                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
 850            );
 851        });
 852        search_bar.read_with(cx, |search_bar, _| {
 853            assert_eq!(search_bar.active_match_index, Some(2));
 854        });
 855
 856        search_bar.update(cx, |search_bar, cx| {
 857            search_bar.select_next_match(&SelectNextMatch, cx);
 858            assert_eq!(
 859                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
 860                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
 861            );
 862        });
 863        search_bar.read_with(cx, |search_bar, _| {
 864            assert_eq!(search_bar.active_match_index, Some(0));
 865        });
 866
 867        search_bar.update(cx, |search_bar, cx| {
 868            search_bar.select_prev_match(&SelectPrevMatch, cx);
 869            assert_eq!(
 870                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
 871                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
 872            );
 873        });
 874        search_bar.read_with(cx, |search_bar, _| {
 875            assert_eq!(search_bar.active_match_index, Some(2));
 876        });
 877
 878        search_bar.update(cx, |search_bar, cx| {
 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, 11)..DisplayPoint::new(3, 13)]
 883            );
 884        });
 885        search_bar.read_with(cx, |search_bar, _| {
 886            assert_eq!(search_bar.active_match_index, Some(1));
 887        });
 888
 889        search_bar.update(cx, |search_bar, cx| {
 890            search_bar.select_prev_match(&SelectPrevMatch, cx);
 891            assert_eq!(
 892                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
 893                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
 894            );
 895        });
 896        search_bar.read_with(cx, |search_bar, _| {
 897            assert_eq!(search_bar.active_match_index, Some(0));
 898        });
 899
 900        // Park the cursor in between matches and ensure that going to the previous match selects
 901        // the closest match to the left.
 902        editor.update(cx, |editor, cx| {
 903            editor.change_selections(None, cx, |s| {
 904                s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
 905            });
 906        });
 907        search_bar.update(cx, |search_bar, cx| {
 908            assert_eq!(search_bar.active_match_index, Some(1));
 909            search_bar.select_prev_match(&SelectPrevMatch, cx);
 910            assert_eq!(
 911                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
 912                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
 913            );
 914        });
 915        search_bar.read_with(cx, |search_bar, _| {
 916            assert_eq!(search_bar.active_match_index, Some(0));
 917        });
 918
 919        // Park the cursor in between matches and ensure that going to the next match selects the
 920        // closest match to the right.
 921        editor.update(cx, |editor, cx| {
 922            editor.change_selections(None, cx, |s| {
 923                s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
 924            });
 925        });
 926        search_bar.update(cx, |search_bar, cx| {
 927            assert_eq!(search_bar.active_match_index, Some(1));
 928            search_bar.select_next_match(&SelectNextMatch, cx);
 929            assert_eq!(
 930                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
 931                [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
 932            );
 933        });
 934        search_bar.read_with(cx, |search_bar, _| {
 935            assert_eq!(search_bar.active_match_index, Some(1));
 936        });
 937
 938        // Park the cursor after the last match and ensure that going to the previous match selects
 939        // the last match.
 940        editor.update(cx, |editor, cx| {
 941            editor.change_selections(None, cx, |s| {
 942                s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
 943            });
 944        });
 945        search_bar.update(cx, |search_bar, cx| {
 946            assert_eq!(search_bar.active_match_index, Some(2));
 947            search_bar.select_prev_match(&SelectPrevMatch, cx);
 948            assert_eq!(
 949                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
 950                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
 951            );
 952        });
 953        search_bar.read_with(cx, |search_bar, _| {
 954            assert_eq!(search_bar.active_match_index, Some(2));
 955        });
 956
 957        // Park the cursor after the last match and ensure that going to the next match selects the
 958        // first match.
 959        editor.update(cx, |editor, cx| {
 960            editor.change_selections(None, cx, |s| {
 961                s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
 962            });
 963        });
 964        search_bar.update(cx, |search_bar, cx| {
 965            assert_eq!(search_bar.active_match_index, Some(2));
 966            search_bar.select_next_match(&SelectNextMatch, cx);
 967            assert_eq!(
 968                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
 969                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
 970            );
 971        });
 972        search_bar.read_with(cx, |search_bar, _| {
 973            assert_eq!(search_bar.active_match_index, Some(0));
 974        });
 975
 976        // Park the cursor before the first match and ensure that going to the previous match
 977        // selects the last match.
 978        editor.update(cx, |editor, cx| {
 979            editor.change_selections(None, cx, |s| {
 980                s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
 981            });
 982        });
 983        search_bar.update(cx, |search_bar, cx| {
 984            assert_eq!(search_bar.active_match_index, Some(0));
 985            search_bar.select_prev_match(&SelectPrevMatch, cx);
 986            assert_eq!(
 987                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
 988                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
 989            );
 990        });
 991        search_bar.read_with(cx, |search_bar, _| {
 992            assert_eq!(search_bar.active_match_index, Some(2));
 993        });
 994    }
 995
 996    #[gpui::test]
 997    async fn test_search_select_all_matches(cx: &mut TestAppContext) {
 998        crate::project_search::tests::init_test(cx);
 999
1000        let buffer_text = r#"
1001        A regular expression (shortened as regex or regexp;[1] also referred to as
1002        rational expression[2][3]) is a sequence of characters that specifies a search
1003        pattern in text. Usually such patterns are used by string-searching algorithms
1004        for "find" or "find and replace" operations on strings, or for input validation.
1005        "#
1006        .unindent();
1007        let expected_query_matches_count = buffer_text
1008            .chars()
1009            .filter(|c| c.to_ascii_lowercase() == 'a')
1010            .count();
1011        assert!(
1012            expected_query_matches_count > 1,
1013            "Should pick a query with multiple results"
1014        );
1015        let buffer = cx.add_model(|cx| Buffer::new(0, buffer_text, cx));
1016        let (window_id, _root_view) = cx.add_window(|_| EmptyView);
1017
1018        let editor = cx.add_view(window_id, |cx| Editor::for_buffer(buffer.clone(), None, cx));
1019
1020        let search_bar = cx.add_view(window_id, |cx| {
1021            let mut search_bar = BufferSearchBar::new(cx);
1022            search_bar.set_active_pane_item(Some(&editor), cx);
1023            search_bar.show(false, true, cx);
1024            search_bar
1025        });
1026
1027        search_bar.update(cx, |search_bar, cx| {
1028            search_bar.set_query("a", cx);
1029        });
1030
1031        editor.next_notification(cx).await;
1032        let initial_selections = editor.update(cx, |editor, cx| {
1033            let initial_selections = editor.selections.display_ranges(cx);
1034            assert_eq!(
1035                initial_selections.len(), 1,
1036                "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
1037            );
1038            initial_selections
1039        });
1040        search_bar.update(cx, |search_bar, _| {
1041            assert_eq!(search_bar.active_match_index, Some(0));
1042        });
1043
1044        search_bar.update(cx, |search_bar, cx| {
1045            search_bar.select_all_matches(&SelectAllMatches, cx);
1046            let all_selections =
1047                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1048            assert_eq!(
1049                all_selections.len(),
1050                expected_query_matches_count,
1051                "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1052            );
1053        });
1054        search_bar.update(cx, |search_bar, _| {
1055            assert_eq!(
1056                search_bar.active_match_index,
1057                Some(0),
1058                "Match index should not change after selecting all matches"
1059            );
1060        });
1061
1062        search_bar.update(cx, |search_bar, cx| {
1063            search_bar.select_next_match(&SelectNextMatch, cx);
1064            let all_selections =
1065                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1066            assert_eq!(
1067                all_selections.len(),
1068                1,
1069                "On next match, should deselect items and select the next match"
1070            );
1071            assert_ne!(
1072                all_selections, initial_selections,
1073                "Next match should be different from the first selection"
1074            );
1075        });
1076        search_bar.update(cx, |search_bar, _| {
1077            assert_eq!(
1078                search_bar.active_match_index,
1079                Some(1),
1080                "Match index should be updated to the next one"
1081            );
1082        });
1083
1084        search_bar.update(cx, |search_bar, cx| {
1085            search_bar.select_all_matches(&SelectAllMatches, cx);
1086            let all_selections =
1087                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1088            assert_eq!(
1089                all_selections.len(),
1090                expected_query_matches_count,
1091                "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1092            );
1093        });
1094        search_bar.update(cx, |search_bar, _| {
1095            assert_eq!(
1096                search_bar.active_match_index,
1097                Some(1),
1098                "Match index should not change after selecting all matches"
1099            );
1100        });
1101
1102        search_bar.update(cx, |search_bar, cx| {
1103            search_bar.select_prev_match(&SelectPrevMatch, cx);
1104            let all_selections =
1105                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1106            assert_eq!(
1107                all_selections.len(),
1108                1,
1109                "On previous match, should deselect items and select the previous item"
1110            );
1111            assert_eq!(
1112                all_selections, initial_selections,
1113                "Previous match should be the same as the first selection"
1114            );
1115        });
1116        search_bar.update(cx, |search_bar, _| {
1117            assert_eq!(
1118                search_bar.active_match_index,
1119                Some(0),
1120                "Match index should be updated to the previous one"
1121            );
1122        });
1123    }
1124}