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
1403            searchable_item.update_matches(matches, Some(new_match_index), *token, window, cx);
1404            searchable_item.activate_match(new_match_index, matches, *token, window, cx);
1405        }
1406    }
1407
1408    pub fn select_first_match(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1409        if let Some(searchable_item) = self.active_searchable_item.as_ref()
1410            && let Some((matches, token)) = self
1411                .searchable_items_with_matches
1412                .get(&searchable_item.downgrade())
1413        {
1414            if matches.is_empty() {
1415                return;
1416            }
1417            searchable_item.update_matches(matches, Some(0), *token, window, cx);
1418            searchable_item.activate_match(0, matches, *token, window, cx);
1419        }
1420    }
1421
1422    pub fn select_last_match(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1423        if let Some(searchable_item) = self.active_searchable_item.as_ref()
1424            && let Some((matches, token)) = self
1425                .searchable_items_with_matches
1426                .get(&searchable_item.downgrade())
1427        {
1428            if matches.is_empty() {
1429                return;
1430            }
1431            let new_match_index = matches.len() - 1;
1432            searchable_item.update_matches(matches, Some(new_match_index), *token, window, cx);
1433            searchable_item.activate_match(new_match_index, matches, *token, window, cx);
1434        }
1435    }
1436
1437    fn on_query_editor_event(
1438        &mut self,
1439        _editor: &Entity<Editor>,
1440        event: &editor::EditorEvent,
1441        window: &mut Window,
1442        cx: &mut Context<Self>,
1443    ) {
1444        match event {
1445            editor::EditorEvent::Focused => self.query_editor_focused = true,
1446            editor::EditorEvent::Blurred => self.query_editor_focused = false,
1447            editor::EditorEvent::Edited { .. } => {
1448                self.smartcase(window, cx);
1449                self.clear_matches(window, cx);
1450                let search = self.update_matches(false, true, window, cx);
1451
1452                cx.spawn_in(window, async move |this, cx| {
1453                    if search.await.is_ok() {
1454                        this.update_in(cx, |this, window, cx| {
1455                            this.activate_current_match(window, cx);
1456                            #[cfg(target_os = "macos")]
1457                            this.update_find_pasteboard(cx);
1458                        })?;
1459                    }
1460                    anyhow::Ok(())
1461                })
1462                .detach_and_log_err(cx);
1463            }
1464            _ => {}
1465        }
1466    }
1467
1468    fn on_replacement_editor_event(
1469        &mut self,
1470        _: Entity<Editor>,
1471        event: &editor::EditorEvent,
1472        _: &mut Context<Self>,
1473    ) {
1474        match event {
1475            editor::EditorEvent::Focused => self.replacement_editor_focused = true,
1476            editor::EditorEvent::Blurred => self.replacement_editor_focused = false,
1477            _ => {}
1478        }
1479    }
1480
1481    fn on_active_searchable_item_event(
1482        &mut self,
1483        event: &SearchEvent,
1484        window: &mut Window,
1485        cx: &mut Context<Self>,
1486    ) {
1487        match event {
1488            SearchEvent::MatchesInvalidated => {
1489                drop(self.update_matches(false, false, window, cx));
1490            }
1491            SearchEvent::ActiveMatchChanged => self.update_match_index(window, cx),
1492        }
1493    }
1494
1495    fn toggle_case_sensitive(
1496        &mut self,
1497        _: &ToggleCaseSensitive,
1498        window: &mut Window,
1499        cx: &mut Context<Self>,
1500    ) {
1501        self.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx)
1502    }
1503
1504    fn toggle_whole_word(
1505        &mut self,
1506        _: &ToggleWholeWord,
1507        window: &mut Window,
1508        cx: &mut Context<Self>,
1509    ) {
1510        self.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx)
1511    }
1512
1513    fn toggle_selection(
1514        &mut self,
1515        _: &ToggleSelection,
1516        window: &mut Window,
1517        cx: &mut Context<Self>,
1518    ) {
1519        self.set_search_within_selection(
1520            if let Some(_) = self.selection_search_enabled {
1521                None
1522            } else {
1523                Some(FilteredSearchRange::Default)
1524            },
1525            window,
1526            cx,
1527        );
1528    }
1529
1530    fn toggle_regex(&mut self, _: &ToggleRegex, window: &mut Window, cx: &mut Context<Self>) {
1531        self.toggle_search_option(SearchOptions::REGEX, window, cx)
1532    }
1533
1534    fn clear_active_searchable_item_matches(&mut self, window: &mut Window, cx: &mut App) {
1535        if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
1536            self.active_match_index = None;
1537            self.searchable_items_with_matches
1538                .remove(&active_searchable_item.downgrade());
1539            active_searchable_item.clear_matches(window, cx);
1540        }
1541    }
1542
1543    pub fn has_active_match(&self) -> bool {
1544        self.active_match_index.is_some()
1545    }
1546
1547    fn clear_matches(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1548        let mut active_item_matches = None;
1549        for (searchable_item, matches) in self.searchable_items_with_matches.drain() {
1550            if let Some(searchable_item) =
1551                WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
1552            {
1553                if Some(&searchable_item) == self.active_searchable_item.as_ref() {
1554                    active_item_matches = Some((searchable_item.downgrade(), matches));
1555                } else {
1556                    searchable_item.clear_matches(window, cx);
1557                }
1558            }
1559        }
1560
1561        self.searchable_items_with_matches
1562            .extend(active_item_matches);
1563    }
1564
1565    fn update_matches(
1566        &mut self,
1567        reuse_existing_query: bool,
1568        add_to_history: bool,
1569        window: &mut Window,
1570        cx: &mut Context<Self>,
1571    ) -> oneshot::Receiver<()> {
1572        let (done_tx, done_rx) = oneshot::channel();
1573        let query = self.query(cx);
1574        self.pending_search.take();
1575        #[cfg(target_os = "macos")]
1576        self.pending_external_query.take();
1577
1578        if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
1579            self.query_error = None;
1580            if query.is_empty() {
1581                self.clear_active_searchable_item_matches(window, cx);
1582                let _ = done_tx.send(());
1583                cx.notify();
1584            } else {
1585                let query: Arc<_> = if let Some(search) =
1586                    self.active_search.take().filter(|_| reuse_existing_query)
1587                {
1588                    search
1589                } else {
1590                    // Value doesn't matter, we only construct empty matchers with it
1591
1592                    if self.search_options.contains(SearchOptions::REGEX) {
1593                        match SearchQuery::regex(
1594                            query,
1595                            self.search_options.contains(SearchOptions::WHOLE_WORD),
1596                            self.search_options.contains(SearchOptions::CASE_SENSITIVE),
1597                            false,
1598                            self.search_options
1599                                .contains(SearchOptions::ONE_MATCH_PER_LINE),
1600                            PathMatcher::default(),
1601                            PathMatcher::default(),
1602                            false,
1603                            None,
1604                        ) {
1605                            Ok(query) => query.with_replacement(self.replacement(cx)),
1606                            Err(e) => {
1607                                self.query_error = Some(e.to_string());
1608                                self.clear_active_searchable_item_matches(window, cx);
1609                                cx.notify();
1610                                return done_rx;
1611                            }
1612                        }
1613                    } else {
1614                        match SearchQuery::text(
1615                            query,
1616                            self.search_options.contains(SearchOptions::WHOLE_WORD),
1617                            self.search_options.contains(SearchOptions::CASE_SENSITIVE),
1618                            false,
1619                            PathMatcher::default(),
1620                            PathMatcher::default(),
1621                            false,
1622                            None,
1623                        ) {
1624                            Ok(query) => query.with_replacement(self.replacement(cx)),
1625                            Err(e) => {
1626                                self.query_error = Some(e.to_string());
1627                                self.clear_active_searchable_item_matches(window, cx);
1628                                cx.notify();
1629                                return done_rx;
1630                            }
1631                        }
1632                    }
1633                    .into()
1634                };
1635
1636                self.active_search = Some(query.clone());
1637                let query_text = query.as_str().to_string();
1638
1639                let matches_with_token =
1640                    active_searchable_item.find_matches_with_token(query, window, cx);
1641
1642                let active_searchable_item = active_searchable_item.downgrade();
1643                self.pending_search = Some(cx.spawn_in(window, async move |this, cx| {
1644                    let (matches, token) = matches_with_token.await;
1645
1646                    this.update_in(cx, |this, window, cx| {
1647                        if let Some(active_searchable_item) =
1648                            WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx)
1649                        {
1650                            this.searchable_items_with_matches
1651                                .insert(active_searchable_item.downgrade(), (matches, token));
1652
1653                            this.update_match_index(window, cx);
1654
1655                            if add_to_history {
1656                                this.search_history
1657                                    .add(&mut this.search_history_cursor, query_text);
1658                            }
1659                            if !this.dismissed {
1660                                let (matches, token) = this
1661                                    .searchable_items_with_matches
1662                                    .get(&active_searchable_item.downgrade())
1663                                    .unwrap();
1664                                if matches.is_empty() {
1665                                    active_searchable_item.clear_matches(window, cx);
1666                                } else {
1667                                    active_searchable_item.update_matches(
1668                                        matches,
1669                                        this.active_match_index,
1670                                        *token,
1671                                        window,
1672                                        cx,
1673                                    );
1674                                }
1675                            }
1676                            let _ = done_tx.send(());
1677                            cx.notify();
1678                        }
1679                    })
1680                    .log_err();
1681                }));
1682            }
1683        }
1684        done_rx
1685    }
1686
1687    fn reverse_direction_if_backwards(&self, direction: Direction) -> Direction {
1688        if self.search_options.contains(SearchOptions::BACKWARDS) {
1689            direction.opposite()
1690        } else {
1691            direction
1692        }
1693    }
1694
1695    pub fn update_match_index(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1696        let direction = self.reverse_direction_if_backwards(Direction::Next);
1697        let new_index = self
1698            .active_searchable_item
1699            .as_ref()
1700            .and_then(|searchable_item| {
1701                let (matches, token) = self
1702                    .searchable_items_with_matches
1703                    .get(&searchable_item.downgrade())?;
1704                searchable_item.active_match_index(direction, matches, *token, window, cx)
1705            });
1706        if new_index != self.active_match_index {
1707            self.active_match_index = new_index;
1708            if !self.dismissed {
1709                if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1710                    if let Some((matches, token)) = self
1711                        .searchable_items_with_matches
1712                        .get(&searchable_item.downgrade())
1713                    {
1714                        if !matches.is_empty() {
1715                            searchable_item.update_matches(matches, new_index, *token, window, cx);
1716                        }
1717                    }
1718                }
1719            }
1720            cx.notify();
1721        }
1722    }
1723
1724    fn tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context<Self>) {
1725        self.cycle_field(Direction::Next, window, cx);
1726    }
1727
1728    fn backtab(&mut self, _: &Backtab, window: &mut Window, cx: &mut Context<Self>) {
1729        self.cycle_field(Direction::Prev, window, cx);
1730    }
1731    fn cycle_field(&mut self, direction: Direction, window: &mut Window, cx: &mut Context<Self>) {
1732        let mut handles = vec![self.query_editor.focus_handle(cx)];
1733        if self.replace_enabled {
1734            handles.push(self.replacement_editor.focus_handle(cx));
1735        }
1736        if let Some(item) = self.active_searchable_item.as_ref() {
1737            handles.push(item.item_focus_handle(cx));
1738        }
1739        let current_index = match handles.iter().position(|focus| focus.is_focused(window)) {
1740            Some(index) => index,
1741            None => return,
1742        };
1743
1744        let new_index = match direction {
1745            Direction::Next => (current_index + 1) % handles.len(),
1746            Direction::Prev if current_index == 0 => handles.len() - 1,
1747            Direction::Prev => (current_index - 1) % handles.len(),
1748        };
1749        let next_focus_handle = &handles[new_index];
1750        self.focus(next_focus_handle, window, cx);
1751        cx.stop_propagation();
1752    }
1753
1754    fn next_history_query(
1755        &mut self,
1756        _: &NextHistoryQuery,
1757        window: &mut Window,
1758        cx: &mut Context<Self>,
1759    ) {
1760        if !should_navigate_history(&self.query_editor, HistoryNavigationDirection::Next, cx) {
1761            cx.propagate();
1762            return;
1763        }
1764
1765        if let Some(new_query) = self
1766            .search_history
1767            .next(&mut self.search_history_cursor)
1768            .map(str::to_string)
1769        {
1770            drop(self.search(&new_query, Some(self.search_options), false, window, cx));
1771        } else if let Some(draft) = self.search_history_cursor.take_draft() {
1772            drop(self.search(&draft, Some(self.search_options), false, window, cx));
1773        }
1774    }
1775
1776    fn previous_history_query(
1777        &mut self,
1778        _: &PreviousHistoryQuery,
1779        window: &mut Window,
1780        cx: &mut Context<Self>,
1781    ) {
1782        if !should_navigate_history(&self.query_editor, HistoryNavigationDirection::Previous, cx) {
1783            cx.propagate();
1784            return;
1785        }
1786
1787        if self.query(cx).is_empty()
1788            && let Some(new_query) = self
1789                .search_history
1790                .current(&self.search_history_cursor)
1791                .map(str::to_string)
1792        {
1793            drop(self.search(&new_query, Some(self.search_options), false, window, cx));
1794            return;
1795        }
1796
1797        let current_query = self.query(cx);
1798        if let Some(new_query) = self
1799            .search_history
1800            .previous(&mut self.search_history_cursor, &current_query)
1801            .map(str::to_string)
1802        {
1803            drop(self.search(&new_query, Some(self.search_options), false, window, cx));
1804        }
1805    }
1806
1807    fn focus(&self, handle: &gpui::FocusHandle, window: &mut Window, cx: &mut App) {
1808        window.invalidate_character_coordinates();
1809        window.focus(handle, cx);
1810    }
1811
1812    fn toggle_replace(&mut self, _: &ToggleReplace, window: &mut Window, cx: &mut Context<Self>) {
1813        if self.active_searchable_item.is_some() {
1814            self.replace_enabled = !self.replace_enabled;
1815            let handle = if self.replace_enabled {
1816                self.replacement_editor.focus_handle(cx)
1817            } else {
1818                self.query_editor.focus_handle(cx)
1819            };
1820            self.focus(&handle, window, cx);
1821            cx.notify();
1822        }
1823    }
1824
1825    fn replace_next(&mut self, _: &ReplaceNext, window: &mut Window, cx: &mut Context<Self>) {
1826        let mut should_propagate = true;
1827        if !self.dismissed
1828            && self.active_search.is_some()
1829            && let Some(searchable_item) = self.active_searchable_item.as_ref()
1830            && let Some(query) = self.active_search.as_ref()
1831            && let Some((matches, token)) = self
1832                .searchable_items_with_matches
1833                .get(&searchable_item.downgrade())
1834        {
1835            if let Some(active_index) = self.active_match_index {
1836                let query = query
1837                    .as_ref()
1838                    .clone()
1839                    .with_replacement(self.replacement(cx));
1840                searchable_item.replace(matches.at(active_index), &query, *token, window, cx);
1841                self.select_next_match(&SelectNextMatch, window, cx);
1842            }
1843            should_propagate = false;
1844        }
1845        if !should_propagate {
1846            cx.stop_propagation();
1847        }
1848    }
1849
1850    pub fn replace_all(&mut self, _: &ReplaceAll, window: &mut Window, cx: &mut Context<Self>) {
1851        if !self.dismissed
1852            && self.active_search.is_some()
1853            && let Some(searchable_item) = self.active_searchable_item.as_ref()
1854            && let Some(query) = self.active_search.as_ref()
1855            && let Some((matches, token)) = self
1856                .searchable_items_with_matches
1857                .get(&searchable_item.downgrade())
1858        {
1859            let query = query
1860                .as_ref()
1861                .clone()
1862                .with_replacement(self.replacement(cx));
1863            searchable_item.replace_all(&mut matches.iter(), &query, *token, window, cx);
1864        }
1865    }
1866
1867    pub fn match_exists(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
1868        self.update_match_index(window, cx);
1869        self.active_match_index.is_some()
1870    }
1871
1872    pub fn should_use_smartcase_search(&mut self, cx: &mut Context<Self>) -> bool {
1873        EditorSettings::get_global(cx).use_smartcase_search
1874    }
1875
1876    pub fn is_contains_uppercase(&mut self, str: &String) -> bool {
1877        str.chars().any(|c| c.is_uppercase())
1878    }
1879
1880    fn smartcase(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1881        if self.should_use_smartcase_search(cx) {
1882            let query = self.query(cx);
1883            if !query.is_empty() {
1884                let is_case = self.is_contains_uppercase(&query);
1885                if self.has_search_option(SearchOptions::CASE_SENSITIVE) != is_case {
1886                    self.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
1887                }
1888            }
1889        }
1890    }
1891
1892    fn adjust_query_regex_language(&self, cx: &mut App) {
1893        let enable = self.search_options.contains(SearchOptions::REGEX);
1894        let query_buffer = self
1895            .query_editor
1896            .read(cx)
1897            .buffer()
1898            .read(cx)
1899            .as_singleton()
1900            .expect("query editor should be backed by a singleton buffer");
1901
1902        if enable {
1903            if let Some(regex_language) = self.regex_language.clone() {
1904                query_buffer.update(cx, |query_buffer, cx| {
1905                    query_buffer.set_language(Some(regex_language), cx);
1906                })
1907            }
1908        } else {
1909            query_buffer.update(cx, |query_buffer, cx| {
1910                query_buffer.set_language(None, cx);
1911            })
1912        }
1913    }
1914
1915    /// Updates the searchable item's case sensitivity option to match the
1916    /// search bar's current case sensitivity setting. This ensures that
1917    /// editor's `select_next`/ `select_previous` operations respect the buffer
1918    /// search bar's search options.
1919    ///
1920    /// Clears the case sensitivity when the search bar is dismissed so that
1921    /// only the editor's settings are respected.
1922    fn sync_select_next_case_sensitivity(&self, cx: &mut Context<Self>) {
1923        let case_sensitive = match self.dismissed {
1924            true => None,
1925            false => Some(self.search_options.contains(SearchOptions::CASE_SENSITIVE)),
1926        };
1927
1928        if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
1929            active_searchable_item.set_search_is_case_sensitive(case_sensitive, cx);
1930        }
1931    }
1932}
1933
1934#[cfg(test)]
1935mod tests {
1936    use std::{ops::Range, time::Duration};
1937
1938    use super::*;
1939    use editor::{
1940        DisplayPoint, Editor, HighlightKey, MultiBuffer, PathKey,
1941        SELECTION_HIGHLIGHT_DEBOUNCE_TIMEOUT, SearchSettings, SelectionEffects,
1942        display_map::DisplayRow, test::editor_test_context::EditorTestContext,
1943    };
1944    use gpui::{Hsla, TestAppContext, UpdateGlobal, VisualTestContext};
1945    use language::{Buffer, Point};
1946    use settings::{SearchSettingsContent, SettingsStore};
1947    use smol::stream::StreamExt as _;
1948    use unindent::Unindent as _;
1949    use util_macros::perf;
1950
1951    fn init_globals(cx: &mut TestAppContext) {
1952        cx.update(|cx| {
1953            let store = settings::SettingsStore::test(cx);
1954            cx.set_global(store);
1955            editor::init(cx);
1956
1957            theme_settings::init(theme::LoadThemes::JustBase, cx);
1958            crate::init(cx);
1959        });
1960    }
1961
1962    fn init_multibuffer_test(
1963        cx: &mut TestAppContext,
1964    ) -> (
1965        Entity<Editor>,
1966        Entity<BufferSearchBar>,
1967        &mut VisualTestContext,
1968    ) {
1969        init_globals(cx);
1970
1971        let buffer1 = cx.new(|cx| {
1972            Buffer::local(
1973                            r#"
1974                            A regular expression (shortened as regex or regexp;[1] also referred to as
1975                            rational expression[2][3]) is a sequence of characters that specifies a search
1976                            pattern in text. Usually such patterns are used by string-searching algorithms
1977                            for "find" or "find and replace" operations on strings, or for input validation.
1978                            "#
1979                            .unindent(),
1980                            cx,
1981                        )
1982        });
1983
1984        let buffer2 = cx.new(|cx| {
1985            Buffer::local(
1986                r#"
1987                            Some Additional text with the term regular expression in it.
1988                            There two lines.
1989                            "#
1990                .unindent(),
1991                cx,
1992            )
1993        });
1994
1995        let multibuffer = cx.new(|cx| {
1996            let mut buffer = MultiBuffer::new(language::Capability::ReadWrite);
1997
1998            //[ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))]
1999            buffer.set_excerpts_for_path(
2000                PathKey::sorted(0),
2001                buffer1,
2002                [Point::new(0, 0)..Point::new(3, 0)],
2003                0,
2004                cx,
2005            );
2006            buffer.set_excerpts_for_path(
2007                PathKey::sorted(1),
2008                buffer2,
2009                [Point::new(0, 0)..Point::new(1, 0)],
2010                0,
2011                cx,
2012            );
2013
2014            buffer
2015        });
2016        let mut editor = None;
2017        let window = cx.add_window(|window, cx| {
2018            let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
2019                "keymaps/default-macos.json",
2020                cx,
2021            )
2022            .unwrap();
2023            cx.bind_keys(default_key_bindings);
2024            editor =
2025                Some(cx.new(|cx| Editor::for_multibuffer(multibuffer.clone(), None, window, cx)));
2026
2027            let mut search_bar = BufferSearchBar::new(None, window, cx);
2028            search_bar.set_active_pane_item(Some(&editor.clone().unwrap()), window, cx);
2029            search_bar.show(window, cx);
2030            search_bar
2031        });
2032        let search_bar = window.root(cx).unwrap();
2033
2034        let cx = VisualTestContext::from_window(*window, cx).into_mut();
2035
2036        (editor.unwrap(), search_bar, cx)
2037    }
2038
2039    fn init_test(
2040        cx: &mut TestAppContext,
2041    ) -> (
2042        Entity<Editor>,
2043        Entity<BufferSearchBar>,
2044        &mut VisualTestContext,
2045    ) {
2046        init_globals(cx);
2047        let buffer = cx.new(|cx| {
2048            Buffer::local(
2049                r#"
2050                A regular expression (shortened as regex or regexp;[1] also referred to as
2051                rational expression[2][3]) is a sequence of characters that specifies a search
2052                pattern in text. Usually such patterns are used by string-searching algorithms
2053                for "find" or "find and replace" operations on strings, or for input validation.
2054                "#
2055                .unindent(),
2056                cx,
2057            )
2058        });
2059        let mut editor = None;
2060        let window = cx.add_window(|window, cx| {
2061            let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
2062                "keymaps/default-macos.json",
2063                cx,
2064            )
2065            .unwrap();
2066            cx.bind_keys(default_key_bindings);
2067            editor = Some(cx.new(|cx| Editor::for_buffer(buffer.clone(), None, window, cx)));
2068            let mut search_bar = BufferSearchBar::new(None, window, cx);
2069            search_bar.set_active_pane_item(Some(&editor.clone().unwrap()), window, cx);
2070            search_bar.show(window, cx);
2071            search_bar
2072        });
2073        let search_bar = window.root(cx).unwrap();
2074
2075        let cx = VisualTestContext::from_window(*window, cx).into_mut();
2076
2077        (editor.unwrap(), search_bar, cx)
2078    }
2079
2080    #[perf]
2081    #[gpui::test]
2082    async fn test_search_simple(cx: &mut TestAppContext) {
2083        let (editor, search_bar, cx) = init_test(cx);
2084        let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
2085            background_highlights
2086                .into_iter()
2087                .map(|(range, _)| range)
2088                .collect::<Vec<_>>()
2089        };
2090        // Search for a string that appears with different casing.
2091        // By default, search is case-insensitive.
2092        search_bar
2093            .update_in(cx, |search_bar, window, cx| {
2094                search_bar.search("us", None, true, window, cx)
2095            })
2096            .await
2097            .unwrap();
2098        editor.update_in(cx, |editor, window, cx| {
2099            assert_eq!(
2100                display_points_of(editor.all_text_background_highlights(window, cx)),
2101                &[
2102                    DisplayPoint::new(DisplayRow(2), 17)..DisplayPoint::new(DisplayRow(2), 19),
2103                    DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),
2104                ]
2105            );
2106        });
2107
2108        // Switch to a case sensitive search.
2109        search_bar.update_in(cx, |search_bar, window, cx| {
2110            search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
2111        });
2112        let mut editor_notifications = cx.notifications(&editor);
2113        editor_notifications.next().await;
2114        editor.update_in(cx, |editor, window, cx| {
2115            assert_eq!(
2116                display_points_of(editor.all_text_background_highlights(window, cx)),
2117                &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
2118            );
2119        });
2120
2121        // Search for a string that appears both as a whole word and
2122        // within other words. By default, all results are found.
2123        search_bar
2124            .update_in(cx, |search_bar, window, cx| {
2125                search_bar.search("or", None, true, window, cx)
2126            })
2127            .await
2128            .unwrap();
2129        editor.update_in(cx, |editor, window, cx| {
2130            assert_eq!(
2131                display_points_of(editor.all_text_background_highlights(window, cx)),
2132                &[
2133                    DisplayPoint::new(DisplayRow(0), 24)..DisplayPoint::new(DisplayRow(0), 26),
2134                    DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
2135                    DisplayPoint::new(DisplayRow(2), 71)..DisplayPoint::new(DisplayRow(2), 73),
2136                    DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 3),
2137                    DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
2138                    DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
2139                    DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 62),
2140                ]
2141            );
2142        });
2143
2144        // Switch to a whole word search.
2145        search_bar.update_in(cx, |search_bar, window, cx| {
2146            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
2147        });
2148        let mut editor_notifications = cx.notifications(&editor);
2149        editor_notifications.next().await;
2150        editor.update_in(cx, |editor, window, cx| {
2151            assert_eq!(
2152                display_points_of(editor.all_text_background_highlights(window, cx)),
2153                &[
2154                    DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
2155                    DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
2156                    DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
2157                ]
2158            );
2159        });
2160
2161        editor.update_in(cx, |editor, window, cx| {
2162            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2163                s.select_display_ranges([
2164                    DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2165                ])
2166            });
2167        });
2168        search_bar.update_in(cx, |search_bar, window, cx| {
2169            assert_eq!(search_bar.active_match_index, Some(0));
2170            search_bar.select_next_match(&SelectNextMatch, window, cx);
2171            assert_eq!(
2172                editor.update(cx, |editor, cx| editor
2173                    .selections
2174                    .display_ranges(&editor.display_snapshot(cx))),
2175                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
2176            );
2177        });
2178        search_bar.read_with(cx, |search_bar, _| {
2179            assert_eq!(search_bar.active_match_index, Some(0));
2180        });
2181
2182        search_bar.update_in(cx, |search_bar, window, cx| {
2183            search_bar.select_next_match(&SelectNextMatch, window, cx);
2184            assert_eq!(
2185                editor.update(cx, |editor, cx| editor
2186                    .selections
2187                    .display_ranges(&editor.display_snapshot(cx))),
2188                [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
2189            );
2190        });
2191        search_bar.read_with(cx, |search_bar, _| {
2192            assert_eq!(search_bar.active_match_index, Some(1));
2193        });
2194
2195        search_bar.update_in(cx, |search_bar, window, cx| {
2196            search_bar.select_next_match(&SelectNextMatch, window, cx);
2197            assert_eq!(
2198                editor.update(cx, |editor, cx| editor
2199                    .selections
2200                    .display_ranges(&editor.display_snapshot(cx))),
2201                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
2202            );
2203        });
2204        search_bar.read_with(cx, |search_bar, _| {
2205            assert_eq!(search_bar.active_match_index, Some(2));
2206        });
2207
2208        search_bar.update_in(cx, |search_bar, window, cx| {
2209            search_bar.select_next_match(&SelectNextMatch, window, cx);
2210            assert_eq!(
2211                editor.update(cx, |editor, cx| editor
2212                    .selections
2213                    .display_ranges(&editor.display_snapshot(cx))),
2214                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
2215            );
2216        });
2217        search_bar.read_with(cx, |search_bar, _| {
2218            assert_eq!(search_bar.active_match_index, Some(0));
2219        });
2220
2221        search_bar.update_in(cx, |search_bar, window, cx| {
2222            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2223            assert_eq!(
2224                editor.update(cx, |editor, cx| editor
2225                    .selections
2226                    .display_ranges(&editor.display_snapshot(cx))),
2227                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
2228            );
2229        });
2230        search_bar.read_with(cx, |search_bar, _| {
2231            assert_eq!(search_bar.active_match_index, Some(2));
2232        });
2233
2234        search_bar.update_in(cx, |search_bar, window, cx| {
2235            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2236            assert_eq!(
2237                editor.update(cx, |editor, cx| editor
2238                    .selections
2239                    .display_ranges(&editor.display_snapshot(cx))),
2240                [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
2241            );
2242        });
2243        search_bar.read_with(cx, |search_bar, _| {
2244            assert_eq!(search_bar.active_match_index, Some(1));
2245        });
2246
2247        search_bar.update_in(cx, |search_bar, window, cx| {
2248            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2249            assert_eq!(
2250                editor.update(cx, |editor, cx| editor
2251                    .selections
2252                    .display_ranges(&editor.display_snapshot(cx))),
2253                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
2254            );
2255        });
2256        search_bar.read_with(cx, |search_bar, _| {
2257            assert_eq!(search_bar.active_match_index, Some(0));
2258        });
2259
2260        // Park the cursor in between matches and ensure that going to the previous match selects
2261        // the closest match to the left.
2262        editor.update_in(cx, |editor, window, cx| {
2263            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2264                s.select_display_ranges([
2265                    DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
2266                ])
2267            });
2268        });
2269        search_bar.update_in(cx, |search_bar, window, cx| {
2270            assert_eq!(search_bar.active_match_index, Some(1));
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 selects the
2284        // closest match to the right.
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            assert_eq!(search_bar.active_match_index, Some(1));
2294            search_bar.select_next_match(&SelectNextMatch, window, cx);
2295            assert_eq!(
2296                editor.update(cx, |editor, cx| editor
2297                    .selections
2298                    .display_ranges(&editor.display_snapshot(cx))),
2299                [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
2300            );
2301        });
2302        search_bar.read_with(cx, |search_bar, _| {
2303            assert_eq!(search_bar.active_match_index, Some(1));
2304        });
2305
2306        // Park the cursor after the last match and ensure that going to the previous match selects
2307        // the last match.
2308        editor.update_in(cx, |editor, window, cx| {
2309            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2310                s.select_display_ranges([
2311                    DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
2312                ])
2313            });
2314        });
2315        search_bar.update_in(cx, |search_bar, window, cx| {
2316            assert_eq!(search_bar.active_match_index, Some(2));
2317            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2318            assert_eq!(
2319                editor.update(cx, |editor, cx| editor
2320                    .selections
2321                    .display_ranges(&editor.display_snapshot(cx))),
2322                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
2323            );
2324        });
2325        search_bar.read_with(cx, |search_bar, _| {
2326            assert_eq!(search_bar.active_match_index, Some(2));
2327        });
2328
2329        // Park the cursor after the last match and ensure that going to the next match selects the
2330        // first match.
2331        editor.update_in(cx, |editor, window, cx| {
2332            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2333                s.select_display_ranges([
2334                    DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
2335                ])
2336            });
2337        });
2338        search_bar.update_in(cx, |search_bar, window, cx| {
2339            assert_eq!(search_bar.active_match_index, Some(2));
2340            search_bar.select_next_match(&SelectNextMatch, window, cx);
2341            assert_eq!(
2342                editor.update(cx, |editor, cx| editor
2343                    .selections
2344                    .display_ranges(&editor.display_snapshot(cx))),
2345                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
2346            );
2347        });
2348        search_bar.read_with(cx, |search_bar, _| {
2349            assert_eq!(search_bar.active_match_index, Some(0));
2350        });
2351
2352        // Park the cursor before the first match and ensure that going to the previous match
2353        // selects the last match.
2354        editor.update_in(cx, |editor, window, cx| {
2355            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2356                s.select_display_ranges([
2357                    DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2358                ])
2359            });
2360        });
2361        search_bar.update_in(cx, |search_bar, window, cx| {
2362            assert_eq!(search_bar.active_match_index, Some(0));
2363            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2364            assert_eq!(
2365                editor.update(cx, |editor, cx| editor
2366                    .selections
2367                    .display_ranges(&editor.display_snapshot(cx))),
2368                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
2369            );
2370        });
2371        search_bar.read_with(cx, |search_bar, _| {
2372            assert_eq!(search_bar.active_match_index, Some(2));
2373        });
2374    }
2375
2376    fn display_points_of(
2377        background_highlights: Vec<(Range<DisplayPoint>, Hsla)>,
2378    ) -> Vec<Range<DisplayPoint>> {
2379        background_highlights
2380            .into_iter()
2381            .map(|(range, _)| range)
2382            .collect::<Vec<_>>()
2383    }
2384
2385    #[perf]
2386    #[gpui::test]
2387    async fn test_search_option_handling(cx: &mut TestAppContext) {
2388        let (editor, search_bar, cx) = init_test(cx);
2389
2390        // show with options should make current search case sensitive
2391        search_bar
2392            .update_in(cx, |search_bar, window, cx| {
2393                search_bar.show(window, cx);
2394                search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), true, window, cx)
2395            })
2396            .await
2397            .unwrap();
2398        editor.update_in(cx, |editor, window, cx| {
2399            assert_eq!(
2400                display_points_of(editor.all_text_background_highlights(window, cx)),
2401                &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
2402            );
2403        });
2404
2405        // search_suggested should restore default options
2406        search_bar.update_in(cx, |search_bar, window, cx| {
2407            search_bar.search_suggested(window, cx);
2408            assert_eq!(search_bar.search_options, SearchOptions::NONE)
2409        });
2410
2411        // toggling a search option should update the defaults
2412        search_bar
2413            .update_in(cx, |search_bar, window, cx| {
2414                search_bar.search(
2415                    "regex",
2416                    Some(SearchOptions::CASE_SENSITIVE),
2417                    true,
2418                    window,
2419                    cx,
2420                )
2421            })
2422            .await
2423            .unwrap();
2424        search_bar.update_in(cx, |search_bar, window, cx| {
2425            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx)
2426        });
2427        let mut editor_notifications = cx.notifications(&editor);
2428        editor_notifications.next().await;
2429        editor.update_in(cx, |editor, window, cx| {
2430            assert_eq!(
2431                display_points_of(editor.all_text_background_highlights(window, cx)),
2432                &[DisplayPoint::new(DisplayRow(0), 35)..DisplayPoint::new(DisplayRow(0), 40),]
2433            );
2434        });
2435
2436        // defaults should still include whole word
2437        search_bar.update_in(cx, |search_bar, window, cx| {
2438            search_bar.search_suggested(window, cx);
2439            assert_eq!(
2440                search_bar.search_options,
2441                SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD
2442            )
2443        });
2444    }
2445
2446    #[perf]
2447    #[gpui::test]
2448    async fn test_search_select_all_matches(cx: &mut TestAppContext) {
2449        init_globals(cx);
2450        let buffer_text = r#"
2451        A regular expression (shortened as regex or regexp;[1] also referred to as
2452        rational expression[2][3]) is a sequence of characters that specifies a search
2453        pattern in text. Usually such patterns are used by string-searching algorithms
2454        for "find" or "find and replace" operations on strings, or for input validation.
2455        "#
2456        .unindent();
2457        let expected_query_matches_count = buffer_text
2458            .chars()
2459            .filter(|c| c.eq_ignore_ascii_case(&'a'))
2460            .count();
2461        assert!(
2462            expected_query_matches_count > 1,
2463            "Should pick a query with multiple results"
2464        );
2465        let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
2466        let window = cx.add_window(|_, _| gpui::Empty);
2467
2468        let editor = window.build_entity(cx, |window, cx| {
2469            Editor::for_buffer(buffer.clone(), None, window, cx)
2470        });
2471
2472        let search_bar = window.build_entity(cx, |window, cx| {
2473            let mut search_bar = BufferSearchBar::new(None, window, cx);
2474            search_bar.set_active_pane_item(Some(&editor), window, cx);
2475            search_bar.show(window, cx);
2476            search_bar
2477        });
2478
2479        window
2480            .update(cx, |_, window, cx| {
2481                search_bar.update(cx, |search_bar, cx| {
2482                    search_bar.search("a", None, true, window, cx)
2483                })
2484            })
2485            .unwrap()
2486            .await
2487            .unwrap();
2488        let initial_selections = window
2489            .update(cx, |_, window, cx| {
2490                search_bar.update(cx, |search_bar, cx| {
2491                    let handle = search_bar.query_editor.focus_handle(cx);
2492                    window.focus(&handle, cx);
2493                    search_bar.activate_current_match(window, cx);
2494                });
2495                assert!(
2496                    !editor.read(cx).is_focused(window),
2497                    "Initially, the editor should not be focused"
2498                );
2499                let initial_selections = editor.update(cx, |editor, cx| {
2500                    let initial_selections = editor.selections.display_ranges(&editor.display_snapshot(cx));
2501                    assert_eq!(
2502                        initial_selections.len(), 1,
2503                        "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
2504                    );
2505                    initial_selections
2506                });
2507                search_bar.update(cx, |search_bar, cx| {
2508                    assert_eq!(search_bar.active_match_index, Some(0));
2509                    let handle = search_bar.query_editor.focus_handle(cx);
2510                    window.focus(&handle, cx);
2511                    search_bar.select_all_matches(&SelectAllMatches, window, cx);
2512                });
2513                assert!(
2514                    editor.read(cx).is_focused(window),
2515                    "Should focus editor after successful SelectAllMatches"
2516                );
2517                search_bar.update(cx, |search_bar, cx| {
2518                    let all_selections =
2519                        editor.update(cx, |editor, cx| editor.selections.display_ranges(&editor.display_snapshot(cx)));
2520                    assert_eq!(
2521                        all_selections.len(),
2522                        expected_query_matches_count,
2523                        "Should select all `a` characters in the buffer, but got: {all_selections:?}"
2524                    );
2525                    assert_eq!(
2526                        search_bar.active_match_index,
2527                        Some(0),
2528                        "Match index should not change after selecting all matches"
2529                    );
2530                });
2531
2532                search_bar.update(cx, |this, cx| this.select_next_match(&SelectNextMatch, window, cx));
2533                initial_selections
2534            }).unwrap();
2535
2536        window
2537            .update(cx, |_, window, cx| {
2538                assert!(
2539                    editor.read(cx).is_focused(window),
2540                    "Should still have editor focused after SelectNextMatch"
2541                );
2542                search_bar.update(cx, |search_bar, cx| {
2543                    let all_selections = editor.update(cx, |editor, cx| {
2544                        editor
2545                            .selections
2546                            .display_ranges(&editor.display_snapshot(cx))
2547                    });
2548                    assert_eq!(
2549                        all_selections.len(),
2550                        1,
2551                        "On next match, should deselect items and select the next match"
2552                    );
2553                    assert_ne!(
2554                        all_selections, initial_selections,
2555                        "Next match should be different from the first selection"
2556                    );
2557                    assert_eq!(
2558                        search_bar.active_match_index,
2559                        Some(1),
2560                        "Match index should be updated to the next one"
2561                    );
2562                    let handle = search_bar.query_editor.focus_handle(cx);
2563                    window.focus(&handle, cx);
2564                    search_bar.select_all_matches(&SelectAllMatches, window, cx);
2565                });
2566            })
2567            .unwrap();
2568        window
2569            .update(cx, |_, window, cx| {
2570                assert!(
2571                    editor.read(cx).is_focused(window),
2572                    "Should focus editor after successful SelectAllMatches"
2573                );
2574                search_bar.update(cx, |search_bar, cx| {
2575                    let all_selections =
2576                        editor.update(cx, |editor, cx| editor.selections.display_ranges(&editor.display_snapshot(cx)));
2577                    assert_eq!(
2578                    all_selections.len(),
2579                    expected_query_matches_count,
2580                    "Should select all `a` characters in the buffer, but got: {all_selections:?}"
2581                );
2582                    assert_eq!(
2583                        search_bar.active_match_index,
2584                        Some(1),
2585                        "Match index should not change after selecting all matches"
2586                    );
2587                });
2588                search_bar.update(cx, |search_bar, cx| {
2589                    search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2590                });
2591            })
2592            .unwrap();
2593        let last_match_selections = window
2594            .update(cx, |_, window, cx| {
2595                assert!(
2596                    editor.read(cx).is_focused(window),
2597                    "Should still have editor focused after SelectPreviousMatch"
2598                );
2599
2600                search_bar.update(cx, |search_bar, cx| {
2601                    let all_selections = editor.update(cx, |editor, cx| {
2602                        editor
2603                            .selections
2604                            .display_ranges(&editor.display_snapshot(cx))
2605                    });
2606                    assert_eq!(
2607                        all_selections.len(),
2608                        1,
2609                        "On previous match, should deselect items and select the previous item"
2610                    );
2611                    assert_eq!(
2612                        all_selections, initial_selections,
2613                        "Previous match should be the same as the first selection"
2614                    );
2615                    assert_eq!(
2616                        search_bar.active_match_index,
2617                        Some(0),
2618                        "Match index should be updated to the previous one"
2619                    );
2620                    all_selections
2621                })
2622            })
2623            .unwrap();
2624
2625        window
2626            .update(cx, |_, window, cx| {
2627                search_bar.update(cx, |search_bar, cx| {
2628                    let handle = search_bar.query_editor.focus_handle(cx);
2629                    window.focus(&handle, cx);
2630                    search_bar.search("abas_nonexistent_match", None, true, window, cx)
2631                })
2632            })
2633            .unwrap()
2634            .await
2635            .unwrap();
2636        window
2637            .update(cx, |_, window, cx| {
2638                search_bar.update(cx, |search_bar, cx| {
2639                    search_bar.select_all_matches(&SelectAllMatches, window, cx);
2640                });
2641                assert!(
2642                    editor.update(cx, |this, _cx| !this.is_focused(window)),
2643                    "Should not switch focus to editor if SelectAllMatches does not find any matches"
2644                );
2645                search_bar.update(cx, |search_bar, cx| {
2646                    let all_selections =
2647                        editor.update(cx, |editor, cx| editor.selections.display_ranges(&editor.display_snapshot(cx)));
2648                    assert_eq!(
2649                        all_selections, last_match_selections,
2650                        "Should not select anything new if there are no matches"
2651                    );
2652                    assert!(
2653                        search_bar.active_match_index.is_none(),
2654                        "For no matches, there should be no active match index"
2655                    );
2656                });
2657            })
2658            .unwrap();
2659    }
2660
2661    #[perf]
2662    #[gpui::test]
2663    async fn test_search_query_with_match_whole_word(cx: &mut TestAppContext) {
2664        init_globals(cx);
2665        let buffer_text = r#"
2666        self.buffer.update(cx, |buffer, cx| {
2667            buffer.edit(
2668                edits,
2669                Some(AutoindentMode::Block {
2670                    original_indent_columns,
2671                }),
2672                cx,
2673            )
2674        });
2675
2676        this.buffer.update(cx, |buffer, cx| {
2677            buffer.edit([(end_of_line..start_of_next_line, replace)], None, cx)
2678        });
2679        "#
2680        .unindent();
2681        let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
2682        let cx = cx.add_empty_window();
2683
2684        let editor =
2685            cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
2686
2687        let search_bar = cx.new_window_entity(|window, cx| {
2688            let mut search_bar = BufferSearchBar::new(None, window, cx);
2689            search_bar.set_active_pane_item(Some(&editor), window, cx);
2690            search_bar.show(window, cx);
2691            search_bar
2692        });
2693
2694        search_bar
2695            .update_in(cx, |search_bar, window, cx| {
2696                search_bar.search(
2697                    "edit\\(",
2698                    Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
2699                    true,
2700                    window,
2701                    cx,
2702                )
2703            })
2704            .await
2705            .unwrap();
2706
2707        search_bar.update_in(cx, |search_bar, window, cx| {
2708            search_bar.select_all_matches(&SelectAllMatches, window, cx);
2709        });
2710        search_bar.update(cx, |_, cx| {
2711            let all_selections = editor.update(cx, |editor, cx| {
2712                editor
2713                    .selections
2714                    .display_ranges(&editor.display_snapshot(cx))
2715            });
2716            assert_eq!(
2717                all_selections.len(),
2718                2,
2719                "Should select all `edit(` in the buffer, but got: {all_selections:?}"
2720            );
2721        });
2722
2723        search_bar
2724            .update_in(cx, |search_bar, window, cx| {
2725                search_bar.search(
2726                    "edit(",
2727                    Some(SearchOptions::WHOLE_WORD | SearchOptions::CASE_SENSITIVE),
2728                    true,
2729                    window,
2730                    cx,
2731                )
2732            })
2733            .await
2734            .unwrap();
2735
2736        search_bar.update_in(cx, |search_bar, window, cx| {
2737            search_bar.select_all_matches(&SelectAllMatches, window, cx);
2738        });
2739        search_bar.update(cx, |_, cx| {
2740            let all_selections = editor.update(cx, |editor, cx| {
2741                editor
2742                    .selections
2743                    .display_ranges(&editor.display_snapshot(cx))
2744            });
2745            assert_eq!(
2746                all_selections.len(),
2747                2,
2748                "Should select all `edit(` in the buffer, but got: {all_selections:?}"
2749            );
2750        });
2751    }
2752
2753    #[perf]
2754    #[gpui::test]
2755    async fn test_search_query_history(cx: &mut TestAppContext) {
2756        let (_editor, search_bar, cx) = init_test(cx);
2757
2758        // Add 3 search items into the history.
2759        search_bar
2760            .update_in(cx, |search_bar, window, cx| {
2761                search_bar.search("a", None, true, window, cx)
2762            })
2763            .await
2764            .unwrap();
2765        search_bar
2766            .update_in(cx, |search_bar, window, cx| {
2767                search_bar.search("b", None, true, window, cx)
2768            })
2769            .await
2770            .unwrap();
2771        search_bar
2772            .update_in(cx, |search_bar, window, cx| {
2773                search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), true, window, cx)
2774            })
2775            .await
2776            .unwrap();
2777        // Ensure that the latest search is active.
2778        search_bar.update(cx, |search_bar, cx| {
2779            assert_eq!(search_bar.query(cx), "c");
2780            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2781        });
2782
2783        // Next history query after the latest should preserve the current query.
2784        search_bar.update_in(cx, |search_bar, window, cx| {
2785            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2786        });
2787        cx.background_executor.run_until_parked();
2788        search_bar.update(cx, |search_bar, cx| {
2789            assert_eq!(search_bar.query(cx), "c");
2790            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2791        });
2792        search_bar.update_in(cx, |search_bar, window, cx| {
2793            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2794        });
2795        cx.background_executor.run_until_parked();
2796        search_bar.update(cx, |search_bar, cx| {
2797            assert_eq!(search_bar.query(cx), "c");
2798            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2799        });
2800
2801        // Previous query should navigate backwards through history.
2802        search_bar.update_in(cx, |search_bar, window, cx| {
2803            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2804        });
2805        cx.background_executor.run_until_parked();
2806        search_bar.update(cx, |search_bar, cx| {
2807            assert_eq!(search_bar.query(cx), "b");
2808            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2809        });
2810
2811        // Further previous items should go over the history in reverse order.
2812        search_bar.update_in(cx, |search_bar, window, cx| {
2813            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2814        });
2815        cx.background_executor.run_until_parked();
2816        search_bar.update(cx, |search_bar, cx| {
2817            assert_eq!(search_bar.query(cx), "a");
2818            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2819        });
2820
2821        // Previous items should never go behind the first history item.
2822        search_bar.update_in(cx, |search_bar, window, cx| {
2823            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2824        });
2825        cx.background_executor.run_until_parked();
2826        search_bar.update(cx, |search_bar, cx| {
2827            assert_eq!(search_bar.query(cx), "a");
2828            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2829        });
2830        search_bar.update_in(cx, |search_bar, window, cx| {
2831            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2832        });
2833        cx.background_executor.run_until_parked();
2834        search_bar.update(cx, |search_bar, cx| {
2835            assert_eq!(search_bar.query(cx), "a");
2836            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2837        });
2838
2839        // Next items should go over the history in the original order.
2840        search_bar.update_in(cx, |search_bar, window, cx| {
2841            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2842        });
2843        cx.background_executor.run_until_parked();
2844        search_bar.update(cx, |search_bar, cx| {
2845            assert_eq!(search_bar.query(cx), "b");
2846            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2847        });
2848
2849        search_bar
2850            .update_in(cx, |search_bar, window, cx| {
2851                search_bar.search("ba", None, true, window, cx)
2852            })
2853            .await
2854            .unwrap();
2855        search_bar.update(cx, |search_bar, cx| {
2856            assert_eq!(search_bar.query(cx), "ba");
2857            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2858        });
2859
2860        // New search input should add another entry to history and move the selection to the end of the history.
2861        search_bar.update_in(cx, |search_bar, window, cx| {
2862            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2863        });
2864        cx.background_executor.run_until_parked();
2865        search_bar.update(cx, |search_bar, cx| {
2866            assert_eq!(search_bar.query(cx), "c");
2867            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2868        });
2869        search_bar.update_in(cx, |search_bar, window, cx| {
2870            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2871        });
2872        cx.background_executor.run_until_parked();
2873        search_bar.update(cx, |search_bar, cx| {
2874            assert_eq!(search_bar.query(cx), "b");
2875            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2876        });
2877        search_bar.update_in(cx, |search_bar, window, cx| {
2878            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2879        });
2880        cx.background_executor.run_until_parked();
2881        search_bar.update(cx, |search_bar, cx| {
2882            assert_eq!(search_bar.query(cx), "c");
2883            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2884        });
2885        search_bar.update_in(cx, |search_bar, window, cx| {
2886            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2887        });
2888        cx.background_executor.run_until_parked();
2889        search_bar.update(cx, |search_bar, cx| {
2890            assert_eq!(search_bar.query(cx), "ba");
2891            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2892        });
2893        search_bar.update_in(cx, |search_bar, window, cx| {
2894            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2895        });
2896        cx.background_executor.run_until_parked();
2897        search_bar.update(cx, |search_bar, cx| {
2898            assert_eq!(search_bar.query(cx), "ba");
2899            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2900        });
2901    }
2902
2903    #[perf]
2904    #[gpui::test]
2905    async fn test_search_query_history_autoscroll(cx: &mut TestAppContext) {
2906        let (_editor, search_bar, cx) = init_test(cx);
2907
2908        // Add a long multi-line query that exceeds the editor's max
2909        // visible height (4 lines), then a short query.
2910        let long_query = "line1\nline2\nline3\nline4\nline5\nline6";
2911        search_bar
2912            .update_in(cx, |search_bar, window, cx| {
2913                search_bar.search(long_query, None, true, window, cx)
2914            })
2915            .await
2916            .unwrap();
2917        search_bar
2918            .update_in(cx, |search_bar, window, cx| {
2919                search_bar.search("short", None, true, window, cx)
2920            })
2921            .await
2922            .unwrap();
2923
2924        // Navigate back to the long entry. Since "short" is single-line,
2925        // the history navigation is allowed.
2926        search_bar.update_in(cx, |search_bar, window, cx| {
2927            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2928        });
2929        cx.background_executor.run_until_parked();
2930        search_bar.update(cx, |search_bar, cx| {
2931            assert_eq!(search_bar.query(cx), long_query);
2932        });
2933
2934        // The cursor should be scrolled into view despite the content
2935        // exceeding the editor's max visible height.
2936        search_bar.update_in(cx, |search_bar, window, cx| {
2937            let snapshot = search_bar
2938                .query_editor
2939                .update(cx, |editor, cx| editor.snapshot(window, cx));
2940            let cursor_row = search_bar
2941                .query_editor
2942                .read(cx)
2943                .selections
2944                .newest_display(&snapshot)
2945                .head()
2946                .row();
2947            let scroll_top = search_bar
2948                .query_editor
2949                .update(cx, |editor, cx| editor.scroll_position(cx).y);
2950            let visible_lines = search_bar
2951                .query_editor
2952                .read(cx)
2953                .visible_line_count()
2954                .unwrap_or(0.0);
2955            let scroll_bottom = scroll_top + visible_lines;
2956            assert!(
2957                (cursor_row.0 as f64) < scroll_bottom,
2958                "cursor row {cursor_row:?} should be visible (scroll range {scroll_top}..{scroll_bottom})"
2959            );
2960        });
2961    }
2962
2963    #[perf]
2964    #[gpui::test]
2965    async fn test_replace_simple(cx: &mut TestAppContext) {
2966        let (editor, search_bar, cx) = init_test(cx);
2967
2968        search_bar
2969            .update_in(cx, |search_bar, window, cx| {
2970                search_bar.search("expression", None, true, window, cx)
2971            })
2972            .await
2973            .unwrap();
2974
2975        search_bar.update_in(cx, |search_bar, window, cx| {
2976            search_bar.replacement_editor.update(cx, |editor, cx| {
2977                // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally.
2978                editor.set_text("expr$1", window, cx);
2979            });
2980            search_bar.replace_all(&ReplaceAll, window, cx)
2981        });
2982        assert_eq!(
2983            editor.read_with(cx, |this, cx| { this.text(cx) }),
2984            r#"
2985        A regular expr$1 (shortened as regex or regexp;[1] also referred to as
2986        rational expr$1[2][3]) is a sequence of characters that specifies a search
2987        pattern in text. Usually such patterns are used by string-searching algorithms
2988        for "find" or "find and replace" operations on strings, or for input validation.
2989        "#
2990            .unindent()
2991        );
2992
2993        // Search for word boundaries and replace just a single one.
2994        search_bar
2995            .update_in(cx, |search_bar, window, cx| {
2996                search_bar.search("or", Some(SearchOptions::WHOLE_WORD), true, window, cx)
2997            })
2998            .await
2999            .unwrap();
3000
3001        search_bar.update_in(cx, |search_bar, window, cx| {
3002            search_bar.replacement_editor.update(cx, |editor, cx| {
3003                editor.set_text("banana", window, cx);
3004            });
3005            search_bar.replace_next(&ReplaceNext, window, cx)
3006        });
3007        // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text.
3008        assert_eq!(
3009            editor.read_with(cx, |this, cx| { this.text(cx) }),
3010            r#"
3011        A regular expr$1 (shortened as regex banana regexp;[1] also referred to as
3012        rational expr$1[2][3]) is a sequence of characters that specifies a search
3013        pattern in text. Usually such patterns are used by string-searching algorithms
3014        for "find" or "find and replace" operations on strings, or for input validation.
3015        "#
3016            .unindent()
3017        );
3018        // Let's turn on regex mode.
3019        search_bar
3020            .update_in(cx, |search_bar, window, cx| {
3021                search_bar.search(
3022                    "\\[([^\\]]+)\\]",
3023                    Some(SearchOptions::REGEX),
3024                    true,
3025                    window,
3026                    cx,
3027                )
3028            })
3029            .await
3030            .unwrap();
3031        search_bar.update_in(cx, |search_bar, window, cx| {
3032            search_bar.replacement_editor.update(cx, |editor, cx| {
3033                editor.set_text("${1}number", window, cx);
3034            });
3035            search_bar.replace_all(&ReplaceAll, window, cx)
3036        });
3037        assert_eq!(
3038            editor.read_with(cx, |this, cx| { this.text(cx) }),
3039            r#"
3040        A regular expr$1 (shortened as regex banana regexp;1number also referred to as
3041        rational expr$12number3number) is a sequence of characters that specifies a search
3042        pattern in text. Usually such patterns are used by string-searching algorithms
3043        for "find" or "find and replace" operations on strings, or for input validation.
3044        "#
3045            .unindent()
3046        );
3047        // Now with a whole-word twist.
3048        search_bar
3049            .update_in(cx, |search_bar, window, cx| {
3050                search_bar.search(
3051                    "a\\w+s",
3052                    Some(SearchOptions::REGEX | SearchOptions::WHOLE_WORD),
3053                    true,
3054                    window,
3055                    cx,
3056                )
3057            })
3058            .await
3059            .unwrap();
3060        search_bar.update_in(cx, |search_bar, window, cx| {
3061            search_bar.replacement_editor.update(cx, |editor, cx| {
3062                editor.set_text("things", window, cx);
3063            });
3064            search_bar.replace_all(&ReplaceAll, window, cx)
3065        });
3066        // The only word affected by this edit should be `algorithms`, even though there's a bunch
3067        // of words in this text that would match this regex if not for WHOLE_WORD.
3068        assert_eq!(
3069            editor.read_with(cx, |this, cx| { this.text(cx) }),
3070            r#"
3071        A regular expr$1 (shortened as regex banana regexp;1number also referred to as
3072        rational expr$12number3number) is a sequence of characters that specifies a search
3073        pattern in text. Usually such patterns are used by string-searching things
3074        for "find" or "find and replace" operations on strings, or for input validation.
3075        "#
3076            .unindent()
3077        );
3078    }
3079
3080    #[gpui::test]
3081    async fn test_replace_focus(cx: &mut TestAppContext) {
3082        let (editor, search_bar, cx) = init_test(cx);
3083
3084        editor.update_in(cx, |editor, window, cx| {
3085            editor.set_text("What a bad day!", window, cx)
3086        });
3087
3088        search_bar
3089            .update_in(cx, |search_bar, window, cx| {
3090                search_bar.search("bad", None, true, window, cx)
3091            })
3092            .await
3093            .unwrap();
3094
3095        // Calling `toggle_replace` in the search bar ensures that the "Replace
3096        // *" buttons are rendered, so we can then simulate clicking the
3097        // buttons.
3098        search_bar.update_in(cx, |search_bar, window, cx| {
3099            search_bar.toggle_replace(&ToggleReplace, window, cx)
3100        });
3101
3102        search_bar.update_in(cx, |search_bar, window, cx| {
3103            search_bar.replacement_editor.update(cx, |editor, cx| {
3104                editor.set_text("great", window, cx);
3105            });
3106        });
3107
3108        // Focus on the editor instead of the search bar, as we want to ensure
3109        // that pressing the "Replace Next Match" button will work, even if the
3110        // search bar is not focused.
3111        cx.focus(&editor);
3112
3113        // We'll not simulate clicking the "Replace Next Match " button, asserting that
3114        // the replacement was done.
3115        let button_bounds = cx
3116            .debug_bounds("ICON-ReplaceNext")
3117            .expect("'Replace Next Match' button should be visible");
3118        cx.simulate_click(button_bounds.center(), gpui::Modifiers::none());
3119
3120        assert_eq!(
3121            editor.read_with(cx, |editor, cx| editor.text(cx)),
3122            "What a great day!"
3123        );
3124    }
3125
3126    struct ReplacementTestParams<'a> {
3127        editor: &'a Entity<Editor>,
3128        search_bar: &'a Entity<BufferSearchBar>,
3129        cx: &'a mut VisualTestContext,
3130        search_text: &'static str,
3131        search_options: Option<SearchOptions>,
3132        replacement_text: &'static str,
3133        replace_all: bool,
3134        expected_text: String,
3135    }
3136
3137    async fn run_replacement_test(options: ReplacementTestParams<'_>) {
3138        options
3139            .search_bar
3140            .update_in(options.cx, |search_bar, window, cx| {
3141                if let Some(options) = options.search_options {
3142                    search_bar.set_search_options(options, cx);
3143                }
3144                search_bar.search(
3145                    options.search_text,
3146                    options.search_options,
3147                    true,
3148                    window,
3149                    cx,
3150                )
3151            })
3152            .await
3153            .unwrap();
3154
3155        options
3156            .search_bar
3157            .update_in(options.cx, |search_bar, window, cx| {
3158                search_bar.replacement_editor.update(cx, |editor, cx| {
3159                    editor.set_text(options.replacement_text, window, cx);
3160                });
3161
3162                if options.replace_all {
3163                    search_bar.replace_all(&ReplaceAll, window, cx)
3164                } else {
3165                    search_bar.replace_next(&ReplaceNext, window, cx)
3166                }
3167            });
3168
3169        assert_eq!(
3170            options
3171                .editor
3172                .read_with(options.cx, |this, cx| { this.text(cx) }),
3173            options.expected_text
3174        );
3175    }
3176
3177    #[perf]
3178    #[gpui::test]
3179    async fn test_replace_special_characters(cx: &mut TestAppContext) {
3180        let (editor, search_bar, cx) = init_test(cx);
3181
3182        run_replacement_test(ReplacementTestParams {
3183            editor: &editor,
3184            search_bar: &search_bar,
3185            cx,
3186            search_text: "expression",
3187            search_options: None,
3188            replacement_text: r"\n",
3189            replace_all: true,
3190            expected_text: r#"
3191            A regular \n (shortened as regex or regexp;[1] also referred to as
3192            rational \n[2][3]) is a sequence of characters that specifies a search
3193            pattern in text. Usually such patterns are used by string-searching algorithms
3194            for "find" or "find and replace" operations on strings, or for input validation.
3195            "#
3196            .unindent(),
3197        })
3198        .await;
3199
3200        run_replacement_test(ReplacementTestParams {
3201            editor: &editor,
3202            search_bar: &search_bar,
3203            cx,
3204            search_text: "or",
3205            search_options: Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
3206            replacement_text: r"\\\n\\\\",
3207            replace_all: false,
3208            expected_text: r#"
3209            A regular \n (shortened as regex \
3210            \\ regexp;[1] also referred to as
3211            rational \n[2][3]) is a sequence of characters that specifies a search
3212            pattern in text. Usually such patterns are used by string-searching algorithms
3213            for "find" or "find and replace" operations on strings, or for input validation.
3214            "#
3215            .unindent(),
3216        })
3217        .await;
3218
3219        run_replacement_test(ReplacementTestParams {
3220            editor: &editor,
3221            search_bar: &search_bar,
3222            cx,
3223            search_text: r"(that|used) ",
3224            search_options: Some(SearchOptions::REGEX),
3225            replacement_text: r"$1\n",
3226            replace_all: true,
3227            expected_text: r#"
3228            A regular \n (shortened as regex \
3229            \\ regexp;[1] also referred to as
3230            rational \n[2][3]) is a sequence of characters that
3231            specifies a search
3232            pattern in text. Usually such patterns are used
3233            by string-searching algorithms
3234            for "find" or "find and replace" operations on strings, or for input validation.
3235            "#
3236            .unindent(),
3237        })
3238        .await;
3239    }
3240
3241    #[gpui::test]
3242    async fn test_deploy_replace_focuses_replacement_editor(cx: &mut TestAppContext) {
3243        init_globals(cx);
3244        let (editor, search_bar, cx) = init_test(cx);
3245
3246        editor.update_in(cx, |editor, window, cx| {
3247            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3248                s.select_display_ranges([
3249                    DisplayPoint::new(DisplayRow(0), 8)..DisplayPoint::new(DisplayRow(0), 16)
3250                ])
3251            });
3252        });
3253
3254        search_bar.update_in(cx, |search_bar, window, cx| {
3255            search_bar.deploy(
3256                &Deploy {
3257                    focus: true,
3258                    replace_enabled: true,
3259                    selection_search_enabled: false,
3260                },
3261                window,
3262                cx,
3263            );
3264        });
3265        cx.run_until_parked();
3266
3267        search_bar.update_in(cx, |search_bar, window, cx| {
3268            assert!(
3269                search_bar
3270                    .replacement_editor
3271                    .focus_handle(cx)
3272                    .is_focused(window),
3273                "replacement editor should be focused when deploying replace with a selection",
3274            );
3275            assert!(
3276                !search_bar.query_editor.focus_handle(cx).is_focused(window),
3277                "search editor should not be focused when replacement editor is focused",
3278            );
3279        });
3280    }
3281
3282    #[perf]
3283    #[gpui::test]
3284    async fn test_find_matches_in_selections_singleton_buffer_multiple_selections(
3285        cx: &mut TestAppContext,
3286    ) {
3287        init_globals(cx);
3288        let buffer = cx.new(|cx| {
3289            Buffer::local(
3290                r#"
3291                aaa bbb aaa ccc
3292                aaa bbb aaa ccc
3293                aaa bbb aaa ccc
3294                aaa bbb aaa ccc
3295                aaa bbb aaa ccc
3296                aaa bbb aaa ccc
3297                "#
3298                .unindent(),
3299                cx,
3300            )
3301        });
3302        let cx = cx.add_empty_window();
3303        let editor =
3304            cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
3305
3306        let search_bar = cx.new_window_entity(|window, cx| {
3307            let mut search_bar = BufferSearchBar::new(None, window, cx);
3308            search_bar.set_active_pane_item(Some(&editor), window, cx);
3309            search_bar.show(window, cx);
3310            search_bar
3311        });
3312
3313        editor.update_in(cx, |editor, window, cx| {
3314            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3315                s.select_ranges(vec![Point::new(1, 0)..Point::new(2, 4)])
3316            })
3317        });
3318
3319        search_bar.update_in(cx, |search_bar, window, cx| {
3320            let deploy = Deploy {
3321                focus: true,
3322                replace_enabled: false,
3323                selection_search_enabled: true,
3324            };
3325            search_bar.deploy(&deploy, window, cx);
3326        });
3327
3328        cx.run_until_parked();
3329
3330        search_bar
3331            .update_in(cx, |search_bar, window, cx| {
3332                search_bar.search("aaa", None, true, window, cx)
3333            })
3334            .await
3335            .unwrap();
3336
3337        editor.update(cx, |editor, cx| {
3338            assert_eq!(
3339                editor.search_background_highlights(cx),
3340                &[
3341                    Point::new(1, 0)..Point::new(1, 3),
3342                    Point::new(1, 8)..Point::new(1, 11),
3343                    Point::new(2, 0)..Point::new(2, 3),
3344                ]
3345            );
3346        });
3347    }
3348
3349    #[perf]
3350    #[gpui::test]
3351    async fn test_find_matches_in_selections_multiple_excerpts_buffer_multiple_selections(
3352        cx: &mut TestAppContext,
3353    ) {
3354        init_globals(cx);
3355        let text = r#"
3356            aaa bbb aaa ccc
3357            aaa bbb aaa ccc
3358            aaa bbb aaa ccc
3359            aaa bbb aaa ccc
3360            aaa bbb aaa ccc
3361            aaa bbb aaa ccc
3362
3363            aaa bbb aaa ccc
3364            aaa bbb aaa ccc
3365            aaa bbb aaa ccc
3366            aaa bbb aaa ccc
3367            aaa bbb aaa ccc
3368            aaa bbb aaa ccc
3369            "#
3370        .unindent();
3371
3372        let cx = cx.add_empty_window();
3373        let editor = cx.new_window_entity(|window, cx| {
3374            let multibuffer = MultiBuffer::build_multi(
3375                [
3376                    (
3377                        &text,
3378                        vec![
3379                            Point::new(0, 0)..Point::new(2, 0),
3380                            Point::new(4, 0)..Point::new(5, 0),
3381                        ],
3382                    ),
3383                    (&text, vec![Point::new(9, 0)..Point::new(11, 0)]),
3384                ],
3385                cx,
3386            );
3387            Editor::for_multibuffer(multibuffer, None, window, cx)
3388        });
3389
3390        let search_bar = cx.new_window_entity(|window, cx| {
3391            let mut search_bar = BufferSearchBar::new(None, window, cx);
3392            search_bar.set_active_pane_item(Some(&editor), window, cx);
3393            search_bar.show(window, cx);
3394            search_bar
3395        });
3396
3397        editor.update_in(cx, |editor, window, cx| {
3398            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3399                s.select_ranges(vec![
3400                    Point::new(1, 0)..Point::new(1, 4),
3401                    Point::new(5, 3)..Point::new(6, 4),
3402                ])
3403            })
3404        });
3405
3406        search_bar.update_in(cx, |search_bar, window, cx| {
3407            let deploy = Deploy {
3408                focus: true,
3409                replace_enabled: false,
3410                selection_search_enabled: true,
3411            };
3412            search_bar.deploy(&deploy, window, cx);
3413        });
3414
3415        cx.run_until_parked();
3416
3417        search_bar
3418            .update_in(cx, |search_bar, window, cx| {
3419                search_bar.search("aaa", None, true, window, cx)
3420            })
3421            .await
3422            .unwrap();
3423
3424        editor.update(cx, |editor, cx| {
3425            assert_eq!(
3426                editor.search_background_highlights(cx),
3427                &[
3428                    Point::new(1, 0)..Point::new(1, 3),
3429                    Point::new(5, 8)..Point::new(5, 11),
3430                    Point::new(6, 0)..Point::new(6, 3),
3431                ]
3432            );
3433        });
3434    }
3435
3436    #[perf]
3437    #[gpui::test]
3438    async fn test_hides_and_uses_secondary_when_in_singleton_buffer(cx: &mut TestAppContext) {
3439        let (editor, search_bar, cx) = init_test(cx);
3440
3441        let initial_location = search_bar.update_in(cx, |search_bar, window, cx| {
3442            search_bar.set_active_pane_item(Some(&editor), window, cx)
3443        });
3444
3445        assert_eq!(initial_location, ToolbarItemLocation::Secondary);
3446
3447        let mut events = cx.events::<ToolbarItemEvent, BufferSearchBar>(&search_bar);
3448
3449        search_bar.update_in(cx, |search_bar, window, cx| {
3450            search_bar.dismiss(&Dismiss, window, cx);
3451        });
3452
3453        assert_eq!(
3454            events.try_recv().unwrap(),
3455            (ToolbarItemEvent::ChangeLocation(ToolbarItemLocation::Hidden))
3456        );
3457
3458        search_bar.update_in(cx, |search_bar, window, cx| {
3459            search_bar.show(window, cx);
3460        });
3461
3462        assert_eq!(
3463            events.try_recv().unwrap(),
3464            (ToolbarItemEvent::ChangeLocation(ToolbarItemLocation::Secondary))
3465        );
3466    }
3467
3468    #[perf]
3469    #[gpui::test]
3470    async fn test_uses_primary_left_when_in_multi_buffer(cx: &mut TestAppContext) {
3471        let (editor, search_bar, cx) = init_multibuffer_test(cx);
3472
3473        let initial_location = search_bar.update_in(cx, |search_bar, window, cx| {
3474            search_bar.set_active_pane_item(Some(&editor), window, cx)
3475        });
3476
3477        assert_eq!(initial_location, ToolbarItemLocation::PrimaryLeft);
3478
3479        let mut events = cx.events::<ToolbarItemEvent, BufferSearchBar>(&search_bar);
3480
3481        search_bar.update_in(cx, |search_bar, window, cx| {
3482            search_bar.dismiss(&Dismiss, window, cx);
3483        });
3484
3485        assert_eq!(
3486            events.try_recv().unwrap(),
3487            (ToolbarItemEvent::ChangeLocation(ToolbarItemLocation::PrimaryLeft))
3488        );
3489
3490        search_bar.update_in(cx, |search_bar, window, cx| {
3491            search_bar.show(window, cx);
3492        });
3493
3494        assert_eq!(
3495            events.try_recv().unwrap(),
3496            (ToolbarItemEvent::ChangeLocation(ToolbarItemLocation::PrimaryLeft))
3497        );
3498    }
3499
3500    #[perf]
3501    #[gpui::test]
3502    async fn test_hides_and_uses_secondary_when_part_of_project_search(cx: &mut TestAppContext) {
3503        let (editor, search_bar, cx) = init_multibuffer_test(cx);
3504
3505        editor.update(cx, |editor, _| {
3506            editor.set_in_project_search(true);
3507        });
3508
3509        let initial_location = search_bar.update_in(cx, |search_bar, window, cx| {
3510            search_bar.set_active_pane_item(Some(&editor), window, cx)
3511        });
3512
3513        assert_eq!(initial_location, ToolbarItemLocation::Hidden);
3514
3515        let mut events = cx.events::<ToolbarItemEvent, BufferSearchBar>(&search_bar);
3516
3517        search_bar.update_in(cx, |search_bar, window, cx| {
3518            search_bar.dismiss(&Dismiss, window, cx);
3519        });
3520
3521        assert_eq!(
3522            events.try_recv().unwrap(),
3523            (ToolbarItemEvent::ChangeLocation(ToolbarItemLocation::Hidden))
3524        );
3525
3526        search_bar.update_in(cx, |search_bar, window, cx| {
3527            search_bar.show(window, cx);
3528        });
3529
3530        assert_eq!(
3531            events.try_recv().unwrap(),
3532            (ToolbarItemEvent::ChangeLocation(ToolbarItemLocation::Secondary))
3533        );
3534    }
3535
3536    #[perf]
3537    #[gpui::test]
3538    async fn test_sets_collapsed_when_editor_fold_events_emitted(cx: &mut TestAppContext) {
3539        let (editor, search_bar, cx) = init_multibuffer_test(cx);
3540
3541        search_bar.update_in(cx, |search_bar, window, cx| {
3542            search_bar.set_active_pane_item(Some(&editor), window, cx);
3543        });
3544
3545        editor.update_in(cx, |editor, window, cx| {
3546            editor.fold_all(&FoldAll, window, cx);
3547        });
3548        cx.run_until_parked();
3549
3550        let is_collapsed = editor.read_with(cx, |editor, cx| editor.has_any_buffer_folded(cx));
3551        assert!(is_collapsed);
3552
3553        editor.update_in(cx, |editor, window, cx| {
3554            editor.unfold_all(&UnfoldAll, window, cx);
3555        });
3556        cx.run_until_parked();
3557
3558        let is_collapsed = editor.read_with(cx, |editor, cx| editor.has_any_buffer_folded(cx));
3559        assert!(!is_collapsed);
3560    }
3561
3562    #[perf]
3563    #[gpui::test]
3564    async fn test_collapse_state_syncs_after_manual_buffer_fold(cx: &mut TestAppContext) {
3565        let (editor, search_bar, cx) = init_multibuffer_test(cx);
3566
3567        search_bar.update_in(cx, |search_bar, window, cx| {
3568            search_bar.set_active_pane_item(Some(&editor), window, cx);
3569        });
3570
3571        // Fold all buffers via fold_all
3572        editor.update_in(cx, |editor, window, cx| {
3573            editor.fold_all(&FoldAll, window, cx);
3574        });
3575        cx.run_until_parked();
3576
3577        let has_any_folded = editor.read_with(cx, |editor, cx| editor.has_any_buffer_folded(cx));
3578        assert!(
3579            has_any_folded,
3580            "All buffers should be folded after fold_all"
3581        );
3582
3583        // Manually unfold one buffer (simulating a chevron click)
3584        let first_buffer_id = editor.read_with(cx, |editor, cx| {
3585            editor
3586                .buffer()
3587                .read(cx)
3588                .snapshot(cx)
3589                .excerpts()
3590                .nth(0)
3591                .unwrap()
3592                .context
3593                .start
3594                .buffer_id
3595        });
3596        editor.update_in(cx, |editor, _window, cx| {
3597            editor.unfold_buffer(first_buffer_id, cx);
3598        });
3599
3600        let has_any_folded = editor.read_with(cx, |editor, cx| editor.has_any_buffer_folded(cx));
3601        assert!(
3602            has_any_folded,
3603            "Should still report folds when only one buffer is unfolded"
3604        );
3605
3606        // Manually unfold the second buffer too
3607        let second_buffer_id = editor.read_with(cx, |editor, cx| {
3608            editor
3609                .buffer()
3610                .read(cx)
3611                .snapshot(cx)
3612                .excerpts()
3613                .nth(1)
3614                .unwrap()
3615                .context
3616                .start
3617                .buffer_id
3618        });
3619        editor.update_in(cx, |editor, _window, cx| {
3620            editor.unfold_buffer(second_buffer_id, cx);
3621        });
3622
3623        let has_any_folded = editor.read_with(cx, |editor, cx| editor.has_any_buffer_folded(cx));
3624        assert!(
3625            !has_any_folded,
3626            "No folds should remain after unfolding all buffers individually"
3627        );
3628
3629        // Manually fold one buffer back
3630        editor.update_in(cx, |editor, _window, cx| {
3631            editor.fold_buffer(first_buffer_id, cx);
3632        });
3633
3634        let has_any_folded = editor.read_with(cx, |editor, cx| editor.has_any_buffer_folded(cx));
3635        assert!(
3636            has_any_folded,
3637            "Should report folds after manually folding one buffer"
3638        );
3639    }
3640
3641    #[perf]
3642    #[gpui::test]
3643    async fn test_search_options_changes(cx: &mut TestAppContext) {
3644        let (_editor, search_bar, cx) = init_test(cx);
3645        update_search_settings(
3646            SearchSettings {
3647                button: true,
3648                whole_word: false,
3649                case_sensitive: false,
3650                include_ignored: false,
3651                regex: false,
3652                center_on_match: false,
3653            },
3654            cx,
3655        );
3656
3657        let deploy = Deploy {
3658            focus: true,
3659            replace_enabled: false,
3660            selection_search_enabled: true,
3661        };
3662
3663        search_bar.update_in(cx, |search_bar, window, cx| {
3664            assert_eq!(
3665                search_bar.search_options,
3666                SearchOptions::NONE,
3667                "Should have no search options enabled by default"
3668            );
3669            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
3670            assert_eq!(
3671                search_bar.search_options,
3672                SearchOptions::WHOLE_WORD,
3673                "Should enable the option toggled"
3674            );
3675            assert!(
3676                !search_bar.dismissed,
3677                "Search bar should be present and visible"
3678            );
3679            search_bar.deploy(&deploy, window, cx);
3680            assert_eq!(
3681                search_bar.search_options,
3682                SearchOptions::WHOLE_WORD,
3683                "After (re)deploying, the option should still be enabled"
3684            );
3685
3686            search_bar.dismiss(&Dismiss, window, cx);
3687            search_bar.deploy(&deploy, window, cx);
3688            assert_eq!(
3689                search_bar.search_options,
3690                SearchOptions::WHOLE_WORD,
3691                "After hiding and showing the search bar, search options should be preserved"
3692            );
3693
3694            search_bar.toggle_search_option(SearchOptions::REGEX, window, cx);
3695            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
3696            assert_eq!(
3697                search_bar.search_options,
3698                SearchOptions::REGEX,
3699                "Should enable the options toggled"
3700            );
3701            assert!(
3702                !search_bar.dismissed,
3703                "Search bar should be present and visible"
3704            );
3705            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
3706        });
3707
3708        update_search_settings(
3709            SearchSettings {
3710                button: true,
3711                whole_word: false,
3712                case_sensitive: true,
3713                include_ignored: false,
3714                regex: false,
3715                center_on_match: false,
3716            },
3717            cx,
3718        );
3719        search_bar.update_in(cx, |search_bar, window, cx| {
3720            assert_eq!(
3721                search_bar.search_options,
3722                SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
3723                "Should have no search options enabled by default"
3724            );
3725
3726            search_bar.deploy(&deploy, window, cx);
3727            assert_eq!(
3728                search_bar.search_options,
3729                SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
3730                "Toggling a non-dismissed search bar with custom options should not change the default options"
3731            );
3732            search_bar.dismiss(&Dismiss, window, cx);
3733            search_bar.deploy(&deploy, window, cx);
3734            assert_eq!(
3735                search_bar.configured_options,
3736                SearchOptions::CASE_SENSITIVE,
3737                "After a settings update and toggling the search bar, configured options should be updated"
3738            );
3739            assert_eq!(
3740                search_bar.search_options,
3741                SearchOptions::CASE_SENSITIVE,
3742                "After a settings update and toggling the search bar, configured options should be used"
3743            );
3744        });
3745
3746        update_search_settings(
3747            SearchSettings {
3748                button: true,
3749                whole_word: true,
3750                case_sensitive: true,
3751                include_ignored: false,
3752                regex: false,
3753                center_on_match: false,
3754            },
3755            cx,
3756        );
3757
3758        search_bar.update_in(cx, |search_bar, window, cx| {
3759            search_bar.deploy(&deploy, window, cx);
3760            search_bar.dismiss(&Dismiss, window, cx);
3761            search_bar.show(window, cx);
3762            assert_eq!(
3763                search_bar.search_options,
3764                SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD,
3765                "Calling deploy on an already deployed search bar should not prevent settings updates from being detected"
3766            );
3767        });
3768    }
3769
3770    #[gpui::test]
3771    async fn test_select_occurrence_case_sensitivity(cx: &mut TestAppContext) {
3772        let (editor, search_bar, cx) = init_test(cx);
3773        let mut editor_cx = EditorTestContext::for_editor_in(editor, cx).await;
3774
3775        // Start with case sensitive search settings.
3776        let mut search_settings = SearchSettings::default();
3777        search_settings.case_sensitive = true;
3778        update_search_settings(search_settings, cx);
3779        search_bar.update(cx, |search_bar, cx| {
3780            let mut search_options = search_bar.search_options;
3781            search_options.insert(SearchOptions::CASE_SENSITIVE);
3782            search_bar.set_search_options(search_options, cx);
3783        });
3784
3785        editor_cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
3786        editor_cx.update_editor(|e, window, cx| {
3787            e.select_next(&Default::default(), window, cx).unwrap();
3788        });
3789        editor_cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
3790
3791        // Update the search bar's case sensitivite toggle, so we can later
3792        // confirm that `select_next` will now be case-insensitive.
3793        editor_cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
3794        search_bar.update_in(cx, |search_bar, window, cx| {
3795            search_bar.toggle_case_sensitive(&Default::default(), window, cx);
3796        });
3797        editor_cx.update_editor(|e, window, cx| {
3798            e.select_next(&Default::default(), window, cx).unwrap();
3799        });
3800        editor_cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\nFoo\nfoo");
3801
3802        // Confirm that, after dismissing the search bar, only the editor's
3803        // search settings actually affect the behavior of `select_next`.
3804        search_bar.update_in(cx, |search_bar, window, cx| {
3805            search_bar.dismiss(&Default::default(), window, cx);
3806        });
3807        editor_cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
3808        editor_cx.update_editor(|e, window, cx| {
3809            e.select_next(&Default::default(), window, cx).unwrap();
3810        });
3811        editor_cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
3812
3813        // Update the editor's search settings, disabling case sensitivity, to
3814        // check that the value is respected.
3815        let mut search_settings = SearchSettings::default();
3816        search_settings.case_sensitive = false;
3817        update_search_settings(search_settings, cx);
3818        editor_cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
3819        editor_cx.update_editor(|e, window, cx| {
3820            e.select_next(&Default::default(), window, cx).unwrap();
3821        });
3822        editor_cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\nFoo\nfoo");
3823    }
3824
3825    #[gpui::test]
3826    async fn test_regex_search_does_not_highlight_non_matching_occurrences(
3827        cx: &mut TestAppContext,
3828    ) {
3829        init_globals(cx);
3830        let buffer = cx.new(|cx| {
3831            Buffer::local(
3832                "something is at the top\nsomething is behind something\nsomething is at the bottom\n",
3833                cx,
3834            )
3835        });
3836        let cx = cx.add_empty_window();
3837        let editor =
3838            cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
3839        let search_bar = cx.new_window_entity(|window, cx| {
3840            let mut search_bar = BufferSearchBar::new(None, window, cx);
3841            search_bar.set_active_pane_item(Some(&editor), window, cx);
3842            search_bar.show(window, cx);
3843            search_bar
3844        });
3845
3846        search_bar.update_in(cx, |search_bar, window, cx| {
3847            search_bar.toggle_search_option(SearchOptions::REGEX, window, cx);
3848        });
3849
3850        search_bar
3851            .update_in(cx, |search_bar, window, cx| {
3852                search_bar.search("^something", None, true, window, cx)
3853            })
3854            .await
3855            .unwrap();
3856
3857        search_bar.update_in(cx, |search_bar, window, cx| {
3858            search_bar.select_next_match(&SelectNextMatch, window, cx);
3859        });
3860
3861        // Advance past the debounce so the selection occurrence highlight would
3862        // have fired if it were not suppressed by the active buffer search.
3863        cx.executor()
3864            .advance_clock(SELECTION_HIGHLIGHT_DEBOUNCE_TIMEOUT + Duration::from_millis(1));
3865        cx.run_until_parked();
3866
3867        editor.update(cx, |editor, cx| {
3868            assert!(
3869                !editor.has_background_highlights(HighlightKey::SelectedTextHighlight),
3870                "selection occurrence highlights must be suppressed during buffer search"
3871            );
3872            assert_eq!(
3873                editor.search_background_highlights(cx).len(),
3874                3,
3875                "expected exactly 3 search highlights (one per line start)"
3876            );
3877        });
3878
3879        // Manually select "something" — this should restore occurrence highlights
3880        // because it clears the search-navigation flag.
3881        editor.update_in(cx, |editor, window, cx| {
3882            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3883                s.select_ranges([Point::new(0, 0)..Point::new(0, 9)])
3884            });
3885        });
3886
3887        cx.executor()
3888            .advance_clock(SELECTION_HIGHLIGHT_DEBOUNCE_TIMEOUT + Duration::from_millis(1));
3889        cx.run_until_parked();
3890
3891        editor.update(cx, |editor, _cx| {
3892            assert!(
3893                editor.has_background_highlights(HighlightKey::SelectedTextHighlight),
3894                "selection occurrence highlights must be restored after a manual selection"
3895            );
3896        });
3897    }
3898
3899    fn update_search_settings(search_settings: SearchSettings, cx: &mut TestAppContext) {
3900        cx.update(|cx| {
3901            SettingsStore::update_global(cx, |store, cx| {
3902                store.update_user_settings(cx, |settings| {
3903                    settings.editor.search = Some(SearchSettingsContent {
3904                        button: Some(search_settings.button),
3905                        whole_word: Some(search_settings.whole_word),
3906                        case_sensitive: Some(search_settings.case_sensitive),
3907                        include_ignored: Some(search_settings.include_ignored),
3908                        regex: Some(search_settings.regex),
3909                        center_on_match: Some(search_settings.center_on_match),
3910                    });
3911                });
3912            });
3913        });
3914    }
3915}