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