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