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