buffer_search.rs

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