buffer_search.rs

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