buffer_search.rs

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