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