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