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