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