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