project_search.rs

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