buffer_search.rs

   1mod registrar;
   2
   3use crate::{
   4    search_bar::render_nav_button, FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll,
   5    ReplaceNext, SearchOptions, SelectAllMatches, SelectNextMatch, SelectPreviousMatch,
   6    ToggleCaseSensitive, ToggleRegex, ToggleReplace, ToggleSelection, ToggleWholeWord,
   7};
   8use any_vec::AnyVec;
   9use anyhow::Context as _;
  10use collections::HashMap;
  11use editor::{
  12    actions::{Backtab, Tab},
  13    DisplayPoint, Editor, EditorElement, EditorSettings, EditorStyle,
  14};
  15use futures::channel::oneshot;
  16use gpui::{
  17    actions, div, impl_actions, Action, App, ClickEvent, Context, Entity, EventEmitter,
  18    FocusHandle, Focusable, InteractiveElement as _, IntoElement, KeyContext, ParentElement as _,
  19    Render, ScrollHandle, Styled, Subscription, Task, TextStyle, Window,
  20};
  21use language::{Language, LanguageRegistry};
  22use project::{
  23    search::SearchQuery,
  24    search_history::{SearchHistory, SearchHistoryCursor},
  25};
  26use schemars::JsonSchema;
  27use serde::Deserialize;
  28use settings::Settings;
  29use std::sync::Arc;
  30use theme::ThemeSettings;
  31use zed_actions::outline::ToggleOutline;
  32
  33use ui::{
  34    h_flex, prelude::*, utils::SearchInputWidth, IconButton, IconButtonShape, IconName, Tooltip,
  35    BASE_REM_SIZE_IN_PX,
  36};
  37use util::ResultExt;
  38use workspace::{
  39    item::ItemHandle,
  40    searchable::{Direction, SearchEvent, SearchableItemHandle, WeakSearchableItemHandle},
  41    ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
  42};
  43
  44pub use registrar::DivRegistrar;
  45use registrar::{ForDeployed, ForDismissed, SearchActionsRegistrar, WithResults};
  46
  47const MAX_BUFFER_SEARCH_HISTORY_SIZE: usize = 50;
  48
  49#[derive(PartialEq, Clone, Deserialize, JsonSchema)]
  50#[serde(deny_unknown_fields)]
  51pub struct Deploy {
  52    #[serde(default = "util::serde::default_true")]
  53    pub focus: bool,
  54    #[serde(default)]
  55    pub replace_enabled: bool,
  56    #[serde(default)]
  57    pub selection_search_enabled: bool,
  58}
  59
  60impl_actions!(buffer_search, [Deploy]);
  61
  62actions!(buffer_search, [DeployReplace, Dismiss, FocusEditor]);
  63
  64impl Deploy {
  65    pub fn find() -> Self {
  66        Self {
  67            focus: true,
  68            replace_enabled: false,
  69            selection_search_enabled: false,
  70        }
  71    }
  72
  73    pub fn replace() -> Self {
  74        Self {
  75            focus: true,
  76            replace_enabled: true,
  77            selection_search_enabled: false,
  78        }
  79    }
  80}
  81
  82pub enum Event {
  83    UpdateLocation,
  84}
  85
  86pub fn init(cx: &mut App) {
  87    cx.observe_new(|workspace: &mut Workspace, _, _| BufferSearchBar::register(workspace))
  88        .detach();
  89}
  90
  91pub struct BufferSearchBar {
  92    query_editor: Entity<Editor>,
  93    query_editor_focused: bool,
  94    replacement_editor: Entity<Editor>,
  95    replacement_editor_focused: bool,
  96    active_searchable_item: Option<Box<dyn SearchableItemHandle>>,
  97    active_match_index: Option<usize>,
  98    active_searchable_item_subscription: Option<Subscription>,
  99    active_search: Option<Arc<SearchQuery>>,
 100    searchable_items_with_matches: HashMap<Box<dyn WeakSearchableItemHandle>, AnyVec<dyn Send>>,
 101    pending_search: Option<Task<()>>,
 102    search_options: SearchOptions,
 103    default_options: SearchOptions,
 104    configured_options: SearchOptions,
 105    query_contains_error: bool,
 106    dismissed: bool,
 107    search_history: SearchHistory,
 108    search_history_cursor: SearchHistoryCursor,
 109    replace_enabled: bool,
 110    selection_search_enabled: bool,
 111    scroll_handle: ScrollHandle,
 112    editor_scroll_handle: ScrollHandle,
 113    editor_needed_width: Pixels,
 114    regex_language: Option<Arc<Language>>,
 115}
 116
 117impl BufferSearchBar {
 118    fn render_text_input(
 119        &self,
 120        editor: &Entity<Editor>,
 121        color_override: Option<Color>,
 122        cx: &mut Context<Self>,
 123    ) -> impl IntoElement {
 124        let (color, use_syntax) = if editor.read(cx).read_only(cx) {
 125            (cx.theme().colors().text_disabled, false)
 126        } else {
 127            match color_override {
 128                Some(color_override) => (color_override.color(cx), false),
 129                None => (cx.theme().colors().text, true),
 130            }
 131        };
 132
 133        let settings = ThemeSettings::get_global(cx);
 134        let text_style = TextStyle {
 135            color,
 136            font_family: settings.buffer_font.family.clone(),
 137            font_features: settings.buffer_font.features.clone(),
 138            font_fallbacks: settings.buffer_font.fallbacks.clone(),
 139            font_size: rems(0.875).into(),
 140            font_weight: settings.buffer_font.weight,
 141            line_height: relative(1.3),
 142            ..TextStyle::default()
 143        };
 144
 145        let mut editor_style = EditorStyle {
 146            background: cx.theme().colors().editor_background,
 147            local_player: cx.theme().players().local(),
 148            text: text_style,
 149            ..EditorStyle::default()
 150        };
 151        if use_syntax {
 152            editor_style.syntax = cx.theme().syntax().clone();
 153        }
 154
 155        EditorElement::new(editor, editor_style)
 156    }
 157
 158    pub fn query_editor_focused(&self) -> bool {
 159        self.query_editor_focused
 160    }
 161}
 162
 163impl EventEmitter<Event> for BufferSearchBar {}
 164impl EventEmitter<workspace::ToolbarItemEvent> for BufferSearchBar {}
 165impl Render for BufferSearchBar {
 166    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 167        if self.dismissed {
 168            return div().id("search_bar");
 169        }
 170
 171        let focus_handle = self.focus_handle(cx);
 172
 173        let narrow_mode =
 174            self.scroll_handle.bounds().size.width / window.rem_size() < 340. / BASE_REM_SIZE_IN_PX;
 175        let hide_inline_icons = self.editor_needed_width
 176            > self.editor_scroll_handle.bounds().size.width - window.rem_size() * 6.;
 177
 178        let supported_options = self.supported_options(cx);
 179
 180        if self.query_editor.update(cx, |query_editor, _cx| {
 181            query_editor.placeholder_text().is_none()
 182        }) {
 183            self.query_editor.update(cx, |editor, cx| {
 184                editor.set_placeholder_text("Search…", cx);
 185            });
 186        }
 187
 188        self.replacement_editor.update(cx, |editor, cx| {
 189            editor.set_placeholder_text("Replace with…", cx);
 190        });
 191
 192        let mut color_override = None;
 193        let match_text = self
 194            .active_searchable_item
 195            .as_ref()
 196            .and_then(|searchable_item| {
 197                if self.query(cx).is_empty() {
 198                    return None;
 199                }
 200                let matches_count = self
 201                    .searchable_items_with_matches
 202                    .get(&searchable_item.downgrade())
 203                    .map(AnyVec::len)
 204                    .unwrap_or(0);
 205                if let Some(match_ix) = self.active_match_index {
 206                    Some(format!("{}/{}", match_ix + 1, matches_count))
 207                } else {
 208                    color_override = Some(Color::Error); // No matches found
 209                    None
 210                }
 211            })
 212            .unwrap_or_else(|| "0/0".to_string());
 213        let should_show_replace_input = self.replace_enabled && supported_options.replacement;
 214        let in_replace = self.replacement_editor.focus_handle(cx).is_focused(window);
 215
 216        let mut key_context = KeyContext::new_with_defaults();
 217        key_context.add("BufferSearchBar");
 218        if in_replace {
 219            key_context.add("in_replace");
 220        }
 221        let editor_border = if self.query_contains_error {
 222            Color::Error.color(cx)
 223        } else {
 224            cx.theme().colors().border
 225        };
 226
 227        let container_width = window.viewport_size().width;
 228        let input_width = SearchInputWidth::calc_width(container_width);
 229
 230        let input_base_styles = || {
 231            h_flex()
 232                .min_w_32()
 233                .w(input_width)
 234                .h_8()
 235                .pl_2()
 236                .pr_1()
 237                .py_1()
 238                .border_1()
 239                .border_color(editor_border)
 240                .rounded_lg()
 241        };
 242
 243        let search_line = h_flex()
 244            .gap_2()
 245            .when(supported_options.find_in_results, |el| {
 246                el.child(Label::new("Find in results").color(Color::Hint))
 247            })
 248            .child(
 249                input_base_styles()
 250                    .id("editor-scroll")
 251                    .track_scroll(&self.editor_scroll_handle)
 252                    .child(self.render_text_input(&self.query_editor, color_override, cx))
 253                    .when(!hide_inline_icons, |div| {
 254                        div.child(
 255                            h_flex()
 256                                .gap_1()
 257                                .children(supported_options.case.then(|| {
 258                                    self.render_search_option_button(
 259                                        SearchOptions::CASE_SENSITIVE,
 260                                        focus_handle.clone(),
 261                                        cx.listener(|this, _, window, cx| {
 262                                            this.toggle_case_sensitive(
 263                                                &ToggleCaseSensitive,
 264                                                window,
 265                                                cx,
 266                                            )
 267                                        }),
 268                                    )
 269                                }))
 270                                .children(supported_options.word.then(|| {
 271                                    self.render_search_option_button(
 272                                        SearchOptions::WHOLE_WORD,
 273                                        focus_handle.clone(),
 274                                        cx.listener(|this, _, window, cx| {
 275                                            this.toggle_whole_word(&ToggleWholeWord, window, cx)
 276                                        }),
 277                                    )
 278                                }))
 279                                .children(supported_options.regex.then(|| {
 280                                    self.render_search_option_button(
 281                                        SearchOptions::REGEX,
 282                                        focus_handle.clone(),
 283                                        cx.listener(|this, _, window, cx| {
 284                                            this.toggle_regex(&ToggleRegex, window, cx)
 285                                        }),
 286                                    )
 287                                })),
 288                        )
 289                    }),
 290            )
 291            .child(
 292                h_flex()
 293                    .gap_1()
 294                    .min_w_64()
 295                    .when(supported_options.replacement, |this| {
 296                        this.child(
 297                            IconButton::new(
 298                                "buffer-search-bar-toggle-replace-button",
 299                                IconName::Replace,
 300                            )
 301                            .style(ButtonStyle::Subtle)
 302                            .shape(IconButtonShape::Square)
 303                            .when(self.replace_enabled, |button| {
 304                                button.style(ButtonStyle::Filled)
 305                            })
 306                            .on_click(cx.listener(|this, _: &ClickEvent, window, cx| {
 307                                this.toggle_replace(&ToggleReplace, window, cx);
 308                            }))
 309                            .toggle_state(self.replace_enabled)
 310                            .tooltip({
 311                                let focus_handle = focus_handle.clone();
 312                                move |window, cx| {
 313                                    Tooltip::for_action_in(
 314                                        "Toggle Replace",
 315                                        &ToggleReplace,
 316                                        &focus_handle,
 317                                        window,
 318                                        cx,
 319                                    )
 320                                }
 321                            }),
 322                        )
 323                    })
 324                    .when(supported_options.selection, |this| {
 325                        this.child(
 326                            IconButton::new(
 327                                "buffer-search-bar-toggle-search-selection-button",
 328                                IconName::SearchSelection,
 329                            )
 330                            .style(ButtonStyle::Subtle)
 331                            .shape(IconButtonShape::Square)
 332                            .when(self.selection_search_enabled, |button| {
 333                                button.style(ButtonStyle::Filled)
 334                            })
 335                            .on_click(cx.listener(|this, _: &ClickEvent, window, cx| {
 336                                this.toggle_selection(&ToggleSelection, window, cx);
 337                            }))
 338                            .toggle_state(self.selection_search_enabled)
 339                            .tooltip({
 340                                let focus_handle = focus_handle.clone();
 341                                move |window, cx| {
 342                                    Tooltip::for_action_in(
 343                                        "Toggle Search Selection",
 344                                        &ToggleSelection,
 345                                        &focus_handle,
 346                                        window,
 347                                        cx,
 348                                    )
 349                                }
 350                            }),
 351                        )
 352                    })
 353                    .when(!supported_options.find_in_results, |el| {
 354                        el.child(
 355                            IconButton::new("select-all", ui::IconName::SelectAll)
 356                                .on_click(|_, window, cx| {
 357                                    window.dispatch_action(SelectAllMatches.boxed_clone(), cx)
 358                                })
 359                                .shape(IconButtonShape::Square)
 360                                .tooltip({
 361                                    let focus_handle = focus_handle.clone();
 362                                    move |window, cx| {
 363                                        Tooltip::for_action_in(
 364                                            "Select All Matches",
 365                                            &SelectAllMatches,
 366                                            &focus_handle,
 367                                            window,
 368                                            cx,
 369                                        )
 370                                    }
 371                                }),
 372                        )
 373                        .child(
 374                            h_flex()
 375                                .pl_2()
 376                                .ml_1()
 377                                .border_l_1()
 378                                .border_color(cx.theme().colors().border_variant)
 379                                .child(render_nav_button(
 380                                    ui::IconName::ChevronLeft,
 381                                    self.active_match_index.is_some(),
 382                                    "Select Previous Match",
 383                                    &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(|buffer_search_bar, mut cx| async move {
 697                let regex_language = languages
 698                    .language_for_name("regex")
 699                    .await
 700                    .context("loading regex language")?;
 701                buffer_search_bar
 702                    .update(&mut 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, |this, mut cx| async move {
 852                search.await?;
 853                this.update_in(&mut 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(
 940        &self,
 941        option: SearchOptions,
 942        focus_handle: FocusHandle,
 943        action: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
 944    ) -> impl IntoElement {
 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, |this, mut cx| async move {
1101                    search.await?;
1102                    this.update_in(&mut 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                            Default::default(),
1235                            Default::default(),
1236                            None,
1237                        ) {
1238                            Ok(query) => query.with_replacement(self.replacement(cx)),
1239                            Err(_) => {
1240                                self.query_contains_error = true;
1241                                self.clear_active_searchable_item_matches(window, cx);
1242                                cx.notify();
1243                                return done_rx;
1244                            }
1245                        }
1246                    } else {
1247                        match SearchQuery::text(
1248                            query,
1249                            self.search_options.contains(SearchOptions::WHOLE_WORD),
1250                            self.search_options.contains(SearchOptions::CASE_SENSITIVE),
1251                            false,
1252                            Default::default(),
1253                            Default::default(),
1254                            None,
1255                        ) {
1256                            Ok(query) => query.with_replacement(self.replacement(cx)),
1257                            Err(_) => {
1258                                self.query_contains_error = true;
1259                                self.clear_active_searchable_item_matches(window, cx);
1260                                cx.notify();
1261                                return done_rx;
1262                            }
1263                        }
1264                    }
1265                    .into()
1266                };
1267
1268                self.active_search = Some(query.clone());
1269                let query_text = query.as_str().to_string();
1270
1271                let matches = active_searchable_item.find_matches(query, window, cx);
1272
1273                let active_searchable_item = active_searchable_item.downgrade();
1274                self.pending_search = Some(cx.spawn_in(window, |this, mut cx| async move {
1275                    let matches = matches.await;
1276
1277                    this.update_in(&mut cx, |this, window, cx| {
1278                        if let Some(active_searchable_item) =
1279                            WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx)
1280                        {
1281                            this.searchable_items_with_matches
1282                                .insert(active_searchable_item.downgrade(), matches);
1283
1284                            this.update_match_index(window, cx);
1285                            this.search_history
1286                                .add(&mut this.search_history_cursor, query_text);
1287                            if !this.dismissed {
1288                                let matches = this
1289                                    .searchable_items_with_matches
1290                                    .get(&active_searchable_item.downgrade())
1291                                    .unwrap();
1292                                if matches.is_empty() {
1293                                    active_searchable_item.clear_matches(window, cx);
1294                                } else {
1295                                    active_searchable_item.update_matches(matches, window, cx);
1296                                }
1297                                let _ = done_tx.send(());
1298                            }
1299                            cx.notify();
1300                        }
1301                    })
1302                    .log_err();
1303                }));
1304            }
1305        }
1306        done_rx
1307    }
1308
1309    pub fn update_match_index(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1310        let new_index = self
1311            .active_searchable_item
1312            .as_ref()
1313            .and_then(|searchable_item| {
1314                let matches = self
1315                    .searchable_items_with_matches
1316                    .get(&searchable_item.downgrade())?;
1317                searchable_item.active_match_index(matches, window, cx)
1318            });
1319        if new_index != self.active_match_index {
1320            self.active_match_index = new_index;
1321            cx.notify();
1322        }
1323    }
1324
1325    fn tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context<Self>) {
1326        // Search -> Replace -> Editor
1327        let focus_handle = if self.replace_enabled && self.query_editor_focused {
1328            self.replacement_editor.focus_handle(cx)
1329        } else if let Some(item) = self.active_searchable_item.as_ref() {
1330            item.item_focus_handle(cx)
1331        } else {
1332            return;
1333        };
1334        self.focus(&focus_handle, window, cx);
1335        cx.stop_propagation();
1336    }
1337
1338    fn backtab(&mut self, _: &Backtab, window: &mut Window, cx: &mut Context<Self>) {
1339        // Search -> Replace -> Search
1340        let focus_handle = if self.replace_enabled && self.query_editor_focused {
1341            self.replacement_editor.focus_handle(cx)
1342        } else if self.replacement_editor_focused {
1343            self.query_editor.focus_handle(cx)
1344        } else {
1345            return;
1346        };
1347        self.focus(&focus_handle, window, cx);
1348        cx.stop_propagation();
1349    }
1350
1351    fn next_history_query(
1352        &mut self,
1353        _: &NextHistoryQuery,
1354        window: &mut Window,
1355        cx: &mut Context<Self>,
1356    ) {
1357        if let Some(new_query) = self
1358            .search_history
1359            .next(&mut self.search_history_cursor)
1360            .map(str::to_string)
1361        {
1362            drop(self.search(&new_query, Some(self.search_options), window, cx));
1363        } else {
1364            self.search_history_cursor.reset();
1365            drop(self.search("", Some(self.search_options), window, cx));
1366        }
1367    }
1368
1369    fn previous_history_query(
1370        &mut self,
1371        _: &PreviousHistoryQuery,
1372        window: &mut Window,
1373        cx: &mut Context<Self>,
1374    ) {
1375        if self.query(cx).is_empty() {
1376            if let Some(new_query) = self
1377                .search_history
1378                .current(&mut self.search_history_cursor)
1379                .map(str::to_string)
1380            {
1381                drop(self.search(&new_query, Some(self.search_options), window, cx));
1382                return;
1383            }
1384        }
1385
1386        if let Some(new_query) = self
1387            .search_history
1388            .previous(&mut self.search_history_cursor)
1389            .map(str::to_string)
1390        {
1391            drop(self.search(&new_query, Some(self.search_options), window, cx));
1392        }
1393    }
1394
1395    fn focus(&self, handle: &gpui::FocusHandle, window: &mut Window, cx: &mut Context<Self>) {
1396        cx.on_next_frame(window, |_, window, _| {
1397            window.invalidate_character_coordinates();
1398        });
1399        window.focus(handle);
1400    }
1401
1402    fn toggle_replace(&mut self, _: &ToggleReplace, window: &mut Window, cx: &mut Context<Self>) {
1403        if self.active_searchable_item.is_some() {
1404            self.replace_enabled = !self.replace_enabled;
1405            let handle = if self.replace_enabled {
1406                self.replacement_editor.focus_handle(cx)
1407            } else {
1408                self.query_editor.focus_handle(cx)
1409            };
1410            self.focus(&handle, window, cx);
1411            cx.notify();
1412        }
1413    }
1414
1415    fn replace_next(&mut self, _: &ReplaceNext, window: &mut Window, cx: &mut Context<Self>) {
1416        let mut should_propagate = true;
1417        if !self.dismissed && self.active_search.is_some() {
1418            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1419                if let Some(query) = self.active_search.as_ref() {
1420                    if let Some(matches) = self
1421                        .searchable_items_with_matches
1422                        .get(&searchable_item.downgrade())
1423                    {
1424                        if let Some(active_index) = self.active_match_index {
1425                            let query = query
1426                                .as_ref()
1427                                .clone()
1428                                .with_replacement(self.replacement(cx));
1429                            searchable_item.replace(matches.at(active_index), &query, window, cx);
1430                            self.select_next_match(&SelectNextMatch, window, cx);
1431                        }
1432                        should_propagate = false;
1433                        self.focus_editor(&FocusEditor, window, cx);
1434                    }
1435                }
1436            }
1437        }
1438        if !should_propagate {
1439            cx.stop_propagation();
1440        }
1441    }
1442
1443    pub fn replace_all(&mut self, _: &ReplaceAll, window: &mut Window, cx: &mut Context<Self>) {
1444        if !self.dismissed && self.active_search.is_some() {
1445            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1446                if let Some(query) = self.active_search.as_ref() {
1447                    if let Some(matches) = self
1448                        .searchable_items_with_matches
1449                        .get(&searchable_item.downgrade())
1450                    {
1451                        let query = query
1452                            .as_ref()
1453                            .clone()
1454                            .with_replacement(self.replacement(cx));
1455                        searchable_item.replace_all(&mut matches.iter(), &query, window, cx);
1456                    }
1457                }
1458            }
1459        }
1460    }
1461
1462    pub fn match_exists(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
1463        self.update_match_index(window, cx);
1464        self.active_match_index.is_some()
1465    }
1466
1467    pub fn should_use_smartcase_search(&mut self, cx: &mut Context<Self>) -> bool {
1468        EditorSettings::get_global(cx).use_smartcase_search
1469    }
1470
1471    pub fn is_contains_uppercase(&mut self, str: &String) -> bool {
1472        str.chars().any(|c| c.is_uppercase())
1473    }
1474
1475    fn smartcase(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1476        if self.should_use_smartcase_search(cx) {
1477            let query = self.query(cx);
1478            if !query.is_empty() {
1479                let is_case = self.is_contains_uppercase(&query);
1480                if self.has_search_option(SearchOptions::CASE_SENSITIVE) != is_case {
1481                    self.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
1482                }
1483            }
1484        }
1485    }
1486
1487    fn adjust_query_regex_language(&self, cx: &mut App) {
1488        let enable = self.search_options.contains(SearchOptions::REGEX);
1489        let query_buffer = self
1490            .query_editor
1491            .read(cx)
1492            .buffer()
1493            .read(cx)
1494            .as_singleton()
1495            .expect("query editor should be backed by a singleton buffer");
1496        if enable {
1497            if let Some(regex_language) = self.regex_language.clone() {
1498                query_buffer.update(cx, |query_buffer, cx| {
1499                    query_buffer.set_language(Some(regex_language), cx);
1500                })
1501            }
1502        } else {
1503            query_buffer.update(cx, |query_buffer, cx| {
1504                query_buffer.set_language(None, cx);
1505            })
1506        }
1507    }
1508}
1509
1510#[cfg(test)]
1511mod tests {
1512    use std::ops::Range;
1513
1514    use super::*;
1515    use editor::{display_map::DisplayRow, DisplayPoint, Editor, MultiBuffer, SearchSettings};
1516    use gpui::{Hsla, TestAppContext, UpdateGlobal, VisualTestContext};
1517    use language::{Buffer, Point};
1518    use project::Project;
1519    use settings::SettingsStore;
1520    use smol::stream::StreamExt as _;
1521    use unindent::Unindent as _;
1522
1523    fn init_globals(cx: &mut TestAppContext) {
1524        cx.update(|cx| {
1525            let store = settings::SettingsStore::test(cx);
1526            cx.set_global(store);
1527            workspace::init_settings(cx);
1528            editor::init(cx);
1529
1530            language::init(cx);
1531            Project::init_settings(cx);
1532            theme::init(theme::LoadThemes::JustBase, cx);
1533            crate::init(cx);
1534        });
1535    }
1536
1537    fn init_test(
1538        cx: &mut TestAppContext,
1539    ) -> (
1540        Entity<Editor>,
1541        Entity<BufferSearchBar>,
1542        &mut VisualTestContext,
1543    ) {
1544        init_globals(cx);
1545        let buffer = cx.new(|cx| {
1546            Buffer::local(
1547                r#"
1548                A regular expression (shortened as regex or regexp;[1] also referred to as
1549                rational expression[2][3]) is a sequence of characters that specifies a search
1550                pattern in text. Usually such patterns are used by string-searching algorithms
1551                for "find" or "find and replace" operations on strings, or for input validation.
1552                "#
1553                .unindent(),
1554                cx,
1555            )
1556        });
1557        let cx = cx.add_empty_window();
1558        let editor =
1559            cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
1560
1561        let search_bar = cx.new_window_entity(|window, cx| {
1562            let mut search_bar = BufferSearchBar::new(None, window, cx);
1563            search_bar.set_active_pane_item(Some(&editor), window, cx);
1564            search_bar.show(window, cx);
1565            search_bar
1566        });
1567
1568        (editor, search_bar, cx)
1569    }
1570
1571    #[gpui::test]
1572    async fn test_search_simple(cx: &mut TestAppContext) {
1573        let (editor, search_bar, cx) = init_test(cx);
1574        let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
1575            background_highlights
1576                .into_iter()
1577                .map(|(range, _)| range)
1578                .collect::<Vec<_>>()
1579        };
1580        // Search for a string that appears with different casing.
1581        // By default, search is case-insensitive.
1582        search_bar
1583            .update_in(cx, |search_bar, window, cx| {
1584                search_bar.search("us", None, window, cx)
1585            })
1586            .await
1587            .unwrap();
1588        editor.update_in(cx, |editor, window, cx| {
1589            assert_eq!(
1590                display_points_of(editor.all_text_background_highlights(window, cx)),
1591                &[
1592                    DisplayPoint::new(DisplayRow(2), 17)..DisplayPoint::new(DisplayRow(2), 19),
1593                    DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),
1594                ]
1595            );
1596        });
1597
1598        // Switch to a case sensitive search.
1599        search_bar.update_in(cx, |search_bar, window, cx| {
1600            search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
1601        });
1602        let mut editor_notifications = cx.notifications(&editor);
1603        editor_notifications.next().await;
1604        editor.update_in(cx, |editor, window, cx| {
1605            assert_eq!(
1606                display_points_of(editor.all_text_background_highlights(window, cx)),
1607                &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
1608            );
1609        });
1610
1611        // Search for a string that appears both as a whole word and
1612        // within other words. By default, all results are found.
1613        search_bar
1614            .update_in(cx, |search_bar, window, cx| {
1615                search_bar.search("or", None, window, cx)
1616            })
1617            .await
1618            .unwrap();
1619        editor.update_in(cx, |editor, window, cx| {
1620            assert_eq!(
1621                display_points_of(editor.all_text_background_highlights(window, cx)),
1622                &[
1623                    DisplayPoint::new(DisplayRow(0), 24)..DisplayPoint::new(DisplayRow(0), 26),
1624                    DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1625                    DisplayPoint::new(DisplayRow(2), 71)..DisplayPoint::new(DisplayRow(2), 73),
1626                    DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 3),
1627                    DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
1628                    DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
1629                    DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 62),
1630                ]
1631            );
1632        });
1633
1634        // Switch to a whole word search.
1635        search_bar.update_in(cx, |search_bar, window, cx| {
1636            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
1637        });
1638        let mut editor_notifications = cx.notifications(&editor);
1639        editor_notifications.next().await;
1640        editor.update_in(cx, |editor, window, cx| {
1641            assert_eq!(
1642                display_points_of(editor.all_text_background_highlights(window, cx)),
1643                &[
1644                    DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1645                    DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
1646                    DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
1647                ]
1648            );
1649        });
1650
1651        editor.update_in(cx, |editor, window, cx| {
1652            editor.change_selections(None, window, cx, |s| {
1653                s.select_display_ranges([
1654                    DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
1655                ])
1656            });
1657        });
1658        search_bar.update_in(cx, |search_bar, window, cx| {
1659            assert_eq!(search_bar.active_match_index, Some(0));
1660            search_bar.select_next_match(&SelectNextMatch, window, cx);
1661            assert_eq!(
1662                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1663                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1664            );
1665        });
1666        search_bar.update(cx, |search_bar, _| {
1667            assert_eq!(search_bar.active_match_index, Some(0));
1668        });
1669
1670        search_bar.update_in(cx, |search_bar, window, cx| {
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(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1675            );
1676        });
1677        search_bar.update(cx, |search_bar, _| {
1678            assert_eq!(search_bar.active_match_index, Some(1));
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), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1686            );
1687        });
1688        search_bar.update(cx, |search_bar, _| {
1689            assert_eq!(search_bar.active_match_index, Some(2));
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(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1697            );
1698        });
1699        search_bar.update(cx, |search_bar, _| {
1700            assert_eq!(search_bar.active_match_index, Some(0));
1701        });
1702
1703        search_bar.update_in(cx, |search_bar, window, cx| {
1704            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1705            assert_eq!(
1706                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1707                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1708            );
1709        });
1710        search_bar.update(cx, |search_bar, _| {
1711            assert_eq!(search_bar.active_match_index, Some(2));
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), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1719            );
1720        });
1721        search_bar.update(cx, |search_bar, _| {
1722            assert_eq!(search_bar.active_match_index, Some(1));
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(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1730            );
1731        });
1732        search_bar.update(cx, |search_bar, _| {
1733            assert_eq!(search_bar.active_match_index, Some(0));
1734        });
1735
1736        // Park the cursor in between matches and ensure that going to the previous match selects
1737        // the closest match to the left.
1738        editor.update_in(cx, |editor, window, cx| {
1739            editor.change_selections(None, window, cx, |s| {
1740                s.select_display_ranges([
1741                    DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
1742                ])
1743            });
1744        });
1745        search_bar.update_in(cx, |search_bar, window, cx| {
1746            assert_eq!(search_bar.active_match_index, Some(1));
1747            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1748            assert_eq!(
1749                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1750                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1751            );
1752        });
1753        search_bar.update(cx, |search_bar, _| {
1754            assert_eq!(search_bar.active_match_index, Some(0));
1755        });
1756
1757        // Park the cursor in between matches and ensure that going to the next match selects the
1758        // closest match to the right.
1759        editor.update_in(cx, |editor, window, cx| {
1760            editor.change_selections(None, window, cx, |s| {
1761                s.select_display_ranges([
1762                    DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
1763                ])
1764            });
1765        });
1766        search_bar.update_in(cx, |search_bar, window, cx| {
1767            assert_eq!(search_bar.active_match_index, Some(1));
1768            search_bar.select_next_match(&SelectNextMatch, window, cx);
1769            assert_eq!(
1770                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1771                [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1772            );
1773        });
1774        search_bar.update(cx, |search_bar, _| {
1775            assert_eq!(search_bar.active_match_index, Some(1));
1776        });
1777
1778        // Park the cursor after the last match and ensure that going to the previous match selects
1779        // the last match.
1780        editor.update_in(cx, |editor, window, cx| {
1781            editor.change_selections(None, window, cx, |s| {
1782                s.select_display_ranges([
1783                    DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
1784                ])
1785            });
1786        });
1787        search_bar.update_in(cx, |search_bar, window, cx| {
1788            assert_eq!(search_bar.active_match_index, Some(2));
1789            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1790            assert_eq!(
1791                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1792                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1793            );
1794        });
1795        search_bar.update(cx, |search_bar, _| {
1796            assert_eq!(search_bar.active_match_index, Some(2));
1797        });
1798
1799        // Park the cursor after the last match and ensure that going to the next match selects the
1800        // first match.
1801        editor.update_in(cx, |editor, window, cx| {
1802            editor.change_selections(None, window, cx, |s| {
1803                s.select_display_ranges([
1804                    DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
1805                ])
1806            });
1807        });
1808        search_bar.update_in(cx, |search_bar, window, cx| {
1809            assert_eq!(search_bar.active_match_index, Some(2));
1810            search_bar.select_next_match(&SelectNextMatch, window, cx);
1811            assert_eq!(
1812                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1813                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1814            );
1815        });
1816        search_bar.update(cx, |search_bar, _| {
1817            assert_eq!(search_bar.active_match_index, Some(0));
1818        });
1819
1820        // Park the cursor before the first match and ensure that going to the previous match
1821        // selects the last match.
1822        editor.update_in(cx, |editor, window, cx| {
1823            editor.change_selections(None, window, cx, |s| {
1824                s.select_display_ranges([
1825                    DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
1826                ])
1827            });
1828        });
1829        search_bar.update_in(cx, |search_bar, window, cx| {
1830            assert_eq!(search_bar.active_match_index, Some(0));
1831            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1832            assert_eq!(
1833                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1834                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1835            );
1836        });
1837        search_bar.update(cx, |search_bar, _| {
1838            assert_eq!(search_bar.active_match_index, Some(2));
1839        });
1840    }
1841
1842    fn display_points_of(
1843        background_highlights: Vec<(Range<DisplayPoint>, Hsla)>,
1844    ) -> Vec<Range<DisplayPoint>> {
1845        background_highlights
1846            .into_iter()
1847            .map(|(range, _)| range)
1848            .collect::<Vec<_>>()
1849    }
1850
1851    #[gpui::test]
1852    async fn test_search_option_handling(cx: &mut TestAppContext) {
1853        let (editor, search_bar, cx) = init_test(cx);
1854
1855        // show with options should make current search case sensitive
1856        search_bar
1857            .update_in(cx, |search_bar, window, cx| {
1858                search_bar.show(window, cx);
1859                search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), window, cx)
1860            })
1861            .await
1862            .unwrap();
1863        editor.update_in(cx, |editor, window, cx| {
1864            assert_eq!(
1865                display_points_of(editor.all_text_background_highlights(window, cx)),
1866                &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
1867            );
1868        });
1869
1870        // search_suggested should restore default options
1871        search_bar.update_in(cx, |search_bar, window, cx| {
1872            search_bar.search_suggested(window, cx);
1873            assert_eq!(search_bar.search_options, SearchOptions::NONE)
1874        });
1875
1876        // toggling a search option should update the defaults
1877        search_bar
1878            .update_in(cx, |search_bar, window, cx| {
1879                search_bar.search("regex", Some(SearchOptions::CASE_SENSITIVE), window, cx)
1880            })
1881            .await
1882            .unwrap();
1883        search_bar.update_in(cx, |search_bar, window, cx| {
1884            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx)
1885        });
1886        let mut editor_notifications = cx.notifications(&editor);
1887        editor_notifications.next().await;
1888        editor.update_in(cx, |editor, window, cx| {
1889            assert_eq!(
1890                display_points_of(editor.all_text_background_highlights(window, cx)),
1891                &[DisplayPoint::new(DisplayRow(0), 35)..DisplayPoint::new(DisplayRow(0), 40),]
1892            );
1893        });
1894
1895        // defaults should still include whole word
1896        search_bar.update_in(cx, |search_bar, window, cx| {
1897            search_bar.search_suggested(window, cx);
1898            assert_eq!(
1899                search_bar.search_options,
1900                SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD
1901            )
1902        });
1903    }
1904
1905    #[gpui::test]
1906    async fn test_search_select_all_matches(cx: &mut TestAppContext) {
1907        init_globals(cx);
1908        let buffer_text = r#"
1909        A regular expression (shortened as regex or regexp;[1] also referred to as
1910        rational expression[2][3]) is a sequence of characters that specifies a search
1911        pattern in text. Usually such patterns are used by string-searching algorithms
1912        for "find" or "find and replace" operations on strings, or for input validation.
1913        "#
1914        .unindent();
1915        let expected_query_matches_count = buffer_text
1916            .chars()
1917            .filter(|c| c.eq_ignore_ascii_case(&'a'))
1918            .count();
1919        assert!(
1920            expected_query_matches_count > 1,
1921            "Should pick a query with multiple results"
1922        );
1923        let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
1924        let window = cx.add_window(|_, _| gpui::Empty);
1925
1926        let editor = window.build_entity(cx, |window, cx| {
1927            Editor::for_buffer(buffer.clone(), None, window, cx)
1928        });
1929
1930        let search_bar = window.build_entity(cx, |window, cx| {
1931            let mut search_bar = BufferSearchBar::new(None, window, cx);
1932            search_bar.set_active_pane_item(Some(&editor), window, cx);
1933            search_bar.show(window, cx);
1934            search_bar
1935        });
1936
1937        window
1938            .update(cx, |_, window, cx| {
1939                search_bar.update(cx, |search_bar, cx| {
1940                    search_bar.search("a", None, window, cx)
1941                })
1942            })
1943            .unwrap()
1944            .await
1945            .unwrap();
1946        let initial_selections = window
1947            .update(cx, |_, window, cx| {
1948                search_bar.update(cx, |search_bar, cx| {
1949                    let handle = search_bar.query_editor.focus_handle(cx);
1950                    window.focus(&handle);
1951                    search_bar.activate_current_match(window, cx);
1952                });
1953                assert!(
1954                    !editor.read(cx).is_focused(window),
1955                    "Initially, the editor should not be focused"
1956                );
1957                let initial_selections = editor.update(cx, |editor, cx| {
1958                    let initial_selections = editor.selections.display_ranges(cx);
1959                    assert_eq!(
1960                        initial_selections.len(), 1,
1961                        "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
1962                    );
1963                    initial_selections
1964                });
1965                search_bar.update(cx, |search_bar, cx| {
1966                    assert_eq!(search_bar.active_match_index, Some(0));
1967                    let handle = search_bar.query_editor.focus_handle(cx);
1968                    window.focus(&handle);
1969                    search_bar.select_all_matches(&SelectAllMatches, window, cx);
1970                });
1971                assert!(
1972                    editor.read(cx).is_focused(window),
1973                    "Should focus editor after successful SelectAllMatches"
1974                );
1975                search_bar.update(cx, |search_bar, cx| {
1976                    let all_selections =
1977                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1978                    assert_eq!(
1979                        all_selections.len(),
1980                        expected_query_matches_count,
1981                        "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1982                    );
1983                    assert_eq!(
1984                        search_bar.active_match_index,
1985                        Some(0),
1986                        "Match index should not change after selecting all matches"
1987                    );
1988                });
1989
1990                search_bar.update(cx, |this, cx| this.select_next_match(&SelectNextMatch, window, cx));
1991                initial_selections
1992            }).unwrap();
1993
1994        window
1995            .update(cx, |_, window, cx| {
1996                assert!(
1997                    editor.read(cx).is_focused(window),
1998                    "Should still have editor focused after SelectNextMatch"
1999                );
2000                search_bar.update(cx, |search_bar, cx| {
2001                    let all_selections =
2002                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2003                    assert_eq!(
2004                        all_selections.len(),
2005                        1,
2006                        "On next match, should deselect items and select the next match"
2007                    );
2008                    assert_ne!(
2009                        all_selections, initial_selections,
2010                        "Next match should be different from the first selection"
2011                    );
2012                    assert_eq!(
2013                        search_bar.active_match_index,
2014                        Some(1),
2015                        "Match index should be updated to the next one"
2016                    );
2017                    let handle = search_bar.query_editor.focus_handle(cx);
2018                    window.focus(&handle);
2019                    search_bar.select_all_matches(&SelectAllMatches, window, cx);
2020                });
2021            })
2022            .unwrap();
2023        window
2024            .update(cx, |_, window, cx| {
2025                assert!(
2026                    editor.read(cx).is_focused(window),
2027                    "Should focus editor after successful SelectAllMatches"
2028                );
2029                search_bar.update(cx, |search_bar, cx| {
2030                    let all_selections =
2031                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2032                    assert_eq!(
2033                    all_selections.len(),
2034                    expected_query_matches_count,
2035                    "Should select all `a` characters in the buffer, but got: {all_selections:?}"
2036                );
2037                    assert_eq!(
2038                        search_bar.active_match_index,
2039                        Some(1),
2040                        "Match index should not change after selecting all matches"
2041                    );
2042                });
2043                search_bar.update(cx, |search_bar, cx| {
2044                    search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2045                });
2046            })
2047            .unwrap();
2048        let last_match_selections = window
2049            .update(cx, |_, window, cx| {
2050                assert!(
2051                    editor.read(cx).is_focused(window),
2052                    "Should still have editor focused after SelectPreviousMatch"
2053                );
2054
2055                search_bar.update(cx, |search_bar, cx| {
2056                    let all_selections =
2057                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2058                    assert_eq!(
2059                        all_selections.len(),
2060                        1,
2061                        "On previous match, should deselect items and select the previous item"
2062                    );
2063                    assert_eq!(
2064                        all_selections, initial_selections,
2065                        "Previous match should be the same as the first selection"
2066                    );
2067                    assert_eq!(
2068                        search_bar.active_match_index,
2069                        Some(0),
2070                        "Match index should be updated to the previous one"
2071                    );
2072                    all_selections
2073                })
2074            })
2075            .unwrap();
2076
2077        window
2078            .update(cx, |_, window, cx| {
2079                search_bar.update(cx, |search_bar, cx| {
2080                    let handle = search_bar.query_editor.focus_handle(cx);
2081                    window.focus(&handle);
2082                    search_bar.search("abas_nonexistent_match", None, window, cx)
2083                })
2084            })
2085            .unwrap()
2086            .await
2087            .unwrap();
2088        window
2089            .update(cx, |_, window, cx| {
2090                search_bar.update(cx, |search_bar, cx| {
2091                    search_bar.select_all_matches(&SelectAllMatches, window, cx);
2092                });
2093                assert!(
2094                    editor.update(cx, |this, _cx| !this.is_focused(window)),
2095                    "Should not switch focus to editor if SelectAllMatches does not find any matches"
2096                );
2097                search_bar.update(cx, |search_bar, cx| {
2098                    let all_selections =
2099                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2100                    assert_eq!(
2101                        all_selections, last_match_selections,
2102                        "Should not select anything new if there are no matches"
2103                    );
2104                    assert!(
2105                        search_bar.active_match_index.is_none(),
2106                        "For no matches, there should be no active match index"
2107                    );
2108                });
2109            })
2110            .unwrap();
2111    }
2112
2113    #[gpui::test]
2114    async fn test_search_query_with_match_whole_word(cx: &mut TestAppContext) {
2115        init_globals(cx);
2116        let buffer_text = r#"
2117        self.buffer.update(cx, |buffer, cx| {
2118            buffer.edit(
2119                edits,
2120                Some(AutoindentMode::Block {
2121                    original_indent_columns,
2122                }),
2123                cx,
2124            )
2125        });
2126
2127        this.buffer.update(cx, |buffer, cx| {
2128            buffer.edit([(end_of_line..start_of_next_line, replace)], None, cx)
2129        });
2130        "#
2131        .unindent();
2132        let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
2133        let cx = cx.add_empty_window();
2134
2135        let editor =
2136            cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
2137
2138        let search_bar = cx.new_window_entity(|window, cx| {
2139            let mut search_bar = BufferSearchBar::new(None, window, cx);
2140            search_bar.set_active_pane_item(Some(&editor), window, cx);
2141            search_bar.show(window, cx);
2142            search_bar
2143        });
2144
2145        search_bar
2146            .update_in(cx, |search_bar, window, cx| {
2147                search_bar.search(
2148                    "edit\\(",
2149                    Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
2150                    window,
2151                    cx,
2152                )
2153            })
2154            .await
2155            .unwrap();
2156
2157        search_bar.update_in(cx, |search_bar, window, cx| {
2158            search_bar.select_all_matches(&SelectAllMatches, window, cx);
2159        });
2160        search_bar.update(cx, |_, cx| {
2161            let all_selections =
2162                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2163            assert_eq!(
2164                all_selections.len(),
2165                2,
2166                "Should select all `edit(` in the buffer, but got: {all_selections:?}"
2167            );
2168        });
2169
2170        search_bar
2171            .update_in(cx, |search_bar, window, cx| {
2172                search_bar.search(
2173                    "edit(",
2174                    Some(SearchOptions::WHOLE_WORD | SearchOptions::CASE_SENSITIVE),
2175                    window,
2176                    cx,
2177                )
2178            })
2179            .await
2180            .unwrap();
2181
2182        search_bar.update_in(cx, |search_bar, window, cx| {
2183            search_bar.select_all_matches(&SelectAllMatches, window, cx);
2184        });
2185        search_bar.update(cx, |_, cx| {
2186            let all_selections =
2187                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2188            assert_eq!(
2189                all_selections.len(),
2190                2,
2191                "Should select all `edit(` in the buffer, but got: {all_selections:?}"
2192            );
2193        });
2194    }
2195
2196    #[gpui::test]
2197    async fn test_search_query_history(cx: &mut TestAppContext) {
2198        init_globals(cx);
2199        let buffer_text = r#"
2200        A regular expression (shortened as regex or regexp;[1] also referred to as
2201        rational expression[2][3]) is a sequence of characters that specifies a search
2202        pattern in text. Usually such patterns are used by string-searching algorithms
2203        for "find" or "find and replace" operations on strings, or for input validation.
2204        "#
2205        .unindent();
2206        let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
2207        let cx = cx.add_empty_window();
2208
2209        let editor =
2210            cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
2211
2212        let search_bar = cx.new_window_entity(|window, cx| {
2213            let mut search_bar = BufferSearchBar::new(None, window, cx);
2214            search_bar.set_active_pane_item(Some(&editor), window, cx);
2215            search_bar.show(window, cx);
2216            search_bar
2217        });
2218
2219        // Add 3 search items into the history.
2220        search_bar
2221            .update_in(cx, |search_bar, window, cx| {
2222                search_bar.search("a", None, window, cx)
2223            })
2224            .await
2225            .unwrap();
2226        search_bar
2227            .update_in(cx, |search_bar, window, cx| {
2228                search_bar.search("b", None, window, cx)
2229            })
2230            .await
2231            .unwrap();
2232        search_bar
2233            .update_in(cx, |search_bar, window, cx| {
2234                search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), window, cx)
2235            })
2236            .await
2237            .unwrap();
2238        // Ensure that the latest search is active.
2239        search_bar.update(cx, |search_bar, cx| {
2240            assert_eq!(search_bar.query(cx), "c");
2241            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2242        });
2243
2244        // Next history query after the latest should set the query to the empty string.
2245        search_bar.update_in(cx, |search_bar, window, cx| {
2246            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2247        });
2248        search_bar.update(cx, |search_bar, cx| {
2249            assert_eq!(search_bar.query(cx), "");
2250            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2251        });
2252        search_bar.update_in(cx, |search_bar, window, cx| {
2253            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2254        });
2255        search_bar.update(cx, |search_bar, cx| {
2256            assert_eq!(search_bar.query(cx), "");
2257            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2258        });
2259
2260        // First previous query for empty current query should set the query to the latest.
2261        search_bar.update_in(cx, |search_bar, window, cx| {
2262            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2263        });
2264        search_bar.update(cx, |search_bar, cx| {
2265            assert_eq!(search_bar.query(cx), "c");
2266            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2267        });
2268
2269        // Further previous items should go over the history in reverse order.
2270        search_bar.update_in(cx, |search_bar, window, cx| {
2271            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2272        });
2273        search_bar.update(cx, |search_bar, cx| {
2274            assert_eq!(search_bar.query(cx), "b");
2275            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2276        });
2277
2278        // Previous items should never go behind the first history item.
2279        search_bar.update_in(cx, |search_bar, window, cx| {
2280            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2281        });
2282        search_bar.update(cx, |search_bar, cx| {
2283            assert_eq!(search_bar.query(cx), "a");
2284            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2285        });
2286        search_bar.update_in(cx, |search_bar, window, cx| {
2287            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2288        });
2289        search_bar.update(cx, |search_bar, cx| {
2290            assert_eq!(search_bar.query(cx), "a");
2291            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2292        });
2293
2294        // Next items should go over the history in the original order.
2295        search_bar.update_in(cx, |search_bar, window, cx| {
2296            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2297        });
2298        search_bar.update(cx, |search_bar, cx| {
2299            assert_eq!(search_bar.query(cx), "b");
2300            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2301        });
2302
2303        search_bar
2304            .update_in(cx, |search_bar, window, cx| {
2305                search_bar.search("ba", None, window, cx)
2306            })
2307            .await
2308            .unwrap();
2309        search_bar.update(cx, |search_bar, cx| {
2310            assert_eq!(search_bar.query(cx), "ba");
2311            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2312        });
2313
2314        // New search input should add another entry to history and move the selection to the end of the history.
2315        search_bar.update_in(cx, |search_bar, window, cx| {
2316            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2317        });
2318        search_bar.update(cx, |search_bar, cx| {
2319            assert_eq!(search_bar.query(cx), "c");
2320            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2321        });
2322        search_bar.update_in(cx, |search_bar, window, cx| {
2323            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2324        });
2325        search_bar.update(cx, |search_bar, cx| {
2326            assert_eq!(search_bar.query(cx), "b");
2327            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2328        });
2329        search_bar.update_in(cx, |search_bar, window, cx| {
2330            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2331        });
2332        search_bar.update(cx, |search_bar, cx| {
2333            assert_eq!(search_bar.query(cx), "c");
2334            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2335        });
2336        search_bar.update_in(cx, |search_bar, window, cx| {
2337            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2338        });
2339        search_bar.update(cx, |search_bar, cx| {
2340            assert_eq!(search_bar.query(cx), "ba");
2341            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2342        });
2343        search_bar.update_in(cx, |search_bar, window, cx| {
2344            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2345        });
2346        search_bar.update(cx, |search_bar, cx| {
2347            assert_eq!(search_bar.query(cx), "");
2348            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2349        });
2350    }
2351
2352    #[gpui::test]
2353    async fn test_replace_simple(cx: &mut TestAppContext) {
2354        let (editor, search_bar, cx) = init_test(cx);
2355
2356        search_bar
2357            .update_in(cx, |search_bar, window, cx| {
2358                search_bar.search("expression", None, window, cx)
2359            })
2360            .await
2361            .unwrap();
2362
2363        search_bar.update_in(cx, |search_bar, window, cx| {
2364            search_bar.replacement_editor.update(cx, |editor, cx| {
2365                // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally.
2366                editor.set_text("expr$1", window, cx);
2367            });
2368            search_bar.replace_all(&ReplaceAll, window, cx)
2369        });
2370        assert_eq!(
2371            editor.update(cx, |this, cx| { this.text(cx) }),
2372            r#"
2373        A regular expr$1 (shortened as regex or regexp;[1] also referred to as
2374        rational expr$1[2][3]) is a sequence of characters that specifies a search
2375        pattern in text. Usually such patterns are used by string-searching algorithms
2376        for "find" or "find and replace" operations on strings, or for input validation.
2377        "#
2378            .unindent()
2379        );
2380
2381        // Search for word boundaries and replace just a single one.
2382        search_bar
2383            .update_in(cx, |search_bar, window, cx| {
2384                search_bar.search("or", Some(SearchOptions::WHOLE_WORD), window, cx)
2385            })
2386            .await
2387            .unwrap();
2388
2389        search_bar.update_in(cx, |search_bar, window, cx| {
2390            search_bar.replacement_editor.update(cx, |editor, cx| {
2391                editor.set_text("banana", window, cx);
2392            });
2393            search_bar.replace_next(&ReplaceNext, window, cx)
2394        });
2395        // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text.
2396        assert_eq!(
2397            editor.update(cx, |this, cx| { this.text(cx) }),
2398            r#"
2399        A regular expr$1 (shortened as regex banana regexp;[1] also referred to as
2400        rational expr$1[2][3]) is a sequence of characters that specifies a search
2401        pattern in text. Usually such patterns are used by string-searching algorithms
2402        for "find" or "find and replace" operations on strings, or for input validation.
2403        "#
2404            .unindent()
2405        );
2406        // Let's turn on regex mode.
2407        search_bar
2408            .update_in(cx, |search_bar, window, cx| {
2409                search_bar.search("\\[([^\\]]+)\\]", Some(SearchOptions::REGEX), window, cx)
2410            })
2411            .await
2412            .unwrap();
2413        search_bar.update_in(cx, |search_bar, window, cx| {
2414            search_bar.replacement_editor.update(cx, |editor, cx| {
2415                editor.set_text("${1}number", window, cx);
2416            });
2417            search_bar.replace_all(&ReplaceAll, window, cx)
2418        });
2419        assert_eq!(
2420            editor.update(cx, |this, cx| { this.text(cx) }),
2421            r#"
2422        A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2423        rational expr$12number3number) is a sequence of characters that specifies a search
2424        pattern in text. Usually such patterns are used by string-searching algorithms
2425        for "find" or "find and replace" operations on strings, or for input validation.
2426        "#
2427            .unindent()
2428        );
2429        // Now with a whole-word twist.
2430        search_bar
2431            .update_in(cx, |search_bar, window, cx| {
2432                search_bar.search(
2433                    "a\\w+s",
2434                    Some(SearchOptions::REGEX | SearchOptions::WHOLE_WORD),
2435                    window,
2436                    cx,
2437                )
2438            })
2439            .await
2440            .unwrap();
2441        search_bar.update_in(cx, |search_bar, window, cx| {
2442            search_bar.replacement_editor.update(cx, |editor, cx| {
2443                editor.set_text("things", window, cx);
2444            });
2445            search_bar.replace_all(&ReplaceAll, window, cx)
2446        });
2447        // The only word affected by this edit should be `algorithms`, even though there's a bunch
2448        // of words in this text that would match this regex if not for WHOLE_WORD.
2449        assert_eq!(
2450            editor.update(cx, |this, cx| { this.text(cx) }),
2451            r#"
2452        A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2453        rational expr$12number3number) is a sequence of characters that specifies a search
2454        pattern in text. Usually such patterns are used by string-searching things
2455        for "find" or "find and replace" operations on strings, or for input validation.
2456        "#
2457            .unindent()
2458        );
2459    }
2460
2461    struct ReplacementTestParams<'a> {
2462        editor: &'a Entity<Editor>,
2463        search_bar: &'a Entity<BufferSearchBar>,
2464        cx: &'a mut VisualTestContext,
2465        search_text: &'static str,
2466        search_options: Option<SearchOptions>,
2467        replacement_text: &'static str,
2468        replace_all: bool,
2469        expected_text: String,
2470    }
2471
2472    async fn run_replacement_test(options: ReplacementTestParams<'_>) {
2473        options
2474            .search_bar
2475            .update_in(options.cx, |search_bar, window, cx| {
2476                if let Some(options) = options.search_options {
2477                    search_bar.set_search_options(options, cx);
2478                }
2479                search_bar.search(options.search_text, options.search_options, window, cx)
2480            })
2481            .await
2482            .unwrap();
2483
2484        options
2485            .search_bar
2486            .update_in(options.cx, |search_bar, window, cx| {
2487                search_bar.replacement_editor.update(cx, |editor, cx| {
2488                    editor.set_text(options.replacement_text, window, cx);
2489                });
2490
2491                if options.replace_all {
2492                    search_bar.replace_all(&ReplaceAll, window, cx)
2493                } else {
2494                    search_bar.replace_next(&ReplaceNext, window, cx)
2495                }
2496            });
2497
2498        assert_eq!(
2499            options
2500                .editor
2501                .update(options.cx, |this, cx| { this.text(cx) }),
2502            options.expected_text
2503        );
2504    }
2505
2506    #[gpui::test]
2507    async fn test_replace_special_characters(cx: &mut TestAppContext) {
2508        let (editor, search_bar, cx) = init_test(cx);
2509
2510        run_replacement_test(ReplacementTestParams {
2511            editor: &editor,
2512            search_bar: &search_bar,
2513            cx,
2514            search_text: "expression",
2515            search_options: None,
2516            replacement_text: r"\n",
2517            replace_all: true,
2518            expected_text: r#"
2519            A regular \n (shortened as regex or regexp;[1] also referred to as
2520            rational \n[2][3]) is a sequence of characters that specifies a search
2521            pattern in text. Usually such patterns are used by string-searching algorithms
2522            for "find" or "find and replace" operations on strings, or for input validation.
2523            "#
2524            .unindent(),
2525        })
2526        .await;
2527
2528        run_replacement_test(ReplacementTestParams {
2529            editor: &editor,
2530            search_bar: &search_bar,
2531            cx,
2532            search_text: "or",
2533            search_options: Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
2534            replacement_text: r"\\\n\\\\",
2535            replace_all: false,
2536            expected_text: r#"
2537            A regular \n (shortened as regex \
2538            \\ regexp;[1] also referred to as
2539            rational \n[2][3]) is a sequence of characters that specifies a search
2540            pattern in text. Usually such patterns are used by string-searching algorithms
2541            for "find" or "find and replace" operations on strings, or for input validation.
2542            "#
2543            .unindent(),
2544        })
2545        .await;
2546
2547        run_replacement_test(ReplacementTestParams {
2548            editor: &editor,
2549            search_bar: &search_bar,
2550            cx,
2551            search_text: r"(that|used) ",
2552            search_options: Some(SearchOptions::REGEX),
2553            replacement_text: r"$1\n",
2554            replace_all: true,
2555            expected_text: r#"
2556            A regular \n (shortened as regex \
2557            \\ regexp;[1] also referred to as
2558            rational \n[2][3]) is a sequence of characters that
2559            specifies a search
2560            pattern in text. Usually such patterns are used
2561            by string-searching algorithms
2562            for "find" or "find and replace" operations on strings, or for input validation.
2563            "#
2564            .unindent(),
2565        })
2566        .await;
2567    }
2568
2569    #[gpui::test]
2570    async fn test_find_matches_in_selections_singleton_buffer_multiple_selections(
2571        cx: &mut TestAppContext,
2572    ) {
2573        init_globals(cx);
2574        let buffer = cx.new(|cx| {
2575            Buffer::local(
2576                r#"
2577                aaa bbb aaa ccc
2578                aaa bbb aaa ccc
2579                aaa bbb aaa ccc
2580                aaa bbb aaa ccc
2581                aaa bbb aaa ccc
2582                aaa bbb aaa ccc
2583                "#
2584                .unindent(),
2585                cx,
2586            )
2587        });
2588        let cx = cx.add_empty_window();
2589        let editor =
2590            cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
2591
2592        let search_bar = cx.new_window_entity(|window, cx| {
2593            let mut search_bar = BufferSearchBar::new(None, window, cx);
2594            search_bar.set_active_pane_item(Some(&editor), window, cx);
2595            search_bar.show(window, cx);
2596            search_bar
2597        });
2598
2599        editor.update_in(cx, |editor, window, cx| {
2600            editor.change_selections(None, window, cx, |s| {
2601                s.select_ranges(vec![Point::new(1, 0)..Point::new(2, 4)])
2602            })
2603        });
2604
2605        search_bar.update_in(cx, |search_bar, window, cx| {
2606            let deploy = Deploy {
2607                focus: true,
2608                replace_enabled: false,
2609                selection_search_enabled: true,
2610            };
2611            search_bar.deploy(&deploy, window, cx);
2612        });
2613
2614        cx.run_until_parked();
2615
2616        search_bar
2617            .update_in(cx, |search_bar, window, cx| {
2618                search_bar.search("aaa", None, window, cx)
2619            })
2620            .await
2621            .unwrap();
2622
2623        editor.update(cx, |editor, cx| {
2624            assert_eq!(
2625                editor.search_background_highlights(cx),
2626                &[
2627                    Point::new(1, 0)..Point::new(1, 3),
2628                    Point::new(1, 8)..Point::new(1, 11),
2629                    Point::new(2, 0)..Point::new(2, 3),
2630                ]
2631            );
2632        });
2633    }
2634
2635    #[gpui::test]
2636    async fn test_find_matches_in_selections_multiple_excerpts_buffer_multiple_selections(
2637        cx: &mut TestAppContext,
2638    ) {
2639        init_globals(cx);
2640        let text = r#"
2641            aaa bbb aaa ccc
2642            aaa bbb aaa ccc
2643            aaa bbb aaa ccc
2644            aaa bbb aaa ccc
2645            aaa bbb aaa ccc
2646            aaa bbb aaa ccc
2647
2648            aaa bbb aaa ccc
2649            aaa bbb aaa ccc
2650            aaa bbb aaa ccc
2651            aaa bbb aaa ccc
2652            aaa bbb aaa ccc
2653            aaa bbb aaa ccc
2654            "#
2655        .unindent();
2656
2657        let cx = cx.add_empty_window();
2658        let editor = cx.new_window_entity(|window, cx| {
2659            let multibuffer = MultiBuffer::build_multi(
2660                [
2661                    (
2662                        &text,
2663                        vec![
2664                            Point::new(0, 0)..Point::new(2, 0),
2665                            Point::new(4, 0)..Point::new(5, 0),
2666                        ],
2667                    ),
2668                    (&text, vec![Point::new(9, 0)..Point::new(11, 0)]),
2669                ],
2670                cx,
2671            );
2672            Editor::for_multibuffer(multibuffer, None, false, window, cx)
2673        });
2674
2675        let search_bar = cx.new_window_entity(|window, cx| {
2676            let mut search_bar = BufferSearchBar::new(None, window, cx);
2677            search_bar.set_active_pane_item(Some(&editor), window, cx);
2678            search_bar.show(window, cx);
2679            search_bar
2680        });
2681
2682        editor.update_in(cx, |editor, window, cx| {
2683            editor.change_selections(None, window, cx, |s| {
2684                s.select_ranges(vec![
2685                    Point::new(1, 0)..Point::new(1, 4),
2686                    Point::new(5, 3)..Point::new(6, 4),
2687                ])
2688            })
2689        });
2690
2691        search_bar.update_in(cx, |search_bar, window, cx| {
2692            let deploy = Deploy {
2693                focus: true,
2694                replace_enabled: false,
2695                selection_search_enabled: true,
2696            };
2697            search_bar.deploy(&deploy, window, cx);
2698        });
2699
2700        cx.run_until_parked();
2701
2702        search_bar
2703            .update_in(cx, |search_bar, window, cx| {
2704                search_bar.search("aaa", None, window, cx)
2705            })
2706            .await
2707            .unwrap();
2708
2709        editor.update(cx, |editor, cx| {
2710            assert_eq!(
2711                editor.search_background_highlights(cx),
2712                &[
2713                    Point::new(1, 0)..Point::new(1, 3),
2714                    Point::new(5, 8)..Point::new(5, 11),
2715                    Point::new(6, 0)..Point::new(6, 3),
2716                ]
2717            );
2718        });
2719    }
2720
2721    #[gpui::test]
2722    async fn test_invalid_regexp_search_after_valid(cx: &mut TestAppContext) {
2723        let (editor, search_bar, cx) = init_test(cx);
2724        // Search using valid regexp
2725        search_bar
2726            .update_in(cx, |search_bar, window, cx| {
2727                search_bar.enable_search_option(SearchOptions::REGEX, window, cx);
2728                search_bar.search("expression", None, window, cx)
2729            })
2730            .await
2731            .unwrap();
2732        editor.update_in(cx, |editor, window, cx| {
2733            assert_eq!(
2734                display_points_of(editor.all_text_background_highlights(window, cx)),
2735                &[
2736                    DisplayPoint::new(DisplayRow(0), 10)..DisplayPoint::new(DisplayRow(0), 20),
2737                    DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 19),
2738                ],
2739            );
2740        });
2741
2742        // Now, the expression is invalid
2743        search_bar
2744            .update_in(cx, |search_bar, window, cx| {
2745                search_bar.search("expression (", None, window, cx)
2746            })
2747            .await
2748            .unwrap_err();
2749        editor.update_in(cx, |editor, window, cx| {
2750            assert!(
2751                display_points_of(editor.all_text_background_highlights(window, cx)).is_empty(),
2752            );
2753        });
2754    }
2755
2756    #[gpui::test]
2757    async fn test_search_options_changes(cx: &mut TestAppContext) {
2758        let (_editor, search_bar, cx) = init_test(cx);
2759        update_search_settings(
2760            SearchSettings {
2761                whole_word: false,
2762                case_sensitive: false,
2763                include_ignored: false,
2764                regex: false,
2765            },
2766            cx,
2767        );
2768
2769        let deploy = Deploy {
2770            focus: true,
2771            replace_enabled: false,
2772            selection_search_enabled: true,
2773        };
2774
2775        search_bar.update_in(cx, |search_bar, window, cx| {
2776            assert_eq!(
2777                search_bar.search_options,
2778                SearchOptions::NONE,
2779                "Should have no search options enabled by default"
2780            );
2781            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
2782            assert_eq!(
2783                search_bar.search_options,
2784                SearchOptions::WHOLE_WORD,
2785                "Should enable the option toggled"
2786            );
2787            assert!(
2788                !search_bar.dismissed,
2789                "Search bar should be present and visible"
2790            );
2791            search_bar.deploy(&deploy, window, cx);
2792            assert_eq!(
2793                search_bar.configured_options,
2794                SearchOptions::NONE,
2795                "Should have configured search options matching the settings"
2796            );
2797            assert_eq!(
2798                search_bar.search_options,
2799                SearchOptions::WHOLE_WORD,
2800                "After (re)deploying, the option should still be enabled"
2801            );
2802
2803            search_bar.dismiss(&Dismiss, window, cx);
2804            search_bar.deploy(&deploy, window, cx);
2805            assert_eq!(
2806                search_bar.search_options,
2807                SearchOptions::NONE,
2808                "After hiding and showing the search bar, default options should be used"
2809            );
2810
2811            search_bar.toggle_search_option(SearchOptions::REGEX, window, cx);
2812            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
2813            assert_eq!(
2814                search_bar.search_options,
2815                SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2816                "Should enable the options toggled"
2817            );
2818            assert!(
2819                !search_bar.dismissed,
2820                "Search bar should be present and visible"
2821            );
2822        });
2823
2824        update_search_settings(
2825            SearchSettings {
2826                whole_word: false,
2827                case_sensitive: true,
2828                include_ignored: false,
2829                regex: false,
2830            },
2831            cx,
2832        );
2833        search_bar.update_in(cx, |search_bar, window, cx| {
2834            assert_eq!(
2835                search_bar.search_options,
2836                SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2837                "Should have no search options enabled by default"
2838            );
2839
2840            search_bar.deploy(&deploy, window, cx);
2841            assert_eq!(
2842                search_bar.configured_options,
2843                SearchOptions::CASE_SENSITIVE,
2844                "Should have configured search options matching the settings"
2845            );
2846            assert_eq!(
2847                search_bar.search_options,
2848                SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2849                "Toggling a non-dismissed search bar with custom options should not change the default options"
2850            );
2851            search_bar.dismiss(&Dismiss, window, cx);
2852            search_bar.deploy(&deploy, window, cx);
2853            assert_eq!(
2854                search_bar.search_options,
2855                SearchOptions::CASE_SENSITIVE,
2856                "After hiding and showing the search bar, default options should be used"
2857            );
2858        });
2859    }
2860
2861    fn update_search_settings(search_settings: SearchSettings, cx: &mut TestAppContext) {
2862        cx.update(|cx| {
2863            SettingsStore::update_global(cx, |store, cx| {
2864                store.update_user_settings::<EditorSettings>(cx, |settings| {
2865                    settings.search = Some(search_settings);
2866                });
2867            });
2868        });
2869    }
2870}