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