buffer_search.rs

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