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