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