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