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