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