buffer_search.rs

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