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    fn row_count(&self, _: &ViewContext<Self>) -> usize {
 371        3
 372    }
 373}
 374
 375impl BufferSearchBar {
 376    pub fn new(cx: &mut ViewContext<Self>) -> Self {
 377        let query_editor = cx.add_view(|cx| {
 378            Editor::auto_height(
 379                2,
 380                Some(Arc::new(|theme| theme.search.editor.input.clone())),
 381                cx,
 382            )
 383        });
 384        cx.subscribe(&query_editor, Self::on_query_editor_event)
 385            .detach();
 386
 387        Self {
 388            query_editor,
 389            active_searchable_item: None,
 390            active_searchable_item_subscription: None,
 391            active_match_index: None,
 392            searchable_items_with_matches: Default::default(),
 393            default_options: SearchOptions::NONE,
 394            search_options: SearchOptions::NONE,
 395            pending_search: None,
 396            query_contains_error: false,
 397            dismissed: true,
 398            search_history: SearchHistory::default(),
 399            current_mode: SearchMode::default(),
 400        }
 401    }
 402
 403    pub fn is_dismissed(&self) -> bool {
 404        self.dismissed
 405    }
 406
 407    pub fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext<Self>) {
 408        self.dismissed = true;
 409        for searchable_item in self.searchable_items_with_matches.keys() {
 410            if let Some(searchable_item) =
 411                WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
 412            {
 413                searchable_item.clear_matches(cx);
 414            }
 415        }
 416        if let Some(active_editor) = self.active_searchable_item.as_ref() {
 417            cx.focus(active_editor.as_any());
 418        }
 419        cx.emit(Event::UpdateLocation);
 420        cx.notify();
 421    }
 422
 423    pub fn show(&mut self, cx: &mut ViewContext<Self>) -> bool {
 424        if self.active_searchable_item.is_none() {
 425            return false;
 426        }
 427        self.dismissed = false;
 428        cx.notify();
 429        cx.emit(Event::UpdateLocation);
 430        true
 431    }
 432
 433    pub fn search_suggested(&mut self, cx: &mut ViewContext<Self>) {
 434        let search = self
 435            .query_suggestion(cx)
 436            .map(|suggestion| self.search(&suggestion, Some(self.default_options), cx));
 437
 438        if let Some(search) = search {
 439            cx.spawn(|this, mut cx| async move {
 440                search.await?;
 441                this.update(&mut cx, |this, cx| this.activate_current_match(cx))
 442            })
 443            .detach_and_log_err(cx);
 444        }
 445    }
 446
 447    pub fn activate_current_match(&mut self, cx: &mut ViewContext<Self>) {
 448        if let Some(match_ix) = self.active_match_index {
 449            if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
 450                if let Some(matches) = self
 451                    .searchable_items_with_matches
 452                    .get(&active_searchable_item.downgrade())
 453                {
 454                    active_searchable_item.activate_match(match_ix, matches, cx)
 455                }
 456            }
 457        }
 458    }
 459
 460    pub fn select_query(&mut self, cx: &mut ViewContext<Self>) {
 461        self.query_editor.update(cx, |query_editor, cx| {
 462            query_editor.select_all(&Default::default(), cx);
 463        });
 464    }
 465
 466    pub fn query(&self, cx: &WindowContext) -> String {
 467        self.query_editor.read(cx).text(cx)
 468    }
 469
 470    pub fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> Option<String> {
 471        self.active_searchable_item
 472            .as_ref()
 473            .map(|searchable_item| searchable_item.query_suggestion(cx))
 474    }
 475
 476    pub fn search(
 477        &mut self,
 478        query: &str,
 479        options: Option<SearchOptions>,
 480        cx: &mut ViewContext<Self>,
 481    ) -> oneshot::Receiver<()> {
 482        let options = options.unwrap_or(self.default_options);
 483        if query != self.query(cx) || self.search_options != options {
 484            self.query_editor.update(cx, |query_editor, cx| {
 485                query_editor.buffer().update(cx, |query_buffer, cx| {
 486                    let len = query_buffer.len(cx);
 487                    query_buffer.edit([(0..len, query)], None, cx);
 488                });
 489            });
 490            self.search_options = options;
 491            self.query_contains_error = false;
 492            self.clear_matches(cx);
 493            cx.notify();
 494        }
 495        self.update_matches(cx)
 496    }
 497
 498    fn render_action_button(
 499        &self,
 500        icon: &'static str,
 501        cx: &mut ViewContext<Self>,
 502    ) -> AnyElement<Self> {
 503        let tooltip = "Select All Matches";
 504        let tooltip_style = theme::current(cx).tooltip.clone();
 505        let action_type_id = 0_usize;
 506
 507        enum ActionButton {}
 508        MouseEventHandler::<ActionButton, _>::new(action_type_id, cx, |state, cx| {
 509            let theme = theme::current(cx);
 510            let style = theme.search.action_button.style_for(state);
 511            Label::new(icon, style.text.clone())
 512                .contained()
 513                .with_style(style.container)
 514        })
 515        .on_click(MouseButton::Left, move |_, this, cx| {
 516            this.select_all_matches(&SelectAllMatches, cx)
 517        })
 518        .with_cursor_style(CursorStyle::PointingHand)
 519        .with_tooltip::<ActionButton>(
 520            action_type_id,
 521            tooltip.to_string(),
 522            Some(Box::new(SelectAllMatches)),
 523            tooltip_style,
 524            cx,
 525        )
 526        .into_any()
 527    }
 528    pub fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext<Self>) {
 529        assert_ne!(
 530            mode,
 531            SearchMode::Semantic,
 532            "Semantic search is not supported in buffer search"
 533        );
 534        if mode == self.current_mode {
 535            return;
 536        }
 537        self.current_mode = mode;
 538        let _ = self.update_matches(cx);
 539        cx.notify();
 540    }
 541    fn deploy(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext<Pane>) {
 542        let mut propagate_action = true;
 543        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
 544            search_bar.update(cx, |search_bar, cx| {
 545                if search_bar.show(cx) {
 546                    search_bar.search_suggested(cx);
 547                    if action.focus {
 548                        search_bar.select_query(cx);
 549                        cx.focus_self();
 550                    }
 551                    propagate_action = false;
 552                }
 553            });
 554        }
 555
 556        if propagate_action {
 557            cx.propagate_action();
 558        }
 559    }
 560
 561    fn handle_editor_cancel(pane: &mut Pane, _: &editor::Cancel, cx: &mut ViewContext<Pane>) {
 562        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
 563            if !search_bar.read(cx).dismissed {
 564                search_bar.update(cx, |search_bar, cx| search_bar.dismiss(&Dismiss, cx));
 565                return;
 566            }
 567        }
 568        cx.propagate_action();
 569    }
 570
 571    pub fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext<Self>) {
 572        if let Some(active_editor) = self.active_searchable_item.as_ref() {
 573            cx.focus(active_editor.as_any());
 574        }
 575    }
 576
 577    fn toggle_search_option(&mut self, search_option: SearchOptions, cx: &mut ViewContext<Self>) {
 578        self.search_options.toggle(search_option);
 579        self.default_options = self.search_options;
 580        let _ = self.update_matches(cx);
 581        cx.notify();
 582    }
 583
 584    pub fn set_search_options(
 585        &mut self,
 586        search_options: SearchOptions,
 587        cx: &mut ViewContext<Self>,
 588    ) {
 589        self.search_options = search_options;
 590        cx.notify();
 591    }
 592
 593    fn select_next_match(&mut self, _: &SelectNextMatch, cx: &mut ViewContext<Self>) {
 594        self.select_match(Direction::Next, 1, cx);
 595    }
 596
 597    fn select_prev_match(&mut self, _: &SelectPrevMatch, cx: &mut ViewContext<Self>) {
 598        self.select_match(Direction::Prev, 1, cx);
 599    }
 600
 601    fn select_all_matches(&mut self, _: &SelectAllMatches, cx: &mut ViewContext<Self>) {
 602        if !self.dismissed && self.active_match_index.is_some() {
 603            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
 604                if let Some(matches) = self
 605                    .searchable_items_with_matches
 606                    .get(&searchable_item.downgrade())
 607                {
 608                    searchable_item.select_matches(matches, cx);
 609                    self.focus_editor(&FocusEditor, cx);
 610                }
 611            }
 612        }
 613    }
 614
 615    pub fn select_match(&mut self, direction: Direction, count: usize, cx: &mut ViewContext<Self>) {
 616        if let Some(index) = self.active_match_index {
 617            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
 618                if let Some(matches) = self
 619                    .searchable_items_with_matches
 620                    .get(&searchable_item.downgrade())
 621                {
 622                    let new_match_index = searchable_item
 623                        .match_index_for_direction(matches, index, direction, count, cx);
 624                    searchable_item.update_matches(matches, cx);
 625                    searchable_item.activate_match(new_match_index, matches, cx);
 626                }
 627            }
 628        }
 629    }
 630
 631    fn select_next_match_on_pane(
 632        pane: &mut Pane,
 633        action: &SelectNextMatch,
 634        cx: &mut ViewContext<Pane>,
 635    ) {
 636        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
 637            search_bar.update(cx, |bar, cx| bar.select_next_match(action, cx));
 638        }
 639    }
 640
 641    fn select_prev_match_on_pane(
 642        pane: &mut Pane,
 643        action: &SelectPrevMatch,
 644        cx: &mut ViewContext<Pane>,
 645    ) {
 646        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
 647            search_bar.update(cx, |bar, cx| bar.select_prev_match(action, cx));
 648        }
 649    }
 650
 651    fn select_all_matches_on_pane(
 652        pane: &mut Pane,
 653        action: &SelectAllMatches,
 654        cx: &mut ViewContext<Pane>,
 655    ) {
 656        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
 657            search_bar.update(cx, |bar, cx| bar.select_all_matches(action, cx));
 658        }
 659    }
 660
 661    fn on_query_editor_event(
 662        &mut self,
 663        _: ViewHandle<Editor>,
 664        event: &editor::Event,
 665        cx: &mut ViewContext<Self>,
 666    ) {
 667        if let editor::Event::Edited { .. } = event {
 668            self.query_contains_error = false;
 669            self.clear_matches(cx);
 670            let search = self.update_matches(cx);
 671            cx.spawn(|this, mut cx| async move {
 672                search.await?;
 673                this.update(&mut cx, |this, cx| this.activate_current_match(cx))
 674            })
 675            .detach_and_log_err(cx);
 676        }
 677    }
 678
 679    fn on_active_searchable_item_event(&mut self, event: SearchEvent, cx: &mut ViewContext<Self>) {
 680        match event {
 681            SearchEvent::MatchesInvalidated => {
 682                let _ = self.update_matches(cx);
 683            }
 684            SearchEvent::ActiveMatchChanged => self.update_match_index(cx),
 685        }
 686    }
 687
 688    fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
 689        let mut active_item_matches = None;
 690        for (searchable_item, matches) in self.searchable_items_with_matches.drain() {
 691            if let Some(searchable_item) =
 692                WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
 693            {
 694                if Some(&searchable_item) == self.active_searchable_item.as_ref() {
 695                    active_item_matches = Some((searchable_item.downgrade(), matches));
 696                } else {
 697                    searchable_item.clear_matches(cx);
 698                }
 699            }
 700        }
 701
 702        self.searchable_items_with_matches
 703            .extend(active_item_matches);
 704    }
 705
 706    fn update_matches(&mut self, cx: &mut ViewContext<Self>) -> oneshot::Receiver<()> {
 707        let (done_tx, done_rx) = oneshot::channel();
 708        let query = self.query(cx);
 709        self.pending_search.take();
 710        if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
 711            if query.is_empty() {
 712                self.active_match_index.take();
 713                active_searchable_item.clear_matches(cx);
 714                let _ = done_tx.send(());
 715                cx.notify();
 716            } else {
 717                let query = if self.current_mode == SearchMode::Regex {
 718                    match SearchQuery::regex(
 719                        query,
 720                        self.search_options.contains(SearchOptions::WHOLE_WORD),
 721                        self.search_options.contains(SearchOptions::CASE_SENSITIVE),
 722                        Vec::new(),
 723                        Vec::new(),
 724                    ) {
 725                        Ok(query) => query,
 726                        Err(_) => {
 727                            self.query_contains_error = true;
 728                            cx.notify();
 729                            return done_rx;
 730                        }
 731                    }
 732                } else {
 733                    SearchQuery::text(
 734                        query,
 735                        self.search_options.contains(SearchOptions::WHOLE_WORD),
 736                        self.search_options.contains(SearchOptions::CASE_SENSITIVE),
 737                        Vec::new(),
 738                        Vec::new(),
 739                    )
 740                };
 741
 742                let query_text = query.as_str().to_string();
 743                let matches = active_searchable_item.find_matches(query, cx);
 744
 745                let active_searchable_item = active_searchable_item.downgrade();
 746                self.pending_search = Some(cx.spawn(|this, mut cx| async move {
 747                    let matches = matches.await;
 748                    this.update(&mut cx, |this, cx| {
 749                        if let Some(active_searchable_item) =
 750                            WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx)
 751                        {
 752                            this.searchable_items_with_matches
 753                                .insert(active_searchable_item.downgrade(), matches);
 754
 755                            this.update_match_index(cx);
 756                            this.search_history.add(query_text);
 757                            if !this.dismissed {
 758                                let matches = this
 759                                    .searchable_items_with_matches
 760                                    .get(&active_searchable_item.downgrade())
 761                                    .unwrap();
 762                                active_searchable_item.update_matches(matches, cx);
 763                                let _ = done_tx.send(());
 764                            }
 765                            cx.notify();
 766                        }
 767                    })
 768                    .log_err();
 769                }));
 770            }
 771        }
 772        done_rx
 773    }
 774
 775    fn update_match_index(&mut self, cx: &mut ViewContext<Self>) {
 776        let new_index = self
 777            .active_searchable_item
 778            .as_ref()
 779            .and_then(|searchable_item| {
 780                let matches = self
 781                    .searchable_items_with_matches
 782                    .get(&searchable_item.downgrade())?;
 783                searchable_item.active_match_index(matches, cx)
 784            });
 785        if new_index != self.active_match_index {
 786            self.active_match_index = new_index;
 787            cx.notify();
 788        }
 789    }
 790
 791    fn next_history_query(&mut self, _: &NextHistoryQuery, cx: &mut ViewContext<Self>) {
 792        if let Some(new_query) = self.search_history.next().map(str::to_string) {
 793            let _ = self.search(&new_query, Some(self.search_options), cx);
 794        } else {
 795            self.search_history.reset_selection();
 796            let _ = self.search("", Some(self.search_options), cx);
 797        }
 798    }
 799
 800    fn previous_history_query(&mut self, _: &PreviousHistoryQuery, cx: &mut ViewContext<Self>) {
 801        if self.query(cx).is_empty() {
 802            if let Some(new_query) = self.search_history.current().map(str::to_string) {
 803                let _ = self.search(&new_query, Some(self.search_options), cx);
 804                return;
 805            }
 806        }
 807
 808        if let Some(new_query) = self.search_history.previous().map(str::to_string) {
 809            let _ = self.search(&new_query, Some(self.search_options), cx);
 810        }
 811    }
 812    fn cycle_mode(&mut self, _: &CycleMode, cx: &mut ViewContext<Self>) {
 813        self.activate_search_mode(next_mode(&self.current_mode, false), cx);
 814    }
 815    fn cycle_mode_on_pane(pane: &mut Pane, action: &CycleMode, cx: &mut ViewContext<Pane>) {
 816        let mut should_propagate = true;
 817        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
 818            search_bar.update(cx, |bar, cx| {
 819                if bar.show(cx) {
 820                    should_propagate = false;
 821                    bar.cycle_mode(action, cx);
 822                    false
 823                } else {
 824                    true
 825                }
 826            });
 827        }
 828        if should_propagate {
 829            cx.propagate_action();
 830        }
 831    }
 832}
 833
 834#[cfg(test)]
 835mod tests {
 836    use super::*;
 837    use editor::{DisplayPoint, Editor};
 838    use gpui::{color::Color, test::EmptyView, TestAppContext};
 839    use language::Buffer;
 840    use unindent::Unindent as _;
 841
 842    fn init_test(cx: &mut TestAppContext) -> (ViewHandle<Editor>, ViewHandle<BufferSearchBar>) {
 843        crate::project_search::tests::init_test(cx);
 844
 845        let buffer = cx.add_model(|cx| {
 846            Buffer::new(
 847                0,
 848                r#"
 849                A regular expression (shortened as regex or regexp;[1] also referred to as
 850                rational expression[2][3]) is a sequence of characters that specifies a search
 851                pattern in text. Usually such patterns are used by string-searching algorithms
 852                for "find" or "find and replace" operations on strings, or for input validation.
 853                "#
 854                .unindent(),
 855                cx,
 856            )
 857        });
 858        let window = cx.add_window(|_| EmptyView);
 859        let editor = window.add_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx));
 860
 861        let search_bar = window.add_view(cx, |cx| {
 862            let mut search_bar = BufferSearchBar::new(cx);
 863            search_bar.set_active_pane_item(Some(&editor), cx);
 864            search_bar.show(cx);
 865            search_bar
 866        });
 867
 868        (editor, search_bar)
 869    }
 870
 871    #[gpui::test]
 872    async fn test_search_simple(cx: &mut TestAppContext) {
 873        let (editor, search_bar) = init_test(cx);
 874
 875        // Search for a string that appears with different casing.
 876        // By default, search is case-insensitive.
 877        search_bar
 878            .update(cx, |search_bar, cx| search_bar.search("us", None, cx))
 879            .await
 880            .unwrap();
 881        editor.update(cx, |editor, cx| {
 882            assert_eq!(
 883                editor.all_background_highlights(cx),
 884                &[
 885                    (
 886                        DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19),
 887                        Color::red(),
 888                    ),
 889                    (
 890                        DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
 891                        Color::red(),
 892                    ),
 893                ]
 894            );
 895        });
 896
 897        // Switch to a case sensitive search.
 898        search_bar.update(cx, |search_bar, cx| {
 899            search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx);
 900        });
 901        editor.next_notification(cx).await;
 902        editor.update(cx, |editor, cx| {
 903            assert_eq!(
 904                editor.all_background_highlights(cx),
 905                &[(
 906                    DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
 907                    Color::red(),
 908                )]
 909            );
 910        });
 911
 912        // Search for a string that appears both as a whole word and
 913        // within other words. By default, all results are found.
 914        search_bar
 915            .update(cx, |search_bar, cx| search_bar.search("or", None, cx))
 916            .await
 917            .unwrap();
 918        editor.update(cx, |editor, cx| {
 919            assert_eq!(
 920                editor.all_background_highlights(cx),
 921                &[
 922                    (
 923                        DisplayPoint::new(0, 24)..DisplayPoint::new(0, 26),
 924                        Color::red(),
 925                    ),
 926                    (
 927                        DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
 928                        Color::red(),
 929                    ),
 930                    (
 931                        DisplayPoint::new(2, 71)..DisplayPoint::new(2, 73),
 932                        Color::red(),
 933                    ),
 934                    (
 935                        DisplayPoint::new(3, 1)..DisplayPoint::new(3, 3),
 936                        Color::red(),
 937                    ),
 938                    (
 939                        DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
 940                        Color::red(),
 941                    ),
 942                    (
 943                        DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
 944                        Color::red(),
 945                    ),
 946                    (
 947                        DisplayPoint::new(3, 60)..DisplayPoint::new(3, 62),
 948                        Color::red(),
 949                    ),
 950                ]
 951            );
 952        });
 953
 954        // Switch to a whole word search.
 955        search_bar.update(cx, |search_bar, cx| {
 956            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
 957        });
 958        editor.next_notification(cx).await;
 959        editor.update(cx, |editor, cx| {
 960            assert_eq!(
 961                editor.all_background_highlights(cx),
 962                &[
 963                    (
 964                        DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
 965                        Color::red(),
 966                    ),
 967                    (
 968                        DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
 969                        Color::red(),
 970                    ),
 971                    (
 972                        DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
 973                        Color::red(),
 974                    ),
 975                ]
 976            );
 977        });
 978
 979        editor.update(cx, |editor, cx| {
 980            editor.change_selections(None, cx, |s| {
 981                s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
 982            });
 983        });
 984        search_bar.update(cx, |search_bar, cx| {
 985            assert_eq!(search_bar.active_match_index, Some(0));
 986            search_bar.select_next_match(&SelectNextMatch, cx);
 987            assert_eq!(
 988                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
 989                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
 990            );
 991        });
 992        search_bar.read_with(cx, |search_bar, _| {
 993            assert_eq!(search_bar.active_match_index, Some(0));
 994        });
 995
 996        search_bar.update(cx, |search_bar, cx| {
 997            search_bar.select_next_match(&SelectNextMatch, cx);
 998            assert_eq!(
 999                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1000                [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
1001            );
1002        });
1003        search_bar.read_with(cx, |search_bar, _| {
1004            assert_eq!(search_bar.active_match_index, Some(1));
1005        });
1006
1007        search_bar.update(cx, |search_bar, cx| {
1008            search_bar.select_next_match(&SelectNextMatch, cx);
1009            assert_eq!(
1010                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1011                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1012            );
1013        });
1014        search_bar.read_with(cx, |search_bar, _| {
1015            assert_eq!(search_bar.active_match_index, Some(2));
1016        });
1017
1018        search_bar.update(cx, |search_bar, cx| {
1019            search_bar.select_next_match(&SelectNextMatch, cx);
1020            assert_eq!(
1021                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1022                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1023            );
1024        });
1025        search_bar.read_with(cx, |search_bar, _| {
1026            assert_eq!(search_bar.active_match_index, Some(0));
1027        });
1028
1029        search_bar.update(cx, |search_bar, cx| {
1030            search_bar.select_prev_match(&SelectPrevMatch, cx);
1031            assert_eq!(
1032                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1033                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1034            );
1035        });
1036        search_bar.read_with(cx, |search_bar, _| {
1037            assert_eq!(search_bar.active_match_index, Some(2));
1038        });
1039
1040        search_bar.update(cx, |search_bar, cx| {
1041            search_bar.select_prev_match(&SelectPrevMatch, cx);
1042            assert_eq!(
1043                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1044                [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
1045            );
1046        });
1047        search_bar.read_with(cx, |search_bar, _| {
1048            assert_eq!(search_bar.active_match_index, Some(1));
1049        });
1050
1051        search_bar.update(cx, |search_bar, cx| {
1052            search_bar.select_prev_match(&SelectPrevMatch, cx);
1053            assert_eq!(
1054                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1055                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1056            );
1057        });
1058        search_bar.read_with(cx, |search_bar, _| {
1059            assert_eq!(search_bar.active_match_index, Some(0));
1060        });
1061
1062        // Park the cursor in between matches and ensure that going to the previous match selects
1063        // the closest match to the left.
1064        editor.update(cx, |editor, cx| {
1065            editor.change_selections(None, cx, |s| {
1066                s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
1067            });
1068        });
1069        search_bar.update(cx, |search_bar, cx| {
1070            assert_eq!(search_bar.active_match_index, Some(1));
1071            search_bar.select_prev_match(&SelectPrevMatch, cx);
1072            assert_eq!(
1073                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1074                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1075            );
1076        });
1077        search_bar.read_with(cx, |search_bar, _| {
1078            assert_eq!(search_bar.active_match_index, Some(0));
1079        });
1080
1081        // Park the cursor in between matches and ensure that going to the next match selects the
1082        // closest match to the right.
1083        editor.update(cx, |editor, cx| {
1084            editor.change_selections(None, cx, |s| {
1085                s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
1086            });
1087        });
1088        search_bar.update(cx, |search_bar, cx| {
1089            assert_eq!(search_bar.active_match_index, Some(1));
1090            search_bar.select_next_match(&SelectNextMatch, cx);
1091            assert_eq!(
1092                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1093                [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
1094            );
1095        });
1096        search_bar.read_with(cx, |search_bar, _| {
1097            assert_eq!(search_bar.active_match_index, Some(1));
1098        });
1099
1100        // Park the cursor after the last match and ensure that going to the previous match selects
1101        // the last match.
1102        editor.update(cx, |editor, cx| {
1103            editor.change_selections(None, cx, |s| {
1104                s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
1105            });
1106        });
1107        search_bar.update(cx, |search_bar, cx| {
1108            assert_eq!(search_bar.active_match_index, Some(2));
1109            search_bar.select_prev_match(&SelectPrevMatch, cx);
1110            assert_eq!(
1111                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1112                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1113            );
1114        });
1115        search_bar.read_with(cx, |search_bar, _| {
1116            assert_eq!(search_bar.active_match_index, Some(2));
1117        });
1118
1119        // Park the cursor after the last match and ensure that going to the next match selects the
1120        // first match.
1121        editor.update(cx, |editor, cx| {
1122            editor.change_selections(None, cx, |s| {
1123                s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
1124            });
1125        });
1126        search_bar.update(cx, |search_bar, cx| {
1127            assert_eq!(search_bar.active_match_index, Some(2));
1128            search_bar.select_next_match(&SelectNextMatch, cx);
1129            assert_eq!(
1130                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1131                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1132            );
1133        });
1134        search_bar.read_with(cx, |search_bar, _| {
1135            assert_eq!(search_bar.active_match_index, Some(0));
1136        });
1137
1138        // Park the cursor before the first match and ensure that going to the previous match
1139        // selects the last match.
1140        editor.update(cx, |editor, cx| {
1141            editor.change_selections(None, cx, |s| {
1142                s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
1143            });
1144        });
1145        search_bar.update(cx, |search_bar, cx| {
1146            assert_eq!(search_bar.active_match_index, Some(0));
1147            search_bar.select_prev_match(&SelectPrevMatch, cx);
1148            assert_eq!(
1149                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1150                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1151            );
1152        });
1153        search_bar.read_with(cx, |search_bar, _| {
1154            assert_eq!(search_bar.active_match_index, Some(2));
1155        });
1156    }
1157
1158    #[gpui::test]
1159    async fn test_search_option_handling(cx: &mut TestAppContext) {
1160        let (editor, search_bar) = init_test(cx);
1161
1162        // show with options should make current search case sensitive
1163        search_bar
1164            .update(cx, |search_bar, cx| {
1165                search_bar.show(cx);
1166                search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), cx)
1167            })
1168            .await
1169            .unwrap();
1170        editor.update(cx, |editor, cx| {
1171            assert_eq!(
1172                editor.all_background_highlights(cx),
1173                &[(
1174                    DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
1175                    Color::red(),
1176                )]
1177            );
1178        });
1179
1180        // search_suggested should restore default options
1181        search_bar.update(cx, |search_bar, cx| {
1182            search_bar.search_suggested(cx);
1183            assert_eq!(search_bar.search_options, SearchOptions::NONE)
1184        });
1185
1186        // toggling a search option should update the defaults
1187        search_bar
1188            .update(cx, |search_bar, cx| {
1189                search_bar.search("regex", Some(SearchOptions::CASE_SENSITIVE), cx)
1190            })
1191            .await
1192            .unwrap();
1193        search_bar.update(cx, |search_bar, cx| {
1194            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx)
1195        });
1196        editor.next_notification(cx).await;
1197        editor.update(cx, |editor, cx| {
1198            assert_eq!(
1199                editor.all_background_highlights(cx),
1200                &[(
1201                    DisplayPoint::new(0, 35)..DisplayPoint::new(0, 40),
1202                    Color::red(),
1203                ),]
1204            );
1205        });
1206
1207        // defaults should still include whole word
1208        search_bar.update(cx, |search_bar, cx| {
1209            search_bar.search_suggested(cx);
1210            assert_eq!(
1211                search_bar.search_options,
1212                SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD
1213            )
1214        });
1215    }
1216
1217    #[gpui::test]
1218    async fn test_search_select_all_matches(cx: &mut TestAppContext) {
1219        crate::project_search::tests::init_test(cx);
1220
1221        let buffer_text = r#"
1222        A regular expression (shortened as regex or regexp;[1] also referred to as
1223        rational expression[2][3]) is a sequence of characters that specifies a search
1224        pattern in text. Usually such patterns are used by string-searching algorithms
1225        for "find" or "find and replace" operations on strings, or for input validation.
1226        "#
1227        .unindent();
1228        let expected_query_matches_count = buffer_text
1229            .chars()
1230            .filter(|c| c.to_ascii_lowercase() == 'a')
1231            .count();
1232        assert!(
1233            expected_query_matches_count > 1,
1234            "Should pick a query with multiple results"
1235        );
1236        let buffer = cx.add_model(|cx| Buffer::new(0, buffer_text, cx));
1237        let window = cx.add_window(|_| EmptyView);
1238        let editor = window.add_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx));
1239
1240        let search_bar = window.add_view(cx, |cx| {
1241            let mut search_bar = BufferSearchBar::new(cx);
1242            search_bar.set_active_pane_item(Some(&editor), cx);
1243            search_bar.show(cx);
1244            search_bar
1245        });
1246
1247        search_bar
1248            .update(cx, |search_bar, cx| search_bar.search("a", None, cx))
1249            .await
1250            .unwrap();
1251        search_bar.update(cx, |search_bar, cx| {
1252            cx.focus(search_bar.query_editor.as_any());
1253            search_bar.activate_current_match(cx);
1254        });
1255
1256        window.read_with(cx, |cx| {
1257            assert!(
1258                !editor.is_focused(cx),
1259                "Initially, the editor should not be focused"
1260            );
1261        });
1262
1263        let initial_selections = editor.update(cx, |editor, cx| {
1264            let initial_selections = editor.selections.display_ranges(cx);
1265            assert_eq!(
1266                initial_selections.len(), 1,
1267                "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
1268            );
1269            initial_selections
1270        });
1271        search_bar.update(cx, |search_bar, _| {
1272            assert_eq!(search_bar.active_match_index, Some(0));
1273        });
1274
1275        search_bar.update(cx, |search_bar, cx| {
1276            cx.focus(search_bar.query_editor.as_any());
1277            search_bar.select_all_matches(&SelectAllMatches, cx);
1278        });
1279        window.read_with(cx, |cx| {
1280            assert!(
1281                editor.is_focused(cx),
1282                "Should focus editor after successful SelectAllMatches"
1283            );
1284        });
1285        search_bar.update(cx, |search_bar, cx| {
1286            let all_selections =
1287                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1288            assert_eq!(
1289                all_selections.len(),
1290                expected_query_matches_count,
1291                "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1292            );
1293            assert_eq!(
1294                search_bar.active_match_index,
1295                Some(0),
1296                "Match index should not change after selecting all matches"
1297            );
1298        });
1299
1300        search_bar.update(cx, |search_bar, cx| {
1301            search_bar.select_next_match(&SelectNextMatch, cx);
1302        });
1303        window.read_with(cx, |cx| {
1304            assert!(
1305                editor.is_focused(cx),
1306                "Should still have editor focused after SelectNextMatch"
1307            );
1308        });
1309        search_bar.update(cx, |search_bar, cx| {
1310            let all_selections =
1311                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1312            assert_eq!(
1313                all_selections.len(),
1314                1,
1315                "On next match, should deselect items and select the next match"
1316            );
1317            assert_ne!(
1318                all_selections, initial_selections,
1319                "Next match should be different from the first selection"
1320            );
1321            assert_eq!(
1322                search_bar.active_match_index,
1323                Some(1),
1324                "Match index should be updated to the next one"
1325            );
1326        });
1327
1328        search_bar.update(cx, |search_bar, cx| {
1329            cx.focus(search_bar.query_editor.as_any());
1330            search_bar.select_all_matches(&SelectAllMatches, cx);
1331        });
1332        window.read_with(cx, |cx| {
1333            assert!(
1334                editor.is_focused(cx),
1335                "Should focus editor after successful SelectAllMatches"
1336            );
1337        });
1338        search_bar.update(cx, |search_bar, cx| {
1339            let all_selections =
1340                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1341            assert_eq!(
1342                all_selections.len(),
1343                expected_query_matches_count,
1344                "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1345            );
1346            assert_eq!(
1347                search_bar.active_match_index,
1348                Some(1),
1349                "Match index should not change after selecting all matches"
1350            );
1351        });
1352
1353        search_bar.update(cx, |search_bar, cx| {
1354            search_bar.select_prev_match(&SelectPrevMatch, cx);
1355        });
1356        window.read_with(cx, |cx| {
1357            assert!(
1358                editor.is_focused(cx),
1359                "Should still have editor focused after SelectPrevMatch"
1360            );
1361        });
1362        let last_match_selections = 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.len(),
1367                1,
1368                "On previous match, should deselect items and select the previous item"
1369            );
1370            assert_eq!(
1371                all_selections, initial_selections,
1372                "Previous match should be the same as the first selection"
1373            );
1374            assert_eq!(
1375                search_bar.active_match_index,
1376                Some(0),
1377                "Match index should be updated to the previous one"
1378            );
1379            all_selections
1380        });
1381
1382        search_bar
1383            .update(cx, |search_bar, cx| {
1384                cx.focus(search_bar.query_editor.as_any());
1385                search_bar.search("abas_nonexistent_match", None, cx)
1386            })
1387            .await
1388            .unwrap();
1389        search_bar.update(cx, |search_bar, cx| {
1390            search_bar.select_all_matches(&SelectAllMatches, cx);
1391        });
1392        window.read_with(cx, |cx| {
1393            assert!(
1394                !editor.is_focused(cx),
1395                "Should not switch focus to editor if SelectAllMatches does not find any matches"
1396            );
1397        });
1398        search_bar.update(cx, |search_bar, cx| {
1399            let all_selections =
1400                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1401            assert_eq!(
1402                all_selections, last_match_selections,
1403                "Should not select anything new if there are no matches"
1404            );
1405            assert!(
1406                search_bar.active_match_index.is_none(),
1407                "For no matches, there should be no active match index"
1408            );
1409        });
1410    }
1411
1412    #[gpui::test]
1413    async fn test_search_query_history(cx: &mut TestAppContext) {
1414        crate::project_search::tests::init_test(cx);
1415
1416        let buffer_text = r#"
1417        A regular expression (shortened as regex or regexp;[1] also referred to as
1418        rational expression[2][3]) is a sequence of characters that specifies a search
1419        pattern in text. Usually such patterns are used by string-searching algorithms
1420        for "find" or "find and replace" operations on strings, or for input validation.
1421        "#
1422        .unindent();
1423        let buffer = cx.add_model(|cx| Buffer::new(0, buffer_text, cx));
1424        let window = cx.add_window(|_| EmptyView);
1425
1426        let editor = window.add_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx));
1427
1428        let search_bar = window.add_view(cx, |cx| {
1429            let mut search_bar = BufferSearchBar::new(cx);
1430            search_bar.set_active_pane_item(Some(&editor), cx);
1431            search_bar.show(cx);
1432            search_bar
1433        });
1434
1435        // Add 3 search items into the history.
1436        search_bar
1437            .update(cx, |search_bar, cx| search_bar.search("a", None, cx))
1438            .await
1439            .unwrap();
1440        search_bar
1441            .update(cx, |search_bar, cx| search_bar.search("b", None, cx))
1442            .await
1443            .unwrap();
1444        search_bar
1445            .update(cx, |search_bar, cx| {
1446                search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), cx)
1447            })
1448            .await
1449            .unwrap();
1450        // Ensure that the latest search is active.
1451        search_bar.read_with(cx, |search_bar, cx| {
1452            assert_eq!(search_bar.query(cx), "c");
1453            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1454        });
1455
1456        // Next history query after the latest should set the query to the empty string.
1457        search_bar.update(cx, |search_bar, cx| {
1458            search_bar.next_history_query(&NextHistoryQuery, cx);
1459        });
1460        search_bar.read_with(cx, |search_bar, cx| {
1461            assert_eq!(search_bar.query(cx), "");
1462            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1463        });
1464        search_bar.update(cx, |search_bar, cx| {
1465            search_bar.next_history_query(&NextHistoryQuery, cx);
1466        });
1467        search_bar.read_with(cx, |search_bar, cx| {
1468            assert_eq!(search_bar.query(cx), "");
1469            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1470        });
1471
1472        // First previous query for empty current query should set the query to the latest.
1473        search_bar.update(cx, |search_bar, cx| {
1474            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1475        });
1476        search_bar.read_with(cx, |search_bar, cx| {
1477            assert_eq!(search_bar.query(cx), "c");
1478            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1479        });
1480
1481        // Further previous items should go over the history in reverse order.
1482        search_bar.update(cx, |search_bar, cx| {
1483            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1484        });
1485        search_bar.read_with(cx, |search_bar, cx| {
1486            assert_eq!(search_bar.query(cx), "b");
1487            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1488        });
1489
1490        // Previous items should never go behind the first history item.
1491        search_bar.update(cx, |search_bar, cx| {
1492            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1493        });
1494        search_bar.read_with(cx, |search_bar, cx| {
1495            assert_eq!(search_bar.query(cx), "a");
1496            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1497        });
1498        search_bar.update(cx, |search_bar, cx| {
1499            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1500        });
1501        search_bar.read_with(cx, |search_bar, cx| {
1502            assert_eq!(search_bar.query(cx), "a");
1503            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1504        });
1505
1506        // Next items should go over the history in the original order.
1507        search_bar.update(cx, |search_bar, cx| {
1508            search_bar.next_history_query(&NextHistoryQuery, cx);
1509        });
1510        search_bar.read_with(cx, |search_bar, cx| {
1511            assert_eq!(search_bar.query(cx), "b");
1512            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1513        });
1514
1515        search_bar
1516            .update(cx, |search_bar, cx| search_bar.search("ba", None, cx))
1517            .await
1518            .unwrap();
1519        search_bar.read_with(cx, |search_bar, cx| {
1520            assert_eq!(search_bar.query(cx), "ba");
1521            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1522        });
1523
1524        // New search input should add another entry to history and move the selection to the end of the history.
1525        search_bar.update(cx, |search_bar, cx| {
1526            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1527        });
1528        search_bar.read_with(cx, |search_bar, cx| {
1529            assert_eq!(search_bar.query(cx), "c");
1530            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1531        });
1532        search_bar.update(cx, |search_bar, cx| {
1533            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1534        });
1535        search_bar.read_with(cx, |search_bar, cx| {
1536            assert_eq!(search_bar.query(cx), "b");
1537            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1538        });
1539        search_bar.update(cx, |search_bar, cx| {
1540            search_bar.next_history_query(&NextHistoryQuery, cx);
1541        });
1542        search_bar.read_with(cx, |search_bar, cx| {
1543            assert_eq!(search_bar.query(cx), "c");
1544            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1545        });
1546        search_bar.update(cx, |search_bar, cx| {
1547            search_bar.next_history_query(&NextHistoryQuery, cx);
1548        });
1549        search_bar.read_with(cx, |search_bar, cx| {
1550            assert_eq!(search_bar.query(cx), "ba");
1551            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1552        });
1553        search_bar.update(cx, |search_bar, cx| {
1554            search_bar.next_history_query(&NextHistoryQuery, cx);
1555        });
1556        search_bar.read_with(cx, |search_bar, cx| {
1557            assert_eq!(search_bar.query(cx), "");
1558            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1559        });
1560    }
1561}