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