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