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