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            // if !self.dismissed {
 621            //     if is_project_search {
 622            //         self.dismiss(&Default::default(), window, cx);
 623            //     } else {
 624            //         if self.needs_expand_collapse_option(cx) {
 625            //             return ToolbarItemLocation::PrimaryLeft;
 626            //         } else {
 627            //             return ToolbarItemLocation::Secondary;
 628            //         }
 629            //     }
 630            // }
 631        }
 632        ToolbarItemLocation::Hidden
 633    }
 634}
 635
 636impl BufferSearchBar {
 637    pub fn query_editor_focused(&self) -> bool {
 638        self.query_editor_focused
 639    }
 640
 641    pub fn register(registrar: &mut impl SearchActionsRegistrar) {
 642        registrar.register_handler(ForDeployed(|this, _: &FocusSearch, window, cx| {
 643            this.query_editor.focus_handle(cx).focus(window, cx);
 644            this.select_query(window, cx);
 645        }));
 646        registrar.register_handler(ForDeployed(
 647            |this, action: &ToggleCaseSensitive, window, cx| {
 648                if this.supported_options(cx).case {
 649                    this.toggle_case_sensitive(action, window, cx);
 650                }
 651            },
 652        ));
 653        registrar.register_handler(ForDeployed(|this, action: &ToggleWholeWord, window, cx| {
 654            if this.supported_options(cx).word {
 655                this.toggle_whole_word(action, window, cx);
 656            }
 657        }));
 658        registrar.register_handler(ForDeployed(|this, action: &ToggleRegex, window, cx| {
 659            if this.supported_options(cx).regex {
 660                this.toggle_regex(action, window, cx);
 661            }
 662        }));
 663        registrar.register_handler(ForDeployed(|this, action: &ToggleSelection, window, cx| {
 664            if this.supported_options(cx).selection {
 665                this.toggle_selection(action, window, cx);
 666            } else {
 667                cx.propagate();
 668            }
 669        }));
 670        registrar.register_handler(ForDeployed(|this, action: &ToggleReplace, window, cx| {
 671            if this.supported_options(cx).replacement {
 672                this.toggle_replace(action, window, cx);
 673            } else {
 674                cx.propagate();
 675            }
 676        }));
 677        registrar.register_handler(WithResults(|this, action: &SelectNextMatch, window, cx| {
 678            if this.supported_options(cx).find_in_results {
 679                cx.propagate();
 680            } else {
 681                this.select_next_match(action, window, cx);
 682            }
 683        }));
 684        registrar.register_handler(WithResults(
 685            |this, action: &SelectPreviousMatch, window, cx| {
 686                if this.supported_options(cx).find_in_results {
 687                    cx.propagate();
 688                } else {
 689                    this.select_prev_match(action, window, cx);
 690                }
 691            },
 692        ));
 693        registrar.register_handler(WithResults(
 694            |this, action: &SelectAllMatches, window, cx| {
 695                if this.supported_options(cx).find_in_results {
 696                    cx.propagate();
 697                } else {
 698                    this.select_all_matches(action, window, cx);
 699                }
 700            },
 701        ));
 702        registrar.register_handler(ForDeployed(
 703            |this, _: &editor::actions::Cancel, window, cx| {
 704                this.dismiss(&Dismiss, window, cx);
 705            },
 706        ));
 707        registrar.register_handler(ForDeployed(|this, _: &Dismiss, window, cx| {
 708            this.dismiss(&Dismiss, window, cx);
 709        }));
 710
 711        // register deploy buffer search for both search bar states, since we want to focus into the search bar
 712        // when the deploy action is triggered in the buffer.
 713        registrar.register_handler(ForDeployed(|this, deploy, window, cx| {
 714            this.deploy(deploy, window, cx);
 715        }));
 716        registrar.register_handler(ForDismissed(|this, deploy, window, cx| {
 717            this.deploy(deploy, window, cx);
 718        }));
 719        registrar.register_handler(ForDeployed(|this, _: &DeployReplace, window, cx| {
 720            if this.supported_options(cx).find_in_results {
 721                cx.propagate();
 722            } else {
 723                this.deploy(&Deploy::replace(), window, cx);
 724            }
 725        }));
 726        registrar.register_handler(ForDismissed(|this, _: &DeployReplace, window, cx| {
 727            if this.supported_options(cx).find_in_results {
 728                cx.propagate();
 729            } else {
 730                this.deploy(&Deploy::replace(), window, cx);
 731            }
 732        }));
 733    }
 734
 735    pub fn new(
 736        languages: Option<Arc<LanguageRegistry>>,
 737        window: &mut Window,
 738        cx: &mut Context<Self>,
 739    ) -> Self {
 740        let query_editor = cx.new(|cx| {
 741            let mut editor = Editor::single_line(window, cx);
 742            editor.set_use_autoclose(false);
 743            editor
 744        });
 745        cx.subscribe_in(&query_editor, window, Self::on_query_editor_event)
 746            .detach();
 747        let replacement_editor = cx.new(|cx| Editor::single_line(window, cx));
 748        cx.subscribe(&replacement_editor, Self::on_replacement_editor_event)
 749            .detach();
 750
 751        let search_options = SearchOptions::from_settings(&EditorSettings::get_global(cx).search);
 752        if let Some(languages) = languages {
 753            let query_buffer = query_editor
 754                .read(cx)
 755                .buffer()
 756                .read(cx)
 757                .as_singleton()
 758                .expect("query editor should be backed by a singleton buffer");
 759
 760            query_buffer
 761                .read(cx)
 762                .set_language_registry(languages.clone());
 763
 764            cx.spawn(async move |buffer_search_bar, cx| {
 765                use anyhow::Context as _;
 766
 767                let regex_language = languages
 768                    .language_for_name("regex")
 769                    .await
 770                    .context("loading regex language")?;
 771
 772                buffer_search_bar
 773                    .update(cx, |buffer_search_bar, cx| {
 774                        buffer_search_bar.regex_language = Some(regex_language);
 775                        buffer_search_bar.adjust_query_regex_language(cx);
 776                    })
 777                    .ok();
 778                anyhow::Ok(())
 779            })
 780            .detach_and_log_err(cx);
 781        }
 782
 783        Self {
 784            query_editor,
 785            query_editor_focused: false,
 786            replacement_editor,
 787            replacement_editor_focused: false,
 788            active_searchable_item: None,
 789            active_searchable_item_subscriptions: None,
 790            active_match_index: None,
 791            searchable_items_with_matches: Default::default(),
 792            default_options: search_options,
 793            configured_options: search_options,
 794            search_options,
 795            pending_search: None,
 796            query_error: None,
 797            dismissed: true,
 798            search_history: SearchHistory::new(
 799                Some(MAX_BUFFER_SEARCH_HISTORY_SIZE),
 800                project::search_history::QueryInsertionBehavior::ReplacePreviousIfContains,
 801            ),
 802            search_history_cursor: Default::default(),
 803            active_search: None,
 804            replace_enabled: false,
 805            selection_search_enabled: None,
 806            scroll_handle: ScrollHandle::new(),
 807            editor_scroll_handle: ScrollHandle::new(),
 808            editor_needed_width: px(0.),
 809            regex_language: None,
 810            is_collapsed: false,
 811        }
 812    }
 813
 814    pub fn is_dismissed(&self) -> bool {
 815        self.dismissed
 816    }
 817
 818    pub fn dismiss(&mut self, _: &Dismiss, window: &mut Window, cx: &mut Context<Self>) {
 819        self.dismissed = true;
 820        self.query_error = None;
 821        self.sync_select_next_case_sensitivity(cx);
 822
 823        for searchable_item in self.searchable_items_with_matches.keys() {
 824            if let Some(searchable_item) =
 825                WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
 826            {
 827                searchable_item.clear_matches(window, cx);
 828            }
 829        }
 830
 831        let needs_collapse_expand = self.needs_expand_collapse_option(cx);
 832        let mut is_in_project_search = false;
 833
 834        if let Some(active_editor) = self.active_searchable_item.as_mut() {
 835            self.selection_search_enabled = None;
 836            self.replace_enabled = false;
 837            active_editor.search_bar_visibility_changed(false, window, cx);
 838            active_editor.toggle_filtered_search_ranges(None, window, cx);
 839            is_in_project_search = active_editor.supported_options(cx).find_in_results;
 840            let handle = active_editor.item_focus_handle(cx);
 841            self.focus(&handle, window, cx);
 842        }
 843
 844        if needs_collapse_expand && !is_in_project_search {
 845            cx.emit(Event::UpdateLocation);
 846            cx.emit(ToolbarItemEvent::ChangeLocation(
 847                ToolbarItemLocation::PrimaryLeft,
 848            ));
 849            cx.notify();
 850            return;
 851        }
 852        cx.emit(Event::UpdateLocation);
 853        cx.emit(ToolbarItemEvent::ChangeLocation(
 854            ToolbarItemLocation::Hidden,
 855        ));
 856        cx.notify();
 857    }
 858
 859    pub fn deploy(&mut self, deploy: &Deploy, window: &mut Window, cx: &mut Context<Self>) -> bool {
 860        let filtered_search_range = if deploy.selection_search_enabled {
 861            Some(FilteredSearchRange::Default)
 862        } else {
 863            None
 864        };
 865        if self.show(window, cx) {
 866            if let Some(active_item) = self.active_searchable_item.as_mut() {
 867                active_item.toggle_filtered_search_ranges(filtered_search_range, window, cx);
 868            }
 869            self.search_suggested(window, cx);
 870            self.smartcase(window, cx);
 871            self.sync_select_next_case_sensitivity(cx);
 872            self.replace_enabled |= deploy.replace_enabled;
 873            self.selection_search_enabled =
 874                self.selection_search_enabled
 875                    .or(if deploy.selection_search_enabled {
 876                        Some(FilteredSearchRange::Default)
 877                    } else {
 878                        None
 879                    });
 880            if deploy.focus {
 881                let mut handle = self.query_editor.focus_handle(cx);
 882                let mut select_query = true;
 883                if deploy.replace_enabled && handle.is_focused(window) {
 884                    handle = self.replacement_editor.focus_handle(cx);
 885                    select_query = false;
 886                };
 887
 888                if select_query {
 889                    self.select_query(window, cx);
 890                }
 891
 892                window.focus(&handle, cx);
 893            }
 894            return true;
 895        }
 896
 897        cx.propagate();
 898        false
 899    }
 900
 901    pub fn toggle(&mut self, action: &Deploy, window: &mut Window, cx: &mut Context<Self>) {
 902        if self.is_dismissed() {
 903            self.deploy(action, window, cx);
 904        } else {
 905            self.dismiss(&Dismiss, window, cx);
 906        }
 907    }
 908
 909    pub fn show(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
 910        let Some(handle) = self.active_searchable_item.as_ref() else {
 911            return false;
 912        };
 913
 914        let configured_options =
 915            SearchOptions::from_settings(&EditorSettings::get_global(cx).search);
 916        let settings_changed = configured_options != self.configured_options;
 917
 918        if self.dismissed && settings_changed {
 919            // Only update configuration options when search bar is dismissed,
 920            // so we don't miss updates even after calling show twice
 921            self.configured_options = configured_options;
 922            self.search_options = configured_options;
 923            self.default_options = configured_options;
 924        }
 925
 926        self.dismissed = false;
 927        self.adjust_query_regex_language(cx);
 928        handle.search_bar_visibility_changed(true, window, cx);
 929        cx.notify();
 930        cx.emit(Event::UpdateLocation);
 931        cx.emit(ToolbarItemEvent::ChangeLocation(
 932            if self.needs_expand_collapse_option(cx) {
 933                ToolbarItemLocation::PrimaryLeft
 934            } else {
 935                ToolbarItemLocation::Secondary
 936            },
 937        ));
 938        true
 939    }
 940
 941    fn supported_options(&self, cx: &mut Context<Self>) -> workspace::searchable::SearchOptions {
 942        self.active_searchable_item
 943            .as_ref()
 944            .map(|item| item.supported_options(cx))
 945            .unwrap_or_default()
 946    }
 947
 948    // TODO we should clean this up
 949    // We only provide an expand/collapse button if we are in a multibuffer and
 950    // not doing a project search. In a project search, the button is already rendered.
 951    // In a singleton buffer, this option doesn't make sense.
 952    fn needs_expand_collapse_option(&self, cx: &App) -> bool {
 953        if let Some(item) = &self.active_searchable_item {
 954            let buffer_kind = item.buffer_kind(cx);
 955
 956            if buffer_kind == ItemBufferKind::Multibuffer {
 957                let workspace::searchable::SearchOptions {
 958                    find_in_results, ..
 959                } = item.supported_options(cx);
 960                !find_in_results
 961            } else {
 962                false
 963            }
 964        } else {
 965            false
 966        }
 967    }
 968
 969    fn toggle_fold_all_in_item(&self, window: &mut Window, cx: &mut Context<Self>) {
 970        let is_collapsed = self.is_collapsed;
 971        if let Some(item) = &self.active_searchable_item {
 972            if let Some(item) = item.act_as_type(TypeId::of::<Editor>(), cx) {
 973                let editor = item.downcast::<Editor>().expect("Is an editor");
 974                editor.update(cx, |editor, cx| {
 975                    if is_collapsed {
 976                        editor.unfold_all(&UnfoldAll, window, cx);
 977                    } else {
 978                        editor.fold_all(&FoldAll, window, cx);
 979                    }
 980                })
 981            }
 982        }
 983    }
 984
 985    pub fn search_suggested(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 986        let search = self.query_suggestion(window, cx).map(|suggestion| {
 987            self.search(&suggestion, Some(self.default_options), true, window, cx)
 988        });
 989
 990        if let Some(search) = search {
 991            cx.spawn_in(window, async move |this, cx| {
 992                if search.await.is_ok() {
 993                    this.update_in(cx, |this, window, cx| {
 994                        this.activate_current_match(window, cx)
 995                    })
 996                } else {
 997                    Ok(())
 998                }
 999            })
1000            .detach_and_log_err(cx);
1001        }
1002    }
1003
1004    pub fn activate_current_match(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1005        if let Some(match_ix) = self.active_match_index
1006            && let Some(active_searchable_item) = self.active_searchable_item.as_ref()
1007            && let Some(matches) = self
1008                .searchable_items_with_matches
1009                .get(&active_searchable_item.downgrade())
1010        {
1011            active_searchable_item.activate_match(match_ix, matches, window, cx)
1012        }
1013    }
1014
1015    pub fn select_query(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1016        self.query_editor.update(cx, |query_editor, cx| {
1017            query_editor.select_all(&Default::default(), window, cx);
1018        });
1019    }
1020
1021    pub fn query(&self, cx: &App) -> String {
1022        self.query_editor.read(cx).text(cx)
1023    }
1024
1025    pub fn replacement(&self, cx: &mut App) -> String {
1026        self.replacement_editor.read(cx).text(cx)
1027    }
1028
1029    pub fn query_suggestion(
1030        &mut self,
1031        window: &mut Window,
1032        cx: &mut Context<Self>,
1033    ) -> Option<String> {
1034        self.active_searchable_item
1035            .as_ref()
1036            .map(|searchable_item| searchable_item.query_suggestion(window, cx))
1037            .filter(|suggestion| !suggestion.is_empty())
1038    }
1039
1040    pub fn set_replacement(&mut self, replacement: Option<&str>, cx: &mut Context<Self>) {
1041        if replacement.is_none() {
1042            self.replace_enabled = false;
1043            return;
1044        }
1045        self.replace_enabled = true;
1046        self.replacement_editor
1047            .update(cx, |replacement_editor, cx| {
1048                replacement_editor
1049                    .buffer()
1050                    .update(cx, |replacement_buffer, cx| {
1051                        let len = replacement_buffer.len(cx);
1052                        replacement_buffer.edit(
1053                            [(MultiBufferOffset(0)..len, replacement.unwrap())],
1054                            None,
1055                            cx,
1056                        );
1057                    });
1058            });
1059    }
1060
1061    pub fn focus_replace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1062        self.focus(&self.replacement_editor.focus_handle(cx), window, cx);
1063        cx.notify();
1064    }
1065
1066    pub fn search(
1067        &mut self,
1068        query: &str,
1069        options: Option<SearchOptions>,
1070        add_to_history: bool,
1071        window: &mut Window,
1072        cx: &mut Context<Self>,
1073    ) -> oneshot::Receiver<()> {
1074        let options = options.unwrap_or(self.default_options);
1075        let updated = query != self.query(cx) || self.search_options != options;
1076        if updated {
1077            self.query_editor.update(cx, |query_editor, cx| {
1078                query_editor.buffer().update(cx, |query_buffer, cx| {
1079                    let len = query_buffer.len(cx);
1080                    query_buffer.edit([(MultiBufferOffset(0)..len, query)], None, cx);
1081                });
1082            });
1083            self.set_search_options(options, cx);
1084            self.clear_matches(window, cx);
1085            #[cfg(target_os = "macos")]
1086            self.update_find_pasteboard(cx);
1087            cx.notify();
1088        }
1089        self.update_matches(!updated, add_to_history, window, cx)
1090    }
1091
1092    #[cfg(target_os = "macos")]
1093    pub fn update_find_pasteboard(&mut self, cx: &mut App) {
1094        cx.write_to_find_pasteboard(gpui::ClipboardItem::new_string_with_metadata(
1095            self.query(cx),
1096            self.search_options.bits().to_string(),
1097        ));
1098    }
1099
1100    pub fn focus_editor(&mut self, _: &FocusEditor, window: &mut Window, cx: &mut Context<Self>) {
1101        if let Some(active_editor) = self.active_searchable_item.as_ref() {
1102            let handle = active_editor.item_focus_handle(cx);
1103            window.focus(&handle, cx);
1104        }
1105    }
1106
1107    pub fn toggle_search_option(
1108        &mut self,
1109        search_option: SearchOptions,
1110        window: &mut Window,
1111        cx: &mut Context<Self>,
1112    ) {
1113        self.search_options.toggle(search_option);
1114        self.default_options = self.search_options;
1115        drop(self.update_matches(false, false, window, cx));
1116        self.adjust_query_regex_language(cx);
1117        self.sync_select_next_case_sensitivity(cx);
1118        cx.notify();
1119    }
1120
1121    pub fn has_search_option(&mut self, search_option: SearchOptions) -> bool {
1122        self.search_options.contains(search_option)
1123    }
1124
1125    pub fn enable_search_option(
1126        &mut self,
1127        search_option: SearchOptions,
1128        window: &mut Window,
1129        cx: &mut Context<Self>,
1130    ) {
1131        if !self.search_options.contains(search_option) {
1132            self.toggle_search_option(search_option, window, cx)
1133        }
1134    }
1135
1136    pub fn set_search_within_selection(
1137        &mut self,
1138        search_within_selection: Option<FilteredSearchRange>,
1139        window: &mut Window,
1140        cx: &mut Context<Self>,
1141    ) -> Option<oneshot::Receiver<()>> {
1142        let active_item = self.active_searchable_item.as_mut()?;
1143        self.selection_search_enabled = search_within_selection;
1144        active_item.toggle_filtered_search_ranges(self.selection_search_enabled, window, cx);
1145        cx.notify();
1146        Some(self.update_matches(false, false, window, cx))
1147    }
1148
1149    pub fn set_search_options(&mut self, search_options: SearchOptions, cx: &mut Context<Self>) {
1150        self.search_options = search_options;
1151        self.adjust_query_regex_language(cx);
1152        self.sync_select_next_case_sensitivity(cx);
1153        cx.notify();
1154    }
1155
1156    pub fn clear_search_within_ranges(
1157        &mut self,
1158        search_options: SearchOptions,
1159        cx: &mut Context<Self>,
1160    ) {
1161        self.search_options = search_options;
1162        self.adjust_query_regex_language(cx);
1163        cx.notify();
1164    }
1165
1166    fn select_next_match(
1167        &mut self,
1168        _: &SelectNextMatch,
1169        window: &mut Window,
1170        cx: &mut Context<Self>,
1171    ) {
1172        self.select_match(Direction::Next, 1, window, cx);
1173    }
1174
1175    fn select_prev_match(
1176        &mut self,
1177        _: &SelectPreviousMatch,
1178        window: &mut Window,
1179        cx: &mut Context<Self>,
1180    ) {
1181        self.select_match(Direction::Prev, 1, window, cx);
1182    }
1183
1184    pub fn select_all_matches(
1185        &mut self,
1186        _: &SelectAllMatches,
1187        window: &mut Window,
1188        cx: &mut Context<Self>,
1189    ) {
1190        if !self.dismissed
1191            && self.active_match_index.is_some()
1192            && let Some(searchable_item) = self.active_searchable_item.as_ref()
1193            && let Some(matches) = self
1194                .searchable_items_with_matches
1195                .get(&searchable_item.downgrade())
1196        {
1197            searchable_item.select_matches(matches, window, cx);
1198            self.focus_editor(&FocusEditor, window, cx);
1199        }
1200    }
1201
1202    pub fn select_match(
1203        &mut self,
1204        direction: Direction,
1205        count: usize,
1206        window: &mut Window,
1207        cx: &mut Context<Self>,
1208    ) {
1209        if let Some(index) = self.active_match_index
1210            && let Some(searchable_item) = self.active_searchable_item.as_ref()
1211            && let Some(matches) = self
1212                .searchable_items_with_matches
1213                .get(&searchable_item.downgrade())
1214                .filter(|matches| !matches.is_empty())
1215        {
1216            // If 'wrapscan' is disabled, searches do not wrap around the end of the file.
1217            if !EditorSettings::get_global(cx).search_wrap
1218                && ((direction == Direction::Next && index + count >= matches.len())
1219                    || (direction == Direction::Prev && index < count))
1220            {
1221                crate::show_no_more_matches(window, cx);
1222                return;
1223            }
1224            let new_match_index = searchable_item
1225                .match_index_for_direction(matches, index, direction, count, window, cx);
1226
1227            searchable_item.update_matches(matches, Some(new_match_index), window, cx);
1228            searchable_item.activate_match(new_match_index, matches, window, cx);
1229        }
1230    }
1231
1232    pub fn select_first_match(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1233        if let Some(searchable_item) = self.active_searchable_item.as_ref()
1234            && let Some(matches) = self
1235                .searchable_items_with_matches
1236                .get(&searchable_item.downgrade())
1237        {
1238            if matches.is_empty() {
1239                return;
1240            }
1241            searchable_item.update_matches(matches, Some(0), window, cx);
1242            searchable_item.activate_match(0, matches, window, cx);
1243        }
1244    }
1245
1246    pub fn select_last_match(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1247        if let Some(searchable_item) = self.active_searchable_item.as_ref()
1248            && let Some(matches) = self
1249                .searchable_items_with_matches
1250                .get(&searchable_item.downgrade())
1251        {
1252            if matches.is_empty() {
1253                return;
1254            }
1255            let new_match_index = matches.len() - 1;
1256            searchable_item.update_matches(matches, Some(new_match_index), window, cx);
1257            searchable_item.activate_match(new_match_index, matches, window, cx);
1258        }
1259    }
1260
1261    fn on_query_editor_event(
1262        &mut self,
1263        editor: &Entity<Editor>,
1264        event: &editor::EditorEvent,
1265        window: &mut Window,
1266        cx: &mut Context<Self>,
1267    ) {
1268        match event {
1269            editor::EditorEvent::Focused => self.query_editor_focused = true,
1270            editor::EditorEvent::Blurred => self.query_editor_focused = false,
1271            editor::EditorEvent::Edited { .. } => {
1272                self.smartcase(window, cx);
1273                self.clear_matches(window, cx);
1274                let search = self.update_matches(false, true, window, cx);
1275
1276                let width = editor.update(cx, |editor, cx| {
1277                    let text_layout_details = editor.text_layout_details(window);
1278                    let snapshot = editor.snapshot(window, cx).display_snapshot;
1279
1280                    snapshot.x_for_display_point(snapshot.max_point(), &text_layout_details)
1281                        - snapshot.x_for_display_point(DisplayPoint::zero(), &text_layout_details)
1282                });
1283                self.editor_needed_width = width;
1284                cx.notify();
1285
1286                cx.spawn_in(window, async move |this, cx| {
1287                    if search.await.is_ok() {
1288                        this.update_in(cx, |this, window, cx| {
1289                            this.activate_current_match(window, cx);
1290                            #[cfg(target_os = "macos")]
1291                            this.update_find_pasteboard(cx);
1292                        })?;
1293                    }
1294                    anyhow::Ok(())
1295                })
1296                .detach_and_log_err(cx);
1297            }
1298            _ => {}
1299        }
1300    }
1301
1302    fn on_replacement_editor_event(
1303        &mut self,
1304        _: Entity<Editor>,
1305        event: &editor::EditorEvent,
1306        _: &mut Context<Self>,
1307    ) {
1308        match event {
1309            editor::EditorEvent::Focused => self.replacement_editor_focused = true,
1310            editor::EditorEvent::Blurred => self.replacement_editor_focused = false,
1311            _ => {}
1312        }
1313    }
1314
1315    fn on_active_searchable_item_event(
1316        &mut self,
1317        event: &SearchEvent,
1318        window: &mut Window,
1319        cx: &mut Context<Self>,
1320    ) {
1321        match event {
1322            SearchEvent::MatchesInvalidated => {
1323                drop(self.update_matches(false, false, window, cx));
1324            }
1325            SearchEvent::ActiveMatchChanged => self.update_match_index(window, cx),
1326            SearchEvent::ResultsCollapsedChanged(collapse_direction) => {
1327                if self.needs_expand_collapse_option(cx) {
1328                    match collapse_direction {
1329                        CollapseDirection::Collapsed => self.is_collapsed = true,
1330                        CollapseDirection::Expanded => self.is_collapsed = false,
1331                    }
1332                }
1333                cx.notify();
1334            }
1335        }
1336    }
1337
1338    fn toggle_case_sensitive(
1339        &mut self,
1340        _: &ToggleCaseSensitive,
1341        window: &mut Window,
1342        cx: &mut Context<Self>,
1343    ) {
1344        self.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx)
1345    }
1346
1347    fn toggle_whole_word(
1348        &mut self,
1349        _: &ToggleWholeWord,
1350        window: &mut Window,
1351        cx: &mut Context<Self>,
1352    ) {
1353        self.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx)
1354    }
1355
1356    fn toggle_selection(
1357        &mut self,
1358        _: &ToggleSelection,
1359        window: &mut Window,
1360        cx: &mut Context<Self>,
1361    ) {
1362        self.set_search_within_selection(
1363            if let Some(_) = self.selection_search_enabled {
1364                None
1365            } else {
1366                Some(FilteredSearchRange::Default)
1367            },
1368            window,
1369            cx,
1370        );
1371    }
1372
1373    fn toggle_regex(&mut self, _: &ToggleRegex, window: &mut Window, cx: &mut Context<Self>) {
1374        self.toggle_search_option(SearchOptions::REGEX, window, cx)
1375    }
1376
1377    fn clear_active_searchable_item_matches(&mut self, window: &mut Window, cx: &mut App) {
1378        if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
1379            self.active_match_index = None;
1380            self.searchable_items_with_matches
1381                .remove(&active_searchable_item.downgrade());
1382            active_searchable_item.clear_matches(window, cx);
1383        }
1384    }
1385
1386    pub fn has_active_match(&self) -> bool {
1387        self.active_match_index.is_some()
1388    }
1389
1390    fn clear_matches(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1391        let mut active_item_matches = None;
1392        for (searchable_item, matches) in self.searchable_items_with_matches.drain() {
1393            if let Some(searchable_item) =
1394                WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
1395            {
1396                if Some(&searchable_item) == self.active_searchable_item.as_ref() {
1397                    active_item_matches = Some((searchable_item.downgrade(), matches));
1398                } else {
1399                    searchable_item.clear_matches(window, cx);
1400                }
1401            }
1402        }
1403
1404        self.searchable_items_with_matches
1405            .extend(active_item_matches);
1406    }
1407
1408    fn update_matches(
1409        &mut self,
1410        reuse_existing_query: bool,
1411        add_to_history: bool,
1412        window: &mut Window,
1413        cx: &mut Context<Self>,
1414    ) -> oneshot::Receiver<()> {
1415        let (done_tx, done_rx) = oneshot::channel();
1416        let query = self.query(cx);
1417        self.pending_search.take();
1418
1419        if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
1420            self.query_error = None;
1421            if query.is_empty() {
1422                self.clear_active_searchable_item_matches(window, cx);
1423                let _ = done_tx.send(());
1424                cx.notify();
1425            } else {
1426                let query: Arc<_> = if let Some(search) =
1427                    self.active_search.take().filter(|_| reuse_existing_query)
1428                {
1429                    search
1430                } else {
1431                    // Value doesn't matter, we only construct empty matchers with it
1432
1433                    if self.search_options.contains(SearchOptions::REGEX) {
1434                        match SearchQuery::regex(
1435                            query,
1436                            self.search_options.contains(SearchOptions::WHOLE_WORD),
1437                            self.search_options.contains(SearchOptions::CASE_SENSITIVE),
1438                            false,
1439                            self.search_options
1440                                .contains(SearchOptions::ONE_MATCH_PER_LINE),
1441                            PathMatcher::default(),
1442                            PathMatcher::default(),
1443                            false,
1444                            None,
1445                        ) {
1446                            Ok(query) => query.with_replacement(self.replacement(cx)),
1447                            Err(e) => {
1448                                self.query_error = Some(e.to_string());
1449                                self.clear_active_searchable_item_matches(window, cx);
1450                                cx.notify();
1451                                return done_rx;
1452                            }
1453                        }
1454                    } else {
1455                        match SearchQuery::text(
1456                            query,
1457                            self.search_options.contains(SearchOptions::WHOLE_WORD),
1458                            self.search_options.contains(SearchOptions::CASE_SENSITIVE),
1459                            false,
1460                            PathMatcher::default(),
1461                            PathMatcher::default(),
1462                            false,
1463                            None,
1464                        ) {
1465                            Ok(query) => query.with_replacement(self.replacement(cx)),
1466                            Err(e) => {
1467                                self.query_error = Some(e.to_string());
1468                                self.clear_active_searchable_item_matches(window, cx);
1469                                cx.notify();
1470                                return done_rx;
1471                            }
1472                        }
1473                    }
1474                    .into()
1475                };
1476
1477                self.active_search = Some(query.clone());
1478                let query_text = query.as_str().to_string();
1479
1480                let matches = active_searchable_item.find_matches(query, window, cx);
1481
1482                let active_searchable_item = active_searchable_item.downgrade();
1483                self.pending_search = Some(cx.spawn_in(window, async move |this, cx| {
1484                    let matches = matches.await;
1485
1486                    this.update_in(cx, |this, window, cx| {
1487                        if let Some(active_searchable_item) =
1488                            WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx)
1489                        {
1490                            this.searchable_items_with_matches
1491                                .insert(active_searchable_item.downgrade(), matches);
1492
1493                            this.update_match_index(window, cx);
1494
1495                            if add_to_history {
1496                                this.search_history
1497                                    .add(&mut this.search_history_cursor, query_text);
1498                            }
1499                            if !this.dismissed {
1500                                let matches = this
1501                                    .searchable_items_with_matches
1502                                    .get(&active_searchable_item.downgrade())
1503                                    .unwrap();
1504                                if matches.is_empty() {
1505                                    active_searchable_item.clear_matches(window, cx);
1506                                } else {
1507                                    active_searchable_item.update_matches(
1508                                        matches,
1509                                        this.active_match_index,
1510                                        window,
1511                                        cx,
1512                                    );
1513                                }
1514                                let _ = done_tx.send(());
1515                            }
1516                            cx.notify();
1517                        }
1518                    })
1519                    .log_err();
1520                }));
1521            }
1522        }
1523        done_rx
1524    }
1525
1526    fn reverse_direction_if_backwards(&self, direction: Direction) -> Direction {
1527        if self.search_options.contains(SearchOptions::BACKWARDS) {
1528            direction.opposite()
1529        } else {
1530            direction
1531        }
1532    }
1533
1534    pub fn update_match_index(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1535        let direction = self.reverse_direction_if_backwards(Direction::Next);
1536        let new_index = self
1537            .active_searchable_item
1538            .as_ref()
1539            .and_then(|searchable_item| {
1540                let matches = self
1541                    .searchable_items_with_matches
1542                    .get(&searchable_item.downgrade())?;
1543                searchable_item.active_match_index(direction, matches, window, cx)
1544            });
1545        if new_index != self.active_match_index {
1546            self.active_match_index = new_index;
1547            if !self.dismissed {
1548                if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1549                    if let Some(matches) = self
1550                        .searchable_items_with_matches
1551                        .get(&searchable_item.downgrade())
1552                    {
1553                        if !matches.is_empty() {
1554                            searchable_item.update_matches(matches, new_index, window, cx);
1555                        }
1556                    }
1557                }
1558            }
1559            cx.notify();
1560        }
1561    }
1562
1563    fn tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context<Self>) {
1564        self.cycle_field(Direction::Next, window, cx);
1565    }
1566
1567    fn backtab(&mut self, _: &Backtab, window: &mut Window, cx: &mut Context<Self>) {
1568        self.cycle_field(Direction::Prev, window, cx);
1569    }
1570    fn cycle_field(&mut self, direction: Direction, window: &mut Window, cx: &mut Context<Self>) {
1571        let mut handles = vec![self.query_editor.focus_handle(cx)];
1572        if self.replace_enabled {
1573            handles.push(self.replacement_editor.focus_handle(cx));
1574        }
1575        if let Some(item) = self.active_searchable_item.as_ref() {
1576            handles.push(item.item_focus_handle(cx));
1577        }
1578        let current_index = match handles.iter().position(|focus| focus.is_focused(window)) {
1579            Some(index) => index,
1580            None => return,
1581        };
1582
1583        let new_index = match direction {
1584            Direction::Next => (current_index + 1) % handles.len(),
1585            Direction::Prev if current_index == 0 => handles.len() - 1,
1586            Direction::Prev => (current_index - 1) % handles.len(),
1587        };
1588        let next_focus_handle = &handles[new_index];
1589        self.focus(next_focus_handle, window, cx);
1590        cx.stop_propagation();
1591    }
1592
1593    fn next_history_query(
1594        &mut self,
1595        _: &NextHistoryQuery,
1596        window: &mut Window,
1597        cx: &mut Context<Self>,
1598    ) {
1599        if let Some(new_query) = self
1600            .search_history
1601            .next(&mut self.search_history_cursor)
1602            .map(str::to_string)
1603        {
1604            drop(self.search(&new_query, Some(self.search_options), false, window, cx));
1605        } else {
1606            self.search_history_cursor.reset();
1607            drop(self.search("", Some(self.search_options), false, window, cx));
1608        }
1609    }
1610
1611    fn previous_history_query(
1612        &mut self,
1613        _: &PreviousHistoryQuery,
1614        window: &mut Window,
1615        cx: &mut Context<Self>,
1616    ) {
1617        if self.query(cx).is_empty()
1618            && let Some(new_query) = self
1619                .search_history
1620                .current(&self.search_history_cursor)
1621                .map(str::to_string)
1622        {
1623            drop(self.search(&new_query, Some(self.search_options), false, window, cx));
1624            return;
1625        }
1626
1627        if let Some(new_query) = self
1628            .search_history
1629            .previous(&mut self.search_history_cursor)
1630            .map(str::to_string)
1631        {
1632            drop(self.search(&new_query, Some(self.search_options), false, window, cx));
1633        }
1634    }
1635
1636    fn focus(&self, handle: &gpui::FocusHandle, window: &mut Window, cx: &mut App) {
1637        window.invalidate_character_coordinates();
1638        window.focus(handle, cx);
1639    }
1640
1641    fn toggle_replace(&mut self, _: &ToggleReplace, window: &mut Window, cx: &mut Context<Self>) {
1642        if self.active_searchable_item.is_some() {
1643            self.replace_enabled = !self.replace_enabled;
1644            let handle = if self.replace_enabled {
1645                self.replacement_editor.focus_handle(cx)
1646            } else {
1647                self.query_editor.focus_handle(cx)
1648            };
1649            self.focus(&handle, window, cx);
1650            cx.notify();
1651        }
1652    }
1653
1654    fn replace_next(&mut self, _: &ReplaceNext, window: &mut Window, cx: &mut Context<Self>) {
1655        let mut should_propagate = true;
1656        if !self.dismissed
1657            && self.active_search.is_some()
1658            && let Some(searchable_item) = self.active_searchable_item.as_ref()
1659            && let Some(query) = self.active_search.as_ref()
1660            && let Some(matches) = self
1661                .searchable_items_with_matches
1662                .get(&searchable_item.downgrade())
1663        {
1664            if let Some(active_index) = self.active_match_index {
1665                let query = query
1666                    .as_ref()
1667                    .clone()
1668                    .with_replacement(self.replacement(cx));
1669                searchable_item.replace(matches.at(active_index), &query, window, cx);
1670                self.select_next_match(&SelectNextMatch, window, cx);
1671            }
1672            should_propagate = false;
1673        }
1674        if !should_propagate {
1675            cx.stop_propagation();
1676        }
1677    }
1678
1679    pub fn replace_all(&mut self, _: &ReplaceAll, window: &mut Window, cx: &mut Context<Self>) {
1680        if !self.dismissed
1681            && self.active_search.is_some()
1682            && let Some(searchable_item) = self.active_searchable_item.as_ref()
1683            && let Some(query) = self.active_search.as_ref()
1684            && let Some(matches) = self
1685                .searchable_items_with_matches
1686                .get(&searchable_item.downgrade())
1687        {
1688            let query = query
1689                .as_ref()
1690                .clone()
1691                .with_replacement(self.replacement(cx));
1692            searchable_item.replace_all(&mut matches.iter(), &query, window, cx);
1693        }
1694    }
1695
1696    pub fn match_exists(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
1697        self.update_match_index(window, cx);
1698        self.active_match_index.is_some()
1699    }
1700
1701    pub fn should_use_smartcase_search(&mut self, cx: &mut Context<Self>) -> bool {
1702        EditorSettings::get_global(cx).use_smartcase_search
1703    }
1704
1705    pub fn is_contains_uppercase(&mut self, str: &String) -> bool {
1706        str.chars().any(|c| c.is_uppercase())
1707    }
1708
1709    fn smartcase(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1710        if self.should_use_smartcase_search(cx) {
1711            let query = self.query(cx);
1712            if !query.is_empty() {
1713                let is_case = self.is_contains_uppercase(&query);
1714                if self.has_search_option(SearchOptions::CASE_SENSITIVE) != is_case {
1715                    self.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
1716                }
1717            }
1718        }
1719    }
1720
1721    fn adjust_query_regex_language(&self, cx: &mut App) {
1722        let enable = self.search_options.contains(SearchOptions::REGEX);
1723        let query_buffer = self
1724            .query_editor
1725            .read(cx)
1726            .buffer()
1727            .read(cx)
1728            .as_singleton()
1729            .expect("query editor should be backed by a singleton buffer");
1730
1731        if enable {
1732            if let Some(regex_language) = self.regex_language.clone() {
1733                query_buffer.update(cx, |query_buffer, cx| {
1734                    query_buffer.set_language(Some(regex_language), cx);
1735                })
1736            }
1737        } else {
1738            query_buffer.update(cx, |query_buffer, cx| {
1739                query_buffer.set_language(None, cx);
1740            })
1741        }
1742    }
1743
1744    /// Updates the searchable item's case sensitivity option to match the
1745    /// search bar's current case sensitivity setting. This ensures that
1746    /// editor's `select_next`/ `select_previous` operations respect the buffer
1747    /// search bar's search options.
1748    ///
1749    /// Clears the case sensitivity when the search bar is dismissed so that
1750    /// only the editor's settings are respected.
1751    fn sync_select_next_case_sensitivity(&self, cx: &mut Context<Self>) {
1752        let case_sensitive = match self.dismissed {
1753            true => None,
1754            false => Some(self.search_options.contains(SearchOptions::CASE_SENSITIVE)),
1755        };
1756
1757        if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
1758            active_searchable_item.set_search_is_case_sensitive(case_sensitive, cx);
1759        }
1760    }
1761}
1762
1763#[cfg(test)]
1764mod tests {
1765    use std::ops::Range;
1766
1767    use super::*;
1768    use editor::{
1769        DisplayPoint, Editor, MultiBuffer, SearchSettings, SelectionEffects,
1770        display_map::DisplayRow, test::editor_test_context::EditorTestContext,
1771    };
1772    use gpui::{Hsla, TestAppContext, UpdateGlobal, VisualTestContext};
1773    use language::{Buffer, Point};
1774    use settings::{SearchSettingsContent, SettingsStore};
1775    use smol::stream::StreamExt as _;
1776    use unindent::Unindent as _;
1777    use util_macros::perf;
1778
1779    fn init_globals(cx: &mut TestAppContext) {
1780        cx.update(|cx| {
1781            let store = settings::SettingsStore::test(cx);
1782            cx.set_global(store);
1783            editor::init(cx);
1784
1785            theme::init(theme::LoadThemes::JustBase, cx);
1786            crate::init(cx);
1787        });
1788    }
1789
1790    fn init_test(
1791        cx: &mut TestAppContext,
1792    ) -> (
1793        Entity<Editor>,
1794        Entity<BufferSearchBar>,
1795        &mut VisualTestContext,
1796    ) {
1797        init_globals(cx);
1798        let buffer = cx.new(|cx| {
1799            Buffer::local(
1800                r#"
1801                A regular expression (shortened as regex or regexp;[1] also referred to as
1802                rational expression[2][3]) is a sequence of characters that specifies a search
1803                pattern in text. Usually such patterns are used by string-searching algorithms
1804                for "find" or "find and replace" operations on strings, or for input validation.
1805                "#
1806                .unindent(),
1807                cx,
1808            )
1809        });
1810        let mut editor = None;
1811        let window = cx.add_window(|window, cx| {
1812            let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
1813                "keymaps/default-macos.json",
1814                cx,
1815            )
1816            .unwrap();
1817            cx.bind_keys(default_key_bindings);
1818            editor = Some(cx.new(|cx| Editor::for_buffer(buffer.clone(), None, window, cx)));
1819            let mut search_bar = BufferSearchBar::new(None, window, cx);
1820            search_bar.set_active_pane_item(Some(&editor.clone().unwrap()), window, cx);
1821            search_bar.show(window, cx);
1822            search_bar
1823        });
1824        let search_bar = window.root(cx).unwrap();
1825
1826        let cx = VisualTestContext::from_window(*window, cx).into_mut();
1827
1828        (editor.unwrap(), search_bar, cx)
1829    }
1830
1831    #[perf]
1832    #[gpui::test]
1833    async fn test_search_simple(cx: &mut TestAppContext) {
1834        let (editor, search_bar, cx) = init_test(cx);
1835        let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
1836            background_highlights
1837                .into_iter()
1838                .map(|(range, _)| range)
1839                .collect::<Vec<_>>()
1840        };
1841        // Search for a string that appears with different casing.
1842        // By default, search is case-insensitive.
1843        search_bar
1844            .update_in(cx, |search_bar, window, cx| {
1845                search_bar.search("us", None, true, window, cx)
1846            })
1847            .await
1848            .unwrap();
1849        editor.update_in(cx, |editor, window, cx| {
1850            assert_eq!(
1851                display_points_of(editor.all_text_background_highlights(window, cx)),
1852                &[
1853                    DisplayPoint::new(DisplayRow(2), 17)..DisplayPoint::new(DisplayRow(2), 19),
1854                    DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),
1855                ]
1856            );
1857        });
1858
1859        // Switch to a case sensitive search.
1860        search_bar.update_in(cx, |search_bar, window, cx| {
1861            search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
1862        });
1863        let mut editor_notifications = cx.notifications(&editor);
1864        editor_notifications.next().await;
1865        editor.update_in(cx, |editor, window, cx| {
1866            assert_eq!(
1867                display_points_of(editor.all_text_background_highlights(window, cx)),
1868                &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
1869            );
1870        });
1871
1872        // Search for a string that appears both as a whole word and
1873        // within other words. By default, all results are found.
1874        search_bar
1875            .update_in(cx, |search_bar, window, cx| {
1876                search_bar.search("or", None, true, window, cx)
1877            })
1878            .await
1879            .unwrap();
1880        editor.update_in(cx, |editor, window, cx| {
1881            assert_eq!(
1882                display_points_of(editor.all_text_background_highlights(window, cx)),
1883                &[
1884                    DisplayPoint::new(DisplayRow(0), 24)..DisplayPoint::new(DisplayRow(0), 26),
1885                    DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1886                    DisplayPoint::new(DisplayRow(2), 71)..DisplayPoint::new(DisplayRow(2), 73),
1887                    DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 3),
1888                    DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
1889                    DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
1890                    DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 62),
1891                ]
1892            );
1893        });
1894
1895        // Switch to a whole word search.
1896        search_bar.update_in(cx, |search_bar, window, cx| {
1897            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
1898        });
1899        let mut editor_notifications = cx.notifications(&editor);
1900        editor_notifications.next().await;
1901        editor.update_in(cx, |editor, window, cx| {
1902            assert_eq!(
1903                display_points_of(editor.all_text_background_highlights(window, cx)),
1904                &[
1905                    DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1906                    DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
1907                    DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
1908                ]
1909            );
1910        });
1911
1912        editor.update_in(cx, |editor, window, cx| {
1913            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1914                s.select_display_ranges([
1915                    DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
1916                ])
1917            });
1918        });
1919        search_bar.update_in(cx, |search_bar, window, cx| {
1920            assert_eq!(search_bar.active_match_index, Some(0));
1921            search_bar.select_next_match(&SelectNextMatch, window, cx);
1922            assert_eq!(
1923                editor.update(cx, |editor, cx| editor
1924                    .selections
1925                    .display_ranges(&editor.display_snapshot(cx))),
1926                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1927            );
1928        });
1929        search_bar.read_with(cx, |search_bar, _| {
1930            assert_eq!(search_bar.active_match_index, Some(0));
1931        });
1932
1933        search_bar.update_in(cx, |search_bar, window, cx| {
1934            search_bar.select_next_match(&SelectNextMatch, window, cx);
1935            assert_eq!(
1936                editor.update(cx, |editor, cx| editor
1937                    .selections
1938                    .display_ranges(&editor.display_snapshot(cx))),
1939                [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1940            );
1941        });
1942        search_bar.read_with(cx, |search_bar, _| {
1943            assert_eq!(search_bar.active_match_index, Some(1));
1944        });
1945
1946        search_bar.update_in(cx, |search_bar, window, cx| {
1947            search_bar.select_next_match(&SelectNextMatch, window, cx);
1948            assert_eq!(
1949                editor.update(cx, |editor, cx| editor
1950                    .selections
1951                    .display_ranges(&editor.display_snapshot(cx))),
1952                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1953            );
1954        });
1955        search_bar.read_with(cx, |search_bar, _| {
1956            assert_eq!(search_bar.active_match_index, Some(2));
1957        });
1958
1959        search_bar.update_in(cx, |search_bar, window, cx| {
1960            search_bar.select_next_match(&SelectNextMatch, window, cx);
1961            assert_eq!(
1962                editor.update(cx, |editor, cx| editor
1963                    .selections
1964                    .display_ranges(&editor.display_snapshot(cx))),
1965                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1966            );
1967        });
1968        search_bar.read_with(cx, |search_bar, _| {
1969            assert_eq!(search_bar.active_match_index, Some(0));
1970        });
1971
1972        search_bar.update_in(cx, |search_bar, window, cx| {
1973            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1974            assert_eq!(
1975                editor.update(cx, |editor, cx| editor
1976                    .selections
1977                    .display_ranges(&editor.display_snapshot(cx))),
1978                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1979            );
1980        });
1981        search_bar.read_with(cx, |search_bar, _| {
1982            assert_eq!(search_bar.active_match_index, Some(2));
1983        });
1984
1985        search_bar.update_in(cx, |search_bar, window, cx| {
1986            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1987            assert_eq!(
1988                editor.update(cx, |editor, cx| editor
1989                    .selections
1990                    .display_ranges(&editor.display_snapshot(cx))),
1991                [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1992            );
1993        });
1994        search_bar.read_with(cx, |search_bar, _| {
1995            assert_eq!(search_bar.active_match_index, Some(1));
1996        });
1997
1998        search_bar.update_in(cx, |search_bar, window, cx| {
1999            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2000            assert_eq!(
2001                editor.update(cx, |editor, cx| editor
2002                    .selections
2003                    .display_ranges(&editor.display_snapshot(cx))),
2004                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
2005            );
2006        });
2007        search_bar.read_with(cx, |search_bar, _| {
2008            assert_eq!(search_bar.active_match_index, Some(0));
2009        });
2010
2011        // Park the cursor in between matches and ensure that going to the previous match selects
2012        // the closest match to the left.
2013        editor.update_in(cx, |editor, window, cx| {
2014            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2015                s.select_display_ranges([
2016                    DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
2017                ])
2018            });
2019        });
2020        search_bar.update_in(cx, |search_bar, window, cx| {
2021            assert_eq!(search_bar.active_match_index, Some(1));
2022            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2023            assert_eq!(
2024                editor.update(cx, |editor, cx| editor
2025                    .selections
2026                    .display_ranges(&editor.display_snapshot(cx))),
2027                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
2028            );
2029        });
2030        search_bar.read_with(cx, |search_bar, _| {
2031            assert_eq!(search_bar.active_match_index, Some(0));
2032        });
2033
2034        // Park the cursor in between matches and ensure that going to the next match selects the
2035        // closest match to the right.
2036        editor.update_in(cx, |editor, window, cx| {
2037            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2038                s.select_display_ranges([
2039                    DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
2040                ])
2041            });
2042        });
2043        search_bar.update_in(cx, |search_bar, window, cx| {
2044            assert_eq!(search_bar.active_match_index, Some(1));
2045            search_bar.select_next_match(&SelectNextMatch, window, cx);
2046            assert_eq!(
2047                editor.update(cx, |editor, cx| editor
2048                    .selections
2049                    .display_ranges(&editor.display_snapshot(cx))),
2050                [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
2051            );
2052        });
2053        search_bar.read_with(cx, |search_bar, _| {
2054            assert_eq!(search_bar.active_match_index, Some(1));
2055        });
2056
2057        // Park the cursor after the last match and ensure that going to the previous match selects
2058        // the last match.
2059        editor.update_in(cx, |editor, window, cx| {
2060            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2061                s.select_display_ranges([
2062                    DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
2063                ])
2064            });
2065        });
2066        search_bar.update_in(cx, |search_bar, window, cx| {
2067            assert_eq!(search_bar.active_match_index, Some(2));
2068            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2069            assert_eq!(
2070                editor.update(cx, |editor, cx| editor
2071                    .selections
2072                    .display_ranges(&editor.display_snapshot(cx))),
2073                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
2074            );
2075        });
2076        search_bar.read_with(cx, |search_bar, _| {
2077            assert_eq!(search_bar.active_match_index, Some(2));
2078        });
2079
2080        // Park the cursor after the last match and ensure that going to the next match selects the
2081        // first match.
2082        editor.update_in(cx, |editor, window, cx| {
2083            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2084                s.select_display_ranges([
2085                    DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
2086                ])
2087            });
2088        });
2089        search_bar.update_in(cx, |search_bar, window, cx| {
2090            assert_eq!(search_bar.active_match_index, Some(2));
2091            search_bar.select_next_match(&SelectNextMatch, window, cx);
2092            assert_eq!(
2093                editor.update(cx, |editor, cx| editor
2094                    .selections
2095                    .display_ranges(&editor.display_snapshot(cx))),
2096                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
2097            );
2098        });
2099        search_bar.read_with(cx, |search_bar, _| {
2100            assert_eq!(search_bar.active_match_index, Some(0));
2101        });
2102
2103        // Park the cursor before the first match and ensure that going to the previous match
2104        // selects the last match.
2105        editor.update_in(cx, |editor, window, cx| {
2106            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2107                s.select_display_ranges([
2108                    DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2109                ])
2110            });
2111        });
2112        search_bar.update_in(cx, |search_bar, window, cx| {
2113            assert_eq!(search_bar.active_match_index, Some(0));
2114            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2115            assert_eq!(
2116                editor.update(cx, |editor, cx| editor
2117                    .selections
2118                    .display_ranges(&editor.display_snapshot(cx))),
2119                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
2120            );
2121        });
2122        search_bar.read_with(cx, |search_bar, _| {
2123            assert_eq!(search_bar.active_match_index, Some(2));
2124        });
2125    }
2126
2127    fn display_points_of(
2128        background_highlights: Vec<(Range<DisplayPoint>, Hsla)>,
2129    ) -> Vec<Range<DisplayPoint>> {
2130        background_highlights
2131            .into_iter()
2132            .map(|(range, _)| range)
2133            .collect::<Vec<_>>()
2134    }
2135
2136    #[perf]
2137    #[gpui::test]
2138    async fn test_search_option_handling(cx: &mut TestAppContext) {
2139        let (editor, search_bar, cx) = init_test(cx);
2140
2141        // show with options should make current search case sensitive
2142        search_bar
2143            .update_in(cx, |search_bar, window, cx| {
2144                search_bar.show(window, cx);
2145                search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), true, window, cx)
2146            })
2147            .await
2148            .unwrap();
2149        editor.update_in(cx, |editor, window, cx| {
2150            assert_eq!(
2151                display_points_of(editor.all_text_background_highlights(window, cx)),
2152                &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
2153            );
2154        });
2155
2156        // search_suggested should restore default options
2157        search_bar.update_in(cx, |search_bar, window, cx| {
2158            search_bar.search_suggested(window, cx);
2159            assert_eq!(search_bar.search_options, SearchOptions::NONE)
2160        });
2161
2162        // toggling a search option should update the defaults
2163        search_bar
2164            .update_in(cx, |search_bar, window, cx| {
2165                search_bar.search(
2166                    "regex",
2167                    Some(SearchOptions::CASE_SENSITIVE),
2168                    true,
2169                    window,
2170                    cx,
2171                )
2172            })
2173            .await
2174            .unwrap();
2175        search_bar.update_in(cx, |search_bar, window, cx| {
2176            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx)
2177        });
2178        let mut editor_notifications = cx.notifications(&editor);
2179        editor_notifications.next().await;
2180        editor.update_in(cx, |editor, window, cx| {
2181            assert_eq!(
2182                display_points_of(editor.all_text_background_highlights(window, cx)),
2183                &[DisplayPoint::new(DisplayRow(0), 35)..DisplayPoint::new(DisplayRow(0), 40),]
2184            );
2185        });
2186
2187        // defaults should still include whole word
2188        search_bar.update_in(cx, |search_bar, window, cx| {
2189            search_bar.search_suggested(window, cx);
2190            assert_eq!(
2191                search_bar.search_options,
2192                SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD
2193            )
2194        });
2195    }
2196
2197    #[perf]
2198    #[gpui::test]
2199    async fn test_search_select_all_matches(cx: &mut TestAppContext) {
2200        init_globals(cx);
2201        let buffer_text = r#"
2202        A regular expression (shortened as regex or regexp;[1] also referred to as
2203        rational expression[2][3]) is a sequence of characters that specifies a search
2204        pattern in text. Usually such patterns are used by string-searching algorithms
2205        for "find" or "find and replace" operations on strings, or for input validation.
2206        "#
2207        .unindent();
2208        let expected_query_matches_count = buffer_text
2209            .chars()
2210            .filter(|c| c.eq_ignore_ascii_case(&'a'))
2211            .count();
2212        assert!(
2213            expected_query_matches_count > 1,
2214            "Should pick a query with multiple results"
2215        );
2216        let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
2217        let window = cx.add_window(|_, _| gpui::Empty);
2218
2219        let editor = window.build_entity(cx, |window, cx| {
2220            Editor::for_buffer(buffer.clone(), None, window, cx)
2221        });
2222
2223        let search_bar = window.build_entity(cx, |window, cx| {
2224            let mut search_bar = BufferSearchBar::new(None, window, cx);
2225            search_bar.set_active_pane_item(Some(&editor), window, cx);
2226            search_bar.show(window, cx);
2227            search_bar
2228        });
2229
2230        window
2231            .update(cx, |_, window, cx| {
2232                search_bar.update(cx, |search_bar, cx| {
2233                    search_bar.search("a", None, true, window, cx)
2234                })
2235            })
2236            .unwrap()
2237            .await
2238            .unwrap();
2239        let initial_selections = window
2240            .update(cx, |_, window, cx| {
2241                search_bar.update(cx, |search_bar, cx| {
2242                    let handle = search_bar.query_editor.focus_handle(cx);
2243                    window.focus(&handle, cx);
2244                    search_bar.activate_current_match(window, cx);
2245                });
2246                assert!(
2247                    !editor.read(cx).is_focused(window),
2248                    "Initially, the editor should not be focused"
2249                );
2250                let initial_selections = editor.update(cx, |editor, cx| {
2251                    let initial_selections = editor.selections.display_ranges(&editor.display_snapshot(cx));
2252                    assert_eq!(
2253                        initial_selections.len(), 1,
2254                        "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
2255                    );
2256                    initial_selections
2257                });
2258                search_bar.update(cx, |search_bar, cx| {
2259                    assert_eq!(search_bar.active_match_index, Some(0));
2260                    let handle = search_bar.query_editor.focus_handle(cx);
2261                    window.focus(&handle, cx);
2262                    search_bar.select_all_matches(&SelectAllMatches, window, cx);
2263                });
2264                assert!(
2265                    editor.read(cx).is_focused(window),
2266                    "Should focus editor after successful SelectAllMatches"
2267                );
2268                search_bar.update(cx, |search_bar, cx| {
2269                    let all_selections =
2270                        editor.update(cx, |editor, cx| editor.selections.display_ranges(&editor.display_snapshot(cx)));
2271                    assert_eq!(
2272                        all_selections.len(),
2273                        expected_query_matches_count,
2274                        "Should select all `a` characters in the buffer, but got: {all_selections:?}"
2275                    );
2276                    assert_eq!(
2277                        search_bar.active_match_index,
2278                        Some(0),
2279                        "Match index should not change after selecting all matches"
2280                    );
2281                });
2282
2283                search_bar.update(cx, |this, cx| this.select_next_match(&SelectNextMatch, window, cx));
2284                initial_selections
2285            }).unwrap();
2286
2287        window
2288            .update(cx, |_, window, cx| {
2289                assert!(
2290                    editor.read(cx).is_focused(window),
2291                    "Should still have editor focused after SelectNextMatch"
2292                );
2293                search_bar.update(cx, |search_bar, cx| {
2294                    let all_selections = editor.update(cx, |editor, cx| {
2295                        editor
2296                            .selections
2297                            .display_ranges(&editor.display_snapshot(cx))
2298                    });
2299                    assert_eq!(
2300                        all_selections.len(),
2301                        1,
2302                        "On next match, should deselect items and select the next match"
2303                    );
2304                    assert_ne!(
2305                        all_selections, initial_selections,
2306                        "Next match should be different from the first selection"
2307                    );
2308                    assert_eq!(
2309                        search_bar.active_match_index,
2310                        Some(1),
2311                        "Match index should be updated to the next one"
2312                    );
2313                    let handle = search_bar.query_editor.focus_handle(cx);
2314                    window.focus(&handle, cx);
2315                    search_bar.select_all_matches(&SelectAllMatches, window, cx);
2316                });
2317            })
2318            .unwrap();
2319        window
2320            .update(cx, |_, window, cx| {
2321                assert!(
2322                    editor.read(cx).is_focused(window),
2323                    "Should focus editor after successful SelectAllMatches"
2324                );
2325                search_bar.update(cx, |search_bar, cx| {
2326                    let all_selections =
2327                        editor.update(cx, |editor, cx| editor.selections.display_ranges(&editor.display_snapshot(cx)));
2328                    assert_eq!(
2329                    all_selections.len(),
2330                    expected_query_matches_count,
2331                    "Should select all `a` characters in the buffer, but got: {all_selections:?}"
2332                );
2333                    assert_eq!(
2334                        search_bar.active_match_index,
2335                        Some(1),
2336                        "Match index should not change after selecting all matches"
2337                    );
2338                });
2339                search_bar.update(cx, |search_bar, cx| {
2340                    search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2341                });
2342            })
2343            .unwrap();
2344        let last_match_selections = window
2345            .update(cx, |_, window, cx| {
2346                assert!(
2347                    editor.read(cx).is_focused(window),
2348                    "Should still have editor focused after SelectPreviousMatch"
2349                );
2350
2351                search_bar.update(cx, |search_bar, cx| {
2352                    let all_selections = editor.update(cx, |editor, cx| {
2353                        editor
2354                            .selections
2355                            .display_ranges(&editor.display_snapshot(cx))
2356                    });
2357                    assert_eq!(
2358                        all_selections.len(),
2359                        1,
2360                        "On previous match, should deselect items and select the previous item"
2361                    );
2362                    assert_eq!(
2363                        all_selections, initial_selections,
2364                        "Previous match should be the same as the first selection"
2365                    );
2366                    assert_eq!(
2367                        search_bar.active_match_index,
2368                        Some(0),
2369                        "Match index should be updated to the previous one"
2370                    );
2371                    all_selections
2372                })
2373            })
2374            .unwrap();
2375
2376        window
2377            .update(cx, |_, window, cx| {
2378                search_bar.update(cx, |search_bar, cx| {
2379                    let handle = search_bar.query_editor.focus_handle(cx);
2380                    window.focus(&handle, cx);
2381                    search_bar.search("abas_nonexistent_match", None, true, window, cx)
2382                })
2383            })
2384            .unwrap()
2385            .await
2386            .unwrap();
2387        window
2388            .update(cx, |_, window, cx| {
2389                search_bar.update(cx, |search_bar, cx| {
2390                    search_bar.select_all_matches(&SelectAllMatches, window, cx);
2391                });
2392                assert!(
2393                    editor.update(cx, |this, _cx| !this.is_focused(window)),
2394                    "Should not switch focus to editor if SelectAllMatches does not find any matches"
2395                );
2396                search_bar.update(cx, |search_bar, cx| {
2397                    let all_selections =
2398                        editor.update(cx, |editor, cx| editor.selections.display_ranges(&editor.display_snapshot(cx)));
2399                    assert_eq!(
2400                        all_selections, last_match_selections,
2401                        "Should not select anything new if there are no matches"
2402                    );
2403                    assert!(
2404                        search_bar.active_match_index.is_none(),
2405                        "For no matches, there should be no active match index"
2406                    );
2407                });
2408            })
2409            .unwrap();
2410    }
2411
2412    #[perf]
2413    #[gpui::test]
2414    async fn test_search_query_with_match_whole_word(cx: &mut TestAppContext) {
2415        init_globals(cx);
2416        let buffer_text = r#"
2417        self.buffer.update(cx, |buffer, cx| {
2418            buffer.edit(
2419                edits,
2420                Some(AutoindentMode::Block {
2421                    original_indent_columns,
2422                }),
2423                cx,
2424            )
2425        });
2426
2427        this.buffer.update(cx, |buffer, cx| {
2428            buffer.edit([(end_of_line..start_of_next_line, replace)], None, cx)
2429        });
2430        "#
2431        .unindent();
2432        let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
2433        let cx = cx.add_empty_window();
2434
2435        let editor =
2436            cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
2437
2438        let search_bar = cx.new_window_entity(|window, cx| {
2439            let mut search_bar = BufferSearchBar::new(None, window, cx);
2440            search_bar.set_active_pane_item(Some(&editor), window, cx);
2441            search_bar.show(window, cx);
2442            search_bar
2443        });
2444
2445        search_bar
2446            .update_in(cx, |search_bar, window, cx| {
2447                search_bar.search(
2448                    "edit\\(",
2449                    Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
2450                    true,
2451                    window,
2452                    cx,
2453                )
2454            })
2455            .await
2456            .unwrap();
2457
2458        search_bar.update_in(cx, |search_bar, window, cx| {
2459            search_bar.select_all_matches(&SelectAllMatches, window, cx);
2460        });
2461        search_bar.update(cx, |_, cx| {
2462            let all_selections = editor.update(cx, |editor, cx| {
2463                editor
2464                    .selections
2465                    .display_ranges(&editor.display_snapshot(cx))
2466            });
2467            assert_eq!(
2468                all_selections.len(),
2469                2,
2470                "Should select all `edit(` in the buffer, but got: {all_selections:?}"
2471            );
2472        });
2473
2474        search_bar
2475            .update_in(cx, |search_bar, window, cx| {
2476                search_bar.search(
2477                    "edit(",
2478                    Some(SearchOptions::WHOLE_WORD | SearchOptions::CASE_SENSITIVE),
2479                    true,
2480                    window,
2481                    cx,
2482                )
2483            })
2484            .await
2485            .unwrap();
2486
2487        search_bar.update_in(cx, |search_bar, window, cx| {
2488            search_bar.select_all_matches(&SelectAllMatches, window, cx);
2489        });
2490        search_bar.update(cx, |_, cx| {
2491            let all_selections = editor.update(cx, |editor, cx| {
2492                editor
2493                    .selections
2494                    .display_ranges(&editor.display_snapshot(cx))
2495            });
2496            assert_eq!(
2497                all_selections.len(),
2498                2,
2499                "Should select all `edit(` in the buffer, but got: {all_selections:?}"
2500            );
2501        });
2502    }
2503
2504    #[perf]
2505    #[gpui::test]
2506    async fn test_search_query_history(cx: &mut TestAppContext) {
2507        let (_editor, search_bar, cx) = init_test(cx);
2508
2509        // Add 3 search items into the history.
2510        search_bar
2511            .update_in(cx, |search_bar, window, cx| {
2512                search_bar.search("a", None, true, window, cx)
2513            })
2514            .await
2515            .unwrap();
2516        search_bar
2517            .update_in(cx, |search_bar, window, cx| {
2518                search_bar.search("b", None, true, window, cx)
2519            })
2520            .await
2521            .unwrap();
2522        search_bar
2523            .update_in(cx, |search_bar, window, cx| {
2524                search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), true, window, cx)
2525            })
2526            .await
2527            .unwrap();
2528        // Ensure that the latest search is active.
2529        search_bar.update(cx, |search_bar, cx| {
2530            assert_eq!(search_bar.query(cx), "c");
2531            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2532        });
2533
2534        // Next history query after the latest should set the query to the empty string.
2535        search_bar.update_in(cx, |search_bar, window, cx| {
2536            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2537        });
2538        cx.background_executor.run_until_parked();
2539        search_bar.update(cx, |search_bar, cx| {
2540            assert_eq!(search_bar.query(cx), "");
2541            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2542        });
2543        search_bar.update_in(cx, |search_bar, window, cx| {
2544            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2545        });
2546        cx.background_executor.run_until_parked();
2547        search_bar.update(cx, |search_bar, cx| {
2548            assert_eq!(search_bar.query(cx), "");
2549            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2550        });
2551
2552        // First previous query for empty current query should set the query to the latest.
2553        search_bar.update_in(cx, |search_bar, window, cx| {
2554            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2555        });
2556        cx.background_executor.run_until_parked();
2557        search_bar.update(cx, |search_bar, cx| {
2558            assert_eq!(search_bar.query(cx), "c");
2559            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2560        });
2561
2562        // Further previous items should go over the history in reverse order.
2563        search_bar.update_in(cx, |search_bar, window, cx| {
2564            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2565        });
2566        cx.background_executor.run_until_parked();
2567        search_bar.update(cx, |search_bar, cx| {
2568            assert_eq!(search_bar.query(cx), "b");
2569            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2570        });
2571
2572        // Previous items should never go behind the first history item.
2573        search_bar.update_in(cx, |search_bar, window, cx| {
2574            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2575        });
2576        cx.background_executor.run_until_parked();
2577        search_bar.update(cx, |search_bar, cx| {
2578            assert_eq!(search_bar.query(cx), "a");
2579            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2580        });
2581        search_bar.update_in(cx, |search_bar, window, cx| {
2582            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2583        });
2584        cx.background_executor.run_until_parked();
2585        search_bar.update(cx, |search_bar, cx| {
2586            assert_eq!(search_bar.query(cx), "a");
2587            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2588        });
2589
2590        // Next items should go over the history in the original order.
2591        search_bar.update_in(cx, |search_bar, window, cx| {
2592            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2593        });
2594        cx.background_executor.run_until_parked();
2595        search_bar.update(cx, |search_bar, cx| {
2596            assert_eq!(search_bar.query(cx), "b");
2597            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2598        });
2599
2600        search_bar
2601            .update_in(cx, |search_bar, window, cx| {
2602                search_bar.search("ba", None, true, window, cx)
2603            })
2604            .await
2605            .unwrap();
2606        search_bar.update(cx, |search_bar, cx| {
2607            assert_eq!(search_bar.query(cx), "ba");
2608            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2609        });
2610
2611        // New search input should add another entry to history and move the selection to the end of the history.
2612        search_bar.update_in(cx, |search_bar, window, cx| {
2613            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2614        });
2615        cx.background_executor.run_until_parked();
2616        search_bar.update(cx, |search_bar, cx| {
2617            assert_eq!(search_bar.query(cx), "c");
2618            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2619        });
2620        search_bar.update_in(cx, |search_bar, window, cx| {
2621            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2622        });
2623        cx.background_executor.run_until_parked();
2624        search_bar.update(cx, |search_bar, cx| {
2625            assert_eq!(search_bar.query(cx), "b");
2626            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2627        });
2628        search_bar.update_in(cx, |search_bar, window, cx| {
2629            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2630        });
2631        cx.background_executor.run_until_parked();
2632        search_bar.update(cx, |search_bar, cx| {
2633            assert_eq!(search_bar.query(cx), "c");
2634            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2635        });
2636        search_bar.update_in(cx, |search_bar, window, cx| {
2637            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2638        });
2639        cx.background_executor.run_until_parked();
2640        search_bar.update(cx, |search_bar, cx| {
2641            assert_eq!(search_bar.query(cx), "ba");
2642            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2643        });
2644        search_bar.update_in(cx, |search_bar, window, cx| {
2645            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2646        });
2647        cx.background_executor.run_until_parked();
2648        search_bar.update(cx, |search_bar, cx| {
2649            assert_eq!(search_bar.query(cx), "");
2650            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2651        });
2652    }
2653
2654    #[perf]
2655    #[gpui::test]
2656    async fn test_replace_simple(cx: &mut TestAppContext) {
2657        let (editor, search_bar, cx) = init_test(cx);
2658
2659        search_bar
2660            .update_in(cx, |search_bar, window, cx| {
2661                search_bar.search("expression", None, true, window, cx)
2662            })
2663            .await
2664            .unwrap();
2665
2666        search_bar.update_in(cx, |search_bar, window, cx| {
2667            search_bar.replacement_editor.update(cx, |editor, cx| {
2668                // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally.
2669                editor.set_text("expr$1", window, cx);
2670            });
2671            search_bar.replace_all(&ReplaceAll, window, cx)
2672        });
2673        assert_eq!(
2674            editor.read_with(cx, |this, cx| { this.text(cx) }),
2675            r#"
2676        A regular expr$1 (shortened as regex or regexp;[1] also referred to as
2677        rational expr$1[2][3]) is a sequence of characters that specifies a search
2678        pattern in text. Usually such patterns are used by string-searching algorithms
2679        for "find" or "find and replace" operations on strings, or for input validation.
2680        "#
2681            .unindent()
2682        );
2683
2684        // Search for word boundaries and replace just a single one.
2685        search_bar
2686            .update_in(cx, |search_bar, window, cx| {
2687                search_bar.search("or", Some(SearchOptions::WHOLE_WORD), true, window, cx)
2688            })
2689            .await
2690            .unwrap();
2691
2692        search_bar.update_in(cx, |search_bar, window, cx| {
2693            search_bar.replacement_editor.update(cx, |editor, cx| {
2694                editor.set_text("banana", window, cx);
2695            });
2696            search_bar.replace_next(&ReplaceNext, window, cx)
2697        });
2698        // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text.
2699        assert_eq!(
2700            editor.read_with(cx, |this, cx| { this.text(cx) }),
2701            r#"
2702        A regular expr$1 (shortened as regex banana regexp;[1] also referred to as
2703        rational expr$1[2][3]) is a sequence of characters that specifies a search
2704        pattern in text. Usually such patterns are used by string-searching algorithms
2705        for "find" or "find and replace" operations on strings, or for input validation.
2706        "#
2707            .unindent()
2708        );
2709        // Let's turn on regex mode.
2710        search_bar
2711            .update_in(cx, |search_bar, window, cx| {
2712                search_bar.search(
2713                    "\\[([^\\]]+)\\]",
2714                    Some(SearchOptions::REGEX),
2715                    true,
2716                    window,
2717                    cx,
2718                )
2719            })
2720            .await
2721            .unwrap();
2722        search_bar.update_in(cx, |search_bar, window, cx| {
2723            search_bar.replacement_editor.update(cx, |editor, cx| {
2724                editor.set_text("${1}number", window, cx);
2725            });
2726            search_bar.replace_all(&ReplaceAll, window, cx)
2727        });
2728        assert_eq!(
2729            editor.read_with(cx, |this, cx| { this.text(cx) }),
2730            r#"
2731        A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2732        rational expr$12number3number) is a sequence of characters that specifies a search
2733        pattern in text. Usually such patterns are used by string-searching algorithms
2734        for "find" or "find and replace" operations on strings, or for input validation.
2735        "#
2736            .unindent()
2737        );
2738        // Now with a whole-word twist.
2739        search_bar
2740            .update_in(cx, |search_bar, window, cx| {
2741                search_bar.search(
2742                    "a\\w+s",
2743                    Some(SearchOptions::REGEX | SearchOptions::WHOLE_WORD),
2744                    true,
2745                    window,
2746                    cx,
2747                )
2748            })
2749            .await
2750            .unwrap();
2751        search_bar.update_in(cx, |search_bar, window, cx| {
2752            search_bar.replacement_editor.update(cx, |editor, cx| {
2753                editor.set_text("things", window, cx);
2754            });
2755            search_bar.replace_all(&ReplaceAll, window, cx)
2756        });
2757        // The only word affected by this edit should be `algorithms`, even though there's a bunch
2758        // of words in this text that would match this regex if not for WHOLE_WORD.
2759        assert_eq!(
2760            editor.read_with(cx, |this, cx| { this.text(cx) }),
2761            r#"
2762        A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2763        rational expr$12number3number) is a sequence of characters that specifies a search
2764        pattern in text. Usually such patterns are used by string-searching things
2765        for "find" or "find and replace" operations on strings, or for input validation.
2766        "#
2767            .unindent()
2768        );
2769    }
2770
2771    #[gpui::test]
2772    async fn test_replace_focus(cx: &mut TestAppContext) {
2773        let (editor, search_bar, cx) = init_test(cx);
2774
2775        editor.update_in(cx, |editor, window, cx| {
2776            editor.set_text("What a bad day!", window, cx)
2777        });
2778
2779        search_bar
2780            .update_in(cx, |search_bar, window, cx| {
2781                search_bar.search("bad", None, true, window, cx)
2782            })
2783            .await
2784            .unwrap();
2785
2786        // Calling `toggle_replace` in the search bar ensures that the "Replace
2787        // *" buttons are rendered, so we can then simulate clicking the
2788        // buttons.
2789        search_bar.update_in(cx, |search_bar, window, cx| {
2790            search_bar.toggle_replace(&ToggleReplace, window, cx)
2791        });
2792
2793        search_bar.update_in(cx, |search_bar, window, cx| {
2794            search_bar.replacement_editor.update(cx, |editor, cx| {
2795                editor.set_text("great", window, cx);
2796            });
2797        });
2798
2799        // Focus on the editor instead of the search bar, as we want to ensure
2800        // that pressing the "Replace Next Match" button will work, even if the
2801        // search bar is not focused.
2802        cx.focus(&editor);
2803
2804        // We'll not simulate clicking the "Replace Next Match " button, asserting that
2805        // the replacement was done.
2806        let button_bounds = cx
2807            .debug_bounds("ICON-ReplaceNext")
2808            .expect("'Replace Next Match' button should be visible");
2809        cx.simulate_click(button_bounds.center(), gpui::Modifiers::none());
2810
2811        assert_eq!(
2812            editor.read_with(cx, |editor, cx| editor.text(cx)),
2813            "What a great day!"
2814        );
2815    }
2816
2817    struct ReplacementTestParams<'a> {
2818        editor: &'a Entity<Editor>,
2819        search_bar: &'a Entity<BufferSearchBar>,
2820        cx: &'a mut VisualTestContext,
2821        search_text: &'static str,
2822        search_options: Option<SearchOptions>,
2823        replacement_text: &'static str,
2824        replace_all: bool,
2825        expected_text: String,
2826    }
2827
2828    async fn run_replacement_test(options: ReplacementTestParams<'_>) {
2829        options
2830            .search_bar
2831            .update_in(options.cx, |search_bar, window, cx| {
2832                if let Some(options) = options.search_options {
2833                    search_bar.set_search_options(options, cx);
2834                }
2835                search_bar.search(
2836                    options.search_text,
2837                    options.search_options,
2838                    true,
2839                    window,
2840                    cx,
2841                )
2842            })
2843            .await
2844            .unwrap();
2845
2846        options
2847            .search_bar
2848            .update_in(options.cx, |search_bar, window, cx| {
2849                search_bar.replacement_editor.update(cx, |editor, cx| {
2850                    editor.set_text(options.replacement_text, window, cx);
2851                });
2852
2853                if options.replace_all {
2854                    search_bar.replace_all(&ReplaceAll, window, cx)
2855                } else {
2856                    search_bar.replace_next(&ReplaceNext, window, cx)
2857                }
2858            });
2859
2860        assert_eq!(
2861            options
2862                .editor
2863                .read_with(options.cx, |this, cx| { this.text(cx) }),
2864            options.expected_text
2865        );
2866    }
2867
2868    #[perf]
2869    #[gpui::test]
2870    async fn test_replace_special_characters(cx: &mut TestAppContext) {
2871        let (editor, search_bar, cx) = init_test(cx);
2872
2873        run_replacement_test(ReplacementTestParams {
2874            editor: &editor,
2875            search_bar: &search_bar,
2876            cx,
2877            search_text: "expression",
2878            search_options: None,
2879            replacement_text: r"\n",
2880            replace_all: true,
2881            expected_text: r#"
2882            A regular \n (shortened as regex or regexp;[1] also referred to as
2883            rational \n[2][3]) is a sequence of characters that specifies a search
2884            pattern in text. Usually such patterns are used by string-searching algorithms
2885            for "find" or "find and replace" operations on strings, or for input validation.
2886            "#
2887            .unindent(),
2888        })
2889        .await;
2890
2891        run_replacement_test(ReplacementTestParams {
2892            editor: &editor,
2893            search_bar: &search_bar,
2894            cx,
2895            search_text: "or",
2896            search_options: Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
2897            replacement_text: r"\\\n\\\\",
2898            replace_all: false,
2899            expected_text: r#"
2900            A regular \n (shortened as regex \
2901            \\ regexp;[1] also referred to as
2902            rational \n[2][3]) is a sequence of characters that specifies a search
2903            pattern in text. Usually such patterns are used by string-searching algorithms
2904            for "find" or "find and replace" operations on strings, or for input validation.
2905            "#
2906            .unindent(),
2907        })
2908        .await;
2909
2910        run_replacement_test(ReplacementTestParams {
2911            editor: &editor,
2912            search_bar: &search_bar,
2913            cx,
2914            search_text: r"(that|used) ",
2915            search_options: Some(SearchOptions::REGEX),
2916            replacement_text: r"$1\n",
2917            replace_all: true,
2918            expected_text: r#"
2919            A regular \n (shortened as regex \
2920            \\ regexp;[1] also referred to as
2921            rational \n[2][3]) is a sequence of characters that
2922            specifies a search
2923            pattern in text. Usually such patterns are used
2924            by string-searching algorithms
2925            for "find" or "find and replace" operations on strings, or for input validation.
2926            "#
2927            .unindent(),
2928        })
2929        .await;
2930    }
2931
2932    #[perf]
2933    #[gpui::test]
2934    async fn test_find_matches_in_selections_singleton_buffer_multiple_selections(
2935        cx: &mut TestAppContext,
2936    ) {
2937        init_globals(cx);
2938        let buffer = cx.new(|cx| {
2939            Buffer::local(
2940                r#"
2941                aaa bbb aaa ccc
2942                aaa bbb aaa ccc
2943                aaa bbb aaa ccc
2944                aaa bbb aaa ccc
2945                aaa bbb aaa ccc
2946                aaa bbb aaa ccc
2947                "#
2948                .unindent(),
2949                cx,
2950            )
2951        });
2952        let cx = cx.add_empty_window();
2953        let editor =
2954            cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
2955
2956        let search_bar = cx.new_window_entity(|window, cx| {
2957            let mut search_bar = BufferSearchBar::new(None, window, cx);
2958            search_bar.set_active_pane_item(Some(&editor), window, cx);
2959            search_bar.show(window, cx);
2960            search_bar
2961        });
2962
2963        editor.update_in(cx, |editor, window, cx| {
2964            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2965                s.select_ranges(vec![Point::new(1, 0)..Point::new(2, 4)])
2966            })
2967        });
2968
2969        search_bar.update_in(cx, |search_bar, window, cx| {
2970            let deploy = Deploy {
2971                focus: true,
2972                replace_enabled: false,
2973                selection_search_enabled: true,
2974            };
2975            search_bar.deploy(&deploy, window, cx);
2976        });
2977
2978        cx.run_until_parked();
2979
2980        search_bar
2981            .update_in(cx, |search_bar, window, cx| {
2982                search_bar.search("aaa", None, true, window, cx)
2983            })
2984            .await
2985            .unwrap();
2986
2987        editor.update(cx, |editor, cx| {
2988            assert_eq!(
2989                editor.search_background_highlights(cx),
2990                &[
2991                    Point::new(1, 0)..Point::new(1, 3),
2992                    Point::new(1, 8)..Point::new(1, 11),
2993                    Point::new(2, 0)..Point::new(2, 3),
2994                ]
2995            );
2996        });
2997    }
2998
2999    #[perf]
3000    #[gpui::test]
3001    async fn test_find_matches_in_selections_multiple_excerpts_buffer_multiple_selections(
3002        cx: &mut TestAppContext,
3003    ) {
3004        init_globals(cx);
3005        let text = r#"
3006            aaa bbb aaa ccc
3007            aaa bbb aaa ccc
3008            aaa bbb aaa ccc
3009            aaa bbb aaa ccc
3010            aaa bbb aaa ccc
3011            aaa bbb aaa ccc
3012
3013            aaa bbb aaa ccc
3014            aaa bbb aaa ccc
3015            aaa bbb aaa ccc
3016            aaa bbb aaa ccc
3017            aaa bbb aaa ccc
3018            aaa bbb aaa ccc
3019            "#
3020        .unindent();
3021
3022        let cx = cx.add_empty_window();
3023        let editor = cx.new_window_entity(|window, cx| {
3024            let multibuffer = MultiBuffer::build_multi(
3025                [
3026                    (
3027                        &text,
3028                        vec![
3029                            Point::new(0, 0)..Point::new(2, 0),
3030                            Point::new(4, 0)..Point::new(5, 0),
3031                        ],
3032                    ),
3033                    (&text, vec![Point::new(9, 0)..Point::new(11, 0)]),
3034                ],
3035                cx,
3036            );
3037            Editor::for_multibuffer(multibuffer, None, window, cx)
3038        });
3039
3040        let search_bar = cx.new_window_entity(|window, cx| {
3041            let mut search_bar = BufferSearchBar::new(None, window, cx);
3042            search_bar.set_active_pane_item(Some(&editor), window, cx);
3043            search_bar.show(window, cx);
3044            search_bar
3045        });
3046
3047        editor.update_in(cx, |editor, window, cx| {
3048            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3049                s.select_ranges(vec![
3050                    Point::new(1, 0)..Point::new(1, 4),
3051                    Point::new(5, 3)..Point::new(6, 4),
3052                ])
3053            })
3054        });
3055
3056        search_bar.update_in(cx, |search_bar, window, cx| {
3057            let deploy = Deploy {
3058                focus: true,
3059                replace_enabled: false,
3060                selection_search_enabled: true,
3061            };
3062            search_bar.deploy(&deploy, window, cx);
3063        });
3064
3065        cx.run_until_parked();
3066
3067        search_bar
3068            .update_in(cx, |search_bar, window, cx| {
3069                search_bar.search("aaa", None, true, window, cx)
3070            })
3071            .await
3072            .unwrap();
3073
3074        editor.update(cx, |editor, cx| {
3075            assert_eq!(
3076                editor.search_background_highlights(cx),
3077                &[
3078                    Point::new(1, 0)..Point::new(1, 3),
3079                    Point::new(5, 8)..Point::new(5, 11),
3080                    Point::new(6, 0)..Point::new(6, 3),
3081                ]
3082            );
3083        });
3084    }
3085
3086    #[perf]
3087    #[gpui::test]
3088    async fn test_invalid_regexp_search_after_valid(cx: &mut TestAppContext) {
3089        let (editor, search_bar, cx) = init_test(cx);
3090        // Search using valid regexp
3091        search_bar
3092            .update_in(cx, |search_bar, window, cx| {
3093                search_bar.enable_search_option(SearchOptions::REGEX, window, cx);
3094                search_bar.search("expression", None, true, window, cx)
3095            })
3096            .await
3097            .unwrap();
3098        editor.update_in(cx, |editor, window, cx| {
3099            assert_eq!(
3100                display_points_of(editor.all_text_background_highlights(window, cx)),
3101                &[
3102                    DisplayPoint::new(DisplayRow(0), 10)..DisplayPoint::new(DisplayRow(0), 20),
3103                    DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 19),
3104                ],
3105            );
3106        });
3107
3108        // Now, the expression is invalid
3109        search_bar
3110            .update_in(cx, |search_bar, window, cx| {
3111                search_bar.search("expression (", None, true, window, cx)
3112            })
3113            .await
3114            .unwrap_err();
3115        editor.update_in(cx, |editor, window, cx| {
3116            assert!(
3117                display_points_of(editor.all_text_background_highlights(window, cx)).is_empty(),
3118            );
3119        });
3120    }
3121
3122    #[perf]
3123    #[gpui::test]
3124    async fn test_search_options_changes(cx: &mut TestAppContext) {
3125        let (_editor, search_bar, cx) = init_test(cx);
3126        update_search_settings(
3127            SearchSettings {
3128                button: true,
3129                whole_word: false,
3130                case_sensitive: false,
3131                include_ignored: false,
3132                regex: false,
3133                center_on_match: false,
3134            },
3135            cx,
3136        );
3137
3138        let deploy = Deploy {
3139            focus: true,
3140            replace_enabled: false,
3141            selection_search_enabled: true,
3142        };
3143
3144        search_bar.update_in(cx, |search_bar, window, cx| {
3145            assert_eq!(
3146                search_bar.search_options,
3147                SearchOptions::NONE,
3148                "Should have no search options enabled by default"
3149            );
3150            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
3151            assert_eq!(
3152                search_bar.search_options,
3153                SearchOptions::WHOLE_WORD,
3154                "Should enable the option toggled"
3155            );
3156            assert!(
3157                !search_bar.dismissed,
3158                "Search bar should be present and visible"
3159            );
3160            search_bar.deploy(&deploy, window, cx);
3161            assert_eq!(
3162                search_bar.search_options,
3163                SearchOptions::WHOLE_WORD,
3164                "After (re)deploying, the option should still be enabled"
3165            );
3166
3167            search_bar.dismiss(&Dismiss, window, cx);
3168            search_bar.deploy(&deploy, window, cx);
3169            assert_eq!(
3170                search_bar.search_options,
3171                SearchOptions::WHOLE_WORD,
3172                "After hiding and showing the search bar, search options should be preserved"
3173            );
3174
3175            search_bar.toggle_search_option(SearchOptions::REGEX, window, cx);
3176            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
3177            assert_eq!(
3178                search_bar.search_options,
3179                SearchOptions::REGEX,
3180                "Should enable the options toggled"
3181            );
3182            assert!(
3183                !search_bar.dismissed,
3184                "Search bar should be present and visible"
3185            );
3186            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
3187        });
3188
3189        update_search_settings(
3190            SearchSettings {
3191                button: true,
3192                whole_word: false,
3193                case_sensitive: true,
3194                include_ignored: false,
3195                regex: false,
3196                center_on_match: false,
3197            },
3198            cx,
3199        );
3200        search_bar.update_in(cx, |search_bar, window, cx| {
3201            assert_eq!(
3202                search_bar.search_options,
3203                SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
3204                "Should have no search options enabled by default"
3205            );
3206
3207            search_bar.deploy(&deploy, window, cx);
3208            assert_eq!(
3209                search_bar.search_options,
3210                SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
3211                "Toggling a non-dismissed search bar with custom options should not change the default options"
3212            );
3213            search_bar.dismiss(&Dismiss, window, cx);
3214            search_bar.deploy(&deploy, window, cx);
3215            assert_eq!(
3216                search_bar.configured_options,
3217                SearchOptions::CASE_SENSITIVE,
3218                "After a settings update and toggling the search bar, configured options should be updated"
3219            );
3220            assert_eq!(
3221                search_bar.search_options,
3222                SearchOptions::CASE_SENSITIVE,
3223                "After a settings update and toggling the search bar, configured options should be used"
3224            );
3225        });
3226
3227        update_search_settings(
3228            SearchSettings {
3229                button: true,
3230                whole_word: true,
3231                case_sensitive: true,
3232                include_ignored: false,
3233                regex: false,
3234                center_on_match: false,
3235            },
3236            cx,
3237        );
3238
3239        search_bar.update_in(cx, |search_bar, window, cx| {
3240            search_bar.deploy(&deploy, window, cx);
3241            search_bar.dismiss(&Dismiss, window, cx);
3242            search_bar.show(window, cx);
3243            assert_eq!(
3244                search_bar.search_options,
3245                SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD,
3246                "Calling deploy on an already deployed search bar should not prevent settings updates from being detected"
3247            );
3248        });
3249    }
3250
3251    #[gpui::test]
3252    async fn test_select_occurrence_case_sensitivity(cx: &mut TestAppContext) {
3253        let (editor, search_bar, cx) = init_test(cx);
3254        let mut editor_cx = EditorTestContext::for_editor_in(editor, cx).await;
3255
3256        // Start with case sensitive search settings.
3257        let mut search_settings = SearchSettings::default();
3258        search_settings.case_sensitive = true;
3259        update_search_settings(search_settings, cx);
3260        search_bar.update(cx, |search_bar, cx| {
3261            let mut search_options = search_bar.search_options;
3262            search_options.insert(SearchOptions::CASE_SENSITIVE);
3263            search_bar.set_search_options(search_options, cx);
3264        });
3265
3266        editor_cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
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»\nFOO\nFoo\n«ˇfoo»");
3271
3272        // Update the search bar's case sensitivite toggle, so we can later
3273        // confirm that `select_next` will now be case-insensitive.
3274        editor_cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
3275        search_bar.update_in(cx, |search_bar, window, cx| {
3276            search_bar.toggle_case_sensitive(&Default::default(), window, cx);
3277        });
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»\n«ˇFOO»\nFoo\nfoo");
3282
3283        // Confirm that, after dismissing the search bar, only the editor's
3284        // search settings actually affect the behavior of `select_next`.
3285        search_bar.update_in(cx, |search_bar, window, cx| {
3286            search_bar.dismiss(&Default::default(), window, cx);
3287        });
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»\nFOO\nFoo\n«ˇfoo»");
3293
3294        // Update the editor's search settings, disabling case sensitivity, to
3295        // check that the value is respected.
3296        let mut search_settings = SearchSettings::default();
3297        search_settings.case_sensitive = false;
3298        update_search_settings(search_settings, cx);
3299        editor_cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
3300        editor_cx.update_editor(|e, window, cx| {
3301            e.select_next(&Default::default(), window, cx).unwrap();
3302        });
3303        editor_cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\nFoo\nfoo");
3304    }
3305
3306    fn update_search_settings(search_settings: SearchSettings, cx: &mut TestAppContext) {
3307        cx.update(|cx| {
3308            SettingsStore::update_global(cx, |store, cx| {
3309                store.update_user_settings(cx, |settings| {
3310                    settings.editor.search = Some(SearchSettingsContent {
3311                        button: Some(search_settings.button),
3312                        whole_word: Some(search_settings.whole_word),
3313                        case_sensitive: Some(search_settings.case_sensitive),
3314                        include_ignored: Some(search_settings.include_ignored),
3315                        regex: Some(search_settings.regex),
3316                        center_on_match: Some(search_settings.center_on_match),
3317                    });
3318                });
3319            });
3320        });
3321    }
3322}