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