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_enabled: 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_enabled && 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_enabled,
 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_child(mode_column)
 351            .with_children(switches_column)
 352            .with_children(replacement)
 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_enabled: 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            .filter(|suggestion| !suggestion.is_empty())
 541    }
 542
 543    pub fn set_replacement(&mut self, replacement: Option<&str>, cx: &mut ViewContext<Self>) {
 544        if replacement.is_none() {
 545            self.replace_enabled = false;
 546            return;
 547        }
 548        self.replace_enabled = true;
 549        self.replacement_editor
 550            .update(cx, |replacement_editor, cx| {
 551                replacement_editor
 552                    .buffer()
 553                    .update(cx, |replacement_buffer, cx| {
 554                        let len = replacement_buffer.len(cx);
 555                        replacement_buffer.edit([(0..len, replacement.unwrap())], None, cx);
 556                    });
 557            });
 558    }
 559
 560    pub fn search(
 561        &mut self,
 562        query: &str,
 563        options: Option<SearchOptions>,
 564        cx: &mut ViewContext<Self>,
 565    ) -> oneshot::Receiver<()> {
 566        let options = options.unwrap_or(self.default_options);
 567        if query != self.query(cx) || self.search_options != options {
 568            self.query_editor.update(cx, |query_editor, cx| {
 569                query_editor.buffer().update(cx, |query_buffer, cx| {
 570                    let len = query_buffer.len(cx);
 571                    query_buffer.edit([(0..len, query)], None, cx);
 572                });
 573            });
 574            self.search_options = options;
 575            self.query_contains_error = false;
 576            self.clear_matches(cx);
 577            cx.notify();
 578        }
 579        self.update_matches(cx)
 580    }
 581
 582    fn render_action_button(
 583        &self,
 584        icon: &'static str,
 585        cx: &mut ViewContext<Self>,
 586    ) -> AnyElement<Self> {
 587        let tooltip = "Select All Matches";
 588        let tooltip_style = theme::current(cx).tooltip.clone();
 589
 590        let theme = theme::current(cx);
 591        let style = theme.search.action_button.clone();
 592
 593        gpui::elements::Component::element(SafeStylable::with_style(
 594            theme::components::action_button::Button::action(SelectAllMatches)
 595                .with_tooltip(tooltip, tooltip_style)
 596                .with_contents(theme::components::svg::Svg::new(icon)),
 597            style,
 598        ))
 599        .into_any()
 600    }
 601
 602    pub fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext<Self>) {
 603        assert_ne!(
 604            mode,
 605            SearchMode::Semantic,
 606            "Semantic search is not supported in buffer search"
 607        );
 608        if mode == self.current_mode {
 609            return;
 610        }
 611        self.current_mode = mode;
 612        let _ = self.update_matches(cx);
 613        cx.notify();
 614    }
 615
 616    fn deploy_bar(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext<Pane>) {
 617        let mut propagate_action = true;
 618        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
 619            search_bar.update(cx, |search_bar, cx| {
 620                if search_bar.deploy(action, cx) {
 621                    propagate_action = false;
 622                }
 623            });
 624        }
 625        if propagate_action {
 626            cx.propagate_action();
 627        }
 628    }
 629
 630    fn handle_editor_cancel(pane: &mut Pane, _: &editor::Cancel, cx: &mut ViewContext<Pane>) {
 631        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
 632            if !search_bar.read(cx).dismissed {
 633                search_bar.update(cx, |search_bar, cx| search_bar.dismiss(&Dismiss, cx));
 634                return;
 635            }
 636        }
 637        cx.propagate_action();
 638    }
 639
 640    pub fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext<Self>) {
 641        if let Some(active_editor) = self.active_searchable_item.as_ref() {
 642            cx.focus(active_editor.as_any());
 643        }
 644    }
 645
 646    fn toggle_search_option(&mut self, search_option: SearchOptions, cx: &mut ViewContext<Self>) {
 647        self.search_options.toggle(search_option);
 648        self.default_options = self.search_options;
 649        let _ = self.update_matches(cx);
 650        cx.notify();
 651    }
 652
 653    pub fn set_search_options(
 654        &mut self,
 655        search_options: SearchOptions,
 656        cx: &mut ViewContext<Self>,
 657    ) {
 658        self.search_options = search_options;
 659        cx.notify();
 660    }
 661
 662    fn select_next_match(&mut self, _: &SelectNextMatch, cx: &mut ViewContext<Self>) {
 663        self.select_match(Direction::Next, 1, cx);
 664    }
 665
 666    fn select_prev_match(&mut self, _: &SelectPrevMatch, cx: &mut ViewContext<Self>) {
 667        self.select_match(Direction::Prev, 1, cx);
 668    }
 669
 670    fn select_all_matches(&mut self, _: &SelectAllMatches, cx: &mut ViewContext<Self>) {
 671        if !self.dismissed && self.active_match_index.is_some() {
 672            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
 673                if let Some(matches) = self
 674                    .searchable_items_with_matches
 675                    .get(&searchable_item.downgrade())
 676                {
 677                    searchable_item.select_matches(matches, cx);
 678                    self.focus_editor(&FocusEditor, cx);
 679                }
 680            }
 681        }
 682    }
 683
 684    pub fn select_match(&mut self, direction: Direction, count: usize, cx: &mut ViewContext<Self>) {
 685        if let Some(index) = self.active_match_index {
 686            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
 687                if let Some(matches) = self
 688                    .searchable_items_with_matches
 689                    .get(&searchable_item.downgrade())
 690                {
 691                    let new_match_index = searchable_item
 692                        .match_index_for_direction(matches, index, direction, count, cx);
 693                    searchable_item.update_matches(matches, cx);
 694                    searchable_item.activate_match(new_match_index, matches, cx);
 695                }
 696            }
 697        }
 698    }
 699
 700    pub fn select_last_match(&mut self, cx: &mut ViewContext<Self>) {
 701        if let Some(searchable_item) = self.active_searchable_item.as_ref() {
 702            if let Some(matches) = self
 703                .searchable_items_with_matches
 704                .get(&searchable_item.downgrade())
 705            {
 706                if matches.len() == 0 {
 707                    return;
 708                }
 709                let new_match_index = matches.len() - 1;
 710                searchable_item.update_matches(matches, cx);
 711                searchable_item.activate_match(new_match_index, matches, cx);
 712            }
 713        }
 714    }
 715
 716    fn select_next_match_on_pane(
 717        pane: &mut Pane,
 718        action: &SelectNextMatch,
 719        cx: &mut ViewContext<Pane>,
 720    ) {
 721        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
 722            search_bar.update(cx, |bar, cx| bar.select_next_match(action, cx));
 723        }
 724    }
 725
 726    fn select_prev_match_on_pane(
 727        pane: &mut Pane,
 728        action: &SelectPrevMatch,
 729        cx: &mut ViewContext<Pane>,
 730    ) {
 731        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
 732            search_bar.update(cx, |bar, cx| bar.select_prev_match(action, cx));
 733        }
 734    }
 735
 736    fn select_all_matches_on_pane(
 737        pane: &mut Pane,
 738        action: &SelectAllMatches,
 739        cx: &mut ViewContext<Pane>,
 740    ) {
 741        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
 742            search_bar.update(cx, |bar, cx| bar.select_all_matches(action, cx));
 743        }
 744    }
 745
 746    fn on_query_editor_event(
 747        &mut self,
 748        _: ViewHandle<Editor>,
 749        event: &editor::Event,
 750        cx: &mut ViewContext<Self>,
 751    ) {
 752        if let editor::Event::Edited { .. } = event {
 753            self.query_contains_error = false;
 754            self.clear_matches(cx);
 755            let search = self.update_matches(cx);
 756            cx.spawn(|this, mut cx| async move {
 757                search.await?;
 758                this.update(&mut cx, |this, cx| this.activate_current_match(cx))
 759            })
 760            .detach_and_log_err(cx);
 761        }
 762    }
 763
 764    fn on_active_searchable_item_event(&mut self, event: SearchEvent, cx: &mut ViewContext<Self>) {
 765        match event {
 766            SearchEvent::MatchesInvalidated => {
 767                let _ = self.update_matches(cx);
 768            }
 769            SearchEvent::ActiveMatchChanged => self.update_match_index(cx),
 770        }
 771    }
 772
 773    fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
 774        let mut active_item_matches = None;
 775        for (searchable_item, matches) in self.searchable_items_with_matches.drain() {
 776            if let Some(searchable_item) =
 777                WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
 778            {
 779                if Some(&searchable_item) == self.active_searchable_item.as_ref() {
 780                    active_item_matches = Some((searchable_item.downgrade(), matches));
 781                } else {
 782                    searchable_item.clear_matches(cx);
 783                }
 784            }
 785        }
 786
 787        self.searchable_items_with_matches
 788            .extend(active_item_matches);
 789    }
 790
 791    fn update_matches(&mut self, cx: &mut ViewContext<Self>) -> oneshot::Receiver<()> {
 792        let (done_tx, done_rx) = oneshot::channel();
 793        let query = self.query(cx);
 794        self.pending_search.take();
 795
 796        if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
 797            if query.is_empty() {
 798                self.active_match_index.take();
 799                active_searchable_item.clear_matches(cx);
 800                let _ = done_tx.send(());
 801                cx.notify();
 802            } else {
 803                let query: Arc<_> = if self.current_mode == SearchMode::Regex {
 804                    match SearchQuery::regex(
 805                        query,
 806                        self.search_options.contains(SearchOptions::WHOLE_WORD),
 807                        self.search_options.contains(SearchOptions::CASE_SENSITIVE),
 808                        false,
 809                        Vec::new(),
 810                        Vec::new(),
 811                    ) {
 812                        Ok(query) => query.with_replacement(self.replacement(cx)),
 813                        Err(_) => {
 814                            self.query_contains_error = true;
 815                            cx.notify();
 816                            return done_rx;
 817                        }
 818                    }
 819                } else {
 820                    match SearchQuery::text(
 821                        query,
 822                        self.search_options.contains(SearchOptions::WHOLE_WORD),
 823                        self.search_options.contains(SearchOptions::CASE_SENSITIVE),
 824                        false,
 825                        Vec::new(),
 826                        Vec::new(),
 827                    ) {
 828                        Ok(query) => query.with_replacement(self.replacement(cx)),
 829                        Err(_) => {
 830                            self.query_contains_error = true;
 831                            cx.notify();
 832                            return done_rx;
 833                        }
 834                    }
 835                }
 836                .into();
 837                self.active_search = Some(query.clone());
 838                let query_text = query.as_str().to_string();
 839                let matches = active_searchable_item.find_matches(query, cx);
 840
 841                let active_searchable_item = active_searchable_item.downgrade();
 842                self.pending_search = Some(cx.spawn(|this, mut cx| async move {
 843                    let matches = matches.await;
 844                    this.update(&mut cx, |this, cx| {
 845                        if let Some(active_searchable_item) =
 846                            WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx)
 847                        {
 848                            this.searchable_items_with_matches
 849                                .insert(active_searchable_item.downgrade(), matches);
 850
 851                            this.update_match_index(cx);
 852                            this.search_history.add(query_text);
 853                            if !this.dismissed {
 854                                let matches = this
 855                                    .searchable_items_with_matches
 856                                    .get(&active_searchable_item.downgrade())
 857                                    .unwrap();
 858                                active_searchable_item.update_matches(matches, cx);
 859                                let _ = done_tx.send(());
 860                            }
 861                            cx.notify();
 862                        }
 863                    })
 864                    .log_err();
 865                }));
 866            }
 867        }
 868        done_rx
 869    }
 870
 871    fn update_match_index(&mut self, cx: &mut ViewContext<Self>) {
 872        let new_index = self
 873            .active_searchable_item
 874            .as_ref()
 875            .and_then(|searchable_item| {
 876                let matches = self
 877                    .searchable_items_with_matches
 878                    .get(&searchable_item.downgrade())?;
 879                searchable_item.active_match_index(matches, cx)
 880            });
 881        if new_index != self.active_match_index {
 882            self.active_match_index = new_index;
 883            cx.notify();
 884        }
 885    }
 886
 887    fn next_history_query(&mut self, _: &NextHistoryQuery, cx: &mut ViewContext<Self>) {
 888        if let Some(new_query) = self.search_history.next().map(str::to_string) {
 889            let _ = self.search(&new_query, Some(self.search_options), cx);
 890        } else {
 891            self.search_history.reset_selection();
 892            let _ = self.search("", Some(self.search_options), cx);
 893        }
 894    }
 895
 896    fn previous_history_query(&mut self, _: &PreviousHistoryQuery, cx: &mut ViewContext<Self>) {
 897        if self.query(cx).is_empty() {
 898            if let Some(new_query) = self.search_history.current().map(str::to_string) {
 899                let _ = self.search(&new_query, Some(self.search_options), cx);
 900                return;
 901            }
 902        }
 903
 904        if let Some(new_query) = self.search_history.previous().map(str::to_string) {
 905            let _ = self.search(&new_query, Some(self.search_options), cx);
 906        }
 907    }
 908    fn cycle_mode(&mut self, _: &CycleMode, cx: &mut ViewContext<Self>) {
 909        self.activate_search_mode(next_mode(&self.current_mode, false), cx);
 910    }
 911    fn cycle_mode_on_pane(pane: &mut Pane, action: &CycleMode, cx: &mut ViewContext<Pane>) {
 912        let mut should_propagate = true;
 913        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
 914            search_bar.update(cx, |bar, cx| {
 915                if bar.show(cx) {
 916                    should_propagate = false;
 917                    bar.cycle_mode(action, cx);
 918                    false
 919                } else {
 920                    true
 921                }
 922            });
 923        }
 924        if should_propagate {
 925            cx.propagate_action();
 926        }
 927    }
 928    fn toggle_replace(&mut self, _: &ToggleReplace, cx: &mut ViewContext<Self>) {
 929        if let Some(_) = &self.active_searchable_item {
 930            self.replace_enabled = !self.replace_enabled;
 931            if !self.replace_enabled {
 932                cx.focus(&self.query_editor);
 933            }
 934            cx.notify();
 935        }
 936    }
 937    fn toggle_replace_on_a_pane(pane: &mut Pane, _: &ToggleReplace, cx: &mut ViewContext<Pane>) {
 938        let mut should_propagate = true;
 939        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
 940            search_bar.update(cx, |bar, cx| {
 941                if let Some(_) = &bar.active_searchable_item {
 942                    should_propagate = false;
 943                    bar.replace_enabled = !bar.replace_enabled;
 944                    if bar.dismissed {
 945                        bar.show(cx);
 946                    }
 947                    if !bar.replace_enabled {
 948                        cx.focus(&bar.query_editor);
 949                    }
 950                    cx.notify();
 951                }
 952            });
 953        }
 954        if should_propagate {
 955            cx.propagate_action();
 956        }
 957    }
 958    fn replace_next(&mut self, _: &ReplaceNext, cx: &mut ViewContext<Self>) {
 959        let mut should_propagate = true;
 960        if !self.dismissed && self.active_search.is_some() {
 961            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
 962                if let Some(query) = self.active_search.as_ref() {
 963                    if let Some(matches) = self
 964                        .searchable_items_with_matches
 965                        .get(&searchable_item.downgrade())
 966                    {
 967                        if let Some(active_index) = self.active_match_index {
 968                            let query = query
 969                                .as_ref()
 970                                .clone()
 971                                .with_replacement(self.replacement(cx));
 972                            searchable_item.replace(&matches[active_index], &query, cx);
 973                            self.select_next_match(&SelectNextMatch, cx);
 974                        }
 975                        should_propagate = false;
 976                        self.focus_editor(&FocusEditor, cx);
 977                    }
 978                }
 979            }
 980        }
 981        if should_propagate {
 982            cx.propagate_action();
 983        }
 984    }
 985    pub fn replace_all(&mut self, _: &ReplaceAll, cx: &mut ViewContext<Self>) {
 986        if !self.dismissed && self.active_search.is_some() {
 987            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
 988                if let Some(query) = self.active_search.as_ref() {
 989                    if let Some(matches) = self
 990                        .searchable_items_with_matches
 991                        .get(&searchable_item.downgrade())
 992                    {
 993                        let query = query
 994                            .as_ref()
 995                            .clone()
 996                            .with_replacement(self.replacement(cx));
 997                        for m in matches {
 998                            searchable_item.replace(m, &query, cx);
 999                        }
1000                    }
1001                }
1002            }
1003        }
1004    }
1005    fn replace_next_on_pane(pane: &mut Pane, action: &ReplaceNext, cx: &mut ViewContext<Pane>) {
1006        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
1007            search_bar.update(cx, |bar, cx| bar.replace_next(action, cx));
1008            return;
1009        }
1010        cx.propagate_action();
1011    }
1012    fn replace_all_on_pane(pane: &mut Pane, action: &ReplaceAll, cx: &mut ViewContext<Pane>) {
1013        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
1014            search_bar.update(cx, |bar, cx| bar.replace_all(action, cx));
1015            return;
1016        }
1017        cx.propagate_action();
1018    }
1019}
1020
1021#[cfg(test)]
1022mod tests {
1023    use super::*;
1024    use editor::{DisplayPoint, Editor};
1025    use gpui::{color::Color, test::EmptyView, TestAppContext};
1026    use language::Buffer;
1027    use unindent::Unindent as _;
1028
1029    fn init_test(cx: &mut TestAppContext) -> (ViewHandle<Editor>, ViewHandle<BufferSearchBar>) {
1030        crate::project_search::tests::init_test(cx);
1031
1032        let buffer = cx.add_model(|cx| {
1033            Buffer::new(
1034                0,
1035                cx.model_id() as u64,
1036                r#"
1037                A regular expression (shortened as regex or regexp;[1] also referred to as
1038                rational expression[2][3]) is a sequence of characters that specifies a search
1039                pattern in text. Usually such patterns are used by string-searching algorithms
1040                for "find" or "find and replace" operations on strings, or for input validation.
1041                "#
1042                .unindent(),
1043            )
1044        });
1045        let window = cx.add_window(|_| EmptyView);
1046        let editor = window.add_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx));
1047
1048        let search_bar = window.add_view(cx, |cx| {
1049            let mut search_bar = BufferSearchBar::new(cx);
1050            search_bar.set_active_pane_item(Some(&editor), cx);
1051            search_bar.show(cx);
1052            search_bar
1053        });
1054
1055        (editor, search_bar)
1056    }
1057
1058    #[gpui::test]
1059    async fn test_search_simple(cx: &mut TestAppContext) {
1060        let (editor, search_bar) = init_test(cx);
1061
1062        // Search for a string that appears with different casing.
1063        // By default, search is case-insensitive.
1064        search_bar
1065            .update(cx, |search_bar, cx| search_bar.search("us", None, cx))
1066            .await
1067            .unwrap();
1068        editor.update(cx, |editor, cx| {
1069            assert_eq!(
1070                editor.all_text_background_highlights(cx),
1071                &[
1072                    (
1073                        DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19),
1074                        Color::red(),
1075                    ),
1076                    (
1077                        DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
1078                        Color::red(),
1079                    ),
1080                ]
1081            );
1082        });
1083
1084        // Switch to a case sensitive search.
1085        search_bar.update(cx, |search_bar, cx| {
1086            search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx);
1087        });
1088        editor.next_notification(cx).await;
1089        editor.update(cx, |editor, cx| {
1090            assert_eq!(
1091                editor.all_text_background_highlights(cx),
1092                &[(
1093                    DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
1094                    Color::red(),
1095                )]
1096            );
1097        });
1098
1099        // Search for a string that appears both as a whole word and
1100        // within other words. By default, all results are found.
1101        search_bar
1102            .update(cx, |search_bar, cx| search_bar.search("or", None, cx))
1103            .await
1104            .unwrap();
1105        editor.update(cx, |editor, cx| {
1106            assert_eq!(
1107                editor.all_text_background_highlights(cx),
1108                &[
1109                    (
1110                        DisplayPoint::new(0, 24)..DisplayPoint::new(0, 26),
1111                        Color::red(),
1112                    ),
1113                    (
1114                        DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
1115                        Color::red(),
1116                    ),
1117                    (
1118                        DisplayPoint::new(2, 71)..DisplayPoint::new(2, 73),
1119                        Color::red(),
1120                    ),
1121                    (
1122                        DisplayPoint::new(3, 1)..DisplayPoint::new(3, 3),
1123                        Color::red(),
1124                    ),
1125                    (
1126                        DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
1127                        Color::red(),
1128                    ),
1129                    (
1130                        DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
1131                        Color::red(),
1132                    ),
1133                    (
1134                        DisplayPoint::new(3, 60)..DisplayPoint::new(3, 62),
1135                        Color::red(),
1136                    ),
1137                ]
1138            );
1139        });
1140
1141        // Switch to a whole word search.
1142        search_bar.update(cx, |search_bar, cx| {
1143            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
1144        });
1145        editor.next_notification(cx).await;
1146        editor.update(cx, |editor, cx| {
1147            assert_eq!(
1148                editor.all_text_background_highlights(cx),
1149                &[
1150                    (
1151                        DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
1152                        Color::red(),
1153                    ),
1154                    (
1155                        DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
1156                        Color::red(),
1157                    ),
1158                    (
1159                        DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
1160                        Color::red(),
1161                    ),
1162                ]
1163            );
1164        });
1165
1166        editor.update(cx, |editor, cx| {
1167            editor.change_selections(None, cx, |s| {
1168                s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
1169            });
1170        });
1171        search_bar.update(cx, |search_bar, cx| {
1172            assert_eq!(search_bar.active_match_index, Some(0));
1173            search_bar.select_next_match(&SelectNextMatch, cx);
1174            assert_eq!(
1175                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1176                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1177            );
1178        });
1179        search_bar.read_with(cx, |search_bar, _| {
1180            assert_eq!(search_bar.active_match_index, Some(0));
1181        });
1182
1183        search_bar.update(cx, |search_bar, cx| {
1184            search_bar.select_next_match(&SelectNextMatch, cx);
1185            assert_eq!(
1186                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1187                [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
1188            );
1189        });
1190        search_bar.read_with(cx, |search_bar, _| {
1191            assert_eq!(search_bar.active_match_index, Some(1));
1192        });
1193
1194        search_bar.update(cx, |search_bar, cx| {
1195            search_bar.select_next_match(&SelectNextMatch, cx);
1196            assert_eq!(
1197                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1198                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1199            );
1200        });
1201        search_bar.read_with(cx, |search_bar, _| {
1202            assert_eq!(search_bar.active_match_index, Some(2));
1203        });
1204
1205        search_bar.update(cx, |search_bar, cx| {
1206            search_bar.select_next_match(&SelectNextMatch, cx);
1207            assert_eq!(
1208                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1209                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1210            );
1211        });
1212        search_bar.read_with(cx, |search_bar, _| {
1213            assert_eq!(search_bar.active_match_index, Some(0));
1214        });
1215
1216        search_bar.update(cx, |search_bar, cx| {
1217            search_bar.select_prev_match(&SelectPrevMatch, cx);
1218            assert_eq!(
1219                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1220                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1221            );
1222        });
1223        search_bar.read_with(cx, |search_bar, _| {
1224            assert_eq!(search_bar.active_match_index, Some(2));
1225        });
1226
1227        search_bar.update(cx, |search_bar, cx| {
1228            search_bar.select_prev_match(&SelectPrevMatch, cx);
1229            assert_eq!(
1230                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1231                [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
1232            );
1233        });
1234        search_bar.read_with(cx, |search_bar, _| {
1235            assert_eq!(search_bar.active_match_index, Some(1));
1236        });
1237
1238        search_bar.update(cx, |search_bar, cx| {
1239            search_bar.select_prev_match(&SelectPrevMatch, cx);
1240            assert_eq!(
1241                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1242                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1243            );
1244        });
1245        search_bar.read_with(cx, |search_bar, _| {
1246            assert_eq!(search_bar.active_match_index, Some(0));
1247        });
1248
1249        // Park the cursor in between matches and ensure that going to the previous match selects
1250        // the closest match to the left.
1251        editor.update(cx, |editor, cx| {
1252            editor.change_selections(None, cx, |s| {
1253                s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
1254            });
1255        });
1256        search_bar.update(cx, |search_bar, cx| {
1257            assert_eq!(search_bar.active_match_index, Some(1));
1258            search_bar.select_prev_match(&SelectPrevMatch, cx);
1259            assert_eq!(
1260                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1261                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1262            );
1263        });
1264        search_bar.read_with(cx, |search_bar, _| {
1265            assert_eq!(search_bar.active_match_index, Some(0));
1266        });
1267
1268        // Park the cursor in between matches and ensure that going to the next match selects the
1269        // closest match to the right.
1270        editor.update(cx, |editor, cx| {
1271            editor.change_selections(None, cx, |s| {
1272                s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
1273            });
1274        });
1275        search_bar.update(cx, |search_bar, cx| {
1276            assert_eq!(search_bar.active_match_index, Some(1));
1277            search_bar.select_next_match(&SelectNextMatch, cx);
1278            assert_eq!(
1279                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1280                [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
1281            );
1282        });
1283        search_bar.read_with(cx, |search_bar, _| {
1284            assert_eq!(search_bar.active_match_index, Some(1));
1285        });
1286
1287        // Park the cursor after the last match and ensure that going to the previous match selects
1288        // the last match.
1289        editor.update(cx, |editor, cx| {
1290            editor.change_selections(None, cx, |s| {
1291                s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
1292            });
1293        });
1294        search_bar.update(cx, |search_bar, cx| {
1295            assert_eq!(search_bar.active_match_index, Some(2));
1296            search_bar.select_prev_match(&SelectPrevMatch, cx);
1297            assert_eq!(
1298                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1299                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1300            );
1301        });
1302        search_bar.read_with(cx, |search_bar, _| {
1303            assert_eq!(search_bar.active_match_index, Some(2));
1304        });
1305
1306        // Park the cursor after the last match and ensure that going to the next match selects the
1307        // first match.
1308        editor.update(cx, |editor, cx| {
1309            editor.change_selections(None, cx, |s| {
1310                s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
1311            });
1312        });
1313        search_bar.update(cx, |search_bar, cx| {
1314            assert_eq!(search_bar.active_match_index, Some(2));
1315            search_bar.select_next_match(&SelectNextMatch, cx);
1316            assert_eq!(
1317                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1318                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1319            );
1320        });
1321        search_bar.read_with(cx, |search_bar, _| {
1322            assert_eq!(search_bar.active_match_index, Some(0));
1323        });
1324
1325        // Park the cursor before the first match and ensure that going to the previous match
1326        // selects the last match.
1327        editor.update(cx, |editor, cx| {
1328            editor.change_selections(None, cx, |s| {
1329                s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
1330            });
1331        });
1332        search_bar.update(cx, |search_bar, cx| {
1333            assert_eq!(search_bar.active_match_index, Some(0));
1334            search_bar.select_prev_match(&SelectPrevMatch, cx);
1335            assert_eq!(
1336                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1337                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1338            );
1339        });
1340        search_bar.read_with(cx, |search_bar, _| {
1341            assert_eq!(search_bar.active_match_index, Some(2));
1342        });
1343    }
1344
1345    #[gpui::test]
1346    async fn test_search_option_handling(cx: &mut TestAppContext) {
1347        let (editor, search_bar) = init_test(cx);
1348
1349        // show with options should make current search case sensitive
1350        search_bar
1351            .update(cx, |search_bar, cx| {
1352                search_bar.show(cx);
1353                search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), cx)
1354            })
1355            .await
1356            .unwrap();
1357        editor.update(cx, |editor, cx| {
1358            assert_eq!(
1359                editor.all_text_background_highlights(cx),
1360                &[(
1361                    DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
1362                    Color::red(),
1363                )]
1364            );
1365        });
1366
1367        // search_suggested should restore default options
1368        search_bar.update(cx, |search_bar, cx| {
1369            search_bar.search_suggested(cx);
1370            assert_eq!(search_bar.search_options, SearchOptions::NONE)
1371        });
1372
1373        // toggling a search option should update the defaults
1374        search_bar
1375            .update(cx, |search_bar, cx| {
1376                search_bar.search("regex", Some(SearchOptions::CASE_SENSITIVE), cx)
1377            })
1378            .await
1379            .unwrap();
1380        search_bar.update(cx, |search_bar, cx| {
1381            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx)
1382        });
1383        editor.next_notification(cx).await;
1384        editor.update(cx, |editor, cx| {
1385            assert_eq!(
1386                editor.all_text_background_highlights(cx),
1387                &[(
1388                    DisplayPoint::new(0, 35)..DisplayPoint::new(0, 40),
1389                    Color::red(),
1390                ),]
1391            );
1392        });
1393
1394        // defaults should still include whole word
1395        search_bar.update(cx, |search_bar, cx| {
1396            search_bar.search_suggested(cx);
1397            assert_eq!(
1398                search_bar.search_options,
1399                SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD
1400            )
1401        });
1402    }
1403
1404    #[gpui::test]
1405    async fn test_search_select_all_matches(cx: &mut TestAppContext) {
1406        crate::project_search::tests::init_test(cx);
1407
1408        let buffer_text = r#"
1409        A regular expression (shortened as regex or regexp;[1] also referred to as
1410        rational expression[2][3]) is a sequence of characters that specifies a search
1411        pattern in text. Usually such patterns are used by string-searching algorithms
1412        for "find" or "find and replace" operations on strings, or for input validation.
1413        "#
1414        .unindent();
1415        let expected_query_matches_count = buffer_text
1416            .chars()
1417            .filter(|c| c.to_ascii_lowercase() == 'a')
1418            .count();
1419        assert!(
1420            expected_query_matches_count > 1,
1421            "Should pick a query with multiple results"
1422        );
1423        let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, buffer_text));
1424        let window = cx.add_window(|_| EmptyView);
1425        let editor = window.add_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx));
1426
1427        let search_bar = window.add_view(cx, |cx| {
1428            let mut search_bar = BufferSearchBar::new(cx);
1429            search_bar.set_active_pane_item(Some(&editor), cx);
1430            search_bar.show(cx);
1431            search_bar
1432        });
1433
1434        search_bar
1435            .update(cx, |search_bar, cx| search_bar.search("a", None, cx))
1436            .await
1437            .unwrap();
1438        search_bar.update(cx, |search_bar, cx| {
1439            cx.focus(search_bar.query_editor.as_any());
1440            search_bar.activate_current_match(cx);
1441        });
1442
1443        window.read_with(cx, |cx| {
1444            assert!(
1445                !editor.is_focused(cx),
1446                "Initially, the editor should not be focused"
1447            );
1448        });
1449
1450        let initial_selections = editor.update(cx, |editor, cx| {
1451            let initial_selections = editor.selections.display_ranges(cx);
1452            assert_eq!(
1453                initial_selections.len(), 1,
1454                "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
1455            );
1456            initial_selections
1457        });
1458        search_bar.update(cx, |search_bar, _| {
1459            assert_eq!(search_bar.active_match_index, Some(0));
1460        });
1461
1462        search_bar.update(cx, |search_bar, cx| {
1463            cx.focus(search_bar.query_editor.as_any());
1464            search_bar.select_all_matches(&SelectAllMatches, cx);
1465        });
1466        window.read_with(cx, |cx| {
1467            assert!(
1468                editor.is_focused(cx),
1469                "Should focus editor after successful SelectAllMatches"
1470            );
1471        });
1472        search_bar.update(cx, |search_bar, cx| {
1473            let all_selections =
1474                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1475            assert_eq!(
1476                all_selections.len(),
1477                expected_query_matches_count,
1478                "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1479            );
1480            assert_eq!(
1481                search_bar.active_match_index,
1482                Some(0),
1483                "Match index should not change after selecting all matches"
1484            );
1485        });
1486
1487        search_bar.update(cx, |search_bar, cx| {
1488            search_bar.select_next_match(&SelectNextMatch, cx);
1489        });
1490        window.read_with(cx, |cx| {
1491            assert!(
1492                editor.is_focused(cx),
1493                "Should still have editor focused after SelectNextMatch"
1494            );
1495        });
1496        search_bar.update(cx, |search_bar, cx| {
1497            let all_selections =
1498                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1499            assert_eq!(
1500                all_selections.len(),
1501                1,
1502                "On next match, should deselect items and select the next match"
1503            );
1504            assert_ne!(
1505                all_selections, initial_selections,
1506                "Next match should be different from the first selection"
1507            );
1508            assert_eq!(
1509                search_bar.active_match_index,
1510                Some(1),
1511                "Match index should be updated to the next one"
1512            );
1513        });
1514
1515        search_bar.update(cx, |search_bar, cx| {
1516            cx.focus(search_bar.query_editor.as_any());
1517            search_bar.select_all_matches(&SelectAllMatches, cx);
1518        });
1519        window.read_with(cx, |cx| {
1520            assert!(
1521                editor.is_focused(cx),
1522                "Should focus editor after successful SelectAllMatches"
1523            );
1524        });
1525        search_bar.update(cx, |search_bar, cx| {
1526            let all_selections =
1527                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1528            assert_eq!(
1529                all_selections.len(),
1530                expected_query_matches_count,
1531                "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1532            );
1533            assert_eq!(
1534                search_bar.active_match_index,
1535                Some(1),
1536                "Match index should not change after selecting all matches"
1537            );
1538        });
1539
1540        search_bar.update(cx, |search_bar, cx| {
1541            search_bar.select_prev_match(&SelectPrevMatch, cx);
1542        });
1543        window.read_with(cx, |cx| {
1544            assert!(
1545                editor.is_focused(cx),
1546                "Should still have editor focused after SelectPrevMatch"
1547            );
1548        });
1549        let last_match_selections = search_bar.update(cx, |search_bar, cx| {
1550            let all_selections =
1551                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1552            assert_eq!(
1553                all_selections.len(),
1554                1,
1555                "On previous match, should deselect items and select the previous item"
1556            );
1557            assert_eq!(
1558                all_selections, initial_selections,
1559                "Previous match should be the same as the first selection"
1560            );
1561            assert_eq!(
1562                search_bar.active_match_index,
1563                Some(0),
1564                "Match index should be updated to the previous one"
1565            );
1566            all_selections
1567        });
1568
1569        search_bar
1570            .update(cx, |search_bar, cx| {
1571                cx.focus(search_bar.query_editor.as_any());
1572                search_bar.search("abas_nonexistent_match", None, cx)
1573            })
1574            .await
1575            .unwrap();
1576        search_bar.update(cx, |search_bar, cx| {
1577            search_bar.select_all_matches(&SelectAllMatches, cx);
1578        });
1579        window.read_with(cx, |cx| {
1580            assert!(
1581                !editor.is_focused(cx),
1582                "Should not switch focus to editor if SelectAllMatches does not find any matches"
1583            );
1584        });
1585        search_bar.update(cx, |search_bar, cx| {
1586            let all_selections =
1587                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1588            assert_eq!(
1589                all_selections, last_match_selections,
1590                "Should not select anything new if there are no matches"
1591            );
1592            assert!(
1593                search_bar.active_match_index.is_none(),
1594                "For no matches, there should be no active match index"
1595            );
1596        });
1597    }
1598
1599    #[gpui::test]
1600    async fn test_search_query_history(cx: &mut TestAppContext) {
1601        crate::project_search::tests::init_test(cx);
1602
1603        let buffer_text = r#"
1604        A regular expression (shortened as regex or regexp;[1] also referred to as
1605        rational expression[2][3]) is a sequence of characters that specifies a search
1606        pattern in text. Usually such patterns are used by string-searching algorithms
1607        for "find" or "find and replace" operations on strings, or for input validation.
1608        "#
1609        .unindent();
1610        let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, buffer_text));
1611        let window = cx.add_window(|_| EmptyView);
1612
1613        let editor = window.add_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx));
1614
1615        let search_bar = window.add_view(cx, |cx| {
1616            let mut search_bar = BufferSearchBar::new(cx);
1617            search_bar.set_active_pane_item(Some(&editor), cx);
1618            search_bar.show(cx);
1619            search_bar
1620        });
1621
1622        // Add 3 search items into the history.
1623        search_bar
1624            .update(cx, |search_bar, cx| search_bar.search("a", None, cx))
1625            .await
1626            .unwrap();
1627        search_bar
1628            .update(cx, |search_bar, cx| search_bar.search("b", None, cx))
1629            .await
1630            .unwrap();
1631        search_bar
1632            .update(cx, |search_bar, cx| {
1633                search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), cx)
1634            })
1635            .await
1636            .unwrap();
1637        // Ensure that the latest search is active.
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::CASE_SENSITIVE);
1641        });
1642
1643        // Next history query after the latest should set the query to the empty string.
1644        search_bar.update(cx, |search_bar, cx| {
1645            search_bar.next_history_query(&NextHistoryQuery, cx);
1646        });
1647        search_bar.read_with(cx, |search_bar, cx| {
1648            assert_eq!(search_bar.query(cx), "");
1649            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1650        });
1651        search_bar.update(cx, |search_bar, cx| {
1652            search_bar.next_history_query(&NextHistoryQuery, cx);
1653        });
1654        search_bar.read_with(cx, |search_bar, cx| {
1655            assert_eq!(search_bar.query(cx), "");
1656            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1657        });
1658
1659        // First previous query for empty current query should set the query to the latest.
1660        search_bar.update(cx, |search_bar, cx| {
1661            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1662        });
1663        search_bar.read_with(cx, |search_bar, cx| {
1664            assert_eq!(search_bar.query(cx), "c");
1665            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1666        });
1667
1668        // Further previous items should go over the history in reverse order.
1669        search_bar.update(cx, |search_bar, cx| {
1670            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1671        });
1672        search_bar.read_with(cx, |search_bar, cx| {
1673            assert_eq!(search_bar.query(cx), "b");
1674            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1675        });
1676
1677        // Previous items should never go behind the first history item.
1678        search_bar.update(cx, |search_bar, cx| {
1679            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1680        });
1681        search_bar.read_with(cx, |search_bar, cx| {
1682            assert_eq!(search_bar.query(cx), "a");
1683            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1684        });
1685        search_bar.update(cx, |search_bar, cx| {
1686            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1687        });
1688        search_bar.read_with(cx, |search_bar, cx| {
1689            assert_eq!(search_bar.query(cx), "a");
1690            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1691        });
1692
1693        // Next items should go over the history in the original order.
1694        search_bar.update(cx, |search_bar, cx| {
1695            search_bar.next_history_query(&NextHistoryQuery, cx);
1696        });
1697        search_bar.read_with(cx, |search_bar, cx| {
1698            assert_eq!(search_bar.query(cx), "b");
1699            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1700        });
1701
1702        search_bar
1703            .update(cx, |search_bar, cx| search_bar.search("ba", None, cx))
1704            .await
1705            .unwrap();
1706        search_bar.read_with(cx, |search_bar, cx| {
1707            assert_eq!(search_bar.query(cx), "ba");
1708            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1709        });
1710
1711        // New search input should add another entry to history and move the selection to the end of the history.
1712        search_bar.update(cx, |search_bar, cx| {
1713            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1714        });
1715        search_bar.read_with(cx, |search_bar, cx| {
1716            assert_eq!(search_bar.query(cx), "c");
1717            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1718        });
1719        search_bar.update(cx, |search_bar, cx| {
1720            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1721        });
1722        search_bar.read_with(cx, |search_bar, cx| {
1723            assert_eq!(search_bar.query(cx), "b");
1724            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1725        });
1726        search_bar.update(cx, |search_bar, cx| {
1727            search_bar.next_history_query(&NextHistoryQuery, cx);
1728        });
1729        search_bar.read_with(cx, |search_bar, cx| {
1730            assert_eq!(search_bar.query(cx), "c");
1731            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1732        });
1733        search_bar.update(cx, |search_bar, cx| {
1734            search_bar.next_history_query(&NextHistoryQuery, cx);
1735        });
1736        search_bar.read_with(cx, |search_bar, cx| {
1737            assert_eq!(search_bar.query(cx), "ba");
1738            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1739        });
1740        search_bar.update(cx, |search_bar, cx| {
1741            search_bar.next_history_query(&NextHistoryQuery, cx);
1742        });
1743        search_bar.read_with(cx, |search_bar, cx| {
1744            assert_eq!(search_bar.query(cx), "");
1745            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1746        });
1747    }
1748    #[gpui::test]
1749    async fn test_replace_simple(cx: &mut TestAppContext) {
1750        let (editor, search_bar) = init_test(cx);
1751
1752        search_bar
1753            .update(cx, |search_bar, cx| {
1754                search_bar.search("expression", None, cx)
1755            })
1756            .await
1757            .unwrap();
1758
1759        search_bar.update(cx, |search_bar, cx| {
1760            search_bar.replacement_editor.update(cx, |editor, cx| {
1761                // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally.
1762                editor.set_text("expr$1", cx);
1763            });
1764            search_bar.replace_all(&ReplaceAll, cx)
1765        });
1766        assert_eq!(
1767            editor.read_with(cx, |this, cx| { this.text(cx) }),
1768            r#"
1769        A regular expr$1 (shortened as regex or regexp;[1] also referred to as
1770        rational expr$1[2][3]) is a sequence of characters that specifies a search
1771        pattern in text. Usually such patterns are used by string-searching algorithms
1772        for "find" or "find and replace" operations on strings, or for input validation.
1773        "#
1774            .unindent()
1775        );
1776
1777        // Search for word boundaries and replace just a single one.
1778        search_bar
1779            .update(cx, |search_bar, cx| {
1780                search_bar.search("or", Some(SearchOptions::WHOLE_WORD), cx)
1781            })
1782            .await
1783            .unwrap();
1784
1785        search_bar.update(cx, |search_bar, cx| {
1786            search_bar.replacement_editor.update(cx, |editor, cx| {
1787                editor.set_text("banana", cx);
1788            });
1789            search_bar.replace_next(&ReplaceNext, cx)
1790        });
1791        // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text.
1792        assert_eq!(
1793            editor.read_with(cx, |this, cx| { this.text(cx) }),
1794            r#"
1795        A regular expr$1 (shortened as regex banana regexp;[1] also referred to as
1796        rational expr$1[2][3]) is a sequence of characters that specifies a search
1797        pattern in text. Usually such patterns are used by string-searching algorithms
1798        for "find" or "find and replace" operations on strings, or for input validation.
1799        "#
1800            .unindent()
1801        );
1802        // Let's turn on regex mode.
1803        search_bar
1804            .update(cx, |search_bar, cx| {
1805                search_bar.activate_search_mode(SearchMode::Regex, cx);
1806                search_bar.search("\\[([^\\]]+)\\]", None, cx)
1807            })
1808            .await
1809            .unwrap();
1810        search_bar.update(cx, |search_bar, cx| {
1811            search_bar.replacement_editor.update(cx, |editor, cx| {
1812                editor.set_text("${1}number", cx);
1813            });
1814            search_bar.replace_all(&ReplaceAll, cx)
1815        });
1816        assert_eq!(
1817            editor.read_with(cx, |this, cx| { this.text(cx) }),
1818            r#"
1819        A regular expr$1 (shortened as regex banana regexp;1number also referred to as
1820        rational expr$12number3number) is a sequence of characters that specifies a search
1821        pattern in text. Usually such patterns are used by string-searching algorithms
1822        for "find" or "find and replace" operations on strings, or for input validation.
1823        "#
1824            .unindent()
1825        );
1826        // Now with a whole-word twist.
1827        search_bar
1828            .update(cx, |search_bar, cx| {
1829                search_bar.activate_search_mode(SearchMode::Regex, cx);
1830                search_bar.search("a\\w+s", Some(SearchOptions::WHOLE_WORD), cx)
1831            })
1832            .await
1833            .unwrap();
1834        search_bar.update(cx, |search_bar, cx| {
1835            search_bar.replacement_editor.update(cx, |editor, cx| {
1836                editor.set_text("things", cx);
1837            });
1838            search_bar.replace_all(&ReplaceAll, cx)
1839        });
1840        // The only word affected by this edit should be `algorithms`, even though there's a bunch
1841        // of words in this text that would match this regex if not for WHOLE_WORD.
1842        assert_eq!(
1843            editor.read_with(cx, |this, cx| { this.text(cx) }),
1844            r#"
1845        A regular expr$1 (shortened as regex banana regexp;1number also referred to as
1846        rational expr$12number3number) is a sequence of characters that specifies a search
1847        pattern in text. Usually such patterns are used by string-searching things
1848        for "find" or "find and replace" operations on strings, or for input validation.
1849        "#
1850            .unindent()
1851        );
1852    }
1853}