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