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