buffer_search.rs

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