project_search.rs

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