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