buffer_search.rs

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