buffer_search.rs

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