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).clone();
 720                let mut select_query = true;
 721                if deploy.replace_enabled && handle.is_focused(window) {
 722                    handle = self.replacement_editor.focus_handle(cx).clone();
 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        self.configured_options =
 753            SearchOptions::from_settings(&EditorSettings::get_global(cx).search);
 754        if self.dismissed
 755            && (self.configured_options != self.default_options
 756                || self.configured_options != self.search_options)
 757        {
 758            self.search_options = self.configured_options;
 759            self.default_options = self.configured_options;
 760        }
 761
 762        self.dismissed = false;
 763        self.adjust_query_regex_language(cx);
 764        handle.search_bar_visibility_changed(true, window, cx);
 765        cx.notify();
 766        cx.emit(Event::UpdateLocation);
 767        cx.emit(ToolbarItemEvent::ChangeLocation(
 768            ToolbarItemLocation::Secondary,
 769        ));
 770        true
 771    }
 772
 773    fn supported_options(&self, cx: &mut Context<Self>) -> workspace::searchable::SearchOptions {
 774        self.active_searchable_item
 775            .as_ref()
 776            .map(|item| item.supported_options(cx))
 777            .unwrap_or_default()
 778    }
 779
 780    pub fn search_suggested(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 781        let search = self
 782            .query_suggestion(window, cx)
 783            .map(|suggestion| self.search(&suggestion, Some(self.default_options), window, cx));
 784
 785        if let Some(search) = search {
 786            cx.spawn_in(window, async move |this, cx| {
 787                search.await?;
 788                this.update_in(cx, |this, window, cx| {
 789                    this.activate_current_match(window, cx)
 790                })
 791            })
 792            .detach_and_log_err(cx);
 793        }
 794    }
 795
 796    pub fn activate_current_match(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 797        if let Some(match_ix) = self.active_match_index {
 798            if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
 799                if let Some(matches) = self
 800                    .searchable_items_with_matches
 801                    .get(&active_searchable_item.downgrade())
 802                {
 803                    active_searchable_item.activate_match(match_ix, matches, window, cx)
 804                }
 805            }
 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 && self.active_match_index.is_some() {
 955            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
 956                if let Some(matches) = self
 957                    .searchable_items_with_matches
 958                    .get(&searchable_item.downgrade())
 959                {
 960                    searchable_item.select_matches(matches, window, cx);
 961                    self.focus_editor(&FocusEditor, window, cx);
 962                }
 963            }
 964        }
 965    }
 966
 967    pub fn select_match(
 968        &mut self,
 969        direction: Direction,
 970        count: usize,
 971        window: &mut Window,
 972        cx: &mut Context<Self>,
 973    ) {
 974        if let Some(index) = self.active_match_index {
 975            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
 976                if let Some(matches) = self
 977                    .searchable_items_with_matches
 978                    .get(&searchable_item.downgrade())
 979                    .filter(|matches| !matches.is_empty())
 980                {
 981                    // If 'wrapscan' is disabled, searches do not wrap around the end of the file.
 982                    if !EditorSettings::get_global(cx).search_wrap
 983                        && ((direction == Direction::Next && index + count >= matches.len())
 984                            || (direction == Direction::Prev && index < count))
 985                    {
 986                        crate::show_no_more_matches(window, cx);
 987                        return;
 988                    }
 989                    let new_match_index = searchable_item
 990                        .match_index_for_direction(matches, index, direction, count, window, cx);
 991
 992                    searchable_item.update_matches(matches, window, cx);
 993                    searchable_item.activate_match(new_match_index, matches, window, cx);
 994                }
 995            }
 996        }
 997    }
 998
 999    pub fn select_first_match(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1000        if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1001            if let Some(matches) = self
1002                .searchable_items_with_matches
1003                .get(&searchable_item.downgrade())
1004            {
1005                if matches.is_empty() {
1006                    return;
1007                }
1008                searchable_item.update_matches(matches, window, cx);
1009                searchable_item.activate_match(0, matches, window, cx);
1010            }
1011        }
1012    }
1013
1014    pub fn select_last_match(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1015        if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1016            if let Some(matches) = self
1017                .searchable_items_with_matches
1018                .get(&searchable_item.downgrade())
1019            {
1020                if matches.is_empty() {
1021                    return;
1022                }
1023                let new_match_index = matches.len() - 1;
1024                searchable_item.update_matches(matches, window, cx);
1025                searchable_item.activate_match(new_match_index, matches, window, cx);
1026            }
1027        }
1028    }
1029
1030    fn on_query_editor_event(
1031        &mut self,
1032        editor: &Entity<Editor>,
1033        event: &editor::EditorEvent,
1034        window: &mut Window,
1035        cx: &mut Context<Self>,
1036    ) {
1037        match event {
1038            editor::EditorEvent::Focused => self.query_editor_focused = true,
1039            editor::EditorEvent::Blurred => self.query_editor_focused = false,
1040            editor::EditorEvent::Edited { .. } => {
1041                self.smartcase(window, cx);
1042                self.clear_matches(window, cx);
1043                let search = self.update_matches(false, window, cx);
1044
1045                let width = editor.update(cx, |editor, cx| {
1046                    let text_layout_details = editor.text_layout_details(window);
1047                    let snapshot = editor.snapshot(window, cx).display_snapshot;
1048
1049                    snapshot.x_for_display_point(snapshot.max_point(), &text_layout_details)
1050                        - snapshot.x_for_display_point(DisplayPoint::zero(), &text_layout_details)
1051                });
1052                self.editor_needed_width = width;
1053                cx.notify();
1054
1055                cx.spawn_in(window, async move |this, cx| {
1056                    search.await?;
1057                    this.update_in(cx, |this, window, cx| {
1058                        this.activate_current_match(window, cx)
1059                    })
1060                })
1061                .detach_and_log_err(cx);
1062            }
1063            _ => {}
1064        }
1065    }
1066
1067    fn on_replacement_editor_event(
1068        &mut self,
1069        _: Entity<Editor>,
1070        event: &editor::EditorEvent,
1071        _: &mut Context<Self>,
1072    ) {
1073        match event {
1074            editor::EditorEvent::Focused => self.replacement_editor_focused = true,
1075            editor::EditorEvent::Blurred => self.replacement_editor_focused = false,
1076            _ => {}
1077        }
1078    }
1079
1080    fn on_active_searchable_item_event(
1081        &mut self,
1082        event: &SearchEvent,
1083        window: &mut Window,
1084        cx: &mut Context<Self>,
1085    ) {
1086        match event {
1087            SearchEvent::MatchesInvalidated => {
1088                drop(self.update_matches(false, window, cx));
1089            }
1090            SearchEvent::ActiveMatchChanged => self.update_match_index(window, cx),
1091        }
1092    }
1093
1094    fn toggle_case_sensitive(
1095        &mut self,
1096        _: &ToggleCaseSensitive,
1097        window: &mut Window,
1098        cx: &mut Context<Self>,
1099    ) {
1100        self.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx)
1101    }
1102
1103    fn toggle_whole_word(
1104        &mut self,
1105        _: &ToggleWholeWord,
1106        window: &mut Window,
1107        cx: &mut Context<Self>,
1108    ) {
1109        self.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx)
1110    }
1111
1112    fn toggle_selection(
1113        &mut self,
1114        _: &ToggleSelection,
1115        window: &mut Window,
1116        cx: &mut Context<Self>,
1117    ) {
1118        if let Some(active_item) = self.active_searchable_item.as_mut() {
1119            self.selection_search_enabled = !self.selection_search_enabled;
1120            active_item.toggle_filtered_search_ranges(self.selection_search_enabled, window, cx);
1121            drop(self.update_matches(false, window, cx));
1122            cx.notify();
1123        }
1124    }
1125
1126    fn toggle_regex(&mut self, _: &ToggleRegex, window: &mut Window, cx: &mut Context<Self>) {
1127        self.toggle_search_option(SearchOptions::REGEX, window, cx)
1128    }
1129
1130    fn clear_active_searchable_item_matches(&mut self, window: &mut Window, cx: &mut App) {
1131        if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
1132            self.active_match_index = None;
1133            self.searchable_items_with_matches
1134                .remove(&active_searchable_item.downgrade());
1135            active_searchable_item.clear_matches(window, cx);
1136        }
1137    }
1138
1139    pub fn has_active_match(&self) -> bool {
1140        self.active_match_index.is_some()
1141    }
1142
1143    fn clear_matches(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1144        let mut active_item_matches = None;
1145        for (searchable_item, matches) in self.searchable_items_with_matches.drain() {
1146            if let Some(searchable_item) =
1147                WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
1148            {
1149                if Some(&searchable_item) == self.active_searchable_item.as_ref() {
1150                    active_item_matches = Some((searchable_item.downgrade(), matches));
1151                } else {
1152                    searchable_item.clear_matches(window, cx);
1153                }
1154            }
1155        }
1156
1157        self.searchable_items_with_matches
1158            .extend(active_item_matches);
1159    }
1160
1161    fn update_matches(
1162        &mut self,
1163        reuse_existing_query: bool,
1164        window: &mut Window,
1165        cx: &mut Context<Self>,
1166    ) -> oneshot::Receiver<()> {
1167        let (done_tx, done_rx) = oneshot::channel();
1168        let query = self.query(cx);
1169        self.pending_search.take();
1170
1171        if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
1172            self.query_error = None;
1173            if query.is_empty() {
1174                self.clear_active_searchable_item_matches(window, cx);
1175                let _ = done_tx.send(());
1176                cx.notify();
1177            } else {
1178                let query: Arc<_> = if let Some(search) =
1179                    self.active_search.take().filter(|_| reuse_existing_query)
1180                {
1181                    search
1182                } else {
1183                    if self.search_options.contains(SearchOptions::REGEX) {
1184                        match SearchQuery::regex(
1185                            query,
1186                            self.search_options.contains(SearchOptions::WHOLE_WORD),
1187                            self.search_options.contains(SearchOptions::CASE_SENSITIVE),
1188                            false,
1189                            self.search_options
1190                                .contains(SearchOptions::ONE_MATCH_PER_LINE),
1191                            Default::default(),
1192                            Default::default(),
1193                            false,
1194                            None,
1195                        ) {
1196                            Ok(query) => query.with_replacement(self.replacement(cx)),
1197                            Err(e) => {
1198                                self.query_error = Some(e.to_string());
1199                                self.clear_active_searchable_item_matches(window, cx);
1200                                cx.notify();
1201                                return done_rx;
1202                            }
1203                        }
1204                    } else {
1205                        match SearchQuery::text(
1206                            query,
1207                            self.search_options.contains(SearchOptions::WHOLE_WORD),
1208                            self.search_options.contains(SearchOptions::CASE_SENSITIVE),
1209                            false,
1210                            Default::default(),
1211                            Default::default(),
1212                            false,
1213                            None,
1214                        ) {
1215                            Ok(query) => query.with_replacement(self.replacement(cx)),
1216                            Err(e) => {
1217                                self.query_error = Some(e.to_string());
1218                                self.clear_active_searchable_item_matches(window, cx);
1219                                cx.notify();
1220                                return done_rx;
1221                            }
1222                        }
1223                    }
1224                    .into()
1225                };
1226
1227                self.active_search = Some(query.clone());
1228                let query_text = query.as_str().to_string();
1229
1230                let matches = active_searchable_item.find_matches(query, window, cx);
1231
1232                let active_searchable_item = active_searchable_item.downgrade();
1233                self.pending_search = Some(cx.spawn_in(window, async move |this, cx| {
1234                    let matches = matches.await;
1235
1236                    this.update_in(cx, |this, window, cx| {
1237                        if let Some(active_searchable_item) =
1238                            WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx)
1239                        {
1240                            this.searchable_items_with_matches
1241                                .insert(active_searchable_item.downgrade(), matches);
1242
1243                            this.update_match_index(window, cx);
1244                            this.search_history
1245                                .add(&mut this.search_history_cursor, query_text);
1246                            if !this.dismissed {
1247                                let matches = this
1248                                    .searchable_items_with_matches
1249                                    .get(&active_searchable_item.downgrade())
1250                                    .unwrap();
1251                                if matches.is_empty() {
1252                                    active_searchable_item.clear_matches(window, cx);
1253                                } else {
1254                                    active_searchable_item.update_matches(matches, window, cx);
1255                                }
1256                                let _ = done_tx.send(());
1257                            }
1258                            cx.notify();
1259                        }
1260                    })
1261                    .log_err();
1262                }));
1263            }
1264        }
1265        done_rx
1266    }
1267
1268    fn reverse_direction_if_backwards(&self, direction: Direction) -> Direction {
1269        if self.search_options.contains(SearchOptions::BACKWARDS) {
1270            direction.opposite()
1271        } else {
1272            direction
1273        }
1274    }
1275
1276    pub fn update_match_index(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1277        let direction = self.reverse_direction_if_backwards(Direction::Next);
1278        let new_index = self
1279            .active_searchable_item
1280            .as_ref()
1281            .and_then(|searchable_item| {
1282                let matches = self
1283                    .searchable_items_with_matches
1284                    .get(&searchable_item.downgrade())?;
1285                searchable_item.active_match_index(direction, matches, window, cx)
1286            });
1287        if new_index != self.active_match_index {
1288            self.active_match_index = new_index;
1289            cx.notify();
1290        }
1291    }
1292
1293    fn tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context<Self>) {
1294        self.cycle_field(Direction::Next, window, cx);
1295    }
1296
1297    fn backtab(&mut self, _: &Backtab, window: &mut Window, cx: &mut Context<Self>) {
1298        self.cycle_field(Direction::Prev, window, cx);
1299    }
1300    fn cycle_field(&mut self, direction: Direction, window: &mut Window, cx: &mut Context<Self>) {
1301        let mut handles = vec![self.query_editor.focus_handle(cx)];
1302        if self.replace_enabled {
1303            handles.push(self.replacement_editor.focus_handle(cx));
1304        }
1305        if let Some(item) = self.active_searchable_item.as_ref() {
1306            handles.push(item.item_focus_handle(cx));
1307        }
1308        let current_index = match handles.iter().position(|focus| focus.is_focused(window)) {
1309            Some(index) => index,
1310            None => return,
1311        };
1312
1313        let new_index = match direction {
1314            Direction::Next => (current_index + 1) % handles.len(),
1315            Direction::Prev if current_index == 0 => handles.len() - 1,
1316            Direction::Prev => (current_index - 1) % handles.len(),
1317        };
1318        let next_focus_handle = &handles[new_index];
1319        self.focus(next_focus_handle, window);
1320        cx.stop_propagation();
1321    }
1322
1323    fn next_history_query(
1324        &mut self,
1325        _: &NextHistoryQuery,
1326        window: &mut Window,
1327        cx: &mut Context<Self>,
1328    ) {
1329        if let Some(new_query) = self
1330            .search_history
1331            .next(&mut self.search_history_cursor)
1332            .map(str::to_string)
1333        {
1334            drop(self.search(&new_query, Some(self.search_options), window, cx));
1335        } else {
1336            self.search_history_cursor.reset();
1337            drop(self.search("", Some(self.search_options), window, cx));
1338        }
1339    }
1340
1341    fn previous_history_query(
1342        &mut self,
1343        _: &PreviousHistoryQuery,
1344        window: &mut Window,
1345        cx: &mut Context<Self>,
1346    ) {
1347        if self.query(cx).is_empty() {
1348            if let Some(new_query) = self
1349                .search_history
1350                .current(&mut self.search_history_cursor)
1351                .map(str::to_string)
1352            {
1353                drop(self.search(&new_query, Some(self.search_options), window, cx));
1354                return;
1355            }
1356        }
1357
1358        if let Some(new_query) = self
1359            .search_history
1360            .previous(&mut self.search_history_cursor)
1361            .map(str::to_string)
1362        {
1363            drop(self.search(&new_query, Some(self.search_options), window, cx));
1364        }
1365    }
1366
1367    fn focus(&self, handle: &gpui::FocusHandle, window: &mut Window) {
1368        window.invalidate_character_coordinates();
1369        window.focus(handle);
1370    }
1371
1372    fn toggle_replace(&mut self, _: &ToggleReplace, window: &mut Window, cx: &mut Context<Self>) {
1373        if self.active_searchable_item.is_some() {
1374            self.replace_enabled = !self.replace_enabled;
1375            let handle = if self.replace_enabled {
1376                self.replacement_editor.focus_handle(cx)
1377            } else {
1378                self.query_editor.focus_handle(cx)
1379            };
1380            self.focus(&handle, window);
1381            cx.notify();
1382        }
1383    }
1384
1385    fn replace_next(&mut self, _: &ReplaceNext, window: &mut Window, cx: &mut Context<Self>) {
1386        let mut should_propagate = true;
1387        if !self.dismissed && self.active_search.is_some() {
1388            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1389                if let Some(query) = self.active_search.as_ref() {
1390                    if let Some(matches) = self
1391                        .searchable_items_with_matches
1392                        .get(&searchable_item.downgrade())
1393                    {
1394                        if let Some(active_index) = self.active_match_index {
1395                            let query = query
1396                                .as_ref()
1397                                .clone()
1398                                .with_replacement(self.replacement(cx));
1399                            searchable_item.replace(matches.at(active_index), &query, window, cx);
1400                            self.select_next_match(&SelectNextMatch, window, cx);
1401                        }
1402                        should_propagate = false;
1403                    }
1404                }
1405            }
1406        }
1407        if !should_propagate {
1408            cx.stop_propagation();
1409        }
1410    }
1411
1412    pub fn replace_all(&mut self, _: &ReplaceAll, window: &mut Window, cx: &mut Context<Self>) {
1413        if !self.dismissed && self.active_search.is_some() {
1414            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1415                if let Some(query) = self.active_search.as_ref() {
1416                    if let Some(matches) = self
1417                        .searchable_items_with_matches
1418                        .get(&searchable_item.downgrade())
1419                    {
1420                        let query = query
1421                            .as_ref()
1422                            .clone()
1423                            .with_replacement(self.replacement(cx));
1424                        searchable_item.replace_all(&mut matches.iter(), &query, window, cx);
1425                    }
1426                }
1427            }
1428        }
1429    }
1430
1431    pub fn match_exists(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
1432        self.update_match_index(window, cx);
1433        self.active_match_index.is_some()
1434    }
1435
1436    pub fn should_use_smartcase_search(&mut self, cx: &mut Context<Self>) -> bool {
1437        EditorSettings::get_global(cx).use_smartcase_search
1438    }
1439
1440    pub fn is_contains_uppercase(&mut self, str: &String) -> bool {
1441        str.chars().any(|c| c.is_uppercase())
1442    }
1443
1444    fn smartcase(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1445        if self.should_use_smartcase_search(cx) {
1446            let query = self.query(cx);
1447            if !query.is_empty() {
1448                let is_case = self.is_contains_uppercase(&query);
1449                if self.has_search_option(SearchOptions::CASE_SENSITIVE) != is_case {
1450                    self.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
1451                }
1452            }
1453        }
1454    }
1455
1456    fn adjust_query_regex_language(&self, cx: &mut App) {
1457        let enable = self.search_options.contains(SearchOptions::REGEX);
1458        let query_buffer = self
1459            .query_editor
1460            .read(cx)
1461            .buffer()
1462            .read(cx)
1463            .as_singleton()
1464            .expect("query editor should be backed by a singleton buffer");
1465        if enable {
1466            if let Some(regex_language) = self.regex_language.clone() {
1467                query_buffer.update(cx, |query_buffer, cx| {
1468                    query_buffer.set_language(Some(regex_language), cx);
1469                })
1470            }
1471        } else {
1472            query_buffer.update(cx, |query_buffer, cx| {
1473                query_buffer.set_language(None, cx);
1474            })
1475        }
1476    }
1477}
1478
1479#[cfg(test)]
1480mod tests {
1481    use std::ops::Range;
1482
1483    use super::*;
1484    use editor::{
1485        DisplayPoint, Editor, MultiBuffer, SearchSettings, SelectionEffects,
1486        display_map::DisplayRow,
1487    };
1488    use gpui::{Hsla, TestAppContext, UpdateGlobal, VisualTestContext};
1489    use language::{Buffer, Point};
1490    use project::Project;
1491    use settings::SettingsStore;
1492    use smol::stream::StreamExt as _;
1493    use unindent::Unindent as _;
1494
1495    fn init_globals(cx: &mut TestAppContext) {
1496        cx.update(|cx| {
1497            let store = settings::SettingsStore::test(cx);
1498            cx.set_global(store);
1499            workspace::init_settings(cx);
1500            editor::init(cx);
1501
1502            language::init(cx);
1503            Project::init_settings(cx);
1504            theme::init(theme::LoadThemes::JustBase, cx);
1505            crate::init(cx);
1506        });
1507    }
1508
1509    fn init_test(
1510        cx: &mut TestAppContext,
1511    ) -> (
1512        Entity<Editor>,
1513        Entity<BufferSearchBar>,
1514        &mut VisualTestContext,
1515    ) {
1516        init_globals(cx);
1517        let buffer = cx.new(|cx| {
1518            Buffer::local(
1519                r#"
1520                A regular expression (shortened as regex or regexp;[1] also referred to as
1521                rational expression[2][3]) is a sequence of characters that specifies a search
1522                pattern in text. Usually such patterns are used by string-searching algorithms
1523                for "find" or "find and replace" operations on strings, or for input validation.
1524                "#
1525                .unindent(),
1526                cx,
1527            )
1528        });
1529        let cx = cx.add_empty_window();
1530        let editor =
1531            cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
1532
1533        let search_bar = cx.new_window_entity(|window, cx| {
1534            let mut search_bar = BufferSearchBar::new(None, window, cx);
1535            search_bar.set_active_pane_item(Some(&editor), window, cx);
1536            search_bar.show(window, cx);
1537            search_bar
1538        });
1539
1540        (editor, search_bar, cx)
1541    }
1542
1543    #[gpui::test]
1544    async fn test_search_simple(cx: &mut TestAppContext) {
1545        let (editor, search_bar, cx) = init_test(cx);
1546        let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
1547            background_highlights
1548                .into_iter()
1549                .map(|(range, _)| range)
1550                .collect::<Vec<_>>()
1551        };
1552        // Search for a string that appears with different casing.
1553        // By default, search is case-insensitive.
1554        search_bar
1555            .update_in(cx, |search_bar, window, cx| {
1556                search_bar.search("us", None, window, cx)
1557            })
1558            .await
1559            .unwrap();
1560        editor.update_in(cx, |editor, window, cx| {
1561            assert_eq!(
1562                display_points_of(editor.all_text_background_highlights(window, cx)),
1563                &[
1564                    DisplayPoint::new(DisplayRow(2), 17)..DisplayPoint::new(DisplayRow(2), 19),
1565                    DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),
1566                ]
1567            );
1568        });
1569
1570        // Switch to a case sensitive search.
1571        search_bar.update_in(cx, |search_bar, window, cx| {
1572            search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
1573        });
1574        let mut editor_notifications = cx.notifications(&editor);
1575        editor_notifications.next().await;
1576        editor.update_in(cx, |editor, window, cx| {
1577            assert_eq!(
1578                display_points_of(editor.all_text_background_highlights(window, cx)),
1579                &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
1580            );
1581        });
1582
1583        // Search for a string that appears both as a whole word and
1584        // within other words. By default, all results are found.
1585        search_bar
1586            .update_in(cx, |search_bar, window, cx| {
1587                search_bar.search("or", None, window, cx)
1588            })
1589            .await
1590            .unwrap();
1591        editor.update_in(cx, |editor, window, cx| {
1592            assert_eq!(
1593                display_points_of(editor.all_text_background_highlights(window, cx)),
1594                &[
1595                    DisplayPoint::new(DisplayRow(0), 24)..DisplayPoint::new(DisplayRow(0), 26),
1596                    DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1597                    DisplayPoint::new(DisplayRow(2), 71)..DisplayPoint::new(DisplayRow(2), 73),
1598                    DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 3),
1599                    DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
1600                    DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
1601                    DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 62),
1602                ]
1603            );
1604        });
1605
1606        // Switch to a whole word search.
1607        search_bar.update_in(cx, |search_bar, window, cx| {
1608            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
1609        });
1610        let mut editor_notifications = cx.notifications(&editor);
1611        editor_notifications.next().await;
1612        editor.update_in(cx, |editor, window, cx| {
1613            assert_eq!(
1614                display_points_of(editor.all_text_background_highlights(window, cx)),
1615                &[
1616                    DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1617                    DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
1618                    DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
1619                ]
1620            );
1621        });
1622
1623        editor.update_in(cx, |editor, window, cx| {
1624            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1625                s.select_display_ranges([
1626                    DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
1627                ])
1628            });
1629        });
1630        search_bar.update_in(cx, |search_bar, window, cx| {
1631            assert_eq!(search_bar.active_match_index, Some(0));
1632            search_bar.select_next_match(&SelectNextMatch, window, cx);
1633            assert_eq!(
1634                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1635                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1636            );
1637        });
1638        search_bar.read_with(cx, |search_bar, _| {
1639            assert_eq!(search_bar.active_match_index, Some(0));
1640        });
1641
1642        search_bar.update_in(cx, |search_bar, window, cx| {
1643            search_bar.select_next_match(&SelectNextMatch, window, cx);
1644            assert_eq!(
1645                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1646                [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1647            );
1648        });
1649        search_bar.read_with(cx, |search_bar, _| {
1650            assert_eq!(search_bar.active_match_index, Some(1));
1651        });
1652
1653        search_bar.update_in(cx, |search_bar, window, cx| {
1654            search_bar.select_next_match(&SelectNextMatch, window, cx);
1655            assert_eq!(
1656                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1657                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1658            );
1659        });
1660        search_bar.read_with(cx, |search_bar, _| {
1661            assert_eq!(search_bar.active_match_index, Some(2));
1662        });
1663
1664        search_bar.update_in(cx, |search_bar, window, cx| {
1665            search_bar.select_next_match(&SelectNextMatch, window, cx);
1666            assert_eq!(
1667                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1668                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1669            );
1670        });
1671        search_bar.read_with(cx, |search_bar, _| {
1672            assert_eq!(search_bar.active_match_index, Some(0));
1673        });
1674
1675        search_bar.update_in(cx, |search_bar, window, cx| {
1676            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1677            assert_eq!(
1678                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1679                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1680            );
1681        });
1682        search_bar.read_with(cx, |search_bar, _| {
1683            assert_eq!(search_bar.active_match_index, Some(2));
1684        });
1685
1686        search_bar.update_in(cx, |search_bar, window, cx| {
1687            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1688            assert_eq!(
1689                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1690                [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1691            );
1692        });
1693        search_bar.read_with(cx, |search_bar, _| {
1694            assert_eq!(search_bar.active_match_index, Some(1));
1695        });
1696
1697        search_bar.update_in(cx, |search_bar, window, cx| {
1698            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1699            assert_eq!(
1700                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1701                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1702            );
1703        });
1704        search_bar.read_with(cx, |search_bar, _| {
1705            assert_eq!(search_bar.active_match_index, Some(0));
1706        });
1707
1708        // Park the cursor in between matches and ensure that going to the previous match selects
1709        // the closest match to the left.
1710        editor.update_in(cx, |editor, window, cx| {
1711            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1712                s.select_display_ranges([
1713                    DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
1714                ])
1715            });
1716        });
1717        search_bar.update_in(cx, |search_bar, window, cx| {
1718            assert_eq!(search_bar.active_match_index, Some(1));
1719            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1720            assert_eq!(
1721                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1722                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1723            );
1724        });
1725        search_bar.read_with(cx, |search_bar, _| {
1726            assert_eq!(search_bar.active_match_index, Some(0));
1727        });
1728
1729        // Park the cursor in between matches and ensure that going to the next match selects the
1730        // closest match to the right.
1731        editor.update_in(cx, |editor, window, cx| {
1732            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1733                s.select_display_ranges([
1734                    DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
1735                ])
1736            });
1737        });
1738        search_bar.update_in(cx, |search_bar, window, cx| {
1739            assert_eq!(search_bar.active_match_index, Some(1));
1740            search_bar.select_next_match(&SelectNextMatch, window, cx);
1741            assert_eq!(
1742                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1743                [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1744            );
1745        });
1746        search_bar.read_with(cx, |search_bar, _| {
1747            assert_eq!(search_bar.active_match_index, Some(1));
1748        });
1749
1750        // Park the cursor after the last match and ensure that going to the previous match selects
1751        // the last match.
1752        editor.update_in(cx, |editor, window, cx| {
1753            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1754                s.select_display_ranges([
1755                    DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
1756                ])
1757            });
1758        });
1759        search_bar.update_in(cx, |search_bar, window, cx| {
1760            assert_eq!(search_bar.active_match_index, Some(2));
1761            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1762            assert_eq!(
1763                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1764                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1765            );
1766        });
1767        search_bar.read_with(cx, |search_bar, _| {
1768            assert_eq!(search_bar.active_match_index, Some(2));
1769        });
1770
1771        // Park the cursor after the last match and ensure that going to the next match selects the
1772        // first match.
1773        editor.update_in(cx, |editor, window, cx| {
1774            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1775                s.select_display_ranges([
1776                    DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
1777                ])
1778            });
1779        });
1780        search_bar.update_in(cx, |search_bar, window, cx| {
1781            assert_eq!(search_bar.active_match_index, Some(2));
1782            search_bar.select_next_match(&SelectNextMatch, window, cx);
1783            assert_eq!(
1784                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1785                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1786            );
1787        });
1788        search_bar.read_with(cx, |search_bar, _| {
1789            assert_eq!(search_bar.active_match_index, Some(0));
1790        });
1791
1792        // Park the cursor before the first match and ensure that going to the previous match
1793        // selects the last match.
1794        editor.update_in(cx, |editor, window, cx| {
1795            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1796                s.select_display_ranges([
1797                    DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
1798                ])
1799            });
1800        });
1801        search_bar.update_in(cx, |search_bar, window, cx| {
1802            assert_eq!(search_bar.active_match_index, Some(0));
1803            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1804            assert_eq!(
1805                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1806                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1807            );
1808        });
1809        search_bar.read_with(cx, |search_bar, _| {
1810            assert_eq!(search_bar.active_match_index, Some(2));
1811        });
1812    }
1813
1814    fn display_points_of(
1815        background_highlights: Vec<(Range<DisplayPoint>, Hsla)>,
1816    ) -> Vec<Range<DisplayPoint>> {
1817        background_highlights
1818            .into_iter()
1819            .map(|(range, _)| range)
1820            .collect::<Vec<_>>()
1821    }
1822
1823    #[gpui::test]
1824    async fn test_search_option_handling(cx: &mut TestAppContext) {
1825        let (editor, search_bar, cx) = init_test(cx);
1826
1827        // show with options should make current search case sensitive
1828        search_bar
1829            .update_in(cx, |search_bar, window, cx| {
1830                search_bar.show(window, cx);
1831                search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), window, cx)
1832            })
1833            .await
1834            .unwrap();
1835        editor.update_in(cx, |editor, window, cx| {
1836            assert_eq!(
1837                display_points_of(editor.all_text_background_highlights(window, cx)),
1838                &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
1839            );
1840        });
1841
1842        // search_suggested should restore default options
1843        search_bar.update_in(cx, |search_bar, window, cx| {
1844            search_bar.search_suggested(window, cx);
1845            assert_eq!(search_bar.search_options, SearchOptions::NONE)
1846        });
1847
1848        // toggling a search option should update the defaults
1849        search_bar
1850            .update_in(cx, |search_bar, window, cx| {
1851                search_bar.search("regex", Some(SearchOptions::CASE_SENSITIVE), window, cx)
1852            })
1853            .await
1854            .unwrap();
1855        search_bar.update_in(cx, |search_bar, window, cx| {
1856            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx)
1857        });
1858        let mut editor_notifications = cx.notifications(&editor);
1859        editor_notifications.next().await;
1860        editor.update_in(cx, |editor, window, cx| {
1861            assert_eq!(
1862                display_points_of(editor.all_text_background_highlights(window, cx)),
1863                &[DisplayPoint::new(DisplayRow(0), 35)..DisplayPoint::new(DisplayRow(0), 40),]
1864            );
1865        });
1866
1867        // defaults should still include whole word
1868        search_bar.update_in(cx, |search_bar, window, cx| {
1869            search_bar.search_suggested(window, cx);
1870            assert_eq!(
1871                search_bar.search_options,
1872                SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD
1873            )
1874        });
1875    }
1876
1877    #[gpui::test]
1878    async fn test_search_select_all_matches(cx: &mut TestAppContext) {
1879        init_globals(cx);
1880        let buffer_text = r#"
1881        A regular expression (shortened as regex or regexp;[1] also referred to as
1882        rational expression[2][3]) is a sequence of characters that specifies a search
1883        pattern in text. Usually such patterns are used by string-searching algorithms
1884        for "find" or "find and replace" operations on strings, or for input validation.
1885        "#
1886        .unindent();
1887        let expected_query_matches_count = buffer_text
1888            .chars()
1889            .filter(|c| c.eq_ignore_ascii_case(&'a'))
1890            .count();
1891        assert!(
1892            expected_query_matches_count > 1,
1893            "Should pick a query with multiple results"
1894        );
1895        let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
1896        let window = cx.add_window(|_, _| gpui::Empty);
1897
1898        let editor = window.build_entity(cx, |window, cx| {
1899            Editor::for_buffer(buffer.clone(), None, window, cx)
1900        });
1901
1902        let search_bar = window.build_entity(cx, |window, cx| {
1903            let mut search_bar = BufferSearchBar::new(None, window, cx);
1904            search_bar.set_active_pane_item(Some(&editor), window, cx);
1905            search_bar.show(window, cx);
1906            search_bar
1907        });
1908
1909        window
1910            .update(cx, |_, window, cx| {
1911                search_bar.update(cx, |search_bar, cx| {
1912                    search_bar.search("a", None, window, cx)
1913                })
1914            })
1915            .unwrap()
1916            .await
1917            .unwrap();
1918        let initial_selections = window
1919            .update(cx, |_, window, cx| {
1920                search_bar.update(cx, |search_bar, cx| {
1921                    let handle = search_bar.query_editor.focus_handle(cx);
1922                    window.focus(&handle);
1923                    search_bar.activate_current_match(window, cx);
1924                });
1925                assert!(
1926                    !editor.read(cx).is_focused(window),
1927                    "Initially, the editor should not be focused"
1928                );
1929                let initial_selections = editor.update(cx, |editor, cx| {
1930                    let initial_selections = editor.selections.display_ranges(cx);
1931                    assert_eq!(
1932                        initial_selections.len(), 1,
1933                        "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
1934                    );
1935                    initial_selections
1936                });
1937                search_bar.update(cx, |search_bar, cx| {
1938                    assert_eq!(search_bar.active_match_index, Some(0));
1939                    let handle = search_bar.query_editor.focus_handle(cx);
1940                    window.focus(&handle);
1941                    search_bar.select_all_matches(&SelectAllMatches, window, cx);
1942                });
1943                assert!(
1944                    editor.read(cx).is_focused(window),
1945                    "Should focus editor after successful SelectAllMatches"
1946                );
1947                search_bar.update(cx, |search_bar, cx| {
1948                    let all_selections =
1949                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1950                    assert_eq!(
1951                        all_selections.len(),
1952                        expected_query_matches_count,
1953                        "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1954                    );
1955                    assert_eq!(
1956                        search_bar.active_match_index,
1957                        Some(0),
1958                        "Match index should not change after selecting all matches"
1959                    );
1960                });
1961
1962                search_bar.update(cx, |this, cx| this.select_next_match(&SelectNextMatch, window, cx));
1963                initial_selections
1964            }).unwrap();
1965
1966        window
1967            .update(cx, |_, window, cx| {
1968                assert!(
1969                    editor.read(cx).is_focused(window),
1970                    "Should still have editor focused after SelectNextMatch"
1971                );
1972                search_bar.update(cx, |search_bar, cx| {
1973                    let all_selections =
1974                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1975                    assert_eq!(
1976                        all_selections.len(),
1977                        1,
1978                        "On next match, should deselect items and select the next match"
1979                    );
1980                    assert_ne!(
1981                        all_selections, initial_selections,
1982                        "Next match should be different from the first selection"
1983                    );
1984                    assert_eq!(
1985                        search_bar.active_match_index,
1986                        Some(1),
1987                        "Match index should be updated to the next one"
1988                    );
1989                    let handle = search_bar.query_editor.focus_handle(cx);
1990                    window.focus(&handle);
1991                    search_bar.select_all_matches(&SelectAllMatches, window, cx);
1992                });
1993            })
1994            .unwrap();
1995        window
1996            .update(cx, |_, window, cx| {
1997                assert!(
1998                    editor.read(cx).is_focused(window),
1999                    "Should focus editor after successful SelectAllMatches"
2000                );
2001                search_bar.update(cx, |search_bar, cx| {
2002                    let all_selections =
2003                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2004                    assert_eq!(
2005                    all_selections.len(),
2006                    expected_query_matches_count,
2007                    "Should select all `a` characters in the buffer, but got: {all_selections:?}"
2008                );
2009                    assert_eq!(
2010                        search_bar.active_match_index,
2011                        Some(1),
2012                        "Match index should not change after selecting all matches"
2013                    );
2014                });
2015                search_bar.update(cx, |search_bar, cx| {
2016                    search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2017                });
2018            })
2019            .unwrap();
2020        let last_match_selections = window
2021            .update(cx, |_, window, cx| {
2022                assert!(
2023                    editor.read(cx).is_focused(window),
2024                    "Should still have editor focused after SelectPreviousMatch"
2025                );
2026
2027                search_bar.update(cx, |search_bar, cx| {
2028                    let all_selections =
2029                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2030                    assert_eq!(
2031                        all_selections.len(),
2032                        1,
2033                        "On previous match, should deselect items and select the previous item"
2034                    );
2035                    assert_eq!(
2036                        all_selections, initial_selections,
2037                        "Previous match should be the same as the first selection"
2038                    );
2039                    assert_eq!(
2040                        search_bar.active_match_index,
2041                        Some(0),
2042                        "Match index should be updated to the previous one"
2043                    );
2044                    all_selections
2045                })
2046            })
2047            .unwrap();
2048
2049        window
2050            .update(cx, |_, window, cx| {
2051                search_bar.update(cx, |search_bar, cx| {
2052                    let handle = search_bar.query_editor.focus_handle(cx);
2053                    window.focus(&handle);
2054                    search_bar.search("abas_nonexistent_match", None, window, cx)
2055                })
2056            })
2057            .unwrap()
2058            .await
2059            .unwrap();
2060        window
2061            .update(cx, |_, window, cx| {
2062                search_bar.update(cx, |search_bar, cx| {
2063                    search_bar.select_all_matches(&SelectAllMatches, window, cx);
2064                });
2065                assert!(
2066                    editor.update(cx, |this, _cx| !this.is_focused(window)),
2067                    "Should not switch focus to editor if SelectAllMatches does not find any matches"
2068                );
2069                search_bar.update(cx, |search_bar, cx| {
2070                    let all_selections =
2071                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2072                    assert_eq!(
2073                        all_selections, last_match_selections,
2074                        "Should not select anything new if there are no matches"
2075                    );
2076                    assert!(
2077                        search_bar.active_match_index.is_none(),
2078                        "For no matches, there should be no active match index"
2079                    );
2080                });
2081            })
2082            .unwrap();
2083    }
2084
2085    #[gpui::test]
2086    async fn test_search_query_with_match_whole_word(cx: &mut TestAppContext) {
2087        init_globals(cx);
2088        let buffer_text = r#"
2089        self.buffer.update(cx, |buffer, cx| {
2090            buffer.edit(
2091                edits,
2092                Some(AutoindentMode::Block {
2093                    original_indent_columns,
2094                }),
2095                cx,
2096            )
2097        });
2098
2099        this.buffer.update(cx, |buffer, cx| {
2100            buffer.edit([(end_of_line..start_of_next_line, replace)], None, cx)
2101        });
2102        "#
2103        .unindent();
2104        let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
2105        let cx = cx.add_empty_window();
2106
2107        let editor =
2108            cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
2109
2110        let search_bar = cx.new_window_entity(|window, cx| {
2111            let mut search_bar = BufferSearchBar::new(None, window, cx);
2112            search_bar.set_active_pane_item(Some(&editor), window, cx);
2113            search_bar.show(window, cx);
2114            search_bar
2115        });
2116
2117        search_bar
2118            .update_in(cx, |search_bar, window, cx| {
2119                search_bar.search(
2120                    "edit\\(",
2121                    Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
2122                    window,
2123                    cx,
2124                )
2125            })
2126            .await
2127            .unwrap();
2128
2129        search_bar.update_in(cx, |search_bar, window, cx| {
2130            search_bar.select_all_matches(&SelectAllMatches, window, cx);
2131        });
2132        search_bar.update(cx, |_, cx| {
2133            let all_selections =
2134                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2135            assert_eq!(
2136                all_selections.len(),
2137                2,
2138                "Should select all `edit(` in the buffer, but got: {all_selections:?}"
2139            );
2140        });
2141
2142        search_bar
2143            .update_in(cx, |search_bar, window, cx| {
2144                search_bar.search(
2145                    "edit(",
2146                    Some(SearchOptions::WHOLE_WORD | SearchOptions::CASE_SENSITIVE),
2147                    window,
2148                    cx,
2149                )
2150            })
2151            .await
2152            .unwrap();
2153
2154        search_bar.update_in(cx, |search_bar, window, cx| {
2155            search_bar.select_all_matches(&SelectAllMatches, window, cx);
2156        });
2157        search_bar.update(cx, |_, cx| {
2158            let all_selections =
2159                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2160            assert_eq!(
2161                all_selections.len(),
2162                2,
2163                "Should select all `edit(` in the buffer, but got: {all_selections:?}"
2164            );
2165        });
2166    }
2167
2168    #[gpui::test]
2169    async fn test_search_query_history(cx: &mut TestAppContext) {
2170        init_globals(cx);
2171        let buffer_text = r#"
2172        A regular expression (shortened as regex or regexp;[1] also referred to as
2173        rational expression[2][3]) is a sequence of characters that specifies a search
2174        pattern in text. Usually such patterns are used by string-searching algorithms
2175        for "find" or "find and replace" operations on strings, or for input validation.
2176        "#
2177        .unindent();
2178        let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
2179        let cx = cx.add_empty_window();
2180
2181        let editor =
2182            cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
2183
2184        let search_bar = cx.new_window_entity(|window, cx| {
2185            let mut search_bar = BufferSearchBar::new(None, window, cx);
2186            search_bar.set_active_pane_item(Some(&editor), window, cx);
2187            search_bar.show(window, cx);
2188            search_bar
2189        });
2190
2191        // Add 3 search items into the history.
2192        search_bar
2193            .update_in(cx, |search_bar, window, cx| {
2194                search_bar.search("a", None, window, cx)
2195            })
2196            .await
2197            .unwrap();
2198        search_bar
2199            .update_in(cx, |search_bar, window, cx| {
2200                search_bar.search("b", None, window, cx)
2201            })
2202            .await
2203            .unwrap();
2204        search_bar
2205            .update_in(cx, |search_bar, window, cx| {
2206                search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), window, cx)
2207            })
2208            .await
2209            .unwrap();
2210        // Ensure that the latest search is active.
2211        search_bar.update(cx, |search_bar, cx| {
2212            assert_eq!(search_bar.query(cx), "c");
2213            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2214        });
2215
2216        // Next history query after the latest should set the query to the empty string.
2217        search_bar.update_in(cx, |search_bar, window, cx| {
2218            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2219        });
2220        search_bar.update(cx, |search_bar, cx| {
2221            assert_eq!(search_bar.query(cx), "");
2222            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2223        });
2224        search_bar.update_in(cx, |search_bar, window, cx| {
2225            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2226        });
2227        search_bar.update(cx, |search_bar, cx| {
2228            assert_eq!(search_bar.query(cx), "");
2229            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2230        });
2231
2232        // First previous query for empty current query should set the query to the latest.
2233        search_bar.update_in(cx, |search_bar, window, cx| {
2234            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2235        });
2236        search_bar.update(cx, |search_bar, cx| {
2237            assert_eq!(search_bar.query(cx), "c");
2238            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2239        });
2240
2241        // Further previous items should go over the history in reverse order.
2242        search_bar.update_in(cx, |search_bar, window, cx| {
2243            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2244        });
2245        search_bar.update(cx, |search_bar, cx| {
2246            assert_eq!(search_bar.query(cx), "b");
2247            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2248        });
2249
2250        // Previous items should never go behind the first history item.
2251        search_bar.update_in(cx, |search_bar, window, cx| {
2252            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2253        });
2254        search_bar.update(cx, |search_bar, cx| {
2255            assert_eq!(search_bar.query(cx), "a");
2256            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2257        });
2258        search_bar.update_in(cx, |search_bar, window, cx| {
2259            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2260        });
2261        search_bar.update(cx, |search_bar, cx| {
2262            assert_eq!(search_bar.query(cx), "a");
2263            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2264        });
2265
2266        // Next items should go over the history in the original order.
2267        search_bar.update_in(cx, |search_bar, window, cx| {
2268            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2269        });
2270        search_bar.update(cx, |search_bar, cx| {
2271            assert_eq!(search_bar.query(cx), "b");
2272            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2273        });
2274
2275        search_bar
2276            .update_in(cx, |search_bar, window, cx| {
2277                search_bar.search("ba", None, window, cx)
2278            })
2279            .await
2280            .unwrap();
2281        search_bar.update(cx, |search_bar, cx| {
2282            assert_eq!(search_bar.query(cx), "ba");
2283            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2284        });
2285
2286        // New search input should add another entry to history and move the selection to the end of the history.
2287        search_bar.update_in(cx, |search_bar, window, cx| {
2288            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2289        });
2290        search_bar.update(cx, |search_bar, cx| {
2291            assert_eq!(search_bar.query(cx), "c");
2292            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2293        });
2294        search_bar.update_in(cx, |search_bar, window, cx| {
2295            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2296        });
2297        search_bar.update(cx, |search_bar, cx| {
2298            assert_eq!(search_bar.query(cx), "b");
2299            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2300        });
2301        search_bar.update_in(cx, |search_bar, window, cx| {
2302            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2303        });
2304        search_bar.update(cx, |search_bar, cx| {
2305            assert_eq!(search_bar.query(cx), "c");
2306            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2307        });
2308        search_bar.update_in(cx, |search_bar, window, cx| {
2309            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2310        });
2311        search_bar.update(cx, |search_bar, cx| {
2312            assert_eq!(search_bar.query(cx), "ba");
2313            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2314        });
2315        search_bar.update_in(cx, |search_bar, window, cx| {
2316            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2317        });
2318        search_bar.update(cx, |search_bar, cx| {
2319            assert_eq!(search_bar.query(cx), "");
2320            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2321        });
2322    }
2323
2324    #[gpui::test]
2325    async fn test_replace_simple(cx: &mut TestAppContext) {
2326        let (editor, search_bar, cx) = init_test(cx);
2327
2328        search_bar
2329            .update_in(cx, |search_bar, window, cx| {
2330                search_bar.search("expression", None, window, cx)
2331            })
2332            .await
2333            .unwrap();
2334
2335        search_bar.update_in(cx, |search_bar, window, cx| {
2336            search_bar.replacement_editor.update(cx, |editor, cx| {
2337                // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally.
2338                editor.set_text("expr$1", window, cx);
2339            });
2340            search_bar.replace_all(&ReplaceAll, window, cx)
2341        });
2342        assert_eq!(
2343            editor.read_with(cx, |this, cx| { this.text(cx) }),
2344            r#"
2345        A regular expr$1 (shortened as regex or regexp;[1] also referred to as
2346        rational expr$1[2][3]) is a sequence of characters that specifies a search
2347        pattern in text. Usually such patterns are used by string-searching algorithms
2348        for "find" or "find and replace" operations on strings, or for input validation.
2349        "#
2350            .unindent()
2351        );
2352
2353        // Search for word boundaries and replace just a single one.
2354        search_bar
2355            .update_in(cx, |search_bar, window, cx| {
2356                search_bar.search("or", Some(SearchOptions::WHOLE_WORD), window, cx)
2357            })
2358            .await
2359            .unwrap();
2360
2361        search_bar.update_in(cx, |search_bar, window, cx| {
2362            search_bar.replacement_editor.update(cx, |editor, cx| {
2363                editor.set_text("banana", window, cx);
2364            });
2365            search_bar.replace_next(&ReplaceNext, window, cx)
2366        });
2367        // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text.
2368        assert_eq!(
2369            editor.read_with(cx, |this, cx| { this.text(cx) }),
2370            r#"
2371        A regular expr$1 (shortened as regex banana regexp;[1] also referred to as
2372        rational expr$1[2][3]) is a sequence of characters that specifies a search
2373        pattern in text. Usually such patterns are used by string-searching algorithms
2374        for "find" or "find and replace" operations on strings, or for input validation.
2375        "#
2376            .unindent()
2377        );
2378        // Let's turn on regex mode.
2379        search_bar
2380            .update_in(cx, |search_bar, window, cx| {
2381                search_bar.search("\\[([^\\]]+)\\]", Some(SearchOptions::REGEX), window, cx)
2382            })
2383            .await
2384            .unwrap();
2385        search_bar.update_in(cx, |search_bar, window, cx| {
2386            search_bar.replacement_editor.update(cx, |editor, cx| {
2387                editor.set_text("${1}number", window, cx);
2388            });
2389            search_bar.replace_all(&ReplaceAll, window, cx)
2390        });
2391        assert_eq!(
2392            editor.read_with(cx, |this, cx| { this.text(cx) }),
2393            r#"
2394        A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2395        rational expr$12number3number) is a sequence of characters that specifies a search
2396        pattern in text. Usually such patterns are used by string-searching algorithms
2397        for "find" or "find and replace" operations on strings, or for input validation.
2398        "#
2399            .unindent()
2400        );
2401        // Now with a whole-word twist.
2402        search_bar
2403            .update_in(cx, |search_bar, window, cx| {
2404                search_bar.search(
2405                    "a\\w+s",
2406                    Some(SearchOptions::REGEX | SearchOptions::WHOLE_WORD),
2407                    window,
2408                    cx,
2409                )
2410            })
2411            .await
2412            .unwrap();
2413        search_bar.update_in(cx, |search_bar, window, cx| {
2414            search_bar.replacement_editor.update(cx, |editor, cx| {
2415                editor.set_text("things", window, cx);
2416            });
2417            search_bar.replace_all(&ReplaceAll, window, cx)
2418        });
2419        // The only word affected by this edit should be `algorithms`, even though there's a bunch
2420        // of words in this text that would match this regex if not for WHOLE_WORD.
2421        assert_eq!(
2422            editor.read_with(cx, |this, cx| { this.text(cx) }),
2423            r#"
2424        A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2425        rational expr$12number3number) is a sequence of characters that specifies a search
2426        pattern in text. Usually such patterns are used by string-searching things
2427        for "find" or "find and replace" operations on strings, or for input validation.
2428        "#
2429            .unindent()
2430        );
2431    }
2432
2433    struct ReplacementTestParams<'a> {
2434        editor: &'a Entity<Editor>,
2435        search_bar: &'a Entity<BufferSearchBar>,
2436        cx: &'a mut VisualTestContext,
2437        search_text: &'static str,
2438        search_options: Option<SearchOptions>,
2439        replacement_text: &'static str,
2440        replace_all: bool,
2441        expected_text: String,
2442    }
2443
2444    async fn run_replacement_test(options: ReplacementTestParams<'_>) {
2445        options
2446            .search_bar
2447            .update_in(options.cx, |search_bar, window, cx| {
2448                if let Some(options) = options.search_options {
2449                    search_bar.set_search_options(options, cx);
2450                }
2451                search_bar.search(options.search_text, options.search_options, window, cx)
2452            })
2453            .await
2454            .unwrap();
2455
2456        options
2457            .search_bar
2458            .update_in(options.cx, |search_bar, window, cx| {
2459                search_bar.replacement_editor.update(cx, |editor, cx| {
2460                    editor.set_text(options.replacement_text, window, cx);
2461                });
2462
2463                if options.replace_all {
2464                    search_bar.replace_all(&ReplaceAll, window, cx)
2465                } else {
2466                    search_bar.replace_next(&ReplaceNext, window, cx)
2467                }
2468            });
2469
2470        assert_eq!(
2471            options
2472                .editor
2473                .read_with(options.cx, |this, cx| { this.text(cx) }),
2474            options.expected_text
2475        );
2476    }
2477
2478    #[gpui::test]
2479    async fn test_replace_special_characters(cx: &mut TestAppContext) {
2480        let (editor, search_bar, cx) = init_test(cx);
2481
2482        run_replacement_test(ReplacementTestParams {
2483            editor: &editor,
2484            search_bar: &search_bar,
2485            cx,
2486            search_text: "expression",
2487            search_options: None,
2488            replacement_text: r"\n",
2489            replace_all: true,
2490            expected_text: r#"
2491            A regular \n (shortened as regex or regexp;[1] also referred to as
2492            rational \n[2][3]) is a sequence of characters that specifies a search
2493            pattern in text. Usually such patterns are used by string-searching algorithms
2494            for "find" or "find and replace" operations on strings, or for input validation.
2495            "#
2496            .unindent(),
2497        })
2498        .await;
2499
2500        run_replacement_test(ReplacementTestParams {
2501            editor: &editor,
2502            search_bar: &search_bar,
2503            cx,
2504            search_text: "or",
2505            search_options: Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
2506            replacement_text: r"\\\n\\\\",
2507            replace_all: false,
2508            expected_text: r#"
2509            A regular \n (shortened as regex \
2510            \\ regexp;[1] also referred to as
2511            rational \n[2][3]) is a sequence of characters that specifies a search
2512            pattern in text. Usually such patterns are used by string-searching algorithms
2513            for "find" or "find and replace" operations on strings, or for input validation.
2514            "#
2515            .unindent(),
2516        })
2517        .await;
2518
2519        run_replacement_test(ReplacementTestParams {
2520            editor: &editor,
2521            search_bar: &search_bar,
2522            cx,
2523            search_text: r"(that|used) ",
2524            search_options: Some(SearchOptions::REGEX),
2525            replacement_text: r"$1\n",
2526            replace_all: true,
2527            expected_text: r#"
2528            A regular \n (shortened as regex \
2529            \\ regexp;[1] also referred to as
2530            rational \n[2][3]) is a sequence of characters that
2531            specifies a search
2532            pattern in text. Usually such patterns are used
2533            by string-searching algorithms
2534            for "find" or "find and replace" operations on strings, or for input validation.
2535            "#
2536            .unindent(),
2537        })
2538        .await;
2539    }
2540
2541    #[gpui::test]
2542    async fn test_find_matches_in_selections_singleton_buffer_multiple_selections(
2543        cx: &mut TestAppContext,
2544    ) {
2545        init_globals(cx);
2546        let buffer = cx.new(|cx| {
2547            Buffer::local(
2548                r#"
2549                aaa bbb aaa ccc
2550                aaa bbb aaa ccc
2551                aaa bbb aaa ccc
2552                aaa bbb aaa ccc
2553                aaa bbb aaa ccc
2554                aaa bbb aaa ccc
2555                "#
2556                .unindent(),
2557                cx,
2558            )
2559        });
2560        let cx = cx.add_empty_window();
2561        let editor =
2562            cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
2563
2564        let search_bar = cx.new_window_entity(|window, cx| {
2565            let mut search_bar = BufferSearchBar::new(None, window, cx);
2566            search_bar.set_active_pane_item(Some(&editor), window, cx);
2567            search_bar.show(window, cx);
2568            search_bar
2569        });
2570
2571        editor.update_in(cx, |editor, window, cx| {
2572            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2573                s.select_ranges(vec![Point::new(1, 0)..Point::new(2, 4)])
2574            })
2575        });
2576
2577        search_bar.update_in(cx, |search_bar, window, cx| {
2578            let deploy = Deploy {
2579                focus: true,
2580                replace_enabled: false,
2581                selection_search_enabled: true,
2582            };
2583            search_bar.deploy(&deploy, window, cx);
2584        });
2585
2586        cx.run_until_parked();
2587
2588        search_bar
2589            .update_in(cx, |search_bar, window, cx| {
2590                search_bar.search("aaa", None, window, cx)
2591            })
2592            .await
2593            .unwrap();
2594
2595        editor.update(cx, |editor, cx| {
2596            assert_eq!(
2597                editor.search_background_highlights(cx),
2598                &[
2599                    Point::new(1, 0)..Point::new(1, 3),
2600                    Point::new(1, 8)..Point::new(1, 11),
2601                    Point::new(2, 0)..Point::new(2, 3),
2602                ]
2603            );
2604        });
2605    }
2606
2607    #[gpui::test]
2608    async fn test_find_matches_in_selections_multiple_excerpts_buffer_multiple_selections(
2609        cx: &mut TestAppContext,
2610    ) {
2611        init_globals(cx);
2612        let text = r#"
2613            aaa bbb aaa ccc
2614            aaa bbb aaa ccc
2615            aaa bbb aaa ccc
2616            aaa bbb aaa ccc
2617            aaa bbb aaa ccc
2618            aaa bbb aaa ccc
2619
2620            aaa bbb aaa ccc
2621            aaa bbb aaa ccc
2622            aaa bbb aaa ccc
2623            aaa bbb aaa ccc
2624            aaa bbb aaa ccc
2625            aaa bbb aaa ccc
2626            "#
2627        .unindent();
2628
2629        let cx = cx.add_empty_window();
2630        let editor = cx.new_window_entity(|window, cx| {
2631            let multibuffer = MultiBuffer::build_multi(
2632                [
2633                    (
2634                        &text,
2635                        vec![
2636                            Point::new(0, 0)..Point::new(2, 0),
2637                            Point::new(4, 0)..Point::new(5, 0),
2638                        ],
2639                    ),
2640                    (&text, vec![Point::new(9, 0)..Point::new(11, 0)]),
2641                ],
2642                cx,
2643            );
2644            Editor::for_multibuffer(multibuffer, None, window, cx)
2645        });
2646
2647        let search_bar = cx.new_window_entity(|window, cx| {
2648            let mut search_bar = BufferSearchBar::new(None, window, cx);
2649            search_bar.set_active_pane_item(Some(&editor), window, cx);
2650            search_bar.show(window, cx);
2651            search_bar
2652        });
2653
2654        editor.update_in(cx, |editor, window, cx| {
2655            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2656                s.select_ranges(vec![
2657                    Point::new(1, 0)..Point::new(1, 4),
2658                    Point::new(5, 3)..Point::new(6, 4),
2659                ])
2660            })
2661        });
2662
2663        search_bar.update_in(cx, |search_bar, window, cx| {
2664            let deploy = Deploy {
2665                focus: true,
2666                replace_enabled: false,
2667                selection_search_enabled: true,
2668            };
2669            search_bar.deploy(&deploy, window, cx);
2670        });
2671
2672        cx.run_until_parked();
2673
2674        search_bar
2675            .update_in(cx, |search_bar, window, cx| {
2676                search_bar.search("aaa", None, window, cx)
2677            })
2678            .await
2679            .unwrap();
2680
2681        editor.update(cx, |editor, cx| {
2682            assert_eq!(
2683                editor.search_background_highlights(cx),
2684                &[
2685                    Point::new(1, 0)..Point::new(1, 3),
2686                    Point::new(5, 8)..Point::new(5, 11),
2687                    Point::new(6, 0)..Point::new(6, 3),
2688                ]
2689            );
2690        });
2691    }
2692
2693    #[gpui::test]
2694    async fn test_invalid_regexp_search_after_valid(cx: &mut TestAppContext) {
2695        let (editor, search_bar, cx) = init_test(cx);
2696        // Search using valid regexp
2697        search_bar
2698            .update_in(cx, |search_bar, window, cx| {
2699                search_bar.enable_search_option(SearchOptions::REGEX, window, cx);
2700                search_bar.search("expression", None, window, cx)
2701            })
2702            .await
2703            .unwrap();
2704        editor.update_in(cx, |editor, window, cx| {
2705            assert_eq!(
2706                display_points_of(editor.all_text_background_highlights(window, cx)),
2707                &[
2708                    DisplayPoint::new(DisplayRow(0), 10)..DisplayPoint::new(DisplayRow(0), 20),
2709                    DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 19),
2710                ],
2711            );
2712        });
2713
2714        // Now, the expression is invalid
2715        search_bar
2716            .update_in(cx, |search_bar, window, cx| {
2717                search_bar.search("expression (", None, window, cx)
2718            })
2719            .await
2720            .unwrap_err();
2721        editor.update_in(cx, |editor, window, cx| {
2722            assert!(
2723                display_points_of(editor.all_text_background_highlights(window, cx)).is_empty(),
2724            );
2725        });
2726    }
2727
2728    #[gpui::test]
2729    async fn test_search_options_changes(cx: &mut TestAppContext) {
2730        let (_editor, search_bar, cx) = init_test(cx);
2731        update_search_settings(
2732            SearchSettings {
2733                button: true,
2734                whole_word: false,
2735                case_sensitive: false,
2736                include_ignored: false,
2737                regex: false,
2738            },
2739            cx,
2740        );
2741
2742        let deploy = Deploy {
2743            focus: true,
2744            replace_enabled: false,
2745            selection_search_enabled: true,
2746        };
2747
2748        search_bar.update_in(cx, |search_bar, window, cx| {
2749            assert_eq!(
2750                search_bar.search_options,
2751                SearchOptions::NONE,
2752                "Should have no search options enabled by default"
2753            );
2754            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
2755            assert_eq!(
2756                search_bar.search_options,
2757                SearchOptions::WHOLE_WORD,
2758                "Should enable the option toggled"
2759            );
2760            assert!(
2761                !search_bar.dismissed,
2762                "Search bar should be present and visible"
2763            );
2764            search_bar.deploy(&deploy, window, cx);
2765            assert_eq!(
2766                search_bar.configured_options,
2767                SearchOptions::NONE,
2768                "Should have configured search options matching the settings"
2769            );
2770            assert_eq!(
2771                search_bar.search_options,
2772                SearchOptions::WHOLE_WORD,
2773                "After (re)deploying, the option should still be enabled"
2774            );
2775
2776            search_bar.dismiss(&Dismiss, window, cx);
2777            search_bar.deploy(&deploy, window, cx);
2778            assert_eq!(
2779                search_bar.search_options,
2780                SearchOptions::NONE,
2781                "After hiding and showing the search bar, default options should be used"
2782            );
2783
2784            search_bar.toggle_search_option(SearchOptions::REGEX, window, cx);
2785            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
2786            assert_eq!(
2787                search_bar.search_options,
2788                SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2789                "Should enable the options toggled"
2790            );
2791            assert!(
2792                !search_bar.dismissed,
2793                "Search bar should be present and visible"
2794            );
2795        });
2796
2797        update_search_settings(
2798            SearchSettings {
2799                button: true,
2800                whole_word: false,
2801                case_sensitive: true,
2802                include_ignored: false,
2803                regex: false,
2804            },
2805            cx,
2806        );
2807        search_bar.update_in(cx, |search_bar, window, cx| {
2808            assert_eq!(
2809                search_bar.search_options,
2810                SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2811                "Should have no search options enabled by default"
2812            );
2813
2814            search_bar.deploy(&deploy, window, cx);
2815            assert_eq!(
2816                search_bar.configured_options,
2817                SearchOptions::CASE_SENSITIVE,
2818                "Should have configured search options matching the settings"
2819            );
2820            assert_eq!(
2821                search_bar.search_options,
2822                SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2823                "Toggling a non-dismissed search bar with custom options should not change the default options"
2824            );
2825            search_bar.dismiss(&Dismiss, window, cx);
2826            search_bar.deploy(&deploy, window, cx);
2827            assert_eq!(
2828                search_bar.search_options,
2829                SearchOptions::CASE_SENSITIVE,
2830                "After hiding and showing the search bar, default options should be used"
2831            );
2832        });
2833    }
2834
2835    fn update_search_settings(search_settings: SearchSettings, cx: &mut TestAppContext) {
2836        cx.update(|cx| {
2837            SettingsStore::update_global(cx, |store, cx| {
2838                store.update_user_settings::<EditorSettings>(cx, |settings| {
2839                    settings.search = Some(search_settings);
2840                });
2841            });
2842        });
2843    }
2844}