buffer_search.rs

   1mod registrar;
   2
   3use crate::{
   4    FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions,
   5    SelectAllMatches, SelectNextMatch, SelectPreviousMatch, ToggleCaseSensitive, ToggleRegex,
   6    ToggleReplace, ToggleSelection, ToggleWholeWord, search_bar::render_nav_button,
   7};
   8use any_vec::AnyVec;
   9use anyhow::Context as _;
  10use collections::HashMap;
  11use editor::{
  12    DisplayPoint, Editor, EditorElement, EditorSettings, EditorStyle,
  13    actions::{Backtab, Tab},
  14};
  15use futures::channel::oneshot;
  16use gpui::{
  17    Action, App, ClickEvent, Context, Entity, EventEmitter, FocusHandle, Focusable,
  18    InteractiveElement as _, IntoElement, KeyContext, ParentElement as _, Render, ScrollHandle,
  19    Styled, Subscription, Task, TextStyle, Window, actions, div,
  20};
  21use language::{Language, LanguageRegistry};
  22use project::{
  23    search::SearchQuery,
  24    search_history::{SearchHistory, SearchHistoryCursor},
  25};
  26use schemars::JsonSchema;
  27use serde::Deserialize;
  28use settings::Settings;
  29use std::sync::Arc;
  30use theme::ThemeSettings;
  31use zed_actions::outline::ToggleOutline;
  32
  33use ui::{
  34    BASE_REM_SIZE_IN_PX, IconButton, IconButtonShape, IconName, Tooltip, h_flex, prelude::*,
  35    utils::SearchInputWidth,
  36};
  37use util::ResultExt;
  38use workspace::{
  39    ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
  40    item::ItemHandle,
  41    searchable::{Direction, SearchEvent, SearchableItemHandle, WeakSearchableItemHandle},
  42};
  43
  44pub use registrar::DivRegistrar;
  45use registrar::{ForDeployed, ForDismissed, SearchActionsRegistrar, WithResults};
  46
  47const MAX_BUFFER_SEARCH_HISTORY_SIZE: usize = 50;
  48
  49/// Opens the buffer search interface with the specified configuration.
  50#[derive(PartialEq, Clone, Deserialize, JsonSchema, Action)]
  51#[action(namespace = buffer_search)]
  52#[serde(deny_unknown_fields)]
  53pub struct Deploy {
  54    #[serde(default = "util::serde::default_true")]
  55    pub focus: bool,
  56    #[serde(default)]
  57    pub replace_enabled: bool,
  58    #[serde(default)]
  59    pub selection_search_enabled: bool,
  60}
  61
  62actions!(
  63    buffer_search,
  64    [
  65        /// Deploys the search and replace interface.
  66        DeployReplace,
  67        /// Dismisses the search bar.
  68        Dismiss,
  69        /// Focuses back on the editor.
  70        FocusEditor
  71    ]
  72);
  73
  74impl Deploy {
  75    pub fn find() -> Self {
  76        Self {
  77            focus: true,
  78            replace_enabled: false,
  79            selection_search_enabled: false,
  80        }
  81    }
  82
  83    pub fn replace() -> Self {
  84        Self {
  85            focus: true,
  86            replace_enabled: true,
  87            selection_search_enabled: false,
  88        }
  89    }
  90}
  91
  92pub enum Event {
  93    UpdateLocation,
  94}
  95
  96pub fn init(cx: &mut App) {
  97    cx.observe_new(|workspace: &mut Workspace, _, _| BufferSearchBar::register(workspace))
  98        .detach();
  99}
 100
 101pub struct BufferSearchBar {
 102    query_editor: Entity<Editor>,
 103    query_editor_focused: bool,
 104    replacement_editor: Entity<Editor>,
 105    replacement_editor_focused: bool,
 106    active_searchable_item: Option<Box<dyn SearchableItemHandle>>,
 107    active_match_index: Option<usize>,
 108    active_searchable_item_subscription: Option<Subscription>,
 109    active_search: Option<Arc<SearchQuery>>,
 110    searchable_items_with_matches: HashMap<Box<dyn WeakSearchableItemHandle>, AnyVec<dyn Send>>,
 111    pending_search: Option<Task<()>>,
 112    search_options: SearchOptions,
 113    default_options: SearchOptions,
 114    configured_options: SearchOptions,
 115    query_error: Option<String>,
 116    dismissed: bool,
 117    search_history: SearchHistory,
 118    search_history_cursor: SearchHistoryCursor,
 119    replace_enabled: bool,
 120    selection_search_enabled: bool,
 121    scroll_handle: ScrollHandle,
 122    editor_scroll_handle: ScrollHandle,
 123    editor_needed_width: Pixels,
 124    regex_language: Option<Arc<Language>>,
 125}
 126
 127impl BufferSearchBar {
 128    fn render_text_input(
 129        &self,
 130        editor: &Entity<Editor>,
 131        color_override: Option<Color>,
 132        cx: &mut Context<Self>,
 133    ) -> impl IntoElement {
 134        let (color, use_syntax) = if editor.read(cx).read_only(cx) {
 135            (cx.theme().colors().text_disabled, false)
 136        } else {
 137            match color_override {
 138                Some(color_override) => (color_override.color(cx), false),
 139                None => (cx.theme().colors().text, true),
 140            }
 141        };
 142
 143        let settings = ThemeSettings::get_global(cx);
 144        let text_style = TextStyle {
 145            color,
 146            font_family: settings.buffer_font.family.clone(),
 147            font_features: settings.buffer_font.features.clone(),
 148            font_fallbacks: settings.buffer_font.fallbacks.clone(),
 149            font_size: rems(0.875).into(),
 150            font_weight: settings.buffer_font.weight,
 151            line_height: relative(1.3),
 152            ..TextStyle::default()
 153        };
 154
 155        let mut editor_style = EditorStyle {
 156            background: cx.theme().colors().toolbar_background,
 157            local_player: cx.theme().players().local(),
 158            text: text_style,
 159            ..EditorStyle::default()
 160        };
 161        if use_syntax {
 162            editor_style.syntax = cx.theme().syntax().clone();
 163        }
 164
 165        EditorElement::new(editor, editor_style)
 166    }
 167
 168    pub fn query_editor_focused(&self) -> bool {
 169        self.query_editor_focused
 170    }
 171}
 172
 173impl EventEmitter<Event> for BufferSearchBar {}
 174impl EventEmitter<workspace::ToolbarItemEvent> for BufferSearchBar {}
 175impl Render for BufferSearchBar {
 176    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 177        if self.dismissed {
 178            return div().id("search_bar");
 179        }
 180
 181        let focus_handle = self.focus_handle(cx);
 182
 183        let narrow_mode =
 184            self.scroll_handle.bounds().size.width / window.rem_size() < 340. / BASE_REM_SIZE_IN_PX;
 185        let hide_inline_icons = self.editor_needed_width
 186            > self.editor_scroll_handle.bounds().size.width - window.rem_size() * 6.;
 187
 188        let supported_options = self.supported_options(cx);
 189
 190        if self.query_editor.update(cx, |query_editor, _cx| {
 191            query_editor.placeholder_text().is_none()
 192        }) {
 193            self.query_editor.update(cx, |editor, cx| {
 194                editor.set_placeholder_text("Search…", cx);
 195            });
 196        }
 197
 198        self.replacement_editor.update(cx, |editor, cx| {
 199            editor.set_placeholder_text("Replace with…", cx);
 200        });
 201
 202        let mut color_override = None;
 203        let match_text = self
 204            .active_searchable_item
 205            .as_ref()
 206            .and_then(|searchable_item| {
 207                if self.query(cx).is_empty() {
 208                    return None;
 209                }
 210                let matches_count = self
 211                    .searchable_items_with_matches
 212                    .get(&searchable_item.downgrade())
 213                    .map(AnyVec::len)
 214                    .unwrap_or(0);
 215                if let Some(match_ix) = self.active_match_index {
 216                    Some(format!("{}/{}", match_ix + 1, matches_count))
 217                } else {
 218                    color_override = Some(Color::Error); // No matches found
 219                    None
 220                }
 221            })
 222            .unwrap_or_else(|| "0/0".to_string());
 223        let should_show_replace_input = self.replace_enabled && supported_options.replacement;
 224        let in_replace = self.replacement_editor.focus_handle(cx).is_focused(window);
 225
 226        let mut key_context = KeyContext::new_with_defaults();
 227        key_context.add("BufferSearchBar");
 228        if in_replace {
 229            key_context.add("in_replace");
 230        }
 231        let editor_border = if self.query_error.is_some() {
 232            Color::Error.color(cx)
 233        } else {
 234            cx.theme().colors().border
 235        };
 236
 237        let container_width = window.viewport_size().width;
 238        let input_width = SearchInputWidth::calc_width(container_width);
 239
 240        let input_base_styles = || {
 241            h_flex()
 242                .min_w_32()
 243                .w(input_width)
 244                .h_8()
 245                .pl_2()
 246                .pr_1()
 247                .py_1()
 248                .border_1()
 249                .border_color(editor_border)
 250                .rounded_lg()
 251        };
 252
 253        let search_line = h_flex()
 254            .gap_2()
 255            .when(supported_options.find_in_results, |el| {
 256                el.child(Label::new("Find in results").color(Color::Hint))
 257            })
 258            .child(
 259                input_base_styles()
 260                    .id("editor-scroll")
 261                    .track_scroll(&self.editor_scroll_handle)
 262                    .child(self.render_text_input(&self.query_editor, color_override, cx))
 263                    .when(!hide_inline_icons, |div| {
 264                        div.child(
 265                            h_flex()
 266                                .gap_1()
 267                                .children(supported_options.case.then(|| {
 268                                    self.render_search_option_button(
 269                                        SearchOptions::CASE_SENSITIVE,
 270                                        focus_handle.clone(),
 271                                        cx.listener(|this, _, window, cx| {
 272                                            this.toggle_case_sensitive(
 273                                                &ToggleCaseSensitive,
 274                                                window,
 275                                                cx,
 276                                            )
 277                                        }),
 278                                    )
 279                                }))
 280                                .children(supported_options.word.then(|| {
 281                                    self.render_search_option_button(
 282                                        SearchOptions::WHOLE_WORD,
 283                                        focus_handle.clone(),
 284                                        cx.listener(|this, _, window, cx| {
 285                                            this.toggle_whole_word(&ToggleWholeWord, window, cx)
 286                                        }),
 287                                    )
 288                                }))
 289                                .children(supported_options.regex.then(|| {
 290                                    self.render_search_option_button(
 291                                        SearchOptions::REGEX,
 292                                        focus_handle.clone(),
 293                                        cx.listener(|this, _, window, cx| {
 294                                            this.toggle_regex(&ToggleRegex, window, cx)
 295                                        }),
 296                                    )
 297                                })),
 298                        )
 299                    }),
 300            )
 301            .child(
 302                h_flex()
 303                    .gap_1()
 304                    .min_w_64()
 305                    .when(supported_options.replacement, |this| {
 306                        this.child(
 307                            IconButton::new(
 308                                "buffer-search-bar-toggle-replace-button",
 309                                IconName::Replace,
 310                            )
 311                            .style(ButtonStyle::Subtle)
 312                            .shape(IconButtonShape::Square)
 313                            .when(self.replace_enabled, |button| {
 314                                button.style(ButtonStyle::Filled)
 315                            })
 316                            .on_click(cx.listener(|this, _: &ClickEvent, window, cx| {
 317                                this.toggle_replace(&ToggleReplace, window, cx);
 318                            }))
 319                            .toggle_state(self.replace_enabled)
 320                            .tooltip({
 321                                let focus_handle = focus_handle.clone();
 322                                move |window, cx| {
 323                                    Tooltip::for_action_in(
 324                                        "Toggle Replace",
 325                                        &ToggleReplace,
 326                                        &focus_handle,
 327                                        window,
 328                                        cx,
 329                                    )
 330                                }
 331                            }),
 332                        )
 333                    })
 334                    .when(supported_options.selection, |this| {
 335                        this.child(
 336                            IconButton::new(
 337                                "buffer-search-bar-toggle-search-selection-button",
 338                                IconName::SearchSelection,
 339                            )
 340                            .style(ButtonStyle::Subtle)
 341                            .shape(IconButtonShape::Square)
 342                            .when(self.selection_search_enabled, |button| {
 343                                button.style(ButtonStyle::Filled)
 344                            })
 345                            .on_click(cx.listener(|this, _: &ClickEvent, window, cx| {
 346                                this.toggle_selection(&ToggleSelection, window, cx);
 347                            }))
 348                            .toggle_state(self.selection_search_enabled)
 349                            .tooltip({
 350                                let focus_handle = focus_handle.clone();
 351                                move |window, cx| {
 352                                    Tooltip::for_action_in(
 353                                        "Toggle Search Selection",
 354                                        &ToggleSelection,
 355                                        &focus_handle,
 356                                        window,
 357                                        cx,
 358                                    )
 359                                }
 360                            }),
 361                        )
 362                    })
 363                    .when(!supported_options.find_in_results, |el| {
 364                        el.child(
 365                            IconButton::new("select-all", ui::IconName::SelectAll)
 366                                .on_click(|_, window, cx| {
 367                                    window.dispatch_action(SelectAllMatches.boxed_clone(), cx)
 368                                })
 369                                .shape(IconButtonShape::Square)
 370                                .tooltip({
 371                                    let focus_handle = focus_handle.clone();
 372                                    move |window, cx| {
 373                                        Tooltip::for_action_in(
 374                                            "Select All Matches",
 375                                            &SelectAllMatches,
 376                                            &focus_handle,
 377                                            window,
 378                                            cx,
 379                                        )
 380                                    }
 381                                }),
 382                        )
 383                        .child(
 384                            h_flex()
 385                                .pl_2()
 386                                .ml_1()
 387                                .border_l_1()
 388                                .border_color(cx.theme().colors().border_variant)
 389                                .child(render_nav_button(
 390                                    ui::IconName::ChevronLeft,
 391                                    self.active_match_index.is_some(),
 392                                    "Select Previous Match",
 393                                    &SelectPreviousMatch,
 394                                    focus_handle.clone(),
 395                                ))
 396                                .child(render_nav_button(
 397                                    ui::IconName::ChevronRight,
 398                                    self.active_match_index.is_some(),
 399                                    "Select Next Match",
 400                                    &SelectNextMatch,
 401                                    focus_handle.clone(),
 402                                )),
 403                        )
 404                        .when(!narrow_mode, |this| {
 405                            this.child(h_flex().ml_2().min_w(rems_from_px(40.)).child(
 406                                Label::new(match_text).size(LabelSize::Small).color(
 407                                    if self.active_match_index.is_some() {
 408                                        Color::Default
 409                                    } else {
 410                                        Color::Disabled
 411                                    },
 412                                ),
 413                            ))
 414                        })
 415                    })
 416                    .when(supported_options.find_in_results, |el| {
 417                        el.child(
 418                            IconButton::new(SharedString::from("Close"), IconName::Close)
 419                                .shape(IconButtonShape::Square)
 420                                .tooltip(move |window, cx| {
 421                                    Tooltip::for_action("Close Search Bar", &Dismiss, window, cx)
 422                                })
 423                                .on_click(cx.listener(|this, _: &ClickEvent, window, cx| {
 424                                    this.dismiss(&Dismiss, window, cx)
 425                                })),
 426                        )
 427                    }),
 428            );
 429
 430        let replace_line = should_show_replace_input.then(|| {
 431            h_flex()
 432                .gap_2()
 433                .child(input_base_styles().child(self.render_text_input(
 434                    &self.replacement_editor,
 435                    None,
 436                    cx,
 437                )))
 438                .child(
 439                    h_flex()
 440                        .min_w_64()
 441                        .gap_1()
 442                        .child(
 443                            IconButton::new("search-replace-next", ui::IconName::ReplaceNext)
 444                                .shape(IconButtonShape::Square)
 445                                .tooltip({
 446                                    let focus_handle = focus_handle.clone();
 447                                    move |window, cx| {
 448                                        Tooltip::for_action_in(
 449                                            "Replace Next Match",
 450                                            &ReplaceNext,
 451                                            &focus_handle,
 452                                            window,
 453                                            cx,
 454                                        )
 455                                    }
 456                                })
 457                                .on_click(cx.listener(|this, _, window, cx| {
 458                                    this.replace_next(&ReplaceNext, window, cx)
 459                                })),
 460                        )
 461                        .child(
 462                            IconButton::new("search-replace-all", ui::IconName::ReplaceAll)
 463                                .shape(IconButtonShape::Square)
 464                                .tooltip({
 465                                    let focus_handle = focus_handle.clone();
 466                                    move |window, cx| {
 467                                        Tooltip::for_action_in(
 468                                            "Replace All Matches",
 469                                            &ReplaceAll,
 470                                            &focus_handle,
 471                                            window,
 472                                            cx,
 473                                        )
 474                                    }
 475                                })
 476                                .on_click(cx.listener(|this, _, window, cx| {
 477                                    this.replace_all(&ReplaceAll, window, cx)
 478                                })),
 479                        ),
 480                )
 481        });
 482
 483        let query_error_line = self.query_error.as_ref().map(|error| {
 484            Label::new(error)
 485                .size(LabelSize::Small)
 486                .color(Color::Error)
 487                .mt_neg_1()
 488                .ml_2()
 489        });
 490
 491        v_flex()
 492            .id("buffer_search")
 493            .gap_2()
 494            .py(px(1.0))
 495            .track_scroll(&self.scroll_handle)
 496            .key_context(key_context)
 497            .capture_action(cx.listener(Self::tab))
 498            .capture_action(cx.listener(Self::backtab))
 499            .on_action(cx.listener(Self::previous_history_query))
 500            .on_action(cx.listener(Self::next_history_query))
 501            .on_action(cx.listener(Self::dismiss))
 502            .on_action(cx.listener(Self::select_next_match))
 503            .on_action(cx.listener(Self::select_prev_match))
 504            .on_action(cx.listener(|this, _: &ToggleOutline, window, cx| {
 505                if let Some(active_searchable_item) = &mut this.active_searchable_item {
 506                    active_searchable_item.relay_action(Box::new(ToggleOutline), window, cx);
 507                }
 508            }))
 509            .when(self.supported_options(cx).replacement, |this| {
 510                this.on_action(cx.listener(Self::toggle_replace))
 511                    .when(in_replace, |this| {
 512                        this.on_action(cx.listener(Self::replace_next))
 513                            .on_action(cx.listener(Self::replace_all))
 514                    })
 515            })
 516            .when(self.supported_options(cx).case, |this| {
 517                this.on_action(cx.listener(Self::toggle_case_sensitive))
 518            })
 519            .when(self.supported_options(cx).word, |this| {
 520                this.on_action(cx.listener(Self::toggle_whole_word))
 521            })
 522            .when(self.supported_options(cx).regex, |this| {
 523                this.on_action(cx.listener(Self::toggle_regex))
 524            })
 525            .when(self.supported_options(cx).selection, |this| {
 526                this.on_action(cx.listener(Self::toggle_selection))
 527            })
 528            .child(h_flex().relative().child(search_line.w_full()).when(
 529                !narrow_mode && !supported_options.find_in_results,
 530                |div| {
 531                    div.child(
 532                        h_flex().absolute().right_0().child(
 533                            IconButton::new(SharedString::from("Close"), IconName::Close)
 534                                .shape(IconButtonShape::Square)
 535                                .tooltip(move |window, cx| {
 536                                    Tooltip::for_action("Close Search Bar", &Dismiss, window, cx)
 537                                })
 538                                .on_click(cx.listener(|this, _: &ClickEvent, window, cx| {
 539                                    this.dismiss(&Dismiss, window, cx)
 540                                })),
 541                        ),
 542                    )
 543                    .w_full()
 544                },
 545            ))
 546            .children(query_error_line)
 547            .children(replace_line)
 548    }
 549}
 550
 551impl Focusable for BufferSearchBar {
 552    fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
 553        self.query_editor.focus_handle(cx)
 554    }
 555}
 556
 557impl ToolbarItemView for BufferSearchBar {
 558    fn set_active_pane_item(
 559        &mut self,
 560        item: Option<&dyn ItemHandle>,
 561        window: &mut Window,
 562        cx: &mut Context<Self>,
 563    ) -> ToolbarItemLocation {
 564        cx.notify();
 565        self.active_searchable_item_subscription.take();
 566        self.active_searchable_item.take();
 567
 568        self.pending_search.take();
 569
 570        if let Some(searchable_item_handle) =
 571            item.and_then(|item| item.to_searchable_item_handle(cx))
 572        {
 573            let this = cx.entity().downgrade();
 574
 575            self.active_searchable_item_subscription =
 576                Some(searchable_item_handle.subscribe_to_search_events(
 577                    window,
 578                    cx,
 579                    Box::new(move |search_event, window, cx| {
 580                        if let Some(this) = this.upgrade() {
 581                            this.update(cx, |this, cx| {
 582                                this.on_active_searchable_item_event(search_event, window, cx)
 583                            });
 584                        }
 585                    }),
 586                ));
 587
 588            let is_project_search = searchable_item_handle.supported_options(cx).find_in_results;
 589            self.active_searchable_item = Some(searchable_item_handle);
 590            drop(self.update_matches(true, window, cx));
 591            if !self.dismissed {
 592                if is_project_search {
 593                    self.dismiss(&Default::default(), window, cx);
 594                } else {
 595                    return ToolbarItemLocation::Secondary;
 596                }
 597            }
 598        }
 599        ToolbarItemLocation::Hidden
 600    }
 601}
 602
 603impl BufferSearchBar {
 604    pub fn register(registrar: &mut impl SearchActionsRegistrar) {
 605        registrar.register_handler(ForDeployed(|this, _: &FocusSearch, window, cx| {
 606            this.query_editor.focus_handle(cx).focus(window);
 607            this.select_query(window, cx);
 608        }));
 609        registrar.register_handler(ForDeployed(
 610            |this, action: &ToggleCaseSensitive, window, cx| {
 611                if this.supported_options(cx).case {
 612                    this.toggle_case_sensitive(action, window, cx);
 613                }
 614            },
 615        ));
 616        registrar.register_handler(ForDeployed(|this, action: &ToggleWholeWord, window, cx| {
 617            if this.supported_options(cx).word {
 618                this.toggle_whole_word(action, window, cx);
 619            }
 620        }));
 621        registrar.register_handler(ForDeployed(|this, action: &ToggleRegex, window, cx| {
 622            if this.supported_options(cx).regex {
 623                this.toggle_regex(action, window, cx);
 624            }
 625        }));
 626        registrar.register_handler(ForDeployed(|this, action: &ToggleSelection, window, cx| {
 627            if this.supported_options(cx).selection {
 628                this.toggle_selection(action, window, cx);
 629            } else {
 630                cx.propagate();
 631            }
 632        }));
 633        registrar.register_handler(ForDeployed(|this, action: &ToggleReplace, window, cx| {
 634            if this.supported_options(cx).replacement {
 635                this.toggle_replace(action, window, cx);
 636            } else {
 637                cx.propagate();
 638            }
 639        }));
 640        registrar.register_handler(WithResults(|this, action: &SelectNextMatch, window, cx| {
 641            if this.supported_options(cx).find_in_results {
 642                cx.propagate();
 643            } else {
 644                this.select_next_match(action, window, cx);
 645            }
 646        }));
 647        registrar.register_handler(WithResults(
 648            |this, action: &SelectPreviousMatch, window, cx| {
 649                if this.supported_options(cx).find_in_results {
 650                    cx.propagate();
 651                } else {
 652                    this.select_prev_match(action, window, cx);
 653                }
 654            },
 655        ));
 656        registrar.register_handler(WithResults(
 657            |this, action: &SelectAllMatches, window, cx| {
 658                if this.supported_options(cx).find_in_results {
 659                    cx.propagate();
 660                } else {
 661                    this.select_all_matches(action, window, cx);
 662                }
 663            },
 664        ));
 665        registrar.register_handler(ForDeployed(
 666            |this, _: &editor::actions::Cancel, window, cx| {
 667                this.dismiss(&Dismiss, window, cx);
 668            },
 669        ));
 670        registrar.register_handler(ForDeployed(|this, _: &Dismiss, window, cx| {
 671            this.dismiss(&Dismiss, window, cx);
 672        }));
 673
 674        // register deploy buffer search for both search bar states, since we want to focus into the search bar
 675        // when the deploy action is triggered in the buffer.
 676        registrar.register_handler(ForDeployed(|this, deploy, window, cx| {
 677            this.deploy(deploy, window, cx);
 678        }));
 679        registrar.register_handler(ForDismissed(|this, deploy, window, cx| {
 680            this.deploy(deploy, window, cx);
 681        }));
 682        registrar.register_handler(ForDeployed(|this, _: &DeployReplace, window, cx| {
 683            if this.supported_options(cx).find_in_results {
 684                cx.propagate();
 685            } else {
 686                this.deploy(&Deploy::replace(), window, cx);
 687            }
 688        }));
 689        registrar.register_handler(ForDismissed(|this, _: &DeployReplace, window, cx| {
 690            if this.supported_options(cx).find_in_results {
 691                cx.propagate();
 692            } else {
 693                this.deploy(&Deploy::replace(), window, cx);
 694            }
 695        }));
 696    }
 697
 698    pub fn new(
 699        languages: Option<Arc<LanguageRegistry>>,
 700        window: &mut Window,
 701        cx: &mut Context<Self>,
 702    ) -> Self {
 703        let query_editor = cx.new(|cx| Editor::single_line(window, cx));
 704        cx.subscribe_in(&query_editor, window, Self::on_query_editor_event)
 705            .detach();
 706        let replacement_editor = cx.new(|cx| Editor::single_line(window, cx));
 707        cx.subscribe(&replacement_editor, Self::on_replacement_editor_event)
 708            .detach();
 709
 710        let search_options = SearchOptions::from_settings(&EditorSettings::get_global(cx).search);
 711        if let Some(languages) = languages {
 712            let query_buffer = query_editor
 713                .read(cx)
 714                .buffer()
 715                .read(cx)
 716                .as_singleton()
 717                .expect("query editor should be backed by a singleton buffer");
 718            query_buffer
 719                .read(cx)
 720                .set_language_registry(languages.clone());
 721
 722            cx.spawn(async move |buffer_search_bar, cx| {
 723                let regex_language = languages
 724                    .language_for_name("regex")
 725                    .await
 726                    .context("loading regex language")?;
 727                buffer_search_bar
 728                    .update(cx, |buffer_search_bar, cx| {
 729                        buffer_search_bar.regex_language = Some(regex_language);
 730                        buffer_search_bar.adjust_query_regex_language(cx);
 731                    })
 732                    .ok();
 733                anyhow::Ok(())
 734            })
 735            .detach_and_log_err(cx);
 736        }
 737
 738        Self {
 739            query_editor,
 740            query_editor_focused: false,
 741            replacement_editor,
 742            replacement_editor_focused: false,
 743            active_searchable_item: None,
 744            active_searchable_item_subscription: None,
 745            active_match_index: None,
 746            searchable_items_with_matches: Default::default(),
 747            default_options: search_options,
 748            configured_options: search_options,
 749            search_options,
 750            pending_search: None,
 751            query_error: None,
 752            dismissed: true,
 753            search_history: SearchHistory::new(
 754                Some(MAX_BUFFER_SEARCH_HISTORY_SIZE),
 755                project::search_history::QueryInsertionBehavior::ReplacePreviousIfContains,
 756            ),
 757            search_history_cursor: Default::default(),
 758            active_search: None,
 759            replace_enabled: false,
 760            selection_search_enabled: false,
 761            scroll_handle: ScrollHandle::new(),
 762            editor_scroll_handle: ScrollHandle::new(),
 763            editor_needed_width: px(0.),
 764            regex_language: None,
 765        }
 766    }
 767
 768    pub fn is_dismissed(&self) -> bool {
 769        self.dismissed
 770    }
 771
 772    pub fn dismiss(&mut self, _: &Dismiss, window: &mut Window, cx: &mut Context<Self>) {
 773        self.dismissed = true;
 774        for searchable_item in self.searchable_items_with_matches.keys() {
 775            if let Some(searchable_item) =
 776                WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
 777            {
 778                searchable_item.clear_matches(window, cx);
 779            }
 780        }
 781        if let Some(active_editor) = self.active_searchable_item.as_mut() {
 782            self.selection_search_enabled = false;
 783            self.replace_enabled = false;
 784            active_editor.search_bar_visibility_changed(false, window, cx);
 785            active_editor.toggle_filtered_search_ranges(false, window, cx);
 786            let handle = active_editor.item_focus_handle(cx);
 787            self.focus(&handle, window, cx);
 788        }
 789        cx.emit(Event::UpdateLocation);
 790        cx.emit(ToolbarItemEvent::ChangeLocation(
 791            ToolbarItemLocation::Hidden,
 792        ));
 793        cx.notify();
 794    }
 795
 796    pub fn deploy(&mut self, deploy: &Deploy, window: &mut Window, cx: &mut Context<Self>) -> bool {
 797        if self.show(window, cx) {
 798            if let Some(active_item) = self.active_searchable_item.as_mut() {
 799                active_item.toggle_filtered_search_ranges(
 800                    deploy.selection_search_enabled,
 801                    window,
 802                    cx,
 803                );
 804            }
 805            self.search_suggested(window, cx);
 806            self.smartcase(window, cx);
 807            self.replace_enabled = deploy.replace_enabled;
 808            self.selection_search_enabled = deploy.selection_search_enabled;
 809            if deploy.focus {
 810                let mut handle = self.query_editor.focus_handle(cx).clone();
 811                let mut select_query = true;
 812                if deploy.replace_enabled && handle.is_focused(window) {
 813                    handle = self.replacement_editor.focus_handle(cx).clone();
 814                    select_query = false;
 815                };
 816
 817                if select_query {
 818                    self.select_query(window, cx);
 819                }
 820
 821                window.focus(&handle);
 822            }
 823            return true;
 824        }
 825
 826        cx.propagate();
 827        false
 828    }
 829
 830    pub fn toggle(&mut self, action: &Deploy, window: &mut Window, cx: &mut Context<Self>) {
 831        if self.is_dismissed() {
 832            self.deploy(action, window, cx);
 833        } else {
 834            self.dismiss(&Dismiss, window, cx);
 835        }
 836    }
 837
 838    pub fn show(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
 839        let Some(handle) = self.active_searchable_item.as_ref() else {
 840            return false;
 841        };
 842
 843        self.configured_options =
 844            SearchOptions::from_settings(&EditorSettings::get_global(cx).search);
 845        if self.dismissed
 846            && (self.configured_options != self.default_options
 847                || self.configured_options != self.search_options)
 848        {
 849            self.search_options = self.configured_options;
 850            self.default_options = self.configured_options;
 851        }
 852
 853        self.dismissed = false;
 854        self.adjust_query_regex_language(cx);
 855        handle.search_bar_visibility_changed(true, window, cx);
 856        cx.notify();
 857        cx.emit(Event::UpdateLocation);
 858        cx.emit(ToolbarItemEvent::ChangeLocation(
 859            ToolbarItemLocation::Secondary,
 860        ));
 861        true
 862    }
 863
 864    fn supported_options(&self, cx: &mut Context<Self>) -> workspace::searchable::SearchOptions {
 865        self.active_searchable_item
 866            .as_ref()
 867            .map(|item| item.supported_options(cx))
 868            .unwrap_or_default()
 869    }
 870
 871    pub fn search_suggested(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 872        let search = self
 873            .query_suggestion(window, cx)
 874            .map(|suggestion| self.search(&suggestion, Some(self.default_options), window, cx));
 875
 876        if let Some(search) = search {
 877            cx.spawn_in(window, async move |this, cx| {
 878                search.await?;
 879                this.update_in(cx, |this, window, cx| {
 880                    this.activate_current_match(window, cx)
 881                })
 882            })
 883            .detach_and_log_err(cx);
 884        }
 885    }
 886
 887    pub fn activate_current_match(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 888        if let Some(match_ix) = self.active_match_index {
 889            if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
 890                if let Some(matches) = self
 891                    .searchable_items_with_matches
 892                    .get(&active_searchable_item.downgrade())
 893                {
 894                    active_searchable_item.activate_match(match_ix, matches, window, cx)
 895                }
 896            }
 897        }
 898    }
 899
 900    pub fn select_query(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 901        self.query_editor.update(cx, |query_editor, cx| {
 902            query_editor.select_all(&Default::default(), window, cx);
 903        });
 904    }
 905
 906    pub fn query(&self, cx: &App) -> String {
 907        self.query_editor.read(cx).text(cx)
 908    }
 909
 910    pub fn replacement(&self, cx: &mut App) -> String {
 911        self.replacement_editor.read(cx).text(cx)
 912    }
 913
 914    pub fn query_suggestion(
 915        &mut self,
 916        window: &mut Window,
 917        cx: &mut Context<Self>,
 918    ) -> Option<String> {
 919        self.active_searchable_item
 920            .as_ref()
 921            .map(|searchable_item| searchable_item.query_suggestion(window, cx))
 922            .filter(|suggestion| !suggestion.is_empty())
 923    }
 924
 925    pub fn set_replacement(&mut self, replacement: Option<&str>, cx: &mut Context<Self>) {
 926        if replacement.is_none() {
 927            self.replace_enabled = false;
 928            return;
 929        }
 930        self.replace_enabled = true;
 931        self.replacement_editor
 932            .update(cx, |replacement_editor, cx| {
 933                replacement_editor
 934                    .buffer()
 935                    .update(cx, |replacement_buffer, cx| {
 936                        let len = replacement_buffer.len(cx);
 937                        replacement_buffer.edit([(0..len, replacement.unwrap())], None, cx);
 938                    });
 939            });
 940    }
 941
 942    pub fn search(
 943        &mut self,
 944        query: &str,
 945        options: Option<SearchOptions>,
 946        window: &mut Window,
 947        cx: &mut Context<Self>,
 948    ) -> oneshot::Receiver<()> {
 949        let options = options.unwrap_or(self.default_options);
 950        let updated = query != self.query(cx) || self.search_options != options;
 951        if updated {
 952            self.query_editor.update(cx, |query_editor, cx| {
 953                query_editor.buffer().update(cx, |query_buffer, cx| {
 954                    let len = query_buffer.len(cx);
 955                    query_buffer.edit([(0..len, query)], None, cx);
 956                });
 957            });
 958            self.set_search_options(options, cx);
 959            self.clear_matches(window, cx);
 960            cx.notify();
 961        }
 962        self.update_matches(!updated, window, cx)
 963    }
 964
 965    fn render_search_option_button<Action: Fn(&ClickEvent, &mut Window, &mut App) + 'static>(
 966        &self,
 967        option: SearchOptions,
 968        focus_handle: FocusHandle,
 969        action: Action,
 970    ) -> impl IntoElement + use<Action> {
 971        let is_active = self.search_options.contains(option);
 972        option.as_button(is_active, focus_handle, action)
 973    }
 974
 975    pub fn focus_editor(&mut self, _: &FocusEditor, window: &mut Window, cx: &mut Context<Self>) {
 976        if let Some(active_editor) = self.active_searchable_item.as_ref() {
 977            let handle = active_editor.item_focus_handle(cx);
 978            window.focus(&handle);
 979        }
 980    }
 981
 982    pub fn toggle_search_option(
 983        &mut self,
 984        search_option: SearchOptions,
 985        window: &mut Window,
 986        cx: &mut Context<Self>,
 987    ) {
 988        self.search_options.toggle(search_option);
 989        self.default_options = self.search_options;
 990        drop(self.update_matches(false, window, cx));
 991        self.adjust_query_regex_language(cx);
 992        cx.notify();
 993    }
 994
 995    pub fn has_search_option(&mut self, search_option: SearchOptions) -> bool {
 996        self.search_options.contains(search_option)
 997    }
 998
 999    pub fn enable_search_option(
1000        &mut self,
1001        search_option: SearchOptions,
1002        window: &mut Window,
1003        cx: &mut Context<Self>,
1004    ) {
1005        if !self.search_options.contains(search_option) {
1006            self.toggle_search_option(search_option, window, cx)
1007        }
1008    }
1009
1010    pub fn set_search_options(&mut self, search_options: SearchOptions, cx: &mut Context<Self>) {
1011        self.search_options = search_options;
1012        self.adjust_query_regex_language(cx);
1013        cx.notify();
1014    }
1015
1016    pub fn clear_search_within_ranges(
1017        &mut self,
1018        search_options: SearchOptions,
1019        cx: &mut Context<Self>,
1020    ) {
1021        self.search_options = search_options;
1022        self.adjust_query_regex_language(cx);
1023        cx.notify();
1024    }
1025
1026    fn select_next_match(
1027        &mut self,
1028        _: &SelectNextMatch,
1029        window: &mut Window,
1030        cx: &mut Context<Self>,
1031    ) {
1032        self.select_match(Direction::Next, 1, window, cx);
1033    }
1034
1035    fn select_prev_match(
1036        &mut self,
1037        _: &SelectPreviousMatch,
1038        window: &mut Window,
1039        cx: &mut Context<Self>,
1040    ) {
1041        self.select_match(Direction::Prev, 1, window, cx);
1042    }
1043
1044    fn select_all_matches(
1045        &mut self,
1046        _: &SelectAllMatches,
1047        window: &mut Window,
1048        cx: &mut Context<Self>,
1049    ) {
1050        if !self.dismissed && self.active_match_index.is_some() {
1051            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1052                if let Some(matches) = self
1053                    .searchable_items_with_matches
1054                    .get(&searchable_item.downgrade())
1055                {
1056                    searchable_item.select_matches(matches, window, cx);
1057                    self.focus_editor(&FocusEditor, window, cx);
1058                }
1059            }
1060        }
1061    }
1062
1063    pub fn select_match(
1064        &mut self,
1065        direction: Direction,
1066        count: usize,
1067        window: &mut Window,
1068        cx: &mut Context<Self>,
1069    ) {
1070        if let Some(index) = self.active_match_index {
1071            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1072                if let Some(matches) = self
1073                    .searchable_items_with_matches
1074                    .get(&searchable_item.downgrade())
1075                    .filter(|matches| !matches.is_empty())
1076                {
1077                    // If 'wrapscan' is disabled, searches do not wrap around the end of the file.
1078                    if !EditorSettings::get_global(cx).search_wrap
1079                        && ((direction == Direction::Next && index + count >= matches.len())
1080                            || (direction == Direction::Prev && index < count))
1081                    {
1082                        crate::show_no_more_matches(window, cx);
1083                        return;
1084                    }
1085                    let new_match_index = searchable_item
1086                        .match_index_for_direction(matches, index, direction, count, window, cx);
1087
1088                    searchable_item.update_matches(matches, window, cx);
1089                    searchable_item.activate_match(new_match_index, matches, window, cx);
1090                }
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}