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