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