buffer_search.rs

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