buffer_search.rs

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