buffer_search.rs

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