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