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(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        if query != self.query(cx) || self.search_options != options {
 705            self.query_editor.update(cx, |query_editor, cx| {
 706                query_editor.buffer().update(cx, |query_buffer, cx| {
 707                    let len = query_buffer.len(cx);
 708                    query_buffer.edit([(0..len, query)], None, cx);
 709                });
 710            });
 711            self.search_options = options;
 712            self.clear_matches(cx);
 713            cx.notify();
 714        }
 715        self.update_matches(cx)
 716    }
 717
 718    fn render_search_option_button(
 719        &self,
 720        option: SearchOptions,
 721        action: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
 722    ) -> impl IntoElement {
 723        let is_active = self.search_options.contains(option);
 724        option.as_button(is_active, action)
 725    }
 726
 727    pub fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext<Self>) {
 728        if let Some(active_editor) = self.active_searchable_item.as_ref() {
 729            let handle = active_editor.focus_handle(cx);
 730            cx.focus(&handle);
 731        }
 732    }
 733
 734    pub fn toggle_search_option(
 735        &mut self,
 736        search_option: SearchOptions,
 737        cx: &mut ViewContext<Self>,
 738    ) {
 739        self.search_options.toggle(search_option);
 740        self.default_options = self.search_options;
 741        drop(self.update_matches(cx));
 742        cx.notify();
 743    }
 744
 745    pub fn has_search_option(&mut self, search_option: SearchOptions) -> bool {
 746        self.search_options.contains(search_option)
 747    }
 748
 749    pub fn enable_search_option(
 750        &mut self,
 751        search_option: SearchOptions,
 752        cx: &mut ViewContext<Self>,
 753    ) {
 754        if !self.search_options.contains(search_option) {
 755            self.toggle_search_option(search_option, cx)
 756        }
 757    }
 758
 759    pub fn set_search_options(
 760        &mut self,
 761        search_options: SearchOptions,
 762        cx: &mut ViewContext<Self>,
 763    ) {
 764        self.search_options = search_options;
 765        cx.notify();
 766    }
 767
 768    fn select_next_match(&mut self, _: &SelectNextMatch, cx: &mut ViewContext<Self>) {
 769        self.select_match(Direction::Next, 1, cx);
 770    }
 771
 772    fn select_prev_match(&mut self, _: &SelectPrevMatch, cx: &mut ViewContext<Self>) {
 773        self.select_match(Direction::Prev, 1, cx);
 774    }
 775
 776    fn select_all_matches(&mut self, _: &SelectAllMatches, cx: &mut ViewContext<Self>) {
 777        if !self.dismissed && self.active_match_index.is_some() {
 778            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
 779                if let Some(matches) = self
 780                    .searchable_items_with_matches
 781                    .get(&searchable_item.downgrade())
 782                {
 783                    searchable_item.select_matches(matches, cx);
 784                    self.focus_editor(&FocusEditor, cx);
 785                }
 786            }
 787        }
 788    }
 789
 790    pub fn select_match(&mut self, direction: Direction, count: usize, cx: &mut ViewContext<Self>) {
 791        if let Some(index) = self.active_match_index {
 792            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
 793                if let Some(matches) = self
 794                    .searchable_items_with_matches
 795                    .get(&searchable_item.downgrade())
 796                    .filter(|matches| !matches.is_empty())
 797                {
 798                    // If 'wrapscan' is disabled, searches do not wrap around the end of the file.
 799                    if !EditorSettings::get_global(cx).search_wrap
 800                        && ((direction == Direction::Next && index + count >= matches.len())
 801                            || (direction == Direction::Prev && index < count))
 802                    {
 803                        crate::show_no_more_matches(cx);
 804                        return;
 805                    }
 806                    let new_match_index = searchable_item
 807                        .match_index_for_direction(matches, index, direction, count, cx);
 808
 809                    searchable_item.update_matches(matches, cx);
 810                    searchable_item.activate_match(new_match_index, matches, cx);
 811                }
 812            }
 813        }
 814    }
 815
 816    pub fn select_last_match(&mut self, cx: &mut ViewContext<Self>) {
 817        if let Some(searchable_item) = self.active_searchable_item.as_ref() {
 818            if let Some(matches) = self
 819                .searchable_items_with_matches
 820                .get(&searchable_item.downgrade())
 821            {
 822                if matches.is_empty() {
 823                    return;
 824                }
 825                let new_match_index = matches.len() - 1;
 826                searchable_item.update_matches(matches, cx);
 827                searchable_item.activate_match(new_match_index, matches, cx);
 828            }
 829        }
 830    }
 831
 832    fn on_query_editor_event(
 833        &mut self,
 834        editor: View<Editor>,
 835        event: &editor::EditorEvent,
 836        cx: &mut ViewContext<Self>,
 837    ) {
 838        match event {
 839            editor::EditorEvent::Focused => self.query_editor_focused = true,
 840            editor::EditorEvent::Blurred => self.query_editor_focused = false,
 841            editor::EditorEvent::Edited { .. } => {
 842                self.smartcase(cx);
 843                self.clear_matches(cx);
 844                let search = self.update_matches(cx);
 845
 846                let width = editor.update(cx, |editor, cx| {
 847                    let text_layout_details = editor.text_layout_details(cx);
 848                    let snapshot = editor.snapshot(cx).display_snapshot;
 849
 850                    snapshot.x_for_display_point(snapshot.max_point(), &text_layout_details)
 851                        - snapshot.x_for_display_point(DisplayPoint::zero(), &text_layout_details)
 852                });
 853                self.editor_needed_width = width;
 854                cx.notify();
 855
 856                cx.spawn(|this, mut cx| async move {
 857                    search.await?;
 858                    this.update(&mut cx, |this, cx| this.activate_current_match(cx))
 859                })
 860                .detach_and_log_err(cx);
 861            }
 862            _ => {}
 863        }
 864    }
 865
 866    fn on_replacement_editor_event(
 867        &mut self,
 868        _: View<Editor>,
 869        event: &editor::EditorEvent,
 870        _: &mut ViewContext<Self>,
 871    ) {
 872        match event {
 873            editor::EditorEvent::Focused => self.replacement_editor_focused = true,
 874            editor::EditorEvent::Blurred => self.replacement_editor_focused = false,
 875            _ => {}
 876        }
 877    }
 878
 879    fn on_active_searchable_item_event(&mut self, event: &SearchEvent, cx: &mut ViewContext<Self>) {
 880        match event {
 881            SearchEvent::MatchesInvalidated => {
 882                drop(self.update_matches(cx));
 883            }
 884            SearchEvent::ActiveMatchChanged => self.update_match_index(cx),
 885        }
 886    }
 887
 888    fn toggle_case_sensitive(&mut self, _: &ToggleCaseSensitive, cx: &mut ViewContext<Self>) {
 889        self.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx)
 890    }
 891
 892    fn toggle_whole_word(&mut self, _: &ToggleWholeWord, cx: &mut ViewContext<Self>) {
 893        self.toggle_search_option(SearchOptions::WHOLE_WORD, cx)
 894    }
 895
 896    fn toggle_selection(&mut self, _: &ToggleSelection, cx: &mut ViewContext<Self>) {
 897        if let Some(active_item) = self.active_searchable_item.as_mut() {
 898            self.selection_search_enabled = !self.selection_search_enabled;
 899            active_item.toggle_filtered_search_ranges(self.selection_search_enabled, cx);
 900            drop(self.update_matches(cx));
 901            cx.notify();
 902        }
 903    }
 904
 905    fn toggle_regex(&mut self, _: &ToggleRegex, cx: &mut ViewContext<Self>) {
 906        self.toggle_search_option(SearchOptions::REGEX, cx)
 907    }
 908
 909    fn clear_active_searchable_item_matches(&mut self, cx: &mut WindowContext) {
 910        if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
 911            self.active_match_index = None;
 912            self.searchable_items_with_matches
 913                .remove(&active_searchable_item.downgrade());
 914            active_searchable_item.clear_matches(cx);
 915        }
 916    }
 917
 918    pub fn has_active_match(&self) -> bool {
 919        self.active_match_index.is_some()
 920    }
 921
 922    fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
 923        let mut active_item_matches = None;
 924        for (searchable_item, matches) in self.searchable_items_with_matches.drain() {
 925            if let Some(searchable_item) =
 926                WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
 927            {
 928                if Some(&searchable_item) == self.active_searchable_item.as_ref() {
 929                    active_item_matches = Some((searchable_item.downgrade(), matches));
 930                } else {
 931                    searchable_item.clear_matches(cx);
 932                }
 933            }
 934        }
 935
 936        self.searchable_items_with_matches
 937            .extend(active_item_matches);
 938    }
 939
 940    fn update_matches(&mut self, cx: &mut ViewContext<Self>) -> oneshot::Receiver<()> {
 941        let (done_tx, done_rx) = oneshot::channel();
 942        let query = self.query(cx);
 943        self.pending_search.take();
 944
 945        if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
 946            self.query_contains_error = false;
 947            if query.is_empty() {
 948                self.clear_active_searchable_item_matches(cx);
 949                let _ = done_tx.send(());
 950                cx.notify();
 951            } else {
 952                let query: Arc<_> = if self.search_options.contains(SearchOptions::REGEX) {
 953                    match SearchQuery::regex(
 954                        query,
 955                        self.search_options.contains(SearchOptions::WHOLE_WORD),
 956                        self.search_options.contains(SearchOptions::CASE_SENSITIVE),
 957                        false,
 958                        Default::default(),
 959                        Default::default(),
 960                        None,
 961                    ) {
 962                        Ok(query) => query.with_replacement(self.replacement(cx)),
 963                        Err(_) => {
 964                            self.query_contains_error = true;
 965                            self.clear_active_searchable_item_matches(cx);
 966                            cx.notify();
 967                            return done_rx;
 968                        }
 969                    }
 970                } else {
 971                    match SearchQuery::text(
 972                        query,
 973                        self.search_options.contains(SearchOptions::WHOLE_WORD),
 974                        self.search_options.contains(SearchOptions::CASE_SENSITIVE),
 975                        false,
 976                        Default::default(),
 977                        Default::default(),
 978                        None,
 979                    ) {
 980                        Ok(query) => query.with_replacement(self.replacement(cx)),
 981                        Err(_) => {
 982                            self.query_contains_error = true;
 983                            self.clear_active_searchable_item_matches(cx);
 984                            cx.notify();
 985                            return done_rx;
 986                        }
 987                    }
 988                }
 989                .into();
 990                self.active_search = Some(query.clone());
 991                let query_text = query.as_str().to_string();
 992
 993                let matches = active_searchable_item.find_matches(query, cx);
 994
 995                let active_searchable_item = active_searchable_item.downgrade();
 996                self.pending_search = Some(cx.spawn(|this, mut cx| async move {
 997                    let matches = matches.await;
 998
 999                    this.update(&mut cx, |this, cx| {
1000                        if let Some(active_searchable_item) =
1001                            WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx)
1002                        {
1003                            this.searchable_items_with_matches
1004                                .insert(active_searchable_item.downgrade(), matches);
1005
1006                            this.update_match_index(cx);
1007                            this.search_history
1008                                .add(&mut this.search_history_cursor, query_text);
1009                            if !this.dismissed {
1010                                let matches = this
1011                                    .searchable_items_with_matches
1012                                    .get(&active_searchable_item.downgrade())
1013                                    .unwrap();
1014                                if matches.is_empty() {
1015                                    active_searchable_item.clear_matches(cx);
1016                                } else {
1017                                    active_searchable_item.update_matches(matches, cx);
1018                                }
1019                                let _ = done_tx.send(());
1020                            }
1021                            cx.notify();
1022                        }
1023                    })
1024                    .log_err();
1025                }));
1026            }
1027        }
1028        done_rx
1029    }
1030
1031    pub fn update_match_index(&mut self, cx: &mut ViewContext<Self>) {
1032        let new_index = self
1033            .active_searchable_item
1034            .as_ref()
1035            .and_then(|searchable_item| {
1036                let matches = self
1037                    .searchable_items_with_matches
1038                    .get(&searchable_item.downgrade())?;
1039                searchable_item.active_match_index(matches, cx)
1040            });
1041        if new_index != self.active_match_index {
1042            self.active_match_index = new_index;
1043            cx.notify();
1044        }
1045    }
1046
1047    fn tab(&mut self, _: &Tab, cx: &mut ViewContext<Self>) {
1048        // Search -> Replace -> Editor
1049        let focus_handle = if self.replace_enabled && self.query_editor_focused {
1050            self.replacement_editor.focus_handle(cx)
1051        } else if let Some(item) = self.active_searchable_item.as_ref() {
1052            item.focus_handle(cx)
1053        } else {
1054            return;
1055        };
1056        self.focus(&focus_handle, cx);
1057        cx.stop_propagation();
1058    }
1059
1060    fn tab_prev(&mut self, _: &TabPrev, cx: &mut ViewContext<Self>) {
1061        // Search -> Replace -> Search
1062        let focus_handle = if self.replace_enabled && self.query_editor_focused {
1063            self.replacement_editor.focus_handle(cx)
1064        } else if self.replacement_editor_focused {
1065            self.query_editor.focus_handle(cx)
1066        } else {
1067            return;
1068        };
1069        self.focus(&focus_handle, cx);
1070        cx.stop_propagation();
1071    }
1072
1073    fn next_history_query(&mut self, _: &NextHistoryQuery, cx: &mut ViewContext<Self>) {
1074        if let Some(new_query) = self
1075            .search_history
1076            .next(&mut self.search_history_cursor)
1077            .map(str::to_string)
1078        {
1079            drop(self.search(&new_query, Some(self.search_options), cx));
1080        } else {
1081            self.search_history_cursor.reset();
1082            drop(self.search("", Some(self.search_options), cx));
1083        }
1084    }
1085
1086    fn previous_history_query(&mut self, _: &PreviousHistoryQuery, cx: &mut ViewContext<Self>) {
1087        if self.query(cx).is_empty() {
1088            if let Some(new_query) = self
1089                .search_history
1090                .current(&mut self.search_history_cursor)
1091                .map(str::to_string)
1092            {
1093                drop(self.search(&new_query, Some(self.search_options), cx));
1094                return;
1095            }
1096        }
1097
1098        if let Some(new_query) = self
1099            .search_history
1100            .previous(&mut self.search_history_cursor)
1101            .map(str::to_string)
1102        {
1103            drop(self.search(&new_query, Some(self.search_options), cx));
1104        }
1105    }
1106
1107    fn focus(&self, handle: &gpui::FocusHandle, cx: &mut ViewContext<Self>) {
1108        cx.on_next_frame(|_, cx| {
1109            cx.invalidate_character_coordinates();
1110        });
1111        cx.focus(handle);
1112    }
1113    fn toggle_replace(&mut self, _: &ToggleReplace, cx: &mut ViewContext<Self>) {
1114        if self.active_searchable_item.is_some() {
1115            self.replace_enabled = !self.replace_enabled;
1116            let handle = if self.replace_enabled {
1117                self.replacement_editor.focus_handle(cx)
1118            } else {
1119                self.query_editor.focus_handle(cx)
1120            };
1121            self.focus(&handle, cx);
1122            cx.notify();
1123        }
1124    }
1125    fn replace_next(&mut self, _: &ReplaceNext, cx: &mut ViewContext<Self>) {
1126        let mut should_propagate = true;
1127        if !self.dismissed && self.active_search.is_some() {
1128            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1129                if let Some(query) = self.active_search.as_ref() {
1130                    if let Some(matches) = self
1131                        .searchable_items_with_matches
1132                        .get(&searchable_item.downgrade())
1133                    {
1134                        if let Some(active_index) = self.active_match_index {
1135                            let query = query
1136                                .as_ref()
1137                                .clone()
1138                                .with_replacement(self.replacement(cx));
1139                            searchable_item.replace(matches.at(active_index), &query, cx);
1140                            self.select_next_match(&SelectNextMatch, cx);
1141                        }
1142                        should_propagate = false;
1143                        self.focus_editor(&FocusEditor, cx);
1144                    }
1145                }
1146            }
1147        }
1148        if !should_propagate {
1149            cx.stop_propagation();
1150        }
1151    }
1152    pub fn replace_all(&mut self, _: &ReplaceAll, cx: &mut ViewContext<Self>) {
1153        if !self.dismissed && self.active_search.is_some() {
1154            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1155                if let Some(query) = self.active_search.as_ref() {
1156                    if let Some(matches) = self
1157                        .searchable_items_with_matches
1158                        .get(&searchable_item.downgrade())
1159                    {
1160                        let query = query
1161                            .as_ref()
1162                            .clone()
1163                            .with_replacement(self.replacement(cx));
1164                        searchable_item.replace_all(&mut matches.iter(), &query, cx);
1165                    }
1166                }
1167            }
1168        }
1169    }
1170
1171    pub fn match_exists(&mut self, cx: &mut ViewContext<Self>) -> bool {
1172        self.update_match_index(cx);
1173        self.active_match_index.is_some()
1174    }
1175
1176    pub fn should_use_smartcase_search(&mut self, cx: &mut ViewContext<Self>) -> bool {
1177        EditorSettings::get_global(cx).use_smartcase_search
1178    }
1179
1180    pub fn is_contains_uppercase(&mut self, str: &String) -> bool {
1181        str.chars().any(|c| c.is_uppercase())
1182    }
1183
1184    fn smartcase(&mut self, cx: &mut ViewContext<Self>) {
1185        if self.should_use_smartcase_search(cx) {
1186            let query = self.query(cx);
1187            if !query.is_empty() {
1188                let is_case = self.is_contains_uppercase(&query);
1189                if self.has_search_option(SearchOptions::CASE_SENSITIVE) != is_case {
1190                    self.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx);
1191                }
1192            }
1193        }
1194    }
1195}
1196
1197#[cfg(test)]
1198mod tests {
1199    use std::ops::Range;
1200
1201    use super::*;
1202    use editor::{display_map::DisplayRow, DisplayPoint, Editor, MultiBuffer, SearchSettings};
1203    use gpui::{Context, Hsla, TestAppContext, UpdateGlobal, VisualTestContext};
1204    use language::{Buffer, Point};
1205    use project::Project;
1206    use settings::SettingsStore;
1207    use smol::stream::StreamExt as _;
1208    use unindent::Unindent as _;
1209
1210    fn init_globals(cx: &mut TestAppContext) {
1211        cx.update(|cx| {
1212            let store = settings::SettingsStore::test(cx);
1213            cx.set_global(store);
1214            editor::init(cx);
1215
1216            language::init(cx);
1217            Project::init_settings(cx);
1218            theme::init(theme::LoadThemes::JustBase, cx);
1219            crate::init(cx);
1220        });
1221    }
1222
1223    fn init_test(
1224        cx: &mut TestAppContext,
1225    ) -> (View<Editor>, View<BufferSearchBar>, &mut VisualTestContext) {
1226        init_globals(cx);
1227        let buffer = cx.new_model(|cx| {
1228            Buffer::local(
1229                r#"
1230                A regular expression (shortened as regex or regexp;[1] also referred to as
1231                rational expression[2][3]) is a sequence of characters that specifies a search
1232                pattern in text. Usually such patterns are used by string-searching algorithms
1233                for "find" or "find and replace" operations on strings, or for input validation.
1234                "#
1235                .unindent(),
1236                cx,
1237            )
1238        });
1239        let cx = cx.add_empty_window();
1240        let editor = cx.new_view(|cx| Editor::for_buffer(buffer.clone(), None, cx));
1241
1242        let search_bar = cx.new_view(|cx| {
1243            let mut search_bar = BufferSearchBar::new(cx);
1244            search_bar.set_active_pane_item(Some(&editor), cx);
1245            search_bar.show(cx);
1246            search_bar
1247        });
1248
1249        (editor, search_bar, cx)
1250    }
1251
1252    #[gpui::test]
1253    async fn test_search_simple(cx: &mut TestAppContext) {
1254        let (editor, search_bar, cx) = init_test(cx);
1255        let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
1256            background_highlights
1257                .into_iter()
1258                .map(|(range, _)| range)
1259                .collect::<Vec<_>>()
1260        };
1261        // Search for a string that appears with different casing.
1262        // By default, search is case-insensitive.
1263        search_bar
1264            .update(cx, |search_bar, cx| search_bar.search("us", None, cx))
1265            .await
1266            .unwrap();
1267        editor.update(cx, |editor, cx| {
1268            assert_eq!(
1269                display_points_of(editor.all_text_background_highlights(cx)),
1270                &[
1271                    DisplayPoint::new(DisplayRow(2), 17)..DisplayPoint::new(DisplayRow(2), 19),
1272                    DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),
1273                ]
1274            );
1275        });
1276
1277        // Switch to a case sensitive search.
1278        search_bar.update(cx, |search_bar, cx| {
1279            search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx);
1280        });
1281        let mut editor_notifications = cx.notifications(&editor);
1282        editor_notifications.next().await;
1283        editor.update(cx, |editor, cx| {
1284            assert_eq!(
1285                display_points_of(editor.all_text_background_highlights(cx)),
1286                &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
1287            );
1288        });
1289
1290        // Search for a string that appears both as a whole word and
1291        // within other words. By default, all results are found.
1292        search_bar
1293            .update(cx, |search_bar, cx| search_bar.search("or", None, cx))
1294            .await
1295            .unwrap();
1296        editor.update(cx, |editor, cx| {
1297            assert_eq!(
1298                display_points_of(editor.all_text_background_highlights(cx)),
1299                &[
1300                    DisplayPoint::new(DisplayRow(0), 24)..DisplayPoint::new(DisplayRow(0), 26),
1301                    DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1302                    DisplayPoint::new(DisplayRow(2), 71)..DisplayPoint::new(DisplayRow(2), 73),
1303                    DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 3),
1304                    DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
1305                    DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
1306                    DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 62),
1307                ]
1308            );
1309        });
1310
1311        // Switch to a whole word search.
1312        search_bar.update(cx, |search_bar, cx| {
1313            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
1314        });
1315        let mut editor_notifications = cx.notifications(&editor);
1316        editor_notifications.next().await;
1317        editor.update(cx, |editor, cx| {
1318            assert_eq!(
1319                display_points_of(editor.all_text_background_highlights(cx)),
1320                &[
1321                    DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1322                    DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
1323                    DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
1324                ]
1325            );
1326        });
1327
1328        editor.update(cx, |editor, cx| {
1329            editor.change_selections(None, cx, |s| {
1330                s.select_display_ranges([
1331                    DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
1332                ])
1333            });
1334        });
1335        search_bar.update(cx, |search_bar, cx| {
1336            assert_eq!(search_bar.active_match_index, Some(0));
1337            search_bar.select_next_match(&SelectNextMatch, cx);
1338            assert_eq!(
1339                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1340                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1341            );
1342        });
1343        search_bar.update(cx, |search_bar, _| {
1344            assert_eq!(search_bar.active_match_index, Some(0));
1345        });
1346
1347        search_bar.update(cx, |search_bar, cx| {
1348            search_bar.select_next_match(&SelectNextMatch, cx);
1349            assert_eq!(
1350                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1351                [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1352            );
1353        });
1354        search_bar.update(cx, |search_bar, _| {
1355            assert_eq!(search_bar.active_match_index, Some(1));
1356        });
1357
1358        search_bar.update(cx, |search_bar, cx| {
1359            search_bar.select_next_match(&SelectNextMatch, cx);
1360            assert_eq!(
1361                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1362                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1363            );
1364        });
1365        search_bar.update(cx, |search_bar, _| {
1366            assert_eq!(search_bar.active_match_index, Some(2));
1367        });
1368
1369        search_bar.update(cx, |search_bar, cx| {
1370            search_bar.select_next_match(&SelectNextMatch, cx);
1371            assert_eq!(
1372                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1373                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1374            );
1375        });
1376        search_bar.update(cx, |search_bar, _| {
1377            assert_eq!(search_bar.active_match_index, Some(0));
1378        });
1379
1380        search_bar.update(cx, |search_bar, cx| {
1381            search_bar.select_prev_match(&SelectPrevMatch, cx);
1382            assert_eq!(
1383                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1384                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1385            );
1386        });
1387        search_bar.update(cx, |search_bar, _| {
1388            assert_eq!(search_bar.active_match_index, Some(2));
1389        });
1390
1391        search_bar.update(cx, |search_bar, cx| {
1392            search_bar.select_prev_match(&SelectPrevMatch, cx);
1393            assert_eq!(
1394                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1395                [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1396            );
1397        });
1398        search_bar.update(cx, |search_bar, _| {
1399            assert_eq!(search_bar.active_match_index, Some(1));
1400        });
1401
1402        search_bar.update(cx, |search_bar, cx| {
1403            search_bar.select_prev_match(&SelectPrevMatch, cx);
1404            assert_eq!(
1405                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1406                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1407            );
1408        });
1409        search_bar.update(cx, |search_bar, _| {
1410            assert_eq!(search_bar.active_match_index, Some(0));
1411        });
1412
1413        // Park the cursor in between matches and ensure that going to the previous match selects
1414        // the closest match to the left.
1415        editor.update(cx, |editor, cx| {
1416            editor.change_selections(None, cx, |s| {
1417                s.select_display_ranges([
1418                    DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
1419                ])
1420            });
1421        });
1422        search_bar.update(cx, |search_bar, cx| {
1423            assert_eq!(search_bar.active_match_index, Some(1));
1424            search_bar.select_prev_match(&SelectPrevMatch, cx);
1425            assert_eq!(
1426                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1427                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1428            );
1429        });
1430        search_bar.update(cx, |search_bar, _| {
1431            assert_eq!(search_bar.active_match_index, Some(0));
1432        });
1433
1434        // Park the cursor in between matches and ensure that going to the next match selects the
1435        // closest match to the right.
1436        editor.update(cx, |editor, cx| {
1437            editor.change_selections(None, cx, |s| {
1438                s.select_display_ranges([
1439                    DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
1440                ])
1441            });
1442        });
1443        search_bar.update(cx, |search_bar, cx| {
1444            assert_eq!(search_bar.active_match_index, Some(1));
1445            search_bar.select_next_match(&SelectNextMatch, cx);
1446            assert_eq!(
1447                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1448                [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1449            );
1450        });
1451        search_bar.update(cx, |search_bar, _| {
1452            assert_eq!(search_bar.active_match_index, Some(1));
1453        });
1454
1455        // Park the cursor after the last match and ensure that going to the previous match selects
1456        // the last match.
1457        editor.update(cx, |editor, cx| {
1458            editor.change_selections(None, cx, |s| {
1459                s.select_display_ranges([
1460                    DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
1461                ])
1462            });
1463        });
1464        search_bar.update(cx, |search_bar, cx| {
1465            assert_eq!(search_bar.active_match_index, Some(2));
1466            search_bar.select_prev_match(&SelectPrevMatch, cx);
1467            assert_eq!(
1468                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1469                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1470            );
1471        });
1472        search_bar.update(cx, |search_bar, _| {
1473            assert_eq!(search_bar.active_match_index, Some(2));
1474        });
1475
1476        // Park the cursor after the last match and ensure that going to the next match selects the
1477        // first match.
1478        editor.update(cx, |editor, cx| {
1479            editor.change_selections(None, cx, |s| {
1480                s.select_display_ranges([
1481                    DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
1482                ])
1483            });
1484        });
1485        search_bar.update(cx, |search_bar, cx| {
1486            assert_eq!(search_bar.active_match_index, Some(2));
1487            search_bar.select_next_match(&SelectNextMatch, cx);
1488            assert_eq!(
1489                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1490                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1491            );
1492        });
1493        search_bar.update(cx, |search_bar, _| {
1494            assert_eq!(search_bar.active_match_index, Some(0));
1495        });
1496
1497        // Park the cursor before the first match and ensure that going to the previous match
1498        // selects the last match.
1499        editor.update(cx, |editor, cx| {
1500            editor.change_selections(None, cx, |s| {
1501                s.select_display_ranges([
1502                    DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
1503                ])
1504            });
1505        });
1506        search_bar.update(cx, |search_bar, cx| {
1507            assert_eq!(search_bar.active_match_index, Some(0));
1508            search_bar.select_prev_match(&SelectPrevMatch, cx);
1509            assert_eq!(
1510                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1511                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1512            );
1513        });
1514        search_bar.update(cx, |search_bar, _| {
1515            assert_eq!(search_bar.active_match_index, Some(2));
1516        });
1517    }
1518
1519    fn display_points_of(
1520        background_highlights: Vec<(Range<DisplayPoint>, Hsla)>,
1521    ) -> Vec<Range<DisplayPoint>> {
1522        background_highlights
1523            .into_iter()
1524            .map(|(range, _)| range)
1525            .collect::<Vec<_>>()
1526    }
1527
1528    #[gpui::test]
1529    async fn test_search_option_handling(cx: &mut TestAppContext) {
1530        let (editor, search_bar, cx) = init_test(cx);
1531
1532        // show with options should make current search case sensitive
1533        search_bar
1534            .update(cx, |search_bar, cx| {
1535                search_bar.show(cx);
1536                search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), cx)
1537            })
1538            .await
1539            .unwrap();
1540        editor.update(cx, |editor, cx| {
1541            assert_eq!(
1542                display_points_of(editor.all_text_background_highlights(cx)),
1543                &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
1544            );
1545        });
1546
1547        // search_suggested should restore default options
1548        search_bar.update(cx, |search_bar, cx| {
1549            search_bar.search_suggested(cx);
1550            assert_eq!(search_bar.search_options, SearchOptions::NONE)
1551        });
1552
1553        // toggling a search option should update the defaults
1554        search_bar
1555            .update(cx, |search_bar, cx| {
1556                search_bar.search("regex", Some(SearchOptions::CASE_SENSITIVE), cx)
1557            })
1558            .await
1559            .unwrap();
1560        search_bar.update(cx, |search_bar, cx| {
1561            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx)
1562        });
1563        let mut editor_notifications = cx.notifications(&editor);
1564        editor_notifications.next().await;
1565        editor.update(cx, |editor, cx| {
1566            assert_eq!(
1567                display_points_of(editor.all_text_background_highlights(cx)),
1568                &[DisplayPoint::new(DisplayRow(0), 35)..DisplayPoint::new(DisplayRow(0), 40),]
1569            );
1570        });
1571
1572        // defaults should still include whole word
1573        search_bar.update(cx, |search_bar, cx| {
1574            search_bar.search_suggested(cx);
1575            assert_eq!(
1576                search_bar.search_options,
1577                SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD
1578            )
1579        });
1580    }
1581
1582    #[gpui::test]
1583    async fn test_search_select_all_matches(cx: &mut TestAppContext) {
1584        init_globals(cx);
1585        let buffer_text = r#"
1586        A regular expression (shortened as regex or regexp;[1] also referred to as
1587        rational expression[2][3]) is a sequence of characters that specifies a search
1588        pattern in text. Usually such patterns are used by string-searching algorithms
1589        for "find" or "find and replace" operations on strings, or for input validation.
1590        "#
1591        .unindent();
1592        let expected_query_matches_count = buffer_text
1593            .chars()
1594            .filter(|c| c.to_ascii_lowercase() == 'a')
1595            .count();
1596        assert!(
1597            expected_query_matches_count > 1,
1598            "Should pick a query with multiple results"
1599        );
1600        let buffer = cx.new_model(|cx| Buffer::local(buffer_text, cx));
1601        let window = cx.add_window(|_| gpui::Empty);
1602
1603        let editor = window.build_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx));
1604
1605        let search_bar = window.build_view(cx, |cx| {
1606            let mut search_bar = BufferSearchBar::new(cx);
1607            search_bar.set_active_pane_item(Some(&editor), cx);
1608            search_bar.show(cx);
1609            search_bar
1610        });
1611
1612        window
1613            .update(cx, |_, cx| {
1614                search_bar.update(cx, |search_bar, cx| search_bar.search("a", None, cx))
1615            })
1616            .unwrap()
1617            .await
1618            .unwrap();
1619        let initial_selections = window
1620            .update(cx, |_, cx| {
1621                search_bar.update(cx, |search_bar, cx| {
1622                    let handle = search_bar.query_editor.focus_handle(cx);
1623                    cx.focus(&handle);
1624                    search_bar.activate_current_match(cx);
1625                });
1626                assert!(
1627                    !editor.read(cx).is_focused(cx),
1628                    "Initially, the editor should not be focused"
1629                );
1630                let initial_selections = editor.update(cx, |editor, cx| {
1631                    let initial_selections = editor.selections.display_ranges(cx);
1632                    assert_eq!(
1633                        initial_selections.len(), 1,
1634                        "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
1635                    );
1636                    initial_selections
1637                });
1638                search_bar.update(cx, |search_bar, cx| {
1639                    assert_eq!(search_bar.active_match_index, Some(0));
1640                    let handle = search_bar.query_editor.focus_handle(cx);
1641                    cx.focus(&handle);
1642                    search_bar.select_all_matches(&SelectAllMatches, cx);
1643                });
1644                assert!(
1645                    editor.read(cx).is_focused(cx),
1646                    "Should focus editor after successful SelectAllMatches"
1647                );
1648                search_bar.update(cx, |search_bar, cx| {
1649                    let all_selections =
1650                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1651                    assert_eq!(
1652                        all_selections.len(),
1653                        expected_query_matches_count,
1654                        "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1655                    );
1656                    assert_eq!(
1657                        search_bar.active_match_index,
1658                        Some(0),
1659                        "Match index should not change after selecting all matches"
1660                    );
1661                });
1662
1663                search_bar.update(cx, |this, cx| this.select_next_match(&SelectNextMatch, cx));
1664                initial_selections
1665            }).unwrap();
1666
1667        window
1668            .update(cx, |_, cx| {
1669                assert!(
1670                    editor.read(cx).is_focused(cx),
1671                    "Should still have editor focused after SelectNextMatch"
1672                );
1673                search_bar.update(cx, |search_bar, cx| {
1674                    let all_selections =
1675                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1676                    assert_eq!(
1677                        all_selections.len(),
1678                        1,
1679                        "On next match, should deselect items and select the next match"
1680                    );
1681                    assert_ne!(
1682                        all_selections, initial_selections,
1683                        "Next match should be different from the first selection"
1684                    );
1685                    assert_eq!(
1686                        search_bar.active_match_index,
1687                        Some(1),
1688                        "Match index should be updated to the next one"
1689                    );
1690                    let handle = search_bar.query_editor.focus_handle(cx);
1691                    cx.focus(&handle);
1692                    search_bar.select_all_matches(&SelectAllMatches, cx);
1693                });
1694            })
1695            .unwrap();
1696        window
1697            .update(cx, |_, cx| {
1698                assert!(
1699                    editor.read(cx).is_focused(cx),
1700                    "Should focus editor after successful SelectAllMatches"
1701                );
1702                search_bar.update(cx, |search_bar, cx| {
1703                    let all_selections =
1704                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1705                    assert_eq!(
1706                    all_selections.len(),
1707                    expected_query_matches_count,
1708                    "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1709                );
1710                    assert_eq!(
1711                        search_bar.active_match_index,
1712                        Some(1),
1713                        "Match index should not change after selecting all matches"
1714                    );
1715                });
1716                search_bar.update(cx, |search_bar, cx| {
1717                    search_bar.select_prev_match(&SelectPrevMatch, cx);
1718                });
1719            })
1720            .unwrap();
1721        let last_match_selections = window
1722            .update(cx, |_, cx| {
1723                assert!(
1724                    editor.read(cx).is_focused(cx),
1725                    "Should still have editor focused after SelectPrevMatch"
1726                );
1727
1728                search_bar.update(cx, |search_bar, cx| {
1729                    let all_selections =
1730                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1731                    assert_eq!(
1732                        all_selections.len(),
1733                        1,
1734                        "On previous match, should deselect items and select the previous item"
1735                    );
1736                    assert_eq!(
1737                        all_selections, initial_selections,
1738                        "Previous match should be the same as the first selection"
1739                    );
1740                    assert_eq!(
1741                        search_bar.active_match_index,
1742                        Some(0),
1743                        "Match index should be updated to the previous one"
1744                    );
1745                    all_selections
1746                })
1747            })
1748            .unwrap();
1749
1750        window
1751            .update(cx, |_, cx| {
1752                search_bar.update(cx, |search_bar, cx| {
1753                    let handle = search_bar.query_editor.focus_handle(cx);
1754                    cx.focus(&handle);
1755                    search_bar.search("abas_nonexistent_match", None, cx)
1756                })
1757            })
1758            .unwrap()
1759            .await
1760            .unwrap();
1761        window
1762            .update(cx, |_, cx| {
1763                search_bar.update(cx, |search_bar, cx| {
1764                    search_bar.select_all_matches(&SelectAllMatches, cx);
1765                });
1766                assert!(
1767                    editor.update(cx, |this, cx| !this.is_focused(cx.window_context())),
1768                    "Should not switch focus to editor if SelectAllMatches does not find any matches"
1769                );
1770                search_bar.update(cx, |search_bar, cx| {
1771                    let all_selections =
1772                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1773                    assert_eq!(
1774                        all_selections, last_match_selections,
1775                        "Should not select anything new if there are no matches"
1776                    );
1777                    assert!(
1778                        search_bar.active_match_index.is_none(),
1779                        "For no matches, there should be no active match index"
1780                    );
1781                });
1782            })
1783            .unwrap();
1784    }
1785
1786    #[gpui::test]
1787    async fn test_search_query_history(cx: &mut TestAppContext) {
1788        init_globals(cx);
1789        let buffer_text = r#"
1790        A regular expression (shortened as regex or regexp;[1] also referred to as
1791        rational expression[2][3]) is a sequence of characters that specifies a search
1792        pattern in text. Usually such patterns are used by string-searching algorithms
1793        for "find" or "find and replace" operations on strings, or for input validation.
1794        "#
1795        .unindent();
1796        let buffer = cx.new_model(|cx| Buffer::local(buffer_text, cx));
1797        let cx = cx.add_empty_window();
1798
1799        let editor = cx.new_view(|cx| Editor::for_buffer(buffer.clone(), None, cx));
1800
1801        let search_bar = cx.new_view(|cx| {
1802            let mut search_bar = BufferSearchBar::new(cx);
1803            search_bar.set_active_pane_item(Some(&editor), cx);
1804            search_bar.show(cx);
1805            search_bar
1806        });
1807
1808        // Add 3 search items into the history.
1809        search_bar
1810            .update(cx, |search_bar, cx| search_bar.search("a", None, cx))
1811            .await
1812            .unwrap();
1813        search_bar
1814            .update(cx, |search_bar, cx| search_bar.search("b", None, cx))
1815            .await
1816            .unwrap();
1817        search_bar
1818            .update(cx, |search_bar, cx| {
1819                search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), cx)
1820            })
1821            .await
1822            .unwrap();
1823        // Ensure that the latest search is active.
1824        search_bar.update(cx, |search_bar, cx| {
1825            assert_eq!(search_bar.query(cx), "c");
1826            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1827        });
1828
1829        // Next history query after the latest should set the query to the empty string.
1830        search_bar.update(cx, |search_bar, cx| {
1831            search_bar.next_history_query(&NextHistoryQuery, cx);
1832        });
1833        search_bar.update(cx, |search_bar, cx| {
1834            assert_eq!(search_bar.query(cx), "");
1835            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1836        });
1837        search_bar.update(cx, |search_bar, cx| {
1838            search_bar.next_history_query(&NextHistoryQuery, cx);
1839        });
1840        search_bar.update(cx, |search_bar, cx| {
1841            assert_eq!(search_bar.query(cx), "");
1842            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1843        });
1844
1845        // First previous query for empty current query should set the query to the latest.
1846        search_bar.update(cx, |search_bar, cx| {
1847            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1848        });
1849        search_bar.update(cx, |search_bar, cx| {
1850            assert_eq!(search_bar.query(cx), "c");
1851            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1852        });
1853
1854        // Further previous items should go over the history in reverse order.
1855        search_bar.update(cx, |search_bar, cx| {
1856            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1857        });
1858        search_bar.update(cx, |search_bar, cx| {
1859            assert_eq!(search_bar.query(cx), "b");
1860            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1861        });
1862
1863        // Previous items should never go behind the first history item.
1864        search_bar.update(cx, |search_bar, cx| {
1865            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1866        });
1867        search_bar.update(cx, |search_bar, cx| {
1868            assert_eq!(search_bar.query(cx), "a");
1869            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1870        });
1871        search_bar.update(cx, |search_bar, cx| {
1872            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1873        });
1874        search_bar.update(cx, |search_bar, cx| {
1875            assert_eq!(search_bar.query(cx), "a");
1876            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1877        });
1878
1879        // Next items should go over the history in the original order.
1880        search_bar.update(cx, |search_bar, cx| {
1881            search_bar.next_history_query(&NextHistoryQuery, cx);
1882        });
1883        search_bar.update(cx, |search_bar, cx| {
1884            assert_eq!(search_bar.query(cx), "b");
1885            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1886        });
1887
1888        search_bar
1889            .update(cx, |search_bar, cx| search_bar.search("ba", None, cx))
1890            .await
1891            .unwrap();
1892        search_bar.update(cx, |search_bar, cx| {
1893            assert_eq!(search_bar.query(cx), "ba");
1894            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1895        });
1896
1897        // New search input should add another entry to history and move the selection to the end of the history.
1898        search_bar.update(cx, |search_bar, cx| {
1899            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1900        });
1901        search_bar.update(cx, |search_bar, cx| {
1902            assert_eq!(search_bar.query(cx), "c");
1903            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1904        });
1905        search_bar.update(cx, |search_bar, cx| {
1906            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1907        });
1908        search_bar.update(cx, |search_bar, cx| {
1909            assert_eq!(search_bar.query(cx), "b");
1910            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1911        });
1912        search_bar.update(cx, |search_bar, cx| {
1913            search_bar.next_history_query(&NextHistoryQuery, cx);
1914        });
1915        search_bar.update(cx, |search_bar, cx| {
1916            assert_eq!(search_bar.query(cx), "c");
1917            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1918        });
1919        search_bar.update(cx, |search_bar, cx| {
1920            search_bar.next_history_query(&NextHistoryQuery, cx);
1921        });
1922        search_bar.update(cx, |search_bar, cx| {
1923            assert_eq!(search_bar.query(cx), "ba");
1924            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1925        });
1926        search_bar.update(cx, |search_bar, cx| {
1927            search_bar.next_history_query(&NextHistoryQuery, cx);
1928        });
1929        search_bar.update(cx, |search_bar, cx| {
1930            assert_eq!(search_bar.query(cx), "");
1931            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1932        });
1933    }
1934
1935    #[gpui::test]
1936    async fn test_replace_simple(cx: &mut TestAppContext) {
1937        let (editor, search_bar, cx) = init_test(cx);
1938
1939        search_bar
1940            .update(cx, |search_bar, cx| {
1941                search_bar.search("expression", None, cx)
1942            })
1943            .await
1944            .unwrap();
1945
1946        search_bar.update(cx, |search_bar, cx| {
1947            search_bar.replacement_editor.update(cx, |editor, cx| {
1948                // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally.
1949                editor.set_text("expr$1", cx);
1950            });
1951            search_bar.replace_all(&ReplaceAll, cx)
1952        });
1953        assert_eq!(
1954            editor.update(cx, |this, cx| { this.text(cx) }),
1955            r#"
1956        A regular expr$1 (shortened as regex or regexp;[1] also referred to as
1957        rational expr$1[2][3]) is a sequence of characters that specifies a search
1958        pattern in text. Usually such patterns are used by string-searching algorithms
1959        for "find" or "find and replace" operations on strings, or for input validation.
1960        "#
1961            .unindent()
1962        );
1963
1964        // Search for word boundaries and replace just a single one.
1965        search_bar
1966            .update(cx, |search_bar, cx| {
1967                search_bar.search("or", Some(SearchOptions::WHOLE_WORD), cx)
1968            })
1969            .await
1970            .unwrap();
1971
1972        search_bar.update(cx, |search_bar, cx| {
1973            search_bar.replacement_editor.update(cx, |editor, cx| {
1974                editor.set_text("banana", cx);
1975            });
1976            search_bar.replace_next(&ReplaceNext, cx)
1977        });
1978        // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text.
1979        assert_eq!(
1980            editor.update(cx, |this, cx| { this.text(cx) }),
1981            r#"
1982        A regular expr$1 (shortened as regex banana regexp;[1] also referred to as
1983        rational expr$1[2][3]) is a sequence of characters that specifies a search
1984        pattern in text. Usually such patterns are used by string-searching algorithms
1985        for "find" or "find and replace" operations on strings, or for input validation.
1986        "#
1987            .unindent()
1988        );
1989        // Let's turn on regex mode.
1990        search_bar
1991            .update(cx, |search_bar, cx| {
1992                search_bar.search("\\[([^\\]]+)\\]", Some(SearchOptions::REGEX), cx)
1993            })
1994            .await
1995            .unwrap();
1996        search_bar.update(cx, |search_bar, cx| {
1997            search_bar.replacement_editor.update(cx, |editor, cx| {
1998                editor.set_text("${1}number", cx);
1999            });
2000            search_bar.replace_all(&ReplaceAll, cx)
2001        });
2002        assert_eq!(
2003            editor.update(cx, |this, cx| { this.text(cx) }),
2004            r#"
2005        A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2006        rational expr$12number3number) is a sequence of characters that specifies a search
2007        pattern in text. Usually such patterns are used by string-searching algorithms
2008        for "find" or "find and replace" operations on strings, or for input validation.
2009        "#
2010            .unindent()
2011        );
2012        // Now with a whole-word twist.
2013        search_bar
2014            .update(cx, |search_bar, cx| {
2015                search_bar.search(
2016                    "a\\w+s",
2017                    Some(SearchOptions::REGEX | SearchOptions::WHOLE_WORD),
2018                    cx,
2019                )
2020            })
2021            .await
2022            .unwrap();
2023        search_bar.update(cx, |search_bar, cx| {
2024            search_bar.replacement_editor.update(cx, |editor, cx| {
2025                editor.set_text("things", cx);
2026            });
2027            search_bar.replace_all(&ReplaceAll, cx)
2028        });
2029        // The only word affected by this edit should be `algorithms`, even though there's a bunch
2030        // of words in this text that would match this regex if not for WHOLE_WORD.
2031        assert_eq!(
2032            editor.update(cx, |this, cx| { this.text(cx) }),
2033            r#"
2034        A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2035        rational expr$12number3number) is a sequence of characters that specifies a search
2036        pattern in text. Usually such patterns are used by string-searching things
2037        for "find" or "find and replace" operations on strings, or for input validation.
2038        "#
2039            .unindent()
2040        );
2041    }
2042
2043    struct ReplacementTestParams<'a> {
2044        editor: &'a View<Editor>,
2045        search_bar: &'a View<BufferSearchBar>,
2046        cx: &'a mut VisualTestContext,
2047        search_text: &'static str,
2048        search_options: Option<SearchOptions>,
2049        replacement_text: &'static str,
2050        replace_all: bool,
2051        expected_text: String,
2052    }
2053
2054    async fn run_replacement_test(options: ReplacementTestParams<'_>) {
2055        options
2056            .search_bar
2057            .update(options.cx, |search_bar, cx| {
2058                if let Some(options) = options.search_options {
2059                    search_bar.set_search_options(options, cx);
2060                }
2061                search_bar.search(options.search_text, options.search_options, cx)
2062            })
2063            .await
2064            .unwrap();
2065
2066        options.search_bar.update(options.cx, |search_bar, cx| {
2067            search_bar.replacement_editor.update(cx, |editor, cx| {
2068                editor.set_text(options.replacement_text, cx);
2069            });
2070
2071            if options.replace_all {
2072                search_bar.replace_all(&ReplaceAll, cx)
2073            } else {
2074                search_bar.replace_next(&ReplaceNext, cx)
2075            }
2076        });
2077
2078        assert_eq!(
2079            options
2080                .editor
2081                .update(options.cx, |this, cx| { this.text(cx) }),
2082            options.expected_text
2083        );
2084    }
2085
2086    #[gpui::test]
2087    async fn test_replace_special_characters(cx: &mut TestAppContext) {
2088        let (editor, search_bar, cx) = init_test(cx);
2089
2090        run_replacement_test(ReplacementTestParams {
2091            editor: &editor,
2092            search_bar: &search_bar,
2093            cx,
2094            search_text: "expression",
2095            search_options: None,
2096            replacement_text: r"\n",
2097            replace_all: true,
2098            expected_text: r#"
2099            A regular \n (shortened as regex or regexp;[1] also referred to as
2100            rational \n[2][3]) is a sequence of characters that specifies a search
2101            pattern in text. Usually such patterns are used by string-searching algorithms
2102            for "find" or "find and replace" operations on strings, or for input validation.
2103            "#
2104            .unindent(),
2105        })
2106        .await;
2107
2108        run_replacement_test(ReplacementTestParams {
2109            editor: &editor,
2110            search_bar: &search_bar,
2111            cx,
2112            search_text: "or",
2113            search_options: Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
2114            replacement_text: r"\\\n\\\\",
2115            replace_all: false,
2116            expected_text: r#"
2117            A regular \n (shortened as regex \
2118            \\ regexp;[1] also referred to as
2119            rational \n[2][3]) is a sequence of characters that specifies a search
2120            pattern in text. Usually such patterns are used by string-searching algorithms
2121            for "find" or "find and replace" operations on strings, or for input validation.
2122            "#
2123            .unindent(),
2124        })
2125        .await;
2126
2127        run_replacement_test(ReplacementTestParams {
2128            editor: &editor,
2129            search_bar: &search_bar,
2130            cx,
2131            search_text: r"(that|used) ",
2132            search_options: Some(SearchOptions::REGEX),
2133            replacement_text: r"$1\n",
2134            replace_all: true,
2135            expected_text: r#"
2136            A regular \n (shortened as regex \
2137            \\ regexp;[1] also referred to as
2138            rational \n[2][3]) is a sequence of characters that
2139            specifies a search
2140            pattern in text. Usually such patterns are used
2141            by string-searching algorithms
2142            for "find" or "find and replace" operations on strings, or for input validation.
2143            "#
2144            .unindent(),
2145        })
2146        .await;
2147    }
2148
2149    #[gpui::test]
2150    async fn test_find_matches_in_selections_singleton_buffer_multiple_selections(
2151        cx: &mut TestAppContext,
2152    ) {
2153        init_globals(cx);
2154        let buffer = cx.new_model(|cx| {
2155            Buffer::local(
2156                r#"
2157                aaa bbb aaa ccc
2158                aaa bbb aaa ccc
2159                aaa bbb aaa ccc
2160                aaa bbb aaa ccc
2161                aaa bbb aaa ccc
2162                aaa bbb aaa ccc
2163                "#
2164                .unindent(),
2165                cx,
2166            )
2167        });
2168        let cx = cx.add_empty_window();
2169        let editor = cx.new_view(|cx| Editor::for_buffer(buffer.clone(), None, cx));
2170
2171        let search_bar = cx.new_view(|cx| {
2172            let mut search_bar = BufferSearchBar::new(cx);
2173            search_bar.set_active_pane_item(Some(&editor), cx);
2174            search_bar.show(cx);
2175            search_bar
2176        });
2177
2178        editor.update(cx, |editor, cx| {
2179            editor.change_selections(None, cx, |s| {
2180                s.select_ranges(vec![Point::new(1, 0)..Point::new(2, 4)])
2181            })
2182        });
2183
2184        search_bar.update(cx, |search_bar, cx| {
2185            let deploy = Deploy {
2186                focus: true,
2187                replace_enabled: false,
2188                selection_search_enabled: true,
2189            };
2190            search_bar.deploy(&deploy, cx);
2191        });
2192
2193        cx.run_until_parked();
2194
2195        search_bar
2196            .update(cx, |search_bar, cx| search_bar.search("aaa", None, cx))
2197            .await
2198            .unwrap();
2199
2200        editor.update(cx, |editor, cx| {
2201            assert_eq!(
2202                editor.search_background_highlights(cx),
2203                &[
2204                    Point::new(1, 0)..Point::new(1, 3),
2205                    Point::new(1, 8)..Point::new(1, 11),
2206                    Point::new(2, 0)..Point::new(2, 3),
2207                ]
2208            );
2209        });
2210    }
2211
2212    #[gpui::test]
2213    async fn test_find_matches_in_selections_multiple_excerpts_buffer_multiple_selections(
2214        cx: &mut TestAppContext,
2215    ) {
2216        init_globals(cx);
2217        let text = r#"
2218            aaa bbb aaa ccc
2219            aaa bbb aaa ccc
2220            aaa bbb aaa ccc
2221            aaa bbb aaa ccc
2222            aaa bbb aaa ccc
2223            aaa bbb aaa ccc
2224
2225            aaa bbb aaa ccc
2226            aaa bbb aaa ccc
2227            aaa bbb aaa ccc
2228            aaa bbb aaa ccc
2229            aaa bbb aaa ccc
2230            aaa bbb aaa ccc
2231            "#
2232        .unindent();
2233
2234        let cx = cx.add_empty_window();
2235        let editor = cx.new_view(|cx| {
2236            let multibuffer = MultiBuffer::build_multi(
2237                [
2238                    (
2239                        &text,
2240                        vec![
2241                            Point::new(0, 0)..Point::new(2, 0),
2242                            Point::new(4, 0)..Point::new(5, 0),
2243                        ],
2244                    ),
2245                    (&text, vec![Point::new(9, 0)..Point::new(11, 0)]),
2246                ],
2247                cx,
2248            );
2249            Editor::for_multibuffer(multibuffer, None, false, cx)
2250        });
2251
2252        let search_bar = cx.new_view(|cx| {
2253            let mut search_bar = BufferSearchBar::new(cx);
2254            search_bar.set_active_pane_item(Some(&editor), cx);
2255            search_bar.show(cx);
2256            search_bar
2257        });
2258
2259        editor.update(cx, |editor, cx| {
2260            editor.change_selections(None, cx, |s| {
2261                s.select_ranges(vec![
2262                    Point::new(1, 0)..Point::new(1, 4),
2263                    Point::new(5, 3)..Point::new(6, 4),
2264                ])
2265            })
2266        });
2267
2268        search_bar.update(cx, |search_bar, cx| {
2269            let deploy = Deploy {
2270                focus: true,
2271                replace_enabled: false,
2272                selection_search_enabled: true,
2273            };
2274            search_bar.deploy(&deploy, cx);
2275        });
2276
2277        cx.run_until_parked();
2278
2279        search_bar
2280            .update(cx, |search_bar, cx| search_bar.search("aaa", None, cx))
2281            .await
2282            .unwrap();
2283
2284        editor.update(cx, |editor, cx| {
2285            assert_eq!(
2286                editor.search_background_highlights(cx),
2287                &[
2288                    Point::new(1, 0)..Point::new(1, 3),
2289                    Point::new(5, 8)..Point::new(5, 11),
2290                    Point::new(6, 0)..Point::new(6, 3),
2291                ]
2292            );
2293        });
2294    }
2295
2296    #[gpui::test]
2297    async fn test_invalid_regexp_search_after_valid(cx: &mut TestAppContext) {
2298        let (editor, search_bar, cx) = init_test(cx);
2299        // Search using valid regexp
2300        search_bar
2301            .update(cx, |search_bar, cx| {
2302                search_bar.enable_search_option(SearchOptions::REGEX, cx);
2303                search_bar.search("expression", None, cx)
2304            })
2305            .await
2306            .unwrap();
2307        editor.update(cx, |editor, cx| {
2308            assert_eq!(
2309                display_points_of(editor.all_text_background_highlights(cx)),
2310                &[
2311                    DisplayPoint::new(DisplayRow(0), 10)..DisplayPoint::new(DisplayRow(0), 20),
2312                    DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 19),
2313                ],
2314            );
2315        });
2316
2317        // Now, the expression is invalid
2318        search_bar
2319            .update(cx, |search_bar, cx| {
2320                search_bar.search("expression (", None, cx)
2321            })
2322            .await
2323            .unwrap_err();
2324        editor.update(cx, |editor, cx| {
2325            assert!(display_points_of(editor.all_text_background_highlights(cx)).is_empty(),);
2326        });
2327    }
2328
2329    #[gpui::test]
2330    async fn test_search_options_changes(cx: &mut TestAppContext) {
2331        let (_editor, search_bar, cx) = init_test(cx);
2332        update_search_settings(
2333            SearchSettings {
2334                whole_word: false,
2335                case_sensitive: false,
2336                include_ignored: false,
2337                regex: false,
2338            },
2339            cx,
2340        );
2341
2342        let deploy = Deploy {
2343            focus: true,
2344            replace_enabled: false,
2345            selection_search_enabled: true,
2346        };
2347
2348        search_bar.update(cx, |search_bar, cx| {
2349            assert_eq!(
2350                search_bar.search_options,
2351                SearchOptions::NONE,
2352                "Should have no search options enabled by default"
2353            );
2354            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
2355            assert_eq!(
2356                search_bar.search_options,
2357                SearchOptions::WHOLE_WORD,
2358                "Should enable the option toggled"
2359            );
2360            assert!(
2361                !search_bar.dismissed,
2362                "Search bar should be present and visible"
2363            );
2364            search_bar.deploy(&deploy, cx);
2365            assert_eq!(
2366                search_bar.configured_options,
2367                SearchOptions::NONE,
2368                "Should have configured search options matching the settings"
2369            );
2370            assert_eq!(
2371                search_bar.search_options,
2372                SearchOptions::WHOLE_WORD,
2373                "After (re)deploying, the option should still be enabled"
2374            );
2375
2376            search_bar.dismiss(&Dismiss, cx);
2377            search_bar.deploy(&deploy, cx);
2378            assert_eq!(
2379                search_bar.search_options,
2380                SearchOptions::NONE,
2381                "After hiding and showing the search bar, default options should be used"
2382            );
2383
2384            search_bar.toggle_search_option(SearchOptions::REGEX, cx);
2385            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
2386            assert_eq!(
2387                search_bar.search_options,
2388                SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2389                "Should enable the options toggled"
2390            );
2391            assert!(
2392                !search_bar.dismissed,
2393                "Search bar should be present and visible"
2394            );
2395        });
2396
2397        update_search_settings(
2398            SearchSettings {
2399                whole_word: false,
2400                case_sensitive: true,
2401                include_ignored: false,
2402                regex: false,
2403            },
2404            cx,
2405        );
2406        search_bar.update(cx, |search_bar, cx| {
2407            assert_eq!(
2408                search_bar.search_options,
2409                SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2410                "Should have no search options enabled by default"
2411            );
2412
2413            search_bar.deploy(&deploy, cx);
2414            assert_eq!(
2415                search_bar.configured_options,
2416                SearchOptions::CASE_SENSITIVE,
2417                "Should have configured search options matching the settings"
2418            );
2419            assert_eq!(
2420                search_bar.search_options,
2421                SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2422                "Toggling a non-dismissed search bar with custom options should not change the default options"
2423            );
2424            search_bar.dismiss(&Dismiss, cx);
2425            search_bar.deploy(&deploy, cx);
2426            assert_eq!(
2427                search_bar.search_options,
2428                SearchOptions::CASE_SENSITIVE,
2429                "After hiding and showing the search bar, default options should be used"
2430            );
2431        });
2432    }
2433
2434    fn update_search_settings(search_settings: SearchSettings, cx: &mut TestAppContext) {
2435        cx.update(|cx| {
2436            SettingsStore::update_global(cx, |store, cx| {
2437                store.update_user_settings::<EditorSettings>(cx, |settings| {
2438                    settings.search = Some(search_settings);
2439                });
2440            });
2441        });
2442    }
2443}