buffer_search.rs

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