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