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