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