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