buffer_search.rs

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