buffer_search.rs

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