buffer_search.rs

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