buffer_search.rs

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