buffer_search.rs

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