buffer_search.rs

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