buffer_search.rs

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