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