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