buffer_search.rs

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