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