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