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