buffer_search.rs

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