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