buffer_search.rs

   1use crate::{
   2    SearchOption, SelectAllMatches, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive,
   3    ToggleRegex, ToggleWholeWord,
   4};
   5use collections::HashMap;
   6use editor::Editor;
   7use gpui::{
   8    actions,
   9    elements::*,
  10    impl_actions,
  11    platform::{CursorStyle, MouseButton},
  12    Action, AnyViewHandle, AppContext, Entity, Subscription, Task, View, ViewContext, ViewHandle,
  13};
  14use project::search::SearchQuery;
  15use serde::Deserialize;
  16use std::{any::Any, sync::Arc};
  17use util::ResultExt;
  18use workspace::{
  19    item::ItemHandle,
  20    searchable::{Direction, SearchEvent, SearchableItemHandle, WeakSearchableItemHandle},
  21    Pane, ToolbarItemLocation, ToolbarItemView,
  22};
  23
  24#[derive(Clone, Deserialize, PartialEq)]
  25pub struct Deploy {
  26    pub focus: bool,
  27}
  28
  29actions!(buffer_search, [Dismiss, FocusEditor]);
  30impl_actions!(buffer_search, [Deploy]);
  31
  32pub enum Event {
  33    UpdateLocation,
  34}
  35
  36pub fn init(cx: &mut AppContext) {
  37    cx.add_action(BufferSearchBar::deploy);
  38    cx.add_action(BufferSearchBar::dismiss);
  39    cx.add_action(BufferSearchBar::focus_editor);
  40    cx.add_action(BufferSearchBar::select_next_match);
  41    cx.add_action(BufferSearchBar::select_prev_match);
  42    cx.add_action(BufferSearchBar::select_all_matches);
  43    cx.add_action(BufferSearchBar::select_next_match_on_pane);
  44    cx.add_action(BufferSearchBar::select_prev_match_on_pane);
  45    cx.add_action(BufferSearchBar::select_all_matches_on_pane);
  46    cx.add_action(BufferSearchBar::handle_editor_cancel);
  47    add_toggle_option_action::<ToggleCaseSensitive>(SearchOption::CaseSensitive, cx);
  48    add_toggle_option_action::<ToggleWholeWord>(SearchOption::WholeWord, cx);
  49    add_toggle_option_action::<ToggleRegex>(SearchOption::Regex, cx);
  50}
  51
  52fn add_toggle_option_action<A: Action>(option: SearchOption, cx: &mut AppContext) {
  53    cx.add_action(move |pane: &mut Pane, _: &A, cx: &mut ViewContext<Pane>| {
  54        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
  55            if search_bar.update(cx, |search_bar, cx| search_bar.show(false, false, cx)) {
  56                search_bar.update(cx, |search_bar, cx| {
  57                    search_bar.toggle_search_option(option, cx);
  58                });
  59                return;
  60            }
  61        }
  62        cx.propagate_action();
  63    });
  64}
  65
  66pub struct BufferSearchBar {
  67    pub query_editor: ViewHandle<Editor>,
  68    active_searchable_item: Option<Box<dyn SearchableItemHandle>>,
  69    active_match_index: Option<usize>,
  70    active_searchable_item_subscription: Option<Subscription>,
  71    searchable_items_with_matches:
  72        HashMap<Box<dyn WeakSearchableItemHandle>, Vec<Box<dyn Any + Send>>>,
  73    pending_search: Option<Task<()>>,
  74    case_sensitive: bool,
  75    whole_word: bool,
  76    regex: bool,
  77    query_contains_error: bool,
  78    dismissed: bool,
  79}
  80
  81impl Entity for BufferSearchBar {
  82    type Event = Event;
  83}
  84
  85impl View for BufferSearchBar {
  86    fn ui_name() -> &'static str {
  87        "BufferSearchBar"
  88    }
  89
  90    fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
  91        if cx.is_self_focused() {
  92            cx.focus(&self.query_editor);
  93        }
  94    }
  95
  96    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
  97        let theme = theme::current(cx).clone();
  98        let editor_container = if self.query_contains_error {
  99            theme.search.invalid_editor
 100        } else {
 101            theme.search.editor.input.container
 102        };
 103        let supported_options = self
 104            .active_searchable_item
 105            .as_ref()
 106            .map(|active_searchable_item| active_searchable_item.supported_options())
 107            .unwrap_or_default();
 108
 109        Flex::row()
 110            .with_child(
 111                Flex::row()
 112                    .with_child(
 113                        Flex::row()
 114                            .with_child(
 115                                ChildView::new(&self.query_editor, cx)
 116                                    .aligned()
 117                                    .left()
 118                                    .flex(1., true),
 119                            )
 120                            .with_children(self.active_searchable_item.as_ref().and_then(
 121                                |searchable_item| {
 122                                    let matches = self
 123                                        .searchable_items_with_matches
 124                                        .get(&searchable_item.downgrade())?;
 125                                    let message = if let Some(match_ix) = self.active_match_index {
 126                                        format!("{}/{}", match_ix + 1, matches.len())
 127                                    } else {
 128                                        "No matches".to_string()
 129                                    };
 130
 131                                    Some(
 132                                        Label::new(message, theme.search.match_index.text.clone())
 133                                            .contained()
 134                                            .with_style(theme.search.match_index.container)
 135                                            .aligned(),
 136                                    )
 137                                },
 138                            ))
 139                            .contained()
 140                            .with_style(editor_container)
 141                            .aligned()
 142                            .constrained()
 143                            .with_min_width(theme.search.editor.min_width)
 144                            .with_max_width(theme.search.editor.max_width)
 145                            .flex(1., false),
 146                    )
 147                    .with_child(
 148                        Flex::row()
 149                            .with_child(self.render_nav_button("<", Direction::Prev, cx))
 150                            .with_child(self.render_nav_button(">", Direction::Next, cx))
 151                            .with_child(self.render_action_button("Select All", cx))
 152                            .aligned(),
 153                    )
 154                    .with_child(
 155                        Flex::row()
 156                            .with_children(self.render_search_option(
 157                                supported_options.case,
 158                                "Case",
 159                                SearchOption::CaseSensitive,
 160                                cx,
 161                            ))
 162                            .with_children(self.render_search_option(
 163                                supported_options.word,
 164                                "Word",
 165                                SearchOption::WholeWord,
 166                                cx,
 167                            ))
 168                            .with_children(self.render_search_option(
 169                                supported_options.regex,
 170                                "Regex",
 171                                SearchOption::Regex,
 172                                cx,
 173                            ))
 174                            .contained()
 175                            .with_style(theme.search.option_button_group)
 176                            .aligned(),
 177                    )
 178                    .flex(1., true),
 179            )
 180            .with_child(self.render_close_button(&theme.search, cx))
 181            .contained()
 182            .with_style(theme.search.container)
 183            .into_any_named("search bar")
 184    }
 185}
 186
 187impl ToolbarItemView for BufferSearchBar {
 188    fn set_active_pane_item(
 189        &mut self,
 190        item: Option<&dyn ItemHandle>,
 191        cx: &mut ViewContext<Self>,
 192    ) -> ToolbarItemLocation {
 193        cx.notify();
 194        self.active_searchable_item_subscription.take();
 195        self.active_searchable_item.take();
 196        self.pending_search.take();
 197
 198        if let Some(searchable_item_handle) =
 199            item.and_then(|item| item.to_searchable_item_handle(cx))
 200        {
 201            let this = cx.weak_handle();
 202            self.active_searchable_item_subscription =
 203                Some(searchable_item_handle.subscribe_to_search_events(
 204                    cx,
 205                    Box::new(move |search_event, cx| {
 206                        if let Some(this) = this.upgrade(cx) {
 207                            this.update(cx, |this, cx| {
 208                                this.on_active_searchable_item_event(search_event, cx)
 209                            });
 210                        }
 211                    }),
 212                ));
 213
 214            self.active_searchable_item = Some(searchable_item_handle);
 215            self.update_matches(false, cx);
 216            if !self.dismissed {
 217                return ToolbarItemLocation::Secondary;
 218            }
 219        }
 220
 221        ToolbarItemLocation::Hidden
 222    }
 223
 224    fn location_for_event(
 225        &self,
 226        _: &Self::Event,
 227        _: ToolbarItemLocation,
 228        _: &AppContext,
 229    ) -> ToolbarItemLocation {
 230        if self.active_searchable_item.is_some() && !self.dismissed {
 231            ToolbarItemLocation::Secondary
 232        } else {
 233            ToolbarItemLocation::Hidden
 234        }
 235    }
 236}
 237
 238impl BufferSearchBar {
 239    pub fn new(cx: &mut ViewContext<Self>) -> Self {
 240        let query_editor = cx.add_view(|cx| {
 241            Editor::auto_height(
 242                2,
 243                Some(Arc::new(|theme| theme.search.editor.input.clone())),
 244                cx,
 245            )
 246        });
 247        cx.subscribe(&query_editor, Self::on_query_editor_event)
 248            .detach();
 249
 250        Self {
 251            query_editor,
 252            active_searchable_item: None,
 253            active_searchable_item_subscription: None,
 254            active_match_index: None,
 255            searchable_items_with_matches: Default::default(),
 256            case_sensitive: false,
 257            whole_word: false,
 258            regex: false,
 259            pending_search: None,
 260            query_contains_error: false,
 261            dismissed: true,
 262        }
 263    }
 264
 265    pub fn is_dismissed(&self) -> bool {
 266        self.dismissed
 267    }
 268
 269    pub fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext<Self>) {
 270        self.dismissed = true;
 271        for searchable_item in self.searchable_items_with_matches.keys() {
 272            if let Some(searchable_item) =
 273                WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
 274            {
 275                searchable_item.clear_matches(cx);
 276            }
 277        }
 278        if let Some(active_editor) = self.active_searchable_item.as_ref() {
 279            cx.focus(active_editor.as_any());
 280        }
 281        cx.emit(Event::UpdateLocation);
 282        cx.notify();
 283    }
 284
 285    pub fn show(&mut self, focus: bool, suggest_query: bool, cx: &mut ViewContext<Self>) -> bool {
 286        let searchable_item = if let Some(searchable_item) = &self.active_searchable_item {
 287            SearchableItemHandle::boxed_clone(searchable_item.as_ref())
 288        } else {
 289            return false;
 290        };
 291
 292        if suggest_query {
 293            let text = searchable_item.query_suggestion(cx);
 294            if !text.is_empty() {
 295                self.set_query(&text, cx);
 296            }
 297        }
 298
 299        if focus {
 300            let query_editor = self.query_editor.clone();
 301            query_editor.update(cx, |query_editor, cx| {
 302                query_editor.select_all(&editor::SelectAll, cx);
 303            });
 304            cx.focus_self();
 305        }
 306
 307        self.dismissed = false;
 308        cx.notify();
 309        cx.emit(Event::UpdateLocation);
 310        true
 311    }
 312
 313    fn set_query(&mut self, query: &str, cx: &mut ViewContext<Self>) {
 314        self.query_editor.update(cx, |query_editor, cx| {
 315            query_editor.buffer().update(cx, |query_buffer, cx| {
 316                let len = query_buffer.len(cx);
 317                query_buffer.edit([(0..len, query)], None, cx);
 318            });
 319        });
 320    }
 321
 322    fn render_search_option(
 323        &self,
 324        option_supported: bool,
 325        icon: &'static str,
 326        option: SearchOption,
 327        cx: &mut ViewContext<Self>,
 328    ) -> Option<AnyElement<Self>> {
 329        if !option_supported {
 330            return None;
 331        }
 332
 333        let tooltip_style = theme::current(cx).tooltip.clone();
 334        let is_active = self.is_search_option_enabled(option);
 335        Some(
 336            MouseEventHandler::<Self, _>::new(option as usize, cx, |state, cx| {
 337                let theme = theme::current(cx);
 338                let style = theme
 339                    .search
 340                    .option_button
 341                    .in_state(is_active)
 342                    .style_for(state);
 343                Label::new(icon, style.text.clone())
 344                    .contained()
 345                    .with_style(style.container)
 346            })
 347            .on_click(MouseButton::Left, move |_, this, cx| {
 348                this.toggle_search_option(option, cx);
 349            })
 350            .with_cursor_style(CursorStyle::PointingHand)
 351            .with_tooltip::<Self>(
 352                option as usize,
 353                format!("Toggle {}", option.label()),
 354                Some(option.to_toggle_action()),
 355                tooltip_style,
 356                cx,
 357            )
 358            .into_any(),
 359        )
 360    }
 361
 362    fn render_nav_button(
 363        &self,
 364        icon: &'static str,
 365        direction: Direction,
 366        cx: &mut ViewContext<Self>,
 367    ) -> AnyElement<Self> {
 368        let action: Box<dyn Action>;
 369        let tooltip;
 370        match direction {
 371            Direction::Prev => {
 372                action = Box::new(SelectPrevMatch);
 373                tooltip = "Select Previous Match";
 374            }
 375            Direction::Next => {
 376                action = Box::new(SelectNextMatch);
 377                tooltip = "Select Next Match";
 378            }
 379        };
 380        let tooltip_style = theme::current(cx).tooltip.clone();
 381
 382        enum NavButton {}
 383        MouseEventHandler::<NavButton, _>::new(direction as usize, cx, |state, cx| {
 384            let theme = theme::current(cx);
 385            let style = theme.search.option_button.inactive_state().style_for(state);
 386            Label::new(icon, style.text.clone())
 387                .contained()
 388                .with_style(style.container)
 389        })
 390        .on_click(MouseButton::Left, {
 391            move |_, this, cx| match direction {
 392                Direction::Prev => this.select_prev_match(&Default::default(), cx),
 393                Direction::Next => this.select_next_match(&Default::default(), cx),
 394            }
 395        })
 396        .with_cursor_style(CursorStyle::PointingHand)
 397        .with_tooltip::<NavButton>(
 398            direction as usize,
 399            tooltip.to_string(),
 400            Some(action),
 401            tooltip_style,
 402            cx,
 403        )
 404        .into_any()
 405    }
 406
 407    fn render_action_button(
 408        &self,
 409        icon: &'static str,
 410        cx: &mut ViewContext<Self>,
 411    ) -> AnyElement<Self> {
 412        let tooltip = "Select All Matches";
 413        let tooltip_style = theme::current(cx).tooltip.clone();
 414        let action_type_id = 0_usize;
 415
 416        enum ActionButton {}
 417        MouseEventHandler::<ActionButton, _>::new(action_type_id, cx, |state, cx| {
 418            let theme = theme::current(cx);
 419            let style = theme.search.action_button.style_for(state);
 420            Label::new(icon, style.text.clone())
 421                .contained()
 422                .with_style(style.container)
 423        })
 424        .on_click(MouseButton::Left, move |_, this, cx| {
 425            this.select_all_matches(&SelectAllMatches, cx)
 426        })
 427        .with_cursor_style(CursorStyle::PointingHand)
 428        .with_tooltip::<ActionButton>(
 429            action_type_id,
 430            tooltip.to_string(),
 431            Some(Box::new(SelectAllMatches)),
 432            tooltip_style,
 433            cx,
 434        )
 435        .into_any()
 436    }
 437
 438    fn render_close_button(
 439        &self,
 440        theme: &theme::Search,
 441        cx: &mut ViewContext<Self>,
 442    ) -> AnyElement<Self> {
 443        let tooltip = "Dismiss Buffer Search";
 444        let tooltip_style = theme::current(cx).tooltip.clone();
 445
 446        enum CloseButton {}
 447        MouseEventHandler::<CloseButton, _>::new(0, cx, |state, _| {
 448            let style = theme.dismiss_button.style_for(state);
 449            Svg::new("icons/x_mark_8.svg")
 450                .with_color(style.color)
 451                .constrained()
 452                .with_width(style.icon_width)
 453                .aligned()
 454                .constrained()
 455                .with_width(style.button_width)
 456                .contained()
 457                .with_style(style.container)
 458        })
 459        .on_click(MouseButton::Left, move |_, this, cx| {
 460            this.dismiss(&Default::default(), cx)
 461        })
 462        .with_cursor_style(CursorStyle::PointingHand)
 463        .with_tooltip::<CloseButton>(
 464            0,
 465            tooltip.to_string(),
 466            Some(Box::new(Dismiss)),
 467            tooltip_style,
 468            cx,
 469        )
 470        .into_any()
 471    }
 472
 473    fn deploy(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext<Pane>) {
 474        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
 475            if search_bar.update(cx, |search_bar, cx| search_bar.show(action.focus, true, cx)) {
 476                return;
 477            }
 478        }
 479        cx.propagate_action();
 480    }
 481
 482    fn handle_editor_cancel(pane: &mut Pane, _: &editor::Cancel, cx: &mut ViewContext<Pane>) {
 483        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
 484            if !search_bar.read(cx).dismissed {
 485                search_bar.update(cx, |search_bar, cx| search_bar.dismiss(&Dismiss, cx));
 486                return;
 487            }
 488        }
 489        cx.propagate_action();
 490    }
 491
 492    fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext<Self>) {
 493        if let Some(active_editor) = self.active_searchable_item.as_ref() {
 494            cx.focus(active_editor.as_any());
 495        }
 496    }
 497
 498    fn is_search_option_enabled(&self, search_option: SearchOption) -> bool {
 499        match search_option {
 500            SearchOption::WholeWord => self.whole_word,
 501            SearchOption::CaseSensitive => self.case_sensitive,
 502            SearchOption::Regex => self.regex,
 503        }
 504    }
 505
 506    fn toggle_search_option(&mut self, search_option: SearchOption, cx: &mut ViewContext<Self>) {
 507        let value = match search_option {
 508            SearchOption::WholeWord => &mut self.whole_word,
 509            SearchOption::CaseSensitive => &mut self.case_sensitive,
 510            SearchOption::Regex => &mut self.regex,
 511        };
 512        *value = !*value;
 513        self.update_matches(false, cx);
 514        cx.notify();
 515    }
 516
 517    fn select_next_match(&mut self, _: &SelectNextMatch, cx: &mut ViewContext<Self>) {
 518        self.select_match(Direction::Next, cx);
 519    }
 520
 521    fn select_prev_match(&mut self, _: &SelectPrevMatch, cx: &mut ViewContext<Self>) {
 522        self.select_match(Direction::Prev, cx);
 523    }
 524
 525    fn select_all_matches(&mut self, _: &SelectAllMatches, cx: &mut ViewContext<Self>) {
 526        if !self.dismissed {
 527            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
 528                if let Some(matches) = self
 529                    .searchable_items_with_matches
 530                    .get(&searchable_item.downgrade())
 531                {
 532                    searchable_item.select_matches(matches, cx);
 533                    self.focus_editor(&FocusEditor, cx);
 534                }
 535            }
 536        }
 537    }
 538
 539    pub fn select_match(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
 540        if let Some(index) = self.active_match_index {
 541            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
 542                if let Some(matches) = self
 543                    .searchable_items_with_matches
 544                    .get(&searchable_item.downgrade())
 545                {
 546                    let new_match_index =
 547                        searchable_item.match_index_for_direction(matches, index, direction, cx);
 548                    searchable_item.update_matches(matches, cx);
 549                    searchable_item.activate_match(new_match_index, matches, cx);
 550                }
 551            }
 552        }
 553    }
 554
 555    fn select_next_match_on_pane(
 556        pane: &mut Pane,
 557        action: &SelectNextMatch,
 558        cx: &mut ViewContext<Pane>,
 559    ) {
 560        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
 561            search_bar.update(cx, |bar, cx| bar.select_next_match(action, cx));
 562        }
 563    }
 564
 565    fn select_prev_match_on_pane(
 566        pane: &mut Pane,
 567        action: &SelectPrevMatch,
 568        cx: &mut ViewContext<Pane>,
 569    ) {
 570        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
 571            search_bar.update(cx, |bar, cx| bar.select_prev_match(action, cx));
 572        }
 573    }
 574
 575    fn select_all_matches_on_pane(
 576        pane: &mut Pane,
 577        action: &SelectAllMatches,
 578        cx: &mut ViewContext<Pane>,
 579    ) {
 580        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
 581            search_bar.update(cx, |bar, cx| bar.select_all_matches(action, cx));
 582        }
 583    }
 584
 585    fn on_query_editor_event(
 586        &mut self,
 587        _: ViewHandle<Editor>,
 588        event: &editor::Event,
 589        cx: &mut ViewContext<Self>,
 590    ) {
 591        if let editor::Event::BufferEdited { .. } = event {
 592            self.query_contains_error = false;
 593            self.clear_matches(cx);
 594            self.update_matches(true, cx);
 595            cx.notify();
 596        }
 597    }
 598
 599    fn on_active_searchable_item_event(&mut self, event: SearchEvent, cx: &mut ViewContext<Self>) {
 600        match event {
 601            SearchEvent::MatchesInvalidated => self.update_matches(false, cx),
 602            SearchEvent::ActiveMatchChanged => self.update_match_index(cx),
 603        }
 604    }
 605
 606    fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
 607        let mut active_item_matches = None;
 608        for (searchable_item, matches) in self.searchable_items_with_matches.drain() {
 609            if let Some(searchable_item) =
 610                WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
 611            {
 612                if Some(&searchable_item) == self.active_searchable_item.as_ref() {
 613                    active_item_matches = Some((searchable_item.downgrade(), matches));
 614                } else {
 615                    searchable_item.clear_matches(cx);
 616                }
 617            }
 618        }
 619
 620        self.searchable_items_with_matches
 621            .extend(active_item_matches);
 622    }
 623
 624    fn update_matches(&mut self, select_closest_match: bool, cx: &mut ViewContext<Self>) {
 625        let query = self.query_editor.read(cx).text(cx);
 626        self.pending_search.take();
 627        if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
 628            if query.is_empty() {
 629                self.active_match_index.take();
 630                active_searchable_item.clear_matches(cx);
 631            } else {
 632                let query = if self.regex {
 633                    match SearchQuery::regex(
 634                        query,
 635                        self.whole_word,
 636                        self.case_sensitive,
 637                        Vec::new(),
 638                        Vec::new(),
 639                    ) {
 640                        Ok(query) => query,
 641                        Err(_) => {
 642                            self.query_contains_error = true;
 643                            cx.notify();
 644                            return;
 645                        }
 646                    }
 647                } else {
 648                    SearchQuery::text(
 649                        query,
 650                        self.whole_word,
 651                        self.case_sensitive,
 652                        Vec::new(),
 653                        Vec::new(),
 654                    )
 655                };
 656
 657                let matches = active_searchable_item.find_matches(query, cx);
 658
 659                let active_searchable_item = active_searchable_item.downgrade();
 660                self.pending_search = Some(cx.spawn(|this, mut cx| async move {
 661                    let matches = matches.await;
 662                    this.update(&mut cx, |this, cx| {
 663                        if let Some(active_searchable_item) =
 664                            WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx)
 665                        {
 666                            this.searchable_items_with_matches
 667                                .insert(active_searchable_item.downgrade(), matches);
 668
 669                            this.update_match_index(cx);
 670                            if !this.dismissed {
 671                                let matches = this
 672                                    .searchable_items_with_matches
 673                                    .get(&active_searchable_item.downgrade())
 674                                    .unwrap();
 675                                active_searchable_item.update_matches(matches, cx);
 676                                if select_closest_match {
 677                                    if let Some(match_ix) = this.active_match_index {
 678                                        active_searchable_item
 679                                            .activate_match(match_ix, matches, cx);
 680                                    }
 681                                }
 682                            }
 683                            cx.notify();
 684                        }
 685                    })
 686                    .log_err();
 687                }));
 688            }
 689        }
 690    }
 691
 692    fn update_match_index(&mut self, cx: &mut ViewContext<Self>) {
 693        let new_index = self
 694            .active_searchable_item
 695            .as_ref()
 696            .and_then(|searchable_item| {
 697                let matches = self
 698                    .searchable_items_with_matches
 699                    .get(&searchable_item.downgrade())?;
 700                searchable_item.active_match_index(matches, cx)
 701            });
 702        if new_index != self.active_match_index {
 703            self.active_match_index = new_index;
 704            cx.notify();
 705        }
 706    }
 707}
 708
 709#[cfg(test)]
 710mod tests {
 711    use super::*;
 712    use editor::{DisplayPoint, Editor};
 713    use gpui::{color::Color, test::EmptyView, TestAppContext};
 714    use language::Buffer;
 715    use unindent::Unindent as _;
 716
 717    #[gpui::test]
 718    async fn test_search_simple(cx: &mut TestAppContext) {
 719        crate::project_search::tests::init_test(cx);
 720
 721        let buffer = cx.add_model(|cx| {
 722            Buffer::new(
 723                0,
 724                r#"
 725                A regular expression (shortened as regex or regexp;[1] also referred to as
 726                rational expression[2][3]) is a sequence of characters that specifies a search
 727                pattern in text. Usually such patterns are used by string-searching algorithms
 728                for "find" or "find and replace" operations on strings, or for input validation.
 729                "#
 730                .unindent(),
 731                cx,
 732            )
 733        });
 734        let (window_id, _root_view) = cx.add_window(|_| EmptyView);
 735
 736        let editor = cx.add_view(window_id, |cx| Editor::for_buffer(buffer.clone(), None, cx));
 737
 738        let search_bar = cx.add_view(window_id, |cx| {
 739            let mut search_bar = BufferSearchBar::new(cx);
 740            search_bar.set_active_pane_item(Some(&editor), cx);
 741            search_bar.show(false, true, cx);
 742            search_bar
 743        });
 744
 745        // Search for a string that appears with different casing.
 746        // By default, search is case-insensitive.
 747        search_bar.update(cx, |search_bar, cx| {
 748            search_bar.set_query("us", cx);
 749        });
 750        editor.next_notification(cx).await;
 751        editor.update(cx, |editor, cx| {
 752            assert_eq!(
 753                editor.all_background_highlights(cx),
 754                &[
 755                    (
 756                        DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19),
 757                        Color::red(),
 758                    ),
 759                    (
 760                        DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
 761                        Color::red(),
 762                    ),
 763                ]
 764            );
 765        });
 766
 767        // Switch to a case sensitive search.
 768        search_bar.update(cx, |search_bar, cx| {
 769            search_bar.toggle_search_option(SearchOption::CaseSensitive, cx);
 770        });
 771        editor.next_notification(cx).await;
 772        editor.update(cx, |editor, cx| {
 773            assert_eq!(
 774                editor.all_background_highlights(cx),
 775                &[(
 776                    DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
 777                    Color::red(),
 778                )]
 779            );
 780        });
 781
 782        // Search for a string that appears both as a whole word and
 783        // within other words. By default, all results are found.
 784        search_bar.update(cx, |search_bar, cx| {
 785            search_bar.set_query("or", cx);
 786        });
 787        editor.next_notification(cx).await;
 788        editor.update(cx, |editor, cx| {
 789            assert_eq!(
 790                editor.all_background_highlights(cx),
 791                &[
 792                    (
 793                        DisplayPoint::new(0, 24)..DisplayPoint::new(0, 26),
 794                        Color::red(),
 795                    ),
 796                    (
 797                        DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
 798                        Color::red(),
 799                    ),
 800                    (
 801                        DisplayPoint::new(2, 71)..DisplayPoint::new(2, 73),
 802                        Color::red(),
 803                    ),
 804                    (
 805                        DisplayPoint::new(3, 1)..DisplayPoint::new(3, 3),
 806                        Color::red(),
 807                    ),
 808                    (
 809                        DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
 810                        Color::red(),
 811                    ),
 812                    (
 813                        DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
 814                        Color::red(),
 815                    ),
 816                    (
 817                        DisplayPoint::new(3, 60)..DisplayPoint::new(3, 62),
 818                        Color::red(),
 819                    ),
 820                ]
 821            );
 822        });
 823
 824        // Switch to a whole word search.
 825        search_bar.update(cx, |search_bar, cx| {
 826            search_bar.toggle_search_option(SearchOption::WholeWord, cx);
 827        });
 828        editor.next_notification(cx).await;
 829        editor.update(cx, |editor, cx| {
 830            assert_eq!(
 831                editor.all_background_highlights(cx),
 832                &[
 833                    (
 834                        DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
 835                        Color::red(),
 836                    ),
 837                    (
 838                        DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
 839                        Color::red(),
 840                    ),
 841                    (
 842                        DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
 843                        Color::red(),
 844                    ),
 845                ]
 846            );
 847        });
 848
 849        editor.update(cx, |editor, cx| {
 850            editor.change_selections(None, cx, |s| {
 851                s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
 852            });
 853        });
 854        search_bar.update(cx, |search_bar, cx| {
 855            assert_eq!(search_bar.active_match_index, Some(0));
 856            search_bar.select_next_match(&SelectNextMatch, cx);
 857            assert_eq!(
 858                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
 859                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
 860            );
 861        });
 862        search_bar.read_with(cx, |search_bar, _| {
 863            assert_eq!(search_bar.active_match_index, Some(0));
 864        });
 865
 866        search_bar.update(cx, |search_bar, cx| {
 867            search_bar.select_next_match(&SelectNextMatch, cx);
 868            assert_eq!(
 869                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
 870                [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
 871            );
 872        });
 873        search_bar.read_with(cx, |search_bar, _| {
 874            assert_eq!(search_bar.active_match_index, Some(1));
 875        });
 876
 877        search_bar.update(cx, |search_bar, cx| {
 878            search_bar.select_next_match(&SelectNextMatch, cx);
 879            assert_eq!(
 880                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
 881                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
 882            );
 883        });
 884        search_bar.read_with(cx, |search_bar, _| {
 885            assert_eq!(search_bar.active_match_index, Some(2));
 886        });
 887
 888        search_bar.update(cx, |search_bar, cx| {
 889            search_bar.select_next_match(&SelectNextMatch, cx);
 890            assert_eq!(
 891                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
 892                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
 893            );
 894        });
 895        search_bar.read_with(cx, |search_bar, _| {
 896            assert_eq!(search_bar.active_match_index, Some(0));
 897        });
 898
 899        search_bar.update(cx, |search_bar, cx| {
 900            search_bar.select_prev_match(&SelectPrevMatch, cx);
 901            assert_eq!(
 902                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
 903                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
 904            );
 905        });
 906        search_bar.read_with(cx, |search_bar, _| {
 907            assert_eq!(search_bar.active_match_index, Some(2));
 908        });
 909
 910        search_bar.update(cx, |search_bar, cx| {
 911            search_bar.select_prev_match(&SelectPrevMatch, cx);
 912            assert_eq!(
 913                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
 914                [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
 915            );
 916        });
 917        search_bar.read_with(cx, |search_bar, _| {
 918            assert_eq!(search_bar.active_match_index, Some(1));
 919        });
 920
 921        search_bar.update(cx, |search_bar, cx| {
 922            search_bar.select_prev_match(&SelectPrevMatch, cx);
 923            assert_eq!(
 924                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
 925                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
 926            );
 927        });
 928        search_bar.read_with(cx, |search_bar, _| {
 929            assert_eq!(search_bar.active_match_index, Some(0));
 930        });
 931
 932        // Park the cursor in between matches and ensure that going to the previous match selects
 933        // the closest match to the left.
 934        editor.update(cx, |editor, cx| {
 935            editor.change_selections(None, cx, |s| {
 936                s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
 937            });
 938        });
 939        search_bar.update(cx, |search_bar, cx| {
 940            assert_eq!(search_bar.active_match_index, Some(1));
 941            search_bar.select_prev_match(&SelectPrevMatch, cx);
 942            assert_eq!(
 943                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
 944                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
 945            );
 946        });
 947        search_bar.read_with(cx, |search_bar, _| {
 948            assert_eq!(search_bar.active_match_index, Some(0));
 949        });
 950
 951        // Park the cursor in between matches and ensure that going to the next match selects the
 952        // closest match to the right.
 953        editor.update(cx, |editor, cx| {
 954            editor.change_selections(None, cx, |s| {
 955                s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
 956            });
 957        });
 958        search_bar.update(cx, |search_bar, cx| {
 959            assert_eq!(search_bar.active_match_index, Some(1));
 960            search_bar.select_next_match(&SelectNextMatch, cx);
 961            assert_eq!(
 962                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
 963                [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
 964            );
 965        });
 966        search_bar.read_with(cx, |search_bar, _| {
 967            assert_eq!(search_bar.active_match_index, Some(1));
 968        });
 969
 970        // Park the cursor after the last match and ensure that going to the previous match selects
 971        // the last match.
 972        editor.update(cx, |editor, cx| {
 973            editor.change_selections(None, cx, |s| {
 974                s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
 975            });
 976        });
 977        search_bar.update(cx, |search_bar, cx| {
 978            assert_eq!(search_bar.active_match_index, Some(2));
 979            search_bar.select_prev_match(&SelectPrevMatch, cx);
 980            assert_eq!(
 981                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
 982                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
 983            );
 984        });
 985        search_bar.read_with(cx, |search_bar, _| {
 986            assert_eq!(search_bar.active_match_index, Some(2));
 987        });
 988
 989        // Park the cursor after the last match and ensure that going to the next match selects the
 990        // first match.
 991        editor.update(cx, |editor, cx| {
 992            editor.change_selections(None, cx, |s| {
 993                s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
 994            });
 995        });
 996        search_bar.update(cx, |search_bar, cx| {
 997            assert_eq!(search_bar.active_match_index, Some(2));
 998            search_bar.select_next_match(&SelectNextMatch, cx);
 999            assert_eq!(
1000                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1001                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1002            );
1003        });
1004        search_bar.read_with(cx, |search_bar, _| {
1005            assert_eq!(search_bar.active_match_index, Some(0));
1006        });
1007
1008        // Park the cursor before the first match and ensure that going to the previous match
1009        // selects the last match.
1010        editor.update(cx, |editor, cx| {
1011            editor.change_selections(None, cx, |s| {
1012                s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
1013            });
1014        });
1015        search_bar.update(cx, |search_bar, cx| {
1016            assert_eq!(search_bar.active_match_index, Some(0));
1017            search_bar.select_prev_match(&SelectPrevMatch, cx);
1018            assert_eq!(
1019                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1020                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1021            );
1022        });
1023        search_bar.read_with(cx, |search_bar, _| {
1024            assert_eq!(search_bar.active_match_index, Some(2));
1025        });
1026    }
1027
1028    #[gpui::test]
1029    async fn test_search_select_all_matches(cx: &mut TestAppContext) {
1030        crate::project_search::tests::init_test(cx);
1031
1032        let buffer_text = r#"
1033        A regular expression (shortened as regex or regexp;[1] also referred to as
1034        rational expression[2][3]) is a sequence of characters that specifies a search
1035        pattern in text. Usually such patterns are used by string-searching algorithms
1036        for "find" or "find and replace" operations on strings, or for input validation.
1037        "#
1038        .unindent();
1039        let expected_query_matches_count = buffer_text
1040            .chars()
1041            .filter(|c| c.to_ascii_lowercase() == 'a')
1042            .count();
1043        assert!(
1044            expected_query_matches_count > 1,
1045            "Should pick a query with multiple results"
1046        );
1047        let buffer = cx.add_model(|cx| Buffer::new(0, buffer_text, cx));
1048        let (window_id, _root_view) = cx.add_window(|_| EmptyView);
1049
1050        let editor = cx.add_view(window_id, |cx| Editor::for_buffer(buffer.clone(), None, cx));
1051
1052        let search_bar = cx.add_view(window_id, |cx| {
1053            let mut search_bar = BufferSearchBar::new(cx);
1054            search_bar.set_active_pane_item(Some(&editor), cx);
1055            search_bar.show(false, true, cx);
1056            search_bar
1057        });
1058
1059        search_bar.update(cx, |search_bar, cx| {
1060            search_bar.set_query("a", cx);
1061        });
1062
1063        editor.next_notification(cx).await;
1064        let initial_selections = editor.update(cx, |editor, cx| {
1065            let initial_selections = editor.selections.display_ranges(cx);
1066            assert_eq!(
1067                initial_selections.len(), 1,
1068                "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
1069            );
1070            initial_selections
1071        });
1072        search_bar.update(cx, |search_bar, _| {
1073            assert_eq!(search_bar.active_match_index, Some(0));
1074        });
1075
1076        search_bar.update(cx, |search_bar, cx| {
1077            search_bar.select_all_matches(&SelectAllMatches, cx);
1078            let all_selections =
1079                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1080            assert_eq!(
1081                all_selections.len(),
1082                expected_query_matches_count,
1083                "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1084            );
1085        });
1086        search_bar.update(cx, |search_bar, _| {
1087            assert_eq!(
1088                search_bar.active_match_index,
1089                Some(0),
1090                "Match index should not change after selecting all matches"
1091            );
1092        });
1093
1094        search_bar.update(cx, |search_bar, cx| {
1095            search_bar.select_next_match(&SelectNextMatch, cx);
1096            let all_selections =
1097                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1098            assert_eq!(
1099                all_selections.len(),
1100                1,
1101                "On next match, should deselect items and select the next match"
1102            );
1103            assert_ne!(
1104                all_selections, initial_selections,
1105                "Next match should be different from the first selection"
1106            );
1107        });
1108        search_bar.update(cx, |search_bar, _| {
1109            assert_eq!(
1110                search_bar.active_match_index,
1111                Some(1),
1112                "Match index should be updated to the next one"
1113            );
1114        });
1115
1116        search_bar.update(cx, |search_bar, cx| {
1117            search_bar.select_all_matches(&SelectAllMatches, cx);
1118            let all_selections =
1119                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1120            assert_eq!(
1121                all_selections.len(),
1122                expected_query_matches_count,
1123                "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1124            );
1125        });
1126        search_bar.update(cx, |search_bar, _| {
1127            assert_eq!(
1128                search_bar.active_match_index,
1129                Some(1),
1130                "Match index should not change after selecting all matches"
1131            );
1132        });
1133
1134        search_bar.update(cx, |search_bar, cx| {
1135            search_bar.select_prev_match(&SelectPrevMatch, cx);
1136            let all_selections =
1137                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1138            assert_eq!(
1139                all_selections.len(),
1140                1,
1141                "On previous match, should deselect items and select the previous item"
1142            );
1143            assert_eq!(
1144                all_selections, initial_selections,
1145                "Previous match should be the same as the first selection"
1146            );
1147        });
1148        search_bar.update(cx, |search_bar, _| {
1149            assert_eq!(
1150                search_bar.active_match_index,
1151                Some(0),
1152                "Match index should be updated to the previous one"
1153            );
1154        });
1155    }
1156}