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