buffer_search.rs

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