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