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