buffer_search.rs

   1use crate::{
   2    history::SearchHistory,
   3    mode::{next_mode, SearchMode, Side},
   4    search_bar::{render_nav_button, render_search_mode_button},
   5    CycleMode, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions,
   6    SelectAllMatches, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleReplace,
   7    ToggleWholeWord,
   8};
   9use collections::HashMap;
  10use editor::Editor;
  11use futures::channel::oneshot;
  12use gpui::{
  13    actions, elements::*, impl_actions, Action, AnyViewHandle, AppContext, Entity, Subscription,
  14    Task, View, ViewContext, ViewHandle, WindowContext,
  15};
  16use project::search::SearchQuery;
  17use serde::Deserialize;
  18use std::{any::Any, sync::Arc};
  19
  20use util::ResultExt;
  21use workspace::{
  22    item::ItemHandle,
  23    searchable::{Direction, SearchEvent, SearchableItemHandle, WeakSearchableItemHandle},
  24    Pane, ToolbarItemLocation, ToolbarItemView,
  25};
  26
  27#[derive(Clone, Deserialize, PartialEq)]
  28pub struct Deploy {
  29    pub focus: bool,
  30}
  31
  32actions!(buffer_search, [Dismiss, FocusEditor]);
  33impl_actions!(buffer_search, [Deploy]);
  34
  35pub enum Event {
  36    UpdateLocation,
  37}
  38
  39pub fn init(cx: &mut AppContext) {
  40    cx.add_action(BufferSearchBar::deploy_bar);
  41    cx.add_action(BufferSearchBar::dismiss);
  42    cx.add_action(BufferSearchBar::focus_editor);
  43    cx.add_action(BufferSearchBar::select_next_match);
  44    cx.add_action(BufferSearchBar::select_prev_match);
  45    cx.add_action(BufferSearchBar::select_all_matches);
  46    cx.add_action(BufferSearchBar::select_next_match_on_pane);
  47    cx.add_action(BufferSearchBar::select_prev_match_on_pane);
  48    cx.add_action(BufferSearchBar::select_all_matches_on_pane);
  49    cx.add_action(BufferSearchBar::handle_editor_cancel);
  50    cx.add_action(BufferSearchBar::next_history_query);
  51    cx.add_action(BufferSearchBar::previous_history_query);
  52    cx.add_action(BufferSearchBar::cycle_mode);
  53    cx.add_action(BufferSearchBar::cycle_mode_on_pane);
  54    cx.add_action(BufferSearchBar::replace_all);
  55    cx.add_action(BufferSearchBar::replace_next);
  56    cx.add_action(BufferSearchBar::replace_all_on_pane);
  57    cx.add_action(BufferSearchBar::replace_next_on_pane);
  58    cx.add_action(BufferSearchBar::toggle_replace);
  59    add_toggle_option_action::<ToggleCaseSensitive>(SearchOptions::CASE_SENSITIVE, cx);
  60    add_toggle_option_action::<ToggleWholeWord>(SearchOptions::WHOLE_WORD, cx);
  61}
  62
  63fn add_toggle_option_action<A: Action>(option: SearchOptions, cx: &mut AppContext) {
  64    cx.add_action(move |pane: &mut Pane, _: &A, cx: &mut ViewContext<Pane>| {
  65        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
  66            search_bar.update(cx, |search_bar, cx| {
  67                if search_bar.show(cx) {
  68                    search_bar.toggle_search_option(option, cx);
  69                }
  70            });
  71        }
  72        cx.propagate_action();
  73    });
  74}
  75
  76pub struct BufferSearchBar {
  77    query_editor: ViewHandle<Editor>,
  78    replacement_editor: ViewHandle<Editor>,
  79    active_searchable_item: Option<Box<dyn SearchableItemHandle>>,
  80    active_match_index: Option<usize>,
  81    active_searchable_item_subscription: Option<Subscription>,
  82    active_search: Option<Arc<SearchQuery>>,
  83    searchable_items_with_matches:
  84        HashMap<Box<dyn WeakSearchableItemHandle>, Vec<Box<dyn Any + Send>>>,
  85    pending_search: Option<Task<()>>,
  86    search_options: SearchOptions,
  87    default_options: SearchOptions,
  88    query_contains_error: bool,
  89    dismissed: bool,
  90    search_history: SearchHistory,
  91    current_mode: SearchMode,
  92    replace_is_active: bool,
  93}
  94
  95impl Entity for BufferSearchBar {
  96    type Event = Event;
  97}
  98
  99impl View for BufferSearchBar {
 100    fn ui_name() -> &'static str {
 101        "BufferSearchBar"
 102    }
 103
 104    fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
 105        if cx.is_self_focused() {
 106            cx.focus(&self.query_editor);
 107        }
 108    }
 109
 110    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
 111        let theme = theme::current(cx).clone();
 112        let query_container_style = if self.query_contains_error {
 113            theme.search.invalid_editor
 114        } else {
 115            theme.search.editor.input.container
 116        };
 117        let supported_options = self
 118            .active_searchable_item
 119            .as_ref()
 120            .map(|active_searchable_item| active_searchable_item.supported_options())
 121            .unwrap_or_default();
 122
 123        let previous_query_keystrokes =
 124            cx.binding_for_action(&PreviousHistoryQuery {})
 125                .map(|binding| {
 126                    binding
 127                        .keystrokes()
 128                        .iter()
 129                        .map(|k| k.to_string())
 130                        .collect::<Vec<_>>()
 131                });
 132        let next_query_keystrokes = cx.binding_for_action(&NextHistoryQuery {}).map(|binding| {
 133            binding
 134                .keystrokes()
 135                .iter()
 136                .map(|k| k.to_string())
 137                .collect::<Vec<_>>()
 138        });
 139        let new_placeholder_text = match (previous_query_keystrokes, next_query_keystrokes) {
 140            (Some(previous_query_keystrokes), Some(next_query_keystrokes)) => {
 141                format!(
 142                    "Search ({}/{} for previous/next query)",
 143                    previous_query_keystrokes.join(" "),
 144                    next_query_keystrokes.join(" ")
 145                )
 146            }
 147            (None, Some(next_query_keystrokes)) => {
 148                format!(
 149                    "Search ({} for next query)",
 150                    next_query_keystrokes.join(" ")
 151                )
 152            }
 153            (Some(previous_query_keystrokes), None) => {
 154                format!(
 155                    "Search ({} for previous query)",
 156                    previous_query_keystrokes.join(" ")
 157                )
 158            }
 159            (None, None) => String::new(),
 160        };
 161        self.query_editor.update(cx, |editor, cx| {
 162            editor.set_placeholder_text(new_placeholder_text, cx);
 163        });
 164        self.replacement_editor.update(cx, |editor, cx| {
 165            editor.set_placeholder_text("Replace with...", cx);
 166        });
 167        let search_button_for_mode = |mode, side, cx: &mut ViewContext<BufferSearchBar>| {
 168            let is_active = self.current_mode == mode;
 169
 170            render_search_mode_button(
 171                mode,
 172                side,
 173                is_active,
 174                move |_, this, cx| {
 175                    this.activate_search_mode(mode, cx);
 176                },
 177                cx,
 178            )
 179        };
 180        let search_option_button = |option| {
 181            let is_active = self.search_options.contains(option);
 182            option.as_button(
 183                is_active,
 184                theme.tooltip.clone(),
 185                theme.search.option_button_component.clone(),
 186            )
 187        };
 188        let match_count = self
 189            .active_searchable_item
 190            .as_ref()
 191            .and_then(|searchable_item| {
 192                if self.query(cx).is_empty() {
 193                    return None;
 194                }
 195                let matches = self
 196                    .searchable_items_with_matches
 197                    .get(&searchable_item.downgrade())?;
 198                let message = if let Some(match_ix) = self.active_match_index {
 199                    format!("{}/{}", match_ix + 1, matches.len())
 200                } else {
 201                    "No matches".to_string()
 202                };
 203
 204                Some(
 205                    Label::new(message, theme.search.match_index.text.clone())
 206                        .contained()
 207                        .with_style(theme.search.match_index.container)
 208                        .aligned(),
 209                )
 210            });
 211        let nav_button_for_direction = |label, direction, cx: &mut ViewContext<Self>| {
 212            render_nav_button(
 213                label,
 214                direction,
 215                self.active_match_index.is_some(),
 216                move |_, this, cx| match direction {
 217                    Direction::Prev => this.select_prev_match(&Default::default(), cx),
 218                    Direction::Next => this.select_next_match(&Default::default(), cx),
 219                },
 220                cx,
 221            )
 222        };
 223        let query_column = Flex::row()
 224            .with_child(
 225                Svg::for_style(theme.search.editor_icon.clone().icon)
 226                    .contained()
 227                    .with_style(theme.search.editor_icon.clone().container),
 228            )
 229            .with_child(ChildView::new(&self.query_editor, cx).flex(1., true))
 230            .with_child(
 231                Flex::row()
 232                    .with_children(
 233                        supported_options
 234                            .case
 235                            .then(|| search_option_button(SearchOptions::CASE_SENSITIVE)),
 236                    )
 237                    .with_children(
 238                        supported_options
 239                            .word
 240                            .then(|| search_option_button(SearchOptions::WHOLE_WORD)),
 241                    )
 242                    .flex_float()
 243                    .contained(),
 244            )
 245            .align_children_center()
 246            .contained()
 247            .with_style(query_container_style)
 248            .constrained()
 249            .with_min_width(theme.search.editor.min_width)
 250            .with_max_width(theme.search.editor.max_width)
 251            .with_height(theme.search.search_bar_row_height)
 252            .flex(1., false);
 253        let should_show_replace_input = self.replace_is_active && supported_options.replacement;
 254
 255        let replacement = should_show_replace_input.then(|| {
 256            Flex::row()
 257                .with_child(
 258                    Svg::for_style(theme.search.replace_icon.clone().icon)
 259                        .contained()
 260                        .with_style(theme.search.replace_icon.clone().container),
 261                )
 262                .with_child(ChildView::new(&self.replacement_editor, cx).flex(1., true))
 263                .align_children_center()
 264                .flex(1., true)
 265                .contained()
 266                .with_style(query_container_style)
 267                .constrained()
 268                .with_min_width(theme.search.editor.min_width)
 269                .with_max_width(theme.search.editor.max_width)
 270                .with_height(theme.search.search_bar_row_height)
 271                .flex(1., false)
 272        });
 273        let replace_all = should_show_replace_input.then(|| {
 274            super::replace_action(
 275                ReplaceAll,
 276                "Replace all",
 277                "icons/replace_all.svg",
 278                theme.tooltip.clone(),
 279                theme.search.action_button.clone(),
 280            )
 281        });
 282        let replace_next = should_show_replace_input.then(|| {
 283            super::replace_action(
 284                ReplaceNext,
 285                "Replace next",
 286                "icons/replace_next.svg",
 287                theme.tooltip.clone(),
 288                theme.search.action_button.clone(),
 289            )
 290        });
 291        let switches_column = supported_options.replacement.then(|| {
 292            Flex::row()
 293                .align_children_center()
 294                .with_child(super::toggle_replace_button(
 295                    self.replace_is_active,
 296                    theme.tooltip.clone(),
 297                    theme.search.option_button_component.clone(),
 298                ))
 299                .constrained()
 300                .with_height(theme.search.search_bar_row_height)
 301                .contained()
 302                .with_style(theme.search.option_button_group)
 303        });
 304        let mode_column = Flex::row()
 305            .with_child(search_button_for_mode(
 306                SearchMode::Text,
 307                Some(Side::Left),
 308                cx,
 309            ))
 310            .with_child(search_button_for_mode(
 311                SearchMode::Regex,
 312                Some(Side::Right),
 313                cx,
 314            ))
 315            .contained()
 316            .with_style(theme.search.modes_container)
 317            .constrained()
 318            .with_height(theme.search.search_bar_row_height);
 319
 320        let nav_column = Flex::row()
 321            .align_children_center()
 322            .with_children(replace_next)
 323            .with_children(replace_all)
 324            .with_child(self.render_action_button("icons/select-all.svg", cx))
 325            .with_child(Flex::row().with_children(match_count))
 326            .with_child(nav_button_for_direction("<", Direction::Prev, cx))
 327            .with_child(nav_button_for_direction(">", Direction::Next, cx))
 328            .constrained()
 329            .with_height(theme.search.search_bar_row_height)
 330            .flex_float();
 331
 332        Flex::row()
 333            .with_child(query_column)
 334            .with_children(switches_column)
 335            .with_children(replacement)
 336            .with_child(mode_column)
 337            .with_child(nav_column)
 338            .contained()
 339            .with_style(theme.search.container)
 340            .into_any_named("search bar")
 341    }
 342}
 343
 344impl ToolbarItemView for BufferSearchBar {
 345    fn set_active_pane_item(
 346        &mut self,
 347        item: Option<&dyn ItemHandle>,
 348        cx: &mut ViewContext<Self>,
 349    ) -> ToolbarItemLocation {
 350        cx.notify();
 351        self.active_searchable_item_subscription.take();
 352        self.active_searchable_item.take();
 353        self.pending_search.take();
 354
 355        if let Some(searchable_item_handle) =
 356            item.and_then(|item| item.to_searchable_item_handle(cx))
 357        {
 358            let this = cx.weak_handle();
 359            self.active_searchable_item_subscription =
 360                Some(searchable_item_handle.subscribe_to_search_events(
 361                    cx,
 362                    Box::new(move |search_event, cx| {
 363                        if let Some(this) = this.upgrade(cx) {
 364                            this.update(cx, |this, cx| {
 365                                this.on_active_searchable_item_event(search_event, cx)
 366                            });
 367                        }
 368                    }),
 369                ));
 370
 371            self.active_searchable_item = Some(searchable_item_handle);
 372            let _ = self.update_matches(cx);
 373            if !self.dismissed {
 374                return ToolbarItemLocation::Secondary;
 375            }
 376        }
 377
 378        ToolbarItemLocation::Hidden
 379    }
 380
 381    fn location_for_event(
 382        &self,
 383        _: &Self::Event,
 384        _: ToolbarItemLocation,
 385        _: &AppContext,
 386    ) -> ToolbarItemLocation {
 387        if self.active_searchable_item.is_some() && !self.dismissed {
 388            ToolbarItemLocation::Secondary
 389        } else {
 390            ToolbarItemLocation::Hidden
 391        }
 392    }
 393
 394    fn row_count(&self, _: &ViewContext<Self>) -> usize {
 395        1
 396    }
 397}
 398
 399impl BufferSearchBar {
 400    pub fn new(cx: &mut ViewContext<Self>) -> Self {
 401        let query_editor = cx.add_view(|cx| {
 402            Editor::auto_height(
 403                2,
 404                Some(Arc::new(|theme| theme.search.editor.input.clone())),
 405                cx,
 406            )
 407        });
 408        cx.subscribe(&query_editor, Self::on_query_editor_event)
 409            .detach();
 410        let replacement_editor = cx.add_view(|cx| {
 411            Editor::auto_height(
 412                2,
 413                Some(Arc::new(|theme| theme.search.editor.input.clone())),
 414                cx,
 415            )
 416        });
 417        // cx.subscribe(&replacement_editor, Self::on_query_editor_event)
 418        //     .detach();
 419        Self {
 420            query_editor,
 421            replacement_editor,
 422            active_searchable_item: None,
 423            active_searchable_item_subscription: None,
 424            active_match_index: None,
 425            searchable_items_with_matches: Default::default(),
 426            default_options: SearchOptions::NONE,
 427            search_options: SearchOptions::NONE,
 428            pending_search: None,
 429            query_contains_error: false,
 430            dismissed: true,
 431            search_history: SearchHistory::default(),
 432            current_mode: SearchMode::default(),
 433            active_search: None,
 434            replace_is_active: false,
 435        }
 436    }
 437
 438    pub fn is_dismissed(&self) -> bool {
 439        self.dismissed
 440    }
 441
 442    pub fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext<Self>) {
 443        self.dismissed = true;
 444        for searchable_item in self.searchable_items_with_matches.keys() {
 445            if let Some(searchable_item) =
 446                WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
 447            {
 448                searchable_item.clear_matches(cx);
 449            }
 450        }
 451        if let Some(active_editor) = self.active_searchable_item.as_ref() {
 452            cx.focus(active_editor.as_any());
 453        }
 454        cx.emit(Event::UpdateLocation);
 455        cx.notify();
 456    }
 457
 458    pub fn deploy(&mut self, deploy: &Deploy, cx: &mut ViewContext<Self>) -> bool {
 459        if self.show(cx) {
 460            self.search_suggested(cx);
 461            if deploy.focus {
 462                self.select_query(cx);
 463                cx.focus_self();
 464            }
 465            return true;
 466        }
 467
 468        false
 469    }
 470
 471    pub fn show(&mut self, cx: &mut ViewContext<Self>) -> bool {
 472        if self.active_searchable_item.is_none() {
 473            return false;
 474        }
 475        self.dismissed = false;
 476        cx.notify();
 477        cx.emit(Event::UpdateLocation);
 478        true
 479    }
 480
 481    pub fn search_suggested(&mut self, cx: &mut ViewContext<Self>) {
 482        let search = self
 483            .query_suggestion(cx)
 484            .map(|suggestion| self.search(&suggestion, Some(self.default_options), cx));
 485
 486        if let Some(search) = search {
 487            cx.spawn(|this, mut cx| async move {
 488                search.await?;
 489                this.update(&mut cx, |this, cx| this.activate_current_match(cx))
 490            })
 491            .detach_and_log_err(cx);
 492        }
 493    }
 494
 495    pub fn activate_current_match(&mut self, cx: &mut ViewContext<Self>) {
 496        if let Some(match_ix) = self.active_match_index {
 497            if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
 498                if let Some(matches) = self
 499                    .searchable_items_with_matches
 500                    .get(&active_searchable_item.downgrade())
 501                {
 502                    active_searchable_item.activate_match(match_ix, matches, cx)
 503                }
 504            }
 505        }
 506    }
 507
 508    pub fn select_query(&mut self, cx: &mut ViewContext<Self>) {
 509        self.query_editor.update(cx, |query_editor, cx| {
 510            query_editor.select_all(&Default::default(), cx);
 511        });
 512    }
 513
 514    pub fn query(&self, cx: &WindowContext) -> String {
 515        self.query_editor.read(cx).text(cx)
 516    }
 517    pub fn replacement(&self, cx: &WindowContext) -> String {
 518        self.replacement_editor.read(cx).text(cx)
 519    }
 520    pub fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> Option<String> {
 521        self.active_searchable_item
 522            .as_ref()
 523            .map(|searchable_item| searchable_item.query_suggestion(cx))
 524    }
 525
 526    pub fn search(
 527        &mut self,
 528        query: &str,
 529        options: Option<SearchOptions>,
 530        cx: &mut ViewContext<Self>,
 531    ) -> oneshot::Receiver<()> {
 532        let options = options.unwrap_or(self.default_options);
 533        if query != self.query(cx) || self.search_options != options {
 534            self.query_editor.update(cx, |query_editor, cx| {
 535                query_editor.buffer().update(cx, |query_buffer, cx| {
 536                    let len = query_buffer.len(cx);
 537                    query_buffer.edit([(0..len, query)], None, cx);
 538                });
 539            });
 540            self.search_options = options;
 541            self.query_contains_error = false;
 542            self.clear_matches(cx);
 543            cx.notify();
 544        }
 545        self.update_matches(cx)
 546    }
 547
 548    fn render_action_button(
 549        &self,
 550        icon: &'static str,
 551        cx: &mut ViewContext<Self>,
 552    ) -> AnyElement<Self> {
 553        let tooltip = "Select All Matches";
 554        let tooltip_style = theme::current(cx).tooltip.clone();
 555
 556        let theme = theme::current(cx);
 557        let style = theme.search.action_button.clone();
 558
 559        gpui::elements::Component::element(SafeStylable::with_style(
 560            theme::components::action_button::Button::action(SelectAllMatches)
 561                .with_tooltip(tooltip, tooltip_style)
 562                .with_contents(theme::components::svg::Svg::new(icon)),
 563            style,
 564        ))
 565        .into_any()
 566    }
 567
 568    pub fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext<Self>) {
 569        assert_ne!(
 570            mode,
 571            SearchMode::Semantic,
 572            "Semantic search is not supported in buffer search"
 573        );
 574        if mode == self.current_mode {
 575            return;
 576        }
 577        self.current_mode = mode;
 578        let _ = self.update_matches(cx);
 579        cx.notify();
 580    }
 581
 582    fn deploy_bar(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext<Pane>) {
 583        let mut propagate_action = true;
 584        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
 585            search_bar.update(cx, |search_bar, cx| {
 586                if search_bar.deploy(action, cx) {
 587                    propagate_action = false;
 588                }
 589            });
 590        }
 591        if propagate_action {
 592            cx.propagate_action();
 593        }
 594    }
 595
 596    fn handle_editor_cancel(pane: &mut Pane, _: &editor::Cancel, cx: &mut ViewContext<Pane>) {
 597        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
 598            if !search_bar.read(cx).dismissed {
 599                search_bar.update(cx, |search_bar, cx| search_bar.dismiss(&Dismiss, cx));
 600                return;
 601            }
 602        }
 603        cx.propagate_action();
 604    }
 605
 606    pub fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext<Self>) {
 607        if let Some(active_editor) = self.active_searchable_item.as_ref() {
 608            cx.focus(active_editor.as_any());
 609        }
 610    }
 611
 612    fn toggle_search_option(&mut self, search_option: SearchOptions, cx: &mut ViewContext<Self>) {
 613        self.search_options.toggle(search_option);
 614        self.default_options = self.search_options;
 615        let _ = self.update_matches(cx);
 616        cx.notify();
 617    }
 618
 619    pub fn set_search_options(
 620        &mut self,
 621        search_options: SearchOptions,
 622        cx: &mut ViewContext<Self>,
 623    ) {
 624        self.search_options = search_options;
 625        cx.notify();
 626    }
 627
 628    fn select_next_match(&mut self, _: &SelectNextMatch, cx: &mut ViewContext<Self>) {
 629        self.select_match(Direction::Next, 1, cx);
 630    }
 631
 632    fn select_prev_match(&mut self, _: &SelectPrevMatch, cx: &mut ViewContext<Self>) {
 633        self.select_match(Direction::Prev, 1, cx);
 634    }
 635
 636    fn select_all_matches(&mut self, _: &SelectAllMatches, cx: &mut ViewContext<Self>) {
 637        if !self.dismissed && self.active_match_index.is_some() {
 638            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
 639                if let Some(matches) = self
 640                    .searchable_items_with_matches
 641                    .get(&searchable_item.downgrade())
 642                {
 643                    searchable_item.select_matches(matches, cx);
 644                    self.focus_editor(&FocusEditor, cx);
 645                }
 646            }
 647        }
 648    }
 649
 650    pub fn select_match(&mut self, direction: Direction, count: usize, cx: &mut ViewContext<Self>) {
 651        if let Some(index) = self.active_match_index {
 652            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
 653                if let Some(matches) = self
 654                    .searchable_items_with_matches
 655                    .get(&searchable_item.downgrade())
 656                {
 657                    let new_match_index = searchable_item
 658                        .match_index_for_direction(matches, index, direction, count, cx);
 659                    searchable_item.update_matches(matches, cx);
 660                    searchable_item.activate_match(new_match_index, matches, cx);
 661                }
 662            }
 663        }
 664    }
 665
 666    fn select_next_match_on_pane(
 667        pane: &mut Pane,
 668        action: &SelectNextMatch,
 669        cx: &mut ViewContext<Pane>,
 670    ) {
 671        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
 672            search_bar.update(cx, |bar, cx| bar.select_next_match(action, cx));
 673        }
 674    }
 675
 676    fn select_prev_match_on_pane(
 677        pane: &mut Pane,
 678        action: &SelectPrevMatch,
 679        cx: &mut ViewContext<Pane>,
 680    ) {
 681        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
 682            search_bar.update(cx, |bar, cx| bar.select_prev_match(action, cx));
 683        }
 684    }
 685
 686    fn select_all_matches_on_pane(
 687        pane: &mut Pane,
 688        action: &SelectAllMatches,
 689        cx: &mut ViewContext<Pane>,
 690    ) {
 691        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
 692            search_bar.update(cx, |bar, cx| bar.select_all_matches(action, cx));
 693        }
 694    }
 695
 696    fn on_query_editor_event(
 697        &mut self,
 698        _: ViewHandle<Editor>,
 699        event: &editor::Event,
 700        cx: &mut ViewContext<Self>,
 701    ) {
 702        if let editor::Event::Edited { .. } = event {
 703            self.query_contains_error = false;
 704            self.clear_matches(cx);
 705            let search = self.update_matches(cx);
 706            cx.spawn(|this, mut cx| async move {
 707                search.await?;
 708                this.update(&mut cx, |this, cx| this.activate_current_match(cx))
 709            })
 710            .detach_and_log_err(cx);
 711        }
 712    }
 713
 714    fn on_active_searchable_item_event(&mut self, event: SearchEvent, cx: &mut ViewContext<Self>) {
 715        match event {
 716            SearchEvent::MatchesInvalidated => {
 717                let _ = self.update_matches(cx);
 718            }
 719            SearchEvent::ActiveMatchChanged => self.update_match_index(cx),
 720        }
 721    }
 722
 723    fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
 724        let mut active_item_matches = None;
 725        for (searchable_item, matches) in self.searchable_items_with_matches.drain() {
 726            if let Some(searchable_item) =
 727                WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
 728            {
 729                if Some(&searchable_item) == self.active_searchable_item.as_ref() {
 730                    active_item_matches = Some((searchable_item.downgrade(), matches));
 731                } else {
 732                    searchable_item.clear_matches(cx);
 733                }
 734            }
 735        }
 736
 737        self.searchable_items_with_matches
 738            .extend(active_item_matches);
 739    }
 740
 741    fn update_matches(&mut self, cx: &mut ViewContext<Self>) -> oneshot::Receiver<()> {
 742        let (done_tx, done_rx) = oneshot::channel();
 743        let query = self.query(cx);
 744        self.pending_search.take();
 745
 746        if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
 747            if query.is_empty() {
 748                self.active_match_index.take();
 749                active_searchable_item.clear_matches(cx);
 750                let _ = done_tx.send(());
 751                cx.notify();
 752            } else {
 753                let query: Arc<_> = if self.current_mode == SearchMode::Regex {
 754                    match SearchQuery::regex(
 755                        query,
 756                        self.search_options.contains(SearchOptions::WHOLE_WORD),
 757                        self.search_options.contains(SearchOptions::CASE_SENSITIVE),
 758                        Vec::new(),
 759                        Vec::new(),
 760                    ) {
 761                        Ok(query) => query
 762                            .with_replacement(Some(self.replacement(cx)).filter(|s| !s.is_empty())),
 763                        Err(_) => {
 764                            self.query_contains_error = true;
 765                            cx.notify();
 766                            return done_rx;
 767                        }
 768                    }
 769                } else {
 770                    SearchQuery::text(
 771                        query,
 772                        self.search_options.contains(SearchOptions::WHOLE_WORD),
 773                        self.search_options.contains(SearchOptions::CASE_SENSITIVE),
 774                        Vec::new(),
 775                        Vec::new(),
 776                    )
 777                    .with_replacement(Some(self.replacement(cx)).filter(|s| !s.is_empty()))
 778                }
 779                .into();
 780                self.active_search = Some(query.clone());
 781                let query_text = query.as_str().to_string();
 782                let matches = active_searchable_item.find_matches(query, cx);
 783
 784                let active_searchable_item = active_searchable_item.downgrade();
 785                self.pending_search = Some(cx.spawn(|this, mut cx| async move {
 786                    let matches = matches.await;
 787                    this.update(&mut cx, |this, cx| {
 788                        if let Some(active_searchable_item) =
 789                            WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx)
 790                        {
 791                            this.searchable_items_with_matches
 792                                .insert(active_searchable_item.downgrade(), matches);
 793
 794                            this.update_match_index(cx);
 795                            this.search_history.add(query_text);
 796                            if !this.dismissed {
 797                                let matches = this
 798                                    .searchable_items_with_matches
 799                                    .get(&active_searchable_item.downgrade())
 800                                    .unwrap();
 801                                active_searchable_item.update_matches(matches, cx);
 802                                let _ = done_tx.send(());
 803                            }
 804                            cx.notify();
 805                        }
 806                    })
 807                    .log_err();
 808                }));
 809            }
 810        }
 811        done_rx
 812    }
 813
 814    fn update_match_index(&mut self, cx: &mut ViewContext<Self>) {
 815        let new_index = self
 816            .active_searchable_item
 817            .as_ref()
 818            .and_then(|searchable_item| {
 819                let matches = self
 820                    .searchable_items_with_matches
 821                    .get(&searchable_item.downgrade())?;
 822                searchable_item.active_match_index(matches, cx)
 823            });
 824        if new_index != self.active_match_index {
 825            self.active_match_index = new_index;
 826            cx.notify();
 827        }
 828    }
 829
 830    fn next_history_query(&mut self, _: &NextHistoryQuery, cx: &mut ViewContext<Self>) {
 831        if let Some(new_query) = self.search_history.next().map(str::to_string) {
 832            let _ = self.search(&new_query, Some(self.search_options), cx);
 833        } else {
 834            self.search_history.reset_selection();
 835            let _ = self.search("", Some(self.search_options), cx);
 836        }
 837    }
 838
 839    fn previous_history_query(&mut self, _: &PreviousHistoryQuery, cx: &mut ViewContext<Self>) {
 840        if self.query(cx).is_empty() {
 841            if let Some(new_query) = self.search_history.current().map(str::to_string) {
 842                let _ = self.search(&new_query, Some(self.search_options), cx);
 843                return;
 844            }
 845        }
 846
 847        if let Some(new_query) = self.search_history.previous().map(str::to_string) {
 848            let _ = self.search(&new_query, Some(self.search_options), cx);
 849        }
 850    }
 851    fn cycle_mode(&mut self, _: &CycleMode, cx: &mut ViewContext<Self>) {
 852        self.activate_search_mode(next_mode(&self.current_mode, false), cx);
 853    }
 854    fn cycle_mode_on_pane(pane: &mut Pane, action: &CycleMode, cx: &mut ViewContext<Pane>) {
 855        let mut should_propagate = true;
 856        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
 857            search_bar.update(cx, |bar, cx| {
 858                if bar.show(cx) {
 859                    should_propagate = false;
 860                    bar.cycle_mode(action, cx);
 861                    false
 862                } else {
 863                    true
 864                }
 865            });
 866        }
 867        if should_propagate {
 868            cx.propagate_action();
 869        }
 870    }
 871    fn toggle_replace(&mut self, _: &ToggleReplace, _: &mut ViewContext<Self>) {
 872        if let Some(_) = &self.active_searchable_item {
 873            self.replace_is_active = !self.replace_is_active;
 874        }
 875    }
 876    fn replace_next(&mut self, _: &ReplaceNext, cx: &mut ViewContext<Self>) {
 877        if !self.dismissed && self.active_search.is_some() {
 878            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
 879                if let Some(query) = self.active_search.as_ref() {
 880                    if let Some(matches) = self
 881                        .searchable_items_with_matches
 882                        .get(&searchable_item.downgrade())
 883                    {
 884                        if let Some(active_index) = self.active_match_index {
 885                            let query = query.as_ref().clone().with_replacement(
 886                                Some(self.replacement(cx)).filter(|rep| !rep.is_empty()),
 887                            );
 888                            searchable_item.replace(&matches[active_index], &query, cx);
 889                        }
 890
 891                        self.focus_editor(&FocusEditor, cx);
 892                    }
 893                }
 894            }
 895        }
 896    }
 897    fn replace_all(&mut self, _: &ReplaceAll, cx: &mut ViewContext<Self>) {
 898        if !self.dismissed && self.active_search.is_some() {
 899            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
 900                if let Some(query) = self.active_search.as_ref() {
 901                    if let Some(matches) = self
 902                        .searchable_items_with_matches
 903                        .get(&searchable_item.downgrade())
 904                    {
 905                        let query = query.as_ref().clone().with_replacement(
 906                            Some(self.replacement(cx)).filter(|rep| !rep.is_empty()),
 907                        );
 908                        for m in matches {
 909                            searchable_item.replace(m, &query, cx);
 910                        }
 911
 912                        self.focus_editor(&FocusEditor, cx);
 913                    }
 914                }
 915            }
 916        }
 917    }
 918    fn replace_next_on_pane(pane: &mut Pane, action: &ReplaceNext, cx: &mut ViewContext<Pane>) {
 919        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
 920            search_bar.update(cx, |bar, cx| bar.replace_next(action, cx));
 921        }
 922    }
 923    fn replace_all_on_pane(pane: &mut Pane, action: &ReplaceAll, cx: &mut ViewContext<Pane>) {
 924        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
 925            search_bar.update(cx, |bar, cx| bar.replace_all(action, cx));
 926        }
 927    }
 928}
 929
 930#[cfg(test)]
 931mod tests {
 932    use super::*;
 933    use editor::{DisplayPoint, Editor};
 934    use gpui::{color::Color, test::EmptyView, TestAppContext};
 935    use language::Buffer;
 936    use unindent::Unindent as _;
 937
 938    fn init_test(cx: &mut TestAppContext) -> (ViewHandle<Editor>, ViewHandle<BufferSearchBar>) {
 939        crate::project_search::tests::init_test(cx);
 940
 941        let buffer = cx.add_model(|cx| {
 942            Buffer::new(
 943                0,
 944                cx.model_id() as u64,
 945                r#"
 946                A regular expression (shortened as regex or regexp;[1] also referred to as
 947                rational expression[2][3]) is a sequence of characters that specifies a search
 948                pattern in text. Usually such patterns are used by string-searching algorithms
 949                for "find" or "find and replace" operations on strings, or for input validation.
 950                "#
 951                .unindent(),
 952            )
 953        });
 954        let window = cx.add_window(|_| EmptyView);
 955        let editor = window.add_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx));
 956
 957        let search_bar = window.add_view(cx, |cx| {
 958            let mut search_bar = BufferSearchBar::new(cx);
 959            search_bar.set_active_pane_item(Some(&editor), cx);
 960            search_bar.show(cx);
 961            search_bar
 962        });
 963
 964        (editor, search_bar)
 965    }
 966
 967    #[gpui::test]
 968    async fn test_search_simple(cx: &mut TestAppContext) {
 969        let (editor, search_bar) = init_test(cx);
 970
 971        // Search for a string that appears with different casing.
 972        // By default, search is case-insensitive.
 973        search_bar
 974            .update(cx, |search_bar, cx| search_bar.search("us", None, cx))
 975            .await
 976            .unwrap();
 977        editor.update(cx, |editor, cx| {
 978            assert_eq!(
 979                editor.all_background_highlights(cx),
 980                &[
 981                    (
 982                        DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19),
 983                        Color::red(),
 984                    ),
 985                    (
 986                        DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
 987                        Color::red(),
 988                    ),
 989                ]
 990            );
 991        });
 992
 993        // Switch to a case sensitive search.
 994        search_bar.update(cx, |search_bar, cx| {
 995            search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx);
 996        });
 997        editor.next_notification(cx).await;
 998        editor.update(cx, |editor, cx| {
 999            assert_eq!(
1000                editor.all_background_highlights(cx),
1001                &[(
1002                    DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
1003                    Color::red(),
1004                )]
1005            );
1006        });
1007
1008        // Search for a string that appears both as a whole word and
1009        // within other words. By default, all results are found.
1010        search_bar
1011            .update(cx, |search_bar, cx| search_bar.search("or", None, cx))
1012            .await
1013            .unwrap();
1014        editor.update(cx, |editor, cx| {
1015            assert_eq!(
1016                editor.all_background_highlights(cx),
1017                &[
1018                    (
1019                        DisplayPoint::new(0, 24)..DisplayPoint::new(0, 26),
1020                        Color::red(),
1021                    ),
1022                    (
1023                        DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
1024                        Color::red(),
1025                    ),
1026                    (
1027                        DisplayPoint::new(2, 71)..DisplayPoint::new(2, 73),
1028                        Color::red(),
1029                    ),
1030                    (
1031                        DisplayPoint::new(3, 1)..DisplayPoint::new(3, 3),
1032                        Color::red(),
1033                    ),
1034                    (
1035                        DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
1036                        Color::red(),
1037                    ),
1038                    (
1039                        DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
1040                        Color::red(),
1041                    ),
1042                    (
1043                        DisplayPoint::new(3, 60)..DisplayPoint::new(3, 62),
1044                        Color::red(),
1045                    ),
1046                ]
1047            );
1048        });
1049
1050        // Switch to a whole word search.
1051        search_bar.update(cx, |search_bar, cx| {
1052            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
1053        });
1054        editor.next_notification(cx).await;
1055        editor.update(cx, |editor, cx| {
1056            assert_eq!(
1057                editor.all_background_highlights(cx),
1058                &[
1059                    (
1060                        DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
1061                        Color::red(),
1062                    ),
1063                    (
1064                        DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
1065                        Color::red(),
1066                    ),
1067                    (
1068                        DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
1069                        Color::red(),
1070                    ),
1071                ]
1072            );
1073        });
1074
1075        editor.update(cx, |editor, cx| {
1076            editor.change_selections(None, cx, |s| {
1077                s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
1078            });
1079        });
1080        search_bar.update(cx, |search_bar, cx| {
1081            assert_eq!(search_bar.active_match_index, Some(0));
1082            search_bar.select_next_match(&SelectNextMatch, cx);
1083            assert_eq!(
1084                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1085                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1086            );
1087        });
1088        search_bar.read_with(cx, |search_bar, _| {
1089            assert_eq!(search_bar.active_match_index, Some(0));
1090        });
1091
1092        search_bar.update(cx, |search_bar, cx| {
1093            search_bar.select_next_match(&SelectNextMatch, cx);
1094            assert_eq!(
1095                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1096                [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
1097            );
1098        });
1099        search_bar.read_with(cx, |search_bar, _| {
1100            assert_eq!(search_bar.active_match_index, Some(1));
1101        });
1102
1103        search_bar.update(cx, |search_bar, cx| {
1104            search_bar.select_next_match(&SelectNextMatch, cx);
1105            assert_eq!(
1106                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1107                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1108            );
1109        });
1110        search_bar.read_with(cx, |search_bar, _| {
1111            assert_eq!(search_bar.active_match_index, Some(2));
1112        });
1113
1114        search_bar.update(cx, |search_bar, cx| {
1115            search_bar.select_next_match(&SelectNextMatch, cx);
1116            assert_eq!(
1117                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1118                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1119            );
1120        });
1121        search_bar.read_with(cx, |search_bar, _| {
1122            assert_eq!(search_bar.active_match_index, Some(0));
1123        });
1124
1125        search_bar.update(cx, |search_bar, cx| {
1126            search_bar.select_prev_match(&SelectPrevMatch, cx);
1127            assert_eq!(
1128                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1129                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1130            );
1131        });
1132        search_bar.read_with(cx, |search_bar, _| {
1133            assert_eq!(search_bar.active_match_index, Some(2));
1134        });
1135
1136        search_bar.update(cx, |search_bar, cx| {
1137            search_bar.select_prev_match(&SelectPrevMatch, cx);
1138            assert_eq!(
1139                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1140                [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
1141            );
1142        });
1143        search_bar.read_with(cx, |search_bar, _| {
1144            assert_eq!(search_bar.active_match_index, Some(1));
1145        });
1146
1147        search_bar.update(cx, |search_bar, cx| {
1148            search_bar.select_prev_match(&SelectPrevMatch, cx);
1149            assert_eq!(
1150                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1151                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1152            );
1153        });
1154        search_bar.read_with(cx, |search_bar, _| {
1155            assert_eq!(search_bar.active_match_index, Some(0));
1156        });
1157
1158        // Park the cursor in between matches and ensure that going to the previous match selects
1159        // the closest match to the left.
1160        editor.update(cx, |editor, cx| {
1161            editor.change_selections(None, cx, |s| {
1162                s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
1163            });
1164        });
1165        search_bar.update(cx, |search_bar, cx| {
1166            assert_eq!(search_bar.active_match_index, Some(1));
1167            search_bar.select_prev_match(&SelectPrevMatch, cx);
1168            assert_eq!(
1169                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1170                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1171            );
1172        });
1173        search_bar.read_with(cx, |search_bar, _| {
1174            assert_eq!(search_bar.active_match_index, Some(0));
1175        });
1176
1177        // Park the cursor in between matches and ensure that going to the next match selects the
1178        // closest match to the right.
1179        editor.update(cx, |editor, cx| {
1180            editor.change_selections(None, cx, |s| {
1181                s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
1182            });
1183        });
1184        search_bar.update(cx, |search_bar, cx| {
1185            assert_eq!(search_bar.active_match_index, Some(1));
1186            search_bar.select_next_match(&SelectNextMatch, cx);
1187            assert_eq!(
1188                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1189                [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
1190            );
1191        });
1192        search_bar.read_with(cx, |search_bar, _| {
1193            assert_eq!(search_bar.active_match_index, Some(1));
1194        });
1195
1196        // Park the cursor after the last match and ensure that going to the previous match selects
1197        // the last match.
1198        editor.update(cx, |editor, cx| {
1199            editor.change_selections(None, cx, |s| {
1200                s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
1201            });
1202        });
1203        search_bar.update(cx, |search_bar, cx| {
1204            assert_eq!(search_bar.active_match_index, Some(2));
1205            search_bar.select_prev_match(&SelectPrevMatch, cx);
1206            assert_eq!(
1207                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1208                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1209            );
1210        });
1211        search_bar.read_with(cx, |search_bar, _| {
1212            assert_eq!(search_bar.active_match_index, Some(2));
1213        });
1214
1215        // Park the cursor after the last match and ensure that going to the next match selects the
1216        // first match.
1217        editor.update(cx, |editor, cx| {
1218            editor.change_selections(None, cx, |s| {
1219                s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
1220            });
1221        });
1222        search_bar.update(cx, |search_bar, cx| {
1223            assert_eq!(search_bar.active_match_index, Some(2));
1224            search_bar.select_next_match(&SelectNextMatch, cx);
1225            assert_eq!(
1226                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1227                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1228            );
1229        });
1230        search_bar.read_with(cx, |search_bar, _| {
1231            assert_eq!(search_bar.active_match_index, Some(0));
1232        });
1233
1234        // Park the cursor before the first match and ensure that going to the previous match
1235        // selects the last match.
1236        editor.update(cx, |editor, cx| {
1237            editor.change_selections(None, cx, |s| {
1238                s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
1239            });
1240        });
1241        search_bar.update(cx, |search_bar, cx| {
1242            assert_eq!(search_bar.active_match_index, Some(0));
1243            search_bar.select_prev_match(&SelectPrevMatch, cx);
1244            assert_eq!(
1245                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1246                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1247            );
1248        });
1249        search_bar.read_with(cx, |search_bar, _| {
1250            assert_eq!(search_bar.active_match_index, Some(2));
1251        });
1252    }
1253
1254    #[gpui::test]
1255    async fn test_search_option_handling(cx: &mut TestAppContext) {
1256        let (editor, search_bar) = init_test(cx);
1257
1258        // show with options should make current search case sensitive
1259        search_bar
1260            .update(cx, |search_bar, cx| {
1261                search_bar.show(cx);
1262                search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), cx)
1263            })
1264            .await
1265            .unwrap();
1266        editor.update(cx, |editor, cx| {
1267            assert_eq!(
1268                editor.all_background_highlights(cx),
1269                &[(
1270                    DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
1271                    Color::red(),
1272                )]
1273            );
1274        });
1275
1276        // search_suggested should restore default options
1277        search_bar.update(cx, |search_bar, cx| {
1278            search_bar.search_suggested(cx);
1279            assert_eq!(search_bar.search_options, SearchOptions::NONE)
1280        });
1281
1282        // toggling a search option should update the defaults
1283        search_bar
1284            .update(cx, |search_bar, cx| {
1285                search_bar.search("regex", Some(SearchOptions::CASE_SENSITIVE), cx)
1286            })
1287            .await
1288            .unwrap();
1289        search_bar.update(cx, |search_bar, cx| {
1290            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx)
1291        });
1292        editor.next_notification(cx).await;
1293        editor.update(cx, |editor, cx| {
1294            assert_eq!(
1295                editor.all_background_highlights(cx),
1296                &[(
1297                    DisplayPoint::new(0, 35)..DisplayPoint::new(0, 40),
1298                    Color::red(),
1299                ),]
1300            );
1301        });
1302
1303        // defaults should still include whole word
1304        search_bar.update(cx, |search_bar, cx| {
1305            search_bar.search_suggested(cx);
1306            assert_eq!(
1307                search_bar.search_options,
1308                SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD
1309            )
1310        });
1311    }
1312
1313    #[gpui::test]
1314    async fn test_search_select_all_matches(cx: &mut TestAppContext) {
1315        crate::project_search::tests::init_test(cx);
1316
1317        let buffer_text = r#"
1318        A regular expression (shortened as regex or regexp;[1] also referred to as
1319        rational expression[2][3]) is a sequence of characters that specifies a search
1320        pattern in text. Usually such patterns are used by string-searching algorithms
1321        for "find" or "find and replace" operations on strings, or for input validation.
1322        "#
1323        .unindent();
1324        let expected_query_matches_count = buffer_text
1325            .chars()
1326            .filter(|c| c.to_ascii_lowercase() == 'a')
1327            .count();
1328        assert!(
1329            expected_query_matches_count > 1,
1330            "Should pick a query with multiple results"
1331        );
1332        let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, buffer_text));
1333        let window = cx.add_window(|_| EmptyView);
1334        let editor = window.add_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx));
1335
1336        let search_bar = window.add_view(cx, |cx| {
1337            let mut search_bar = BufferSearchBar::new(cx);
1338            search_bar.set_active_pane_item(Some(&editor), cx);
1339            search_bar.show(cx);
1340            search_bar
1341        });
1342
1343        search_bar
1344            .update(cx, |search_bar, cx| search_bar.search("a", None, cx))
1345            .await
1346            .unwrap();
1347        search_bar.update(cx, |search_bar, cx| {
1348            cx.focus(search_bar.query_editor.as_any());
1349            search_bar.activate_current_match(cx);
1350        });
1351
1352        window.read_with(cx, |cx| {
1353            assert!(
1354                !editor.is_focused(cx),
1355                "Initially, the editor should not be focused"
1356            );
1357        });
1358
1359        let initial_selections = editor.update(cx, |editor, cx| {
1360            let initial_selections = editor.selections.display_ranges(cx);
1361            assert_eq!(
1362                initial_selections.len(), 1,
1363                "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
1364            );
1365            initial_selections
1366        });
1367        search_bar.update(cx, |search_bar, _| {
1368            assert_eq!(search_bar.active_match_index, Some(0));
1369        });
1370
1371        search_bar.update(cx, |search_bar, cx| {
1372            cx.focus(search_bar.query_editor.as_any());
1373            search_bar.select_all_matches(&SelectAllMatches, cx);
1374        });
1375        window.read_with(cx, |cx| {
1376            assert!(
1377                editor.is_focused(cx),
1378                "Should focus editor after successful SelectAllMatches"
1379            );
1380        });
1381        search_bar.update(cx, |search_bar, cx| {
1382            let all_selections =
1383                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1384            assert_eq!(
1385                all_selections.len(),
1386                expected_query_matches_count,
1387                "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1388            );
1389            assert_eq!(
1390                search_bar.active_match_index,
1391                Some(0),
1392                "Match index should not change after selecting all matches"
1393            );
1394        });
1395
1396        search_bar.update(cx, |search_bar, cx| {
1397            search_bar.select_next_match(&SelectNextMatch, cx);
1398        });
1399        window.read_with(cx, |cx| {
1400            assert!(
1401                editor.is_focused(cx),
1402                "Should still have editor focused after SelectNextMatch"
1403            );
1404        });
1405        search_bar.update(cx, |search_bar, cx| {
1406            let all_selections =
1407                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1408            assert_eq!(
1409                all_selections.len(),
1410                1,
1411                "On next match, should deselect items and select the next match"
1412            );
1413            assert_ne!(
1414                all_selections, initial_selections,
1415                "Next match should be different from the first selection"
1416            );
1417            assert_eq!(
1418                search_bar.active_match_index,
1419                Some(1),
1420                "Match index should be updated to the next one"
1421            );
1422        });
1423
1424        search_bar.update(cx, |search_bar, cx| {
1425            cx.focus(search_bar.query_editor.as_any());
1426            search_bar.select_all_matches(&SelectAllMatches, cx);
1427        });
1428        window.read_with(cx, |cx| {
1429            assert!(
1430                editor.is_focused(cx),
1431                "Should focus editor after successful SelectAllMatches"
1432            );
1433        });
1434        search_bar.update(cx, |search_bar, cx| {
1435            let all_selections =
1436                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1437            assert_eq!(
1438                all_selections.len(),
1439                expected_query_matches_count,
1440                "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1441            );
1442            assert_eq!(
1443                search_bar.active_match_index,
1444                Some(1),
1445                "Match index should not change after selecting all matches"
1446            );
1447        });
1448
1449        search_bar.update(cx, |search_bar, cx| {
1450            search_bar.select_prev_match(&SelectPrevMatch, cx);
1451        });
1452        window.read_with(cx, |cx| {
1453            assert!(
1454                editor.is_focused(cx),
1455                "Should still have editor focused after SelectPrevMatch"
1456            );
1457        });
1458        let last_match_selections = search_bar.update(cx, |search_bar, cx| {
1459            let all_selections =
1460                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1461            assert_eq!(
1462                all_selections.len(),
1463                1,
1464                "On previous match, should deselect items and select the previous item"
1465            );
1466            assert_eq!(
1467                all_selections, initial_selections,
1468                "Previous match should be the same as the first selection"
1469            );
1470            assert_eq!(
1471                search_bar.active_match_index,
1472                Some(0),
1473                "Match index should be updated to the previous one"
1474            );
1475            all_selections
1476        });
1477
1478        search_bar
1479            .update(cx, |search_bar, cx| {
1480                cx.focus(search_bar.query_editor.as_any());
1481                search_bar.search("abas_nonexistent_match", None, cx)
1482            })
1483            .await
1484            .unwrap();
1485        search_bar.update(cx, |search_bar, cx| {
1486            search_bar.select_all_matches(&SelectAllMatches, cx);
1487        });
1488        window.read_with(cx, |cx| {
1489            assert!(
1490                !editor.is_focused(cx),
1491                "Should not switch focus to editor if SelectAllMatches does not find any matches"
1492            );
1493        });
1494        search_bar.update(cx, |search_bar, cx| {
1495            let all_selections =
1496                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1497            assert_eq!(
1498                all_selections, last_match_selections,
1499                "Should not select anything new if there are no matches"
1500            );
1501            assert!(
1502                search_bar.active_match_index.is_none(),
1503                "For no matches, there should be no active match index"
1504            );
1505        });
1506    }
1507
1508    #[gpui::test]
1509    async fn test_search_query_history(cx: &mut TestAppContext) {
1510        crate::project_search::tests::init_test(cx);
1511
1512        let buffer_text = r#"
1513        A regular expression (shortened as regex or regexp;[1] also referred to as
1514        rational expression[2][3]) is a sequence of characters that specifies a search
1515        pattern in text. Usually such patterns are used by string-searching algorithms
1516        for "find" or "find and replace" operations on strings, or for input validation.
1517        "#
1518        .unindent();
1519        let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, buffer_text));
1520        let window = cx.add_window(|_| EmptyView);
1521
1522        let editor = window.add_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx));
1523
1524        let search_bar = window.add_view(cx, |cx| {
1525            let mut search_bar = BufferSearchBar::new(cx);
1526            search_bar.set_active_pane_item(Some(&editor), cx);
1527            search_bar.show(cx);
1528            search_bar
1529        });
1530
1531        // Add 3 search items into the history.
1532        search_bar
1533            .update(cx, |search_bar, cx| search_bar.search("a", None, cx))
1534            .await
1535            .unwrap();
1536        search_bar
1537            .update(cx, |search_bar, cx| search_bar.search("b", None, cx))
1538            .await
1539            .unwrap();
1540        search_bar
1541            .update(cx, |search_bar, cx| {
1542                search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), cx)
1543            })
1544            .await
1545            .unwrap();
1546        // Ensure that the latest search is active.
1547        search_bar.read_with(cx, |search_bar, cx| {
1548            assert_eq!(search_bar.query(cx), "c");
1549            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1550        });
1551
1552        // Next history query after the latest should set the query to the empty string.
1553        search_bar.update(cx, |search_bar, cx| {
1554            search_bar.next_history_query(&NextHistoryQuery, cx);
1555        });
1556        search_bar.read_with(cx, |search_bar, cx| {
1557            assert_eq!(search_bar.query(cx), "");
1558            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1559        });
1560        search_bar.update(cx, |search_bar, cx| {
1561            search_bar.next_history_query(&NextHistoryQuery, cx);
1562        });
1563        search_bar.read_with(cx, |search_bar, cx| {
1564            assert_eq!(search_bar.query(cx), "");
1565            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1566        });
1567
1568        // First previous query for empty current query should set the query to the latest.
1569        search_bar.update(cx, |search_bar, cx| {
1570            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1571        });
1572        search_bar.read_with(cx, |search_bar, cx| {
1573            assert_eq!(search_bar.query(cx), "c");
1574            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1575        });
1576
1577        // Further previous items should go over the history in reverse order.
1578        search_bar.update(cx, |search_bar, cx| {
1579            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1580        });
1581        search_bar.read_with(cx, |search_bar, cx| {
1582            assert_eq!(search_bar.query(cx), "b");
1583            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1584        });
1585
1586        // Previous items should never go behind the first history item.
1587        search_bar.update(cx, |search_bar, cx| {
1588            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1589        });
1590        search_bar.read_with(cx, |search_bar, cx| {
1591            assert_eq!(search_bar.query(cx), "a");
1592            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1593        });
1594        search_bar.update(cx, |search_bar, cx| {
1595            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1596        });
1597        search_bar.read_with(cx, |search_bar, cx| {
1598            assert_eq!(search_bar.query(cx), "a");
1599            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1600        });
1601
1602        // Next items should go over the history in the original order.
1603        search_bar.update(cx, |search_bar, cx| {
1604            search_bar.next_history_query(&NextHistoryQuery, cx);
1605        });
1606        search_bar.read_with(cx, |search_bar, cx| {
1607            assert_eq!(search_bar.query(cx), "b");
1608            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1609        });
1610
1611        search_bar
1612            .update(cx, |search_bar, cx| search_bar.search("ba", None, cx))
1613            .await
1614            .unwrap();
1615        search_bar.read_with(cx, |search_bar, cx| {
1616            assert_eq!(search_bar.query(cx), "ba");
1617            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1618        });
1619
1620        // New search input should add another entry to history and move the selection to the end of the history.
1621        search_bar.update(cx, |search_bar, cx| {
1622            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1623        });
1624        search_bar.read_with(cx, |search_bar, cx| {
1625            assert_eq!(search_bar.query(cx), "c");
1626            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1627        });
1628        search_bar.update(cx, |search_bar, cx| {
1629            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1630        });
1631        search_bar.read_with(cx, |search_bar, cx| {
1632            assert_eq!(search_bar.query(cx), "b");
1633            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1634        });
1635        search_bar.update(cx, |search_bar, cx| {
1636            search_bar.next_history_query(&NextHistoryQuery, cx);
1637        });
1638        search_bar.read_with(cx, |search_bar, cx| {
1639            assert_eq!(search_bar.query(cx), "c");
1640            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1641        });
1642        search_bar.update(cx, |search_bar, cx| {
1643            search_bar.next_history_query(&NextHistoryQuery, cx);
1644        });
1645        search_bar.read_with(cx, |search_bar, cx| {
1646            assert_eq!(search_bar.query(cx), "ba");
1647            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1648        });
1649        search_bar.update(cx, |search_bar, cx| {
1650            search_bar.next_history_query(&NextHistoryQuery, cx);
1651        });
1652        search_bar.read_with(cx, |search_bar, cx| {
1653            assert_eq!(search_bar.query(cx), "");
1654            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1655        });
1656    }
1657    #[gpui::test]
1658    async fn test_replace_simple(cx: &mut TestAppContext) {
1659        let (editor, search_bar) = init_test(cx);
1660
1661        search_bar
1662            .update(cx, |search_bar, cx| {
1663                search_bar.search("expression", None, cx)
1664            })
1665            .await
1666            .unwrap();
1667
1668        search_bar.update(cx, |search_bar, cx| {
1669            search_bar.replacement_editor.update(cx, |editor, cx| {
1670                // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally.
1671                editor.set_text("expr$1", cx);
1672            });
1673            search_bar.replace_all(&ReplaceAll, cx)
1674        });
1675        assert_eq!(
1676            editor.read_with(cx, |this, cx| { this.text(cx) }),
1677            r#"
1678        A regular expr$1 (shortened as regex or regexp;[1] also referred to as
1679        rational expr$1[2][3]) is a sequence of characters that specifies a search
1680        pattern in text. Usually such patterns are used by string-searching algorithms
1681        for "find" or "find and replace" operations on strings, or for input validation.
1682        "#
1683            .unindent()
1684        );
1685
1686        // Search for word boundaries and replace just a single one.
1687        search_bar
1688            .update(cx, |search_bar, cx| {
1689                search_bar.search("or", Some(SearchOptions::WHOLE_WORD), cx)
1690            })
1691            .await
1692            .unwrap();
1693
1694        search_bar.update(cx, |search_bar, cx| {
1695            search_bar.replacement_editor.update(cx, |editor, cx| {
1696                editor.set_text("banana", cx);
1697            });
1698            search_bar.replace_next(&ReplaceNext, cx)
1699        });
1700        // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text.
1701        assert_eq!(
1702            editor.read_with(cx, |this, cx| { this.text(cx) }),
1703            r#"
1704        A regular expr$1 (shortened as regex banana regexp;[1] also referred to as
1705        rational expr$1[2][3]) is a sequence of characters that specifies a search
1706        pattern in text. Usually such patterns are used by string-searching algorithms
1707        for "find" or "find and replace" operations on strings, or for input validation.
1708        "#
1709            .unindent()
1710        );
1711        // Let's turn on regex mode.
1712        search_bar
1713            .update(cx, |search_bar, cx| {
1714                search_bar.activate_search_mode(SearchMode::Regex, cx);
1715                search_bar.search("\\[([^\\]]+)\\]", None, cx)
1716            })
1717            .await
1718            .unwrap();
1719        search_bar.update(cx, |search_bar, cx| {
1720            search_bar.replacement_editor.update(cx, |editor, cx| {
1721                editor.set_text("${1}number", cx);
1722            });
1723            search_bar.replace_all(&ReplaceAll, cx)
1724        });
1725        assert_eq!(
1726            editor.read_with(cx, |this, cx| { this.text(cx) }),
1727            r#"
1728        A regular expr$1 (shortened as regex banana regexp;1number also referred to as
1729        rational expr$12number3number) is a sequence of characters that specifies a search
1730        pattern in text. Usually such patterns are used by string-searching algorithms
1731        for "find" or "find and replace" operations on strings, or for input validation.
1732        "#
1733            .unindent()
1734        );
1735        // Now with a whole-word twist.
1736        search_bar
1737            .update(cx, |search_bar, cx| {
1738                search_bar.activate_search_mode(SearchMode::Regex, cx);
1739                search_bar.search("a\\w+s", Some(SearchOptions::WHOLE_WORD), cx)
1740            })
1741            .await
1742            .unwrap();
1743        search_bar.update(cx, |search_bar, cx| {
1744            search_bar.replacement_editor.update(cx, |editor, cx| {
1745                editor.set_text("things", cx);
1746            });
1747            search_bar.replace_all(&ReplaceAll, cx)
1748        });
1749        // The only word affected by this edit should be `algorithms`, even though there's a bunch
1750        // of words in this text that would match this regex if not for WHOLE_WORD.
1751        assert_eq!(
1752            editor.read_with(cx, |this, cx| { this.text(cx) }),
1753            r#"
1754        A regular expr$1 (shortened as regex banana regexp;1number also referred to as
1755        rational expr$12number3number) is a sequence of characters that specifies a search
1756        pattern in text. Usually such patterns are used by string-searching things
1757        for "find" or "find and replace" operations on strings, or for input validation.
1758        "#
1759            .unindent()
1760        );
1761    }
1762}