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