buffer_search.rs

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