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