buffer_search.rs

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