buffer_search.rs

   1mod registrar;
   2
   3use crate::{
   4    FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOption,
   5    SearchOptions, SearchSource, SelectAllMatches, SelectNextMatch, SelectPreviousMatch,
   6    ToggleCaseSensitive, ToggleRegex, ToggleReplace, ToggleSelection, ToggleWholeWord,
   7    buffer_search::registrar::WithResultsOrExternalQuery,
   8    search_bar::{
   9        ActionButtonState, alignment_element, filter_search_results_input, input_base_styles,
  10        render_action_button, render_text_input,
  11    },
  12};
  13use any_vec::AnyVec;
  14use collections::HashMap;
  15use editor::{
  16    DisplayPoint, Editor, EditorSettings, MultiBufferOffset,
  17    actions::{Backtab, FoldAll, Tab, ToggleFoldAll, UnfoldAll},
  18};
  19use futures::channel::oneshot;
  20use gpui::{
  21    Action, App, ClickEvent, Context, Entity, EventEmitter, Focusable, InteractiveElement as _,
  22    IntoElement, KeyContext, ParentElement as _, Render, ScrollHandle, Styled, Subscription, Task,
  23    Window, actions, div,
  24};
  25use language::{Language, LanguageRegistry};
  26use project::{
  27    search::SearchQuery,
  28    search_history::{SearchHistory, SearchHistoryCursor},
  29};
  30use schemars::JsonSchema;
  31use serde::Deserialize;
  32use settings::Settings;
  33use std::{any::TypeId, sync::Arc};
  34use zed_actions::{outline::ToggleOutline, workspace::CopyPath, workspace::CopyRelativePath};
  35
  36use ui::{BASE_REM_SIZE_IN_PX, IconButtonShape, Tooltip, prelude::*, utils::SearchInputWidth};
  37use util::{ResultExt, paths::PathMatcher};
  38use workspace::{
  39    ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
  40    item::{ItemBufferKind, ItemHandle},
  41    searchable::{
  42        CollapseDirection, Direction, FilteredSearchRange, SearchEvent, SearchableItemHandle,
  43        WeakSearchableItemHandle,
  44    },
  45};
  46
  47pub use registrar::DivRegistrar;
  48use registrar::{ForDeployed, ForDismissed, SearchActionsRegistrar};
  49
  50const MAX_BUFFER_SEARCH_HISTORY_SIZE: usize = 50;
  51
  52/// Opens the buffer search interface with the specified configuration.
  53#[derive(PartialEq, Clone, Deserialize, JsonSchema, Action)]
  54#[action(namespace = buffer_search)]
  55#[serde(deny_unknown_fields)]
  56pub struct Deploy {
  57    #[serde(default = "util::serde::default_true")]
  58    pub focus: bool,
  59    #[serde(default)]
  60    pub replace_enabled: bool,
  61    #[serde(default)]
  62    pub selection_search_enabled: bool,
  63}
  64
  65actions!(
  66    buffer_search,
  67    [
  68        /// Deploys the search and replace interface.
  69        DeployReplace,
  70        /// Dismisses the search bar.
  71        Dismiss,
  72        /// Focuses back on the editor.
  73        FocusEditor
  74    ]
  75);
  76
  77impl Deploy {
  78    pub fn find() -> Self {
  79        Self {
  80            focus: true,
  81            replace_enabled: false,
  82            selection_search_enabled: false,
  83        }
  84    }
  85
  86    pub fn replace() -> Self {
  87        Self {
  88            focus: true,
  89            replace_enabled: true,
  90            selection_search_enabled: false,
  91        }
  92    }
  93}
  94
  95pub enum Event {
  96    UpdateLocation,
  97}
  98
  99pub fn init(cx: &mut App) {
 100    cx.observe_new(|workspace: &mut Workspace, _, _| BufferSearchBar::register(workspace))
 101        .detach();
 102}
 103
 104pub struct BufferSearchBar {
 105    query_editor: Entity<Editor>,
 106    query_editor_focused: bool,
 107    replacement_editor: Entity<Editor>,
 108    replacement_editor_focused: bool,
 109    active_searchable_item: Option<Box<dyn SearchableItemHandle>>,
 110    active_match_index: Option<usize>,
 111    #[cfg(target_os = "macos")]
 112    active_searchable_item_subscriptions: Option<[Subscription; 2]>,
 113    #[cfg(not(target_os = "macos"))]
 114    active_searchable_item_subscriptions: Option<Subscription>,
 115    #[cfg(target_os = "macos")]
 116    pending_external_query: Option<(String, SearchOptions)>,
 117    active_search: Option<Arc<SearchQuery>>,
 118    searchable_items_with_matches: HashMap<Box<dyn WeakSearchableItemHandle>, AnyVec<dyn Send>>,
 119    pending_search: Option<Task<()>>,
 120    search_options: SearchOptions,
 121    default_options: SearchOptions,
 122    configured_options: SearchOptions,
 123    query_error: Option<String>,
 124    dismissed: bool,
 125    search_history: SearchHistory,
 126    search_history_cursor: SearchHistoryCursor,
 127    replace_enabled: bool,
 128    selection_search_enabled: Option<FilteredSearchRange>,
 129    scroll_handle: ScrollHandle,
 130    editor_scroll_handle: ScrollHandle,
 131    editor_needed_width: Pixels,
 132    regex_language: Option<Arc<Language>>,
 133    is_collapsed: bool,
 134}
 135
 136impl EventEmitter<Event> for BufferSearchBar {}
 137impl EventEmitter<workspace::ToolbarItemEvent> for BufferSearchBar {}
 138impl Render for BufferSearchBar {
 139    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 140        let focus_handle = self.focus_handle(cx);
 141
 142        let collapse_expand_button = if self.needs_expand_collapse_option(cx) {
 143            let query_editor_focus = self.query_editor.focus_handle(cx);
 144
 145            let (icon, label, tooltip_label) = if self.is_collapsed {
 146                (
 147                    IconName::ChevronUpDown,
 148                    "Expand All",
 149                    "Expand All Search Results",
 150                )
 151            } else {
 152                (
 153                    IconName::ChevronDownUp,
 154                    "Collapse All",
 155                    "Collapse All Search Results",
 156                )
 157            };
 158
 159            if self.dismissed {
 160                let button = Button::new("multibuffer-collapse-expand-empty", label)
 161                    .icon_position(IconPosition::Start)
 162                    .icon(icon)
 163                    .tooltip(move |_, cx| {
 164                        Tooltip::for_action_in(
 165                            tooltip_label,
 166                            &ToggleFoldAll,
 167                            &query_editor_focus.clone(),
 168                            cx,
 169                        )
 170                    })
 171                    .on_click(|_event, window, cx| {
 172                        window.dispatch_action(ToggleFoldAll.boxed_clone(), cx)
 173                    })
 174                    .into_any_element();
 175
 176                return button;
 177            }
 178
 179            Some(
 180                IconButton::new("multibuffer-collapse-expand", icon)
 181                    .shape(IconButtonShape::Square)
 182                    .tooltip(move |_, cx| {
 183                        Tooltip::for_action_in(
 184                            tooltip_label,
 185                            &ToggleFoldAll,
 186                            &query_editor_focus,
 187                            cx,
 188                        )
 189                    })
 190                    .on_click(|_event, window, cx| {
 191                        window.dispatch_action(ToggleFoldAll.boxed_clone(), cx)
 192                    })
 193                    .into_any_element(),
 194            )
 195        } else {
 196            None
 197        };
 198
 199        let narrow_mode =
 200            self.scroll_handle.bounds().size.width / window.rem_size() < 340. / BASE_REM_SIZE_IN_PX;
 201        let hide_inline_icons = self.editor_needed_width
 202            > self.editor_scroll_handle.bounds().size.width - window.rem_size() * 6.;
 203
 204        let workspace::searchable::SearchOptions {
 205            case,
 206            word,
 207            regex,
 208            replacement,
 209            selection,
 210            find_in_results,
 211        } = self.supported_options(cx);
 212
 213        self.query_editor.update(cx, |query_editor, cx| {
 214            if query_editor.placeholder_text(cx).is_none() {
 215                query_editor.set_placeholder_text("Search…", window, cx);
 216            }
 217        });
 218
 219        self.replacement_editor.update(cx, |editor, cx| {
 220            editor.set_placeholder_text("Replace with…", window, cx);
 221        });
 222
 223        let mut color_override = None;
 224        let match_text = self
 225            .active_searchable_item
 226            .as_ref()
 227            .and_then(|searchable_item| {
 228                if self.query(cx).is_empty() {
 229                    return None;
 230                }
 231                let matches_count = self
 232                    .searchable_items_with_matches
 233                    .get(&searchable_item.downgrade())
 234                    .map(AnyVec::len)
 235                    .unwrap_or(0);
 236                if let Some(match_ix) = self.active_match_index {
 237                    Some(format!("{}/{}", match_ix + 1, matches_count))
 238                } else {
 239                    color_override = Some(Color::Error); // No matches found
 240                    None
 241                }
 242            })
 243            .unwrap_or_else(|| "0/0".to_string());
 244        let should_show_replace_input = self.replace_enabled && replacement;
 245        let in_replace = self.replacement_editor.focus_handle(cx).is_focused(window);
 246
 247        let theme_colors = cx.theme().colors();
 248        let query_border = if self.query_error.is_some() {
 249            Color::Error.color(cx)
 250        } else {
 251            theme_colors.border
 252        };
 253        let replacement_border = theme_colors.border;
 254
 255        let container_width = window.viewport_size().width;
 256        let input_width = SearchInputWidth::calc_width(container_width);
 257
 258        let input_base_styles =
 259            |border_color| input_base_styles(border_color, |div| div.w(input_width));
 260
 261        let input_style = if find_in_results {
 262            filter_search_results_input(query_border, |div| div.w(input_width), cx)
 263        } else {
 264            input_base_styles(query_border)
 265        };
 266
 267        let query_column = input_style
 268            .id("editor-scroll")
 269            .track_scroll(&self.editor_scroll_handle)
 270            .child(render_text_input(&self.query_editor, color_override, cx))
 271            .when(!hide_inline_icons, |div| {
 272                div.child(
 273                    h_flex()
 274                        .gap_1()
 275                        .when(case, |div| {
 276                            div.child(SearchOption::CaseSensitive.as_button(
 277                                self.search_options,
 278                                SearchSource::Buffer,
 279                                focus_handle.clone(),
 280                            ))
 281                        })
 282                        .when(word, |div| {
 283                            div.child(SearchOption::WholeWord.as_button(
 284                                self.search_options,
 285                                SearchSource::Buffer,
 286                                focus_handle.clone(),
 287                            ))
 288                        })
 289                        .when(regex, |div| {
 290                            div.child(SearchOption::Regex.as_button(
 291                                self.search_options,
 292                                SearchSource::Buffer,
 293                                focus_handle.clone(),
 294                            ))
 295                        }),
 296                )
 297            });
 298
 299        let mode_column = h_flex()
 300            .gap_1()
 301            .min_w_64()
 302            .when(replacement, |this| {
 303                this.child(render_action_button(
 304                    "buffer-search-bar-toggle",
 305                    IconName::Replace,
 306                    self.replace_enabled.then_some(ActionButtonState::Toggled),
 307                    "Toggle Replace",
 308                    &ToggleReplace,
 309                    focus_handle.clone(),
 310                ))
 311            })
 312            .when(selection, |this| {
 313                this.child(
 314                    IconButton::new(
 315                        "buffer-search-bar-toggle-search-selection-button",
 316                        IconName::Quote,
 317                    )
 318                    .style(ButtonStyle::Subtle)
 319                    .shape(IconButtonShape::Square)
 320                    .when(self.selection_search_enabled.is_some(), |button| {
 321                        button.style(ButtonStyle::Filled)
 322                    })
 323                    .on_click(cx.listener(|this, _: &ClickEvent, window, cx| {
 324                        this.toggle_selection(&ToggleSelection, window, cx);
 325                    }))
 326                    .toggle_state(self.selection_search_enabled.is_some())
 327                    .tooltip({
 328                        let focus_handle = focus_handle.clone();
 329                        move |_window, cx| {
 330                            Tooltip::for_action_in(
 331                                "Toggle Search Selection",
 332                                &ToggleSelection,
 333                                &focus_handle,
 334                                cx,
 335                            )
 336                        }
 337                    }),
 338                )
 339            })
 340            .when(!find_in_results, |el| {
 341                let query_focus = self.query_editor.focus_handle(cx);
 342                let matches_column = h_flex()
 343                    .pl_2()
 344                    .ml_2()
 345                    .border_l_1()
 346                    .border_color(theme_colors.border_variant)
 347                    .child(render_action_button(
 348                        "buffer-search-nav-button",
 349                        ui::IconName::ChevronLeft,
 350                        self.active_match_index
 351                            .is_none()
 352                            .then_some(ActionButtonState::Disabled),
 353                        "Select Previous Match",
 354                        &SelectPreviousMatch,
 355                        query_focus.clone(),
 356                    ))
 357                    .child(render_action_button(
 358                        "buffer-search-nav-button",
 359                        ui::IconName::ChevronRight,
 360                        self.active_match_index
 361                            .is_none()
 362                            .then_some(ActionButtonState::Disabled),
 363                        "Select Next Match",
 364                        &SelectNextMatch,
 365                        query_focus.clone(),
 366                    ))
 367                    .when(!narrow_mode, |this| {
 368                        this.child(div().ml_2().min_w(rems_from_px(40.)).child(
 369                            Label::new(match_text).size(LabelSize::Small).color(
 370                                if self.active_match_index.is_some() {
 371                                    Color::Default
 372                                } else {
 373                                    Color::Disabled
 374                                },
 375                            ),
 376                        ))
 377                    });
 378
 379                el.child(render_action_button(
 380                    "buffer-search-nav-button",
 381                    IconName::SelectAll,
 382                    Default::default(),
 383                    "Select All Matches",
 384                    &SelectAllMatches,
 385                    query_focus,
 386                ))
 387                .child(matches_column)
 388            })
 389            .when(find_in_results, |el| {
 390                el.child(render_action_button(
 391                    "buffer-search",
 392                    IconName::Close,
 393                    Default::default(),
 394                    "Close Search Bar",
 395                    &Dismiss,
 396                    focus_handle.clone(),
 397                ))
 398            });
 399
 400        let has_collapse_button = collapse_expand_button.is_some();
 401
 402        let search_line = h_flex()
 403            .w_full()
 404            .gap_2()
 405            .when(find_in_results, |el| el.child(alignment_element()))
 406            .when(!find_in_results && has_collapse_button, |el| {
 407                el.pl_0p5().child(collapse_expand_button.expect("button"))
 408            })
 409            .child(query_column)
 410            .child(mode_column);
 411
 412        let replace_line =
 413            should_show_replace_input.then(|| {
 414                let replace_column = input_base_styles(replacement_border)
 415                    .child(render_text_input(&self.replacement_editor, None, cx));
 416                let focus_handle = self.replacement_editor.read(cx).focus_handle(cx);
 417
 418                let replace_actions = h_flex()
 419                    .min_w_64()
 420                    .gap_1()
 421                    .child(render_action_button(
 422                        "buffer-search-replace-button",
 423                        IconName::ReplaceNext,
 424                        Default::default(),
 425                        "Replace Next Match",
 426                        &ReplaceNext,
 427                        focus_handle.clone(),
 428                    ))
 429                    .child(render_action_button(
 430                        "buffer-search-replace-button",
 431                        IconName::ReplaceAll,
 432                        Default::default(),
 433                        "Replace All Matches",
 434                        &ReplaceAll,
 435                        focus_handle,
 436                    ));
 437
 438                h_flex()
 439                    .w_full()
 440                    .gap_2()
 441                    .when(has_collapse_button, |this| this.child(alignment_element()))
 442                    .child(replace_column)
 443                    .child(replace_actions)
 444            });
 445
 446        let mut key_context = KeyContext::new_with_defaults();
 447        key_context.add("BufferSearchBar");
 448        if in_replace {
 449            key_context.add("in_replace");
 450        }
 451
 452        let query_error_line = self.query_error.as_ref().map(|error| {
 453            Label::new(error)
 454                .size(LabelSize::Small)
 455                .color(Color::Error)
 456                .mt_neg_1()
 457                .ml_2()
 458        });
 459
 460        let search_line =
 461            h_flex()
 462                .relative()
 463                .child(search_line)
 464                .when(!narrow_mode && !find_in_results, |this| {
 465                    this.child(
 466                        h_flex()
 467                            .absolute()
 468                            .right_0()
 469                            .when(has_collapse_button, |this| {
 470                                this.pr_2()
 471                                    .border_r_1()
 472                                    .border_color(cx.theme().colors().border_variant)
 473                            })
 474                            .child(render_action_button(
 475                                "buffer-search",
 476                                IconName::Close,
 477                                Default::default(),
 478                                "Close Search Bar",
 479                                &Dismiss,
 480                                focus_handle.clone(),
 481                            )),
 482                    )
 483                });
 484
 485        v_flex()
 486            .id("buffer_search")
 487            .gap_2()
 488            .w_full()
 489            .track_scroll(&self.scroll_handle)
 490            .key_context(key_context)
 491            .capture_action(cx.listener(Self::tab))
 492            .capture_action(cx.listener(Self::backtab))
 493            .capture_action(cx.listener(Self::toggle_fold_all))
 494            .on_action(cx.listener(Self::previous_history_query))
 495            .on_action(cx.listener(Self::next_history_query))
 496            .on_action(cx.listener(Self::dismiss))
 497            .on_action(cx.listener(Self::select_next_match))
 498            .on_action(cx.listener(Self::select_prev_match))
 499            .on_action(cx.listener(|this, _: &ToggleOutline, window, cx| {
 500                if let Some(active_searchable_item) = &mut this.active_searchable_item {
 501                    active_searchable_item.relay_action(Box::new(ToggleOutline), window, cx);
 502                }
 503            }))
 504            .on_action(cx.listener(|this, _: &CopyPath, window, cx| {
 505                if let Some(active_searchable_item) = &mut this.active_searchable_item {
 506                    active_searchable_item.relay_action(Box::new(CopyPath), window, cx);
 507                }
 508            }))
 509            .on_action(cx.listener(|this, _: &CopyRelativePath, window, cx| {
 510                if let Some(active_searchable_item) = &mut this.active_searchable_item {
 511                    active_searchable_item.relay_action(Box::new(CopyRelativePath), window, cx);
 512                }
 513            }))
 514            .when(replacement, |this| {
 515                this.on_action(cx.listener(Self::toggle_replace))
 516                    .on_action(cx.listener(Self::replace_next))
 517                    .on_action(cx.listener(Self::replace_all))
 518            })
 519            .when(case, |this| {
 520                this.on_action(cx.listener(Self::toggle_case_sensitive))
 521            })
 522            .when(word, |this| {
 523                this.on_action(cx.listener(Self::toggle_whole_word))
 524            })
 525            .when(regex, |this| {
 526                this.on_action(cx.listener(Self::toggle_regex))
 527            })
 528            .when(selection, |this| {
 529                this.on_action(cx.listener(Self::toggle_selection))
 530            })
 531            .child(search_line)
 532            .children(query_error_line)
 533            .children(replace_line)
 534            .into_any_element()
 535    }
 536}
 537
 538impl Focusable for BufferSearchBar {
 539    fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
 540        self.query_editor.focus_handle(cx)
 541    }
 542}
 543
 544impl ToolbarItemView for BufferSearchBar {
 545    fn contribute_context(&self, context: &mut KeyContext, _cx: &App) {
 546        if !self.dismissed {
 547            context.add("buffer_search_deployed");
 548        }
 549    }
 550
 551    fn set_active_pane_item(
 552        &mut self,
 553        item: Option<&dyn ItemHandle>,
 554        window: &mut Window,
 555        cx: &mut Context<Self>,
 556    ) -> ToolbarItemLocation {
 557        cx.notify();
 558        self.active_searchable_item_subscriptions.take();
 559        self.active_searchable_item.take();
 560
 561        self.pending_search.take();
 562
 563        if let Some(searchable_item_handle) =
 564            item.and_then(|item| item.to_searchable_item_handle(cx))
 565        {
 566            let this = cx.entity().downgrade();
 567
 568            let search_event_subscription = searchable_item_handle.subscribe_to_search_events(
 569                window,
 570                cx,
 571                Box::new(move |search_event, window, cx| {
 572                    if let Some(this) = this.upgrade() {
 573                        this.update(cx, |this, cx| {
 574                            this.on_active_searchable_item_event(search_event, window, cx)
 575                        });
 576                    }
 577                }),
 578            );
 579
 580            #[cfg(target_os = "macos")]
 581            {
 582                let item_focus_handle = searchable_item_handle.item_focus_handle(cx);
 583
 584                self.active_searchable_item_subscriptions = Some([
 585                    search_event_subscription,
 586                    cx.on_focus(&item_focus_handle, window, |this, window, cx| {
 587                        if this.query_editor_focused || this.replacement_editor_focused {
 588                            // no need to read pasteboard since focus came from toolbar
 589                            return;
 590                        }
 591
 592                        cx.defer_in(window, |this, window, cx| {
 593                            let Some(item) = cx.read_from_find_pasteboard() else {
 594                                return;
 595                            };
 596                            let Some(text) = item.text() else {
 597                                return;
 598                            };
 599
 600                            if this.query(cx) == text {
 601                                return;
 602                            }
 603
 604                            let search_options = item
 605                                .metadata()
 606                                .and_then(|m| m.parse().ok())
 607                                .and_then(SearchOptions::from_bits)
 608                                .unwrap_or(this.search_options);
 609
 610                            if this.dismissed {
 611                                this.pending_external_query = Some((text, search_options));
 612                            } else {
 613                                drop(this.search(&text, Some(search_options), true, window, cx));
 614                            }
 615                        });
 616                    }),
 617                ]);
 618            }
 619            #[cfg(not(target_os = "macos"))]
 620            {
 621                self.active_searchable_item_subscriptions = Some(search_event_subscription);
 622            }
 623
 624            let is_project_search = searchable_item_handle.supported_options(cx).find_in_results;
 625            self.active_searchable_item = Some(searchable_item_handle);
 626            drop(self.update_matches(true, false, window, cx));
 627            if self.needs_expand_collapse_option(cx) {
 628                return ToolbarItemLocation::PrimaryLeft;
 629            } else if !self.is_dismissed() {
 630                if is_project_search {
 631                    self.dismiss(&Default::default(), window, cx);
 632                } else {
 633                    return ToolbarItemLocation::Secondary;
 634                }
 635            }
 636        }
 637        ToolbarItemLocation::Hidden
 638    }
 639}
 640
 641impl BufferSearchBar {
 642    pub fn query_editor_focused(&self) -> bool {
 643        self.query_editor_focused
 644    }
 645
 646    pub fn register(registrar: &mut impl SearchActionsRegistrar) {
 647        registrar.register_handler(ForDeployed(|this, _: &FocusSearch, window, cx| {
 648            this.query_editor.focus_handle(cx).focus(window, cx);
 649            this.select_query(window, cx);
 650        }));
 651        registrar.register_handler(ForDeployed(
 652            |this, action: &ToggleCaseSensitive, window, cx| {
 653                if this.supported_options(cx).case {
 654                    this.toggle_case_sensitive(action, window, cx);
 655                }
 656            },
 657        ));
 658        registrar.register_handler(ForDeployed(|this, action: &ToggleWholeWord, window, cx| {
 659            if this.supported_options(cx).word {
 660                this.toggle_whole_word(action, window, cx);
 661            }
 662        }));
 663        registrar.register_handler(ForDeployed(|this, action: &ToggleRegex, window, cx| {
 664            if this.supported_options(cx).regex {
 665                this.toggle_regex(action, window, cx);
 666            }
 667        }));
 668        registrar.register_handler(ForDeployed(|this, action: &ToggleSelection, window, cx| {
 669            if this.supported_options(cx).selection {
 670                this.toggle_selection(action, window, cx);
 671            } else {
 672                cx.propagate();
 673            }
 674        }));
 675        registrar.register_handler(ForDeployed(|this, action: &ToggleReplace, window, cx| {
 676            if this.supported_options(cx).replacement {
 677                this.toggle_replace(action, window, cx);
 678            } else {
 679                cx.propagate();
 680            }
 681        }));
 682        registrar.register_handler(WithResultsOrExternalQuery(
 683            |this, action: &SelectNextMatch, window, cx| {
 684                if this.supported_options(cx).find_in_results {
 685                    cx.propagate();
 686                } else {
 687                    this.select_next_match(action, window, cx);
 688                }
 689            },
 690        ));
 691        registrar.register_handler(WithResultsOrExternalQuery(
 692            |this, action: &SelectPreviousMatch, window, cx| {
 693                if this.supported_options(cx).find_in_results {
 694                    cx.propagate();
 695                } else {
 696                    this.select_prev_match(action, window, cx);
 697                }
 698            },
 699        ));
 700        registrar.register_handler(WithResultsOrExternalQuery(
 701            |this, action: &SelectAllMatches, window, cx| {
 702                if this.supported_options(cx).find_in_results {
 703                    cx.propagate();
 704                } else {
 705                    this.select_all_matches(action, window, cx);
 706                }
 707            },
 708        ));
 709        registrar.register_handler(ForDeployed(
 710            |this, _: &editor::actions::Cancel, window, cx| {
 711                this.dismiss(&Dismiss, window, cx);
 712            },
 713        ));
 714        registrar.register_handler(ForDeployed(|this, _: &Dismiss, window, cx| {
 715            this.dismiss(&Dismiss, window, cx);
 716        }));
 717
 718        // register deploy buffer search for both search bar states, since we want to focus into the search bar
 719        // when the deploy action is triggered in the buffer.
 720        registrar.register_handler(ForDeployed(|this, deploy, window, cx| {
 721            this.deploy(deploy, window, cx);
 722        }));
 723        registrar.register_handler(ForDismissed(|this, deploy, window, cx| {
 724            this.deploy(deploy, window, cx);
 725        }));
 726        registrar.register_handler(ForDeployed(|this, _: &DeployReplace, window, cx| {
 727            if this.supported_options(cx).find_in_results {
 728                cx.propagate();
 729            } else {
 730                this.deploy(&Deploy::replace(), window, cx);
 731            }
 732        }));
 733        registrar.register_handler(ForDismissed(|this, _: &DeployReplace, window, cx| {
 734            if this.supported_options(cx).find_in_results {
 735                cx.propagate();
 736            } else {
 737                this.deploy(&Deploy::replace(), window, cx);
 738            }
 739        }));
 740    }
 741
 742    pub fn new(
 743        languages: Option<Arc<LanguageRegistry>>,
 744        window: &mut Window,
 745        cx: &mut Context<Self>,
 746    ) -> Self {
 747        let query_editor = cx.new(|cx| {
 748            let mut editor = Editor::single_line(window, cx);
 749            editor.set_use_autoclose(false);
 750            editor
 751        });
 752        cx.subscribe_in(&query_editor, window, Self::on_query_editor_event)
 753            .detach();
 754        let replacement_editor = cx.new(|cx| Editor::single_line(window, cx));
 755        cx.subscribe(&replacement_editor, Self::on_replacement_editor_event)
 756            .detach();
 757
 758        let search_options = SearchOptions::from_settings(&EditorSettings::get_global(cx).search);
 759        if let Some(languages) = languages {
 760            let query_buffer = query_editor
 761                .read(cx)
 762                .buffer()
 763                .read(cx)
 764                .as_singleton()
 765                .expect("query editor should be backed by a singleton buffer");
 766
 767            query_buffer
 768                .read(cx)
 769                .set_language_registry(languages.clone());
 770
 771            cx.spawn(async move |buffer_search_bar, cx| {
 772                use anyhow::Context as _;
 773
 774                let regex_language = languages
 775                    .language_for_name("regex")
 776                    .await
 777                    .context("loading regex language")?;
 778
 779                buffer_search_bar
 780                    .update(cx, |buffer_search_bar, cx| {
 781                        buffer_search_bar.regex_language = Some(regex_language);
 782                        buffer_search_bar.adjust_query_regex_language(cx);
 783                    })
 784                    .ok();
 785                anyhow::Ok(())
 786            })
 787            .detach_and_log_err(cx);
 788        }
 789
 790        Self {
 791            query_editor,
 792            query_editor_focused: false,
 793            replacement_editor,
 794            replacement_editor_focused: false,
 795            active_searchable_item: None,
 796            active_searchable_item_subscriptions: None,
 797            #[cfg(target_os = "macos")]
 798            pending_external_query: None,
 799            active_match_index: None,
 800            searchable_items_with_matches: Default::default(),
 801            default_options: search_options,
 802            configured_options: search_options,
 803            search_options,
 804            pending_search: None,
 805            query_error: None,
 806            dismissed: true,
 807            search_history: SearchHistory::new(
 808                Some(MAX_BUFFER_SEARCH_HISTORY_SIZE),
 809                project::search_history::QueryInsertionBehavior::ReplacePreviousIfContains,
 810            ),
 811            search_history_cursor: Default::default(),
 812            active_search: None,
 813            replace_enabled: false,
 814            selection_search_enabled: None,
 815            scroll_handle: ScrollHandle::new(),
 816            editor_scroll_handle: ScrollHandle::new(),
 817            editor_needed_width: px(0.),
 818            regex_language: None,
 819            is_collapsed: false,
 820        }
 821    }
 822
 823    pub fn is_dismissed(&self) -> bool {
 824        self.dismissed
 825    }
 826
 827    pub fn dismiss(&mut self, _: &Dismiss, window: &mut Window, cx: &mut Context<Self>) {
 828        self.dismissed = true;
 829        self.query_error = None;
 830        self.sync_select_next_case_sensitivity(cx);
 831
 832        for searchable_item in self.searchable_items_with_matches.keys() {
 833            if let Some(searchable_item) =
 834                WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
 835            {
 836                searchable_item.clear_matches(window, cx);
 837            }
 838        }
 839
 840        let needs_collapse_expand = self.needs_expand_collapse_option(cx);
 841
 842        if let Some(active_editor) = self.active_searchable_item.as_mut() {
 843            self.selection_search_enabled = None;
 844            self.replace_enabled = false;
 845            active_editor.search_bar_visibility_changed(false, window, cx);
 846            active_editor.toggle_filtered_search_ranges(None, window, cx);
 847            let handle = active_editor.item_focus_handle(cx);
 848            self.focus(&handle, window, cx);
 849        }
 850
 851        if needs_collapse_expand {
 852            cx.emit(Event::UpdateLocation);
 853            cx.emit(ToolbarItemEvent::ChangeLocation(
 854                ToolbarItemLocation::PrimaryLeft,
 855            ));
 856            cx.notify();
 857            return;
 858        }
 859        cx.emit(Event::UpdateLocation);
 860        cx.emit(ToolbarItemEvent::ChangeLocation(
 861            ToolbarItemLocation::Hidden,
 862        ));
 863        cx.notify();
 864    }
 865
 866    pub fn deploy(&mut self, deploy: &Deploy, window: &mut Window, cx: &mut Context<Self>) -> bool {
 867        let filtered_search_range = if deploy.selection_search_enabled {
 868            Some(FilteredSearchRange::Default)
 869        } else {
 870            None
 871        };
 872        if self.show(window, cx) {
 873            if let Some(active_item) = self.active_searchable_item.as_mut() {
 874                active_item.toggle_filtered_search_ranges(filtered_search_range, window, cx);
 875            }
 876            self.search_suggested(window, cx);
 877            self.smartcase(window, cx);
 878            self.sync_select_next_case_sensitivity(cx);
 879            self.replace_enabled |= deploy.replace_enabled;
 880            self.selection_search_enabled =
 881                self.selection_search_enabled
 882                    .or(if deploy.selection_search_enabled {
 883                        Some(FilteredSearchRange::Default)
 884                    } else {
 885                        None
 886                    });
 887            if deploy.focus {
 888                let mut handle = self.query_editor.focus_handle(cx);
 889                let mut select_query = true;
 890                if deploy.replace_enabled && handle.is_focused(window) {
 891                    handle = self.replacement_editor.focus_handle(cx);
 892                    select_query = false;
 893                };
 894
 895                if select_query {
 896                    self.select_query(window, cx);
 897                }
 898
 899                window.focus(&handle, cx);
 900            }
 901            return true;
 902        }
 903
 904        cx.propagate();
 905        false
 906    }
 907
 908    pub fn toggle(&mut self, action: &Deploy, window: &mut Window, cx: &mut Context<Self>) {
 909        if self.is_dismissed() {
 910            self.deploy(action, window, cx);
 911        } else {
 912            self.dismiss(&Dismiss, window, cx);
 913        }
 914    }
 915
 916    pub fn show(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
 917        let Some(handle) = self.active_searchable_item.as_ref() else {
 918            return false;
 919        };
 920
 921        let configured_options =
 922            SearchOptions::from_settings(&EditorSettings::get_global(cx).search);
 923        let settings_changed = configured_options != self.configured_options;
 924
 925        if self.dismissed && settings_changed {
 926            // Only update configuration options when search bar is dismissed,
 927            // so we don't miss updates even after calling show twice
 928            self.configured_options = configured_options;
 929            self.search_options = configured_options;
 930            self.default_options = configured_options;
 931        }
 932
 933        // This isn't a normal setting; it's only applicable to vim search.
 934        self.search_options.remove(SearchOptions::BACKWARDS);
 935
 936        self.dismissed = false;
 937        self.adjust_query_regex_language(cx);
 938        handle.search_bar_visibility_changed(true, window, cx);
 939        cx.notify();
 940        cx.emit(Event::UpdateLocation);
 941        cx.emit(ToolbarItemEvent::ChangeLocation(
 942            if self.needs_expand_collapse_option(cx) {
 943                ToolbarItemLocation::PrimaryLeft
 944            } else {
 945                ToolbarItemLocation::Secondary
 946            },
 947        ));
 948        true
 949    }
 950
 951    fn supported_options(&self, cx: &mut Context<Self>) -> workspace::searchable::SearchOptions {
 952        self.active_searchable_item
 953            .as_ref()
 954            .map(|item| item.supported_options(cx))
 955            .unwrap_or_default()
 956    }
 957
 958    // We provide an expand/collapse button if we are in a multibuffer
 959    // and not doing a project search.
 960    fn needs_expand_collapse_option(&self, cx: &App) -> bool {
 961        if let Some(item) = &self.active_searchable_item {
 962            let buffer_kind = item.buffer_kind(cx);
 963
 964            if buffer_kind == ItemBufferKind::Singleton {
 965                return false;
 966            }
 967
 968            let workspace::searchable::SearchOptions {
 969                find_in_results, ..
 970            } = item.supported_options(cx);
 971            !find_in_results
 972        } else {
 973            false
 974        }
 975    }
 976
 977    fn toggle_fold_all(&mut self, _: &ToggleFoldAll, window: &mut Window, cx: &mut Context<Self>) {
 978        self.toggle_fold_all_in_item(window, cx);
 979    }
 980
 981    fn toggle_fold_all_in_item(&self, window: &mut Window, cx: &mut Context<Self>) {
 982        let is_collapsed = self.is_collapsed;
 983        if let Some(item) = &self.active_searchable_item {
 984            if let Some(item) = item.act_as_type(TypeId::of::<Editor>(), cx) {
 985                let editor = item.downcast::<Editor>().expect("Is an editor");
 986                editor.update(cx, |editor, cx| {
 987                    if is_collapsed {
 988                        editor.unfold_all(&UnfoldAll, window, cx);
 989                    } else {
 990                        editor.fold_all(&FoldAll, window, cx);
 991                    }
 992                })
 993            }
 994        }
 995    }
 996
 997    pub fn search_suggested(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 998        let search = self.query_suggestion(window, cx).map(|suggestion| {
 999            self.search(&suggestion, Some(self.default_options), true, window, cx)
1000        });
1001
1002        #[cfg(target_os = "macos")]
1003        let search = search.or_else(|| {
1004            self.pending_external_query
1005                .take()
1006                .map(|(query, options)| self.search(&query, Some(options), true, window, cx))
1007        });
1008
1009        if let Some(search) = search {
1010            cx.spawn_in(window, async move |this, cx| {
1011                if search.await.is_ok() {
1012                    this.update_in(cx, |this, window, cx| {
1013                        if !this.dismissed {
1014                            this.activate_current_match(window, cx)
1015                        }
1016                    })
1017                } else {
1018                    Ok(())
1019                }
1020            })
1021            .detach_and_log_err(cx);
1022        }
1023    }
1024
1025    pub fn activate_current_match(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1026        if let Some(match_ix) = self.active_match_index
1027            && let Some(active_searchable_item) = self.active_searchable_item.as_ref()
1028            && let Some(matches) = self
1029                .searchable_items_with_matches
1030                .get(&active_searchable_item.downgrade())
1031        {
1032            active_searchable_item.activate_match(match_ix, matches, window, cx)
1033        }
1034    }
1035
1036    pub fn select_query(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1037        self.query_editor.update(cx, |query_editor, cx| {
1038            query_editor.select_all(&Default::default(), window, cx);
1039        });
1040    }
1041
1042    pub fn query(&self, cx: &App) -> String {
1043        self.query_editor.read(cx).text(cx)
1044    }
1045
1046    pub fn replacement(&self, cx: &mut App) -> String {
1047        self.replacement_editor.read(cx).text(cx)
1048    }
1049
1050    pub fn query_suggestion(
1051        &mut self,
1052        window: &mut Window,
1053        cx: &mut Context<Self>,
1054    ) -> Option<String> {
1055        self.active_searchable_item
1056            .as_ref()
1057            .map(|searchable_item| searchable_item.query_suggestion(window, cx))
1058            .filter(|suggestion| !suggestion.is_empty())
1059    }
1060
1061    pub fn set_replacement(&mut self, replacement: Option<&str>, cx: &mut Context<Self>) {
1062        if replacement.is_none() {
1063            self.replace_enabled = false;
1064            return;
1065        }
1066        self.replace_enabled = true;
1067        self.replacement_editor
1068            .update(cx, |replacement_editor, cx| {
1069                replacement_editor
1070                    .buffer()
1071                    .update(cx, |replacement_buffer, cx| {
1072                        let len = replacement_buffer.len(cx);
1073                        replacement_buffer.edit(
1074                            [(MultiBufferOffset(0)..len, replacement.unwrap())],
1075                            None,
1076                            cx,
1077                        );
1078                    });
1079            });
1080    }
1081
1082    pub fn focus_replace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1083        self.focus(&self.replacement_editor.focus_handle(cx), window, cx);
1084        cx.notify();
1085    }
1086
1087    pub fn search(
1088        &mut self,
1089        query: &str,
1090        options: Option<SearchOptions>,
1091        add_to_history: bool,
1092        window: &mut Window,
1093        cx: &mut Context<Self>,
1094    ) -> oneshot::Receiver<()> {
1095        let options = options.unwrap_or(self.default_options);
1096        let updated = query != self.query(cx) || self.search_options != options;
1097        if updated {
1098            self.query_editor.update(cx, |query_editor, cx| {
1099                query_editor.buffer().update(cx, |query_buffer, cx| {
1100                    let len = query_buffer.len(cx);
1101                    query_buffer.edit([(MultiBufferOffset(0)..len, query)], None, cx);
1102                });
1103            });
1104            self.set_search_options(options, cx);
1105            self.clear_matches(window, cx);
1106            #[cfg(target_os = "macos")]
1107            self.update_find_pasteboard(cx);
1108            cx.notify();
1109        }
1110        self.update_matches(!updated, add_to_history, window, cx)
1111    }
1112
1113    #[cfg(target_os = "macos")]
1114    pub fn update_find_pasteboard(&mut self, cx: &mut App) {
1115        cx.write_to_find_pasteboard(gpui::ClipboardItem::new_string_with_metadata(
1116            self.query(cx),
1117            self.search_options.bits().to_string(),
1118        ));
1119    }
1120
1121    pub fn focus_editor(&mut self, _: &FocusEditor, window: &mut Window, cx: &mut Context<Self>) {
1122        if let Some(active_editor) = self.active_searchable_item.as_ref() {
1123            let handle = active_editor.item_focus_handle(cx);
1124            window.focus(&handle, cx);
1125        }
1126    }
1127
1128    pub fn toggle_search_option(
1129        &mut self,
1130        search_option: SearchOptions,
1131        window: &mut Window,
1132        cx: &mut Context<Self>,
1133    ) {
1134        self.search_options.toggle(search_option);
1135        self.default_options = self.search_options;
1136        drop(self.update_matches(false, false, window, cx));
1137        self.adjust_query_regex_language(cx);
1138        self.sync_select_next_case_sensitivity(cx);
1139        cx.notify();
1140    }
1141
1142    pub fn has_search_option(&mut self, search_option: SearchOptions) -> bool {
1143        self.search_options.contains(search_option)
1144    }
1145
1146    pub fn enable_search_option(
1147        &mut self,
1148        search_option: SearchOptions,
1149        window: &mut Window,
1150        cx: &mut Context<Self>,
1151    ) {
1152        if !self.search_options.contains(search_option) {
1153            self.toggle_search_option(search_option, window, cx)
1154        }
1155    }
1156
1157    pub fn set_search_within_selection(
1158        &mut self,
1159        search_within_selection: Option<FilteredSearchRange>,
1160        window: &mut Window,
1161        cx: &mut Context<Self>,
1162    ) -> Option<oneshot::Receiver<()>> {
1163        let active_item = self.active_searchable_item.as_mut()?;
1164        self.selection_search_enabled = search_within_selection;
1165        active_item.toggle_filtered_search_ranges(self.selection_search_enabled, window, cx);
1166        cx.notify();
1167        Some(self.update_matches(false, false, window, cx))
1168    }
1169
1170    pub fn set_search_options(&mut self, search_options: SearchOptions, cx: &mut Context<Self>) {
1171        self.search_options = search_options;
1172        self.adjust_query_regex_language(cx);
1173        self.sync_select_next_case_sensitivity(cx);
1174        cx.notify();
1175    }
1176
1177    pub fn clear_search_within_ranges(
1178        &mut self,
1179        search_options: SearchOptions,
1180        cx: &mut Context<Self>,
1181    ) {
1182        self.search_options = search_options;
1183        self.adjust_query_regex_language(cx);
1184        cx.notify();
1185    }
1186
1187    fn select_next_match(
1188        &mut self,
1189        _: &SelectNextMatch,
1190        window: &mut Window,
1191        cx: &mut Context<Self>,
1192    ) {
1193        self.select_match(Direction::Next, 1, window, cx);
1194    }
1195
1196    fn select_prev_match(
1197        &mut self,
1198        _: &SelectPreviousMatch,
1199        window: &mut Window,
1200        cx: &mut Context<Self>,
1201    ) {
1202        self.select_match(Direction::Prev, 1, window, cx);
1203    }
1204
1205    pub fn select_all_matches(
1206        &mut self,
1207        _: &SelectAllMatches,
1208        window: &mut Window,
1209        cx: &mut Context<Self>,
1210    ) {
1211        if !self.dismissed
1212            && self.active_match_index.is_some()
1213            && let Some(searchable_item) = self.active_searchable_item.as_ref()
1214            && let Some(matches) = self
1215                .searchable_items_with_matches
1216                .get(&searchable_item.downgrade())
1217        {
1218            searchable_item.select_matches(matches, window, cx);
1219            self.focus_editor(&FocusEditor, window, cx);
1220        }
1221    }
1222
1223    pub fn select_match(
1224        &mut self,
1225        direction: Direction,
1226        count: usize,
1227        window: &mut Window,
1228        cx: &mut Context<Self>,
1229    ) {
1230        #[cfg(target_os = "macos")]
1231        if let Some((query, options)) = self.pending_external_query.take() {
1232            let search_rx = self.search(&query, Some(options), true, window, cx);
1233            cx.spawn_in(window, async move |this, cx| {
1234                if search_rx.await.is_ok() {
1235                    this.update_in(cx, |this, window, cx| {
1236                        this.activate_current_match(window, cx);
1237                    })
1238                    .ok();
1239                }
1240            })
1241            .detach();
1242
1243            return;
1244        }
1245
1246        if let Some(index) = self.active_match_index
1247            && let Some(searchable_item) = self.active_searchable_item.as_ref()
1248            && let Some(matches) = self
1249                .searchable_items_with_matches
1250                .get(&searchable_item.downgrade())
1251                .filter(|matches| !matches.is_empty())
1252        {
1253            // If 'wrapscan' is disabled, searches do not wrap around the end of the file.
1254            if !EditorSettings::get_global(cx).search_wrap
1255                && ((direction == Direction::Next && index + count >= matches.len())
1256                    || (direction == Direction::Prev && index < count))
1257            {
1258                crate::show_no_more_matches(window, cx);
1259                return;
1260            }
1261            let new_match_index = searchable_item
1262                .match_index_for_direction(matches, index, direction, count, window, cx);
1263
1264            searchable_item.update_matches(matches, Some(new_match_index), window, cx);
1265            searchable_item.activate_match(new_match_index, matches, window, cx);
1266        }
1267    }
1268
1269    pub fn select_first_match(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1270        if let Some(searchable_item) = self.active_searchable_item.as_ref()
1271            && let Some(matches) = self
1272                .searchable_items_with_matches
1273                .get(&searchable_item.downgrade())
1274        {
1275            if matches.is_empty() {
1276                return;
1277            }
1278            searchable_item.update_matches(matches, Some(0), window, cx);
1279            searchable_item.activate_match(0, matches, window, cx);
1280        }
1281    }
1282
1283    pub fn select_last_match(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1284        if let Some(searchable_item) = self.active_searchable_item.as_ref()
1285            && let Some(matches) = self
1286                .searchable_items_with_matches
1287                .get(&searchable_item.downgrade())
1288        {
1289            if matches.is_empty() {
1290                return;
1291            }
1292            let new_match_index = matches.len() - 1;
1293            searchable_item.update_matches(matches, Some(new_match_index), window, cx);
1294            searchable_item.activate_match(new_match_index, matches, window, cx);
1295        }
1296    }
1297
1298    fn on_query_editor_event(
1299        &mut self,
1300        editor: &Entity<Editor>,
1301        event: &editor::EditorEvent,
1302        window: &mut Window,
1303        cx: &mut Context<Self>,
1304    ) {
1305        match event {
1306            editor::EditorEvent::Focused => self.query_editor_focused = true,
1307            editor::EditorEvent::Blurred => self.query_editor_focused = false,
1308            editor::EditorEvent::Edited { .. } => {
1309                self.smartcase(window, cx);
1310                self.clear_matches(window, cx);
1311                let search = self.update_matches(false, true, window, cx);
1312
1313                let width = editor.update(cx, |editor, cx| {
1314                    let text_layout_details = editor.text_layout_details(window);
1315                    let snapshot = editor.snapshot(window, cx).display_snapshot;
1316
1317                    snapshot.x_for_display_point(snapshot.max_point(), &text_layout_details)
1318                        - snapshot.x_for_display_point(DisplayPoint::zero(), &text_layout_details)
1319                });
1320                self.editor_needed_width = width;
1321                cx.notify();
1322
1323                cx.spawn_in(window, async move |this, cx| {
1324                    if search.await.is_ok() {
1325                        this.update_in(cx, |this, window, cx| {
1326                            this.activate_current_match(window, cx);
1327                            #[cfg(target_os = "macos")]
1328                            this.update_find_pasteboard(cx);
1329                        })?;
1330                    }
1331                    anyhow::Ok(())
1332                })
1333                .detach_and_log_err(cx);
1334            }
1335            _ => {}
1336        }
1337    }
1338
1339    fn on_replacement_editor_event(
1340        &mut self,
1341        _: Entity<Editor>,
1342        event: &editor::EditorEvent,
1343        _: &mut Context<Self>,
1344    ) {
1345        match event {
1346            editor::EditorEvent::Focused => self.replacement_editor_focused = true,
1347            editor::EditorEvent::Blurred => self.replacement_editor_focused = false,
1348            _ => {}
1349        }
1350    }
1351
1352    fn on_active_searchable_item_event(
1353        &mut self,
1354        event: &SearchEvent,
1355        window: &mut Window,
1356        cx: &mut Context<Self>,
1357    ) {
1358        match event {
1359            SearchEvent::MatchesInvalidated => {
1360                drop(self.update_matches(false, false, window, cx));
1361            }
1362            SearchEvent::ActiveMatchChanged => self.update_match_index(window, cx),
1363            SearchEvent::ResultsCollapsedChanged(collapse_direction) => {
1364                if self.needs_expand_collapse_option(cx) {
1365                    match collapse_direction {
1366                        CollapseDirection::Collapsed => self.is_collapsed = true,
1367                        CollapseDirection::Expanded => self.is_collapsed = false,
1368                    }
1369                }
1370                cx.notify();
1371            }
1372        }
1373    }
1374
1375    fn toggle_case_sensitive(
1376        &mut self,
1377        _: &ToggleCaseSensitive,
1378        window: &mut Window,
1379        cx: &mut Context<Self>,
1380    ) {
1381        self.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx)
1382    }
1383
1384    fn toggle_whole_word(
1385        &mut self,
1386        _: &ToggleWholeWord,
1387        window: &mut Window,
1388        cx: &mut Context<Self>,
1389    ) {
1390        self.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx)
1391    }
1392
1393    fn toggle_selection(
1394        &mut self,
1395        _: &ToggleSelection,
1396        window: &mut Window,
1397        cx: &mut Context<Self>,
1398    ) {
1399        self.set_search_within_selection(
1400            if let Some(_) = self.selection_search_enabled {
1401                None
1402            } else {
1403                Some(FilteredSearchRange::Default)
1404            },
1405            window,
1406            cx,
1407        );
1408    }
1409
1410    fn toggle_regex(&mut self, _: &ToggleRegex, window: &mut Window, cx: &mut Context<Self>) {
1411        self.toggle_search_option(SearchOptions::REGEX, window, cx)
1412    }
1413
1414    fn clear_active_searchable_item_matches(&mut self, window: &mut Window, cx: &mut App) {
1415        if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
1416            self.active_match_index = None;
1417            self.searchable_items_with_matches
1418                .remove(&active_searchable_item.downgrade());
1419            active_searchable_item.clear_matches(window, cx);
1420        }
1421    }
1422
1423    pub fn has_active_match(&self) -> bool {
1424        self.active_match_index.is_some()
1425    }
1426
1427    fn clear_matches(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1428        let mut active_item_matches = None;
1429        for (searchable_item, matches) in self.searchable_items_with_matches.drain() {
1430            if let Some(searchable_item) =
1431                WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
1432            {
1433                if Some(&searchable_item) == self.active_searchable_item.as_ref() {
1434                    active_item_matches = Some((searchable_item.downgrade(), matches));
1435                } else {
1436                    searchable_item.clear_matches(window, cx);
1437                }
1438            }
1439        }
1440
1441        self.searchable_items_with_matches
1442            .extend(active_item_matches);
1443    }
1444
1445    fn update_matches(
1446        &mut self,
1447        reuse_existing_query: bool,
1448        add_to_history: bool,
1449        window: &mut Window,
1450        cx: &mut Context<Self>,
1451    ) -> oneshot::Receiver<()> {
1452        let (done_tx, done_rx) = oneshot::channel();
1453        let query = self.query(cx);
1454        self.pending_search.take();
1455        #[cfg(target_os = "macos")]
1456        self.pending_external_query.take();
1457
1458        if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
1459            self.query_error = None;
1460            if query.is_empty() {
1461                self.clear_active_searchable_item_matches(window, cx);
1462                let _ = done_tx.send(());
1463                cx.notify();
1464            } else {
1465                let query: Arc<_> = if let Some(search) =
1466                    self.active_search.take().filter(|_| reuse_existing_query)
1467                {
1468                    search
1469                } else {
1470                    // Value doesn't matter, we only construct empty matchers with it
1471
1472                    if self.search_options.contains(SearchOptions::REGEX) {
1473                        match SearchQuery::regex(
1474                            query,
1475                            self.search_options.contains(SearchOptions::WHOLE_WORD),
1476                            self.search_options.contains(SearchOptions::CASE_SENSITIVE),
1477                            false,
1478                            self.search_options
1479                                .contains(SearchOptions::ONE_MATCH_PER_LINE),
1480                            PathMatcher::default(),
1481                            PathMatcher::default(),
1482                            false,
1483                            None,
1484                        ) {
1485                            Ok(query) => query.with_replacement(self.replacement(cx)),
1486                            Err(e) => {
1487                                self.query_error = Some(e.to_string());
1488                                self.clear_active_searchable_item_matches(window, cx);
1489                                cx.notify();
1490                                return done_rx;
1491                            }
1492                        }
1493                    } else {
1494                        match SearchQuery::text(
1495                            query,
1496                            self.search_options.contains(SearchOptions::WHOLE_WORD),
1497                            self.search_options.contains(SearchOptions::CASE_SENSITIVE),
1498                            false,
1499                            PathMatcher::default(),
1500                            PathMatcher::default(),
1501                            false,
1502                            None,
1503                        ) {
1504                            Ok(query) => query.with_replacement(self.replacement(cx)),
1505                            Err(e) => {
1506                                self.query_error = Some(e.to_string());
1507                                self.clear_active_searchable_item_matches(window, cx);
1508                                cx.notify();
1509                                return done_rx;
1510                            }
1511                        }
1512                    }
1513                    .into()
1514                };
1515
1516                self.active_search = Some(query.clone());
1517                let query_text = query.as_str().to_string();
1518
1519                let matches = active_searchable_item.find_matches(query, window, cx);
1520
1521                let active_searchable_item = active_searchable_item.downgrade();
1522                self.pending_search = Some(cx.spawn_in(window, async move |this, cx| {
1523                    let matches = matches.await;
1524
1525                    this.update_in(cx, |this, window, cx| {
1526                        if let Some(active_searchable_item) =
1527                            WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx)
1528                        {
1529                            this.searchable_items_with_matches
1530                                .insert(active_searchable_item.downgrade(), matches);
1531
1532                            this.update_match_index(window, cx);
1533
1534                            if add_to_history {
1535                                this.search_history
1536                                    .add(&mut this.search_history_cursor, query_text);
1537                            }
1538                            if !this.dismissed {
1539                                let matches = this
1540                                    .searchable_items_with_matches
1541                                    .get(&active_searchable_item.downgrade())
1542                                    .unwrap();
1543                                if matches.is_empty() {
1544                                    active_searchable_item.clear_matches(window, cx);
1545                                } else {
1546                                    active_searchable_item.update_matches(
1547                                        matches,
1548                                        this.active_match_index,
1549                                        window,
1550                                        cx,
1551                                    );
1552                                }
1553                            }
1554                            let _ = done_tx.send(());
1555                            cx.notify();
1556                        }
1557                    })
1558                    .log_err();
1559                }));
1560            }
1561        }
1562        done_rx
1563    }
1564
1565    fn reverse_direction_if_backwards(&self, direction: Direction) -> Direction {
1566        if self.search_options.contains(SearchOptions::BACKWARDS) {
1567            direction.opposite()
1568        } else {
1569            direction
1570        }
1571    }
1572
1573    pub fn update_match_index(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1574        let direction = self.reverse_direction_if_backwards(Direction::Next);
1575        let new_index = self
1576            .active_searchable_item
1577            .as_ref()
1578            .and_then(|searchable_item| {
1579                let matches = self
1580                    .searchable_items_with_matches
1581                    .get(&searchable_item.downgrade())?;
1582                searchable_item.active_match_index(direction, matches, window, cx)
1583            });
1584        if new_index != self.active_match_index {
1585            self.active_match_index = new_index;
1586            if !self.dismissed {
1587                if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1588                    if let Some(matches) = self
1589                        .searchable_items_with_matches
1590                        .get(&searchable_item.downgrade())
1591                    {
1592                        if !matches.is_empty() {
1593                            searchable_item.update_matches(matches, new_index, window, cx);
1594                        }
1595                    }
1596                }
1597            }
1598            cx.notify();
1599        }
1600    }
1601
1602    fn tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context<Self>) {
1603        self.cycle_field(Direction::Next, window, cx);
1604    }
1605
1606    fn backtab(&mut self, _: &Backtab, window: &mut Window, cx: &mut Context<Self>) {
1607        self.cycle_field(Direction::Prev, window, cx);
1608    }
1609    fn cycle_field(&mut self, direction: Direction, window: &mut Window, cx: &mut Context<Self>) {
1610        let mut handles = vec![self.query_editor.focus_handle(cx)];
1611        if self.replace_enabled {
1612            handles.push(self.replacement_editor.focus_handle(cx));
1613        }
1614        if let Some(item) = self.active_searchable_item.as_ref() {
1615            handles.push(item.item_focus_handle(cx));
1616        }
1617        let current_index = match handles.iter().position(|focus| focus.is_focused(window)) {
1618            Some(index) => index,
1619            None => return,
1620        };
1621
1622        let new_index = match direction {
1623            Direction::Next => (current_index + 1) % handles.len(),
1624            Direction::Prev if current_index == 0 => handles.len() - 1,
1625            Direction::Prev => (current_index - 1) % handles.len(),
1626        };
1627        let next_focus_handle = &handles[new_index];
1628        self.focus(next_focus_handle, window, cx);
1629        cx.stop_propagation();
1630    }
1631
1632    fn next_history_query(
1633        &mut self,
1634        _: &NextHistoryQuery,
1635        window: &mut Window,
1636        cx: &mut Context<Self>,
1637    ) {
1638        if let Some(new_query) = self
1639            .search_history
1640            .next(&mut self.search_history_cursor)
1641            .map(str::to_string)
1642        {
1643            drop(self.search(&new_query, Some(self.search_options), false, window, cx));
1644        } else {
1645            self.search_history_cursor.reset();
1646            drop(self.search("", Some(self.search_options), false, window, cx));
1647        }
1648    }
1649
1650    fn previous_history_query(
1651        &mut self,
1652        _: &PreviousHistoryQuery,
1653        window: &mut Window,
1654        cx: &mut Context<Self>,
1655    ) {
1656        if self.query(cx).is_empty()
1657            && let Some(new_query) = self
1658                .search_history
1659                .current(&self.search_history_cursor)
1660                .map(str::to_string)
1661        {
1662            drop(self.search(&new_query, Some(self.search_options), false, window, cx));
1663            return;
1664        }
1665
1666        if let Some(new_query) = self
1667            .search_history
1668            .previous(&mut self.search_history_cursor)
1669            .map(str::to_string)
1670        {
1671            drop(self.search(&new_query, Some(self.search_options), false, window, cx));
1672        }
1673    }
1674
1675    fn focus(&self, handle: &gpui::FocusHandle, window: &mut Window, cx: &mut App) {
1676        window.invalidate_character_coordinates();
1677        window.focus(handle, cx);
1678    }
1679
1680    fn toggle_replace(&mut self, _: &ToggleReplace, window: &mut Window, cx: &mut Context<Self>) {
1681        if self.active_searchable_item.is_some() {
1682            self.replace_enabled = !self.replace_enabled;
1683            let handle = if self.replace_enabled {
1684                self.replacement_editor.focus_handle(cx)
1685            } else {
1686                self.query_editor.focus_handle(cx)
1687            };
1688            self.focus(&handle, window, cx);
1689            cx.notify();
1690        }
1691    }
1692
1693    fn replace_next(&mut self, _: &ReplaceNext, window: &mut Window, cx: &mut Context<Self>) {
1694        let mut should_propagate = true;
1695        if !self.dismissed
1696            && self.active_search.is_some()
1697            && let Some(searchable_item) = self.active_searchable_item.as_ref()
1698            && let Some(query) = self.active_search.as_ref()
1699            && let Some(matches) = self
1700                .searchable_items_with_matches
1701                .get(&searchable_item.downgrade())
1702        {
1703            if let Some(active_index) = self.active_match_index {
1704                let query = query
1705                    .as_ref()
1706                    .clone()
1707                    .with_replacement(self.replacement(cx));
1708                searchable_item.replace(matches.at(active_index), &query, window, cx);
1709                self.select_next_match(&SelectNextMatch, window, cx);
1710            }
1711            should_propagate = false;
1712        }
1713        if !should_propagate {
1714            cx.stop_propagation();
1715        }
1716    }
1717
1718    pub fn replace_all(&mut self, _: &ReplaceAll, window: &mut Window, cx: &mut Context<Self>) {
1719        if !self.dismissed
1720            && self.active_search.is_some()
1721            && let Some(searchable_item) = self.active_searchable_item.as_ref()
1722            && let Some(query) = self.active_search.as_ref()
1723            && let Some(matches) = self
1724                .searchable_items_with_matches
1725                .get(&searchable_item.downgrade())
1726        {
1727            let query = query
1728                .as_ref()
1729                .clone()
1730                .with_replacement(self.replacement(cx));
1731            searchable_item.replace_all(&mut matches.iter(), &query, window, cx);
1732        }
1733    }
1734
1735    pub fn match_exists(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
1736        self.update_match_index(window, cx);
1737        self.active_match_index.is_some()
1738    }
1739
1740    pub fn should_use_smartcase_search(&mut self, cx: &mut Context<Self>) -> bool {
1741        EditorSettings::get_global(cx).use_smartcase_search
1742    }
1743
1744    pub fn is_contains_uppercase(&mut self, str: &String) -> bool {
1745        str.chars().any(|c| c.is_uppercase())
1746    }
1747
1748    fn smartcase(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1749        if self.should_use_smartcase_search(cx) {
1750            let query = self.query(cx);
1751            if !query.is_empty() {
1752                let is_case = self.is_contains_uppercase(&query);
1753                if self.has_search_option(SearchOptions::CASE_SENSITIVE) != is_case {
1754                    self.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
1755                }
1756            }
1757        }
1758    }
1759
1760    fn adjust_query_regex_language(&self, cx: &mut App) {
1761        let enable = self.search_options.contains(SearchOptions::REGEX);
1762        let query_buffer = self
1763            .query_editor
1764            .read(cx)
1765            .buffer()
1766            .read(cx)
1767            .as_singleton()
1768            .expect("query editor should be backed by a singleton buffer");
1769
1770        if enable {
1771            if let Some(regex_language) = self.regex_language.clone() {
1772                query_buffer.update(cx, |query_buffer, cx| {
1773                    query_buffer.set_language(Some(regex_language), cx);
1774                })
1775            }
1776        } else {
1777            query_buffer.update(cx, |query_buffer, cx| {
1778                query_buffer.set_language(None, cx);
1779            })
1780        }
1781    }
1782
1783    /// Updates the searchable item's case sensitivity option to match the
1784    /// search bar's current case sensitivity setting. This ensures that
1785    /// editor's `select_next`/ `select_previous` operations respect the buffer
1786    /// search bar's search options.
1787    ///
1788    /// Clears the case sensitivity when the search bar is dismissed so that
1789    /// only the editor's settings are respected.
1790    fn sync_select_next_case_sensitivity(&self, cx: &mut Context<Self>) {
1791        let case_sensitive = match self.dismissed {
1792            true => None,
1793            false => Some(self.search_options.contains(SearchOptions::CASE_SENSITIVE)),
1794        };
1795
1796        if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
1797            active_searchable_item.set_search_is_case_sensitive(case_sensitive, cx);
1798        }
1799    }
1800}
1801
1802#[cfg(test)]
1803mod tests {
1804    use std::ops::Range;
1805
1806    use super::*;
1807    use editor::{
1808        DisplayPoint, Editor, ExcerptRange, MultiBuffer, SearchSettings, SelectionEffects,
1809        display_map::DisplayRow, test::editor_test_context::EditorTestContext,
1810    };
1811    use gpui::{Hsla, TestAppContext, UpdateGlobal, VisualTestContext};
1812    use language::{Buffer, Point};
1813    use settings::{SearchSettingsContent, SettingsStore};
1814    use smol::stream::StreamExt as _;
1815    use unindent::Unindent as _;
1816    use util_macros::perf;
1817
1818    fn init_globals(cx: &mut TestAppContext) {
1819        cx.update(|cx| {
1820            let store = settings::SettingsStore::test(cx);
1821            cx.set_global(store);
1822            editor::init(cx);
1823
1824            theme::init(theme::LoadThemes::JustBase, cx);
1825            crate::init(cx);
1826        });
1827    }
1828
1829    fn init_multibuffer_test(
1830        cx: &mut TestAppContext,
1831    ) -> (
1832        Entity<Editor>,
1833        Entity<BufferSearchBar>,
1834        &mut VisualTestContext,
1835    ) {
1836        init_globals(cx);
1837
1838        let buffer1 = cx.new(|cx| {
1839            Buffer::local(
1840                            r#"
1841                            A regular expression (shortened as regex or regexp;[1] also referred to as
1842                            rational expression[2][3]) is a sequence of characters that specifies a search
1843                            pattern in text. Usually such patterns are used by string-searching algorithms
1844                            for "find" or "find and replace" operations on strings, or for input validation.
1845                            "#
1846                            .unindent(),
1847                            cx,
1848                        )
1849        });
1850
1851        let buffer2 = cx.new(|cx| {
1852            Buffer::local(
1853                r#"
1854                            Some Additional text with the term regular expression in it.
1855                            There two lines.
1856                            "#
1857                .unindent(),
1858                cx,
1859            )
1860        });
1861
1862        let multibuffer = cx.new(|cx| {
1863            let mut buffer = MultiBuffer::new(language::Capability::ReadWrite);
1864
1865            //[ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))]
1866            buffer.push_excerpts(
1867                buffer1,
1868                [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
1869                cx,
1870            );
1871            buffer.push_excerpts(
1872                buffer2,
1873                [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
1874                cx,
1875            );
1876
1877            buffer
1878        });
1879        let mut editor = None;
1880        let window = cx.add_window(|window, cx| {
1881            let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
1882                "keymaps/default-macos.json",
1883                cx,
1884            )
1885            .unwrap();
1886            cx.bind_keys(default_key_bindings);
1887            editor =
1888                Some(cx.new(|cx| Editor::for_multibuffer(multibuffer.clone(), None, window, cx)));
1889
1890            let mut search_bar = BufferSearchBar::new(None, window, cx);
1891            search_bar.set_active_pane_item(Some(&editor.clone().unwrap()), window, cx);
1892            search_bar.show(window, cx);
1893            search_bar
1894        });
1895        let search_bar = window.root(cx).unwrap();
1896
1897        let cx = VisualTestContext::from_window(*window, cx).into_mut();
1898
1899        (editor.unwrap(), search_bar, cx)
1900    }
1901
1902    fn init_test(
1903        cx: &mut TestAppContext,
1904    ) -> (
1905        Entity<Editor>,
1906        Entity<BufferSearchBar>,
1907        &mut VisualTestContext,
1908    ) {
1909        init_globals(cx);
1910        let buffer = cx.new(|cx| {
1911            Buffer::local(
1912                r#"
1913                A regular expression (shortened as regex or regexp;[1] also referred to as
1914                rational expression[2][3]) is a sequence of characters that specifies a search
1915                pattern in text. Usually such patterns are used by string-searching algorithms
1916                for "find" or "find and replace" operations on strings, or for input validation.
1917                "#
1918                .unindent(),
1919                cx,
1920            )
1921        });
1922        let mut editor = None;
1923        let window = cx.add_window(|window, cx| {
1924            let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
1925                "keymaps/default-macos.json",
1926                cx,
1927            )
1928            .unwrap();
1929            cx.bind_keys(default_key_bindings);
1930            editor = Some(cx.new(|cx| Editor::for_buffer(buffer.clone(), None, window, cx)));
1931            let mut search_bar = BufferSearchBar::new(None, window, cx);
1932            search_bar.set_active_pane_item(Some(&editor.clone().unwrap()), window, cx);
1933            search_bar.show(window, cx);
1934            search_bar
1935        });
1936        let search_bar = window.root(cx).unwrap();
1937
1938        let cx = VisualTestContext::from_window(*window, cx).into_mut();
1939
1940        (editor.unwrap(), search_bar, cx)
1941    }
1942
1943    #[perf]
1944    #[gpui::test]
1945    async fn test_search_simple(cx: &mut TestAppContext) {
1946        let (editor, search_bar, cx) = init_test(cx);
1947        let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
1948            background_highlights
1949                .into_iter()
1950                .map(|(range, _)| range)
1951                .collect::<Vec<_>>()
1952        };
1953        // Search for a string that appears with different casing.
1954        // By default, search is case-insensitive.
1955        search_bar
1956            .update_in(cx, |search_bar, window, cx| {
1957                search_bar.search("us", None, true, window, cx)
1958            })
1959            .await
1960            .unwrap();
1961        editor.update_in(cx, |editor, window, cx| {
1962            assert_eq!(
1963                display_points_of(editor.all_text_background_highlights(window, cx)),
1964                &[
1965                    DisplayPoint::new(DisplayRow(2), 17)..DisplayPoint::new(DisplayRow(2), 19),
1966                    DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),
1967                ]
1968            );
1969        });
1970
1971        // Switch to a case sensitive search.
1972        search_bar.update_in(cx, |search_bar, window, cx| {
1973            search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
1974        });
1975        let mut editor_notifications = cx.notifications(&editor);
1976        editor_notifications.next().await;
1977        editor.update_in(cx, |editor, window, cx| {
1978            assert_eq!(
1979                display_points_of(editor.all_text_background_highlights(window, cx)),
1980                &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
1981            );
1982        });
1983
1984        // Search for a string that appears both as a whole word and
1985        // within other words. By default, all results are found.
1986        search_bar
1987            .update_in(cx, |search_bar, window, cx| {
1988                search_bar.search("or", None, true, window, cx)
1989            })
1990            .await
1991            .unwrap();
1992        editor.update_in(cx, |editor, window, cx| {
1993            assert_eq!(
1994                display_points_of(editor.all_text_background_highlights(window, cx)),
1995                &[
1996                    DisplayPoint::new(DisplayRow(0), 24)..DisplayPoint::new(DisplayRow(0), 26),
1997                    DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1998                    DisplayPoint::new(DisplayRow(2), 71)..DisplayPoint::new(DisplayRow(2), 73),
1999                    DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 3),
2000                    DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
2001                    DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
2002                    DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 62),
2003                ]
2004            );
2005        });
2006
2007        // Switch to a whole word search.
2008        search_bar.update_in(cx, |search_bar, window, cx| {
2009            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
2010        });
2011        let mut editor_notifications = cx.notifications(&editor);
2012        editor_notifications.next().await;
2013        editor.update_in(cx, |editor, window, cx| {
2014            assert_eq!(
2015                display_points_of(editor.all_text_background_highlights(window, cx)),
2016                &[
2017                    DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
2018                    DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
2019                    DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
2020                ]
2021            );
2022        });
2023
2024        editor.update_in(cx, |editor, window, cx| {
2025            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2026                s.select_display_ranges([
2027                    DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2028                ])
2029            });
2030        });
2031        search_bar.update_in(cx, |search_bar, window, cx| {
2032            assert_eq!(search_bar.active_match_index, Some(0));
2033            search_bar.select_next_match(&SelectNextMatch, window, cx);
2034            assert_eq!(
2035                editor.update(cx, |editor, cx| editor
2036                    .selections
2037                    .display_ranges(&editor.display_snapshot(cx))),
2038                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
2039            );
2040        });
2041        search_bar.read_with(cx, |search_bar, _| {
2042            assert_eq!(search_bar.active_match_index, Some(0));
2043        });
2044
2045        search_bar.update_in(cx, |search_bar, window, cx| {
2046            search_bar.select_next_match(&SelectNextMatch, window, cx);
2047            assert_eq!(
2048                editor.update(cx, |editor, cx| editor
2049                    .selections
2050                    .display_ranges(&editor.display_snapshot(cx))),
2051                [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
2052            );
2053        });
2054        search_bar.read_with(cx, |search_bar, _| {
2055            assert_eq!(search_bar.active_match_index, Some(1));
2056        });
2057
2058        search_bar.update_in(cx, |search_bar, window, cx| {
2059            search_bar.select_next_match(&SelectNextMatch, window, cx);
2060            assert_eq!(
2061                editor.update(cx, |editor, cx| editor
2062                    .selections
2063                    .display_ranges(&editor.display_snapshot(cx))),
2064                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
2065            );
2066        });
2067        search_bar.read_with(cx, |search_bar, _| {
2068            assert_eq!(search_bar.active_match_index, Some(2));
2069        });
2070
2071        search_bar.update_in(cx, |search_bar, window, cx| {
2072            search_bar.select_next_match(&SelectNextMatch, window, cx);
2073            assert_eq!(
2074                editor.update(cx, |editor, cx| editor
2075                    .selections
2076                    .display_ranges(&editor.display_snapshot(cx))),
2077                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
2078            );
2079        });
2080        search_bar.read_with(cx, |search_bar, _| {
2081            assert_eq!(search_bar.active_match_index, Some(0));
2082        });
2083
2084        search_bar.update_in(cx, |search_bar, window, cx| {
2085            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2086            assert_eq!(
2087                editor.update(cx, |editor, cx| editor
2088                    .selections
2089                    .display_ranges(&editor.display_snapshot(cx))),
2090                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
2091            );
2092        });
2093        search_bar.read_with(cx, |search_bar, _| {
2094            assert_eq!(search_bar.active_match_index, Some(2));
2095        });
2096
2097        search_bar.update_in(cx, |search_bar, window, cx| {
2098            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2099            assert_eq!(
2100                editor.update(cx, |editor, cx| editor
2101                    .selections
2102                    .display_ranges(&editor.display_snapshot(cx))),
2103                [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
2104            );
2105        });
2106        search_bar.read_with(cx, |search_bar, _| {
2107            assert_eq!(search_bar.active_match_index, Some(1));
2108        });
2109
2110        search_bar.update_in(cx, |search_bar, window, cx| {
2111            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2112            assert_eq!(
2113                editor.update(cx, |editor, cx| editor
2114                    .selections
2115                    .display_ranges(&editor.display_snapshot(cx))),
2116                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
2117            );
2118        });
2119        search_bar.read_with(cx, |search_bar, _| {
2120            assert_eq!(search_bar.active_match_index, Some(0));
2121        });
2122
2123        // Park the cursor in between matches and ensure that going to the previous match selects
2124        // the closest match to the left.
2125        editor.update_in(cx, |editor, window, cx| {
2126            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2127                s.select_display_ranges([
2128                    DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
2129                ])
2130            });
2131        });
2132        search_bar.update_in(cx, |search_bar, window, cx| {
2133            assert_eq!(search_bar.active_match_index, Some(1));
2134            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2135            assert_eq!(
2136                editor.update(cx, |editor, cx| editor
2137                    .selections
2138                    .display_ranges(&editor.display_snapshot(cx))),
2139                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
2140            );
2141        });
2142        search_bar.read_with(cx, |search_bar, _| {
2143            assert_eq!(search_bar.active_match_index, Some(0));
2144        });
2145
2146        // Park the cursor in between matches and ensure that going to the next match selects the
2147        // closest match to the right.
2148        editor.update_in(cx, |editor, window, cx| {
2149            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2150                s.select_display_ranges([
2151                    DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
2152                ])
2153            });
2154        });
2155        search_bar.update_in(cx, |search_bar, window, cx| {
2156            assert_eq!(search_bar.active_match_index, Some(1));
2157            search_bar.select_next_match(&SelectNextMatch, window, cx);
2158            assert_eq!(
2159                editor.update(cx, |editor, cx| editor
2160                    .selections
2161                    .display_ranges(&editor.display_snapshot(cx))),
2162                [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
2163            );
2164        });
2165        search_bar.read_with(cx, |search_bar, _| {
2166            assert_eq!(search_bar.active_match_index, Some(1));
2167        });
2168
2169        // Park the cursor after the last match and ensure that going to the previous match selects
2170        // the last match.
2171        editor.update_in(cx, |editor, window, cx| {
2172            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2173                s.select_display_ranges([
2174                    DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
2175                ])
2176            });
2177        });
2178        search_bar.update_in(cx, |search_bar, window, cx| {
2179            assert_eq!(search_bar.active_match_index, Some(2));
2180            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2181            assert_eq!(
2182                editor.update(cx, |editor, cx| editor
2183                    .selections
2184                    .display_ranges(&editor.display_snapshot(cx))),
2185                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
2186            );
2187        });
2188        search_bar.read_with(cx, |search_bar, _| {
2189            assert_eq!(search_bar.active_match_index, Some(2));
2190        });
2191
2192        // Park the cursor after the last match and ensure that going to the next match selects the
2193        // first match.
2194        editor.update_in(cx, |editor, window, cx| {
2195            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2196                s.select_display_ranges([
2197                    DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
2198                ])
2199            });
2200        });
2201        search_bar.update_in(cx, |search_bar, window, cx| {
2202            assert_eq!(search_bar.active_match_index, Some(2));
2203            search_bar.select_next_match(&SelectNextMatch, window, cx);
2204            assert_eq!(
2205                editor.update(cx, |editor, cx| editor
2206                    .selections
2207                    .display_ranges(&editor.display_snapshot(cx))),
2208                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
2209            );
2210        });
2211        search_bar.read_with(cx, |search_bar, _| {
2212            assert_eq!(search_bar.active_match_index, Some(0));
2213        });
2214
2215        // Park the cursor before the first match and ensure that going to the previous match
2216        // selects the last match.
2217        editor.update_in(cx, |editor, window, cx| {
2218            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2219                s.select_display_ranges([
2220                    DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2221                ])
2222            });
2223        });
2224        search_bar.update_in(cx, |search_bar, window, cx| {
2225            assert_eq!(search_bar.active_match_index, Some(0));
2226            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2227            assert_eq!(
2228                editor.update(cx, |editor, cx| editor
2229                    .selections
2230                    .display_ranges(&editor.display_snapshot(cx))),
2231                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
2232            );
2233        });
2234        search_bar.read_with(cx, |search_bar, _| {
2235            assert_eq!(search_bar.active_match_index, Some(2));
2236        });
2237    }
2238
2239    fn display_points_of(
2240        background_highlights: Vec<(Range<DisplayPoint>, Hsla)>,
2241    ) -> Vec<Range<DisplayPoint>> {
2242        background_highlights
2243            .into_iter()
2244            .map(|(range, _)| range)
2245            .collect::<Vec<_>>()
2246    }
2247
2248    #[perf]
2249    #[gpui::test]
2250    async fn test_search_option_handling(cx: &mut TestAppContext) {
2251        let (editor, search_bar, cx) = init_test(cx);
2252
2253        // show with options should make current search case sensitive
2254        search_bar
2255            .update_in(cx, |search_bar, window, cx| {
2256                search_bar.show(window, cx);
2257                search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), true, window, cx)
2258            })
2259            .await
2260            .unwrap();
2261        editor.update_in(cx, |editor, window, cx| {
2262            assert_eq!(
2263                display_points_of(editor.all_text_background_highlights(window, cx)),
2264                &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
2265            );
2266        });
2267
2268        // search_suggested should restore default options
2269        search_bar.update_in(cx, |search_bar, window, cx| {
2270            search_bar.search_suggested(window, cx);
2271            assert_eq!(search_bar.search_options, SearchOptions::NONE)
2272        });
2273
2274        // toggling a search option should update the defaults
2275        search_bar
2276            .update_in(cx, |search_bar, window, cx| {
2277                search_bar.search(
2278                    "regex",
2279                    Some(SearchOptions::CASE_SENSITIVE),
2280                    true,
2281                    window,
2282                    cx,
2283                )
2284            })
2285            .await
2286            .unwrap();
2287        search_bar.update_in(cx, |search_bar, window, cx| {
2288            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx)
2289        });
2290        let mut editor_notifications = cx.notifications(&editor);
2291        editor_notifications.next().await;
2292        editor.update_in(cx, |editor, window, cx| {
2293            assert_eq!(
2294                display_points_of(editor.all_text_background_highlights(window, cx)),
2295                &[DisplayPoint::new(DisplayRow(0), 35)..DisplayPoint::new(DisplayRow(0), 40),]
2296            );
2297        });
2298
2299        // defaults should still include whole word
2300        search_bar.update_in(cx, |search_bar, window, cx| {
2301            search_bar.search_suggested(window, cx);
2302            assert_eq!(
2303                search_bar.search_options,
2304                SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD
2305            )
2306        });
2307    }
2308
2309    #[perf]
2310    #[gpui::test]
2311    async fn test_search_select_all_matches(cx: &mut TestAppContext) {
2312        init_globals(cx);
2313        let buffer_text = r#"
2314        A regular expression (shortened as regex or regexp;[1] also referred to as
2315        rational expression[2][3]) is a sequence of characters that specifies a search
2316        pattern in text. Usually such patterns are used by string-searching algorithms
2317        for "find" or "find and replace" operations on strings, or for input validation.
2318        "#
2319        .unindent();
2320        let expected_query_matches_count = buffer_text
2321            .chars()
2322            .filter(|c| c.eq_ignore_ascii_case(&'a'))
2323            .count();
2324        assert!(
2325            expected_query_matches_count > 1,
2326            "Should pick a query with multiple results"
2327        );
2328        let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
2329        let window = cx.add_window(|_, _| gpui::Empty);
2330
2331        let editor = window.build_entity(cx, |window, cx| {
2332            Editor::for_buffer(buffer.clone(), None, window, cx)
2333        });
2334
2335        let search_bar = window.build_entity(cx, |window, cx| {
2336            let mut search_bar = BufferSearchBar::new(None, window, cx);
2337            search_bar.set_active_pane_item(Some(&editor), window, cx);
2338            search_bar.show(window, cx);
2339            search_bar
2340        });
2341
2342        window
2343            .update(cx, |_, window, cx| {
2344                search_bar.update(cx, |search_bar, cx| {
2345                    search_bar.search("a", None, true, window, cx)
2346                })
2347            })
2348            .unwrap()
2349            .await
2350            .unwrap();
2351        let initial_selections = window
2352            .update(cx, |_, window, cx| {
2353                search_bar.update(cx, |search_bar, cx| {
2354                    let handle = search_bar.query_editor.focus_handle(cx);
2355                    window.focus(&handle, cx);
2356                    search_bar.activate_current_match(window, cx);
2357                });
2358                assert!(
2359                    !editor.read(cx).is_focused(window),
2360                    "Initially, the editor should not be focused"
2361                );
2362                let initial_selections = editor.update(cx, |editor, cx| {
2363                    let initial_selections = editor.selections.display_ranges(&editor.display_snapshot(cx));
2364                    assert_eq!(
2365                        initial_selections.len(), 1,
2366                        "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
2367                    );
2368                    initial_selections
2369                });
2370                search_bar.update(cx, |search_bar, cx| {
2371                    assert_eq!(search_bar.active_match_index, Some(0));
2372                    let handle = search_bar.query_editor.focus_handle(cx);
2373                    window.focus(&handle, cx);
2374                    search_bar.select_all_matches(&SelectAllMatches, window, cx);
2375                });
2376                assert!(
2377                    editor.read(cx).is_focused(window),
2378                    "Should focus editor after successful SelectAllMatches"
2379                );
2380                search_bar.update(cx, |search_bar, cx| {
2381                    let all_selections =
2382                        editor.update(cx, |editor, cx| editor.selections.display_ranges(&editor.display_snapshot(cx)));
2383                    assert_eq!(
2384                        all_selections.len(),
2385                        expected_query_matches_count,
2386                        "Should select all `a` characters in the buffer, but got: {all_selections:?}"
2387                    );
2388                    assert_eq!(
2389                        search_bar.active_match_index,
2390                        Some(0),
2391                        "Match index should not change after selecting all matches"
2392                    );
2393                });
2394
2395                search_bar.update(cx, |this, cx| this.select_next_match(&SelectNextMatch, window, cx));
2396                initial_selections
2397            }).unwrap();
2398
2399        window
2400            .update(cx, |_, window, cx| {
2401                assert!(
2402                    editor.read(cx).is_focused(window),
2403                    "Should still have editor focused after SelectNextMatch"
2404                );
2405                search_bar.update(cx, |search_bar, cx| {
2406                    let all_selections = editor.update(cx, |editor, cx| {
2407                        editor
2408                            .selections
2409                            .display_ranges(&editor.display_snapshot(cx))
2410                    });
2411                    assert_eq!(
2412                        all_selections.len(),
2413                        1,
2414                        "On next match, should deselect items and select the next match"
2415                    );
2416                    assert_ne!(
2417                        all_selections, initial_selections,
2418                        "Next match should be different from the first selection"
2419                    );
2420                    assert_eq!(
2421                        search_bar.active_match_index,
2422                        Some(1),
2423                        "Match index should be updated to the next one"
2424                    );
2425                    let handle = search_bar.query_editor.focus_handle(cx);
2426                    window.focus(&handle, cx);
2427                    search_bar.select_all_matches(&SelectAllMatches, window, cx);
2428                });
2429            })
2430            .unwrap();
2431        window
2432            .update(cx, |_, window, cx| {
2433                assert!(
2434                    editor.read(cx).is_focused(window),
2435                    "Should focus editor after successful SelectAllMatches"
2436                );
2437                search_bar.update(cx, |search_bar, cx| {
2438                    let all_selections =
2439                        editor.update(cx, |editor, cx| editor.selections.display_ranges(&editor.display_snapshot(cx)));
2440                    assert_eq!(
2441                    all_selections.len(),
2442                    expected_query_matches_count,
2443                    "Should select all `a` characters in the buffer, but got: {all_selections:?}"
2444                );
2445                    assert_eq!(
2446                        search_bar.active_match_index,
2447                        Some(1),
2448                        "Match index should not change after selecting all matches"
2449                    );
2450                });
2451                search_bar.update(cx, |search_bar, cx| {
2452                    search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2453                });
2454            })
2455            .unwrap();
2456        let last_match_selections = window
2457            .update(cx, |_, window, cx| {
2458                assert!(
2459                    editor.read(cx).is_focused(window),
2460                    "Should still have editor focused after SelectPreviousMatch"
2461                );
2462
2463                search_bar.update(cx, |search_bar, cx| {
2464                    let all_selections = editor.update(cx, |editor, cx| {
2465                        editor
2466                            .selections
2467                            .display_ranges(&editor.display_snapshot(cx))
2468                    });
2469                    assert_eq!(
2470                        all_selections.len(),
2471                        1,
2472                        "On previous match, should deselect items and select the previous item"
2473                    );
2474                    assert_eq!(
2475                        all_selections, initial_selections,
2476                        "Previous match should be the same as the first selection"
2477                    );
2478                    assert_eq!(
2479                        search_bar.active_match_index,
2480                        Some(0),
2481                        "Match index should be updated to the previous one"
2482                    );
2483                    all_selections
2484                })
2485            })
2486            .unwrap();
2487
2488        window
2489            .update(cx, |_, window, cx| {
2490                search_bar.update(cx, |search_bar, cx| {
2491                    let handle = search_bar.query_editor.focus_handle(cx);
2492                    window.focus(&handle, cx);
2493                    search_bar.search("abas_nonexistent_match", None, true, window, cx)
2494                })
2495            })
2496            .unwrap()
2497            .await
2498            .unwrap();
2499        window
2500            .update(cx, |_, window, cx| {
2501                search_bar.update(cx, |search_bar, cx| {
2502                    search_bar.select_all_matches(&SelectAllMatches, window, cx);
2503                });
2504                assert!(
2505                    editor.update(cx, |this, _cx| !this.is_focused(window)),
2506                    "Should not switch focus to editor if SelectAllMatches does not find any matches"
2507                );
2508                search_bar.update(cx, |search_bar, cx| {
2509                    let all_selections =
2510                        editor.update(cx, |editor, cx| editor.selections.display_ranges(&editor.display_snapshot(cx)));
2511                    assert_eq!(
2512                        all_selections, last_match_selections,
2513                        "Should not select anything new if there are no matches"
2514                    );
2515                    assert!(
2516                        search_bar.active_match_index.is_none(),
2517                        "For no matches, there should be no active match index"
2518                    );
2519                });
2520            })
2521            .unwrap();
2522    }
2523
2524    #[perf]
2525    #[gpui::test]
2526    async fn test_search_query_with_match_whole_word(cx: &mut TestAppContext) {
2527        init_globals(cx);
2528        let buffer_text = r#"
2529        self.buffer.update(cx, |buffer, cx| {
2530            buffer.edit(
2531                edits,
2532                Some(AutoindentMode::Block {
2533                    original_indent_columns,
2534                }),
2535                cx,
2536            )
2537        });
2538
2539        this.buffer.update(cx, |buffer, cx| {
2540            buffer.edit([(end_of_line..start_of_next_line, replace)], None, cx)
2541        });
2542        "#
2543        .unindent();
2544        let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
2545        let cx = cx.add_empty_window();
2546
2547        let editor =
2548            cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
2549
2550        let search_bar = cx.new_window_entity(|window, cx| {
2551            let mut search_bar = BufferSearchBar::new(None, window, cx);
2552            search_bar.set_active_pane_item(Some(&editor), window, cx);
2553            search_bar.show(window, cx);
2554            search_bar
2555        });
2556
2557        search_bar
2558            .update_in(cx, |search_bar, window, cx| {
2559                search_bar.search(
2560                    "edit\\(",
2561                    Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
2562                    true,
2563                    window,
2564                    cx,
2565                )
2566            })
2567            .await
2568            .unwrap();
2569
2570        search_bar.update_in(cx, |search_bar, window, cx| {
2571            search_bar.select_all_matches(&SelectAllMatches, window, cx);
2572        });
2573        search_bar.update(cx, |_, cx| {
2574            let all_selections = editor.update(cx, |editor, cx| {
2575                editor
2576                    .selections
2577                    .display_ranges(&editor.display_snapshot(cx))
2578            });
2579            assert_eq!(
2580                all_selections.len(),
2581                2,
2582                "Should select all `edit(` in the buffer, but got: {all_selections:?}"
2583            );
2584        });
2585
2586        search_bar
2587            .update_in(cx, |search_bar, window, cx| {
2588                search_bar.search(
2589                    "edit(",
2590                    Some(SearchOptions::WHOLE_WORD | SearchOptions::CASE_SENSITIVE),
2591                    true,
2592                    window,
2593                    cx,
2594                )
2595            })
2596            .await
2597            .unwrap();
2598
2599        search_bar.update_in(cx, |search_bar, window, cx| {
2600            search_bar.select_all_matches(&SelectAllMatches, window, cx);
2601        });
2602        search_bar.update(cx, |_, cx| {
2603            let all_selections = editor.update(cx, |editor, cx| {
2604                editor
2605                    .selections
2606                    .display_ranges(&editor.display_snapshot(cx))
2607            });
2608            assert_eq!(
2609                all_selections.len(),
2610                2,
2611                "Should select all `edit(` in the buffer, but got: {all_selections:?}"
2612            );
2613        });
2614    }
2615
2616    #[perf]
2617    #[gpui::test]
2618    async fn test_search_query_history(cx: &mut TestAppContext) {
2619        let (_editor, search_bar, cx) = init_test(cx);
2620
2621        // Add 3 search items into the history.
2622        search_bar
2623            .update_in(cx, |search_bar, window, cx| {
2624                search_bar.search("a", None, true, window, cx)
2625            })
2626            .await
2627            .unwrap();
2628        search_bar
2629            .update_in(cx, |search_bar, window, cx| {
2630                search_bar.search("b", None, true, window, cx)
2631            })
2632            .await
2633            .unwrap();
2634        search_bar
2635            .update_in(cx, |search_bar, window, cx| {
2636                search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), true, window, cx)
2637            })
2638            .await
2639            .unwrap();
2640        // Ensure that the latest search is active.
2641        search_bar.update(cx, |search_bar, cx| {
2642            assert_eq!(search_bar.query(cx), "c");
2643            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2644        });
2645
2646        // Next history query after the latest should set the query to the empty string.
2647        search_bar.update_in(cx, |search_bar, window, cx| {
2648            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2649        });
2650        cx.background_executor.run_until_parked();
2651        search_bar.update(cx, |search_bar, cx| {
2652            assert_eq!(search_bar.query(cx), "");
2653            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2654        });
2655        search_bar.update_in(cx, |search_bar, window, cx| {
2656            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2657        });
2658        cx.background_executor.run_until_parked();
2659        search_bar.update(cx, |search_bar, cx| {
2660            assert_eq!(search_bar.query(cx), "");
2661            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2662        });
2663
2664        // First previous query for empty current query should set the query to the latest.
2665        search_bar.update_in(cx, |search_bar, window, cx| {
2666            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2667        });
2668        cx.background_executor.run_until_parked();
2669        search_bar.update(cx, |search_bar, cx| {
2670            assert_eq!(search_bar.query(cx), "c");
2671            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2672        });
2673
2674        // Further previous items should go over the history in reverse order.
2675        search_bar.update_in(cx, |search_bar, window, cx| {
2676            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2677        });
2678        cx.background_executor.run_until_parked();
2679        search_bar.update(cx, |search_bar, cx| {
2680            assert_eq!(search_bar.query(cx), "b");
2681            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2682        });
2683
2684        // Previous items should never go behind the first history item.
2685        search_bar.update_in(cx, |search_bar, window, cx| {
2686            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2687        });
2688        cx.background_executor.run_until_parked();
2689        search_bar.update(cx, |search_bar, cx| {
2690            assert_eq!(search_bar.query(cx), "a");
2691            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2692        });
2693        search_bar.update_in(cx, |search_bar, window, cx| {
2694            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2695        });
2696        cx.background_executor.run_until_parked();
2697        search_bar.update(cx, |search_bar, cx| {
2698            assert_eq!(search_bar.query(cx), "a");
2699            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2700        });
2701
2702        // Next items should go over the history in the original order.
2703        search_bar.update_in(cx, |search_bar, window, cx| {
2704            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2705        });
2706        cx.background_executor.run_until_parked();
2707        search_bar.update(cx, |search_bar, cx| {
2708            assert_eq!(search_bar.query(cx), "b");
2709            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2710        });
2711
2712        search_bar
2713            .update_in(cx, |search_bar, window, cx| {
2714                search_bar.search("ba", None, true, window, cx)
2715            })
2716            .await
2717            .unwrap();
2718        search_bar.update(cx, |search_bar, cx| {
2719            assert_eq!(search_bar.query(cx), "ba");
2720            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2721        });
2722
2723        // New search input should add another entry to history and move the selection to the end of the history.
2724        search_bar.update_in(cx, |search_bar, window, cx| {
2725            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2726        });
2727        cx.background_executor.run_until_parked();
2728        search_bar.update(cx, |search_bar, cx| {
2729            assert_eq!(search_bar.query(cx), "c");
2730            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2731        });
2732        search_bar.update_in(cx, |search_bar, window, cx| {
2733            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2734        });
2735        cx.background_executor.run_until_parked();
2736        search_bar.update(cx, |search_bar, cx| {
2737            assert_eq!(search_bar.query(cx), "b");
2738            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2739        });
2740        search_bar.update_in(cx, |search_bar, window, cx| {
2741            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2742        });
2743        cx.background_executor.run_until_parked();
2744        search_bar.update(cx, |search_bar, cx| {
2745            assert_eq!(search_bar.query(cx), "c");
2746            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2747        });
2748        search_bar.update_in(cx, |search_bar, window, cx| {
2749            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2750        });
2751        cx.background_executor.run_until_parked();
2752        search_bar.update(cx, |search_bar, cx| {
2753            assert_eq!(search_bar.query(cx), "ba");
2754            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2755        });
2756        search_bar.update_in(cx, |search_bar, window, cx| {
2757            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2758        });
2759        cx.background_executor.run_until_parked();
2760        search_bar.update(cx, |search_bar, cx| {
2761            assert_eq!(search_bar.query(cx), "");
2762            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2763        });
2764    }
2765
2766    #[perf]
2767    #[gpui::test]
2768    async fn test_replace_simple(cx: &mut TestAppContext) {
2769        let (editor, search_bar, cx) = init_test(cx);
2770
2771        search_bar
2772            .update_in(cx, |search_bar, window, cx| {
2773                search_bar.search("expression", None, true, window, cx)
2774            })
2775            .await
2776            .unwrap();
2777
2778        search_bar.update_in(cx, |search_bar, window, cx| {
2779            search_bar.replacement_editor.update(cx, |editor, cx| {
2780                // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally.
2781                editor.set_text("expr$1", window, cx);
2782            });
2783            search_bar.replace_all(&ReplaceAll, window, cx)
2784        });
2785        assert_eq!(
2786            editor.read_with(cx, |this, cx| { this.text(cx) }),
2787            r#"
2788        A regular expr$1 (shortened as regex or regexp;[1] also referred to as
2789        rational expr$1[2][3]) is a sequence of characters that specifies a search
2790        pattern in text. Usually such patterns are used by string-searching algorithms
2791        for "find" or "find and replace" operations on strings, or for input validation.
2792        "#
2793            .unindent()
2794        );
2795
2796        // Search for word boundaries and replace just a single one.
2797        search_bar
2798            .update_in(cx, |search_bar, window, cx| {
2799                search_bar.search("or", Some(SearchOptions::WHOLE_WORD), true, window, cx)
2800            })
2801            .await
2802            .unwrap();
2803
2804        search_bar.update_in(cx, |search_bar, window, cx| {
2805            search_bar.replacement_editor.update(cx, |editor, cx| {
2806                editor.set_text("banana", window, cx);
2807            });
2808            search_bar.replace_next(&ReplaceNext, window, cx)
2809        });
2810        // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text.
2811        assert_eq!(
2812            editor.read_with(cx, |this, cx| { this.text(cx) }),
2813            r#"
2814        A regular expr$1 (shortened as regex banana regexp;[1] also referred to as
2815        rational expr$1[2][3]) is a sequence of characters that specifies a search
2816        pattern in text. Usually such patterns are used by string-searching algorithms
2817        for "find" or "find and replace" operations on strings, or for input validation.
2818        "#
2819            .unindent()
2820        );
2821        // Let's turn on regex mode.
2822        search_bar
2823            .update_in(cx, |search_bar, window, cx| {
2824                search_bar.search(
2825                    "\\[([^\\]]+)\\]",
2826                    Some(SearchOptions::REGEX),
2827                    true,
2828                    window,
2829                    cx,
2830                )
2831            })
2832            .await
2833            .unwrap();
2834        search_bar.update_in(cx, |search_bar, window, cx| {
2835            search_bar.replacement_editor.update(cx, |editor, cx| {
2836                editor.set_text("${1}number", window, cx);
2837            });
2838            search_bar.replace_all(&ReplaceAll, window, cx)
2839        });
2840        assert_eq!(
2841            editor.read_with(cx, |this, cx| { this.text(cx) }),
2842            r#"
2843        A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2844        rational expr$12number3number) is a sequence of characters that specifies a search
2845        pattern in text. Usually such patterns are used by string-searching algorithms
2846        for "find" or "find and replace" operations on strings, or for input validation.
2847        "#
2848            .unindent()
2849        );
2850        // Now with a whole-word twist.
2851        search_bar
2852            .update_in(cx, |search_bar, window, cx| {
2853                search_bar.search(
2854                    "a\\w+s",
2855                    Some(SearchOptions::REGEX | SearchOptions::WHOLE_WORD),
2856                    true,
2857                    window,
2858                    cx,
2859                )
2860            })
2861            .await
2862            .unwrap();
2863        search_bar.update_in(cx, |search_bar, window, cx| {
2864            search_bar.replacement_editor.update(cx, |editor, cx| {
2865                editor.set_text("things", window, cx);
2866            });
2867            search_bar.replace_all(&ReplaceAll, window, cx)
2868        });
2869        // The only word affected by this edit should be `algorithms`, even though there's a bunch
2870        // of words in this text that would match this regex if not for WHOLE_WORD.
2871        assert_eq!(
2872            editor.read_with(cx, |this, cx| { this.text(cx) }),
2873            r#"
2874        A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2875        rational expr$12number3number) is a sequence of characters that specifies a search
2876        pattern in text. Usually such patterns are used by string-searching things
2877        for "find" or "find and replace" operations on strings, or for input validation.
2878        "#
2879            .unindent()
2880        );
2881    }
2882
2883    #[gpui::test]
2884    async fn test_replace_focus(cx: &mut TestAppContext) {
2885        let (editor, search_bar, cx) = init_test(cx);
2886
2887        editor.update_in(cx, |editor, window, cx| {
2888            editor.set_text("What a bad day!", window, cx)
2889        });
2890
2891        search_bar
2892            .update_in(cx, |search_bar, window, cx| {
2893                search_bar.search("bad", None, true, window, cx)
2894            })
2895            .await
2896            .unwrap();
2897
2898        // Calling `toggle_replace` in the search bar ensures that the "Replace
2899        // *" buttons are rendered, so we can then simulate clicking the
2900        // buttons.
2901        search_bar.update_in(cx, |search_bar, window, cx| {
2902            search_bar.toggle_replace(&ToggleReplace, window, cx)
2903        });
2904
2905        search_bar.update_in(cx, |search_bar, window, cx| {
2906            search_bar.replacement_editor.update(cx, |editor, cx| {
2907                editor.set_text("great", window, cx);
2908            });
2909        });
2910
2911        // Focus on the editor instead of the search bar, as we want to ensure
2912        // that pressing the "Replace Next Match" button will work, even if the
2913        // search bar is not focused.
2914        cx.focus(&editor);
2915
2916        // We'll not simulate clicking the "Replace Next Match " button, asserting that
2917        // the replacement was done.
2918        let button_bounds = cx
2919            .debug_bounds("ICON-ReplaceNext")
2920            .expect("'Replace Next Match' button should be visible");
2921        cx.simulate_click(button_bounds.center(), gpui::Modifiers::none());
2922
2923        assert_eq!(
2924            editor.read_with(cx, |editor, cx| editor.text(cx)),
2925            "What a great day!"
2926        );
2927    }
2928
2929    struct ReplacementTestParams<'a> {
2930        editor: &'a Entity<Editor>,
2931        search_bar: &'a Entity<BufferSearchBar>,
2932        cx: &'a mut VisualTestContext,
2933        search_text: &'static str,
2934        search_options: Option<SearchOptions>,
2935        replacement_text: &'static str,
2936        replace_all: bool,
2937        expected_text: String,
2938    }
2939
2940    async fn run_replacement_test(options: ReplacementTestParams<'_>) {
2941        options
2942            .search_bar
2943            .update_in(options.cx, |search_bar, window, cx| {
2944                if let Some(options) = options.search_options {
2945                    search_bar.set_search_options(options, cx);
2946                }
2947                search_bar.search(
2948                    options.search_text,
2949                    options.search_options,
2950                    true,
2951                    window,
2952                    cx,
2953                )
2954            })
2955            .await
2956            .unwrap();
2957
2958        options
2959            .search_bar
2960            .update_in(options.cx, |search_bar, window, cx| {
2961                search_bar.replacement_editor.update(cx, |editor, cx| {
2962                    editor.set_text(options.replacement_text, window, cx);
2963                });
2964
2965                if options.replace_all {
2966                    search_bar.replace_all(&ReplaceAll, window, cx)
2967                } else {
2968                    search_bar.replace_next(&ReplaceNext, window, cx)
2969                }
2970            });
2971
2972        assert_eq!(
2973            options
2974                .editor
2975                .read_with(options.cx, |this, cx| { this.text(cx) }),
2976            options.expected_text
2977        );
2978    }
2979
2980    #[perf]
2981    #[gpui::test]
2982    async fn test_replace_special_characters(cx: &mut TestAppContext) {
2983        let (editor, search_bar, cx) = init_test(cx);
2984
2985        run_replacement_test(ReplacementTestParams {
2986            editor: &editor,
2987            search_bar: &search_bar,
2988            cx,
2989            search_text: "expression",
2990            search_options: None,
2991            replacement_text: r"\n",
2992            replace_all: true,
2993            expected_text: r#"
2994            A regular \n (shortened as regex or regexp;[1] also referred to as
2995            rational \n[2][3]) is a sequence of characters that specifies a search
2996            pattern in text. Usually such patterns are used by string-searching algorithms
2997            for "find" or "find and replace" operations on strings, or for input validation.
2998            "#
2999            .unindent(),
3000        })
3001        .await;
3002
3003        run_replacement_test(ReplacementTestParams {
3004            editor: &editor,
3005            search_bar: &search_bar,
3006            cx,
3007            search_text: "or",
3008            search_options: Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
3009            replacement_text: r"\\\n\\\\",
3010            replace_all: false,
3011            expected_text: r#"
3012            A regular \n (shortened as regex \
3013            \\ regexp;[1] also referred to as
3014            rational \n[2][3]) is a sequence of characters that specifies a search
3015            pattern in text. Usually such patterns are used by string-searching algorithms
3016            for "find" or "find and replace" operations on strings, or for input validation.
3017            "#
3018            .unindent(),
3019        })
3020        .await;
3021
3022        run_replacement_test(ReplacementTestParams {
3023            editor: &editor,
3024            search_bar: &search_bar,
3025            cx,
3026            search_text: r"(that|used) ",
3027            search_options: Some(SearchOptions::REGEX),
3028            replacement_text: r"$1\n",
3029            replace_all: true,
3030            expected_text: r#"
3031            A regular \n (shortened as regex \
3032            \\ regexp;[1] also referred to as
3033            rational \n[2][3]) is a sequence of characters that
3034            specifies a search
3035            pattern in text. Usually such patterns are used
3036            by string-searching algorithms
3037            for "find" or "find and replace" operations on strings, or for input validation.
3038            "#
3039            .unindent(),
3040        })
3041        .await;
3042    }
3043
3044    #[perf]
3045    #[gpui::test]
3046    async fn test_find_matches_in_selections_singleton_buffer_multiple_selections(
3047        cx: &mut TestAppContext,
3048    ) {
3049        init_globals(cx);
3050        let buffer = cx.new(|cx| {
3051            Buffer::local(
3052                r#"
3053                aaa bbb aaa ccc
3054                aaa bbb aaa ccc
3055                aaa bbb aaa ccc
3056                aaa bbb aaa ccc
3057                aaa bbb aaa ccc
3058                aaa bbb aaa ccc
3059                "#
3060                .unindent(),
3061                cx,
3062            )
3063        });
3064        let cx = cx.add_empty_window();
3065        let editor =
3066            cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
3067
3068        let search_bar = cx.new_window_entity(|window, cx| {
3069            let mut search_bar = BufferSearchBar::new(None, window, cx);
3070            search_bar.set_active_pane_item(Some(&editor), window, cx);
3071            search_bar.show(window, cx);
3072            search_bar
3073        });
3074
3075        editor.update_in(cx, |editor, window, cx| {
3076            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3077                s.select_ranges(vec![Point::new(1, 0)..Point::new(2, 4)])
3078            })
3079        });
3080
3081        search_bar.update_in(cx, |search_bar, window, cx| {
3082            let deploy = Deploy {
3083                focus: true,
3084                replace_enabled: false,
3085                selection_search_enabled: true,
3086            };
3087            search_bar.deploy(&deploy, window, cx);
3088        });
3089
3090        cx.run_until_parked();
3091
3092        search_bar
3093            .update_in(cx, |search_bar, window, cx| {
3094                search_bar.search("aaa", None, true, window, cx)
3095            })
3096            .await
3097            .unwrap();
3098
3099        editor.update(cx, |editor, cx| {
3100            assert_eq!(
3101                editor.search_background_highlights(cx),
3102                &[
3103                    Point::new(1, 0)..Point::new(1, 3),
3104                    Point::new(1, 8)..Point::new(1, 11),
3105                    Point::new(2, 0)..Point::new(2, 3),
3106                ]
3107            );
3108        });
3109    }
3110
3111    #[perf]
3112    #[gpui::test]
3113    async fn test_find_matches_in_selections_multiple_excerpts_buffer_multiple_selections(
3114        cx: &mut TestAppContext,
3115    ) {
3116        init_globals(cx);
3117        let text = r#"
3118            aaa bbb aaa ccc
3119            aaa bbb aaa ccc
3120            aaa bbb aaa ccc
3121            aaa bbb aaa ccc
3122            aaa bbb aaa ccc
3123            aaa bbb aaa ccc
3124
3125            aaa bbb aaa ccc
3126            aaa bbb aaa ccc
3127            aaa bbb aaa ccc
3128            aaa bbb aaa ccc
3129            aaa bbb aaa ccc
3130            aaa bbb aaa ccc
3131            "#
3132        .unindent();
3133
3134        let cx = cx.add_empty_window();
3135        let editor = cx.new_window_entity(|window, cx| {
3136            let multibuffer = MultiBuffer::build_multi(
3137                [
3138                    (
3139                        &text,
3140                        vec![
3141                            Point::new(0, 0)..Point::new(2, 0),
3142                            Point::new(4, 0)..Point::new(5, 0),
3143                        ],
3144                    ),
3145                    (&text, vec![Point::new(9, 0)..Point::new(11, 0)]),
3146                ],
3147                cx,
3148            );
3149            Editor::for_multibuffer(multibuffer, None, window, cx)
3150        });
3151
3152        let search_bar = cx.new_window_entity(|window, cx| {
3153            let mut search_bar = BufferSearchBar::new(None, window, cx);
3154            search_bar.set_active_pane_item(Some(&editor), window, cx);
3155            search_bar.show(window, cx);
3156            search_bar
3157        });
3158
3159        editor.update_in(cx, |editor, window, cx| {
3160            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3161                s.select_ranges(vec![
3162                    Point::new(1, 0)..Point::new(1, 4),
3163                    Point::new(5, 3)..Point::new(6, 4),
3164                ])
3165            })
3166        });
3167
3168        search_bar.update_in(cx, |search_bar, window, cx| {
3169            let deploy = Deploy {
3170                focus: true,
3171                replace_enabled: false,
3172                selection_search_enabled: true,
3173            };
3174            search_bar.deploy(&deploy, window, cx);
3175        });
3176
3177        cx.run_until_parked();
3178
3179        search_bar
3180            .update_in(cx, |search_bar, window, cx| {
3181                search_bar.search("aaa", None, true, window, cx)
3182            })
3183            .await
3184            .unwrap();
3185
3186        editor.update(cx, |editor, cx| {
3187            assert_eq!(
3188                editor.search_background_highlights(cx),
3189                &[
3190                    Point::new(1, 0)..Point::new(1, 3),
3191                    Point::new(5, 8)..Point::new(5, 11),
3192                    Point::new(6, 0)..Point::new(6, 3),
3193                ]
3194            );
3195        });
3196    }
3197
3198    #[perf]
3199    #[gpui::test]
3200    async fn test_hides_and_uses_secondary_when_in_singleton_buffer(cx: &mut TestAppContext) {
3201        let (editor, search_bar, cx) = init_test(cx);
3202
3203        let initial_location = search_bar.update_in(cx, |search_bar, window, cx| {
3204            search_bar.set_active_pane_item(Some(&editor), window, cx)
3205        });
3206
3207        assert_eq!(initial_location, ToolbarItemLocation::Secondary);
3208
3209        let mut events = cx.events(&search_bar);
3210
3211        search_bar.update_in(cx, |search_bar, window, cx| {
3212            search_bar.dismiss(&Dismiss, window, cx);
3213        });
3214
3215        assert_eq!(
3216            events.try_next().unwrap(),
3217            Some(ToolbarItemEvent::ChangeLocation(
3218                ToolbarItemLocation::Hidden
3219            ))
3220        );
3221
3222        search_bar.update_in(cx, |search_bar, window, cx| {
3223            search_bar.show(window, cx);
3224        });
3225
3226        assert_eq!(
3227            events.try_next().unwrap(),
3228            Some(ToolbarItemEvent::ChangeLocation(
3229                ToolbarItemLocation::Secondary
3230            ))
3231        );
3232    }
3233
3234    #[perf]
3235    #[gpui::test]
3236    async fn test_uses_primary_left_when_in_multi_buffer(cx: &mut TestAppContext) {
3237        let (editor, search_bar, cx) = init_multibuffer_test(cx);
3238
3239        let initial_location = search_bar.update_in(cx, |search_bar, window, cx| {
3240            search_bar.set_active_pane_item(Some(&editor), window, cx)
3241        });
3242
3243        assert_eq!(initial_location, ToolbarItemLocation::PrimaryLeft);
3244
3245        let mut events = cx.events(&search_bar);
3246
3247        search_bar.update_in(cx, |search_bar, window, cx| {
3248            search_bar.dismiss(&Dismiss, window, cx);
3249        });
3250
3251        assert_eq!(
3252            events.try_next().unwrap(),
3253            Some(ToolbarItemEvent::ChangeLocation(
3254                ToolbarItemLocation::PrimaryLeft
3255            ))
3256        );
3257
3258        search_bar.update_in(cx, |search_bar, window, cx| {
3259            search_bar.show(window, cx);
3260        });
3261
3262        assert_eq!(
3263            events.try_next().unwrap(),
3264            Some(ToolbarItemEvent::ChangeLocation(
3265                ToolbarItemLocation::PrimaryLeft
3266            ))
3267        );
3268    }
3269
3270    #[perf]
3271    #[gpui::test]
3272    async fn test_hides_and_uses_secondary_when_part_of_project_search(cx: &mut TestAppContext) {
3273        let (editor, search_bar, cx) = init_multibuffer_test(cx);
3274
3275        editor.update(cx, |editor, _| {
3276            editor.set_in_project_search(true);
3277        });
3278
3279        let initial_location = search_bar.update_in(cx, |search_bar, window, cx| {
3280            search_bar.set_active_pane_item(Some(&editor), window, cx)
3281        });
3282
3283        assert_eq!(initial_location, ToolbarItemLocation::Hidden);
3284
3285        let mut events = cx.events(&search_bar);
3286
3287        search_bar.update_in(cx, |search_bar, window, cx| {
3288            search_bar.dismiss(&Dismiss, window, cx);
3289        });
3290
3291        assert_eq!(
3292            events.try_next().unwrap(),
3293            Some(ToolbarItemEvent::ChangeLocation(
3294                ToolbarItemLocation::Hidden
3295            ))
3296        );
3297
3298        search_bar.update_in(cx, |search_bar, window, cx| {
3299            search_bar.show(window, cx);
3300        });
3301
3302        assert_eq!(
3303            events.try_next().unwrap(),
3304            Some(ToolbarItemEvent::ChangeLocation(
3305                ToolbarItemLocation::Secondary
3306            ))
3307        );
3308    }
3309
3310    #[perf]
3311    #[gpui::test]
3312    async fn test_sets_collapsed_when_editor_fold_events_emitted(cx: &mut TestAppContext) {
3313        let (editor, search_bar, cx) = init_multibuffer_test(cx);
3314
3315        search_bar.update_in(cx, |search_bar, window, cx| {
3316            search_bar.set_active_pane_item(Some(&editor), window, cx);
3317        });
3318
3319        editor.update_in(cx, |editor, window, cx| {
3320            editor.fold_all(&FoldAll, window, cx);
3321        });
3322
3323        let is_collapsed = search_bar.read_with(cx, |search_bar, _| search_bar.is_collapsed);
3324
3325        assert!(is_collapsed);
3326
3327        editor.update_in(cx, |editor, window, cx| {
3328            editor.unfold_all(&UnfoldAll, window, cx);
3329        });
3330
3331        let is_collapsed = search_bar.read_with(cx, |search_bar, _| search_bar.is_collapsed);
3332
3333        assert!(!is_collapsed);
3334    }
3335
3336    #[perf]
3337    #[gpui::test]
3338    async fn test_search_options_changes(cx: &mut TestAppContext) {
3339        let (_editor, search_bar, cx) = init_test(cx);
3340        update_search_settings(
3341            SearchSettings {
3342                button: true,
3343                whole_word: false,
3344                case_sensitive: false,
3345                include_ignored: false,
3346                regex: false,
3347                center_on_match: false,
3348            },
3349            cx,
3350        );
3351
3352        let deploy = Deploy {
3353            focus: true,
3354            replace_enabled: false,
3355            selection_search_enabled: true,
3356        };
3357
3358        search_bar.update_in(cx, |search_bar, window, cx| {
3359            assert_eq!(
3360                search_bar.search_options,
3361                SearchOptions::NONE,
3362                "Should have no search options enabled by default"
3363            );
3364            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
3365            assert_eq!(
3366                search_bar.search_options,
3367                SearchOptions::WHOLE_WORD,
3368                "Should enable the option toggled"
3369            );
3370            assert!(
3371                !search_bar.dismissed,
3372                "Search bar should be present and visible"
3373            );
3374            search_bar.deploy(&deploy, window, cx);
3375            assert_eq!(
3376                search_bar.search_options,
3377                SearchOptions::WHOLE_WORD,
3378                "After (re)deploying, the option should still be enabled"
3379            );
3380
3381            search_bar.dismiss(&Dismiss, window, cx);
3382            search_bar.deploy(&deploy, window, cx);
3383            assert_eq!(
3384                search_bar.search_options,
3385                SearchOptions::WHOLE_WORD,
3386                "After hiding and showing the search bar, search options should be preserved"
3387            );
3388
3389            search_bar.toggle_search_option(SearchOptions::REGEX, window, cx);
3390            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
3391            assert_eq!(
3392                search_bar.search_options,
3393                SearchOptions::REGEX,
3394                "Should enable the options toggled"
3395            );
3396            assert!(
3397                !search_bar.dismissed,
3398                "Search bar should be present and visible"
3399            );
3400            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
3401        });
3402
3403        update_search_settings(
3404            SearchSettings {
3405                button: true,
3406                whole_word: false,
3407                case_sensitive: true,
3408                include_ignored: false,
3409                regex: false,
3410                center_on_match: false,
3411            },
3412            cx,
3413        );
3414        search_bar.update_in(cx, |search_bar, window, cx| {
3415            assert_eq!(
3416                search_bar.search_options,
3417                SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
3418                "Should have no search options enabled by default"
3419            );
3420
3421            search_bar.deploy(&deploy, window, cx);
3422            assert_eq!(
3423                search_bar.search_options,
3424                SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
3425                "Toggling a non-dismissed search bar with custom options should not change the default options"
3426            );
3427            search_bar.dismiss(&Dismiss, window, cx);
3428            search_bar.deploy(&deploy, window, cx);
3429            assert_eq!(
3430                search_bar.configured_options,
3431                SearchOptions::CASE_SENSITIVE,
3432                "After a settings update and toggling the search bar, configured options should be updated"
3433            );
3434            assert_eq!(
3435                search_bar.search_options,
3436                SearchOptions::CASE_SENSITIVE,
3437                "After a settings update and toggling the search bar, configured options should be used"
3438            );
3439        });
3440
3441        update_search_settings(
3442            SearchSettings {
3443                button: true,
3444                whole_word: true,
3445                case_sensitive: true,
3446                include_ignored: false,
3447                regex: false,
3448                center_on_match: false,
3449            },
3450            cx,
3451        );
3452
3453        search_bar.update_in(cx, |search_bar, window, cx| {
3454            search_bar.deploy(&deploy, window, cx);
3455            search_bar.dismiss(&Dismiss, window, cx);
3456            search_bar.show(window, cx);
3457            assert_eq!(
3458                search_bar.search_options,
3459                SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD,
3460                "Calling deploy on an already deployed search bar should not prevent settings updates from being detected"
3461            );
3462        });
3463    }
3464
3465    #[gpui::test]
3466    async fn test_select_occurrence_case_sensitivity(cx: &mut TestAppContext) {
3467        let (editor, search_bar, cx) = init_test(cx);
3468        let mut editor_cx = EditorTestContext::for_editor_in(editor, cx).await;
3469
3470        // Start with case sensitive search settings.
3471        let mut search_settings = SearchSettings::default();
3472        search_settings.case_sensitive = true;
3473        update_search_settings(search_settings, cx);
3474        search_bar.update(cx, |search_bar, cx| {
3475            let mut search_options = search_bar.search_options;
3476            search_options.insert(SearchOptions::CASE_SENSITIVE);
3477            search_bar.set_search_options(search_options, cx);
3478        });
3479
3480        editor_cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
3481        editor_cx.update_editor(|e, window, cx| {
3482            e.select_next(&Default::default(), window, cx).unwrap();
3483        });
3484        editor_cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
3485
3486        // Update the search bar's case sensitivite toggle, so we can later
3487        // confirm that `select_next` will now be case-insensitive.
3488        editor_cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
3489        search_bar.update_in(cx, |search_bar, window, cx| {
3490            search_bar.toggle_case_sensitive(&Default::default(), window, cx);
3491        });
3492        editor_cx.update_editor(|e, window, cx| {
3493            e.select_next(&Default::default(), window, cx).unwrap();
3494        });
3495        editor_cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\nFoo\nfoo");
3496
3497        // Confirm that, after dismissing the search bar, only the editor's
3498        // search settings actually affect the behavior of `select_next`.
3499        search_bar.update_in(cx, |search_bar, window, cx| {
3500            search_bar.dismiss(&Default::default(), window, cx);
3501        });
3502        editor_cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
3503        editor_cx.update_editor(|e, window, cx| {
3504            e.select_next(&Default::default(), window, cx).unwrap();
3505        });
3506        editor_cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
3507
3508        // Update the editor's search settings, disabling case sensitivity, to
3509        // check that the value is respected.
3510        let mut search_settings = SearchSettings::default();
3511        search_settings.case_sensitive = false;
3512        update_search_settings(search_settings, cx);
3513        editor_cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
3514        editor_cx.update_editor(|e, window, cx| {
3515            e.select_next(&Default::default(), window, cx).unwrap();
3516        });
3517        editor_cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\nFoo\nfoo");
3518    }
3519
3520    fn update_search_settings(search_settings: SearchSettings, cx: &mut TestAppContext) {
3521        cx.update(|cx| {
3522            SettingsStore::update_global(cx, |store, cx| {
3523                store.update_user_settings(cx, |settings| {
3524                    settings.editor.search = Some(SearchSettingsContent {
3525                        button: Some(search_settings.button),
3526                        whole_word: Some(search_settings.whole_word),
3527                        case_sensitive: Some(search_settings.case_sensitive),
3528                        include_ignored: Some(search_settings.include_ignored),
3529                        regex: Some(search_settings.regex),
3530                        center_on_match: Some(search_settings.center_on_match),
3531                    });
3532                });
3533            });
3534        });
3535    }
3536}