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::<Self, _>::new(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::<NavButton, _>::new(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::<ActionButton, _>::new(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::<CloseButton, _>::new(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
 854        let editor = cx.add_view(window.window_id(), |cx| {
 855            Editor::for_buffer(buffer.clone(), None, cx)
 856        });
 857
 858        let search_bar = cx.add_view(window.window_id(), |cx| {
 859            let mut search_bar = BufferSearchBar::new(cx);
 860            search_bar.set_active_pane_item(Some(&editor), cx);
 861            search_bar.show(cx);
 862            search_bar
 863        });
 864
 865        (editor, search_bar)
 866    }
 867
 868    #[gpui::test]
 869    async fn test_search_simple(cx: &mut TestAppContext) {
 870        let (editor, search_bar) = init_test(cx);
 871
 872        // Search for a string that appears with different casing.
 873        // By default, search is case-insensitive.
 874        search_bar
 875            .update(cx, |search_bar, cx| search_bar.search("us", None, cx))
 876            .await
 877            .unwrap();
 878        editor.update(cx, |editor, cx| {
 879            assert_eq!(
 880                editor.all_background_highlights(cx),
 881                &[
 882                    (
 883                        DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19),
 884                        Color::red(),
 885                    ),
 886                    (
 887                        DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
 888                        Color::red(),
 889                    ),
 890                ]
 891            );
 892        });
 893
 894        // Switch to a case sensitive search.
 895        search_bar.update(cx, |search_bar, cx| {
 896            search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx);
 897        });
 898        editor.next_notification(cx).await;
 899        editor.update(cx, |editor, cx| {
 900            assert_eq!(
 901                editor.all_background_highlights(cx),
 902                &[(
 903                    DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
 904                    Color::red(),
 905                )]
 906            );
 907        });
 908
 909        // Search for a string that appears both as a whole word and
 910        // within other words. By default, all results are found.
 911        search_bar
 912            .update(cx, |search_bar, cx| search_bar.search("or", None, cx))
 913            .await
 914            .unwrap();
 915        editor.update(cx, |editor, cx| {
 916            assert_eq!(
 917                editor.all_background_highlights(cx),
 918                &[
 919                    (
 920                        DisplayPoint::new(0, 24)..DisplayPoint::new(0, 26),
 921                        Color::red(),
 922                    ),
 923                    (
 924                        DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
 925                        Color::red(),
 926                    ),
 927                    (
 928                        DisplayPoint::new(2, 71)..DisplayPoint::new(2, 73),
 929                        Color::red(),
 930                    ),
 931                    (
 932                        DisplayPoint::new(3, 1)..DisplayPoint::new(3, 3),
 933                        Color::red(),
 934                    ),
 935                    (
 936                        DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
 937                        Color::red(),
 938                    ),
 939                    (
 940                        DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
 941                        Color::red(),
 942                    ),
 943                    (
 944                        DisplayPoint::new(3, 60)..DisplayPoint::new(3, 62),
 945                        Color::red(),
 946                    ),
 947                ]
 948            );
 949        });
 950
 951        // Switch to a whole word search.
 952        search_bar.update(cx, |search_bar, cx| {
 953            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
 954        });
 955        editor.next_notification(cx).await;
 956        editor.update(cx, |editor, cx| {
 957            assert_eq!(
 958                editor.all_background_highlights(cx),
 959                &[
 960                    (
 961                        DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
 962                        Color::red(),
 963                    ),
 964                    (
 965                        DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
 966                        Color::red(),
 967                    ),
 968                    (
 969                        DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
 970                        Color::red(),
 971                    ),
 972                ]
 973            );
 974        });
 975
 976        editor.update(cx, |editor, cx| {
 977            editor.change_selections(None, cx, |s| {
 978                s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
 979            });
 980        });
 981        search_bar.update(cx, |search_bar, cx| {
 982            assert_eq!(search_bar.active_match_index, Some(0));
 983            search_bar.select_next_match(&SelectNextMatch, cx);
 984            assert_eq!(
 985                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
 986                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
 987            );
 988        });
 989        search_bar.read_with(cx, |search_bar, _| {
 990            assert_eq!(search_bar.active_match_index, Some(0));
 991        });
 992
 993        search_bar.update(cx, |search_bar, cx| {
 994            search_bar.select_next_match(&SelectNextMatch, cx);
 995            assert_eq!(
 996                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
 997                [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
 998            );
 999        });
1000        search_bar.read_with(cx, |search_bar, _| {
1001            assert_eq!(search_bar.active_match_index, Some(1));
1002        });
1003
1004        search_bar.update(cx, |search_bar, cx| {
1005            search_bar.select_next_match(&SelectNextMatch, cx);
1006            assert_eq!(
1007                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1008                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1009            );
1010        });
1011        search_bar.read_with(cx, |search_bar, _| {
1012            assert_eq!(search_bar.active_match_index, Some(2));
1013        });
1014
1015        search_bar.update(cx, |search_bar, cx| {
1016            search_bar.select_next_match(&SelectNextMatch, cx);
1017            assert_eq!(
1018                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1019                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1020            );
1021        });
1022        search_bar.read_with(cx, |search_bar, _| {
1023            assert_eq!(search_bar.active_match_index, Some(0));
1024        });
1025
1026        search_bar.update(cx, |search_bar, cx| {
1027            search_bar.select_prev_match(&SelectPrevMatch, cx);
1028            assert_eq!(
1029                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1030                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1031            );
1032        });
1033        search_bar.read_with(cx, |search_bar, _| {
1034            assert_eq!(search_bar.active_match_index, Some(2));
1035        });
1036
1037        search_bar.update(cx, |search_bar, cx| {
1038            search_bar.select_prev_match(&SelectPrevMatch, cx);
1039            assert_eq!(
1040                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1041                [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
1042            );
1043        });
1044        search_bar.read_with(cx, |search_bar, _| {
1045            assert_eq!(search_bar.active_match_index, Some(1));
1046        });
1047
1048        search_bar.update(cx, |search_bar, cx| {
1049            search_bar.select_prev_match(&SelectPrevMatch, cx);
1050            assert_eq!(
1051                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1052                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1053            );
1054        });
1055        search_bar.read_with(cx, |search_bar, _| {
1056            assert_eq!(search_bar.active_match_index, Some(0));
1057        });
1058
1059        // Park the cursor in between matches and ensure that going to the previous match selects
1060        // the closest match to the left.
1061        editor.update(cx, |editor, cx| {
1062            editor.change_selections(None, cx, |s| {
1063                s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
1064            });
1065        });
1066        search_bar.update(cx, |search_bar, cx| {
1067            assert_eq!(search_bar.active_match_index, Some(1));
1068            search_bar.select_prev_match(&SelectPrevMatch, cx);
1069            assert_eq!(
1070                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1071                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1072            );
1073        });
1074        search_bar.read_with(cx, |search_bar, _| {
1075            assert_eq!(search_bar.active_match_index, Some(0));
1076        });
1077
1078        // Park the cursor in between matches and ensure that going to the next match selects the
1079        // closest match to the right.
1080        editor.update(cx, |editor, cx| {
1081            editor.change_selections(None, cx, |s| {
1082                s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
1083            });
1084        });
1085        search_bar.update(cx, |search_bar, cx| {
1086            assert_eq!(search_bar.active_match_index, Some(1));
1087            search_bar.select_next_match(&SelectNextMatch, cx);
1088            assert_eq!(
1089                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1090                [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
1091            );
1092        });
1093        search_bar.read_with(cx, |search_bar, _| {
1094            assert_eq!(search_bar.active_match_index, Some(1));
1095        });
1096
1097        // Park the cursor after the last match and ensure that going to the previous match selects
1098        // the last match.
1099        editor.update(cx, |editor, cx| {
1100            editor.change_selections(None, cx, |s| {
1101                s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
1102            });
1103        });
1104        search_bar.update(cx, |search_bar, cx| {
1105            assert_eq!(search_bar.active_match_index, Some(2));
1106            search_bar.select_prev_match(&SelectPrevMatch, cx);
1107            assert_eq!(
1108                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1109                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1110            );
1111        });
1112        search_bar.read_with(cx, |search_bar, _| {
1113            assert_eq!(search_bar.active_match_index, Some(2));
1114        });
1115
1116        // Park the cursor after the last match and ensure that going to the next match selects the
1117        // first match.
1118        editor.update(cx, |editor, cx| {
1119            editor.change_selections(None, cx, |s| {
1120                s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
1121            });
1122        });
1123        search_bar.update(cx, |search_bar, cx| {
1124            assert_eq!(search_bar.active_match_index, Some(2));
1125            search_bar.select_next_match(&SelectNextMatch, cx);
1126            assert_eq!(
1127                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1128                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1129            );
1130        });
1131        search_bar.read_with(cx, |search_bar, _| {
1132            assert_eq!(search_bar.active_match_index, Some(0));
1133        });
1134
1135        // Park the cursor before the first match and ensure that going to the previous match
1136        // selects the last match.
1137        editor.update(cx, |editor, cx| {
1138            editor.change_selections(None, cx, |s| {
1139                s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
1140            });
1141        });
1142        search_bar.update(cx, |search_bar, cx| {
1143            assert_eq!(search_bar.active_match_index, Some(0));
1144            search_bar.select_prev_match(&SelectPrevMatch, cx);
1145            assert_eq!(
1146                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1147                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1148            );
1149        });
1150        search_bar.read_with(cx, |search_bar, _| {
1151            assert_eq!(search_bar.active_match_index, Some(2));
1152        });
1153    }
1154
1155    #[gpui::test]
1156    async fn test_search_option_handling(cx: &mut TestAppContext) {
1157        let (editor, search_bar) = init_test(cx);
1158
1159        // show with options should make current search case sensitive
1160        search_bar
1161            .update(cx, |search_bar, cx| {
1162                search_bar.show(cx);
1163                search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), cx)
1164            })
1165            .await
1166            .unwrap();
1167        editor.update(cx, |editor, cx| {
1168            assert_eq!(
1169                editor.all_background_highlights(cx),
1170                &[(
1171                    DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
1172                    Color::red(),
1173                )]
1174            );
1175        });
1176
1177        // search_suggested should restore default options
1178        search_bar.update(cx, |search_bar, cx| {
1179            search_bar.search_suggested(cx);
1180            assert_eq!(search_bar.search_options, SearchOptions::NONE)
1181        });
1182
1183        // toggling a search option should update the defaults
1184        search_bar
1185            .update(cx, |search_bar, cx| {
1186                search_bar.search("regex", Some(SearchOptions::CASE_SENSITIVE), cx)
1187            })
1188            .await
1189            .unwrap();
1190        search_bar.update(cx, |search_bar, cx| {
1191            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx)
1192        });
1193        editor.next_notification(cx).await;
1194        editor.update(cx, |editor, cx| {
1195            assert_eq!(
1196                editor.all_background_highlights(cx),
1197                &[(
1198                    DisplayPoint::new(0, 35)..DisplayPoint::new(0, 40),
1199                    Color::red(),
1200                ),]
1201            );
1202        });
1203
1204        // defaults should still include whole word
1205        search_bar.update(cx, |search_bar, cx| {
1206            search_bar.search_suggested(cx);
1207            assert_eq!(
1208                search_bar.search_options,
1209                SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD
1210            )
1211        });
1212    }
1213
1214    #[gpui::test]
1215    async fn test_search_select_all_matches(cx: &mut TestAppContext) {
1216        crate::project_search::tests::init_test(cx);
1217
1218        let buffer_text = r#"
1219        A regular expression (shortened as regex or regexp;[1] also referred to as
1220        rational expression[2][3]) is a sequence of characters that specifies a search
1221        pattern in text. Usually such patterns are used by string-searching algorithms
1222        for "find" or "find and replace" operations on strings, or for input validation.
1223        "#
1224        .unindent();
1225        let expected_query_matches_count = buffer_text
1226            .chars()
1227            .filter(|c| c.to_ascii_lowercase() == 'a')
1228            .count();
1229        assert!(
1230            expected_query_matches_count > 1,
1231            "Should pick a query with multiple results"
1232        );
1233        let buffer = cx.add_model(|cx| Buffer::new(0, buffer_text, cx));
1234        let window = cx.add_window(|_| EmptyView);
1235        let window_id = window.window_id();
1236
1237        let editor = cx.add_view(window_id, |cx| Editor::for_buffer(buffer.clone(), None, cx));
1238
1239        let search_bar = cx.add_view(window_id, |cx| {
1240            let mut search_bar = BufferSearchBar::new(cx);
1241            search_bar.set_active_pane_item(Some(&editor), cx);
1242            search_bar.show(cx);
1243            search_bar
1244        });
1245
1246        search_bar
1247            .update(cx, |search_bar, cx| search_bar.search("a", None, cx))
1248            .await
1249            .unwrap();
1250        search_bar.update(cx, |search_bar, cx| {
1251            cx.focus(search_bar.query_editor.as_any());
1252            search_bar.activate_current_match(cx);
1253        });
1254
1255        cx.read_window(window_id, |cx| {
1256            assert!(
1257                !editor.is_focused(cx),
1258                "Initially, the editor should not be focused"
1259            );
1260        });
1261        let initial_selections = editor.update(cx, |editor, cx| {
1262            let initial_selections = editor.selections.display_ranges(cx);
1263            assert_eq!(
1264                initial_selections.len(), 1,
1265                "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
1266            );
1267            initial_selections
1268        });
1269        search_bar.update(cx, |search_bar, _| {
1270            assert_eq!(search_bar.active_match_index, Some(0));
1271        });
1272
1273        search_bar.update(cx, |search_bar, cx| {
1274            cx.focus(search_bar.query_editor.as_any());
1275            search_bar.select_all_matches(&SelectAllMatches, cx);
1276        });
1277        cx.read_window(window_id, |cx| {
1278            assert!(
1279                editor.is_focused(cx),
1280                "Should focus editor after successful SelectAllMatches"
1281            );
1282        });
1283        search_bar.update(cx, |search_bar, cx| {
1284            let all_selections =
1285                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1286            assert_eq!(
1287                all_selections.len(),
1288                expected_query_matches_count,
1289                "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1290            );
1291            assert_eq!(
1292                search_bar.active_match_index,
1293                Some(0),
1294                "Match index should not change after selecting all matches"
1295            );
1296        });
1297
1298        search_bar.update(cx, |search_bar, cx| {
1299            search_bar.select_next_match(&SelectNextMatch, cx);
1300        });
1301        cx.read_window(window_id, |cx| {
1302            assert!(
1303                editor.is_focused(cx),
1304                "Should still have editor focused after SelectNextMatch"
1305            );
1306        });
1307        search_bar.update(cx, |search_bar, cx| {
1308            let all_selections =
1309                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1310            assert_eq!(
1311                all_selections.len(),
1312                1,
1313                "On next match, should deselect items and select the next match"
1314            );
1315            assert_ne!(
1316                all_selections, initial_selections,
1317                "Next match should be different from the first selection"
1318            );
1319            assert_eq!(
1320                search_bar.active_match_index,
1321                Some(1),
1322                "Match index should be updated to the next one"
1323            );
1324        });
1325
1326        search_bar.update(cx, |search_bar, cx| {
1327            cx.focus(search_bar.query_editor.as_any());
1328            search_bar.select_all_matches(&SelectAllMatches, cx);
1329        });
1330        cx.read_window(window_id, |cx| {
1331            assert!(
1332                editor.is_focused(cx),
1333                "Should focus editor after successful SelectAllMatches"
1334            );
1335        });
1336        search_bar.update(cx, |search_bar, cx| {
1337            let all_selections =
1338                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1339            assert_eq!(
1340                all_selections.len(),
1341                expected_query_matches_count,
1342                "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1343            );
1344            assert_eq!(
1345                search_bar.active_match_index,
1346                Some(1),
1347                "Match index should not change after selecting all matches"
1348            );
1349        });
1350
1351        search_bar.update(cx, |search_bar, cx| {
1352            search_bar.select_prev_match(&SelectPrevMatch, cx);
1353        });
1354        cx.read_window(window_id, |cx| {
1355            assert!(
1356                editor.is_focused(cx),
1357                "Should still have editor focused after SelectPrevMatch"
1358            );
1359        });
1360        let last_match_selections = search_bar.update(cx, |search_bar, cx| {
1361            let all_selections =
1362                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1363            assert_eq!(
1364                all_selections.len(),
1365                1,
1366                "On previous match, should deselect items and select the previous item"
1367            );
1368            assert_eq!(
1369                all_selections, initial_selections,
1370                "Previous match should be the same as the first selection"
1371            );
1372            assert_eq!(
1373                search_bar.active_match_index,
1374                Some(0),
1375                "Match index should be updated to the previous one"
1376            );
1377            all_selections
1378        });
1379
1380        search_bar
1381            .update(cx, |search_bar, cx| {
1382                cx.focus(search_bar.query_editor.as_any());
1383                search_bar.search("abas_nonexistent_match", None, cx)
1384            })
1385            .await
1386            .unwrap();
1387        search_bar.update(cx, |search_bar, cx| {
1388            search_bar.select_all_matches(&SelectAllMatches, cx);
1389        });
1390        cx.read_window(window_id, |cx| {
1391            assert!(
1392                !editor.is_focused(cx),
1393                "Should not switch focus to editor if SelectAllMatches does not find any matches"
1394            );
1395        });
1396        search_bar.update(cx, |search_bar, cx| {
1397            let all_selections =
1398                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1399            assert_eq!(
1400                all_selections, last_match_selections,
1401                "Should not select anything new if there are no matches"
1402            );
1403            assert!(
1404                search_bar.active_match_index.is_none(),
1405                "For no matches, there should be no active match index"
1406            );
1407        });
1408    }
1409
1410    #[gpui::test]
1411    async fn test_search_query_history(cx: &mut TestAppContext) {
1412        crate::project_search::tests::init_test(cx);
1413
1414        let buffer_text = r#"
1415        A regular expression (shortened as regex or regexp;[1] also referred to as
1416        rational expression[2][3]) is a sequence of characters that specifies a search
1417        pattern in text. Usually such patterns are used by string-searching algorithms
1418        for "find" or "find and replace" operations on strings, or for input validation.
1419        "#
1420        .unindent();
1421        let buffer = cx.add_model(|cx| Buffer::new(0, buffer_text, cx));
1422        let window = cx.add_window(|_| EmptyView);
1423
1424        let editor = cx.add_view(window.window_id(), |cx| {
1425            Editor::for_buffer(buffer.clone(), None, cx)
1426        });
1427
1428        let search_bar = cx.add_view(window.window_id(), |cx| {
1429            let mut search_bar = BufferSearchBar::new(cx);
1430            search_bar.set_active_pane_item(Some(&editor), cx);
1431            search_bar.show(cx);
1432            search_bar
1433        });
1434
1435        // Add 3 search items into the history.
1436        search_bar
1437            .update(cx, |search_bar, cx| search_bar.search("a", None, cx))
1438            .await
1439            .unwrap();
1440        search_bar
1441            .update(cx, |search_bar, cx| search_bar.search("b", None, cx))
1442            .await
1443            .unwrap();
1444        search_bar
1445            .update(cx, |search_bar, cx| {
1446                search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), cx)
1447            })
1448            .await
1449            .unwrap();
1450        // Ensure that the latest search is active.
1451        search_bar.read_with(cx, |search_bar, cx| {
1452            assert_eq!(search_bar.query(cx), "c");
1453            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1454        });
1455
1456        // Next history query after the latest should set the query to the empty string.
1457        search_bar.update(cx, |search_bar, cx| {
1458            search_bar.next_history_query(&NextHistoryQuery, cx);
1459        });
1460        search_bar.read_with(cx, |search_bar, cx| {
1461            assert_eq!(search_bar.query(cx), "");
1462            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1463        });
1464        search_bar.update(cx, |search_bar, cx| {
1465            search_bar.next_history_query(&NextHistoryQuery, cx);
1466        });
1467        search_bar.read_with(cx, |search_bar, cx| {
1468            assert_eq!(search_bar.query(cx), "");
1469            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1470        });
1471
1472        // First previous query for empty current query should set the query to the latest.
1473        search_bar.update(cx, |search_bar, cx| {
1474            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1475        });
1476        search_bar.read_with(cx, |search_bar, cx| {
1477            assert_eq!(search_bar.query(cx), "c");
1478            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1479        });
1480
1481        // Further previous items should go over the history in reverse order.
1482        search_bar.update(cx, |search_bar, cx| {
1483            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1484        });
1485        search_bar.read_with(cx, |search_bar, cx| {
1486            assert_eq!(search_bar.query(cx), "b");
1487            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1488        });
1489
1490        // Previous items should never go behind the first history item.
1491        search_bar.update(cx, |search_bar, cx| {
1492            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1493        });
1494        search_bar.read_with(cx, |search_bar, cx| {
1495            assert_eq!(search_bar.query(cx), "a");
1496            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1497        });
1498        search_bar.update(cx, |search_bar, cx| {
1499            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1500        });
1501        search_bar.read_with(cx, |search_bar, cx| {
1502            assert_eq!(search_bar.query(cx), "a");
1503            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1504        });
1505
1506        // Next items should go over the history in the original order.
1507        search_bar.update(cx, |search_bar, cx| {
1508            search_bar.next_history_query(&NextHistoryQuery, cx);
1509        });
1510        search_bar.read_with(cx, |search_bar, cx| {
1511            assert_eq!(search_bar.query(cx), "b");
1512            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1513        });
1514
1515        search_bar
1516            .update(cx, |search_bar, cx| search_bar.search("ba", None, cx))
1517            .await
1518            .unwrap();
1519        search_bar.read_with(cx, |search_bar, cx| {
1520            assert_eq!(search_bar.query(cx), "ba");
1521            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1522        });
1523
1524        // New search input should add another entry to history and move the selection to the end of the history.
1525        search_bar.update(cx, |search_bar, cx| {
1526            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1527        });
1528        search_bar.read_with(cx, |search_bar, cx| {
1529            assert_eq!(search_bar.query(cx), "c");
1530            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1531        });
1532        search_bar.update(cx, |search_bar, cx| {
1533            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1534        });
1535        search_bar.read_with(cx, |search_bar, cx| {
1536            assert_eq!(search_bar.query(cx), "b");
1537            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1538        });
1539        search_bar.update(cx, |search_bar, cx| {
1540            search_bar.next_history_query(&NextHistoryQuery, cx);
1541        });
1542        search_bar.read_with(cx, |search_bar, cx| {
1543            assert_eq!(search_bar.query(cx), "c");
1544            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1545        });
1546        search_bar.update(cx, |search_bar, cx| {
1547            search_bar.next_history_query(&NextHistoryQuery, cx);
1548        });
1549        search_bar.read_with(cx, |search_bar, cx| {
1550            assert_eq!(search_bar.query(cx), "ba");
1551            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1552        });
1553        search_bar.update(cx, |search_bar, cx| {
1554            search_bar.next_history_query(&NextHistoryQuery, cx);
1555        });
1556        search_bar.read_with(cx, |search_bar, cx| {
1557            assert_eq!(search_bar.query(cx), "");
1558            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1559        });
1560    }
1561}