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            cx.focus(&handle);
 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                                active_searchable_item.update_matches(matches, cx);
 992                                let _ = done_tx.send(());
 993                            }
 994                            cx.notify();
 995                        }
 996                    })
 997                    .log_err();
 998                }));
 999            }
1000        }
1001        done_rx
1002    }
1003
1004    pub fn update_match_index(&mut self, cx: &mut ViewContext<Self>) {
1005        let new_index = self
1006            .active_searchable_item
1007            .as_ref()
1008            .and_then(|searchable_item| {
1009                let matches = self
1010                    .searchable_items_with_matches
1011                    .get(&searchable_item.downgrade())?;
1012                searchable_item.active_match_index(matches, cx)
1013            });
1014        if new_index != self.active_match_index {
1015            self.active_match_index = new_index;
1016            cx.notify();
1017        }
1018    }
1019
1020    fn tab(&mut self, _: &Tab, cx: &mut ViewContext<Self>) {
1021        // Search -> Replace -> Editor
1022        let focus_handle = if self.replace_enabled && self.query_editor_focused {
1023            self.replacement_editor.focus_handle(cx)
1024        } else if let Some(item) = self.active_searchable_item.as_ref() {
1025            item.focus_handle(cx)
1026        } else {
1027            return;
1028        };
1029        cx.focus(&focus_handle);
1030        cx.stop_propagation();
1031    }
1032
1033    fn tab_prev(&mut self, _: &TabPrev, cx: &mut ViewContext<Self>) {
1034        // Search -> Replace -> Search
1035        let focus_handle = if self.replace_enabled && self.query_editor_focused {
1036            self.replacement_editor.focus_handle(cx)
1037        } else if self.replacement_editor_focused {
1038            self.query_editor.focus_handle(cx)
1039        } else {
1040            return;
1041        };
1042        cx.focus(&focus_handle);
1043        cx.stop_propagation();
1044    }
1045
1046    fn next_history_query(&mut self, _: &NextHistoryQuery, cx: &mut ViewContext<Self>) {
1047        if let Some(new_query) = self
1048            .search_history
1049            .next(&mut self.search_history_cursor)
1050            .map(str::to_string)
1051        {
1052            drop(self.search(&new_query, Some(self.search_options), cx));
1053        } else {
1054            self.search_history_cursor.reset();
1055            drop(self.search("", Some(self.search_options), cx));
1056        }
1057    }
1058
1059    fn previous_history_query(&mut self, _: &PreviousHistoryQuery, cx: &mut ViewContext<Self>) {
1060        if self.query(cx).is_empty() {
1061            if let Some(new_query) = self
1062                .search_history
1063                .current(&mut self.search_history_cursor)
1064                .map(str::to_string)
1065            {
1066                drop(self.search(&new_query, Some(self.search_options), cx));
1067                return;
1068            }
1069        }
1070
1071        if let Some(new_query) = self
1072            .search_history
1073            .previous(&mut self.search_history_cursor)
1074            .map(str::to_string)
1075        {
1076            drop(self.search(&new_query, Some(self.search_options), cx));
1077        }
1078    }
1079
1080    fn toggle_replace(&mut self, _: &ToggleReplace, cx: &mut ViewContext<Self>) {
1081        if let Some(_) = &self.active_searchable_item {
1082            self.replace_enabled = !self.replace_enabled;
1083            let handle = if self.replace_enabled {
1084                self.replacement_editor.focus_handle(cx)
1085            } else {
1086                self.query_editor.focus_handle(cx)
1087            };
1088            cx.focus(&handle);
1089            cx.notify();
1090        }
1091    }
1092    fn replace_next(&mut self, _: &ReplaceNext, cx: &mut ViewContext<Self>) {
1093        let mut should_propagate = true;
1094        if !self.dismissed && self.active_search.is_some() {
1095            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1096                if let Some(query) = self.active_search.as_ref() {
1097                    if let Some(matches) = self
1098                        .searchable_items_with_matches
1099                        .get(&searchable_item.downgrade())
1100                    {
1101                        if let Some(active_index) = self.active_match_index {
1102                            let query = query
1103                                .as_ref()
1104                                .clone()
1105                                .with_replacement(self.replacement(cx));
1106                            searchable_item.replace(matches.at(active_index), &query, cx);
1107                            self.select_next_match(&SelectNextMatch, cx);
1108                        }
1109                        should_propagate = false;
1110                        self.focus_editor(&FocusEditor, cx);
1111                    }
1112                }
1113            }
1114        }
1115        if !should_propagate {
1116            cx.stop_propagation();
1117        }
1118    }
1119    pub fn replace_all(&mut self, _: &ReplaceAll, cx: &mut ViewContext<Self>) {
1120        if !self.dismissed && self.active_search.is_some() {
1121            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1122                if let Some(query) = self.active_search.as_ref() {
1123                    if let Some(matches) = self
1124                        .searchable_items_with_matches
1125                        .get(&searchable_item.downgrade())
1126                    {
1127                        let query = query
1128                            .as_ref()
1129                            .clone()
1130                            .with_replacement(self.replacement(cx));
1131                        searchable_item.replace_all(&mut matches.iter(), &query, cx);
1132                    }
1133                }
1134            }
1135        }
1136    }
1137
1138    pub fn match_exists(&mut self, cx: &mut ViewContext<Self>) -> bool {
1139        self.update_match_index(cx);
1140        self.active_match_index.is_some()
1141    }
1142}
1143
1144#[cfg(test)]
1145mod tests {
1146    use std::ops::Range;
1147
1148    use super::*;
1149    use editor::{display_map::DisplayRow, DisplayPoint, Editor, MultiBuffer};
1150    use gpui::{Context, Hsla, TestAppContext, VisualTestContext};
1151    use language::{Buffer, Point};
1152    use project::Project;
1153    use smol::stream::StreamExt as _;
1154    use unindent::Unindent as _;
1155
1156    fn init_globals(cx: &mut TestAppContext) {
1157        cx.update(|cx| {
1158            let store = settings::SettingsStore::test(cx);
1159            cx.set_global(store);
1160            editor::init(cx);
1161
1162            language::init(cx);
1163            Project::init_settings(cx);
1164            theme::init(theme::LoadThemes::JustBase, cx);
1165        });
1166    }
1167
1168    fn init_test(
1169        cx: &mut TestAppContext,
1170    ) -> (View<Editor>, View<BufferSearchBar>, &mut VisualTestContext) {
1171        init_globals(cx);
1172        let buffer = cx.new_model(|cx| {
1173            Buffer::local(
1174                r#"
1175                A regular expression (shortened as regex or regexp;[1] also referred to as
1176                rational expression[2][3]) is a sequence of characters that specifies a search
1177                pattern in text. Usually such patterns are used by string-searching algorithms
1178                for "find" or "find and replace" operations on strings, or for input validation.
1179                "#
1180                .unindent(),
1181                cx,
1182            )
1183        });
1184        let cx = cx.add_empty_window();
1185        let editor = cx.new_view(|cx| Editor::for_buffer(buffer.clone(), None, cx));
1186
1187        let search_bar = cx.new_view(|cx| {
1188            let mut search_bar = BufferSearchBar::new(cx);
1189            search_bar.set_active_pane_item(Some(&editor), cx);
1190            search_bar.show(cx);
1191            search_bar
1192        });
1193
1194        (editor, search_bar, cx)
1195    }
1196
1197    #[gpui::test]
1198    async fn test_search_simple(cx: &mut TestAppContext) {
1199        let (editor, search_bar, cx) = init_test(cx);
1200        let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
1201            background_highlights
1202                .into_iter()
1203                .map(|(range, _)| range)
1204                .collect::<Vec<_>>()
1205        };
1206        // Search for a string that appears with different casing.
1207        // By default, search is case-insensitive.
1208        search_bar
1209            .update(cx, |search_bar, cx| search_bar.search("us", None, cx))
1210            .await
1211            .unwrap();
1212        editor.update(cx, |editor, cx| {
1213            assert_eq!(
1214                display_points_of(editor.all_text_background_highlights(cx)),
1215                &[
1216                    DisplayPoint::new(DisplayRow(2), 17)..DisplayPoint::new(DisplayRow(2), 19),
1217                    DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),
1218                ]
1219            );
1220        });
1221
1222        // Switch to a case sensitive search.
1223        search_bar.update(cx, |search_bar, cx| {
1224            search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx);
1225        });
1226        let mut editor_notifications = cx.notifications(&editor);
1227        editor_notifications.next().await;
1228        editor.update(cx, |editor, cx| {
1229            assert_eq!(
1230                display_points_of(editor.all_text_background_highlights(cx)),
1231                &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
1232            );
1233        });
1234
1235        // Search for a string that appears both as a whole word and
1236        // within other words. By default, all results are found.
1237        search_bar
1238            .update(cx, |search_bar, cx| search_bar.search("or", None, cx))
1239            .await
1240            .unwrap();
1241        editor.update(cx, |editor, cx| {
1242            assert_eq!(
1243                display_points_of(editor.all_text_background_highlights(cx)),
1244                &[
1245                    DisplayPoint::new(DisplayRow(0), 24)..DisplayPoint::new(DisplayRow(0), 26),
1246                    DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1247                    DisplayPoint::new(DisplayRow(2), 71)..DisplayPoint::new(DisplayRow(2), 73),
1248                    DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 3),
1249                    DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
1250                    DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
1251                    DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 62),
1252                ]
1253            );
1254        });
1255
1256        // Switch to a whole word search.
1257        search_bar.update(cx, |search_bar, cx| {
1258            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
1259        });
1260        let mut editor_notifications = cx.notifications(&editor);
1261        editor_notifications.next().await;
1262        editor.update(cx, |editor, cx| {
1263            assert_eq!(
1264                display_points_of(editor.all_text_background_highlights(cx)),
1265                &[
1266                    DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1267                    DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
1268                    DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
1269                ]
1270            );
1271        });
1272
1273        editor.update(cx, |editor, cx| {
1274            editor.change_selections(None, cx, |s| {
1275                s.select_display_ranges([
1276                    DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
1277                ])
1278            });
1279        });
1280        search_bar.update(cx, |search_bar, cx| {
1281            assert_eq!(search_bar.active_match_index, Some(0));
1282            search_bar.select_next_match(&SelectNextMatch, cx);
1283            assert_eq!(
1284                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1285                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1286            );
1287        });
1288        search_bar.update(cx, |search_bar, _| {
1289            assert_eq!(search_bar.active_match_index, Some(0));
1290        });
1291
1292        search_bar.update(cx, |search_bar, cx| {
1293            search_bar.select_next_match(&SelectNextMatch, cx);
1294            assert_eq!(
1295                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1296                [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1297            );
1298        });
1299        search_bar.update(cx, |search_bar, _| {
1300            assert_eq!(search_bar.active_match_index, Some(1));
1301        });
1302
1303        search_bar.update(cx, |search_bar, cx| {
1304            search_bar.select_next_match(&SelectNextMatch, cx);
1305            assert_eq!(
1306                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1307                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1308            );
1309        });
1310        search_bar.update(cx, |search_bar, _| {
1311            assert_eq!(search_bar.active_match_index, Some(2));
1312        });
1313
1314        search_bar.update(cx, |search_bar, cx| {
1315            search_bar.select_next_match(&SelectNextMatch, cx);
1316            assert_eq!(
1317                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1318                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1319            );
1320        });
1321        search_bar.update(cx, |search_bar, _| {
1322            assert_eq!(search_bar.active_match_index, Some(0));
1323        });
1324
1325        search_bar.update(cx, |search_bar, cx| {
1326            search_bar.select_prev_match(&SelectPrevMatch, cx);
1327            assert_eq!(
1328                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1329                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1330            );
1331        });
1332        search_bar.update(cx, |search_bar, _| {
1333            assert_eq!(search_bar.active_match_index, Some(2));
1334        });
1335
1336        search_bar.update(cx, |search_bar, cx| {
1337            search_bar.select_prev_match(&SelectPrevMatch, cx);
1338            assert_eq!(
1339                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1340                [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1341            );
1342        });
1343        search_bar.update(cx, |search_bar, _| {
1344            assert_eq!(search_bar.active_match_index, Some(1));
1345        });
1346
1347        search_bar.update(cx, |search_bar, cx| {
1348            search_bar.select_prev_match(&SelectPrevMatch, cx);
1349            assert_eq!(
1350                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1351                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1352            );
1353        });
1354        search_bar.update(cx, |search_bar, _| {
1355            assert_eq!(search_bar.active_match_index, Some(0));
1356        });
1357
1358        // Park the cursor in between matches and ensure that going to the previous match selects
1359        // the closest match to the left.
1360        editor.update(cx, |editor, cx| {
1361            editor.change_selections(None, cx, |s| {
1362                s.select_display_ranges([
1363                    DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
1364                ])
1365            });
1366        });
1367        search_bar.update(cx, |search_bar, cx| {
1368            assert_eq!(search_bar.active_match_index, Some(1));
1369            search_bar.select_prev_match(&SelectPrevMatch, cx);
1370            assert_eq!(
1371                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1372                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1373            );
1374        });
1375        search_bar.update(cx, |search_bar, _| {
1376            assert_eq!(search_bar.active_match_index, Some(0));
1377        });
1378
1379        // Park the cursor in between matches and ensure that going to the next match selects the
1380        // closest match to the right.
1381        editor.update(cx, |editor, cx| {
1382            editor.change_selections(None, cx, |s| {
1383                s.select_display_ranges([
1384                    DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
1385                ])
1386            });
1387        });
1388        search_bar.update(cx, |search_bar, cx| {
1389            assert_eq!(search_bar.active_match_index, Some(1));
1390            search_bar.select_next_match(&SelectNextMatch, cx);
1391            assert_eq!(
1392                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1393                [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1394            );
1395        });
1396        search_bar.update(cx, |search_bar, _| {
1397            assert_eq!(search_bar.active_match_index, Some(1));
1398        });
1399
1400        // Park the cursor after the last match and ensure that going to the previous match selects
1401        // the last match.
1402        editor.update(cx, |editor, cx| {
1403            editor.change_selections(None, cx, |s| {
1404                s.select_display_ranges([
1405                    DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
1406                ])
1407            });
1408        });
1409        search_bar.update(cx, |search_bar, cx| {
1410            assert_eq!(search_bar.active_match_index, Some(2));
1411            search_bar.select_prev_match(&SelectPrevMatch, cx);
1412            assert_eq!(
1413                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1414                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1415            );
1416        });
1417        search_bar.update(cx, |search_bar, _| {
1418            assert_eq!(search_bar.active_match_index, Some(2));
1419        });
1420
1421        // Park the cursor after the last match and ensure that going to the next match selects the
1422        // first match.
1423        editor.update(cx, |editor, cx| {
1424            editor.change_selections(None, cx, |s| {
1425                s.select_display_ranges([
1426                    DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
1427                ])
1428            });
1429        });
1430        search_bar.update(cx, |search_bar, cx| {
1431            assert_eq!(search_bar.active_match_index, Some(2));
1432            search_bar.select_next_match(&SelectNextMatch, cx);
1433            assert_eq!(
1434                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1435                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1436            );
1437        });
1438        search_bar.update(cx, |search_bar, _| {
1439            assert_eq!(search_bar.active_match_index, Some(0));
1440        });
1441
1442        // Park the cursor before the first match and ensure that going to the previous match
1443        // selects the last match.
1444        editor.update(cx, |editor, cx| {
1445            editor.change_selections(None, cx, |s| {
1446                s.select_display_ranges([
1447                    DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
1448                ])
1449            });
1450        });
1451        search_bar.update(cx, |search_bar, cx| {
1452            assert_eq!(search_bar.active_match_index, Some(0));
1453            search_bar.select_prev_match(&SelectPrevMatch, cx);
1454            assert_eq!(
1455                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1456                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1457            );
1458        });
1459        search_bar.update(cx, |search_bar, _| {
1460            assert_eq!(search_bar.active_match_index, Some(2));
1461        });
1462    }
1463
1464    fn display_points_of(
1465        background_highlights: Vec<(Range<DisplayPoint>, Hsla)>,
1466    ) -> Vec<Range<DisplayPoint>> {
1467        background_highlights
1468            .into_iter()
1469            .map(|(range, _)| range)
1470            .collect::<Vec<_>>()
1471    }
1472
1473    #[gpui::test]
1474    async fn test_search_option_handling(cx: &mut TestAppContext) {
1475        let (editor, search_bar, cx) = init_test(cx);
1476
1477        // show with options should make current search case sensitive
1478        search_bar
1479            .update(cx, |search_bar, cx| {
1480                search_bar.show(cx);
1481                search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), cx)
1482            })
1483            .await
1484            .unwrap();
1485        editor.update(cx, |editor, cx| {
1486            assert_eq!(
1487                display_points_of(editor.all_text_background_highlights(cx)),
1488                &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
1489            );
1490        });
1491
1492        // search_suggested should restore default options
1493        search_bar.update(cx, |search_bar, cx| {
1494            search_bar.search_suggested(cx);
1495            assert_eq!(search_bar.search_options, SearchOptions::NONE)
1496        });
1497
1498        // toggling a search option should update the defaults
1499        search_bar
1500            .update(cx, |search_bar, cx| {
1501                search_bar.search("regex", Some(SearchOptions::CASE_SENSITIVE), cx)
1502            })
1503            .await
1504            .unwrap();
1505        search_bar.update(cx, |search_bar, cx| {
1506            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx)
1507        });
1508        let mut editor_notifications = cx.notifications(&editor);
1509        editor_notifications.next().await;
1510        editor.update(cx, |editor, cx| {
1511            assert_eq!(
1512                display_points_of(editor.all_text_background_highlights(cx)),
1513                &[DisplayPoint::new(DisplayRow(0), 35)..DisplayPoint::new(DisplayRow(0), 40),]
1514            );
1515        });
1516
1517        // defaults should still include whole word
1518        search_bar.update(cx, |search_bar, cx| {
1519            search_bar.search_suggested(cx);
1520            assert_eq!(
1521                search_bar.search_options,
1522                SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD
1523            )
1524        });
1525    }
1526
1527    #[gpui::test]
1528    async fn test_search_select_all_matches(cx: &mut TestAppContext) {
1529        init_globals(cx);
1530        let buffer_text = r#"
1531        A regular expression (shortened as regex or regexp;[1] also referred to as
1532        rational expression[2][3]) is a sequence of characters that specifies a search
1533        pattern in text. Usually such patterns are used by string-searching algorithms
1534        for "find" or "find and replace" operations on strings, or for input validation.
1535        "#
1536        .unindent();
1537        let expected_query_matches_count = buffer_text
1538            .chars()
1539            .filter(|c| c.to_ascii_lowercase() == 'a')
1540            .count();
1541        assert!(
1542            expected_query_matches_count > 1,
1543            "Should pick a query with multiple results"
1544        );
1545        let buffer = cx.new_model(|cx| Buffer::local(buffer_text, cx));
1546        let window = cx.add_window(|_| gpui::Empty);
1547
1548        let editor = window.build_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx));
1549
1550        let search_bar = window.build_view(cx, |cx| {
1551            let mut search_bar = BufferSearchBar::new(cx);
1552            search_bar.set_active_pane_item(Some(&editor), cx);
1553            search_bar.show(cx);
1554            search_bar
1555        });
1556
1557        window
1558            .update(cx, |_, cx| {
1559                search_bar.update(cx, |search_bar, cx| search_bar.search("a", None, cx))
1560            })
1561            .unwrap()
1562            .await
1563            .unwrap();
1564        let initial_selections = window
1565            .update(cx, |_, cx| {
1566                search_bar.update(cx, |search_bar, cx| {
1567                    let handle = search_bar.query_editor.focus_handle(cx);
1568                    cx.focus(&handle);
1569                    search_bar.activate_current_match(cx);
1570                });
1571                assert!(
1572                    !editor.read(cx).is_focused(cx),
1573                    "Initially, the editor should not be focused"
1574                );
1575                let initial_selections = editor.update(cx, |editor, cx| {
1576                    let initial_selections = editor.selections.display_ranges(cx);
1577                    assert_eq!(
1578                        initial_selections.len(), 1,
1579                        "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
1580                    );
1581                    initial_selections
1582                });
1583                search_bar.update(cx, |search_bar, cx| {
1584                    assert_eq!(search_bar.active_match_index, Some(0));
1585                    let handle = search_bar.query_editor.focus_handle(cx);
1586                    cx.focus(&handle);
1587                    search_bar.select_all_matches(&SelectAllMatches, cx);
1588                });
1589                assert!(
1590                    editor.read(cx).is_focused(cx),
1591                    "Should focus editor after successful SelectAllMatches"
1592                );
1593                search_bar.update(cx, |search_bar, cx| {
1594                    let all_selections =
1595                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1596                    assert_eq!(
1597                        all_selections.len(),
1598                        expected_query_matches_count,
1599                        "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1600                    );
1601                    assert_eq!(
1602                        search_bar.active_match_index,
1603                        Some(0),
1604                        "Match index should not change after selecting all matches"
1605                    );
1606                });
1607
1608                search_bar.update(cx, |this, cx| this.select_next_match(&SelectNextMatch, cx));
1609                initial_selections
1610            }).unwrap();
1611
1612        window
1613            .update(cx, |_, cx| {
1614                assert!(
1615                    editor.read(cx).is_focused(cx),
1616                    "Should still have editor focused after SelectNextMatch"
1617                );
1618                search_bar.update(cx, |search_bar, cx| {
1619                    let all_selections =
1620                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1621                    assert_eq!(
1622                        all_selections.len(),
1623                        1,
1624                        "On next match, should deselect items and select the next match"
1625                    );
1626                    assert_ne!(
1627                        all_selections, initial_selections,
1628                        "Next match should be different from the first selection"
1629                    );
1630                    assert_eq!(
1631                        search_bar.active_match_index,
1632                        Some(1),
1633                        "Match index should be updated to the next one"
1634                    );
1635                    let handle = search_bar.query_editor.focus_handle(cx);
1636                    cx.focus(&handle);
1637                    search_bar.select_all_matches(&SelectAllMatches, cx);
1638                });
1639            })
1640            .unwrap();
1641        window
1642            .update(cx, |_, cx| {
1643                assert!(
1644                    editor.read(cx).is_focused(cx),
1645                    "Should focus editor after successful SelectAllMatches"
1646                );
1647                search_bar.update(cx, |search_bar, cx| {
1648                    let all_selections =
1649                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1650                    assert_eq!(
1651                    all_selections.len(),
1652                    expected_query_matches_count,
1653                    "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1654                );
1655                    assert_eq!(
1656                        search_bar.active_match_index,
1657                        Some(1),
1658                        "Match index should not change after selecting all matches"
1659                    );
1660                });
1661                search_bar.update(cx, |search_bar, cx| {
1662                    search_bar.select_prev_match(&SelectPrevMatch, cx);
1663                });
1664            })
1665            .unwrap();
1666        let last_match_selections = window
1667            .update(cx, |_, cx| {
1668                assert!(
1669                    editor.read(cx).is_focused(&cx),
1670                    "Should still have editor focused after SelectPrevMatch"
1671                );
1672
1673                search_bar.update(cx, |search_bar, cx| {
1674                    let all_selections =
1675                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1676                    assert_eq!(
1677                        all_selections.len(),
1678                        1,
1679                        "On previous match, should deselect items and select the previous item"
1680                    );
1681                    assert_eq!(
1682                        all_selections, initial_selections,
1683                        "Previous match should be the same as the first selection"
1684                    );
1685                    assert_eq!(
1686                        search_bar.active_match_index,
1687                        Some(0),
1688                        "Match index should be updated to the previous one"
1689                    );
1690                    all_selections
1691                })
1692            })
1693            .unwrap();
1694
1695        window
1696            .update(cx, |_, cx| {
1697                search_bar.update(cx, |search_bar, cx| {
1698                    let handle = search_bar.query_editor.focus_handle(cx);
1699                    cx.focus(&handle);
1700                    search_bar.search("abas_nonexistent_match", None, cx)
1701                })
1702            })
1703            .unwrap()
1704            .await
1705            .unwrap();
1706        window
1707            .update(cx, |_, cx| {
1708                search_bar.update(cx, |search_bar, cx| {
1709                    search_bar.select_all_matches(&SelectAllMatches, cx);
1710                });
1711                assert!(
1712                    editor.update(cx, |this, cx| !this.is_focused(cx.window_context())),
1713                    "Should not switch focus to editor if SelectAllMatches does not find any matches"
1714                );
1715                search_bar.update(cx, |search_bar, cx| {
1716                    let all_selections =
1717                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1718                    assert_eq!(
1719                        all_selections, last_match_selections,
1720                        "Should not select anything new if there are no matches"
1721                    );
1722                    assert!(
1723                        search_bar.active_match_index.is_none(),
1724                        "For no matches, there should be no active match index"
1725                    );
1726                });
1727            })
1728            .unwrap();
1729    }
1730
1731    #[gpui::test]
1732    async fn test_search_query_history(cx: &mut TestAppContext) {
1733        init_globals(cx);
1734        let buffer_text = r#"
1735        A regular expression (shortened as regex or regexp;[1] also referred to as
1736        rational expression[2][3]) is a sequence of characters that specifies a search
1737        pattern in text. Usually such patterns are used by string-searching algorithms
1738        for "find" or "find and replace" operations on strings, or for input validation.
1739        "#
1740        .unindent();
1741        let buffer = cx.new_model(|cx| Buffer::local(buffer_text, cx));
1742        let cx = cx.add_empty_window();
1743
1744        let editor = cx.new_view(|cx| Editor::for_buffer(buffer.clone(), None, cx));
1745
1746        let search_bar = cx.new_view(|cx| {
1747            let mut search_bar = BufferSearchBar::new(cx);
1748            search_bar.set_active_pane_item(Some(&editor), cx);
1749            search_bar.show(cx);
1750            search_bar
1751        });
1752
1753        // Add 3 search items into the history.
1754        search_bar
1755            .update(cx, |search_bar, cx| search_bar.search("a", None, cx))
1756            .await
1757            .unwrap();
1758        search_bar
1759            .update(cx, |search_bar, cx| search_bar.search("b", None, cx))
1760            .await
1761            .unwrap();
1762        search_bar
1763            .update(cx, |search_bar, cx| {
1764                search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), cx)
1765            })
1766            .await
1767            .unwrap();
1768        // Ensure that the latest search is active.
1769        search_bar.update(cx, |search_bar, cx| {
1770            assert_eq!(search_bar.query(cx), "c");
1771            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1772        });
1773
1774        // Next history query after the latest should set the query to the empty string.
1775        search_bar.update(cx, |search_bar, cx| {
1776            search_bar.next_history_query(&NextHistoryQuery, cx);
1777        });
1778        search_bar.update(cx, |search_bar, cx| {
1779            assert_eq!(search_bar.query(cx), "");
1780            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1781        });
1782        search_bar.update(cx, |search_bar, cx| {
1783            search_bar.next_history_query(&NextHistoryQuery, cx);
1784        });
1785        search_bar.update(cx, |search_bar, cx| {
1786            assert_eq!(search_bar.query(cx), "");
1787            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1788        });
1789
1790        // First previous query for empty current query should set the query to the latest.
1791        search_bar.update(cx, |search_bar, cx| {
1792            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1793        });
1794        search_bar.update(cx, |search_bar, cx| {
1795            assert_eq!(search_bar.query(cx), "c");
1796            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1797        });
1798
1799        // Further previous items should go over the history in reverse order.
1800        search_bar.update(cx, |search_bar, cx| {
1801            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1802        });
1803        search_bar.update(cx, |search_bar, cx| {
1804            assert_eq!(search_bar.query(cx), "b");
1805            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1806        });
1807
1808        // Previous items should never go behind the first history item.
1809        search_bar.update(cx, |search_bar, cx| {
1810            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1811        });
1812        search_bar.update(cx, |search_bar, cx| {
1813            assert_eq!(search_bar.query(cx), "a");
1814            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1815        });
1816        search_bar.update(cx, |search_bar, cx| {
1817            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1818        });
1819        search_bar.update(cx, |search_bar, cx| {
1820            assert_eq!(search_bar.query(cx), "a");
1821            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1822        });
1823
1824        // Next items should go over the history in the original order.
1825        search_bar.update(cx, |search_bar, cx| {
1826            search_bar.next_history_query(&NextHistoryQuery, cx);
1827        });
1828        search_bar.update(cx, |search_bar, cx| {
1829            assert_eq!(search_bar.query(cx), "b");
1830            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1831        });
1832
1833        search_bar
1834            .update(cx, |search_bar, cx| search_bar.search("ba", None, cx))
1835            .await
1836            .unwrap();
1837        search_bar.update(cx, |search_bar, cx| {
1838            assert_eq!(search_bar.query(cx), "ba");
1839            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1840        });
1841
1842        // New search input should add another entry to history and move the selection to the end of the history.
1843        search_bar.update(cx, |search_bar, cx| {
1844            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1845        });
1846        search_bar.update(cx, |search_bar, cx| {
1847            assert_eq!(search_bar.query(cx), "c");
1848            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1849        });
1850        search_bar.update(cx, |search_bar, cx| {
1851            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1852        });
1853        search_bar.update(cx, |search_bar, cx| {
1854            assert_eq!(search_bar.query(cx), "b");
1855            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1856        });
1857        search_bar.update(cx, |search_bar, cx| {
1858            search_bar.next_history_query(&NextHistoryQuery, cx);
1859        });
1860        search_bar.update(cx, |search_bar, cx| {
1861            assert_eq!(search_bar.query(cx), "c");
1862            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1863        });
1864        search_bar.update(cx, |search_bar, cx| {
1865            search_bar.next_history_query(&NextHistoryQuery, cx);
1866        });
1867        search_bar.update(cx, |search_bar, cx| {
1868            assert_eq!(search_bar.query(cx), "ba");
1869            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1870        });
1871        search_bar.update(cx, |search_bar, cx| {
1872            search_bar.next_history_query(&NextHistoryQuery, cx);
1873        });
1874        search_bar.update(cx, |search_bar, cx| {
1875            assert_eq!(search_bar.query(cx), "");
1876            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1877        });
1878    }
1879
1880    #[gpui::test]
1881    async fn test_replace_simple(cx: &mut TestAppContext) {
1882        let (editor, search_bar, cx) = init_test(cx);
1883
1884        search_bar
1885            .update(cx, |search_bar, cx| {
1886                search_bar.search("expression", None, cx)
1887            })
1888            .await
1889            .unwrap();
1890
1891        search_bar.update(cx, |search_bar, cx| {
1892            search_bar.replacement_editor.update(cx, |editor, cx| {
1893                // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally.
1894                editor.set_text("expr$1", cx);
1895            });
1896            search_bar.replace_all(&ReplaceAll, cx)
1897        });
1898        assert_eq!(
1899            editor.update(cx, |this, cx| { this.text(cx) }),
1900            r#"
1901        A regular expr$1 (shortened as regex or regexp;[1] also referred to as
1902        rational expr$1[2][3]) is a sequence of characters that specifies a search
1903        pattern in text. Usually such patterns are used by string-searching algorithms
1904        for "find" or "find and replace" operations on strings, or for input validation.
1905        "#
1906            .unindent()
1907        );
1908
1909        // Search for word boundaries and replace just a single one.
1910        search_bar
1911            .update(cx, |search_bar, cx| {
1912                search_bar.search("or", Some(SearchOptions::WHOLE_WORD), cx)
1913            })
1914            .await
1915            .unwrap();
1916
1917        search_bar.update(cx, |search_bar, cx| {
1918            search_bar.replacement_editor.update(cx, |editor, cx| {
1919                editor.set_text("banana", cx);
1920            });
1921            search_bar.replace_next(&ReplaceNext, cx)
1922        });
1923        // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text.
1924        assert_eq!(
1925            editor.update(cx, |this, cx| { this.text(cx) }),
1926            r#"
1927        A regular expr$1 (shortened as regex banana regexp;[1] also referred to as
1928        rational expr$1[2][3]) is a sequence of characters that specifies a search
1929        pattern in text. Usually such patterns are used by string-searching algorithms
1930        for "find" or "find and replace" operations on strings, or for input validation.
1931        "#
1932            .unindent()
1933        );
1934        // Let's turn on regex mode.
1935        search_bar
1936            .update(cx, |search_bar, cx| {
1937                search_bar.search("\\[([^\\]]+)\\]", Some(SearchOptions::REGEX), cx)
1938            })
1939            .await
1940            .unwrap();
1941        search_bar.update(cx, |search_bar, cx| {
1942            search_bar.replacement_editor.update(cx, |editor, cx| {
1943                editor.set_text("${1}number", cx);
1944            });
1945            search_bar.replace_all(&ReplaceAll, cx)
1946        });
1947        assert_eq!(
1948            editor.update(cx, |this, cx| { this.text(cx) }),
1949            r#"
1950        A regular expr$1 (shortened as regex banana regexp;1number also referred to as
1951        rational expr$12number3number) is a sequence of characters that specifies a search
1952        pattern in text. Usually such patterns are used by string-searching algorithms
1953        for "find" or "find and replace" operations on strings, or for input validation.
1954        "#
1955            .unindent()
1956        );
1957        // Now with a whole-word twist.
1958        search_bar
1959            .update(cx, |search_bar, cx| {
1960                search_bar.search(
1961                    "a\\w+s",
1962                    Some(SearchOptions::REGEX | SearchOptions::WHOLE_WORD),
1963                    cx,
1964                )
1965            })
1966            .await
1967            .unwrap();
1968        search_bar.update(cx, |search_bar, cx| {
1969            search_bar.replacement_editor.update(cx, |editor, cx| {
1970                editor.set_text("things", cx);
1971            });
1972            search_bar.replace_all(&ReplaceAll, cx)
1973        });
1974        // The only word affected by this edit should be `algorithms`, even though there's a bunch
1975        // of words in this text that would match this regex if not for WHOLE_WORD.
1976        assert_eq!(
1977            editor.update(cx, |this, cx| { this.text(cx) }),
1978            r#"
1979        A regular expr$1 (shortened as regex banana regexp;1number also referred to as
1980        rational expr$12number3number) is a sequence of characters that specifies a search
1981        pattern in text. Usually such patterns are used by string-searching things
1982        for "find" or "find and replace" operations on strings, or for input validation.
1983        "#
1984            .unindent()
1985        );
1986    }
1987
1988    struct ReplacementTestParams<'a> {
1989        editor: &'a View<Editor>,
1990        search_bar: &'a View<BufferSearchBar>,
1991        cx: &'a mut VisualTestContext,
1992        search_text: &'static str,
1993        search_options: Option<SearchOptions>,
1994        replacement_text: &'static str,
1995        replace_all: bool,
1996        expected_text: String,
1997    }
1998
1999    async fn run_replacement_test(options: ReplacementTestParams<'_>) {
2000        options
2001            .search_bar
2002            .update(options.cx, |search_bar, cx| {
2003                if let Some(options) = options.search_options {
2004                    search_bar.set_search_options(options, cx);
2005                }
2006                search_bar.search(options.search_text, options.search_options, cx)
2007            })
2008            .await
2009            .unwrap();
2010
2011        options.search_bar.update(options.cx, |search_bar, cx| {
2012            search_bar.replacement_editor.update(cx, |editor, cx| {
2013                editor.set_text(options.replacement_text, cx);
2014            });
2015
2016            if options.replace_all {
2017                search_bar.replace_all(&ReplaceAll, cx)
2018            } else {
2019                search_bar.replace_next(&ReplaceNext, cx)
2020            }
2021        });
2022
2023        assert_eq!(
2024            options
2025                .editor
2026                .update(options.cx, |this, cx| { this.text(cx) }),
2027            options.expected_text
2028        );
2029    }
2030
2031    #[gpui::test]
2032    async fn test_replace_special_characters(cx: &mut TestAppContext) {
2033        let (editor, search_bar, cx) = init_test(cx);
2034
2035        run_replacement_test(ReplacementTestParams {
2036            editor: &editor,
2037            search_bar: &search_bar,
2038            cx,
2039            search_text: "expression",
2040            search_options: None,
2041            replacement_text: r"\n",
2042            replace_all: true,
2043            expected_text: r#"
2044            A regular \n (shortened as regex or regexp;[1] also referred to as
2045            rational \n[2][3]) is a sequence of characters that specifies a search
2046            pattern in text. Usually such patterns are used by string-searching algorithms
2047            for "find" or "find and replace" operations on strings, or for input validation.
2048            "#
2049            .unindent(),
2050        })
2051        .await;
2052
2053        run_replacement_test(ReplacementTestParams {
2054            editor: &editor,
2055            search_bar: &search_bar,
2056            cx,
2057            search_text: "or",
2058            search_options: Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
2059            replacement_text: r"\\\n\\\\",
2060            replace_all: false,
2061            expected_text: r#"
2062            A regular \n (shortened as regex \
2063            \\ regexp;[1] also referred to as
2064            rational \n[2][3]) is a sequence of characters that specifies a search
2065            pattern in text. Usually such patterns are used by string-searching algorithms
2066            for "find" or "find and replace" operations on strings, or for input validation.
2067            "#
2068            .unindent(),
2069        })
2070        .await;
2071
2072        run_replacement_test(ReplacementTestParams {
2073            editor: &editor,
2074            search_bar: &search_bar,
2075            cx,
2076            search_text: r"(that|used) ",
2077            search_options: Some(SearchOptions::REGEX),
2078            replacement_text: r"$1\n",
2079            replace_all: true,
2080            expected_text: r#"
2081            A regular \n (shortened as regex \
2082            \\ regexp;[1] also referred to as
2083            rational \n[2][3]) is a sequence of characters that
2084            specifies a search
2085            pattern in text. Usually such patterns are used
2086            by string-searching algorithms
2087            for "find" or "find and replace" operations on strings, or for input validation.
2088            "#
2089            .unindent(),
2090        })
2091        .await;
2092    }
2093
2094    #[gpui::test]
2095    async fn test_find_matches_in_selections_singleton_buffer_multiple_selections(
2096        cx: &mut TestAppContext,
2097    ) {
2098        init_globals(cx);
2099        let buffer = cx.new_model(|cx| {
2100            Buffer::local(
2101                r#"
2102                aaa bbb aaa ccc
2103                aaa bbb aaa ccc
2104                aaa bbb aaa ccc
2105                aaa bbb aaa ccc
2106                aaa bbb aaa ccc
2107                aaa bbb aaa ccc
2108                "#
2109                .unindent(),
2110                cx,
2111            )
2112        });
2113        let cx = cx.add_empty_window();
2114        let editor = cx.new_view(|cx| Editor::for_buffer(buffer.clone(), None, cx));
2115
2116        let search_bar = cx.new_view(|cx| {
2117            let mut search_bar = BufferSearchBar::new(cx);
2118            search_bar.set_active_pane_item(Some(&editor), cx);
2119            search_bar.show(cx);
2120            search_bar
2121        });
2122
2123        editor.update(cx, |editor, cx| {
2124            editor.change_selections(None, cx, |s| {
2125                s.select_ranges(vec![Point::new(1, 0)..Point::new(2, 4)])
2126            })
2127        });
2128
2129        search_bar.update(cx, |search_bar, cx| {
2130            let deploy = Deploy {
2131                focus: true,
2132                replace_enabled: false,
2133                selection_search_enabled: true,
2134            };
2135            search_bar.deploy(&deploy, cx);
2136        });
2137
2138        cx.run_until_parked();
2139
2140        search_bar
2141            .update(cx, |search_bar, cx| search_bar.search("aaa", None, cx))
2142            .await
2143            .unwrap();
2144
2145        editor.update(cx, |editor, cx| {
2146            assert_eq!(
2147                editor.search_background_highlights(cx),
2148                &[
2149                    Point::new(1, 0)..Point::new(1, 3),
2150                    Point::new(1, 8)..Point::new(1, 11),
2151                    Point::new(2, 0)..Point::new(2, 3),
2152                ]
2153            );
2154        });
2155    }
2156
2157    #[gpui::test]
2158    async fn test_find_matches_in_selections_multiple_excerpts_buffer_multiple_selections(
2159        cx: &mut TestAppContext,
2160    ) {
2161        init_globals(cx);
2162        let text = r#"
2163            aaa bbb aaa ccc
2164            aaa bbb aaa ccc
2165            aaa bbb aaa ccc
2166            aaa bbb aaa ccc
2167            aaa bbb aaa ccc
2168            aaa bbb aaa ccc
2169
2170            aaa bbb aaa ccc
2171            aaa bbb aaa ccc
2172            aaa bbb aaa ccc
2173            aaa bbb aaa ccc
2174            aaa bbb aaa ccc
2175            aaa bbb aaa ccc
2176            "#
2177        .unindent();
2178
2179        let cx = cx.add_empty_window();
2180        let editor = cx.new_view(|cx| {
2181            let multibuffer = MultiBuffer::build_multi(
2182                [
2183                    (
2184                        &text,
2185                        vec![
2186                            Point::new(0, 0)..Point::new(2, 0),
2187                            Point::new(4, 0)..Point::new(5, 0),
2188                        ],
2189                    ),
2190                    (&text, vec![Point::new(9, 0)..Point::new(11, 0)]),
2191                ],
2192                cx,
2193            );
2194            Editor::for_multibuffer(multibuffer, None, false, cx)
2195        });
2196
2197        let search_bar = cx.new_view(|cx| {
2198            let mut search_bar = BufferSearchBar::new(cx);
2199            search_bar.set_active_pane_item(Some(&editor), cx);
2200            search_bar.show(cx);
2201            search_bar
2202        });
2203
2204        editor.update(cx, |editor, cx| {
2205            editor.change_selections(None, cx, |s| {
2206                s.select_ranges(vec![
2207                    Point::new(1, 0)..Point::new(1, 4),
2208                    Point::new(5, 3)..Point::new(6, 4),
2209                ])
2210            })
2211        });
2212
2213        search_bar.update(cx, |search_bar, cx| {
2214            let deploy = Deploy {
2215                focus: true,
2216                replace_enabled: false,
2217                selection_search_enabled: true,
2218            };
2219            search_bar.deploy(&deploy, cx);
2220        });
2221
2222        cx.run_until_parked();
2223
2224        search_bar
2225            .update(cx, |search_bar, cx| search_bar.search("aaa", None, cx))
2226            .await
2227            .unwrap();
2228
2229        editor.update(cx, |editor, cx| {
2230            assert_eq!(
2231                editor.search_background_highlights(cx),
2232                &[
2233                    Point::new(1, 0)..Point::new(1, 3),
2234                    Point::new(5, 8)..Point::new(5, 11),
2235                    Point::new(6, 0)..Point::new(6, 3),
2236                ]
2237            );
2238        });
2239    }
2240
2241    #[gpui::test]
2242    async fn test_invalid_regexp_search_after_valid(cx: &mut TestAppContext) {
2243        let (editor, search_bar, cx) = init_test(cx);
2244        // Search using valid regexp
2245        search_bar
2246            .update(cx, |search_bar, cx| {
2247                search_bar.enable_search_option(SearchOptions::REGEX, cx);
2248                search_bar.search("expression", None, cx)
2249            })
2250            .await
2251            .unwrap();
2252        editor.update(cx, |editor, cx| {
2253            assert_eq!(
2254                display_points_of(editor.all_text_background_highlights(cx)),
2255                &[
2256                    DisplayPoint::new(DisplayRow(0), 10)..DisplayPoint::new(DisplayRow(0), 20),
2257                    DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 19),
2258                ],
2259            );
2260        });
2261
2262        // Now, the expression is invalid
2263        search_bar
2264            .update(cx, |search_bar, cx| {
2265                search_bar.search("expression (", None, cx)
2266            })
2267            .await
2268            .unwrap_err();
2269        editor.update(cx, |editor, cx| {
2270            assert!(display_points_of(editor.all_text_background_highlights(cx)).is_empty(),);
2271        });
2272    }
2273}