buffer_search.rs

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