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