buffer_search.rs

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