buffer_search.rs

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