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