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