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