buffer_search.rs

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