buffer_search.rs

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