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