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