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