buffer_search.rs

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