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