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