buffer_search.rs

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