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