buffer_search.rs

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