buffer_search.rs

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