buffer_search.rs

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