buffer_search.rs

   1mod registrar;
   2
   3use crate::{
   4    FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOption,
   5    SearchOptions, SearchSource, SelectAllMatches, SelectNextMatch, SelectPreviousMatch,
   6    ToggleCaseSensitive, ToggleRegex, ToggleReplace, ToggleSelection, ToggleWholeWord,
   7    buffer_search::registrar::WithResultsOrExternalQuery,
   8    search_bar::{
   9        ActionButtonState, alignment_element, filter_search_results_input, input_base_styles,
  10        render_action_button, render_text_input,
  11    },
  12};
  13use any_vec::AnyVec;
  14use collections::HashMap;
  15use editor::{
  16    DisplayPoint, Editor, EditorSettings, MultiBufferOffset, SplittableEditor, ToggleSplitDiff,
  17    actions::{Backtab, FoldAll, Tab, ToggleFoldAll, UnfoldAll},
  18};
  19use futures::channel::oneshot;
  20use gpui::{
  21    Action, App, ClickEvent, Context, Entity, EventEmitter, Focusable, InteractiveElement as _,
  22    IntoElement, KeyContext, ParentElement as _, Render, ScrollHandle, Styled, Subscription, Task,
  23    WeakEntity, Window, div,
  24};
  25use language::{Language, LanguageRegistry};
  26use project::{
  27    search::SearchQuery,
  28    search_history::{SearchHistory, SearchHistoryCursor},
  29};
  30
  31use fs::Fs;
  32use settings::{DiffViewStyle, Settings, update_settings_file};
  33use std::{any::TypeId, sync::Arc};
  34use zed_actions::{outline::ToggleOutline, workspace::CopyPath, workspace::CopyRelativePath};
  35
  36use ui::{
  37    BASE_REM_SIZE_IN_PX, IconButtonShape, PlatformStyle, TextSize, Tooltip, prelude::*,
  38    render_modifiers, utils::SearchInputWidth,
  39};
  40use util::{ResultExt, paths::PathMatcher};
  41use workspace::{
  42    ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
  43    item::{ItemBufferKind, ItemHandle},
  44    searchable::{
  45        CollapseDirection, Direction, FilteredSearchRange, SearchEvent, SearchToken,
  46        SearchableItemHandle, WeakSearchableItemHandle,
  47    },
  48};
  49
  50pub use registrar::{DivRegistrar, register_pane_search_actions};
  51use registrar::{ForDeployed, ForDismissed, SearchActionsRegistrar};
  52
  53const MAX_BUFFER_SEARCH_HISTORY_SIZE: usize = 50;
  54
  55pub use zed_actions::buffer_search::{Deploy, DeployReplace, Dismiss, FocusEditor};
  56
  57pub enum Event {
  58    UpdateLocation,
  59    Dismissed,
  60}
  61
  62pub fn init(cx: &mut App) {
  63    cx.observe_new(|workspace: &mut Workspace, _, _| BufferSearchBar::register(workspace))
  64        .detach();
  65}
  66
  67pub struct BufferSearchBar {
  68    query_editor: Entity<Editor>,
  69    query_editor_focused: bool,
  70    replacement_editor: Entity<Editor>,
  71    replacement_editor_focused: bool,
  72    active_searchable_item: Option<Box<dyn SearchableItemHandle>>,
  73    active_match_index: Option<usize>,
  74    #[cfg(target_os = "macos")]
  75    active_searchable_item_subscriptions: Option<[Subscription; 2]>,
  76    #[cfg(not(target_os = "macos"))]
  77    active_searchable_item_subscriptions: Option<Subscription>,
  78    #[cfg(target_os = "macos")]
  79    pending_external_query: Option<(String, SearchOptions)>,
  80    active_search: Option<Arc<SearchQuery>>,
  81    searchable_items_with_matches:
  82        HashMap<Box<dyn WeakSearchableItemHandle>, (AnyVec<dyn Send>, SearchToken)>,
  83    pending_search: Option<Task<()>>,
  84    search_options: SearchOptions,
  85    default_options: SearchOptions,
  86    configured_options: SearchOptions,
  87    query_error: Option<String>,
  88    dismissed: bool,
  89    search_history: SearchHistory,
  90    search_history_cursor: SearchHistoryCursor,
  91    replace_enabled: bool,
  92    selection_search_enabled: Option<FilteredSearchRange>,
  93    scroll_handle: ScrollHandle,
  94    editor_scroll_handle: ScrollHandle,
  95    editor_needed_width: Pixels,
  96    regex_language: Option<Arc<Language>>,
  97    is_collapsed: bool,
  98    splittable_editor: Option<WeakEntity<SplittableEditor>>,
  99    _splittable_editor_subscription: Option<Subscription>,
 100}
 101
 102impl EventEmitter<Event> for BufferSearchBar {}
 103impl EventEmitter<workspace::ToolbarItemEvent> for BufferSearchBar {}
 104impl Render for BufferSearchBar {
 105    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 106        let focus_handle = self.focus_handle(cx);
 107
 108        let has_splittable_editor = self.splittable_editor.is_some();
 109        let split_buttons = if has_splittable_editor {
 110            self.splittable_editor
 111                .as_ref()
 112                .and_then(|weak| weak.upgrade())
 113                .map(|splittable_editor| {
 114                    let is_split = splittable_editor.read(cx).is_split();
 115                    let focus_handle = splittable_editor.focus_handle(cx);
 116                    h_flex()
 117                        .gap_1()
 118                        .child(
 119                            IconButton::new("diff-unified", IconName::DiffUnified)
 120                                .shape(IconButtonShape::Square)
 121                                .toggle_state(!is_split)
 122                                .tooltip(Tooltip::element(move |_, cx| {
 123                                    v_flex()
 124                                        .gap_1()
 125                                        .child(Label::new("Unified"))
 126                                        .child(
 127                                            h_flex()
 128                                                .gap_0p5()
 129                                                .text_sm()
 130                                                .text_color(Color::Muted.color(cx))
 131                                                .children(render_modifiers(
 132                                                    &gpui::Modifiers::secondary_key(),
 133                                                    PlatformStyle::platform(),
 134                                                    None,
 135                                                    Some(TextSize::Default.rems(cx).into()),
 136                                                    false,
 137                                                ))
 138                                                .child("click to set as default"),
 139                                        )
 140                                        .into_any()
 141                                }))
 142                                .on_click({
 143                                    let focus_handle = focus_handle.clone();
 144                                    move |_, window, cx| {
 145                                        if window.modifiers().secondary() {
 146                                            update_settings_file(
 147                                                <dyn Fs>::global(cx),
 148                                                cx,
 149                                                |settings, _| {
 150                                                    settings.editor.diff_view_style =
 151                                                        Some(DiffViewStyle::Unified);
 152                                                },
 153                                            );
 154                                        }
 155                                        if is_split {
 156                                            focus_handle.focus(window, cx);
 157                                            window
 158                                                .dispatch_action(ToggleSplitDiff.boxed_clone(), cx);
 159                                        }
 160                                    }
 161                                }),
 162                        )
 163                        .child(
 164                            IconButton::new("diff-split", IconName::DiffSplit)
 165                                .shape(IconButtonShape::Square)
 166                                .toggle_state(is_split)
 167                                .tooltip(Tooltip::element(move |_, cx| {
 168                                    v_flex()
 169                                        .gap_1()
 170                                        .child(Label::new("Split"))
 171                                        .child(
 172                                            h_flex()
 173                                                .gap_0p5()
 174                                                .text_sm()
 175                                                .text_color(Color::Muted.color(cx))
 176                                                .children(render_modifiers(
 177                                                    &gpui::Modifiers::secondary_key(),
 178                                                    PlatformStyle::platform(),
 179                                                    None,
 180                                                    Some(TextSize::Default.rems(cx).into()),
 181                                                    false,
 182                                                ))
 183                                                .child("click to set as default"),
 184                                        )
 185                                        .into_any()
 186                                }))
 187                                .on_click({
 188                                    move |_, window, cx| {
 189                                        if window.modifiers().secondary() {
 190                                            update_settings_file(
 191                                                <dyn Fs>::global(cx),
 192                                                cx,
 193                                                |settings, _| {
 194                                                    settings.editor.diff_view_style =
 195                                                        Some(DiffViewStyle::Split);
 196                                                },
 197                                            );
 198                                        }
 199                                        if !is_split {
 200                                            focus_handle.focus(window, cx);
 201                                            window
 202                                                .dispatch_action(ToggleSplitDiff.boxed_clone(), cx);
 203                                        }
 204                                    }
 205                                }),
 206                        )
 207                })
 208        } else {
 209            None
 210        };
 211
 212        let collapse_expand_button = if self.needs_expand_collapse_option(cx) {
 213            let query_editor_focus = self.query_editor.focus_handle(cx);
 214
 215            let (icon, tooltip_label) = if self.is_collapsed {
 216                (IconName::ChevronUpDown, "Expand All Files")
 217            } else {
 218                (IconName::ChevronDownUp, "Collapse All Files")
 219            };
 220
 221            let collapse_expand_icon_button = |id| {
 222                IconButton::new(id, icon)
 223                    .shape(IconButtonShape::Square)
 224                    .tooltip(move |_, cx| {
 225                        Tooltip::for_action_in(
 226                            tooltip_label,
 227                            &ToggleFoldAll,
 228                            &query_editor_focus,
 229                            cx,
 230                        )
 231                    })
 232                    .on_click(|_event, window, cx| {
 233                        window.dispatch_action(ToggleFoldAll.boxed_clone(), cx)
 234                    })
 235            };
 236
 237            if self.dismissed {
 238                return h_flex()
 239                    .pl_0p5()
 240                    .gap_1()
 241                    .child(collapse_expand_icon_button(
 242                        "multibuffer-collapse-expand-empty",
 243                    ))
 244                    .when(has_splittable_editor, |this| this.children(split_buttons))
 245                    .into_any_element();
 246            }
 247
 248            Some(
 249                h_flex()
 250                    .gap_1()
 251                    .child(collapse_expand_icon_button("multibuffer-collapse-expand"))
 252                    .children(split_buttons)
 253                    .into_any_element(),
 254            )
 255        } else {
 256            None
 257        };
 258
 259        let narrow_mode =
 260            self.scroll_handle.bounds().size.width / window.rem_size() < 340. / BASE_REM_SIZE_IN_PX;
 261        let hide_inline_icons = self.editor_needed_width
 262            > self.editor_scroll_handle.bounds().size.width - window.rem_size() * 6.;
 263
 264        let workspace::searchable::SearchOptions {
 265            case,
 266            word,
 267            regex,
 268            replacement,
 269            selection,
 270            find_in_results,
 271        } = self.supported_options(cx);
 272
 273        self.query_editor.update(cx, |query_editor, cx| {
 274            if query_editor.placeholder_text(cx).is_none() {
 275                query_editor.set_placeholder_text("Search…", window, cx);
 276            }
 277        });
 278
 279        self.replacement_editor.update(cx, |editor, cx| {
 280            editor.set_placeholder_text("Replace with…", window, cx);
 281        });
 282
 283        let mut color_override = None;
 284        let match_text = self
 285            .active_searchable_item
 286            .as_ref()
 287            .and_then(|searchable_item| {
 288                if self.query(cx).is_empty() {
 289                    return None;
 290                }
 291                let matches_count = self
 292                    .searchable_items_with_matches
 293                    .get(&searchable_item.downgrade())
 294                    .map(|(matches, _)| matches.len())
 295                    .unwrap_or(0);
 296                if let Some(match_ix) = self.active_match_index {
 297                    Some(format!("{}/{}", match_ix + 1, matches_count))
 298                } else {
 299                    color_override = Some(Color::Error); // No matches found
 300                    None
 301                }
 302            })
 303            .unwrap_or_else(|| "0/0".to_string());
 304        let should_show_replace_input = self.replace_enabled && replacement;
 305        let in_replace = self.replacement_editor.focus_handle(cx).is_focused(window);
 306
 307        let theme_colors = cx.theme().colors();
 308        let query_border = if self.query_error.is_some() {
 309            Color::Error.color(cx)
 310        } else {
 311            theme_colors.border
 312        };
 313        let replacement_border = theme_colors.border;
 314
 315        let container_width = window.viewport_size().width;
 316        let input_width = SearchInputWidth::calc_width(container_width);
 317
 318        let input_base_styles =
 319            |border_color| input_base_styles(border_color, |div| div.w(input_width));
 320
 321        let input_style = if find_in_results {
 322            filter_search_results_input(query_border, |div| div.w(input_width), cx)
 323        } else {
 324            input_base_styles(query_border)
 325        };
 326
 327        let query_column = input_style
 328            .id("editor-scroll")
 329            .track_scroll(&self.editor_scroll_handle)
 330            .child(render_text_input(&self.query_editor, color_override, cx))
 331            .when(!hide_inline_icons, |div| {
 332                div.child(
 333                    h_flex()
 334                        .gap_1()
 335                        .when(case, |div| {
 336                            div.child(SearchOption::CaseSensitive.as_button(
 337                                self.search_options,
 338                                SearchSource::Buffer,
 339                                focus_handle.clone(),
 340                            ))
 341                        })
 342                        .when(word, |div| {
 343                            div.child(SearchOption::WholeWord.as_button(
 344                                self.search_options,
 345                                SearchSource::Buffer,
 346                                focus_handle.clone(),
 347                            ))
 348                        })
 349                        .when(regex, |div| {
 350                            div.child(SearchOption::Regex.as_button(
 351                                self.search_options,
 352                                SearchSource::Buffer,
 353                                focus_handle.clone(),
 354                            ))
 355                        }),
 356                )
 357            });
 358
 359        let mode_column = h_flex()
 360            .gap_1()
 361            .min_w_64()
 362            .when(replacement, |this| {
 363                this.child(render_action_button(
 364                    "buffer-search-bar-toggle",
 365                    IconName::Replace,
 366                    self.replace_enabled.then_some(ActionButtonState::Toggled),
 367                    "Toggle Replace",
 368                    &ToggleReplace,
 369                    focus_handle.clone(),
 370                ))
 371            })
 372            .when(selection, |this| {
 373                this.child(
 374                    IconButton::new(
 375                        "buffer-search-bar-toggle-search-selection-button",
 376                        IconName::Quote,
 377                    )
 378                    .style(ButtonStyle::Subtle)
 379                    .shape(IconButtonShape::Square)
 380                    .when(self.selection_search_enabled.is_some(), |button| {
 381                        button.style(ButtonStyle::Filled)
 382                    })
 383                    .on_click(cx.listener(|this, _: &ClickEvent, window, cx| {
 384                        this.toggle_selection(&ToggleSelection, window, cx);
 385                    }))
 386                    .toggle_state(self.selection_search_enabled.is_some())
 387                    .tooltip({
 388                        let focus_handle = focus_handle.clone();
 389                        move |_window, cx| {
 390                            Tooltip::for_action_in(
 391                                "Toggle Search Selection",
 392                                &ToggleSelection,
 393                                &focus_handle,
 394                                cx,
 395                            )
 396                        }
 397                    }),
 398                )
 399            })
 400            .when(!find_in_results, |el| {
 401                let query_focus = self.query_editor.focus_handle(cx);
 402                let matches_column = h_flex()
 403                    .pl_2()
 404                    .ml_2()
 405                    .border_l_1()
 406                    .border_color(theme_colors.border_variant)
 407                    .child(render_action_button(
 408                        "buffer-search-nav-button",
 409                        ui::IconName::ChevronLeft,
 410                        self.active_match_index
 411                            .is_none()
 412                            .then_some(ActionButtonState::Disabled),
 413                        "Select Previous Match",
 414                        &SelectPreviousMatch,
 415                        query_focus.clone(),
 416                    ))
 417                    .child(render_action_button(
 418                        "buffer-search-nav-button",
 419                        ui::IconName::ChevronRight,
 420                        self.active_match_index
 421                            .is_none()
 422                            .then_some(ActionButtonState::Disabled),
 423                        "Select Next Match",
 424                        &SelectNextMatch,
 425                        query_focus.clone(),
 426                    ))
 427                    .when(!narrow_mode, |this| {
 428                        this.child(div().ml_2().min_w(rems_from_px(40.)).child(
 429                            Label::new(match_text).size(LabelSize::Small).color(
 430                                if self.active_match_index.is_some() {
 431                                    Color::Default
 432                                } else {
 433                                    Color::Disabled
 434                                },
 435                            ),
 436                        ))
 437                    });
 438
 439                el.child(render_action_button(
 440                    "buffer-search-nav-button",
 441                    IconName::SelectAll,
 442                    Default::default(),
 443                    "Select All Matches",
 444                    &SelectAllMatches,
 445                    query_focus,
 446                ))
 447                .child(matches_column)
 448            })
 449            .when(find_in_results, |el| {
 450                el.child(render_action_button(
 451                    "buffer-search",
 452                    IconName::Close,
 453                    Default::default(),
 454                    "Close Search Bar",
 455                    &Dismiss,
 456                    focus_handle.clone(),
 457                ))
 458            });
 459
 460        let has_collapse_button = collapse_expand_button.is_some();
 461
 462        let search_line = h_flex()
 463            .w_full()
 464            .gap_2()
 465            .when(find_in_results, |el| el.child(alignment_element()))
 466            .when(!find_in_results && has_collapse_button, |el| {
 467                el.pl_0p5().child(collapse_expand_button.expect("button"))
 468            })
 469            .child(query_column)
 470            .child(mode_column);
 471
 472        let replace_line =
 473            should_show_replace_input.then(|| {
 474                let replace_column = input_base_styles(replacement_border)
 475                    .child(render_text_input(&self.replacement_editor, None, cx));
 476                let focus_handle = self.replacement_editor.read(cx).focus_handle(cx);
 477
 478                let replace_actions = h_flex()
 479                    .min_w_64()
 480                    .gap_1()
 481                    .child(render_action_button(
 482                        "buffer-search-replace-button",
 483                        IconName::ReplaceNext,
 484                        Default::default(),
 485                        "Replace Next Match",
 486                        &ReplaceNext,
 487                        focus_handle.clone(),
 488                    ))
 489                    .child(render_action_button(
 490                        "buffer-search-replace-button",
 491                        IconName::ReplaceAll,
 492                        Default::default(),
 493                        "Replace All Matches",
 494                        &ReplaceAll,
 495                        focus_handle,
 496                    ));
 497
 498                h_flex()
 499                    .w_full()
 500                    .gap_2()
 501                    .when(has_collapse_button, |this| this.child(alignment_element()))
 502                    .child(replace_column)
 503                    .child(replace_actions)
 504            });
 505
 506        let mut key_context = KeyContext::new_with_defaults();
 507        key_context.add("BufferSearchBar");
 508        if in_replace {
 509            key_context.add("in_replace");
 510        }
 511
 512        let query_error_line = self.query_error.as_ref().map(|error| {
 513            Label::new(error)
 514                .size(LabelSize::Small)
 515                .color(Color::Error)
 516                .mt_neg_1()
 517                .ml_2()
 518        });
 519
 520        let search_line =
 521            h_flex()
 522                .relative()
 523                .child(search_line)
 524                .when(!narrow_mode && !find_in_results, |this| {
 525                    this.child(
 526                        h_flex()
 527                            .absolute()
 528                            .right_0()
 529                            .when(has_collapse_button, |this| {
 530                                this.pr_2()
 531                                    .border_r_1()
 532                                    .border_color(cx.theme().colors().border_variant)
 533                            })
 534                            .child(render_action_button(
 535                                "buffer-search",
 536                                IconName::Close,
 537                                Default::default(),
 538                                "Close Search Bar",
 539                                &Dismiss,
 540                                focus_handle.clone(),
 541                            )),
 542                    )
 543                });
 544
 545        v_flex()
 546            .id("buffer_search")
 547            .gap_2()
 548            .w_full()
 549            .track_scroll(&self.scroll_handle)
 550            .key_context(key_context)
 551            .capture_action(cx.listener(Self::tab))
 552            .capture_action(cx.listener(Self::backtab))
 553            .capture_action(cx.listener(Self::toggle_fold_all))
 554            .on_action(cx.listener(Self::previous_history_query))
 555            .on_action(cx.listener(Self::next_history_query))
 556            .on_action(cx.listener(Self::dismiss))
 557            .on_action(cx.listener(Self::select_next_match))
 558            .on_action(cx.listener(Self::select_prev_match))
 559            .on_action(cx.listener(|this, _: &ToggleOutline, window, cx| {
 560                if let Some(active_searchable_item) = &mut this.active_searchable_item {
 561                    active_searchable_item.relay_action(Box::new(ToggleOutline), window, cx);
 562                }
 563            }))
 564            .on_action(cx.listener(|this, _: &CopyPath, window, cx| {
 565                if let Some(active_searchable_item) = &mut this.active_searchable_item {
 566                    active_searchable_item.relay_action(Box::new(CopyPath), window, cx);
 567                }
 568            }))
 569            .on_action(cx.listener(|this, _: &CopyRelativePath, window, cx| {
 570                if let Some(active_searchable_item) = &mut this.active_searchable_item {
 571                    active_searchable_item.relay_action(Box::new(CopyRelativePath), window, cx);
 572                }
 573            }))
 574            .when(replacement, |this| {
 575                this.on_action(cx.listener(Self::toggle_replace))
 576                    .on_action(cx.listener(Self::replace_next))
 577                    .on_action(cx.listener(Self::replace_all))
 578            })
 579            .when(case, |this| {
 580                this.on_action(cx.listener(Self::toggle_case_sensitive))
 581            })
 582            .when(word, |this| {
 583                this.on_action(cx.listener(Self::toggle_whole_word))
 584            })
 585            .when(regex, |this| {
 586                this.on_action(cx.listener(Self::toggle_regex))
 587            })
 588            .when(selection, |this| {
 589                this.on_action(cx.listener(Self::toggle_selection))
 590            })
 591            .child(search_line)
 592            .children(query_error_line)
 593            .children(replace_line)
 594            .into_any_element()
 595    }
 596}
 597
 598impl Focusable for BufferSearchBar {
 599    fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
 600        self.query_editor.focus_handle(cx)
 601    }
 602}
 603
 604impl ToolbarItemView for BufferSearchBar {
 605    fn contribute_context(&self, context: &mut KeyContext, _cx: &App) {
 606        if !self.dismissed {
 607            context.add("buffer_search_deployed");
 608        }
 609    }
 610
 611    fn set_active_pane_item(
 612        &mut self,
 613        item: Option<&dyn ItemHandle>,
 614        window: &mut Window,
 615        cx: &mut Context<Self>,
 616    ) -> ToolbarItemLocation {
 617        cx.notify();
 618        self.active_searchable_item_subscriptions.take();
 619        self.active_searchable_item.take();
 620        self.splittable_editor = None;
 621        self._splittable_editor_subscription = None;
 622
 623        self.pending_search.take();
 624
 625        if let Some(splittable_editor) = item
 626            .and_then(|item| item.act_as_type(TypeId::of::<SplittableEditor>(), cx))
 627            .and_then(|entity| entity.downcast::<SplittableEditor>().ok())
 628        {
 629            self._splittable_editor_subscription =
 630                Some(cx.observe(&splittable_editor, |_, _, cx| cx.notify()));
 631            self.splittable_editor = Some(splittable_editor.downgrade());
 632        }
 633
 634        if let Some(searchable_item_handle) =
 635            item.and_then(|item| item.to_searchable_item_handle(cx))
 636        {
 637            let this = cx.entity().downgrade();
 638
 639            let search_event_subscription = searchable_item_handle.subscribe_to_search_events(
 640                window,
 641                cx,
 642                Box::new(move |search_event, window, cx| {
 643                    if let Some(this) = this.upgrade() {
 644                        this.update(cx, |this, cx| {
 645                            this.on_active_searchable_item_event(search_event, window, cx)
 646                        });
 647                    }
 648                }),
 649            );
 650
 651            #[cfg(target_os = "macos")]
 652            {
 653                let item_focus_handle = searchable_item_handle.item_focus_handle(cx);
 654
 655                self.active_searchable_item_subscriptions = Some([
 656                    search_event_subscription,
 657                    cx.on_focus(&item_focus_handle, window, |this, window, cx| {
 658                        if this.query_editor_focused || this.replacement_editor_focused {
 659                            // no need to read pasteboard since focus came from toolbar
 660                            return;
 661                        }
 662
 663                        cx.defer_in(window, |this, window, cx| {
 664                            let Some(item) = cx.read_from_find_pasteboard() else {
 665                                return;
 666                            };
 667                            let Some(text) = item.text() else {
 668                                return;
 669                            };
 670
 671                            if this.query(cx) == text {
 672                                return;
 673                            }
 674
 675                            let search_options = item
 676                                .metadata()
 677                                .and_then(|m| m.parse().ok())
 678                                .and_then(SearchOptions::from_bits)
 679                                .unwrap_or(this.search_options);
 680
 681                            if this.dismissed {
 682                                this.pending_external_query = Some((text, search_options));
 683                            } else {
 684                                drop(this.search(&text, Some(search_options), true, window, cx));
 685                            }
 686                        });
 687                    }),
 688                ]);
 689            }
 690            #[cfg(not(target_os = "macos"))]
 691            {
 692                self.active_searchable_item_subscriptions = Some(search_event_subscription);
 693            }
 694
 695            let is_project_search = searchable_item_handle.supported_options(cx).find_in_results;
 696            self.active_searchable_item = Some(searchable_item_handle);
 697            drop(self.update_matches(true, false, window, cx));
 698            if self.needs_expand_collapse_option(cx) {
 699                return ToolbarItemLocation::PrimaryLeft;
 700            } else if !self.is_dismissed() {
 701                if is_project_search {
 702                    self.dismiss(&Default::default(), window, cx);
 703                } else {
 704                    return ToolbarItemLocation::Secondary;
 705                }
 706            }
 707        }
 708        ToolbarItemLocation::Hidden
 709    }
 710}
 711
 712impl BufferSearchBar {
 713    pub fn query_editor_focused(&self) -> bool {
 714        self.query_editor_focused
 715    }
 716
 717    pub fn register(registrar: &mut impl SearchActionsRegistrar) {
 718        registrar.register_handler(ForDeployed(|this, _: &FocusSearch, window, cx| {
 719            this.query_editor.focus_handle(cx).focus(window, cx);
 720            this.select_query(window, cx);
 721        }));
 722        registrar.register_handler(ForDeployed(
 723            |this, action: &ToggleCaseSensitive, window, cx| {
 724                if this.supported_options(cx).case {
 725                    this.toggle_case_sensitive(action, window, cx);
 726                }
 727            },
 728        ));
 729        registrar.register_handler(ForDeployed(|this, action: &ToggleWholeWord, window, cx| {
 730            if this.supported_options(cx).word {
 731                this.toggle_whole_word(action, window, cx);
 732            }
 733        }));
 734        registrar.register_handler(ForDeployed(|this, action: &ToggleRegex, window, cx| {
 735            if this.supported_options(cx).regex {
 736                this.toggle_regex(action, window, cx);
 737            }
 738        }));
 739        registrar.register_handler(ForDeployed(|this, action: &ToggleSelection, window, cx| {
 740            if this.supported_options(cx).selection {
 741                this.toggle_selection(action, window, cx);
 742            } else {
 743                cx.propagate();
 744            }
 745        }));
 746        registrar.register_handler(ForDeployed(|this, action: &ToggleReplace, window, cx| {
 747            if this.supported_options(cx).replacement {
 748                this.toggle_replace(action, window, cx);
 749            } else {
 750                cx.propagate();
 751            }
 752        }));
 753        registrar.register_handler(WithResultsOrExternalQuery(
 754            |this, action: &SelectNextMatch, window, cx| {
 755                if this.supported_options(cx).find_in_results {
 756                    cx.propagate();
 757                } else {
 758                    this.select_next_match(action, window, cx);
 759                }
 760            },
 761        ));
 762        registrar.register_handler(WithResultsOrExternalQuery(
 763            |this, action: &SelectPreviousMatch, window, cx| {
 764                if this.supported_options(cx).find_in_results {
 765                    cx.propagate();
 766                } else {
 767                    this.select_prev_match(action, window, cx);
 768                }
 769            },
 770        ));
 771        registrar.register_handler(WithResultsOrExternalQuery(
 772            |this, action: &SelectAllMatches, window, cx| {
 773                if this.supported_options(cx).find_in_results {
 774                    cx.propagate();
 775                } else {
 776                    this.select_all_matches(action, window, cx);
 777                }
 778            },
 779        ));
 780        registrar.register_handler(ForDeployed(
 781            |this, _: &editor::actions::Cancel, window, cx| {
 782                this.dismiss(&Dismiss, window, cx);
 783            },
 784        ));
 785        registrar.register_handler(ForDeployed(|this, _: &Dismiss, window, cx| {
 786            this.dismiss(&Dismiss, window, cx);
 787        }));
 788
 789        // register deploy buffer search for both search bar states, since we want to focus into the search bar
 790        // when the deploy action is triggered in the buffer.
 791        registrar.register_handler(ForDeployed(|this, deploy, window, cx| {
 792            this.deploy(deploy, window, cx);
 793        }));
 794        registrar.register_handler(ForDismissed(|this, deploy, window, cx| {
 795            this.deploy(deploy, window, cx);
 796        }));
 797        registrar.register_handler(ForDeployed(|this, _: &DeployReplace, window, cx| {
 798            if this.supported_options(cx).find_in_results {
 799                cx.propagate();
 800            } else {
 801                this.deploy(&Deploy::replace(), window, cx);
 802            }
 803        }));
 804        registrar.register_handler(ForDismissed(|this, _: &DeployReplace, window, cx| {
 805            if this.supported_options(cx).find_in_results {
 806                cx.propagate();
 807            } else {
 808                this.deploy(&Deploy::replace(), window, cx);
 809            }
 810        }));
 811    }
 812
 813    pub fn new(
 814        languages: Option<Arc<LanguageRegistry>>,
 815        window: &mut Window,
 816        cx: &mut Context<Self>,
 817    ) -> Self {
 818        let query_editor = cx.new(|cx| {
 819            let mut editor = Editor::single_line(window, cx);
 820            editor.set_use_autoclose(false);
 821            editor
 822        });
 823        cx.subscribe_in(&query_editor, window, Self::on_query_editor_event)
 824            .detach();
 825        let replacement_editor = cx.new(|cx| Editor::single_line(window, cx));
 826        cx.subscribe(&replacement_editor, Self::on_replacement_editor_event)
 827            .detach();
 828
 829        let search_options = SearchOptions::from_settings(&EditorSettings::get_global(cx).search);
 830        if let Some(languages) = languages {
 831            let query_buffer = query_editor
 832                .read(cx)
 833                .buffer()
 834                .read(cx)
 835                .as_singleton()
 836                .expect("query editor should be backed by a singleton buffer");
 837
 838            query_buffer
 839                .read(cx)
 840                .set_language_registry(languages.clone());
 841
 842            cx.spawn(async move |buffer_search_bar, cx| {
 843                use anyhow::Context as _;
 844
 845                let regex_language = languages
 846                    .language_for_name("regex")
 847                    .await
 848                    .context("loading regex language")?;
 849
 850                buffer_search_bar
 851                    .update(cx, |buffer_search_bar, cx| {
 852                        buffer_search_bar.regex_language = Some(regex_language);
 853                        buffer_search_bar.adjust_query_regex_language(cx);
 854                    })
 855                    .ok();
 856                anyhow::Ok(())
 857            })
 858            .detach_and_log_err(cx);
 859        }
 860
 861        Self {
 862            query_editor,
 863            query_editor_focused: false,
 864            replacement_editor,
 865            replacement_editor_focused: false,
 866            active_searchable_item: None,
 867            active_searchable_item_subscriptions: None,
 868            #[cfg(target_os = "macos")]
 869            pending_external_query: None,
 870            active_match_index: None,
 871            searchable_items_with_matches: Default::default(),
 872            default_options: search_options,
 873            configured_options: search_options,
 874            search_options,
 875            pending_search: None,
 876            query_error: None,
 877            dismissed: true,
 878            search_history: SearchHistory::new(
 879                Some(MAX_BUFFER_SEARCH_HISTORY_SIZE),
 880                project::search_history::QueryInsertionBehavior::ReplacePreviousIfContains,
 881            ),
 882            search_history_cursor: Default::default(),
 883            active_search: None,
 884            replace_enabled: false,
 885            selection_search_enabled: None,
 886            scroll_handle: ScrollHandle::new(),
 887            editor_scroll_handle: ScrollHandle::new(),
 888            editor_needed_width: px(0.),
 889            regex_language: None,
 890            is_collapsed: false,
 891            splittable_editor: None,
 892            _splittable_editor_subscription: None,
 893        }
 894    }
 895
 896    pub fn is_dismissed(&self) -> bool {
 897        self.dismissed
 898    }
 899
 900    pub fn dismiss(&mut self, _: &Dismiss, window: &mut Window, cx: &mut Context<Self>) {
 901        self.dismissed = true;
 902        cx.emit(Event::Dismissed);
 903        self.query_error = None;
 904        self.sync_select_next_case_sensitivity(cx);
 905
 906        for searchable_item in self.searchable_items_with_matches.keys() {
 907            if let Some(searchable_item) =
 908                WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
 909            {
 910                searchable_item.clear_matches(window, cx);
 911            }
 912        }
 913
 914        let needs_collapse_expand = self.needs_expand_collapse_option(cx);
 915
 916        if let Some(active_editor) = self.active_searchable_item.as_mut() {
 917            self.selection_search_enabled = None;
 918            self.replace_enabled = false;
 919            active_editor.search_bar_visibility_changed(false, window, cx);
 920            active_editor.toggle_filtered_search_ranges(None, window, cx);
 921            let handle = active_editor.item_focus_handle(cx);
 922            self.focus(&handle, window, cx);
 923        }
 924
 925        if needs_collapse_expand {
 926            cx.emit(Event::UpdateLocation);
 927            cx.emit(ToolbarItemEvent::ChangeLocation(
 928                ToolbarItemLocation::PrimaryLeft,
 929            ));
 930            cx.notify();
 931            return;
 932        }
 933        cx.emit(Event::UpdateLocation);
 934        cx.emit(ToolbarItemEvent::ChangeLocation(
 935            ToolbarItemLocation::Hidden,
 936        ));
 937        cx.notify();
 938    }
 939
 940    pub fn deploy(&mut self, deploy: &Deploy, window: &mut Window, cx: &mut Context<Self>) -> bool {
 941        let filtered_search_range = if deploy.selection_search_enabled {
 942            Some(FilteredSearchRange::Default)
 943        } else {
 944            None
 945        };
 946        if self.show(window, cx) {
 947            if let Some(active_item) = self.active_searchable_item.as_mut() {
 948                active_item.toggle_filtered_search_ranges(filtered_search_range, window, cx);
 949            }
 950            self.search_suggested(window, cx);
 951            self.smartcase(window, cx);
 952            self.sync_select_next_case_sensitivity(cx);
 953            self.replace_enabled |= deploy.replace_enabled;
 954            self.selection_search_enabled =
 955                self.selection_search_enabled
 956                    .or(if deploy.selection_search_enabled {
 957                        Some(FilteredSearchRange::Default)
 958                    } else {
 959                        None
 960                    });
 961            if deploy.focus {
 962                let mut handle = self.query_editor.focus_handle(cx);
 963                let mut select_query = true;
 964                if deploy.replace_enabled && handle.is_focused(window) {
 965                    handle = self.replacement_editor.focus_handle(cx);
 966                    select_query = false;
 967                };
 968
 969                if select_query {
 970                    self.select_query(window, cx);
 971                }
 972
 973                window.focus(&handle, cx);
 974            }
 975            return true;
 976        }
 977
 978        cx.propagate();
 979        false
 980    }
 981
 982    pub fn toggle(&mut self, action: &Deploy, window: &mut Window, cx: &mut Context<Self>) {
 983        if self.is_dismissed() {
 984            self.deploy(action, window, cx);
 985        } else {
 986            self.dismiss(&Dismiss, window, cx);
 987        }
 988    }
 989
 990    pub fn show(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
 991        let Some(handle) = self.active_searchable_item.as_ref() else {
 992            return false;
 993        };
 994
 995        let configured_options =
 996            SearchOptions::from_settings(&EditorSettings::get_global(cx).search);
 997        let settings_changed = configured_options != self.configured_options;
 998
 999        if self.dismissed && settings_changed {
1000            // Only update configuration options when search bar is dismissed,
1001            // so we don't miss updates even after calling show twice
1002            self.configured_options = configured_options;
1003            self.search_options = configured_options;
1004            self.default_options = configured_options;
1005        }
1006
1007        // This isn't a normal setting; it's only applicable to vim search.
1008        self.search_options.remove(SearchOptions::BACKWARDS);
1009
1010        self.dismissed = false;
1011        self.adjust_query_regex_language(cx);
1012        handle.search_bar_visibility_changed(true, window, cx);
1013        cx.notify();
1014        cx.emit(Event::UpdateLocation);
1015        cx.emit(ToolbarItemEvent::ChangeLocation(
1016            if self.needs_expand_collapse_option(cx) {
1017                ToolbarItemLocation::PrimaryLeft
1018            } else {
1019                ToolbarItemLocation::Secondary
1020            },
1021        ));
1022        true
1023    }
1024
1025    fn supported_options(&self, cx: &mut Context<Self>) -> workspace::searchable::SearchOptions {
1026        self.active_searchable_item
1027            .as_ref()
1028            .map(|item| item.supported_options(cx))
1029            .unwrap_or_default()
1030    }
1031
1032    // We provide an expand/collapse button if we are in a multibuffer
1033    // and not doing a project search.
1034    fn needs_expand_collapse_option(&self, cx: &App) -> bool {
1035        if let Some(item) = &self.active_searchable_item {
1036            let buffer_kind = item.buffer_kind(cx);
1037
1038            if buffer_kind == ItemBufferKind::Singleton {
1039                return false;
1040            }
1041
1042            let workspace::searchable::SearchOptions {
1043                find_in_results, ..
1044            } = item.supported_options(cx);
1045            !find_in_results
1046        } else {
1047            false
1048        }
1049    }
1050
1051    fn toggle_fold_all(&mut self, _: &ToggleFoldAll, window: &mut Window, cx: &mut Context<Self>) {
1052        self.toggle_fold_all_in_item(window, cx);
1053    }
1054
1055    fn toggle_fold_all_in_item(&self, window: &mut Window, cx: &mut Context<Self>) {
1056        let is_collapsed = self.is_collapsed;
1057        if let Some(item) = &self.active_searchable_item {
1058            if let Some(item) = item.act_as_type(TypeId::of::<Editor>(), cx) {
1059                let editor = item.downcast::<Editor>().expect("Is an editor");
1060                editor.update(cx, |editor, cx| {
1061                    if is_collapsed {
1062                        editor.unfold_all(&UnfoldAll, window, cx);
1063                    } else {
1064                        editor.fold_all(&FoldAll, window, cx);
1065                    }
1066                })
1067            }
1068        }
1069    }
1070
1071    pub fn search_suggested(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1072        let search = self.query_suggestion(window, cx).map(|suggestion| {
1073            self.search(&suggestion, Some(self.default_options), true, window, cx)
1074        });
1075
1076        #[cfg(target_os = "macos")]
1077        let search = search.or_else(|| {
1078            self.pending_external_query
1079                .take()
1080                .map(|(query, options)| self.search(&query, Some(options), true, window, cx))
1081        });
1082
1083        if let Some(search) = search {
1084            cx.spawn_in(window, async move |this, cx| {
1085                if search.await.is_ok() {
1086                    this.update_in(cx, |this, window, cx| {
1087                        if !this.dismissed {
1088                            this.activate_current_match(window, cx)
1089                        }
1090                    })
1091                } else {
1092                    Ok(())
1093                }
1094            })
1095            .detach_and_log_err(cx);
1096        }
1097    }
1098
1099    pub fn activate_current_match(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1100        if let Some(match_ix) = self.active_match_index
1101            && let Some(active_searchable_item) = self.active_searchable_item.as_ref()
1102            && let Some((matches, token)) = self
1103                .searchable_items_with_matches
1104                .get(&active_searchable_item.downgrade())
1105        {
1106            active_searchable_item.activate_match(match_ix, matches, *token, window, cx)
1107        }
1108    }
1109
1110    pub fn select_query(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1111        self.query_editor.update(cx, |query_editor, cx| {
1112            query_editor.select_all(&Default::default(), window, cx);
1113        });
1114    }
1115
1116    pub fn query(&self, cx: &App) -> String {
1117        self.query_editor.read(cx).text(cx)
1118    }
1119
1120    pub fn replacement(&self, cx: &mut App) -> String {
1121        self.replacement_editor.read(cx).text(cx)
1122    }
1123
1124    pub fn query_suggestion(
1125        &mut self,
1126        window: &mut Window,
1127        cx: &mut Context<Self>,
1128    ) -> Option<String> {
1129        self.active_searchable_item
1130            .as_ref()
1131            .map(|searchable_item| searchable_item.query_suggestion(window, cx))
1132            .filter(|suggestion| !suggestion.is_empty())
1133    }
1134
1135    pub fn set_replacement(&mut self, replacement: Option<&str>, cx: &mut Context<Self>) {
1136        if replacement.is_none() {
1137            self.replace_enabled = false;
1138            return;
1139        }
1140        self.replace_enabled = true;
1141        self.replacement_editor
1142            .update(cx, |replacement_editor, cx| {
1143                replacement_editor
1144                    .buffer()
1145                    .update(cx, |replacement_buffer, cx| {
1146                        let len = replacement_buffer.len(cx);
1147                        replacement_buffer.edit(
1148                            [(MultiBufferOffset(0)..len, replacement.unwrap())],
1149                            None,
1150                            cx,
1151                        );
1152                    });
1153            });
1154    }
1155
1156    pub fn focus_replace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1157        self.focus(&self.replacement_editor.focus_handle(cx), window, cx);
1158        cx.notify();
1159    }
1160
1161    pub fn search(
1162        &mut self,
1163        query: &str,
1164        options: Option<SearchOptions>,
1165        add_to_history: bool,
1166        window: &mut Window,
1167        cx: &mut Context<Self>,
1168    ) -> oneshot::Receiver<()> {
1169        let options = options.unwrap_or(self.default_options);
1170        let updated = query != self.query(cx) || self.search_options != options;
1171        if updated {
1172            self.query_editor.update(cx, |query_editor, cx| {
1173                query_editor.buffer().update(cx, |query_buffer, cx| {
1174                    let len = query_buffer.len(cx);
1175                    query_buffer.edit([(MultiBufferOffset(0)..len, query)], None, cx);
1176                });
1177            });
1178            self.set_search_options(options, cx);
1179            self.clear_matches(window, cx);
1180            #[cfg(target_os = "macos")]
1181            self.update_find_pasteboard(cx);
1182            cx.notify();
1183        }
1184        self.update_matches(!updated, add_to_history, window, cx)
1185    }
1186
1187    #[cfg(target_os = "macos")]
1188    pub fn update_find_pasteboard(&mut self, cx: &mut App) {
1189        cx.write_to_find_pasteboard(gpui::ClipboardItem::new_string_with_metadata(
1190            self.query(cx),
1191            self.search_options.bits().to_string(),
1192        ));
1193    }
1194
1195    pub fn focus_editor(&mut self, _: &FocusEditor, window: &mut Window, cx: &mut Context<Self>) {
1196        if let Some(active_editor) = self.active_searchable_item.as_ref() {
1197            let handle = active_editor.item_focus_handle(cx);
1198            window.focus(&handle, cx);
1199        }
1200    }
1201
1202    pub fn toggle_search_option(
1203        &mut self,
1204        search_option: SearchOptions,
1205        window: &mut Window,
1206        cx: &mut Context<Self>,
1207    ) {
1208        self.search_options.toggle(search_option);
1209        self.default_options = self.search_options;
1210        drop(self.update_matches(false, false, window, cx));
1211        self.adjust_query_regex_language(cx);
1212        self.sync_select_next_case_sensitivity(cx);
1213        cx.notify();
1214    }
1215
1216    pub fn has_search_option(&mut self, search_option: SearchOptions) -> bool {
1217        self.search_options.contains(search_option)
1218    }
1219
1220    pub fn enable_search_option(
1221        &mut self,
1222        search_option: SearchOptions,
1223        window: &mut Window,
1224        cx: &mut Context<Self>,
1225    ) {
1226        if !self.search_options.contains(search_option) {
1227            self.toggle_search_option(search_option, window, cx)
1228        }
1229    }
1230
1231    pub fn set_search_within_selection(
1232        &mut self,
1233        search_within_selection: Option<FilteredSearchRange>,
1234        window: &mut Window,
1235        cx: &mut Context<Self>,
1236    ) -> Option<oneshot::Receiver<()>> {
1237        let active_item = self.active_searchable_item.as_mut()?;
1238        self.selection_search_enabled = search_within_selection;
1239        active_item.toggle_filtered_search_ranges(self.selection_search_enabled, window, cx);
1240        cx.notify();
1241        Some(self.update_matches(false, false, window, cx))
1242    }
1243
1244    pub fn set_search_options(&mut self, search_options: SearchOptions, cx: &mut Context<Self>) {
1245        self.search_options = search_options;
1246        self.adjust_query_regex_language(cx);
1247        self.sync_select_next_case_sensitivity(cx);
1248        cx.notify();
1249    }
1250
1251    pub fn clear_search_within_ranges(
1252        &mut self,
1253        search_options: SearchOptions,
1254        cx: &mut Context<Self>,
1255    ) {
1256        self.search_options = search_options;
1257        self.adjust_query_regex_language(cx);
1258        cx.notify();
1259    }
1260
1261    fn select_next_match(
1262        &mut self,
1263        _: &SelectNextMatch,
1264        window: &mut Window,
1265        cx: &mut Context<Self>,
1266    ) {
1267        self.select_match(Direction::Next, 1, window, cx);
1268    }
1269
1270    fn select_prev_match(
1271        &mut self,
1272        _: &SelectPreviousMatch,
1273        window: &mut Window,
1274        cx: &mut Context<Self>,
1275    ) {
1276        self.select_match(Direction::Prev, 1, window, cx);
1277    }
1278
1279    pub fn select_all_matches(
1280        &mut self,
1281        _: &SelectAllMatches,
1282        window: &mut Window,
1283        cx: &mut Context<Self>,
1284    ) {
1285        if !self.dismissed
1286            && self.active_match_index.is_some()
1287            && let Some(searchable_item) = self.active_searchable_item.as_ref()
1288            && let Some((matches, token)) = self
1289                .searchable_items_with_matches
1290                .get(&searchable_item.downgrade())
1291        {
1292            searchable_item.select_matches(matches, *token, window, cx);
1293            self.focus_editor(&FocusEditor, window, cx);
1294        }
1295    }
1296
1297    pub fn select_match(
1298        &mut self,
1299        direction: Direction,
1300        count: usize,
1301        window: &mut Window,
1302        cx: &mut Context<Self>,
1303    ) {
1304        #[cfg(target_os = "macos")]
1305        if let Some((query, options)) = self.pending_external_query.take() {
1306            let search_rx = self.search(&query, Some(options), true, window, cx);
1307            cx.spawn_in(window, async move |this, cx| {
1308                if search_rx.await.is_ok() {
1309                    this.update_in(cx, |this, window, cx| {
1310                        this.activate_current_match(window, cx);
1311                    })
1312                    .ok();
1313                }
1314            })
1315            .detach();
1316
1317            return;
1318        }
1319
1320        if let Some(index) = self.active_match_index
1321            && let Some(searchable_item) = self.active_searchable_item.as_ref()
1322            && let Some((matches, token)) = self
1323                .searchable_items_with_matches
1324                .get(&searchable_item.downgrade())
1325                .filter(|(matches, _)| !matches.is_empty())
1326        {
1327            // If 'wrapscan' is disabled, searches do not wrap around the end of the file.
1328            if !EditorSettings::get_global(cx).search_wrap
1329                && ((direction == Direction::Next && index + count >= matches.len())
1330                    || (direction == Direction::Prev && index < count))
1331            {
1332                crate::show_no_more_matches(window, cx);
1333                return;
1334            }
1335            let new_match_index = searchable_item
1336                .match_index_for_direction(matches, index, direction, count, *token, window, cx);
1337
1338            searchable_item.update_matches(matches, Some(new_match_index), *token, window, cx);
1339            searchable_item.activate_match(new_match_index, matches, *token, window, cx);
1340        }
1341    }
1342
1343    pub fn select_first_match(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1344        if let Some(searchable_item) = self.active_searchable_item.as_ref()
1345            && let Some((matches, token)) = self
1346                .searchable_items_with_matches
1347                .get(&searchable_item.downgrade())
1348        {
1349            if matches.is_empty() {
1350                return;
1351            }
1352            searchable_item.update_matches(matches, Some(0), *token, window, cx);
1353            searchable_item.activate_match(0, matches, *token, window, cx);
1354        }
1355    }
1356
1357    pub fn select_last_match(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1358        if let Some(searchable_item) = self.active_searchable_item.as_ref()
1359            && let Some((matches, token)) = self
1360                .searchable_items_with_matches
1361                .get(&searchable_item.downgrade())
1362        {
1363            if matches.is_empty() {
1364                return;
1365            }
1366            let new_match_index = matches.len() - 1;
1367            searchable_item.update_matches(matches, Some(new_match_index), *token, window, cx);
1368            searchable_item.activate_match(new_match_index, matches, *token, window, cx);
1369        }
1370    }
1371
1372    fn on_query_editor_event(
1373        &mut self,
1374        editor: &Entity<Editor>,
1375        event: &editor::EditorEvent,
1376        window: &mut Window,
1377        cx: &mut Context<Self>,
1378    ) {
1379        match event {
1380            editor::EditorEvent::Focused => self.query_editor_focused = true,
1381            editor::EditorEvent::Blurred => self.query_editor_focused = false,
1382            editor::EditorEvent::Edited { .. } => {
1383                self.smartcase(window, cx);
1384                self.clear_matches(window, cx);
1385                let search = self.update_matches(false, true, window, cx);
1386
1387                let width = editor.update(cx, |editor, cx| {
1388                    let text_layout_details = editor.text_layout_details(window, cx);
1389                    let snapshot = editor.snapshot(window, cx).display_snapshot;
1390
1391                    snapshot.x_for_display_point(snapshot.max_point(), &text_layout_details)
1392                        - snapshot.x_for_display_point(DisplayPoint::zero(), &text_layout_details)
1393                });
1394                self.editor_needed_width = width;
1395                cx.notify();
1396
1397                cx.spawn_in(window, async move |this, cx| {
1398                    if search.await.is_ok() {
1399                        this.update_in(cx, |this, window, cx| {
1400                            this.activate_current_match(window, cx);
1401                            #[cfg(target_os = "macos")]
1402                            this.update_find_pasteboard(cx);
1403                        })?;
1404                    }
1405                    anyhow::Ok(())
1406                })
1407                .detach_and_log_err(cx);
1408            }
1409            _ => {}
1410        }
1411    }
1412
1413    fn on_replacement_editor_event(
1414        &mut self,
1415        _: Entity<Editor>,
1416        event: &editor::EditorEvent,
1417        _: &mut Context<Self>,
1418    ) {
1419        match event {
1420            editor::EditorEvent::Focused => self.replacement_editor_focused = true,
1421            editor::EditorEvent::Blurred => self.replacement_editor_focused = false,
1422            _ => {}
1423        }
1424    }
1425
1426    fn on_active_searchable_item_event(
1427        &mut self,
1428        event: &SearchEvent,
1429        window: &mut Window,
1430        cx: &mut Context<Self>,
1431    ) {
1432        match event {
1433            SearchEvent::MatchesInvalidated => {
1434                drop(self.update_matches(false, false, window, cx));
1435            }
1436            SearchEvent::ActiveMatchChanged => self.update_match_index(window, cx),
1437            SearchEvent::ResultsCollapsedChanged(collapse_direction) => {
1438                if self.needs_expand_collapse_option(cx) {
1439                    match collapse_direction {
1440                        CollapseDirection::Collapsed => self.is_collapsed = true,
1441                        CollapseDirection::Expanded => self.is_collapsed = false,
1442                    }
1443                }
1444                cx.notify();
1445            }
1446        }
1447    }
1448
1449    fn toggle_case_sensitive(
1450        &mut self,
1451        _: &ToggleCaseSensitive,
1452        window: &mut Window,
1453        cx: &mut Context<Self>,
1454    ) {
1455        self.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx)
1456    }
1457
1458    fn toggle_whole_word(
1459        &mut self,
1460        _: &ToggleWholeWord,
1461        window: &mut Window,
1462        cx: &mut Context<Self>,
1463    ) {
1464        self.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx)
1465    }
1466
1467    fn toggle_selection(
1468        &mut self,
1469        _: &ToggleSelection,
1470        window: &mut Window,
1471        cx: &mut Context<Self>,
1472    ) {
1473        self.set_search_within_selection(
1474            if let Some(_) = self.selection_search_enabled {
1475                None
1476            } else {
1477                Some(FilteredSearchRange::Default)
1478            },
1479            window,
1480            cx,
1481        );
1482    }
1483
1484    fn toggle_regex(&mut self, _: &ToggleRegex, window: &mut Window, cx: &mut Context<Self>) {
1485        self.toggle_search_option(SearchOptions::REGEX, window, cx)
1486    }
1487
1488    fn clear_active_searchable_item_matches(&mut self, window: &mut Window, cx: &mut App) {
1489        if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
1490            self.active_match_index = None;
1491            self.searchable_items_with_matches
1492                .remove(&active_searchable_item.downgrade());
1493            active_searchable_item.clear_matches(window, cx);
1494        }
1495    }
1496
1497    pub fn has_active_match(&self) -> bool {
1498        self.active_match_index.is_some()
1499    }
1500
1501    fn clear_matches(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1502        let mut active_item_matches = None;
1503        for (searchable_item, matches) in self.searchable_items_with_matches.drain() {
1504            if let Some(searchable_item) =
1505                WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
1506            {
1507                if Some(&searchable_item) == self.active_searchable_item.as_ref() {
1508                    active_item_matches = Some((searchable_item.downgrade(), matches));
1509                } else {
1510                    searchable_item.clear_matches(window, cx);
1511                }
1512            }
1513        }
1514
1515        self.searchable_items_with_matches
1516            .extend(active_item_matches);
1517    }
1518
1519    fn update_matches(
1520        &mut self,
1521        reuse_existing_query: bool,
1522        add_to_history: bool,
1523        window: &mut Window,
1524        cx: &mut Context<Self>,
1525    ) -> oneshot::Receiver<()> {
1526        let (done_tx, done_rx) = oneshot::channel();
1527        let query = self.query(cx);
1528        self.pending_search.take();
1529        #[cfg(target_os = "macos")]
1530        self.pending_external_query.take();
1531
1532        if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
1533            self.query_error = None;
1534            if query.is_empty() {
1535                self.clear_active_searchable_item_matches(window, cx);
1536                let _ = done_tx.send(());
1537                cx.notify();
1538            } else {
1539                let query: Arc<_> = if let Some(search) =
1540                    self.active_search.take().filter(|_| reuse_existing_query)
1541                {
1542                    search
1543                } else {
1544                    // Value doesn't matter, we only construct empty matchers with it
1545
1546                    if self.search_options.contains(SearchOptions::REGEX) {
1547                        match SearchQuery::regex(
1548                            query,
1549                            self.search_options.contains(SearchOptions::WHOLE_WORD),
1550                            self.search_options.contains(SearchOptions::CASE_SENSITIVE),
1551                            false,
1552                            self.search_options
1553                                .contains(SearchOptions::ONE_MATCH_PER_LINE),
1554                            PathMatcher::default(),
1555                            PathMatcher::default(),
1556                            false,
1557                            None,
1558                        ) {
1559                            Ok(query) => query.with_replacement(self.replacement(cx)),
1560                            Err(e) => {
1561                                self.query_error = Some(e.to_string());
1562                                self.clear_active_searchable_item_matches(window, cx);
1563                                cx.notify();
1564                                return done_rx;
1565                            }
1566                        }
1567                    } else {
1568                        match SearchQuery::text(
1569                            query,
1570                            self.search_options.contains(SearchOptions::WHOLE_WORD),
1571                            self.search_options.contains(SearchOptions::CASE_SENSITIVE),
1572                            false,
1573                            PathMatcher::default(),
1574                            PathMatcher::default(),
1575                            false,
1576                            None,
1577                        ) {
1578                            Ok(query) => query.with_replacement(self.replacement(cx)),
1579                            Err(e) => {
1580                                self.query_error = Some(e.to_string());
1581                                self.clear_active_searchable_item_matches(window, cx);
1582                                cx.notify();
1583                                return done_rx;
1584                            }
1585                        }
1586                    }
1587                    .into()
1588                };
1589
1590                self.active_search = Some(query.clone());
1591                let query_text = query.as_str().to_string();
1592
1593                let matches_with_token =
1594                    active_searchable_item.find_matches_with_token(query, window, cx);
1595
1596                let active_searchable_item = active_searchable_item.downgrade();
1597                self.pending_search = Some(cx.spawn_in(window, async move |this, cx| {
1598                    let (matches, token) = matches_with_token.await;
1599
1600                    this.update_in(cx, |this, window, cx| {
1601                        if let Some(active_searchable_item) =
1602                            WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx)
1603                        {
1604                            this.searchable_items_with_matches
1605                                .insert(active_searchable_item.downgrade(), (matches, token));
1606
1607                            this.update_match_index(window, cx);
1608
1609                            if add_to_history {
1610                                this.search_history
1611                                    .add(&mut this.search_history_cursor, query_text);
1612                            }
1613                            if !this.dismissed {
1614                                let (matches, token) = this
1615                                    .searchable_items_with_matches
1616                                    .get(&active_searchable_item.downgrade())
1617                                    .unwrap();
1618                                if matches.is_empty() {
1619                                    active_searchable_item.clear_matches(window, cx);
1620                                } else {
1621                                    active_searchable_item.update_matches(
1622                                        matches,
1623                                        this.active_match_index,
1624                                        *token,
1625                                        window,
1626                                        cx,
1627                                    );
1628                                }
1629                            }
1630                            let _ = done_tx.send(());
1631                            cx.notify();
1632                        }
1633                    })
1634                    .log_err();
1635                }));
1636            }
1637        }
1638        done_rx
1639    }
1640
1641    fn reverse_direction_if_backwards(&self, direction: Direction) -> Direction {
1642        if self.search_options.contains(SearchOptions::BACKWARDS) {
1643            direction.opposite()
1644        } else {
1645            direction
1646        }
1647    }
1648
1649    pub fn update_match_index(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1650        let direction = self.reverse_direction_if_backwards(Direction::Next);
1651        let new_index = self
1652            .active_searchable_item
1653            .as_ref()
1654            .and_then(|searchable_item| {
1655                let (matches, token) = self
1656                    .searchable_items_with_matches
1657                    .get(&searchable_item.downgrade())?;
1658                searchable_item.active_match_index(direction, matches, *token, window, cx)
1659            });
1660        if new_index != self.active_match_index {
1661            self.active_match_index = new_index;
1662            if !self.dismissed {
1663                if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1664                    if let Some((matches, token)) = self
1665                        .searchable_items_with_matches
1666                        .get(&searchable_item.downgrade())
1667                    {
1668                        if !matches.is_empty() {
1669                            searchable_item.update_matches(matches, new_index, *token, window, cx);
1670                        }
1671                    }
1672                }
1673            }
1674            cx.notify();
1675        }
1676    }
1677
1678    fn tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context<Self>) {
1679        self.cycle_field(Direction::Next, window, cx);
1680    }
1681
1682    fn backtab(&mut self, _: &Backtab, window: &mut Window, cx: &mut Context<Self>) {
1683        self.cycle_field(Direction::Prev, window, cx);
1684    }
1685    fn cycle_field(&mut self, direction: Direction, window: &mut Window, cx: &mut Context<Self>) {
1686        let mut handles = vec![self.query_editor.focus_handle(cx)];
1687        if self.replace_enabled {
1688            handles.push(self.replacement_editor.focus_handle(cx));
1689        }
1690        if let Some(item) = self.active_searchable_item.as_ref() {
1691            handles.push(item.item_focus_handle(cx));
1692        }
1693        let current_index = match handles.iter().position(|focus| focus.is_focused(window)) {
1694            Some(index) => index,
1695            None => return,
1696        };
1697
1698        let new_index = match direction {
1699            Direction::Next => (current_index + 1) % handles.len(),
1700            Direction::Prev if current_index == 0 => handles.len() - 1,
1701            Direction::Prev => (current_index - 1) % handles.len(),
1702        };
1703        let next_focus_handle = &handles[new_index];
1704        self.focus(next_focus_handle, window, cx);
1705        cx.stop_propagation();
1706    }
1707
1708    fn next_history_query(
1709        &mut self,
1710        _: &NextHistoryQuery,
1711        window: &mut Window,
1712        cx: &mut Context<Self>,
1713    ) {
1714        if let Some(new_query) = self
1715            .search_history
1716            .next(&mut self.search_history_cursor)
1717            .map(str::to_string)
1718        {
1719            drop(self.search(&new_query, Some(self.search_options), false, window, cx));
1720        } else {
1721            self.search_history_cursor.reset();
1722            drop(self.search("", Some(self.search_options), false, window, cx));
1723        }
1724    }
1725
1726    fn previous_history_query(
1727        &mut self,
1728        _: &PreviousHistoryQuery,
1729        window: &mut Window,
1730        cx: &mut Context<Self>,
1731    ) {
1732        if self.query(cx).is_empty()
1733            && let Some(new_query) = self
1734                .search_history
1735                .current(&self.search_history_cursor)
1736                .map(str::to_string)
1737        {
1738            drop(self.search(&new_query, Some(self.search_options), false, window, cx));
1739            return;
1740        }
1741
1742        if let Some(new_query) = self
1743            .search_history
1744            .previous(&mut self.search_history_cursor)
1745            .map(str::to_string)
1746        {
1747            drop(self.search(&new_query, Some(self.search_options), false, window, cx));
1748        }
1749    }
1750
1751    fn focus(&self, handle: &gpui::FocusHandle, window: &mut Window, cx: &mut App) {
1752        window.invalidate_character_coordinates();
1753        window.focus(handle, cx);
1754    }
1755
1756    fn toggle_replace(&mut self, _: &ToggleReplace, window: &mut Window, cx: &mut Context<Self>) {
1757        if self.active_searchable_item.is_some() {
1758            self.replace_enabled = !self.replace_enabled;
1759            let handle = if self.replace_enabled {
1760                self.replacement_editor.focus_handle(cx)
1761            } else {
1762                self.query_editor.focus_handle(cx)
1763            };
1764            self.focus(&handle, window, cx);
1765            cx.notify();
1766        }
1767    }
1768
1769    fn replace_next(&mut self, _: &ReplaceNext, window: &mut Window, cx: &mut Context<Self>) {
1770        let mut should_propagate = true;
1771        if !self.dismissed
1772            && self.active_search.is_some()
1773            && let Some(searchable_item) = self.active_searchable_item.as_ref()
1774            && let Some(query) = self.active_search.as_ref()
1775            && let Some((matches, token)) = self
1776                .searchable_items_with_matches
1777                .get(&searchable_item.downgrade())
1778        {
1779            if let Some(active_index) = self.active_match_index {
1780                let query = query
1781                    .as_ref()
1782                    .clone()
1783                    .with_replacement(self.replacement(cx));
1784                searchable_item.replace(matches.at(active_index), &query, *token, window, cx);
1785                self.select_next_match(&SelectNextMatch, window, cx);
1786            }
1787            should_propagate = false;
1788        }
1789        if !should_propagate {
1790            cx.stop_propagation();
1791        }
1792    }
1793
1794    pub fn replace_all(&mut self, _: &ReplaceAll, window: &mut Window, cx: &mut Context<Self>) {
1795        if !self.dismissed
1796            && self.active_search.is_some()
1797            && let Some(searchable_item) = self.active_searchable_item.as_ref()
1798            && let Some(query) = self.active_search.as_ref()
1799            && let Some((matches, token)) = self
1800                .searchable_items_with_matches
1801                .get(&searchable_item.downgrade())
1802        {
1803            let query = query
1804                .as_ref()
1805                .clone()
1806                .with_replacement(self.replacement(cx));
1807            searchable_item.replace_all(&mut matches.iter(), &query, *token, window, cx);
1808        }
1809    }
1810
1811    pub fn match_exists(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
1812        self.update_match_index(window, cx);
1813        self.active_match_index.is_some()
1814    }
1815
1816    pub fn should_use_smartcase_search(&mut self, cx: &mut Context<Self>) -> bool {
1817        EditorSettings::get_global(cx).use_smartcase_search
1818    }
1819
1820    pub fn is_contains_uppercase(&mut self, str: &String) -> bool {
1821        str.chars().any(|c| c.is_uppercase())
1822    }
1823
1824    fn smartcase(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1825        if self.should_use_smartcase_search(cx) {
1826            let query = self.query(cx);
1827            if !query.is_empty() {
1828                let is_case = self.is_contains_uppercase(&query);
1829                if self.has_search_option(SearchOptions::CASE_SENSITIVE) != is_case {
1830                    self.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
1831                }
1832            }
1833        }
1834    }
1835
1836    fn adjust_query_regex_language(&self, cx: &mut App) {
1837        let enable = self.search_options.contains(SearchOptions::REGEX);
1838        let query_buffer = self
1839            .query_editor
1840            .read(cx)
1841            .buffer()
1842            .read(cx)
1843            .as_singleton()
1844            .expect("query editor should be backed by a singleton buffer");
1845
1846        if enable {
1847            if let Some(regex_language) = self.regex_language.clone() {
1848                query_buffer.update(cx, |query_buffer, cx| {
1849                    query_buffer.set_language(Some(regex_language), cx);
1850                })
1851            }
1852        } else {
1853            query_buffer.update(cx, |query_buffer, cx| {
1854                query_buffer.set_language(None, cx);
1855            })
1856        }
1857    }
1858
1859    /// Updates the searchable item's case sensitivity option to match the
1860    /// search bar's current case sensitivity setting. This ensures that
1861    /// editor's `select_next`/ `select_previous` operations respect the buffer
1862    /// search bar's search options.
1863    ///
1864    /// Clears the case sensitivity when the search bar is dismissed so that
1865    /// only the editor's settings are respected.
1866    fn sync_select_next_case_sensitivity(&self, cx: &mut Context<Self>) {
1867        let case_sensitive = match self.dismissed {
1868            true => None,
1869            false => Some(self.search_options.contains(SearchOptions::CASE_SENSITIVE)),
1870        };
1871
1872        if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
1873            active_searchable_item.set_search_is_case_sensitive(case_sensitive, cx);
1874        }
1875    }
1876}
1877
1878#[cfg(test)]
1879mod tests {
1880    use std::ops::Range;
1881
1882    use super::*;
1883    use editor::{
1884        DisplayPoint, Editor, ExcerptRange, MultiBuffer, SearchSettings, SelectionEffects,
1885        display_map::DisplayRow, test::editor_test_context::EditorTestContext,
1886    };
1887    use gpui::{Hsla, TestAppContext, UpdateGlobal, VisualTestContext};
1888    use language::{Buffer, Point};
1889    use settings::{SearchSettingsContent, SettingsStore};
1890    use smol::stream::StreamExt as _;
1891    use unindent::Unindent as _;
1892    use util_macros::perf;
1893
1894    fn init_globals(cx: &mut TestAppContext) {
1895        cx.update(|cx| {
1896            let store = settings::SettingsStore::test(cx);
1897            cx.set_global(store);
1898            editor::init(cx);
1899
1900            theme::init(theme::LoadThemes::JustBase, cx);
1901            crate::init(cx);
1902        });
1903    }
1904
1905    fn init_multibuffer_test(
1906        cx: &mut TestAppContext,
1907    ) -> (
1908        Entity<Editor>,
1909        Entity<BufferSearchBar>,
1910        &mut VisualTestContext,
1911    ) {
1912        init_globals(cx);
1913
1914        let buffer1 = cx.new(|cx| {
1915            Buffer::local(
1916                            r#"
1917                            A regular expression (shortened as regex or regexp;[1] also referred to as
1918                            rational expression[2][3]) is a sequence of characters that specifies a search
1919                            pattern in text. Usually such patterns are used by string-searching algorithms
1920                            for "find" or "find and replace" operations on strings, or for input validation.
1921                            "#
1922                            .unindent(),
1923                            cx,
1924                        )
1925        });
1926
1927        let buffer2 = cx.new(|cx| {
1928            Buffer::local(
1929                r#"
1930                            Some Additional text with the term regular expression in it.
1931                            There two lines.
1932                            "#
1933                .unindent(),
1934                cx,
1935            )
1936        });
1937
1938        let multibuffer = cx.new(|cx| {
1939            let mut buffer = MultiBuffer::new(language::Capability::ReadWrite);
1940
1941            //[ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))]
1942            buffer.push_excerpts(
1943                buffer1,
1944                [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
1945                cx,
1946            );
1947            buffer.push_excerpts(
1948                buffer2,
1949                [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
1950                cx,
1951            );
1952
1953            buffer
1954        });
1955        let mut editor = None;
1956        let window = cx.add_window(|window, cx| {
1957            let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
1958                "keymaps/default-macos.json",
1959                cx,
1960            )
1961            .unwrap();
1962            cx.bind_keys(default_key_bindings);
1963            editor =
1964                Some(cx.new(|cx| Editor::for_multibuffer(multibuffer.clone(), None, window, cx)));
1965
1966            let mut search_bar = BufferSearchBar::new(None, window, cx);
1967            search_bar.set_active_pane_item(Some(&editor.clone().unwrap()), window, cx);
1968            search_bar.show(window, cx);
1969            search_bar
1970        });
1971        let search_bar = window.root(cx).unwrap();
1972
1973        let cx = VisualTestContext::from_window(*window, cx).into_mut();
1974
1975        (editor.unwrap(), search_bar, cx)
1976    }
1977
1978    fn init_test(
1979        cx: &mut TestAppContext,
1980    ) -> (
1981        Entity<Editor>,
1982        Entity<BufferSearchBar>,
1983        &mut VisualTestContext,
1984    ) {
1985        init_globals(cx);
1986        let buffer = cx.new(|cx| {
1987            Buffer::local(
1988                r#"
1989                A regular expression (shortened as regex or regexp;[1] also referred to as
1990                rational expression[2][3]) is a sequence of characters that specifies a search
1991                pattern in text. Usually such patterns are used by string-searching algorithms
1992                for "find" or "find and replace" operations on strings, or for input validation.
1993                "#
1994                .unindent(),
1995                cx,
1996            )
1997        });
1998        let mut editor = None;
1999        let window = cx.add_window(|window, cx| {
2000            let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
2001                "keymaps/default-macos.json",
2002                cx,
2003            )
2004            .unwrap();
2005            cx.bind_keys(default_key_bindings);
2006            editor = Some(cx.new(|cx| Editor::for_buffer(buffer.clone(), None, window, cx)));
2007            let mut search_bar = BufferSearchBar::new(None, window, cx);
2008            search_bar.set_active_pane_item(Some(&editor.clone().unwrap()), window, cx);
2009            search_bar.show(window, cx);
2010            search_bar
2011        });
2012        let search_bar = window.root(cx).unwrap();
2013
2014        let cx = VisualTestContext::from_window(*window, cx).into_mut();
2015
2016        (editor.unwrap(), search_bar, cx)
2017    }
2018
2019    #[perf]
2020    #[gpui::test]
2021    async fn test_search_simple(cx: &mut TestAppContext) {
2022        let (editor, search_bar, cx) = init_test(cx);
2023        let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
2024            background_highlights
2025                .into_iter()
2026                .map(|(range, _)| range)
2027                .collect::<Vec<_>>()
2028        };
2029        // Search for a string that appears with different casing.
2030        // By default, search is case-insensitive.
2031        search_bar
2032            .update_in(cx, |search_bar, window, cx| {
2033                search_bar.search("us", None, true, window, cx)
2034            })
2035            .await
2036            .unwrap();
2037        editor.update_in(cx, |editor, window, cx| {
2038            assert_eq!(
2039                display_points_of(editor.all_text_background_highlights(window, cx)),
2040                &[
2041                    DisplayPoint::new(DisplayRow(2), 17)..DisplayPoint::new(DisplayRow(2), 19),
2042                    DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),
2043                ]
2044            );
2045        });
2046
2047        // Switch to a case sensitive search.
2048        search_bar.update_in(cx, |search_bar, window, cx| {
2049            search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
2050        });
2051        let mut editor_notifications = cx.notifications(&editor);
2052        editor_notifications.next().await;
2053        editor.update_in(cx, |editor, window, cx| {
2054            assert_eq!(
2055                display_points_of(editor.all_text_background_highlights(window, cx)),
2056                &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
2057            );
2058        });
2059
2060        // Search for a string that appears both as a whole word and
2061        // within other words. By default, all results are found.
2062        search_bar
2063            .update_in(cx, |search_bar, window, cx| {
2064                search_bar.search("or", None, true, window, cx)
2065            })
2066            .await
2067            .unwrap();
2068        editor.update_in(cx, |editor, window, cx| {
2069            assert_eq!(
2070                display_points_of(editor.all_text_background_highlights(window, cx)),
2071                &[
2072                    DisplayPoint::new(DisplayRow(0), 24)..DisplayPoint::new(DisplayRow(0), 26),
2073                    DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
2074                    DisplayPoint::new(DisplayRow(2), 71)..DisplayPoint::new(DisplayRow(2), 73),
2075                    DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 3),
2076                    DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
2077                    DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
2078                    DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 62),
2079                ]
2080            );
2081        });
2082
2083        // Switch to a whole word search.
2084        search_bar.update_in(cx, |search_bar, window, cx| {
2085            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
2086        });
2087        let mut editor_notifications = cx.notifications(&editor);
2088        editor_notifications.next().await;
2089        editor.update_in(cx, |editor, window, cx| {
2090            assert_eq!(
2091                display_points_of(editor.all_text_background_highlights(window, cx)),
2092                &[
2093                    DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
2094                    DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
2095                    DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
2096                ]
2097            );
2098        });
2099
2100        editor.update_in(cx, |editor, window, cx| {
2101            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2102                s.select_display_ranges([
2103                    DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2104                ])
2105            });
2106        });
2107        search_bar.update_in(cx, |search_bar, window, cx| {
2108            assert_eq!(search_bar.active_match_index, Some(0));
2109            search_bar.select_next_match(&SelectNextMatch, window, cx);
2110            assert_eq!(
2111                editor.update(cx, |editor, cx| editor
2112                    .selections
2113                    .display_ranges(&editor.display_snapshot(cx))),
2114                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
2115            );
2116        });
2117        search_bar.read_with(cx, |search_bar, _| {
2118            assert_eq!(search_bar.active_match_index, Some(0));
2119        });
2120
2121        search_bar.update_in(cx, |search_bar, window, cx| {
2122            search_bar.select_next_match(&SelectNextMatch, window, cx);
2123            assert_eq!(
2124                editor.update(cx, |editor, cx| editor
2125                    .selections
2126                    .display_ranges(&editor.display_snapshot(cx))),
2127                [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
2128            );
2129        });
2130        search_bar.read_with(cx, |search_bar, _| {
2131            assert_eq!(search_bar.active_match_index, Some(1));
2132        });
2133
2134        search_bar.update_in(cx, |search_bar, window, cx| {
2135            search_bar.select_next_match(&SelectNextMatch, window, cx);
2136            assert_eq!(
2137                editor.update(cx, |editor, cx| editor
2138                    .selections
2139                    .display_ranges(&editor.display_snapshot(cx))),
2140                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
2141            );
2142        });
2143        search_bar.read_with(cx, |search_bar, _| {
2144            assert_eq!(search_bar.active_match_index, Some(2));
2145        });
2146
2147        search_bar.update_in(cx, |search_bar, window, cx| {
2148            search_bar.select_next_match(&SelectNextMatch, window, cx);
2149            assert_eq!(
2150                editor.update(cx, |editor, cx| editor
2151                    .selections
2152                    .display_ranges(&editor.display_snapshot(cx))),
2153                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
2154            );
2155        });
2156        search_bar.read_with(cx, |search_bar, _| {
2157            assert_eq!(search_bar.active_match_index, Some(0));
2158        });
2159
2160        search_bar.update_in(cx, |search_bar, window, cx| {
2161            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2162            assert_eq!(
2163                editor.update(cx, |editor, cx| editor
2164                    .selections
2165                    .display_ranges(&editor.display_snapshot(cx))),
2166                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
2167            );
2168        });
2169        search_bar.read_with(cx, |search_bar, _| {
2170            assert_eq!(search_bar.active_match_index, Some(2));
2171        });
2172
2173        search_bar.update_in(cx, |search_bar, window, cx| {
2174            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2175            assert_eq!(
2176                editor.update(cx, |editor, cx| editor
2177                    .selections
2178                    .display_ranges(&editor.display_snapshot(cx))),
2179                [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
2180            );
2181        });
2182        search_bar.read_with(cx, |search_bar, _| {
2183            assert_eq!(search_bar.active_match_index, Some(1));
2184        });
2185
2186        search_bar.update_in(cx, |search_bar, window, cx| {
2187            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2188            assert_eq!(
2189                editor.update(cx, |editor, cx| editor
2190                    .selections
2191                    .display_ranges(&editor.display_snapshot(cx))),
2192                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
2193            );
2194        });
2195        search_bar.read_with(cx, |search_bar, _| {
2196            assert_eq!(search_bar.active_match_index, Some(0));
2197        });
2198
2199        // Park the cursor in between matches and ensure that going to the previous match selects
2200        // the closest match to the left.
2201        editor.update_in(cx, |editor, window, cx| {
2202            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2203                s.select_display_ranges([
2204                    DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
2205                ])
2206            });
2207        });
2208        search_bar.update_in(cx, |search_bar, window, cx| {
2209            assert_eq!(search_bar.active_match_index, Some(1));
2210            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2211            assert_eq!(
2212                editor.update(cx, |editor, cx| editor
2213                    .selections
2214                    .display_ranges(&editor.display_snapshot(cx))),
2215                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
2216            );
2217        });
2218        search_bar.read_with(cx, |search_bar, _| {
2219            assert_eq!(search_bar.active_match_index, Some(0));
2220        });
2221
2222        // Park the cursor in between matches and ensure that going to the next match selects the
2223        // closest match to the right.
2224        editor.update_in(cx, |editor, window, cx| {
2225            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2226                s.select_display_ranges([
2227                    DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
2228                ])
2229            });
2230        });
2231        search_bar.update_in(cx, |search_bar, window, cx| {
2232            assert_eq!(search_bar.active_match_index, Some(1));
2233            search_bar.select_next_match(&SelectNextMatch, window, cx);
2234            assert_eq!(
2235                editor.update(cx, |editor, cx| editor
2236                    .selections
2237                    .display_ranges(&editor.display_snapshot(cx))),
2238                [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
2239            );
2240        });
2241        search_bar.read_with(cx, |search_bar, _| {
2242            assert_eq!(search_bar.active_match_index, Some(1));
2243        });
2244
2245        // Park the cursor after the last match and ensure that going to the previous match selects
2246        // the last match.
2247        editor.update_in(cx, |editor, window, cx| {
2248            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2249                s.select_display_ranges([
2250                    DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
2251                ])
2252            });
2253        });
2254        search_bar.update_in(cx, |search_bar, window, cx| {
2255            assert_eq!(search_bar.active_match_index, Some(2));
2256            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2257            assert_eq!(
2258                editor.update(cx, |editor, cx| editor
2259                    .selections
2260                    .display_ranges(&editor.display_snapshot(cx))),
2261                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
2262            );
2263        });
2264        search_bar.read_with(cx, |search_bar, _| {
2265            assert_eq!(search_bar.active_match_index, Some(2));
2266        });
2267
2268        // Park the cursor after the last match and ensure that going to the next match selects the
2269        // first match.
2270        editor.update_in(cx, |editor, window, cx| {
2271            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2272                s.select_display_ranges([
2273                    DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
2274                ])
2275            });
2276        });
2277        search_bar.update_in(cx, |search_bar, window, cx| {
2278            assert_eq!(search_bar.active_match_index, Some(2));
2279            search_bar.select_next_match(&SelectNextMatch, window, cx);
2280            assert_eq!(
2281                editor.update(cx, |editor, cx| editor
2282                    .selections
2283                    .display_ranges(&editor.display_snapshot(cx))),
2284                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
2285            );
2286        });
2287        search_bar.read_with(cx, |search_bar, _| {
2288            assert_eq!(search_bar.active_match_index, Some(0));
2289        });
2290
2291        // Park the cursor before the first match and ensure that going to the previous match
2292        // selects the last match.
2293        editor.update_in(cx, |editor, window, cx| {
2294            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2295                s.select_display_ranges([
2296                    DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2297                ])
2298            });
2299        });
2300        search_bar.update_in(cx, |search_bar, window, cx| {
2301            assert_eq!(search_bar.active_match_index, Some(0));
2302            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2303            assert_eq!(
2304                editor.update(cx, |editor, cx| editor
2305                    .selections
2306                    .display_ranges(&editor.display_snapshot(cx))),
2307                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
2308            );
2309        });
2310        search_bar.read_with(cx, |search_bar, _| {
2311            assert_eq!(search_bar.active_match_index, Some(2));
2312        });
2313    }
2314
2315    fn display_points_of(
2316        background_highlights: Vec<(Range<DisplayPoint>, Hsla)>,
2317    ) -> Vec<Range<DisplayPoint>> {
2318        background_highlights
2319            .into_iter()
2320            .map(|(range, _)| range)
2321            .collect::<Vec<_>>()
2322    }
2323
2324    #[perf]
2325    #[gpui::test]
2326    async fn test_search_option_handling(cx: &mut TestAppContext) {
2327        let (editor, search_bar, cx) = init_test(cx);
2328
2329        // show with options should make current search case sensitive
2330        search_bar
2331            .update_in(cx, |search_bar, window, cx| {
2332                search_bar.show(window, cx);
2333                search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), true, window, cx)
2334            })
2335            .await
2336            .unwrap();
2337        editor.update_in(cx, |editor, window, cx| {
2338            assert_eq!(
2339                display_points_of(editor.all_text_background_highlights(window, cx)),
2340                &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
2341            );
2342        });
2343
2344        // search_suggested should restore default options
2345        search_bar.update_in(cx, |search_bar, window, cx| {
2346            search_bar.search_suggested(window, cx);
2347            assert_eq!(search_bar.search_options, SearchOptions::NONE)
2348        });
2349
2350        // toggling a search option should update the defaults
2351        search_bar
2352            .update_in(cx, |search_bar, window, cx| {
2353                search_bar.search(
2354                    "regex",
2355                    Some(SearchOptions::CASE_SENSITIVE),
2356                    true,
2357                    window,
2358                    cx,
2359                )
2360            })
2361            .await
2362            .unwrap();
2363        search_bar.update_in(cx, |search_bar, window, cx| {
2364            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx)
2365        });
2366        let mut editor_notifications = cx.notifications(&editor);
2367        editor_notifications.next().await;
2368        editor.update_in(cx, |editor, window, cx| {
2369            assert_eq!(
2370                display_points_of(editor.all_text_background_highlights(window, cx)),
2371                &[DisplayPoint::new(DisplayRow(0), 35)..DisplayPoint::new(DisplayRow(0), 40),]
2372            );
2373        });
2374
2375        // defaults should still include whole word
2376        search_bar.update_in(cx, |search_bar, window, cx| {
2377            search_bar.search_suggested(window, cx);
2378            assert_eq!(
2379                search_bar.search_options,
2380                SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD
2381            )
2382        });
2383    }
2384
2385    #[perf]
2386    #[gpui::test]
2387    async fn test_search_select_all_matches(cx: &mut TestAppContext) {
2388        init_globals(cx);
2389        let buffer_text = r#"
2390        A regular expression (shortened as regex or regexp;[1] also referred to as
2391        rational expression[2][3]) is a sequence of characters that specifies a search
2392        pattern in text. Usually such patterns are used by string-searching algorithms
2393        for "find" or "find and replace" operations on strings, or for input validation.
2394        "#
2395        .unindent();
2396        let expected_query_matches_count = buffer_text
2397            .chars()
2398            .filter(|c| c.eq_ignore_ascii_case(&'a'))
2399            .count();
2400        assert!(
2401            expected_query_matches_count > 1,
2402            "Should pick a query with multiple results"
2403        );
2404        let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
2405        let window = cx.add_window(|_, _| gpui::Empty);
2406
2407        let editor = window.build_entity(cx, |window, cx| {
2408            Editor::for_buffer(buffer.clone(), None, window, cx)
2409        });
2410
2411        let search_bar = window.build_entity(cx, |window, cx| {
2412            let mut search_bar = BufferSearchBar::new(None, window, cx);
2413            search_bar.set_active_pane_item(Some(&editor), window, cx);
2414            search_bar.show(window, cx);
2415            search_bar
2416        });
2417
2418        window
2419            .update(cx, |_, window, cx| {
2420                search_bar.update(cx, |search_bar, cx| {
2421                    search_bar.search("a", None, true, window, cx)
2422                })
2423            })
2424            .unwrap()
2425            .await
2426            .unwrap();
2427        let initial_selections = window
2428            .update(cx, |_, window, cx| {
2429                search_bar.update(cx, |search_bar, cx| {
2430                    let handle = search_bar.query_editor.focus_handle(cx);
2431                    window.focus(&handle, cx);
2432                    search_bar.activate_current_match(window, cx);
2433                });
2434                assert!(
2435                    !editor.read(cx).is_focused(window),
2436                    "Initially, the editor should not be focused"
2437                );
2438                let initial_selections = editor.update(cx, |editor, cx| {
2439                    let initial_selections = editor.selections.display_ranges(&editor.display_snapshot(cx));
2440                    assert_eq!(
2441                        initial_selections.len(), 1,
2442                        "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
2443                    );
2444                    initial_selections
2445                });
2446                search_bar.update(cx, |search_bar, cx| {
2447                    assert_eq!(search_bar.active_match_index, Some(0));
2448                    let handle = search_bar.query_editor.focus_handle(cx);
2449                    window.focus(&handle, cx);
2450                    search_bar.select_all_matches(&SelectAllMatches, window, cx);
2451                });
2452                assert!(
2453                    editor.read(cx).is_focused(window),
2454                    "Should focus editor after successful SelectAllMatches"
2455                );
2456                search_bar.update(cx, |search_bar, cx| {
2457                    let all_selections =
2458                        editor.update(cx, |editor, cx| editor.selections.display_ranges(&editor.display_snapshot(cx)));
2459                    assert_eq!(
2460                        all_selections.len(),
2461                        expected_query_matches_count,
2462                        "Should select all `a` characters in the buffer, but got: {all_selections:?}"
2463                    );
2464                    assert_eq!(
2465                        search_bar.active_match_index,
2466                        Some(0),
2467                        "Match index should not change after selecting all matches"
2468                    );
2469                });
2470
2471                search_bar.update(cx, |this, cx| this.select_next_match(&SelectNextMatch, window, cx));
2472                initial_selections
2473            }).unwrap();
2474
2475        window
2476            .update(cx, |_, window, cx| {
2477                assert!(
2478                    editor.read(cx).is_focused(window),
2479                    "Should still have editor focused after SelectNextMatch"
2480                );
2481                search_bar.update(cx, |search_bar, cx| {
2482                    let all_selections = editor.update(cx, |editor, cx| {
2483                        editor
2484                            .selections
2485                            .display_ranges(&editor.display_snapshot(cx))
2486                    });
2487                    assert_eq!(
2488                        all_selections.len(),
2489                        1,
2490                        "On next match, should deselect items and select the next match"
2491                    );
2492                    assert_ne!(
2493                        all_selections, initial_selections,
2494                        "Next match should be different from the first selection"
2495                    );
2496                    assert_eq!(
2497                        search_bar.active_match_index,
2498                        Some(1),
2499                        "Match index should be updated to the next one"
2500                    );
2501                    let handle = search_bar.query_editor.focus_handle(cx);
2502                    window.focus(&handle, cx);
2503                    search_bar.select_all_matches(&SelectAllMatches, window, cx);
2504                });
2505            })
2506            .unwrap();
2507        window
2508            .update(cx, |_, window, cx| {
2509                assert!(
2510                    editor.read(cx).is_focused(window),
2511                    "Should focus editor after successful SelectAllMatches"
2512                );
2513                search_bar.update(cx, |search_bar, cx| {
2514                    let all_selections =
2515                        editor.update(cx, |editor, cx| editor.selections.display_ranges(&editor.display_snapshot(cx)));
2516                    assert_eq!(
2517                    all_selections.len(),
2518                    expected_query_matches_count,
2519                    "Should select all `a` characters in the buffer, but got: {all_selections:?}"
2520                );
2521                    assert_eq!(
2522                        search_bar.active_match_index,
2523                        Some(1),
2524                        "Match index should not change after selecting all matches"
2525                    );
2526                });
2527                search_bar.update(cx, |search_bar, cx| {
2528                    search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2529                });
2530            })
2531            .unwrap();
2532        let last_match_selections = window
2533            .update(cx, |_, window, cx| {
2534                assert!(
2535                    editor.read(cx).is_focused(window),
2536                    "Should still have editor focused after SelectPreviousMatch"
2537                );
2538
2539                search_bar.update(cx, |search_bar, cx| {
2540                    let all_selections = editor.update(cx, |editor, cx| {
2541                        editor
2542                            .selections
2543                            .display_ranges(&editor.display_snapshot(cx))
2544                    });
2545                    assert_eq!(
2546                        all_selections.len(),
2547                        1,
2548                        "On previous match, should deselect items and select the previous item"
2549                    );
2550                    assert_eq!(
2551                        all_selections, initial_selections,
2552                        "Previous match should be the same as the first selection"
2553                    );
2554                    assert_eq!(
2555                        search_bar.active_match_index,
2556                        Some(0),
2557                        "Match index should be updated to the previous one"
2558                    );
2559                    all_selections
2560                })
2561            })
2562            .unwrap();
2563
2564        window
2565            .update(cx, |_, window, cx| {
2566                search_bar.update(cx, |search_bar, cx| {
2567                    let handle = search_bar.query_editor.focus_handle(cx);
2568                    window.focus(&handle, cx);
2569                    search_bar.search("abas_nonexistent_match", None, true, window, cx)
2570                })
2571            })
2572            .unwrap()
2573            .await
2574            .unwrap();
2575        window
2576            .update(cx, |_, window, cx| {
2577                search_bar.update(cx, |search_bar, cx| {
2578                    search_bar.select_all_matches(&SelectAllMatches, window, cx);
2579                });
2580                assert!(
2581                    editor.update(cx, |this, _cx| !this.is_focused(window)),
2582                    "Should not switch focus to editor if SelectAllMatches does not find any matches"
2583                );
2584                search_bar.update(cx, |search_bar, cx| {
2585                    let all_selections =
2586                        editor.update(cx, |editor, cx| editor.selections.display_ranges(&editor.display_snapshot(cx)));
2587                    assert_eq!(
2588                        all_selections, last_match_selections,
2589                        "Should not select anything new if there are no matches"
2590                    );
2591                    assert!(
2592                        search_bar.active_match_index.is_none(),
2593                        "For no matches, there should be no active match index"
2594                    );
2595                });
2596            })
2597            .unwrap();
2598    }
2599
2600    #[perf]
2601    #[gpui::test]
2602    async fn test_search_query_with_match_whole_word(cx: &mut TestAppContext) {
2603        init_globals(cx);
2604        let buffer_text = r#"
2605        self.buffer.update(cx, |buffer, cx| {
2606            buffer.edit(
2607                edits,
2608                Some(AutoindentMode::Block {
2609                    original_indent_columns,
2610                }),
2611                cx,
2612            )
2613        });
2614
2615        this.buffer.update(cx, |buffer, cx| {
2616            buffer.edit([(end_of_line..start_of_next_line, replace)], None, cx)
2617        });
2618        "#
2619        .unindent();
2620        let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
2621        let cx = cx.add_empty_window();
2622
2623        let editor =
2624            cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
2625
2626        let search_bar = cx.new_window_entity(|window, cx| {
2627            let mut search_bar = BufferSearchBar::new(None, window, cx);
2628            search_bar.set_active_pane_item(Some(&editor), window, cx);
2629            search_bar.show(window, cx);
2630            search_bar
2631        });
2632
2633        search_bar
2634            .update_in(cx, |search_bar, window, cx| {
2635                search_bar.search(
2636                    "edit\\(",
2637                    Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
2638                    true,
2639                    window,
2640                    cx,
2641                )
2642            })
2643            .await
2644            .unwrap();
2645
2646        search_bar.update_in(cx, |search_bar, window, cx| {
2647            search_bar.select_all_matches(&SelectAllMatches, window, cx);
2648        });
2649        search_bar.update(cx, |_, cx| {
2650            let all_selections = editor.update(cx, |editor, cx| {
2651                editor
2652                    .selections
2653                    .display_ranges(&editor.display_snapshot(cx))
2654            });
2655            assert_eq!(
2656                all_selections.len(),
2657                2,
2658                "Should select all `edit(` in the buffer, but got: {all_selections:?}"
2659            );
2660        });
2661
2662        search_bar
2663            .update_in(cx, |search_bar, window, cx| {
2664                search_bar.search(
2665                    "edit(",
2666                    Some(SearchOptions::WHOLE_WORD | SearchOptions::CASE_SENSITIVE),
2667                    true,
2668                    window,
2669                    cx,
2670                )
2671            })
2672            .await
2673            .unwrap();
2674
2675        search_bar.update_in(cx, |search_bar, window, cx| {
2676            search_bar.select_all_matches(&SelectAllMatches, window, cx);
2677        });
2678        search_bar.update(cx, |_, cx| {
2679            let all_selections = editor.update(cx, |editor, cx| {
2680                editor
2681                    .selections
2682                    .display_ranges(&editor.display_snapshot(cx))
2683            });
2684            assert_eq!(
2685                all_selections.len(),
2686                2,
2687                "Should select all `edit(` in the buffer, but got: {all_selections:?}"
2688            );
2689        });
2690    }
2691
2692    #[perf]
2693    #[gpui::test]
2694    async fn test_search_query_history(cx: &mut TestAppContext) {
2695        let (_editor, search_bar, cx) = init_test(cx);
2696
2697        // Add 3 search items into the history.
2698        search_bar
2699            .update_in(cx, |search_bar, window, cx| {
2700                search_bar.search("a", None, true, window, cx)
2701            })
2702            .await
2703            .unwrap();
2704        search_bar
2705            .update_in(cx, |search_bar, window, cx| {
2706                search_bar.search("b", None, true, window, cx)
2707            })
2708            .await
2709            .unwrap();
2710        search_bar
2711            .update_in(cx, |search_bar, window, cx| {
2712                search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), true, window, cx)
2713            })
2714            .await
2715            .unwrap();
2716        // Ensure that the latest search is active.
2717        search_bar.update(cx, |search_bar, cx| {
2718            assert_eq!(search_bar.query(cx), "c");
2719            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2720        });
2721
2722        // Next history query after the latest should set the query to the empty string.
2723        search_bar.update_in(cx, |search_bar, window, cx| {
2724            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2725        });
2726        cx.background_executor.run_until_parked();
2727        search_bar.update(cx, |search_bar, cx| {
2728            assert_eq!(search_bar.query(cx), "");
2729            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2730        });
2731        search_bar.update_in(cx, |search_bar, window, cx| {
2732            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2733        });
2734        cx.background_executor.run_until_parked();
2735        search_bar.update(cx, |search_bar, cx| {
2736            assert_eq!(search_bar.query(cx), "");
2737            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2738        });
2739
2740        // First previous query for empty current query should set the query to the latest.
2741        search_bar.update_in(cx, |search_bar, window, cx| {
2742            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2743        });
2744        cx.background_executor.run_until_parked();
2745        search_bar.update(cx, |search_bar, cx| {
2746            assert_eq!(search_bar.query(cx), "c");
2747            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2748        });
2749
2750        // Further previous items should go over the history in reverse order.
2751        search_bar.update_in(cx, |search_bar, window, cx| {
2752            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2753        });
2754        cx.background_executor.run_until_parked();
2755        search_bar.update(cx, |search_bar, cx| {
2756            assert_eq!(search_bar.query(cx), "b");
2757            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2758        });
2759
2760        // Previous items should never go behind the first history item.
2761        search_bar.update_in(cx, |search_bar, window, cx| {
2762            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2763        });
2764        cx.background_executor.run_until_parked();
2765        search_bar.update(cx, |search_bar, cx| {
2766            assert_eq!(search_bar.query(cx), "a");
2767            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2768        });
2769        search_bar.update_in(cx, |search_bar, window, cx| {
2770            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2771        });
2772        cx.background_executor.run_until_parked();
2773        search_bar.update(cx, |search_bar, cx| {
2774            assert_eq!(search_bar.query(cx), "a");
2775            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2776        });
2777
2778        // Next items should go over the history in the original order.
2779        search_bar.update_in(cx, |search_bar, window, cx| {
2780            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2781        });
2782        cx.background_executor.run_until_parked();
2783        search_bar.update(cx, |search_bar, cx| {
2784            assert_eq!(search_bar.query(cx), "b");
2785            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2786        });
2787
2788        search_bar
2789            .update_in(cx, |search_bar, window, cx| {
2790                search_bar.search("ba", None, true, window, cx)
2791            })
2792            .await
2793            .unwrap();
2794        search_bar.update(cx, |search_bar, cx| {
2795            assert_eq!(search_bar.query(cx), "ba");
2796            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2797        });
2798
2799        // New search input should add another entry to history and move the selection to the end of the history.
2800        search_bar.update_in(cx, |search_bar, window, cx| {
2801            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2802        });
2803        cx.background_executor.run_until_parked();
2804        search_bar.update(cx, |search_bar, cx| {
2805            assert_eq!(search_bar.query(cx), "c");
2806            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2807        });
2808        search_bar.update_in(cx, |search_bar, window, cx| {
2809            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2810        });
2811        cx.background_executor.run_until_parked();
2812        search_bar.update(cx, |search_bar, cx| {
2813            assert_eq!(search_bar.query(cx), "b");
2814            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2815        });
2816        search_bar.update_in(cx, |search_bar, window, cx| {
2817            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2818        });
2819        cx.background_executor.run_until_parked();
2820        search_bar.update(cx, |search_bar, cx| {
2821            assert_eq!(search_bar.query(cx), "c");
2822            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2823        });
2824        search_bar.update_in(cx, |search_bar, window, cx| {
2825            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2826        });
2827        cx.background_executor.run_until_parked();
2828        search_bar.update(cx, |search_bar, cx| {
2829            assert_eq!(search_bar.query(cx), "ba");
2830            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2831        });
2832        search_bar.update_in(cx, |search_bar, window, cx| {
2833            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2834        });
2835        cx.background_executor.run_until_parked();
2836        search_bar.update(cx, |search_bar, cx| {
2837            assert_eq!(search_bar.query(cx), "");
2838            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2839        });
2840    }
2841
2842    #[perf]
2843    #[gpui::test]
2844    async fn test_replace_simple(cx: &mut TestAppContext) {
2845        let (editor, search_bar, cx) = init_test(cx);
2846
2847        search_bar
2848            .update_in(cx, |search_bar, window, cx| {
2849                search_bar.search("expression", None, true, window, cx)
2850            })
2851            .await
2852            .unwrap();
2853
2854        search_bar.update_in(cx, |search_bar, window, cx| {
2855            search_bar.replacement_editor.update(cx, |editor, cx| {
2856                // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally.
2857                editor.set_text("expr$1", window, cx);
2858            });
2859            search_bar.replace_all(&ReplaceAll, window, cx)
2860        });
2861        assert_eq!(
2862            editor.read_with(cx, |this, cx| { this.text(cx) }),
2863            r#"
2864        A regular expr$1 (shortened as regex or regexp;[1] also referred to as
2865        rational expr$1[2][3]) is a sequence of characters that specifies a search
2866        pattern in text. Usually such patterns are used by string-searching algorithms
2867        for "find" or "find and replace" operations on strings, or for input validation.
2868        "#
2869            .unindent()
2870        );
2871
2872        // Search for word boundaries and replace just a single one.
2873        search_bar
2874            .update_in(cx, |search_bar, window, cx| {
2875                search_bar.search("or", Some(SearchOptions::WHOLE_WORD), true, window, cx)
2876            })
2877            .await
2878            .unwrap();
2879
2880        search_bar.update_in(cx, |search_bar, window, cx| {
2881            search_bar.replacement_editor.update(cx, |editor, cx| {
2882                editor.set_text("banana", window, cx);
2883            });
2884            search_bar.replace_next(&ReplaceNext, window, cx)
2885        });
2886        // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text.
2887        assert_eq!(
2888            editor.read_with(cx, |this, cx| { this.text(cx) }),
2889            r#"
2890        A regular expr$1 (shortened as regex banana regexp;[1] also referred to as
2891        rational expr$1[2][3]) is a sequence of characters that specifies a search
2892        pattern in text. Usually such patterns are used by string-searching algorithms
2893        for "find" or "find and replace" operations on strings, or for input validation.
2894        "#
2895            .unindent()
2896        );
2897        // Let's turn on regex mode.
2898        search_bar
2899            .update_in(cx, |search_bar, window, cx| {
2900                search_bar.search(
2901                    "\\[([^\\]]+)\\]",
2902                    Some(SearchOptions::REGEX),
2903                    true,
2904                    window,
2905                    cx,
2906                )
2907            })
2908            .await
2909            .unwrap();
2910        search_bar.update_in(cx, |search_bar, window, cx| {
2911            search_bar.replacement_editor.update(cx, |editor, cx| {
2912                editor.set_text("${1}number", window, cx);
2913            });
2914            search_bar.replace_all(&ReplaceAll, window, cx)
2915        });
2916        assert_eq!(
2917            editor.read_with(cx, |this, cx| { this.text(cx) }),
2918            r#"
2919        A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2920        rational expr$12number3number) is a sequence of characters that specifies a search
2921        pattern in text. Usually such patterns are used by string-searching algorithms
2922        for "find" or "find and replace" operations on strings, or for input validation.
2923        "#
2924            .unindent()
2925        );
2926        // Now with a whole-word twist.
2927        search_bar
2928            .update_in(cx, |search_bar, window, cx| {
2929                search_bar.search(
2930                    "a\\w+s",
2931                    Some(SearchOptions::REGEX | SearchOptions::WHOLE_WORD),
2932                    true,
2933                    window,
2934                    cx,
2935                )
2936            })
2937            .await
2938            .unwrap();
2939        search_bar.update_in(cx, |search_bar, window, cx| {
2940            search_bar.replacement_editor.update(cx, |editor, cx| {
2941                editor.set_text("things", window, cx);
2942            });
2943            search_bar.replace_all(&ReplaceAll, window, cx)
2944        });
2945        // The only word affected by this edit should be `algorithms`, even though there's a bunch
2946        // of words in this text that would match this regex if not for WHOLE_WORD.
2947        assert_eq!(
2948            editor.read_with(cx, |this, cx| { this.text(cx) }),
2949            r#"
2950        A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2951        rational expr$12number3number) is a sequence of characters that specifies a search
2952        pattern in text. Usually such patterns are used by string-searching things
2953        for "find" or "find and replace" operations on strings, or for input validation.
2954        "#
2955            .unindent()
2956        );
2957    }
2958
2959    #[gpui::test]
2960    async fn test_replace_focus(cx: &mut TestAppContext) {
2961        let (editor, search_bar, cx) = init_test(cx);
2962
2963        editor.update_in(cx, |editor, window, cx| {
2964            editor.set_text("What a bad day!", window, cx)
2965        });
2966
2967        search_bar
2968            .update_in(cx, |search_bar, window, cx| {
2969                search_bar.search("bad", None, true, window, cx)
2970            })
2971            .await
2972            .unwrap();
2973
2974        // Calling `toggle_replace` in the search bar ensures that the "Replace
2975        // *" buttons are rendered, so we can then simulate clicking the
2976        // buttons.
2977        search_bar.update_in(cx, |search_bar, window, cx| {
2978            search_bar.toggle_replace(&ToggleReplace, window, cx)
2979        });
2980
2981        search_bar.update_in(cx, |search_bar, window, cx| {
2982            search_bar.replacement_editor.update(cx, |editor, cx| {
2983                editor.set_text("great", window, cx);
2984            });
2985        });
2986
2987        // Focus on the editor instead of the search bar, as we want to ensure
2988        // that pressing the "Replace Next Match" button will work, even if the
2989        // search bar is not focused.
2990        cx.focus(&editor);
2991
2992        // We'll not simulate clicking the "Replace Next Match " button, asserting that
2993        // the replacement was done.
2994        let button_bounds = cx
2995            .debug_bounds("ICON-ReplaceNext")
2996            .expect("'Replace Next Match' button should be visible");
2997        cx.simulate_click(button_bounds.center(), gpui::Modifiers::none());
2998
2999        assert_eq!(
3000            editor.read_with(cx, |editor, cx| editor.text(cx)),
3001            "What a great day!"
3002        );
3003    }
3004
3005    struct ReplacementTestParams<'a> {
3006        editor: &'a Entity<Editor>,
3007        search_bar: &'a Entity<BufferSearchBar>,
3008        cx: &'a mut VisualTestContext,
3009        search_text: &'static str,
3010        search_options: Option<SearchOptions>,
3011        replacement_text: &'static str,
3012        replace_all: bool,
3013        expected_text: String,
3014    }
3015
3016    async fn run_replacement_test(options: ReplacementTestParams<'_>) {
3017        options
3018            .search_bar
3019            .update_in(options.cx, |search_bar, window, cx| {
3020                if let Some(options) = options.search_options {
3021                    search_bar.set_search_options(options, cx);
3022                }
3023                search_bar.search(
3024                    options.search_text,
3025                    options.search_options,
3026                    true,
3027                    window,
3028                    cx,
3029                )
3030            })
3031            .await
3032            .unwrap();
3033
3034        options
3035            .search_bar
3036            .update_in(options.cx, |search_bar, window, cx| {
3037                search_bar.replacement_editor.update(cx, |editor, cx| {
3038                    editor.set_text(options.replacement_text, window, cx);
3039                });
3040
3041                if options.replace_all {
3042                    search_bar.replace_all(&ReplaceAll, window, cx)
3043                } else {
3044                    search_bar.replace_next(&ReplaceNext, window, cx)
3045                }
3046            });
3047
3048        assert_eq!(
3049            options
3050                .editor
3051                .read_with(options.cx, |this, cx| { this.text(cx) }),
3052            options.expected_text
3053        );
3054    }
3055
3056    #[perf]
3057    #[gpui::test]
3058    async fn test_replace_special_characters(cx: &mut TestAppContext) {
3059        let (editor, search_bar, cx) = init_test(cx);
3060
3061        run_replacement_test(ReplacementTestParams {
3062            editor: &editor,
3063            search_bar: &search_bar,
3064            cx,
3065            search_text: "expression",
3066            search_options: None,
3067            replacement_text: r"\n",
3068            replace_all: true,
3069            expected_text: r#"
3070            A regular \n (shortened as regex or regexp;[1] also referred to as
3071            rational \n[2][3]) is a sequence of characters that specifies a search
3072            pattern in text. Usually such patterns are used by string-searching algorithms
3073            for "find" or "find and replace" operations on strings, or for input validation.
3074            "#
3075            .unindent(),
3076        })
3077        .await;
3078
3079        run_replacement_test(ReplacementTestParams {
3080            editor: &editor,
3081            search_bar: &search_bar,
3082            cx,
3083            search_text: "or",
3084            search_options: Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
3085            replacement_text: r"\\\n\\\\",
3086            replace_all: false,
3087            expected_text: r#"
3088            A regular \n (shortened as regex \
3089            \\ regexp;[1] also referred to as
3090            rational \n[2][3]) is a sequence of characters that specifies a search
3091            pattern in text. Usually such patterns are used by string-searching algorithms
3092            for "find" or "find and replace" operations on strings, or for input validation.
3093            "#
3094            .unindent(),
3095        })
3096        .await;
3097
3098        run_replacement_test(ReplacementTestParams {
3099            editor: &editor,
3100            search_bar: &search_bar,
3101            cx,
3102            search_text: r"(that|used) ",
3103            search_options: Some(SearchOptions::REGEX),
3104            replacement_text: r"$1\n",
3105            replace_all: true,
3106            expected_text: r#"
3107            A regular \n (shortened as regex \
3108            \\ regexp;[1] also referred to as
3109            rational \n[2][3]) is a sequence of characters that
3110            specifies a search
3111            pattern in text. Usually such patterns are used
3112            by string-searching algorithms
3113            for "find" or "find and replace" operations on strings, or for input validation.
3114            "#
3115            .unindent(),
3116        })
3117        .await;
3118    }
3119
3120    #[perf]
3121    #[gpui::test]
3122    async fn test_find_matches_in_selections_singleton_buffer_multiple_selections(
3123        cx: &mut TestAppContext,
3124    ) {
3125        init_globals(cx);
3126        let buffer = cx.new(|cx| {
3127            Buffer::local(
3128                r#"
3129                aaa bbb aaa ccc
3130                aaa bbb aaa ccc
3131                aaa bbb aaa ccc
3132                aaa bbb aaa ccc
3133                aaa bbb aaa ccc
3134                aaa bbb aaa ccc
3135                "#
3136                .unindent(),
3137                cx,
3138            )
3139        });
3140        let cx = cx.add_empty_window();
3141        let editor =
3142            cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
3143
3144        let search_bar = cx.new_window_entity(|window, cx| {
3145            let mut search_bar = BufferSearchBar::new(None, window, cx);
3146            search_bar.set_active_pane_item(Some(&editor), window, cx);
3147            search_bar.show(window, cx);
3148            search_bar
3149        });
3150
3151        editor.update_in(cx, |editor, window, cx| {
3152            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3153                s.select_ranges(vec![Point::new(1, 0)..Point::new(2, 4)])
3154            })
3155        });
3156
3157        search_bar.update_in(cx, |search_bar, window, cx| {
3158            let deploy = Deploy {
3159                focus: true,
3160                replace_enabled: false,
3161                selection_search_enabled: true,
3162            };
3163            search_bar.deploy(&deploy, window, cx);
3164        });
3165
3166        cx.run_until_parked();
3167
3168        search_bar
3169            .update_in(cx, |search_bar, window, cx| {
3170                search_bar.search("aaa", None, true, window, cx)
3171            })
3172            .await
3173            .unwrap();
3174
3175        editor.update(cx, |editor, cx| {
3176            assert_eq!(
3177                editor.search_background_highlights(cx),
3178                &[
3179                    Point::new(1, 0)..Point::new(1, 3),
3180                    Point::new(1, 8)..Point::new(1, 11),
3181                    Point::new(2, 0)..Point::new(2, 3),
3182                ]
3183            );
3184        });
3185    }
3186
3187    #[perf]
3188    #[gpui::test]
3189    async fn test_find_matches_in_selections_multiple_excerpts_buffer_multiple_selections(
3190        cx: &mut TestAppContext,
3191    ) {
3192        init_globals(cx);
3193        let text = r#"
3194            aaa bbb aaa ccc
3195            aaa bbb aaa ccc
3196            aaa bbb aaa ccc
3197            aaa bbb aaa ccc
3198            aaa bbb aaa ccc
3199            aaa bbb aaa ccc
3200
3201            aaa bbb aaa ccc
3202            aaa bbb aaa ccc
3203            aaa bbb aaa ccc
3204            aaa bbb aaa ccc
3205            aaa bbb aaa ccc
3206            aaa bbb aaa ccc
3207            "#
3208        .unindent();
3209
3210        let cx = cx.add_empty_window();
3211        let editor = cx.new_window_entity(|window, cx| {
3212            let multibuffer = MultiBuffer::build_multi(
3213                [
3214                    (
3215                        &text,
3216                        vec![
3217                            Point::new(0, 0)..Point::new(2, 0),
3218                            Point::new(4, 0)..Point::new(5, 0),
3219                        ],
3220                    ),
3221                    (&text, vec![Point::new(9, 0)..Point::new(11, 0)]),
3222                ],
3223                cx,
3224            );
3225            Editor::for_multibuffer(multibuffer, None, window, cx)
3226        });
3227
3228        let search_bar = cx.new_window_entity(|window, cx| {
3229            let mut search_bar = BufferSearchBar::new(None, window, cx);
3230            search_bar.set_active_pane_item(Some(&editor), window, cx);
3231            search_bar.show(window, cx);
3232            search_bar
3233        });
3234
3235        editor.update_in(cx, |editor, window, cx| {
3236            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3237                s.select_ranges(vec![
3238                    Point::new(1, 0)..Point::new(1, 4),
3239                    Point::new(5, 3)..Point::new(6, 4),
3240                ])
3241            })
3242        });
3243
3244        search_bar.update_in(cx, |search_bar, window, cx| {
3245            let deploy = Deploy {
3246                focus: true,
3247                replace_enabled: false,
3248                selection_search_enabled: true,
3249            };
3250            search_bar.deploy(&deploy, window, cx);
3251        });
3252
3253        cx.run_until_parked();
3254
3255        search_bar
3256            .update_in(cx, |search_bar, window, cx| {
3257                search_bar.search("aaa", None, true, window, cx)
3258            })
3259            .await
3260            .unwrap();
3261
3262        editor.update(cx, |editor, cx| {
3263            assert_eq!(
3264                editor.search_background_highlights(cx),
3265                &[
3266                    Point::new(1, 0)..Point::new(1, 3),
3267                    Point::new(5, 8)..Point::new(5, 11),
3268                    Point::new(6, 0)..Point::new(6, 3),
3269                ]
3270            );
3271        });
3272    }
3273
3274    #[perf]
3275    #[gpui::test]
3276    async fn test_hides_and_uses_secondary_when_in_singleton_buffer(cx: &mut TestAppContext) {
3277        let (editor, search_bar, cx) = init_test(cx);
3278
3279        let initial_location = search_bar.update_in(cx, |search_bar, window, cx| {
3280            search_bar.set_active_pane_item(Some(&editor), window, cx)
3281        });
3282
3283        assert_eq!(initial_location, ToolbarItemLocation::Secondary);
3284
3285        let mut events = cx.events(&search_bar);
3286
3287        search_bar.update_in(cx, |search_bar, window, cx| {
3288            search_bar.dismiss(&Dismiss, window, cx);
3289        });
3290
3291        assert_eq!(
3292            events.try_next().unwrap(),
3293            Some(ToolbarItemEvent::ChangeLocation(
3294                ToolbarItemLocation::Hidden
3295            ))
3296        );
3297
3298        search_bar.update_in(cx, |search_bar, window, cx| {
3299            search_bar.show(window, cx);
3300        });
3301
3302        assert_eq!(
3303            events.try_next().unwrap(),
3304            Some(ToolbarItemEvent::ChangeLocation(
3305                ToolbarItemLocation::Secondary
3306            ))
3307        );
3308    }
3309
3310    #[perf]
3311    #[gpui::test]
3312    async fn test_uses_primary_left_when_in_multi_buffer(cx: &mut TestAppContext) {
3313        let (editor, search_bar, cx) = init_multibuffer_test(cx);
3314
3315        let initial_location = search_bar.update_in(cx, |search_bar, window, cx| {
3316            search_bar.set_active_pane_item(Some(&editor), window, cx)
3317        });
3318
3319        assert_eq!(initial_location, ToolbarItemLocation::PrimaryLeft);
3320
3321        let mut events = cx.events(&search_bar);
3322
3323        search_bar.update_in(cx, |search_bar, window, cx| {
3324            search_bar.dismiss(&Dismiss, window, cx);
3325        });
3326
3327        assert_eq!(
3328            events.try_next().unwrap(),
3329            Some(ToolbarItemEvent::ChangeLocation(
3330                ToolbarItemLocation::PrimaryLeft
3331            ))
3332        );
3333
3334        search_bar.update_in(cx, |search_bar, window, cx| {
3335            search_bar.show(window, cx);
3336        });
3337
3338        assert_eq!(
3339            events.try_next().unwrap(),
3340            Some(ToolbarItemEvent::ChangeLocation(
3341                ToolbarItemLocation::PrimaryLeft
3342            ))
3343        );
3344    }
3345
3346    #[perf]
3347    #[gpui::test]
3348    async fn test_hides_and_uses_secondary_when_part_of_project_search(cx: &mut TestAppContext) {
3349        let (editor, search_bar, cx) = init_multibuffer_test(cx);
3350
3351        editor.update(cx, |editor, _| {
3352            editor.set_in_project_search(true);
3353        });
3354
3355        let initial_location = search_bar.update_in(cx, |search_bar, window, cx| {
3356            search_bar.set_active_pane_item(Some(&editor), window, cx)
3357        });
3358
3359        assert_eq!(initial_location, ToolbarItemLocation::Hidden);
3360
3361        let mut events = cx.events(&search_bar);
3362
3363        search_bar.update_in(cx, |search_bar, window, cx| {
3364            search_bar.dismiss(&Dismiss, window, cx);
3365        });
3366
3367        assert_eq!(
3368            events.try_next().unwrap(),
3369            Some(ToolbarItemEvent::ChangeLocation(
3370                ToolbarItemLocation::Hidden
3371            ))
3372        );
3373
3374        search_bar.update_in(cx, |search_bar, window, cx| {
3375            search_bar.show(window, cx);
3376        });
3377
3378        assert_eq!(
3379            events.try_next().unwrap(),
3380            Some(ToolbarItemEvent::ChangeLocation(
3381                ToolbarItemLocation::Secondary
3382            ))
3383        );
3384    }
3385
3386    #[perf]
3387    #[gpui::test]
3388    async fn test_sets_collapsed_when_editor_fold_events_emitted(cx: &mut TestAppContext) {
3389        let (editor, search_bar, cx) = init_multibuffer_test(cx);
3390
3391        search_bar.update_in(cx, |search_bar, window, cx| {
3392            search_bar.set_active_pane_item(Some(&editor), window, cx);
3393        });
3394
3395        editor.update_in(cx, |editor, window, cx| {
3396            editor.fold_all(&FoldAll, window, cx);
3397        });
3398
3399        let is_collapsed = search_bar.read_with(cx, |search_bar, _| search_bar.is_collapsed);
3400
3401        assert!(is_collapsed);
3402
3403        editor.update_in(cx, |editor, window, cx| {
3404            editor.unfold_all(&UnfoldAll, window, cx);
3405        });
3406
3407        let is_collapsed = search_bar.read_with(cx, |search_bar, _| search_bar.is_collapsed);
3408
3409        assert!(!is_collapsed);
3410    }
3411
3412    #[perf]
3413    #[gpui::test]
3414    async fn test_search_options_changes(cx: &mut TestAppContext) {
3415        let (_editor, search_bar, cx) = init_test(cx);
3416        update_search_settings(
3417            SearchSettings {
3418                button: true,
3419                whole_word: false,
3420                case_sensitive: false,
3421                include_ignored: false,
3422                regex: false,
3423                center_on_match: false,
3424            },
3425            cx,
3426        );
3427
3428        let deploy = Deploy {
3429            focus: true,
3430            replace_enabled: false,
3431            selection_search_enabled: true,
3432        };
3433
3434        search_bar.update_in(cx, |search_bar, window, cx| {
3435            assert_eq!(
3436                search_bar.search_options,
3437                SearchOptions::NONE,
3438                "Should have no search options enabled by default"
3439            );
3440            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
3441            assert_eq!(
3442                search_bar.search_options,
3443                SearchOptions::WHOLE_WORD,
3444                "Should enable the option toggled"
3445            );
3446            assert!(
3447                !search_bar.dismissed,
3448                "Search bar should be present and visible"
3449            );
3450            search_bar.deploy(&deploy, window, cx);
3451            assert_eq!(
3452                search_bar.search_options,
3453                SearchOptions::WHOLE_WORD,
3454                "After (re)deploying, the option should still be enabled"
3455            );
3456
3457            search_bar.dismiss(&Dismiss, window, cx);
3458            search_bar.deploy(&deploy, window, cx);
3459            assert_eq!(
3460                search_bar.search_options,
3461                SearchOptions::WHOLE_WORD,
3462                "After hiding and showing the search bar, search options should be preserved"
3463            );
3464
3465            search_bar.toggle_search_option(SearchOptions::REGEX, window, cx);
3466            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
3467            assert_eq!(
3468                search_bar.search_options,
3469                SearchOptions::REGEX,
3470                "Should enable the options toggled"
3471            );
3472            assert!(
3473                !search_bar.dismissed,
3474                "Search bar should be present and visible"
3475            );
3476            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
3477        });
3478
3479        update_search_settings(
3480            SearchSettings {
3481                button: true,
3482                whole_word: false,
3483                case_sensitive: true,
3484                include_ignored: false,
3485                regex: false,
3486                center_on_match: false,
3487            },
3488            cx,
3489        );
3490        search_bar.update_in(cx, |search_bar, window, cx| {
3491            assert_eq!(
3492                search_bar.search_options,
3493                SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
3494                "Should have no search options enabled by default"
3495            );
3496
3497            search_bar.deploy(&deploy, window, cx);
3498            assert_eq!(
3499                search_bar.search_options,
3500                SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
3501                "Toggling a non-dismissed search bar with custom options should not change the default options"
3502            );
3503            search_bar.dismiss(&Dismiss, window, cx);
3504            search_bar.deploy(&deploy, window, cx);
3505            assert_eq!(
3506                search_bar.configured_options,
3507                SearchOptions::CASE_SENSITIVE,
3508                "After a settings update and toggling the search bar, configured options should be updated"
3509            );
3510            assert_eq!(
3511                search_bar.search_options,
3512                SearchOptions::CASE_SENSITIVE,
3513                "After a settings update and toggling the search bar, configured options should be used"
3514            );
3515        });
3516
3517        update_search_settings(
3518            SearchSettings {
3519                button: true,
3520                whole_word: true,
3521                case_sensitive: true,
3522                include_ignored: false,
3523                regex: false,
3524                center_on_match: false,
3525            },
3526            cx,
3527        );
3528
3529        search_bar.update_in(cx, |search_bar, window, cx| {
3530            search_bar.deploy(&deploy, window, cx);
3531            search_bar.dismiss(&Dismiss, window, cx);
3532            search_bar.show(window, cx);
3533            assert_eq!(
3534                search_bar.search_options,
3535                SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD,
3536                "Calling deploy on an already deployed search bar should not prevent settings updates from being detected"
3537            );
3538        });
3539    }
3540
3541    #[gpui::test]
3542    async fn test_select_occurrence_case_sensitivity(cx: &mut TestAppContext) {
3543        let (editor, search_bar, cx) = init_test(cx);
3544        let mut editor_cx = EditorTestContext::for_editor_in(editor, cx).await;
3545
3546        // Start with case sensitive search settings.
3547        let mut search_settings = SearchSettings::default();
3548        search_settings.case_sensitive = true;
3549        update_search_settings(search_settings, cx);
3550        search_bar.update(cx, |search_bar, cx| {
3551            let mut search_options = search_bar.search_options;
3552            search_options.insert(SearchOptions::CASE_SENSITIVE);
3553            search_bar.set_search_options(search_options, cx);
3554        });
3555
3556        editor_cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
3557        editor_cx.update_editor(|e, window, cx| {
3558            e.select_next(&Default::default(), window, cx).unwrap();
3559        });
3560        editor_cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
3561
3562        // Update the search bar's case sensitivite toggle, so we can later
3563        // confirm that `select_next` will now be case-insensitive.
3564        editor_cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
3565        search_bar.update_in(cx, |search_bar, window, cx| {
3566            search_bar.toggle_case_sensitive(&Default::default(), window, cx);
3567        });
3568        editor_cx.update_editor(|e, window, cx| {
3569            e.select_next(&Default::default(), window, cx).unwrap();
3570        });
3571        editor_cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\nFoo\nfoo");
3572
3573        // Confirm that, after dismissing the search bar, only the editor's
3574        // search settings actually affect the behavior of `select_next`.
3575        search_bar.update_in(cx, |search_bar, window, cx| {
3576            search_bar.dismiss(&Default::default(), window, cx);
3577        });
3578        editor_cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
3579        editor_cx.update_editor(|e, window, cx| {
3580            e.select_next(&Default::default(), window, cx).unwrap();
3581        });
3582        editor_cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
3583
3584        // Update the editor's search settings, disabling case sensitivity, to
3585        // check that the value is respected.
3586        let mut search_settings = SearchSettings::default();
3587        search_settings.case_sensitive = false;
3588        update_search_settings(search_settings, cx);
3589        editor_cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
3590        editor_cx.update_editor(|e, window, cx| {
3591            e.select_next(&Default::default(), window, cx).unwrap();
3592        });
3593        editor_cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\nFoo\nfoo");
3594    }
3595
3596    fn update_search_settings(search_settings: SearchSettings, cx: &mut TestAppContext) {
3597        cx.update(|cx| {
3598            SettingsStore::update_global(cx, |store, cx| {
3599                store.update_user_settings(cx, |settings| {
3600                    settings.editor.search = Some(SearchSettingsContent {
3601                        button: Some(search_settings.button),
3602                        whole_word: Some(search_settings.whole_word),
3603                        case_sensitive: Some(search_settings.case_sensitive),
3604                        include_ignored: Some(search_settings.include_ignored),
3605                        regex: Some(search_settings.regex),
3606                        center_on_match: Some(search_settings.center_on_match),
3607                    });
3608                });
3609            });
3610        });
3611    }
3612}