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 futures::channel::mpsc::UnboundedReceiver;
1762    use gpui::{Hsla, TestAppContext, UpdateGlobal, VisualTestContext};
1763    use language::{Buffer, Point};
1764    use settings::{SearchSettingsContent, SettingsStore, initial_local_debug_tasks_content};
1765    use smol::stream::StreamExt as _;
1766    use tracing::instrument::WithSubscriber;
1767    use unindent::Unindent as _;
1768    use util_macros::perf;
1769
1770    fn init_globals(cx: &mut TestAppContext) {
1771        cx.update(|cx| {
1772            let store = settings::SettingsStore::test(cx);
1773            cx.set_global(store);
1774            editor::init(cx);
1775
1776            theme::init(theme::LoadThemes::JustBase, cx);
1777            crate::init(cx);
1778        });
1779    }
1780
1781    fn init_multibuffer_test(
1782        cx: &mut TestAppContext,
1783    ) -> (
1784        Entity<Editor>,
1785        Entity<BufferSearchBar>,
1786        &mut VisualTestContext,
1787    ) {
1788        init_globals(cx);
1789
1790        let buffer1 = cx.new(|cx| {
1791            Buffer::local(
1792                            r#"
1793                            A regular expression (shortened as regex or regexp;[1] also referred to as
1794                            rational expression[2][3]) is a sequence of characters that specifies a search
1795                            pattern in text. Usually such patterns are used by string-searching algorithms
1796                            for "find" or "find and replace" operations on strings, or for input validation.
1797                            "#
1798                            .unindent(),
1799                            cx,
1800                        )
1801        });
1802
1803        let buffer2 = cx.new(|cx| {
1804            Buffer::local(
1805                r#"
1806                            Some Additional text with the term regular expression in it.
1807                            There two lines.
1808                            "#
1809                .unindent(),
1810                cx,
1811            )
1812        });
1813
1814        let multibuffer = cx.new(|cx| {
1815            let mut buffer = MultiBuffer::new(language::Capability::ReadWrite);
1816
1817            //[ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))]
1818            buffer.push_excerpts(
1819                buffer1,
1820                [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
1821                cx,
1822            );
1823            buffer.push_excerpts(
1824                buffer2,
1825                [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
1826                cx,
1827            );
1828
1829            buffer
1830        });
1831        let mut editor = None;
1832        let window = cx.add_window(|window, cx| {
1833            let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
1834                "keymaps/default-macos.json",
1835                cx,
1836            )
1837            .unwrap();
1838            cx.bind_keys(default_key_bindings);
1839            editor =
1840                Some(cx.new(|cx| Editor::for_multibuffer(multibuffer.clone(), None, window, cx)));
1841
1842            let mut search_bar = BufferSearchBar::new(None, window, cx);
1843            search_bar.set_active_pane_item(Some(&editor.clone().unwrap()), window, cx);
1844            search_bar.show(window, cx);
1845            search_bar
1846        });
1847        let search_bar = window.root(cx).unwrap();
1848
1849        let cx = VisualTestContext::from_window(*window, cx).into_mut();
1850
1851        (editor.unwrap(), search_bar, cx)
1852    }
1853
1854    fn init_test(
1855        cx: &mut TestAppContext,
1856    ) -> (
1857        Entity<Editor>,
1858        Entity<BufferSearchBar>,
1859        &mut VisualTestContext,
1860    ) {
1861        init_globals(cx);
1862        let buffer = cx.new(|cx| {
1863            Buffer::local(
1864                r#"
1865                A regular expression (shortened as regex or regexp;[1] also referred to as
1866                rational expression[2][3]) is a sequence of characters that specifies a search
1867                pattern in text. Usually such patterns are used by string-searching algorithms
1868                for "find" or "find and replace" operations on strings, or for input validation.
1869                "#
1870                .unindent(),
1871                cx,
1872            )
1873        });
1874        let mut editor = None;
1875        let window = cx.add_window(|window, cx| {
1876            let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
1877                "keymaps/default-macos.json",
1878                cx,
1879            )
1880            .unwrap();
1881            cx.bind_keys(default_key_bindings);
1882            editor = Some(cx.new(|cx| Editor::for_buffer(buffer.clone(), None, window, cx)));
1883            let mut search_bar = BufferSearchBar::new(None, window, cx);
1884            search_bar.set_active_pane_item(Some(&editor.clone().unwrap()), window, cx);
1885            search_bar.show(window, cx);
1886            search_bar
1887        });
1888        let search_bar = window.root(cx).unwrap();
1889
1890        let cx = VisualTestContext::from_window(*window, cx).into_mut();
1891
1892        (editor.unwrap(), search_bar, cx)
1893    }
1894
1895    #[perf]
1896    #[gpui::test]
1897    async fn test_search_simple(cx: &mut TestAppContext) {
1898        let (editor, search_bar, cx) = init_test(cx);
1899        let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
1900            background_highlights
1901                .into_iter()
1902                .map(|(range, _)| range)
1903                .collect::<Vec<_>>()
1904        };
1905        // Search for a string that appears with different casing.
1906        // By default, search is case-insensitive.
1907        search_bar
1908            .update_in(cx, |search_bar, window, cx| {
1909                search_bar.search("us", None, true, window, cx)
1910            })
1911            .await
1912            .unwrap();
1913        editor.update_in(cx, |editor, window, cx| {
1914            assert_eq!(
1915                display_points_of(editor.all_text_background_highlights(window, cx)),
1916                &[
1917                    DisplayPoint::new(DisplayRow(2), 17)..DisplayPoint::new(DisplayRow(2), 19),
1918                    DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),
1919                ]
1920            );
1921        });
1922
1923        // Switch to a case sensitive search.
1924        search_bar.update_in(cx, |search_bar, window, cx| {
1925            search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
1926        });
1927        let mut editor_notifications = cx.notifications(&editor);
1928        editor_notifications.next().await;
1929        editor.update_in(cx, |editor, window, cx| {
1930            assert_eq!(
1931                display_points_of(editor.all_text_background_highlights(window, cx)),
1932                &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
1933            );
1934        });
1935
1936        // Search for a string that appears both as a whole word and
1937        // within other words. By default, all results are found.
1938        search_bar
1939            .update_in(cx, |search_bar, window, cx| {
1940                search_bar.search("or", None, true, window, cx)
1941            })
1942            .await
1943            .unwrap();
1944        editor.update_in(cx, |editor, window, cx| {
1945            assert_eq!(
1946                display_points_of(editor.all_text_background_highlights(window, cx)),
1947                &[
1948                    DisplayPoint::new(DisplayRow(0), 24)..DisplayPoint::new(DisplayRow(0), 26),
1949                    DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1950                    DisplayPoint::new(DisplayRow(2), 71)..DisplayPoint::new(DisplayRow(2), 73),
1951                    DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 3),
1952                    DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
1953                    DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
1954                    DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 62),
1955                ]
1956            );
1957        });
1958
1959        // Switch to a whole word search.
1960        search_bar.update_in(cx, |search_bar, window, cx| {
1961            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
1962        });
1963        let mut editor_notifications = cx.notifications(&editor);
1964        editor_notifications.next().await;
1965        editor.update_in(cx, |editor, window, cx| {
1966            assert_eq!(
1967                display_points_of(editor.all_text_background_highlights(window, cx)),
1968                &[
1969                    DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1970                    DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
1971                    DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
1972                ]
1973            );
1974        });
1975
1976        editor.update_in(cx, |editor, window, cx| {
1977            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1978                s.select_display_ranges([
1979                    DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
1980                ])
1981            });
1982        });
1983        search_bar.update_in(cx, |search_bar, window, cx| {
1984            assert_eq!(search_bar.active_match_index, Some(0));
1985            search_bar.select_next_match(&SelectNextMatch, window, cx);
1986            assert_eq!(
1987                editor.update(cx, |editor, cx| editor
1988                    .selections
1989                    .display_ranges(&editor.display_snapshot(cx))),
1990                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1991            );
1992        });
1993        search_bar.read_with(cx, |search_bar, _| {
1994            assert_eq!(search_bar.active_match_index, Some(0));
1995        });
1996
1997        search_bar.update_in(cx, |search_bar, window, cx| {
1998            search_bar.select_next_match(&SelectNextMatch, window, cx);
1999            assert_eq!(
2000                editor.update(cx, |editor, cx| editor
2001                    .selections
2002                    .display_ranges(&editor.display_snapshot(cx))),
2003                [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
2004            );
2005        });
2006        search_bar.read_with(cx, |search_bar, _| {
2007            assert_eq!(search_bar.active_match_index, Some(1));
2008        });
2009
2010        search_bar.update_in(cx, |search_bar, window, cx| {
2011            search_bar.select_next_match(&SelectNextMatch, window, cx);
2012            assert_eq!(
2013                editor.update(cx, |editor, cx| editor
2014                    .selections
2015                    .display_ranges(&editor.display_snapshot(cx))),
2016                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
2017            );
2018        });
2019        search_bar.read_with(cx, |search_bar, _| {
2020            assert_eq!(search_bar.active_match_index, Some(2));
2021        });
2022
2023        search_bar.update_in(cx, |search_bar, window, cx| {
2024            search_bar.select_next_match(&SelectNextMatch, window, cx);
2025            assert_eq!(
2026                editor.update(cx, |editor, cx| editor
2027                    .selections
2028                    .display_ranges(&editor.display_snapshot(cx))),
2029                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
2030            );
2031        });
2032        search_bar.read_with(cx, |search_bar, _| {
2033            assert_eq!(search_bar.active_match_index, Some(0));
2034        });
2035
2036        search_bar.update_in(cx, |search_bar, window, cx| {
2037            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2038            assert_eq!(
2039                editor.update(cx, |editor, cx| editor
2040                    .selections
2041                    .display_ranges(&editor.display_snapshot(cx))),
2042                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
2043            );
2044        });
2045        search_bar.read_with(cx, |search_bar, _| {
2046            assert_eq!(search_bar.active_match_index, Some(2));
2047        });
2048
2049        search_bar.update_in(cx, |search_bar, window, cx| {
2050            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2051            assert_eq!(
2052                editor.update(cx, |editor, cx| editor
2053                    .selections
2054                    .display_ranges(&editor.display_snapshot(cx))),
2055                [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
2056            );
2057        });
2058        search_bar.read_with(cx, |search_bar, _| {
2059            assert_eq!(search_bar.active_match_index, Some(1));
2060        });
2061
2062        search_bar.update_in(cx, |search_bar, window, cx| {
2063            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2064            assert_eq!(
2065                editor.update(cx, |editor, cx| editor
2066                    .selections
2067                    .display_ranges(&editor.display_snapshot(cx))),
2068                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
2069            );
2070        });
2071        search_bar.read_with(cx, |search_bar, _| {
2072            assert_eq!(search_bar.active_match_index, Some(0));
2073        });
2074
2075        // Park the cursor in between matches and ensure that going to the previous match selects
2076        // the closest match to the left.
2077        editor.update_in(cx, |editor, window, cx| {
2078            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2079                s.select_display_ranges([
2080                    DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
2081                ])
2082            });
2083        });
2084        search_bar.update_in(cx, |search_bar, window, cx| {
2085            assert_eq!(search_bar.active_match_index, Some(1));
2086            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2087            assert_eq!(
2088                editor.update(cx, |editor, cx| editor
2089                    .selections
2090                    .display_ranges(&editor.display_snapshot(cx))),
2091                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
2092            );
2093        });
2094        search_bar.read_with(cx, |search_bar, _| {
2095            assert_eq!(search_bar.active_match_index, Some(0));
2096        });
2097
2098        // Park the cursor in between matches and ensure that going to the next match selects the
2099        // closest match to the right.
2100        editor.update_in(cx, |editor, window, cx| {
2101            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2102                s.select_display_ranges([
2103                    DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
2104                ])
2105            });
2106        });
2107        search_bar.update_in(cx, |search_bar, window, cx| {
2108            assert_eq!(search_bar.active_match_index, Some(1));
2109            search_bar.select_next_match(&SelectNextMatch, window, cx);
2110            assert_eq!(
2111                editor.update(cx, |editor, cx| editor
2112                    .selections
2113                    .display_ranges(&editor.display_snapshot(cx))),
2114                [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
2115            );
2116        });
2117        search_bar.read_with(cx, |search_bar, _| {
2118            assert_eq!(search_bar.active_match_index, Some(1));
2119        });
2120
2121        // Park the cursor after the last match and ensure that going to the previous match selects
2122        // the last match.
2123        editor.update_in(cx, |editor, window, cx| {
2124            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2125                s.select_display_ranges([
2126                    DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
2127                ])
2128            });
2129        });
2130        search_bar.update_in(cx, |search_bar, window, cx| {
2131            assert_eq!(search_bar.active_match_index, Some(2));
2132            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2133            assert_eq!(
2134                editor.update(cx, |editor, cx| editor
2135                    .selections
2136                    .display_ranges(&editor.display_snapshot(cx))),
2137                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
2138            );
2139        });
2140        search_bar.read_with(cx, |search_bar, _| {
2141            assert_eq!(search_bar.active_match_index, Some(2));
2142        });
2143
2144        // Park the cursor after the last match and ensure that going to the next match selects the
2145        // first match.
2146        editor.update_in(cx, |editor, window, cx| {
2147            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2148                s.select_display_ranges([
2149                    DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
2150                ])
2151            });
2152        });
2153        search_bar.update_in(cx, |search_bar, window, cx| {
2154            assert_eq!(search_bar.active_match_index, Some(2));
2155            search_bar.select_next_match(&SelectNextMatch, window, cx);
2156            assert_eq!(
2157                editor.update(cx, |editor, cx| editor
2158                    .selections
2159                    .display_ranges(&editor.display_snapshot(cx))),
2160                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
2161            );
2162        });
2163        search_bar.read_with(cx, |search_bar, _| {
2164            assert_eq!(search_bar.active_match_index, Some(0));
2165        });
2166
2167        // Park the cursor before the first match and ensure that going to the previous match
2168        // selects the last match.
2169        editor.update_in(cx, |editor, window, cx| {
2170            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2171                s.select_display_ranges([
2172                    DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2173                ])
2174            });
2175        });
2176        search_bar.update_in(cx, |search_bar, window, cx| {
2177            assert_eq!(search_bar.active_match_index, Some(0));
2178            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2179            assert_eq!(
2180                editor.update(cx, |editor, cx| editor
2181                    .selections
2182                    .display_ranges(&editor.display_snapshot(cx))),
2183                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
2184            );
2185        });
2186        search_bar.read_with(cx, |search_bar, _| {
2187            assert_eq!(search_bar.active_match_index, Some(2));
2188        });
2189    }
2190
2191    fn display_points_of(
2192        background_highlights: Vec<(Range<DisplayPoint>, Hsla)>,
2193    ) -> Vec<Range<DisplayPoint>> {
2194        background_highlights
2195            .into_iter()
2196            .map(|(range, _)| range)
2197            .collect::<Vec<_>>()
2198    }
2199
2200    #[perf]
2201    #[gpui::test]
2202    async fn test_search_option_handling(cx: &mut TestAppContext) {
2203        let (editor, search_bar, cx) = init_test(cx);
2204
2205        // show with options should make current search case sensitive
2206        search_bar
2207            .update_in(cx, |search_bar, window, cx| {
2208                search_bar.show(window, cx);
2209                search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), true, window, cx)
2210            })
2211            .await
2212            .unwrap();
2213        editor.update_in(cx, |editor, window, cx| {
2214            assert_eq!(
2215                display_points_of(editor.all_text_background_highlights(window, cx)),
2216                &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
2217            );
2218        });
2219
2220        // search_suggested should restore default options
2221        search_bar.update_in(cx, |search_bar, window, cx| {
2222            search_bar.search_suggested(window, cx);
2223            assert_eq!(search_bar.search_options, SearchOptions::NONE)
2224        });
2225
2226        // toggling a search option should update the defaults
2227        search_bar
2228            .update_in(cx, |search_bar, window, cx| {
2229                search_bar.search(
2230                    "regex",
2231                    Some(SearchOptions::CASE_SENSITIVE),
2232                    true,
2233                    window,
2234                    cx,
2235                )
2236            })
2237            .await
2238            .unwrap();
2239        search_bar.update_in(cx, |search_bar, window, cx| {
2240            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx)
2241        });
2242        let mut editor_notifications = cx.notifications(&editor);
2243        editor_notifications.next().await;
2244        editor.update_in(cx, |editor, window, cx| {
2245            assert_eq!(
2246                display_points_of(editor.all_text_background_highlights(window, cx)),
2247                &[DisplayPoint::new(DisplayRow(0), 35)..DisplayPoint::new(DisplayRow(0), 40),]
2248            );
2249        });
2250
2251        // defaults should still include whole word
2252        search_bar.update_in(cx, |search_bar, window, cx| {
2253            search_bar.search_suggested(window, cx);
2254            assert_eq!(
2255                search_bar.search_options,
2256                SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD
2257            )
2258        });
2259    }
2260
2261    #[perf]
2262    #[gpui::test]
2263    async fn test_search_select_all_matches(cx: &mut TestAppContext) {
2264        init_globals(cx);
2265        let buffer_text = r#"
2266        A regular expression (shortened as regex or regexp;[1] also referred to as
2267        rational expression[2][3]) is a sequence of characters that specifies a search
2268        pattern in text. Usually such patterns are used by string-searching algorithms
2269        for "find" or "find and replace" operations on strings, or for input validation.
2270        "#
2271        .unindent();
2272        let expected_query_matches_count = buffer_text
2273            .chars()
2274            .filter(|c| c.eq_ignore_ascii_case(&'a'))
2275            .count();
2276        assert!(
2277            expected_query_matches_count > 1,
2278            "Should pick a query with multiple results"
2279        );
2280        let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
2281        let window = cx.add_window(|_, _| gpui::Empty);
2282
2283        let editor = window.build_entity(cx, |window, cx| {
2284            Editor::for_buffer(buffer.clone(), None, window, cx)
2285        });
2286
2287        let search_bar = window.build_entity(cx, |window, cx| {
2288            let mut search_bar = BufferSearchBar::new(None, window, cx);
2289            search_bar.set_active_pane_item(Some(&editor), window, cx);
2290            search_bar.show(window, cx);
2291            search_bar
2292        });
2293
2294        window
2295            .update(cx, |_, window, cx| {
2296                search_bar.update(cx, |search_bar, cx| {
2297                    search_bar.search("a", None, true, window, cx)
2298                })
2299            })
2300            .unwrap()
2301            .await
2302            .unwrap();
2303        let initial_selections = window
2304            .update(cx, |_, window, cx| {
2305                search_bar.update(cx, |search_bar, cx| {
2306                    let handle = search_bar.query_editor.focus_handle(cx);
2307                    window.focus(&handle, cx);
2308                    search_bar.activate_current_match(window, cx);
2309                });
2310                assert!(
2311                    !editor.read(cx).is_focused(window),
2312                    "Initially, the editor should not be focused"
2313                );
2314                let initial_selections = editor.update(cx, |editor, cx| {
2315                    let initial_selections = editor.selections.display_ranges(&editor.display_snapshot(cx));
2316                    assert_eq!(
2317                        initial_selections.len(), 1,
2318                        "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
2319                    );
2320                    initial_selections
2321                });
2322                search_bar.update(cx, |search_bar, cx| {
2323                    assert_eq!(search_bar.active_match_index, Some(0));
2324                    let handle = search_bar.query_editor.focus_handle(cx);
2325                    window.focus(&handle, cx);
2326                    search_bar.select_all_matches(&SelectAllMatches, window, cx);
2327                });
2328                assert!(
2329                    editor.read(cx).is_focused(window),
2330                    "Should focus editor after successful SelectAllMatches"
2331                );
2332                search_bar.update(cx, |search_bar, cx| {
2333                    let all_selections =
2334                        editor.update(cx, |editor, cx| editor.selections.display_ranges(&editor.display_snapshot(cx)));
2335                    assert_eq!(
2336                        all_selections.len(),
2337                        expected_query_matches_count,
2338                        "Should select all `a` characters in the buffer, but got: {all_selections:?}"
2339                    );
2340                    assert_eq!(
2341                        search_bar.active_match_index,
2342                        Some(0),
2343                        "Match index should not change after selecting all matches"
2344                    );
2345                });
2346
2347                search_bar.update(cx, |this, cx| this.select_next_match(&SelectNextMatch, window, cx));
2348                initial_selections
2349            }).unwrap();
2350
2351        window
2352            .update(cx, |_, window, cx| {
2353                assert!(
2354                    editor.read(cx).is_focused(window),
2355                    "Should still have editor focused after SelectNextMatch"
2356                );
2357                search_bar.update(cx, |search_bar, cx| {
2358                    let all_selections = editor.update(cx, |editor, cx| {
2359                        editor
2360                            .selections
2361                            .display_ranges(&editor.display_snapshot(cx))
2362                    });
2363                    assert_eq!(
2364                        all_selections.len(),
2365                        1,
2366                        "On next match, should deselect items and select the next match"
2367                    );
2368                    assert_ne!(
2369                        all_selections, initial_selections,
2370                        "Next match should be different from the first selection"
2371                    );
2372                    assert_eq!(
2373                        search_bar.active_match_index,
2374                        Some(1),
2375                        "Match index should be updated to the next one"
2376                    );
2377                    let handle = search_bar.query_editor.focus_handle(cx);
2378                    window.focus(&handle, cx);
2379                    search_bar.select_all_matches(&SelectAllMatches, window, cx);
2380                });
2381            })
2382            .unwrap();
2383        window
2384            .update(cx, |_, window, cx| {
2385                assert!(
2386                    editor.read(cx).is_focused(window),
2387                    "Should focus editor after successful SelectAllMatches"
2388                );
2389                search_bar.update(cx, |search_bar, cx| {
2390                    let all_selections =
2391                        editor.update(cx, |editor, cx| editor.selections.display_ranges(&editor.display_snapshot(cx)));
2392                    assert_eq!(
2393                    all_selections.len(),
2394                    expected_query_matches_count,
2395                    "Should select all `a` characters in the buffer, but got: {all_selections:?}"
2396                );
2397                    assert_eq!(
2398                        search_bar.active_match_index,
2399                        Some(1),
2400                        "Match index should not change after selecting all matches"
2401                    );
2402                });
2403                search_bar.update(cx, |search_bar, cx| {
2404                    search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2405                });
2406            })
2407            .unwrap();
2408        let last_match_selections = window
2409            .update(cx, |_, window, cx| {
2410                assert!(
2411                    editor.read(cx).is_focused(window),
2412                    "Should still have editor focused after SelectPreviousMatch"
2413                );
2414
2415                search_bar.update(cx, |search_bar, cx| {
2416                    let all_selections = editor.update(cx, |editor, cx| {
2417                        editor
2418                            .selections
2419                            .display_ranges(&editor.display_snapshot(cx))
2420                    });
2421                    assert_eq!(
2422                        all_selections.len(),
2423                        1,
2424                        "On previous match, should deselect items and select the previous item"
2425                    );
2426                    assert_eq!(
2427                        all_selections, initial_selections,
2428                        "Previous match should be the same as the first selection"
2429                    );
2430                    assert_eq!(
2431                        search_bar.active_match_index,
2432                        Some(0),
2433                        "Match index should be updated to the previous one"
2434                    );
2435                    all_selections
2436                })
2437            })
2438            .unwrap();
2439
2440        window
2441            .update(cx, |_, window, cx| {
2442                search_bar.update(cx, |search_bar, cx| {
2443                    let handle = search_bar.query_editor.focus_handle(cx);
2444                    window.focus(&handle, cx);
2445                    search_bar.search("abas_nonexistent_match", None, true, window, cx)
2446                })
2447            })
2448            .unwrap()
2449            .await
2450            .unwrap();
2451        window
2452            .update(cx, |_, window, cx| {
2453                search_bar.update(cx, |search_bar, cx| {
2454                    search_bar.select_all_matches(&SelectAllMatches, window, cx);
2455                });
2456                assert!(
2457                    editor.update(cx, |this, _cx| !this.is_focused(window)),
2458                    "Should not switch focus to editor if SelectAllMatches does not find any matches"
2459                );
2460                search_bar.update(cx, |search_bar, cx| {
2461                    let all_selections =
2462                        editor.update(cx, |editor, cx| editor.selections.display_ranges(&editor.display_snapshot(cx)));
2463                    assert_eq!(
2464                        all_selections, last_match_selections,
2465                        "Should not select anything new if there are no matches"
2466                    );
2467                    assert!(
2468                        search_bar.active_match_index.is_none(),
2469                        "For no matches, there should be no active match index"
2470                    );
2471                });
2472            })
2473            .unwrap();
2474    }
2475
2476    #[perf]
2477    #[gpui::test]
2478    async fn test_search_query_with_match_whole_word(cx: &mut TestAppContext) {
2479        init_globals(cx);
2480        let buffer_text = r#"
2481        self.buffer.update(cx, |buffer, cx| {
2482            buffer.edit(
2483                edits,
2484                Some(AutoindentMode::Block {
2485                    original_indent_columns,
2486                }),
2487                cx,
2488            )
2489        });
2490
2491        this.buffer.update(cx, |buffer, cx| {
2492            buffer.edit([(end_of_line..start_of_next_line, replace)], None, cx)
2493        });
2494        "#
2495        .unindent();
2496        let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
2497        let cx = cx.add_empty_window();
2498
2499        let editor =
2500            cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
2501
2502        let search_bar = cx.new_window_entity(|window, cx| {
2503            let mut search_bar = BufferSearchBar::new(None, window, cx);
2504            search_bar.set_active_pane_item(Some(&editor), window, cx);
2505            search_bar.show(window, cx);
2506            search_bar
2507        });
2508
2509        search_bar
2510            .update_in(cx, |search_bar, window, cx| {
2511                search_bar.search(
2512                    "edit\\(",
2513                    Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
2514                    true,
2515                    window,
2516                    cx,
2517                )
2518            })
2519            .await
2520            .unwrap();
2521
2522        search_bar.update_in(cx, |search_bar, window, cx| {
2523            search_bar.select_all_matches(&SelectAllMatches, window, cx);
2524        });
2525        search_bar.update(cx, |_, cx| {
2526            let all_selections = editor.update(cx, |editor, cx| {
2527                editor
2528                    .selections
2529                    .display_ranges(&editor.display_snapshot(cx))
2530            });
2531            assert_eq!(
2532                all_selections.len(),
2533                2,
2534                "Should select all `edit(` in the buffer, but got: {all_selections:?}"
2535            );
2536        });
2537
2538        search_bar
2539            .update_in(cx, |search_bar, window, cx| {
2540                search_bar.search(
2541                    "edit(",
2542                    Some(SearchOptions::WHOLE_WORD | SearchOptions::CASE_SENSITIVE),
2543                    true,
2544                    window,
2545                    cx,
2546                )
2547            })
2548            .await
2549            .unwrap();
2550
2551        search_bar.update_in(cx, |search_bar, window, cx| {
2552            search_bar.select_all_matches(&SelectAllMatches, window, cx);
2553        });
2554        search_bar.update(cx, |_, cx| {
2555            let all_selections = editor.update(cx, |editor, cx| {
2556                editor
2557                    .selections
2558                    .display_ranges(&editor.display_snapshot(cx))
2559            });
2560            assert_eq!(
2561                all_selections.len(),
2562                2,
2563                "Should select all `edit(` in the buffer, but got: {all_selections:?}"
2564            );
2565        });
2566    }
2567
2568    #[perf]
2569    #[gpui::test]
2570    async fn test_search_query_history(cx: &mut TestAppContext) {
2571        let (_editor, search_bar, cx) = init_test(cx);
2572
2573        // Add 3 search items into the history.
2574        search_bar
2575            .update_in(cx, |search_bar, window, cx| {
2576                search_bar.search("a", None, true, window, cx)
2577            })
2578            .await
2579            .unwrap();
2580        search_bar
2581            .update_in(cx, |search_bar, window, cx| {
2582                search_bar.search("b", None, true, window, cx)
2583            })
2584            .await
2585            .unwrap();
2586        search_bar
2587            .update_in(cx, |search_bar, window, cx| {
2588                search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), true, window, cx)
2589            })
2590            .await
2591            .unwrap();
2592        // Ensure that the latest search is active.
2593        search_bar.update(cx, |search_bar, cx| {
2594            assert_eq!(search_bar.query(cx), "c");
2595            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2596        });
2597
2598        // Next history query after the latest should set the query to the empty string.
2599        search_bar.update_in(cx, |search_bar, window, cx| {
2600            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2601        });
2602        cx.background_executor.run_until_parked();
2603        search_bar.update(cx, |search_bar, cx| {
2604            assert_eq!(search_bar.query(cx), "");
2605            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2606        });
2607        search_bar.update_in(cx, |search_bar, window, cx| {
2608            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2609        });
2610        cx.background_executor.run_until_parked();
2611        search_bar.update(cx, |search_bar, cx| {
2612            assert_eq!(search_bar.query(cx), "");
2613            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2614        });
2615
2616        // First previous query for empty current query should set the query to the latest.
2617        search_bar.update_in(cx, |search_bar, window, cx| {
2618            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2619        });
2620        cx.background_executor.run_until_parked();
2621        search_bar.update(cx, |search_bar, cx| {
2622            assert_eq!(search_bar.query(cx), "c");
2623            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2624        });
2625
2626        // Further previous items should go over the history in reverse order.
2627        search_bar.update_in(cx, |search_bar, window, cx| {
2628            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2629        });
2630        cx.background_executor.run_until_parked();
2631        search_bar.update(cx, |search_bar, cx| {
2632            assert_eq!(search_bar.query(cx), "b");
2633            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2634        });
2635
2636        // Previous items should never go behind the first history item.
2637        search_bar.update_in(cx, |search_bar, window, cx| {
2638            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2639        });
2640        cx.background_executor.run_until_parked();
2641        search_bar.update(cx, |search_bar, cx| {
2642            assert_eq!(search_bar.query(cx), "a");
2643            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2644        });
2645        search_bar.update_in(cx, |search_bar, window, cx| {
2646            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2647        });
2648        cx.background_executor.run_until_parked();
2649        search_bar.update(cx, |search_bar, cx| {
2650            assert_eq!(search_bar.query(cx), "a");
2651            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2652        });
2653
2654        // Next items should go over the history in the original order.
2655        search_bar.update_in(cx, |search_bar, window, cx| {
2656            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2657        });
2658        cx.background_executor.run_until_parked();
2659        search_bar.update(cx, |search_bar, cx| {
2660            assert_eq!(search_bar.query(cx), "b");
2661            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2662        });
2663
2664        search_bar
2665            .update_in(cx, |search_bar, window, cx| {
2666                search_bar.search("ba", None, true, window, cx)
2667            })
2668            .await
2669            .unwrap();
2670        search_bar.update(cx, |search_bar, cx| {
2671            assert_eq!(search_bar.query(cx), "ba");
2672            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2673        });
2674
2675        // New search input should add another entry to history and move the selection to the end of the history.
2676        search_bar.update_in(cx, |search_bar, window, cx| {
2677            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2678        });
2679        cx.background_executor.run_until_parked();
2680        search_bar.update(cx, |search_bar, cx| {
2681            assert_eq!(search_bar.query(cx), "c");
2682            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2683        });
2684        search_bar.update_in(cx, |search_bar, window, cx| {
2685            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2686        });
2687        cx.background_executor.run_until_parked();
2688        search_bar.update(cx, |search_bar, cx| {
2689            assert_eq!(search_bar.query(cx), "b");
2690            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2691        });
2692        search_bar.update_in(cx, |search_bar, window, cx| {
2693            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2694        });
2695        cx.background_executor.run_until_parked();
2696        search_bar.update(cx, |search_bar, cx| {
2697            assert_eq!(search_bar.query(cx), "c");
2698            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2699        });
2700        search_bar.update_in(cx, |search_bar, window, cx| {
2701            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2702        });
2703        cx.background_executor.run_until_parked();
2704        search_bar.update(cx, |search_bar, cx| {
2705            assert_eq!(search_bar.query(cx), "ba");
2706            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2707        });
2708        search_bar.update_in(cx, |search_bar, window, cx| {
2709            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2710        });
2711        cx.background_executor.run_until_parked();
2712        search_bar.update(cx, |search_bar, cx| {
2713            assert_eq!(search_bar.query(cx), "");
2714            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2715        });
2716    }
2717
2718    #[perf]
2719    #[gpui::test]
2720    async fn test_replace_simple(cx: &mut TestAppContext) {
2721        let (editor, search_bar, cx) = init_test(cx);
2722
2723        search_bar
2724            .update_in(cx, |search_bar, window, cx| {
2725                search_bar.search("expression", None, true, window, cx)
2726            })
2727            .await
2728            .unwrap();
2729
2730        search_bar.update_in(cx, |search_bar, window, cx| {
2731            search_bar.replacement_editor.update(cx, |editor, cx| {
2732                // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally.
2733                editor.set_text("expr$1", window, cx);
2734            });
2735            search_bar.replace_all(&ReplaceAll, window, cx)
2736        });
2737        assert_eq!(
2738            editor.read_with(cx, |this, cx| { this.text(cx) }),
2739            r#"
2740        A regular expr$1 (shortened as regex or regexp;[1] also referred to as
2741        rational expr$1[2][3]) is a sequence of characters that specifies a search
2742        pattern in text. Usually such patterns are used by string-searching algorithms
2743        for "find" or "find and replace" operations on strings, or for input validation.
2744        "#
2745            .unindent()
2746        );
2747
2748        // Search for word boundaries and replace just a single one.
2749        search_bar
2750            .update_in(cx, |search_bar, window, cx| {
2751                search_bar.search("or", Some(SearchOptions::WHOLE_WORD), true, window, cx)
2752            })
2753            .await
2754            .unwrap();
2755
2756        search_bar.update_in(cx, |search_bar, window, cx| {
2757            search_bar.replacement_editor.update(cx, |editor, cx| {
2758                editor.set_text("banana", window, cx);
2759            });
2760            search_bar.replace_next(&ReplaceNext, window, cx)
2761        });
2762        // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text.
2763        assert_eq!(
2764            editor.read_with(cx, |this, cx| { this.text(cx) }),
2765            r#"
2766        A regular expr$1 (shortened as regex banana regexp;[1] also referred to as
2767        rational expr$1[2][3]) is a sequence of characters that specifies a search
2768        pattern in text. Usually such patterns are used by string-searching algorithms
2769        for "find" or "find and replace" operations on strings, or for input validation.
2770        "#
2771            .unindent()
2772        );
2773        // Let's turn on regex mode.
2774        search_bar
2775            .update_in(cx, |search_bar, window, cx| {
2776                search_bar.search(
2777                    "\\[([^\\]]+)\\]",
2778                    Some(SearchOptions::REGEX),
2779                    true,
2780                    window,
2781                    cx,
2782                )
2783            })
2784            .await
2785            .unwrap();
2786        search_bar.update_in(cx, |search_bar, window, cx| {
2787            search_bar.replacement_editor.update(cx, |editor, cx| {
2788                editor.set_text("${1}number", window, cx);
2789            });
2790            search_bar.replace_all(&ReplaceAll, window, cx)
2791        });
2792        assert_eq!(
2793            editor.read_with(cx, |this, cx| { this.text(cx) }),
2794            r#"
2795        A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2796        rational expr$12number3number) is a sequence of characters that specifies a search
2797        pattern in text. Usually such patterns are used by string-searching algorithms
2798        for "find" or "find and replace" operations on strings, or for input validation.
2799        "#
2800            .unindent()
2801        );
2802        // Now with a whole-word twist.
2803        search_bar
2804            .update_in(cx, |search_bar, window, cx| {
2805                search_bar.search(
2806                    "a\\w+s",
2807                    Some(SearchOptions::REGEX | SearchOptions::WHOLE_WORD),
2808                    true,
2809                    window,
2810                    cx,
2811                )
2812            })
2813            .await
2814            .unwrap();
2815        search_bar.update_in(cx, |search_bar, window, cx| {
2816            search_bar.replacement_editor.update(cx, |editor, cx| {
2817                editor.set_text("things", window, cx);
2818            });
2819            search_bar.replace_all(&ReplaceAll, window, cx)
2820        });
2821        // The only word affected by this edit should be `algorithms`, even though there's a bunch
2822        // of words in this text that would match this regex if not for WHOLE_WORD.
2823        assert_eq!(
2824            editor.read_with(cx, |this, cx| { this.text(cx) }),
2825            r#"
2826        A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2827        rational expr$12number3number) is a sequence of characters that specifies a search
2828        pattern in text. Usually such patterns are used by string-searching things
2829        for "find" or "find and replace" operations on strings, or for input validation.
2830        "#
2831            .unindent()
2832        );
2833    }
2834
2835    #[gpui::test]
2836    async fn test_replace_focus(cx: &mut TestAppContext) {
2837        let (editor, search_bar, cx) = init_test(cx);
2838
2839        editor.update_in(cx, |editor, window, cx| {
2840            editor.set_text("What a bad day!", window, cx)
2841        });
2842
2843        search_bar
2844            .update_in(cx, |search_bar, window, cx| {
2845                search_bar.search("bad", None, true, window, cx)
2846            })
2847            .await
2848            .unwrap();
2849
2850        // Calling `toggle_replace` in the search bar ensures that the "Replace
2851        // *" buttons are rendered, so we can then simulate clicking the
2852        // buttons.
2853        search_bar.update_in(cx, |search_bar, window, cx| {
2854            search_bar.toggle_replace(&ToggleReplace, window, cx)
2855        });
2856
2857        search_bar.update_in(cx, |search_bar, window, cx| {
2858            search_bar.replacement_editor.update(cx, |editor, cx| {
2859                editor.set_text("great", window, cx);
2860            });
2861        });
2862
2863        // Focus on the editor instead of the search bar, as we want to ensure
2864        // that pressing the "Replace Next Match" button will work, even if the
2865        // search bar is not focused.
2866        cx.focus(&editor);
2867
2868        // We'll not simulate clicking the "Replace Next Match " button, asserting that
2869        // the replacement was done.
2870        let button_bounds = cx
2871            .debug_bounds("ICON-ReplaceNext")
2872            .expect("'Replace Next Match' button should be visible");
2873        cx.simulate_click(button_bounds.center(), gpui::Modifiers::none());
2874
2875        assert_eq!(
2876            editor.read_with(cx, |editor, cx| editor.text(cx)),
2877            "What a great day!"
2878        );
2879    }
2880
2881    struct ReplacementTestParams<'a> {
2882        editor: &'a Entity<Editor>,
2883        search_bar: &'a Entity<BufferSearchBar>,
2884        cx: &'a mut VisualTestContext,
2885        search_text: &'static str,
2886        search_options: Option<SearchOptions>,
2887        replacement_text: &'static str,
2888        replace_all: bool,
2889        expected_text: String,
2890    }
2891
2892    async fn run_replacement_test(options: ReplacementTestParams<'_>) {
2893        options
2894            .search_bar
2895            .update_in(options.cx, |search_bar, window, cx| {
2896                if let Some(options) = options.search_options {
2897                    search_bar.set_search_options(options, cx);
2898                }
2899                search_bar.search(
2900                    options.search_text,
2901                    options.search_options,
2902                    true,
2903                    window,
2904                    cx,
2905                )
2906            })
2907            .await
2908            .unwrap();
2909
2910        options
2911            .search_bar
2912            .update_in(options.cx, |search_bar, window, cx| {
2913                search_bar.replacement_editor.update(cx, |editor, cx| {
2914                    editor.set_text(options.replacement_text, window, cx);
2915                });
2916
2917                if options.replace_all {
2918                    search_bar.replace_all(&ReplaceAll, window, cx)
2919                } else {
2920                    search_bar.replace_next(&ReplaceNext, window, cx)
2921                }
2922            });
2923
2924        assert_eq!(
2925            options
2926                .editor
2927                .read_with(options.cx, |this, cx| { this.text(cx) }),
2928            options.expected_text
2929        );
2930    }
2931
2932    #[perf]
2933    #[gpui::test]
2934    async fn test_replace_special_characters(cx: &mut TestAppContext) {
2935        let (editor, search_bar, cx) = init_test(cx);
2936
2937        run_replacement_test(ReplacementTestParams {
2938            editor: &editor,
2939            search_bar: &search_bar,
2940            cx,
2941            search_text: "expression",
2942            search_options: None,
2943            replacement_text: r"\n",
2944            replace_all: true,
2945            expected_text: r#"
2946            A regular \n (shortened as regex or regexp;[1] also referred to as
2947            rational \n[2][3]) is a sequence of characters that specifies a search
2948            pattern in text. Usually such patterns are used by string-searching algorithms
2949            for "find" or "find and replace" operations on strings, or for input validation.
2950            "#
2951            .unindent(),
2952        })
2953        .await;
2954
2955        run_replacement_test(ReplacementTestParams {
2956            editor: &editor,
2957            search_bar: &search_bar,
2958            cx,
2959            search_text: "or",
2960            search_options: Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
2961            replacement_text: r"\\\n\\\\",
2962            replace_all: false,
2963            expected_text: r#"
2964            A regular \n (shortened as regex \
2965            \\ regexp;[1] also referred to as
2966            rational \n[2][3]) is a sequence of characters that specifies a search
2967            pattern in text. Usually such patterns are used by string-searching algorithms
2968            for "find" or "find and replace" operations on strings, or for input validation.
2969            "#
2970            .unindent(),
2971        })
2972        .await;
2973
2974        run_replacement_test(ReplacementTestParams {
2975            editor: &editor,
2976            search_bar: &search_bar,
2977            cx,
2978            search_text: r"(that|used) ",
2979            search_options: Some(SearchOptions::REGEX),
2980            replacement_text: r"$1\n",
2981            replace_all: true,
2982            expected_text: r#"
2983            A regular \n (shortened as regex \
2984            \\ regexp;[1] also referred to as
2985            rational \n[2][3]) is a sequence of characters that
2986            specifies a search
2987            pattern in text. Usually such patterns are used
2988            by string-searching algorithms
2989            for "find" or "find and replace" operations on strings, or for input validation.
2990            "#
2991            .unindent(),
2992        })
2993        .await;
2994    }
2995
2996    #[perf]
2997    #[gpui::test]
2998    async fn test_find_matches_in_selections_singleton_buffer_multiple_selections(
2999        cx: &mut TestAppContext,
3000    ) {
3001        init_globals(cx);
3002        let buffer = cx.new(|cx| {
3003            Buffer::local(
3004                r#"
3005                aaa bbb aaa ccc
3006                aaa bbb aaa ccc
3007                aaa bbb aaa ccc
3008                aaa bbb aaa ccc
3009                aaa bbb aaa ccc
3010                aaa bbb aaa ccc
3011                "#
3012                .unindent(),
3013                cx,
3014            )
3015        });
3016        let cx = cx.add_empty_window();
3017        let editor =
3018            cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
3019
3020        let search_bar = cx.new_window_entity(|window, cx| {
3021            let mut search_bar = BufferSearchBar::new(None, window, cx);
3022            search_bar.set_active_pane_item(Some(&editor), window, cx);
3023            search_bar.show(window, cx);
3024            search_bar
3025        });
3026
3027        editor.update_in(cx, |editor, window, cx| {
3028            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3029                s.select_ranges(vec![Point::new(1, 0)..Point::new(2, 4)])
3030            })
3031        });
3032
3033        search_bar.update_in(cx, |search_bar, window, cx| {
3034            let deploy = Deploy {
3035                focus: true,
3036                replace_enabled: false,
3037                selection_search_enabled: true,
3038            };
3039            search_bar.deploy(&deploy, window, cx);
3040        });
3041
3042        cx.run_until_parked();
3043
3044        search_bar
3045            .update_in(cx, |search_bar, window, cx| {
3046                search_bar.search("aaa", None, true, window, cx)
3047            })
3048            .await
3049            .unwrap();
3050
3051        editor.update(cx, |editor, cx| {
3052            assert_eq!(
3053                editor.search_background_highlights(cx),
3054                &[
3055                    Point::new(1, 0)..Point::new(1, 3),
3056                    Point::new(1, 8)..Point::new(1, 11),
3057                    Point::new(2, 0)..Point::new(2, 3),
3058                ]
3059            );
3060        });
3061    }
3062
3063    #[perf]
3064    #[gpui::test]
3065    async fn test_find_matches_in_selections_multiple_excerpts_buffer_multiple_selections(
3066        cx: &mut TestAppContext,
3067    ) {
3068        init_globals(cx);
3069        let text = r#"
3070            aaa bbb aaa ccc
3071            aaa bbb aaa ccc
3072            aaa bbb aaa ccc
3073            aaa bbb aaa ccc
3074            aaa bbb aaa ccc
3075            aaa bbb aaa ccc
3076
3077            aaa bbb aaa ccc
3078            aaa bbb aaa ccc
3079            aaa bbb aaa ccc
3080            aaa bbb aaa ccc
3081            aaa bbb aaa ccc
3082            aaa bbb aaa ccc
3083            "#
3084        .unindent();
3085
3086        let cx = cx.add_empty_window();
3087        let editor = cx.new_window_entity(|window, cx| {
3088            let multibuffer = MultiBuffer::build_multi(
3089                [
3090                    (
3091                        &text,
3092                        vec![
3093                            Point::new(0, 0)..Point::new(2, 0),
3094                            Point::new(4, 0)..Point::new(5, 0),
3095                        ],
3096                    ),
3097                    (&text, vec![Point::new(9, 0)..Point::new(11, 0)]),
3098                ],
3099                cx,
3100            );
3101            Editor::for_multibuffer(multibuffer, None, window, cx)
3102        });
3103
3104        let search_bar = cx.new_window_entity(|window, cx| {
3105            let mut search_bar = BufferSearchBar::new(None, window, cx);
3106            search_bar.set_active_pane_item(Some(&editor), window, cx);
3107            search_bar.show(window, cx);
3108            search_bar
3109        });
3110
3111        editor.update_in(cx, |editor, window, cx| {
3112            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3113                s.select_ranges(vec![
3114                    Point::new(1, 0)..Point::new(1, 4),
3115                    Point::new(5, 3)..Point::new(6, 4),
3116                ])
3117            })
3118        });
3119
3120        search_bar.update_in(cx, |search_bar, window, cx| {
3121            let deploy = Deploy {
3122                focus: true,
3123                replace_enabled: false,
3124                selection_search_enabled: true,
3125            };
3126            search_bar.deploy(&deploy, window, cx);
3127        });
3128
3129        cx.run_until_parked();
3130
3131        search_bar
3132            .update_in(cx, |search_bar, window, cx| {
3133                search_bar.search("aaa", None, true, window, cx)
3134            })
3135            .await
3136            .unwrap();
3137
3138        editor.update(cx, |editor, cx| {
3139            assert_eq!(
3140                editor.search_background_highlights(cx),
3141                &[
3142                    Point::new(1, 0)..Point::new(1, 3),
3143                    Point::new(5, 8)..Point::new(5, 11),
3144                    Point::new(6, 0)..Point::new(6, 3),
3145                ]
3146            );
3147        });
3148    }
3149
3150    #[perf]
3151    #[gpui::test]
3152    async fn test_hides_and_uses_secondary_when_in_singleton_buffer(cx: &mut TestAppContext) {
3153        let (editor, search_bar, cx) = init_test(cx);
3154
3155        let initial_location = search_bar.update_in(cx, |search_bar, window, cx| {
3156            search_bar.set_active_pane_item(Some(&editor), window, cx)
3157        });
3158
3159        assert_eq!(initial_location, ToolbarItemLocation::Secondary);
3160
3161        let mut events = cx.events(&search_bar);
3162
3163        search_bar.update_in(cx, |search_bar, window, cx| {
3164            search_bar.dismiss(&Dismiss, window, cx);
3165        });
3166
3167        assert_eq!(
3168            events.try_next().unwrap(),
3169            Some(ToolbarItemEvent::ChangeLocation(
3170                ToolbarItemLocation::Hidden
3171            ))
3172        );
3173
3174        search_bar.update_in(cx, |search_bar, window, cx| {
3175            search_bar.show(window, cx);
3176        });
3177
3178        assert_eq!(
3179            events.try_next().unwrap(),
3180            Some(ToolbarItemEvent::ChangeLocation(
3181                ToolbarItemLocation::Secondary
3182            ))
3183        );
3184    }
3185
3186    #[perf]
3187    #[gpui::test]
3188    async fn test_uses_primary_left_when_in_multi_buffer(cx: &mut TestAppContext) {
3189        let (editor, search_bar, cx) = init_multibuffer_test(cx);
3190
3191        let initial_location = search_bar.update_in(cx, |search_bar, window, cx| {
3192            search_bar.set_active_pane_item(Some(&editor), window, cx)
3193        });
3194
3195        assert_eq!(initial_location, ToolbarItemLocation::PrimaryLeft);
3196
3197        let mut events = cx.events(&search_bar);
3198
3199        search_bar.update_in(cx, |search_bar, window, cx| {
3200            search_bar.dismiss(&Dismiss, window, cx);
3201        });
3202
3203        assert_eq!(
3204            events.try_next().unwrap(),
3205            Some(ToolbarItemEvent::ChangeLocation(
3206                ToolbarItemLocation::PrimaryLeft
3207            ))
3208        );
3209
3210        search_bar.update_in(cx, |search_bar, window, cx| {
3211            search_bar.show(window, cx);
3212        });
3213
3214        assert_eq!(
3215            events.try_next().unwrap(),
3216            Some(ToolbarItemEvent::ChangeLocation(
3217                ToolbarItemLocation::PrimaryLeft
3218            ))
3219        );
3220    }
3221
3222    #[perf]
3223    #[gpui::test]
3224    async fn test_hides_and_uses_secondary_when_part_of_project_search(cx: &mut TestAppContext) {
3225        let (editor, search_bar, cx) = init_multibuffer_test(cx);
3226
3227        editor.update(cx, |editor, _| {
3228            editor.set_in_project_search(true);
3229        });
3230
3231        let initial_location = search_bar.update_in(cx, |search_bar, window, cx| {
3232            search_bar.set_active_pane_item(Some(&editor), window, cx)
3233        });
3234
3235        assert_eq!(initial_location, ToolbarItemLocation::Secondary);
3236
3237        let mut events = cx.events(&search_bar);
3238
3239        search_bar.update_in(cx, |search_bar, window, cx| {
3240            search_bar.dismiss(&Dismiss, window, cx);
3241        });
3242
3243        assert_eq!(
3244            events.try_next().unwrap(),
3245            Some(ToolbarItemEvent::ChangeLocation(
3246                ToolbarItemLocation::Hidden
3247            ))
3248        );
3249
3250        search_bar.update_in(cx, |search_bar, window, cx| {
3251            search_bar.show(window, cx);
3252        });
3253
3254        assert_eq!(
3255            events.try_next().unwrap(),
3256            Some(ToolbarItemEvent::ChangeLocation(
3257                ToolbarItemLocation::Secondary
3258            ))
3259        );
3260    }
3261
3262    #[perf]
3263    #[gpui::test]
3264    async fn test_sets_collapsed_when_editor_fold_events_emitted(cx: &mut TestAppContext) {
3265        let (editor, search_bar, cx) = init_multibuffer_test(cx);
3266
3267        search_bar.update_in(cx, |search_bar, window, cx| {
3268            search_bar.set_active_pane_item(Some(&editor), window, cx);
3269        });
3270
3271        editor.update_in(cx, |editor, window, cx| {
3272            editor.fold_all(&FoldAll, window, cx);
3273        });
3274
3275        let is_collapsed = search_bar.read_with(cx, |search_bar, _| search_bar.is_collapsed);
3276
3277        assert!(is_collapsed);
3278
3279        editor.update_in(cx, |editor, window, cx| {
3280            editor.unfold_all(&UnfoldAll, window, cx);
3281        });
3282
3283        let is_collapsed = search_bar.read_with(cx, |search_bar, cx| search_bar.is_collapsed);
3284
3285        assert!(!is_collapsed);
3286    }
3287
3288    #[perf]
3289    #[gpui::test]
3290    async fn test_search_options_changes(cx: &mut TestAppContext) {
3291        let (_editor, search_bar, cx) = init_test(cx);
3292        update_search_settings(
3293            SearchSettings {
3294                button: true,
3295                whole_word: false,
3296                case_sensitive: false,
3297                include_ignored: false,
3298                regex: false,
3299                center_on_match: false,
3300            },
3301            cx,
3302        );
3303
3304        let deploy = Deploy {
3305            focus: true,
3306            replace_enabled: false,
3307            selection_search_enabled: true,
3308        };
3309
3310        search_bar.update_in(cx, |search_bar, window, cx| {
3311            assert_eq!(
3312                search_bar.search_options,
3313                SearchOptions::NONE,
3314                "Should have no search options enabled by default"
3315            );
3316            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
3317            assert_eq!(
3318                search_bar.search_options,
3319                SearchOptions::WHOLE_WORD,
3320                "Should enable the option toggled"
3321            );
3322            assert!(
3323                !search_bar.dismissed,
3324                "Search bar should be present and visible"
3325            );
3326            search_bar.deploy(&deploy, window, cx);
3327            assert_eq!(
3328                search_bar.search_options,
3329                SearchOptions::WHOLE_WORD,
3330                "After (re)deploying, the option should still be enabled"
3331            );
3332
3333            search_bar.dismiss(&Dismiss, window, cx);
3334            search_bar.deploy(&deploy, window, cx);
3335            assert_eq!(
3336                search_bar.search_options,
3337                SearchOptions::WHOLE_WORD,
3338                "After hiding and showing the search bar, search options should be preserved"
3339            );
3340
3341            search_bar.toggle_search_option(SearchOptions::REGEX, window, cx);
3342            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
3343            assert_eq!(
3344                search_bar.search_options,
3345                SearchOptions::REGEX,
3346                "Should enable the options toggled"
3347            );
3348            assert!(
3349                !search_bar.dismissed,
3350                "Search bar should be present and visible"
3351            );
3352            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
3353        });
3354
3355        update_search_settings(
3356            SearchSettings {
3357                button: true,
3358                whole_word: false,
3359                case_sensitive: true,
3360                include_ignored: false,
3361                regex: false,
3362                center_on_match: false,
3363            },
3364            cx,
3365        );
3366        search_bar.update_in(cx, |search_bar, window, cx| {
3367            assert_eq!(
3368                search_bar.search_options,
3369                SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
3370                "Should have no search options enabled by default"
3371            );
3372
3373            search_bar.deploy(&deploy, window, cx);
3374            assert_eq!(
3375                search_bar.search_options,
3376                SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
3377                "Toggling a non-dismissed search bar with custom options should not change the default options"
3378            );
3379            search_bar.dismiss(&Dismiss, window, cx);
3380            search_bar.deploy(&deploy, window, cx);
3381            assert_eq!(
3382                search_bar.configured_options,
3383                SearchOptions::CASE_SENSITIVE,
3384                "After a settings update and toggling the search bar, configured options should be updated"
3385            );
3386            assert_eq!(
3387                search_bar.search_options,
3388                SearchOptions::CASE_SENSITIVE,
3389                "After a settings update and toggling the search bar, configured options should be used"
3390            );
3391        });
3392
3393        update_search_settings(
3394            SearchSettings {
3395                button: true,
3396                whole_word: true,
3397                case_sensitive: true,
3398                include_ignored: false,
3399                regex: false,
3400                center_on_match: false,
3401            },
3402            cx,
3403        );
3404
3405        search_bar.update_in(cx, |search_bar, window, cx| {
3406            search_bar.deploy(&deploy, window, cx);
3407            search_bar.dismiss(&Dismiss, window, cx);
3408            search_bar.show(window, cx);
3409            assert_eq!(
3410                search_bar.search_options,
3411                SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD,
3412                "Calling deploy on an already deployed search bar should not prevent settings updates from being detected"
3413            );
3414        });
3415    }
3416
3417    #[gpui::test]
3418    async fn test_select_occurrence_case_sensitivity(cx: &mut TestAppContext) {
3419        let (editor, search_bar, cx) = init_test(cx);
3420        let mut editor_cx = EditorTestContext::for_editor_in(editor, cx).await;
3421
3422        // Start with case sensitive search settings.
3423        let mut search_settings = SearchSettings::default();
3424        search_settings.case_sensitive = true;
3425        update_search_settings(search_settings, cx);
3426        search_bar.update(cx, |search_bar, cx| {
3427            let mut search_options = search_bar.search_options;
3428            search_options.insert(SearchOptions::CASE_SENSITIVE);
3429            search_bar.set_search_options(search_options, cx);
3430        });
3431
3432        editor_cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
3433        editor_cx.update_editor(|e, window, cx| {
3434            e.select_next(&Default::default(), window, cx).unwrap();
3435        });
3436        editor_cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
3437
3438        // Update the search bar's case sensitivite toggle, so we can later
3439        // confirm that `select_next` will now be case-insensitive.
3440        editor_cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
3441        search_bar.update_in(cx, |search_bar, window, cx| {
3442            search_bar.toggle_case_sensitive(&Default::default(), window, cx);
3443        });
3444        editor_cx.update_editor(|e, window, cx| {
3445            e.select_next(&Default::default(), window, cx).unwrap();
3446        });
3447        editor_cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\nFoo\nfoo");
3448
3449        // Confirm that, after dismissing the search bar, only the editor's
3450        // search settings actually affect the behavior of `select_next`.
3451        search_bar.update_in(cx, |search_bar, window, cx| {
3452            search_bar.dismiss(&Default::default(), window, cx);
3453        });
3454        editor_cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
3455        editor_cx.update_editor(|e, window, cx| {
3456            e.select_next(&Default::default(), window, cx).unwrap();
3457        });
3458        editor_cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
3459
3460        // Update the editor's search settings, disabling case sensitivity, to
3461        // check that the value is respected.
3462        let mut search_settings = SearchSettings::default();
3463        search_settings.case_sensitive = false;
3464        update_search_settings(search_settings, cx);
3465        editor_cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
3466        editor_cx.update_editor(|e, window, cx| {
3467            e.select_next(&Default::default(), window, cx).unwrap();
3468        });
3469        editor_cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\nFoo\nfoo");
3470    }
3471
3472    fn update_search_settings(search_settings: SearchSettings, cx: &mut TestAppContext) {
3473        cx.update(|cx| {
3474            SettingsStore::update_global(cx, |store, cx| {
3475                store.update_user_settings(cx, |settings| {
3476                    settings.editor.search = Some(SearchSettingsContent {
3477                        button: Some(search_settings.button),
3478                        whole_word: Some(search_settings.whole_word),
3479                        case_sensitive: Some(search_settings.case_sensitive),
3480                        include_ignored: Some(search_settings.include_ignored),
3481                        regex: Some(search_settings.regex),
3482                        center_on_match: Some(search_settings.center_on_match),
3483                    });
3484                });
3485            });
3486        });
3487    }
3488}