buffer_search.rs

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