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