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