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