buffer_search.rs

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