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