project_search.rs

   1use crate::{
   2    BufferSearchBar, FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext,
   3    SearchOption, SearchOptions, SearchSource, SelectNextMatch, SelectPreviousMatch,
   4    ToggleCaseSensitive, ToggleIncludeIgnored, ToggleRegex, ToggleReplace, ToggleWholeWord,
   5    buffer_search::Deploy,
   6    search_bar::{ActionButtonState, input_base_styles, render_action_button, render_text_input},
   7};
   8use anyhow::Context as _;
   9use collections::HashMap;
  10use editor::{
  11    Anchor, Editor, EditorEvent, EditorSettings, MAX_TAB_TITLE_LEN, MultiBuffer, SelectionEffects,
  12    actions::{Backtab, SelectAll, Tab},
  13    items::active_match_index,
  14    multibuffer_context_lines,
  15};
  16use futures::StreamExt;
  17use gpui::{
  18    Action, AnyElement, AnyView, App, Axis, Context, Entity, EntityId, EventEmitter, FocusHandle,
  19    Focusable, Global, Hsla, InteractiveElement, IntoElement, KeyContext, ParentElement, Point,
  20    Render, SharedString, Styled, Subscription, Task, UpdateGlobal, WeakEntity, Window, actions,
  21    div,
  22};
  23use language::{Buffer, Language};
  24use menu::Confirm;
  25use project::{
  26    Project, ProjectPath,
  27    search::{SearchInputKind, SearchQuery},
  28    search_history::SearchHistoryCursor,
  29};
  30use settings::Settings;
  31use std::{
  32    any::{Any, TypeId},
  33    mem,
  34    ops::{Not, Range},
  35    pin::pin,
  36    sync::Arc,
  37};
  38use ui::{IconButtonShape, KeyBinding, Toggleable, Tooltip, prelude::*, utils::SearchInputWidth};
  39use util::{ResultExt as _, paths::PathMatcher, rel_path::RelPath};
  40use workspace::{
  41    DeploySearch, ItemNavHistory, NewSearch, ToolbarItemEvent, ToolbarItemLocation,
  42    ToolbarItemView, Workspace, WorkspaceId,
  43    item::{BreadcrumbText, Item, ItemEvent, ItemHandle, SaveOptions},
  44    searchable::{Direction, SearchableItem, SearchableItemHandle},
  45};
  46
  47actions!(
  48    project_search,
  49    [
  50        /// Searches in a new project search tab.
  51        SearchInNew,
  52        /// Toggles focus between the search bar and the search results.
  53        ToggleFocus,
  54        /// Moves to the next input field.
  55        NextField,
  56        /// Toggles the search filters panel.
  57        ToggleFilters
  58    ]
  59);
  60
  61#[derive(Default)]
  62struct ActiveSettings(HashMap<WeakEntity<Project>, ProjectSearchSettings>);
  63
  64impl Global for ActiveSettings {}
  65
  66pub fn init(cx: &mut App) {
  67    cx.set_global(ActiveSettings::default());
  68    cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
  69        register_workspace_action(workspace, move |search_bar, _: &Deploy, window, cx| {
  70            search_bar.focus_search(window, cx);
  71        });
  72        register_workspace_action(workspace, move |search_bar, _: &FocusSearch, window, cx| {
  73            search_bar.focus_search(window, cx);
  74        });
  75        register_workspace_action(
  76            workspace,
  77            move |search_bar, _: &ToggleFilters, window, cx| {
  78                search_bar.toggle_filters(window, cx);
  79            },
  80        );
  81        register_workspace_action(
  82            workspace,
  83            move |search_bar, _: &ToggleCaseSensitive, window, cx| {
  84                search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
  85            },
  86        );
  87        register_workspace_action(
  88            workspace,
  89            move |search_bar, _: &ToggleWholeWord, window, cx| {
  90                search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
  91            },
  92        );
  93        register_workspace_action(workspace, move |search_bar, _: &ToggleRegex, window, cx| {
  94            search_bar.toggle_search_option(SearchOptions::REGEX, window, cx);
  95        });
  96        register_workspace_action(
  97            workspace,
  98            move |search_bar, action: &ToggleReplace, window, cx| {
  99                search_bar.toggle_replace(action, window, cx)
 100            },
 101        );
 102        register_workspace_action(
 103            workspace,
 104            move |search_bar, action: &SelectPreviousMatch, window, cx| {
 105                search_bar.select_prev_match(action, window, cx)
 106            },
 107        );
 108        register_workspace_action(
 109            workspace,
 110            move |search_bar, action: &SelectNextMatch, window, cx| {
 111                search_bar.select_next_match(action, window, cx)
 112            },
 113        );
 114
 115        // Only handle search_in_new if there is a search present
 116        register_workspace_action_for_present_search(workspace, |workspace, action, window, cx| {
 117            ProjectSearchView::search_in_new(workspace, action, window, cx)
 118        });
 119
 120        register_workspace_action_for_present_search(
 121            workspace,
 122            |workspace, _: &menu::Cancel, window, cx| {
 123                if let Some(project_search_bar) = workspace
 124                    .active_pane()
 125                    .read(cx)
 126                    .toolbar()
 127                    .read(cx)
 128                    .item_of_type::<ProjectSearchBar>()
 129                {
 130                    project_search_bar.update(cx, |project_search_bar, cx| {
 131                        let search_is_focused = project_search_bar
 132                            .active_project_search
 133                            .as_ref()
 134                            .is_some_and(|search_view| {
 135                                search_view
 136                                    .read(cx)
 137                                    .query_editor
 138                                    .read(cx)
 139                                    .focus_handle(cx)
 140                                    .is_focused(window)
 141                            });
 142                        if search_is_focused {
 143                            project_search_bar.move_focus_to_results(window, cx);
 144                        } else {
 145                            project_search_bar.focus_search(window, cx)
 146                        }
 147                    });
 148                } else {
 149                    cx.propagate();
 150                }
 151            },
 152        );
 153
 154        // Both on present and dismissed search, we need to unconditionally handle those actions to focus from the editor.
 155        workspace.register_action(move |workspace, action: &DeploySearch, window, cx| {
 156            if workspace.has_active_modal(window, cx) {
 157                cx.propagate();
 158                return;
 159            }
 160            ProjectSearchView::deploy_search(workspace, action, window, cx);
 161            cx.notify();
 162        });
 163        workspace.register_action(move |workspace, action: &NewSearch, window, cx| {
 164            if workspace.has_active_modal(window, cx) {
 165                cx.propagate();
 166                return;
 167            }
 168            ProjectSearchView::new_search(workspace, action, window, cx);
 169            cx.notify();
 170        });
 171    })
 172    .detach();
 173}
 174
 175fn contains_uppercase(str: &str) -> bool {
 176    str.chars().any(|c| c.is_uppercase())
 177}
 178
 179pub struct ProjectSearch {
 180    project: Entity<Project>,
 181    excerpts: Entity<MultiBuffer>,
 182    pending_search: Option<Task<Option<()>>>,
 183    match_ranges: Vec<Range<Anchor>>,
 184    active_query: Option<SearchQuery>,
 185    last_search_query_text: Option<String>,
 186    search_id: usize,
 187    no_results: Option<bool>,
 188    limit_reached: bool,
 189    search_history_cursor: SearchHistoryCursor,
 190    search_included_history_cursor: SearchHistoryCursor,
 191    search_excluded_history_cursor: SearchHistoryCursor,
 192}
 193
 194#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
 195enum InputPanel {
 196    Query,
 197    Replacement,
 198    Exclude,
 199    Include,
 200}
 201
 202pub struct ProjectSearchView {
 203    workspace: WeakEntity<Workspace>,
 204    focus_handle: FocusHandle,
 205    entity: Entity<ProjectSearch>,
 206    query_editor: Entity<Editor>,
 207    replacement_editor: Entity<Editor>,
 208    results_editor: Entity<Editor>,
 209    search_options: SearchOptions,
 210    panels_with_errors: HashMap<InputPanel, String>,
 211    active_match_index: Option<usize>,
 212    search_id: usize,
 213    included_files_editor: Entity<Editor>,
 214    excluded_files_editor: Entity<Editor>,
 215    filters_enabled: bool,
 216    replace_enabled: bool,
 217    included_opened_only: bool,
 218    regex_language: Option<Arc<Language>>,
 219    _subscriptions: Vec<Subscription>,
 220}
 221
 222#[derive(Debug, Clone)]
 223pub struct ProjectSearchSettings {
 224    search_options: SearchOptions,
 225    filters_enabled: bool,
 226}
 227
 228pub struct ProjectSearchBar {
 229    active_project_search: Option<Entity<ProjectSearchView>>,
 230    subscription: Option<Subscription>,
 231}
 232
 233impl ProjectSearch {
 234    pub fn new(project: Entity<Project>, cx: &mut Context<Self>) -> Self {
 235        let capability = project.read(cx).capability();
 236
 237        Self {
 238            project,
 239            excerpts: cx.new(|_| MultiBuffer::new(capability)),
 240            pending_search: Default::default(),
 241            match_ranges: Default::default(),
 242            active_query: None,
 243            last_search_query_text: None,
 244            search_id: 0,
 245            no_results: None,
 246            limit_reached: false,
 247            search_history_cursor: Default::default(),
 248            search_included_history_cursor: Default::default(),
 249            search_excluded_history_cursor: Default::default(),
 250        }
 251    }
 252
 253    fn clone(&self, cx: &mut Context<Self>) -> Entity<Self> {
 254        cx.new(|cx| Self {
 255            project: self.project.clone(),
 256            excerpts: self
 257                .excerpts
 258                .update(cx, |excerpts, cx| cx.new(|cx| excerpts.clone(cx))),
 259            pending_search: Default::default(),
 260            match_ranges: self.match_ranges.clone(),
 261            active_query: self.active_query.clone(),
 262            last_search_query_text: self.last_search_query_text.clone(),
 263            search_id: self.search_id,
 264            no_results: self.no_results,
 265            limit_reached: self.limit_reached,
 266            search_history_cursor: self.search_history_cursor.clone(),
 267            search_included_history_cursor: self.search_included_history_cursor.clone(),
 268            search_excluded_history_cursor: self.search_excluded_history_cursor.clone(),
 269        })
 270    }
 271    fn cursor(&self, kind: SearchInputKind) -> &SearchHistoryCursor {
 272        match kind {
 273            SearchInputKind::Query => &self.search_history_cursor,
 274            SearchInputKind::Include => &self.search_included_history_cursor,
 275            SearchInputKind::Exclude => &self.search_excluded_history_cursor,
 276        }
 277    }
 278    fn cursor_mut(&mut self, kind: SearchInputKind) -> &mut SearchHistoryCursor {
 279        match kind {
 280            SearchInputKind::Query => &mut self.search_history_cursor,
 281            SearchInputKind::Include => &mut self.search_included_history_cursor,
 282            SearchInputKind::Exclude => &mut self.search_excluded_history_cursor,
 283        }
 284    }
 285
 286    fn search(&mut self, query: SearchQuery, cx: &mut Context<Self>) {
 287        let search = self.project.update(cx, |project, cx| {
 288            project
 289                .search_history_mut(SearchInputKind::Query)
 290                .add(&mut self.search_history_cursor, query.as_str().to_string());
 291            let included = query.as_inner().files_to_include().sources().join(",");
 292            if !included.is_empty() {
 293                project
 294                    .search_history_mut(SearchInputKind::Include)
 295                    .add(&mut self.search_included_history_cursor, included);
 296            }
 297            let excluded = query.as_inner().files_to_exclude().sources().join(",");
 298            if !excluded.is_empty() {
 299                project
 300                    .search_history_mut(SearchInputKind::Exclude)
 301                    .add(&mut self.search_excluded_history_cursor, excluded);
 302            }
 303            project.search(query.clone(), cx)
 304        });
 305        self.last_search_query_text = Some(query.as_str().to_string());
 306        self.search_id += 1;
 307        self.active_query = Some(query);
 308        self.match_ranges.clear();
 309        self.pending_search = Some(cx.spawn(async move |project_search, cx| {
 310            let mut matches = pin!(search.ready_chunks(1024));
 311            project_search
 312                .update(cx, |project_search, cx| {
 313                    project_search.match_ranges.clear();
 314                    project_search
 315                        .excerpts
 316                        .update(cx, |excerpts, cx| excerpts.clear(cx));
 317                    project_search.no_results = Some(true);
 318                    project_search.limit_reached = false;
 319                })
 320                .ok()?;
 321
 322            let mut limit_reached = false;
 323            while let Some(results) = matches.next().await {
 324                for result in results {
 325                    match result {
 326                        project::search::SearchResult::Buffer { buffer, ranges } => {
 327                            let new_ranges = project_search
 328                                .update(cx, |project_search, cx| {
 329                                    project_search.excerpts.update(cx, |excerpts, cx| {
 330                                        excerpts.set_anchored_excerpts_for_path(
 331                                            buffer,
 332                                            ranges,
 333                                            multibuffer_context_lines(cx),
 334                                            cx,
 335                                        )
 336                                    })
 337                                })
 338                                .ok()?
 339                                .await;
 340
 341                            project_search
 342                                .update(cx, |project_search, cx| {
 343                                    project_search.match_ranges.extend(new_ranges);
 344                                    cx.notify();
 345                                })
 346                                .ok()?;
 347                        }
 348                        project::search::SearchResult::LimitReached => {
 349                            limit_reached = true;
 350                        }
 351                    }
 352                }
 353            }
 354
 355            project_search
 356                .update(cx, |project_search, cx| {
 357                    if !project_search.match_ranges.is_empty() {
 358                        project_search.no_results = Some(false);
 359                    }
 360                    project_search.limit_reached = limit_reached;
 361                    project_search.pending_search.take();
 362                    cx.notify();
 363                })
 364                .ok()?;
 365
 366            None
 367        }));
 368        cx.notify();
 369    }
 370}
 371
 372#[derive(Clone, Debug, PartialEq, Eq)]
 373pub enum ViewEvent {
 374    UpdateTab,
 375    Activate,
 376    EditorEvent(editor::EditorEvent),
 377    Dismiss,
 378}
 379
 380impl EventEmitter<ViewEvent> for ProjectSearchView {}
 381
 382impl Render for ProjectSearchView {
 383    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 384        if self.has_matches() {
 385            div()
 386                .flex_1()
 387                .size_full()
 388                .track_focus(&self.focus_handle(cx))
 389                .child(self.results_editor.clone())
 390        } else {
 391            let model = self.entity.read(cx);
 392            let has_no_results = model.no_results.unwrap_or(false);
 393            let is_search_underway = model.pending_search.is_some();
 394
 395            let heading_text = if is_search_underway {
 396                "Searching…"
 397            } else if has_no_results {
 398                "No Results"
 399            } else {
 400                "Search All Files"
 401            };
 402
 403            let heading_text = div()
 404                .justify_center()
 405                .child(Label::new(heading_text).size(LabelSize::Large));
 406
 407            let page_content: Option<AnyElement> = if let Some(no_results) = model.no_results {
 408                if model.pending_search.is_none() && no_results {
 409                    Some(
 410                        Label::new("No results found in this project for the provided query")
 411                            .size(LabelSize::Small)
 412                            .into_any_element(),
 413                    )
 414                } else {
 415                    None
 416                }
 417            } else {
 418                Some(self.landing_text_minor(window, cx).into_any_element())
 419            };
 420
 421            let page_content = page_content.map(|text| div().child(text));
 422
 423            h_flex()
 424                .size_full()
 425                .items_center()
 426                .justify_center()
 427                .overflow_hidden()
 428                .bg(cx.theme().colors().editor_background)
 429                .track_focus(&self.focus_handle(cx))
 430                .child(
 431                    v_flex()
 432                        .id("project-search-landing-page")
 433                        .overflow_y_scroll()
 434                        .gap_1()
 435                        .child(heading_text)
 436                        .children(page_content),
 437                )
 438        }
 439    }
 440}
 441
 442impl Focusable for ProjectSearchView {
 443    fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
 444        self.focus_handle.clone()
 445    }
 446}
 447
 448impl Item for ProjectSearchView {
 449    type Event = ViewEvent;
 450    fn tab_tooltip_text(&self, cx: &App) -> Option<SharedString> {
 451        let query_text = self.query_editor.read(cx).text(cx);
 452
 453        query_text
 454            .is_empty()
 455            .not()
 456            .then(|| query_text.into())
 457            .or_else(|| Some("Project Search".into()))
 458    }
 459
 460    fn act_as_type<'a>(
 461        &'a self,
 462        type_id: TypeId,
 463        self_handle: &'a Entity<Self>,
 464        _: &'a App,
 465    ) -> Option<AnyView> {
 466        if type_id == TypeId::of::<Self>() {
 467            Some(self_handle.clone().into())
 468        } else if type_id == TypeId::of::<Editor>() {
 469            Some(self.results_editor.clone().into())
 470        } else {
 471            None
 472        }
 473    }
 474    fn as_searchable(&self, _: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
 475        Some(Box::new(self.results_editor.clone()))
 476    }
 477
 478    fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 479        self.results_editor
 480            .update(cx, |editor, cx| editor.deactivated(window, cx));
 481    }
 482
 483    fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
 484        Some(Icon::new(IconName::MagnifyingGlass))
 485    }
 486
 487    fn tab_content_text(&self, _detail: usize, cx: &App) -> SharedString {
 488        let last_query: Option<SharedString> = self
 489            .entity
 490            .read(cx)
 491            .last_search_query_text
 492            .as_ref()
 493            .map(|query| {
 494                let query = query.replace('\n', "");
 495                let query_text = util::truncate_and_trailoff(&query, MAX_TAB_TITLE_LEN);
 496                query_text.into()
 497            });
 498
 499        last_query
 500            .filter(|query| !query.is_empty())
 501            .unwrap_or_else(|| "Project Search".into())
 502    }
 503
 504    fn telemetry_event_text(&self) -> Option<&'static str> {
 505        Some("Project Search Opened")
 506    }
 507
 508    fn for_each_project_item(
 509        &self,
 510        cx: &App,
 511        f: &mut dyn FnMut(EntityId, &dyn project::ProjectItem),
 512    ) {
 513        self.results_editor.for_each_project_item(cx, f)
 514    }
 515
 516    fn can_save(&self, _: &App) -> bool {
 517        true
 518    }
 519
 520    fn is_dirty(&self, cx: &App) -> bool {
 521        self.results_editor.read(cx).is_dirty(cx)
 522    }
 523
 524    fn has_conflict(&self, cx: &App) -> bool {
 525        self.results_editor.read(cx).has_conflict(cx)
 526    }
 527
 528    fn save(
 529        &mut self,
 530        options: SaveOptions,
 531        project: Entity<Project>,
 532        window: &mut Window,
 533        cx: &mut Context<Self>,
 534    ) -> Task<anyhow::Result<()>> {
 535        self.results_editor
 536            .update(cx, |editor, cx| editor.save(options, project, window, cx))
 537    }
 538
 539    fn save_as(
 540        &mut self,
 541        _: Entity<Project>,
 542        _: ProjectPath,
 543        _window: &mut Window,
 544        _: &mut Context<Self>,
 545    ) -> Task<anyhow::Result<()>> {
 546        unreachable!("save_as should not have been called")
 547    }
 548
 549    fn reload(
 550        &mut self,
 551        project: Entity<Project>,
 552        window: &mut Window,
 553        cx: &mut Context<Self>,
 554    ) -> Task<anyhow::Result<()>> {
 555        self.results_editor
 556            .update(cx, |editor, cx| editor.reload(project, window, cx))
 557    }
 558
 559    fn clone_on_split(
 560        &self,
 561        _workspace_id: Option<WorkspaceId>,
 562        window: &mut Window,
 563        cx: &mut Context<Self>,
 564    ) -> Option<Entity<Self>>
 565    where
 566        Self: Sized,
 567    {
 568        let model = self.entity.update(cx, |model, cx| model.clone(cx));
 569        Some(cx.new(|cx| Self::new(self.workspace.clone(), model, window, cx, None)))
 570    }
 571
 572    fn added_to_workspace(
 573        &mut self,
 574        workspace: &mut Workspace,
 575        window: &mut Window,
 576        cx: &mut Context<Self>,
 577    ) {
 578        self.results_editor.update(cx, |editor, cx| {
 579            editor.added_to_workspace(workspace, window, cx)
 580        });
 581    }
 582
 583    fn set_nav_history(
 584        &mut self,
 585        nav_history: ItemNavHistory,
 586        _: &mut Window,
 587        cx: &mut Context<Self>,
 588    ) {
 589        self.results_editor.update(cx, |editor, _| {
 590            editor.set_nav_history(Some(nav_history));
 591        });
 592    }
 593
 594    fn navigate(
 595        &mut self,
 596        data: Box<dyn Any>,
 597        window: &mut Window,
 598        cx: &mut Context<Self>,
 599    ) -> bool {
 600        self.results_editor
 601            .update(cx, |editor, cx| editor.navigate(data, window, cx))
 602    }
 603
 604    fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) {
 605        match event {
 606            ViewEvent::UpdateTab => {
 607                f(ItemEvent::UpdateBreadcrumbs);
 608                f(ItemEvent::UpdateTab);
 609            }
 610            ViewEvent::EditorEvent(editor_event) => {
 611                Editor::to_item_events(editor_event, f);
 612            }
 613            ViewEvent::Dismiss => f(ItemEvent::CloseItem),
 614            _ => {}
 615        }
 616    }
 617
 618    fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation {
 619        if self.has_matches() {
 620            ToolbarItemLocation::Secondary
 621        } else {
 622            ToolbarItemLocation::Hidden
 623        }
 624    }
 625
 626    fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option<Vec<BreadcrumbText>> {
 627        self.results_editor.breadcrumbs(theme, cx)
 628    }
 629}
 630
 631impl ProjectSearchView {
 632    pub fn get_matches(&self, cx: &App) -> Vec<Range<Anchor>> {
 633        self.entity.read(cx).match_ranges.clone()
 634    }
 635
 636    fn toggle_filters(&mut self, cx: &mut Context<Self>) {
 637        self.filters_enabled = !self.filters_enabled;
 638        ActiveSettings::update_global(cx, |settings, cx| {
 639            settings.0.insert(
 640                self.entity.read(cx).project.downgrade(),
 641                self.current_settings(),
 642            );
 643        });
 644    }
 645
 646    fn current_settings(&self) -> ProjectSearchSettings {
 647        ProjectSearchSettings {
 648            search_options: self.search_options,
 649            filters_enabled: self.filters_enabled,
 650        }
 651    }
 652
 653    fn toggle_search_option(&mut self, option: SearchOptions, cx: &mut Context<Self>) {
 654        self.search_options.toggle(option);
 655        ActiveSettings::update_global(cx, |settings, cx| {
 656            settings.0.insert(
 657                self.entity.read(cx).project.downgrade(),
 658                self.current_settings(),
 659            );
 660        });
 661        self.adjust_query_regex_language(cx);
 662    }
 663
 664    fn toggle_opened_only(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {
 665        self.included_opened_only = !self.included_opened_only;
 666    }
 667
 668    pub fn replacement(&self, cx: &App) -> String {
 669        self.replacement_editor.read(cx).text(cx)
 670    }
 671
 672    fn replace_next(&mut self, _: &ReplaceNext, window: &mut Window, cx: &mut Context<Self>) {
 673        if let Some(last_search_query_text) = &self.entity.read(cx).last_search_query_text
 674            && self.query_editor.read(cx).text(cx) != *last_search_query_text
 675        {
 676            // search query has changed, restart search and bail
 677            self.search(cx);
 678            return;
 679        }
 680        if self.entity.read(cx).match_ranges.is_empty() {
 681            return;
 682        }
 683        let Some(active_index) = self.active_match_index else {
 684            return;
 685        };
 686
 687        let query = self.entity.read(cx).active_query.clone();
 688        if let Some(query) = query {
 689            let query = query.with_replacement(self.replacement(cx));
 690
 691            // TODO: Do we need the clone here?
 692            let mat = self.entity.read(cx).match_ranges[active_index].clone();
 693            self.results_editor.update(cx, |editor, cx| {
 694                editor.replace(&mat, &query, window, cx);
 695            });
 696            self.select_match(Direction::Next, window, cx)
 697        }
 698    }
 699    fn replace_all(&mut self, _: &ReplaceAll, window: &mut Window, cx: &mut Context<Self>) {
 700        if let Some(last_search_query_text) = &self.entity.read(cx).last_search_query_text
 701            && self.query_editor.read(cx).text(cx) != *last_search_query_text
 702        {
 703            // search query has changed, restart search and bail
 704            self.search(cx);
 705            return;
 706        }
 707        if self.active_match_index.is_none() {
 708            return;
 709        }
 710        let Some(query) = self.entity.read(cx).active_query.as_ref() else {
 711            return;
 712        };
 713        let query = query.clone().with_replacement(self.replacement(cx));
 714
 715        let match_ranges = self
 716            .entity
 717            .update(cx, |model, _| mem::take(&mut model.match_ranges));
 718        if match_ranges.is_empty() {
 719            return;
 720        }
 721
 722        self.results_editor.update(cx, |editor, cx| {
 723            editor.replace_all(&mut match_ranges.iter(), &query, window, cx);
 724        });
 725
 726        self.entity.update(cx, |model, _cx| {
 727            model.match_ranges = match_ranges;
 728        });
 729    }
 730
 731    pub fn new(
 732        workspace: WeakEntity<Workspace>,
 733        entity: Entity<ProjectSearch>,
 734        window: &mut Window,
 735        cx: &mut Context<Self>,
 736        settings: Option<ProjectSearchSettings>,
 737    ) -> Self {
 738        let project;
 739        let excerpts;
 740        let mut replacement_text = None;
 741        let mut query_text = String::new();
 742        let mut subscriptions = Vec::new();
 743
 744        // Read in settings if available
 745        let (mut options, filters_enabled) = if let Some(settings) = settings {
 746            (settings.search_options, settings.filters_enabled)
 747        } else {
 748            let search_options =
 749                SearchOptions::from_settings(&EditorSettings::get_global(cx).search);
 750            (search_options, false)
 751        };
 752
 753        {
 754            let entity = entity.read(cx);
 755            project = entity.project.clone();
 756            excerpts = entity.excerpts.clone();
 757            if let Some(active_query) = entity.active_query.as_ref() {
 758                query_text = active_query.as_str().to_string();
 759                replacement_text = active_query.replacement().map(ToOwned::to_owned);
 760                options = SearchOptions::from_query(active_query);
 761            }
 762        }
 763        subscriptions.push(cx.observe_in(&entity, window, |this, _, window, cx| {
 764            this.entity_changed(window, cx)
 765        }));
 766
 767        let query_editor = cx.new(|cx| {
 768            let mut editor = Editor::single_line(window, cx);
 769            editor.set_placeholder_text("Search all files…", window, cx);
 770            editor.set_text(query_text, window, cx);
 771            editor
 772        });
 773        // Subscribe to query_editor in order to reraise editor events for workspace item activation purposes
 774        subscriptions.push(
 775            cx.subscribe(&query_editor, |this, _, event: &EditorEvent, cx| {
 776                if let EditorEvent::Edited { .. } = event
 777                    && EditorSettings::get_global(cx).use_smartcase_search
 778                {
 779                    let query = this.search_query_text(cx);
 780                    if !query.is_empty()
 781                        && this.search_options.contains(SearchOptions::CASE_SENSITIVE)
 782                            != contains_uppercase(&query)
 783                    {
 784                        this.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx);
 785                    }
 786                }
 787                cx.emit(ViewEvent::EditorEvent(event.clone()))
 788            }),
 789        );
 790        let replacement_editor = cx.new(|cx| {
 791            let mut editor = Editor::single_line(window, cx);
 792            editor.set_placeholder_text("Replace in project…", window, cx);
 793            if let Some(text) = replacement_text {
 794                editor.set_text(text, window, cx);
 795            }
 796            editor
 797        });
 798        let results_editor = cx.new(|cx| {
 799            let mut editor = Editor::for_multibuffer(excerpts, Some(project.clone()), window, cx);
 800            editor.set_searchable(false);
 801            editor.set_in_project_search(true);
 802            editor
 803        });
 804        subscriptions.push(cx.observe(&results_editor, |_, _, cx| cx.emit(ViewEvent::UpdateTab)));
 805
 806        subscriptions.push(
 807            cx.subscribe(&results_editor, |this, _, event: &EditorEvent, cx| {
 808                if matches!(event, editor::EditorEvent::SelectionsChanged { .. }) {
 809                    this.update_match_index(cx);
 810                }
 811                // Reraise editor events for workspace item activation purposes
 812                cx.emit(ViewEvent::EditorEvent(event.clone()));
 813            }),
 814        );
 815
 816        let included_files_editor = cx.new(|cx| {
 817            let mut editor = Editor::single_line(window, cx);
 818            editor.set_placeholder_text("Include: crates/**/*.toml", window, cx);
 819
 820            editor
 821        });
 822        // Subscribe to include_files_editor in order to reraise editor events for workspace item activation purposes
 823        subscriptions.push(
 824            cx.subscribe(&included_files_editor, |_, _, event: &EditorEvent, cx| {
 825                cx.emit(ViewEvent::EditorEvent(event.clone()))
 826            }),
 827        );
 828
 829        let excluded_files_editor = cx.new(|cx| {
 830            let mut editor = Editor::single_line(window, cx);
 831            editor.set_placeholder_text("Exclude: vendor/*, *.lock", window, cx);
 832
 833            editor
 834        });
 835        // Subscribe to excluded_files_editor in order to reraise editor events for workspace item activation purposes
 836        subscriptions.push(
 837            cx.subscribe(&excluded_files_editor, |_, _, event: &EditorEvent, cx| {
 838                cx.emit(ViewEvent::EditorEvent(event.clone()))
 839            }),
 840        );
 841
 842        let focus_handle = cx.focus_handle();
 843        subscriptions.push(cx.on_focus(&focus_handle, window, |_, window, cx| {
 844            cx.on_next_frame(window, |this, window, cx| {
 845                if this.focus_handle.is_focused(window) {
 846                    if this.has_matches() {
 847                        this.results_editor.focus_handle(cx).focus(window);
 848                    } else {
 849                        this.query_editor.focus_handle(cx).focus(window);
 850                    }
 851                }
 852            });
 853        }));
 854
 855        let languages = project.read(cx).languages().clone();
 856        cx.spawn(async move |project_search_view, cx| {
 857            let regex_language = languages
 858                .language_for_name("regex")
 859                .await
 860                .context("loading regex language")?;
 861            project_search_view
 862                .update(cx, |project_search_view, cx| {
 863                    project_search_view.regex_language = Some(regex_language);
 864                    project_search_view.adjust_query_regex_language(cx);
 865                })
 866                .ok();
 867            anyhow::Ok(())
 868        })
 869        .detach_and_log_err(cx);
 870
 871        // Check if Worktrees have all been previously indexed
 872        let mut this = ProjectSearchView {
 873            workspace,
 874            focus_handle,
 875            replacement_editor,
 876            search_id: entity.read(cx).search_id,
 877            entity,
 878            query_editor,
 879            results_editor,
 880            search_options: options,
 881            panels_with_errors: HashMap::default(),
 882            active_match_index: None,
 883            included_files_editor,
 884            excluded_files_editor,
 885            filters_enabled,
 886            replace_enabled: false,
 887            included_opened_only: false,
 888            regex_language: None,
 889            _subscriptions: subscriptions,
 890        };
 891        this.entity_changed(window, cx);
 892        this
 893    }
 894
 895    pub fn new_search_in_directory(
 896        workspace: &mut Workspace,
 897        dir_path: &RelPath,
 898        window: &mut Window,
 899        cx: &mut Context<Workspace>,
 900    ) {
 901        let filter_str = dir_path.display(workspace.path_style(cx));
 902
 903        let weak_workspace = cx.entity().downgrade();
 904
 905        let entity = cx.new(|cx| ProjectSearch::new(workspace.project().clone(), cx));
 906        let search = cx.new(|cx| ProjectSearchView::new(weak_workspace, entity, window, cx, None));
 907        workspace.add_item_to_active_pane(Box::new(search.clone()), None, true, window, cx);
 908        search.update(cx, |search, cx| {
 909            search
 910                .included_files_editor
 911                .update(cx, |editor, cx| editor.set_text(filter_str, window, cx));
 912            search.filters_enabled = true;
 913            search.focus_query_editor(window, cx)
 914        });
 915    }
 916
 917    /// Re-activate the most recently activated search in this pane or the most recent if it has been closed.
 918    /// If no search exists in the workspace, create a new one.
 919    pub fn deploy_search(
 920        workspace: &mut Workspace,
 921        action: &workspace::DeploySearch,
 922        window: &mut Window,
 923        cx: &mut Context<Workspace>,
 924    ) {
 925        let existing = workspace
 926            .active_pane()
 927            .read(cx)
 928            .items()
 929            .find_map(|item| item.downcast::<ProjectSearchView>());
 930
 931        Self::existing_or_new_search(workspace, existing, action, window, cx);
 932    }
 933
 934    fn search_in_new(
 935        workspace: &mut Workspace,
 936        _: &SearchInNew,
 937        window: &mut Window,
 938        cx: &mut Context<Workspace>,
 939    ) {
 940        if let Some(search_view) = workspace
 941            .active_item(cx)
 942            .and_then(|item| item.downcast::<ProjectSearchView>())
 943        {
 944            let new_query = search_view.update(cx, |search_view, cx| {
 945                let open_buffers = if search_view.included_opened_only {
 946                    Some(search_view.open_buffers(cx, workspace))
 947                } else {
 948                    None
 949                };
 950                let new_query = search_view.build_search_query(cx, open_buffers);
 951                if new_query.is_some()
 952                    && let Some(old_query) = search_view.entity.read(cx).active_query.clone()
 953                {
 954                    search_view.query_editor.update(cx, |editor, cx| {
 955                        editor.set_text(old_query.as_str(), window, cx);
 956                    });
 957                    search_view.search_options = SearchOptions::from_query(&old_query);
 958                    search_view.adjust_query_regex_language(cx);
 959                }
 960                new_query
 961            });
 962            if let Some(new_query) = new_query {
 963                let entity = cx.new(|cx| {
 964                    let mut entity = ProjectSearch::new(workspace.project().clone(), cx);
 965                    entity.search(new_query, cx);
 966                    entity
 967                });
 968                let weak_workspace = cx.entity().downgrade();
 969                workspace.add_item_to_active_pane(
 970                    Box::new(cx.new(|cx| {
 971                        ProjectSearchView::new(weak_workspace, entity, window, cx, None)
 972                    })),
 973                    None,
 974                    true,
 975                    window,
 976                    cx,
 977                );
 978            }
 979        }
 980    }
 981
 982    // Add another search tab to the workspace.
 983    fn new_search(
 984        workspace: &mut Workspace,
 985        _: &workspace::NewSearch,
 986        window: &mut Window,
 987        cx: &mut Context<Workspace>,
 988    ) {
 989        Self::existing_or_new_search(workspace, None, &DeploySearch::find(), window, cx)
 990    }
 991
 992    fn existing_or_new_search(
 993        workspace: &mut Workspace,
 994        existing: Option<Entity<ProjectSearchView>>,
 995        action: &workspace::DeploySearch,
 996        window: &mut Window,
 997        cx: &mut Context<Workspace>,
 998    ) {
 999        let query = workspace.active_item(cx).and_then(|item| {
1000            if let Some(buffer_search_query) = buffer_search_query(workspace, item.as_ref(), cx) {
1001                return Some(buffer_search_query);
1002            }
1003
1004            let editor = item.act_as::<Editor>(cx)?;
1005            let query = editor.query_suggestion(window, cx);
1006            if query.is_empty() { None } else { Some(query) }
1007        });
1008
1009        let search = if let Some(existing) = existing {
1010            workspace.activate_item(&existing, true, true, window, cx);
1011            existing
1012        } else {
1013            let settings = cx
1014                .global::<ActiveSettings>()
1015                .0
1016                .get(&workspace.project().downgrade());
1017
1018            let settings = settings.cloned();
1019
1020            let weak_workspace = cx.entity().downgrade();
1021
1022            let project_search = cx.new(|cx| ProjectSearch::new(workspace.project().clone(), cx));
1023            let project_search_view = cx.new(|cx| {
1024                ProjectSearchView::new(weak_workspace, project_search, window, cx, settings)
1025            });
1026
1027            workspace.add_item_to_active_pane(
1028                Box::new(project_search_view.clone()),
1029                None,
1030                true,
1031                window,
1032                cx,
1033            );
1034            project_search_view
1035        };
1036
1037        search.update(cx, |search, cx| {
1038            search.replace_enabled = action.replace_enabled;
1039            if let Some(query) = query {
1040                search.set_query(&query, window, cx);
1041            }
1042            if let Some(included_files) = action.included_files.as_deref() {
1043                search
1044                    .included_files_editor
1045                    .update(cx, |editor, cx| editor.set_text(included_files, window, cx));
1046                search.filters_enabled = true;
1047            }
1048            if let Some(excluded_files) = action.excluded_files.as_deref() {
1049                search
1050                    .excluded_files_editor
1051                    .update(cx, |editor, cx| editor.set_text(excluded_files, window, cx));
1052                search.filters_enabled = true;
1053            }
1054            search.focus_query_editor(window, cx)
1055        });
1056    }
1057
1058    fn prompt_to_save_if_dirty_then_search(
1059        &mut self,
1060        window: &mut Window,
1061        cx: &mut Context<Self>,
1062    ) -> Task<anyhow::Result<()>> {
1063        let project = self.entity.read(cx).project.clone();
1064
1065        let can_autosave = self.results_editor.can_autosave(cx);
1066        let autosave_setting = self.results_editor.workspace_settings(cx).autosave;
1067
1068        let will_autosave = can_autosave && autosave_setting.should_save_on_close();
1069
1070        let is_dirty = self.is_dirty(cx);
1071
1072        cx.spawn_in(window, async move |this, cx| {
1073            let skip_save_on_close = this
1074                .read_with(cx, |this, cx| {
1075                    this.workspace.read_with(cx, |workspace, cx| {
1076                        workspace::Pane::skip_save_on_close(&this.results_editor, workspace, cx)
1077                    })
1078                })?
1079                .unwrap_or(false);
1080
1081            let should_prompt_to_save = !skip_save_on_close && !will_autosave && is_dirty;
1082
1083            let should_search = if should_prompt_to_save {
1084                let options = &["Save", "Don't Save", "Cancel"];
1085                let result_channel = this.update_in(cx, |_, window, cx| {
1086                    window.prompt(
1087                        gpui::PromptLevel::Warning,
1088                        "Project search buffer contains unsaved edits. Do you want to save it?",
1089                        None,
1090                        options,
1091                        cx,
1092                    )
1093                })?;
1094                let result = result_channel.await?;
1095                let should_save = result == 0;
1096                if should_save {
1097                    this.update_in(cx, |this, window, cx| {
1098                        this.save(
1099                            SaveOptions {
1100                                format: true,
1101                                autosave: false,
1102                            },
1103                            project,
1104                            window,
1105                            cx,
1106                        )
1107                    })?
1108                    .await
1109                    .log_err();
1110                }
1111
1112                result != 2
1113            } else {
1114                true
1115            };
1116            if should_search {
1117                this.update(cx, |this, cx| {
1118                    this.search(cx);
1119                })?;
1120            }
1121            anyhow::Ok(())
1122        })
1123    }
1124
1125    fn search(&mut self, cx: &mut Context<Self>) {
1126        let open_buffers = if self.included_opened_only {
1127            self.workspace
1128                .update(cx, |workspace, cx| self.open_buffers(cx, workspace))
1129                .ok()
1130        } else {
1131            None
1132        };
1133        if let Some(query) = self.build_search_query(cx, open_buffers) {
1134            self.entity.update(cx, |model, cx| model.search(query, cx));
1135        }
1136    }
1137
1138    pub fn search_query_text(&self, cx: &App) -> String {
1139        self.query_editor.read(cx).text(cx)
1140    }
1141
1142    fn build_search_query(
1143        &mut self,
1144        cx: &mut Context<Self>,
1145        open_buffers: Option<Vec<Entity<Buffer>>>,
1146    ) -> Option<SearchQuery> {
1147        // Do not bail early in this function, as we want to fill out `self.panels_with_errors`.
1148
1149        let text = self.search_query_text(cx);
1150        let included_files = self
1151            .filters_enabled
1152            .then(|| {
1153                match self.parse_path_matches(self.included_files_editor.read(cx).text(cx), cx) {
1154                    Ok(included_files) => {
1155                        let should_unmark_error =
1156                            self.panels_with_errors.remove(&InputPanel::Include);
1157                        if should_unmark_error.is_some() {
1158                            cx.notify();
1159                        }
1160                        included_files
1161                    }
1162                    Err(e) => {
1163                        let should_mark_error = self
1164                            .panels_with_errors
1165                            .insert(InputPanel::Include, e.to_string());
1166                        if should_mark_error.is_none() {
1167                            cx.notify();
1168                        }
1169                        PathMatcher::default()
1170                    }
1171                }
1172            })
1173            .unwrap_or(PathMatcher::default());
1174        let excluded_files = self
1175            .filters_enabled
1176            .then(|| {
1177                match self.parse_path_matches(self.excluded_files_editor.read(cx).text(cx), cx) {
1178                    Ok(excluded_files) => {
1179                        let should_unmark_error =
1180                            self.panels_with_errors.remove(&InputPanel::Exclude);
1181                        if should_unmark_error.is_some() {
1182                            cx.notify();
1183                        }
1184
1185                        excluded_files
1186                    }
1187                    Err(e) => {
1188                        let should_mark_error = self
1189                            .panels_with_errors
1190                            .insert(InputPanel::Exclude, e.to_string());
1191                        if should_mark_error.is_none() {
1192                            cx.notify();
1193                        }
1194                        PathMatcher::default()
1195                    }
1196                }
1197            })
1198            .unwrap_or(PathMatcher::default());
1199
1200        // If the project contains multiple visible worktrees, we match the
1201        // include/exclude patterns against full paths to allow them to be
1202        // disambiguated. For single worktree projects we use worktree relative
1203        // paths for convenience.
1204        let match_full_paths = self
1205            .entity
1206            .read(cx)
1207            .project
1208            .read(cx)
1209            .visible_worktrees(cx)
1210            .count()
1211            > 1;
1212
1213        let query = if self.search_options.contains(SearchOptions::REGEX) {
1214            match SearchQuery::regex(
1215                text,
1216                self.search_options.contains(SearchOptions::WHOLE_WORD),
1217                self.search_options.contains(SearchOptions::CASE_SENSITIVE),
1218                self.search_options.contains(SearchOptions::INCLUDE_IGNORED),
1219                self.search_options
1220                    .contains(SearchOptions::ONE_MATCH_PER_LINE),
1221                included_files,
1222                excluded_files,
1223                match_full_paths,
1224                open_buffers,
1225            ) {
1226                Ok(query) => {
1227                    let should_unmark_error = self.panels_with_errors.remove(&InputPanel::Query);
1228                    if should_unmark_error.is_some() {
1229                        cx.notify();
1230                    }
1231
1232                    Some(query)
1233                }
1234                Err(e) => {
1235                    let should_mark_error = self
1236                        .panels_with_errors
1237                        .insert(InputPanel::Query, e.to_string());
1238                    if should_mark_error.is_none() {
1239                        cx.notify();
1240                    }
1241
1242                    None
1243                }
1244            }
1245        } else {
1246            match SearchQuery::text(
1247                text,
1248                self.search_options.contains(SearchOptions::WHOLE_WORD),
1249                self.search_options.contains(SearchOptions::CASE_SENSITIVE),
1250                self.search_options.contains(SearchOptions::INCLUDE_IGNORED),
1251                included_files,
1252                excluded_files,
1253                match_full_paths,
1254                open_buffers,
1255            ) {
1256                Ok(query) => {
1257                    let should_unmark_error = self.panels_with_errors.remove(&InputPanel::Query);
1258                    if should_unmark_error.is_some() {
1259                        cx.notify();
1260                    }
1261
1262                    Some(query)
1263                }
1264                Err(e) => {
1265                    let should_mark_error = self
1266                        .panels_with_errors
1267                        .insert(InputPanel::Query, e.to_string());
1268                    if should_mark_error.is_none() {
1269                        cx.notify();
1270                    }
1271
1272                    None
1273                }
1274            }
1275        };
1276        if !self.panels_with_errors.is_empty() {
1277            return None;
1278        }
1279        if query.as_ref().is_some_and(|query| query.is_empty()) {
1280            return None;
1281        }
1282        query
1283    }
1284
1285    fn open_buffers(&self, cx: &App, workspace: &Workspace) -> Vec<Entity<Buffer>> {
1286        let mut buffers = Vec::new();
1287        for editor in workspace.items_of_type::<Editor>(cx) {
1288            if let Some(buffer) = editor.read(cx).buffer().read(cx).as_singleton() {
1289                buffers.push(buffer);
1290            }
1291        }
1292        buffers
1293    }
1294
1295    fn parse_path_matches(&self, text: String, cx: &App) -> anyhow::Result<PathMatcher> {
1296        let path_style = self.entity.read(cx).project.read(cx).path_style(cx);
1297        let queries = text
1298            .split(',')
1299            .map(str::trim)
1300            .filter(|maybe_glob_str| !maybe_glob_str.is_empty())
1301            .map(str::to_owned)
1302            .collect::<Vec<_>>();
1303        Ok(PathMatcher::new(&queries, path_style)?)
1304    }
1305
1306    fn select_match(&mut self, direction: Direction, window: &mut Window, cx: &mut Context<Self>) {
1307        if let Some(index) = self.active_match_index {
1308            let match_ranges = self.entity.read(cx).match_ranges.clone();
1309
1310            if !EditorSettings::get_global(cx).search_wrap
1311                && ((direction == Direction::Next && index + 1 >= match_ranges.len())
1312                    || (direction == Direction::Prev && index == 0))
1313            {
1314                crate::show_no_more_matches(window, cx);
1315                return;
1316            }
1317
1318            let new_index = self.results_editor.update(cx, |editor, cx| {
1319                editor.match_index_for_direction(&match_ranges, index, direction, 1, window, cx)
1320            });
1321
1322            let range_to_select = match_ranges[new_index].clone();
1323            self.results_editor.update(cx, |editor, cx| {
1324                let range_to_select = editor.range_for_match(&range_to_select);
1325                editor.unfold_ranges(std::slice::from_ref(&range_to_select), false, true, cx);
1326                editor.change_selections(Default::default(), window, cx, |s| {
1327                    s.select_ranges([range_to_select])
1328                });
1329            });
1330        }
1331    }
1332
1333    fn focus_query_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1334        self.query_editor.update(cx, |query_editor, cx| {
1335            query_editor.select_all(&SelectAll, window, cx);
1336        });
1337        let editor_handle = self.query_editor.focus_handle(cx);
1338        window.focus(&editor_handle);
1339    }
1340
1341    fn set_query(&mut self, query: &str, window: &mut Window, cx: &mut Context<Self>) {
1342        self.set_search_editor(SearchInputKind::Query, query, window, cx);
1343        if EditorSettings::get_global(cx).use_smartcase_search
1344            && !query.is_empty()
1345            && self.search_options.contains(SearchOptions::CASE_SENSITIVE)
1346                != contains_uppercase(query)
1347        {
1348            self.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx)
1349        }
1350    }
1351
1352    fn set_search_editor(
1353        &mut self,
1354        kind: SearchInputKind,
1355        text: &str,
1356        window: &mut Window,
1357        cx: &mut Context<Self>,
1358    ) {
1359        let editor = match kind {
1360            SearchInputKind::Query => &self.query_editor,
1361            SearchInputKind::Include => &self.included_files_editor,
1362
1363            SearchInputKind::Exclude => &self.excluded_files_editor,
1364        };
1365        editor.update(cx, |included_editor, cx| {
1366            included_editor.set_text(text, window, cx)
1367        });
1368    }
1369
1370    fn focus_results_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1371        self.query_editor.update(cx, |query_editor, cx| {
1372            let cursor = query_editor.selections.newest_anchor().head();
1373            query_editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1374                s.select_ranges([cursor..cursor])
1375            });
1376        });
1377        let results_handle = self.results_editor.focus_handle(cx);
1378        window.focus(&results_handle);
1379    }
1380
1381    fn entity_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1382        let match_ranges = self.entity.read(cx).match_ranges.clone();
1383        if match_ranges.is_empty() {
1384            self.active_match_index = None;
1385            self.results_editor.update(cx, |editor, cx| {
1386                editor.clear_background_highlights::<Self>(cx);
1387            });
1388        } else {
1389            self.active_match_index = Some(0);
1390            self.update_match_index(cx);
1391            let prev_search_id = mem::replace(&mut self.search_id, self.entity.read(cx).search_id);
1392            let is_new_search = self.search_id != prev_search_id;
1393            self.results_editor.update(cx, |editor, cx| {
1394                if is_new_search {
1395                    let range_to_select = match_ranges
1396                        .first()
1397                        .map(|range| editor.range_for_match(range));
1398                    editor.change_selections(Default::default(), window, cx, |s| {
1399                        s.select_ranges(range_to_select)
1400                    });
1401                    editor.scroll(Point::default(), Some(Axis::Vertical), window, cx);
1402                }
1403                editor.highlight_background::<Self>(
1404                    &match_ranges,
1405                    |theme| theme.colors().search_match_background,
1406                    cx,
1407                );
1408            });
1409            if is_new_search && self.query_editor.focus_handle(cx).is_focused(window) {
1410                self.focus_results_editor(window, cx);
1411            }
1412        }
1413
1414        cx.emit(ViewEvent::UpdateTab);
1415        cx.notify();
1416    }
1417
1418    fn update_match_index(&mut self, cx: &mut Context<Self>) {
1419        let results_editor = self.results_editor.read(cx);
1420        let new_index = active_match_index(
1421            Direction::Next,
1422            &self.entity.read(cx).match_ranges,
1423            &results_editor.selections.newest_anchor().head(),
1424            &results_editor.buffer().read(cx).snapshot(cx),
1425        );
1426        if self.active_match_index != new_index {
1427            self.active_match_index = new_index;
1428            cx.notify();
1429        }
1430    }
1431
1432    pub fn has_matches(&self) -> bool {
1433        self.active_match_index.is_some()
1434    }
1435
1436    fn landing_text_minor(&self, window: &mut Window, cx: &App) -> impl IntoElement {
1437        let focus_handle = self.focus_handle.clone();
1438        v_flex()
1439            .gap_1()
1440            .child(
1441                Label::new("Hit enter to search. For more options:")
1442                    .color(Color::Muted)
1443                    .mb_2(),
1444            )
1445            .child(
1446                Button::new("filter-paths", "Include/exclude specific paths")
1447                    .icon(IconName::Filter)
1448                    .icon_position(IconPosition::Start)
1449                    .icon_size(IconSize::Small)
1450                    .key_binding(KeyBinding::for_action_in(
1451                        &ToggleFilters,
1452                        &focus_handle,
1453                        window,
1454                        cx,
1455                    ))
1456                    .on_click(|_event, window, cx| {
1457                        window.dispatch_action(ToggleFilters.boxed_clone(), cx)
1458                    }),
1459            )
1460            .child(
1461                Button::new("find-replace", "Find and replace")
1462                    .icon(IconName::Replace)
1463                    .icon_position(IconPosition::Start)
1464                    .icon_size(IconSize::Small)
1465                    .key_binding(KeyBinding::for_action_in(
1466                        &ToggleReplace,
1467                        &focus_handle,
1468                        window,
1469                        cx,
1470                    ))
1471                    .on_click(|_event, window, cx| {
1472                        window.dispatch_action(ToggleReplace.boxed_clone(), cx)
1473                    }),
1474            )
1475            .child(
1476                Button::new("regex", "Match with regex")
1477                    .icon(IconName::Regex)
1478                    .icon_position(IconPosition::Start)
1479                    .icon_size(IconSize::Small)
1480                    .key_binding(KeyBinding::for_action_in(
1481                        &ToggleRegex,
1482                        &focus_handle,
1483                        window,
1484                        cx,
1485                    ))
1486                    .on_click(|_event, window, cx| {
1487                        window.dispatch_action(ToggleRegex.boxed_clone(), cx)
1488                    }),
1489            )
1490            .child(
1491                Button::new("match-case", "Match case")
1492                    .icon(IconName::CaseSensitive)
1493                    .icon_position(IconPosition::Start)
1494                    .icon_size(IconSize::Small)
1495                    .key_binding(KeyBinding::for_action_in(
1496                        &ToggleCaseSensitive,
1497                        &focus_handle,
1498                        window,
1499                        cx,
1500                    ))
1501                    .on_click(|_event, window, cx| {
1502                        window.dispatch_action(ToggleCaseSensitive.boxed_clone(), cx)
1503                    }),
1504            )
1505            .child(
1506                Button::new("match-whole-words", "Match whole words")
1507                    .icon(IconName::WholeWord)
1508                    .icon_position(IconPosition::Start)
1509                    .icon_size(IconSize::Small)
1510                    .key_binding(KeyBinding::for_action_in(
1511                        &ToggleWholeWord,
1512                        &focus_handle,
1513                        window,
1514                        cx,
1515                    ))
1516                    .on_click(|_event, window, cx| {
1517                        window.dispatch_action(ToggleWholeWord.boxed_clone(), cx)
1518                    }),
1519            )
1520    }
1521
1522    fn border_color_for(&self, panel: InputPanel, cx: &App) -> Hsla {
1523        if self.panels_with_errors.contains_key(&panel) {
1524            Color::Error.color(cx)
1525        } else {
1526            cx.theme().colors().border
1527        }
1528    }
1529
1530    fn move_focus_to_results(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1531        if !self.results_editor.focus_handle(cx).is_focused(window)
1532            && !self.entity.read(cx).match_ranges.is_empty()
1533        {
1534            cx.stop_propagation();
1535            self.focus_results_editor(window, cx)
1536        }
1537    }
1538
1539    #[cfg(any(test, feature = "test-support"))]
1540    pub fn results_editor(&self) -> &Entity<Editor> {
1541        &self.results_editor
1542    }
1543
1544    fn adjust_query_regex_language(&self, cx: &mut App) {
1545        let enable = self.search_options.contains(SearchOptions::REGEX);
1546        let query_buffer = self
1547            .query_editor
1548            .read(cx)
1549            .buffer()
1550            .read(cx)
1551            .as_singleton()
1552            .expect("query editor should be backed by a singleton buffer");
1553        if enable {
1554            if let Some(regex_language) = self.regex_language.clone() {
1555                query_buffer.update(cx, |query_buffer, cx| {
1556                    query_buffer.set_language(Some(regex_language), cx);
1557                })
1558            }
1559        } else {
1560            query_buffer.update(cx, |query_buffer, cx| {
1561                query_buffer.set_language(None, cx);
1562            })
1563        }
1564    }
1565}
1566
1567fn buffer_search_query(
1568    workspace: &mut Workspace,
1569    item: &dyn ItemHandle,
1570    cx: &mut Context<Workspace>,
1571) -> Option<String> {
1572    let buffer_search_bar = workspace
1573        .pane_for(item)
1574        .and_then(|pane| {
1575            pane.read(cx)
1576                .toolbar()
1577                .read(cx)
1578                .item_of_type::<BufferSearchBar>()
1579        })?
1580        .read(cx);
1581    if buffer_search_bar.query_editor_focused() {
1582        let buffer_search_query = buffer_search_bar.query(cx);
1583        if !buffer_search_query.is_empty() {
1584            return Some(buffer_search_query);
1585        }
1586    }
1587    None
1588}
1589
1590impl Default for ProjectSearchBar {
1591    fn default() -> Self {
1592        Self::new()
1593    }
1594}
1595
1596impl ProjectSearchBar {
1597    pub fn new() -> Self {
1598        Self {
1599            active_project_search: None,
1600            subscription: None,
1601        }
1602    }
1603
1604    fn confirm(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
1605        if let Some(search_view) = self.active_project_search.as_ref() {
1606            search_view.update(cx, |search_view, cx| {
1607                if !search_view
1608                    .replacement_editor
1609                    .focus_handle(cx)
1610                    .is_focused(window)
1611                {
1612                    cx.stop_propagation();
1613                    search_view
1614                        .prompt_to_save_if_dirty_then_search(window, cx)
1615                        .detach_and_log_err(cx);
1616                }
1617            });
1618        }
1619    }
1620
1621    fn tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context<Self>) {
1622        self.cycle_field(Direction::Next, window, cx);
1623    }
1624
1625    fn backtab(&mut self, _: &Backtab, window: &mut Window, cx: &mut Context<Self>) {
1626        self.cycle_field(Direction::Prev, window, cx);
1627    }
1628
1629    fn focus_search(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1630        if let Some(search_view) = self.active_project_search.as_ref() {
1631            search_view.update(cx, |search_view, cx| {
1632                search_view.query_editor.focus_handle(cx).focus(window);
1633            });
1634        }
1635    }
1636
1637    fn cycle_field(&mut self, direction: Direction, window: &mut Window, cx: &mut Context<Self>) {
1638        let active_project_search = match &self.active_project_search {
1639            Some(active_project_search) => active_project_search,
1640            None => return,
1641        };
1642
1643        active_project_search.update(cx, |project_view, cx| {
1644            let mut views = vec![project_view.query_editor.focus_handle(cx)];
1645            if project_view.replace_enabled {
1646                views.push(project_view.replacement_editor.focus_handle(cx));
1647            }
1648            if project_view.filters_enabled {
1649                views.extend([
1650                    project_view.included_files_editor.focus_handle(cx),
1651                    project_view.excluded_files_editor.focus_handle(cx),
1652                ]);
1653            }
1654            let current_index = match views.iter().position(|focus| focus.is_focused(window)) {
1655                Some(index) => index,
1656                None => return,
1657            };
1658
1659            let new_index = match direction {
1660                Direction::Next => (current_index + 1) % views.len(),
1661                Direction::Prev if current_index == 0 => views.len() - 1,
1662                Direction::Prev => (current_index - 1) % views.len(),
1663            };
1664            let next_focus_handle = &views[new_index];
1665            window.focus(next_focus_handle);
1666            cx.stop_propagation();
1667        });
1668    }
1669
1670    pub(crate) fn toggle_search_option(
1671        &mut self,
1672        option: SearchOptions,
1673        window: &mut Window,
1674        cx: &mut Context<Self>,
1675    ) -> bool {
1676        if self.active_project_search.is_none() {
1677            return false;
1678        }
1679
1680        cx.spawn_in(window, async move |this, cx| {
1681            let task = this.update_in(cx, |this, window, cx| {
1682                let search_view = this.active_project_search.as_ref()?;
1683                search_view.update(cx, |search_view, cx| {
1684                    search_view.toggle_search_option(option, cx);
1685                    search_view
1686                        .entity
1687                        .read(cx)
1688                        .active_query
1689                        .is_some()
1690                        .then(|| search_view.prompt_to_save_if_dirty_then_search(window, cx))
1691                })
1692            })?;
1693            if let Some(task) = task {
1694                task.await?;
1695            }
1696            this.update(cx, |_, cx| {
1697                cx.notify();
1698            })?;
1699            anyhow::Ok(())
1700        })
1701        .detach();
1702        true
1703    }
1704
1705    fn toggle_replace(&mut self, _: &ToggleReplace, window: &mut Window, cx: &mut Context<Self>) {
1706        if let Some(search) = &self.active_project_search {
1707            search.update(cx, |this, cx| {
1708                this.replace_enabled = !this.replace_enabled;
1709                let editor_to_focus = if this.replace_enabled {
1710                    this.replacement_editor.focus_handle(cx)
1711                } else {
1712                    this.query_editor.focus_handle(cx)
1713                };
1714                window.focus(&editor_to_focus);
1715                cx.notify();
1716            });
1717        }
1718    }
1719
1720    fn toggle_filters(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
1721        if let Some(search_view) = self.active_project_search.as_ref() {
1722            search_view.update(cx, |search_view, cx| {
1723                search_view.toggle_filters(cx);
1724                search_view
1725                    .included_files_editor
1726                    .update(cx, |_, cx| cx.notify());
1727                search_view
1728                    .excluded_files_editor
1729                    .update(cx, |_, cx| cx.notify());
1730                window.refresh();
1731                cx.notify();
1732            });
1733            cx.notify();
1734            true
1735        } else {
1736            false
1737        }
1738    }
1739
1740    fn toggle_opened_only(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
1741        if self.active_project_search.is_none() {
1742            return false;
1743        }
1744
1745        cx.spawn_in(window, async move |this, cx| {
1746            let task = this.update_in(cx, |this, window, cx| {
1747                let search_view = this.active_project_search.as_ref()?;
1748                search_view.update(cx, |search_view, cx| {
1749                    search_view.toggle_opened_only(window, cx);
1750                    search_view
1751                        .entity
1752                        .read(cx)
1753                        .active_query
1754                        .is_some()
1755                        .then(|| search_view.prompt_to_save_if_dirty_then_search(window, cx))
1756                })
1757            })?;
1758            if let Some(task) = task {
1759                task.await?;
1760            }
1761            this.update(cx, |_, cx| {
1762                cx.notify();
1763            })?;
1764            anyhow::Ok(())
1765        })
1766        .detach();
1767        true
1768    }
1769
1770    fn is_opened_only_enabled(&self, cx: &App) -> bool {
1771        if let Some(search_view) = self.active_project_search.as_ref() {
1772            search_view.read(cx).included_opened_only
1773        } else {
1774            false
1775        }
1776    }
1777
1778    fn move_focus_to_results(&self, window: &mut Window, cx: &mut Context<Self>) {
1779        if let Some(search_view) = self.active_project_search.as_ref() {
1780            search_view.update(cx, |search_view, cx| {
1781                search_view.move_focus_to_results(window, cx);
1782            });
1783            cx.notify();
1784        }
1785    }
1786
1787    fn next_history_query(
1788        &mut self,
1789        _: &NextHistoryQuery,
1790        window: &mut Window,
1791        cx: &mut Context<Self>,
1792    ) {
1793        if let Some(search_view) = self.active_project_search.as_ref() {
1794            search_view.update(cx, |search_view, cx| {
1795                for (editor, kind) in [
1796                    (search_view.query_editor.clone(), SearchInputKind::Query),
1797                    (
1798                        search_view.included_files_editor.clone(),
1799                        SearchInputKind::Include,
1800                    ),
1801                    (
1802                        search_view.excluded_files_editor.clone(),
1803                        SearchInputKind::Exclude,
1804                    ),
1805                ] {
1806                    if editor.focus_handle(cx).is_focused(window) {
1807                        let new_query = search_view.entity.update(cx, |model, cx| {
1808                            let project = model.project.clone();
1809
1810                            if let Some(new_query) = project.update(cx, |project, _| {
1811                                project
1812                                    .search_history_mut(kind)
1813                                    .next(model.cursor_mut(kind))
1814                                    .map(str::to_string)
1815                            }) {
1816                                new_query
1817                            } else {
1818                                model.cursor_mut(kind).reset();
1819                                String::new()
1820                            }
1821                        });
1822                        search_view.set_search_editor(kind, &new_query, window, cx);
1823                    }
1824                }
1825            });
1826        }
1827    }
1828
1829    fn previous_history_query(
1830        &mut self,
1831        _: &PreviousHistoryQuery,
1832        window: &mut Window,
1833        cx: &mut Context<Self>,
1834    ) {
1835        if let Some(search_view) = self.active_project_search.as_ref() {
1836            search_view.update(cx, |search_view, cx| {
1837                for (editor, kind) in [
1838                    (search_view.query_editor.clone(), SearchInputKind::Query),
1839                    (
1840                        search_view.included_files_editor.clone(),
1841                        SearchInputKind::Include,
1842                    ),
1843                    (
1844                        search_view.excluded_files_editor.clone(),
1845                        SearchInputKind::Exclude,
1846                    ),
1847                ] {
1848                    if editor.focus_handle(cx).is_focused(window) {
1849                        if editor.read(cx).text(cx).is_empty()
1850                            && let Some(new_query) = search_view
1851                                .entity
1852                                .read(cx)
1853                                .project
1854                                .read(cx)
1855                                .search_history(kind)
1856                                .current(search_view.entity.read(cx).cursor(kind))
1857                                .map(str::to_string)
1858                        {
1859                            search_view.set_search_editor(kind, &new_query, window, cx);
1860                            return;
1861                        }
1862
1863                        if let Some(new_query) = search_view.entity.update(cx, |model, cx| {
1864                            let project = model.project.clone();
1865                            project.update(cx, |project, _| {
1866                                project
1867                                    .search_history_mut(kind)
1868                                    .previous(model.cursor_mut(kind))
1869                                    .map(str::to_string)
1870                            })
1871                        }) {
1872                            search_view.set_search_editor(kind, &new_query, window, cx);
1873                        }
1874                    }
1875                }
1876            });
1877        }
1878    }
1879
1880    fn select_next_match(
1881        &mut self,
1882        _: &SelectNextMatch,
1883        window: &mut Window,
1884        cx: &mut Context<Self>,
1885    ) {
1886        if let Some(search) = self.active_project_search.as_ref() {
1887            search.update(cx, |this, cx| {
1888                this.select_match(Direction::Next, window, cx);
1889            })
1890        }
1891    }
1892
1893    fn select_prev_match(
1894        &mut self,
1895        _: &SelectPreviousMatch,
1896        window: &mut Window,
1897        cx: &mut Context<Self>,
1898    ) {
1899        if let Some(search) = self.active_project_search.as_ref() {
1900            search.update(cx, |this, cx| {
1901                this.select_match(Direction::Prev, window, cx);
1902            })
1903        }
1904    }
1905}
1906
1907impl Render for ProjectSearchBar {
1908    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1909        let Some(search) = self.active_project_search.clone() else {
1910            return div();
1911        };
1912        let search = search.read(cx);
1913        let focus_handle = search.focus_handle(cx);
1914
1915        let container_width = window.viewport_size().width;
1916        let input_width = SearchInputWidth::calc_width(container_width);
1917
1918        let input_base_styles = |panel: InputPanel| {
1919            input_base_styles(search.border_color_for(panel, cx), |div| match panel {
1920                InputPanel::Query | InputPanel::Replacement => div.w(input_width),
1921                InputPanel::Include | InputPanel::Exclude => div.flex_grow(),
1922            })
1923        };
1924        let theme_colors = cx.theme().colors();
1925        let project_search = search.entity.read(cx);
1926        let limit_reached = project_search.limit_reached;
1927
1928        let color_override = match (
1929            &project_search.pending_search,
1930            project_search.no_results,
1931            &project_search.active_query,
1932            &project_search.last_search_query_text,
1933        ) {
1934            (None, Some(true), Some(q), Some(p)) if q.as_str() == p => Some(Color::Error),
1935            _ => None,
1936        };
1937
1938        let match_text = search
1939            .active_match_index
1940            .and_then(|index| {
1941                let index = index + 1;
1942                let match_quantity = project_search.match_ranges.len();
1943                if match_quantity > 0 {
1944                    debug_assert!(match_quantity >= index);
1945                    if limit_reached {
1946                        Some(format!("{index}/{match_quantity}+"))
1947                    } else {
1948                        Some(format!("{index}/{match_quantity}"))
1949                    }
1950                } else {
1951                    None
1952                }
1953            })
1954            .unwrap_or_else(|| "0/0".to_string());
1955
1956        let query_column = input_base_styles(InputPanel::Query)
1957            .on_action(cx.listener(|this, action, window, cx| this.confirm(action, window, cx)))
1958            .on_action(cx.listener(|this, action, window, cx| {
1959                this.previous_history_query(action, window, cx)
1960            }))
1961            .on_action(
1962                cx.listener(|this, action, window, cx| this.next_history_query(action, window, cx)),
1963            )
1964            .child(render_text_input(&search.query_editor, color_override, cx))
1965            .child(
1966                h_flex()
1967                    .gap_1()
1968                    .child(SearchOption::CaseSensitive.as_button(
1969                        search.search_options,
1970                        SearchSource::Project(cx),
1971                        focus_handle.clone(),
1972                    ))
1973                    .child(SearchOption::WholeWord.as_button(
1974                        search.search_options,
1975                        SearchSource::Project(cx),
1976                        focus_handle.clone(),
1977                    ))
1978                    .child(SearchOption::Regex.as_button(
1979                        search.search_options,
1980                        SearchSource::Project(cx),
1981                        focus_handle.clone(),
1982                    )),
1983            );
1984
1985        let query_focus = search.query_editor.focus_handle(cx);
1986
1987        let matches_column = h_flex()
1988            .pl_2()
1989            .ml_2()
1990            .border_l_1()
1991            .border_color(theme_colors.border_variant)
1992            .child(render_action_button(
1993                "project-search-nav-button",
1994                IconName::ChevronLeft,
1995                search
1996                    .active_match_index
1997                    .is_none()
1998                    .then_some(ActionButtonState::Disabled),
1999                "Select Previous Match",
2000                &SelectPreviousMatch,
2001                query_focus.clone(),
2002            ))
2003            .child(render_action_button(
2004                "project-search-nav-button",
2005                IconName::ChevronRight,
2006                search
2007                    .active_match_index
2008                    .is_none()
2009                    .then_some(ActionButtonState::Disabled),
2010                "Select Next Match",
2011                &SelectNextMatch,
2012                query_focus,
2013            ))
2014            .child(
2015                div()
2016                    .id("matches")
2017                    .ml_2()
2018                    .min_w(rems_from_px(40.))
2019                    .child(Label::new(match_text).size(LabelSize::Small).color(
2020                        if search.active_match_index.is_some() {
2021                            Color::Default
2022                        } else {
2023                            Color::Disabled
2024                        },
2025                    ))
2026                    .when(limit_reached, |el| {
2027                        el.tooltip(Tooltip::text(
2028                            "Search limits reached.\nTry narrowing your search.",
2029                        ))
2030                    }),
2031            );
2032
2033        let mode_column = h_flex()
2034            .gap_1()
2035            .min_w_64()
2036            .child(
2037                IconButton::new("project-search-filter-button", IconName::Filter)
2038                    .shape(IconButtonShape::Square)
2039                    .tooltip(|window, cx| {
2040                        Tooltip::for_action("Toggle Filters", &ToggleFilters, window, cx)
2041                    })
2042                    .on_click(cx.listener(|this, _, window, cx| {
2043                        this.toggle_filters(window, cx);
2044                    }))
2045                    .toggle_state(
2046                        self.active_project_search
2047                            .as_ref()
2048                            .map(|search| search.read(cx).filters_enabled)
2049                            .unwrap_or_default(),
2050                    )
2051                    .tooltip({
2052                        let focus_handle = focus_handle.clone();
2053                        move |window, cx| {
2054                            Tooltip::for_action_in(
2055                                "Toggle Filters",
2056                                &ToggleFilters,
2057                                &focus_handle,
2058                                window,
2059                                cx,
2060                            )
2061                        }
2062                    }),
2063            )
2064            .child(render_action_button(
2065                "project-search",
2066                IconName::Replace,
2067                self.active_project_search
2068                    .as_ref()
2069                    .map(|search| search.read(cx).replace_enabled)
2070                    .and_then(|enabled| enabled.then_some(ActionButtonState::Toggled)),
2071                "Toggle Replace",
2072                &ToggleReplace,
2073                focus_handle.clone(),
2074            ))
2075            .child(matches_column);
2076
2077        let search_line = h_flex()
2078            .w_full()
2079            .gap_2()
2080            .child(query_column)
2081            .child(mode_column);
2082
2083        let replace_line = search.replace_enabled.then(|| {
2084            let replace_column = input_base_styles(InputPanel::Replacement)
2085                .child(render_text_input(&search.replacement_editor, None, cx));
2086
2087            let focus_handle = search.replacement_editor.read(cx).focus_handle(cx);
2088
2089            let replace_actions = h_flex()
2090                .min_w_64()
2091                .gap_1()
2092                .child(render_action_button(
2093                    "project-search-replace-button",
2094                    IconName::ReplaceNext,
2095                    Default::default(),
2096                    "Replace Next Match",
2097                    &ReplaceNext,
2098                    focus_handle.clone(),
2099                ))
2100                .child(render_action_button(
2101                    "project-search-replace-button",
2102                    IconName::ReplaceAll,
2103                    Default::default(),
2104                    "Replace All Matches",
2105                    &ReplaceAll,
2106                    focus_handle,
2107                ));
2108
2109            h_flex()
2110                .w_full()
2111                .gap_2()
2112                .child(replace_column)
2113                .child(replace_actions)
2114        });
2115
2116        let filter_line = search.filters_enabled.then(|| {
2117            let include = input_base_styles(InputPanel::Include)
2118                .on_action(cx.listener(|this, action, window, cx| {
2119                    this.previous_history_query(action, window, cx)
2120                }))
2121                .on_action(cx.listener(|this, action, window, cx| {
2122                    this.next_history_query(action, window, cx)
2123                }))
2124                .child(render_text_input(&search.included_files_editor, None, cx));
2125            let exclude = input_base_styles(InputPanel::Exclude)
2126                .on_action(cx.listener(|this, action, window, cx| {
2127                    this.previous_history_query(action, window, cx)
2128                }))
2129                .on_action(cx.listener(|this, action, window, cx| {
2130                    this.next_history_query(action, window, cx)
2131                }))
2132                .child(render_text_input(&search.excluded_files_editor, None, cx));
2133            let mode_column = h_flex()
2134                .gap_1()
2135                .min_w_64()
2136                .child(
2137                    IconButton::new("project-search-opened-only", IconName::FolderSearch)
2138                        .shape(IconButtonShape::Square)
2139                        .toggle_state(self.is_opened_only_enabled(cx))
2140                        .tooltip(Tooltip::text("Only Search Open Files"))
2141                        .on_click(cx.listener(|this, _, window, cx| {
2142                            this.toggle_opened_only(window, cx);
2143                        })),
2144                )
2145                .child(SearchOption::IncludeIgnored.as_button(
2146                    search.search_options,
2147                    SearchSource::Project(cx),
2148                    focus_handle.clone(),
2149                ));
2150            h_flex()
2151                .w_full()
2152                .gap_2()
2153                .child(
2154                    h_flex()
2155                        .gap_2()
2156                        .w(input_width)
2157                        .child(include)
2158                        .child(exclude),
2159                )
2160                .child(mode_column)
2161        });
2162
2163        let mut key_context = KeyContext::default();
2164        key_context.add("ProjectSearchBar");
2165        if search
2166            .replacement_editor
2167            .focus_handle(cx)
2168            .is_focused(window)
2169        {
2170            key_context.add("in_replace");
2171        }
2172
2173        let query_error_line = search
2174            .panels_with_errors
2175            .get(&InputPanel::Query)
2176            .map(|error| {
2177                Label::new(error)
2178                    .size(LabelSize::Small)
2179                    .color(Color::Error)
2180                    .mt_neg_1()
2181                    .ml_2()
2182            });
2183
2184        let filter_error_line = search
2185            .panels_with_errors
2186            .get(&InputPanel::Include)
2187            .or_else(|| search.panels_with_errors.get(&InputPanel::Exclude))
2188            .map(|error| {
2189                Label::new(error)
2190                    .size(LabelSize::Small)
2191                    .color(Color::Error)
2192                    .mt_neg_1()
2193                    .ml_2()
2194            });
2195
2196        v_flex()
2197            .gap_2()
2198            .py(px(1.0))
2199            .w_full()
2200            .key_context(key_context)
2201            .on_action(cx.listener(|this, _: &ToggleFocus, window, cx| {
2202                this.move_focus_to_results(window, cx)
2203            }))
2204            .on_action(cx.listener(|this, _: &ToggleFilters, window, cx| {
2205                this.toggle_filters(window, cx);
2206            }))
2207            .capture_action(cx.listener(Self::tab))
2208            .capture_action(cx.listener(Self::backtab))
2209            .on_action(cx.listener(|this, action, window, cx| this.confirm(action, window, cx)))
2210            .on_action(cx.listener(|this, action, window, cx| {
2211                this.toggle_replace(action, window, cx);
2212            }))
2213            .on_action(cx.listener(|this, _: &ToggleWholeWord, window, cx| {
2214                this.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
2215            }))
2216            .on_action(cx.listener(|this, _: &ToggleCaseSensitive, window, cx| {
2217                this.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
2218            }))
2219            .on_action(cx.listener(|this, action, window, cx| {
2220                if let Some(search) = this.active_project_search.as_ref() {
2221                    search.update(cx, |this, cx| {
2222                        this.replace_next(action, window, cx);
2223                    })
2224                }
2225            }))
2226            .on_action(cx.listener(|this, action, window, cx| {
2227                if let Some(search) = this.active_project_search.as_ref() {
2228                    search.update(cx, |this, cx| {
2229                        this.replace_all(action, window, cx);
2230                    })
2231                }
2232            }))
2233            .when(search.filters_enabled, |this| {
2234                this.on_action(cx.listener(|this, _: &ToggleIncludeIgnored, window, cx| {
2235                    this.toggle_search_option(SearchOptions::INCLUDE_IGNORED, window, cx);
2236                }))
2237            })
2238            .on_action(cx.listener(Self::select_next_match))
2239            .on_action(cx.listener(Self::select_prev_match))
2240            .child(search_line)
2241            .children(query_error_line)
2242            .children(replace_line)
2243            .children(filter_line)
2244            .children(filter_error_line)
2245    }
2246}
2247
2248impl EventEmitter<ToolbarItemEvent> for ProjectSearchBar {}
2249
2250impl ToolbarItemView for ProjectSearchBar {
2251    fn set_active_pane_item(
2252        &mut self,
2253        active_pane_item: Option<&dyn ItemHandle>,
2254        _: &mut Window,
2255        cx: &mut Context<Self>,
2256    ) -> ToolbarItemLocation {
2257        cx.notify();
2258        self.subscription = None;
2259        self.active_project_search = None;
2260        if let Some(search) = active_pane_item.and_then(|i| i.downcast::<ProjectSearchView>()) {
2261            self.subscription = Some(cx.observe(&search, |_, _, cx| cx.notify()));
2262            self.active_project_search = Some(search);
2263            ToolbarItemLocation::PrimaryLeft {}
2264        } else {
2265            ToolbarItemLocation::Hidden
2266        }
2267    }
2268}
2269
2270fn register_workspace_action<A: Action>(
2271    workspace: &mut Workspace,
2272    callback: fn(&mut ProjectSearchBar, &A, &mut Window, &mut Context<ProjectSearchBar>),
2273) {
2274    workspace.register_action(move |workspace, action: &A, window, cx| {
2275        if workspace.has_active_modal(window, cx) {
2276            cx.propagate();
2277            return;
2278        }
2279
2280        workspace.active_pane().update(cx, |pane, cx| {
2281            pane.toolbar().update(cx, move |workspace, cx| {
2282                if let Some(search_bar) = workspace.item_of_type::<ProjectSearchBar>() {
2283                    search_bar.update(cx, move |search_bar, cx| {
2284                        if search_bar.active_project_search.is_some() {
2285                            callback(search_bar, action, window, cx);
2286                            cx.notify();
2287                        } else {
2288                            cx.propagate();
2289                        }
2290                    });
2291                }
2292            });
2293        })
2294    });
2295}
2296
2297fn register_workspace_action_for_present_search<A: Action>(
2298    workspace: &mut Workspace,
2299    callback: fn(&mut Workspace, &A, &mut Window, &mut Context<Workspace>),
2300) {
2301    workspace.register_action(move |workspace, action: &A, window, cx| {
2302        if workspace.has_active_modal(window, cx) {
2303            cx.propagate();
2304            return;
2305        }
2306
2307        let should_notify = workspace
2308            .active_pane()
2309            .read(cx)
2310            .toolbar()
2311            .read(cx)
2312            .item_of_type::<ProjectSearchBar>()
2313            .map(|search_bar| search_bar.read(cx).active_project_search.is_some())
2314            .unwrap_or(false);
2315        if should_notify {
2316            callback(workspace, action, window, cx);
2317            cx.notify();
2318        } else {
2319            cx.propagate();
2320        }
2321    });
2322}
2323
2324#[cfg(any(test, feature = "test-support"))]
2325pub fn perform_project_search(
2326    search_view: &Entity<ProjectSearchView>,
2327    text: impl Into<std::sync::Arc<str>>,
2328    cx: &mut gpui::VisualTestContext,
2329) {
2330    cx.run_until_parked();
2331    search_view.update_in(cx, |search_view, window, cx| {
2332        search_view.query_editor.update(cx, |query_editor, cx| {
2333            query_editor.set_text(text, window, cx)
2334        });
2335        search_view.search(cx);
2336    });
2337    cx.run_until_parked();
2338}
2339
2340#[cfg(test)]
2341pub mod tests {
2342    use std::{ops::Deref as _, sync::Arc, time::Duration};
2343
2344    use super::*;
2345    use editor::{DisplayPoint, display_map::DisplayRow};
2346    use gpui::{Action, TestAppContext, VisualTestContext, WindowHandle};
2347    use project::FakeFs;
2348    use serde_json::json;
2349    use settings::SettingsStore;
2350    use util::{path, paths::PathStyle, rel_path::rel_path};
2351    use workspace::DeploySearch;
2352
2353    #[gpui::test]
2354    async fn test_project_search(cx: &mut TestAppContext) {
2355        init_test(cx);
2356
2357        let fs = FakeFs::new(cx.background_executor.clone());
2358        fs.insert_tree(
2359            path!("/dir"),
2360            json!({
2361                "one.rs": "const ONE: usize = 1;",
2362                "two.rs": "const TWO: usize = one::ONE + one::ONE;",
2363                "three.rs": "const THREE: usize = one::ONE + two::TWO;",
2364                "four.rs": "const FOUR: usize = one::ONE + three::THREE;",
2365            }),
2366        )
2367        .await;
2368        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
2369        let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2370        let workspace = window.root(cx).unwrap();
2371        let search = cx.new(|cx| ProjectSearch::new(project.clone(), cx));
2372        let search_view = cx.add_window(|window, cx| {
2373            ProjectSearchView::new(workspace.downgrade(), search.clone(), window, cx, None)
2374        });
2375
2376        perform_search(search_view, "TWO", cx);
2377        search_view.update(cx, |search_view, window, cx| {
2378            assert_eq!(
2379                search_view
2380                    .results_editor
2381                    .update(cx, |editor, cx| editor.display_text(cx)),
2382                "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;"
2383            );
2384            let match_background_color = cx.theme().colors().search_match_background;
2385            let selection_background_color = cx.theme().colors().editor_document_highlight_bracket_background;
2386            assert_eq!(
2387                search_view
2388                    .results_editor
2389                    .update(cx, |editor, cx| editor.all_text_background_highlights(window, cx)),
2390                &[
2391                    (
2392                        DisplayPoint::new(DisplayRow(2), 32)..DisplayPoint::new(DisplayRow(2), 35),
2393                        match_background_color
2394                    ),
2395                    (
2396                        DisplayPoint::new(DisplayRow(2), 37)..DisplayPoint::new(DisplayRow(2), 40),
2397                        selection_background_color
2398                    ),
2399                    (
2400                        DisplayPoint::new(DisplayRow(2), 37)..DisplayPoint::new(DisplayRow(2), 40),
2401                        match_background_color
2402                    ),
2403                    (
2404                        DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 9),
2405                        selection_background_color
2406                    ),
2407                    (
2408                        DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 9),
2409                        match_background_color
2410                    ),
2411
2412                ]
2413            );
2414            assert_eq!(search_view.active_match_index, Some(0));
2415            assert_eq!(
2416                search_view
2417                    .results_editor
2418                    .update(cx, |editor, cx| editor.selections.display_ranges(cx)),
2419                [DisplayPoint::new(DisplayRow(2), 32)..DisplayPoint::new(DisplayRow(2), 35)]
2420            );
2421
2422            search_view.select_match(Direction::Next, window, cx);
2423        }).unwrap();
2424
2425        search_view
2426            .update(cx, |search_view, window, cx| {
2427                assert_eq!(search_view.active_match_index, Some(1));
2428                assert_eq!(
2429                    search_view
2430                        .results_editor
2431                        .update(cx, |editor, cx| editor.selections.display_ranges(cx)),
2432                    [DisplayPoint::new(DisplayRow(2), 37)..DisplayPoint::new(DisplayRow(2), 40)]
2433                );
2434                search_view.select_match(Direction::Next, window, cx);
2435            })
2436            .unwrap();
2437
2438        search_view
2439            .update(cx, |search_view, window, cx| {
2440                assert_eq!(search_view.active_match_index, Some(2));
2441                assert_eq!(
2442                    search_view
2443                        .results_editor
2444                        .update(cx, |editor, cx| editor.selections.display_ranges(cx)),
2445                    [DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 9)]
2446                );
2447                search_view.select_match(Direction::Next, window, cx);
2448            })
2449            .unwrap();
2450
2451        search_view
2452            .update(cx, |search_view, window, cx| {
2453                assert_eq!(search_view.active_match_index, Some(0));
2454                assert_eq!(
2455                    search_view
2456                        .results_editor
2457                        .update(cx, |editor, cx| editor.selections.display_ranges(cx)),
2458                    [DisplayPoint::new(DisplayRow(2), 32)..DisplayPoint::new(DisplayRow(2), 35)]
2459                );
2460                search_view.select_match(Direction::Prev, window, cx);
2461            })
2462            .unwrap();
2463
2464        search_view
2465            .update(cx, |search_view, window, cx| {
2466                assert_eq!(search_view.active_match_index, Some(2));
2467                assert_eq!(
2468                    search_view
2469                        .results_editor
2470                        .update(cx, |editor, cx| editor.selections.display_ranges(cx)),
2471                    [DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 9)]
2472                );
2473                search_view.select_match(Direction::Prev, window, cx);
2474            })
2475            .unwrap();
2476
2477        search_view
2478            .update(cx, |search_view, _, cx| {
2479                assert_eq!(search_view.active_match_index, Some(1));
2480                assert_eq!(
2481                    search_view
2482                        .results_editor
2483                        .update(cx, |editor, cx| editor.selections.display_ranges(cx)),
2484                    [DisplayPoint::new(DisplayRow(2), 37)..DisplayPoint::new(DisplayRow(2), 40)]
2485                );
2486            })
2487            .unwrap();
2488    }
2489
2490    #[gpui::test]
2491    async fn test_deploy_project_search_focus(cx: &mut TestAppContext) {
2492        init_test(cx);
2493
2494        let fs = FakeFs::new(cx.background_executor.clone());
2495        fs.insert_tree(
2496            "/dir",
2497            json!({
2498                "one.rs": "const ONE: usize = 1;",
2499                "two.rs": "const TWO: usize = one::ONE + one::ONE;",
2500                "three.rs": "const THREE: usize = one::ONE + two::TWO;",
2501                "four.rs": "const FOUR: usize = one::ONE + three::THREE;",
2502            }),
2503        )
2504        .await;
2505        let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
2506        let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
2507        let workspace = window;
2508        let search_bar = window.build_entity(cx, |_, _| ProjectSearchBar::new());
2509
2510        let active_item = cx.read(|cx| {
2511            workspace
2512                .read(cx)
2513                .unwrap()
2514                .active_pane()
2515                .read(cx)
2516                .active_item()
2517                .and_then(|item| item.downcast::<ProjectSearchView>())
2518        });
2519        assert!(
2520            active_item.is_none(),
2521            "Expected no search panel to be active"
2522        );
2523
2524        window
2525            .update(cx, move |workspace, window, cx| {
2526                assert_eq!(workspace.panes().len(), 1);
2527                workspace.panes()[0].update(cx, |pane, cx| {
2528                    pane.toolbar()
2529                        .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx))
2530                });
2531
2532                ProjectSearchView::deploy_search(
2533                    workspace,
2534                    &workspace::DeploySearch::find(),
2535                    window,
2536                    cx,
2537                )
2538            })
2539            .unwrap();
2540
2541        let Some(search_view) = cx.read(|cx| {
2542            workspace
2543                .read(cx)
2544                .unwrap()
2545                .active_pane()
2546                .read(cx)
2547                .active_item()
2548                .and_then(|item| item.downcast::<ProjectSearchView>())
2549        }) else {
2550            panic!("Search view expected to appear after new search event trigger")
2551        };
2552
2553        cx.spawn(|mut cx| async move {
2554            window
2555                .update(&mut cx, |_, window, cx| {
2556                    window.dispatch_action(ToggleFocus.boxed_clone(), cx)
2557                })
2558                .unwrap();
2559        })
2560        .detach();
2561        cx.background_executor.run_until_parked();
2562        window
2563            .update(cx, |_, window, cx| {
2564                search_view.update(cx, |search_view, cx| {
2565                    assert!(
2566                        search_view.query_editor.focus_handle(cx).is_focused(window),
2567                        "Empty search view should be focused after the toggle focus event: no results panel to focus on",
2568                    );
2569                });
2570        }).unwrap();
2571
2572        window
2573            .update(cx, |_, window, cx| {
2574                search_view.update(cx, |search_view, cx| {
2575                    let query_editor = &search_view.query_editor;
2576                    assert!(
2577                        query_editor.focus_handle(cx).is_focused(window),
2578                        "Search view should be focused after the new search view is activated",
2579                    );
2580                    let query_text = query_editor.read(cx).text(cx);
2581                    assert!(
2582                        query_text.is_empty(),
2583                        "New search query should be empty but got '{query_text}'",
2584                    );
2585                    let results_text = search_view
2586                        .results_editor
2587                        .update(cx, |editor, cx| editor.display_text(cx));
2588                    assert!(
2589                        results_text.is_empty(),
2590                        "Empty search view should have no results but got '{results_text}'"
2591                    );
2592                });
2593            })
2594            .unwrap();
2595
2596        window
2597            .update(cx, |_, window, cx| {
2598                search_view.update(cx, |search_view, cx| {
2599                    search_view.query_editor.update(cx, |query_editor, cx| {
2600                        query_editor.set_text("sOMETHINGtHATsURELYdOESnOTeXIST", window, cx)
2601                    });
2602                    search_view.search(cx);
2603                });
2604            })
2605            .unwrap();
2606        cx.background_executor.run_until_parked();
2607        window
2608            .update(cx, |_, window, cx| {
2609                search_view.update(cx, |search_view, cx| {
2610                    let results_text = search_view
2611                        .results_editor
2612                        .update(cx, |editor, cx| editor.display_text(cx));
2613                    assert!(
2614                        results_text.is_empty(),
2615                        "Search view for mismatching query should have no results but got '{results_text}'"
2616                    );
2617                    assert!(
2618                        search_view.query_editor.focus_handle(cx).is_focused(window),
2619                        "Search view should be focused after mismatching query had been used in search",
2620                    );
2621                });
2622            }).unwrap();
2623
2624        cx.spawn(|mut cx| async move {
2625            window.update(&mut cx, |_, window, cx| {
2626                window.dispatch_action(ToggleFocus.boxed_clone(), cx)
2627            })
2628        })
2629        .detach();
2630        cx.background_executor.run_until_parked();
2631        window.update(cx, |_, window, cx| {
2632            search_view.update(cx, |search_view, cx| {
2633                assert!(
2634                    search_view.query_editor.focus_handle(cx).is_focused(window),
2635                    "Search view with mismatching query should be focused after the toggle focus event: still no results panel to focus on",
2636                );
2637            });
2638        }).unwrap();
2639
2640        window
2641            .update(cx, |_, window, cx| {
2642                search_view.update(cx, |search_view, cx| {
2643                    search_view.query_editor.update(cx, |query_editor, cx| {
2644                        query_editor.set_text("TWO", window, cx)
2645                    });
2646                    search_view.search(cx);
2647                });
2648            })
2649            .unwrap();
2650        cx.background_executor.run_until_parked();
2651        window.update(cx, |_, window, cx| {
2652            search_view.update(cx, |search_view, cx| {
2653                assert_eq!(
2654                    search_view
2655                        .results_editor
2656                        .update(cx, |editor, cx| editor.display_text(cx)),
2657                    "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;",
2658                    "Search view results should match the query"
2659                );
2660                assert!(
2661                    search_view.results_editor.focus_handle(cx).is_focused(window),
2662                    "Search view with mismatching query should be focused after search results are available",
2663                );
2664            });
2665        }).unwrap();
2666        cx.spawn(|mut cx| async move {
2667            window
2668                .update(&mut cx, |_, window, cx| {
2669                    window.dispatch_action(ToggleFocus.boxed_clone(), cx)
2670                })
2671                .unwrap();
2672        })
2673        .detach();
2674        cx.background_executor.run_until_parked();
2675        window.update(cx, |_, window, cx| {
2676            search_view.update(cx, |search_view, cx| {
2677                assert!(
2678                    search_view.results_editor.focus_handle(cx).is_focused(window),
2679                    "Search view with matching query should still have its results editor focused after the toggle focus event",
2680                );
2681            });
2682        }).unwrap();
2683
2684        workspace
2685            .update(cx, |workspace, window, cx| {
2686                ProjectSearchView::deploy_search(
2687                    workspace,
2688                    &workspace::DeploySearch::find(),
2689                    window,
2690                    cx,
2691                )
2692            })
2693            .unwrap();
2694        window.update(cx, |_, window, cx| {
2695            search_view.update(cx, |search_view, cx| {
2696                assert_eq!(search_view.query_editor.read(cx).text(cx), "two", "Query should be updated to first search result after search view 2nd open in a row");
2697                assert_eq!(
2698                    search_view
2699                        .results_editor
2700                        .update(cx, |editor, cx| editor.display_text(cx)),
2701                    "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;",
2702                    "Results should be unchanged after search view 2nd open in a row"
2703                );
2704                assert!(
2705                    search_view.query_editor.focus_handle(cx).is_focused(window),
2706                    "Focus should be moved into query editor again after search view 2nd open in a row"
2707                );
2708            });
2709        }).unwrap();
2710
2711        cx.spawn(|mut cx| async move {
2712            window
2713                .update(&mut cx, |_, window, cx| {
2714                    window.dispatch_action(ToggleFocus.boxed_clone(), cx)
2715                })
2716                .unwrap();
2717        })
2718        .detach();
2719        cx.background_executor.run_until_parked();
2720        window.update(cx, |_, window, cx| {
2721            search_view.update(cx, |search_view, cx| {
2722                assert!(
2723                    search_view.results_editor.focus_handle(cx).is_focused(window),
2724                    "Search view with matching query should switch focus to the results editor after the toggle focus event",
2725                );
2726            });
2727        }).unwrap();
2728    }
2729
2730    #[gpui::test]
2731    async fn test_filters_consider_toggle_state(cx: &mut TestAppContext) {
2732        init_test(cx);
2733
2734        let fs = FakeFs::new(cx.background_executor.clone());
2735        fs.insert_tree(
2736            "/dir",
2737            json!({
2738                "one.rs": "const ONE: usize = 1;",
2739                "two.rs": "const TWO: usize = one::ONE + one::ONE;",
2740                "three.rs": "const THREE: usize = one::ONE + two::TWO;",
2741                "four.rs": "const FOUR: usize = one::ONE + three::THREE;",
2742            }),
2743        )
2744        .await;
2745        let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
2746        let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
2747        let workspace = window;
2748        let search_bar = window.build_entity(cx, |_, _| ProjectSearchBar::new());
2749
2750        window
2751            .update(cx, move |workspace, window, cx| {
2752                workspace.panes()[0].update(cx, |pane, cx| {
2753                    pane.toolbar()
2754                        .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx))
2755                });
2756
2757                ProjectSearchView::deploy_search(
2758                    workspace,
2759                    &workspace::DeploySearch::find(),
2760                    window,
2761                    cx,
2762                )
2763            })
2764            .unwrap();
2765
2766        let Some(search_view) = cx.read(|cx| {
2767            workspace
2768                .read(cx)
2769                .unwrap()
2770                .active_pane()
2771                .read(cx)
2772                .active_item()
2773                .and_then(|item| item.downcast::<ProjectSearchView>())
2774        }) else {
2775            panic!("Search view expected to appear after new search event trigger")
2776        };
2777
2778        cx.spawn(|mut cx| async move {
2779            window
2780                .update(&mut cx, |_, window, cx| {
2781                    window.dispatch_action(ToggleFocus.boxed_clone(), cx)
2782                })
2783                .unwrap();
2784        })
2785        .detach();
2786        cx.background_executor.run_until_parked();
2787
2788        window
2789            .update(cx, |_, window, cx| {
2790                search_view.update(cx, |search_view, cx| {
2791                    search_view.query_editor.update(cx, |query_editor, cx| {
2792                        query_editor.set_text("const FOUR", window, cx)
2793                    });
2794                    search_view.toggle_filters(cx);
2795                    search_view
2796                        .excluded_files_editor
2797                        .update(cx, |exclude_editor, cx| {
2798                            exclude_editor.set_text("four.rs", window, cx)
2799                        });
2800                    search_view.search(cx);
2801                });
2802            })
2803            .unwrap();
2804        cx.background_executor.run_until_parked();
2805        window
2806            .update(cx, |_, _, cx| {
2807                search_view.update(cx, |search_view, cx| {
2808                    let results_text = search_view
2809                        .results_editor
2810                        .update(cx, |editor, cx| editor.display_text(cx));
2811                    assert!(
2812                        results_text.is_empty(),
2813                        "Search view for query with the only match in an excluded file should have no results but got '{results_text}'"
2814                    );
2815                });
2816            }).unwrap();
2817
2818        cx.spawn(|mut cx| async move {
2819            window.update(&mut cx, |_, window, cx| {
2820                window.dispatch_action(ToggleFocus.boxed_clone(), cx)
2821            })
2822        })
2823        .detach();
2824        cx.background_executor.run_until_parked();
2825
2826        window
2827            .update(cx, |_, _, cx| {
2828                search_view.update(cx, |search_view, cx| {
2829                    search_view.toggle_filters(cx);
2830                    search_view.search(cx);
2831                });
2832            })
2833            .unwrap();
2834        cx.background_executor.run_until_parked();
2835        window
2836            .update(cx, |_, _, cx| {
2837                search_view.update(cx, |search_view, cx| {
2838                assert_eq!(
2839                    search_view
2840                        .results_editor
2841                        .update(cx, |editor, cx| editor.display_text(cx)),
2842                    "\n\nconst FOUR: usize = one::ONE + three::THREE;",
2843                    "Search view results should contain the queried result in the previously excluded file with filters toggled off"
2844                );
2845            });
2846            })
2847            .unwrap();
2848    }
2849
2850    #[gpui::test]
2851    async fn test_new_project_search_focus(cx: &mut TestAppContext) {
2852        init_test(cx);
2853
2854        let fs = FakeFs::new(cx.background_executor.clone());
2855        fs.insert_tree(
2856            path!("/dir"),
2857            json!({
2858                "one.rs": "const ONE: usize = 1;",
2859                "two.rs": "const TWO: usize = one::ONE + one::ONE;",
2860                "three.rs": "const THREE: usize = one::ONE + two::TWO;",
2861                "four.rs": "const FOUR: usize = one::ONE + three::THREE;",
2862            }),
2863        )
2864        .await;
2865        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
2866        let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
2867        let workspace = window;
2868        let search_bar = window.build_entity(cx, |_, _| ProjectSearchBar::new());
2869
2870        let active_item = cx.read(|cx| {
2871            workspace
2872                .read(cx)
2873                .unwrap()
2874                .active_pane()
2875                .read(cx)
2876                .active_item()
2877                .and_then(|item| item.downcast::<ProjectSearchView>())
2878        });
2879        assert!(
2880            active_item.is_none(),
2881            "Expected no search panel to be active"
2882        );
2883
2884        window
2885            .update(cx, move |workspace, window, cx| {
2886                assert_eq!(workspace.panes().len(), 1);
2887                workspace.panes()[0].update(cx, |pane, cx| {
2888                    pane.toolbar()
2889                        .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx))
2890                });
2891
2892                ProjectSearchView::new_search(workspace, &workspace::NewSearch, window, cx)
2893            })
2894            .unwrap();
2895
2896        let Some(search_view) = cx.read(|cx| {
2897            workspace
2898                .read(cx)
2899                .unwrap()
2900                .active_pane()
2901                .read(cx)
2902                .active_item()
2903                .and_then(|item| item.downcast::<ProjectSearchView>())
2904        }) else {
2905            panic!("Search view expected to appear after new search event trigger")
2906        };
2907
2908        cx.spawn(|mut cx| async move {
2909            window
2910                .update(&mut cx, |_, window, cx| {
2911                    window.dispatch_action(ToggleFocus.boxed_clone(), cx)
2912                })
2913                .unwrap();
2914        })
2915        .detach();
2916        cx.background_executor.run_until_parked();
2917
2918        window.update(cx, |_, window, cx| {
2919            search_view.update(cx, |search_view, cx| {
2920                    assert!(
2921                        search_view.query_editor.focus_handle(cx).is_focused(window),
2922                        "Empty search view should be focused after the toggle focus event: no results panel to focus on",
2923                    );
2924                });
2925        }).unwrap();
2926
2927        window
2928            .update(cx, |_, window, cx| {
2929                search_view.update(cx, |search_view, cx| {
2930                    let query_editor = &search_view.query_editor;
2931                    assert!(
2932                        query_editor.focus_handle(cx).is_focused(window),
2933                        "Search view should be focused after the new search view is activated",
2934                    );
2935                    let query_text = query_editor.read(cx).text(cx);
2936                    assert!(
2937                        query_text.is_empty(),
2938                        "New search query should be empty but got '{query_text}'",
2939                    );
2940                    let results_text = search_view
2941                        .results_editor
2942                        .update(cx, |editor, cx| editor.display_text(cx));
2943                    assert!(
2944                        results_text.is_empty(),
2945                        "Empty search view should have no results but got '{results_text}'"
2946                    );
2947                });
2948            })
2949            .unwrap();
2950
2951        window
2952            .update(cx, |_, window, cx| {
2953                search_view.update(cx, |search_view, cx| {
2954                    search_view.query_editor.update(cx, |query_editor, cx| {
2955                        query_editor.set_text("sOMETHINGtHATsURELYdOESnOTeXIST", window, cx)
2956                    });
2957                    search_view.search(cx);
2958                });
2959            })
2960            .unwrap();
2961
2962        cx.background_executor.run_until_parked();
2963        window
2964            .update(cx, |_, window, cx| {
2965                search_view.update(cx, |search_view, cx| {
2966                    let results_text = search_view
2967                        .results_editor
2968                        .update(cx, |editor, cx| editor.display_text(cx));
2969                    assert!(
2970                results_text.is_empty(),
2971                "Search view for mismatching query should have no results but got '{results_text}'"
2972            );
2973                    assert!(
2974                search_view.query_editor.focus_handle(cx).is_focused(window),
2975                "Search view should be focused after mismatching query had been used in search",
2976            );
2977                });
2978            })
2979            .unwrap();
2980        cx.spawn(|mut cx| async move {
2981            window.update(&mut cx, |_, window, cx| {
2982                window.dispatch_action(ToggleFocus.boxed_clone(), cx)
2983            })
2984        })
2985        .detach();
2986        cx.background_executor.run_until_parked();
2987        window.update(cx, |_, window, cx| {
2988            search_view.update(cx, |search_view, cx| {
2989                    assert!(
2990                        search_view.query_editor.focus_handle(cx).is_focused(window),
2991                        "Search view with mismatching query should be focused after the toggle focus event: still no results panel to focus on",
2992                    );
2993                });
2994        }).unwrap();
2995
2996        window
2997            .update(cx, |_, window, cx| {
2998                search_view.update(cx, |search_view, cx| {
2999                    search_view.query_editor.update(cx, |query_editor, cx| {
3000                        query_editor.set_text("TWO", window, cx)
3001                    });
3002                    search_view.search(cx);
3003                })
3004            })
3005            .unwrap();
3006        cx.background_executor.run_until_parked();
3007        window.update(cx, |_, window, cx|
3008        search_view.update(cx, |search_view, cx| {
3009                assert_eq!(
3010                    search_view
3011                        .results_editor
3012                        .update(cx, |editor, cx| editor.display_text(cx)),
3013                    "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;",
3014                    "Search view results should match the query"
3015                );
3016                assert!(
3017                    search_view.results_editor.focus_handle(cx).is_focused(window),
3018                    "Search view with mismatching query should be focused after search results are available",
3019                );
3020            })).unwrap();
3021        cx.spawn(|mut cx| async move {
3022            window
3023                .update(&mut cx, |_, window, cx| {
3024                    window.dispatch_action(ToggleFocus.boxed_clone(), cx)
3025                })
3026                .unwrap();
3027        })
3028        .detach();
3029        cx.background_executor.run_until_parked();
3030        window.update(cx, |_, window, cx| {
3031            search_view.update(cx, |search_view, cx| {
3032                    assert!(
3033                        search_view.results_editor.focus_handle(cx).is_focused(window),
3034                        "Search view with matching query should still have its results editor focused after the toggle focus event",
3035                    );
3036                });
3037        }).unwrap();
3038
3039        workspace
3040            .update(cx, |workspace, window, cx| {
3041                ProjectSearchView::new_search(workspace, &workspace::NewSearch, window, cx)
3042            })
3043            .unwrap();
3044        cx.background_executor.run_until_parked();
3045        let Some(search_view_2) = cx.read(|cx| {
3046            workspace
3047                .read(cx)
3048                .unwrap()
3049                .active_pane()
3050                .read(cx)
3051                .active_item()
3052                .and_then(|item| item.downcast::<ProjectSearchView>())
3053        }) else {
3054            panic!("Search view expected to appear after new search event trigger")
3055        };
3056        assert!(
3057            search_view_2 != search_view,
3058            "New search view should be open after `workspace::NewSearch` event"
3059        );
3060
3061        window.update(cx, |_, window, cx| {
3062            search_view.update(cx, |search_view, cx| {
3063                    assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO", "First search view should not have an updated query");
3064                    assert_eq!(
3065                        search_view
3066                            .results_editor
3067                            .update(cx, |editor, cx| editor.display_text(cx)),
3068                        "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;",
3069                        "Results of the first search view should not update too"
3070                    );
3071                    assert!(
3072                        !search_view.query_editor.focus_handle(cx).is_focused(window),
3073                        "Focus should be moved away from the first search view"
3074                    );
3075                });
3076        }).unwrap();
3077
3078        window.update(cx, |_, window, cx| {
3079            search_view_2.update(cx, |search_view_2, cx| {
3080                    assert_eq!(
3081                        search_view_2.query_editor.read(cx).text(cx),
3082                        "two",
3083                        "New search view should get the query from the text cursor was at during the event spawn (first search view's first result)"
3084                    );
3085                    assert_eq!(
3086                        search_view_2
3087                            .results_editor
3088                            .update(cx, |editor, cx| editor.display_text(cx)),
3089                        "",
3090                        "No search results should be in the 2nd view yet, as we did not spawn a search for it"
3091                    );
3092                    assert!(
3093                        search_view_2.query_editor.focus_handle(cx).is_focused(window),
3094                        "Focus should be moved into query editor of the new window"
3095                    );
3096                });
3097        }).unwrap();
3098
3099        window
3100            .update(cx, |_, window, cx| {
3101                search_view_2.update(cx, |search_view_2, cx| {
3102                    search_view_2.query_editor.update(cx, |query_editor, cx| {
3103                        query_editor.set_text("FOUR", window, cx)
3104                    });
3105                    search_view_2.search(cx);
3106                });
3107            })
3108            .unwrap();
3109
3110        cx.background_executor.run_until_parked();
3111        window.update(cx, |_, window, cx| {
3112            search_view_2.update(cx, |search_view_2, cx| {
3113                    assert_eq!(
3114                        search_view_2
3115                            .results_editor
3116                            .update(cx, |editor, cx| editor.display_text(cx)),
3117                        "\n\nconst FOUR: usize = one::ONE + three::THREE;",
3118                        "New search view with the updated query should have new search results"
3119                    );
3120                    assert!(
3121                        search_view_2.results_editor.focus_handle(cx).is_focused(window),
3122                        "Search view with mismatching query should be focused after search results are available",
3123                    );
3124                });
3125        }).unwrap();
3126
3127        cx.spawn(|mut cx| async move {
3128            window
3129                .update(&mut cx, |_, window, cx| {
3130                    window.dispatch_action(ToggleFocus.boxed_clone(), cx)
3131                })
3132                .unwrap();
3133        })
3134        .detach();
3135        cx.background_executor.run_until_parked();
3136        window.update(cx, |_, window, cx| {
3137            search_view_2.update(cx, |search_view_2, cx| {
3138                    assert!(
3139                        search_view_2.results_editor.focus_handle(cx).is_focused(window),
3140                        "Search view with matching query should switch focus to the results editor after the toggle focus event",
3141                    );
3142                });}).unwrap();
3143    }
3144
3145    #[gpui::test]
3146    async fn test_new_project_search_in_directory(cx: &mut TestAppContext) {
3147        init_test(cx);
3148
3149        let fs = FakeFs::new(cx.background_executor.clone());
3150        fs.insert_tree(
3151            path!("/dir"),
3152            json!({
3153                "a": {
3154                    "one.rs": "const ONE: usize = 1;",
3155                    "two.rs": "const TWO: usize = one::ONE + one::ONE;",
3156                },
3157                "b": {
3158                    "three.rs": "const THREE: usize = one::ONE + two::TWO;",
3159                    "four.rs": "const FOUR: usize = one::ONE + three::THREE;",
3160                },
3161            }),
3162        )
3163        .await;
3164        let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3165        let worktree_id = project.read_with(cx, |project, cx| {
3166            project.worktrees(cx).next().unwrap().read(cx).id()
3167        });
3168        let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
3169        let workspace = window.root(cx).unwrap();
3170        let search_bar = window.build_entity(cx, |_, _| ProjectSearchBar::new());
3171
3172        let active_item = cx.read(|cx| {
3173            workspace
3174                .read(cx)
3175                .active_pane()
3176                .read(cx)
3177                .active_item()
3178                .and_then(|item| item.downcast::<ProjectSearchView>())
3179        });
3180        assert!(
3181            active_item.is_none(),
3182            "Expected no search panel to be active"
3183        );
3184
3185        window
3186            .update(cx, move |workspace, window, cx| {
3187                assert_eq!(workspace.panes().len(), 1);
3188                workspace.panes()[0].update(cx, move |pane, cx| {
3189                    pane.toolbar()
3190                        .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx))
3191                });
3192            })
3193            .unwrap();
3194
3195        let a_dir_entry = cx.update(|cx| {
3196            workspace
3197                .read(cx)
3198                .project()
3199                .read(cx)
3200                .entry_for_path(&(worktree_id, rel_path("a")).into(), cx)
3201                .expect("no entry for /a/ directory")
3202                .clone()
3203        });
3204        assert!(a_dir_entry.is_dir());
3205        window
3206            .update(cx, |workspace, window, cx| {
3207                ProjectSearchView::new_search_in_directory(workspace, &a_dir_entry.path, window, cx)
3208            })
3209            .unwrap();
3210
3211        let Some(search_view) = cx.read(|cx| {
3212            workspace
3213                .read(cx)
3214                .active_pane()
3215                .read(cx)
3216                .active_item()
3217                .and_then(|item| item.downcast::<ProjectSearchView>())
3218        }) else {
3219            panic!("Search view expected to appear after new search in directory event trigger")
3220        };
3221        cx.background_executor.run_until_parked();
3222        window
3223            .update(cx, |_, window, cx| {
3224                search_view.update(cx, |search_view, cx| {
3225                    assert!(
3226                        search_view.query_editor.focus_handle(cx).is_focused(window),
3227                        "On new search in directory, focus should be moved into query editor"
3228                    );
3229                    search_view.excluded_files_editor.update(cx, |editor, cx| {
3230                        assert!(
3231                            editor.display_text(cx).is_empty(),
3232                            "New search in directory should not have any excluded files"
3233                        );
3234                    });
3235                    search_view.included_files_editor.update(cx, |editor, cx| {
3236                        assert_eq!(
3237                            editor.display_text(cx),
3238                            a_dir_entry.path.display(PathStyle::local()),
3239                            "New search in directory should have included dir entry path"
3240                        );
3241                    });
3242                });
3243            })
3244            .unwrap();
3245        window
3246            .update(cx, |_, window, cx| {
3247                search_view.update(cx, |search_view, cx| {
3248                    search_view.query_editor.update(cx, |query_editor, cx| {
3249                        query_editor.set_text("const", window, cx)
3250                    });
3251                    search_view.search(cx);
3252                });
3253            })
3254            .unwrap();
3255        cx.background_executor.run_until_parked();
3256        window
3257            .update(cx, |_, _, cx| {
3258                search_view.update(cx, |search_view, cx| {
3259                    assert_eq!(
3260                search_view
3261                    .results_editor
3262                    .update(cx, |editor, cx| editor.display_text(cx)),
3263                "\n\nconst ONE: usize = 1;\n\n\nconst TWO: usize = one::ONE + one::ONE;",
3264                "New search in directory should have a filter that matches a certain directory"
3265            );
3266                })
3267            })
3268            .unwrap();
3269    }
3270
3271    #[gpui::test]
3272    async fn test_search_query_history(cx: &mut TestAppContext) {
3273        init_test(cx);
3274
3275        let fs = FakeFs::new(cx.background_executor.clone());
3276        fs.insert_tree(
3277            path!("/dir"),
3278            json!({
3279                "one.rs": "const ONE: usize = 1;",
3280                "two.rs": "const TWO: usize = one::ONE + one::ONE;",
3281                "three.rs": "const THREE: usize = one::ONE + two::TWO;",
3282                "four.rs": "const FOUR: usize = one::ONE + three::THREE;",
3283            }),
3284        )
3285        .await;
3286        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
3287        let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
3288        let workspace = window.root(cx).unwrap();
3289        let search_bar = window.build_entity(cx, |_, _| ProjectSearchBar::new());
3290
3291        window
3292            .update(cx, {
3293                let search_bar = search_bar.clone();
3294                |workspace, window, cx| {
3295                    assert_eq!(workspace.panes().len(), 1);
3296                    workspace.panes()[0].update(cx, |pane, cx| {
3297                        pane.toolbar()
3298                            .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx))
3299                    });
3300
3301                    ProjectSearchView::new_search(workspace, &workspace::NewSearch, window, cx)
3302                }
3303            })
3304            .unwrap();
3305
3306        let search_view = cx.read(|cx| {
3307            workspace
3308                .read(cx)
3309                .active_pane()
3310                .read(cx)
3311                .active_item()
3312                .and_then(|item| item.downcast::<ProjectSearchView>())
3313                .expect("Search view expected to appear after new search event trigger")
3314        });
3315
3316        // Add 3 search items into the history + another unsubmitted one.
3317        window
3318            .update(cx, |_, window, cx| {
3319                search_view.update(cx, |search_view, cx| {
3320                    search_view.search_options = SearchOptions::CASE_SENSITIVE;
3321                    search_view.query_editor.update(cx, |query_editor, cx| {
3322                        query_editor.set_text("ONE", window, cx)
3323                    });
3324                    search_view.search(cx);
3325                });
3326            })
3327            .unwrap();
3328
3329        cx.background_executor.run_until_parked();
3330        window
3331            .update(cx, |_, window, cx| {
3332                search_view.update(cx, |search_view, cx| {
3333                    search_view.query_editor.update(cx, |query_editor, cx| {
3334                        query_editor.set_text("TWO", window, cx)
3335                    });
3336                    search_view.search(cx);
3337                });
3338            })
3339            .unwrap();
3340        cx.background_executor.run_until_parked();
3341        window
3342            .update(cx, |_, window, cx| {
3343                search_view.update(cx, |search_view, cx| {
3344                    search_view.query_editor.update(cx, |query_editor, cx| {
3345                        query_editor.set_text("THREE", window, cx)
3346                    });
3347                    search_view.search(cx);
3348                })
3349            })
3350            .unwrap();
3351        cx.background_executor.run_until_parked();
3352        window
3353            .update(cx, |_, window, cx| {
3354                search_view.update(cx, |search_view, cx| {
3355                    search_view.query_editor.update(cx, |query_editor, cx| {
3356                        query_editor.set_text("JUST_TEXT_INPUT", window, cx)
3357                    });
3358                })
3359            })
3360            .unwrap();
3361        cx.background_executor.run_until_parked();
3362
3363        // Ensure that the latest input with search settings is active.
3364        window
3365            .update(cx, |_, _, cx| {
3366                search_view.update(cx, |search_view, cx| {
3367                    assert_eq!(
3368                        search_view.query_editor.read(cx).text(cx),
3369                        "JUST_TEXT_INPUT"
3370                    );
3371                    assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
3372                });
3373            })
3374            .unwrap();
3375
3376        // Next history query after the latest should set the query to the empty string.
3377        window
3378            .update(cx, |_, window, cx| {
3379                search_bar.update(cx, |search_bar, cx| {
3380                    search_bar.focus_search(window, cx);
3381                    search_bar.next_history_query(&NextHistoryQuery, window, cx);
3382                })
3383            })
3384            .unwrap();
3385        window
3386            .update(cx, |_, _, cx| {
3387                search_view.update(cx, |search_view, cx| {
3388                    assert_eq!(search_view.query_editor.read(cx).text(cx), "");
3389                    assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
3390                });
3391            })
3392            .unwrap();
3393        window
3394            .update(cx, |_, window, cx| {
3395                search_bar.update(cx, |search_bar, cx| {
3396                    search_bar.focus_search(window, cx);
3397                    search_bar.next_history_query(&NextHistoryQuery, window, cx);
3398                })
3399            })
3400            .unwrap();
3401        window
3402            .update(cx, |_, _, cx| {
3403                search_view.update(cx, |search_view, cx| {
3404                    assert_eq!(search_view.query_editor.read(cx).text(cx), "");
3405                    assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
3406                });
3407            })
3408            .unwrap();
3409
3410        // First previous query for empty current query should set the query to the latest submitted one.
3411        window
3412            .update(cx, |_, window, cx| {
3413                search_bar.update(cx, |search_bar, cx| {
3414                    search_bar.focus_search(window, cx);
3415                    search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
3416                });
3417            })
3418            .unwrap();
3419        window
3420            .update(cx, |_, _, cx| {
3421                search_view.update(cx, |search_view, cx| {
3422                    assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE");
3423                    assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
3424                });
3425            })
3426            .unwrap();
3427
3428        // Further previous items should go over the history in reverse order.
3429        window
3430            .update(cx, |_, window, cx| {
3431                search_bar.update(cx, |search_bar, cx| {
3432                    search_bar.focus_search(window, cx);
3433                    search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
3434                });
3435            })
3436            .unwrap();
3437        window
3438            .update(cx, |_, _, cx| {
3439                search_view.update(cx, |search_view, cx| {
3440                    assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO");
3441                    assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
3442                });
3443            })
3444            .unwrap();
3445
3446        // Previous items should never go behind the first history item.
3447        window
3448            .update(cx, |_, window, cx| {
3449                search_bar.update(cx, |search_bar, cx| {
3450                    search_bar.focus_search(window, cx);
3451                    search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
3452                });
3453            })
3454            .unwrap();
3455        window
3456            .update(cx, |_, _, cx| {
3457                search_view.update(cx, |search_view, cx| {
3458                    assert_eq!(search_view.query_editor.read(cx).text(cx), "ONE");
3459                    assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
3460                });
3461            })
3462            .unwrap();
3463        window
3464            .update(cx, |_, window, cx| {
3465                search_bar.update(cx, |search_bar, cx| {
3466                    search_bar.focus_search(window, cx);
3467                    search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
3468                });
3469            })
3470            .unwrap();
3471        window
3472            .update(cx, |_, _, cx| {
3473                search_view.update(cx, |search_view, cx| {
3474                    assert_eq!(search_view.query_editor.read(cx).text(cx), "ONE");
3475                    assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
3476                });
3477            })
3478            .unwrap();
3479
3480        // Next items should go over the history in the original order.
3481        window
3482            .update(cx, |_, window, cx| {
3483                search_bar.update(cx, |search_bar, cx| {
3484                    search_bar.focus_search(window, cx);
3485                    search_bar.next_history_query(&NextHistoryQuery, window, cx);
3486                });
3487            })
3488            .unwrap();
3489        window
3490            .update(cx, |_, _, cx| {
3491                search_view.update(cx, |search_view, cx| {
3492                    assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO");
3493                    assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
3494                });
3495            })
3496            .unwrap();
3497
3498        window
3499            .update(cx, |_, window, cx| {
3500                search_view.update(cx, |search_view, cx| {
3501                    search_view.query_editor.update(cx, |query_editor, cx| {
3502                        query_editor.set_text("TWO_NEW", window, cx)
3503                    });
3504                    search_view.search(cx);
3505                });
3506            })
3507            .unwrap();
3508        cx.background_executor.run_until_parked();
3509        window
3510            .update(cx, |_, _, cx| {
3511                search_view.update(cx, |search_view, cx| {
3512                    assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO_NEW");
3513                    assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
3514                });
3515            })
3516            .unwrap();
3517
3518        // New search input should add another entry to history and move the selection to the end of the history.
3519        window
3520            .update(cx, |_, window, cx| {
3521                search_bar.update(cx, |search_bar, cx| {
3522                    search_bar.focus_search(window, cx);
3523                    search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
3524                });
3525            })
3526            .unwrap();
3527        window
3528            .update(cx, |_, _, cx| {
3529                search_view.update(cx, |search_view, cx| {
3530                    assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE");
3531                    assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
3532                });
3533            })
3534            .unwrap();
3535        window
3536            .update(cx, |_, window, cx| {
3537                search_bar.update(cx, |search_bar, cx| {
3538                    search_bar.focus_search(window, cx);
3539                    search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
3540                });
3541            })
3542            .unwrap();
3543        window
3544            .update(cx, |_, _, cx| {
3545                search_view.update(cx, |search_view, cx| {
3546                    assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO");
3547                    assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
3548                });
3549            })
3550            .unwrap();
3551        window
3552            .update(cx, |_, window, cx| {
3553                search_bar.update(cx, |search_bar, cx| {
3554                    search_bar.focus_search(window, cx);
3555                    search_bar.next_history_query(&NextHistoryQuery, window, cx);
3556                });
3557            })
3558            .unwrap();
3559        window
3560            .update(cx, |_, _, cx| {
3561                search_view.update(cx, |search_view, cx| {
3562                    assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE");
3563                    assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
3564                });
3565            })
3566            .unwrap();
3567        window
3568            .update(cx, |_, window, cx| {
3569                search_bar.update(cx, |search_bar, cx| {
3570                    search_bar.focus_search(window, cx);
3571                    search_bar.next_history_query(&NextHistoryQuery, window, cx);
3572                });
3573            })
3574            .unwrap();
3575        window
3576            .update(cx, |_, _, cx| {
3577                search_view.update(cx, |search_view, cx| {
3578                    assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO_NEW");
3579                    assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
3580                });
3581            })
3582            .unwrap();
3583        window
3584            .update(cx, |_, window, cx| {
3585                search_bar.update(cx, |search_bar, cx| {
3586                    search_bar.focus_search(window, cx);
3587                    search_bar.next_history_query(&NextHistoryQuery, window, cx);
3588                });
3589            })
3590            .unwrap();
3591        window
3592            .update(cx, |_, _, cx| {
3593                search_view.update(cx, |search_view, cx| {
3594                    assert_eq!(search_view.query_editor.read(cx).text(cx), "");
3595                    assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
3596                });
3597            })
3598            .unwrap();
3599    }
3600
3601    #[gpui::test]
3602    async fn test_search_query_history_with_multiple_views(cx: &mut TestAppContext) {
3603        init_test(cx);
3604
3605        let fs = FakeFs::new(cx.background_executor.clone());
3606        fs.insert_tree(
3607            path!("/dir"),
3608            json!({
3609                "one.rs": "const ONE: usize = 1;",
3610            }),
3611        )
3612        .await;
3613        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
3614        let worktree_id = project.update(cx, |this, cx| {
3615            this.worktrees(cx).next().unwrap().read(cx).id()
3616        });
3617
3618        let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
3619        let workspace = window.root(cx).unwrap();
3620
3621        let panes: Vec<_> = window
3622            .update(cx, |this, _, _| this.panes().to_owned())
3623            .unwrap();
3624
3625        let search_bar_1 = window.build_entity(cx, |_, _| ProjectSearchBar::new());
3626        let search_bar_2 = window.build_entity(cx, |_, _| ProjectSearchBar::new());
3627
3628        assert_eq!(panes.len(), 1);
3629        let first_pane = panes.first().cloned().unwrap();
3630        assert_eq!(cx.update(|cx| first_pane.read(cx).items_len()), 0);
3631        window
3632            .update(cx, |workspace, window, cx| {
3633                workspace.open_path(
3634                    (worktree_id, rel_path("one.rs")),
3635                    Some(first_pane.downgrade()),
3636                    true,
3637                    window,
3638                    cx,
3639                )
3640            })
3641            .unwrap()
3642            .await
3643            .unwrap();
3644        assert_eq!(cx.update(|cx| first_pane.read(cx).items_len()), 1);
3645
3646        // Add a project search item to the first pane
3647        window
3648            .update(cx, {
3649                let search_bar = search_bar_1.clone();
3650                |workspace, window, cx| {
3651                    first_pane.update(cx, |pane, cx| {
3652                        pane.toolbar()
3653                            .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx))
3654                    });
3655
3656                    ProjectSearchView::new_search(workspace, &workspace::NewSearch, window, cx)
3657                }
3658            })
3659            .unwrap();
3660        let search_view_1 = cx.read(|cx| {
3661            workspace
3662                .read(cx)
3663                .active_item(cx)
3664                .and_then(|item| item.downcast::<ProjectSearchView>())
3665                .expect("Search view expected to appear after new search event trigger")
3666        });
3667
3668        let second_pane = window
3669            .update(cx, |workspace, window, cx| {
3670                workspace.split_and_clone(
3671                    first_pane.clone(),
3672                    workspace::SplitDirection::Right,
3673                    window,
3674                    cx,
3675                )
3676            })
3677            .unwrap()
3678            .unwrap();
3679        assert_eq!(cx.update(|cx| second_pane.read(cx).items_len()), 1);
3680
3681        assert_eq!(cx.update(|cx| second_pane.read(cx).items_len()), 1);
3682        assert_eq!(cx.update(|cx| first_pane.read(cx).items_len()), 2);
3683
3684        // Add a project search item to the second pane
3685        window
3686            .update(cx, {
3687                let search_bar = search_bar_2.clone();
3688                let pane = second_pane.clone();
3689                move |workspace, window, cx| {
3690                    assert_eq!(workspace.panes().len(), 2);
3691                    pane.update(cx, |pane, cx| {
3692                        pane.toolbar()
3693                            .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx))
3694                    });
3695
3696                    ProjectSearchView::new_search(workspace, &workspace::NewSearch, window, cx)
3697                }
3698            })
3699            .unwrap();
3700
3701        let search_view_2 = cx.read(|cx| {
3702            workspace
3703                .read(cx)
3704                .active_item(cx)
3705                .and_then(|item| item.downcast::<ProjectSearchView>())
3706                .expect("Search view expected to appear after new search event trigger")
3707        });
3708
3709        cx.run_until_parked();
3710        assert_eq!(cx.update(|cx| first_pane.read(cx).items_len()), 2);
3711        assert_eq!(cx.update(|cx| second_pane.read(cx).items_len()), 2);
3712
3713        let update_search_view =
3714            |search_view: &Entity<ProjectSearchView>, query: &str, cx: &mut TestAppContext| {
3715                window
3716                    .update(cx, |_, window, cx| {
3717                        search_view.update(cx, |search_view, cx| {
3718                            search_view.query_editor.update(cx, |query_editor, cx| {
3719                                query_editor.set_text(query, window, cx)
3720                            });
3721                            search_view.search(cx);
3722                        });
3723                    })
3724                    .unwrap();
3725            };
3726
3727        let active_query =
3728            |search_view: &Entity<ProjectSearchView>, cx: &mut TestAppContext| -> String {
3729                window
3730                    .update(cx, |_, _, cx| {
3731                        search_view.update(cx, |search_view, cx| {
3732                            search_view.query_editor.read(cx).text(cx)
3733                        })
3734                    })
3735                    .unwrap()
3736            };
3737
3738        let select_prev_history_item =
3739            |search_bar: &Entity<ProjectSearchBar>, cx: &mut TestAppContext| {
3740                window
3741                    .update(cx, |_, window, cx| {
3742                        search_bar.update(cx, |search_bar, cx| {
3743                            search_bar.focus_search(window, cx);
3744                            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
3745                        })
3746                    })
3747                    .unwrap();
3748            };
3749
3750        let select_next_history_item =
3751            |search_bar: &Entity<ProjectSearchBar>, cx: &mut TestAppContext| {
3752                window
3753                    .update(cx, |_, window, cx| {
3754                        search_bar.update(cx, |search_bar, cx| {
3755                            search_bar.focus_search(window, cx);
3756                            search_bar.next_history_query(&NextHistoryQuery, window, cx);
3757                        })
3758                    })
3759                    .unwrap();
3760            };
3761
3762        update_search_view(&search_view_1, "ONE", cx);
3763        cx.background_executor.run_until_parked();
3764
3765        update_search_view(&search_view_2, "TWO", cx);
3766        cx.background_executor.run_until_parked();
3767
3768        assert_eq!(active_query(&search_view_1, cx), "ONE");
3769        assert_eq!(active_query(&search_view_2, cx), "TWO");
3770
3771        // Selecting previous history item should select the query from search view 1.
3772        select_prev_history_item(&search_bar_2, cx);
3773        assert_eq!(active_query(&search_view_2, cx), "ONE");
3774
3775        // Selecting the previous history item should not change the query as it is already the first item.
3776        select_prev_history_item(&search_bar_2, cx);
3777        assert_eq!(active_query(&search_view_2, cx), "ONE");
3778
3779        // Changing the query in search view 2 should not affect the history of search view 1.
3780        assert_eq!(active_query(&search_view_1, cx), "ONE");
3781
3782        // Deploying a new search in search view 2
3783        update_search_view(&search_view_2, "THREE", cx);
3784        cx.background_executor.run_until_parked();
3785
3786        select_next_history_item(&search_bar_2, cx);
3787        assert_eq!(active_query(&search_view_2, cx), "");
3788
3789        select_prev_history_item(&search_bar_2, cx);
3790        assert_eq!(active_query(&search_view_2, cx), "THREE");
3791
3792        select_prev_history_item(&search_bar_2, cx);
3793        assert_eq!(active_query(&search_view_2, cx), "TWO");
3794
3795        select_prev_history_item(&search_bar_2, cx);
3796        assert_eq!(active_query(&search_view_2, cx), "ONE");
3797
3798        select_prev_history_item(&search_bar_2, cx);
3799        assert_eq!(active_query(&search_view_2, cx), "ONE");
3800
3801        // Search view 1 should now see the query from search view 2.
3802        assert_eq!(active_query(&search_view_1, cx), "ONE");
3803
3804        select_next_history_item(&search_bar_2, cx);
3805        assert_eq!(active_query(&search_view_2, cx), "TWO");
3806
3807        // Here is the new query from search view 2
3808        select_next_history_item(&search_bar_2, cx);
3809        assert_eq!(active_query(&search_view_2, cx), "THREE");
3810
3811        select_next_history_item(&search_bar_2, cx);
3812        assert_eq!(active_query(&search_view_2, cx), "");
3813
3814        select_next_history_item(&search_bar_1, cx);
3815        assert_eq!(active_query(&search_view_1, cx), "TWO");
3816
3817        select_next_history_item(&search_bar_1, cx);
3818        assert_eq!(active_query(&search_view_1, cx), "THREE");
3819
3820        select_next_history_item(&search_bar_1, cx);
3821        assert_eq!(active_query(&search_view_1, cx), "");
3822    }
3823
3824    #[gpui::test]
3825    async fn test_deploy_search_with_multiple_panes(cx: &mut TestAppContext) {
3826        init_test(cx);
3827
3828        // Setup 2 panes, both with a file open and one with a project search.
3829        let fs = FakeFs::new(cx.background_executor.clone());
3830        fs.insert_tree(
3831            path!("/dir"),
3832            json!({
3833                "one.rs": "const ONE: usize = 1;",
3834            }),
3835        )
3836        .await;
3837        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
3838        let worktree_id = project.update(cx, |this, cx| {
3839            this.worktrees(cx).next().unwrap().read(cx).id()
3840        });
3841        let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
3842        let panes: Vec<_> = window
3843            .update(cx, |this, _, _| this.panes().to_owned())
3844            .unwrap();
3845        assert_eq!(panes.len(), 1);
3846        let first_pane = panes.first().cloned().unwrap();
3847        assert_eq!(cx.update(|cx| first_pane.read(cx).items_len()), 0);
3848        window
3849            .update(cx, |workspace, window, cx| {
3850                workspace.open_path(
3851                    (worktree_id, rel_path("one.rs")),
3852                    Some(first_pane.downgrade()),
3853                    true,
3854                    window,
3855                    cx,
3856                )
3857            })
3858            .unwrap()
3859            .await
3860            .unwrap();
3861        assert_eq!(cx.update(|cx| first_pane.read(cx).items_len()), 1);
3862        let second_pane = window
3863            .update(cx, |workspace, window, cx| {
3864                workspace.split_and_clone(
3865                    first_pane.clone(),
3866                    workspace::SplitDirection::Right,
3867                    window,
3868                    cx,
3869                )
3870            })
3871            .unwrap()
3872            .unwrap();
3873        assert_eq!(cx.update(|cx| second_pane.read(cx).items_len()), 1);
3874        assert!(
3875            window
3876                .update(cx, |_, window, cx| second_pane
3877                    .focus_handle(cx)
3878                    .contains_focused(window, cx))
3879                .unwrap()
3880        );
3881        let search_bar = window.build_entity(cx, |_, _| ProjectSearchBar::new());
3882        window
3883            .update(cx, {
3884                let search_bar = search_bar.clone();
3885                let pane = first_pane.clone();
3886                move |workspace, window, cx| {
3887                    assert_eq!(workspace.panes().len(), 2);
3888                    pane.update(cx, move |pane, cx| {
3889                        pane.toolbar()
3890                            .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx))
3891                    });
3892                }
3893            })
3894            .unwrap();
3895
3896        // Add a project search item to the second pane
3897        window
3898            .update(cx, {
3899                |workspace, window, cx| {
3900                    assert_eq!(workspace.panes().len(), 2);
3901                    second_pane.update(cx, |pane, cx| {
3902                        pane.toolbar()
3903                            .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx))
3904                    });
3905
3906                    ProjectSearchView::new_search(workspace, &workspace::NewSearch, window, cx)
3907                }
3908            })
3909            .unwrap();
3910
3911        cx.run_until_parked();
3912        assert_eq!(cx.update(|cx| second_pane.read(cx).items_len()), 2);
3913        assert_eq!(cx.update(|cx| first_pane.read(cx).items_len()), 1);
3914
3915        // Focus the first pane
3916        window
3917            .update(cx, |workspace, window, cx| {
3918                assert_eq!(workspace.active_pane(), &second_pane);
3919                second_pane.update(cx, |this, cx| {
3920                    assert_eq!(this.active_item_index(), 1);
3921                    this.activate_previous_item(&Default::default(), window, cx);
3922                    assert_eq!(this.active_item_index(), 0);
3923                });
3924                workspace.activate_pane_in_direction(workspace::SplitDirection::Left, window, cx);
3925            })
3926            .unwrap();
3927        window
3928            .update(cx, |workspace, _, cx| {
3929                assert_eq!(workspace.active_pane(), &first_pane);
3930                assert_eq!(first_pane.read(cx).items_len(), 1);
3931                assert_eq!(second_pane.read(cx).items_len(), 2);
3932            })
3933            .unwrap();
3934
3935        // Deploy a new search
3936        cx.dispatch_action(window.into(), DeploySearch::find());
3937
3938        // Both panes should now have a project search in them
3939        window
3940            .update(cx, |workspace, window, cx| {
3941                assert_eq!(workspace.active_pane(), &first_pane);
3942                first_pane.read_with(cx, |this, _| {
3943                    assert_eq!(this.active_item_index(), 1);
3944                    assert_eq!(this.items_len(), 2);
3945                });
3946                second_pane.update(cx, |this, cx| {
3947                    assert!(!cx.focus_handle().contains_focused(window, cx));
3948                    assert_eq!(this.items_len(), 2);
3949                });
3950            })
3951            .unwrap();
3952
3953        // Focus the second pane's non-search item
3954        window
3955            .update(cx, |_workspace, window, cx| {
3956                second_pane.update(cx, |pane, cx| {
3957                    pane.activate_next_item(&Default::default(), window, cx)
3958                });
3959            })
3960            .unwrap();
3961
3962        // Deploy a new search
3963        cx.dispatch_action(window.into(), DeploySearch::find());
3964
3965        // The project search view should now be focused in the second pane
3966        // And the number of items should be unchanged.
3967        window
3968            .update(cx, |_workspace, _, cx| {
3969                second_pane.update(cx, |pane, _cx| {
3970                    assert!(
3971                        pane.active_item()
3972                            .unwrap()
3973                            .downcast::<ProjectSearchView>()
3974                            .is_some()
3975                    );
3976
3977                    assert_eq!(pane.items_len(), 2);
3978                });
3979            })
3980            .unwrap();
3981    }
3982
3983    #[gpui::test]
3984    async fn test_scroll_search_results_to_top(cx: &mut TestAppContext) {
3985        init_test(cx);
3986
3987        // We need many lines in the search results to be able to scroll the window
3988        let fs = FakeFs::new(cx.background_executor.clone());
3989        fs.insert_tree(
3990            path!("/dir"),
3991            json!({
3992                "1.txt": "\n\n\n\n\n A \n\n\n\n\n",
3993                "2.txt": "\n\n\n\n\n A \n\n\n\n\n",
3994                "3.rs": "\n\n\n\n\n A \n\n\n\n\n",
3995                "4.rs": "\n\n\n\n\n A \n\n\n\n\n",
3996                "5.rs": "\n\n\n\n\n A \n\n\n\n\n",
3997                "6.rs": "\n\n\n\n\n A \n\n\n\n\n",
3998                "7.rs": "\n\n\n\n\n A \n\n\n\n\n",
3999                "8.rs": "\n\n\n\n\n A \n\n\n\n\n",
4000                "9.rs": "\n\n\n\n\n A \n\n\n\n\n",
4001                "a.rs": "\n\n\n\n\n A \n\n\n\n\n",
4002                "b.rs": "\n\n\n\n\n B \n\n\n\n\n",
4003                "c.rs": "\n\n\n\n\n B \n\n\n\n\n",
4004                "d.rs": "\n\n\n\n\n B \n\n\n\n\n",
4005                "e.rs": "\n\n\n\n\n B \n\n\n\n\n",
4006                "f.rs": "\n\n\n\n\n B \n\n\n\n\n",
4007                "g.rs": "\n\n\n\n\n B \n\n\n\n\n",
4008                "h.rs": "\n\n\n\n\n B \n\n\n\n\n",
4009                "i.rs": "\n\n\n\n\n B \n\n\n\n\n",
4010                "j.rs": "\n\n\n\n\n B \n\n\n\n\n",
4011                "k.rs": "\n\n\n\n\n B \n\n\n\n\n",
4012            }),
4013        )
4014        .await;
4015        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
4016        let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4017        let workspace = window.root(cx).unwrap();
4018        let search = cx.new(|cx| ProjectSearch::new(project, cx));
4019        let search_view = cx.add_window(|window, cx| {
4020            ProjectSearchView::new(workspace.downgrade(), search.clone(), window, cx, None)
4021        });
4022
4023        // First search
4024        perform_search(search_view, "A", cx);
4025        search_view
4026            .update(cx, |search_view, window, cx| {
4027                search_view.results_editor.update(cx, |results_editor, cx| {
4028                    // Results are correct and scrolled to the top
4029                    assert_eq!(
4030                        results_editor.display_text(cx).match_indices(" A ").count(),
4031                        10
4032                    );
4033                    assert_eq!(results_editor.scroll_position(cx), Point::default());
4034
4035                    // Scroll results all the way down
4036                    results_editor.scroll(
4037                        Point::new(0., f64::MAX),
4038                        Some(Axis::Vertical),
4039                        window,
4040                        cx,
4041                    );
4042                });
4043            })
4044            .expect("unable to update search view");
4045
4046        // Second search
4047        perform_search(search_view, "B", cx);
4048        search_view
4049            .update(cx, |search_view, _, cx| {
4050                search_view.results_editor.update(cx, |results_editor, cx| {
4051                    // Results are correct...
4052                    assert_eq!(
4053                        results_editor.display_text(cx).match_indices(" B ").count(),
4054                        10
4055                    );
4056                    // ...and scrolled back to the top
4057                    assert_eq!(results_editor.scroll_position(cx), Point::default());
4058                });
4059            })
4060            .expect("unable to update search view");
4061    }
4062
4063    #[gpui::test]
4064    async fn test_buffer_search_query_reused(cx: &mut TestAppContext) {
4065        init_test(cx);
4066
4067        let fs = FakeFs::new(cx.background_executor.clone());
4068        fs.insert_tree(
4069            path!("/dir"),
4070            json!({
4071                "one.rs": "const ONE: usize = 1;",
4072            }),
4073        )
4074        .await;
4075        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
4076        let worktree_id = project.update(cx, |this, cx| {
4077            this.worktrees(cx).next().unwrap().read(cx).id()
4078        });
4079        let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4080        let workspace = window.root(cx).unwrap();
4081        let mut cx = VisualTestContext::from_window(*window.deref(), cx);
4082
4083        let editor = workspace
4084            .update_in(&mut cx, |workspace, window, cx| {
4085                workspace.open_path((worktree_id, rel_path("one.rs")), None, true, window, cx)
4086            })
4087            .await
4088            .unwrap()
4089            .downcast::<Editor>()
4090            .unwrap();
4091
4092        // Wait for the unstaged changes to be loaded
4093        cx.run_until_parked();
4094
4095        let buffer_search_bar = cx.new_window_entity(|window, cx| {
4096            let mut search_bar =
4097                BufferSearchBar::new(Some(project.read(cx).languages().clone()), window, cx);
4098            search_bar.set_active_pane_item(Some(&editor), window, cx);
4099            search_bar.show(window, cx);
4100            search_bar
4101        });
4102
4103        let panes: Vec<_> = window
4104            .update(&mut cx, |this, _, _| this.panes().to_owned())
4105            .unwrap();
4106        assert_eq!(panes.len(), 1);
4107        let pane = panes.first().cloned().unwrap();
4108        pane.update_in(&mut cx, |pane, window, cx| {
4109            pane.toolbar().update(cx, |toolbar, cx| {
4110                toolbar.add_item(buffer_search_bar.clone(), window, cx);
4111            })
4112        });
4113
4114        let buffer_search_query = "search bar query";
4115        buffer_search_bar
4116            .update_in(&mut cx, |buffer_search_bar, window, cx| {
4117                buffer_search_bar.focus_handle(cx).focus(window);
4118                buffer_search_bar.search(buffer_search_query, None, true, window, cx)
4119            })
4120            .await
4121            .unwrap();
4122
4123        workspace.update_in(&mut cx, |workspace, window, cx| {
4124            ProjectSearchView::new_search(workspace, &workspace::NewSearch, window, cx)
4125        });
4126        cx.run_until_parked();
4127        let project_search_view = pane
4128            .read_with(&cx, |pane, _| {
4129                pane.active_item()
4130                    .and_then(|item| item.downcast::<ProjectSearchView>())
4131            })
4132            .expect("should open a project search view after spawning a new search");
4133        project_search_view.update(&mut cx, |search_view, cx| {
4134            assert_eq!(
4135                search_view.search_query_text(cx),
4136                buffer_search_query,
4137                "Project search should take the query from the buffer search bar since it got focused and had a query inside"
4138            );
4139        });
4140    }
4141
4142    fn init_test(cx: &mut TestAppContext) {
4143        cx.update(|cx| {
4144            let settings = SettingsStore::test(cx);
4145            cx.set_global(settings);
4146
4147            theme::init(theme::LoadThemes::JustBase, cx);
4148
4149            language::init(cx);
4150            client::init_settings(cx);
4151            editor::init(cx);
4152            workspace::init_settings(cx);
4153            Project::init_settings(cx);
4154            crate::init(cx);
4155        });
4156    }
4157
4158    fn perform_search(
4159        search_view: WindowHandle<ProjectSearchView>,
4160        text: impl Into<Arc<str>>,
4161        cx: &mut TestAppContext,
4162    ) {
4163        search_view
4164            .update(cx, |search_view, window, cx| {
4165                search_view.query_editor.update(cx, |query_editor, cx| {
4166                    query_editor.set_text(text, window, cx)
4167                });
4168                search_view.search(cx);
4169            })
4170            .unwrap();
4171        // Ensure editor highlights appear after the search is done
4172        cx.executor().advance_clock(
4173            editor::SELECTION_HIGHLIGHT_DEBOUNCE_TIMEOUT + Duration::from_millis(100),
4174        );
4175        cx.background_executor.run_until_parked();
4176    }
4177}