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