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