buffer_search.rs

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