buffer_search.rs

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