buffer_search.rs

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