buffer_search.rs

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