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