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