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