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, 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, 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, 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(matches, window, cx);
1304                                }
1305                                let _ = done_tx.send(());
1306                            }
1307                            cx.notify();
1308                        }
1309                    })
1310                    .log_err();
1311                }));
1312            }
1313        }
1314        done_rx
1315    }
1316
1317    fn reverse_direction_if_backwards(&self, direction: Direction) -> Direction {
1318        if self.search_options.contains(SearchOptions::BACKWARDS) {
1319            direction.opposite()
1320        } else {
1321            direction
1322        }
1323    }
1324
1325    pub fn update_match_index(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1326        let direction = self.reverse_direction_if_backwards(Direction::Next);
1327        let new_index = self
1328            .active_searchable_item
1329            .as_ref()
1330            .and_then(|searchable_item| {
1331                let matches = self
1332                    .searchable_items_with_matches
1333                    .get(&searchable_item.downgrade())?;
1334                searchable_item.active_match_index(direction, matches, window, cx)
1335            });
1336        if new_index != self.active_match_index {
1337            self.active_match_index = new_index;
1338            cx.notify();
1339        }
1340    }
1341
1342    fn tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context<Self>) {
1343        self.cycle_field(Direction::Next, window, cx);
1344    }
1345
1346    fn backtab(&mut self, _: &Backtab, window: &mut Window, cx: &mut Context<Self>) {
1347        self.cycle_field(Direction::Prev, window, cx);
1348    }
1349    fn cycle_field(&mut self, direction: Direction, window: &mut Window, cx: &mut Context<Self>) {
1350        let mut handles = vec![self.query_editor.focus_handle(cx)];
1351        if self.replace_enabled {
1352            handles.push(self.replacement_editor.focus_handle(cx));
1353        }
1354        if let Some(item) = self.active_searchable_item.as_ref() {
1355            handles.push(item.item_focus_handle(cx));
1356        }
1357        let current_index = match handles.iter().position(|focus| focus.is_focused(window)) {
1358            Some(index) => index,
1359            None => return,
1360        };
1361
1362        let new_index = match direction {
1363            Direction::Next => (current_index + 1) % handles.len(),
1364            Direction::Prev if current_index == 0 => handles.len() - 1,
1365            Direction::Prev => (current_index - 1) % handles.len(),
1366        };
1367        let next_focus_handle = &handles[new_index];
1368        self.focus(next_focus_handle, window);
1369        cx.stop_propagation();
1370    }
1371
1372    fn next_history_query(
1373        &mut self,
1374        _: &NextHistoryQuery,
1375        window: &mut Window,
1376        cx: &mut Context<Self>,
1377    ) {
1378        if let Some(new_query) = self
1379            .search_history
1380            .next(&mut self.search_history_cursor)
1381            .map(str::to_string)
1382        {
1383            drop(self.search(&new_query, Some(self.search_options), false, window, cx));
1384        } else {
1385            self.search_history_cursor.reset();
1386            drop(self.search("", Some(self.search_options), false, window, cx));
1387        }
1388    }
1389
1390    fn previous_history_query(
1391        &mut self,
1392        _: &PreviousHistoryQuery,
1393        window: &mut Window,
1394        cx: &mut Context<Self>,
1395    ) {
1396        if self.query(cx).is_empty()
1397            && let Some(new_query) = self
1398                .search_history
1399                .current(&self.search_history_cursor)
1400                .map(str::to_string)
1401        {
1402            drop(self.search(&new_query, Some(self.search_options), false, window, cx));
1403            return;
1404        }
1405
1406        if let Some(new_query) = self
1407            .search_history
1408            .previous(&mut self.search_history_cursor)
1409            .map(str::to_string)
1410        {
1411            drop(self.search(&new_query, Some(self.search_options), false, window, cx));
1412        }
1413    }
1414
1415    fn focus(&self, handle: &gpui::FocusHandle, window: &mut Window) {
1416        window.invalidate_character_coordinates();
1417        window.focus(handle);
1418    }
1419
1420    fn toggle_replace(&mut self, _: &ToggleReplace, window: &mut Window, cx: &mut Context<Self>) {
1421        if self.active_searchable_item.is_some() {
1422            self.replace_enabled = !self.replace_enabled;
1423            let handle = if self.replace_enabled {
1424                self.replacement_editor.focus_handle(cx)
1425            } else {
1426                self.query_editor.focus_handle(cx)
1427            };
1428            self.focus(&handle, window);
1429            cx.notify();
1430        }
1431    }
1432
1433    fn replace_next(&mut self, _: &ReplaceNext, window: &mut Window, cx: &mut Context<Self>) {
1434        let mut should_propagate = true;
1435        if !self.dismissed
1436            && self.active_search.is_some()
1437            && let Some(searchable_item) = self.active_searchable_item.as_ref()
1438            && let Some(query) = self.active_search.as_ref()
1439            && let Some(matches) = self
1440                .searchable_items_with_matches
1441                .get(&searchable_item.downgrade())
1442        {
1443            if let Some(active_index) = self.active_match_index {
1444                let query = query
1445                    .as_ref()
1446                    .clone()
1447                    .with_replacement(self.replacement(cx));
1448                searchable_item.replace(matches.at(active_index), &query, window, cx);
1449                self.select_next_match(&SelectNextMatch, window, cx);
1450            }
1451            should_propagate = false;
1452        }
1453        if !should_propagate {
1454            cx.stop_propagation();
1455        }
1456    }
1457
1458    pub fn replace_all(&mut self, _: &ReplaceAll, window: &mut Window, cx: &mut Context<Self>) {
1459        if !self.dismissed
1460            && self.active_search.is_some()
1461            && let Some(searchable_item) = self.active_searchable_item.as_ref()
1462            && let Some(query) = self.active_search.as_ref()
1463            && let Some(matches) = self
1464                .searchable_items_with_matches
1465                .get(&searchable_item.downgrade())
1466        {
1467            let query = query
1468                .as_ref()
1469                .clone()
1470                .with_replacement(self.replacement(cx));
1471            searchable_item.replace_all(&mut matches.iter(), &query, window, cx);
1472        }
1473    }
1474
1475    pub fn match_exists(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
1476        self.update_match_index(window, cx);
1477        self.active_match_index.is_some()
1478    }
1479
1480    pub fn should_use_smartcase_search(&mut self, cx: &mut Context<Self>) -> bool {
1481        EditorSettings::get_global(cx).use_smartcase_search
1482    }
1483
1484    pub fn is_contains_uppercase(&mut self, str: &String) -> bool {
1485        str.chars().any(|c| c.is_uppercase())
1486    }
1487
1488    fn smartcase(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1489        if self.should_use_smartcase_search(cx) {
1490            let query = self.query(cx);
1491            if !query.is_empty() {
1492                let is_case = self.is_contains_uppercase(&query);
1493                if self.has_search_option(SearchOptions::CASE_SENSITIVE) != is_case {
1494                    self.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
1495                }
1496            }
1497        }
1498    }
1499
1500    fn adjust_query_regex_language(&self, cx: &mut App) {
1501        let enable = self.search_options.contains(SearchOptions::REGEX);
1502        let query_buffer = self
1503            .query_editor
1504            .read(cx)
1505            .buffer()
1506            .read(cx)
1507            .as_singleton()
1508            .expect("query editor should be backed by a singleton buffer");
1509
1510        if enable {
1511            if let Some(regex_language) = self.regex_language.clone() {
1512                query_buffer.update(cx, |query_buffer, cx| {
1513                    query_buffer.set_language_immediate(Some(regex_language), cx);
1514                })
1515            }
1516        } else {
1517            query_buffer.update(cx, |query_buffer, cx| {
1518                query_buffer.set_language_immediate(None, cx);
1519            })
1520        }
1521    }
1522
1523    /// Updates the searchable item's case sensitivity option to match the
1524    /// search bar's current case sensitivity setting. This ensures that
1525    /// editor's `select_next`/ `select_previous` operations respect the buffer
1526    /// search bar's search options.
1527    ///
1528    /// Clears the case sensitivity when the search bar is dismissed so that
1529    /// only the editor's settings are respected.
1530    fn sync_select_next_case_sensitivity(&self, cx: &mut Context<Self>) {
1531        let case_sensitive = match self.dismissed {
1532            true => None,
1533            false => Some(self.search_options.contains(SearchOptions::CASE_SENSITIVE)),
1534        };
1535
1536        if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
1537            active_searchable_item.set_search_is_case_sensitive(case_sensitive, cx);
1538        }
1539    }
1540}
1541
1542#[cfg(test)]
1543mod tests {
1544    use std::ops::Range;
1545
1546    use super::*;
1547    use editor::{
1548        DisplayPoint, Editor, MultiBuffer, SearchSettings, SelectionEffects,
1549        display_map::DisplayRow, test::editor_test_context::EditorTestContext,
1550    };
1551    use gpui::{Hsla, TestAppContext, UpdateGlobal, VisualTestContext};
1552    use language::{Buffer, Point};
1553    use settings::{SearchSettingsContent, SettingsStore};
1554    use smol::stream::StreamExt as _;
1555    use unindent::Unindent as _;
1556    use util_macros::perf;
1557
1558    fn init_globals(cx: &mut TestAppContext) {
1559        cx.update(|cx| {
1560            let store = settings::SettingsStore::test(cx);
1561            cx.set_global(store);
1562            editor::init(cx);
1563
1564            theme::init(theme::LoadThemes::JustBase, cx);
1565            crate::init(cx);
1566        });
1567    }
1568
1569    fn init_test(
1570        cx: &mut TestAppContext,
1571    ) -> (
1572        Entity<Editor>,
1573        Entity<BufferSearchBar>,
1574        &mut VisualTestContext,
1575    ) {
1576        init_globals(cx);
1577        let buffer = cx.new(|cx| {
1578            Buffer::local(
1579                r#"
1580                A regular expression (shortened as regex or regexp;[1] also referred to as
1581                rational expression[2][3]) is a sequence of characters that specifies a search
1582                pattern in text. Usually such patterns are used by string-searching algorithms
1583                for "find" or "find and replace" operations on strings, or for input validation.
1584                "#
1585                .unindent(),
1586                cx,
1587            )
1588        });
1589        let mut editor = None;
1590        let window = cx.add_window(|window, cx| {
1591            let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
1592                "keymaps/default-macos.json",
1593                cx,
1594            )
1595            .unwrap();
1596            cx.bind_keys(default_key_bindings);
1597            editor = Some(cx.new(|cx| Editor::for_buffer(buffer.clone(), None, window, cx)));
1598            let mut search_bar = BufferSearchBar::new(None, window, cx);
1599            search_bar.set_active_pane_item(Some(&editor.clone().unwrap()), window, cx);
1600            search_bar.show(window, cx);
1601            search_bar
1602        });
1603        let search_bar = window.root(cx).unwrap();
1604
1605        let cx = VisualTestContext::from_window(*window, cx).into_mut();
1606
1607        (editor.unwrap(), search_bar, cx)
1608    }
1609
1610    #[perf]
1611    #[gpui::test]
1612    async fn test_search_simple(cx: &mut TestAppContext) {
1613        let (editor, search_bar, cx) = init_test(cx);
1614        let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
1615            background_highlights
1616                .into_iter()
1617                .map(|(range, _)| range)
1618                .collect::<Vec<_>>()
1619        };
1620        // Search for a string that appears with different casing.
1621        // By default, search is case-insensitive.
1622        search_bar
1623            .update_in(cx, |search_bar, window, cx| {
1624                search_bar.search("us", None, true, window, cx)
1625            })
1626            .await
1627            .unwrap();
1628        editor.update_in(cx, |editor, window, cx| {
1629            assert_eq!(
1630                display_points_of(editor.all_text_background_highlights(window, cx)),
1631                &[
1632                    DisplayPoint::new(DisplayRow(2), 17)..DisplayPoint::new(DisplayRow(2), 19),
1633                    DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),
1634                ]
1635            );
1636        });
1637
1638        // Switch to a case sensitive search.
1639        search_bar.update_in(cx, |search_bar, window, cx| {
1640            search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
1641        });
1642        let mut editor_notifications = cx.notifications(&editor);
1643        editor_notifications.next().await;
1644        editor.update_in(cx, |editor, window, cx| {
1645            assert_eq!(
1646                display_points_of(editor.all_text_background_highlights(window, cx)),
1647                &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
1648            );
1649        });
1650
1651        // Search for a string that appears both as a whole word and
1652        // within other words. By default, all results are found.
1653        search_bar
1654            .update_in(cx, |search_bar, window, cx| {
1655                search_bar.search("or", None, true, window, cx)
1656            })
1657            .await
1658            .unwrap();
1659        editor.update_in(cx, |editor, window, cx| {
1660            assert_eq!(
1661                display_points_of(editor.all_text_background_highlights(window, cx)),
1662                &[
1663                    DisplayPoint::new(DisplayRow(0), 24)..DisplayPoint::new(DisplayRow(0), 26),
1664                    DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1665                    DisplayPoint::new(DisplayRow(2), 71)..DisplayPoint::new(DisplayRow(2), 73),
1666                    DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 3),
1667                    DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
1668                    DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
1669                    DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 62),
1670                ]
1671            );
1672        });
1673
1674        // Switch to a whole word search.
1675        search_bar.update_in(cx, |search_bar, window, cx| {
1676            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
1677        });
1678        let mut editor_notifications = cx.notifications(&editor);
1679        editor_notifications.next().await;
1680        editor.update_in(cx, |editor, window, cx| {
1681            assert_eq!(
1682                display_points_of(editor.all_text_background_highlights(window, cx)),
1683                &[
1684                    DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1685                    DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
1686                    DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
1687                ]
1688            );
1689        });
1690
1691        editor.update_in(cx, |editor, window, cx| {
1692            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1693                s.select_display_ranges([
1694                    DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
1695                ])
1696            });
1697        });
1698        search_bar.update_in(cx, |search_bar, window, cx| {
1699            assert_eq!(search_bar.active_match_index, Some(0));
1700            search_bar.select_next_match(&SelectNextMatch, window, cx);
1701            assert_eq!(
1702                editor.update(cx, |editor, cx| editor
1703                    .selections
1704                    .display_ranges(&editor.display_snapshot(cx))),
1705                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1706            );
1707        });
1708        search_bar.read_with(cx, |search_bar, _| {
1709            assert_eq!(search_bar.active_match_index, Some(0));
1710        });
1711
1712        search_bar.update_in(cx, |search_bar, window, cx| {
1713            search_bar.select_next_match(&SelectNextMatch, window, cx);
1714            assert_eq!(
1715                editor.update(cx, |editor, cx| editor
1716                    .selections
1717                    .display_ranges(&editor.display_snapshot(cx))),
1718                [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1719            );
1720        });
1721        search_bar.read_with(cx, |search_bar, _| {
1722            assert_eq!(search_bar.active_match_index, Some(1));
1723        });
1724
1725        search_bar.update_in(cx, |search_bar, window, cx| {
1726            search_bar.select_next_match(&SelectNextMatch, window, cx);
1727            assert_eq!(
1728                editor.update(cx, |editor, cx| editor
1729                    .selections
1730                    .display_ranges(&editor.display_snapshot(cx))),
1731                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1732            );
1733        });
1734        search_bar.read_with(cx, |search_bar, _| {
1735            assert_eq!(search_bar.active_match_index, Some(2));
1736        });
1737
1738        search_bar.update_in(cx, |search_bar, window, cx| {
1739            search_bar.select_next_match(&SelectNextMatch, window, cx);
1740            assert_eq!(
1741                editor.update(cx, |editor, cx| editor
1742                    .selections
1743                    .display_ranges(&editor.display_snapshot(cx))),
1744                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1745            );
1746        });
1747        search_bar.read_with(cx, |search_bar, _| {
1748            assert_eq!(search_bar.active_match_index, Some(0));
1749        });
1750
1751        search_bar.update_in(cx, |search_bar, window, cx| {
1752            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1753            assert_eq!(
1754                editor.update(cx, |editor, cx| editor
1755                    .selections
1756                    .display_ranges(&editor.display_snapshot(cx))),
1757                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1758            );
1759        });
1760        search_bar.read_with(cx, |search_bar, _| {
1761            assert_eq!(search_bar.active_match_index, Some(2));
1762        });
1763
1764        search_bar.update_in(cx, |search_bar, window, cx| {
1765            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1766            assert_eq!(
1767                editor.update(cx, |editor, cx| editor
1768                    .selections
1769                    .display_ranges(&editor.display_snapshot(cx))),
1770                [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1771            );
1772        });
1773        search_bar.read_with(cx, |search_bar, _| {
1774            assert_eq!(search_bar.active_match_index, Some(1));
1775        });
1776
1777        search_bar.update_in(cx, |search_bar, window, cx| {
1778            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1779            assert_eq!(
1780                editor.update(cx, |editor, cx| editor
1781                    .selections
1782                    .display_ranges(&editor.display_snapshot(cx))),
1783                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1784            );
1785        });
1786        search_bar.read_with(cx, |search_bar, _| {
1787            assert_eq!(search_bar.active_match_index, Some(0));
1788        });
1789
1790        // Park the cursor in between matches and ensure that going to the previous match selects
1791        // the closest match to the left.
1792        editor.update_in(cx, |editor, window, cx| {
1793            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1794                s.select_display_ranges([
1795                    DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
1796                ])
1797            });
1798        });
1799        search_bar.update_in(cx, |search_bar, window, cx| {
1800            assert_eq!(search_bar.active_match_index, Some(1));
1801            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1802            assert_eq!(
1803                editor.update(cx, |editor, cx| editor
1804                    .selections
1805                    .display_ranges(&editor.display_snapshot(cx))),
1806                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1807            );
1808        });
1809        search_bar.read_with(cx, |search_bar, _| {
1810            assert_eq!(search_bar.active_match_index, Some(0));
1811        });
1812
1813        // Park the cursor in between matches and ensure that going to the next match selects the
1814        // closest match to the right.
1815        editor.update_in(cx, |editor, window, cx| {
1816            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1817                s.select_display_ranges([
1818                    DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
1819                ])
1820            });
1821        });
1822        search_bar.update_in(cx, |search_bar, window, cx| {
1823            assert_eq!(search_bar.active_match_index, Some(1));
1824            search_bar.select_next_match(&SelectNextMatch, window, cx);
1825            assert_eq!(
1826                editor.update(cx, |editor, cx| editor
1827                    .selections
1828                    .display_ranges(&editor.display_snapshot(cx))),
1829                [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1830            );
1831        });
1832        search_bar.read_with(cx, |search_bar, _| {
1833            assert_eq!(search_bar.active_match_index, Some(1));
1834        });
1835
1836        // Park the cursor after the last match and ensure that going to the previous match selects
1837        // the last match.
1838        editor.update_in(cx, |editor, window, cx| {
1839            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1840                s.select_display_ranges([
1841                    DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
1842                ])
1843            });
1844        });
1845        search_bar.update_in(cx, |search_bar, window, cx| {
1846            assert_eq!(search_bar.active_match_index, Some(2));
1847            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1848            assert_eq!(
1849                editor.update(cx, |editor, cx| editor
1850                    .selections
1851                    .display_ranges(&editor.display_snapshot(cx))),
1852                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1853            );
1854        });
1855        search_bar.read_with(cx, |search_bar, _| {
1856            assert_eq!(search_bar.active_match_index, Some(2));
1857        });
1858
1859        // Park the cursor after the last match and ensure that going to the next match selects the
1860        // first match.
1861        editor.update_in(cx, |editor, window, cx| {
1862            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1863                s.select_display_ranges([
1864                    DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
1865                ])
1866            });
1867        });
1868        search_bar.update_in(cx, |search_bar, window, cx| {
1869            assert_eq!(search_bar.active_match_index, Some(2));
1870            search_bar.select_next_match(&SelectNextMatch, window, cx);
1871            assert_eq!(
1872                editor.update(cx, |editor, cx| editor
1873                    .selections
1874                    .display_ranges(&editor.display_snapshot(cx))),
1875                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1876            );
1877        });
1878        search_bar.read_with(cx, |search_bar, _| {
1879            assert_eq!(search_bar.active_match_index, Some(0));
1880        });
1881
1882        // Park the cursor before the first match and ensure that going to the previous match
1883        // selects the last match.
1884        editor.update_in(cx, |editor, window, cx| {
1885            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1886                s.select_display_ranges([
1887                    DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
1888                ])
1889            });
1890        });
1891        search_bar.update_in(cx, |search_bar, window, cx| {
1892            assert_eq!(search_bar.active_match_index, Some(0));
1893            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1894            assert_eq!(
1895                editor.update(cx, |editor, cx| editor
1896                    .selections
1897                    .display_ranges(&editor.display_snapshot(cx))),
1898                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1899            );
1900        });
1901        search_bar.read_with(cx, |search_bar, _| {
1902            assert_eq!(search_bar.active_match_index, Some(2));
1903        });
1904    }
1905
1906    fn display_points_of(
1907        background_highlights: Vec<(Range<DisplayPoint>, Hsla)>,
1908    ) -> Vec<Range<DisplayPoint>> {
1909        background_highlights
1910            .into_iter()
1911            .map(|(range, _)| range)
1912            .collect::<Vec<_>>()
1913    }
1914
1915    #[perf]
1916    #[gpui::test]
1917    async fn test_search_option_handling(cx: &mut TestAppContext) {
1918        let (editor, search_bar, cx) = init_test(cx);
1919
1920        // show with options should make current search case sensitive
1921        search_bar
1922            .update_in(cx, |search_bar, window, cx| {
1923                search_bar.show(window, cx);
1924                search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), true, window, cx)
1925            })
1926            .await
1927            .unwrap();
1928        editor.update_in(cx, |editor, window, cx| {
1929            assert_eq!(
1930                display_points_of(editor.all_text_background_highlights(window, cx)),
1931                &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
1932            );
1933        });
1934
1935        // search_suggested should restore default options
1936        search_bar.update_in(cx, |search_bar, window, cx| {
1937            search_bar.search_suggested(window, cx);
1938            assert_eq!(search_bar.search_options, SearchOptions::NONE)
1939        });
1940
1941        // toggling a search option should update the defaults
1942        search_bar
1943            .update_in(cx, |search_bar, window, cx| {
1944                search_bar.search(
1945                    "regex",
1946                    Some(SearchOptions::CASE_SENSITIVE),
1947                    true,
1948                    window,
1949                    cx,
1950                )
1951            })
1952            .await
1953            .unwrap();
1954        search_bar.update_in(cx, |search_bar, window, cx| {
1955            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx)
1956        });
1957        let mut editor_notifications = cx.notifications(&editor);
1958        editor_notifications.next().await;
1959        editor.update_in(cx, |editor, window, cx| {
1960            assert_eq!(
1961                display_points_of(editor.all_text_background_highlights(window, cx)),
1962                &[DisplayPoint::new(DisplayRow(0), 35)..DisplayPoint::new(DisplayRow(0), 40),]
1963            );
1964        });
1965
1966        // defaults should still include whole word
1967        search_bar.update_in(cx, |search_bar, window, cx| {
1968            search_bar.search_suggested(window, cx);
1969            assert_eq!(
1970                search_bar.search_options,
1971                SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD
1972            )
1973        });
1974    }
1975
1976    #[perf]
1977    #[gpui::test]
1978    async fn test_search_select_all_matches(cx: &mut TestAppContext) {
1979        init_globals(cx);
1980        let buffer_text = r#"
1981        A regular expression (shortened as regex or regexp;[1] also referred to as
1982        rational expression[2][3]) is a sequence of characters that specifies a search
1983        pattern in text. Usually such patterns are used by string-searching algorithms
1984        for "find" or "find and replace" operations on strings, or for input validation.
1985        "#
1986        .unindent();
1987        let expected_query_matches_count = buffer_text
1988            .chars()
1989            .filter(|c| c.eq_ignore_ascii_case(&'a'))
1990            .count();
1991        assert!(
1992            expected_query_matches_count > 1,
1993            "Should pick a query with multiple results"
1994        );
1995        let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
1996        let window = cx.add_window(|_, _| gpui::Empty);
1997
1998        let editor = window.build_entity(cx, |window, cx| {
1999            Editor::for_buffer(buffer.clone(), None, window, cx)
2000        });
2001
2002        let search_bar = window.build_entity(cx, |window, cx| {
2003            let mut search_bar = BufferSearchBar::new(None, window, cx);
2004            search_bar.set_active_pane_item(Some(&editor), window, cx);
2005            search_bar.show(window, cx);
2006            search_bar
2007        });
2008
2009        window
2010            .update(cx, |_, window, cx| {
2011                search_bar.update(cx, |search_bar, cx| {
2012                    search_bar.search("a", None, true, window, cx)
2013                })
2014            })
2015            .unwrap()
2016            .await
2017            .unwrap();
2018        let initial_selections = window
2019            .update(cx, |_, window, cx| {
2020                search_bar.update(cx, |search_bar, cx| {
2021                    let handle = search_bar.query_editor.focus_handle(cx);
2022                    window.focus(&handle);
2023                    search_bar.activate_current_match(window, cx);
2024                });
2025                assert!(
2026                    !editor.read(cx).is_focused(window),
2027                    "Initially, the editor should not be focused"
2028                );
2029                let initial_selections = editor.update(cx, |editor, cx| {
2030                    let initial_selections = editor.selections.display_ranges(&editor.display_snapshot(cx));
2031                    assert_eq!(
2032                        initial_selections.len(), 1,
2033                        "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
2034                    );
2035                    initial_selections
2036                });
2037                search_bar.update(cx, |search_bar, cx| {
2038                    assert_eq!(search_bar.active_match_index, Some(0));
2039                    let handle = search_bar.query_editor.focus_handle(cx);
2040                    window.focus(&handle);
2041                    search_bar.select_all_matches(&SelectAllMatches, window, cx);
2042                });
2043                assert!(
2044                    editor.read(cx).is_focused(window),
2045                    "Should focus editor after successful SelectAllMatches"
2046                );
2047                search_bar.update(cx, |search_bar, cx| {
2048                    let all_selections =
2049                        editor.update(cx, |editor, cx| editor.selections.display_ranges(&editor.display_snapshot(cx)));
2050                    assert_eq!(
2051                        all_selections.len(),
2052                        expected_query_matches_count,
2053                        "Should select all `a` characters in the buffer, but got: {all_selections:?}"
2054                    );
2055                    assert_eq!(
2056                        search_bar.active_match_index,
2057                        Some(0),
2058                        "Match index should not change after selecting all matches"
2059                    );
2060                });
2061
2062                search_bar.update(cx, |this, cx| this.select_next_match(&SelectNextMatch, window, cx));
2063                initial_selections
2064            }).unwrap();
2065
2066        window
2067            .update(cx, |_, window, cx| {
2068                assert!(
2069                    editor.read(cx).is_focused(window),
2070                    "Should still have editor focused after SelectNextMatch"
2071                );
2072                search_bar.update(cx, |search_bar, cx| {
2073                    let all_selections = editor.update(cx, |editor, cx| {
2074                        editor
2075                            .selections
2076                            .display_ranges(&editor.display_snapshot(cx))
2077                    });
2078                    assert_eq!(
2079                        all_selections.len(),
2080                        1,
2081                        "On next match, should deselect items and select the next match"
2082                    );
2083                    assert_ne!(
2084                        all_selections, initial_selections,
2085                        "Next match should be different from the first selection"
2086                    );
2087                    assert_eq!(
2088                        search_bar.active_match_index,
2089                        Some(1),
2090                        "Match index should be updated to the next one"
2091                    );
2092                    let handle = search_bar.query_editor.focus_handle(cx);
2093                    window.focus(&handle);
2094                    search_bar.select_all_matches(&SelectAllMatches, window, cx);
2095                });
2096            })
2097            .unwrap();
2098        window
2099            .update(cx, |_, window, cx| {
2100                assert!(
2101                    editor.read(cx).is_focused(window),
2102                    "Should focus editor after successful SelectAllMatches"
2103                );
2104                search_bar.update(cx, |search_bar, cx| {
2105                    let all_selections =
2106                        editor.update(cx, |editor, cx| editor.selections.display_ranges(&editor.display_snapshot(cx)));
2107                    assert_eq!(
2108                    all_selections.len(),
2109                    expected_query_matches_count,
2110                    "Should select all `a` characters in the buffer, but got: {all_selections:?}"
2111                );
2112                    assert_eq!(
2113                        search_bar.active_match_index,
2114                        Some(1),
2115                        "Match index should not change after selecting all matches"
2116                    );
2117                });
2118                search_bar.update(cx, |search_bar, cx| {
2119                    search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2120                });
2121            })
2122            .unwrap();
2123        let last_match_selections = window
2124            .update(cx, |_, window, cx| {
2125                assert!(
2126                    editor.read(cx).is_focused(window),
2127                    "Should still have editor focused after SelectPreviousMatch"
2128                );
2129
2130                search_bar.update(cx, |search_bar, cx| {
2131                    let all_selections = editor.update(cx, |editor, cx| {
2132                        editor
2133                            .selections
2134                            .display_ranges(&editor.display_snapshot(cx))
2135                    });
2136                    assert_eq!(
2137                        all_selections.len(),
2138                        1,
2139                        "On previous match, should deselect items and select the previous item"
2140                    );
2141                    assert_eq!(
2142                        all_selections, initial_selections,
2143                        "Previous match should be the same as the first selection"
2144                    );
2145                    assert_eq!(
2146                        search_bar.active_match_index,
2147                        Some(0),
2148                        "Match index should be updated to the previous one"
2149                    );
2150                    all_selections
2151                })
2152            })
2153            .unwrap();
2154
2155        window
2156            .update(cx, |_, window, cx| {
2157                search_bar.update(cx, |search_bar, cx| {
2158                    let handle = search_bar.query_editor.focus_handle(cx);
2159                    window.focus(&handle);
2160                    search_bar.search("abas_nonexistent_match", None, true, window, cx)
2161                })
2162            })
2163            .unwrap()
2164            .await
2165            .unwrap();
2166        window
2167            .update(cx, |_, window, cx| {
2168                search_bar.update(cx, |search_bar, cx| {
2169                    search_bar.select_all_matches(&SelectAllMatches, window, cx);
2170                });
2171                assert!(
2172                    editor.update(cx, |this, _cx| !this.is_focused(window)),
2173                    "Should not switch focus to editor if SelectAllMatches does not find any matches"
2174                );
2175                search_bar.update(cx, |search_bar, cx| {
2176                    let all_selections =
2177                        editor.update(cx, |editor, cx| editor.selections.display_ranges(&editor.display_snapshot(cx)));
2178                    assert_eq!(
2179                        all_selections, last_match_selections,
2180                        "Should not select anything new if there are no matches"
2181                    );
2182                    assert!(
2183                        search_bar.active_match_index.is_none(),
2184                        "For no matches, there should be no active match index"
2185                    );
2186                });
2187            })
2188            .unwrap();
2189    }
2190
2191    #[perf]
2192    #[gpui::test]
2193    async fn test_search_query_with_match_whole_word(cx: &mut TestAppContext) {
2194        init_globals(cx);
2195        let buffer_text = r#"
2196        self.buffer.update(cx, |buffer, cx| {
2197            buffer.edit(
2198                edits,
2199                Some(AutoindentMode::Block {
2200                    original_indent_columns,
2201                }),
2202                cx,
2203            )
2204        });
2205
2206        this.buffer.update(cx, |buffer, cx| {
2207            buffer.edit([(end_of_line..start_of_next_line, replace)], None, cx)
2208        });
2209        "#
2210        .unindent();
2211        let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
2212        let cx = cx.add_empty_window();
2213
2214        let editor =
2215            cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
2216
2217        let search_bar = cx.new_window_entity(|window, cx| {
2218            let mut search_bar = BufferSearchBar::new(None, window, cx);
2219            search_bar.set_active_pane_item(Some(&editor), window, cx);
2220            search_bar.show(window, cx);
2221            search_bar
2222        });
2223
2224        search_bar
2225            .update_in(cx, |search_bar, window, cx| {
2226                search_bar.search(
2227                    "edit\\(",
2228                    Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
2229                    true,
2230                    window,
2231                    cx,
2232                )
2233            })
2234            .await
2235            .unwrap();
2236
2237        search_bar.update_in(cx, |search_bar, window, cx| {
2238            search_bar.select_all_matches(&SelectAllMatches, window, cx);
2239        });
2240        search_bar.update(cx, |_, cx| {
2241            let all_selections = editor.update(cx, |editor, cx| {
2242                editor
2243                    .selections
2244                    .display_ranges(&editor.display_snapshot(cx))
2245            });
2246            assert_eq!(
2247                all_selections.len(),
2248                2,
2249                "Should select all `edit(` in the buffer, but got: {all_selections:?}"
2250            );
2251        });
2252
2253        search_bar
2254            .update_in(cx, |search_bar, window, cx| {
2255                search_bar.search(
2256                    "edit(",
2257                    Some(SearchOptions::WHOLE_WORD | SearchOptions::CASE_SENSITIVE),
2258                    true,
2259                    window,
2260                    cx,
2261                )
2262            })
2263            .await
2264            .unwrap();
2265
2266        search_bar.update_in(cx, |search_bar, window, cx| {
2267            search_bar.select_all_matches(&SelectAllMatches, window, cx);
2268        });
2269        search_bar.update(cx, |_, cx| {
2270            let all_selections = editor.update(cx, |editor, cx| {
2271                editor
2272                    .selections
2273                    .display_ranges(&editor.display_snapshot(cx))
2274            });
2275            assert_eq!(
2276                all_selections.len(),
2277                2,
2278                "Should select all `edit(` in the buffer, but got: {all_selections:?}"
2279            );
2280        });
2281    }
2282
2283    #[perf]
2284    #[gpui::test]
2285    async fn test_search_query_history(cx: &mut TestAppContext) {
2286        let (_editor, search_bar, cx) = init_test(cx);
2287
2288        // Add 3 search items into the history.
2289        search_bar
2290            .update_in(cx, |search_bar, window, cx| {
2291                search_bar.search("a", None, true, window, cx)
2292            })
2293            .await
2294            .unwrap();
2295        search_bar
2296            .update_in(cx, |search_bar, window, cx| {
2297                search_bar.search("b", None, true, window, cx)
2298            })
2299            .await
2300            .unwrap();
2301        search_bar
2302            .update_in(cx, |search_bar, window, cx| {
2303                search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), true, window, cx)
2304            })
2305            .await
2306            .unwrap();
2307        // Ensure that the latest search is active.
2308        search_bar.update(cx, |search_bar, cx| {
2309            assert_eq!(search_bar.query(cx), "c");
2310            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2311        });
2312
2313        // Next history query after the latest should set the query to the empty string.
2314        search_bar.update_in(cx, |search_bar, window, cx| {
2315            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2316        });
2317        cx.background_executor.run_until_parked();
2318        search_bar.update(cx, |search_bar, cx| {
2319            assert_eq!(search_bar.query(cx), "");
2320            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2321        });
2322        search_bar.update_in(cx, |search_bar, window, cx| {
2323            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2324        });
2325        cx.background_executor.run_until_parked();
2326        search_bar.update(cx, |search_bar, cx| {
2327            assert_eq!(search_bar.query(cx), "");
2328            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2329        });
2330
2331        // First previous query for empty current query should set the query to the latest.
2332        search_bar.update_in(cx, |search_bar, window, cx| {
2333            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2334        });
2335        cx.background_executor.run_until_parked();
2336        search_bar.update(cx, |search_bar, cx| {
2337            assert_eq!(search_bar.query(cx), "c");
2338            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2339        });
2340
2341        // Further previous items should go over the history in reverse order.
2342        search_bar.update_in(cx, |search_bar, window, cx| {
2343            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2344        });
2345        cx.background_executor.run_until_parked();
2346        search_bar.update(cx, |search_bar, cx| {
2347            assert_eq!(search_bar.query(cx), "b");
2348            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2349        });
2350
2351        // Previous items should never go behind the first history item.
2352        search_bar.update_in(cx, |search_bar, window, cx| {
2353            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2354        });
2355        cx.background_executor.run_until_parked();
2356        search_bar.update(cx, |search_bar, cx| {
2357            assert_eq!(search_bar.query(cx), "a");
2358            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2359        });
2360        search_bar.update_in(cx, |search_bar, window, cx| {
2361            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2362        });
2363        cx.background_executor.run_until_parked();
2364        search_bar.update(cx, |search_bar, cx| {
2365            assert_eq!(search_bar.query(cx), "a");
2366            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2367        });
2368
2369        // Next items should go over the history in the original order.
2370        search_bar.update_in(cx, |search_bar, window, cx| {
2371            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2372        });
2373        cx.background_executor.run_until_parked();
2374        search_bar.update(cx, |search_bar, cx| {
2375            assert_eq!(search_bar.query(cx), "b");
2376            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2377        });
2378
2379        search_bar
2380            .update_in(cx, |search_bar, window, cx| {
2381                search_bar.search("ba", None, true, window, cx)
2382            })
2383            .await
2384            .unwrap();
2385        search_bar.update(cx, |search_bar, cx| {
2386            assert_eq!(search_bar.query(cx), "ba");
2387            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2388        });
2389
2390        // New search input should add another entry to history and move the selection to the end of the history.
2391        search_bar.update_in(cx, |search_bar, window, cx| {
2392            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2393        });
2394        cx.background_executor.run_until_parked();
2395        search_bar.update(cx, |search_bar, cx| {
2396            assert_eq!(search_bar.query(cx), "c");
2397            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2398        });
2399        search_bar.update_in(cx, |search_bar, window, cx| {
2400            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2401        });
2402        cx.background_executor.run_until_parked();
2403        search_bar.update(cx, |search_bar, cx| {
2404            assert_eq!(search_bar.query(cx), "b");
2405            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2406        });
2407        search_bar.update_in(cx, |search_bar, window, cx| {
2408            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2409        });
2410        cx.background_executor.run_until_parked();
2411        search_bar.update(cx, |search_bar, cx| {
2412            assert_eq!(search_bar.query(cx), "c");
2413            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2414        });
2415        search_bar.update_in(cx, |search_bar, window, cx| {
2416            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2417        });
2418        cx.background_executor.run_until_parked();
2419        search_bar.update(cx, |search_bar, cx| {
2420            assert_eq!(search_bar.query(cx), "ba");
2421            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2422        });
2423        search_bar.update_in(cx, |search_bar, window, cx| {
2424            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2425        });
2426        cx.background_executor.run_until_parked();
2427        search_bar.update(cx, |search_bar, cx| {
2428            assert_eq!(search_bar.query(cx), "");
2429            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2430        });
2431    }
2432
2433    #[perf]
2434    #[gpui::test]
2435    async fn test_replace_simple(cx: &mut TestAppContext) {
2436        let (editor, search_bar, cx) = init_test(cx);
2437
2438        search_bar
2439            .update_in(cx, |search_bar, window, cx| {
2440                search_bar.search("expression", None, true, window, cx)
2441            })
2442            .await
2443            .unwrap();
2444
2445        search_bar.update_in(cx, |search_bar, window, cx| {
2446            search_bar.replacement_editor.update(cx, |editor, cx| {
2447                // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally.
2448                editor.set_text("expr$1", window, cx);
2449            });
2450            search_bar.replace_all(&ReplaceAll, window, cx)
2451        });
2452        assert_eq!(
2453            editor.read_with(cx, |this, cx| { this.text(cx) }),
2454            r#"
2455        A regular expr$1 (shortened as regex or regexp;[1] also referred to as
2456        rational expr$1[2][3]) is a sequence of characters that specifies a search
2457        pattern in text. Usually such patterns are used by string-searching algorithms
2458        for "find" or "find and replace" operations on strings, or for input validation.
2459        "#
2460            .unindent()
2461        );
2462
2463        // Search for word boundaries and replace just a single one.
2464        search_bar
2465            .update_in(cx, |search_bar, window, cx| {
2466                search_bar.search("or", Some(SearchOptions::WHOLE_WORD), true, window, cx)
2467            })
2468            .await
2469            .unwrap();
2470
2471        search_bar.update_in(cx, |search_bar, window, cx| {
2472            search_bar.replacement_editor.update(cx, |editor, cx| {
2473                editor.set_text("banana", window, cx);
2474            });
2475            search_bar.replace_next(&ReplaceNext, window, cx)
2476        });
2477        // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text.
2478        assert_eq!(
2479            editor.read_with(cx, |this, cx| { this.text(cx) }),
2480            r#"
2481        A regular expr$1 (shortened as regex banana regexp;[1] also referred to as
2482        rational expr$1[2][3]) is a sequence of characters that specifies a search
2483        pattern in text. Usually such patterns are used by string-searching algorithms
2484        for "find" or "find and replace" operations on strings, or for input validation.
2485        "#
2486            .unindent()
2487        );
2488        // Let's turn on regex mode.
2489        search_bar
2490            .update_in(cx, |search_bar, window, cx| {
2491                search_bar.search(
2492                    "\\[([^\\]]+)\\]",
2493                    Some(SearchOptions::REGEX),
2494                    true,
2495                    window,
2496                    cx,
2497                )
2498            })
2499            .await
2500            .unwrap();
2501        search_bar.update_in(cx, |search_bar, window, cx| {
2502            search_bar.replacement_editor.update(cx, |editor, cx| {
2503                editor.set_text("${1}number", window, cx);
2504            });
2505            search_bar.replace_all(&ReplaceAll, window, cx)
2506        });
2507        assert_eq!(
2508            editor.read_with(cx, |this, cx| { this.text(cx) }),
2509            r#"
2510        A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2511        rational expr$12number3number) is a sequence of characters that specifies a search
2512        pattern in text. Usually such patterns are used by string-searching algorithms
2513        for "find" or "find and replace" operations on strings, or for input validation.
2514        "#
2515            .unindent()
2516        );
2517        // Now with a whole-word twist.
2518        search_bar
2519            .update_in(cx, |search_bar, window, cx| {
2520                search_bar.search(
2521                    "a\\w+s",
2522                    Some(SearchOptions::REGEX | SearchOptions::WHOLE_WORD),
2523                    true,
2524                    window,
2525                    cx,
2526                )
2527            })
2528            .await
2529            .unwrap();
2530        search_bar.update_in(cx, |search_bar, window, cx| {
2531            search_bar.replacement_editor.update(cx, |editor, cx| {
2532                editor.set_text("things", window, cx);
2533            });
2534            search_bar.replace_all(&ReplaceAll, window, cx)
2535        });
2536        // The only word affected by this edit should be `algorithms`, even though there's a bunch
2537        // of words in this text that would match this regex if not for WHOLE_WORD.
2538        assert_eq!(
2539            editor.read_with(cx, |this, cx| { this.text(cx) }),
2540            r#"
2541        A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2542        rational expr$12number3number) is a sequence of characters that specifies a search
2543        pattern in text. Usually such patterns are used by string-searching things
2544        for "find" or "find and replace" operations on strings, or for input validation.
2545        "#
2546            .unindent()
2547        );
2548    }
2549
2550    #[gpui::test]
2551    async fn test_replace_focus(cx: &mut TestAppContext) {
2552        let (editor, search_bar, cx) = init_test(cx);
2553
2554        editor.update_in(cx, |editor, window, cx| {
2555            editor.set_text("What a bad day!", window, cx)
2556        });
2557
2558        search_bar
2559            .update_in(cx, |search_bar, window, cx| {
2560                search_bar.search("bad", None, true, window, cx)
2561            })
2562            .await
2563            .unwrap();
2564
2565        // Calling `toggle_replace` in the search bar ensures that the "Replace
2566        // *" buttons are rendered, so we can then simulate clicking the
2567        // buttons.
2568        search_bar.update_in(cx, |search_bar, window, cx| {
2569            search_bar.toggle_replace(&ToggleReplace, window, cx)
2570        });
2571
2572        search_bar.update_in(cx, |search_bar, window, cx| {
2573            search_bar.replacement_editor.update(cx, |editor, cx| {
2574                editor.set_text("great", window, cx);
2575            });
2576        });
2577
2578        // Focus on the editor instead of the search bar, as we want to ensure
2579        // that pressing the "Replace Next Match" button will work, even if the
2580        // search bar is not focused.
2581        cx.focus(&editor);
2582
2583        // We'll not simulate clicking the "Replace Next Match " button, asserting that
2584        // the replacement was done.
2585        let button_bounds = cx
2586            .debug_bounds("ICON-ReplaceNext")
2587            .expect("'Replace Next Match' button should be visible");
2588        cx.simulate_click(button_bounds.center(), gpui::Modifiers::none());
2589
2590        assert_eq!(
2591            editor.read_with(cx, |editor, cx| editor.text(cx)),
2592            "What a great day!"
2593        );
2594    }
2595
2596    struct ReplacementTestParams<'a> {
2597        editor: &'a Entity<Editor>,
2598        search_bar: &'a Entity<BufferSearchBar>,
2599        cx: &'a mut VisualTestContext,
2600        search_text: &'static str,
2601        search_options: Option<SearchOptions>,
2602        replacement_text: &'static str,
2603        replace_all: bool,
2604        expected_text: String,
2605    }
2606
2607    async fn run_replacement_test(options: ReplacementTestParams<'_>) {
2608        options
2609            .search_bar
2610            .update_in(options.cx, |search_bar, window, cx| {
2611                if let Some(options) = options.search_options {
2612                    search_bar.set_search_options(options, cx);
2613                }
2614                search_bar.search(
2615                    options.search_text,
2616                    options.search_options,
2617                    true,
2618                    window,
2619                    cx,
2620                )
2621            })
2622            .await
2623            .unwrap();
2624
2625        options
2626            .search_bar
2627            .update_in(options.cx, |search_bar, window, cx| {
2628                search_bar.replacement_editor.update(cx, |editor, cx| {
2629                    editor.set_text(options.replacement_text, window, cx);
2630                });
2631
2632                if options.replace_all {
2633                    search_bar.replace_all(&ReplaceAll, window, cx)
2634                } else {
2635                    search_bar.replace_next(&ReplaceNext, window, cx)
2636                }
2637            });
2638
2639        assert_eq!(
2640            options
2641                .editor
2642                .read_with(options.cx, |this, cx| { this.text(cx) }),
2643            options.expected_text
2644        );
2645    }
2646
2647    #[perf]
2648    #[gpui::test]
2649    async fn test_replace_special_characters(cx: &mut TestAppContext) {
2650        let (editor, search_bar, cx) = init_test(cx);
2651
2652        run_replacement_test(ReplacementTestParams {
2653            editor: &editor,
2654            search_bar: &search_bar,
2655            cx,
2656            search_text: "expression",
2657            search_options: None,
2658            replacement_text: r"\n",
2659            replace_all: true,
2660            expected_text: r#"
2661            A regular \n (shortened as regex or regexp;[1] also referred to as
2662            rational \n[2][3]) is a sequence of characters that specifies a search
2663            pattern in text. Usually such patterns are used by string-searching algorithms
2664            for "find" or "find and replace" operations on strings, or for input validation.
2665            "#
2666            .unindent(),
2667        })
2668        .await;
2669
2670        run_replacement_test(ReplacementTestParams {
2671            editor: &editor,
2672            search_bar: &search_bar,
2673            cx,
2674            search_text: "or",
2675            search_options: Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
2676            replacement_text: r"\\\n\\\\",
2677            replace_all: false,
2678            expected_text: r#"
2679            A regular \n (shortened as regex \
2680            \\ regexp;[1] also referred to as
2681            rational \n[2][3]) is a sequence of characters that specifies a search
2682            pattern in text. Usually such patterns are used by string-searching algorithms
2683            for "find" or "find and replace" operations on strings, or for input validation.
2684            "#
2685            .unindent(),
2686        })
2687        .await;
2688
2689        run_replacement_test(ReplacementTestParams {
2690            editor: &editor,
2691            search_bar: &search_bar,
2692            cx,
2693            search_text: r"(that|used) ",
2694            search_options: Some(SearchOptions::REGEX),
2695            replacement_text: r"$1\n",
2696            replace_all: true,
2697            expected_text: r#"
2698            A regular \n (shortened as regex \
2699            \\ regexp;[1] also referred to as
2700            rational \n[2][3]) is a sequence of characters that
2701            specifies a search
2702            pattern in text. Usually such patterns are used
2703            by string-searching algorithms
2704            for "find" or "find and replace" operations on strings, or for input validation.
2705            "#
2706            .unindent(),
2707        })
2708        .await;
2709    }
2710
2711    #[perf]
2712    #[gpui::test]
2713    async fn test_find_matches_in_selections_singleton_buffer_multiple_selections(
2714        cx: &mut TestAppContext,
2715    ) {
2716        init_globals(cx);
2717        let buffer = cx.new(|cx| {
2718            Buffer::local(
2719                r#"
2720                aaa bbb aaa ccc
2721                aaa bbb aaa ccc
2722                aaa bbb aaa ccc
2723                aaa bbb aaa ccc
2724                aaa bbb aaa ccc
2725                aaa bbb aaa ccc
2726                "#
2727                .unindent(),
2728                cx,
2729            )
2730        });
2731        let cx = cx.add_empty_window();
2732        let editor =
2733            cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
2734
2735        let search_bar = cx.new_window_entity(|window, cx| {
2736            let mut search_bar = BufferSearchBar::new(None, window, cx);
2737            search_bar.set_active_pane_item(Some(&editor), window, cx);
2738            search_bar.show(window, cx);
2739            search_bar
2740        });
2741
2742        editor.update_in(cx, |editor, window, cx| {
2743            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2744                s.select_ranges(vec![Point::new(1, 0)..Point::new(2, 4)])
2745            })
2746        });
2747
2748        search_bar.update_in(cx, |search_bar, window, cx| {
2749            let deploy = Deploy {
2750                focus: true,
2751                replace_enabled: false,
2752                selection_search_enabled: true,
2753            };
2754            search_bar.deploy(&deploy, window, cx);
2755        });
2756
2757        cx.run_until_parked();
2758
2759        search_bar
2760            .update_in(cx, |search_bar, window, cx| {
2761                search_bar.search("aaa", None, true, window, cx)
2762            })
2763            .await
2764            .unwrap();
2765
2766        editor.update(cx, |editor, cx| {
2767            assert_eq!(
2768                editor.search_background_highlights(cx),
2769                &[
2770                    Point::new(1, 0)..Point::new(1, 3),
2771                    Point::new(1, 8)..Point::new(1, 11),
2772                    Point::new(2, 0)..Point::new(2, 3),
2773                ]
2774            );
2775        });
2776    }
2777
2778    #[perf]
2779    #[gpui::test]
2780    async fn test_find_matches_in_selections_multiple_excerpts_buffer_multiple_selections(
2781        cx: &mut TestAppContext,
2782    ) {
2783        init_globals(cx);
2784        let text = r#"
2785            aaa bbb aaa ccc
2786            aaa bbb aaa ccc
2787            aaa bbb aaa ccc
2788            aaa bbb aaa ccc
2789            aaa bbb aaa ccc
2790            aaa bbb aaa ccc
2791
2792            aaa bbb aaa ccc
2793            aaa bbb aaa ccc
2794            aaa bbb aaa ccc
2795            aaa bbb aaa ccc
2796            aaa bbb aaa ccc
2797            aaa bbb aaa ccc
2798            "#
2799        .unindent();
2800
2801        let cx = cx.add_empty_window();
2802        let editor = cx.new_window_entity(|window, cx| {
2803            let multibuffer = MultiBuffer::build_multi(
2804                [
2805                    (
2806                        &text,
2807                        vec![
2808                            Point::new(0, 0)..Point::new(2, 0),
2809                            Point::new(4, 0)..Point::new(5, 0),
2810                        ],
2811                    ),
2812                    (&text, vec![Point::new(9, 0)..Point::new(11, 0)]),
2813                ],
2814                cx,
2815            );
2816            Editor::for_multibuffer(multibuffer, None, window, cx)
2817        });
2818
2819        let search_bar = cx.new_window_entity(|window, cx| {
2820            let mut search_bar = BufferSearchBar::new(None, window, cx);
2821            search_bar.set_active_pane_item(Some(&editor), window, cx);
2822            search_bar.show(window, cx);
2823            search_bar
2824        });
2825
2826        editor.update_in(cx, |editor, window, cx| {
2827            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2828                s.select_ranges(vec![
2829                    Point::new(1, 0)..Point::new(1, 4),
2830                    Point::new(5, 3)..Point::new(6, 4),
2831                ])
2832            })
2833        });
2834
2835        search_bar.update_in(cx, |search_bar, window, cx| {
2836            let deploy = Deploy {
2837                focus: true,
2838                replace_enabled: false,
2839                selection_search_enabled: true,
2840            };
2841            search_bar.deploy(&deploy, window, cx);
2842        });
2843
2844        cx.run_until_parked();
2845
2846        search_bar
2847            .update_in(cx, |search_bar, window, cx| {
2848                search_bar.search("aaa", None, true, window, cx)
2849            })
2850            .await
2851            .unwrap();
2852
2853        editor.update(cx, |editor, cx| {
2854            assert_eq!(
2855                editor.search_background_highlights(cx),
2856                &[
2857                    Point::new(1, 0)..Point::new(1, 3),
2858                    Point::new(5, 8)..Point::new(5, 11),
2859                    Point::new(6, 0)..Point::new(6, 3),
2860                ]
2861            );
2862        });
2863    }
2864
2865    #[perf]
2866    #[gpui::test]
2867    async fn test_invalid_regexp_search_after_valid(cx: &mut TestAppContext) {
2868        let (editor, search_bar, cx) = init_test(cx);
2869        // Search using valid regexp
2870        search_bar
2871            .update_in(cx, |search_bar, window, cx| {
2872                search_bar.enable_search_option(SearchOptions::REGEX, window, cx);
2873                search_bar.search("expression", None, true, window, cx)
2874            })
2875            .await
2876            .unwrap();
2877        editor.update_in(cx, |editor, window, cx| {
2878            assert_eq!(
2879                display_points_of(editor.all_text_background_highlights(window, cx)),
2880                &[
2881                    DisplayPoint::new(DisplayRow(0), 10)..DisplayPoint::new(DisplayRow(0), 20),
2882                    DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 19),
2883                ],
2884            );
2885        });
2886
2887        // Now, the expression is invalid
2888        search_bar
2889            .update_in(cx, |search_bar, window, cx| {
2890                search_bar.search("expression (", None, true, window, cx)
2891            })
2892            .await
2893            .unwrap_err();
2894        editor.update_in(cx, |editor, window, cx| {
2895            assert!(
2896                display_points_of(editor.all_text_background_highlights(window, cx)).is_empty(),
2897            );
2898        });
2899    }
2900
2901    #[perf]
2902    #[gpui::test]
2903    async fn test_search_options_changes(cx: &mut TestAppContext) {
2904        let (_editor, search_bar, cx) = init_test(cx);
2905        update_search_settings(
2906            SearchSettings {
2907                button: true,
2908                whole_word: false,
2909                case_sensitive: false,
2910                include_ignored: false,
2911                regex: false,
2912                center_on_match: false,
2913            },
2914            cx,
2915        );
2916
2917        let deploy = Deploy {
2918            focus: true,
2919            replace_enabled: false,
2920            selection_search_enabled: true,
2921        };
2922
2923        search_bar.update_in(cx, |search_bar, window, cx| {
2924            assert_eq!(
2925                search_bar.search_options,
2926                SearchOptions::NONE,
2927                "Should have no search options enabled by default"
2928            );
2929            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
2930            assert_eq!(
2931                search_bar.search_options,
2932                SearchOptions::WHOLE_WORD,
2933                "Should enable the option toggled"
2934            );
2935            assert!(
2936                !search_bar.dismissed,
2937                "Search bar should be present and visible"
2938            );
2939            search_bar.deploy(&deploy, window, cx);
2940            assert_eq!(
2941                search_bar.search_options,
2942                SearchOptions::WHOLE_WORD,
2943                "After (re)deploying, the option should still be enabled"
2944            );
2945
2946            search_bar.dismiss(&Dismiss, window, cx);
2947            search_bar.deploy(&deploy, window, cx);
2948            assert_eq!(
2949                search_bar.search_options,
2950                SearchOptions::WHOLE_WORD,
2951                "After hiding and showing the search bar, search options should be preserved"
2952            );
2953
2954            search_bar.toggle_search_option(SearchOptions::REGEX, window, cx);
2955            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
2956            assert_eq!(
2957                search_bar.search_options,
2958                SearchOptions::REGEX,
2959                "Should enable the options toggled"
2960            );
2961            assert!(
2962                !search_bar.dismissed,
2963                "Search bar should be present and visible"
2964            );
2965            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
2966        });
2967
2968        update_search_settings(
2969            SearchSettings {
2970                button: true,
2971                whole_word: false,
2972                case_sensitive: true,
2973                include_ignored: false,
2974                regex: false,
2975                center_on_match: false,
2976            },
2977            cx,
2978        );
2979        search_bar.update_in(cx, |search_bar, window, cx| {
2980            assert_eq!(
2981                search_bar.search_options,
2982                SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2983                "Should have no search options enabled by default"
2984            );
2985
2986            search_bar.deploy(&deploy, window, cx);
2987            assert_eq!(
2988                search_bar.search_options,
2989                SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2990                "Toggling a non-dismissed search bar with custom options should not change the default options"
2991            );
2992            search_bar.dismiss(&Dismiss, window, cx);
2993            search_bar.deploy(&deploy, window, cx);
2994            assert_eq!(
2995                search_bar.configured_options,
2996                SearchOptions::CASE_SENSITIVE,
2997                "After a settings update and toggling the search bar, configured options should be updated"
2998            );
2999            assert_eq!(
3000                search_bar.search_options,
3001                SearchOptions::CASE_SENSITIVE,
3002                "After a settings update and toggling the search bar, configured options should be used"
3003            );
3004        });
3005
3006        update_search_settings(
3007            SearchSettings {
3008                button: true,
3009                whole_word: true,
3010                case_sensitive: true,
3011                include_ignored: false,
3012                regex: false,
3013                center_on_match: false,
3014            },
3015            cx,
3016        );
3017
3018        search_bar.update_in(cx, |search_bar, window, cx| {
3019            search_bar.deploy(&deploy, window, cx);
3020            search_bar.dismiss(&Dismiss, window, cx);
3021            search_bar.show(window, cx);
3022            assert_eq!(
3023                search_bar.search_options,
3024                SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD,
3025                "Calling deploy on an already deployed search bar should not prevent settings updates from being detected"
3026            );
3027        });
3028    }
3029
3030    #[gpui::test]
3031    async fn test_select_occurrence_case_sensitivity(cx: &mut TestAppContext) {
3032        let (editor, search_bar, cx) = init_test(cx);
3033        let mut editor_cx = EditorTestContext::for_editor_in(editor, cx).await;
3034
3035        // Start with case sensitive search settings.
3036        let mut search_settings = SearchSettings::default();
3037        search_settings.case_sensitive = true;
3038        update_search_settings(search_settings, cx);
3039        search_bar.update(cx, |search_bar, cx| {
3040            let mut search_options = search_bar.search_options;
3041            search_options.insert(SearchOptions::CASE_SENSITIVE);
3042            search_bar.set_search_options(search_options, cx);
3043        });
3044
3045        editor_cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
3046        editor_cx.update_editor(|e, window, cx| {
3047            e.select_next(&Default::default(), window, cx).unwrap();
3048        });
3049        editor_cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
3050
3051        // Update the search bar's case sensitivite toggle, so we can later
3052        // confirm that `select_next` will now be case-insensitive.
3053        editor_cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
3054        search_bar.update_in(cx, |search_bar, window, cx| {
3055            search_bar.toggle_case_sensitive(&Default::default(), window, cx);
3056        });
3057        editor_cx.update_editor(|e, window, cx| {
3058            e.select_next(&Default::default(), window, cx).unwrap();
3059        });
3060        editor_cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\nFoo\nfoo");
3061
3062        // Confirm that, after dismissing the search bar, only the editor's
3063        // search settings actually affect the behavior of `select_next`.
3064        search_bar.update_in(cx, |search_bar, window, cx| {
3065            search_bar.dismiss(&Default::default(), window, cx);
3066        });
3067        editor_cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
3068        editor_cx.update_editor(|e, window, cx| {
3069            e.select_next(&Default::default(), window, cx).unwrap();
3070        });
3071        editor_cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
3072
3073        // Update the editor's search settings, disabling case sensitivity, to
3074        // check that the value is respected.
3075        let mut search_settings = SearchSettings::default();
3076        search_settings.case_sensitive = false;
3077        update_search_settings(search_settings, cx);
3078        editor_cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
3079        editor_cx.update_editor(|e, window, cx| {
3080            e.select_next(&Default::default(), window, cx).unwrap();
3081        });
3082        editor_cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\nFoo\nfoo");
3083    }
3084
3085    fn update_search_settings(search_settings: SearchSettings, cx: &mut TestAppContext) {
3086        cx.update(|cx| {
3087            SettingsStore::update_global(cx, |store, cx| {
3088                store.update_user_settings(cx, |settings| {
3089                    settings.editor.search = Some(SearchSettingsContent {
3090                        button: Some(search_settings.button),
3091                        whole_word: Some(search_settings.whole_word),
3092                        case_sensitive: Some(search_settings.case_sensitive),
3093                        include_ignored: Some(search_settings.include_ignored),
3094                        regex: Some(search_settings.regex),
3095                        center_on_match: Some(search_settings.center_on_match),
3096                    });
3097                });
3098            });
3099        });
3100    }
3101}