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