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, time::Duration};
1904
1905    use super::*;
1906    use editor::{
1907        DisplayPoint, Editor, HighlightKey, MultiBuffer, PathKey,
1908        SELECTION_HIGHLIGHT_DEBOUNCE_TIMEOUT, SearchSettings, SelectionEffects,
1909        display_map::DisplayRow, test::editor_test_context::EditorTestContext,
1910    };
1911    use gpui::{Hsla, TestAppContext, UpdateGlobal, VisualTestContext};
1912    use language::{Buffer, Point};
1913    use settings::{SearchSettingsContent, SettingsStore};
1914    use smol::stream::StreamExt as _;
1915    use unindent::Unindent as _;
1916    use util_macros::perf;
1917
1918    fn init_globals(cx: &mut TestAppContext) {
1919        cx.update(|cx| {
1920            let store = settings::SettingsStore::test(cx);
1921            cx.set_global(store);
1922            editor::init(cx);
1923
1924            theme_settings::init(theme::LoadThemes::JustBase, cx);
1925            crate::init(cx);
1926        });
1927    }
1928
1929    fn init_multibuffer_test(
1930        cx: &mut TestAppContext,
1931    ) -> (
1932        Entity<Editor>,
1933        Entity<BufferSearchBar>,
1934        &mut VisualTestContext,
1935    ) {
1936        init_globals(cx);
1937
1938        let buffer1 = cx.new(|cx| {
1939            Buffer::local(
1940                            r#"
1941                            A regular expression (shortened as regex or regexp;[1] also referred to as
1942                            rational expression[2][3]) is a sequence of characters that specifies a search
1943                            pattern in text. Usually such patterns are used by string-searching algorithms
1944                            for "find" or "find and replace" operations on strings, or for input validation.
1945                            "#
1946                            .unindent(),
1947                            cx,
1948                        )
1949        });
1950
1951        let buffer2 = cx.new(|cx| {
1952            Buffer::local(
1953                r#"
1954                            Some Additional text with the term regular expression in it.
1955                            There two lines.
1956                            "#
1957                .unindent(),
1958                cx,
1959            )
1960        });
1961
1962        let multibuffer = cx.new(|cx| {
1963            let mut buffer = MultiBuffer::new(language::Capability::ReadWrite);
1964
1965            //[ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))]
1966            buffer.set_excerpts_for_path(
1967                PathKey::sorted(0),
1968                buffer1,
1969                [Point::new(0, 0)..Point::new(3, 0)],
1970                0,
1971                cx,
1972            );
1973            buffer.set_excerpts_for_path(
1974                PathKey::sorted(1),
1975                buffer2,
1976                [Point::new(0, 0)..Point::new(1, 0)],
1977                0,
1978                cx,
1979            );
1980
1981            buffer
1982        });
1983        let mut editor = None;
1984        let window = cx.add_window(|window, cx| {
1985            let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
1986                "keymaps/default-macos.json",
1987                cx,
1988            )
1989            .unwrap();
1990            cx.bind_keys(default_key_bindings);
1991            editor =
1992                Some(cx.new(|cx| Editor::for_multibuffer(multibuffer.clone(), None, window, cx)));
1993
1994            let mut search_bar = BufferSearchBar::new(None, window, cx);
1995            search_bar.set_active_pane_item(Some(&editor.clone().unwrap()), window, cx);
1996            search_bar.show(window, cx);
1997            search_bar
1998        });
1999        let search_bar = window.root(cx).unwrap();
2000
2001        let cx = VisualTestContext::from_window(*window, cx).into_mut();
2002
2003        (editor.unwrap(), search_bar, cx)
2004    }
2005
2006    fn init_test(
2007        cx: &mut TestAppContext,
2008    ) -> (
2009        Entity<Editor>,
2010        Entity<BufferSearchBar>,
2011        &mut VisualTestContext,
2012    ) {
2013        init_globals(cx);
2014        let buffer = cx.new(|cx| {
2015            Buffer::local(
2016                r#"
2017                A regular expression (shortened as regex or regexp;[1] also referred to as
2018                rational expression[2][3]) is a sequence of characters that specifies a search
2019                pattern in text. Usually such patterns are used by string-searching algorithms
2020                for "find" or "find and replace" operations on strings, or for input validation.
2021                "#
2022                .unindent(),
2023                cx,
2024            )
2025        });
2026        let mut editor = None;
2027        let window = cx.add_window(|window, cx| {
2028            let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
2029                "keymaps/default-macos.json",
2030                cx,
2031            )
2032            .unwrap();
2033            cx.bind_keys(default_key_bindings);
2034            editor = Some(cx.new(|cx| Editor::for_buffer(buffer.clone(), None, window, cx)));
2035            let mut search_bar = BufferSearchBar::new(None, window, cx);
2036            search_bar.set_active_pane_item(Some(&editor.clone().unwrap()), window, cx);
2037            search_bar.show(window, cx);
2038            search_bar
2039        });
2040        let search_bar = window.root(cx).unwrap();
2041
2042        let cx = VisualTestContext::from_window(*window, cx).into_mut();
2043
2044        (editor.unwrap(), search_bar, cx)
2045    }
2046
2047    #[perf]
2048    #[gpui::test]
2049    async fn test_search_simple(cx: &mut TestAppContext) {
2050        let (editor, search_bar, cx) = init_test(cx);
2051        let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
2052            background_highlights
2053                .into_iter()
2054                .map(|(range, _)| range)
2055                .collect::<Vec<_>>()
2056        };
2057        // Search for a string that appears with different casing.
2058        // By default, search is case-insensitive.
2059        search_bar
2060            .update_in(cx, |search_bar, window, cx| {
2061                search_bar.search("us", None, true, window, cx)
2062            })
2063            .await
2064            .unwrap();
2065        editor.update_in(cx, |editor, window, cx| {
2066            assert_eq!(
2067                display_points_of(editor.all_text_background_highlights(window, cx)),
2068                &[
2069                    DisplayPoint::new(DisplayRow(2), 17)..DisplayPoint::new(DisplayRow(2), 19),
2070                    DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),
2071                ]
2072            );
2073        });
2074
2075        // Switch to a case sensitive search.
2076        search_bar.update_in(cx, |search_bar, window, cx| {
2077            search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
2078        });
2079        let mut editor_notifications = cx.notifications(&editor);
2080        editor_notifications.next().await;
2081        editor.update_in(cx, |editor, window, cx| {
2082            assert_eq!(
2083                display_points_of(editor.all_text_background_highlights(window, cx)),
2084                &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
2085            );
2086        });
2087
2088        // Search for a string that appears both as a whole word and
2089        // within other words. By default, all results are found.
2090        search_bar
2091            .update_in(cx, |search_bar, window, cx| {
2092                search_bar.search("or", None, true, window, cx)
2093            })
2094            .await
2095            .unwrap();
2096        editor.update_in(cx, |editor, window, cx| {
2097            assert_eq!(
2098                display_points_of(editor.all_text_background_highlights(window, cx)),
2099                &[
2100                    DisplayPoint::new(DisplayRow(0), 24)..DisplayPoint::new(DisplayRow(0), 26),
2101                    DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
2102                    DisplayPoint::new(DisplayRow(2), 71)..DisplayPoint::new(DisplayRow(2), 73),
2103                    DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 3),
2104                    DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
2105                    DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
2106                    DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 62),
2107                ]
2108            );
2109        });
2110
2111        // Switch to a whole word search.
2112        search_bar.update_in(cx, |search_bar, window, cx| {
2113            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
2114        });
2115        let mut editor_notifications = cx.notifications(&editor);
2116        editor_notifications.next().await;
2117        editor.update_in(cx, |editor, window, cx| {
2118            assert_eq!(
2119                display_points_of(editor.all_text_background_highlights(window, cx)),
2120                &[
2121                    DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
2122                    DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
2123                    DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
2124                ]
2125            );
2126        });
2127
2128        editor.update_in(cx, |editor, window, cx| {
2129            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2130                s.select_display_ranges([
2131                    DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2132                ])
2133            });
2134        });
2135        search_bar.update_in(cx, |search_bar, window, cx| {
2136            assert_eq!(search_bar.active_match_index, Some(0));
2137            search_bar.select_next_match(&SelectNextMatch, window, cx);
2138            assert_eq!(
2139                editor.update(cx, |editor, cx| editor
2140                    .selections
2141                    .display_ranges(&editor.display_snapshot(cx))),
2142                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
2143            );
2144        });
2145        search_bar.read_with(cx, |search_bar, _| {
2146            assert_eq!(search_bar.active_match_index, Some(0));
2147        });
2148
2149        search_bar.update_in(cx, |search_bar, window, cx| {
2150            search_bar.select_next_match(&SelectNextMatch, window, cx);
2151            assert_eq!(
2152                editor.update(cx, |editor, cx| editor
2153                    .selections
2154                    .display_ranges(&editor.display_snapshot(cx))),
2155                [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
2156            );
2157        });
2158        search_bar.read_with(cx, |search_bar, _| {
2159            assert_eq!(search_bar.active_match_index, Some(1));
2160        });
2161
2162        search_bar.update_in(cx, |search_bar, window, cx| {
2163            search_bar.select_next_match(&SelectNextMatch, window, cx);
2164            assert_eq!(
2165                editor.update(cx, |editor, cx| editor
2166                    .selections
2167                    .display_ranges(&editor.display_snapshot(cx))),
2168                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
2169            );
2170        });
2171        search_bar.read_with(cx, |search_bar, _| {
2172            assert_eq!(search_bar.active_match_index, Some(2));
2173        });
2174
2175        search_bar.update_in(cx, |search_bar, window, cx| {
2176            search_bar.select_next_match(&SelectNextMatch, window, cx);
2177            assert_eq!(
2178                editor.update(cx, |editor, cx| editor
2179                    .selections
2180                    .display_ranges(&editor.display_snapshot(cx))),
2181                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
2182            );
2183        });
2184        search_bar.read_with(cx, |search_bar, _| {
2185            assert_eq!(search_bar.active_match_index, Some(0));
2186        });
2187
2188        search_bar.update_in(cx, |search_bar, window, cx| {
2189            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2190            assert_eq!(
2191                editor.update(cx, |editor, cx| editor
2192                    .selections
2193                    .display_ranges(&editor.display_snapshot(cx))),
2194                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
2195            );
2196        });
2197        search_bar.read_with(cx, |search_bar, _| {
2198            assert_eq!(search_bar.active_match_index, Some(2));
2199        });
2200
2201        search_bar.update_in(cx, |search_bar, window, cx| {
2202            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2203            assert_eq!(
2204                editor.update(cx, |editor, cx| editor
2205                    .selections
2206                    .display_ranges(&editor.display_snapshot(cx))),
2207                [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
2208            );
2209        });
2210        search_bar.read_with(cx, |search_bar, _| {
2211            assert_eq!(search_bar.active_match_index, Some(1));
2212        });
2213
2214        search_bar.update_in(cx, |search_bar, window, cx| {
2215            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2216            assert_eq!(
2217                editor.update(cx, |editor, cx| editor
2218                    .selections
2219                    .display_ranges(&editor.display_snapshot(cx))),
2220                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
2221            );
2222        });
2223        search_bar.read_with(cx, |search_bar, _| {
2224            assert_eq!(search_bar.active_match_index, Some(0));
2225        });
2226
2227        // Park the cursor in between matches and ensure that going to the previous match selects
2228        // the closest match to the left.
2229        editor.update_in(cx, |editor, window, cx| {
2230            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2231                s.select_display_ranges([
2232                    DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
2233                ])
2234            });
2235        });
2236        search_bar.update_in(cx, |search_bar, window, cx| {
2237            assert_eq!(search_bar.active_match_index, Some(1));
2238            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2239            assert_eq!(
2240                editor.update(cx, |editor, cx| editor
2241                    .selections
2242                    .display_ranges(&editor.display_snapshot(cx))),
2243                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
2244            );
2245        });
2246        search_bar.read_with(cx, |search_bar, _| {
2247            assert_eq!(search_bar.active_match_index, Some(0));
2248        });
2249
2250        // Park the cursor in between matches and ensure that going to the next match selects the
2251        // closest match to the right.
2252        editor.update_in(cx, |editor, window, cx| {
2253            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2254                s.select_display_ranges([
2255                    DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
2256                ])
2257            });
2258        });
2259        search_bar.update_in(cx, |search_bar, window, cx| {
2260            assert_eq!(search_bar.active_match_index, Some(1));
2261            search_bar.select_next_match(&SelectNextMatch, window, cx);
2262            assert_eq!(
2263                editor.update(cx, |editor, cx| editor
2264                    .selections
2265                    .display_ranges(&editor.display_snapshot(cx))),
2266                [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
2267            );
2268        });
2269        search_bar.read_with(cx, |search_bar, _| {
2270            assert_eq!(search_bar.active_match_index, Some(1));
2271        });
2272
2273        // Park the cursor after the last match and ensure that going to the previous match selects
2274        // the last match.
2275        editor.update_in(cx, |editor, window, cx| {
2276            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2277                s.select_display_ranges([
2278                    DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
2279                ])
2280            });
2281        });
2282        search_bar.update_in(cx, |search_bar, window, cx| {
2283            assert_eq!(search_bar.active_match_index, Some(2));
2284            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2285            assert_eq!(
2286                editor.update(cx, |editor, cx| editor
2287                    .selections
2288                    .display_ranges(&editor.display_snapshot(cx))),
2289                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
2290            );
2291        });
2292        search_bar.read_with(cx, |search_bar, _| {
2293            assert_eq!(search_bar.active_match_index, Some(2));
2294        });
2295
2296        // Park the cursor after the last match and ensure that going to the next match selects the
2297        // first match.
2298        editor.update_in(cx, |editor, window, cx| {
2299            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2300                s.select_display_ranges([
2301                    DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
2302                ])
2303            });
2304        });
2305        search_bar.update_in(cx, |search_bar, window, cx| {
2306            assert_eq!(search_bar.active_match_index, Some(2));
2307            search_bar.select_next_match(&SelectNextMatch, window, cx);
2308            assert_eq!(
2309                editor.update(cx, |editor, cx| editor
2310                    .selections
2311                    .display_ranges(&editor.display_snapshot(cx))),
2312                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
2313            );
2314        });
2315        search_bar.read_with(cx, |search_bar, _| {
2316            assert_eq!(search_bar.active_match_index, Some(0));
2317        });
2318
2319        // Park the cursor before the first match and ensure that going to the previous match
2320        // selects the last match.
2321        editor.update_in(cx, |editor, window, cx| {
2322            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2323                s.select_display_ranges([
2324                    DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2325                ])
2326            });
2327        });
2328        search_bar.update_in(cx, |search_bar, window, cx| {
2329            assert_eq!(search_bar.active_match_index, Some(0));
2330            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2331            assert_eq!(
2332                editor.update(cx, |editor, cx| editor
2333                    .selections
2334                    .display_ranges(&editor.display_snapshot(cx))),
2335                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
2336            );
2337        });
2338        search_bar.read_with(cx, |search_bar, _| {
2339            assert_eq!(search_bar.active_match_index, Some(2));
2340        });
2341    }
2342
2343    fn display_points_of(
2344        background_highlights: Vec<(Range<DisplayPoint>, Hsla)>,
2345    ) -> Vec<Range<DisplayPoint>> {
2346        background_highlights
2347            .into_iter()
2348            .map(|(range, _)| range)
2349            .collect::<Vec<_>>()
2350    }
2351
2352    #[perf]
2353    #[gpui::test]
2354    async fn test_search_option_handling(cx: &mut TestAppContext) {
2355        let (editor, search_bar, cx) = init_test(cx);
2356
2357        // show with options should make current search case sensitive
2358        search_bar
2359            .update_in(cx, |search_bar, window, cx| {
2360                search_bar.show(window, cx);
2361                search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), true, window, cx)
2362            })
2363            .await
2364            .unwrap();
2365        editor.update_in(cx, |editor, window, cx| {
2366            assert_eq!(
2367                display_points_of(editor.all_text_background_highlights(window, cx)),
2368                &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
2369            );
2370        });
2371
2372        // search_suggested should restore default options
2373        search_bar.update_in(cx, |search_bar, window, cx| {
2374            search_bar.search_suggested(window, cx);
2375            assert_eq!(search_bar.search_options, SearchOptions::NONE)
2376        });
2377
2378        // toggling a search option should update the defaults
2379        search_bar
2380            .update_in(cx, |search_bar, window, cx| {
2381                search_bar.search(
2382                    "regex",
2383                    Some(SearchOptions::CASE_SENSITIVE),
2384                    true,
2385                    window,
2386                    cx,
2387                )
2388            })
2389            .await
2390            .unwrap();
2391        search_bar.update_in(cx, |search_bar, window, cx| {
2392            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx)
2393        });
2394        let mut editor_notifications = cx.notifications(&editor);
2395        editor_notifications.next().await;
2396        editor.update_in(cx, |editor, window, cx| {
2397            assert_eq!(
2398                display_points_of(editor.all_text_background_highlights(window, cx)),
2399                &[DisplayPoint::new(DisplayRow(0), 35)..DisplayPoint::new(DisplayRow(0), 40),]
2400            );
2401        });
2402
2403        // defaults should still include whole word
2404        search_bar.update_in(cx, |search_bar, window, cx| {
2405            search_bar.search_suggested(window, cx);
2406            assert_eq!(
2407                search_bar.search_options,
2408                SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD
2409            )
2410        });
2411    }
2412
2413    #[perf]
2414    #[gpui::test]
2415    async fn test_search_select_all_matches(cx: &mut TestAppContext) {
2416        init_globals(cx);
2417        let buffer_text = r#"
2418        A regular expression (shortened as regex or regexp;[1] also referred to as
2419        rational expression[2][3]) is a sequence of characters that specifies a search
2420        pattern in text. Usually such patterns are used by string-searching algorithms
2421        for "find" or "find and replace" operations on strings, or for input validation.
2422        "#
2423        .unindent();
2424        let expected_query_matches_count = buffer_text
2425            .chars()
2426            .filter(|c| c.eq_ignore_ascii_case(&'a'))
2427            .count();
2428        assert!(
2429            expected_query_matches_count > 1,
2430            "Should pick a query with multiple results"
2431        );
2432        let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
2433        let window = cx.add_window(|_, _| gpui::Empty);
2434
2435        let editor = window.build_entity(cx, |window, cx| {
2436            Editor::for_buffer(buffer.clone(), None, window, cx)
2437        });
2438
2439        let search_bar = window.build_entity(cx, |window, cx| {
2440            let mut search_bar = BufferSearchBar::new(None, window, cx);
2441            search_bar.set_active_pane_item(Some(&editor), window, cx);
2442            search_bar.show(window, cx);
2443            search_bar
2444        });
2445
2446        window
2447            .update(cx, |_, window, cx| {
2448                search_bar.update(cx, |search_bar, cx| {
2449                    search_bar.search("a", None, true, window, cx)
2450                })
2451            })
2452            .unwrap()
2453            .await
2454            .unwrap();
2455        let initial_selections = window
2456            .update(cx, |_, window, cx| {
2457                search_bar.update(cx, |search_bar, cx| {
2458                    let handle = search_bar.query_editor.focus_handle(cx);
2459                    window.focus(&handle, cx);
2460                    search_bar.activate_current_match(window, cx);
2461                });
2462                assert!(
2463                    !editor.read(cx).is_focused(window),
2464                    "Initially, the editor should not be focused"
2465                );
2466                let initial_selections = editor.update(cx, |editor, cx| {
2467                    let initial_selections = editor.selections.display_ranges(&editor.display_snapshot(cx));
2468                    assert_eq!(
2469                        initial_selections.len(), 1,
2470                        "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
2471                    );
2472                    initial_selections
2473                });
2474                search_bar.update(cx, |search_bar, cx| {
2475                    assert_eq!(search_bar.active_match_index, Some(0));
2476                    let handle = search_bar.query_editor.focus_handle(cx);
2477                    window.focus(&handle, cx);
2478                    search_bar.select_all_matches(&SelectAllMatches, window, cx);
2479                });
2480                assert!(
2481                    editor.read(cx).is_focused(window),
2482                    "Should focus editor after successful SelectAllMatches"
2483                );
2484                search_bar.update(cx, |search_bar, cx| {
2485                    let all_selections =
2486                        editor.update(cx, |editor, cx| editor.selections.display_ranges(&editor.display_snapshot(cx)));
2487                    assert_eq!(
2488                        all_selections.len(),
2489                        expected_query_matches_count,
2490                        "Should select all `a` characters in the buffer, but got: {all_selections:?}"
2491                    );
2492                    assert_eq!(
2493                        search_bar.active_match_index,
2494                        Some(0),
2495                        "Match index should not change after selecting all matches"
2496                    );
2497                });
2498
2499                search_bar.update(cx, |this, cx| this.select_next_match(&SelectNextMatch, window, cx));
2500                initial_selections
2501            }).unwrap();
2502
2503        window
2504            .update(cx, |_, window, cx| {
2505                assert!(
2506                    editor.read(cx).is_focused(window),
2507                    "Should still have editor focused after SelectNextMatch"
2508                );
2509                search_bar.update(cx, |search_bar, cx| {
2510                    let all_selections = editor.update(cx, |editor, cx| {
2511                        editor
2512                            .selections
2513                            .display_ranges(&editor.display_snapshot(cx))
2514                    });
2515                    assert_eq!(
2516                        all_selections.len(),
2517                        1,
2518                        "On next match, should deselect items and select the next match"
2519                    );
2520                    assert_ne!(
2521                        all_selections, initial_selections,
2522                        "Next match should be different from the first selection"
2523                    );
2524                    assert_eq!(
2525                        search_bar.active_match_index,
2526                        Some(1),
2527                        "Match index should be updated to the next one"
2528                    );
2529                    let handle = search_bar.query_editor.focus_handle(cx);
2530                    window.focus(&handle, cx);
2531                    search_bar.select_all_matches(&SelectAllMatches, window, cx);
2532                });
2533            })
2534            .unwrap();
2535        window
2536            .update(cx, |_, window, cx| {
2537                assert!(
2538                    editor.read(cx).is_focused(window),
2539                    "Should focus editor after successful SelectAllMatches"
2540                );
2541                search_bar.update(cx, |search_bar, cx| {
2542                    let all_selections =
2543                        editor.update(cx, |editor, cx| editor.selections.display_ranges(&editor.display_snapshot(cx)));
2544                    assert_eq!(
2545                    all_selections.len(),
2546                    expected_query_matches_count,
2547                    "Should select all `a` characters in the buffer, but got: {all_selections:?}"
2548                );
2549                    assert_eq!(
2550                        search_bar.active_match_index,
2551                        Some(1),
2552                        "Match index should not change after selecting all matches"
2553                    );
2554                });
2555                search_bar.update(cx, |search_bar, cx| {
2556                    search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2557                });
2558            })
2559            .unwrap();
2560        let last_match_selections = window
2561            .update(cx, |_, window, cx| {
2562                assert!(
2563                    editor.read(cx).is_focused(window),
2564                    "Should still have editor focused after SelectPreviousMatch"
2565                );
2566
2567                search_bar.update(cx, |search_bar, cx| {
2568                    let all_selections = editor.update(cx, |editor, cx| {
2569                        editor
2570                            .selections
2571                            .display_ranges(&editor.display_snapshot(cx))
2572                    });
2573                    assert_eq!(
2574                        all_selections.len(),
2575                        1,
2576                        "On previous match, should deselect items and select the previous item"
2577                    );
2578                    assert_eq!(
2579                        all_selections, initial_selections,
2580                        "Previous match should be the same as the first selection"
2581                    );
2582                    assert_eq!(
2583                        search_bar.active_match_index,
2584                        Some(0),
2585                        "Match index should be updated to the previous one"
2586                    );
2587                    all_selections
2588                })
2589            })
2590            .unwrap();
2591
2592        window
2593            .update(cx, |_, window, cx| {
2594                search_bar.update(cx, |search_bar, cx| {
2595                    let handle = search_bar.query_editor.focus_handle(cx);
2596                    window.focus(&handle, cx);
2597                    search_bar.search("abas_nonexistent_match", None, true, window, cx)
2598                })
2599            })
2600            .unwrap()
2601            .await
2602            .unwrap();
2603        window
2604            .update(cx, |_, window, cx| {
2605                search_bar.update(cx, |search_bar, cx| {
2606                    search_bar.select_all_matches(&SelectAllMatches, window, cx);
2607                });
2608                assert!(
2609                    editor.update(cx, |this, _cx| !this.is_focused(window)),
2610                    "Should not switch focus to editor if SelectAllMatches does not find any matches"
2611                );
2612                search_bar.update(cx, |search_bar, cx| {
2613                    let all_selections =
2614                        editor.update(cx, |editor, cx| editor.selections.display_ranges(&editor.display_snapshot(cx)));
2615                    assert_eq!(
2616                        all_selections, last_match_selections,
2617                        "Should not select anything new if there are no matches"
2618                    );
2619                    assert!(
2620                        search_bar.active_match_index.is_none(),
2621                        "For no matches, there should be no active match index"
2622                    );
2623                });
2624            })
2625            .unwrap();
2626    }
2627
2628    #[perf]
2629    #[gpui::test]
2630    async fn test_search_query_with_match_whole_word(cx: &mut TestAppContext) {
2631        init_globals(cx);
2632        let buffer_text = r#"
2633        self.buffer.update(cx, |buffer, cx| {
2634            buffer.edit(
2635                edits,
2636                Some(AutoindentMode::Block {
2637                    original_indent_columns,
2638                }),
2639                cx,
2640            )
2641        });
2642
2643        this.buffer.update(cx, |buffer, cx| {
2644            buffer.edit([(end_of_line..start_of_next_line, replace)], None, cx)
2645        });
2646        "#
2647        .unindent();
2648        let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
2649        let cx = cx.add_empty_window();
2650
2651        let editor =
2652            cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
2653
2654        let search_bar = cx.new_window_entity(|window, cx| {
2655            let mut search_bar = BufferSearchBar::new(None, window, cx);
2656            search_bar.set_active_pane_item(Some(&editor), window, cx);
2657            search_bar.show(window, cx);
2658            search_bar
2659        });
2660
2661        search_bar
2662            .update_in(cx, |search_bar, window, cx| {
2663                search_bar.search(
2664                    "edit\\(",
2665                    Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
2666                    true,
2667                    window,
2668                    cx,
2669                )
2670            })
2671            .await
2672            .unwrap();
2673
2674        search_bar.update_in(cx, |search_bar, window, cx| {
2675            search_bar.select_all_matches(&SelectAllMatches, window, cx);
2676        });
2677        search_bar.update(cx, |_, cx| {
2678            let all_selections = editor.update(cx, |editor, cx| {
2679                editor
2680                    .selections
2681                    .display_ranges(&editor.display_snapshot(cx))
2682            });
2683            assert_eq!(
2684                all_selections.len(),
2685                2,
2686                "Should select all `edit(` in the buffer, but got: {all_selections:?}"
2687            );
2688        });
2689
2690        search_bar
2691            .update_in(cx, |search_bar, window, cx| {
2692                search_bar.search(
2693                    "edit(",
2694                    Some(SearchOptions::WHOLE_WORD | SearchOptions::CASE_SENSITIVE),
2695                    true,
2696                    window,
2697                    cx,
2698                )
2699            })
2700            .await
2701            .unwrap();
2702
2703        search_bar.update_in(cx, |search_bar, window, cx| {
2704            search_bar.select_all_matches(&SelectAllMatches, window, cx);
2705        });
2706        search_bar.update(cx, |_, cx| {
2707            let all_selections = editor.update(cx, |editor, cx| {
2708                editor
2709                    .selections
2710                    .display_ranges(&editor.display_snapshot(cx))
2711            });
2712            assert_eq!(
2713                all_selections.len(),
2714                2,
2715                "Should select all `edit(` in the buffer, but got: {all_selections:?}"
2716            );
2717        });
2718    }
2719
2720    #[perf]
2721    #[gpui::test]
2722    async fn test_search_query_history(cx: &mut TestAppContext) {
2723        let (_editor, search_bar, cx) = init_test(cx);
2724
2725        // Add 3 search items into the history.
2726        search_bar
2727            .update_in(cx, |search_bar, window, cx| {
2728                search_bar.search("a", None, true, window, cx)
2729            })
2730            .await
2731            .unwrap();
2732        search_bar
2733            .update_in(cx, |search_bar, window, cx| {
2734                search_bar.search("b", None, true, window, cx)
2735            })
2736            .await
2737            .unwrap();
2738        search_bar
2739            .update_in(cx, |search_bar, window, cx| {
2740                search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), true, window, cx)
2741            })
2742            .await
2743            .unwrap();
2744        // Ensure that the latest search is active.
2745        search_bar.update(cx, |search_bar, cx| {
2746            assert_eq!(search_bar.query(cx), "c");
2747            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2748        });
2749
2750        // Next history query after the latest should preserve the current query.
2751        search_bar.update_in(cx, |search_bar, window, cx| {
2752            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2753        });
2754        cx.background_executor.run_until_parked();
2755        search_bar.update(cx, |search_bar, cx| {
2756            assert_eq!(search_bar.query(cx), "c");
2757            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2758        });
2759        search_bar.update_in(cx, |search_bar, window, cx| {
2760            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2761        });
2762        cx.background_executor.run_until_parked();
2763        search_bar.update(cx, |search_bar, cx| {
2764            assert_eq!(search_bar.query(cx), "c");
2765            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2766        });
2767
2768        // Previous query should navigate backwards through history.
2769        search_bar.update_in(cx, |search_bar, window, cx| {
2770            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2771        });
2772        cx.background_executor.run_until_parked();
2773        search_bar.update(cx, |search_bar, cx| {
2774            assert_eq!(search_bar.query(cx), "b");
2775            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2776        });
2777
2778        // Further previous items should go over the history in reverse order.
2779        search_bar.update_in(cx, |search_bar, window, cx| {
2780            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2781        });
2782        cx.background_executor.run_until_parked();
2783        search_bar.update(cx, |search_bar, cx| {
2784            assert_eq!(search_bar.query(cx), "a");
2785            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2786        });
2787
2788        // Previous items should never go behind the first history item.
2789        search_bar.update_in(cx, |search_bar, window, cx| {
2790            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2791        });
2792        cx.background_executor.run_until_parked();
2793        search_bar.update(cx, |search_bar, cx| {
2794            assert_eq!(search_bar.query(cx), "a");
2795            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2796        });
2797        search_bar.update_in(cx, |search_bar, window, cx| {
2798            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2799        });
2800        cx.background_executor.run_until_parked();
2801        search_bar.update(cx, |search_bar, cx| {
2802            assert_eq!(search_bar.query(cx), "a");
2803            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2804        });
2805
2806        // Next items should go over the history in the original order.
2807        search_bar.update_in(cx, |search_bar, window, cx| {
2808            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2809        });
2810        cx.background_executor.run_until_parked();
2811        search_bar.update(cx, |search_bar, cx| {
2812            assert_eq!(search_bar.query(cx), "b");
2813            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2814        });
2815
2816        search_bar
2817            .update_in(cx, |search_bar, window, cx| {
2818                search_bar.search("ba", None, true, window, cx)
2819            })
2820            .await
2821            .unwrap();
2822        search_bar.update(cx, |search_bar, cx| {
2823            assert_eq!(search_bar.query(cx), "ba");
2824            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2825        });
2826
2827        // New search input should add another entry to history and move the selection to the end of the history.
2828        search_bar.update_in(cx, |search_bar, window, cx| {
2829            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2830        });
2831        cx.background_executor.run_until_parked();
2832        search_bar.update(cx, |search_bar, cx| {
2833            assert_eq!(search_bar.query(cx), "c");
2834            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2835        });
2836        search_bar.update_in(cx, |search_bar, window, cx| {
2837            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2838        });
2839        cx.background_executor.run_until_parked();
2840        search_bar.update(cx, |search_bar, cx| {
2841            assert_eq!(search_bar.query(cx), "b");
2842            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2843        });
2844        search_bar.update_in(cx, |search_bar, window, cx| {
2845            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2846        });
2847        cx.background_executor.run_until_parked();
2848        search_bar.update(cx, |search_bar, cx| {
2849            assert_eq!(search_bar.query(cx), "c");
2850            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2851        });
2852        search_bar.update_in(cx, |search_bar, window, cx| {
2853            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2854        });
2855        cx.background_executor.run_until_parked();
2856        search_bar.update(cx, |search_bar, cx| {
2857            assert_eq!(search_bar.query(cx), "ba");
2858            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2859        });
2860        search_bar.update_in(cx, |search_bar, window, cx| {
2861            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2862        });
2863        cx.background_executor.run_until_parked();
2864        search_bar.update(cx, |search_bar, cx| {
2865            assert_eq!(search_bar.query(cx), "ba");
2866            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2867        });
2868    }
2869
2870    #[perf]
2871    #[gpui::test]
2872    async fn test_search_query_history_autoscroll(cx: &mut TestAppContext) {
2873        let (_editor, search_bar, cx) = init_test(cx);
2874
2875        // Add a long multi-line query that exceeds the editor's max
2876        // visible height (4 lines), then a short query.
2877        let long_query = "line1\nline2\nline3\nline4\nline5\nline6";
2878        search_bar
2879            .update_in(cx, |search_bar, window, cx| {
2880                search_bar.search(long_query, None, true, window, cx)
2881            })
2882            .await
2883            .unwrap();
2884        search_bar
2885            .update_in(cx, |search_bar, window, cx| {
2886                search_bar.search("short", None, true, window, cx)
2887            })
2888            .await
2889            .unwrap();
2890
2891        // Navigate back to the long entry. Since "short" is single-line,
2892        // the history navigation is allowed.
2893        search_bar.update_in(cx, |search_bar, window, cx| {
2894            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2895        });
2896        cx.background_executor.run_until_parked();
2897        search_bar.update(cx, |search_bar, cx| {
2898            assert_eq!(search_bar.query(cx), long_query);
2899        });
2900
2901        // The cursor should be scrolled into view despite the content
2902        // exceeding the editor's max visible height.
2903        search_bar.update_in(cx, |search_bar, window, cx| {
2904            let snapshot = search_bar
2905                .query_editor
2906                .update(cx, |editor, cx| editor.snapshot(window, cx));
2907            let cursor_row = search_bar
2908                .query_editor
2909                .read(cx)
2910                .selections
2911                .newest_display(&snapshot)
2912                .head()
2913                .row();
2914            let scroll_top = search_bar
2915                .query_editor
2916                .update(cx, |editor, cx| editor.scroll_position(cx).y);
2917            let visible_lines = search_bar
2918                .query_editor
2919                .read(cx)
2920                .visible_line_count()
2921                .unwrap_or(0.0);
2922            let scroll_bottom = scroll_top + visible_lines;
2923            assert!(
2924                (cursor_row.0 as f64) < scroll_bottom,
2925                "cursor row {cursor_row:?} should be visible (scroll range {scroll_top}..{scroll_bottom})"
2926            );
2927        });
2928    }
2929
2930    #[perf]
2931    #[gpui::test]
2932    async fn test_replace_simple(cx: &mut TestAppContext) {
2933        let (editor, search_bar, cx) = init_test(cx);
2934
2935        search_bar
2936            .update_in(cx, |search_bar, window, cx| {
2937                search_bar.search("expression", None, true, window, cx)
2938            })
2939            .await
2940            .unwrap();
2941
2942        search_bar.update_in(cx, |search_bar, window, cx| {
2943            search_bar.replacement_editor.update(cx, |editor, cx| {
2944                // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally.
2945                editor.set_text("expr$1", window, cx);
2946            });
2947            search_bar.replace_all(&ReplaceAll, window, cx)
2948        });
2949        assert_eq!(
2950            editor.read_with(cx, |this, cx| { this.text(cx) }),
2951            r#"
2952        A regular expr$1 (shortened as regex or regexp;[1] also referred to as
2953        rational expr$1[2][3]) is a sequence of characters that specifies a search
2954        pattern in text. Usually such patterns are used by string-searching algorithms
2955        for "find" or "find and replace" operations on strings, or for input validation.
2956        "#
2957            .unindent()
2958        );
2959
2960        // Search for word boundaries and replace just a single one.
2961        search_bar
2962            .update_in(cx, |search_bar, window, cx| {
2963                search_bar.search("or", Some(SearchOptions::WHOLE_WORD), true, window, cx)
2964            })
2965            .await
2966            .unwrap();
2967
2968        search_bar.update_in(cx, |search_bar, window, cx| {
2969            search_bar.replacement_editor.update(cx, |editor, cx| {
2970                editor.set_text("banana", window, cx);
2971            });
2972            search_bar.replace_next(&ReplaceNext, window, cx)
2973        });
2974        // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text.
2975        assert_eq!(
2976            editor.read_with(cx, |this, cx| { this.text(cx) }),
2977            r#"
2978        A regular expr$1 (shortened as regex banana regexp;[1] also referred to as
2979        rational expr$1[2][3]) is a sequence of characters that specifies a search
2980        pattern in text. Usually such patterns are used by string-searching algorithms
2981        for "find" or "find and replace" operations on strings, or for input validation.
2982        "#
2983            .unindent()
2984        );
2985        // Let's turn on regex mode.
2986        search_bar
2987            .update_in(cx, |search_bar, window, cx| {
2988                search_bar.search(
2989                    "\\[([^\\]]+)\\]",
2990                    Some(SearchOptions::REGEX),
2991                    true,
2992                    window,
2993                    cx,
2994                )
2995            })
2996            .await
2997            .unwrap();
2998        search_bar.update_in(cx, |search_bar, window, cx| {
2999            search_bar.replacement_editor.update(cx, |editor, cx| {
3000                editor.set_text("${1}number", window, cx);
3001            });
3002            search_bar.replace_all(&ReplaceAll, window, cx)
3003        });
3004        assert_eq!(
3005            editor.read_with(cx, |this, cx| { this.text(cx) }),
3006            r#"
3007        A regular expr$1 (shortened as regex banana regexp;1number also referred to as
3008        rational expr$12number3number) is a sequence of characters that specifies a search
3009        pattern in text. Usually such patterns are used by string-searching algorithms
3010        for "find" or "find and replace" operations on strings, or for input validation.
3011        "#
3012            .unindent()
3013        );
3014        // Now with a whole-word twist.
3015        search_bar
3016            .update_in(cx, |search_bar, window, cx| {
3017                search_bar.search(
3018                    "a\\w+s",
3019                    Some(SearchOptions::REGEX | SearchOptions::WHOLE_WORD),
3020                    true,
3021                    window,
3022                    cx,
3023                )
3024            })
3025            .await
3026            .unwrap();
3027        search_bar.update_in(cx, |search_bar, window, cx| {
3028            search_bar.replacement_editor.update(cx, |editor, cx| {
3029                editor.set_text("things", window, cx);
3030            });
3031            search_bar.replace_all(&ReplaceAll, window, cx)
3032        });
3033        // The only word affected by this edit should be `algorithms`, even though there's a bunch
3034        // of words in this text that would match this regex if not for WHOLE_WORD.
3035        assert_eq!(
3036            editor.read_with(cx, |this, cx| { this.text(cx) }),
3037            r#"
3038        A regular expr$1 (shortened as regex banana regexp;1number also referred to as
3039        rational expr$12number3number) is a sequence of characters that specifies a search
3040        pattern in text. Usually such patterns are used by string-searching things
3041        for "find" or "find and replace" operations on strings, or for input validation.
3042        "#
3043            .unindent()
3044        );
3045    }
3046
3047    #[gpui::test]
3048    async fn test_replace_focus(cx: &mut TestAppContext) {
3049        let (editor, search_bar, cx) = init_test(cx);
3050
3051        editor.update_in(cx, |editor, window, cx| {
3052            editor.set_text("What a bad day!", window, cx)
3053        });
3054
3055        search_bar
3056            .update_in(cx, |search_bar, window, cx| {
3057                search_bar.search("bad", None, true, window, cx)
3058            })
3059            .await
3060            .unwrap();
3061
3062        // Calling `toggle_replace` in the search bar ensures that the "Replace
3063        // *" buttons are rendered, so we can then simulate clicking the
3064        // buttons.
3065        search_bar.update_in(cx, |search_bar, window, cx| {
3066            search_bar.toggle_replace(&ToggleReplace, window, cx)
3067        });
3068
3069        search_bar.update_in(cx, |search_bar, window, cx| {
3070            search_bar.replacement_editor.update(cx, |editor, cx| {
3071                editor.set_text("great", window, cx);
3072            });
3073        });
3074
3075        // Focus on the editor instead of the search bar, as we want to ensure
3076        // that pressing the "Replace Next Match" button will work, even if the
3077        // search bar is not focused.
3078        cx.focus(&editor);
3079
3080        // We'll not simulate clicking the "Replace Next Match " button, asserting that
3081        // the replacement was done.
3082        let button_bounds = cx
3083            .debug_bounds("ICON-ReplaceNext")
3084            .expect("'Replace Next Match' button should be visible");
3085        cx.simulate_click(button_bounds.center(), gpui::Modifiers::none());
3086
3087        assert_eq!(
3088            editor.read_with(cx, |editor, cx| editor.text(cx)),
3089            "What a great day!"
3090        );
3091    }
3092
3093    struct ReplacementTestParams<'a> {
3094        editor: &'a Entity<Editor>,
3095        search_bar: &'a Entity<BufferSearchBar>,
3096        cx: &'a mut VisualTestContext,
3097        search_text: &'static str,
3098        search_options: Option<SearchOptions>,
3099        replacement_text: &'static str,
3100        replace_all: bool,
3101        expected_text: String,
3102    }
3103
3104    async fn run_replacement_test(options: ReplacementTestParams<'_>) {
3105        options
3106            .search_bar
3107            .update_in(options.cx, |search_bar, window, cx| {
3108                if let Some(options) = options.search_options {
3109                    search_bar.set_search_options(options, cx);
3110                }
3111                search_bar.search(
3112                    options.search_text,
3113                    options.search_options,
3114                    true,
3115                    window,
3116                    cx,
3117                )
3118            })
3119            .await
3120            .unwrap();
3121
3122        options
3123            .search_bar
3124            .update_in(options.cx, |search_bar, window, cx| {
3125                search_bar.replacement_editor.update(cx, |editor, cx| {
3126                    editor.set_text(options.replacement_text, window, cx);
3127                });
3128
3129                if options.replace_all {
3130                    search_bar.replace_all(&ReplaceAll, window, cx)
3131                } else {
3132                    search_bar.replace_next(&ReplaceNext, window, cx)
3133                }
3134            });
3135
3136        assert_eq!(
3137            options
3138                .editor
3139                .read_with(options.cx, |this, cx| { this.text(cx) }),
3140            options.expected_text
3141        );
3142    }
3143
3144    #[perf]
3145    #[gpui::test]
3146    async fn test_replace_special_characters(cx: &mut TestAppContext) {
3147        let (editor, search_bar, cx) = init_test(cx);
3148
3149        run_replacement_test(ReplacementTestParams {
3150            editor: &editor,
3151            search_bar: &search_bar,
3152            cx,
3153            search_text: "expression",
3154            search_options: None,
3155            replacement_text: r"\n",
3156            replace_all: true,
3157            expected_text: r#"
3158            A regular \n (shortened as regex or regexp;[1] also referred to as
3159            rational \n[2][3]) is a sequence of characters that specifies a search
3160            pattern in text. Usually such patterns are used by string-searching algorithms
3161            for "find" or "find and replace" operations on strings, or for input validation.
3162            "#
3163            .unindent(),
3164        })
3165        .await;
3166
3167        run_replacement_test(ReplacementTestParams {
3168            editor: &editor,
3169            search_bar: &search_bar,
3170            cx,
3171            search_text: "or",
3172            search_options: Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
3173            replacement_text: r"\\\n\\\\",
3174            replace_all: false,
3175            expected_text: r#"
3176            A regular \n (shortened as regex \
3177            \\ regexp;[1] also referred to as
3178            rational \n[2][3]) is a sequence of characters that specifies a search
3179            pattern in text. Usually such patterns are used by string-searching algorithms
3180            for "find" or "find and replace" operations on strings, or for input validation.
3181            "#
3182            .unindent(),
3183        })
3184        .await;
3185
3186        run_replacement_test(ReplacementTestParams {
3187            editor: &editor,
3188            search_bar: &search_bar,
3189            cx,
3190            search_text: r"(that|used) ",
3191            search_options: Some(SearchOptions::REGEX),
3192            replacement_text: r"$1\n",
3193            replace_all: true,
3194            expected_text: r#"
3195            A regular \n (shortened as regex \
3196            \\ regexp;[1] also referred to as
3197            rational \n[2][3]) is a sequence of characters that
3198            specifies a search
3199            pattern in text. Usually such patterns are used
3200            by string-searching algorithms
3201            for "find" or "find and replace" operations on strings, or for input validation.
3202            "#
3203            .unindent(),
3204        })
3205        .await;
3206    }
3207
3208    #[gpui::test]
3209    async fn test_deploy_replace_focuses_replacement_editor(cx: &mut TestAppContext) {
3210        init_globals(cx);
3211        let (editor, search_bar, cx) = init_test(cx);
3212
3213        editor.update_in(cx, |editor, window, cx| {
3214            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3215                s.select_display_ranges([
3216                    DisplayPoint::new(DisplayRow(0), 8)..DisplayPoint::new(DisplayRow(0), 16)
3217                ])
3218            });
3219        });
3220
3221        search_bar.update_in(cx, |search_bar, window, cx| {
3222            search_bar.deploy(
3223                &Deploy {
3224                    focus: true,
3225                    replace_enabled: true,
3226                    selection_search_enabled: false,
3227                },
3228                window,
3229                cx,
3230            );
3231        });
3232        cx.run_until_parked();
3233
3234        search_bar.update_in(cx, |search_bar, window, cx| {
3235            assert!(
3236                search_bar
3237                    .replacement_editor
3238                    .focus_handle(cx)
3239                    .is_focused(window),
3240                "replacement editor should be focused when deploying replace with a selection",
3241            );
3242            assert!(
3243                !search_bar.query_editor.focus_handle(cx).is_focused(window),
3244                "search editor should not be focused when replacement editor is focused",
3245            );
3246        });
3247    }
3248
3249    #[perf]
3250    #[gpui::test]
3251    async fn test_find_matches_in_selections_singleton_buffer_multiple_selections(
3252        cx: &mut TestAppContext,
3253    ) {
3254        init_globals(cx);
3255        let buffer = cx.new(|cx| {
3256            Buffer::local(
3257                r#"
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                aaa bbb aaa ccc
3264                "#
3265                .unindent(),
3266                cx,
3267            )
3268        });
3269        let cx = cx.add_empty_window();
3270        let editor =
3271            cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
3272
3273        let search_bar = cx.new_window_entity(|window, cx| {
3274            let mut search_bar = BufferSearchBar::new(None, window, cx);
3275            search_bar.set_active_pane_item(Some(&editor), window, cx);
3276            search_bar.show(window, cx);
3277            search_bar
3278        });
3279
3280        editor.update_in(cx, |editor, window, cx| {
3281            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3282                s.select_ranges(vec![Point::new(1, 0)..Point::new(2, 4)])
3283            })
3284        });
3285
3286        search_bar.update_in(cx, |search_bar, window, cx| {
3287            let deploy = Deploy {
3288                focus: true,
3289                replace_enabled: false,
3290                selection_search_enabled: true,
3291            };
3292            search_bar.deploy(&deploy, window, cx);
3293        });
3294
3295        cx.run_until_parked();
3296
3297        search_bar
3298            .update_in(cx, |search_bar, window, cx| {
3299                search_bar.search("aaa", None, true, window, cx)
3300            })
3301            .await
3302            .unwrap();
3303
3304        editor.update(cx, |editor, cx| {
3305            assert_eq!(
3306                editor.search_background_highlights(cx),
3307                &[
3308                    Point::new(1, 0)..Point::new(1, 3),
3309                    Point::new(1, 8)..Point::new(1, 11),
3310                    Point::new(2, 0)..Point::new(2, 3),
3311                ]
3312            );
3313        });
3314    }
3315
3316    #[perf]
3317    #[gpui::test]
3318    async fn test_find_matches_in_selections_multiple_excerpts_buffer_multiple_selections(
3319        cx: &mut TestAppContext,
3320    ) {
3321        init_globals(cx);
3322        let text = r#"
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            aaa bbb aaa ccc
3329
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            aaa bbb aaa ccc
3336            "#
3337        .unindent();
3338
3339        let cx = cx.add_empty_window();
3340        let editor = cx.new_window_entity(|window, cx| {
3341            let multibuffer = MultiBuffer::build_multi(
3342                [
3343                    (
3344                        &text,
3345                        vec![
3346                            Point::new(0, 0)..Point::new(2, 0),
3347                            Point::new(4, 0)..Point::new(5, 0),
3348                        ],
3349                    ),
3350                    (&text, vec![Point::new(9, 0)..Point::new(11, 0)]),
3351                ],
3352                cx,
3353            );
3354            Editor::for_multibuffer(multibuffer, None, window, cx)
3355        });
3356
3357        let search_bar = cx.new_window_entity(|window, cx| {
3358            let mut search_bar = BufferSearchBar::new(None, window, cx);
3359            search_bar.set_active_pane_item(Some(&editor), window, cx);
3360            search_bar.show(window, cx);
3361            search_bar
3362        });
3363
3364        editor.update_in(cx, |editor, window, cx| {
3365            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3366                s.select_ranges(vec![
3367                    Point::new(1, 0)..Point::new(1, 4),
3368                    Point::new(5, 3)..Point::new(6, 4),
3369                ])
3370            })
3371        });
3372
3373        search_bar.update_in(cx, |search_bar, window, cx| {
3374            let deploy = Deploy {
3375                focus: true,
3376                replace_enabled: false,
3377                selection_search_enabled: true,
3378            };
3379            search_bar.deploy(&deploy, window, cx);
3380        });
3381
3382        cx.run_until_parked();
3383
3384        search_bar
3385            .update_in(cx, |search_bar, window, cx| {
3386                search_bar.search("aaa", None, true, window, cx)
3387            })
3388            .await
3389            .unwrap();
3390
3391        editor.update(cx, |editor, cx| {
3392            assert_eq!(
3393                editor.search_background_highlights(cx),
3394                &[
3395                    Point::new(1, 0)..Point::new(1, 3),
3396                    Point::new(5, 8)..Point::new(5, 11),
3397                    Point::new(6, 0)..Point::new(6, 3),
3398                ]
3399            );
3400        });
3401    }
3402
3403    #[perf]
3404    #[gpui::test]
3405    async fn test_hides_and_uses_secondary_when_in_singleton_buffer(cx: &mut TestAppContext) {
3406        let (editor, search_bar, cx) = init_test(cx);
3407
3408        let initial_location = search_bar.update_in(cx, |search_bar, window, cx| {
3409            search_bar.set_active_pane_item(Some(&editor), window, cx)
3410        });
3411
3412        assert_eq!(initial_location, ToolbarItemLocation::Secondary);
3413
3414        let mut events = cx.events::<ToolbarItemEvent, BufferSearchBar>(&search_bar);
3415
3416        search_bar.update_in(cx, |search_bar, window, cx| {
3417            search_bar.dismiss(&Dismiss, window, cx);
3418        });
3419
3420        assert_eq!(
3421            events.try_recv().unwrap(),
3422            (ToolbarItemEvent::ChangeLocation(ToolbarItemLocation::Hidden))
3423        );
3424
3425        search_bar.update_in(cx, |search_bar, window, cx| {
3426            search_bar.show(window, cx);
3427        });
3428
3429        assert_eq!(
3430            events.try_recv().unwrap(),
3431            (ToolbarItemEvent::ChangeLocation(ToolbarItemLocation::Secondary))
3432        );
3433    }
3434
3435    #[perf]
3436    #[gpui::test]
3437    async fn test_uses_primary_left_when_in_multi_buffer(cx: &mut TestAppContext) {
3438        let (editor, search_bar, cx) = init_multibuffer_test(cx);
3439
3440        let initial_location = search_bar.update_in(cx, |search_bar, window, cx| {
3441            search_bar.set_active_pane_item(Some(&editor), window, cx)
3442        });
3443
3444        assert_eq!(initial_location, ToolbarItemLocation::PrimaryLeft);
3445
3446        let mut events = cx.events::<ToolbarItemEvent, BufferSearchBar>(&search_bar);
3447
3448        search_bar.update_in(cx, |search_bar, window, cx| {
3449            search_bar.dismiss(&Dismiss, window, cx);
3450        });
3451
3452        assert_eq!(
3453            events.try_recv().unwrap(),
3454            (ToolbarItemEvent::ChangeLocation(ToolbarItemLocation::PrimaryLeft))
3455        );
3456
3457        search_bar.update_in(cx, |search_bar, window, cx| {
3458            search_bar.show(window, cx);
3459        });
3460
3461        assert_eq!(
3462            events.try_recv().unwrap(),
3463            (ToolbarItemEvent::ChangeLocation(ToolbarItemLocation::PrimaryLeft))
3464        );
3465    }
3466
3467    #[perf]
3468    #[gpui::test]
3469    async fn test_hides_and_uses_secondary_when_part_of_project_search(cx: &mut TestAppContext) {
3470        let (editor, search_bar, cx) = init_multibuffer_test(cx);
3471
3472        editor.update(cx, |editor, _| {
3473            editor.set_in_project_search(true);
3474        });
3475
3476        let initial_location = search_bar.update_in(cx, |search_bar, window, cx| {
3477            search_bar.set_active_pane_item(Some(&editor), window, cx)
3478        });
3479
3480        assert_eq!(initial_location, ToolbarItemLocation::Hidden);
3481
3482        let mut events = cx.events::<ToolbarItemEvent, BufferSearchBar>(&search_bar);
3483
3484        search_bar.update_in(cx, |search_bar, window, cx| {
3485            search_bar.dismiss(&Dismiss, window, cx);
3486        });
3487
3488        assert_eq!(
3489            events.try_recv().unwrap(),
3490            (ToolbarItemEvent::ChangeLocation(ToolbarItemLocation::Hidden))
3491        );
3492
3493        search_bar.update_in(cx, |search_bar, window, cx| {
3494            search_bar.show(window, cx);
3495        });
3496
3497        assert_eq!(
3498            events.try_recv().unwrap(),
3499            (ToolbarItemEvent::ChangeLocation(ToolbarItemLocation::Secondary))
3500        );
3501    }
3502
3503    #[perf]
3504    #[gpui::test]
3505    async fn test_sets_collapsed_when_editor_fold_events_emitted(cx: &mut TestAppContext) {
3506        let (editor, search_bar, cx) = init_multibuffer_test(cx);
3507
3508        search_bar.update_in(cx, |search_bar, window, cx| {
3509            search_bar.set_active_pane_item(Some(&editor), window, cx);
3510        });
3511
3512        editor.update_in(cx, |editor, window, cx| {
3513            editor.fold_all(&FoldAll, window, cx);
3514        });
3515        cx.run_until_parked();
3516
3517        let is_collapsed = editor.read_with(cx, |editor, cx| editor.has_any_buffer_folded(cx));
3518        assert!(is_collapsed);
3519
3520        editor.update_in(cx, |editor, window, cx| {
3521            editor.unfold_all(&UnfoldAll, window, cx);
3522        });
3523        cx.run_until_parked();
3524
3525        let is_collapsed = editor.read_with(cx, |editor, cx| editor.has_any_buffer_folded(cx));
3526        assert!(!is_collapsed);
3527    }
3528
3529    #[perf]
3530    #[gpui::test]
3531    async fn test_collapse_state_syncs_after_manual_buffer_fold(cx: &mut TestAppContext) {
3532        let (editor, search_bar, cx) = init_multibuffer_test(cx);
3533
3534        search_bar.update_in(cx, |search_bar, window, cx| {
3535            search_bar.set_active_pane_item(Some(&editor), window, cx);
3536        });
3537
3538        // Fold all buffers via fold_all
3539        editor.update_in(cx, |editor, window, cx| {
3540            editor.fold_all(&FoldAll, window, cx);
3541        });
3542        cx.run_until_parked();
3543
3544        let has_any_folded = editor.read_with(cx, |editor, cx| editor.has_any_buffer_folded(cx));
3545        assert!(
3546            has_any_folded,
3547            "All buffers should be folded after fold_all"
3548        );
3549
3550        // Manually unfold one buffer (simulating a chevron click)
3551        let first_buffer_id = editor.read_with(cx, |editor, cx| {
3552            editor
3553                .buffer()
3554                .read(cx)
3555                .snapshot(cx)
3556                .excerpts()
3557                .nth(0)
3558                .unwrap()
3559                .context
3560                .start
3561                .buffer_id
3562        });
3563        editor.update_in(cx, |editor, _window, cx| {
3564            editor.unfold_buffer(first_buffer_id, cx);
3565        });
3566
3567        let has_any_folded = editor.read_with(cx, |editor, cx| editor.has_any_buffer_folded(cx));
3568        assert!(
3569            has_any_folded,
3570            "Should still report folds when only one buffer is unfolded"
3571        );
3572
3573        // Manually unfold the second buffer too
3574        let second_buffer_id = editor.read_with(cx, |editor, cx| {
3575            editor
3576                .buffer()
3577                .read(cx)
3578                .snapshot(cx)
3579                .excerpts()
3580                .nth(1)
3581                .unwrap()
3582                .context
3583                .start
3584                .buffer_id
3585        });
3586        editor.update_in(cx, |editor, _window, cx| {
3587            editor.unfold_buffer(second_buffer_id, cx);
3588        });
3589
3590        let has_any_folded = editor.read_with(cx, |editor, cx| editor.has_any_buffer_folded(cx));
3591        assert!(
3592            !has_any_folded,
3593            "No folds should remain after unfolding all buffers individually"
3594        );
3595
3596        // Manually fold one buffer back
3597        editor.update_in(cx, |editor, _window, cx| {
3598            editor.fold_buffer(first_buffer_id, cx);
3599        });
3600
3601        let has_any_folded = editor.read_with(cx, |editor, cx| editor.has_any_buffer_folded(cx));
3602        assert!(
3603            has_any_folded,
3604            "Should report folds after manually folding one buffer"
3605        );
3606    }
3607
3608    #[perf]
3609    #[gpui::test]
3610    async fn test_search_options_changes(cx: &mut TestAppContext) {
3611        let (_editor, search_bar, cx) = init_test(cx);
3612        update_search_settings(
3613            SearchSettings {
3614                button: true,
3615                whole_word: false,
3616                case_sensitive: false,
3617                include_ignored: false,
3618                regex: false,
3619                center_on_match: false,
3620            },
3621            cx,
3622        );
3623
3624        let deploy = Deploy {
3625            focus: true,
3626            replace_enabled: false,
3627            selection_search_enabled: true,
3628        };
3629
3630        search_bar.update_in(cx, |search_bar, window, cx| {
3631            assert_eq!(
3632                search_bar.search_options,
3633                SearchOptions::NONE,
3634                "Should have no search options enabled by default"
3635            );
3636            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
3637            assert_eq!(
3638                search_bar.search_options,
3639                SearchOptions::WHOLE_WORD,
3640                "Should enable the option toggled"
3641            );
3642            assert!(
3643                !search_bar.dismissed,
3644                "Search bar should be present and visible"
3645            );
3646            search_bar.deploy(&deploy, window, cx);
3647            assert_eq!(
3648                search_bar.search_options,
3649                SearchOptions::WHOLE_WORD,
3650                "After (re)deploying, the option should still be enabled"
3651            );
3652
3653            search_bar.dismiss(&Dismiss, window, cx);
3654            search_bar.deploy(&deploy, window, cx);
3655            assert_eq!(
3656                search_bar.search_options,
3657                SearchOptions::WHOLE_WORD,
3658                "After hiding and showing the search bar, search options should be preserved"
3659            );
3660
3661            search_bar.toggle_search_option(SearchOptions::REGEX, window, cx);
3662            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
3663            assert_eq!(
3664                search_bar.search_options,
3665                SearchOptions::REGEX,
3666                "Should enable the options toggled"
3667            );
3668            assert!(
3669                !search_bar.dismissed,
3670                "Search bar should be present and visible"
3671            );
3672            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
3673        });
3674
3675        update_search_settings(
3676            SearchSettings {
3677                button: true,
3678                whole_word: false,
3679                case_sensitive: true,
3680                include_ignored: false,
3681                regex: false,
3682                center_on_match: false,
3683            },
3684            cx,
3685        );
3686        search_bar.update_in(cx, |search_bar, window, cx| {
3687            assert_eq!(
3688                search_bar.search_options,
3689                SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
3690                "Should have no search options enabled by default"
3691            );
3692
3693            search_bar.deploy(&deploy, window, cx);
3694            assert_eq!(
3695                search_bar.search_options,
3696                SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
3697                "Toggling a non-dismissed search bar with custom options should not change the default options"
3698            );
3699            search_bar.dismiss(&Dismiss, window, cx);
3700            search_bar.deploy(&deploy, window, cx);
3701            assert_eq!(
3702                search_bar.configured_options,
3703                SearchOptions::CASE_SENSITIVE,
3704                "After a settings update and toggling the search bar, configured options should be updated"
3705            );
3706            assert_eq!(
3707                search_bar.search_options,
3708                SearchOptions::CASE_SENSITIVE,
3709                "After a settings update and toggling the search bar, configured options should be used"
3710            );
3711        });
3712
3713        update_search_settings(
3714            SearchSettings {
3715                button: true,
3716                whole_word: true,
3717                case_sensitive: true,
3718                include_ignored: false,
3719                regex: false,
3720                center_on_match: false,
3721            },
3722            cx,
3723        );
3724
3725        search_bar.update_in(cx, |search_bar, window, cx| {
3726            search_bar.deploy(&deploy, window, cx);
3727            search_bar.dismiss(&Dismiss, window, cx);
3728            search_bar.show(window, cx);
3729            assert_eq!(
3730                search_bar.search_options,
3731                SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD,
3732                "Calling deploy on an already deployed search bar should not prevent settings updates from being detected"
3733            );
3734        });
3735    }
3736
3737    #[gpui::test]
3738    async fn test_select_occurrence_case_sensitivity(cx: &mut TestAppContext) {
3739        let (editor, search_bar, cx) = init_test(cx);
3740        let mut editor_cx = EditorTestContext::for_editor_in(editor, cx).await;
3741
3742        // Start with case sensitive search settings.
3743        let mut search_settings = SearchSettings::default();
3744        search_settings.case_sensitive = true;
3745        update_search_settings(search_settings, cx);
3746        search_bar.update(cx, |search_bar, cx| {
3747            let mut search_options = search_bar.search_options;
3748            search_options.insert(SearchOptions::CASE_SENSITIVE);
3749            search_bar.set_search_options(search_options, cx);
3750        });
3751
3752        editor_cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
3753        editor_cx.update_editor(|e, window, cx| {
3754            e.select_next(&Default::default(), window, cx).unwrap();
3755        });
3756        editor_cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
3757
3758        // Update the search bar's case sensitivite toggle, so we can later
3759        // confirm that `select_next` will now be case-insensitive.
3760        editor_cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
3761        search_bar.update_in(cx, |search_bar, window, cx| {
3762            search_bar.toggle_case_sensitive(&Default::default(), window, cx);
3763        });
3764        editor_cx.update_editor(|e, window, cx| {
3765            e.select_next(&Default::default(), window, cx).unwrap();
3766        });
3767        editor_cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\nFoo\nfoo");
3768
3769        // Confirm that, after dismissing the search bar, only the editor's
3770        // search settings actually affect the behavior of `select_next`.
3771        search_bar.update_in(cx, |search_bar, window, cx| {
3772            search_bar.dismiss(&Default::default(), window, cx);
3773        });
3774        editor_cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
3775        editor_cx.update_editor(|e, window, cx| {
3776            e.select_next(&Default::default(), window, cx).unwrap();
3777        });
3778        editor_cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
3779
3780        // Update the editor's search settings, disabling case sensitivity, to
3781        // check that the value is respected.
3782        let mut search_settings = SearchSettings::default();
3783        search_settings.case_sensitive = false;
3784        update_search_settings(search_settings, cx);
3785        editor_cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
3786        editor_cx.update_editor(|e, window, cx| {
3787            e.select_next(&Default::default(), window, cx).unwrap();
3788        });
3789        editor_cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\nFoo\nfoo");
3790    }
3791
3792    #[gpui::test]
3793    async fn test_regex_search_does_not_highlight_non_matching_occurrences(
3794        cx: &mut TestAppContext,
3795    ) {
3796        init_globals(cx);
3797        let buffer = cx.new(|cx| {
3798            Buffer::local(
3799                "something is at the top\nsomething is behind something\nsomething is at the bottom\n",
3800                cx,
3801            )
3802        });
3803        let cx = cx.add_empty_window();
3804        let editor =
3805            cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
3806        let search_bar = cx.new_window_entity(|window, cx| {
3807            let mut search_bar = BufferSearchBar::new(None, window, cx);
3808            search_bar.set_active_pane_item(Some(&editor), window, cx);
3809            search_bar.show(window, cx);
3810            search_bar
3811        });
3812
3813        search_bar.update_in(cx, |search_bar, window, cx| {
3814            search_bar.toggle_search_option(SearchOptions::REGEX, window, cx);
3815        });
3816
3817        search_bar
3818            .update_in(cx, |search_bar, window, cx| {
3819                search_bar.search("^something", None, true, window, cx)
3820            })
3821            .await
3822            .unwrap();
3823
3824        search_bar.update_in(cx, |search_bar, window, cx| {
3825            search_bar.select_next_match(&SelectNextMatch, window, cx);
3826        });
3827
3828        // Advance past the debounce so the selection occurrence highlight would
3829        // have fired if it were not suppressed by the active buffer search.
3830        cx.executor()
3831            .advance_clock(SELECTION_HIGHLIGHT_DEBOUNCE_TIMEOUT + Duration::from_millis(1));
3832        cx.run_until_parked();
3833
3834        editor.update(cx, |editor, cx| {
3835            assert!(
3836                !editor.has_background_highlights(HighlightKey::SelectedTextHighlight),
3837                "selection occurrence highlights must be suppressed during buffer search"
3838            );
3839            assert_eq!(
3840                editor.search_background_highlights(cx).len(),
3841                3,
3842                "expected exactly 3 search highlights (one per line start)"
3843            );
3844        });
3845
3846        // Manually select "something" — this should restore occurrence highlights
3847        // because it clears the search-navigation flag.
3848        editor.update_in(cx, |editor, window, cx| {
3849            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3850                s.select_ranges([Point::new(0, 0)..Point::new(0, 9)])
3851            });
3852        });
3853
3854        cx.executor()
3855            .advance_clock(SELECTION_HIGHLIGHT_DEBOUNCE_TIMEOUT + Duration::from_millis(1));
3856        cx.run_until_parked();
3857
3858        editor.update(cx, |editor, _cx| {
3859            assert!(
3860                editor.has_background_highlights(HighlightKey::SelectedTextHighlight),
3861                "selection occurrence highlights must be restored after a manual selection"
3862            );
3863        });
3864    }
3865
3866    fn update_search_settings(search_settings: SearchSettings, cx: &mut TestAppContext) {
3867        cx.update(|cx| {
3868            SettingsStore::update_global(cx, |store, cx| {
3869                store.update_user_settings(cx, |settings| {
3870                    settings.editor.search = Some(SearchSettingsContent {
3871                        button: Some(search_settings.button),
3872                        whole_word: Some(search_settings.whole_word),
3873                        case_sensitive: Some(search_settings.case_sensitive),
3874                        include_ignored: Some(search_settings.include_ignored),
3875                        regex: Some(search_settings.regex),
3876                        center_on_match: Some(search_settings.center_on_match),
3877                    });
3878                });
3879            });
3880        });
3881    }
3882}