buffer_search.rs

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