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
 700                .read(cx)
 701                .set_language_registry(languages.clone());
 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                    }
1464                }
1465            }
1466        }
1467        if !should_propagate {
1468            cx.stop_propagation();
1469        }
1470    }
1471
1472    pub fn replace_all(&mut self, _: &ReplaceAll, window: &mut Window, cx: &mut Context<Self>) {
1473        if !self.dismissed && self.active_search.is_some() {
1474            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1475                if let Some(query) = self.active_search.as_ref() {
1476                    if let Some(matches) = self
1477                        .searchable_items_with_matches
1478                        .get(&searchable_item.downgrade())
1479                    {
1480                        let query = query
1481                            .as_ref()
1482                            .clone()
1483                            .with_replacement(self.replacement(cx));
1484                        searchable_item.replace_all(&mut matches.iter(), &query, window, cx);
1485                    }
1486                }
1487            }
1488        }
1489    }
1490
1491    pub fn match_exists(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
1492        self.update_match_index(window, cx);
1493        self.active_match_index.is_some()
1494    }
1495
1496    pub fn should_use_smartcase_search(&mut self, cx: &mut Context<Self>) -> bool {
1497        EditorSettings::get_global(cx).use_smartcase_search
1498    }
1499
1500    pub fn is_contains_uppercase(&mut self, str: &String) -> bool {
1501        str.chars().any(|c| c.is_uppercase())
1502    }
1503
1504    fn smartcase(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1505        if self.should_use_smartcase_search(cx) {
1506            let query = self.query(cx);
1507            if !query.is_empty() {
1508                let is_case = self.is_contains_uppercase(&query);
1509                if self.has_search_option(SearchOptions::CASE_SENSITIVE) != is_case {
1510                    self.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
1511                }
1512            }
1513        }
1514    }
1515
1516    fn adjust_query_regex_language(&self, cx: &mut App) {
1517        let enable = self.search_options.contains(SearchOptions::REGEX);
1518        let query_buffer = self
1519            .query_editor
1520            .read(cx)
1521            .buffer()
1522            .read(cx)
1523            .as_singleton()
1524            .expect("query editor should be backed by a singleton buffer");
1525        if enable {
1526            if let Some(regex_language) = self.regex_language.clone() {
1527                query_buffer.update(cx, |query_buffer, cx| {
1528                    query_buffer.set_language(Some(regex_language), cx);
1529                })
1530            }
1531        } else {
1532            query_buffer.update(cx, |query_buffer, cx| {
1533                query_buffer.set_language(None, cx);
1534            })
1535        }
1536    }
1537}
1538
1539#[cfg(test)]
1540mod tests {
1541    use std::ops::Range;
1542
1543    use super::*;
1544    use editor::{DisplayPoint, Editor, MultiBuffer, SearchSettings, display_map::DisplayRow};
1545    use gpui::{Hsla, TestAppContext, UpdateGlobal, VisualTestContext};
1546    use language::{Buffer, Point};
1547    use project::Project;
1548    use settings::SettingsStore;
1549    use smol::stream::StreamExt as _;
1550    use unindent::Unindent as _;
1551
1552    fn init_globals(cx: &mut TestAppContext) {
1553        cx.update(|cx| {
1554            let store = settings::SettingsStore::test(cx);
1555            cx.set_global(store);
1556            workspace::init_settings(cx);
1557            editor::init(cx);
1558
1559            language::init(cx);
1560            Project::init_settings(cx);
1561            theme::init(theme::LoadThemes::JustBase, cx);
1562            crate::init(cx);
1563        });
1564    }
1565
1566    fn init_test(
1567        cx: &mut TestAppContext,
1568    ) -> (
1569        Entity<Editor>,
1570        Entity<BufferSearchBar>,
1571        &mut VisualTestContext,
1572    ) {
1573        init_globals(cx);
1574        let buffer = cx.new(|cx| {
1575            Buffer::local(
1576                r#"
1577                A regular expression (shortened as regex or regexp;[1] also referred to as
1578                rational expression[2][3]) is a sequence of characters that specifies a search
1579                pattern in text. Usually such patterns are used by string-searching algorithms
1580                for "find" or "find and replace" operations on strings, or for input validation.
1581                "#
1582                .unindent(),
1583                cx,
1584            )
1585        });
1586        let cx = cx.add_empty_window();
1587        let editor =
1588            cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
1589
1590        let search_bar = cx.new_window_entity(|window, cx| {
1591            let mut search_bar = BufferSearchBar::new(None, window, cx);
1592            search_bar.set_active_pane_item(Some(&editor), window, cx);
1593            search_bar.show(window, cx);
1594            search_bar
1595        });
1596
1597        (editor, search_bar, cx)
1598    }
1599
1600    #[gpui::test]
1601    async fn test_search_simple(cx: &mut TestAppContext) {
1602        let (editor, search_bar, cx) = init_test(cx);
1603        let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
1604            background_highlights
1605                .into_iter()
1606                .map(|(range, _)| range)
1607                .collect::<Vec<_>>()
1608        };
1609        // Search for a string that appears with different casing.
1610        // By default, search is case-insensitive.
1611        search_bar
1612            .update_in(cx, |search_bar, window, cx| {
1613                search_bar.search("us", None, window, cx)
1614            })
1615            .await
1616            .unwrap();
1617        editor.update_in(cx, |editor, window, cx| {
1618            assert_eq!(
1619                display_points_of(editor.all_text_background_highlights(window, cx)),
1620                &[
1621                    DisplayPoint::new(DisplayRow(2), 17)..DisplayPoint::new(DisplayRow(2), 19),
1622                    DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),
1623                ]
1624            );
1625        });
1626
1627        // Switch to a case sensitive search.
1628        search_bar.update_in(cx, |search_bar, window, cx| {
1629            search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
1630        });
1631        let mut editor_notifications = cx.notifications(&editor);
1632        editor_notifications.next().await;
1633        editor.update_in(cx, |editor, window, cx| {
1634            assert_eq!(
1635                display_points_of(editor.all_text_background_highlights(window, cx)),
1636                &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
1637            );
1638        });
1639
1640        // Search for a string that appears both as a whole word and
1641        // within other words. By default, all results are found.
1642        search_bar
1643            .update_in(cx, |search_bar, window, cx| {
1644                search_bar.search("or", None, window, cx)
1645            })
1646            .await
1647            .unwrap();
1648        editor.update_in(cx, |editor, window, cx| {
1649            assert_eq!(
1650                display_points_of(editor.all_text_background_highlights(window, cx)),
1651                &[
1652                    DisplayPoint::new(DisplayRow(0), 24)..DisplayPoint::new(DisplayRow(0), 26),
1653                    DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1654                    DisplayPoint::new(DisplayRow(2), 71)..DisplayPoint::new(DisplayRow(2), 73),
1655                    DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 3),
1656                    DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
1657                    DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
1658                    DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 62),
1659                ]
1660            );
1661        });
1662
1663        // Switch to a whole word search.
1664        search_bar.update_in(cx, |search_bar, window, cx| {
1665            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
1666        });
1667        let mut editor_notifications = cx.notifications(&editor);
1668        editor_notifications.next().await;
1669        editor.update_in(cx, |editor, window, cx| {
1670            assert_eq!(
1671                display_points_of(editor.all_text_background_highlights(window, cx)),
1672                &[
1673                    DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1674                    DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
1675                    DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
1676                ]
1677            );
1678        });
1679
1680        editor.update_in(cx, |editor, window, cx| {
1681            editor.change_selections(None, window, cx, |s| {
1682                s.select_display_ranges([
1683                    DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
1684                ])
1685            });
1686        });
1687        search_bar.update_in(cx, |search_bar, window, cx| {
1688            assert_eq!(search_bar.active_match_index, Some(0));
1689            search_bar.select_next_match(&SelectNextMatch, window, cx);
1690            assert_eq!(
1691                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1692                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1693            );
1694        });
1695        search_bar.read_with(cx, |search_bar, _| {
1696            assert_eq!(search_bar.active_match_index, Some(0));
1697        });
1698
1699        search_bar.update_in(cx, |search_bar, window, cx| {
1700            search_bar.select_next_match(&SelectNextMatch, window, cx);
1701            assert_eq!(
1702                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1703                [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1704            );
1705        });
1706        search_bar.read_with(cx, |search_bar, _| {
1707            assert_eq!(search_bar.active_match_index, Some(1));
1708        });
1709
1710        search_bar.update_in(cx, |search_bar, window, cx| {
1711            search_bar.select_next_match(&SelectNextMatch, window, cx);
1712            assert_eq!(
1713                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1714                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1715            );
1716        });
1717        search_bar.read_with(cx, |search_bar, _| {
1718            assert_eq!(search_bar.active_match_index, Some(2));
1719        });
1720
1721        search_bar.update_in(cx, |search_bar, window, cx| {
1722            search_bar.select_next_match(&SelectNextMatch, window, cx);
1723            assert_eq!(
1724                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1725                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1726            );
1727        });
1728        search_bar.read_with(cx, |search_bar, _| {
1729            assert_eq!(search_bar.active_match_index, Some(0));
1730        });
1731
1732        search_bar.update_in(cx, |search_bar, window, cx| {
1733            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1734            assert_eq!(
1735                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1736                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1737            );
1738        });
1739        search_bar.read_with(cx, |search_bar, _| {
1740            assert_eq!(search_bar.active_match_index, Some(2));
1741        });
1742
1743        search_bar.update_in(cx, |search_bar, window, cx| {
1744            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1745            assert_eq!(
1746                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1747                [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1748            );
1749        });
1750        search_bar.read_with(cx, |search_bar, _| {
1751            assert_eq!(search_bar.active_match_index, Some(1));
1752        });
1753
1754        search_bar.update_in(cx, |search_bar, window, cx| {
1755            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1756            assert_eq!(
1757                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1758                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1759            );
1760        });
1761        search_bar.read_with(cx, |search_bar, _| {
1762            assert_eq!(search_bar.active_match_index, Some(0));
1763        });
1764
1765        // Park the cursor in between matches and ensure that going to the previous match selects
1766        // the closest match to the left.
1767        editor.update_in(cx, |editor, window, cx| {
1768            editor.change_selections(None, window, cx, |s| {
1769                s.select_display_ranges([
1770                    DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
1771                ])
1772            });
1773        });
1774        search_bar.update_in(cx, |search_bar, window, cx| {
1775            assert_eq!(search_bar.active_match_index, Some(1));
1776            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1777            assert_eq!(
1778                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1779                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1780            );
1781        });
1782        search_bar.read_with(cx, |search_bar, _| {
1783            assert_eq!(search_bar.active_match_index, Some(0));
1784        });
1785
1786        // Park the cursor in between matches and ensure that going to the next match selects the
1787        // closest match to the right.
1788        editor.update_in(cx, |editor, window, cx| {
1789            editor.change_selections(None, window, cx, |s| {
1790                s.select_display_ranges([
1791                    DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
1792                ])
1793            });
1794        });
1795        search_bar.update_in(cx, |search_bar, window, cx| {
1796            assert_eq!(search_bar.active_match_index, Some(1));
1797            search_bar.select_next_match(&SelectNextMatch, window, cx);
1798            assert_eq!(
1799                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1800                [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1801            );
1802        });
1803        search_bar.read_with(cx, |search_bar, _| {
1804            assert_eq!(search_bar.active_match_index, Some(1));
1805        });
1806
1807        // Park the cursor after the last match and ensure that going to the previous match selects
1808        // the last match.
1809        editor.update_in(cx, |editor, window, cx| {
1810            editor.change_selections(None, window, cx, |s| {
1811                s.select_display_ranges([
1812                    DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
1813                ])
1814            });
1815        });
1816        search_bar.update_in(cx, |search_bar, window, cx| {
1817            assert_eq!(search_bar.active_match_index, Some(2));
1818            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1819            assert_eq!(
1820                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1821                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1822            );
1823        });
1824        search_bar.read_with(cx, |search_bar, _| {
1825            assert_eq!(search_bar.active_match_index, Some(2));
1826        });
1827
1828        // Park the cursor after the last match and ensure that going to the next match selects the
1829        // first match.
1830        editor.update_in(cx, |editor, window, cx| {
1831            editor.change_selections(None, window, cx, |s| {
1832                s.select_display_ranges([
1833                    DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
1834                ])
1835            });
1836        });
1837        search_bar.update_in(cx, |search_bar, window, cx| {
1838            assert_eq!(search_bar.active_match_index, Some(2));
1839            search_bar.select_next_match(&SelectNextMatch, window, cx);
1840            assert_eq!(
1841                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1842                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1843            );
1844        });
1845        search_bar.read_with(cx, |search_bar, _| {
1846            assert_eq!(search_bar.active_match_index, Some(0));
1847        });
1848
1849        // Park the cursor before the first match and ensure that going to the previous match
1850        // selects the last match.
1851        editor.update_in(cx, |editor, window, cx| {
1852            editor.change_selections(None, window, cx, |s| {
1853                s.select_display_ranges([
1854                    DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
1855                ])
1856            });
1857        });
1858        search_bar.update_in(cx, |search_bar, window, cx| {
1859            assert_eq!(search_bar.active_match_index, Some(0));
1860            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1861            assert_eq!(
1862                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1863                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1864            );
1865        });
1866        search_bar.read_with(cx, |search_bar, _| {
1867            assert_eq!(search_bar.active_match_index, Some(2));
1868        });
1869    }
1870
1871    fn display_points_of(
1872        background_highlights: Vec<(Range<DisplayPoint>, Hsla)>,
1873    ) -> Vec<Range<DisplayPoint>> {
1874        background_highlights
1875            .into_iter()
1876            .map(|(range, _)| range)
1877            .collect::<Vec<_>>()
1878    }
1879
1880    #[gpui::test]
1881    async fn test_search_option_handling(cx: &mut TestAppContext) {
1882        let (editor, search_bar, cx) = init_test(cx);
1883
1884        // show with options should make current search case sensitive
1885        search_bar
1886            .update_in(cx, |search_bar, window, cx| {
1887                search_bar.show(window, cx);
1888                search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), window, cx)
1889            })
1890            .await
1891            .unwrap();
1892        editor.update_in(cx, |editor, window, cx| {
1893            assert_eq!(
1894                display_points_of(editor.all_text_background_highlights(window, cx)),
1895                &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
1896            );
1897        });
1898
1899        // search_suggested should restore default options
1900        search_bar.update_in(cx, |search_bar, window, cx| {
1901            search_bar.search_suggested(window, cx);
1902            assert_eq!(search_bar.search_options, SearchOptions::NONE)
1903        });
1904
1905        // toggling a search option should update the defaults
1906        search_bar
1907            .update_in(cx, |search_bar, window, cx| {
1908                search_bar.search("regex", Some(SearchOptions::CASE_SENSITIVE), window, cx)
1909            })
1910            .await
1911            .unwrap();
1912        search_bar.update_in(cx, |search_bar, window, cx| {
1913            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx)
1914        });
1915        let mut editor_notifications = cx.notifications(&editor);
1916        editor_notifications.next().await;
1917        editor.update_in(cx, |editor, window, cx| {
1918            assert_eq!(
1919                display_points_of(editor.all_text_background_highlights(window, cx)),
1920                &[DisplayPoint::new(DisplayRow(0), 35)..DisplayPoint::new(DisplayRow(0), 40),]
1921            );
1922        });
1923
1924        // defaults should still include whole word
1925        search_bar.update_in(cx, |search_bar, window, cx| {
1926            search_bar.search_suggested(window, cx);
1927            assert_eq!(
1928                search_bar.search_options,
1929                SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD
1930            )
1931        });
1932    }
1933
1934    #[gpui::test]
1935    async fn test_search_select_all_matches(cx: &mut TestAppContext) {
1936        init_globals(cx);
1937        let buffer_text = r#"
1938        A regular expression (shortened as regex or regexp;[1] also referred to as
1939        rational expression[2][3]) is a sequence of characters that specifies a search
1940        pattern in text. Usually such patterns are used by string-searching algorithms
1941        for "find" or "find and replace" operations on strings, or for input validation.
1942        "#
1943        .unindent();
1944        let expected_query_matches_count = buffer_text
1945            .chars()
1946            .filter(|c| c.eq_ignore_ascii_case(&'a'))
1947            .count();
1948        assert!(
1949            expected_query_matches_count > 1,
1950            "Should pick a query with multiple results"
1951        );
1952        let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
1953        let window = cx.add_window(|_, _| gpui::Empty);
1954
1955        let editor = window.build_entity(cx, |window, cx| {
1956            Editor::for_buffer(buffer.clone(), None, window, cx)
1957        });
1958
1959        let search_bar = window.build_entity(cx, |window, cx| {
1960            let mut search_bar = BufferSearchBar::new(None, window, cx);
1961            search_bar.set_active_pane_item(Some(&editor), window, cx);
1962            search_bar.show(window, cx);
1963            search_bar
1964        });
1965
1966        window
1967            .update(cx, |_, window, cx| {
1968                search_bar.update(cx, |search_bar, cx| {
1969                    search_bar.search("a", None, window, cx)
1970                })
1971            })
1972            .unwrap()
1973            .await
1974            .unwrap();
1975        let initial_selections = window
1976            .update(cx, |_, window, cx| {
1977                search_bar.update(cx, |search_bar, cx| {
1978                    let handle = search_bar.query_editor.focus_handle(cx);
1979                    window.focus(&handle);
1980                    search_bar.activate_current_match(window, cx);
1981                });
1982                assert!(
1983                    !editor.read(cx).is_focused(window),
1984                    "Initially, the editor should not be focused"
1985                );
1986                let initial_selections = editor.update(cx, |editor, cx| {
1987                    let initial_selections = editor.selections.display_ranges(cx);
1988                    assert_eq!(
1989                        initial_selections.len(), 1,
1990                        "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
1991                    );
1992                    initial_selections
1993                });
1994                search_bar.update(cx, |search_bar, cx| {
1995                    assert_eq!(search_bar.active_match_index, Some(0));
1996                    let handle = search_bar.query_editor.focus_handle(cx);
1997                    window.focus(&handle);
1998                    search_bar.select_all_matches(&SelectAllMatches, window, cx);
1999                });
2000                assert!(
2001                    editor.read(cx).is_focused(window),
2002                    "Should focus editor after successful SelectAllMatches"
2003                );
2004                search_bar.update(cx, |search_bar, cx| {
2005                    let all_selections =
2006                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2007                    assert_eq!(
2008                        all_selections.len(),
2009                        expected_query_matches_count,
2010                        "Should select all `a` characters in the buffer, but got: {all_selections:?}"
2011                    );
2012                    assert_eq!(
2013                        search_bar.active_match_index,
2014                        Some(0),
2015                        "Match index should not change after selecting all matches"
2016                    );
2017                });
2018
2019                search_bar.update(cx, |this, cx| this.select_next_match(&SelectNextMatch, window, cx));
2020                initial_selections
2021            }).unwrap();
2022
2023        window
2024            .update(cx, |_, window, cx| {
2025                assert!(
2026                    editor.read(cx).is_focused(window),
2027                    "Should still have editor focused after SelectNextMatch"
2028                );
2029                search_bar.update(cx, |search_bar, cx| {
2030                    let all_selections =
2031                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2032                    assert_eq!(
2033                        all_selections.len(),
2034                        1,
2035                        "On next match, should deselect items and select the next match"
2036                    );
2037                    assert_ne!(
2038                        all_selections, initial_selections,
2039                        "Next match should be different from the first selection"
2040                    );
2041                    assert_eq!(
2042                        search_bar.active_match_index,
2043                        Some(1),
2044                        "Match index should be updated to the next one"
2045                    );
2046                    let handle = search_bar.query_editor.focus_handle(cx);
2047                    window.focus(&handle);
2048                    search_bar.select_all_matches(&SelectAllMatches, window, cx);
2049                });
2050            })
2051            .unwrap();
2052        window
2053            .update(cx, |_, window, cx| {
2054                assert!(
2055                    editor.read(cx).is_focused(window),
2056                    "Should focus editor after successful SelectAllMatches"
2057                );
2058                search_bar.update(cx, |search_bar, cx| {
2059                    let all_selections =
2060                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2061                    assert_eq!(
2062                    all_selections.len(),
2063                    expected_query_matches_count,
2064                    "Should select all `a` characters in the buffer, but got: {all_selections:?}"
2065                );
2066                    assert_eq!(
2067                        search_bar.active_match_index,
2068                        Some(1),
2069                        "Match index should not change after selecting all matches"
2070                    );
2071                });
2072                search_bar.update(cx, |search_bar, cx| {
2073                    search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2074                });
2075            })
2076            .unwrap();
2077        let last_match_selections = window
2078            .update(cx, |_, window, cx| {
2079                assert!(
2080                    editor.read(cx).is_focused(window),
2081                    "Should still have editor focused after SelectPreviousMatch"
2082                );
2083
2084                search_bar.update(cx, |search_bar, cx| {
2085                    let all_selections =
2086                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2087                    assert_eq!(
2088                        all_selections.len(),
2089                        1,
2090                        "On previous match, should deselect items and select the previous item"
2091                    );
2092                    assert_eq!(
2093                        all_selections, initial_selections,
2094                        "Previous match should be the same as the first selection"
2095                    );
2096                    assert_eq!(
2097                        search_bar.active_match_index,
2098                        Some(0),
2099                        "Match index should be updated to the previous one"
2100                    );
2101                    all_selections
2102                })
2103            })
2104            .unwrap();
2105
2106        window
2107            .update(cx, |_, window, cx| {
2108                search_bar.update(cx, |search_bar, cx| {
2109                    let handle = search_bar.query_editor.focus_handle(cx);
2110                    window.focus(&handle);
2111                    search_bar.search("abas_nonexistent_match", None, window, cx)
2112                })
2113            })
2114            .unwrap()
2115            .await
2116            .unwrap();
2117        window
2118            .update(cx, |_, window, cx| {
2119                search_bar.update(cx, |search_bar, cx| {
2120                    search_bar.select_all_matches(&SelectAllMatches, window, cx);
2121                });
2122                assert!(
2123                    editor.update(cx, |this, _cx| !this.is_focused(window)),
2124                    "Should not switch focus to editor if SelectAllMatches does not find any matches"
2125                );
2126                search_bar.update(cx, |search_bar, cx| {
2127                    let all_selections =
2128                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2129                    assert_eq!(
2130                        all_selections, last_match_selections,
2131                        "Should not select anything new if there are no matches"
2132                    );
2133                    assert!(
2134                        search_bar.active_match_index.is_none(),
2135                        "For no matches, there should be no active match index"
2136                    );
2137                });
2138            })
2139            .unwrap();
2140    }
2141
2142    #[gpui::test]
2143    async fn test_search_query_with_match_whole_word(cx: &mut TestAppContext) {
2144        init_globals(cx);
2145        let buffer_text = r#"
2146        self.buffer.update(cx, |buffer, cx| {
2147            buffer.edit(
2148                edits,
2149                Some(AutoindentMode::Block {
2150                    original_indent_columns,
2151                }),
2152                cx,
2153            )
2154        });
2155
2156        this.buffer.update(cx, |buffer, cx| {
2157            buffer.edit([(end_of_line..start_of_next_line, replace)], None, cx)
2158        });
2159        "#
2160        .unindent();
2161        let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
2162        let cx = cx.add_empty_window();
2163
2164        let editor =
2165            cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
2166
2167        let search_bar = cx.new_window_entity(|window, cx| {
2168            let mut search_bar = BufferSearchBar::new(None, window, cx);
2169            search_bar.set_active_pane_item(Some(&editor), window, cx);
2170            search_bar.show(window, cx);
2171            search_bar
2172        });
2173
2174        search_bar
2175            .update_in(cx, |search_bar, window, cx| {
2176                search_bar.search(
2177                    "edit\\(",
2178                    Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
2179                    window,
2180                    cx,
2181                )
2182            })
2183            .await
2184            .unwrap();
2185
2186        search_bar.update_in(cx, |search_bar, window, cx| {
2187            search_bar.select_all_matches(&SelectAllMatches, window, cx);
2188        });
2189        search_bar.update(cx, |_, cx| {
2190            let all_selections =
2191                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2192            assert_eq!(
2193                all_selections.len(),
2194                2,
2195                "Should select all `edit(` in the buffer, but got: {all_selections:?}"
2196            );
2197        });
2198
2199        search_bar
2200            .update_in(cx, |search_bar, window, cx| {
2201                search_bar.search(
2202                    "edit(",
2203                    Some(SearchOptions::WHOLE_WORD | SearchOptions::CASE_SENSITIVE),
2204                    window,
2205                    cx,
2206                )
2207            })
2208            .await
2209            .unwrap();
2210
2211        search_bar.update_in(cx, |search_bar, window, cx| {
2212            search_bar.select_all_matches(&SelectAllMatches, window, cx);
2213        });
2214        search_bar.update(cx, |_, cx| {
2215            let all_selections =
2216                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2217            assert_eq!(
2218                all_selections.len(),
2219                2,
2220                "Should select all `edit(` in the buffer, but got: {all_selections:?}"
2221            );
2222        });
2223    }
2224
2225    #[gpui::test]
2226    async fn test_search_query_history(cx: &mut TestAppContext) {
2227        init_globals(cx);
2228        let buffer_text = r#"
2229        A regular expression (shortened as regex or regexp;[1] also referred to as
2230        rational expression[2][3]) is a sequence of characters that specifies a search
2231        pattern in text. Usually such patterns are used by string-searching algorithms
2232        for "find" or "find and replace" operations on strings, or for input validation.
2233        "#
2234        .unindent();
2235        let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
2236        let cx = cx.add_empty_window();
2237
2238        let editor =
2239            cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
2240
2241        let search_bar = cx.new_window_entity(|window, cx| {
2242            let mut search_bar = BufferSearchBar::new(None, window, cx);
2243            search_bar.set_active_pane_item(Some(&editor), window, cx);
2244            search_bar.show(window, cx);
2245            search_bar
2246        });
2247
2248        // Add 3 search items into the history.
2249        search_bar
2250            .update_in(cx, |search_bar, window, cx| {
2251                search_bar.search("a", None, window, cx)
2252            })
2253            .await
2254            .unwrap();
2255        search_bar
2256            .update_in(cx, |search_bar, window, cx| {
2257                search_bar.search("b", None, window, cx)
2258            })
2259            .await
2260            .unwrap();
2261        search_bar
2262            .update_in(cx, |search_bar, window, cx| {
2263                search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), window, cx)
2264            })
2265            .await
2266            .unwrap();
2267        // Ensure that the latest search is active.
2268        search_bar.update(cx, |search_bar, cx| {
2269            assert_eq!(search_bar.query(cx), "c");
2270            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2271        });
2272
2273        // Next history query after the latest should set the query to the empty string.
2274        search_bar.update_in(cx, |search_bar, window, cx| {
2275            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2276        });
2277        search_bar.update(cx, |search_bar, cx| {
2278            assert_eq!(search_bar.query(cx), "");
2279            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2280        });
2281        search_bar.update_in(cx, |search_bar, window, cx| {
2282            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2283        });
2284        search_bar.update(cx, |search_bar, cx| {
2285            assert_eq!(search_bar.query(cx), "");
2286            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2287        });
2288
2289        // First previous query for empty current query should set the query to the latest.
2290        search_bar.update_in(cx, |search_bar, window, cx| {
2291            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2292        });
2293        search_bar.update(cx, |search_bar, cx| {
2294            assert_eq!(search_bar.query(cx), "c");
2295            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2296        });
2297
2298        // Further previous items should go over the history in reverse order.
2299        search_bar.update_in(cx, |search_bar, window, cx| {
2300            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2301        });
2302        search_bar.update(cx, |search_bar, cx| {
2303            assert_eq!(search_bar.query(cx), "b");
2304            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2305        });
2306
2307        // Previous items should never go behind the first history item.
2308        search_bar.update_in(cx, |search_bar, window, cx| {
2309            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2310        });
2311        search_bar.update(cx, |search_bar, cx| {
2312            assert_eq!(search_bar.query(cx), "a");
2313            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2314        });
2315        search_bar.update_in(cx, |search_bar, window, cx| {
2316            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2317        });
2318        search_bar.update(cx, |search_bar, cx| {
2319            assert_eq!(search_bar.query(cx), "a");
2320            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2321        });
2322
2323        // Next items should go over the history in the original order.
2324        search_bar.update_in(cx, |search_bar, window, cx| {
2325            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2326        });
2327        search_bar.update(cx, |search_bar, cx| {
2328            assert_eq!(search_bar.query(cx), "b");
2329            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2330        });
2331
2332        search_bar
2333            .update_in(cx, |search_bar, window, cx| {
2334                search_bar.search("ba", None, window, cx)
2335            })
2336            .await
2337            .unwrap();
2338        search_bar.update(cx, |search_bar, cx| {
2339            assert_eq!(search_bar.query(cx), "ba");
2340            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2341        });
2342
2343        // New search input should add another entry to history and move the selection to the end of the history.
2344        search_bar.update_in(cx, |search_bar, window, cx| {
2345            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2346        });
2347        search_bar.update(cx, |search_bar, cx| {
2348            assert_eq!(search_bar.query(cx), "c");
2349            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2350        });
2351        search_bar.update_in(cx, |search_bar, window, cx| {
2352            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2353        });
2354        search_bar.update(cx, |search_bar, cx| {
2355            assert_eq!(search_bar.query(cx), "b");
2356            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2357        });
2358        search_bar.update_in(cx, |search_bar, window, cx| {
2359            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2360        });
2361        search_bar.update(cx, |search_bar, cx| {
2362            assert_eq!(search_bar.query(cx), "c");
2363            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2364        });
2365        search_bar.update_in(cx, |search_bar, window, cx| {
2366            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2367        });
2368        search_bar.update(cx, |search_bar, cx| {
2369            assert_eq!(search_bar.query(cx), "ba");
2370            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2371        });
2372        search_bar.update_in(cx, |search_bar, window, cx| {
2373            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2374        });
2375        search_bar.update(cx, |search_bar, cx| {
2376            assert_eq!(search_bar.query(cx), "");
2377            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2378        });
2379    }
2380
2381    #[gpui::test]
2382    async fn test_replace_simple(cx: &mut TestAppContext) {
2383        let (editor, search_bar, cx) = init_test(cx);
2384
2385        search_bar
2386            .update_in(cx, |search_bar, window, cx| {
2387                search_bar.search("expression", None, window, cx)
2388            })
2389            .await
2390            .unwrap();
2391
2392        search_bar.update_in(cx, |search_bar, window, cx| {
2393            search_bar.replacement_editor.update(cx, |editor, cx| {
2394                // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally.
2395                editor.set_text("expr$1", window, cx);
2396            });
2397            search_bar.replace_all(&ReplaceAll, window, cx)
2398        });
2399        assert_eq!(
2400            editor.read_with(cx, |this, cx| { this.text(cx) }),
2401            r#"
2402        A regular expr$1 (shortened as regex or regexp;[1] also referred to as
2403        rational expr$1[2][3]) is a sequence of characters that specifies a search
2404        pattern in text. Usually such patterns are used by string-searching algorithms
2405        for "find" or "find and replace" operations on strings, or for input validation.
2406        "#
2407            .unindent()
2408        );
2409
2410        // Search for word boundaries and replace just a single one.
2411        search_bar
2412            .update_in(cx, |search_bar, window, cx| {
2413                search_bar.search("or", Some(SearchOptions::WHOLE_WORD), window, cx)
2414            })
2415            .await
2416            .unwrap();
2417
2418        search_bar.update_in(cx, |search_bar, window, cx| {
2419            search_bar.replacement_editor.update(cx, |editor, cx| {
2420                editor.set_text("banana", window, cx);
2421            });
2422            search_bar.replace_next(&ReplaceNext, window, cx)
2423        });
2424        // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text.
2425        assert_eq!(
2426            editor.read_with(cx, |this, cx| { this.text(cx) }),
2427            r#"
2428        A regular expr$1 (shortened as regex banana regexp;[1] also referred to as
2429        rational expr$1[2][3]) is a sequence of characters that specifies a search
2430        pattern in text. Usually such patterns are used by string-searching algorithms
2431        for "find" or "find and replace" operations on strings, or for input validation.
2432        "#
2433            .unindent()
2434        );
2435        // Let's turn on regex mode.
2436        search_bar
2437            .update_in(cx, |search_bar, window, cx| {
2438                search_bar.search("\\[([^\\]]+)\\]", Some(SearchOptions::REGEX), window, cx)
2439            })
2440            .await
2441            .unwrap();
2442        search_bar.update_in(cx, |search_bar, window, cx| {
2443            search_bar.replacement_editor.update(cx, |editor, cx| {
2444                editor.set_text("${1}number", window, cx);
2445            });
2446            search_bar.replace_all(&ReplaceAll, window, cx)
2447        });
2448        assert_eq!(
2449            editor.read_with(cx, |this, cx| { this.text(cx) }),
2450            r#"
2451        A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2452        rational expr$12number3number) is a sequence of characters that specifies a search
2453        pattern in text. Usually such patterns are used by string-searching algorithms
2454        for "find" or "find and replace" operations on strings, or for input validation.
2455        "#
2456            .unindent()
2457        );
2458        // Now with a whole-word twist.
2459        search_bar
2460            .update_in(cx, |search_bar, window, cx| {
2461                search_bar.search(
2462                    "a\\w+s",
2463                    Some(SearchOptions::REGEX | SearchOptions::WHOLE_WORD),
2464                    window,
2465                    cx,
2466                )
2467            })
2468            .await
2469            .unwrap();
2470        search_bar.update_in(cx, |search_bar, window, cx| {
2471            search_bar.replacement_editor.update(cx, |editor, cx| {
2472                editor.set_text("things", window, cx);
2473            });
2474            search_bar.replace_all(&ReplaceAll, window, cx)
2475        });
2476        // The only word affected by this edit should be `algorithms`, even though there's a bunch
2477        // of words in this text that would match this regex if not for WHOLE_WORD.
2478        assert_eq!(
2479            editor.read_with(cx, |this, cx| { this.text(cx) }),
2480            r#"
2481        A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2482        rational expr$12number3number) is a sequence of characters that specifies a search
2483        pattern in text. Usually such patterns are used by string-searching things
2484        for "find" or "find and replace" operations on strings, or for input validation.
2485        "#
2486            .unindent()
2487        );
2488    }
2489
2490    struct ReplacementTestParams<'a> {
2491        editor: &'a Entity<Editor>,
2492        search_bar: &'a Entity<BufferSearchBar>,
2493        cx: &'a mut VisualTestContext,
2494        search_text: &'static str,
2495        search_options: Option<SearchOptions>,
2496        replacement_text: &'static str,
2497        replace_all: bool,
2498        expected_text: String,
2499    }
2500
2501    async fn run_replacement_test(options: ReplacementTestParams<'_>) {
2502        options
2503            .search_bar
2504            .update_in(options.cx, |search_bar, window, cx| {
2505                if let Some(options) = options.search_options {
2506                    search_bar.set_search_options(options, cx);
2507                }
2508                search_bar.search(options.search_text, options.search_options, window, cx)
2509            })
2510            .await
2511            .unwrap();
2512
2513        options
2514            .search_bar
2515            .update_in(options.cx, |search_bar, window, cx| {
2516                search_bar.replacement_editor.update(cx, |editor, cx| {
2517                    editor.set_text(options.replacement_text, window, cx);
2518                });
2519
2520                if options.replace_all {
2521                    search_bar.replace_all(&ReplaceAll, window, cx)
2522                } else {
2523                    search_bar.replace_next(&ReplaceNext, window, cx)
2524                }
2525            });
2526
2527        assert_eq!(
2528            options
2529                .editor
2530                .read_with(options.cx, |this, cx| { this.text(cx) }),
2531            options.expected_text
2532        );
2533    }
2534
2535    #[gpui::test]
2536    async fn test_replace_special_characters(cx: &mut TestAppContext) {
2537        let (editor, search_bar, cx) = init_test(cx);
2538
2539        run_replacement_test(ReplacementTestParams {
2540            editor: &editor,
2541            search_bar: &search_bar,
2542            cx,
2543            search_text: "expression",
2544            search_options: None,
2545            replacement_text: r"\n",
2546            replace_all: true,
2547            expected_text: r#"
2548            A regular \n (shortened as regex or regexp;[1] also referred to as
2549            rational \n[2][3]) is a sequence of characters that specifies a search
2550            pattern in text. Usually such patterns are used by string-searching algorithms
2551            for "find" or "find and replace" operations on strings, or for input validation.
2552            "#
2553            .unindent(),
2554        })
2555        .await;
2556
2557        run_replacement_test(ReplacementTestParams {
2558            editor: &editor,
2559            search_bar: &search_bar,
2560            cx,
2561            search_text: "or",
2562            search_options: Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
2563            replacement_text: r"\\\n\\\\",
2564            replace_all: false,
2565            expected_text: r#"
2566            A regular \n (shortened as regex \
2567            \\ regexp;[1] also referred to as
2568            rational \n[2][3]) is a sequence of characters that specifies a search
2569            pattern in text. Usually such patterns are used by string-searching algorithms
2570            for "find" or "find and replace" operations on strings, or for input validation.
2571            "#
2572            .unindent(),
2573        })
2574        .await;
2575
2576        run_replacement_test(ReplacementTestParams {
2577            editor: &editor,
2578            search_bar: &search_bar,
2579            cx,
2580            search_text: r"(that|used) ",
2581            search_options: Some(SearchOptions::REGEX),
2582            replacement_text: r"$1\n",
2583            replace_all: true,
2584            expected_text: r#"
2585            A regular \n (shortened as regex \
2586            \\ regexp;[1] also referred to as
2587            rational \n[2][3]) is a sequence of characters that
2588            specifies a search
2589            pattern in text. Usually such patterns are used
2590            by string-searching algorithms
2591            for "find" or "find and replace" operations on strings, or for input validation.
2592            "#
2593            .unindent(),
2594        })
2595        .await;
2596    }
2597
2598    #[gpui::test]
2599    async fn test_find_matches_in_selections_singleton_buffer_multiple_selections(
2600        cx: &mut TestAppContext,
2601    ) {
2602        init_globals(cx);
2603        let buffer = cx.new(|cx| {
2604            Buffer::local(
2605                r#"
2606                aaa bbb aaa ccc
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                "#
2613                .unindent(),
2614                cx,
2615            )
2616        });
2617        let cx = cx.add_empty_window();
2618        let editor =
2619            cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
2620
2621        let search_bar = cx.new_window_entity(|window, cx| {
2622            let mut search_bar = BufferSearchBar::new(None, window, cx);
2623            search_bar.set_active_pane_item(Some(&editor), window, cx);
2624            search_bar.show(window, cx);
2625            search_bar
2626        });
2627
2628        editor.update_in(cx, |editor, window, cx| {
2629            editor.change_selections(None, window, cx, |s| {
2630                s.select_ranges(vec![Point::new(1, 0)..Point::new(2, 4)])
2631            })
2632        });
2633
2634        search_bar.update_in(cx, |search_bar, window, cx| {
2635            let deploy = Deploy {
2636                focus: true,
2637                replace_enabled: false,
2638                selection_search_enabled: true,
2639            };
2640            search_bar.deploy(&deploy, window, cx);
2641        });
2642
2643        cx.run_until_parked();
2644
2645        search_bar
2646            .update_in(cx, |search_bar, window, cx| {
2647                search_bar.search("aaa", None, window, cx)
2648            })
2649            .await
2650            .unwrap();
2651
2652        editor.update(cx, |editor, cx| {
2653            assert_eq!(
2654                editor.search_background_highlights(cx),
2655                &[
2656                    Point::new(1, 0)..Point::new(1, 3),
2657                    Point::new(1, 8)..Point::new(1, 11),
2658                    Point::new(2, 0)..Point::new(2, 3),
2659                ]
2660            );
2661        });
2662    }
2663
2664    #[gpui::test]
2665    async fn test_find_matches_in_selections_multiple_excerpts_buffer_multiple_selections(
2666        cx: &mut TestAppContext,
2667    ) {
2668        init_globals(cx);
2669        let text = r#"
2670            aaa bbb aaa ccc
2671            aaa bbb aaa ccc
2672            aaa bbb aaa ccc
2673            aaa bbb aaa ccc
2674            aaa bbb aaa ccc
2675            aaa bbb aaa ccc
2676
2677            aaa bbb aaa ccc
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            "#
2684        .unindent();
2685
2686        let cx = cx.add_empty_window();
2687        let editor = cx.new_window_entity(|window, cx| {
2688            let multibuffer = MultiBuffer::build_multi(
2689                [
2690                    (
2691                        &text,
2692                        vec![
2693                            Point::new(0, 0)..Point::new(2, 0),
2694                            Point::new(4, 0)..Point::new(5, 0),
2695                        ],
2696                    ),
2697                    (&text, vec![Point::new(9, 0)..Point::new(11, 0)]),
2698                ],
2699                cx,
2700            );
2701            Editor::for_multibuffer(multibuffer, None, window, cx)
2702        });
2703
2704        let search_bar = cx.new_window_entity(|window, cx| {
2705            let mut search_bar = BufferSearchBar::new(None, window, cx);
2706            search_bar.set_active_pane_item(Some(&editor), window, cx);
2707            search_bar.show(window, cx);
2708            search_bar
2709        });
2710
2711        editor.update_in(cx, |editor, window, cx| {
2712            editor.change_selections(None, window, cx, |s| {
2713                s.select_ranges(vec![
2714                    Point::new(1, 0)..Point::new(1, 4),
2715                    Point::new(5, 3)..Point::new(6, 4),
2716                ])
2717            })
2718        });
2719
2720        search_bar.update_in(cx, |search_bar, window, cx| {
2721            let deploy = Deploy {
2722                focus: true,
2723                replace_enabled: false,
2724                selection_search_enabled: true,
2725            };
2726            search_bar.deploy(&deploy, window, cx);
2727        });
2728
2729        cx.run_until_parked();
2730
2731        search_bar
2732            .update_in(cx, |search_bar, window, cx| {
2733                search_bar.search("aaa", None, window, cx)
2734            })
2735            .await
2736            .unwrap();
2737
2738        editor.update(cx, |editor, cx| {
2739            assert_eq!(
2740                editor.search_background_highlights(cx),
2741                &[
2742                    Point::new(1, 0)..Point::new(1, 3),
2743                    Point::new(5, 8)..Point::new(5, 11),
2744                    Point::new(6, 0)..Point::new(6, 3),
2745                ]
2746            );
2747        });
2748    }
2749
2750    #[gpui::test]
2751    async fn test_invalid_regexp_search_after_valid(cx: &mut TestAppContext) {
2752        let (editor, search_bar, cx) = init_test(cx);
2753        // Search using valid regexp
2754        search_bar
2755            .update_in(cx, |search_bar, window, cx| {
2756                search_bar.enable_search_option(SearchOptions::REGEX, window, cx);
2757                search_bar.search("expression", None, window, cx)
2758            })
2759            .await
2760            .unwrap();
2761        editor.update_in(cx, |editor, window, cx| {
2762            assert_eq!(
2763                display_points_of(editor.all_text_background_highlights(window, cx)),
2764                &[
2765                    DisplayPoint::new(DisplayRow(0), 10)..DisplayPoint::new(DisplayRow(0), 20),
2766                    DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 19),
2767                ],
2768            );
2769        });
2770
2771        // Now, the expression is invalid
2772        search_bar
2773            .update_in(cx, |search_bar, window, cx| {
2774                search_bar.search("expression (", None, window, cx)
2775            })
2776            .await
2777            .unwrap_err();
2778        editor.update_in(cx, |editor, window, cx| {
2779            assert!(
2780                display_points_of(editor.all_text_background_highlights(window, cx)).is_empty(),
2781            );
2782        });
2783    }
2784
2785    #[gpui::test]
2786    async fn test_search_options_changes(cx: &mut TestAppContext) {
2787        let (_editor, search_bar, cx) = init_test(cx);
2788        update_search_settings(
2789            SearchSettings {
2790                button: true,
2791                whole_word: false,
2792                case_sensitive: false,
2793                include_ignored: false,
2794                regex: false,
2795            },
2796            cx,
2797        );
2798
2799        let deploy = Deploy {
2800            focus: true,
2801            replace_enabled: false,
2802            selection_search_enabled: true,
2803        };
2804
2805        search_bar.update_in(cx, |search_bar, window, cx| {
2806            assert_eq!(
2807                search_bar.search_options,
2808                SearchOptions::NONE,
2809                "Should have no search options enabled by default"
2810            );
2811            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
2812            assert_eq!(
2813                search_bar.search_options,
2814                SearchOptions::WHOLE_WORD,
2815                "Should enable the option toggled"
2816            );
2817            assert!(
2818                !search_bar.dismissed,
2819                "Search bar should be present and visible"
2820            );
2821            search_bar.deploy(&deploy, window, cx);
2822            assert_eq!(
2823                search_bar.configured_options,
2824                SearchOptions::NONE,
2825                "Should have configured search options matching the settings"
2826            );
2827            assert_eq!(
2828                search_bar.search_options,
2829                SearchOptions::WHOLE_WORD,
2830                "After (re)deploying, the option should still be enabled"
2831            );
2832
2833            search_bar.dismiss(&Dismiss, window, cx);
2834            search_bar.deploy(&deploy, window, cx);
2835            assert_eq!(
2836                search_bar.search_options,
2837                SearchOptions::NONE,
2838                "After hiding and showing the search bar, default options should be used"
2839            );
2840
2841            search_bar.toggle_search_option(SearchOptions::REGEX, window, cx);
2842            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
2843            assert_eq!(
2844                search_bar.search_options,
2845                SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2846                "Should enable the options toggled"
2847            );
2848            assert!(
2849                !search_bar.dismissed,
2850                "Search bar should be present and visible"
2851            );
2852        });
2853
2854        update_search_settings(
2855            SearchSettings {
2856                button: true,
2857                whole_word: false,
2858                case_sensitive: true,
2859                include_ignored: false,
2860                regex: false,
2861            },
2862            cx,
2863        );
2864        search_bar.update_in(cx, |search_bar, window, cx| {
2865            assert_eq!(
2866                search_bar.search_options,
2867                SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2868                "Should have no search options enabled by default"
2869            );
2870
2871            search_bar.deploy(&deploy, window, cx);
2872            assert_eq!(
2873                search_bar.configured_options,
2874                SearchOptions::CASE_SENSITIVE,
2875                "Should have configured search options matching the settings"
2876            );
2877            assert_eq!(
2878                search_bar.search_options,
2879                SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2880                "Toggling a non-dismissed search bar with custom options should not change the default options"
2881            );
2882            search_bar.dismiss(&Dismiss, window, cx);
2883            search_bar.deploy(&deploy, window, cx);
2884            assert_eq!(
2885                search_bar.search_options,
2886                SearchOptions::CASE_SENSITIVE,
2887                "After hiding and showing the search bar, default options should be used"
2888            );
2889        });
2890    }
2891
2892    fn update_search_settings(search_settings: SearchSettings, cx: &mut TestAppContext) {
2893        cx.update(|cx| {
2894            SettingsStore::update_global(cx, |store, cx| {
2895                store.update_user_settings::<EditorSettings>(cx, |settings| {
2896                    settings.search = Some(search_settings);
2897                });
2898            });
2899        });
2900    }
2901}