buffer_search.rs

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