buffer_search.rs

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