buffer_search.rs

   1mod registrar;
   2
   3use crate::{
   4    search_bar::render_nav_button, FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll,
   5    ReplaceNext, SearchOptions, SelectAllMatches, SelectNextMatch, SelectPrevMatch,
   6    ToggleCaseSensitive, ToggleRegex, ToggleReplace, ToggleSelection, ToggleWholeWord,
   7};
   8use any_vec::AnyVec;
   9use collections::HashMap;
  10use editor::{
  11    actions::{Tab, TabPrev},
  12    DisplayPoint, Editor, EditorElement, EditorSettings, EditorStyle,
  13};
  14use futures::channel::oneshot;
  15use gpui::{
  16    actions, div, impl_actions, Action, AppContext, ClickEvent, EventEmitter, FocusHandle,
  17    FocusableView, Hsla, InteractiveElement as _, IntoElement, KeyContext, ParentElement as _,
  18    Render, ScrollHandle, Styled, Subscription, Task, TextStyle, View, ViewContext,
  19    VisualContext as _, WindowContext,
  20};
  21use project::{
  22    search::SearchQuery,
  23    search_history::{SearchHistory, SearchHistoryCursor},
  24};
  25use serde::Deserialize;
  26use settings::Settings;
  27use std::sync::Arc;
  28use theme::ThemeSettings;
  29
  30use ui::{
  31    h_flex, prelude::*, utils::SearchInputWidth, IconButton, IconButtonShape, IconName, Tooltip,
  32    BASE_REM_SIZE_IN_PX,
  33};
  34use util::ResultExt;
  35use workspace::{
  36    item::ItemHandle,
  37    searchable::{Direction, SearchEvent, SearchableItemHandle, WeakSearchableItemHandle},
  38    ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
  39};
  40
  41pub use registrar::DivRegistrar;
  42use registrar::{ForDeployed, ForDismissed, SearchActionsRegistrar, WithResults};
  43
  44const MAX_BUFFER_SEARCH_HISTORY_SIZE: usize = 50;
  45
  46#[derive(PartialEq, Clone, Deserialize)]
  47pub struct Deploy {
  48    #[serde(default = "util::serde::default_true")]
  49    pub focus: bool,
  50    #[serde(default)]
  51    pub replace_enabled: bool,
  52    #[serde(default)]
  53    pub selection_search_enabled: bool,
  54}
  55
  56impl_actions!(buffer_search, [Deploy]);
  57
  58actions!(buffer_search, [Dismiss, FocusEditor]);
  59
  60impl Deploy {
  61    pub fn find() -> Self {
  62        Self {
  63            focus: true,
  64            replace_enabled: false,
  65            selection_search_enabled: false,
  66        }
  67    }
  68}
  69
  70pub enum Event {
  71    UpdateLocation,
  72}
  73
  74pub fn init(cx: &mut AppContext) {
  75    cx.observe_new_views(|workspace: &mut Workspace, _| BufferSearchBar::register(workspace))
  76        .detach();
  77}
  78
  79pub struct BufferSearchBar {
  80    query_editor: View<Editor>,
  81    query_editor_focused: bool,
  82    replacement_editor: View<Editor>,
  83    replacement_editor_focused: bool,
  84    active_searchable_item: Option<Box<dyn SearchableItemHandle>>,
  85    active_match_index: Option<usize>,
  86    active_searchable_item_subscription: Option<Subscription>,
  87    active_search: Option<Arc<SearchQuery>>,
  88    searchable_items_with_matches: HashMap<Box<dyn WeakSearchableItemHandle>, AnyVec<dyn Send>>,
  89    pending_search: Option<Task<()>>,
  90    search_options: SearchOptions,
  91    default_options: SearchOptions,
  92    configured_options: SearchOptions,
  93    query_contains_error: bool,
  94    dismissed: bool,
  95    search_history: SearchHistory,
  96    search_history_cursor: SearchHistoryCursor,
  97    replace_enabled: bool,
  98    selection_search_enabled: bool,
  99    scroll_handle: ScrollHandle,
 100    editor_scroll_handle: ScrollHandle,
 101    editor_needed_width: Pixels,
 102}
 103
 104impl BufferSearchBar {
 105    fn render_text_input(
 106        &self,
 107        editor: &View<Editor>,
 108        color: Hsla,
 109        cx: &ViewContext<Self>,
 110    ) -> impl IntoElement {
 111        let settings = ThemeSettings::get_global(cx);
 112        let text_style = TextStyle {
 113            color: if editor.read(cx).read_only(cx) {
 114                cx.theme().colors().text_disabled
 115            } else {
 116                color
 117            },
 118            font_family: settings.buffer_font.family.clone(),
 119            font_features: settings.buffer_font.features.clone(),
 120            font_fallbacks: settings.buffer_font.fallbacks.clone(),
 121            font_size: rems(0.875).into(),
 122            font_weight: settings.buffer_font.weight,
 123            line_height: relative(1.3),
 124            ..Default::default()
 125        };
 126
 127        EditorElement::new(
 128            editor,
 129            EditorStyle {
 130                background: cx.theme().colors().editor_background,
 131                local_player: cx.theme().players().local(),
 132                text: text_style,
 133                ..Default::default()
 134            },
 135        )
 136    }
 137
 138    pub fn query_editor_focused(&self) -> bool {
 139        self.query_editor_focused
 140    }
 141}
 142
 143impl EventEmitter<Event> for BufferSearchBar {}
 144impl EventEmitter<workspace::ToolbarItemEvent> for BufferSearchBar {}
 145impl Render for BufferSearchBar {
 146    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
 147        if self.dismissed {
 148            return div().id("search_bar");
 149        }
 150
 151        let focus_handle = self.focus_handle(cx);
 152
 153        let narrow_mode =
 154            self.scroll_handle.bounds().size.width / cx.rem_size() < 340. / BASE_REM_SIZE_IN_PX;
 155        let hide_inline_icons = self.editor_needed_width
 156            > self.editor_scroll_handle.bounds().size.width - cx.rem_size() * 6.;
 157
 158        let supported_options = self.supported_options();
 159
 160        if self.query_editor.update(cx, |query_editor, cx| {
 161            query_editor.placeholder_text(cx).is_none()
 162        }) {
 163            self.query_editor.update(cx, |editor, cx| {
 164                editor.set_placeholder_text("Search…", cx);
 165            });
 166        }
 167
 168        self.replacement_editor.update(cx, |editor, cx| {
 169            editor.set_placeholder_text("Replace with…", cx);
 170        });
 171
 172        let mut text_color = Color::Default;
 173        let match_text = self
 174            .active_searchable_item
 175            .as_ref()
 176            .and_then(|searchable_item| {
 177                if self.query(cx).is_empty() {
 178                    return None;
 179                }
 180                let matches_count = self
 181                    .searchable_items_with_matches
 182                    .get(&searchable_item.downgrade())
 183                    .map(AnyVec::len)
 184                    .unwrap_or(0);
 185                if let Some(match_ix) = self.active_match_index {
 186                    Some(format!("{}/{}", match_ix + 1, matches_count))
 187                } else {
 188                    text_color = Color::Error; // No matches found
 189                    None
 190                }
 191            })
 192            .unwrap_or_else(|| "0/0".to_string());
 193        let should_show_replace_input = self.replace_enabled && supported_options.replacement;
 194        let in_replace = self.replacement_editor.focus_handle(cx).is_focused(cx);
 195
 196        let mut key_context = KeyContext::new_with_defaults();
 197        key_context.add("BufferSearchBar");
 198        if in_replace {
 199            key_context.add("in_replace");
 200        }
 201        let editor_border = if self.query_contains_error {
 202            Color::Error.color(cx)
 203        } else {
 204            cx.theme().colors().border
 205        };
 206
 207        let container_width = cx.viewport_size().width;
 208        let input_width = SearchInputWidth::calc_width(container_width);
 209
 210        let input_base_styles = || {
 211            h_flex()
 212                .w(input_width)
 213                .h_8()
 214                .px_2()
 215                .py_1()
 216                .border_1()
 217                .border_color(editor_border)
 218                .rounded_lg()
 219        };
 220
 221        let search_line = h_flex()
 222            .gap_2()
 223            .child(
 224                input_base_styles()
 225                    .id("editor-scroll")
 226                    .track_scroll(&self.editor_scroll_handle)
 227                    .child(self.render_text_input(&self.query_editor, text_color.color(cx), cx))
 228                    .when(!hide_inline_icons, |div| {
 229                        div.children(supported_options.case.then(|| {
 230                            self.render_search_option_button(
 231                                SearchOptions::CASE_SENSITIVE,
 232                                focus_handle.clone(),
 233                                cx.listener(|this, _, cx| {
 234                                    this.toggle_case_sensitive(&ToggleCaseSensitive, cx)
 235                                }),
 236                            )
 237                        }))
 238                        .children(supported_options.word.then(|| {
 239                            self.render_search_option_button(
 240                                SearchOptions::WHOLE_WORD,
 241                                focus_handle.clone(),
 242                                cx.listener(|this, _, cx| {
 243                                    this.toggle_whole_word(&ToggleWholeWord, cx)
 244                                }),
 245                            )
 246                        }))
 247                        .children(supported_options.regex.then(|| {
 248                            self.render_search_option_button(
 249                                SearchOptions::REGEX,
 250                                focus_handle.clone(),
 251                                cx.listener(|this, _, cx| this.toggle_regex(&ToggleRegex, cx)),
 252                            )
 253                        }))
 254                    }),
 255            )
 256            .child(
 257                h_flex()
 258                    .gap_1()
 259                    .min_w_64()
 260                    .when(supported_options.replacement, |this| {
 261                        this.child(
 262                            IconButton::new(
 263                                "buffer-search-bar-toggle-replace-button",
 264                                IconName::Replace,
 265                            )
 266                            .style(ButtonStyle::Subtle)
 267                            .shape(IconButtonShape::Square)
 268                            .when(self.replace_enabled, |button| {
 269                                button.style(ButtonStyle::Filled)
 270                            })
 271                            .on_click(cx.listener(|this, _: &ClickEvent, cx| {
 272                                this.toggle_replace(&ToggleReplace, cx);
 273                            }))
 274                            .selected(self.replace_enabled)
 275                            .tooltip({
 276                                let focus_handle = focus_handle.clone();
 277                                move |cx| {
 278                                    Tooltip::for_action_in(
 279                                        "Toggle Replace",
 280                                        &ToggleReplace,
 281                                        &focus_handle,
 282                                        cx,
 283                                    )
 284                                }
 285                            }),
 286                        )
 287                    })
 288                    .when(supported_options.selection, |this| {
 289                        this.child(
 290                            IconButton::new(
 291                                "buffer-search-bar-toggle-search-selection-button",
 292                                IconName::SearchSelection,
 293                            )
 294                            .style(ButtonStyle::Subtle)
 295                            .shape(IconButtonShape::Square)
 296                            .when(self.selection_search_enabled, |button| {
 297                                button.style(ButtonStyle::Filled)
 298                            })
 299                            .on_click(cx.listener(|this, _: &ClickEvent, cx| {
 300                                this.toggle_selection(&ToggleSelection, cx);
 301                            }))
 302                            .selected(self.selection_search_enabled)
 303                            .tooltip({
 304                                let focus_handle = focus_handle.clone();
 305                                move |cx| {
 306                                    Tooltip::for_action_in(
 307                                        "Toggle Search Selection",
 308                                        &ToggleSelection,
 309                                        &focus_handle,
 310                                        cx,
 311                                    )
 312                                }
 313                            }),
 314                        )
 315                    })
 316                    .child(
 317                        IconButton::new("select-all", ui::IconName::SelectAll)
 318                            .on_click(|_, cx| cx.dispatch_action(SelectAllMatches.boxed_clone()))
 319                            .shape(IconButtonShape::Square)
 320                            .tooltip({
 321                                let focus_handle = focus_handle.clone();
 322                                move |cx| {
 323                                    Tooltip::for_action_in(
 324                                        "Select All Matches",
 325                                        &SelectAllMatches,
 326                                        &focus_handle,
 327                                        cx,
 328                                    )
 329                                }
 330                            }),
 331                    )
 332                    .child(
 333                        h_flex()
 334                            .pl_2()
 335                            .ml_2()
 336                            .border_l_1()
 337                            .border_color(cx.theme().colors().border_variant)
 338                            .child(render_nav_button(
 339                                ui::IconName::ChevronLeft,
 340                                self.active_match_index.is_some(),
 341                                "Select Previous Match",
 342                                &SelectPrevMatch,
 343                                focus_handle.clone(),
 344                            ))
 345                            .child(render_nav_button(
 346                                ui::IconName::ChevronRight,
 347                                self.active_match_index.is_some(),
 348                                "Select Next Match",
 349                                &SelectNextMatch,
 350                                focus_handle.clone(),
 351                            )),
 352                    )
 353                    .when(!narrow_mode, |this| {
 354                        this.child(h_flex().ml_2().min_w(rems_from_px(40.)).child(
 355                            Label::new(match_text).size(LabelSize::Small).color(
 356                                if self.active_match_index.is_some() {
 357                                    Color::Default
 358                                } else {
 359                                    Color::Disabled
 360                                },
 361                            ),
 362                        ))
 363                    }),
 364            );
 365
 366        let replace_line = should_show_replace_input.then(|| {
 367            h_flex()
 368                .gap_2()
 369                .child(input_base_styles().child(self.render_text_input(
 370                    &self.replacement_editor,
 371                    cx.theme().colors().text,
 372                    cx,
 373                )))
 374                .child(
 375                    h_flex()
 376                        .min_w_64()
 377                        .gap_1()
 378                        .child(
 379                            IconButton::new("search-replace-next", ui::IconName::ReplaceNext)
 380                                .shape(IconButtonShape::Square)
 381                                .tooltip({
 382                                    let focus_handle = focus_handle.clone();
 383                                    move |cx| {
 384                                        Tooltip::for_action_in(
 385                                            "Replace Next Match",
 386                                            &ReplaceNext,
 387                                            &focus_handle,
 388                                            cx,
 389                                        )
 390                                    }
 391                                })
 392                                .on_click(
 393                                    cx.listener(|this, _, cx| this.replace_next(&ReplaceNext, cx)),
 394                                ),
 395                        )
 396                        .child(
 397                            IconButton::new("search-replace-all", ui::IconName::ReplaceAll)
 398                                .shape(IconButtonShape::Square)
 399                                .tooltip({
 400                                    let focus_handle = focus_handle.clone();
 401                                    move |cx| {
 402                                        Tooltip::for_action_in(
 403                                            "Replace All Matches",
 404                                            &ReplaceAll,
 405                                            &focus_handle,
 406                                            cx,
 407                                        )
 408                                    }
 409                                })
 410                                .on_click(
 411                                    cx.listener(|this, _, cx| this.replace_all(&ReplaceAll, cx)),
 412                                ),
 413                        ),
 414                )
 415        });
 416
 417        v_flex()
 418            .id("buffer_search")
 419            .gap_2()
 420            .track_scroll(&self.scroll_handle)
 421            .key_context(key_context)
 422            .capture_action(cx.listener(Self::tab))
 423            .capture_action(cx.listener(Self::tab_prev))
 424            .on_action(cx.listener(Self::previous_history_query))
 425            .on_action(cx.listener(Self::next_history_query))
 426            .on_action(cx.listener(Self::dismiss))
 427            .on_action(cx.listener(Self::select_next_match))
 428            .on_action(cx.listener(Self::select_prev_match))
 429            .when(self.supported_options().replacement, |this| {
 430                this.on_action(cx.listener(Self::toggle_replace))
 431                    .when(in_replace, |this| {
 432                        this.on_action(cx.listener(Self::replace_next))
 433                            .on_action(cx.listener(Self::replace_all))
 434                    })
 435            })
 436            .when(self.supported_options().case, |this| {
 437                this.on_action(cx.listener(Self::toggle_case_sensitive))
 438            })
 439            .when(self.supported_options().word, |this| {
 440                this.on_action(cx.listener(Self::toggle_whole_word))
 441            })
 442            .when(self.supported_options().regex, |this| {
 443                this.on_action(cx.listener(Self::toggle_regex))
 444            })
 445            .when(self.supported_options().selection, |this| {
 446                this.on_action(cx.listener(Self::toggle_selection))
 447            })
 448            .child(
 449                h_flex()
 450                    .relative()
 451                    .child(search_line.w_full())
 452                    .when(!narrow_mode, |div| {
 453                        div.child(
 454                            h_flex().absolute().right_0().child(
 455                                IconButton::new(SharedString::from("Close"), IconName::Close)
 456                                    .shape(IconButtonShape::Square)
 457                                    .tooltip(move |cx| {
 458                                        Tooltip::for_action("Close Search Bar", &Dismiss, cx)
 459                                    })
 460                                    .on_click(cx.listener(|this, _: &ClickEvent, cx| {
 461                                        this.dismiss(&Dismiss, cx)
 462                                    })),
 463                            ),
 464                        )
 465                    }),
 466            )
 467            .children(replace_line)
 468    }
 469}
 470
 471impl FocusableView for BufferSearchBar {
 472    fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
 473        self.query_editor.focus_handle(cx)
 474    }
 475}
 476
 477impl ToolbarItemView for BufferSearchBar {
 478    fn set_active_pane_item(
 479        &mut self,
 480        item: Option<&dyn ItemHandle>,
 481        cx: &mut ViewContext<Self>,
 482    ) -> ToolbarItemLocation {
 483        cx.notify();
 484        self.active_searchable_item_subscription.take();
 485        self.active_searchable_item.take();
 486
 487        self.pending_search.take();
 488
 489        if let Some(searchable_item_handle) =
 490            item.and_then(|item| item.to_searchable_item_handle(cx))
 491        {
 492            let this = cx.view().downgrade();
 493
 494            self.active_searchable_item_subscription =
 495                Some(searchable_item_handle.subscribe_to_search_events(
 496                    cx,
 497                    Box::new(move |search_event, cx| {
 498                        if let Some(this) = this.upgrade() {
 499                            this.update(cx, |this, cx| {
 500                                this.on_active_searchable_item_event(search_event, cx)
 501                            });
 502                        }
 503                    }),
 504                ));
 505
 506            self.active_searchable_item = Some(searchable_item_handle);
 507            drop(self.update_matches(true, cx));
 508            if !self.dismissed {
 509                return ToolbarItemLocation::Secondary;
 510            }
 511        }
 512        ToolbarItemLocation::Hidden
 513    }
 514}
 515
 516impl BufferSearchBar {
 517    pub fn register(registrar: &mut impl SearchActionsRegistrar) {
 518        registrar.register_handler(ForDeployed(|this, _: &FocusSearch, cx| {
 519            this.query_editor.focus_handle(cx).focus(cx);
 520            this.select_query(cx);
 521        }));
 522        registrar.register_handler(ForDeployed(|this, action: &ToggleCaseSensitive, cx| {
 523            if this.supported_options().case {
 524                this.toggle_case_sensitive(action, cx);
 525            }
 526        }));
 527        registrar.register_handler(ForDeployed(|this, action: &ToggleWholeWord, cx| {
 528            if this.supported_options().word {
 529                this.toggle_whole_word(action, cx);
 530            }
 531        }));
 532        registrar.register_handler(ForDeployed(|this, action: &ToggleRegex, cx| {
 533            if this.supported_options().regex {
 534                this.toggle_regex(action, cx);
 535            }
 536        }));
 537        registrar.register_handler(ForDeployed(|this, action: &ToggleSelection, cx| {
 538            if this.supported_options().selection {
 539                this.toggle_selection(action, cx);
 540            }
 541        }));
 542        registrar.register_handler(ForDeployed(|this, action: &ToggleReplace, cx| {
 543            if this.supported_options().replacement {
 544                this.toggle_replace(action, cx);
 545            }
 546        }));
 547        registrar.register_handler(WithResults(|this, action: &SelectNextMatch, cx| {
 548            this.select_next_match(action, cx);
 549        }));
 550        registrar.register_handler(WithResults(|this, action: &SelectPrevMatch, cx| {
 551            this.select_prev_match(action, cx);
 552        }));
 553        registrar.register_handler(WithResults(|this, action: &SelectAllMatches, cx| {
 554            this.select_all_matches(action, cx);
 555        }));
 556        registrar.register_handler(ForDeployed(|this, _: &editor::actions::Cancel, cx| {
 557            this.dismiss(&Dismiss, cx);
 558        }));
 559        registrar.register_handler(ForDeployed(|this, _: &Dismiss, cx| {
 560            this.dismiss(&Dismiss, cx);
 561        }));
 562
 563        // register deploy buffer search for both search bar states, since we want to focus into the search bar
 564        // when the deploy action is triggered in the buffer.
 565        registrar.register_handler(ForDeployed(|this, deploy, cx| {
 566            this.deploy(deploy, cx);
 567        }));
 568        registrar.register_handler(ForDismissed(|this, deploy, cx| {
 569            this.deploy(deploy, cx);
 570        }))
 571    }
 572
 573    pub fn new(cx: &mut ViewContext<Self>) -> Self {
 574        let query_editor = cx.new_view(Editor::single_line);
 575        cx.subscribe(&query_editor, Self::on_query_editor_event)
 576            .detach();
 577        let replacement_editor = cx.new_view(Editor::single_line);
 578        cx.subscribe(&replacement_editor, Self::on_replacement_editor_event)
 579            .detach();
 580
 581        let search_options = SearchOptions::from_settings(&EditorSettings::get_global(cx).search);
 582
 583        Self {
 584            query_editor,
 585            query_editor_focused: false,
 586            replacement_editor,
 587            replacement_editor_focused: false,
 588            active_searchable_item: None,
 589            active_searchable_item_subscription: None,
 590            active_match_index: None,
 591            searchable_items_with_matches: Default::default(),
 592            default_options: search_options,
 593            configured_options: search_options,
 594            search_options,
 595            pending_search: None,
 596            query_contains_error: false,
 597            dismissed: true,
 598            search_history: SearchHistory::new(
 599                Some(MAX_BUFFER_SEARCH_HISTORY_SIZE),
 600                project::search_history::QueryInsertionBehavior::ReplacePreviousIfContains,
 601            ),
 602            search_history_cursor: Default::default(),
 603            active_search: None,
 604            replace_enabled: false,
 605            selection_search_enabled: false,
 606            scroll_handle: ScrollHandle::new(),
 607            editor_scroll_handle: ScrollHandle::new(),
 608            editor_needed_width: px(0.),
 609        }
 610    }
 611
 612    pub fn is_dismissed(&self) -> bool {
 613        self.dismissed
 614    }
 615
 616    pub fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext<Self>) {
 617        self.dismissed = true;
 618        for searchable_item in self.searchable_items_with_matches.keys() {
 619            if let Some(searchable_item) =
 620                WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
 621            {
 622                searchable_item.clear_matches(cx);
 623            }
 624        }
 625        if let Some(active_editor) = self.active_searchable_item.as_mut() {
 626            self.selection_search_enabled = false;
 627            self.replace_enabled = false;
 628            active_editor.search_bar_visibility_changed(false, cx);
 629            active_editor.toggle_filtered_search_ranges(false, cx);
 630            let handle = active_editor.focus_handle(cx);
 631            self.focus(&handle, cx);
 632        }
 633        cx.emit(Event::UpdateLocation);
 634        cx.emit(ToolbarItemEvent::ChangeLocation(
 635            ToolbarItemLocation::Hidden,
 636        ));
 637        cx.notify();
 638    }
 639
 640    pub fn deploy(&mut self, deploy: &Deploy, cx: &mut ViewContext<Self>) -> bool {
 641        if self.show(cx) {
 642            if let Some(active_item) = self.active_searchable_item.as_mut() {
 643                active_item.toggle_filtered_search_ranges(deploy.selection_search_enabled, cx);
 644            }
 645            self.search_suggested(cx);
 646            self.smartcase(cx);
 647            self.replace_enabled = deploy.replace_enabled;
 648            self.selection_search_enabled = deploy.selection_search_enabled;
 649            if deploy.focus {
 650                let mut handle = self.query_editor.focus_handle(cx).clone();
 651                let mut select_query = true;
 652                if deploy.replace_enabled && handle.is_focused(cx) {
 653                    handle = self.replacement_editor.focus_handle(cx).clone();
 654                    select_query = false;
 655                };
 656
 657                if select_query {
 658                    self.select_query(cx);
 659                }
 660
 661                cx.focus(&handle);
 662            }
 663            return true;
 664        }
 665
 666        cx.propagate();
 667        false
 668    }
 669
 670    pub fn toggle(&mut self, action: &Deploy, cx: &mut ViewContext<Self>) {
 671        if self.is_dismissed() {
 672            self.deploy(action, cx);
 673        } else {
 674            self.dismiss(&Dismiss, cx);
 675        }
 676    }
 677
 678    pub fn show(&mut self, cx: &mut ViewContext<Self>) -> bool {
 679        let Some(handle) = self.active_searchable_item.as_ref() else {
 680            return false;
 681        };
 682
 683        self.configured_options =
 684            SearchOptions::from_settings(&EditorSettings::get_global(cx).search);
 685        if self.dismissed && self.configured_options != self.default_options {
 686            self.search_options = self.configured_options;
 687            self.default_options = self.configured_options;
 688        }
 689
 690        self.dismissed = false;
 691        handle.search_bar_visibility_changed(true, cx);
 692        cx.notify();
 693        cx.emit(Event::UpdateLocation);
 694        cx.emit(ToolbarItemEvent::ChangeLocation(
 695            ToolbarItemLocation::Secondary,
 696        ));
 697        true
 698    }
 699
 700    fn supported_options(&self) -> workspace::searchable::SearchOptions {
 701        self.active_searchable_item
 702            .as_deref()
 703            .map(SearchableItemHandle::supported_options)
 704            .unwrap_or_default()
 705    }
 706
 707    pub fn search_suggested(&mut self, cx: &mut ViewContext<Self>) {
 708        let search = self
 709            .query_suggestion(cx)
 710            .map(|suggestion| self.search(&suggestion, Some(self.default_options), cx));
 711
 712        if let Some(search) = search {
 713            cx.spawn(|this, mut cx| async move {
 714                search.await?;
 715                this.update(&mut cx, |this, cx| this.activate_current_match(cx))
 716            })
 717            .detach_and_log_err(cx);
 718        }
 719    }
 720
 721    pub fn activate_current_match(&mut self, cx: &mut ViewContext<Self>) {
 722        if let Some(match_ix) = self.active_match_index {
 723            if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
 724                if let Some(matches) = self
 725                    .searchable_items_with_matches
 726                    .get(&active_searchable_item.downgrade())
 727                {
 728                    active_searchable_item.activate_match(match_ix, matches, cx)
 729                }
 730            }
 731        }
 732    }
 733
 734    pub fn select_query(&mut self, cx: &mut ViewContext<Self>) {
 735        self.query_editor.update(cx, |query_editor, cx| {
 736            query_editor.select_all(&Default::default(), cx);
 737        });
 738    }
 739
 740    pub fn query(&self, cx: &WindowContext) -> String {
 741        self.query_editor.read(cx).text(cx)
 742    }
 743    pub fn replacement(&self, cx: &WindowContext) -> String {
 744        self.replacement_editor.read(cx).text(cx)
 745    }
 746    pub fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> Option<String> {
 747        self.active_searchable_item
 748            .as_ref()
 749            .map(|searchable_item| searchable_item.query_suggestion(cx))
 750            .filter(|suggestion| !suggestion.is_empty())
 751    }
 752
 753    pub fn set_replacement(&mut self, replacement: Option<&str>, cx: &mut ViewContext<Self>) {
 754        if replacement.is_none() {
 755            self.replace_enabled = false;
 756            return;
 757        }
 758        self.replace_enabled = true;
 759        self.replacement_editor
 760            .update(cx, |replacement_editor, cx| {
 761                replacement_editor
 762                    .buffer()
 763                    .update(cx, |replacement_buffer, cx| {
 764                        let len = replacement_buffer.len(cx);
 765                        replacement_buffer.edit([(0..len, replacement.unwrap())], None, cx);
 766                    });
 767            });
 768    }
 769
 770    pub fn search(
 771        &mut self,
 772        query: &str,
 773        options: Option<SearchOptions>,
 774        cx: &mut ViewContext<Self>,
 775    ) -> oneshot::Receiver<()> {
 776        let options = options.unwrap_or(self.default_options);
 777        let updated = query != self.query(cx) || self.search_options != options;
 778        if updated {
 779            self.query_editor.update(cx, |query_editor, cx| {
 780                query_editor.buffer().update(cx, |query_buffer, cx| {
 781                    let len = query_buffer.len(cx);
 782                    query_buffer.edit([(0..len, query)], None, cx);
 783                });
 784            });
 785            self.search_options = options;
 786            self.clear_matches(cx);
 787            cx.notify();
 788        }
 789        self.update_matches(!updated, cx)
 790    }
 791
 792    fn render_search_option_button(
 793        &self,
 794        option: SearchOptions,
 795        focus_handle: FocusHandle,
 796        action: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
 797    ) -> impl IntoElement {
 798        let is_active = self.search_options.contains(option);
 799        option.as_button(is_active, focus_handle, action)
 800    }
 801
 802    pub fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext<Self>) {
 803        if let Some(active_editor) = self.active_searchable_item.as_ref() {
 804            let handle = active_editor.focus_handle(cx);
 805            cx.focus(&handle);
 806        }
 807    }
 808
 809    pub fn toggle_search_option(
 810        &mut self,
 811        search_option: SearchOptions,
 812        cx: &mut ViewContext<Self>,
 813    ) {
 814        self.search_options.toggle(search_option);
 815        self.default_options = self.search_options;
 816        drop(self.update_matches(false, cx));
 817        cx.notify();
 818    }
 819
 820    pub fn has_search_option(&mut self, search_option: SearchOptions) -> bool {
 821        self.search_options.contains(search_option)
 822    }
 823
 824    pub fn enable_search_option(
 825        &mut self,
 826        search_option: SearchOptions,
 827        cx: &mut ViewContext<Self>,
 828    ) {
 829        if !self.search_options.contains(search_option) {
 830            self.toggle_search_option(search_option, cx)
 831        }
 832    }
 833
 834    pub fn set_search_options(
 835        &mut self,
 836        search_options: SearchOptions,
 837        cx: &mut ViewContext<Self>,
 838    ) {
 839        self.search_options = search_options;
 840        cx.notify();
 841    }
 842
 843    fn select_next_match(&mut self, _: &SelectNextMatch, cx: &mut ViewContext<Self>) {
 844        self.select_match(Direction::Next, 1, cx);
 845    }
 846
 847    fn select_prev_match(&mut self, _: &SelectPrevMatch, cx: &mut ViewContext<Self>) {
 848        self.select_match(Direction::Prev, 1, cx);
 849    }
 850
 851    fn select_all_matches(&mut self, _: &SelectAllMatches, cx: &mut ViewContext<Self>) {
 852        if !self.dismissed && self.active_match_index.is_some() {
 853            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
 854                if let Some(matches) = self
 855                    .searchable_items_with_matches
 856                    .get(&searchable_item.downgrade())
 857                {
 858                    searchable_item.select_matches(matches, cx);
 859                    self.focus_editor(&FocusEditor, cx);
 860                }
 861            }
 862        }
 863    }
 864
 865    pub fn select_match(&mut self, direction: Direction, count: usize, cx: &mut ViewContext<Self>) {
 866        if let Some(index) = self.active_match_index {
 867            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
 868                if let Some(matches) = self
 869                    .searchable_items_with_matches
 870                    .get(&searchable_item.downgrade())
 871                    .filter(|matches| !matches.is_empty())
 872                {
 873                    // If 'wrapscan' is disabled, searches do not wrap around the end of the file.
 874                    if !EditorSettings::get_global(cx).search_wrap
 875                        && ((direction == Direction::Next && index + count >= matches.len())
 876                            || (direction == Direction::Prev && index < count))
 877                    {
 878                        crate::show_no_more_matches(cx);
 879                        return;
 880                    }
 881                    let new_match_index = searchable_item
 882                        .match_index_for_direction(matches, index, direction, count, cx);
 883
 884                    searchable_item.update_matches(matches, cx);
 885                    searchable_item.activate_match(new_match_index, matches, cx);
 886                }
 887            }
 888        }
 889    }
 890
 891    pub fn select_last_match(&mut self, cx: &mut ViewContext<Self>) {
 892        if let Some(searchable_item) = self.active_searchable_item.as_ref() {
 893            if let Some(matches) = self
 894                .searchable_items_with_matches
 895                .get(&searchable_item.downgrade())
 896            {
 897                if matches.is_empty() {
 898                    return;
 899                }
 900                let new_match_index = matches.len() - 1;
 901                searchable_item.update_matches(matches, cx);
 902                searchable_item.activate_match(new_match_index, matches, cx);
 903            }
 904        }
 905    }
 906
 907    fn on_query_editor_event(
 908        &mut self,
 909        editor: View<Editor>,
 910        event: &editor::EditorEvent,
 911        cx: &mut ViewContext<Self>,
 912    ) {
 913        match event {
 914            editor::EditorEvent::Focused => self.query_editor_focused = true,
 915            editor::EditorEvent::Blurred => self.query_editor_focused = false,
 916            editor::EditorEvent::Edited { .. } => {
 917                self.smartcase(cx);
 918                self.clear_matches(cx);
 919                let search = self.update_matches(false, cx);
 920
 921                let width = editor.update(cx, |editor, cx| {
 922                    let text_layout_details = editor.text_layout_details(cx);
 923                    let snapshot = editor.snapshot(cx).display_snapshot;
 924
 925                    snapshot.x_for_display_point(snapshot.max_point(), &text_layout_details)
 926                        - snapshot.x_for_display_point(DisplayPoint::zero(), &text_layout_details)
 927                });
 928                self.editor_needed_width = width;
 929                cx.notify();
 930
 931                cx.spawn(|this, mut cx| async move {
 932                    search.await?;
 933                    this.update(&mut cx, |this, cx| this.activate_current_match(cx))
 934                })
 935                .detach_and_log_err(cx);
 936            }
 937            _ => {}
 938        }
 939    }
 940
 941    fn on_replacement_editor_event(
 942        &mut self,
 943        _: View<Editor>,
 944        event: &editor::EditorEvent,
 945        _: &mut ViewContext<Self>,
 946    ) {
 947        match event {
 948            editor::EditorEvent::Focused => self.replacement_editor_focused = true,
 949            editor::EditorEvent::Blurred => self.replacement_editor_focused = false,
 950            _ => {}
 951        }
 952    }
 953
 954    fn on_active_searchable_item_event(&mut self, event: &SearchEvent, cx: &mut ViewContext<Self>) {
 955        match event {
 956            SearchEvent::MatchesInvalidated => {
 957                drop(self.update_matches(false, cx));
 958            }
 959            SearchEvent::ActiveMatchChanged => self.update_match_index(cx),
 960        }
 961    }
 962
 963    fn toggle_case_sensitive(&mut self, _: &ToggleCaseSensitive, cx: &mut ViewContext<Self>) {
 964        self.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx)
 965    }
 966
 967    fn toggle_whole_word(&mut self, _: &ToggleWholeWord, cx: &mut ViewContext<Self>) {
 968        self.toggle_search_option(SearchOptions::WHOLE_WORD, cx)
 969    }
 970
 971    fn toggle_selection(&mut self, _: &ToggleSelection, cx: &mut ViewContext<Self>) {
 972        if let Some(active_item) = self.active_searchable_item.as_mut() {
 973            self.selection_search_enabled = !self.selection_search_enabled;
 974            active_item.toggle_filtered_search_ranges(self.selection_search_enabled, cx);
 975            drop(self.update_matches(false, cx));
 976            cx.notify();
 977        }
 978    }
 979
 980    fn toggle_regex(&mut self, _: &ToggleRegex, cx: &mut ViewContext<Self>) {
 981        self.toggle_search_option(SearchOptions::REGEX, cx)
 982    }
 983
 984    fn clear_active_searchable_item_matches(&mut self, cx: &mut WindowContext) {
 985        if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
 986            self.active_match_index = None;
 987            self.searchable_items_with_matches
 988                .remove(&active_searchable_item.downgrade());
 989            active_searchable_item.clear_matches(cx);
 990        }
 991    }
 992
 993    pub fn has_active_match(&self) -> bool {
 994        self.active_match_index.is_some()
 995    }
 996
 997    fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
 998        let mut active_item_matches = None;
 999        for (searchable_item, matches) in self.searchable_items_with_matches.drain() {
1000            if let Some(searchable_item) =
1001                WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
1002            {
1003                if Some(&searchable_item) == self.active_searchable_item.as_ref() {
1004                    active_item_matches = Some((searchable_item.downgrade(), matches));
1005                } else {
1006                    searchable_item.clear_matches(cx);
1007                }
1008            }
1009        }
1010
1011        self.searchable_items_with_matches
1012            .extend(active_item_matches);
1013    }
1014
1015    fn update_matches(
1016        &mut self,
1017        reuse_existing_query: bool,
1018        cx: &mut ViewContext<Self>,
1019    ) -> oneshot::Receiver<()> {
1020        let (done_tx, done_rx) = oneshot::channel();
1021        let query = self.query(cx);
1022        self.pending_search.take();
1023
1024        if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
1025            self.query_contains_error = false;
1026            if query.is_empty() {
1027                self.clear_active_searchable_item_matches(cx);
1028                let _ = done_tx.send(());
1029                cx.notify();
1030            } else {
1031                let query: Arc<_> = if let Some(search) =
1032                    self.active_search.take().filter(|_| reuse_existing_query)
1033                {
1034                    search
1035                } else {
1036                    if self.search_options.contains(SearchOptions::REGEX) {
1037                        match SearchQuery::regex(
1038                            query,
1039                            self.search_options.contains(SearchOptions::WHOLE_WORD),
1040                            self.search_options.contains(SearchOptions::CASE_SENSITIVE),
1041                            false,
1042                            Default::default(),
1043                            Default::default(),
1044                            None,
1045                        ) {
1046                            Ok(query) => query.with_replacement(self.replacement(cx)),
1047                            Err(_) => {
1048                                self.query_contains_error = true;
1049                                self.clear_active_searchable_item_matches(cx);
1050                                cx.notify();
1051                                return done_rx;
1052                            }
1053                        }
1054                    } else {
1055                        match SearchQuery::text(
1056                            query,
1057                            self.search_options.contains(SearchOptions::WHOLE_WORD),
1058                            self.search_options.contains(SearchOptions::CASE_SENSITIVE),
1059                            false,
1060                            Default::default(),
1061                            Default::default(),
1062                            None,
1063                        ) {
1064                            Ok(query) => query.with_replacement(self.replacement(cx)),
1065                            Err(_) => {
1066                                self.query_contains_error = true;
1067                                self.clear_active_searchable_item_matches(cx);
1068                                cx.notify();
1069                                return done_rx;
1070                            }
1071                        }
1072                    }
1073                    .into()
1074                };
1075
1076                self.active_search = Some(query.clone());
1077                let query_text = query.as_str().to_string();
1078
1079                let matches = active_searchable_item.find_matches(query, cx);
1080
1081                let active_searchable_item = active_searchable_item.downgrade();
1082                self.pending_search = Some(cx.spawn(|this, mut cx| async move {
1083                    let matches = matches.await;
1084
1085                    this.update(&mut cx, |this, cx| {
1086                        if let Some(active_searchable_item) =
1087                            WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx)
1088                        {
1089                            this.searchable_items_with_matches
1090                                .insert(active_searchable_item.downgrade(), matches);
1091
1092                            this.update_match_index(cx);
1093                            this.search_history
1094                                .add(&mut this.search_history_cursor, query_text);
1095                            if !this.dismissed {
1096                                let matches = this
1097                                    .searchable_items_with_matches
1098                                    .get(&active_searchable_item.downgrade())
1099                                    .unwrap();
1100                                if matches.is_empty() {
1101                                    active_searchable_item.clear_matches(cx);
1102                                } else {
1103                                    active_searchable_item.update_matches(matches, cx);
1104                                }
1105                                let _ = done_tx.send(());
1106                            }
1107                            cx.notify();
1108                        }
1109                    })
1110                    .log_err();
1111                }));
1112            }
1113        }
1114        done_rx
1115    }
1116
1117    pub fn update_match_index(&mut self, cx: &mut ViewContext<Self>) {
1118        let new_index = self
1119            .active_searchable_item
1120            .as_ref()
1121            .and_then(|searchable_item| {
1122                let matches = self
1123                    .searchable_items_with_matches
1124                    .get(&searchable_item.downgrade())?;
1125                searchable_item.active_match_index(matches, cx)
1126            });
1127        if new_index != self.active_match_index {
1128            self.active_match_index = new_index;
1129            cx.notify();
1130        }
1131    }
1132
1133    fn tab(&mut self, _: &Tab, cx: &mut ViewContext<Self>) {
1134        // Search -> Replace -> Editor
1135        let focus_handle = if self.replace_enabled && self.query_editor_focused {
1136            self.replacement_editor.focus_handle(cx)
1137        } else if let Some(item) = self.active_searchable_item.as_ref() {
1138            item.focus_handle(cx)
1139        } else {
1140            return;
1141        };
1142        self.focus(&focus_handle, cx);
1143        cx.stop_propagation();
1144    }
1145
1146    fn tab_prev(&mut self, _: &TabPrev, cx: &mut ViewContext<Self>) {
1147        // Search -> Replace -> Search
1148        let focus_handle = if self.replace_enabled && self.query_editor_focused {
1149            self.replacement_editor.focus_handle(cx)
1150        } else if self.replacement_editor_focused {
1151            self.query_editor.focus_handle(cx)
1152        } else {
1153            return;
1154        };
1155        self.focus(&focus_handle, cx);
1156        cx.stop_propagation();
1157    }
1158
1159    fn next_history_query(&mut self, _: &NextHistoryQuery, cx: &mut ViewContext<Self>) {
1160        if let Some(new_query) = self
1161            .search_history
1162            .next(&mut self.search_history_cursor)
1163            .map(str::to_string)
1164        {
1165            drop(self.search(&new_query, Some(self.search_options), cx));
1166        } else {
1167            self.search_history_cursor.reset();
1168            drop(self.search("", Some(self.search_options), cx));
1169        }
1170    }
1171
1172    fn previous_history_query(&mut self, _: &PreviousHistoryQuery, cx: &mut ViewContext<Self>) {
1173        if self.query(cx).is_empty() {
1174            if let Some(new_query) = self
1175                .search_history
1176                .current(&mut self.search_history_cursor)
1177                .map(str::to_string)
1178            {
1179                drop(self.search(&new_query, Some(self.search_options), cx));
1180                return;
1181            }
1182        }
1183
1184        if let Some(new_query) = self
1185            .search_history
1186            .previous(&mut self.search_history_cursor)
1187            .map(str::to_string)
1188        {
1189            drop(self.search(&new_query, Some(self.search_options), cx));
1190        }
1191    }
1192
1193    fn focus(&self, handle: &gpui::FocusHandle, cx: &mut ViewContext<Self>) {
1194        cx.on_next_frame(|_, cx| {
1195            cx.invalidate_character_coordinates();
1196        });
1197        cx.focus(handle);
1198    }
1199
1200    fn toggle_replace(&mut self, _: &ToggleReplace, cx: &mut ViewContext<Self>) {
1201        if self.active_searchable_item.is_some() {
1202            self.replace_enabled = !self.replace_enabled;
1203            let handle = if self.replace_enabled {
1204                self.replacement_editor.focus_handle(cx)
1205            } else {
1206                self.query_editor.focus_handle(cx)
1207            };
1208            self.focus(&handle, cx);
1209            cx.notify();
1210        }
1211    }
1212
1213    fn replace_next(&mut self, _: &ReplaceNext, cx: &mut ViewContext<Self>) {
1214        let mut should_propagate = true;
1215        if !self.dismissed && self.active_search.is_some() {
1216            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1217                if let Some(query) = self.active_search.as_ref() {
1218                    if let Some(matches) = self
1219                        .searchable_items_with_matches
1220                        .get(&searchable_item.downgrade())
1221                    {
1222                        if let Some(active_index) = self.active_match_index {
1223                            let query = query
1224                                .as_ref()
1225                                .clone()
1226                                .with_replacement(self.replacement(cx));
1227                            searchable_item.replace(matches.at(active_index), &query, cx);
1228                            self.select_next_match(&SelectNextMatch, cx);
1229                        }
1230                        should_propagate = false;
1231                        self.focus_editor(&FocusEditor, cx);
1232                    }
1233                }
1234            }
1235        }
1236        if !should_propagate {
1237            cx.stop_propagation();
1238        }
1239    }
1240
1241    pub fn replace_all(&mut self, _: &ReplaceAll, cx: &mut ViewContext<Self>) {
1242        if !self.dismissed && self.active_search.is_some() {
1243            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1244                if let Some(query) = self.active_search.as_ref() {
1245                    if let Some(matches) = self
1246                        .searchable_items_with_matches
1247                        .get(&searchable_item.downgrade())
1248                    {
1249                        let query = query
1250                            .as_ref()
1251                            .clone()
1252                            .with_replacement(self.replacement(cx));
1253                        searchable_item.replace_all(&mut matches.iter(), &query, cx);
1254                    }
1255                }
1256            }
1257        }
1258    }
1259
1260    pub fn match_exists(&mut self, cx: &mut ViewContext<Self>) -> bool {
1261        self.update_match_index(cx);
1262        self.active_match_index.is_some()
1263    }
1264
1265    pub fn should_use_smartcase_search(&mut self, cx: &mut ViewContext<Self>) -> bool {
1266        EditorSettings::get_global(cx).use_smartcase_search
1267    }
1268
1269    pub fn is_contains_uppercase(&mut self, str: &String) -> bool {
1270        str.chars().any(|c| c.is_uppercase())
1271    }
1272
1273    fn smartcase(&mut self, cx: &mut ViewContext<Self>) {
1274        if self.should_use_smartcase_search(cx) {
1275            let query = self.query(cx);
1276            if !query.is_empty() {
1277                let is_case = self.is_contains_uppercase(&query);
1278                if self.has_search_option(SearchOptions::CASE_SENSITIVE) != is_case {
1279                    self.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx);
1280                }
1281            }
1282        }
1283    }
1284}
1285
1286#[cfg(test)]
1287mod tests {
1288    use std::ops::Range;
1289
1290    use super::*;
1291    use editor::{display_map::DisplayRow, DisplayPoint, Editor, MultiBuffer, SearchSettings};
1292    use gpui::{Context, Hsla, TestAppContext, UpdateGlobal, VisualTestContext};
1293    use language::{Buffer, Point};
1294    use project::Project;
1295    use settings::SettingsStore;
1296    use smol::stream::StreamExt as _;
1297    use unindent::Unindent as _;
1298
1299    fn init_globals(cx: &mut TestAppContext) {
1300        cx.update(|cx| {
1301            let store = settings::SettingsStore::test(cx);
1302            cx.set_global(store);
1303            editor::init(cx);
1304
1305            language::init(cx);
1306            Project::init_settings(cx);
1307            theme::init(theme::LoadThemes::JustBase, cx);
1308            crate::init(cx);
1309        });
1310    }
1311
1312    fn init_test(
1313        cx: &mut TestAppContext,
1314    ) -> (View<Editor>, View<BufferSearchBar>, &mut VisualTestContext) {
1315        init_globals(cx);
1316        let buffer = cx.new_model(|cx| {
1317            Buffer::local(
1318                r#"
1319                A regular expression (shortened as regex or regexp;[1] also referred to as
1320                rational expression[2][3]) is a sequence of characters that specifies a search
1321                pattern in text. Usually such patterns are used by string-searching algorithms
1322                for "find" or "find and replace" operations on strings, or for input validation.
1323                "#
1324                .unindent(),
1325                cx,
1326            )
1327        });
1328        let cx = cx.add_empty_window();
1329        let editor = cx.new_view(|cx| Editor::for_buffer(buffer.clone(), None, cx));
1330
1331        let search_bar = cx.new_view(|cx| {
1332            let mut search_bar = BufferSearchBar::new(cx);
1333            search_bar.set_active_pane_item(Some(&editor), cx);
1334            search_bar.show(cx);
1335            search_bar
1336        });
1337
1338        (editor, search_bar, cx)
1339    }
1340
1341    #[gpui::test]
1342    async fn test_search_simple(cx: &mut TestAppContext) {
1343        let (editor, search_bar, cx) = init_test(cx);
1344        let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
1345            background_highlights
1346                .into_iter()
1347                .map(|(range, _)| range)
1348                .collect::<Vec<_>>()
1349        };
1350        // Search for a string that appears with different casing.
1351        // By default, search is case-insensitive.
1352        search_bar
1353            .update(cx, |search_bar, cx| search_bar.search("us", None, cx))
1354            .await
1355            .unwrap();
1356        editor.update(cx, |editor, cx| {
1357            assert_eq!(
1358                display_points_of(editor.all_text_background_highlights(cx)),
1359                &[
1360                    DisplayPoint::new(DisplayRow(2), 17)..DisplayPoint::new(DisplayRow(2), 19),
1361                    DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),
1362                ]
1363            );
1364        });
1365
1366        // Switch to a case sensitive search.
1367        search_bar.update(cx, |search_bar, cx| {
1368            search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx);
1369        });
1370        let mut editor_notifications = cx.notifications(&editor);
1371        editor_notifications.next().await;
1372        editor.update(cx, |editor, cx| {
1373            assert_eq!(
1374                display_points_of(editor.all_text_background_highlights(cx)),
1375                &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
1376            );
1377        });
1378
1379        // Search for a string that appears both as a whole word and
1380        // within other words. By default, all results are found.
1381        search_bar
1382            .update(cx, |search_bar, cx| search_bar.search("or", None, cx))
1383            .await
1384            .unwrap();
1385        editor.update(cx, |editor, cx| {
1386            assert_eq!(
1387                display_points_of(editor.all_text_background_highlights(cx)),
1388                &[
1389                    DisplayPoint::new(DisplayRow(0), 24)..DisplayPoint::new(DisplayRow(0), 26),
1390                    DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1391                    DisplayPoint::new(DisplayRow(2), 71)..DisplayPoint::new(DisplayRow(2), 73),
1392                    DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 3),
1393                    DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
1394                    DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
1395                    DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 62),
1396                ]
1397            );
1398        });
1399
1400        // Switch to a whole word search.
1401        search_bar.update(cx, |search_bar, cx| {
1402            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
1403        });
1404        let mut editor_notifications = cx.notifications(&editor);
1405        editor_notifications.next().await;
1406        editor.update(cx, |editor, cx| {
1407            assert_eq!(
1408                display_points_of(editor.all_text_background_highlights(cx)),
1409                &[
1410                    DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1411                    DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
1412                    DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
1413                ]
1414            );
1415        });
1416
1417        editor.update(cx, |editor, cx| {
1418            editor.change_selections(None, cx, |s| {
1419                s.select_display_ranges([
1420                    DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
1421                ])
1422            });
1423        });
1424        search_bar.update(cx, |search_bar, cx| {
1425            assert_eq!(search_bar.active_match_index, Some(0));
1426            search_bar.select_next_match(&SelectNextMatch, cx);
1427            assert_eq!(
1428                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1429                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1430            );
1431        });
1432        search_bar.update(cx, |search_bar, _| {
1433            assert_eq!(search_bar.active_match_index, Some(0));
1434        });
1435
1436        search_bar.update(cx, |search_bar, cx| {
1437            search_bar.select_next_match(&SelectNextMatch, cx);
1438            assert_eq!(
1439                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1440                [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1441            );
1442        });
1443        search_bar.update(cx, |search_bar, _| {
1444            assert_eq!(search_bar.active_match_index, Some(1));
1445        });
1446
1447        search_bar.update(cx, |search_bar, cx| {
1448            search_bar.select_next_match(&SelectNextMatch, cx);
1449            assert_eq!(
1450                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1451                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1452            );
1453        });
1454        search_bar.update(cx, |search_bar, _| {
1455            assert_eq!(search_bar.active_match_index, Some(2));
1456        });
1457
1458        search_bar.update(cx, |search_bar, cx| {
1459            search_bar.select_next_match(&SelectNextMatch, cx);
1460            assert_eq!(
1461                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1462                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1463            );
1464        });
1465        search_bar.update(cx, |search_bar, _| {
1466            assert_eq!(search_bar.active_match_index, Some(0));
1467        });
1468
1469        search_bar.update(cx, |search_bar, cx| {
1470            search_bar.select_prev_match(&SelectPrevMatch, cx);
1471            assert_eq!(
1472                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1473                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1474            );
1475        });
1476        search_bar.update(cx, |search_bar, _| {
1477            assert_eq!(search_bar.active_match_index, Some(2));
1478        });
1479
1480        search_bar.update(cx, |search_bar, cx| {
1481            search_bar.select_prev_match(&SelectPrevMatch, cx);
1482            assert_eq!(
1483                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1484                [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1485            );
1486        });
1487        search_bar.update(cx, |search_bar, _| {
1488            assert_eq!(search_bar.active_match_index, Some(1));
1489        });
1490
1491        search_bar.update(cx, |search_bar, cx| {
1492            search_bar.select_prev_match(&SelectPrevMatch, cx);
1493            assert_eq!(
1494                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1495                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1496            );
1497        });
1498        search_bar.update(cx, |search_bar, _| {
1499            assert_eq!(search_bar.active_match_index, Some(0));
1500        });
1501
1502        // Park the cursor in between matches and ensure that going to the previous match selects
1503        // the closest match to the left.
1504        editor.update(cx, |editor, cx| {
1505            editor.change_selections(None, cx, |s| {
1506                s.select_display_ranges([
1507                    DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
1508                ])
1509            });
1510        });
1511        search_bar.update(cx, |search_bar, cx| {
1512            assert_eq!(search_bar.active_match_index, Some(1));
1513            search_bar.select_prev_match(&SelectPrevMatch, cx);
1514            assert_eq!(
1515                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1516                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1517            );
1518        });
1519        search_bar.update(cx, |search_bar, _| {
1520            assert_eq!(search_bar.active_match_index, Some(0));
1521        });
1522
1523        // Park the cursor in between matches and ensure that going to the next match selects the
1524        // closest match to the right.
1525        editor.update(cx, |editor, cx| {
1526            editor.change_selections(None, cx, |s| {
1527                s.select_display_ranges([
1528                    DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
1529                ])
1530            });
1531        });
1532        search_bar.update(cx, |search_bar, cx| {
1533            assert_eq!(search_bar.active_match_index, Some(1));
1534            search_bar.select_next_match(&SelectNextMatch, cx);
1535            assert_eq!(
1536                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1537                [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1538            );
1539        });
1540        search_bar.update(cx, |search_bar, _| {
1541            assert_eq!(search_bar.active_match_index, Some(1));
1542        });
1543
1544        // Park the cursor after the last match and ensure that going to the previous match selects
1545        // the last match.
1546        editor.update(cx, |editor, cx| {
1547            editor.change_selections(None, cx, |s| {
1548                s.select_display_ranges([
1549                    DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
1550                ])
1551            });
1552        });
1553        search_bar.update(cx, |search_bar, cx| {
1554            assert_eq!(search_bar.active_match_index, Some(2));
1555            search_bar.select_prev_match(&SelectPrevMatch, cx);
1556            assert_eq!(
1557                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1558                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1559            );
1560        });
1561        search_bar.update(cx, |search_bar, _| {
1562            assert_eq!(search_bar.active_match_index, Some(2));
1563        });
1564
1565        // Park the cursor after the last match and ensure that going to the next match selects the
1566        // first match.
1567        editor.update(cx, |editor, cx| {
1568            editor.change_selections(None, cx, |s| {
1569                s.select_display_ranges([
1570                    DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
1571                ])
1572            });
1573        });
1574        search_bar.update(cx, |search_bar, cx| {
1575            assert_eq!(search_bar.active_match_index, Some(2));
1576            search_bar.select_next_match(&SelectNextMatch, cx);
1577            assert_eq!(
1578                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1579                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1580            );
1581        });
1582        search_bar.update(cx, |search_bar, _| {
1583            assert_eq!(search_bar.active_match_index, Some(0));
1584        });
1585
1586        // Park the cursor before the first match and ensure that going to the previous match
1587        // selects the last match.
1588        editor.update(cx, |editor, cx| {
1589            editor.change_selections(None, cx, |s| {
1590                s.select_display_ranges([
1591                    DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
1592                ])
1593            });
1594        });
1595        search_bar.update(cx, |search_bar, cx| {
1596            assert_eq!(search_bar.active_match_index, Some(0));
1597            search_bar.select_prev_match(&SelectPrevMatch, cx);
1598            assert_eq!(
1599                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1600                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1601            );
1602        });
1603        search_bar.update(cx, |search_bar, _| {
1604            assert_eq!(search_bar.active_match_index, Some(2));
1605        });
1606    }
1607
1608    fn display_points_of(
1609        background_highlights: Vec<(Range<DisplayPoint>, Hsla)>,
1610    ) -> Vec<Range<DisplayPoint>> {
1611        background_highlights
1612            .into_iter()
1613            .map(|(range, _)| range)
1614            .collect::<Vec<_>>()
1615    }
1616
1617    #[gpui::test]
1618    async fn test_search_option_handling(cx: &mut TestAppContext) {
1619        let (editor, search_bar, cx) = init_test(cx);
1620
1621        // show with options should make current search case sensitive
1622        search_bar
1623            .update(cx, |search_bar, cx| {
1624                search_bar.show(cx);
1625                search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), cx)
1626            })
1627            .await
1628            .unwrap();
1629        editor.update(cx, |editor, cx| {
1630            assert_eq!(
1631                display_points_of(editor.all_text_background_highlights(cx)),
1632                &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
1633            );
1634        });
1635
1636        // search_suggested should restore default options
1637        search_bar.update(cx, |search_bar, cx| {
1638            search_bar.search_suggested(cx);
1639            assert_eq!(search_bar.search_options, SearchOptions::NONE)
1640        });
1641
1642        // toggling a search option should update the defaults
1643        search_bar
1644            .update(cx, |search_bar, cx| {
1645                search_bar.search("regex", Some(SearchOptions::CASE_SENSITIVE), cx)
1646            })
1647            .await
1648            .unwrap();
1649        search_bar.update(cx, |search_bar, cx| {
1650            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx)
1651        });
1652        let mut editor_notifications = cx.notifications(&editor);
1653        editor_notifications.next().await;
1654        editor.update(cx, |editor, cx| {
1655            assert_eq!(
1656                display_points_of(editor.all_text_background_highlights(cx)),
1657                &[DisplayPoint::new(DisplayRow(0), 35)..DisplayPoint::new(DisplayRow(0), 40),]
1658            );
1659        });
1660
1661        // defaults should still include whole word
1662        search_bar.update(cx, |search_bar, cx| {
1663            search_bar.search_suggested(cx);
1664            assert_eq!(
1665                search_bar.search_options,
1666                SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD
1667            )
1668        });
1669    }
1670
1671    #[gpui::test]
1672    async fn test_search_select_all_matches(cx: &mut TestAppContext) {
1673        init_globals(cx);
1674        let buffer_text = r#"
1675        A regular expression (shortened as regex or regexp;[1] also referred to as
1676        rational expression[2][3]) is a sequence of characters that specifies a search
1677        pattern in text. Usually such patterns are used by string-searching algorithms
1678        for "find" or "find and replace" operations on strings, or for input validation.
1679        "#
1680        .unindent();
1681        let expected_query_matches_count = buffer_text
1682            .chars()
1683            .filter(|c| c.to_ascii_lowercase() == 'a')
1684            .count();
1685        assert!(
1686            expected_query_matches_count > 1,
1687            "Should pick a query with multiple results"
1688        );
1689        let buffer = cx.new_model(|cx| Buffer::local(buffer_text, cx));
1690        let window = cx.add_window(|_| gpui::Empty);
1691
1692        let editor = window.build_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx));
1693
1694        let search_bar = window.build_view(cx, |cx| {
1695            let mut search_bar = BufferSearchBar::new(cx);
1696            search_bar.set_active_pane_item(Some(&editor), cx);
1697            search_bar.show(cx);
1698            search_bar
1699        });
1700
1701        window
1702            .update(cx, |_, cx| {
1703                search_bar.update(cx, |search_bar, cx| search_bar.search("a", None, cx))
1704            })
1705            .unwrap()
1706            .await
1707            .unwrap();
1708        let initial_selections = window
1709            .update(cx, |_, cx| {
1710                search_bar.update(cx, |search_bar, cx| {
1711                    let handle = search_bar.query_editor.focus_handle(cx);
1712                    cx.focus(&handle);
1713                    search_bar.activate_current_match(cx);
1714                });
1715                assert!(
1716                    !editor.read(cx).is_focused(cx),
1717                    "Initially, the editor should not be focused"
1718                );
1719                let initial_selections = editor.update(cx, |editor, cx| {
1720                    let initial_selections = editor.selections.display_ranges(cx);
1721                    assert_eq!(
1722                        initial_selections.len(), 1,
1723                        "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
1724                    );
1725                    initial_selections
1726                });
1727                search_bar.update(cx, |search_bar, cx| {
1728                    assert_eq!(search_bar.active_match_index, Some(0));
1729                    let handle = search_bar.query_editor.focus_handle(cx);
1730                    cx.focus(&handle);
1731                    search_bar.select_all_matches(&SelectAllMatches, cx);
1732                });
1733                assert!(
1734                    editor.read(cx).is_focused(cx),
1735                    "Should focus editor after successful SelectAllMatches"
1736                );
1737                search_bar.update(cx, |search_bar, cx| {
1738                    let all_selections =
1739                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1740                    assert_eq!(
1741                        all_selections.len(),
1742                        expected_query_matches_count,
1743                        "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1744                    );
1745                    assert_eq!(
1746                        search_bar.active_match_index,
1747                        Some(0),
1748                        "Match index should not change after selecting all matches"
1749                    );
1750                });
1751
1752                search_bar.update(cx, |this, cx| this.select_next_match(&SelectNextMatch, cx));
1753                initial_selections
1754            }).unwrap();
1755
1756        window
1757            .update(cx, |_, cx| {
1758                assert!(
1759                    editor.read(cx).is_focused(cx),
1760                    "Should still have editor focused after SelectNextMatch"
1761                );
1762                search_bar.update(cx, |search_bar, cx| {
1763                    let all_selections =
1764                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1765                    assert_eq!(
1766                        all_selections.len(),
1767                        1,
1768                        "On next match, should deselect items and select the next match"
1769                    );
1770                    assert_ne!(
1771                        all_selections, initial_selections,
1772                        "Next match should be different from the first selection"
1773                    );
1774                    assert_eq!(
1775                        search_bar.active_match_index,
1776                        Some(1),
1777                        "Match index should be updated to the next one"
1778                    );
1779                    let handle = search_bar.query_editor.focus_handle(cx);
1780                    cx.focus(&handle);
1781                    search_bar.select_all_matches(&SelectAllMatches, cx);
1782                });
1783            })
1784            .unwrap();
1785        window
1786            .update(cx, |_, cx| {
1787                assert!(
1788                    editor.read(cx).is_focused(cx),
1789                    "Should focus editor after successful SelectAllMatches"
1790                );
1791                search_bar.update(cx, |search_bar, cx| {
1792                    let all_selections =
1793                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1794                    assert_eq!(
1795                    all_selections.len(),
1796                    expected_query_matches_count,
1797                    "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1798                );
1799                    assert_eq!(
1800                        search_bar.active_match_index,
1801                        Some(1),
1802                        "Match index should not change after selecting all matches"
1803                    );
1804                });
1805                search_bar.update(cx, |search_bar, cx| {
1806                    search_bar.select_prev_match(&SelectPrevMatch, cx);
1807                });
1808            })
1809            .unwrap();
1810        let last_match_selections = window
1811            .update(cx, |_, cx| {
1812                assert!(
1813                    editor.read(cx).is_focused(cx),
1814                    "Should still have editor focused after SelectPrevMatch"
1815                );
1816
1817                search_bar.update(cx, |search_bar, cx| {
1818                    let all_selections =
1819                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1820                    assert_eq!(
1821                        all_selections.len(),
1822                        1,
1823                        "On previous match, should deselect items and select the previous item"
1824                    );
1825                    assert_eq!(
1826                        all_selections, initial_selections,
1827                        "Previous match should be the same as the first selection"
1828                    );
1829                    assert_eq!(
1830                        search_bar.active_match_index,
1831                        Some(0),
1832                        "Match index should be updated to the previous one"
1833                    );
1834                    all_selections
1835                })
1836            })
1837            .unwrap();
1838
1839        window
1840            .update(cx, |_, cx| {
1841                search_bar.update(cx, |search_bar, cx| {
1842                    let handle = search_bar.query_editor.focus_handle(cx);
1843                    cx.focus(&handle);
1844                    search_bar.search("abas_nonexistent_match", None, cx)
1845                })
1846            })
1847            .unwrap()
1848            .await
1849            .unwrap();
1850        window
1851            .update(cx, |_, cx| {
1852                search_bar.update(cx, |search_bar, cx| {
1853                    search_bar.select_all_matches(&SelectAllMatches, cx);
1854                });
1855                assert!(
1856                    editor.update(cx, |this, cx| !this.is_focused(cx.window_context())),
1857                    "Should not switch focus to editor if SelectAllMatches does not find any matches"
1858                );
1859                search_bar.update(cx, |search_bar, cx| {
1860                    let all_selections =
1861                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1862                    assert_eq!(
1863                        all_selections, last_match_selections,
1864                        "Should not select anything new if there are no matches"
1865                    );
1866                    assert!(
1867                        search_bar.active_match_index.is_none(),
1868                        "For no matches, there should be no active match index"
1869                    );
1870                });
1871            })
1872            .unwrap();
1873    }
1874
1875    #[gpui::test]
1876    async fn test_search_query_with_match_whole_word(cx: &mut TestAppContext) {
1877        init_globals(cx);
1878        let buffer_text = r#"
1879        self.buffer.update(cx, |buffer, cx| {
1880            buffer.edit(
1881                edits,
1882                Some(AutoindentMode::Block {
1883                    original_indent_columns,
1884                }),
1885                cx,
1886            )
1887        });
1888
1889        this.buffer.update(cx, |buffer, cx| {
1890            buffer.edit([(end_of_line..start_of_next_line, replace)], None, cx)
1891        });
1892        "#
1893        .unindent();
1894        let buffer = cx.new_model(|cx| Buffer::local(buffer_text, cx));
1895        let cx = cx.add_empty_window();
1896
1897        let editor = cx.new_view(|cx| Editor::for_buffer(buffer.clone(), None, cx));
1898
1899        let search_bar = cx.new_view(|cx| {
1900            let mut search_bar = BufferSearchBar::new(cx);
1901            search_bar.set_active_pane_item(Some(&editor), cx);
1902            search_bar.show(cx);
1903            search_bar
1904        });
1905
1906        search_bar
1907            .update(cx, |search_bar, cx| {
1908                search_bar.search(
1909                    "edit\\(",
1910                    Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
1911                    cx,
1912                )
1913            })
1914            .await
1915            .unwrap();
1916
1917        search_bar.update(cx, |search_bar, cx| {
1918            search_bar.select_all_matches(&SelectAllMatches, cx);
1919        });
1920        search_bar.update(cx, |_, cx| {
1921            let all_selections =
1922                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1923            assert_eq!(
1924                all_selections.len(),
1925                2,
1926                "Should select all `edit(` in the buffer, but got: {all_selections:?}"
1927            );
1928        });
1929
1930        search_bar
1931            .update(cx, |search_bar, cx| {
1932                search_bar.search(
1933                    "edit(",
1934                    Some(SearchOptions::WHOLE_WORD | SearchOptions::CASE_SENSITIVE),
1935                    cx,
1936                )
1937            })
1938            .await
1939            .unwrap();
1940
1941        search_bar.update(cx, |search_bar, cx| {
1942            search_bar.select_all_matches(&SelectAllMatches, cx);
1943        });
1944        search_bar.update(cx, |_, cx| {
1945            let all_selections =
1946                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1947            assert_eq!(
1948                all_selections.len(),
1949                2,
1950                "Should select all `edit(` in the buffer, but got: {all_selections:?}"
1951            );
1952        });
1953    }
1954
1955    #[gpui::test]
1956    async fn test_search_query_history(cx: &mut TestAppContext) {
1957        init_globals(cx);
1958        let buffer_text = r#"
1959        A regular expression (shortened as regex or regexp;[1] also referred to as
1960        rational expression[2][3]) is a sequence of characters that specifies a search
1961        pattern in text. Usually such patterns are used by string-searching algorithms
1962        for "find" or "find and replace" operations on strings, or for input validation.
1963        "#
1964        .unindent();
1965        let buffer = cx.new_model(|cx| Buffer::local(buffer_text, cx));
1966        let cx = cx.add_empty_window();
1967
1968        let editor = cx.new_view(|cx| Editor::for_buffer(buffer.clone(), None, cx));
1969
1970        let search_bar = cx.new_view(|cx| {
1971            let mut search_bar = BufferSearchBar::new(cx);
1972            search_bar.set_active_pane_item(Some(&editor), cx);
1973            search_bar.show(cx);
1974            search_bar
1975        });
1976
1977        // Add 3 search items into the history.
1978        search_bar
1979            .update(cx, |search_bar, cx| search_bar.search("a", None, cx))
1980            .await
1981            .unwrap();
1982        search_bar
1983            .update(cx, |search_bar, cx| search_bar.search("b", None, cx))
1984            .await
1985            .unwrap();
1986        search_bar
1987            .update(cx, |search_bar, cx| {
1988                search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), cx)
1989            })
1990            .await
1991            .unwrap();
1992        // Ensure that the latest search is active.
1993        search_bar.update(cx, |search_bar, cx| {
1994            assert_eq!(search_bar.query(cx), "c");
1995            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1996        });
1997
1998        // Next history query after the latest should set the query to the empty string.
1999        search_bar.update(cx, |search_bar, cx| {
2000            search_bar.next_history_query(&NextHistoryQuery, cx);
2001        });
2002        search_bar.update(cx, |search_bar, cx| {
2003            assert_eq!(search_bar.query(cx), "");
2004            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2005        });
2006        search_bar.update(cx, |search_bar, cx| {
2007            search_bar.next_history_query(&NextHistoryQuery, cx);
2008        });
2009        search_bar.update(cx, |search_bar, cx| {
2010            assert_eq!(search_bar.query(cx), "");
2011            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2012        });
2013
2014        // First previous query for empty current query should set the query to the latest.
2015        search_bar.update(cx, |search_bar, cx| {
2016            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
2017        });
2018        search_bar.update(cx, |search_bar, cx| {
2019            assert_eq!(search_bar.query(cx), "c");
2020            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2021        });
2022
2023        // Further previous items should go over the history in reverse order.
2024        search_bar.update(cx, |search_bar, cx| {
2025            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
2026        });
2027        search_bar.update(cx, |search_bar, cx| {
2028            assert_eq!(search_bar.query(cx), "b");
2029            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2030        });
2031
2032        // Previous items should never go behind the first history item.
2033        search_bar.update(cx, |search_bar, cx| {
2034            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
2035        });
2036        search_bar.update(cx, |search_bar, cx| {
2037            assert_eq!(search_bar.query(cx), "a");
2038            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2039        });
2040        search_bar.update(cx, |search_bar, cx| {
2041            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
2042        });
2043        search_bar.update(cx, |search_bar, cx| {
2044            assert_eq!(search_bar.query(cx), "a");
2045            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2046        });
2047
2048        // Next items should go over the history in the original order.
2049        search_bar.update(cx, |search_bar, cx| {
2050            search_bar.next_history_query(&NextHistoryQuery, cx);
2051        });
2052        search_bar.update(cx, |search_bar, cx| {
2053            assert_eq!(search_bar.query(cx), "b");
2054            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2055        });
2056
2057        search_bar
2058            .update(cx, |search_bar, cx| search_bar.search("ba", None, cx))
2059            .await
2060            .unwrap();
2061        search_bar.update(cx, |search_bar, cx| {
2062            assert_eq!(search_bar.query(cx), "ba");
2063            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2064        });
2065
2066        // New search input should add another entry to history and move the selection to the end of the history.
2067        search_bar.update(cx, |search_bar, cx| {
2068            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
2069        });
2070        search_bar.update(cx, |search_bar, cx| {
2071            assert_eq!(search_bar.query(cx), "c");
2072            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2073        });
2074        search_bar.update(cx, |search_bar, cx| {
2075            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
2076        });
2077        search_bar.update(cx, |search_bar, cx| {
2078            assert_eq!(search_bar.query(cx), "b");
2079            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2080        });
2081        search_bar.update(cx, |search_bar, cx| {
2082            search_bar.next_history_query(&NextHistoryQuery, cx);
2083        });
2084        search_bar.update(cx, |search_bar, cx| {
2085            assert_eq!(search_bar.query(cx), "c");
2086            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2087        });
2088        search_bar.update(cx, |search_bar, cx| {
2089            search_bar.next_history_query(&NextHistoryQuery, cx);
2090        });
2091        search_bar.update(cx, |search_bar, cx| {
2092            assert_eq!(search_bar.query(cx), "ba");
2093            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2094        });
2095        search_bar.update(cx, |search_bar, cx| {
2096            search_bar.next_history_query(&NextHistoryQuery, cx);
2097        });
2098        search_bar.update(cx, |search_bar, cx| {
2099            assert_eq!(search_bar.query(cx), "");
2100            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2101        });
2102    }
2103
2104    #[gpui::test]
2105    async fn test_replace_simple(cx: &mut TestAppContext) {
2106        let (editor, search_bar, cx) = init_test(cx);
2107
2108        search_bar
2109            .update(cx, |search_bar, cx| {
2110                search_bar.search("expression", None, cx)
2111            })
2112            .await
2113            .unwrap();
2114
2115        search_bar.update(cx, |search_bar, cx| {
2116            search_bar.replacement_editor.update(cx, |editor, cx| {
2117                // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally.
2118                editor.set_text("expr$1", cx);
2119            });
2120            search_bar.replace_all(&ReplaceAll, cx)
2121        });
2122        assert_eq!(
2123            editor.update(cx, |this, cx| { this.text(cx) }),
2124            r#"
2125        A regular expr$1 (shortened as regex or regexp;[1] also referred to as
2126        rational expr$1[2][3]) is a sequence of characters that specifies a search
2127        pattern in text. Usually such patterns are used by string-searching algorithms
2128        for "find" or "find and replace" operations on strings, or for input validation.
2129        "#
2130            .unindent()
2131        );
2132
2133        // Search for word boundaries and replace just a single one.
2134        search_bar
2135            .update(cx, |search_bar, cx| {
2136                search_bar.search("or", Some(SearchOptions::WHOLE_WORD), cx)
2137            })
2138            .await
2139            .unwrap();
2140
2141        search_bar.update(cx, |search_bar, cx| {
2142            search_bar.replacement_editor.update(cx, |editor, cx| {
2143                editor.set_text("banana", cx);
2144            });
2145            search_bar.replace_next(&ReplaceNext, cx)
2146        });
2147        // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text.
2148        assert_eq!(
2149            editor.update(cx, |this, cx| { this.text(cx) }),
2150            r#"
2151        A regular expr$1 (shortened as regex banana regexp;[1] also referred to as
2152        rational expr$1[2][3]) is a sequence of characters that specifies a search
2153        pattern in text. Usually such patterns are used by string-searching algorithms
2154        for "find" or "find and replace" operations on strings, or for input validation.
2155        "#
2156            .unindent()
2157        );
2158        // Let's turn on regex mode.
2159        search_bar
2160            .update(cx, |search_bar, cx| {
2161                search_bar.search("\\[([^\\]]+)\\]", Some(SearchOptions::REGEX), cx)
2162            })
2163            .await
2164            .unwrap();
2165        search_bar.update(cx, |search_bar, cx| {
2166            search_bar.replacement_editor.update(cx, |editor, cx| {
2167                editor.set_text("${1}number", cx);
2168            });
2169            search_bar.replace_all(&ReplaceAll, cx)
2170        });
2171        assert_eq!(
2172            editor.update(cx, |this, cx| { this.text(cx) }),
2173            r#"
2174        A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2175        rational expr$12number3number) is a sequence of characters that specifies a search
2176        pattern in text. Usually such patterns are used by string-searching algorithms
2177        for "find" or "find and replace" operations on strings, or for input validation.
2178        "#
2179            .unindent()
2180        );
2181        // Now with a whole-word twist.
2182        search_bar
2183            .update(cx, |search_bar, cx| {
2184                search_bar.search(
2185                    "a\\w+s",
2186                    Some(SearchOptions::REGEX | SearchOptions::WHOLE_WORD),
2187                    cx,
2188                )
2189            })
2190            .await
2191            .unwrap();
2192        search_bar.update(cx, |search_bar, cx| {
2193            search_bar.replacement_editor.update(cx, |editor, cx| {
2194                editor.set_text("things", cx);
2195            });
2196            search_bar.replace_all(&ReplaceAll, cx)
2197        });
2198        // The only word affected by this edit should be `algorithms`, even though there's a bunch
2199        // of words in this text that would match this regex if not for WHOLE_WORD.
2200        assert_eq!(
2201            editor.update(cx, |this, cx| { this.text(cx) }),
2202            r#"
2203        A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2204        rational expr$12number3number) is a sequence of characters that specifies a search
2205        pattern in text. Usually such patterns are used by string-searching things
2206        for "find" or "find and replace" operations on strings, or for input validation.
2207        "#
2208            .unindent()
2209        );
2210    }
2211
2212    struct ReplacementTestParams<'a> {
2213        editor: &'a View<Editor>,
2214        search_bar: &'a View<BufferSearchBar>,
2215        cx: &'a mut VisualTestContext,
2216        search_text: &'static str,
2217        search_options: Option<SearchOptions>,
2218        replacement_text: &'static str,
2219        replace_all: bool,
2220        expected_text: String,
2221    }
2222
2223    async fn run_replacement_test(options: ReplacementTestParams<'_>) {
2224        options
2225            .search_bar
2226            .update(options.cx, |search_bar, cx| {
2227                if let Some(options) = options.search_options {
2228                    search_bar.set_search_options(options, cx);
2229                }
2230                search_bar.search(options.search_text, options.search_options, cx)
2231            })
2232            .await
2233            .unwrap();
2234
2235        options.search_bar.update(options.cx, |search_bar, cx| {
2236            search_bar.replacement_editor.update(cx, |editor, cx| {
2237                editor.set_text(options.replacement_text, cx);
2238            });
2239
2240            if options.replace_all {
2241                search_bar.replace_all(&ReplaceAll, cx)
2242            } else {
2243                search_bar.replace_next(&ReplaceNext, cx)
2244            }
2245        });
2246
2247        assert_eq!(
2248            options
2249                .editor
2250                .update(options.cx, |this, cx| { this.text(cx) }),
2251            options.expected_text
2252        );
2253    }
2254
2255    #[gpui::test]
2256    async fn test_replace_special_characters(cx: &mut TestAppContext) {
2257        let (editor, search_bar, cx) = init_test(cx);
2258
2259        run_replacement_test(ReplacementTestParams {
2260            editor: &editor,
2261            search_bar: &search_bar,
2262            cx,
2263            search_text: "expression",
2264            search_options: None,
2265            replacement_text: r"\n",
2266            replace_all: true,
2267            expected_text: r#"
2268            A regular \n (shortened as regex or regexp;[1] also referred to as
2269            rational \n[2][3]) is a sequence of characters that specifies a search
2270            pattern in text. Usually such patterns are used by string-searching algorithms
2271            for "find" or "find and replace" operations on strings, or for input validation.
2272            "#
2273            .unindent(),
2274        })
2275        .await;
2276
2277        run_replacement_test(ReplacementTestParams {
2278            editor: &editor,
2279            search_bar: &search_bar,
2280            cx,
2281            search_text: "or",
2282            search_options: Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
2283            replacement_text: r"\\\n\\\\",
2284            replace_all: false,
2285            expected_text: r#"
2286            A regular \n (shortened as regex \
2287            \\ regexp;[1] also referred to as
2288            rational \n[2][3]) is a sequence of characters that specifies a search
2289            pattern in text. Usually such patterns are used by string-searching algorithms
2290            for "find" or "find and replace" operations on strings, or for input validation.
2291            "#
2292            .unindent(),
2293        })
2294        .await;
2295
2296        run_replacement_test(ReplacementTestParams {
2297            editor: &editor,
2298            search_bar: &search_bar,
2299            cx,
2300            search_text: r"(that|used) ",
2301            search_options: Some(SearchOptions::REGEX),
2302            replacement_text: r"$1\n",
2303            replace_all: true,
2304            expected_text: r#"
2305            A regular \n (shortened as regex \
2306            \\ regexp;[1] also referred to as
2307            rational \n[2][3]) is a sequence of characters that
2308            specifies a search
2309            pattern in text. Usually such patterns are used
2310            by string-searching algorithms
2311            for "find" or "find and replace" operations on strings, or for input validation.
2312            "#
2313            .unindent(),
2314        })
2315        .await;
2316    }
2317
2318    #[gpui::test]
2319    async fn test_find_matches_in_selections_singleton_buffer_multiple_selections(
2320        cx: &mut TestAppContext,
2321    ) {
2322        init_globals(cx);
2323        let buffer = cx.new_model(|cx| {
2324            Buffer::local(
2325                r#"
2326                aaa bbb aaa ccc
2327                aaa bbb aaa ccc
2328                aaa bbb aaa ccc
2329                aaa bbb aaa ccc
2330                aaa bbb aaa ccc
2331                aaa bbb aaa ccc
2332                "#
2333                .unindent(),
2334                cx,
2335            )
2336        });
2337        let cx = cx.add_empty_window();
2338        let editor = cx.new_view(|cx| Editor::for_buffer(buffer.clone(), None, cx));
2339
2340        let search_bar = cx.new_view(|cx| {
2341            let mut search_bar = BufferSearchBar::new(cx);
2342            search_bar.set_active_pane_item(Some(&editor), cx);
2343            search_bar.show(cx);
2344            search_bar
2345        });
2346
2347        editor.update(cx, |editor, cx| {
2348            editor.change_selections(None, cx, |s| {
2349                s.select_ranges(vec![Point::new(1, 0)..Point::new(2, 4)])
2350            })
2351        });
2352
2353        search_bar.update(cx, |search_bar, cx| {
2354            let deploy = Deploy {
2355                focus: true,
2356                replace_enabled: false,
2357                selection_search_enabled: true,
2358            };
2359            search_bar.deploy(&deploy, cx);
2360        });
2361
2362        cx.run_until_parked();
2363
2364        search_bar
2365            .update(cx, |search_bar, cx| search_bar.search("aaa", None, cx))
2366            .await
2367            .unwrap();
2368
2369        editor.update(cx, |editor, cx| {
2370            assert_eq!(
2371                editor.search_background_highlights(cx),
2372                &[
2373                    Point::new(1, 0)..Point::new(1, 3),
2374                    Point::new(1, 8)..Point::new(1, 11),
2375                    Point::new(2, 0)..Point::new(2, 3),
2376                ]
2377            );
2378        });
2379    }
2380
2381    #[gpui::test]
2382    async fn test_find_matches_in_selections_multiple_excerpts_buffer_multiple_selections(
2383        cx: &mut TestAppContext,
2384    ) {
2385        init_globals(cx);
2386        let text = r#"
2387            aaa bbb aaa ccc
2388            aaa bbb aaa ccc
2389            aaa bbb aaa ccc
2390            aaa bbb aaa ccc
2391            aaa bbb aaa ccc
2392            aaa bbb aaa ccc
2393
2394            aaa bbb aaa ccc
2395            aaa bbb aaa ccc
2396            aaa bbb aaa ccc
2397            aaa bbb aaa ccc
2398            aaa bbb aaa ccc
2399            aaa bbb aaa ccc
2400            "#
2401        .unindent();
2402
2403        let cx = cx.add_empty_window();
2404        let editor = cx.new_view(|cx| {
2405            let multibuffer = MultiBuffer::build_multi(
2406                [
2407                    (
2408                        &text,
2409                        vec![
2410                            Point::new(0, 0)..Point::new(2, 0),
2411                            Point::new(4, 0)..Point::new(5, 0),
2412                        ],
2413                    ),
2414                    (&text, vec![Point::new(9, 0)..Point::new(11, 0)]),
2415                ],
2416                cx,
2417            );
2418            Editor::for_multibuffer(multibuffer, None, false, cx)
2419        });
2420
2421        let search_bar = cx.new_view(|cx| {
2422            let mut search_bar = BufferSearchBar::new(cx);
2423            search_bar.set_active_pane_item(Some(&editor), cx);
2424            search_bar.show(cx);
2425            search_bar
2426        });
2427
2428        editor.update(cx, |editor, cx| {
2429            editor.change_selections(None, cx, |s| {
2430                s.select_ranges(vec![
2431                    Point::new(1, 0)..Point::new(1, 4),
2432                    Point::new(5, 3)..Point::new(6, 4),
2433                ])
2434            })
2435        });
2436
2437        search_bar.update(cx, |search_bar, cx| {
2438            let deploy = Deploy {
2439                focus: true,
2440                replace_enabled: false,
2441                selection_search_enabled: true,
2442            };
2443            search_bar.deploy(&deploy, cx);
2444        });
2445
2446        cx.run_until_parked();
2447
2448        search_bar
2449            .update(cx, |search_bar, cx| search_bar.search("aaa", None, cx))
2450            .await
2451            .unwrap();
2452
2453        editor.update(cx, |editor, cx| {
2454            assert_eq!(
2455                editor.search_background_highlights(cx),
2456                &[
2457                    Point::new(1, 0)..Point::new(1, 3),
2458                    Point::new(5, 8)..Point::new(5, 11),
2459                    Point::new(6, 0)..Point::new(6, 3),
2460                ]
2461            );
2462        });
2463    }
2464
2465    #[gpui::test]
2466    async fn test_invalid_regexp_search_after_valid(cx: &mut TestAppContext) {
2467        let (editor, search_bar, cx) = init_test(cx);
2468        // Search using valid regexp
2469        search_bar
2470            .update(cx, |search_bar, cx| {
2471                search_bar.enable_search_option(SearchOptions::REGEX, cx);
2472                search_bar.search("expression", None, cx)
2473            })
2474            .await
2475            .unwrap();
2476        editor.update(cx, |editor, cx| {
2477            assert_eq!(
2478                display_points_of(editor.all_text_background_highlights(cx)),
2479                &[
2480                    DisplayPoint::new(DisplayRow(0), 10)..DisplayPoint::new(DisplayRow(0), 20),
2481                    DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 19),
2482                ],
2483            );
2484        });
2485
2486        // Now, the expression is invalid
2487        search_bar
2488            .update(cx, |search_bar, cx| {
2489                search_bar.search("expression (", None, cx)
2490            })
2491            .await
2492            .unwrap_err();
2493        editor.update(cx, |editor, cx| {
2494            assert!(display_points_of(editor.all_text_background_highlights(cx)).is_empty(),);
2495        });
2496    }
2497
2498    #[gpui::test]
2499    async fn test_search_options_changes(cx: &mut TestAppContext) {
2500        let (_editor, search_bar, cx) = init_test(cx);
2501        update_search_settings(
2502            SearchSettings {
2503                whole_word: false,
2504                case_sensitive: false,
2505                include_ignored: false,
2506                regex: false,
2507            },
2508            cx,
2509        );
2510
2511        let deploy = Deploy {
2512            focus: true,
2513            replace_enabled: false,
2514            selection_search_enabled: true,
2515        };
2516
2517        search_bar.update(cx, |search_bar, cx| {
2518            assert_eq!(
2519                search_bar.search_options,
2520                SearchOptions::NONE,
2521                "Should have no search options enabled by default"
2522            );
2523            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
2524            assert_eq!(
2525                search_bar.search_options,
2526                SearchOptions::WHOLE_WORD,
2527                "Should enable the option toggled"
2528            );
2529            assert!(
2530                !search_bar.dismissed,
2531                "Search bar should be present and visible"
2532            );
2533            search_bar.deploy(&deploy, cx);
2534            assert_eq!(
2535                search_bar.configured_options,
2536                SearchOptions::NONE,
2537                "Should have configured search options matching the settings"
2538            );
2539            assert_eq!(
2540                search_bar.search_options,
2541                SearchOptions::WHOLE_WORD,
2542                "After (re)deploying, the option should still be enabled"
2543            );
2544
2545            search_bar.dismiss(&Dismiss, cx);
2546            search_bar.deploy(&deploy, cx);
2547            assert_eq!(
2548                search_bar.search_options,
2549                SearchOptions::NONE,
2550                "After hiding and showing the search bar, default options should be used"
2551            );
2552
2553            search_bar.toggle_search_option(SearchOptions::REGEX, cx);
2554            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
2555            assert_eq!(
2556                search_bar.search_options,
2557                SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2558                "Should enable the options toggled"
2559            );
2560            assert!(
2561                !search_bar.dismissed,
2562                "Search bar should be present and visible"
2563            );
2564        });
2565
2566        update_search_settings(
2567            SearchSettings {
2568                whole_word: false,
2569                case_sensitive: true,
2570                include_ignored: false,
2571                regex: false,
2572            },
2573            cx,
2574        );
2575        search_bar.update(cx, |search_bar, cx| {
2576            assert_eq!(
2577                search_bar.search_options,
2578                SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2579                "Should have no search options enabled by default"
2580            );
2581
2582            search_bar.deploy(&deploy, cx);
2583            assert_eq!(
2584                search_bar.configured_options,
2585                SearchOptions::CASE_SENSITIVE,
2586                "Should have configured search options matching the settings"
2587            );
2588            assert_eq!(
2589                search_bar.search_options,
2590                SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2591                "Toggling a non-dismissed search bar with custom options should not change the default options"
2592            );
2593            search_bar.dismiss(&Dismiss, cx);
2594            search_bar.deploy(&deploy, cx);
2595            assert_eq!(
2596                search_bar.search_options,
2597                SearchOptions::CASE_SENSITIVE,
2598                "After hiding and showing the search bar, default options should be used"
2599            );
2600        });
2601    }
2602
2603    fn update_search_settings(search_settings: SearchSettings, cx: &mut TestAppContext) {
2604        cx.update(|cx| {
2605            SettingsStore::update_global(cx, |store, cx| {
2606                store.update_user_settings::<EditorSettings>(cx, |settings| {
2607                    settings.search = Some(search_settings);
2608                });
2609            });
2610        });
2611    }
2612}