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