buffer_search.rs

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