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