buffer_search.rs

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