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