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, cx);
 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, cx);
 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        // Search -> Replace -> Editor
1299        let focus_handle = if self.replace_enabled && self.query_editor_focused {
1300            self.replacement_editor.focus_handle(cx)
1301        } else if let Some(item) = self.active_searchable_item.as_ref() {
1302            item.item_focus_handle(cx)
1303        } else {
1304            return;
1305        };
1306        self.focus(&focus_handle, window, cx);
1307        cx.stop_propagation();
1308    }
1309
1310    fn backtab(&mut self, _: &Backtab, window: &mut Window, cx: &mut Context<Self>) {
1311        // Search -> Replace -> Search
1312        let focus_handle = if self.replace_enabled && self.query_editor_focused {
1313            self.replacement_editor.focus_handle(cx)
1314        } else if self.replacement_editor_focused {
1315            self.query_editor.focus_handle(cx)
1316        } else {
1317            return;
1318        };
1319        self.focus(&focus_handle, window, cx);
1320        cx.stop_propagation();
1321    }
1322
1323    fn next_history_query(
1324        &mut self,
1325        _: &NextHistoryQuery,
1326        window: &mut Window,
1327        cx: &mut Context<Self>,
1328    ) {
1329        if let Some(new_query) = self
1330            .search_history
1331            .next(&mut self.search_history_cursor)
1332            .map(str::to_string)
1333        {
1334            drop(self.search(&new_query, Some(self.search_options), window, cx));
1335        } else {
1336            self.search_history_cursor.reset();
1337            drop(self.search("", Some(self.search_options), window, cx));
1338        }
1339    }
1340
1341    fn previous_history_query(
1342        &mut self,
1343        _: &PreviousHistoryQuery,
1344        window: &mut Window,
1345        cx: &mut Context<Self>,
1346    ) {
1347        if self.query(cx).is_empty() {
1348            if let Some(new_query) = self
1349                .search_history
1350                .current(&mut self.search_history_cursor)
1351                .map(str::to_string)
1352            {
1353                drop(self.search(&new_query, Some(self.search_options), window, cx));
1354                return;
1355            }
1356        }
1357
1358        if let Some(new_query) = self
1359            .search_history
1360            .previous(&mut self.search_history_cursor)
1361            .map(str::to_string)
1362        {
1363            drop(self.search(&new_query, Some(self.search_options), window, cx));
1364        }
1365    }
1366
1367    fn focus(&self, handle: &gpui::FocusHandle, window: &mut Window, cx: &mut Context<Self>) {
1368        cx.on_next_frame(window, |_, window, _| {
1369            window.invalidate_character_coordinates();
1370        });
1371        window.focus(handle);
1372    }
1373
1374    fn toggle_replace(&mut self, _: &ToggleReplace, window: &mut Window, cx: &mut Context<Self>) {
1375        if self.active_searchable_item.is_some() {
1376            self.replace_enabled = !self.replace_enabled;
1377            let handle = if self.replace_enabled {
1378                self.replacement_editor.focus_handle(cx)
1379            } else {
1380                self.query_editor.focus_handle(cx)
1381            };
1382            self.focus(&handle, window, cx);
1383            cx.notify();
1384        }
1385    }
1386
1387    fn replace_next(&mut self, _: &ReplaceNext, window: &mut Window, cx: &mut Context<Self>) {
1388        let mut should_propagate = true;
1389        if !self.dismissed && self.active_search.is_some() {
1390            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1391                if let Some(query) = self.active_search.as_ref() {
1392                    if let Some(matches) = self
1393                        .searchable_items_with_matches
1394                        .get(&searchable_item.downgrade())
1395                    {
1396                        if let Some(active_index) = self.active_match_index {
1397                            let query = query
1398                                .as_ref()
1399                                .clone()
1400                                .with_replacement(self.replacement(cx));
1401                            searchable_item.replace(matches.at(active_index), &query, window, cx);
1402                            self.select_next_match(&SelectNextMatch, window, cx);
1403                        }
1404                        should_propagate = false;
1405                    }
1406                }
1407            }
1408        }
1409        if !should_propagate {
1410            cx.stop_propagation();
1411        }
1412    }
1413
1414    pub fn replace_all(&mut self, _: &ReplaceAll, window: &mut Window, cx: &mut Context<Self>) {
1415        if !self.dismissed && self.active_search.is_some() {
1416            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1417                if let Some(query) = self.active_search.as_ref() {
1418                    if let Some(matches) = self
1419                        .searchable_items_with_matches
1420                        .get(&searchable_item.downgrade())
1421                    {
1422                        let query = query
1423                            .as_ref()
1424                            .clone()
1425                            .with_replacement(self.replacement(cx));
1426                        searchable_item.replace_all(&mut matches.iter(), &query, window, cx);
1427                    }
1428                }
1429            }
1430        }
1431    }
1432
1433    pub fn match_exists(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
1434        self.update_match_index(window, cx);
1435        self.active_match_index.is_some()
1436    }
1437
1438    pub fn should_use_smartcase_search(&mut self, cx: &mut Context<Self>) -> bool {
1439        EditorSettings::get_global(cx).use_smartcase_search
1440    }
1441
1442    pub fn is_contains_uppercase(&mut self, str: &String) -> bool {
1443        str.chars().any(|c| c.is_uppercase())
1444    }
1445
1446    fn smartcase(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1447        if self.should_use_smartcase_search(cx) {
1448            let query = self.query(cx);
1449            if !query.is_empty() {
1450                let is_case = self.is_contains_uppercase(&query);
1451                if self.has_search_option(SearchOptions::CASE_SENSITIVE) != is_case {
1452                    self.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
1453                }
1454            }
1455        }
1456    }
1457
1458    fn adjust_query_regex_language(&self, cx: &mut App) {
1459        let enable = self.search_options.contains(SearchOptions::REGEX);
1460        let query_buffer = self
1461            .query_editor
1462            .read(cx)
1463            .buffer()
1464            .read(cx)
1465            .as_singleton()
1466            .expect("query editor should be backed by a singleton buffer");
1467        if enable {
1468            if let Some(regex_language) = self.regex_language.clone() {
1469                query_buffer.update(cx, |query_buffer, cx| {
1470                    query_buffer.set_language(Some(regex_language), cx);
1471                })
1472            }
1473        } else {
1474            query_buffer.update(cx, |query_buffer, cx| {
1475                query_buffer.set_language(None, cx);
1476            })
1477        }
1478    }
1479}
1480
1481#[cfg(test)]
1482mod tests {
1483    use std::ops::Range;
1484
1485    use super::*;
1486    use editor::{
1487        DisplayPoint, Editor, MultiBuffer, SearchSettings, SelectionEffects,
1488        display_map::DisplayRow,
1489    };
1490    use gpui::{Hsla, TestAppContext, UpdateGlobal, VisualTestContext};
1491    use language::{Buffer, Point};
1492    use project::Project;
1493    use settings::SettingsStore;
1494    use smol::stream::StreamExt as _;
1495    use unindent::Unindent as _;
1496
1497    fn init_globals(cx: &mut TestAppContext) {
1498        cx.update(|cx| {
1499            let store = settings::SettingsStore::test(cx);
1500            cx.set_global(store);
1501            workspace::init_settings(cx);
1502            editor::init(cx);
1503
1504            language::init(cx);
1505            Project::init_settings(cx);
1506            theme::init(theme::LoadThemes::JustBase, cx);
1507            crate::init(cx);
1508        });
1509    }
1510
1511    fn init_test(
1512        cx: &mut TestAppContext,
1513    ) -> (
1514        Entity<Editor>,
1515        Entity<BufferSearchBar>,
1516        &mut VisualTestContext,
1517    ) {
1518        init_globals(cx);
1519        let buffer = cx.new(|cx| {
1520            Buffer::local(
1521                r#"
1522                A regular expression (shortened as regex or regexp;[1] also referred to as
1523                rational expression[2][3]) is a sequence of characters that specifies a search
1524                pattern in text. Usually such patterns are used by string-searching algorithms
1525                for "find" or "find and replace" operations on strings, or for input validation.
1526                "#
1527                .unindent(),
1528                cx,
1529            )
1530        });
1531        let cx = cx.add_empty_window();
1532        let editor =
1533            cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
1534
1535        let search_bar = cx.new_window_entity(|window, cx| {
1536            let mut search_bar = BufferSearchBar::new(None, window, cx);
1537            search_bar.set_active_pane_item(Some(&editor), window, cx);
1538            search_bar.show(window, cx);
1539            search_bar
1540        });
1541
1542        (editor, search_bar, cx)
1543    }
1544
1545    #[gpui::test]
1546    async fn test_search_simple(cx: &mut TestAppContext) {
1547        let (editor, search_bar, cx) = init_test(cx);
1548        let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
1549            background_highlights
1550                .into_iter()
1551                .map(|(range, _)| range)
1552                .collect::<Vec<_>>()
1553        };
1554        // Search for a string that appears with different casing.
1555        // By default, search is case-insensitive.
1556        search_bar
1557            .update_in(cx, |search_bar, window, cx| {
1558                search_bar.search("us", None, window, cx)
1559            })
1560            .await
1561            .unwrap();
1562        editor.update_in(cx, |editor, window, cx| {
1563            assert_eq!(
1564                display_points_of(editor.all_text_background_highlights(window, cx)),
1565                &[
1566                    DisplayPoint::new(DisplayRow(2), 17)..DisplayPoint::new(DisplayRow(2), 19),
1567                    DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),
1568                ]
1569            );
1570        });
1571
1572        // Switch to a case sensitive search.
1573        search_bar.update_in(cx, |search_bar, window, cx| {
1574            search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
1575        });
1576        let mut editor_notifications = cx.notifications(&editor);
1577        editor_notifications.next().await;
1578        editor.update_in(cx, |editor, window, cx| {
1579            assert_eq!(
1580                display_points_of(editor.all_text_background_highlights(window, cx)),
1581                &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
1582            );
1583        });
1584
1585        // Search for a string that appears both as a whole word and
1586        // within other words. By default, all results are found.
1587        search_bar
1588            .update_in(cx, |search_bar, window, cx| {
1589                search_bar.search("or", None, window, cx)
1590            })
1591            .await
1592            .unwrap();
1593        editor.update_in(cx, |editor, window, cx| {
1594            assert_eq!(
1595                display_points_of(editor.all_text_background_highlights(window, cx)),
1596                &[
1597                    DisplayPoint::new(DisplayRow(0), 24)..DisplayPoint::new(DisplayRow(0), 26),
1598                    DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1599                    DisplayPoint::new(DisplayRow(2), 71)..DisplayPoint::new(DisplayRow(2), 73),
1600                    DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 3),
1601                    DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
1602                    DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
1603                    DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 62),
1604                ]
1605            );
1606        });
1607
1608        // Switch to a whole word search.
1609        search_bar.update_in(cx, |search_bar, window, cx| {
1610            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
1611        });
1612        let mut editor_notifications = cx.notifications(&editor);
1613        editor_notifications.next().await;
1614        editor.update_in(cx, |editor, window, cx| {
1615            assert_eq!(
1616                display_points_of(editor.all_text_background_highlights(window, cx)),
1617                &[
1618                    DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1619                    DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
1620                    DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
1621                ]
1622            );
1623        });
1624
1625        editor.update_in(cx, |editor, window, cx| {
1626            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1627                s.select_display_ranges([
1628                    DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
1629                ])
1630            });
1631        });
1632        search_bar.update_in(cx, |search_bar, window, cx| {
1633            assert_eq!(search_bar.active_match_index, Some(0));
1634            search_bar.select_next_match(&SelectNextMatch, window, cx);
1635            assert_eq!(
1636                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1637                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1638            );
1639        });
1640        search_bar.read_with(cx, |search_bar, _| {
1641            assert_eq!(search_bar.active_match_index, Some(0));
1642        });
1643
1644        search_bar.update_in(cx, |search_bar, window, cx| {
1645            search_bar.select_next_match(&SelectNextMatch, window, cx);
1646            assert_eq!(
1647                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1648                [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1649            );
1650        });
1651        search_bar.read_with(cx, |search_bar, _| {
1652            assert_eq!(search_bar.active_match_index, Some(1));
1653        });
1654
1655        search_bar.update_in(cx, |search_bar, window, cx| {
1656            search_bar.select_next_match(&SelectNextMatch, window, cx);
1657            assert_eq!(
1658                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1659                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1660            );
1661        });
1662        search_bar.read_with(cx, |search_bar, _| {
1663            assert_eq!(search_bar.active_match_index, Some(2));
1664        });
1665
1666        search_bar.update_in(cx, |search_bar, window, cx| {
1667            search_bar.select_next_match(&SelectNextMatch, window, cx);
1668            assert_eq!(
1669                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1670                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1671            );
1672        });
1673        search_bar.read_with(cx, |search_bar, _| {
1674            assert_eq!(search_bar.active_match_index, Some(0));
1675        });
1676
1677        search_bar.update_in(cx, |search_bar, window, cx| {
1678            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1679            assert_eq!(
1680                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1681                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1682            );
1683        });
1684        search_bar.read_with(cx, |search_bar, _| {
1685            assert_eq!(search_bar.active_match_index, Some(2));
1686        });
1687
1688        search_bar.update_in(cx, |search_bar, window, cx| {
1689            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1690            assert_eq!(
1691                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1692                [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1693            );
1694        });
1695        search_bar.read_with(cx, |search_bar, _| {
1696            assert_eq!(search_bar.active_match_index, Some(1));
1697        });
1698
1699        search_bar.update_in(cx, |search_bar, window, cx| {
1700            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1701            assert_eq!(
1702                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1703                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1704            );
1705        });
1706        search_bar.read_with(cx, |search_bar, _| {
1707            assert_eq!(search_bar.active_match_index, Some(0));
1708        });
1709
1710        // Park the cursor in between matches and ensure that going to the previous match selects
1711        // the closest match to the left.
1712        editor.update_in(cx, |editor, window, cx| {
1713            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1714                s.select_display_ranges([
1715                    DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
1716                ])
1717            });
1718        });
1719        search_bar.update_in(cx, |search_bar, window, cx| {
1720            assert_eq!(search_bar.active_match_index, Some(1));
1721            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1722            assert_eq!(
1723                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1724                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1725            );
1726        });
1727        search_bar.read_with(cx, |search_bar, _| {
1728            assert_eq!(search_bar.active_match_index, Some(0));
1729        });
1730
1731        // Park the cursor in between matches and ensure that going to the next match selects the
1732        // closest match to the right.
1733        editor.update_in(cx, |editor, window, cx| {
1734            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1735                s.select_display_ranges([
1736                    DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
1737                ])
1738            });
1739        });
1740        search_bar.update_in(cx, |search_bar, window, cx| {
1741            assert_eq!(search_bar.active_match_index, Some(1));
1742            search_bar.select_next_match(&SelectNextMatch, window, cx);
1743            assert_eq!(
1744                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1745                [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1746            );
1747        });
1748        search_bar.read_with(cx, |search_bar, _| {
1749            assert_eq!(search_bar.active_match_index, Some(1));
1750        });
1751
1752        // Park the cursor after the last match and ensure that going to the previous match selects
1753        // the last match.
1754        editor.update_in(cx, |editor, window, cx| {
1755            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1756                s.select_display_ranges([
1757                    DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
1758                ])
1759            });
1760        });
1761        search_bar.update_in(cx, |search_bar, window, cx| {
1762            assert_eq!(search_bar.active_match_index, Some(2));
1763            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1764            assert_eq!(
1765                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1766                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1767            );
1768        });
1769        search_bar.read_with(cx, |search_bar, _| {
1770            assert_eq!(search_bar.active_match_index, Some(2));
1771        });
1772
1773        // Park the cursor after the last match and ensure that going to the next match selects the
1774        // first match.
1775        editor.update_in(cx, |editor, window, cx| {
1776            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1777                s.select_display_ranges([
1778                    DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
1779                ])
1780            });
1781        });
1782        search_bar.update_in(cx, |search_bar, window, cx| {
1783            assert_eq!(search_bar.active_match_index, Some(2));
1784            search_bar.select_next_match(&SelectNextMatch, window, cx);
1785            assert_eq!(
1786                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1787                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1788            );
1789        });
1790        search_bar.read_with(cx, |search_bar, _| {
1791            assert_eq!(search_bar.active_match_index, Some(0));
1792        });
1793
1794        // Park the cursor before the first match and ensure that going to the previous match
1795        // selects the last match.
1796        editor.update_in(cx, |editor, window, cx| {
1797            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1798                s.select_display_ranges([
1799                    DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
1800                ])
1801            });
1802        });
1803        search_bar.update_in(cx, |search_bar, window, cx| {
1804            assert_eq!(search_bar.active_match_index, Some(0));
1805            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1806            assert_eq!(
1807                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1808                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1809            );
1810        });
1811        search_bar.read_with(cx, |search_bar, _| {
1812            assert_eq!(search_bar.active_match_index, Some(2));
1813        });
1814    }
1815
1816    fn display_points_of(
1817        background_highlights: Vec<(Range<DisplayPoint>, Hsla)>,
1818    ) -> Vec<Range<DisplayPoint>> {
1819        background_highlights
1820            .into_iter()
1821            .map(|(range, _)| range)
1822            .collect::<Vec<_>>()
1823    }
1824
1825    #[gpui::test]
1826    async fn test_search_option_handling(cx: &mut TestAppContext) {
1827        let (editor, search_bar, cx) = init_test(cx);
1828
1829        // show with options should make current search case sensitive
1830        search_bar
1831            .update_in(cx, |search_bar, window, cx| {
1832                search_bar.show(window, cx);
1833                search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), window, cx)
1834            })
1835            .await
1836            .unwrap();
1837        editor.update_in(cx, |editor, window, cx| {
1838            assert_eq!(
1839                display_points_of(editor.all_text_background_highlights(window, cx)),
1840                &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
1841            );
1842        });
1843
1844        // search_suggested should restore default options
1845        search_bar.update_in(cx, |search_bar, window, cx| {
1846            search_bar.search_suggested(window, cx);
1847            assert_eq!(search_bar.search_options, SearchOptions::NONE)
1848        });
1849
1850        // toggling a search option should update the defaults
1851        search_bar
1852            .update_in(cx, |search_bar, window, cx| {
1853                search_bar.search("regex", Some(SearchOptions::CASE_SENSITIVE), window, cx)
1854            })
1855            .await
1856            .unwrap();
1857        search_bar.update_in(cx, |search_bar, window, cx| {
1858            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx)
1859        });
1860        let mut editor_notifications = cx.notifications(&editor);
1861        editor_notifications.next().await;
1862        editor.update_in(cx, |editor, window, cx| {
1863            assert_eq!(
1864                display_points_of(editor.all_text_background_highlights(window, cx)),
1865                &[DisplayPoint::new(DisplayRow(0), 35)..DisplayPoint::new(DisplayRow(0), 40),]
1866            );
1867        });
1868
1869        // defaults should still include whole word
1870        search_bar.update_in(cx, |search_bar, window, cx| {
1871            search_bar.search_suggested(window, cx);
1872            assert_eq!(
1873                search_bar.search_options,
1874                SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD
1875            )
1876        });
1877    }
1878
1879    #[gpui::test]
1880    async fn test_search_select_all_matches(cx: &mut TestAppContext) {
1881        init_globals(cx);
1882        let buffer_text = r#"
1883        A regular expression (shortened as regex or regexp;[1] also referred to as
1884        rational expression[2][3]) is a sequence of characters that specifies a search
1885        pattern in text. Usually such patterns are used by string-searching algorithms
1886        for "find" or "find and replace" operations on strings, or for input validation.
1887        "#
1888        .unindent();
1889        let expected_query_matches_count = buffer_text
1890            .chars()
1891            .filter(|c| c.eq_ignore_ascii_case(&'a'))
1892            .count();
1893        assert!(
1894            expected_query_matches_count > 1,
1895            "Should pick a query with multiple results"
1896        );
1897        let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
1898        let window = cx.add_window(|_, _| gpui::Empty);
1899
1900        let editor = window.build_entity(cx, |window, cx| {
1901            Editor::for_buffer(buffer.clone(), None, window, cx)
1902        });
1903
1904        let search_bar = window.build_entity(cx, |window, cx| {
1905            let mut search_bar = BufferSearchBar::new(None, window, cx);
1906            search_bar.set_active_pane_item(Some(&editor), window, cx);
1907            search_bar.show(window, cx);
1908            search_bar
1909        });
1910
1911        window
1912            .update(cx, |_, window, cx| {
1913                search_bar.update(cx, |search_bar, cx| {
1914                    search_bar.search("a", None, window, cx)
1915                })
1916            })
1917            .unwrap()
1918            .await
1919            .unwrap();
1920        let initial_selections = window
1921            .update(cx, |_, window, cx| {
1922                search_bar.update(cx, |search_bar, cx| {
1923                    let handle = search_bar.query_editor.focus_handle(cx);
1924                    window.focus(&handle);
1925                    search_bar.activate_current_match(window, cx);
1926                });
1927                assert!(
1928                    !editor.read(cx).is_focused(window),
1929                    "Initially, the editor should not be focused"
1930                );
1931                let initial_selections = editor.update(cx, |editor, cx| {
1932                    let initial_selections = editor.selections.display_ranges(cx);
1933                    assert_eq!(
1934                        initial_selections.len(), 1,
1935                        "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
1936                    );
1937                    initial_selections
1938                });
1939                search_bar.update(cx, |search_bar, cx| {
1940                    assert_eq!(search_bar.active_match_index, Some(0));
1941                    let handle = search_bar.query_editor.focus_handle(cx);
1942                    window.focus(&handle);
1943                    search_bar.select_all_matches(&SelectAllMatches, window, cx);
1944                });
1945                assert!(
1946                    editor.read(cx).is_focused(window),
1947                    "Should focus editor after successful SelectAllMatches"
1948                );
1949                search_bar.update(cx, |search_bar, cx| {
1950                    let all_selections =
1951                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1952                    assert_eq!(
1953                        all_selections.len(),
1954                        expected_query_matches_count,
1955                        "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1956                    );
1957                    assert_eq!(
1958                        search_bar.active_match_index,
1959                        Some(0),
1960                        "Match index should not change after selecting all matches"
1961                    );
1962                });
1963
1964                search_bar.update(cx, |this, cx| this.select_next_match(&SelectNextMatch, window, cx));
1965                initial_selections
1966            }).unwrap();
1967
1968        window
1969            .update(cx, |_, window, cx| {
1970                assert!(
1971                    editor.read(cx).is_focused(window),
1972                    "Should still have editor focused after SelectNextMatch"
1973                );
1974                search_bar.update(cx, |search_bar, cx| {
1975                    let all_selections =
1976                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1977                    assert_eq!(
1978                        all_selections.len(),
1979                        1,
1980                        "On next match, should deselect items and select the next match"
1981                    );
1982                    assert_ne!(
1983                        all_selections, initial_selections,
1984                        "Next match should be different from the first selection"
1985                    );
1986                    assert_eq!(
1987                        search_bar.active_match_index,
1988                        Some(1),
1989                        "Match index should be updated to the next one"
1990                    );
1991                    let handle = search_bar.query_editor.focus_handle(cx);
1992                    window.focus(&handle);
1993                    search_bar.select_all_matches(&SelectAllMatches, window, cx);
1994                });
1995            })
1996            .unwrap();
1997        window
1998            .update(cx, |_, window, cx| {
1999                assert!(
2000                    editor.read(cx).is_focused(window),
2001                    "Should focus editor after successful SelectAllMatches"
2002                );
2003                search_bar.update(cx, |search_bar, cx| {
2004                    let all_selections =
2005                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2006                    assert_eq!(
2007                    all_selections.len(),
2008                    expected_query_matches_count,
2009                    "Should select all `a` characters in the buffer, but got: {all_selections:?}"
2010                );
2011                    assert_eq!(
2012                        search_bar.active_match_index,
2013                        Some(1),
2014                        "Match index should not change after selecting all matches"
2015                    );
2016                });
2017                search_bar.update(cx, |search_bar, cx| {
2018                    search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2019                });
2020            })
2021            .unwrap();
2022        let last_match_selections = window
2023            .update(cx, |_, window, cx| {
2024                assert!(
2025                    editor.read(cx).is_focused(window),
2026                    "Should still have editor focused after SelectPreviousMatch"
2027                );
2028
2029                search_bar.update(cx, |search_bar, cx| {
2030                    let all_selections =
2031                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2032                    assert_eq!(
2033                        all_selections.len(),
2034                        1,
2035                        "On previous match, should deselect items and select the previous item"
2036                    );
2037                    assert_eq!(
2038                        all_selections, initial_selections,
2039                        "Previous match should be the same as the first selection"
2040                    );
2041                    assert_eq!(
2042                        search_bar.active_match_index,
2043                        Some(0),
2044                        "Match index should be updated to the previous one"
2045                    );
2046                    all_selections
2047                })
2048            })
2049            .unwrap();
2050
2051        window
2052            .update(cx, |_, window, cx| {
2053                search_bar.update(cx, |search_bar, cx| {
2054                    let handle = search_bar.query_editor.focus_handle(cx);
2055                    window.focus(&handle);
2056                    search_bar.search("abas_nonexistent_match", None, window, cx)
2057                })
2058            })
2059            .unwrap()
2060            .await
2061            .unwrap();
2062        window
2063            .update(cx, |_, window, cx| {
2064                search_bar.update(cx, |search_bar, cx| {
2065                    search_bar.select_all_matches(&SelectAllMatches, window, cx);
2066                });
2067                assert!(
2068                    editor.update(cx, |this, _cx| !this.is_focused(window)),
2069                    "Should not switch focus to editor if SelectAllMatches does not find any matches"
2070                );
2071                search_bar.update(cx, |search_bar, cx| {
2072                    let all_selections =
2073                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2074                    assert_eq!(
2075                        all_selections, last_match_selections,
2076                        "Should not select anything new if there are no matches"
2077                    );
2078                    assert!(
2079                        search_bar.active_match_index.is_none(),
2080                        "For no matches, there should be no active match index"
2081                    );
2082                });
2083            })
2084            .unwrap();
2085    }
2086
2087    #[gpui::test]
2088    async fn test_search_query_with_match_whole_word(cx: &mut TestAppContext) {
2089        init_globals(cx);
2090        let buffer_text = r#"
2091        self.buffer.update(cx, |buffer, cx| {
2092            buffer.edit(
2093                edits,
2094                Some(AutoindentMode::Block {
2095                    original_indent_columns,
2096                }),
2097                cx,
2098            )
2099        });
2100
2101        this.buffer.update(cx, |buffer, cx| {
2102            buffer.edit([(end_of_line..start_of_next_line, replace)], None, cx)
2103        });
2104        "#
2105        .unindent();
2106        let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
2107        let cx = cx.add_empty_window();
2108
2109        let editor =
2110            cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
2111
2112        let search_bar = cx.new_window_entity(|window, cx| {
2113            let mut search_bar = BufferSearchBar::new(None, window, cx);
2114            search_bar.set_active_pane_item(Some(&editor), window, cx);
2115            search_bar.show(window, cx);
2116            search_bar
2117        });
2118
2119        search_bar
2120            .update_in(cx, |search_bar, window, cx| {
2121                search_bar.search(
2122                    "edit\\(",
2123                    Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
2124                    window,
2125                    cx,
2126                )
2127            })
2128            .await
2129            .unwrap();
2130
2131        search_bar.update_in(cx, |search_bar, window, cx| {
2132            search_bar.select_all_matches(&SelectAllMatches, window, cx);
2133        });
2134        search_bar.update(cx, |_, cx| {
2135            let all_selections =
2136                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2137            assert_eq!(
2138                all_selections.len(),
2139                2,
2140                "Should select all `edit(` in the buffer, but got: {all_selections:?}"
2141            );
2142        });
2143
2144        search_bar
2145            .update_in(cx, |search_bar, window, cx| {
2146                search_bar.search(
2147                    "edit(",
2148                    Some(SearchOptions::WHOLE_WORD | SearchOptions::CASE_SENSITIVE),
2149                    window,
2150                    cx,
2151                )
2152            })
2153            .await
2154            .unwrap();
2155
2156        search_bar.update_in(cx, |search_bar, window, cx| {
2157            search_bar.select_all_matches(&SelectAllMatches, window, cx);
2158        });
2159        search_bar.update(cx, |_, cx| {
2160            let all_selections =
2161                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2162            assert_eq!(
2163                all_selections.len(),
2164                2,
2165                "Should select all `edit(` in the buffer, but got: {all_selections:?}"
2166            );
2167        });
2168    }
2169
2170    #[gpui::test]
2171    async fn test_search_query_history(cx: &mut TestAppContext) {
2172        init_globals(cx);
2173        let buffer_text = r#"
2174        A regular expression (shortened as regex or regexp;[1] also referred to as
2175        rational expression[2][3]) is a sequence of characters that specifies a search
2176        pattern in text. Usually such patterns are used by string-searching algorithms
2177        for "find" or "find and replace" operations on strings, or for input validation.
2178        "#
2179        .unindent();
2180        let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
2181        let cx = cx.add_empty_window();
2182
2183        let editor =
2184            cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
2185
2186        let search_bar = cx.new_window_entity(|window, cx| {
2187            let mut search_bar = BufferSearchBar::new(None, window, cx);
2188            search_bar.set_active_pane_item(Some(&editor), window, cx);
2189            search_bar.show(window, cx);
2190            search_bar
2191        });
2192
2193        // Add 3 search items into the history.
2194        search_bar
2195            .update_in(cx, |search_bar, window, cx| {
2196                search_bar.search("a", None, window, cx)
2197            })
2198            .await
2199            .unwrap();
2200        search_bar
2201            .update_in(cx, |search_bar, window, cx| {
2202                search_bar.search("b", None, window, cx)
2203            })
2204            .await
2205            .unwrap();
2206        search_bar
2207            .update_in(cx, |search_bar, window, cx| {
2208                search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), window, cx)
2209            })
2210            .await
2211            .unwrap();
2212        // Ensure that the latest search is active.
2213        search_bar.update(cx, |search_bar, cx| {
2214            assert_eq!(search_bar.query(cx), "c");
2215            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2216        });
2217
2218        // Next history query after the latest should set the query to the empty string.
2219        search_bar.update_in(cx, |search_bar, window, cx| {
2220            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2221        });
2222        search_bar.update(cx, |search_bar, cx| {
2223            assert_eq!(search_bar.query(cx), "");
2224            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2225        });
2226        search_bar.update_in(cx, |search_bar, window, cx| {
2227            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2228        });
2229        search_bar.update(cx, |search_bar, cx| {
2230            assert_eq!(search_bar.query(cx), "");
2231            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2232        });
2233
2234        // First previous query for empty current query should set the query to the latest.
2235        search_bar.update_in(cx, |search_bar, window, cx| {
2236            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2237        });
2238        search_bar.update(cx, |search_bar, cx| {
2239            assert_eq!(search_bar.query(cx), "c");
2240            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2241        });
2242
2243        // Further previous items should go over the history in reverse order.
2244        search_bar.update_in(cx, |search_bar, window, cx| {
2245            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2246        });
2247        search_bar.update(cx, |search_bar, cx| {
2248            assert_eq!(search_bar.query(cx), "b");
2249            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2250        });
2251
2252        // Previous items should never go behind the first history item.
2253        search_bar.update_in(cx, |search_bar, window, cx| {
2254            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2255        });
2256        search_bar.update(cx, |search_bar, cx| {
2257            assert_eq!(search_bar.query(cx), "a");
2258            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2259        });
2260        search_bar.update_in(cx, |search_bar, window, cx| {
2261            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2262        });
2263        search_bar.update(cx, |search_bar, cx| {
2264            assert_eq!(search_bar.query(cx), "a");
2265            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2266        });
2267
2268        // Next items should go over the history in the original order.
2269        search_bar.update_in(cx, |search_bar, window, cx| {
2270            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2271        });
2272        search_bar.update(cx, |search_bar, cx| {
2273            assert_eq!(search_bar.query(cx), "b");
2274            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2275        });
2276
2277        search_bar
2278            .update_in(cx, |search_bar, window, cx| {
2279                search_bar.search("ba", None, window, cx)
2280            })
2281            .await
2282            .unwrap();
2283        search_bar.update(cx, |search_bar, cx| {
2284            assert_eq!(search_bar.query(cx), "ba");
2285            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2286        });
2287
2288        // New search input should add another entry to history and move the selection to the end of the history.
2289        search_bar.update_in(cx, |search_bar, window, cx| {
2290            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2291        });
2292        search_bar.update(cx, |search_bar, cx| {
2293            assert_eq!(search_bar.query(cx), "c");
2294            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2295        });
2296        search_bar.update_in(cx, |search_bar, window, cx| {
2297            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2298        });
2299        search_bar.update(cx, |search_bar, cx| {
2300            assert_eq!(search_bar.query(cx), "b");
2301            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2302        });
2303        search_bar.update_in(cx, |search_bar, window, cx| {
2304            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2305        });
2306        search_bar.update(cx, |search_bar, cx| {
2307            assert_eq!(search_bar.query(cx), "c");
2308            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2309        });
2310        search_bar.update_in(cx, |search_bar, window, cx| {
2311            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2312        });
2313        search_bar.update(cx, |search_bar, cx| {
2314            assert_eq!(search_bar.query(cx), "ba");
2315            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2316        });
2317        search_bar.update_in(cx, |search_bar, window, cx| {
2318            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2319        });
2320        search_bar.update(cx, |search_bar, cx| {
2321            assert_eq!(search_bar.query(cx), "");
2322            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2323        });
2324    }
2325
2326    #[gpui::test]
2327    async fn test_replace_simple(cx: &mut TestAppContext) {
2328        let (editor, search_bar, cx) = init_test(cx);
2329
2330        search_bar
2331            .update_in(cx, |search_bar, window, cx| {
2332                search_bar.search("expression", None, window, cx)
2333            })
2334            .await
2335            .unwrap();
2336
2337        search_bar.update_in(cx, |search_bar, window, cx| {
2338            search_bar.replacement_editor.update(cx, |editor, cx| {
2339                // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally.
2340                editor.set_text("expr$1", window, cx);
2341            });
2342            search_bar.replace_all(&ReplaceAll, window, cx)
2343        });
2344        assert_eq!(
2345            editor.read_with(cx, |this, cx| { this.text(cx) }),
2346            r#"
2347        A regular expr$1 (shortened as regex or regexp;[1] also referred to as
2348        rational expr$1[2][3]) is a sequence of characters that specifies a search
2349        pattern in text. Usually such patterns are used by string-searching algorithms
2350        for "find" or "find and replace" operations on strings, or for input validation.
2351        "#
2352            .unindent()
2353        );
2354
2355        // Search for word boundaries and replace just a single one.
2356        search_bar
2357            .update_in(cx, |search_bar, window, cx| {
2358                search_bar.search("or", Some(SearchOptions::WHOLE_WORD), window, cx)
2359            })
2360            .await
2361            .unwrap();
2362
2363        search_bar.update_in(cx, |search_bar, window, cx| {
2364            search_bar.replacement_editor.update(cx, |editor, cx| {
2365                editor.set_text("banana", window, cx);
2366            });
2367            search_bar.replace_next(&ReplaceNext, window, cx)
2368        });
2369        // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text.
2370        assert_eq!(
2371            editor.read_with(cx, |this, cx| { this.text(cx) }),
2372            r#"
2373        A regular expr$1 (shortened as regex banana regexp;[1] also referred to as
2374        rational expr$1[2][3]) is a sequence of characters that specifies a search
2375        pattern in text. Usually such patterns are used by string-searching algorithms
2376        for "find" or "find and replace" operations on strings, or for input validation.
2377        "#
2378            .unindent()
2379        );
2380        // Let's turn on regex mode.
2381        search_bar
2382            .update_in(cx, |search_bar, window, cx| {
2383                search_bar.search("\\[([^\\]]+)\\]", Some(SearchOptions::REGEX), window, cx)
2384            })
2385            .await
2386            .unwrap();
2387        search_bar.update_in(cx, |search_bar, window, cx| {
2388            search_bar.replacement_editor.update(cx, |editor, cx| {
2389                editor.set_text("${1}number", window, cx);
2390            });
2391            search_bar.replace_all(&ReplaceAll, window, cx)
2392        });
2393        assert_eq!(
2394            editor.read_with(cx, |this, cx| { this.text(cx) }),
2395            r#"
2396        A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2397        rational expr$12number3number) is a sequence of characters that specifies a search
2398        pattern in text. Usually such patterns are used by string-searching algorithms
2399        for "find" or "find and replace" operations on strings, or for input validation.
2400        "#
2401            .unindent()
2402        );
2403        // Now with a whole-word twist.
2404        search_bar
2405            .update_in(cx, |search_bar, window, cx| {
2406                search_bar.search(
2407                    "a\\w+s",
2408                    Some(SearchOptions::REGEX | SearchOptions::WHOLE_WORD),
2409                    window,
2410                    cx,
2411                )
2412            })
2413            .await
2414            .unwrap();
2415        search_bar.update_in(cx, |search_bar, window, cx| {
2416            search_bar.replacement_editor.update(cx, |editor, cx| {
2417                editor.set_text("things", window, cx);
2418            });
2419            search_bar.replace_all(&ReplaceAll, window, cx)
2420        });
2421        // The only word affected by this edit should be `algorithms`, even though there's a bunch
2422        // of words in this text that would match this regex if not for WHOLE_WORD.
2423        assert_eq!(
2424            editor.read_with(cx, |this, cx| { this.text(cx) }),
2425            r#"
2426        A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2427        rational expr$12number3number) is a sequence of characters that specifies a search
2428        pattern in text. Usually such patterns are used by string-searching things
2429        for "find" or "find and replace" operations on strings, or for input validation.
2430        "#
2431            .unindent()
2432        );
2433    }
2434
2435    struct ReplacementTestParams<'a> {
2436        editor: &'a Entity<Editor>,
2437        search_bar: &'a Entity<BufferSearchBar>,
2438        cx: &'a mut VisualTestContext,
2439        search_text: &'static str,
2440        search_options: Option<SearchOptions>,
2441        replacement_text: &'static str,
2442        replace_all: bool,
2443        expected_text: String,
2444    }
2445
2446    async fn run_replacement_test(options: ReplacementTestParams<'_>) {
2447        options
2448            .search_bar
2449            .update_in(options.cx, |search_bar, window, cx| {
2450                if let Some(options) = options.search_options {
2451                    search_bar.set_search_options(options, cx);
2452                }
2453                search_bar.search(options.search_text, options.search_options, window, cx)
2454            })
2455            .await
2456            .unwrap();
2457
2458        options
2459            .search_bar
2460            .update_in(options.cx, |search_bar, window, cx| {
2461                search_bar.replacement_editor.update(cx, |editor, cx| {
2462                    editor.set_text(options.replacement_text, window, cx);
2463                });
2464
2465                if options.replace_all {
2466                    search_bar.replace_all(&ReplaceAll, window, cx)
2467                } else {
2468                    search_bar.replace_next(&ReplaceNext, window, cx)
2469                }
2470            });
2471
2472        assert_eq!(
2473            options
2474                .editor
2475                .read_with(options.cx, |this, cx| { this.text(cx) }),
2476            options.expected_text
2477        );
2478    }
2479
2480    #[gpui::test]
2481    async fn test_replace_special_characters(cx: &mut TestAppContext) {
2482        let (editor, search_bar, cx) = init_test(cx);
2483
2484        run_replacement_test(ReplacementTestParams {
2485            editor: &editor,
2486            search_bar: &search_bar,
2487            cx,
2488            search_text: "expression",
2489            search_options: None,
2490            replacement_text: r"\n",
2491            replace_all: true,
2492            expected_text: r#"
2493            A regular \n (shortened as regex or regexp;[1] also referred to as
2494            rational \n[2][3]) is a sequence of characters that specifies a search
2495            pattern in text. Usually such patterns are used by string-searching algorithms
2496            for "find" or "find and replace" operations on strings, or for input validation.
2497            "#
2498            .unindent(),
2499        })
2500        .await;
2501
2502        run_replacement_test(ReplacementTestParams {
2503            editor: &editor,
2504            search_bar: &search_bar,
2505            cx,
2506            search_text: "or",
2507            search_options: Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
2508            replacement_text: r"\\\n\\\\",
2509            replace_all: false,
2510            expected_text: r#"
2511            A regular \n (shortened as regex \
2512            \\ regexp;[1] also referred to as
2513            rational \n[2][3]) is a sequence of characters that specifies a search
2514            pattern in text. Usually such patterns are used by string-searching algorithms
2515            for "find" or "find and replace" operations on strings, or for input validation.
2516            "#
2517            .unindent(),
2518        })
2519        .await;
2520
2521        run_replacement_test(ReplacementTestParams {
2522            editor: &editor,
2523            search_bar: &search_bar,
2524            cx,
2525            search_text: r"(that|used) ",
2526            search_options: Some(SearchOptions::REGEX),
2527            replacement_text: r"$1\n",
2528            replace_all: true,
2529            expected_text: r#"
2530            A regular \n (shortened as regex \
2531            \\ regexp;[1] also referred to as
2532            rational \n[2][3]) is a sequence of characters that
2533            specifies a search
2534            pattern in text. Usually such patterns are used
2535            by string-searching algorithms
2536            for "find" or "find and replace" operations on strings, or for input validation.
2537            "#
2538            .unindent(),
2539        })
2540        .await;
2541    }
2542
2543    #[gpui::test]
2544    async fn test_find_matches_in_selections_singleton_buffer_multiple_selections(
2545        cx: &mut TestAppContext,
2546    ) {
2547        init_globals(cx);
2548        let buffer = cx.new(|cx| {
2549            Buffer::local(
2550                r#"
2551                aaa bbb aaa ccc
2552                aaa bbb aaa ccc
2553                aaa bbb aaa ccc
2554                aaa bbb aaa ccc
2555                aaa bbb aaa ccc
2556                aaa bbb aaa ccc
2557                "#
2558                .unindent(),
2559                cx,
2560            )
2561        });
2562        let cx = cx.add_empty_window();
2563        let editor =
2564            cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
2565
2566        let search_bar = cx.new_window_entity(|window, cx| {
2567            let mut search_bar = BufferSearchBar::new(None, window, cx);
2568            search_bar.set_active_pane_item(Some(&editor), window, cx);
2569            search_bar.show(window, cx);
2570            search_bar
2571        });
2572
2573        editor.update_in(cx, |editor, window, cx| {
2574            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2575                s.select_ranges(vec![Point::new(1, 0)..Point::new(2, 4)])
2576            })
2577        });
2578
2579        search_bar.update_in(cx, |search_bar, window, cx| {
2580            let deploy = Deploy {
2581                focus: true,
2582                replace_enabled: false,
2583                selection_search_enabled: true,
2584            };
2585            search_bar.deploy(&deploy, window, cx);
2586        });
2587
2588        cx.run_until_parked();
2589
2590        search_bar
2591            .update_in(cx, |search_bar, window, cx| {
2592                search_bar.search("aaa", None, window, cx)
2593            })
2594            .await
2595            .unwrap();
2596
2597        editor.update(cx, |editor, cx| {
2598            assert_eq!(
2599                editor.search_background_highlights(cx),
2600                &[
2601                    Point::new(1, 0)..Point::new(1, 3),
2602                    Point::new(1, 8)..Point::new(1, 11),
2603                    Point::new(2, 0)..Point::new(2, 3),
2604                ]
2605            );
2606        });
2607    }
2608
2609    #[gpui::test]
2610    async fn test_find_matches_in_selections_multiple_excerpts_buffer_multiple_selections(
2611        cx: &mut TestAppContext,
2612    ) {
2613        init_globals(cx);
2614        let text = r#"
2615            aaa bbb aaa ccc
2616            aaa bbb aaa ccc
2617            aaa bbb aaa ccc
2618            aaa bbb aaa ccc
2619            aaa bbb aaa ccc
2620            aaa bbb aaa ccc
2621
2622            aaa bbb aaa ccc
2623            aaa bbb aaa ccc
2624            aaa bbb aaa ccc
2625            aaa bbb aaa ccc
2626            aaa bbb aaa ccc
2627            aaa bbb aaa ccc
2628            "#
2629        .unindent();
2630
2631        let cx = cx.add_empty_window();
2632        let editor = cx.new_window_entity(|window, cx| {
2633            let multibuffer = MultiBuffer::build_multi(
2634                [
2635                    (
2636                        &text,
2637                        vec![
2638                            Point::new(0, 0)..Point::new(2, 0),
2639                            Point::new(4, 0)..Point::new(5, 0),
2640                        ],
2641                    ),
2642                    (&text, vec![Point::new(9, 0)..Point::new(11, 0)]),
2643                ],
2644                cx,
2645            );
2646            Editor::for_multibuffer(multibuffer, None, window, cx)
2647        });
2648
2649        let search_bar = cx.new_window_entity(|window, cx| {
2650            let mut search_bar = BufferSearchBar::new(None, window, cx);
2651            search_bar.set_active_pane_item(Some(&editor), window, cx);
2652            search_bar.show(window, cx);
2653            search_bar
2654        });
2655
2656        editor.update_in(cx, |editor, window, cx| {
2657            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2658                s.select_ranges(vec![
2659                    Point::new(1, 0)..Point::new(1, 4),
2660                    Point::new(5, 3)..Point::new(6, 4),
2661                ])
2662            })
2663        });
2664
2665        search_bar.update_in(cx, |search_bar, window, cx| {
2666            let deploy = Deploy {
2667                focus: true,
2668                replace_enabled: false,
2669                selection_search_enabled: true,
2670            };
2671            search_bar.deploy(&deploy, window, cx);
2672        });
2673
2674        cx.run_until_parked();
2675
2676        search_bar
2677            .update_in(cx, |search_bar, window, cx| {
2678                search_bar.search("aaa", None, window, cx)
2679            })
2680            .await
2681            .unwrap();
2682
2683        editor.update(cx, |editor, cx| {
2684            assert_eq!(
2685                editor.search_background_highlights(cx),
2686                &[
2687                    Point::new(1, 0)..Point::new(1, 3),
2688                    Point::new(5, 8)..Point::new(5, 11),
2689                    Point::new(6, 0)..Point::new(6, 3),
2690                ]
2691            );
2692        });
2693    }
2694
2695    #[gpui::test]
2696    async fn test_invalid_regexp_search_after_valid(cx: &mut TestAppContext) {
2697        let (editor, search_bar, cx) = init_test(cx);
2698        // Search using valid regexp
2699        search_bar
2700            .update_in(cx, |search_bar, window, cx| {
2701                search_bar.enable_search_option(SearchOptions::REGEX, window, cx);
2702                search_bar.search("expression", None, window, cx)
2703            })
2704            .await
2705            .unwrap();
2706        editor.update_in(cx, |editor, window, cx| {
2707            assert_eq!(
2708                display_points_of(editor.all_text_background_highlights(window, cx)),
2709                &[
2710                    DisplayPoint::new(DisplayRow(0), 10)..DisplayPoint::new(DisplayRow(0), 20),
2711                    DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 19),
2712                ],
2713            );
2714        });
2715
2716        // Now, the expression is invalid
2717        search_bar
2718            .update_in(cx, |search_bar, window, cx| {
2719                search_bar.search("expression (", None, window, cx)
2720            })
2721            .await
2722            .unwrap_err();
2723        editor.update_in(cx, |editor, window, cx| {
2724            assert!(
2725                display_points_of(editor.all_text_background_highlights(window, cx)).is_empty(),
2726            );
2727        });
2728    }
2729
2730    #[gpui::test]
2731    async fn test_search_options_changes(cx: &mut TestAppContext) {
2732        let (_editor, search_bar, cx) = init_test(cx);
2733        update_search_settings(
2734            SearchSettings {
2735                button: true,
2736                whole_word: false,
2737                case_sensitive: false,
2738                include_ignored: false,
2739                regex: false,
2740            },
2741            cx,
2742        );
2743
2744        let deploy = Deploy {
2745            focus: true,
2746            replace_enabled: false,
2747            selection_search_enabled: true,
2748        };
2749
2750        search_bar.update_in(cx, |search_bar, window, cx| {
2751            assert_eq!(
2752                search_bar.search_options,
2753                SearchOptions::NONE,
2754                "Should have no search options enabled by default"
2755            );
2756            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
2757            assert_eq!(
2758                search_bar.search_options,
2759                SearchOptions::WHOLE_WORD,
2760                "Should enable the option toggled"
2761            );
2762            assert!(
2763                !search_bar.dismissed,
2764                "Search bar should be present and visible"
2765            );
2766            search_bar.deploy(&deploy, window, cx);
2767            assert_eq!(
2768                search_bar.configured_options,
2769                SearchOptions::NONE,
2770                "Should have configured search options matching the settings"
2771            );
2772            assert_eq!(
2773                search_bar.search_options,
2774                SearchOptions::WHOLE_WORD,
2775                "After (re)deploying, the option should still be enabled"
2776            );
2777
2778            search_bar.dismiss(&Dismiss, window, cx);
2779            search_bar.deploy(&deploy, window, cx);
2780            assert_eq!(
2781                search_bar.search_options,
2782                SearchOptions::NONE,
2783                "After hiding and showing the search bar, default options should be used"
2784            );
2785
2786            search_bar.toggle_search_option(SearchOptions::REGEX, window, cx);
2787            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
2788            assert_eq!(
2789                search_bar.search_options,
2790                SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2791                "Should enable the options toggled"
2792            );
2793            assert!(
2794                !search_bar.dismissed,
2795                "Search bar should be present and visible"
2796            );
2797        });
2798
2799        update_search_settings(
2800            SearchSettings {
2801                button: true,
2802                whole_word: false,
2803                case_sensitive: true,
2804                include_ignored: false,
2805                regex: false,
2806            },
2807            cx,
2808        );
2809        search_bar.update_in(cx, |search_bar, window, cx| {
2810            assert_eq!(
2811                search_bar.search_options,
2812                SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2813                "Should have no search options enabled by default"
2814            );
2815
2816            search_bar.deploy(&deploy, window, cx);
2817            assert_eq!(
2818                search_bar.configured_options,
2819                SearchOptions::CASE_SENSITIVE,
2820                "Should have configured search options matching the settings"
2821            );
2822            assert_eq!(
2823                search_bar.search_options,
2824                SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2825                "Toggling a non-dismissed search bar with custom options should not change the default options"
2826            );
2827            search_bar.dismiss(&Dismiss, window, cx);
2828            search_bar.deploy(&deploy, window, cx);
2829            assert_eq!(
2830                search_bar.search_options,
2831                SearchOptions::CASE_SENSITIVE,
2832                "After hiding and showing the search bar, default options should be used"
2833            );
2834        });
2835    }
2836
2837    fn update_search_settings(search_settings: SearchSettings, cx: &mut TestAppContext) {
2838        cx.update(|cx| {
2839            SettingsStore::update_global(cx, |store, cx| {
2840                store.update_user_settings::<EditorSettings>(cx, |settings| {
2841                    settings.search = Some(search_settings);
2842                });
2843            });
2844        });
2845    }
2846}