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