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