buffer_search.rs

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