buffer_search.rs

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