buffer_search.rs

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