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