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