buffer_search.rs

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