buffer_search.rs

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