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