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