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