buffer_search.rs

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