buffer_search.rs

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