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