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