buffer_search.rs

   1mod registrar;
   2
   3use crate::{
   4    search_bar::render_nav_button, FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll,
   5    ReplaceNext, SearchOptions, SelectAllMatches, SelectNextMatch, SelectPrevMatch,
   6    ToggleCaseSensitive, ToggleRegex, ToggleReplace, ToggleSelection, ToggleWholeWord,
   7};
   8use any_vec::AnyVec;
   9use collections::HashMap;
  10use editor::{
  11    actions::{Tab, TabPrev},
  12    DisplayPoint, Editor, EditorElement, EditorSettings, EditorStyle,
  13};
  14use futures::channel::oneshot;
  15use gpui::{
  16    actions, div, impl_actions, Action, AppContext, ClickEvent, EventEmitter, FocusableView, Hsla,
  17    InteractiveElement as _, IntoElement, KeyContext, ParentElement as _, Render, ScrollHandle,
  18    Styled, Subscription, Task, TextStyle, View, ViewContext, VisualContext as _, WindowContext,
  19};
  20use project::{
  21    search::SearchQuery,
  22    search_history::{SearchHistory, SearchHistoryCursor},
  23};
  24use serde::Deserialize;
  25use settings::Settings;
  26use std::sync::Arc;
  27use theme::ThemeSettings;
  28
  29use ui::{h_flex, prelude::*, IconButton, IconName, Tooltip, BASE_REM_SIZE_IN_PX};
  30use util::ResultExt;
  31use workspace::{
  32    item::ItemHandle,
  33    searchable::{Direction, SearchEvent, SearchableItemHandle, WeakSearchableItemHandle},
  34    ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
  35};
  36
  37pub use registrar::DivRegistrar;
  38use registrar::{ForDeployed, ForDismissed, SearchActionsRegistrar, WithResults};
  39
  40const MIN_INPUT_WIDTH_REMS: f32 = 10.;
  41const MAX_INPUT_WIDTH_REMS: f32 = 30.;
  42const MAX_BUFFER_SEARCH_HISTORY_SIZE: usize = 50;
  43
  44#[derive(PartialEq, Clone, Deserialize)]
  45pub struct Deploy {
  46    #[serde(default = "util::serde::default_true")]
  47    pub focus: bool,
  48    #[serde(default)]
  49    pub replace_enabled: bool,
  50    #[serde(default)]
  51    pub selection_search_enabled: bool,
  52}
  53
  54impl_actions!(buffer_search, [Deploy]);
  55
  56actions!(buffer_search, [Dismiss, FocusEditor]);
  57
  58impl Deploy {
  59    pub fn find() -> Self {
  60        Self {
  61            focus: true,
  62            replace_enabled: false,
  63            selection_search_enabled: false,
  64        }
  65    }
  66}
  67
  68pub enum Event {
  69    UpdateLocation,
  70}
  71
  72pub fn init(cx: &mut AppContext) {
  73    cx.observe_new_views(|workspace: &mut Workspace, _| BufferSearchBar::register(workspace))
  74        .detach();
  75}
  76
  77pub struct BufferSearchBar {
  78    query_editor: View<Editor>,
  79    query_editor_focused: bool,
  80    replacement_editor: View<Editor>,
  81    replacement_editor_focused: bool,
  82    active_searchable_item: Option<Box<dyn SearchableItemHandle>>,
  83    active_match_index: Option<usize>,
  84    active_searchable_item_subscription: Option<Subscription>,
  85    active_search: Option<Arc<SearchQuery>>,
  86    searchable_items_with_matches: HashMap<Box<dyn WeakSearchableItemHandle>, AnyVec<dyn Send>>,
  87    pending_search: Option<Task<()>>,
  88    search_options: SearchOptions,
  89    default_options: SearchOptions,
  90    query_contains_error: bool,
  91    dismissed: bool,
  92    search_history: SearchHistory,
  93    search_history_cursor: SearchHistoryCursor,
  94    replace_enabled: bool,
  95    selection_search_enabled: bool,
  96    scroll_handle: ScrollHandle,
  97    editor_scroll_handle: ScrollHandle,
  98    editor_needed_width: Pixels,
  99}
 100
 101impl BufferSearchBar {
 102    fn render_text_input(
 103        &self,
 104        editor: &View<Editor>,
 105        color: Hsla,
 106        cx: &ViewContext<Self>,
 107    ) -> impl IntoElement {
 108        let settings = ThemeSettings::get_global(cx);
 109        let text_style = TextStyle {
 110            color: if editor.read(cx).read_only(cx) {
 111                cx.theme().colors().text_disabled
 112            } else {
 113                color
 114            },
 115            font_family: settings.buffer_font.family.clone(),
 116            font_features: settings.buffer_font.features.clone(),
 117            font_size: rems(0.875).into(),
 118            font_weight: settings.buffer_font.weight,
 119            line_height: relative(1.3),
 120            ..Default::default()
 121        };
 122
 123        EditorElement::new(
 124            &editor,
 125            EditorStyle {
 126                background: cx.theme().colors().editor_background,
 127                local_player: cx.theme().players().local(),
 128                text: text_style,
 129                ..Default::default()
 130            },
 131        )
 132    }
 133}
 134
 135impl EventEmitter<Event> for BufferSearchBar {}
 136impl EventEmitter<workspace::ToolbarItemEvent> for BufferSearchBar {}
 137impl Render for BufferSearchBar {
 138    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
 139        if self.dismissed {
 140            return div().id("search_bar");
 141        }
 142
 143        let narrow_mode =
 144            self.scroll_handle.bounds().size.width / cx.rem_size() < 340. / BASE_REM_SIZE_IN_PX;
 145        let hide_inline_icons = self.editor_needed_width
 146            > self.editor_scroll_handle.bounds().size.width - cx.rem_size() * 6.;
 147
 148        let supported_options = self.supported_options();
 149
 150        if self.query_editor.update(cx, |query_editor, cx| {
 151            query_editor.placeholder_text(cx).is_none()
 152        }) {
 153            self.query_editor.update(cx, |editor, cx| {
 154                editor.set_placeholder_text("Search", cx);
 155            });
 156        }
 157
 158        self.replacement_editor.update(cx, |editor, cx| {
 159            editor.set_placeholder_text("Replace with...", cx);
 160        });
 161
 162        let mut text_color = Color::Default;
 163        let match_text = self
 164            .active_searchable_item
 165            .as_ref()
 166            .and_then(|searchable_item| {
 167                if self.query(cx).is_empty() {
 168                    return None;
 169                }
 170                let matches_count = self
 171                    .searchable_items_with_matches
 172                    .get(&searchable_item.downgrade())
 173                    .map(AnyVec::len)
 174                    .unwrap_or(0);
 175                if let Some(match_ix) = self.active_match_index {
 176                    Some(format!("{}/{}", match_ix + 1, matches_count))
 177                } else {
 178                    text_color = Color::Error; // No matches found
 179                    None
 180                }
 181            })
 182            .unwrap_or_else(|| "0/0".to_string());
 183        let should_show_replace_input = self.replace_enabled && supported_options.replacement;
 184        let in_replace = self.replacement_editor.focus_handle(cx).is_focused(cx);
 185
 186        let mut key_context = KeyContext::new_with_defaults();
 187        key_context.add("BufferSearchBar");
 188        if in_replace {
 189            key_context.add("in_replace");
 190        }
 191        let editor_border = if self.query_contains_error {
 192            Color::Error.color(cx)
 193        } else {
 194            cx.theme().colors().border
 195        };
 196
 197        let search_line = h_flex()
 198            .child(
 199                h_flex()
 200                    .id("editor-scroll")
 201                    .track_scroll(&self.editor_scroll_handle)
 202                    .flex_1()
 203                    .h_8()
 204                    .px_2()
 205                    .mr_2()
 206                    .py_1()
 207                    .border_1()
 208                    .border_color(editor_border)
 209                    .min_w(rems(MIN_INPUT_WIDTH_REMS))
 210                    .max_w(rems(MAX_INPUT_WIDTH_REMS))
 211                    .rounded_lg()
 212                    .child(self.render_text_input(&self.query_editor, text_color.color(cx), cx))
 213                    .when(!hide_inline_icons, |div| {
 214                        div.children(supported_options.case.then(|| {
 215                            self.render_search_option_button(
 216                                SearchOptions::CASE_SENSITIVE,
 217                                cx.listener(|this, _, cx| {
 218                                    this.toggle_case_sensitive(&ToggleCaseSensitive, cx)
 219                                }),
 220                            )
 221                        }))
 222                        .children(supported_options.word.then(|| {
 223                            self.render_search_option_button(
 224                                SearchOptions::WHOLE_WORD,
 225                                cx.listener(|this, _, cx| {
 226                                    this.toggle_whole_word(&ToggleWholeWord, cx)
 227                                }),
 228                            )
 229                        }))
 230                        .children(supported_options.regex.then(|| {
 231                            self.render_search_option_button(
 232                                SearchOptions::REGEX,
 233                                cx.listener(|this, _, cx| this.toggle_regex(&ToggleRegex, cx)),
 234                            )
 235                        }))
 236                    }),
 237            )
 238            .when(supported_options.replacement, |this| {
 239                this.child(
 240                    IconButton::new("buffer-search-bar-toggle-replace-button", IconName::Replace)
 241                        .style(ButtonStyle::Subtle)
 242                        .when(self.replace_enabled, |button| {
 243                            button.style(ButtonStyle::Filled)
 244                        })
 245                        .on_click(cx.listener(|this, _: &ClickEvent, cx| {
 246                            this.toggle_replace(&ToggleReplace, cx);
 247                        }))
 248                        .selected(self.replace_enabled)
 249                        .size(ButtonSize::Compact)
 250                        .tooltip(|cx| Tooltip::for_action("Toggle replace", &ToggleReplace, cx)),
 251                )
 252            })
 253            .when(supported_options.selection, |this| {
 254                this.child(
 255                    IconButton::new(
 256                        "buffer-search-bar-toggle-search-selection-button",
 257                        IconName::SearchSelection,
 258                    )
 259                    .style(ButtonStyle::Subtle)
 260                    .when(self.selection_search_enabled, |button| {
 261                        button.style(ButtonStyle::Filled)
 262                    })
 263                    .on_click(cx.listener(|this, _: &ClickEvent, cx| {
 264                        this.toggle_selection(&ToggleSelection, cx);
 265                    }))
 266                    .selected(self.selection_search_enabled)
 267                    .size(ButtonSize::Compact)
 268                    .tooltip(|cx| {
 269                        Tooltip::for_action("Toggle search selection", &ToggleSelection, cx)
 270                    }),
 271                )
 272            })
 273            .child(
 274                h_flex()
 275                    .flex_none()
 276                    .child(
 277                        IconButton::new("select-all", ui::IconName::SelectAll)
 278                            .on_click(|_, cx| cx.dispatch_action(SelectAllMatches.boxed_clone()))
 279                            .size(ButtonSize::Compact)
 280                            .tooltip(|cx| {
 281                                Tooltip::for_action("Select all matches", &SelectAllMatches, cx)
 282                            }),
 283                    )
 284                    .child(render_nav_button(
 285                        ui::IconName::ChevronLeft,
 286                        self.active_match_index.is_some(),
 287                        "Select previous match",
 288                        &SelectPrevMatch,
 289                    ))
 290                    .child(render_nav_button(
 291                        ui::IconName::ChevronRight,
 292                        self.active_match_index.is_some(),
 293                        "Select next match",
 294                        &SelectNextMatch,
 295                    ))
 296                    .when(!narrow_mode, |this| {
 297                        this.child(h_flex().min_w(rems_from_px(40.)).child(
 298                            Label::new(match_text).color(if self.active_match_index.is_some() {
 299                                Color::Default
 300                            } else {
 301                                Color::Disabled
 302                            }),
 303                        ))
 304                    }),
 305            );
 306
 307        let replace_line = should_show_replace_input.then(|| {
 308            h_flex()
 309                .gap_2()
 310                .flex_1()
 311                .child(
 312                    h_flex()
 313                        .flex_1()
 314                        // We're giving this a fixed height to match the height of the search input,
 315                        // which has an icon inside that is increasing its height.
 316                        .h_8()
 317                        .px_2()
 318                        .py_1()
 319                        .border_1()
 320                        .border_color(cx.theme().colors().border)
 321                        .rounded_lg()
 322                        .min_w(rems(MIN_INPUT_WIDTH_REMS))
 323                        .max_w(rems(MAX_INPUT_WIDTH_REMS))
 324                        .child(self.render_text_input(
 325                            &self.replacement_editor,
 326                            cx.theme().colors().text,
 327                            cx,
 328                        )),
 329                )
 330                .child(
 331                    h_flex()
 332                        .flex_none()
 333                        .child(
 334                            IconButton::new("search-replace-next", ui::IconName::ReplaceNext)
 335                                .tooltip(move |cx| {
 336                                    Tooltip::for_action("Replace next", &ReplaceNext, cx)
 337                                })
 338                                .on_click(
 339                                    cx.listener(|this, _, cx| this.replace_next(&ReplaceNext, cx)),
 340                                ),
 341                        )
 342                        .child(
 343                            IconButton::new("search-replace-all", ui::IconName::ReplaceAll)
 344                                .tooltip(move |cx| {
 345                                    Tooltip::for_action("Replace all", &ReplaceAll, cx)
 346                                })
 347                                .on_click(
 348                                    cx.listener(|this, _, cx| this.replace_all(&ReplaceAll, cx)),
 349                                ),
 350                        ),
 351                )
 352        });
 353
 354        v_flex()
 355            .id("buffer_search")
 356            .track_scroll(&self.scroll_handle)
 357            .key_context(key_context)
 358            .capture_action(cx.listener(Self::tab))
 359            .capture_action(cx.listener(Self::tab_prev))
 360            .on_action(cx.listener(Self::previous_history_query))
 361            .on_action(cx.listener(Self::next_history_query))
 362            .on_action(cx.listener(Self::dismiss))
 363            .on_action(cx.listener(Self::select_next_match))
 364            .on_action(cx.listener(Self::select_prev_match))
 365            .when(self.supported_options().replacement, |this| {
 366                this.on_action(cx.listener(Self::toggle_replace))
 367                    .when(in_replace, |this| {
 368                        this.on_action(cx.listener(Self::replace_next))
 369                            .on_action(cx.listener(Self::replace_all))
 370                    })
 371            })
 372            .when(self.supported_options().case, |this| {
 373                this.on_action(cx.listener(Self::toggle_case_sensitive))
 374            })
 375            .when(self.supported_options().word, |this| {
 376                this.on_action(cx.listener(Self::toggle_whole_word))
 377            })
 378            .when(self.supported_options().regex, |this| {
 379                this.on_action(cx.listener(Self::toggle_regex))
 380            })
 381            .when(self.supported_options().selection, |this| {
 382                this.on_action(cx.listener(Self::toggle_selection))
 383            })
 384            .gap_2()
 385            .child(
 386                h_flex()
 387                    .child(search_line.w_full())
 388                    .when(!narrow_mode, |div| {
 389                        div.child(
 390                            IconButton::new(SharedString::from("Close"), IconName::Close)
 391                                .tooltip(move |cx| {
 392                                    Tooltip::for_action("Close search bar", &Dismiss, cx)
 393                                })
 394                                .on_click(cx.listener(|this, _: &ClickEvent, cx| {
 395                                    this.dismiss(&Dismiss, cx)
 396                                })),
 397                        )
 398                    }),
 399            )
 400            .children(replace_line)
 401    }
 402}
 403
 404impl FocusableView for BufferSearchBar {
 405    fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
 406        self.query_editor.focus_handle(cx)
 407    }
 408}
 409
 410impl ToolbarItemView for BufferSearchBar {
 411    fn set_active_pane_item(
 412        &mut self,
 413        item: Option<&dyn ItemHandle>,
 414        cx: &mut ViewContext<Self>,
 415    ) -> ToolbarItemLocation {
 416        cx.notify();
 417        self.active_searchable_item_subscription.take();
 418        self.active_searchable_item.take();
 419
 420        self.pending_search.take();
 421
 422        if let Some(searchable_item_handle) =
 423            item.and_then(|item| item.to_searchable_item_handle(cx))
 424        {
 425            let this = cx.view().downgrade();
 426
 427            self.active_searchable_item_subscription =
 428                Some(searchable_item_handle.subscribe_to_search_events(
 429                    cx,
 430                    Box::new(move |search_event, cx| {
 431                        if let Some(this) = this.upgrade() {
 432                            this.update(cx, |this, cx| {
 433                                this.on_active_searchable_item_event(search_event, cx)
 434                            });
 435                        }
 436                    }),
 437                ));
 438
 439            self.active_searchable_item = Some(searchable_item_handle);
 440            let _ = self.update_matches(cx);
 441            if !self.dismissed {
 442                return ToolbarItemLocation::Secondary;
 443            }
 444        }
 445        ToolbarItemLocation::Hidden
 446    }
 447}
 448
 449impl BufferSearchBar {
 450    pub fn register(registrar: &mut impl SearchActionsRegistrar) {
 451        registrar.register_handler(ForDeployed(|this, _: &FocusSearch, cx| {
 452            this.query_editor.focus_handle(cx).focus(cx);
 453            this.select_query(cx);
 454        }));
 455        registrar.register_handler(ForDeployed(|this, action: &ToggleCaseSensitive, cx| {
 456            if this.supported_options().case {
 457                this.toggle_case_sensitive(action, cx);
 458            }
 459        }));
 460        registrar.register_handler(ForDeployed(|this, action: &ToggleWholeWord, cx| {
 461            if this.supported_options().word {
 462                this.toggle_whole_word(action, cx);
 463            }
 464        }));
 465        registrar.register_handler(ForDeployed(|this, action: &ToggleSelection, cx| {
 466            if this.supported_options().selection {
 467                this.toggle_selection(action, cx);
 468            }
 469        }));
 470        registrar.register_handler(ForDeployed(|this, action: &ToggleReplace, cx| {
 471            if this.supported_options().replacement {
 472                this.toggle_replace(action, cx);
 473            }
 474        }));
 475        registrar.register_handler(WithResults(|this, action: &SelectNextMatch, cx| {
 476            this.select_next_match(action, cx);
 477        }));
 478        registrar.register_handler(WithResults(|this, action: &SelectPrevMatch, cx| {
 479            this.select_prev_match(action, cx);
 480        }));
 481        registrar.register_handler(WithResults(|this, action: &SelectAllMatches, cx| {
 482            this.select_all_matches(action, cx);
 483        }));
 484        registrar.register_handler(ForDeployed(|this, _: &editor::actions::Cancel, cx| {
 485            this.dismiss(&Dismiss, cx);
 486        }));
 487
 488        // register deploy buffer search for both search bar states, since we want to focus into the search bar
 489        // when the deploy action is triggered in the buffer.
 490        registrar.register_handler(ForDeployed(|this, deploy, cx| {
 491            this.deploy(deploy, cx);
 492        }));
 493        registrar.register_handler(ForDismissed(|this, deploy, cx| {
 494            this.deploy(deploy, cx);
 495        }))
 496    }
 497
 498    pub fn new(cx: &mut ViewContext<Self>) -> Self {
 499        let query_editor = cx.new_view(|cx| Editor::single_line(cx));
 500        cx.subscribe(&query_editor, Self::on_query_editor_event)
 501            .detach();
 502        let replacement_editor = cx.new_view(|cx| Editor::single_line(cx));
 503        cx.subscribe(&replacement_editor, Self::on_replacement_editor_event)
 504            .detach();
 505
 506        Self {
 507            query_editor,
 508            query_editor_focused: false,
 509            replacement_editor,
 510            replacement_editor_focused: false,
 511            active_searchable_item: None,
 512            active_searchable_item_subscription: None,
 513            active_match_index: None,
 514            searchable_items_with_matches: Default::default(),
 515            default_options: SearchOptions::NONE,
 516            search_options: SearchOptions::NONE,
 517            pending_search: None,
 518            query_contains_error: false,
 519            dismissed: true,
 520            search_history: SearchHistory::new(
 521                Some(MAX_BUFFER_SEARCH_HISTORY_SIZE),
 522                project::search_history::QueryInsertionBehavior::ReplacePreviousIfContains,
 523            ),
 524            search_history_cursor: Default::default(),
 525            active_search: None,
 526            replace_enabled: false,
 527            selection_search_enabled: false,
 528            scroll_handle: ScrollHandle::new(),
 529            editor_scroll_handle: ScrollHandle::new(),
 530            editor_needed_width: px(0.),
 531        }
 532    }
 533
 534    pub fn is_dismissed(&self) -> bool {
 535        self.dismissed
 536    }
 537
 538    pub fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext<Self>) {
 539        self.dismissed = true;
 540        for searchable_item in self.searchable_items_with_matches.keys() {
 541            if let Some(searchable_item) =
 542                WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
 543            {
 544                searchable_item.clear_matches(cx);
 545            }
 546        }
 547        if let Some(active_editor) = self.active_searchable_item.as_mut() {
 548            self.selection_search_enabled = false;
 549            self.replace_enabled = false;
 550            active_editor.search_bar_visibility_changed(false, cx);
 551            active_editor.toggle_filtered_search_ranges(false, cx);
 552            let handle = active_editor.focus_handle(cx);
 553            cx.focus(&handle);
 554        }
 555        cx.emit(Event::UpdateLocation);
 556        cx.emit(ToolbarItemEvent::ChangeLocation(
 557            ToolbarItemLocation::Hidden,
 558        ));
 559        cx.notify();
 560    }
 561
 562    pub fn deploy(&mut self, deploy: &Deploy, cx: &mut ViewContext<Self>) -> bool {
 563        if self.show(cx) {
 564            if let Some(active_item) = self.active_searchable_item.as_mut() {
 565                active_item.toggle_filtered_search_ranges(deploy.selection_search_enabled, cx);
 566            }
 567            self.search_suggested(cx);
 568            self.replace_enabled = deploy.replace_enabled;
 569            self.selection_search_enabled = deploy.selection_search_enabled;
 570            if deploy.focus {
 571                let mut handle = self.query_editor.focus_handle(cx).clone();
 572                let mut select_query = true;
 573                if deploy.replace_enabled && handle.is_focused(cx) {
 574                    handle = self.replacement_editor.focus_handle(cx).clone();
 575                    select_query = false;
 576                };
 577
 578                if select_query {
 579                    self.select_query(cx);
 580                }
 581
 582                cx.focus(&handle);
 583            }
 584            return true;
 585        }
 586
 587        false
 588    }
 589
 590    pub fn toggle(&mut self, action: &Deploy, cx: &mut ViewContext<Self>) {
 591        if self.is_dismissed() {
 592            self.deploy(action, cx);
 593        } else {
 594            self.dismiss(&Dismiss, cx);
 595        }
 596    }
 597
 598    pub fn show(&mut self, cx: &mut ViewContext<Self>) -> bool {
 599        let Some(handle) = self.active_searchable_item.as_ref() else {
 600            return false;
 601        };
 602
 603        self.dismissed = false;
 604        handle.search_bar_visibility_changed(true, cx);
 605        cx.notify();
 606        cx.emit(Event::UpdateLocation);
 607        cx.emit(ToolbarItemEvent::ChangeLocation(
 608            ToolbarItemLocation::Secondary,
 609        ));
 610        true
 611    }
 612
 613    fn supported_options(&self) -> workspace::searchable::SearchOptions {
 614        self.active_searchable_item
 615            .as_deref()
 616            .map(SearchableItemHandle::supported_options)
 617            .unwrap_or_default()
 618    }
 619    pub fn search_suggested(&mut self, cx: &mut ViewContext<Self>) {
 620        let search = self
 621            .query_suggestion(cx)
 622            .map(|suggestion| self.search(&suggestion, Some(self.default_options), cx));
 623
 624        if let Some(search) = search {
 625            cx.spawn(|this, mut cx| async move {
 626                search.await?;
 627                this.update(&mut cx, |this, cx| this.activate_current_match(cx))
 628            })
 629            .detach_and_log_err(cx);
 630        }
 631    }
 632
 633    pub fn activate_current_match(&mut self, cx: &mut ViewContext<Self>) {
 634        if let Some(match_ix) = self.active_match_index {
 635            if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
 636                if let Some(matches) = self
 637                    .searchable_items_with_matches
 638                    .get(&active_searchable_item.downgrade())
 639                {
 640                    active_searchable_item.activate_match(match_ix, matches, cx)
 641                }
 642            }
 643        }
 644    }
 645
 646    pub fn select_query(&mut self, cx: &mut ViewContext<Self>) {
 647        self.query_editor.update(cx, |query_editor, cx| {
 648            query_editor.select_all(&Default::default(), cx);
 649        });
 650    }
 651
 652    pub fn query(&self, cx: &WindowContext) -> String {
 653        self.query_editor.read(cx).text(cx)
 654    }
 655    pub fn replacement(&self, cx: &WindowContext) -> String {
 656        self.replacement_editor.read(cx).text(cx)
 657    }
 658    pub fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> Option<String> {
 659        self.active_searchable_item
 660            .as_ref()
 661            .map(|searchable_item| searchable_item.query_suggestion(cx))
 662            .filter(|suggestion| !suggestion.is_empty())
 663    }
 664
 665    pub fn set_replacement(&mut self, replacement: Option<&str>, cx: &mut ViewContext<Self>) {
 666        if replacement.is_none() {
 667            self.replace_enabled = false;
 668            return;
 669        }
 670        self.replace_enabled = true;
 671        self.replacement_editor
 672            .update(cx, |replacement_editor, cx| {
 673                replacement_editor
 674                    .buffer()
 675                    .update(cx, |replacement_buffer, cx| {
 676                        let len = replacement_buffer.len(cx);
 677                        replacement_buffer.edit([(0..len, replacement.unwrap())], None, cx);
 678                    });
 679            });
 680    }
 681
 682    pub fn search(
 683        &mut self,
 684        query: &str,
 685        options: Option<SearchOptions>,
 686        cx: &mut ViewContext<Self>,
 687    ) -> oneshot::Receiver<()> {
 688        let options = options.unwrap_or(self.default_options);
 689        if query != self.query(cx) || self.search_options != options {
 690            self.query_editor.update(cx, |query_editor, cx| {
 691                query_editor.buffer().update(cx, |query_buffer, cx| {
 692                    let len = query_buffer.len(cx);
 693                    query_buffer.edit([(0..len, query)], None, cx);
 694                });
 695            });
 696            self.search_options = options;
 697            self.clear_matches(cx);
 698            cx.notify();
 699        }
 700        self.update_matches(cx)
 701    }
 702
 703    fn render_search_option_button(
 704        &self,
 705        option: SearchOptions,
 706        action: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
 707    ) -> impl IntoElement {
 708        let is_active = self.search_options.contains(option);
 709        option.as_button(is_active, action)
 710    }
 711
 712    pub fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext<Self>) {
 713        if let Some(active_editor) = self.active_searchable_item.as_ref() {
 714            let handle = active_editor.focus_handle(cx);
 715            cx.focus(&handle);
 716        }
 717    }
 718
 719    fn toggle_search_option(&mut self, search_option: SearchOptions, cx: &mut ViewContext<Self>) {
 720        self.search_options.toggle(search_option);
 721        self.default_options = self.search_options;
 722        let _ = self.update_matches(cx);
 723        cx.notify();
 724    }
 725
 726    pub fn enable_search_option(
 727        &mut self,
 728        search_option: SearchOptions,
 729        cx: &mut ViewContext<Self>,
 730    ) {
 731        if !self.search_options.contains(search_option) {
 732            self.toggle_search_option(search_option, cx)
 733        }
 734    }
 735
 736    pub fn set_search_options(
 737        &mut self,
 738        search_options: SearchOptions,
 739        cx: &mut ViewContext<Self>,
 740    ) {
 741        self.search_options = search_options;
 742        cx.notify();
 743    }
 744
 745    fn select_next_match(&mut self, _: &SelectNextMatch, cx: &mut ViewContext<Self>) {
 746        self.select_match(Direction::Next, 1, cx);
 747    }
 748
 749    fn select_prev_match(&mut self, _: &SelectPrevMatch, cx: &mut ViewContext<Self>) {
 750        self.select_match(Direction::Prev, 1, cx);
 751    }
 752
 753    fn select_all_matches(&mut self, _: &SelectAllMatches, cx: &mut ViewContext<Self>) {
 754        if !self.dismissed && self.active_match_index.is_some() {
 755            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
 756                if let Some(matches) = self
 757                    .searchable_items_with_matches
 758                    .get(&searchable_item.downgrade())
 759                {
 760                    searchable_item.select_matches(matches, cx);
 761                    self.focus_editor(&FocusEditor, cx);
 762                }
 763            }
 764        }
 765    }
 766
 767    pub fn select_match(&mut self, direction: Direction, count: usize, cx: &mut ViewContext<Self>) {
 768        if let Some(index) = self.active_match_index {
 769            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
 770                if let Some(matches) = self
 771                    .searchable_items_with_matches
 772                    .get(&searchable_item.downgrade())
 773                    .filter(|matches| !matches.is_empty())
 774                {
 775                    // If 'wrapscan' is disabled, searches do not wrap around the end of the file.
 776                    if !EditorSettings::get_global(cx).search_wrap {
 777                        if (direction == Direction::Next && index + count >= matches.len())
 778                            || (direction == Direction::Prev && index < count)
 779                        {
 780                            crate::show_no_more_matches(cx);
 781                            return;
 782                        }
 783                    }
 784                    let new_match_index = searchable_item
 785                        .match_index_for_direction(matches, index, direction, count, cx);
 786
 787                    searchable_item.update_matches(matches, cx);
 788                    searchable_item.activate_match(new_match_index, matches, cx);
 789                }
 790            }
 791        }
 792    }
 793
 794    pub fn select_last_match(&mut self, cx: &mut ViewContext<Self>) {
 795        if let Some(searchable_item) = self.active_searchable_item.as_ref() {
 796            if let Some(matches) = self
 797                .searchable_items_with_matches
 798                .get(&searchable_item.downgrade())
 799            {
 800                if matches.len() == 0 {
 801                    return;
 802                }
 803                let new_match_index = matches.len() - 1;
 804                searchable_item.update_matches(matches, cx);
 805                searchable_item.activate_match(new_match_index, matches, cx);
 806            }
 807        }
 808    }
 809
 810    fn on_query_editor_event(
 811        &mut self,
 812        editor: View<Editor>,
 813        event: &editor::EditorEvent,
 814        cx: &mut ViewContext<Self>,
 815    ) {
 816        match event {
 817            editor::EditorEvent::Focused => self.query_editor_focused = true,
 818            editor::EditorEvent::Blurred => self.query_editor_focused = false,
 819            editor::EditorEvent::Edited { .. } => {
 820                self.clear_matches(cx);
 821                let search = self.update_matches(cx);
 822
 823                let width = editor.update(cx, |editor, cx| {
 824                    let text_layout_details = editor.text_layout_details(cx);
 825                    let snapshot = editor.snapshot(cx).display_snapshot;
 826
 827                    snapshot.x_for_display_point(snapshot.max_point(), &text_layout_details)
 828                        - snapshot.x_for_display_point(DisplayPoint::zero(), &text_layout_details)
 829                });
 830                self.editor_needed_width = width;
 831                cx.notify();
 832
 833                cx.spawn(|this, mut cx| async move {
 834                    search.await?;
 835                    this.update(&mut cx, |this, cx| this.activate_current_match(cx))
 836                })
 837                .detach_and_log_err(cx);
 838            }
 839            _ => {}
 840        }
 841    }
 842
 843    fn on_replacement_editor_event(
 844        &mut self,
 845        _: View<Editor>,
 846        event: &editor::EditorEvent,
 847        _: &mut ViewContext<Self>,
 848    ) {
 849        match event {
 850            editor::EditorEvent::Focused => self.replacement_editor_focused = true,
 851            editor::EditorEvent::Blurred => self.replacement_editor_focused = false,
 852            _ => {}
 853        }
 854    }
 855
 856    fn on_active_searchable_item_event(&mut self, event: &SearchEvent, cx: &mut ViewContext<Self>) {
 857        match event {
 858            SearchEvent::MatchesInvalidated => {
 859                let _ = self.update_matches(cx);
 860            }
 861            SearchEvent::ActiveMatchChanged => self.update_match_index(cx),
 862        }
 863    }
 864
 865    fn toggle_case_sensitive(&mut self, _: &ToggleCaseSensitive, cx: &mut ViewContext<Self>) {
 866        self.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx)
 867    }
 868
 869    fn toggle_whole_word(&mut self, _: &ToggleWholeWord, cx: &mut ViewContext<Self>) {
 870        self.toggle_search_option(SearchOptions::WHOLE_WORD, cx)
 871    }
 872
 873    fn toggle_selection(&mut self, _: &ToggleSelection, cx: &mut ViewContext<Self>) {
 874        if let Some(active_item) = self.active_searchable_item.as_mut() {
 875            self.selection_search_enabled = !self.selection_search_enabled;
 876            active_item.toggle_filtered_search_ranges(self.selection_search_enabled, cx);
 877            let _ = self.update_matches(cx);
 878            cx.notify();
 879        }
 880    }
 881
 882    fn toggle_regex(&mut self, _: &ToggleRegex, cx: &mut ViewContext<Self>) {
 883        self.toggle_search_option(SearchOptions::REGEX, cx)
 884    }
 885
 886    fn clear_active_searchable_item_matches(&mut self, cx: &mut WindowContext) {
 887        if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
 888            self.active_match_index = None;
 889            self.searchable_items_with_matches
 890                .remove(&active_searchable_item.downgrade());
 891            active_searchable_item.clear_matches(cx);
 892        }
 893    }
 894
 895    pub fn has_active_match(&self) -> bool {
 896        self.active_match_index.is_some()
 897    }
 898
 899    fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
 900        let mut active_item_matches = None;
 901        for (searchable_item, matches) in self.searchable_items_with_matches.drain() {
 902            if let Some(searchable_item) =
 903                WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
 904            {
 905                if Some(&searchable_item) == self.active_searchable_item.as_ref() {
 906                    active_item_matches = Some((searchable_item.downgrade(), matches));
 907                } else {
 908                    searchable_item.clear_matches(cx);
 909                }
 910            }
 911        }
 912
 913        self.searchable_items_with_matches
 914            .extend(active_item_matches);
 915    }
 916
 917    fn update_matches(&mut self, cx: &mut ViewContext<Self>) -> oneshot::Receiver<()> {
 918        let (done_tx, done_rx) = oneshot::channel();
 919        let query = self.query(cx);
 920        self.pending_search.take();
 921
 922        if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
 923            self.query_contains_error = false;
 924            if query.is_empty() {
 925                self.clear_active_searchable_item_matches(cx);
 926                let _ = done_tx.send(());
 927                cx.notify();
 928            } else {
 929                let query: Arc<_> = if self.search_options.contains(SearchOptions::REGEX) {
 930                    match SearchQuery::regex(
 931                        query,
 932                        self.search_options.contains(SearchOptions::WHOLE_WORD),
 933                        self.search_options.contains(SearchOptions::CASE_SENSITIVE),
 934                        false,
 935                        Default::default(),
 936                        Default::default(),
 937                    ) {
 938                        Ok(query) => query.with_replacement(self.replacement(cx)),
 939                        Err(_) => {
 940                            self.query_contains_error = true;
 941                            self.clear_active_searchable_item_matches(cx);
 942                            cx.notify();
 943                            return done_rx;
 944                        }
 945                    }
 946                } else {
 947                    match SearchQuery::text(
 948                        query,
 949                        self.search_options.contains(SearchOptions::WHOLE_WORD),
 950                        self.search_options.contains(SearchOptions::CASE_SENSITIVE),
 951                        false,
 952                        Default::default(),
 953                        Default::default(),
 954                    ) {
 955                        Ok(query) => query.with_replacement(self.replacement(cx)),
 956                        Err(_) => {
 957                            self.query_contains_error = true;
 958                            self.clear_active_searchable_item_matches(cx);
 959                            cx.notify();
 960                            return done_rx;
 961                        }
 962                    }
 963                }
 964                .into();
 965                self.active_search = Some(query.clone());
 966                let query_text = query.as_str().to_string();
 967
 968                let matches = active_searchable_item.find_matches(query, cx);
 969
 970                let active_searchable_item = active_searchable_item.downgrade();
 971                self.pending_search = Some(cx.spawn(|this, mut cx| async move {
 972                    let matches = matches.await;
 973
 974                    this.update(&mut cx, |this, cx| {
 975                        if let Some(active_searchable_item) =
 976                            WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx)
 977                        {
 978                            this.searchable_items_with_matches
 979                                .insert(active_searchable_item.downgrade(), matches);
 980
 981                            this.update_match_index(cx);
 982                            this.search_history
 983                                .add(&mut this.search_history_cursor, query_text);
 984                            if !this.dismissed {
 985                                let matches = this
 986                                    .searchable_items_with_matches
 987                                    .get(&active_searchable_item.downgrade())
 988                                    .unwrap();
 989                                active_searchable_item.update_matches(matches, cx);
 990                                let _ = done_tx.send(());
 991                            }
 992                            cx.notify();
 993                        }
 994                    })
 995                    .log_err();
 996                }));
 997            }
 998        }
 999        done_rx
1000    }
1001
1002    pub fn update_match_index(&mut self, cx: &mut ViewContext<Self>) {
1003        let new_index = self
1004            .active_searchable_item
1005            .as_ref()
1006            .and_then(|searchable_item| {
1007                let matches = self
1008                    .searchable_items_with_matches
1009                    .get(&searchable_item.downgrade())?;
1010                searchable_item.active_match_index(matches, cx)
1011            });
1012        if new_index != self.active_match_index {
1013            self.active_match_index = new_index;
1014            cx.notify();
1015        }
1016    }
1017
1018    fn tab(&mut self, _: &Tab, cx: &mut ViewContext<Self>) {
1019        // Search -> Replace -> Editor
1020        let focus_handle = if self.replace_enabled && self.query_editor_focused {
1021            self.replacement_editor.focus_handle(cx)
1022        } else if let Some(item) = self.active_searchable_item.as_ref() {
1023            item.focus_handle(cx)
1024        } else {
1025            return;
1026        };
1027        cx.focus(&focus_handle);
1028        cx.stop_propagation();
1029    }
1030
1031    fn tab_prev(&mut self, _: &TabPrev, cx: &mut ViewContext<Self>) {
1032        // Search -> Replace -> Search
1033        let focus_handle = if self.replace_enabled && self.query_editor_focused {
1034            self.replacement_editor.focus_handle(cx)
1035        } else if self.replacement_editor_focused {
1036            self.query_editor.focus_handle(cx)
1037        } else {
1038            return;
1039        };
1040        cx.focus(&focus_handle);
1041        cx.stop_propagation();
1042    }
1043
1044    fn next_history_query(&mut self, _: &NextHistoryQuery, cx: &mut ViewContext<Self>) {
1045        if let Some(new_query) = self
1046            .search_history
1047            .next(&mut self.search_history_cursor)
1048            .map(str::to_string)
1049        {
1050            let _ = self.search(&new_query, Some(self.search_options), cx);
1051        } else {
1052            self.search_history_cursor.reset();
1053            let _ = self.search("", Some(self.search_options), cx);
1054        }
1055    }
1056
1057    fn previous_history_query(&mut self, _: &PreviousHistoryQuery, cx: &mut ViewContext<Self>) {
1058        if self.query(cx).is_empty() {
1059            if let Some(new_query) = self
1060                .search_history
1061                .current(&mut self.search_history_cursor)
1062                .map(str::to_string)
1063            {
1064                let _ = self.search(&new_query, Some(self.search_options), cx);
1065                return;
1066            }
1067        }
1068
1069        if let Some(new_query) = self
1070            .search_history
1071            .previous(&mut self.search_history_cursor)
1072            .map(str::to_string)
1073        {
1074            let _ = self.search(&new_query, Some(self.search_options), cx);
1075        }
1076    }
1077
1078    fn toggle_replace(&mut self, _: &ToggleReplace, cx: &mut ViewContext<Self>) {
1079        if let Some(_) = &self.active_searchable_item {
1080            self.replace_enabled = !self.replace_enabled;
1081            let handle = if self.replace_enabled {
1082                self.replacement_editor.focus_handle(cx)
1083            } else {
1084                self.query_editor.focus_handle(cx)
1085            };
1086            cx.focus(&handle);
1087            cx.notify();
1088        }
1089    }
1090    fn replace_next(&mut self, _: &ReplaceNext, cx: &mut ViewContext<Self>) {
1091        let mut should_propagate = true;
1092        if !self.dismissed && self.active_search.is_some() {
1093            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1094                if let Some(query) = self.active_search.as_ref() {
1095                    if let Some(matches) = self
1096                        .searchable_items_with_matches
1097                        .get(&searchable_item.downgrade())
1098                    {
1099                        if let Some(active_index) = self.active_match_index {
1100                            let query = query
1101                                .as_ref()
1102                                .clone()
1103                                .with_replacement(self.replacement(cx));
1104                            searchable_item.replace(matches.at(active_index), &query, cx);
1105                            self.select_next_match(&SelectNextMatch, cx);
1106                        }
1107                        should_propagate = false;
1108                        self.focus_editor(&FocusEditor, cx);
1109                    }
1110                }
1111            }
1112        }
1113        if !should_propagate {
1114            cx.stop_propagation();
1115        }
1116    }
1117    pub fn replace_all(&mut self, _: &ReplaceAll, cx: &mut ViewContext<Self>) {
1118        if !self.dismissed && self.active_search.is_some() {
1119            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1120                if let Some(query) = self.active_search.as_ref() {
1121                    if let Some(matches) = self
1122                        .searchable_items_with_matches
1123                        .get(&searchable_item.downgrade())
1124                    {
1125                        let query = query
1126                            .as_ref()
1127                            .clone()
1128                            .with_replacement(self.replacement(cx));
1129                        searchable_item.replace_all(&mut matches.iter(), &query, cx);
1130                    }
1131                }
1132            }
1133        }
1134    }
1135
1136    pub fn match_exists(&mut self, cx: &mut ViewContext<Self>) -> bool {
1137        self.update_match_index(cx);
1138        self.active_match_index.is_some()
1139    }
1140}
1141
1142#[cfg(test)]
1143mod tests {
1144    use std::ops::Range;
1145
1146    use super::*;
1147    use editor::{display_map::DisplayRow, DisplayPoint, Editor, MultiBuffer};
1148    use gpui::{Context, Hsla, TestAppContext, VisualTestContext};
1149    use language::{Buffer, Point};
1150    use project::Project;
1151    use smol::stream::StreamExt as _;
1152    use unindent::Unindent as _;
1153
1154    fn init_globals(cx: &mut TestAppContext) {
1155        cx.update(|cx| {
1156            let store = settings::SettingsStore::test(cx);
1157            cx.set_global(store);
1158            editor::init(cx);
1159
1160            language::init(cx);
1161            Project::init_settings(cx);
1162            theme::init(theme::LoadThemes::JustBase, cx);
1163        });
1164    }
1165
1166    fn init_test(
1167        cx: &mut TestAppContext,
1168    ) -> (View<Editor>, View<BufferSearchBar>, &mut VisualTestContext) {
1169        init_globals(cx);
1170        let buffer = cx.new_model(|cx| {
1171            Buffer::local(
1172                r#"
1173                A regular expression (shortened as regex or regexp;[1] also referred to as
1174                rational expression[2][3]) is a sequence of characters that specifies a search
1175                pattern in text. Usually such patterns are used by string-searching algorithms
1176                for "find" or "find and replace" operations on strings, or for input validation.
1177                "#
1178                .unindent(),
1179                cx,
1180            )
1181        });
1182        let cx = cx.add_empty_window();
1183        let editor = cx.new_view(|cx| Editor::for_buffer(buffer.clone(), None, cx));
1184
1185        let search_bar = cx.new_view(|cx| {
1186            let mut search_bar = BufferSearchBar::new(cx);
1187            search_bar.set_active_pane_item(Some(&editor), cx);
1188            search_bar.show(cx);
1189            search_bar
1190        });
1191
1192        (editor, search_bar, cx)
1193    }
1194
1195    #[gpui::test]
1196    async fn test_search_simple(cx: &mut TestAppContext) {
1197        let (editor, search_bar, cx) = init_test(cx);
1198        let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
1199            background_highlights
1200                .into_iter()
1201                .map(|(range, _)| range)
1202                .collect::<Vec<_>>()
1203        };
1204        // Search for a string that appears with different casing.
1205        // By default, search is case-insensitive.
1206        search_bar
1207            .update(cx, |search_bar, cx| search_bar.search("us", None, cx))
1208            .await
1209            .unwrap();
1210        editor.update(cx, |editor, cx| {
1211            assert_eq!(
1212                display_points_of(editor.all_text_background_highlights(cx)),
1213                &[
1214                    DisplayPoint::new(DisplayRow(2), 17)..DisplayPoint::new(DisplayRow(2), 19),
1215                    DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),
1216                ]
1217            );
1218        });
1219
1220        // Switch to a case sensitive search.
1221        search_bar.update(cx, |search_bar, cx| {
1222            search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx);
1223        });
1224        let mut editor_notifications = cx.notifications(&editor);
1225        editor_notifications.next().await;
1226        editor.update(cx, |editor, cx| {
1227            assert_eq!(
1228                display_points_of(editor.all_text_background_highlights(cx)),
1229                &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
1230            );
1231        });
1232
1233        // Search for a string that appears both as a whole word and
1234        // within other words. By default, all results are found.
1235        search_bar
1236            .update(cx, |search_bar, cx| search_bar.search("or", None, cx))
1237            .await
1238            .unwrap();
1239        editor.update(cx, |editor, cx| {
1240            assert_eq!(
1241                display_points_of(editor.all_text_background_highlights(cx)),
1242                &[
1243                    DisplayPoint::new(DisplayRow(0), 24)..DisplayPoint::new(DisplayRow(0), 26),
1244                    DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1245                    DisplayPoint::new(DisplayRow(2), 71)..DisplayPoint::new(DisplayRow(2), 73),
1246                    DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 3),
1247                    DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
1248                    DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
1249                    DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 62),
1250                ]
1251            );
1252        });
1253
1254        // Switch to a whole word search.
1255        search_bar.update(cx, |search_bar, cx| {
1256            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
1257        });
1258        let mut editor_notifications = cx.notifications(&editor);
1259        editor_notifications.next().await;
1260        editor.update(cx, |editor, cx| {
1261            assert_eq!(
1262                display_points_of(editor.all_text_background_highlights(cx)),
1263                &[
1264                    DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1265                    DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
1266                    DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
1267                ]
1268            );
1269        });
1270
1271        editor.update(cx, |editor, cx| {
1272            editor.change_selections(None, cx, |s| {
1273                s.select_display_ranges([
1274                    DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
1275                ])
1276            });
1277        });
1278        search_bar.update(cx, |search_bar, cx| {
1279            assert_eq!(search_bar.active_match_index, Some(0));
1280            search_bar.select_next_match(&SelectNextMatch, cx);
1281            assert_eq!(
1282                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1283                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1284            );
1285        });
1286        search_bar.update(cx, |search_bar, _| {
1287            assert_eq!(search_bar.active_match_index, Some(0));
1288        });
1289
1290        search_bar.update(cx, |search_bar, cx| {
1291            search_bar.select_next_match(&SelectNextMatch, cx);
1292            assert_eq!(
1293                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1294                [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1295            );
1296        });
1297        search_bar.update(cx, |search_bar, _| {
1298            assert_eq!(search_bar.active_match_index, Some(1));
1299        });
1300
1301        search_bar.update(cx, |search_bar, cx| {
1302            search_bar.select_next_match(&SelectNextMatch, cx);
1303            assert_eq!(
1304                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1305                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1306            );
1307        });
1308        search_bar.update(cx, |search_bar, _| {
1309            assert_eq!(search_bar.active_match_index, Some(2));
1310        });
1311
1312        search_bar.update(cx, |search_bar, cx| {
1313            search_bar.select_next_match(&SelectNextMatch, cx);
1314            assert_eq!(
1315                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1316                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1317            );
1318        });
1319        search_bar.update(cx, |search_bar, _| {
1320            assert_eq!(search_bar.active_match_index, Some(0));
1321        });
1322
1323        search_bar.update(cx, |search_bar, cx| {
1324            search_bar.select_prev_match(&SelectPrevMatch, cx);
1325            assert_eq!(
1326                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1327                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1328            );
1329        });
1330        search_bar.update(cx, |search_bar, _| {
1331            assert_eq!(search_bar.active_match_index, Some(2));
1332        });
1333
1334        search_bar.update(cx, |search_bar, cx| {
1335            search_bar.select_prev_match(&SelectPrevMatch, cx);
1336            assert_eq!(
1337                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1338                [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1339            );
1340        });
1341        search_bar.update(cx, |search_bar, _| {
1342            assert_eq!(search_bar.active_match_index, Some(1));
1343        });
1344
1345        search_bar.update(cx, |search_bar, cx| {
1346            search_bar.select_prev_match(&SelectPrevMatch, cx);
1347            assert_eq!(
1348                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1349                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1350            );
1351        });
1352        search_bar.update(cx, |search_bar, _| {
1353            assert_eq!(search_bar.active_match_index, Some(0));
1354        });
1355
1356        // Park the cursor in between matches and ensure that going to the previous match selects
1357        // the closest match to the left.
1358        editor.update(cx, |editor, cx| {
1359            editor.change_selections(None, cx, |s| {
1360                s.select_display_ranges([
1361                    DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
1362                ])
1363            });
1364        });
1365        search_bar.update(cx, |search_bar, cx| {
1366            assert_eq!(search_bar.active_match_index, Some(1));
1367            search_bar.select_prev_match(&SelectPrevMatch, cx);
1368            assert_eq!(
1369                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1370                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1371            );
1372        });
1373        search_bar.update(cx, |search_bar, _| {
1374            assert_eq!(search_bar.active_match_index, Some(0));
1375        });
1376
1377        // Park the cursor in between matches and ensure that going to the next match selects the
1378        // closest match to the right.
1379        editor.update(cx, |editor, cx| {
1380            editor.change_selections(None, cx, |s| {
1381                s.select_display_ranges([
1382                    DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
1383                ])
1384            });
1385        });
1386        search_bar.update(cx, |search_bar, cx| {
1387            assert_eq!(search_bar.active_match_index, Some(1));
1388            search_bar.select_next_match(&SelectNextMatch, cx);
1389            assert_eq!(
1390                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1391                [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1392            );
1393        });
1394        search_bar.update(cx, |search_bar, _| {
1395            assert_eq!(search_bar.active_match_index, Some(1));
1396        });
1397
1398        // Park the cursor after the last match and ensure that going to the previous match selects
1399        // the last match.
1400        editor.update(cx, |editor, cx| {
1401            editor.change_selections(None, cx, |s| {
1402                s.select_display_ranges([
1403                    DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
1404                ])
1405            });
1406        });
1407        search_bar.update(cx, |search_bar, cx| {
1408            assert_eq!(search_bar.active_match_index, Some(2));
1409            search_bar.select_prev_match(&SelectPrevMatch, cx);
1410            assert_eq!(
1411                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1412                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1413            );
1414        });
1415        search_bar.update(cx, |search_bar, _| {
1416            assert_eq!(search_bar.active_match_index, Some(2));
1417        });
1418
1419        // Park the cursor after the last match and ensure that going to the next match selects the
1420        // first match.
1421        editor.update(cx, |editor, cx| {
1422            editor.change_selections(None, cx, |s| {
1423                s.select_display_ranges([
1424                    DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
1425                ])
1426            });
1427        });
1428        search_bar.update(cx, |search_bar, cx| {
1429            assert_eq!(search_bar.active_match_index, Some(2));
1430            search_bar.select_next_match(&SelectNextMatch, cx);
1431            assert_eq!(
1432                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1433                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1434            );
1435        });
1436        search_bar.update(cx, |search_bar, _| {
1437            assert_eq!(search_bar.active_match_index, Some(0));
1438        });
1439
1440        // Park the cursor before the first match and ensure that going to the previous match
1441        // selects the last match.
1442        editor.update(cx, |editor, cx| {
1443            editor.change_selections(None, cx, |s| {
1444                s.select_display_ranges([
1445                    DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
1446                ])
1447            });
1448        });
1449        search_bar.update(cx, |search_bar, cx| {
1450            assert_eq!(search_bar.active_match_index, Some(0));
1451            search_bar.select_prev_match(&SelectPrevMatch, cx);
1452            assert_eq!(
1453                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1454                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1455            );
1456        });
1457        search_bar.update(cx, |search_bar, _| {
1458            assert_eq!(search_bar.active_match_index, Some(2));
1459        });
1460    }
1461
1462    fn display_points_of(
1463        background_highlights: Vec<(Range<DisplayPoint>, Hsla)>,
1464    ) -> Vec<Range<DisplayPoint>> {
1465        background_highlights
1466            .into_iter()
1467            .map(|(range, _)| range)
1468            .collect::<Vec<_>>()
1469    }
1470
1471    #[gpui::test]
1472    async fn test_search_option_handling(cx: &mut TestAppContext) {
1473        let (editor, search_bar, cx) = init_test(cx);
1474
1475        // show with options should make current search case sensitive
1476        search_bar
1477            .update(cx, |search_bar, cx| {
1478                search_bar.show(cx);
1479                search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), cx)
1480            })
1481            .await
1482            .unwrap();
1483        editor.update(cx, |editor, cx| {
1484            assert_eq!(
1485                display_points_of(editor.all_text_background_highlights(cx)),
1486                &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
1487            );
1488        });
1489
1490        // search_suggested should restore default options
1491        search_bar.update(cx, |search_bar, cx| {
1492            search_bar.search_suggested(cx);
1493            assert_eq!(search_bar.search_options, SearchOptions::NONE)
1494        });
1495
1496        // toggling a search option should update the defaults
1497        search_bar
1498            .update(cx, |search_bar, cx| {
1499                search_bar.search("regex", Some(SearchOptions::CASE_SENSITIVE), cx)
1500            })
1501            .await
1502            .unwrap();
1503        search_bar.update(cx, |search_bar, cx| {
1504            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx)
1505        });
1506        let mut editor_notifications = cx.notifications(&editor);
1507        editor_notifications.next().await;
1508        editor.update(cx, |editor, cx| {
1509            assert_eq!(
1510                display_points_of(editor.all_text_background_highlights(cx)),
1511                &[DisplayPoint::new(DisplayRow(0), 35)..DisplayPoint::new(DisplayRow(0), 40),]
1512            );
1513        });
1514
1515        // defaults should still include whole word
1516        search_bar.update(cx, |search_bar, cx| {
1517            search_bar.search_suggested(cx);
1518            assert_eq!(
1519                search_bar.search_options,
1520                SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD
1521            )
1522        });
1523    }
1524
1525    #[gpui::test]
1526    async fn test_search_select_all_matches(cx: &mut TestAppContext) {
1527        init_globals(cx);
1528        let buffer_text = r#"
1529        A regular expression (shortened as regex or regexp;[1] also referred to as
1530        rational expression[2][3]) is a sequence of characters that specifies a search
1531        pattern in text. Usually such patterns are used by string-searching algorithms
1532        for "find" or "find and replace" operations on strings, or for input validation.
1533        "#
1534        .unindent();
1535        let expected_query_matches_count = buffer_text
1536            .chars()
1537            .filter(|c| c.to_ascii_lowercase() == 'a')
1538            .count();
1539        assert!(
1540            expected_query_matches_count > 1,
1541            "Should pick a query with multiple results"
1542        );
1543        let buffer = cx.new_model(|cx| Buffer::local(buffer_text, cx));
1544        let window = cx.add_window(|_| gpui::Empty);
1545
1546        let editor = window.build_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx));
1547
1548        let search_bar = window.build_view(cx, |cx| {
1549            let mut search_bar = BufferSearchBar::new(cx);
1550            search_bar.set_active_pane_item(Some(&editor), cx);
1551            search_bar.show(cx);
1552            search_bar
1553        });
1554
1555        window
1556            .update(cx, |_, cx| {
1557                search_bar.update(cx, |search_bar, cx| search_bar.search("a", None, cx))
1558            })
1559            .unwrap()
1560            .await
1561            .unwrap();
1562        let initial_selections = window
1563            .update(cx, |_, cx| {
1564                search_bar.update(cx, |search_bar, cx| {
1565                    let handle = search_bar.query_editor.focus_handle(cx);
1566                    cx.focus(&handle);
1567                    search_bar.activate_current_match(cx);
1568                });
1569                assert!(
1570                    !editor.read(cx).is_focused(cx),
1571                    "Initially, the editor should not be focused"
1572                );
1573                let initial_selections = editor.update(cx, |editor, cx| {
1574                    let initial_selections = editor.selections.display_ranges(cx);
1575                    assert_eq!(
1576                        initial_selections.len(), 1,
1577                        "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
1578                    );
1579                    initial_selections
1580                });
1581                search_bar.update(cx, |search_bar, cx| {
1582                    assert_eq!(search_bar.active_match_index, Some(0));
1583                    let handle = search_bar.query_editor.focus_handle(cx);
1584                    cx.focus(&handle);
1585                    search_bar.select_all_matches(&SelectAllMatches, cx);
1586                });
1587                assert!(
1588                    editor.read(cx).is_focused(cx),
1589                    "Should focus editor after successful SelectAllMatches"
1590                );
1591                search_bar.update(cx, |search_bar, cx| {
1592                    let all_selections =
1593                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1594                    assert_eq!(
1595                        all_selections.len(),
1596                        expected_query_matches_count,
1597                        "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1598                    );
1599                    assert_eq!(
1600                        search_bar.active_match_index,
1601                        Some(0),
1602                        "Match index should not change after selecting all matches"
1603                    );
1604                });
1605
1606                search_bar.update(cx, |this, cx| this.select_next_match(&SelectNextMatch, cx));
1607                initial_selections
1608            }).unwrap();
1609
1610        window
1611            .update(cx, |_, cx| {
1612                assert!(
1613                    editor.read(cx).is_focused(cx),
1614                    "Should still have editor focused after SelectNextMatch"
1615                );
1616                search_bar.update(cx, |search_bar, cx| {
1617                    let all_selections =
1618                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1619                    assert_eq!(
1620                        all_selections.len(),
1621                        1,
1622                        "On next match, should deselect items and select the next match"
1623                    );
1624                    assert_ne!(
1625                        all_selections, initial_selections,
1626                        "Next match should be different from the first selection"
1627                    );
1628                    assert_eq!(
1629                        search_bar.active_match_index,
1630                        Some(1),
1631                        "Match index should be updated to the next one"
1632                    );
1633                    let handle = search_bar.query_editor.focus_handle(cx);
1634                    cx.focus(&handle);
1635                    search_bar.select_all_matches(&SelectAllMatches, cx);
1636                });
1637            })
1638            .unwrap();
1639        window
1640            .update(cx, |_, cx| {
1641                assert!(
1642                    editor.read(cx).is_focused(cx),
1643                    "Should focus editor after successful SelectAllMatches"
1644                );
1645                search_bar.update(cx, |search_bar, cx| {
1646                    let all_selections =
1647                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1648                    assert_eq!(
1649                    all_selections.len(),
1650                    expected_query_matches_count,
1651                    "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1652                );
1653                    assert_eq!(
1654                        search_bar.active_match_index,
1655                        Some(1),
1656                        "Match index should not change after selecting all matches"
1657                    );
1658                });
1659                search_bar.update(cx, |search_bar, cx| {
1660                    search_bar.select_prev_match(&SelectPrevMatch, cx);
1661                });
1662            })
1663            .unwrap();
1664        let last_match_selections = window
1665            .update(cx, |_, cx| {
1666                assert!(
1667                    editor.read(cx).is_focused(&cx),
1668                    "Should still have editor focused after SelectPrevMatch"
1669                );
1670
1671                search_bar.update(cx, |search_bar, cx| {
1672                    let all_selections =
1673                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1674                    assert_eq!(
1675                        all_selections.len(),
1676                        1,
1677                        "On previous match, should deselect items and select the previous item"
1678                    );
1679                    assert_eq!(
1680                        all_selections, initial_selections,
1681                        "Previous match should be the same as the first selection"
1682                    );
1683                    assert_eq!(
1684                        search_bar.active_match_index,
1685                        Some(0),
1686                        "Match index should be updated to the previous one"
1687                    );
1688                    all_selections
1689                })
1690            })
1691            .unwrap();
1692
1693        window
1694            .update(cx, |_, cx| {
1695                search_bar.update(cx, |search_bar, cx| {
1696                    let handle = search_bar.query_editor.focus_handle(cx);
1697                    cx.focus(&handle);
1698                    search_bar.search("abas_nonexistent_match", None, cx)
1699                })
1700            })
1701            .unwrap()
1702            .await
1703            .unwrap();
1704        window
1705            .update(cx, |_, cx| {
1706                search_bar.update(cx, |search_bar, cx| {
1707                    search_bar.select_all_matches(&SelectAllMatches, cx);
1708                });
1709                assert!(
1710                    editor.update(cx, |this, cx| !this.is_focused(cx.window_context())),
1711                    "Should not switch focus to editor if SelectAllMatches does not find any matches"
1712                );
1713                search_bar.update(cx, |search_bar, cx| {
1714                    let all_selections =
1715                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1716                    assert_eq!(
1717                        all_selections, last_match_selections,
1718                        "Should not select anything new if there are no matches"
1719                    );
1720                    assert!(
1721                        search_bar.active_match_index.is_none(),
1722                        "For no matches, there should be no active match index"
1723                    );
1724                });
1725            })
1726            .unwrap();
1727    }
1728
1729    #[gpui::test]
1730    async fn test_search_query_history(cx: &mut TestAppContext) {
1731        init_globals(cx);
1732        let buffer_text = r#"
1733        A regular expression (shortened as regex or regexp;[1] also referred to as
1734        rational expression[2][3]) is a sequence of characters that specifies a search
1735        pattern in text. Usually such patterns are used by string-searching algorithms
1736        for "find" or "find and replace" operations on strings, or for input validation.
1737        "#
1738        .unindent();
1739        let buffer = cx.new_model(|cx| Buffer::local(buffer_text, cx));
1740        let cx = cx.add_empty_window();
1741
1742        let editor = cx.new_view(|cx| Editor::for_buffer(buffer.clone(), None, cx));
1743
1744        let search_bar = cx.new_view(|cx| {
1745            let mut search_bar = BufferSearchBar::new(cx);
1746            search_bar.set_active_pane_item(Some(&editor), cx);
1747            search_bar.show(cx);
1748            search_bar
1749        });
1750
1751        // Add 3 search items into the history.
1752        search_bar
1753            .update(cx, |search_bar, cx| search_bar.search("a", None, cx))
1754            .await
1755            .unwrap();
1756        search_bar
1757            .update(cx, |search_bar, cx| search_bar.search("b", None, cx))
1758            .await
1759            .unwrap();
1760        search_bar
1761            .update(cx, |search_bar, cx| {
1762                search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), cx)
1763            })
1764            .await
1765            .unwrap();
1766        // Ensure that the latest search is active.
1767        search_bar.update(cx, |search_bar, cx| {
1768            assert_eq!(search_bar.query(cx), "c");
1769            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1770        });
1771
1772        // Next history query after the latest should set the query to the empty string.
1773        search_bar.update(cx, |search_bar, cx| {
1774            search_bar.next_history_query(&NextHistoryQuery, cx);
1775        });
1776        search_bar.update(cx, |search_bar, cx| {
1777            assert_eq!(search_bar.query(cx), "");
1778            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1779        });
1780        search_bar.update(cx, |search_bar, cx| {
1781            search_bar.next_history_query(&NextHistoryQuery, cx);
1782        });
1783        search_bar.update(cx, |search_bar, cx| {
1784            assert_eq!(search_bar.query(cx), "");
1785            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1786        });
1787
1788        // First previous query for empty current query should set the query to the latest.
1789        search_bar.update(cx, |search_bar, cx| {
1790            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1791        });
1792        search_bar.update(cx, |search_bar, cx| {
1793            assert_eq!(search_bar.query(cx), "c");
1794            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1795        });
1796
1797        // Further previous items should go over the history in reverse order.
1798        search_bar.update(cx, |search_bar, cx| {
1799            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1800        });
1801        search_bar.update(cx, |search_bar, cx| {
1802            assert_eq!(search_bar.query(cx), "b");
1803            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1804        });
1805
1806        // Previous items should never go behind the first history item.
1807        search_bar.update(cx, |search_bar, cx| {
1808            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1809        });
1810        search_bar.update(cx, |search_bar, cx| {
1811            assert_eq!(search_bar.query(cx), "a");
1812            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1813        });
1814        search_bar.update(cx, |search_bar, cx| {
1815            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1816        });
1817        search_bar.update(cx, |search_bar, cx| {
1818            assert_eq!(search_bar.query(cx), "a");
1819            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1820        });
1821
1822        // Next items should go over the history in the original order.
1823        search_bar.update(cx, |search_bar, cx| {
1824            search_bar.next_history_query(&NextHistoryQuery, cx);
1825        });
1826        search_bar.update(cx, |search_bar, cx| {
1827            assert_eq!(search_bar.query(cx), "b");
1828            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1829        });
1830
1831        search_bar
1832            .update(cx, |search_bar, cx| search_bar.search("ba", None, cx))
1833            .await
1834            .unwrap();
1835        search_bar.update(cx, |search_bar, cx| {
1836            assert_eq!(search_bar.query(cx), "ba");
1837            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1838        });
1839
1840        // New search input should add another entry to history and move the selection to the end of the history.
1841        search_bar.update(cx, |search_bar, cx| {
1842            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1843        });
1844        search_bar.update(cx, |search_bar, cx| {
1845            assert_eq!(search_bar.query(cx), "c");
1846            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1847        });
1848        search_bar.update(cx, |search_bar, cx| {
1849            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1850        });
1851        search_bar.update(cx, |search_bar, cx| {
1852            assert_eq!(search_bar.query(cx), "b");
1853            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1854        });
1855        search_bar.update(cx, |search_bar, cx| {
1856            search_bar.next_history_query(&NextHistoryQuery, cx);
1857        });
1858        search_bar.update(cx, |search_bar, cx| {
1859            assert_eq!(search_bar.query(cx), "c");
1860            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1861        });
1862        search_bar.update(cx, |search_bar, cx| {
1863            search_bar.next_history_query(&NextHistoryQuery, cx);
1864        });
1865        search_bar.update(cx, |search_bar, cx| {
1866            assert_eq!(search_bar.query(cx), "ba");
1867            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1868        });
1869        search_bar.update(cx, |search_bar, cx| {
1870            search_bar.next_history_query(&NextHistoryQuery, cx);
1871        });
1872        search_bar.update(cx, |search_bar, cx| {
1873            assert_eq!(search_bar.query(cx), "");
1874            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1875        });
1876    }
1877
1878    #[gpui::test]
1879    async fn test_replace_simple(cx: &mut TestAppContext) {
1880        let (editor, search_bar, cx) = init_test(cx);
1881
1882        search_bar
1883            .update(cx, |search_bar, cx| {
1884                search_bar.search("expression", None, cx)
1885            })
1886            .await
1887            .unwrap();
1888
1889        search_bar.update(cx, |search_bar, cx| {
1890            search_bar.replacement_editor.update(cx, |editor, cx| {
1891                // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally.
1892                editor.set_text("expr$1", cx);
1893            });
1894            search_bar.replace_all(&ReplaceAll, cx)
1895        });
1896        assert_eq!(
1897            editor.update(cx, |this, cx| { this.text(cx) }),
1898            r#"
1899        A regular expr$1 (shortened as regex or regexp;[1] also referred to as
1900        rational expr$1[2][3]) is a sequence of characters that specifies a search
1901        pattern in text. Usually such patterns are used by string-searching algorithms
1902        for "find" or "find and replace" operations on strings, or for input validation.
1903        "#
1904            .unindent()
1905        );
1906
1907        // Search for word boundaries and replace just a single one.
1908        search_bar
1909            .update(cx, |search_bar, cx| {
1910                search_bar.search("or", Some(SearchOptions::WHOLE_WORD), cx)
1911            })
1912            .await
1913            .unwrap();
1914
1915        search_bar.update(cx, |search_bar, cx| {
1916            search_bar.replacement_editor.update(cx, |editor, cx| {
1917                editor.set_text("banana", cx);
1918            });
1919            search_bar.replace_next(&ReplaceNext, cx)
1920        });
1921        // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text.
1922        assert_eq!(
1923            editor.update(cx, |this, cx| { this.text(cx) }),
1924            r#"
1925        A regular expr$1 (shortened as regex banana regexp;[1] also referred to as
1926        rational expr$1[2][3]) is a sequence of characters that specifies a search
1927        pattern in text. Usually such patterns are used by string-searching algorithms
1928        for "find" or "find and replace" operations on strings, or for input validation.
1929        "#
1930            .unindent()
1931        );
1932        // Let's turn on regex mode.
1933        search_bar
1934            .update(cx, |search_bar, cx| {
1935                search_bar.search("\\[([^\\]]+)\\]", Some(SearchOptions::REGEX), cx)
1936            })
1937            .await
1938            .unwrap();
1939        search_bar.update(cx, |search_bar, cx| {
1940            search_bar.replacement_editor.update(cx, |editor, cx| {
1941                editor.set_text("${1}number", cx);
1942            });
1943            search_bar.replace_all(&ReplaceAll, cx)
1944        });
1945        assert_eq!(
1946            editor.update(cx, |this, cx| { this.text(cx) }),
1947            r#"
1948        A regular expr$1 (shortened as regex banana regexp;1number also referred to as
1949        rational expr$12number3number) is a sequence of characters that specifies a search
1950        pattern in text. Usually such patterns are used by string-searching algorithms
1951        for "find" or "find and replace" operations on strings, or for input validation.
1952        "#
1953            .unindent()
1954        );
1955        // Now with a whole-word twist.
1956        search_bar
1957            .update(cx, |search_bar, cx| {
1958                search_bar.search(
1959                    "a\\w+s",
1960                    Some(SearchOptions::REGEX | SearchOptions::WHOLE_WORD),
1961                    cx,
1962                )
1963            })
1964            .await
1965            .unwrap();
1966        search_bar.update(cx, |search_bar, cx| {
1967            search_bar.replacement_editor.update(cx, |editor, cx| {
1968                editor.set_text("things", cx);
1969            });
1970            search_bar.replace_all(&ReplaceAll, cx)
1971        });
1972        // The only word affected by this edit should be `algorithms`, even though there's a bunch
1973        // of words in this text that would match this regex if not for WHOLE_WORD.
1974        assert_eq!(
1975            editor.update(cx, |this, cx| { this.text(cx) }),
1976            r#"
1977        A regular expr$1 (shortened as regex banana regexp;1number also referred to as
1978        rational expr$12number3number) is a sequence of characters that specifies a search
1979        pattern in text. Usually such patterns are used by string-searching things
1980        for "find" or "find and replace" operations on strings, or for input validation.
1981        "#
1982            .unindent()
1983        );
1984    }
1985
1986    struct ReplacementTestParams<'a> {
1987        editor: &'a View<Editor>,
1988        search_bar: &'a View<BufferSearchBar>,
1989        cx: &'a mut VisualTestContext,
1990        search_text: &'static str,
1991        search_options: Option<SearchOptions>,
1992        replacement_text: &'static str,
1993        replace_all: bool,
1994        expected_text: String,
1995    }
1996
1997    async fn run_replacement_test(options: ReplacementTestParams<'_>) {
1998        options
1999            .search_bar
2000            .update(options.cx, |search_bar, cx| {
2001                if let Some(options) = options.search_options {
2002                    search_bar.set_search_options(options, cx);
2003                }
2004                search_bar.search(options.search_text, options.search_options, cx)
2005            })
2006            .await
2007            .unwrap();
2008
2009        options.search_bar.update(options.cx, |search_bar, cx| {
2010            search_bar.replacement_editor.update(cx, |editor, cx| {
2011                editor.set_text(options.replacement_text, cx);
2012            });
2013
2014            if options.replace_all {
2015                search_bar.replace_all(&ReplaceAll, cx)
2016            } else {
2017                search_bar.replace_next(&ReplaceNext, cx)
2018            }
2019        });
2020
2021        assert_eq!(
2022            options
2023                .editor
2024                .update(options.cx, |this, cx| { this.text(cx) }),
2025            options.expected_text
2026        );
2027    }
2028
2029    #[gpui::test]
2030    async fn test_replace_special_characters(cx: &mut TestAppContext) {
2031        let (editor, search_bar, cx) = init_test(cx);
2032
2033        run_replacement_test(ReplacementTestParams {
2034            editor: &editor,
2035            search_bar: &search_bar,
2036            cx,
2037            search_text: "expression",
2038            search_options: None,
2039            replacement_text: r"\n",
2040            replace_all: true,
2041            expected_text: r#"
2042            A regular \n (shortened as regex or regexp;[1] also referred to as
2043            rational \n[2][3]) is a sequence of characters that specifies a search
2044            pattern in text. Usually such patterns are used by string-searching algorithms
2045            for "find" or "find and replace" operations on strings, or for input validation.
2046            "#
2047            .unindent(),
2048        })
2049        .await;
2050
2051        run_replacement_test(ReplacementTestParams {
2052            editor: &editor,
2053            search_bar: &search_bar,
2054            cx,
2055            search_text: "or",
2056            search_options: Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
2057            replacement_text: r"\\\n\\\\",
2058            replace_all: false,
2059            expected_text: r#"
2060            A regular \n (shortened as regex \
2061            \\ regexp;[1] also referred to as
2062            rational \n[2][3]) is a sequence of characters that specifies a search
2063            pattern in text. Usually such patterns are used by string-searching algorithms
2064            for "find" or "find and replace" operations on strings, or for input validation.
2065            "#
2066            .unindent(),
2067        })
2068        .await;
2069
2070        run_replacement_test(ReplacementTestParams {
2071            editor: &editor,
2072            search_bar: &search_bar,
2073            cx,
2074            search_text: r"(that|used) ",
2075            search_options: Some(SearchOptions::REGEX),
2076            replacement_text: r"$1\n",
2077            replace_all: true,
2078            expected_text: r#"
2079            A regular \n (shortened as regex \
2080            \\ regexp;[1] also referred to as
2081            rational \n[2][3]) is a sequence of characters that
2082            specifies a search
2083            pattern in text. Usually such patterns are used
2084            by string-searching algorithms
2085            for "find" or "find and replace" operations on strings, or for input validation.
2086            "#
2087            .unindent(),
2088        })
2089        .await;
2090    }
2091
2092    #[gpui::test]
2093    async fn test_find_matches_in_selections_singleton_buffer_multiple_selections(
2094        cx: &mut TestAppContext,
2095    ) {
2096        init_globals(cx);
2097        let buffer = cx.new_model(|cx| {
2098            Buffer::local(
2099                r#"
2100                aaa bbb aaa ccc
2101                aaa bbb aaa ccc
2102                aaa bbb aaa ccc
2103                aaa bbb aaa ccc
2104                aaa bbb aaa ccc
2105                aaa bbb aaa ccc
2106                "#
2107                .unindent(),
2108                cx,
2109            )
2110        });
2111        let cx = cx.add_empty_window();
2112        let editor = cx.new_view(|cx| Editor::for_buffer(buffer.clone(), None, cx));
2113
2114        let search_bar = cx.new_view(|cx| {
2115            let mut search_bar = BufferSearchBar::new(cx);
2116            search_bar.set_active_pane_item(Some(&editor), cx);
2117            search_bar.show(cx);
2118            search_bar
2119        });
2120
2121        editor.update(cx, |editor, cx| {
2122            editor.change_selections(None, cx, |s| {
2123                s.select_ranges(vec![Point::new(1, 0)..Point::new(2, 4)])
2124            })
2125        });
2126
2127        search_bar.update(cx, |search_bar, cx| {
2128            let deploy = Deploy {
2129                focus: true,
2130                replace_enabled: false,
2131                selection_search_enabled: true,
2132            };
2133            search_bar.deploy(&deploy, cx);
2134        });
2135
2136        cx.run_until_parked();
2137
2138        search_bar
2139            .update(cx, |search_bar, cx| search_bar.search("aaa", None, cx))
2140            .await
2141            .unwrap();
2142
2143        editor.update(cx, |editor, cx| {
2144            assert_eq!(
2145                editor.search_background_highlights(cx),
2146                &[
2147                    Point::new(1, 0)..Point::new(1, 3),
2148                    Point::new(1, 8)..Point::new(1, 11),
2149                    Point::new(2, 0)..Point::new(2, 3),
2150                ]
2151            );
2152        });
2153    }
2154
2155    #[gpui::test]
2156    async fn test_find_matches_in_selections_multiple_excerpts_buffer_multiple_selections(
2157        cx: &mut TestAppContext,
2158    ) {
2159        init_globals(cx);
2160        let text = r#"
2161            aaa bbb aaa ccc
2162            aaa bbb aaa ccc
2163            aaa bbb aaa ccc
2164            aaa bbb aaa ccc
2165            aaa bbb aaa ccc
2166            aaa bbb aaa ccc
2167
2168            aaa bbb aaa ccc
2169            aaa bbb aaa ccc
2170            aaa bbb aaa ccc
2171            aaa bbb aaa ccc
2172            aaa bbb aaa ccc
2173            aaa bbb aaa ccc
2174            "#
2175        .unindent();
2176
2177        let cx = cx.add_empty_window();
2178        let editor = cx.new_view(|cx| {
2179            let multibuffer = MultiBuffer::build_multi(
2180                [
2181                    (
2182                        &text,
2183                        vec![
2184                            Point::new(0, 0)..Point::new(2, 0),
2185                            Point::new(4, 0)..Point::new(5, 0),
2186                        ],
2187                    ),
2188                    (&text, vec![Point::new(9, 0)..Point::new(11, 0)]),
2189                ],
2190                cx,
2191            );
2192            Editor::for_multibuffer(multibuffer, None, false, cx)
2193        });
2194
2195        let search_bar = cx.new_view(|cx| {
2196            let mut search_bar = BufferSearchBar::new(cx);
2197            search_bar.set_active_pane_item(Some(&editor), cx);
2198            search_bar.show(cx);
2199            search_bar
2200        });
2201
2202        editor.update(cx, |editor, cx| {
2203            editor.change_selections(None, cx, |s| {
2204                s.select_ranges(vec![
2205                    Point::new(1, 0)..Point::new(1, 4),
2206                    Point::new(5, 3)..Point::new(6, 4),
2207                ])
2208            })
2209        });
2210
2211        search_bar.update(cx, |search_bar, cx| {
2212            let deploy = Deploy {
2213                focus: true,
2214                replace_enabled: false,
2215                selection_search_enabled: true,
2216            };
2217            search_bar.deploy(&deploy, cx);
2218        });
2219
2220        cx.run_until_parked();
2221
2222        search_bar
2223            .update(cx, |search_bar, cx| search_bar.search("aaa", None, cx))
2224            .await
2225            .unwrap();
2226
2227        editor.update(cx, |editor, cx| {
2228            assert_eq!(
2229                editor.search_background_highlights(cx),
2230                &[
2231                    Point::new(1, 0)..Point::new(1, 3),
2232                    Point::new(5, 8)..Point::new(5, 11),
2233                    Point::new(6, 0)..Point::new(6, 3),
2234                ]
2235            );
2236        });
2237    }
2238
2239    #[gpui::test]
2240    async fn test_invalid_regexp_search_after_valid(cx: &mut TestAppContext) {
2241        let (editor, search_bar, cx) = init_test(cx);
2242        // Search using valid regexp
2243        search_bar
2244            .update(cx, |search_bar, cx| {
2245                search_bar.enable_search_option(SearchOptions::REGEX, cx);
2246                search_bar.search("expression", None, cx)
2247            })
2248            .await
2249            .unwrap();
2250        editor.update(cx, |editor, cx| {
2251            assert_eq!(
2252                display_points_of(editor.all_text_background_highlights(cx)),
2253                &[
2254                    DisplayPoint::new(DisplayRow(0), 10)..DisplayPoint::new(DisplayRow(0), 20),
2255                    DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 19),
2256                ],
2257            );
2258        });
2259
2260        // Now, the expression is invalid
2261        search_bar
2262            .update(cx, |search_bar, cx| {
2263                search_bar.search("expression (", None, cx)
2264            })
2265            .await
2266            .unwrap_err();
2267        editor.update(cx, |editor, cx| {
2268            assert!(display_points_of(editor.all_text_background_highlights(cx)).is_empty(),);
2269        });
2270    }
2271}