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