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