buffer_search.rs

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