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