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