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