project_panel.rs

   1mod project_panel_settings;
   2mod utils;
   3
   4use anyhow::{anyhow, Context as _, Result};
   5use client::{ErrorCode, ErrorExt};
   6use collections::{hash_map, BTreeSet, HashMap};
   7use command_palette_hooks::CommandPaletteFilter;
   8use db::kvp::KEY_VALUE_STORE;
   9use editor::{
  10    items::{
  11        entry_diagnostic_aware_icon_decoration_and_color,
  12        entry_diagnostic_aware_icon_name_and_color, entry_git_aware_label_color,
  13    },
  14    scroll::{Autoscroll, ScrollbarAutoHide},
  15    Editor, EditorEvent, EditorSettings, ShowScrollbar,
  16};
  17use file_icons::FileIcons;
  18use git::status::GitSummary;
  19use gpui::{
  20    actions, anchored, deferred, div, impl_actions, point, px, size, uniform_list, Action,
  21    AnyElement, App, ArcCow, AsyncWindowContext, Bounds, ClipboardItem, Context, DismissEvent, Div,
  22    DragMoveEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, Hsla,
  23    InteractiveElement, KeyContext, ListHorizontalSizingBehavior, ListSizingBehavior, MouseButton,
  24    MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render, ScrollStrategy, Stateful,
  25    Styled, Subscription, Task, UniformListScrollHandle, WeakEntity, Window,
  26};
  27use indexmap::IndexMap;
  28use language::DiagnosticSeverity;
  29use menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrevious};
  30use project::{
  31    relativize_path, Entry, EntryKind, Fs, Project, ProjectEntryId, ProjectPath, Worktree,
  32    WorktreeId,
  33};
  34use project_panel_settings::{
  35    ProjectPanelDockPosition, ProjectPanelSettings, ShowDiagnostics, ShowIndentGuides,
  36};
  37use schemars::JsonSchema;
  38use serde::{Deserialize, Serialize};
  39use settings::{Settings, SettingsStore};
  40use smallvec::SmallVec;
  41use std::any::TypeId;
  42use std::{
  43    cell::OnceCell,
  44    cmp,
  45    collections::HashSet,
  46    ffi::OsStr,
  47    ops::Range,
  48    path::{Path, PathBuf},
  49    sync::Arc,
  50    time::Duration,
  51};
  52use theme::ThemeSettings;
  53use ui::{
  54    prelude::*, v_flex, ContextMenu, DecoratedIcon, Icon, IconDecoration, IconDecorationKind,
  55    IndentGuideColors, IndentGuideLayout, KeyBinding, Label, ListItem, ListItemSpacing, Scrollbar,
  56    ScrollbarState, Tooltip,
  57};
  58use util::{maybe, paths::compare_paths, ResultExt, TakeUntilExt, TryFutureExt};
  59use workspace::{
  60    dock::{DockPosition, Panel, PanelEvent},
  61    notifications::{DetachAndPromptErr, NotifyTaskExt},
  62    DraggedSelection, OpenInTerminal, OpenOptions, OpenVisible, PreviewTabsSettings, SelectedEntry,
  63    Workspace,
  64};
  65use worktree::{CreatedEntry, GitEntry, GitEntryRef};
  66
  67const PROJECT_PANEL_KEY: &str = "ProjectPanel";
  68const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX;
  69
  70pub struct ProjectPanel {
  71    project: Entity<Project>,
  72    fs: Arc<dyn Fs>,
  73    focus_handle: FocusHandle,
  74    scroll_handle: UniformListScrollHandle,
  75    // An update loop that keeps incrementing/decrementing scroll offset while there is a dragged entry that's
  76    // hovered over the start/end of a list.
  77    hover_scroll_task: Option<Task<()>>,
  78    visible_entries: Vec<(WorktreeId, Vec<GitEntry>, OnceCell<HashSet<Arc<Path>>>)>,
  79    /// Maps from leaf project entry ID to the currently selected ancestor.
  80    /// Relevant only for auto-fold dirs, where a single project panel entry may actually consist of several
  81    /// project entries (and all non-leaf nodes are guaranteed to be directories).
  82    ancestors: HashMap<ProjectEntryId, FoldedAncestors>,
  83    folded_directory_drag_target: Option<FoldedDirectoryDragTarget>,
  84    last_worktree_root_id: Option<ProjectEntryId>,
  85    last_selection_drag_over_entry: Option<ProjectEntryId>,
  86    last_external_paths_drag_over_entry: Option<ProjectEntryId>,
  87    expanded_dir_ids: HashMap<WorktreeId, Vec<ProjectEntryId>>,
  88    unfolded_dir_ids: HashSet<ProjectEntryId>,
  89    // Currently selected leaf entry (see auto-folding for a definition of that) in a file tree
  90    selection: Option<SelectedEntry>,
  91    marked_entries: BTreeSet<SelectedEntry>,
  92    context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
  93    edit_state: Option<EditState>,
  94    filename_editor: Entity<Editor>,
  95    clipboard: Option<ClipboardEntry>,
  96    _dragged_entry_destination: Option<Arc<Path>>,
  97    workspace: WeakEntity<Workspace>,
  98    width: Option<Pixels>,
  99    pending_serialization: Task<Option<()>>,
 100    show_scrollbar: bool,
 101    vertical_scrollbar_state: ScrollbarState,
 102    horizontal_scrollbar_state: ScrollbarState,
 103    hide_scrollbar_task: Option<Task<()>>,
 104    diagnostics: HashMap<(WorktreeId, PathBuf), DiagnosticSeverity>,
 105    max_width_item_index: Option<usize>,
 106    // We keep track of the mouse down state on entries so we don't flash the UI
 107    // in case a user clicks to open a file.
 108    mouse_down: bool,
 109    hover_expand_task: Option<Task<()>>,
 110}
 111
 112#[derive(Copy, Clone, Debug)]
 113struct FoldedDirectoryDragTarget {
 114    entry_id: ProjectEntryId,
 115    index: usize,
 116    /// Whether we are dragging over the delimiter rather than the component itself.
 117    is_delimiter_target: bool,
 118}
 119
 120#[derive(Clone, Debug)]
 121struct EditState {
 122    worktree_id: WorktreeId,
 123    entry_id: ProjectEntryId,
 124    leaf_entry_id: Option<ProjectEntryId>,
 125    is_dir: bool,
 126    depth: usize,
 127    processing_filename: Option<String>,
 128    previously_focused: Option<SelectedEntry>,
 129}
 130
 131impl EditState {
 132    fn is_new_entry(&self) -> bool {
 133        self.leaf_entry_id.is_none()
 134    }
 135}
 136
 137#[derive(Clone, Debug)]
 138enum ClipboardEntry {
 139    Copied(BTreeSet<SelectedEntry>),
 140    Cut(BTreeSet<SelectedEntry>),
 141}
 142
 143#[derive(Debug, PartialEq, Eq, Clone)]
 144struct EntryDetails {
 145    filename: String,
 146    icon: Option<SharedString>,
 147    path: Arc<Path>,
 148    depth: usize,
 149    kind: EntryKind,
 150    is_ignored: bool,
 151    is_expanded: bool,
 152    is_selected: bool,
 153    is_marked: bool,
 154    is_editing: bool,
 155    is_processing: bool,
 156    is_cut: bool,
 157    filename_text_color: Color,
 158    diagnostic_severity: Option<DiagnosticSeverity>,
 159    git_status: GitSummary,
 160    is_private: bool,
 161    worktree_id: WorktreeId,
 162    canonical_path: Option<Arc<Path>>,
 163}
 164
 165#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema)]
 166#[serde(deny_unknown_fields)]
 167struct Delete {
 168    #[serde(default)]
 169    pub skip_prompt: bool,
 170}
 171
 172#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema)]
 173#[serde(deny_unknown_fields)]
 174struct Trash {
 175    #[serde(default)]
 176    pub skip_prompt: bool,
 177}
 178
 179impl_actions!(project_panel, [Delete, Trash]);
 180
 181actions!(
 182    project_panel,
 183    [
 184        ExpandSelectedEntry,
 185        CollapseSelectedEntry,
 186        CollapseAllEntries,
 187        NewDirectory,
 188        NewFile,
 189        Copy,
 190        Duplicate,
 191        RevealInFileManager,
 192        RemoveFromProject,
 193        OpenWithSystem,
 194        Cut,
 195        Paste,
 196        Rename,
 197        Open,
 198        OpenPermanent,
 199        ToggleFocus,
 200        NewSearchInDirectory,
 201        UnfoldDirectory,
 202        FoldDirectory,
 203        SelectParent,
 204        SelectNextGitEntry,
 205        SelectPrevGitEntry,
 206        SelectNextDiagnostic,
 207        SelectPrevDiagnostic,
 208        SelectNextDirectory,
 209        SelectPrevDirectory,
 210    ]
 211);
 212
 213#[derive(Debug, Default)]
 214struct FoldedAncestors {
 215    current_ancestor_depth: usize,
 216    ancestors: Vec<ProjectEntryId>,
 217}
 218
 219impl FoldedAncestors {
 220    fn max_ancestor_depth(&self) -> usize {
 221        self.ancestors.len()
 222    }
 223}
 224
 225pub fn init_settings(cx: &mut App) {
 226    ProjectPanelSettings::register(cx);
 227}
 228
 229pub fn init(cx: &mut App) {
 230    init_settings(cx);
 231
 232    cx.observe_new(|workspace: &mut Workspace, _, _| {
 233        workspace.register_action(|workspace, _: &ToggleFocus, window, cx| {
 234            workspace.toggle_panel_focus::<ProjectPanel>(window, cx);
 235        });
 236    })
 237    .detach();
 238}
 239
 240#[derive(Debug)]
 241pub enum Event {
 242    OpenedEntry {
 243        entry_id: ProjectEntryId,
 244        focus_opened_item: bool,
 245        allow_preview: bool,
 246    },
 247    SplitEntry {
 248        entry_id: ProjectEntryId,
 249    },
 250    Focus,
 251}
 252
 253#[derive(Serialize, Deserialize)]
 254struct SerializedProjectPanel {
 255    width: Option<Pixels>,
 256}
 257
 258struct DraggedProjectEntryView {
 259    selection: SelectedEntry,
 260    details: EntryDetails,
 261    click_offset: Point<Pixels>,
 262    selections: Arc<BTreeSet<SelectedEntry>>,
 263}
 264
 265struct ItemColors {
 266    default: Hsla,
 267    hover: Hsla,
 268    drag_over: Hsla,
 269    marked: Hsla,
 270    focused: Hsla,
 271}
 272
 273fn get_item_color(cx: &App) -> ItemColors {
 274    let colors = cx.theme().colors();
 275
 276    ItemColors {
 277        default: colors.panel_background,
 278        hover: colors.element_hover,
 279        marked: colors.element_selected,
 280        focused: colors.panel_focused_border,
 281        drag_over: colors.drop_target_background,
 282    }
 283}
 284
 285impl ProjectPanel {
 286    fn new(
 287        workspace: &mut Workspace,
 288        window: &mut Window,
 289        cx: &mut Context<Workspace>,
 290    ) -> Entity<Self> {
 291        let project = workspace.project().clone();
 292        let project_panel = cx.new(|cx| {
 293            let focus_handle = cx.focus_handle();
 294            cx.on_focus(&focus_handle, window, Self::focus_in).detach();
 295            cx.on_focus_out(&focus_handle, window, |this, _, window, cx| {
 296                this.focus_out(window, cx);
 297                this.hide_scrollbar(window, cx);
 298            })
 299            .detach();
 300            cx.subscribe(&project, |this, project, event, cx| match event {
 301                project::Event::ActiveEntryChanged(Some(entry_id)) => {
 302                    if ProjectPanelSettings::get_global(cx).auto_reveal_entries {
 303                        this.reveal_entry(project.clone(), *entry_id, true, cx);
 304                    }
 305                }
 306                project::Event::ActiveEntryChanged(None) => {
 307                    this.marked_entries.clear();
 308                }
 309                project::Event::RevealInProjectPanel(entry_id) => {
 310                    this.reveal_entry(project.clone(), *entry_id, false, cx);
 311                    cx.emit(PanelEvent::Activate);
 312                }
 313                project::Event::ActivateProjectPanel => {
 314                    cx.emit(PanelEvent::Activate);
 315                }
 316                project::Event::DiskBasedDiagnosticsFinished { .. }
 317                | project::Event::DiagnosticsUpdated { .. } => {
 318                    if ProjectPanelSettings::get_global(cx).show_diagnostics != ShowDiagnostics::Off
 319                    {
 320                        this.update_diagnostics(cx);
 321                        cx.notify();
 322                    }
 323                }
 324                project::Event::WorktreeRemoved(id) => {
 325                    this.expanded_dir_ids.remove(id);
 326                    this.update_visible_entries(None, cx);
 327                    cx.notify();
 328                }
 329                project::Event::WorktreeUpdatedGitRepositories(_)
 330                | project::Event::WorktreeUpdatedEntries(_, _)
 331                | project::Event::WorktreeAdded(_)
 332                | project::Event::WorktreeOrderChanged => {
 333                    this.update_visible_entries(None, cx);
 334                    cx.notify();
 335                }
 336                project::Event::ExpandedAllForEntry(worktree_id, entry_id) => {
 337                    if let Some((worktree, expanded_dir_ids)) = project
 338                        .read(cx)
 339                        .worktree_for_id(*worktree_id, cx)
 340                        .zip(this.expanded_dir_ids.get_mut(&worktree_id))
 341                    {
 342                        let worktree = worktree.read(cx);
 343
 344                        let Some(entry) = worktree.entry_for_id(*entry_id) else {
 345                            return;
 346                        };
 347                        let include_ignored_dirs = !entry.is_ignored;
 348
 349                        let mut dirs_to_expand = vec![*entry_id];
 350                        while let Some(current_id) = dirs_to_expand.pop() {
 351                            let Some(current_entry) = worktree.entry_for_id(current_id) else {
 352                                continue;
 353                            };
 354                            for child in worktree.child_entries(&current_entry.path) {
 355                                if !child.is_dir() || (include_ignored_dirs && child.is_ignored) {
 356                                    continue;
 357                                }
 358
 359                                dirs_to_expand.push(child.id);
 360
 361                                if let Err(ix) = expanded_dir_ids.binary_search(&child.id) {
 362                                    expanded_dir_ids.insert(ix, child.id);
 363                                }
 364                                this.unfolded_dir_ids.insert(child.id);
 365                            }
 366                        }
 367                        this.update_visible_entries(None, cx);
 368                        cx.notify();
 369                    }
 370                }
 371                _ => {}
 372            })
 373            .detach();
 374
 375            let trash_action = [TypeId::of::<Trash>()];
 376            let is_remote = project.read(cx).is_via_collab();
 377
 378            if is_remote {
 379                CommandPaletteFilter::update_global(cx, |filter, _cx| {
 380                    filter.hide_action_types(&trash_action);
 381                });
 382            }
 383
 384            let filename_editor = cx.new(|cx| Editor::single_line(window, cx));
 385
 386            cx.subscribe(
 387                &filename_editor,
 388                |project_panel, _, editor_event, cx| match editor_event {
 389                    EditorEvent::BufferEdited | EditorEvent::SelectionsChanged { .. } => {
 390                        project_panel.autoscroll(cx);
 391                    }
 392                    EditorEvent::Blurred => {
 393                        if project_panel
 394                            .edit_state
 395                            .as_ref()
 396                            .map_or(false, |state| state.processing_filename.is_none())
 397                        {
 398                            project_panel.edit_state = None;
 399                            project_panel.update_visible_entries(None, cx);
 400                            cx.notify();
 401                        }
 402                    }
 403                    _ => {}
 404                },
 405            )
 406            .detach();
 407
 408            cx.observe_global::<FileIcons>(|_, cx| {
 409                cx.notify();
 410            })
 411            .detach();
 412
 413            let mut project_panel_settings = *ProjectPanelSettings::get_global(cx);
 414            cx.observe_global::<SettingsStore>(move |this, cx| {
 415                let new_settings = *ProjectPanelSettings::get_global(cx);
 416                if project_panel_settings != new_settings {
 417                    project_panel_settings = new_settings;
 418                    this.update_diagnostics(cx);
 419                    cx.notify();
 420                }
 421            })
 422            .detach();
 423
 424            let scroll_handle = UniformListScrollHandle::new();
 425            let mut this = Self {
 426                project: project.clone(),
 427                hover_scroll_task: None,
 428                fs: workspace.app_state().fs.clone(),
 429                focus_handle,
 430                visible_entries: Default::default(),
 431                ancestors: Default::default(),
 432                folded_directory_drag_target: None,
 433                last_worktree_root_id: Default::default(),
 434                last_external_paths_drag_over_entry: None,
 435                last_selection_drag_over_entry: None,
 436                expanded_dir_ids: Default::default(),
 437                unfolded_dir_ids: Default::default(),
 438                selection: None,
 439                marked_entries: Default::default(),
 440                edit_state: None,
 441                context_menu: None,
 442                filename_editor,
 443                clipboard: None,
 444                _dragged_entry_destination: None,
 445                workspace: workspace.weak_handle(),
 446                width: None,
 447                pending_serialization: Task::ready(None),
 448                show_scrollbar: !Self::should_autohide_scrollbar(cx),
 449                hide_scrollbar_task: None,
 450                vertical_scrollbar_state: ScrollbarState::new(scroll_handle.clone())
 451                    .parent_entity(&cx.entity()),
 452                horizontal_scrollbar_state: ScrollbarState::new(scroll_handle.clone())
 453                    .parent_entity(&cx.entity()),
 454                max_width_item_index: None,
 455                diagnostics: Default::default(),
 456                scroll_handle,
 457                mouse_down: false,
 458                hover_expand_task: None,
 459            };
 460            this.update_visible_entries(None, cx);
 461
 462            this
 463        });
 464
 465        cx.subscribe_in(&project_panel, window, {
 466            let project_panel = project_panel.downgrade();
 467            move |workspace, _, event, window, cx| match event {
 468                &Event::OpenedEntry {
 469                    entry_id,
 470                    focus_opened_item,
 471                    allow_preview,
 472                } => {
 473                    if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) {
 474                        if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) {
 475                            let file_path = entry.path.clone();
 476                            let worktree_id = worktree.read(cx).id();
 477                            let entry_id = entry.id;
 478                            let is_via_ssh = project.read(cx).is_via_ssh();
 479
 480                            workspace
 481                                .open_path_preview(
 482                                    ProjectPath {
 483                                        worktree_id,
 484                                        path: file_path.clone(),
 485                                    },
 486                                    None,
 487                                    focus_opened_item,
 488                                    allow_preview,
 489                                    true,
 490                                    window, cx,
 491                                )
 492                                .detach_and_prompt_err("Failed to open file", window, cx, move |e, _, _| {
 493                                    match e.error_code() {
 494                                        ErrorCode::Disconnected => if is_via_ssh {
 495                                            Some("Disconnected from SSH host".to_string())
 496                                        } else {
 497                                            Some("Disconnected from remote project".to_string())
 498                                        },
 499                                        ErrorCode::UnsharedItem => Some(format!(
 500                                            "{} is not shared by the host. This could be because it has been marked as `private`",
 501                                            file_path.display()
 502                                        )),
 503                                        _ => None,
 504                                    }
 505                                });
 506
 507                            if let Some(project_panel) = project_panel.upgrade() {
 508                                // Always select and mark the entry, regardless of whether it is opened or not.
 509                                project_panel.update(cx, |project_panel, _| {
 510                                    let entry = SelectedEntry { worktree_id, entry_id };
 511                                    project_panel.marked_entries.clear();
 512                                    project_panel.marked_entries.insert(entry);
 513                                    project_panel.selection = Some(entry);
 514                                });
 515                                if !focus_opened_item {
 516                                    let focus_handle = project_panel.read(cx).focus_handle.clone();
 517                                    window.focus(&focus_handle);
 518                                }
 519                            }
 520                        }
 521                    }
 522                }
 523                &Event::SplitEntry { entry_id } => {
 524                    if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) {
 525                        if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) {
 526                            workspace
 527                                .split_path(
 528                                    ProjectPath {
 529                                        worktree_id: worktree.read(cx).id(),
 530                                        path: entry.path.clone(),
 531                                    },
 532                                    window, cx,
 533                                )
 534                                .detach_and_log_err(cx);
 535                        }
 536                    }
 537                }
 538
 539                _ => {}
 540            }
 541        })
 542        .detach();
 543
 544        project_panel
 545    }
 546
 547    pub async fn load(
 548        workspace: WeakEntity<Workspace>,
 549        mut cx: AsyncWindowContext,
 550    ) -> Result<Entity<Self>> {
 551        let serialized_panel = cx
 552            .background_spawn(async move { KEY_VALUE_STORE.read_kvp(PROJECT_PANEL_KEY) })
 553            .await
 554            .map_err(|e| anyhow!("Failed to load project panel: {}", e))
 555            .log_err()
 556            .flatten()
 557            .map(|panel| serde_json::from_str::<SerializedProjectPanel>(&panel))
 558            .transpose()
 559            .log_err()
 560            .flatten();
 561
 562        workspace.update_in(&mut cx, |workspace, window, cx| {
 563            let panel = ProjectPanel::new(workspace, window, cx);
 564            if let Some(serialized_panel) = serialized_panel {
 565                panel.update(cx, |panel, cx| {
 566                    panel.width = serialized_panel.width.map(|px| px.round());
 567                    cx.notify();
 568                });
 569            }
 570            panel
 571        })
 572    }
 573
 574    fn update_diagnostics(&mut self, cx: &mut Context<Self>) {
 575        let mut diagnostics: HashMap<(WorktreeId, PathBuf), DiagnosticSeverity> =
 576            Default::default();
 577        let show_diagnostics_setting = ProjectPanelSettings::get_global(cx).show_diagnostics;
 578
 579        if show_diagnostics_setting != ShowDiagnostics::Off {
 580            self.project
 581                .read(cx)
 582                .diagnostic_summaries(false, cx)
 583                .filter_map(|(path, _, diagnostic_summary)| {
 584                    if diagnostic_summary.error_count > 0 {
 585                        Some((path, DiagnosticSeverity::ERROR))
 586                    } else if show_diagnostics_setting == ShowDiagnostics::All
 587                        && diagnostic_summary.warning_count > 0
 588                    {
 589                        Some((path, DiagnosticSeverity::WARNING))
 590                    } else {
 591                        None
 592                    }
 593                })
 594                .for_each(|(project_path, diagnostic_severity)| {
 595                    let mut path_buffer = PathBuf::new();
 596                    Self::update_strongest_diagnostic_severity(
 597                        &mut diagnostics,
 598                        &project_path,
 599                        path_buffer.clone(),
 600                        diagnostic_severity,
 601                    );
 602
 603                    for component in project_path.path.components() {
 604                        path_buffer.push(component);
 605                        Self::update_strongest_diagnostic_severity(
 606                            &mut diagnostics,
 607                            &project_path,
 608                            path_buffer.clone(),
 609                            diagnostic_severity,
 610                        );
 611                    }
 612                });
 613        }
 614        self.diagnostics = diagnostics;
 615    }
 616
 617    fn update_strongest_diagnostic_severity(
 618        diagnostics: &mut HashMap<(WorktreeId, PathBuf), DiagnosticSeverity>,
 619        project_path: &ProjectPath,
 620        path_buffer: PathBuf,
 621        diagnostic_severity: DiagnosticSeverity,
 622    ) {
 623        diagnostics
 624            .entry((project_path.worktree_id, path_buffer.clone()))
 625            .and_modify(|strongest_diagnostic_severity| {
 626                *strongest_diagnostic_severity =
 627                    cmp::min(*strongest_diagnostic_severity, diagnostic_severity);
 628            })
 629            .or_insert(diagnostic_severity);
 630    }
 631
 632    fn serialize(&mut self, cx: &mut Context<Self>) {
 633        let width = self.width;
 634        self.pending_serialization = cx.background_spawn(
 635            async move {
 636                KEY_VALUE_STORE
 637                    .write_kvp(
 638                        PROJECT_PANEL_KEY.into(),
 639                        serde_json::to_string(&SerializedProjectPanel { width })?,
 640                    )
 641                    .await?;
 642                anyhow::Ok(())
 643            }
 644            .log_err(),
 645        );
 646    }
 647
 648    fn focus_in(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 649        if !self.focus_handle.contains_focused(window, cx) {
 650            cx.emit(Event::Focus);
 651        }
 652    }
 653
 654    fn focus_out(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 655        if !self.focus_handle.is_focused(window) {
 656            self.confirm(&Confirm, window, cx);
 657        }
 658    }
 659
 660    fn deploy_context_menu(
 661        &mut self,
 662        position: Point<Pixels>,
 663        entry_id: ProjectEntryId,
 664        window: &mut Window,
 665        cx: &mut Context<Self>,
 666    ) {
 667        let project = self.project.read(cx);
 668
 669        let worktree_id = if let Some(id) = project.worktree_id_for_entry(entry_id, cx) {
 670            id
 671        } else {
 672            return;
 673        };
 674
 675        self.selection = Some(SelectedEntry {
 676            worktree_id,
 677            entry_id,
 678        });
 679
 680        if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
 681            let auto_fold_dirs = ProjectPanelSettings::get_global(cx).auto_fold_dirs;
 682            let worktree = worktree.read(cx);
 683            let is_root = Some(entry) == worktree.root_entry();
 684            let is_dir = entry.is_dir();
 685            let is_foldable = auto_fold_dirs && self.is_foldable(entry, worktree);
 686            let is_unfoldable = auto_fold_dirs && self.is_unfoldable(entry, worktree);
 687            let is_read_only = project.is_read_only(cx);
 688            let is_remote = project.is_via_collab();
 689            let is_local = project.is_local();
 690
 691            let context_menu = ContextMenu::build(window, cx, |menu, _, _| {
 692                menu.context(self.focus_handle.clone()).map(|menu| {
 693                    if is_read_only {
 694                        menu.when(is_dir, |menu| {
 695                            menu.action("Search Inside", Box::new(NewSearchInDirectory))
 696                        })
 697                    } else {
 698                        menu.action("New File", Box::new(NewFile))
 699                            .action("New Folder", Box::new(NewDirectory))
 700                            .separator()
 701                            .when(is_local && cfg!(target_os = "macos"), |menu| {
 702                                menu.action("Reveal in Finder", Box::new(RevealInFileManager))
 703                            })
 704                            .when(is_local && cfg!(not(target_os = "macos")), |menu| {
 705                                menu.action("Reveal in File Manager", Box::new(RevealInFileManager))
 706                            })
 707                            .when(is_local, |menu| {
 708                                menu.action("Open in Default App", Box::new(OpenWithSystem))
 709                            })
 710                            .action("Open in Terminal", Box::new(OpenInTerminal))
 711                            .when(is_dir, |menu| {
 712                                menu.separator()
 713                                    .action("Find in Folder…", Box::new(NewSearchInDirectory))
 714                            })
 715                            .when(is_unfoldable, |menu| {
 716                                menu.action("Unfold Directory", Box::new(UnfoldDirectory))
 717                            })
 718                            .when(is_foldable, |menu| {
 719                                menu.action("Fold Directory", Box::new(FoldDirectory))
 720                            })
 721                            .separator()
 722                            .action("Cut", Box::new(Cut))
 723                            .action("Copy", Box::new(Copy))
 724                            .action("Duplicate", Box::new(Duplicate))
 725                            // TODO: Paste should always be visible, cbut disabled when clipboard is empty
 726                            .map(|menu| {
 727                                if self.clipboard.as_ref().is_some() {
 728                                    menu.action("Paste", Box::new(Paste))
 729                                } else {
 730                                    menu.disabled_action("Paste", Box::new(Paste))
 731                                }
 732                            })
 733                            .separator()
 734                            .action("Copy Path", Box::new(zed_actions::workspace::CopyPath))
 735                            .action(
 736                                "Copy Relative Path",
 737                                Box::new(zed_actions::workspace::CopyRelativePath),
 738                            )
 739                            .separator()
 740                            .when(!is_root || !cfg!(target_os = "windows"), |menu| {
 741                                menu.action("Rename", Box::new(Rename))
 742                            })
 743                            .when(!is_root & !is_remote, |menu| {
 744                                menu.action("Trash", Box::new(Trash { skip_prompt: false }))
 745                            })
 746                            .when(!is_root, |menu| {
 747                                menu.action("Delete", Box::new(Delete { skip_prompt: false }))
 748                            })
 749                            .when(!is_remote & is_root, |menu| {
 750                                menu.separator()
 751                                    .action(
 752                                        "Add Folder to Project…",
 753                                        Box::new(workspace::AddFolderToProject),
 754                                    )
 755                                    .action("Remove from Project", Box::new(RemoveFromProject))
 756                            })
 757                            .when(is_root, |menu| {
 758                                menu.separator()
 759                                    .action("Collapse All", Box::new(CollapseAllEntries))
 760                            })
 761                    }
 762                })
 763            });
 764
 765            window.focus(&context_menu.focus_handle(cx));
 766            let subscription = cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| {
 767                this.context_menu.take();
 768                cx.notify();
 769            });
 770            self.context_menu = Some((context_menu, position, subscription));
 771        }
 772
 773        cx.notify();
 774    }
 775
 776    fn is_unfoldable(&self, entry: &Entry, worktree: &Worktree) -> bool {
 777        if !entry.is_dir() || self.unfolded_dir_ids.contains(&entry.id) {
 778            return false;
 779        }
 780
 781        if let Some(parent_path) = entry.path.parent() {
 782            let snapshot = worktree.snapshot();
 783            let mut child_entries = snapshot.child_entries(parent_path);
 784            if let Some(child) = child_entries.next() {
 785                if child_entries.next().is_none() {
 786                    return child.kind.is_dir();
 787                }
 788            }
 789        };
 790        false
 791    }
 792
 793    fn is_foldable(&self, entry: &Entry, worktree: &Worktree) -> bool {
 794        if entry.is_dir() {
 795            let snapshot = worktree.snapshot();
 796
 797            let mut child_entries = snapshot.child_entries(&entry.path);
 798            if let Some(child) = child_entries.next() {
 799                if child_entries.next().is_none() {
 800                    return child.kind.is_dir();
 801                }
 802            }
 803        }
 804        false
 805    }
 806
 807    fn expand_selected_entry(
 808        &mut self,
 809        _: &ExpandSelectedEntry,
 810        window: &mut Window,
 811        cx: &mut Context<Self>,
 812    ) {
 813        if let Some((worktree, entry)) = self.selected_entry(cx) {
 814            if let Some(folded_ancestors) = self.ancestors.get_mut(&entry.id) {
 815                if folded_ancestors.current_ancestor_depth > 0 {
 816                    folded_ancestors.current_ancestor_depth -= 1;
 817                    cx.notify();
 818                    return;
 819                }
 820            }
 821            if entry.is_dir() {
 822                let worktree_id = worktree.id();
 823                let entry_id = entry.id;
 824                let expanded_dir_ids =
 825                    if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) {
 826                        expanded_dir_ids
 827                    } else {
 828                        return;
 829                    };
 830
 831                match expanded_dir_ids.binary_search(&entry_id) {
 832                    Ok(_) => self.select_next(&SelectNext, window, cx),
 833                    Err(ix) => {
 834                        self.project.update(cx, |project, cx| {
 835                            project.expand_entry(worktree_id, entry_id, cx);
 836                        });
 837
 838                        expanded_dir_ids.insert(ix, entry_id);
 839                        self.update_visible_entries(None, cx);
 840                        cx.notify();
 841                    }
 842                }
 843            }
 844        }
 845    }
 846
 847    fn collapse_selected_entry(
 848        &mut self,
 849        _: &CollapseSelectedEntry,
 850        _: &mut Window,
 851        cx: &mut Context<Self>,
 852    ) {
 853        let Some((worktree, entry)) = self.selected_entry_handle(cx) else {
 854            return;
 855        };
 856        self.collapse_entry(entry.clone(), worktree, cx)
 857    }
 858
 859    fn collapse_entry(&mut self, entry: Entry, worktree: Entity<Worktree>, cx: &mut Context<Self>) {
 860        let worktree = worktree.read(cx);
 861        if let Some(folded_ancestors) = self.ancestors.get_mut(&entry.id) {
 862            if folded_ancestors.current_ancestor_depth + 1 < folded_ancestors.max_ancestor_depth() {
 863                folded_ancestors.current_ancestor_depth += 1;
 864                cx.notify();
 865                return;
 866            }
 867        }
 868        let worktree_id = worktree.id();
 869        let expanded_dir_ids =
 870            if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) {
 871                expanded_dir_ids
 872            } else {
 873                return;
 874            };
 875
 876        let mut entry = &entry;
 877        loop {
 878            let entry_id = entry.id;
 879            match expanded_dir_ids.binary_search(&entry_id) {
 880                Ok(ix) => {
 881                    expanded_dir_ids.remove(ix);
 882                    self.update_visible_entries(Some((worktree_id, entry_id)), cx);
 883                    cx.notify();
 884                    break;
 885                }
 886                Err(_) => {
 887                    if let Some(parent_entry) =
 888                        entry.path.parent().and_then(|p| worktree.entry_for_path(p))
 889                    {
 890                        entry = parent_entry;
 891                    } else {
 892                        break;
 893                    }
 894                }
 895            }
 896        }
 897    }
 898
 899    pub fn collapse_all_entries(
 900        &mut self,
 901        _: &CollapseAllEntries,
 902        _: &mut Window,
 903        cx: &mut Context<Self>,
 904    ) {
 905        // By keeping entries for fully collapsed worktrees, we avoid expanding them within update_visible_entries
 906        // (which is it's default behavior when there's no entry for a worktree in expanded_dir_ids).
 907        self.expanded_dir_ids
 908            .retain(|_, expanded_entries| expanded_entries.is_empty());
 909        self.update_visible_entries(None, cx);
 910        cx.notify();
 911    }
 912
 913    fn toggle_expanded(
 914        &mut self,
 915        entry_id: ProjectEntryId,
 916        window: &mut Window,
 917        cx: &mut Context<Self>,
 918    ) {
 919        if let Some(worktree_id) = self.project.read(cx).worktree_id_for_entry(entry_id, cx) {
 920            if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) {
 921                self.project.update(cx, |project, cx| {
 922                    match expanded_dir_ids.binary_search(&entry_id) {
 923                        Ok(ix) => {
 924                            expanded_dir_ids.remove(ix);
 925                        }
 926                        Err(ix) => {
 927                            project.expand_entry(worktree_id, entry_id, cx);
 928                            expanded_dir_ids.insert(ix, entry_id);
 929                        }
 930                    }
 931                });
 932                self.update_visible_entries(Some((worktree_id, entry_id)), cx);
 933                window.focus(&self.focus_handle);
 934                cx.notify();
 935            }
 936        }
 937    }
 938
 939    fn toggle_expand_all(
 940        &mut self,
 941        entry_id: ProjectEntryId,
 942        window: &mut Window,
 943        cx: &mut Context<Self>,
 944    ) {
 945        if let Some(worktree_id) = self.project.read(cx).worktree_id_for_entry(entry_id, cx) {
 946            if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) {
 947                match expanded_dir_ids.binary_search(&entry_id) {
 948                    Ok(_ix) => {
 949                        self.collapse_all_for_entry(worktree_id, entry_id, cx);
 950                    }
 951                    Err(_ix) => {
 952                        self.expand_all_for_entry(worktree_id, entry_id, cx);
 953                    }
 954                }
 955                self.update_visible_entries(Some((worktree_id, entry_id)), cx);
 956                window.focus(&self.focus_handle);
 957                cx.notify();
 958            }
 959        }
 960    }
 961
 962    fn expand_all_for_entry(
 963        &mut self,
 964        worktree_id: WorktreeId,
 965        entry_id: ProjectEntryId,
 966        cx: &mut Context<Self>,
 967    ) {
 968        self.project.update(cx, |project, cx| {
 969            if let Some((worktree, expanded_dir_ids)) = project
 970                .worktree_for_id(worktree_id, cx)
 971                .zip(self.expanded_dir_ids.get_mut(&worktree_id))
 972            {
 973                if let Some(task) = project.expand_all_for_entry(worktree_id, entry_id, cx) {
 974                    task.detach();
 975                }
 976
 977                let worktree = worktree.read(cx);
 978
 979                if let Some(mut entry) = worktree.entry_for_id(entry_id) {
 980                    loop {
 981                        if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) {
 982                            expanded_dir_ids.insert(ix, entry.id);
 983                        }
 984
 985                        if let Some(parent_entry) =
 986                            entry.path.parent().and_then(|p| worktree.entry_for_path(p))
 987                        {
 988                            entry = parent_entry;
 989                        } else {
 990                            break;
 991                        }
 992                    }
 993                }
 994            }
 995        });
 996    }
 997
 998    fn collapse_all_for_entry(
 999        &mut self,
1000        worktree_id: WorktreeId,
1001        entry_id: ProjectEntryId,
1002        cx: &mut Context<Self>,
1003    ) {
1004        self.project.update(cx, |project, cx| {
1005            if let Some((worktree, expanded_dir_ids)) = project
1006                .worktree_for_id(worktree_id, cx)
1007                .zip(self.expanded_dir_ids.get_mut(&worktree_id))
1008            {
1009                let worktree = worktree.read(cx);
1010                let mut dirs_to_collapse = vec![entry_id];
1011                let auto_fold_enabled = ProjectPanelSettings::get_global(cx).auto_fold_dirs;
1012                while let Some(current_id) = dirs_to_collapse.pop() {
1013                    let Some(current_entry) = worktree.entry_for_id(current_id) else {
1014                        continue;
1015                    };
1016                    if let Ok(ix) = expanded_dir_ids.binary_search(&current_id) {
1017                        expanded_dir_ids.remove(ix);
1018                    }
1019                    if auto_fold_enabled {
1020                        self.unfolded_dir_ids.remove(&current_id);
1021                    }
1022                    for child in worktree.child_entries(&current_entry.path) {
1023                        if child.is_dir() {
1024                            dirs_to_collapse.push(child.id);
1025                        }
1026                    }
1027                }
1028            }
1029        });
1030    }
1031
1032    fn select_previous(&mut self, _: &SelectPrevious, window: &mut Window, cx: &mut Context<Self>) {
1033        if let Some(edit_state) = &self.edit_state {
1034            if edit_state.processing_filename.is_none() {
1035                self.filename_editor.update(cx, |editor, cx| {
1036                    editor.move_to_beginning_of_line(
1037                        &editor::actions::MoveToBeginningOfLine {
1038                            stop_at_soft_wraps: false,
1039                            stop_at_indent: false,
1040                        },
1041                        window,
1042                        cx,
1043                    );
1044                });
1045                return;
1046            }
1047        }
1048        if let Some(selection) = self.selection {
1049            let (mut worktree_ix, mut entry_ix, _) =
1050                self.index_for_selection(selection).unwrap_or_default();
1051            if entry_ix > 0 {
1052                entry_ix -= 1;
1053            } else if worktree_ix > 0 {
1054                worktree_ix -= 1;
1055                entry_ix = self.visible_entries[worktree_ix].1.len() - 1;
1056            } else {
1057                return;
1058            }
1059
1060            let (worktree_id, worktree_entries, _) = &self.visible_entries[worktree_ix];
1061            let selection = SelectedEntry {
1062                worktree_id: *worktree_id,
1063                entry_id: worktree_entries[entry_ix].id,
1064            };
1065            self.selection = Some(selection);
1066            if window.modifiers().shift {
1067                self.marked_entries.insert(selection);
1068            }
1069            self.autoscroll(cx);
1070            cx.notify();
1071        } else {
1072            self.select_first(&SelectFirst {}, window, cx);
1073        }
1074    }
1075
1076    fn confirm(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
1077        if let Some(task) = self.confirm_edit(window, cx) {
1078            task.detach_and_notify_err(window, cx);
1079        }
1080    }
1081
1082    fn open(&mut self, _: &Open, window: &mut Window, cx: &mut Context<Self>) {
1083        let preview_tabs_enabled = PreviewTabsSettings::get_global(cx).enabled;
1084        self.open_internal(true, !preview_tabs_enabled, window, cx);
1085    }
1086
1087    fn open_permanent(&mut self, _: &OpenPermanent, window: &mut Window, cx: &mut Context<Self>) {
1088        self.open_internal(false, true, window, cx);
1089    }
1090
1091    fn open_internal(
1092        &mut self,
1093        allow_preview: bool,
1094        focus_opened_item: bool,
1095        window: &mut Window,
1096        cx: &mut Context<Self>,
1097    ) {
1098        if let Some((_, entry)) = self.selected_entry(cx) {
1099            if entry.is_file() {
1100                self.open_entry(entry.id, focus_opened_item, allow_preview, cx);
1101                cx.notify();
1102            } else {
1103                self.toggle_expanded(entry.id, window, cx);
1104            }
1105        }
1106    }
1107
1108    fn confirm_edit(
1109        &mut self,
1110        window: &mut Window,
1111        cx: &mut Context<Self>,
1112    ) -> Option<Task<Result<()>>> {
1113        let edit_state = self.edit_state.as_mut()?;
1114        window.focus(&self.focus_handle);
1115
1116        let worktree_id = edit_state.worktree_id;
1117        let is_new_entry = edit_state.is_new_entry();
1118        let filename = self.filename_editor.read(cx).text(cx);
1119        #[cfg(not(target_os = "windows"))]
1120        let filename_indicates_dir = filename.ends_with("/");
1121        // On Windows, path separator could be either `/` or `\`.
1122        #[cfg(target_os = "windows")]
1123        let filename_indicates_dir = filename.ends_with("/") || filename.ends_with("\\");
1124        edit_state.is_dir =
1125            edit_state.is_dir || (edit_state.is_new_entry() && filename_indicates_dir);
1126        let is_dir = edit_state.is_dir;
1127        let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?;
1128        let entry = worktree.read(cx).entry_for_id(edit_state.entry_id)?.clone();
1129
1130        let path_already_exists = |path| worktree.read(cx).entry_for_path(path).is_some();
1131        let edit_task;
1132        let edited_entry_id;
1133        if is_new_entry {
1134            self.selection = Some(SelectedEntry {
1135                worktree_id,
1136                entry_id: NEW_ENTRY_ID,
1137            });
1138            let new_path = entry.path.join(filename.trim_start_matches('/'));
1139            if path_already_exists(new_path.as_path()) {
1140                return None;
1141            }
1142
1143            edited_entry_id = NEW_ENTRY_ID;
1144            edit_task = self.project.update(cx, |project, cx| {
1145                project.create_entry((worktree_id, &new_path), is_dir, cx)
1146            });
1147        } else {
1148            let new_path = if let Some(parent) = entry.path.clone().parent() {
1149                parent.join(&filename)
1150            } else {
1151                filename.clone().into()
1152            };
1153            if path_already_exists(new_path.as_path()) {
1154                return None;
1155            }
1156            edited_entry_id = entry.id;
1157            edit_task = self.project.update(cx, |project, cx| {
1158                project.rename_entry(entry.id, new_path.as_path(), cx)
1159            });
1160        };
1161
1162        edit_state.processing_filename = Some(filename);
1163        cx.notify();
1164
1165        Some(cx.spawn_in(window, |project_panel, mut cx| async move {
1166            let new_entry = edit_task.await;
1167            project_panel.update(&mut cx, |project_panel, cx| {
1168                project_panel.edit_state = None;
1169                cx.notify();
1170            })?;
1171
1172            match new_entry {
1173                Err(e) => {
1174                    project_panel.update(&mut cx, |project_panel, cx| {
1175                        project_panel.marked_entries.clear();
1176                        project_panel.update_visible_entries(None,  cx);
1177                    }).ok();
1178                    Err(e)?;
1179                }
1180                Ok(CreatedEntry::Included(new_entry)) => {
1181                    project_panel.update(&mut cx, |project_panel, cx| {
1182                        if let Some(selection) = &mut project_panel.selection {
1183                            if selection.entry_id == edited_entry_id {
1184                                selection.worktree_id = worktree_id;
1185                                selection.entry_id = new_entry.id;
1186                                project_panel.marked_entries.clear();
1187                                project_panel.expand_to_selection(cx);
1188                            }
1189                        }
1190                        project_panel.update_visible_entries(None, cx);
1191                        if is_new_entry && !is_dir {
1192                            project_panel.open_entry(new_entry.id, true, false, cx);
1193                        }
1194                        cx.notify();
1195                    })?;
1196                }
1197                Ok(CreatedEntry::Excluded { abs_path }) => {
1198                    if let Some(open_task) = project_panel
1199                        .update_in(&mut cx, |project_panel, window, cx| {
1200                            project_panel.marked_entries.clear();
1201                            project_panel.update_visible_entries(None,  cx);
1202
1203                            if is_dir {
1204                                project_panel.project.update(cx, |_, cx| {
1205                                    cx.emit(project::Event::Toast {
1206                                        notification_id: "excluded-directory".into(),
1207                                        message: format!("Created an excluded directory at {abs_path:?}.\nAlter `file_scan_exclusions` in the settings to show it in the panel")
1208                                    })
1209                                });
1210                                None
1211                            } else {
1212                                project_panel
1213                                    .workspace
1214                                    .update(cx, |workspace, cx| {
1215                                        workspace.open_abs_path(abs_path, OpenOptions { visible: Some(OpenVisible::All), ..Default::default() }, window, cx)
1216                                    })
1217                                    .ok()
1218                            }
1219                        })
1220                        .ok()
1221                        .flatten()
1222                    {
1223                        let _ = open_task.await?;
1224                    }
1225                }
1226            }
1227            Ok(())
1228        }))
1229    }
1230
1231    fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) {
1232        let previous_edit_state = self.edit_state.take();
1233        self.update_visible_entries(None, cx);
1234        self.marked_entries.clear();
1235
1236        if let Some(previously_focused) =
1237            previous_edit_state.and_then(|edit_state| edit_state.previously_focused)
1238        {
1239            self.selection = Some(previously_focused);
1240            self.autoscroll(cx);
1241        }
1242
1243        window.focus(&self.focus_handle);
1244        cx.notify();
1245    }
1246
1247    fn open_entry(
1248        &mut self,
1249        entry_id: ProjectEntryId,
1250        focus_opened_item: bool,
1251        allow_preview: bool,
1252
1253        cx: &mut Context<Self>,
1254    ) {
1255        cx.emit(Event::OpenedEntry {
1256            entry_id,
1257            focus_opened_item,
1258            allow_preview,
1259        });
1260    }
1261
1262    fn split_entry(&mut self, entry_id: ProjectEntryId, cx: &mut Context<Self>) {
1263        cx.emit(Event::SplitEntry { entry_id });
1264    }
1265
1266    fn new_file(&mut self, _: &NewFile, window: &mut Window, cx: &mut Context<Self>) {
1267        self.add_entry(false, window, cx)
1268    }
1269
1270    fn new_directory(&mut self, _: &NewDirectory, window: &mut Window, cx: &mut Context<Self>) {
1271        self.add_entry(true, window, cx)
1272    }
1273
1274    fn add_entry(&mut self, is_dir: bool, window: &mut Window, cx: &mut Context<Self>) {
1275        if let Some(SelectedEntry {
1276            worktree_id,
1277            entry_id,
1278        }) = self.selection
1279        {
1280            let directory_id;
1281            let new_entry_id = self.resolve_entry(entry_id);
1282            if let Some((worktree, expanded_dir_ids)) = self
1283                .project
1284                .read(cx)
1285                .worktree_for_id(worktree_id, cx)
1286                .zip(self.expanded_dir_ids.get_mut(&worktree_id))
1287            {
1288                let worktree = worktree.read(cx);
1289                if let Some(mut entry) = worktree.entry_for_id(new_entry_id) {
1290                    loop {
1291                        if entry.is_dir() {
1292                            if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) {
1293                                expanded_dir_ids.insert(ix, entry.id);
1294                            }
1295                            directory_id = entry.id;
1296                            break;
1297                        } else {
1298                            if let Some(parent_path) = entry.path.parent() {
1299                                if let Some(parent_entry) = worktree.entry_for_path(parent_path) {
1300                                    entry = parent_entry;
1301                                    continue;
1302                                }
1303                            }
1304                            return;
1305                        }
1306                    }
1307                } else {
1308                    return;
1309                };
1310            } else {
1311                return;
1312            };
1313            self.marked_entries.clear();
1314            self.edit_state = Some(EditState {
1315                worktree_id,
1316                entry_id: directory_id,
1317                leaf_entry_id: None,
1318                is_dir,
1319                processing_filename: None,
1320                previously_focused: self.selection,
1321                depth: 0,
1322            });
1323            self.filename_editor.update(cx, |editor, cx| {
1324                editor.clear(window, cx);
1325                window.focus(&editor.focus_handle(cx));
1326            });
1327            self.update_visible_entries(Some((worktree_id, NEW_ENTRY_ID)), cx);
1328            self.autoscroll(cx);
1329            cx.notify();
1330        }
1331    }
1332
1333    fn unflatten_entry_id(&self, leaf_entry_id: ProjectEntryId) -> ProjectEntryId {
1334        if let Some(ancestors) = self.ancestors.get(&leaf_entry_id) {
1335            ancestors
1336                .ancestors
1337                .get(ancestors.current_ancestor_depth)
1338                .copied()
1339                .unwrap_or(leaf_entry_id)
1340        } else {
1341            leaf_entry_id
1342        }
1343    }
1344
1345    fn rename_impl(
1346        &mut self,
1347        selection: Option<Range<usize>>,
1348        window: &mut Window,
1349        cx: &mut Context<Self>,
1350    ) {
1351        if let Some(SelectedEntry {
1352            worktree_id,
1353            entry_id,
1354        }) = self.selection
1355        {
1356            if let Some(worktree) = self.project.read(cx).worktree_for_id(worktree_id, cx) {
1357                let sub_entry_id = self.unflatten_entry_id(entry_id);
1358                if let Some(entry) = worktree.read(cx).entry_for_id(sub_entry_id) {
1359                    #[cfg(target_os = "windows")]
1360                    if Some(entry) == worktree.read(cx).root_entry() {
1361                        return;
1362                    }
1363                    self.edit_state = Some(EditState {
1364                        worktree_id,
1365                        entry_id: sub_entry_id,
1366                        leaf_entry_id: Some(entry_id),
1367                        is_dir: entry.is_dir(),
1368                        processing_filename: None,
1369                        previously_focused: None,
1370                        depth: 0,
1371                    });
1372                    let file_name = entry
1373                        .path
1374                        .file_name()
1375                        .map(|s| s.to_string_lossy())
1376                        .unwrap_or_default()
1377                        .to_string();
1378                    let selection = selection.unwrap_or_else(|| {
1379                        let file_stem = entry.path.file_stem().map(|s| s.to_string_lossy());
1380                        let selection_end =
1381                            file_stem.map_or(file_name.len(), |file_stem| file_stem.len());
1382                        0..selection_end
1383                    });
1384                    self.filename_editor.update(cx, |editor, cx| {
1385                        editor.set_text(file_name, window, cx);
1386                        editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
1387                            s.select_ranges([selection])
1388                        });
1389                        window.focus(&editor.focus_handle(cx));
1390                    });
1391                    self.update_visible_entries(None, cx);
1392                    self.autoscroll(cx);
1393                    cx.notify();
1394                }
1395            }
1396        }
1397    }
1398
1399    fn rename(&mut self, _: &Rename, window: &mut Window, cx: &mut Context<Self>) {
1400        self.rename_impl(None, window, cx);
1401    }
1402
1403    fn trash(&mut self, action: &Trash, window: &mut Window, cx: &mut Context<Self>) {
1404        self.remove(true, action.skip_prompt, window, cx);
1405    }
1406
1407    fn delete(&mut self, action: &Delete, window: &mut Window, cx: &mut Context<Self>) {
1408        self.remove(false, action.skip_prompt, window, cx);
1409    }
1410
1411    fn remove(
1412        &mut self,
1413        trash: bool,
1414        skip_prompt: bool,
1415        window: &mut Window,
1416        cx: &mut Context<ProjectPanel>,
1417    ) {
1418        maybe!({
1419            let items_to_delete = self.disjoint_entries(cx);
1420            if items_to_delete.is_empty() {
1421                return None;
1422            }
1423            let project = self.project.read(cx);
1424
1425            let mut dirty_buffers = 0;
1426            let file_paths = items_to_delete
1427                .iter()
1428                .filter_map(|selection| {
1429                    let project_path = project.path_for_entry(selection.entry_id, cx)?;
1430                    dirty_buffers +=
1431                        project.dirty_buffers(cx).any(|path| path == project_path) as usize;
1432                    Some((
1433                        selection.entry_id,
1434                        project_path
1435                            .path
1436                            .file_name()?
1437                            .to_string_lossy()
1438                            .into_owned(),
1439                    ))
1440                })
1441                .collect::<Vec<_>>();
1442            if file_paths.is_empty() {
1443                return None;
1444            }
1445            let answer = if !skip_prompt {
1446                let operation = if trash { "Trash" } else { "Delete" };
1447                let prompt = match file_paths.first() {
1448                    Some((_, path)) if file_paths.len() == 1 => {
1449                        let unsaved_warning = if dirty_buffers > 0 {
1450                            "\n\nIt has unsaved changes, which will be lost."
1451                        } else {
1452                            ""
1453                        };
1454
1455                        format!("{operation} {path}?{unsaved_warning}")
1456                    }
1457                    _ => {
1458                        const CUTOFF_POINT: usize = 10;
1459                        let names = if file_paths.len() > CUTOFF_POINT {
1460                            let truncated_path_counts = file_paths.len() - CUTOFF_POINT;
1461                            let mut paths = file_paths
1462                                .iter()
1463                                .map(|(_, path)| path.clone())
1464                                .take(CUTOFF_POINT)
1465                                .collect::<Vec<_>>();
1466                            paths.truncate(CUTOFF_POINT);
1467                            if truncated_path_counts == 1 {
1468                                paths.push(".. 1 file not shown".into());
1469                            } else {
1470                                paths.push(format!(".. {} files not shown", truncated_path_counts));
1471                            }
1472                            paths
1473                        } else {
1474                            file_paths.iter().map(|(_, path)| path.clone()).collect()
1475                        };
1476                        let unsaved_warning = if dirty_buffers == 0 {
1477                            String::new()
1478                        } else if dirty_buffers == 1 {
1479                            "\n\n1 of these has unsaved changes, which will be lost.".to_string()
1480                        } else {
1481                            format!("\n\n{dirty_buffers} of these have unsaved changes, which will be lost.")
1482                        };
1483
1484                        format!(
1485                            "Do you want to {} the following {} files?\n{}{unsaved_warning}",
1486                            operation.to_lowercase(),
1487                            file_paths.len(),
1488                            names.join("\n")
1489                        )
1490                    }
1491                };
1492                Some(window.prompt(PromptLevel::Info, &prompt, None, &[operation, "Cancel"], cx))
1493            } else {
1494                None
1495            };
1496            let next_selection = self.find_next_selection_after_deletion(items_to_delete, cx);
1497            cx.spawn_in(window, |panel, mut cx| async move {
1498                if let Some(answer) = answer {
1499                    if answer.await != Ok(0) {
1500                        return anyhow::Ok(());
1501                    }
1502                }
1503                for (entry_id, _) in file_paths {
1504                    panel
1505                        .update(&mut cx, |panel, cx| {
1506                            panel
1507                                .project
1508                                .update(cx, |project, cx| project.delete_entry(entry_id, trash, cx))
1509                                .context("no such entry")
1510                        })??
1511                        .await?;
1512                }
1513                panel.update_in(&mut cx, |panel, window, cx| {
1514                    if let Some(next_selection) = next_selection {
1515                        panel.selection = Some(next_selection);
1516                        panel.autoscroll(cx);
1517                    } else {
1518                        panel.select_last(&SelectLast {}, window, cx);
1519                    }
1520                })?;
1521                Ok(())
1522            })
1523            .detach_and_log_err(cx);
1524            Some(())
1525        });
1526    }
1527
1528    fn find_next_selection_after_deletion(
1529        &self,
1530        sanitized_entries: BTreeSet<SelectedEntry>,
1531        cx: &mut Context<Self>,
1532    ) -> Option<SelectedEntry> {
1533        if sanitized_entries.is_empty() {
1534            return None;
1535        }
1536
1537        let project = self.project.read(cx);
1538        let (worktree_id, worktree) = sanitized_entries
1539            .iter()
1540            .map(|entry| entry.worktree_id)
1541            .filter_map(|id| project.worktree_for_id(id, cx).map(|w| (id, w.read(cx))))
1542            .max_by(|(_, a), (_, b)| a.root_name().cmp(b.root_name()))?;
1543
1544        let marked_entries_in_worktree = sanitized_entries
1545            .iter()
1546            .filter(|e| e.worktree_id == worktree_id)
1547            .collect::<HashSet<_>>();
1548        let latest_entry = marked_entries_in_worktree
1549            .iter()
1550            .max_by(|a, b| {
1551                match (
1552                    worktree.entry_for_id(a.entry_id),
1553                    worktree.entry_for_id(b.entry_id),
1554                ) {
1555                    (Some(a), Some(b)) => {
1556                        compare_paths((&a.path, a.is_file()), (&b.path, b.is_file()))
1557                    }
1558                    _ => cmp::Ordering::Equal,
1559                }
1560            })
1561            .and_then(|e| worktree.entry_for_id(e.entry_id))?;
1562
1563        let parent_path = latest_entry.path.parent()?;
1564        let parent_entry = worktree.entry_for_path(parent_path)?;
1565
1566        // Remove all siblings that are being deleted except the last marked entry
1567        let mut siblings: Vec<_> = worktree
1568            .snapshot()
1569            .child_entries(parent_path)
1570            .with_git_statuses()
1571            .filter(|sibling| {
1572                sibling.id == latest_entry.id
1573                    || !marked_entries_in_worktree.contains(&&SelectedEntry {
1574                        worktree_id,
1575                        entry_id: sibling.id,
1576                    })
1577            })
1578            .map(|entry| entry.to_owned())
1579            .collect();
1580
1581        project::sort_worktree_entries(&mut siblings);
1582        let sibling_entry_index = siblings
1583            .iter()
1584            .position(|sibling| sibling.id == latest_entry.id)?;
1585
1586        if let Some(next_sibling) = sibling_entry_index
1587            .checked_add(1)
1588            .and_then(|i| siblings.get(i))
1589        {
1590            return Some(SelectedEntry {
1591                worktree_id,
1592                entry_id: next_sibling.id,
1593            });
1594        }
1595        if let Some(prev_sibling) = sibling_entry_index
1596            .checked_sub(1)
1597            .and_then(|i| siblings.get(i))
1598        {
1599            return Some(SelectedEntry {
1600                worktree_id,
1601                entry_id: prev_sibling.id,
1602            });
1603        }
1604        // No neighbour sibling found, fall back to parent
1605        Some(SelectedEntry {
1606            worktree_id,
1607            entry_id: parent_entry.id,
1608        })
1609    }
1610
1611    fn unfold_directory(&mut self, _: &UnfoldDirectory, _: &mut Window, cx: &mut Context<Self>) {
1612        if let Some((worktree, entry)) = self.selected_entry(cx) {
1613            self.unfolded_dir_ids.insert(entry.id);
1614
1615            let snapshot = worktree.snapshot();
1616            let mut parent_path = entry.path.parent();
1617            while let Some(path) = parent_path {
1618                if let Some(parent_entry) = worktree.entry_for_path(path) {
1619                    let mut children_iter = snapshot.child_entries(path);
1620
1621                    if children_iter.by_ref().take(2).count() > 1 {
1622                        break;
1623                    }
1624
1625                    self.unfolded_dir_ids.insert(parent_entry.id);
1626                    parent_path = path.parent();
1627                } else {
1628                    break;
1629                }
1630            }
1631
1632            self.update_visible_entries(None, cx);
1633            self.autoscroll(cx);
1634            cx.notify();
1635        }
1636    }
1637
1638    fn fold_directory(&mut self, _: &FoldDirectory, _: &mut Window, cx: &mut Context<Self>) {
1639        if let Some((worktree, entry)) = self.selected_entry(cx) {
1640            self.unfolded_dir_ids.remove(&entry.id);
1641
1642            let snapshot = worktree.snapshot();
1643            let mut path = &*entry.path;
1644            loop {
1645                let mut child_entries_iter = snapshot.child_entries(path);
1646                if let Some(child) = child_entries_iter.next() {
1647                    if child_entries_iter.next().is_none() && child.is_dir() {
1648                        self.unfolded_dir_ids.remove(&child.id);
1649                        path = &*child.path;
1650                    } else {
1651                        break;
1652                    }
1653                } else {
1654                    break;
1655                }
1656            }
1657
1658            self.update_visible_entries(None, cx);
1659            self.autoscroll(cx);
1660            cx.notify();
1661        }
1662    }
1663
1664    fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
1665        if let Some(edit_state) = &self.edit_state {
1666            if edit_state.processing_filename.is_none() {
1667                self.filename_editor.update(cx, |editor, cx| {
1668                    editor.move_to_end_of_line(
1669                        &editor::actions::MoveToEndOfLine {
1670                            stop_at_soft_wraps: false,
1671                        },
1672                        window,
1673                        cx,
1674                    );
1675                });
1676                return;
1677            }
1678        }
1679        if let Some(selection) = self.selection {
1680            let (mut worktree_ix, mut entry_ix, _) =
1681                self.index_for_selection(selection).unwrap_or_default();
1682            if let Some((_, worktree_entries, _)) = self.visible_entries.get(worktree_ix) {
1683                if entry_ix + 1 < worktree_entries.len() {
1684                    entry_ix += 1;
1685                } else {
1686                    worktree_ix += 1;
1687                    entry_ix = 0;
1688                }
1689            }
1690
1691            if let Some((worktree_id, worktree_entries, _)) = self.visible_entries.get(worktree_ix)
1692            {
1693                if let Some(entry) = worktree_entries.get(entry_ix) {
1694                    let selection = SelectedEntry {
1695                        worktree_id: *worktree_id,
1696                        entry_id: entry.id,
1697                    };
1698                    self.selection = Some(selection);
1699                    if window.modifiers().shift {
1700                        self.marked_entries.insert(selection);
1701                    }
1702
1703                    self.autoscroll(cx);
1704                    cx.notify();
1705                }
1706            }
1707        } else {
1708            self.select_first(&SelectFirst {}, window, cx);
1709        }
1710    }
1711
1712    fn select_prev_diagnostic(
1713        &mut self,
1714        _: &SelectPrevDiagnostic,
1715        _: &mut Window,
1716        cx: &mut Context<Self>,
1717    ) {
1718        let selection = self.find_entry(
1719            self.selection.as_ref(),
1720            true,
1721            |entry, worktree_id| {
1722                (self.selection.is_none()
1723                    || self.selection.is_some_and(|selection| {
1724                        if selection.worktree_id == worktree_id {
1725                            selection.entry_id != entry.id
1726                        } else {
1727                            true
1728                        }
1729                    }))
1730                    && entry.is_file()
1731                    && self
1732                        .diagnostics
1733                        .contains_key(&(worktree_id, entry.path.to_path_buf()))
1734            },
1735            cx,
1736        );
1737
1738        if let Some(selection) = selection {
1739            self.selection = Some(selection);
1740            self.expand_entry(selection.worktree_id, selection.entry_id, cx);
1741            self.update_visible_entries(Some((selection.worktree_id, selection.entry_id)), cx);
1742            self.autoscroll(cx);
1743            cx.notify();
1744        }
1745    }
1746
1747    fn select_next_diagnostic(
1748        &mut self,
1749        _: &SelectNextDiagnostic,
1750        _: &mut Window,
1751        cx: &mut Context<Self>,
1752    ) {
1753        let selection = self.find_entry(
1754            self.selection.as_ref(),
1755            false,
1756            |entry, worktree_id| {
1757                (self.selection.is_none()
1758                    || self.selection.is_some_and(|selection| {
1759                        if selection.worktree_id == worktree_id {
1760                            selection.entry_id != entry.id
1761                        } else {
1762                            true
1763                        }
1764                    }))
1765                    && entry.is_file()
1766                    && self
1767                        .diagnostics
1768                        .contains_key(&(worktree_id, entry.path.to_path_buf()))
1769            },
1770            cx,
1771        );
1772
1773        if let Some(selection) = selection {
1774            self.selection = Some(selection);
1775            self.expand_entry(selection.worktree_id, selection.entry_id, cx);
1776            self.update_visible_entries(Some((selection.worktree_id, selection.entry_id)), cx);
1777            self.autoscroll(cx);
1778            cx.notify();
1779        }
1780    }
1781
1782    fn select_prev_git_entry(
1783        &mut self,
1784        _: &SelectPrevGitEntry,
1785        _: &mut Window,
1786        cx: &mut Context<Self>,
1787    ) {
1788        let selection = self.find_entry(
1789            self.selection.as_ref(),
1790            true,
1791            |entry, worktree_id| {
1792                (self.selection.is_none()
1793                    || self.selection.is_some_and(|selection| {
1794                        if selection.worktree_id == worktree_id {
1795                            selection.entry_id != entry.id
1796                        } else {
1797                            true
1798                        }
1799                    }))
1800                    && entry.is_file()
1801                    && entry.git_summary.index.modified + entry.git_summary.worktree.modified > 0
1802            },
1803            cx,
1804        );
1805
1806        if let Some(selection) = selection {
1807            self.selection = Some(selection);
1808            self.expand_entry(selection.worktree_id, selection.entry_id, cx);
1809            self.update_visible_entries(Some((selection.worktree_id, selection.entry_id)), cx);
1810            self.autoscroll(cx);
1811            cx.notify();
1812        }
1813    }
1814
1815    fn select_prev_directory(
1816        &mut self,
1817        _: &SelectPrevDirectory,
1818        _: &mut Window,
1819        cx: &mut Context<Self>,
1820    ) {
1821        let selection = self.find_visible_entry(
1822            self.selection.as_ref(),
1823            true,
1824            |entry, worktree_id| {
1825                (self.selection.is_none()
1826                    || self.selection.is_some_and(|selection| {
1827                        if selection.worktree_id == worktree_id {
1828                            selection.entry_id != entry.id
1829                        } else {
1830                            true
1831                        }
1832                    }))
1833                    && entry.is_dir()
1834            },
1835            cx,
1836        );
1837
1838        if let Some(selection) = selection {
1839            self.selection = Some(selection);
1840            self.autoscroll(cx);
1841            cx.notify();
1842        }
1843    }
1844
1845    fn select_next_directory(
1846        &mut self,
1847        _: &SelectNextDirectory,
1848        _: &mut Window,
1849        cx: &mut Context<Self>,
1850    ) {
1851        let selection = self.find_visible_entry(
1852            self.selection.as_ref(),
1853            false,
1854            |entry, worktree_id| {
1855                (self.selection.is_none()
1856                    || self.selection.is_some_and(|selection| {
1857                        if selection.worktree_id == worktree_id {
1858                            selection.entry_id != entry.id
1859                        } else {
1860                            true
1861                        }
1862                    }))
1863                    && entry.is_dir()
1864            },
1865            cx,
1866        );
1867
1868        if let Some(selection) = selection {
1869            self.selection = Some(selection);
1870            self.autoscroll(cx);
1871            cx.notify();
1872        }
1873    }
1874
1875    fn select_next_git_entry(
1876        &mut self,
1877        _: &SelectNextGitEntry,
1878        _: &mut Window,
1879        cx: &mut Context<Self>,
1880    ) {
1881        let selection = self.find_entry(
1882            self.selection.as_ref(),
1883            false,
1884            |entry, worktree_id| {
1885                (self.selection.is_none()
1886                    || self.selection.is_some_and(|selection| {
1887                        if selection.worktree_id == worktree_id {
1888                            selection.entry_id != entry.id
1889                        } else {
1890                            true
1891                        }
1892                    }))
1893                    && entry.is_file()
1894                    && entry.git_summary.index.modified + entry.git_summary.worktree.modified > 0
1895            },
1896            cx,
1897        );
1898
1899        if let Some(selection) = selection {
1900            self.selection = Some(selection);
1901            self.expand_entry(selection.worktree_id, selection.entry_id, cx);
1902            self.update_visible_entries(Some((selection.worktree_id, selection.entry_id)), cx);
1903            self.autoscroll(cx);
1904            cx.notify();
1905        }
1906    }
1907
1908    fn select_parent(&mut self, _: &SelectParent, window: &mut Window, cx: &mut Context<Self>) {
1909        if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
1910            if let Some(parent) = entry.path.parent() {
1911                let worktree = worktree.read(cx);
1912                if let Some(parent_entry) = worktree.entry_for_path(parent) {
1913                    self.selection = Some(SelectedEntry {
1914                        worktree_id: worktree.id(),
1915                        entry_id: parent_entry.id,
1916                    });
1917                    self.autoscroll(cx);
1918                    cx.notify();
1919                }
1920            }
1921        } else {
1922            self.select_first(&SelectFirst {}, window, cx);
1923        }
1924    }
1925
1926    fn select_first(&mut self, _: &SelectFirst, window: &mut Window, cx: &mut Context<Self>) {
1927        let worktree = self
1928            .visible_entries
1929            .first()
1930            .and_then(|(worktree_id, _, _)| {
1931                self.project.read(cx).worktree_for_id(*worktree_id, cx)
1932            });
1933        if let Some(worktree) = worktree {
1934            let worktree = worktree.read(cx);
1935            let worktree_id = worktree.id();
1936            if let Some(root_entry) = worktree.root_entry() {
1937                let selection = SelectedEntry {
1938                    worktree_id,
1939                    entry_id: root_entry.id,
1940                };
1941                self.selection = Some(selection);
1942                if window.modifiers().shift {
1943                    self.marked_entries.insert(selection);
1944                }
1945                self.autoscroll(cx);
1946                cx.notify();
1947            }
1948        }
1949    }
1950
1951    fn select_last(&mut self, _: &SelectLast, _: &mut Window, cx: &mut Context<Self>) {
1952        if let Some((worktree_id, visible_worktree_entries, _)) = self.visible_entries.last() {
1953            let worktree = self.project.read(cx).worktree_for_id(*worktree_id, cx);
1954            if let (Some(worktree), Some(entry)) = (worktree, visible_worktree_entries.last()) {
1955                let worktree = worktree.read(cx);
1956                if let Some(entry) = worktree.entry_for_id(entry.id) {
1957                    let selection = SelectedEntry {
1958                        worktree_id: *worktree_id,
1959                        entry_id: entry.id,
1960                    };
1961                    self.selection = Some(selection);
1962                    self.autoscroll(cx);
1963                    cx.notify();
1964                }
1965            }
1966        }
1967    }
1968
1969    fn autoscroll(&mut self, cx: &mut Context<Self>) {
1970        if let Some((_, _, index)) = self.selection.and_then(|s| self.index_for_selection(s)) {
1971            self.scroll_handle
1972                .scroll_to_item(index, ScrollStrategy::Center);
1973            cx.notify();
1974        }
1975    }
1976
1977    fn cut(&mut self, _: &Cut, _: &mut Window, cx: &mut Context<Self>) {
1978        let entries = self.disjoint_entries(cx);
1979        if !entries.is_empty() {
1980            self.clipboard = Some(ClipboardEntry::Cut(entries));
1981            cx.notify();
1982        }
1983    }
1984
1985    fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context<Self>) {
1986        let entries = self.disjoint_entries(cx);
1987        if !entries.is_empty() {
1988            self.clipboard = Some(ClipboardEntry::Copied(entries));
1989            cx.notify();
1990        }
1991    }
1992
1993    fn create_paste_path(
1994        &self,
1995        source: &SelectedEntry,
1996        (worktree, target_entry): (Entity<Worktree>, &Entry),
1997        cx: &App,
1998    ) -> Option<(PathBuf, Option<Range<usize>>)> {
1999        let mut new_path = target_entry.path.to_path_buf();
2000        // If we're pasting into a file, or a directory into itself, go up one level.
2001        if target_entry.is_file() || (target_entry.is_dir() && target_entry.id == source.entry_id) {
2002            new_path.pop();
2003        }
2004        let clipboard_entry_file_name = self
2005            .project
2006            .read(cx)
2007            .path_for_entry(source.entry_id, cx)?
2008            .path
2009            .file_name()?
2010            .to_os_string();
2011        new_path.push(&clipboard_entry_file_name);
2012        let extension = new_path.extension().map(|e| e.to_os_string());
2013        let file_name_without_extension = Path::new(&clipboard_entry_file_name).file_stem()?;
2014        let file_name_len = file_name_without_extension.to_string_lossy().len();
2015        let mut disambiguation_range = None;
2016        let mut ix = 0;
2017        {
2018            let worktree = worktree.read(cx);
2019            while worktree.entry_for_path(&new_path).is_some() {
2020                new_path.pop();
2021
2022                let mut new_file_name = file_name_without_extension.to_os_string();
2023
2024                let disambiguation = " copy";
2025                let mut disambiguation_len = disambiguation.len();
2026
2027                new_file_name.push(disambiguation);
2028
2029                if ix > 0 {
2030                    let extra_disambiguation = format!(" {}", ix);
2031                    disambiguation_len += extra_disambiguation.len();
2032
2033                    new_file_name.push(extra_disambiguation);
2034                }
2035                if let Some(extension) = extension.as_ref() {
2036                    new_file_name.push(".");
2037                    new_file_name.push(extension);
2038                }
2039
2040                new_path.push(new_file_name);
2041                disambiguation_range = Some(file_name_len..(file_name_len + disambiguation_len));
2042                ix += 1;
2043            }
2044        }
2045        Some((new_path, disambiguation_range))
2046    }
2047
2048    fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
2049        maybe!({
2050            let (worktree, entry) = self.selected_entry_handle(cx)?;
2051            let entry = entry.clone();
2052            let worktree_id = worktree.read(cx).id();
2053            let clipboard_entries = self
2054                .clipboard
2055                .as_ref()
2056                .filter(|clipboard| !clipboard.items().is_empty())?;
2057            enum PasteTask {
2058                Rename(Task<Result<CreatedEntry>>),
2059                Copy(Task<Result<Option<Entry>>>),
2060            }
2061            let mut paste_entry_tasks: IndexMap<(ProjectEntryId, bool), PasteTask> =
2062                IndexMap::default();
2063            let mut disambiguation_range = None;
2064            let clip_is_cut = clipboard_entries.is_cut();
2065            for clipboard_entry in clipboard_entries.items() {
2066                let (new_path, new_disambiguation_range) =
2067                    self.create_paste_path(clipboard_entry, self.selected_sub_entry(cx)?, cx)?;
2068                let clip_entry_id = clipboard_entry.entry_id;
2069                let is_same_worktree = clipboard_entry.worktree_id == worktree_id;
2070                let relative_worktree_source_path = if !is_same_worktree {
2071                    let target_base_path = worktree.read(cx).abs_path();
2072                    let clipboard_project_path =
2073                        self.project.read(cx).path_for_entry(clip_entry_id, cx)?;
2074                    let clipboard_abs_path = self
2075                        .project
2076                        .read(cx)
2077                        .absolute_path(&clipboard_project_path, cx)?;
2078                    Some(relativize_path(
2079                        &target_base_path,
2080                        clipboard_abs_path.as_path(),
2081                    ))
2082                } else {
2083                    None
2084                };
2085                let task = if clip_is_cut && is_same_worktree {
2086                    let task = self.project.update(cx, |project, cx| {
2087                        project.rename_entry(clip_entry_id, new_path, cx)
2088                    });
2089                    PasteTask::Rename(task)
2090                } else {
2091                    let entry_id = if is_same_worktree {
2092                        clip_entry_id
2093                    } else {
2094                        entry.id
2095                    };
2096                    let task = self.project.update(cx, |project, cx| {
2097                        project.copy_entry(entry_id, relative_worktree_source_path, new_path, cx)
2098                    });
2099                    PasteTask::Copy(task)
2100                };
2101                let needs_delete = !is_same_worktree && clip_is_cut;
2102                paste_entry_tasks.insert((clip_entry_id, needs_delete), task);
2103                disambiguation_range = new_disambiguation_range.or(disambiguation_range);
2104            }
2105
2106            let item_count = paste_entry_tasks.len();
2107
2108            cx.spawn_in(window, |project_panel, mut cx| async move {
2109                let mut last_succeed = None;
2110                let mut need_delete_ids = Vec::new();
2111                for ((entry_id, need_delete), task) in paste_entry_tasks.into_iter() {
2112                    match task {
2113                        PasteTask::Rename(task) => {
2114                            if let Some(CreatedEntry::Included(entry)) = task.await.log_err() {
2115                                last_succeed = Some(entry);
2116                            }
2117                        }
2118                        PasteTask::Copy(task) => {
2119                            if let Some(Some(entry)) = task.await.log_err() {
2120                                last_succeed = Some(entry);
2121                                if need_delete {
2122                                    need_delete_ids.push(entry_id);
2123                                }
2124                            }
2125                        }
2126                    }
2127                }
2128                // remove entry for cut in difference worktree
2129                for entry_id in need_delete_ids {
2130                    project_panel
2131                        .update(&mut cx, |project_panel, cx| {
2132                            project_panel
2133                                .project
2134                                .update(cx, |project, cx| project.delete_entry(entry_id, true, cx))
2135                                .ok_or_else(|| anyhow!("no such entry"))
2136                        })??
2137                        .await?;
2138                }
2139                // update selection
2140                if let Some(entry) = last_succeed {
2141                    project_panel
2142                        .update_in(&mut cx, |project_panel, window, cx| {
2143                            project_panel.selection = Some(SelectedEntry {
2144                                worktree_id,
2145                                entry_id: entry.id,
2146                            });
2147
2148                            if item_count == 1 {
2149                                // open entry if not dir, and only focus if rename is not pending
2150                                if !entry.is_dir() {
2151                                    project_panel.open_entry(
2152                                        entry.id,
2153                                        disambiguation_range.is_none(),
2154                                        false,
2155                                        cx,
2156                                    );
2157                                }
2158
2159                                // if only one entry was pasted and it was disambiguated, open the rename editor
2160                                if disambiguation_range.is_some() {
2161                                    cx.defer_in(window, |this, window, cx| {
2162                                        this.rename_impl(disambiguation_range, window, cx);
2163                                    });
2164                                }
2165                            }
2166                        })
2167                        .ok();
2168                }
2169
2170                anyhow::Ok(())
2171            })
2172            .detach_and_log_err(cx);
2173
2174            self.expand_entry(worktree_id, entry.id, cx);
2175            Some(())
2176        });
2177    }
2178
2179    fn duplicate(&mut self, _: &Duplicate, window: &mut Window, cx: &mut Context<Self>) {
2180        self.copy(&Copy {}, window, cx);
2181        self.paste(&Paste {}, window, cx);
2182    }
2183
2184    fn copy_path(
2185        &mut self,
2186        _: &zed_actions::workspace::CopyPath,
2187        _: &mut Window,
2188        cx: &mut Context<Self>,
2189    ) {
2190        let abs_file_paths = {
2191            let project = self.project.read(cx);
2192            self.effective_entries()
2193                .into_iter()
2194                .filter_map(|entry| {
2195                    let entry_path = project.path_for_entry(entry.entry_id, cx)?.path;
2196                    Some(
2197                        project
2198                            .worktree_for_id(entry.worktree_id, cx)?
2199                            .read(cx)
2200                            .abs_path()
2201                            .join(entry_path)
2202                            .to_string_lossy()
2203                            .to_string(),
2204                    )
2205                })
2206                .collect::<Vec<_>>()
2207        };
2208        if !abs_file_paths.is_empty() {
2209            cx.write_to_clipboard(ClipboardItem::new_string(abs_file_paths.join("\n")));
2210        }
2211    }
2212
2213    fn copy_relative_path(
2214        &mut self,
2215        _: &zed_actions::workspace::CopyRelativePath,
2216        _: &mut Window,
2217        cx: &mut Context<Self>,
2218    ) {
2219        let file_paths = {
2220            let project = self.project.read(cx);
2221            self.effective_entries()
2222                .into_iter()
2223                .filter_map(|entry| {
2224                    Some(
2225                        project
2226                            .path_for_entry(entry.entry_id, cx)?
2227                            .path
2228                            .to_string_lossy()
2229                            .to_string(),
2230                    )
2231                })
2232                .collect::<Vec<_>>()
2233        };
2234        if !file_paths.is_empty() {
2235            cx.write_to_clipboard(ClipboardItem::new_string(file_paths.join("\n")));
2236        }
2237    }
2238
2239    fn reveal_in_finder(
2240        &mut self,
2241        _: &RevealInFileManager,
2242        _: &mut Window,
2243        cx: &mut Context<Self>,
2244    ) {
2245        if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
2246            cx.reveal_path(&worktree.read(cx).abs_path().join(&entry.path));
2247        }
2248    }
2249
2250    fn remove_from_project(
2251        &mut self,
2252        _: &RemoveFromProject,
2253        _window: &mut Window,
2254        cx: &mut Context<Self>,
2255    ) {
2256        for entry in self.effective_entries().iter() {
2257            let worktree_id = entry.worktree_id;
2258            self.project
2259                .update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
2260        }
2261    }
2262
2263    fn open_system(&mut self, _: &OpenWithSystem, _: &mut Window, cx: &mut Context<Self>) {
2264        if let Some((worktree, entry)) = self.selected_entry(cx) {
2265            let abs_path = worktree.abs_path().join(&entry.path);
2266            cx.open_with_system(&abs_path);
2267        }
2268    }
2269
2270    fn open_in_terminal(
2271        &mut self,
2272        _: &OpenInTerminal,
2273        window: &mut Window,
2274        cx: &mut Context<Self>,
2275    ) {
2276        if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
2277            let abs_path = match &entry.canonical_path {
2278                Some(canonical_path) => Some(canonical_path.to_path_buf()),
2279                None => worktree.read(cx).absolutize(&entry.path).ok(),
2280            };
2281
2282            let working_directory = if entry.is_dir() {
2283                abs_path
2284            } else {
2285                abs_path.and_then(|path| Some(path.parent()?.to_path_buf()))
2286            };
2287            if let Some(working_directory) = working_directory {
2288                window.dispatch_action(
2289                    workspace::OpenTerminal { working_directory }.boxed_clone(),
2290                    cx,
2291                )
2292            }
2293        }
2294    }
2295
2296    pub fn new_search_in_directory(
2297        &mut self,
2298        _: &NewSearchInDirectory,
2299        window: &mut Window,
2300        cx: &mut Context<Self>,
2301    ) {
2302        if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
2303            let dir_path = if entry.is_dir() {
2304                entry.path.clone()
2305            } else {
2306                // entry is a file, use its parent directory
2307                match entry.path.parent() {
2308                    Some(parent) => Arc::from(parent),
2309                    None => {
2310                        // File at root, open search with empty filter
2311                        self.workspace
2312                            .update(cx, |workspace, cx| {
2313                                search::ProjectSearchView::new_search_in_directory(
2314                                    workspace,
2315                                    Path::new(""),
2316                                    window,
2317                                    cx,
2318                                );
2319                            })
2320                            .ok();
2321                        return;
2322                    }
2323                }
2324            };
2325
2326            let include_root = self.project.read(cx).visible_worktrees(cx).count() > 1;
2327            let dir_path = if include_root {
2328                let mut full_path = PathBuf::from(worktree.read(cx).root_name());
2329                full_path.push(&dir_path);
2330                Arc::from(full_path)
2331            } else {
2332                dir_path
2333            };
2334
2335            self.workspace
2336                .update(cx, |workspace, cx| {
2337                    search::ProjectSearchView::new_search_in_directory(
2338                        workspace, &dir_path, window, cx,
2339                    );
2340                })
2341                .ok();
2342        }
2343    }
2344
2345    fn move_entry(
2346        &mut self,
2347        entry_to_move: ProjectEntryId,
2348        destination: ProjectEntryId,
2349        destination_is_file: bool,
2350        cx: &mut Context<Self>,
2351    ) {
2352        if self
2353            .project
2354            .read(cx)
2355            .entry_is_worktree_root(entry_to_move, cx)
2356        {
2357            self.move_worktree_root(entry_to_move, destination, cx)
2358        } else {
2359            self.move_worktree_entry(entry_to_move, destination, destination_is_file, cx)
2360        }
2361    }
2362
2363    fn move_worktree_root(
2364        &mut self,
2365        entry_to_move: ProjectEntryId,
2366        destination: ProjectEntryId,
2367        cx: &mut Context<Self>,
2368    ) {
2369        self.project.update(cx, |project, cx| {
2370            let Some(worktree_to_move) = project.worktree_for_entry(entry_to_move, cx) else {
2371                return;
2372            };
2373            let Some(destination_worktree) = project.worktree_for_entry(destination, cx) else {
2374                return;
2375            };
2376
2377            let worktree_id = worktree_to_move.read(cx).id();
2378            let destination_id = destination_worktree.read(cx).id();
2379
2380            project
2381                .move_worktree(worktree_id, destination_id, cx)
2382                .log_err();
2383        });
2384    }
2385
2386    fn move_worktree_entry(
2387        &mut self,
2388        entry_to_move: ProjectEntryId,
2389        destination: ProjectEntryId,
2390        destination_is_file: bool,
2391        cx: &mut Context<Self>,
2392    ) {
2393        if entry_to_move == destination {
2394            return;
2395        }
2396
2397        let destination_worktree = self.project.update(cx, |project, cx| {
2398            let entry_path = project.path_for_entry(entry_to_move, cx)?;
2399            let destination_entry_path = project.path_for_entry(destination, cx)?.path.clone();
2400
2401            let mut destination_path = destination_entry_path.as_ref();
2402            if destination_is_file {
2403                destination_path = destination_path.parent()?;
2404            }
2405
2406            let mut new_path = destination_path.to_path_buf();
2407            new_path.push(entry_path.path.file_name()?);
2408            if new_path != entry_path.path.as_ref() {
2409                let task = project.rename_entry(entry_to_move, new_path, cx);
2410                cx.foreground_executor().spawn(task).detach_and_log_err(cx);
2411            }
2412
2413            project.worktree_id_for_entry(destination, cx)
2414        });
2415
2416        if let Some(destination_worktree) = destination_worktree {
2417            self.expand_entry(destination_worktree, destination, cx);
2418        }
2419    }
2420
2421    fn index_for_selection(&self, selection: SelectedEntry) -> Option<(usize, usize, usize)> {
2422        let mut entry_index = 0;
2423        let mut visible_entries_index = 0;
2424        for (worktree_index, (worktree_id, worktree_entries, _)) in
2425            self.visible_entries.iter().enumerate()
2426        {
2427            if *worktree_id == selection.worktree_id {
2428                for entry in worktree_entries {
2429                    if entry.id == selection.entry_id {
2430                        return Some((worktree_index, entry_index, visible_entries_index));
2431                    } else {
2432                        visible_entries_index += 1;
2433                        entry_index += 1;
2434                    }
2435                }
2436                break;
2437            } else {
2438                visible_entries_index += worktree_entries.len();
2439            }
2440        }
2441        None
2442    }
2443
2444    fn disjoint_entries(&self, cx: &App) -> BTreeSet<SelectedEntry> {
2445        let marked_entries = self.effective_entries();
2446        let mut sanitized_entries = BTreeSet::new();
2447        if marked_entries.is_empty() {
2448            return sanitized_entries;
2449        }
2450
2451        let project = self.project.read(cx);
2452        let marked_entries_by_worktree: HashMap<WorktreeId, Vec<SelectedEntry>> = marked_entries
2453            .into_iter()
2454            .filter(|entry| !project.entry_is_worktree_root(entry.entry_id, cx))
2455            .fold(HashMap::default(), |mut map, entry| {
2456                map.entry(entry.worktree_id).or_default().push(entry);
2457                map
2458            });
2459
2460        for (worktree_id, marked_entries) in marked_entries_by_worktree {
2461            if let Some(worktree) = project.worktree_for_id(worktree_id, cx) {
2462                let worktree = worktree.read(cx);
2463                let marked_dir_paths = marked_entries
2464                    .iter()
2465                    .filter_map(|entry| {
2466                        worktree.entry_for_id(entry.entry_id).and_then(|entry| {
2467                            if entry.is_dir() {
2468                                Some(entry.path.as_ref())
2469                            } else {
2470                                None
2471                            }
2472                        })
2473                    })
2474                    .collect::<BTreeSet<_>>();
2475
2476                sanitized_entries.extend(marked_entries.into_iter().filter(|entry| {
2477                    let Some(entry_info) = worktree.entry_for_id(entry.entry_id) else {
2478                        return false;
2479                    };
2480                    let entry_path = entry_info.path.as_ref();
2481                    let inside_marked_dir = marked_dir_paths.iter().any(|&marked_dir_path| {
2482                        entry_path != marked_dir_path && entry_path.starts_with(marked_dir_path)
2483                    });
2484                    !inside_marked_dir
2485                }));
2486            }
2487        }
2488
2489        sanitized_entries
2490    }
2491
2492    fn effective_entries(&self) -> BTreeSet<SelectedEntry> {
2493        if let Some(selection) = self.selection {
2494            let selection = SelectedEntry {
2495                entry_id: self.resolve_entry(selection.entry_id),
2496                worktree_id: selection.worktree_id,
2497            };
2498
2499            // Default to using just the selected item when nothing is marked.
2500            if self.marked_entries.is_empty() {
2501                return BTreeSet::from([selection]);
2502            }
2503
2504            // Allow operating on the selected item even when something else is marked,
2505            // making it easier to perform one-off actions without clearing a mark.
2506            if self.marked_entries.len() == 1 && !self.marked_entries.contains(&selection) {
2507                return BTreeSet::from([selection]);
2508            }
2509        }
2510
2511        // Return only marked entries since we've already handled special cases where
2512        // only selection should take precedence. At this point, marked entries may or
2513        // may not include the current selection, which is intentional.
2514        self.marked_entries
2515            .iter()
2516            .map(|entry| SelectedEntry {
2517                entry_id: self.resolve_entry(entry.entry_id),
2518                worktree_id: entry.worktree_id,
2519            })
2520            .collect::<BTreeSet<_>>()
2521    }
2522
2523    /// Finds the currently selected subentry for a given leaf entry id. If a given entry
2524    /// has no ancestors, the project entry ID that's passed in is returned as-is.
2525    fn resolve_entry(&self, id: ProjectEntryId) -> ProjectEntryId {
2526        self.ancestors
2527            .get(&id)
2528            .and_then(|ancestors| {
2529                if ancestors.current_ancestor_depth == 0 {
2530                    return None;
2531                }
2532                ancestors.ancestors.get(ancestors.current_ancestor_depth)
2533            })
2534            .copied()
2535            .unwrap_or(id)
2536    }
2537
2538    pub fn selected_entry<'a>(&self, cx: &'a App) -> Option<(&'a Worktree, &'a project::Entry)> {
2539        let (worktree, entry) = self.selected_entry_handle(cx)?;
2540        Some((worktree.read(cx), entry))
2541    }
2542
2543    /// Compared to selected_entry, this function resolves to the currently
2544    /// selected subentry if dir auto-folding is enabled.
2545    fn selected_sub_entry<'a>(
2546        &self,
2547        cx: &'a App,
2548    ) -> Option<(Entity<Worktree>, &'a project::Entry)> {
2549        let (worktree, mut entry) = self.selected_entry_handle(cx)?;
2550
2551        let resolved_id = self.resolve_entry(entry.id);
2552        if resolved_id != entry.id {
2553            let worktree = worktree.read(cx);
2554            entry = worktree.entry_for_id(resolved_id)?;
2555        }
2556        Some((worktree, entry))
2557    }
2558    fn selected_entry_handle<'a>(
2559        &self,
2560        cx: &'a App,
2561    ) -> Option<(Entity<Worktree>, &'a project::Entry)> {
2562        let selection = self.selection?;
2563        let project = self.project.read(cx);
2564        let worktree = project.worktree_for_id(selection.worktree_id, cx)?;
2565        let entry = worktree.read(cx).entry_for_id(selection.entry_id)?;
2566        Some((worktree, entry))
2567    }
2568
2569    fn expand_to_selection(&mut self, cx: &mut Context<Self>) -> Option<()> {
2570        let (worktree, entry) = self.selected_entry(cx)?;
2571        let expanded_dir_ids = self.expanded_dir_ids.entry(worktree.id()).or_default();
2572
2573        for path in entry.path.ancestors() {
2574            let Some(entry) = worktree.entry_for_path(path) else {
2575                continue;
2576            };
2577            if entry.is_dir() {
2578                if let Err(idx) = expanded_dir_ids.binary_search(&entry.id) {
2579                    expanded_dir_ids.insert(idx, entry.id);
2580                }
2581            }
2582        }
2583
2584        Some(())
2585    }
2586
2587    fn update_visible_entries(
2588        &mut self,
2589        new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,
2590        cx: &mut Context<Self>,
2591    ) {
2592        let auto_collapse_dirs = ProjectPanelSettings::get_global(cx).auto_fold_dirs;
2593        let project = self.project.read(cx);
2594        self.last_worktree_root_id = project
2595            .visible_worktrees(cx)
2596            .next_back()
2597            .and_then(|worktree| worktree.read(cx).root_entry())
2598            .map(|entry| entry.id);
2599
2600        let old_ancestors = std::mem::take(&mut self.ancestors);
2601        self.visible_entries.clear();
2602        let mut max_width_item = None;
2603        for worktree in project.visible_worktrees(cx) {
2604            let snapshot = worktree.read(cx).snapshot();
2605            let worktree_id = snapshot.id();
2606
2607            let expanded_dir_ids = match self.expanded_dir_ids.entry(worktree_id) {
2608                hash_map::Entry::Occupied(e) => e.into_mut(),
2609                hash_map::Entry::Vacant(e) => {
2610                    // The first time a worktree's root entry becomes available,
2611                    // mark that root entry as expanded.
2612                    if let Some(entry) = snapshot.root_entry() {
2613                        e.insert(vec![entry.id]).as_slice()
2614                    } else {
2615                        &[]
2616                    }
2617                }
2618            };
2619
2620            let mut new_entry_parent_id = None;
2621            let mut new_entry_kind = EntryKind::Dir;
2622            if let Some(edit_state) = &self.edit_state {
2623                if edit_state.worktree_id == worktree_id && edit_state.is_new_entry() {
2624                    new_entry_parent_id = Some(edit_state.entry_id);
2625                    new_entry_kind = if edit_state.is_dir {
2626                        EntryKind::Dir
2627                    } else {
2628                        EntryKind::File
2629                    };
2630                }
2631            }
2632
2633            let mut visible_worktree_entries = Vec::new();
2634            let mut entry_iter = snapshot.entries(true, 0).with_git_statuses();
2635            let mut auto_folded_ancestors = vec![];
2636            while let Some(entry) = entry_iter.entry() {
2637                if auto_collapse_dirs && entry.kind.is_dir() {
2638                    auto_folded_ancestors.push(entry.id);
2639                    if !self.unfolded_dir_ids.contains(&entry.id) {
2640                        if let Some(root_path) = snapshot.root_entry() {
2641                            let mut child_entries = snapshot.child_entries(&entry.path);
2642                            if let Some(child) = child_entries.next() {
2643                                if entry.path != root_path.path
2644                                    && child_entries.next().is_none()
2645                                    && child.kind.is_dir()
2646                                {
2647                                    entry_iter.advance();
2648
2649                                    continue;
2650                                }
2651                            }
2652                        }
2653                    }
2654                    let depth = old_ancestors
2655                        .get(&entry.id)
2656                        .map(|ancestor| ancestor.current_ancestor_depth)
2657                        .unwrap_or_default()
2658                        .min(auto_folded_ancestors.len());
2659                    if let Some(edit_state) = &mut self.edit_state {
2660                        if edit_state.entry_id == entry.id {
2661                            edit_state.depth = depth;
2662                        }
2663                    }
2664                    let mut ancestors = std::mem::take(&mut auto_folded_ancestors);
2665                    if ancestors.len() > 1 {
2666                        ancestors.reverse();
2667                        self.ancestors.insert(
2668                            entry.id,
2669                            FoldedAncestors {
2670                                current_ancestor_depth: depth,
2671                                ancestors,
2672                            },
2673                        );
2674                    }
2675                }
2676                auto_folded_ancestors.clear();
2677                visible_worktree_entries.push(entry.to_owned());
2678                let precedes_new_entry = if let Some(new_entry_id) = new_entry_parent_id {
2679                    entry.id == new_entry_id || {
2680                        self.ancestors.get(&entry.id).map_or(false, |entries| {
2681                            entries
2682                                .ancestors
2683                                .iter()
2684                                .any(|entry_id| *entry_id == new_entry_id)
2685                        })
2686                    }
2687                } else {
2688                    false
2689                };
2690                if precedes_new_entry {
2691                    visible_worktree_entries.push(GitEntry {
2692                        entry: Entry {
2693                            id: NEW_ENTRY_ID,
2694                            kind: new_entry_kind,
2695                            path: entry.path.join("\0").into(),
2696                            inode: 0,
2697                            mtime: entry.mtime,
2698                            size: entry.size,
2699                            is_ignored: entry.is_ignored,
2700                            is_external: false,
2701                            is_private: false,
2702                            is_always_included: entry.is_always_included,
2703                            canonical_path: entry.canonical_path.clone(),
2704                            char_bag: entry.char_bag,
2705                            is_fifo: entry.is_fifo,
2706                        },
2707                        git_summary: entry.git_summary,
2708                    });
2709                }
2710                let worktree_abs_path = worktree.read(cx).abs_path();
2711                let (depth, path) = if Some(entry.entry) == worktree.read(cx).root_entry() {
2712                    let Some(path_name) = worktree_abs_path
2713                        .file_name()
2714                        .with_context(|| {
2715                            format!("Worktree abs path has no file name, root entry: {entry:?}")
2716                        })
2717                        .log_err()
2718                    else {
2719                        continue;
2720                    };
2721                    let path = ArcCow::Borrowed(Path::new(path_name));
2722                    let depth = 0;
2723                    (depth, path)
2724                } else if entry.is_file() {
2725                    let Some(path_name) = entry
2726                        .path
2727                        .file_name()
2728                        .with_context(|| format!("Non-root entry has no file name: {entry:?}"))
2729                        .log_err()
2730                    else {
2731                        continue;
2732                    };
2733                    let path = ArcCow::Borrowed(Path::new(path_name));
2734                    let depth = entry.path.ancestors().count() - 1;
2735                    (depth, path)
2736                } else {
2737                    let path = self
2738                        .ancestors
2739                        .get(&entry.id)
2740                        .and_then(|ancestors| {
2741                            let outermost_ancestor = ancestors.ancestors.last()?;
2742                            let root_folded_entry = worktree
2743                                .read(cx)
2744                                .entry_for_id(*outermost_ancestor)?
2745                                .path
2746                                .as_ref();
2747                            entry
2748                                .path
2749                                .strip_prefix(root_folded_entry)
2750                                .ok()
2751                                .and_then(|suffix| {
2752                                    let full_path = Path::new(root_folded_entry.file_name()?);
2753                                    Some(ArcCow::Owned(Arc::<Path>::from(full_path.join(suffix))))
2754                                })
2755                        })
2756                        .or_else(|| entry.path.file_name().map(Path::new).map(ArcCow::Borrowed))
2757                        .unwrap_or_else(|| ArcCow::Owned(entry.path.clone()));
2758                    let depth = path.components().count();
2759                    (depth, path)
2760                };
2761                let width_estimate = item_width_estimate(
2762                    depth,
2763                    path.to_string_lossy().chars().count(),
2764                    entry.canonical_path.is_some(),
2765                );
2766
2767                match max_width_item.as_mut() {
2768                    Some((id, worktree_id, width)) => {
2769                        if *width < width_estimate {
2770                            *id = entry.id;
2771                            *worktree_id = worktree.read(cx).id();
2772                            *width = width_estimate;
2773                        }
2774                    }
2775                    None => {
2776                        max_width_item = Some((entry.id, worktree.read(cx).id(), width_estimate))
2777                    }
2778                }
2779
2780                if expanded_dir_ids.binary_search(&entry.id).is_err()
2781                    && entry_iter.advance_to_sibling()
2782                {
2783                    continue;
2784                }
2785                entry_iter.advance();
2786            }
2787
2788            project::sort_worktree_entries(&mut visible_worktree_entries);
2789
2790            self.visible_entries
2791                .push((worktree_id, visible_worktree_entries, OnceCell::new()));
2792        }
2793
2794        if let Some((project_entry_id, worktree_id, _)) = max_width_item {
2795            let mut visited_worktrees_length = 0;
2796            let index = self.visible_entries.iter().find_map(|(id, entries, _)| {
2797                if worktree_id == *id {
2798                    entries
2799                        .iter()
2800                        .position(|entry| entry.id == project_entry_id)
2801                } else {
2802                    visited_worktrees_length += entries.len();
2803                    None
2804                }
2805            });
2806            if let Some(index) = index {
2807                self.max_width_item_index = Some(visited_worktrees_length + index);
2808            }
2809        }
2810        if let Some((worktree_id, entry_id)) = new_selected_entry {
2811            self.selection = Some(SelectedEntry {
2812                worktree_id,
2813                entry_id,
2814            });
2815        }
2816    }
2817
2818    fn expand_entry(
2819        &mut self,
2820        worktree_id: WorktreeId,
2821        entry_id: ProjectEntryId,
2822        cx: &mut Context<Self>,
2823    ) {
2824        self.project.update(cx, |project, cx| {
2825            if let Some((worktree, expanded_dir_ids)) = project
2826                .worktree_for_id(worktree_id, cx)
2827                .zip(self.expanded_dir_ids.get_mut(&worktree_id))
2828            {
2829                project.expand_entry(worktree_id, entry_id, cx);
2830                let worktree = worktree.read(cx);
2831
2832                if let Some(mut entry) = worktree.entry_for_id(entry_id) {
2833                    loop {
2834                        if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) {
2835                            expanded_dir_ids.insert(ix, entry.id);
2836                        }
2837
2838                        if let Some(parent_entry) =
2839                            entry.path.parent().and_then(|p| worktree.entry_for_path(p))
2840                        {
2841                            entry = parent_entry;
2842                        } else {
2843                            break;
2844                        }
2845                    }
2846                }
2847            }
2848        });
2849    }
2850
2851    fn drop_external_files(
2852        &mut self,
2853        paths: &[PathBuf],
2854        entry_id: ProjectEntryId,
2855        window: &mut Window,
2856        cx: &mut Context<Self>,
2857    ) {
2858        let mut paths: Vec<Arc<Path>> = paths.iter().map(|path| Arc::from(path.clone())).collect();
2859
2860        let open_file_after_drop = paths.len() == 1 && paths[0].is_file();
2861
2862        let Some((target_directory, worktree)) = maybe!({
2863            let worktree = self.project.read(cx).worktree_for_entry(entry_id, cx)?;
2864            let entry = worktree.read(cx).entry_for_id(entry_id)?;
2865            let path = worktree.read(cx).absolutize(&entry.path).ok()?;
2866            let target_directory = if path.is_dir() {
2867                path
2868            } else {
2869                path.parent()?.to_path_buf()
2870            };
2871            Some((target_directory, worktree))
2872        }) else {
2873            return;
2874        };
2875
2876        let mut paths_to_replace = Vec::new();
2877        for path in &paths {
2878            if let Some(name) = path.file_name() {
2879                let mut target_path = target_directory.clone();
2880                target_path.push(name);
2881                if target_path.exists() {
2882                    paths_to_replace.push((name.to_string_lossy().to_string(), path.clone()));
2883                }
2884            }
2885        }
2886
2887        cx.spawn_in(window, |this, mut cx| {
2888            async move {
2889                for (filename, original_path) in &paths_to_replace {
2890                    let answer = cx.update(|window, cx| {
2891                        window
2892                            .prompt(
2893                                PromptLevel::Info,
2894                                format!("A file or folder with name {filename} already exists in the destination folder. Do you want to replace it?").as_str(),
2895                                None,
2896                                &["Replace", "Cancel"],
2897                                cx,
2898                            )
2899                    })?.await?;
2900
2901                    if answer == 1 {
2902                        if let Some(item_idx) = paths.iter().position(|p| p == original_path) {
2903                            paths.remove(item_idx);
2904                        }
2905                    }
2906                }
2907
2908                if paths.is_empty() {
2909                    return Ok(());
2910                }
2911
2912                let task = worktree.update(&mut cx, |worktree, cx| {
2913                    worktree.copy_external_entries(target_directory, paths, true, cx)
2914                })?;
2915
2916                let opened_entries = task.await?;
2917                this.update(&mut cx, |this, cx| {
2918                    if open_file_after_drop && !opened_entries.is_empty() {
2919                        this.open_entry(opened_entries[0], true, false, cx);
2920                    }
2921                })
2922            }
2923            .log_err()
2924        })
2925        .detach();
2926    }
2927
2928    fn drag_onto(
2929        &mut self,
2930        selections: &DraggedSelection,
2931        target_entry_id: ProjectEntryId,
2932        is_file: bool,
2933        window: &mut Window,
2934        cx: &mut Context<Self>,
2935    ) {
2936        let should_copy = window.modifiers().alt;
2937        if should_copy {
2938            let _ = maybe!({
2939                let project = self.project.read(cx);
2940                let target_worktree = project.worktree_for_entry(target_entry_id, cx)?;
2941                let worktree_id = target_worktree.read(cx).id();
2942                let target_entry = target_worktree
2943                    .read(cx)
2944                    .entry_for_id(target_entry_id)?
2945                    .clone();
2946
2947                let mut copy_tasks = Vec::new();
2948                let mut disambiguation_range = None;
2949                for selection in selections.items() {
2950                    let (new_path, new_disambiguation_range) = self.create_paste_path(
2951                        selection,
2952                        (target_worktree.clone(), &target_entry),
2953                        cx,
2954                    )?;
2955
2956                    let task = self.project.update(cx, |project, cx| {
2957                        project.copy_entry(selection.entry_id, None, new_path, cx)
2958                    });
2959                    copy_tasks.push(task);
2960                    disambiguation_range = new_disambiguation_range.or(disambiguation_range);
2961                }
2962
2963                let item_count = copy_tasks.len();
2964
2965                cx.spawn_in(window, |project_panel, mut cx| async move {
2966                    let mut last_succeed = None;
2967                    for task in copy_tasks.into_iter() {
2968                        if let Some(Some(entry)) = task.await.log_err() {
2969                            last_succeed = Some(entry.id);
2970                        }
2971                    }
2972                    // update selection
2973                    if let Some(entry_id) = last_succeed {
2974                        project_panel
2975                            .update_in(&mut cx, |project_panel, window, cx| {
2976                                project_panel.selection = Some(SelectedEntry {
2977                                    worktree_id,
2978                                    entry_id,
2979                                });
2980
2981                                // if only one entry was dragged and it was disambiguated, open the rename editor
2982                                if item_count == 1 && disambiguation_range.is_some() {
2983                                    project_panel.rename_impl(disambiguation_range, window, cx);
2984                                }
2985                            })
2986                            .ok();
2987                    }
2988                })
2989                .detach();
2990                Some(())
2991            });
2992        } else {
2993            for selection in selections.items() {
2994                self.move_entry(selection.entry_id, target_entry_id, is_file, cx);
2995            }
2996        }
2997    }
2998
2999    fn index_for_entry(
3000        &self,
3001        entry_id: ProjectEntryId,
3002        worktree_id: WorktreeId,
3003    ) -> Option<(usize, usize, usize)> {
3004        let mut worktree_ix = 0;
3005        let mut total_ix = 0;
3006        for (current_worktree_id, visible_worktree_entries, _) in &self.visible_entries {
3007            if worktree_id != *current_worktree_id {
3008                total_ix += visible_worktree_entries.len();
3009                worktree_ix += 1;
3010                continue;
3011            }
3012
3013            return visible_worktree_entries
3014                .iter()
3015                .enumerate()
3016                .find(|(_, entry)| entry.id == entry_id)
3017                .map(|(ix, _)| (worktree_ix, ix, total_ix + ix));
3018        }
3019        None
3020    }
3021
3022    fn entry_at_index(&self, index: usize) -> Option<(WorktreeId, GitEntryRef)> {
3023        let mut offset = 0;
3024        for (worktree_id, visible_worktree_entries, _) in &self.visible_entries {
3025            if visible_worktree_entries.len() > offset + index {
3026                return visible_worktree_entries
3027                    .get(index)
3028                    .map(|entry| (*worktree_id, entry.to_ref()));
3029            }
3030            offset += visible_worktree_entries.len();
3031        }
3032        None
3033    }
3034
3035    fn iter_visible_entries(
3036        &self,
3037        range: Range<usize>,
3038        window: &mut Window,
3039        cx: &mut Context<ProjectPanel>,
3040        mut callback: impl FnMut(&Entry, &HashSet<Arc<Path>>, &mut Window, &mut Context<ProjectPanel>),
3041    ) {
3042        let mut ix = 0;
3043        for (_, visible_worktree_entries, entries_paths) in &self.visible_entries {
3044            if ix >= range.end {
3045                return;
3046            }
3047
3048            if ix + visible_worktree_entries.len() <= range.start {
3049                ix += visible_worktree_entries.len();
3050                continue;
3051            }
3052
3053            let end_ix = range.end.min(ix + visible_worktree_entries.len());
3054            let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
3055            let entries = entries_paths.get_or_init(|| {
3056                visible_worktree_entries
3057                    .iter()
3058                    .map(|e| (e.path.clone()))
3059                    .collect()
3060            });
3061            for entry in visible_worktree_entries[entry_range].iter() {
3062                callback(&entry, entries, window, cx);
3063            }
3064            ix = end_ix;
3065        }
3066    }
3067
3068    fn for_each_visible_entry(
3069        &self,
3070        range: Range<usize>,
3071        window: &mut Window,
3072        cx: &mut Context<ProjectPanel>,
3073        mut callback: impl FnMut(ProjectEntryId, EntryDetails, &mut Window, &mut Context<ProjectPanel>),
3074    ) {
3075        let mut ix = 0;
3076        for (worktree_id, visible_worktree_entries, entries_paths) in &self.visible_entries {
3077            if ix >= range.end {
3078                return;
3079            }
3080
3081            if ix + visible_worktree_entries.len() <= range.start {
3082                ix += visible_worktree_entries.len();
3083                continue;
3084            }
3085
3086            let end_ix = range.end.min(ix + visible_worktree_entries.len());
3087            let (git_status_setting, show_file_icons, show_folder_icons) = {
3088                let settings = ProjectPanelSettings::get_global(cx);
3089                (
3090                    settings.git_status,
3091                    settings.file_icons,
3092                    settings.folder_icons,
3093                )
3094            };
3095            if let Some(worktree) = self.project.read(cx).worktree_for_id(*worktree_id, cx) {
3096                let snapshot = worktree.read(cx).snapshot();
3097                let root_name = OsStr::new(snapshot.root_name());
3098                let expanded_entry_ids = self
3099                    .expanded_dir_ids
3100                    .get(&snapshot.id())
3101                    .map(Vec::as_slice)
3102                    .unwrap_or(&[]);
3103
3104                let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
3105                let entries = entries_paths.get_or_init(|| {
3106                    visible_worktree_entries
3107                        .iter()
3108                        .map(|e| (e.path.clone()))
3109                        .collect()
3110                });
3111                for entry in visible_worktree_entries[entry_range].iter() {
3112                    let status = git_status_setting
3113                        .then_some(entry.git_summary)
3114                        .unwrap_or_default();
3115                    let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok();
3116                    let icon = match entry.kind {
3117                        EntryKind::File => {
3118                            if show_file_icons {
3119                                FileIcons::get_icon(&entry.path, cx)
3120                            } else {
3121                                None
3122                            }
3123                        }
3124                        _ => {
3125                            if show_folder_icons {
3126                                FileIcons::get_folder_icon(is_expanded, cx)
3127                            } else {
3128                                FileIcons::get_chevron_icon(is_expanded, cx)
3129                            }
3130                        }
3131                    };
3132
3133                    let (depth, difference) =
3134                        ProjectPanel::calculate_depth_and_difference(&entry, entries);
3135
3136                    let filename = match difference {
3137                        diff if diff > 1 => entry
3138                            .path
3139                            .iter()
3140                            .skip(entry.path.components().count() - diff)
3141                            .collect::<PathBuf>()
3142                            .to_str()
3143                            .unwrap_or_default()
3144                            .to_string(),
3145                        _ => entry
3146                            .path
3147                            .file_name()
3148                            .map(|name| name.to_string_lossy().into_owned())
3149                            .unwrap_or_else(|| root_name.to_string_lossy().to_string()),
3150                    };
3151                    let selection = SelectedEntry {
3152                        worktree_id: snapshot.id(),
3153                        entry_id: entry.id,
3154                    };
3155
3156                    let is_marked = self.marked_entries.contains(&selection);
3157
3158                    let diagnostic_severity = self
3159                        .diagnostics
3160                        .get(&(*worktree_id, entry.path.to_path_buf()))
3161                        .cloned();
3162
3163                    let filename_text_color =
3164                        entry_git_aware_label_color(status, entry.is_ignored, is_marked);
3165
3166                    let mut details = EntryDetails {
3167                        filename,
3168                        icon,
3169                        path: entry.path.clone(),
3170                        depth,
3171                        kind: entry.kind,
3172                        is_ignored: entry.is_ignored,
3173                        is_expanded,
3174                        is_selected: self.selection == Some(selection),
3175                        is_marked,
3176                        is_editing: false,
3177                        is_processing: false,
3178                        is_cut: self
3179                            .clipboard
3180                            .as_ref()
3181                            .map_or(false, |e| e.is_cut() && e.items().contains(&selection)),
3182                        filename_text_color,
3183                        diagnostic_severity,
3184                        git_status: status,
3185                        is_private: entry.is_private,
3186                        worktree_id: *worktree_id,
3187                        canonical_path: entry.canonical_path.clone(),
3188                    };
3189
3190                    if let Some(edit_state) = &self.edit_state {
3191                        let is_edited_entry = if edit_state.is_new_entry() {
3192                            entry.id == NEW_ENTRY_ID
3193                        } else {
3194                            entry.id == edit_state.entry_id
3195                                || self
3196                                    .ancestors
3197                                    .get(&entry.id)
3198                                    .is_some_and(|auto_folded_dirs| {
3199                                        auto_folded_dirs
3200                                            .ancestors
3201                                            .iter()
3202                                            .any(|entry_id| *entry_id == edit_state.entry_id)
3203                                    })
3204                        };
3205
3206                        if is_edited_entry {
3207                            if let Some(processing_filename) = &edit_state.processing_filename {
3208                                details.is_processing = true;
3209                                if let Some(ancestors) = edit_state
3210                                    .leaf_entry_id
3211                                    .and_then(|entry| self.ancestors.get(&entry))
3212                                {
3213                                    let position = ancestors.ancestors.iter().position(|entry_id| *entry_id == edit_state.entry_id).expect("Edited sub-entry should be an ancestor of selected leaf entry") + 1;
3214                                    let all_components = ancestors.ancestors.len();
3215
3216                                    let prefix_components = all_components - position;
3217                                    let suffix_components = position.checked_sub(1);
3218                                    let mut previous_components =
3219                                        Path::new(&details.filename).components();
3220                                    let mut new_path = previous_components
3221                                        .by_ref()
3222                                        .take(prefix_components)
3223                                        .collect::<PathBuf>();
3224                                    if let Some(last_component) =
3225                                        Path::new(processing_filename).components().last()
3226                                    {
3227                                        new_path.push(last_component);
3228                                        previous_components.next();
3229                                    }
3230
3231                                    if let Some(_) = suffix_components {
3232                                        new_path.push(previous_components);
3233                                    }
3234                                    if let Some(str) = new_path.to_str() {
3235                                        details.filename.clear();
3236                                        details.filename.push_str(str);
3237                                    }
3238                                } else {
3239                                    details.filename.clear();
3240                                    details.filename.push_str(processing_filename);
3241                                }
3242                            } else {
3243                                if edit_state.is_new_entry() {
3244                                    details.filename.clear();
3245                                }
3246                                details.is_editing = true;
3247                            }
3248                        }
3249                    }
3250
3251                    callback(entry.id, details, window, cx);
3252                }
3253            }
3254            ix = end_ix;
3255        }
3256    }
3257
3258    fn find_entry_in_worktree(
3259        &self,
3260        worktree_id: WorktreeId,
3261        reverse_search: bool,
3262        only_visible_entries: bool,
3263        predicate: impl Fn(GitEntryRef, WorktreeId) -> bool,
3264        cx: &mut Context<Self>,
3265    ) -> Option<GitEntry> {
3266        if only_visible_entries {
3267            let entries = self
3268                .visible_entries
3269                .iter()
3270                .find_map(|(tree_id, entries, _)| {
3271                    if worktree_id == *tree_id {
3272                        Some(entries)
3273                    } else {
3274                        None
3275                    }
3276                })?
3277                .clone();
3278
3279            return utils::ReversibleIterable::new(entries.iter(), reverse_search)
3280                .find(|ele| predicate(ele.to_ref(), worktree_id))
3281                .cloned();
3282        }
3283
3284        let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?;
3285        worktree.update(cx, |tree, _| {
3286            utils::ReversibleIterable::new(
3287                tree.entries(true, 0usize).with_git_statuses(),
3288                reverse_search,
3289            )
3290            .find_single_ended(|ele| predicate(*ele, worktree_id))
3291            .map(|ele| ele.to_owned())
3292        })
3293    }
3294
3295    fn find_entry(
3296        &self,
3297        start: Option<&SelectedEntry>,
3298        reverse_search: bool,
3299        predicate: impl Fn(GitEntryRef, WorktreeId) -> bool,
3300        cx: &mut Context<Self>,
3301    ) -> Option<SelectedEntry> {
3302        let mut worktree_ids: Vec<_> = self
3303            .visible_entries
3304            .iter()
3305            .map(|(worktree_id, _, _)| *worktree_id)
3306            .collect();
3307
3308        let mut last_found: Option<SelectedEntry> = None;
3309
3310        if let Some(start) = start {
3311            let worktree = self
3312                .project
3313                .read(cx)
3314                .worktree_for_id(start.worktree_id, cx)?;
3315
3316            let search = worktree.update(cx, |tree, _| {
3317                let entry = tree.entry_for_id(start.entry_id)?;
3318                let root_entry = tree.root_entry()?;
3319                let tree_id = tree.id();
3320
3321                let mut first_iter = tree
3322                    .traverse_from_path(true, true, true, entry.path.as_ref())
3323                    .with_git_statuses();
3324
3325                if reverse_search {
3326                    first_iter.next();
3327                }
3328
3329                let first = first_iter
3330                    .enumerate()
3331                    .take_until(|(count, entry)| entry.entry == root_entry && *count != 0usize)
3332                    .map(|(_, entry)| entry)
3333                    .find(|ele| predicate(*ele, tree_id))
3334                    .map(|ele| ele.to_owned());
3335
3336                let second_iter = tree.entries(true, 0usize).with_git_statuses();
3337
3338                let second = if reverse_search {
3339                    second_iter
3340                        .take_until(|ele| ele.id == start.entry_id)
3341                        .filter(|ele| predicate(*ele, tree_id))
3342                        .last()
3343                        .map(|ele| ele.to_owned())
3344                } else {
3345                    second_iter
3346                        .take_while(|ele| ele.id != start.entry_id)
3347                        .filter(|ele| predicate(*ele, tree_id))
3348                        .last()
3349                        .map(|ele| ele.to_owned())
3350                };
3351
3352                if reverse_search {
3353                    Some((second, first))
3354                } else {
3355                    Some((first, second))
3356                }
3357            });
3358
3359            if let Some((first, second)) = search {
3360                let first = first.map(|entry| SelectedEntry {
3361                    worktree_id: start.worktree_id,
3362                    entry_id: entry.id,
3363                });
3364
3365                let second = second.map(|entry| SelectedEntry {
3366                    worktree_id: start.worktree_id,
3367                    entry_id: entry.id,
3368                });
3369
3370                if first.is_some() {
3371                    return first;
3372                }
3373                last_found = second;
3374
3375                let idx = worktree_ids
3376                    .iter()
3377                    .enumerate()
3378                    .find(|(_, ele)| **ele == start.worktree_id)
3379                    .map(|(idx, _)| idx);
3380
3381                if let Some(idx) = idx {
3382                    worktree_ids.rotate_left(idx + 1usize);
3383                    worktree_ids.pop();
3384                }
3385            }
3386        }
3387
3388        for tree_id in worktree_ids.into_iter() {
3389            if let Some(found) =
3390                self.find_entry_in_worktree(tree_id, reverse_search, false, &predicate, cx)
3391            {
3392                return Some(SelectedEntry {
3393                    worktree_id: tree_id,
3394                    entry_id: found.id,
3395                });
3396            }
3397        }
3398
3399        last_found
3400    }
3401
3402    fn find_visible_entry(
3403        &self,
3404        start: Option<&SelectedEntry>,
3405        reverse_search: bool,
3406        predicate: impl Fn(GitEntryRef, WorktreeId) -> bool,
3407        cx: &mut Context<Self>,
3408    ) -> Option<SelectedEntry> {
3409        let mut worktree_ids: Vec<_> = self
3410            .visible_entries
3411            .iter()
3412            .map(|(worktree_id, _, _)| *worktree_id)
3413            .collect();
3414
3415        let mut last_found: Option<SelectedEntry> = None;
3416
3417        if let Some(start) = start {
3418            let entries = self
3419                .visible_entries
3420                .iter()
3421                .find(|(worktree_id, _, _)| *worktree_id == start.worktree_id)
3422                .map(|(_, entries, _)| entries)?;
3423
3424            let mut start_idx = entries
3425                .iter()
3426                .enumerate()
3427                .find(|(_, ele)| ele.id == start.entry_id)
3428                .map(|(idx, _)| idx)?;
3429
3430            if reverse_search {
3431                start_idx = start_idx.saturating_add(1usize);
3432            }
3433
3434            let (left, right) = entries.split_at_checked(start_idx)?;
3435
3436            let (first_iter, second_iter) = if reverse_search {
3437                (
3438                    utils::ReversibleIterable::new(left.iter(), reverse_search),
3439                    utils::ReversibleIterable::new(right.iter(), reverse_search),
3440                )
3441            } else {
3442                (
3443                    utils::ReversibleIterable::new(right.iter(), reverse_search),
3444                    utils::ReversibleIterable::new(left.iter(), reverse_search),
3445                )
3446            };
3447
3448            let first_search = first_iter.find(|ele| predicate(ele.to_ref(), start.worktree_id));
3449            let second_search = second_iter.find(|ele| predicate(ele.to_ref(), start.worktree_id));
3450
3451            if first_search.is_some() {
3452                return first_search.map(|entry| SelectedEntry {
3453                    worktree_id: start.worktree_id,
3454                    entry_id: entry.id,
3455                });
3456            }
3457
3458            last_found = second_search.map(|entry| SelectedEntry {
3459                worktree_id: start.worktree_id,
3460                entry_id: entry.id,
3461            });
3462
3463            let idx = worktree_ids
3464                .iter()
3465                .enumerate()
3466                .find(|(_, ele)| **ele == start.worktree_id)
3467                .map(|(idx, _)| idx);
3468
3469            if let Some(idx) = idx {
3470                worktree_ids.rotate_left(idx + 1usize);
3471                worktree_ids.pop();
3472            }
3473        }
3474
3475        for tree_id in worktree_ids.into_iter() {
3476            if let Some(found) =
3477                self.find_entry_in_worktree(tree_id, reverse_search, true, &predicate, cx)
3478            {
3479                return Some(SelectedEntry {
3480                    worktree_id: tree_id,
3481                    entry_id: found.id,
3482                });
3483            }
3484        }
3485
3486        last_found
3487    }
3488
3489    fn calculate_depth_and_difference(
3490        entry: &Entry,
3491        visible_worktree_entries: &HashSet<Arc<Path>>,
3492    ) -> (usize, usize) {
3493        let (depth, difference) = entry
3494            .path
3495            .ancestors()
3496            .skip(1) // Skip the entry itself
3497            .find_map(|ancestor| {
3498                if let Some(parent_entry) = visible_worktree_entries.get(ancestor) {
3499                    let entry_path_components_count = entry.path.components().count();
3500                    let parent_path_components_count = parent_entry.components().count();
3501                    let difference = entry_path_components_count - parent_path_components_count;
3502                    let depth = parent_entry
3503                        .ancestors()
3504                        .skip(1)
3505                        .filter(|ancestor| visible_worktree_entries.contains(*ancestor))
3506                        .count();
3507                    Some((depth + 1, difference))
3508                } else {
3509                    None
3510                }
3511            })
3512            .unwrap_or((0, 0));
3513
3514        (depth, difference)
3515    }
3516
3517    fn render_entry(
3518        &self,
3519        entry_id: ProjectEntryId,
3520        details: EntryDetails,
3521        window: &mut Window,
3522        cx: &mut Context<Self>,
3523    ) -> Stateful<Div> {
3524        const GROUP_NAME: &str = "project_entry";
3525
3526        let kind = details.kind;
3527        let settings = ProjectPanelSettings::get_global(cx);
3528        let show_editor = details.is_editing && !details.is_processing;
3529
3530        let selection = SelectedEntry {
3531            worktree_id: details.worktree_id,
3532            entry_id,
3533        };
3534
3535        let is_marked = self.marked_entries.contains(&selection);
3536        let is_active = self
3537            .selection
3538            .map_or(false, |selection| selection.entry_id == entry_id);
3539
3540        let file_name = details.filename.clone();
3541
3542        let mut icon = details.icon.clone();
3543        if settings.file_icons && show_editor && details.kind.is_file() {
3544            let filename = self.filename_editor.read(cx).text(cx);
3545            if filename.len() > 2 {
3546                icon = FileIcons::get_icon(Path::new(&filename), cx);
3547            }
3548        }
3549
3550        let filename_text_color = details.filename_text_color;
3551        let diagnostic_severity = details.diagnostic_severity;
3552        let item_colors = get_item_color(cx);
3553
3554        let canonical_path = details
3555            .canonical_path
3556            .as_ref()
3557            .map(|f| f.to_string_lossy().to_string());
3558        let path = details.path.clone();
3559
3560        let depth = details.depth;
3561        let worktree_id = details.worktree_id;
3562        let selections = Arc::new(self.marked_entries.clone());
3563        let is_local = self.project.read(cx).is_local();
3564
3565        let dragged_selection = DraggedSelection {
3566            active_selection: selection,
3567            marked_selections: selections,
3568        };
3569
3570        let bg_color = if is_marked {
3571            item_colors.marked
3572        } else {
3573            item_colors.default
3574        };
3575
3576        let bg_hover_color = if is_marked {
3577            item_colors.marked
3578        } else {
3579            item_colors.hover
3580        };
3581
3582        let border_color =
3583            if !self.mouse_down && is_active && self.focus_handle.contains_focused(window, cx) {
3584                item_colors.focused
3585            } else {
3586                bg_color
3587            };
3588
3589        let border_hover_color =
3590            if !self.mouse_down && is_active && self.focus_handle.contains_focused(window, cx) {
3591                item_colors.focused
3592            } else {
3593                bg_hover_color
3594            };
3595
3596        let folded_directory_drag_target = self.folded_directory_drag_target;
3597
3598        div()
3599            .id(entry_id.to_proto() as usize)
3600            .group(GROUP_NAME)
3601            .cursor_pointer()
3602            .rounded_none()
3603            .bg(bg_color)
3604            .border_1()
3605            .border_r_2()
3606            .border_color(border_color)
3607            .hover(|style| style.bg(bg_hover_color).border_color(border_hover_color))
3608            .when(is_local, |div| {
3609                div.on_drag_move::<ExternalPaths>(cx.listener(
3610                    move |this, event: &DragMoveEvent<ExternalPaths>, _, cx| {
3611                        if event.bounds.contains(&event.event.position) {
3612                            if this.last_external_paths_drag_over_entry == Some(entry_id) {
3613                                return;
3614                            }
3615                            this.last_external_paths_drag_over_entry = Some(entry_id);
3616                            this.marked_entries.clear();
3617
3618                            let Some((worktree, path, entry)) = maybe!({
3619                                let worktree = this
3620                                    .project
3621                                    .read(cx)
3622                                    .worktree_for_id(selection.worktree_id, cx)?;
3623                                let worktree = worktree.read(cx);
3624                                let abs_path = worktree.absolutize(&path).log_err()?;
3625                                let path = if abs_path.is_dir() {
3626                                    path.as_ref()
3627                                } else {
3628                                    path.parent()?
3629                                };
3630                                let entry = worktree.entry_for_path(path)?;
3631                                Some((worktree, path, entry))
3632                            }) else {
3633                                return;
3634                            };
3635
3636                            this.marked_entries.insert(SelectedEntry {
3637                                entry_id: entry.id,
3638                                worktree_id: worktree.id(),
3639                            });
3640
3641                            for entry in worktree.child_entries(path) {
3642                                this.marked_entries.insert(SelectedEntry {
3643                                    entry_id: entry.id,
3644                                    worktree_id: worktree.id(),
3645                                });
3646                            }
3647
3648                            cx.notify();
3649                        }
3650                    },
3651                ))
3652                .on_drop(cx.listener(
3653                    move |this, external_paths: &ExternalPaths, window, cx| {
3654                        this.hover_scroll_task.take();
3655                        this.last_external_paths_drag_over_entry = None;
3656                        this.marked_entries.clear();
3657                        this.drop_external_files(external_paths.paths(), entry_id, window, cx);
3658                        cx.stop_propagation();
3659                    },
3660                ))
3661            })
3662            .on_drag_move::<DraggedSelection>(cx.listener(
3663                move |this, event: &DragMoveEvent<DraggedSelection>, window, cx| {
3664                    if event.bounds.contains(&event.event.position) {
3665                        if this.last_selection_drag_over_entry == Some(entry_id) {
3666                            return;
3667                        }
3668                        this.last_selection_drag_over_entry = Some(entry_id);
3669                        this.hover_expand_task.take();
3670
3671                        if !kind.is_dir()
3672                            || this
3673                                .expanded_dir_ids
3674                                .get(&details.worktree_id)
3675                                .map_or(false, |ids| ids.binary_search(&entry_id).is_ok())
3676                        {
3677                            return;
3678                        }
3679
3680                        let bounds = event.bounds;
3681                        this.hover_expand_task =
3682                            Some(cx.spawn_in(window, |this, mut cx| async move {
3683                                cx.background_executor()
3684                                    .timer(Duration::from_millis(500))
3685                                    .await;
3686                                this.update_in(&mut cx, |this, window, cx| {
3687                                    this.hover_expand_task.take();
3688                                    if this.last_selection_drag_over_entry == Some(entry_id)
3689                                        && bounds.contains(&window.mouse_position())
3690                                    {
3691                                        this.expand_entry(worktree_id, entry_id, cx);
3692                                        this.update_visible_entries(
3693                                            Some((worktree_id, entry_id)),
3694                                            cx,
3695                                        );
3696                                        cx.notify();
3697                                    }
3698                                })
3699                                .ok();
3700                            }));
3701                    }
3702                },
3703            ))
3704            .on_drag(
3705                dragged_selection,
3706                move |selection, click_offset, _window, cx| {
3707                    cx.new(|_| DraggedProjectEntryView {
3708                        details: details.clone(),
3709                        click_offset,
3710                        selection: selection.active_selection,
3711                        selections: selection.marked_selections.clone(),
3712                    })
3713                },
3714            )
3715            .drag_over::<DraggedSelection>(move |style, _, _, _| {
3716                if  folded_directory_drag_target.is_some() {
3717                    return style;
3718                }
3719                style.bg(item_colors.drag_over)
3720            })
3721            .on_drop(
3722                cx.listener(move |this, selections: &DraggedSelection, window, cx| {
3723                    this.hover_scroll_task.take();
3724                    this.hover_expand_task.take();
3725                    if  folded_directory_drag_target.is_some() {
3726                        return;
3727                    }
3728                    this.drag_onto(selections, entry_id, kind.is_file(), window, cx);
3729                }),
3730            )
3731            .on_mouse_down(
3732                MouseButton::Left,
3733                cx.listener(move |this, _, _, cx| {
3734                    this.mouse_down = true;
3735                    cx.propagate();
3736                }),
3737            )
3738            .on_click(
3739                cx.listener(move |this, event: &gpui::ClickEvent, window, cx| {
3740                    if event.down.button == MouseButton::Right
3741                        || event.down.first_mouse
3742                        || show_editor
3743                    {
3744                        return;
3745                    }
3746                    if event.down.button == MouseButton::Left {
3747                        this.mouse_down = false;
3748                    }
3749                    cx.stop_propagation();
3750
3751                    if let Some(selection) = this.selection.filter(|_| event.modifiers().shift) {
3752                        let current_selection = this.index_for_selection(selection);
3753                        let clicked_entry = SelectedEntry {
3754                            entry_id,
3755                            worktree_id,
3756                        };
3757                        let target_selection = this.index_for_selection(clicked_entry);
3758                        if let Some(((_, _, source_index), (_, _, target_index))) =
3759                            current_selection.zip(target_selection)
3760                        {
3761                            let range_start = source_index.min(target_index);
3762                            let range_end = source_index.max(target_index) + 1; // Make the range inclusive.
3763                            let mut new_selections = BTreeSet::new();
3764                            this.for_each_visible_entry(
3765                                range_start..range_end,
3766                                window,
3767                                cx,
3768                                |entry_id, details, _, _| {
3769                                    new_selections.insert(SelectedEntry {
3770                                        entry_id,
3771                                        worktree_id: details.worktree_id,
3772                                    });
3773                                },
3774                            );
3775
3776                            this.marked_entries = this
3777                                .marked_entries
3778                                .union(&new_selections)
3779                                .cloned()
3780                                .collect();
3781
3782                            this.selection = Some(clicked_entry);
3783                            this.marked_entries.insert(clicked_entry);
3784                        }
3785                    } else if event.modifiers().secondary() {
3786                        if event.down.click_count > 1 {
3787                            this.split_entry(entry_id, cx);
3788                        } else {
3789                            this.selection = Some(selection);
3790                            if !this.marked_entries.insert(selection) {
3791                                this.marked_entries.remove(&selection);
3792                            }
3793                        }
3794                    } else if kind.is_dir() {
3795                        this.marked_entries.clear();
3796                        if event.modifiers().alt {
3797                            this.toggle_expand_all(entry_id, window, cx);
3798                        } else {
3799                            this.toggle_expanded(entry_id, window, cx);
3800                        }
3801                    } else {
3802                        let preview_tabs_enabled = PreviewTabsSettings::get_global(cx).enabled;
3803                        let click_count = event.up.click_count;
3804                        let focus_opened_item = !preview_tabs_enabled || click_count > 1;
3805                        let allow_preview = preview_tabs_enabled && click_count == 1;
3806                        this.open_entry(entry_id, focus_opened_item, allow_preview, cx);
3807                    }
3808                }),
3809            )
3810            .child(
3811                ListItem::new(entry_id.to_proto() as usize)
3812                    .indent_level(depth)
3813                    .indent_step_size(px(settings.indent_size))
3814                    .spacing(match settings.entry_spacing {
3815                        project_panel_settings::EntrySpacing::Comfortable => ListItemSpacing::Dense,
3816                        project_panel_settings::EntrySpacing::Standard => {
3817                            ListItemSpacing::ExtraDense
3818                        }
3819                    })
3820                    .selectable(false)
3821                    .when_some(canonical_path, |this, path| {
3822                        this.end_slot::<AnyElement>(
3823                            div()
3824                                .id("symlink_icon")
3825                                .pr_3()
3826                                .tooltip(move |window, cx| {
3827                                    Tooltip::with_meta(
3828                                        path.to_string(),
3829                                        None,
3830                                        "Symbolic Link",
3831                                        window,
3832                                        cx,
3833                                    )
3834                                })
3835                                .child(
3836                                    Icon::new(IconName::ArrowUpRight)
3837                                        .size(IconSize::Indicator)
3838                                        .color(filename_text_color),
3839                                )
3840                                .into_any_element(),
3841                        )
3842                    })
3843                    .child(if let Some(icon) = &icon {
3844                        if let Some((_, decoration_color)) =
3845                            entry_diagnostic_aware_icon_decoration_and_color(diagnostic_severity)
3846                        {
3847                            let is_warning = diagnostic_severity
3848                                .map(|severity| matches!(severity, DiagnosticSeverity::WARNING))
3849                                .unwrap_or(false);
3850                            div().child(
3851                                DecoratedIcon::new(
3852                                    Icon::from_path(icon.clone()).color(Color::Muted),
3853                                    Some(
3854                                        IconDecoration::new(
3855                                            if kind.is_file() {
3856                                                if is_warning {
3857                                                    IconDecorationKind::Triangle
3858                                                } else {
3859                                                    IconDecorationKind::X
3860                                                }
3861                                            } else {
3862                                                IconDecorationKind::Dot
3863                                            },
3864                                            bg_color,
3865                                            cx,
3866                                        )
3867                                        .group_name(Some(GROUP_NAME.into()))
3868                                        .knockout_hover_color(bg_hover_color)
3869                                        .color(decoration_color.color(cx))
3870                                        .position(Point {
3871                                            x: px(-2.),
3872                                            y: px(-2.),
3873                                        }),
3874                                    ),
3875                                )
3876                                .into_any_element(),
3877                            )
3878                        } else {
3879                            h_flex().child(Icon::from_path(icon.to_string()).color(Color::Muted))
3880                        }
3881                    } else {
3882                        if let Some((icon_name, color)) =
3883                            entry_diagnostic_aware_icon_name_and_color(diagnostic_severity)
3884                        {
3885                            h_flex()
3886                                .size(IconSize::default().rems())
3887                                .child(Icon::new(icon_name).color(color).size(IconSize::Small))
3888                        } else {
3889                            h_flex()
3890                                .size(IconSize::default().rems())
3891                                .invisible()
3892                                .flex_none()
3893                        }
3894                    })
3895                    .child(
3896                        if let (Some(editor), true) = (Some(&self.filename_editor), show_editor) {
3897                            h_flex().h_6().w_full().child(editor.clone())
3898                        } else {
3899                            h_flex().h_6().map(|mut this| {
3900                                if let Some(folded_ancestors) = self.ancestors.get(&entry_id) {
3901                                    let components = Path::new(&file_name)
3902                                        .components()
3903                                        .map(|comp| {
3904                                            let comp_str =
3905                                                comp.as_os_str().to_string_lossy().into_owned();
3906                                            comp_str
3907                                        })
3908                                        .collect::<Vec<_>>();
3909
3910                                    let components_len = components.len();
3911                                    let active_index = components_len
3912                                        - 1
3913                                        - folded_ancestors.current_ancestor_depth;
3914                                        const DELIMITER: SharedString =
3915                                        SharedString::new_static(std::path::MAIN_SEPARATOR_STR);
3916                                    for (index, component) in components.into_iter().enumerate() {
3917                                        if index != 0 {
3918                                                let delimiter_target_index = index - 1;
3919                                                let target_entry_id = folded_ancestors.ancestors.get(components_len - 1 - delimiter_target_index).cloned();
3920                                                this = this.child(
3921                                                    div()
3922                                                    .on_drop(cx.listener(move |this, selections: &DraggedSelection, window, cx| {
3923                                                        this.hover_scroll_task.take();
3924                                                        this.folded_directory_drag_target = None;
3925                                                        if let Some(target_entry_id) = target_entry_id {
3926                                                            this.drag_onto(selections, target_entry_id, kind.is_file(), window, cx);
3927                                                        }
3928                                                    }))
3929                                                    .on_drag_move(cx.listener(
3930                                                        move |this, event: &DragMoveEvent<DraggedSelection>, _, _| {
3931                                                            if event.bounds.contains(&event.event.position) {
3932                                                                this.folded_directory_drag_target = Some(
3933                                                                    FoldedDirectoryDragTarget {
3934                                                                        entry_id,
3935                                                                        index: delimiter_target_index,
3936                                                                        is_delimiter_target: true,
3937                                                                    }
3938                                                                );
3939                                                            } else {
3940                                                                let is_current_target = this.folded_directory_drag_target
3941                                                                    .map_or(false, |target|
3942                                                                        target.entry_id == entry_id &&
3943                                                                        target.index == delimiter_target_index &&
3944                                                                        target.is_delimiter_target
3945                                                                    );
3946                                                                if is_current_target {
3947                                                                    this.folded_directory_drag_target = None;
3948                                                                }
3949                                                            }
3950
3951                                                        },
3952                                                    ))
3953                                                    .child(
3954                                                        Label::new(DELIMITER.clone())
3955                                                            .single_line()
3956                                                            .color(filename_text_color)
3957                                                    )
3958                                                );
3959                                        }
3960                                        let id = SharedString::from(format!(
3961                                            "project_panel_path_component_{}_{index}",
3962                                            entry_id.to_usize()
3963                                        ));
3964                                        let label = div()
3965                                            .id(id)
3966                                            .on_click(cx.listener(move |this, _, _, cx| {
3967                                                if index != active_index {
3968                                                    if let Some(folds) =
3969                                                        this.ancestors.get_mut(&entry_id)
3970                                                    {
3971                                                        folds.current_ancestor_depth =
3972                                                            components_len - 1 - index;
3973                                                        cx.notify();
3974                                                    }
3975                                                }
3976                                            }))
3977                                            .when(index != components_len - 1, |div|{
3978                                                let target_entry_id = folded_ancestors.ancestors.get(components_len - 1 - index).cloned();
3979                                                div
3980                                                .on_drag_move(cx.listener(
3981                                                    move |this, event: &DragMoveEvent<DraggedSelection>, _, _| {
3982                                                    if event.bounds.contains(&event.event.position) {
3983                                                            this.folded_directory_drag_target = Some(
3984                                                                FoldedDirectoryDragTarget {
3985                                                                    entry_id,
3986                                                                    index,
3987                                                                    is_delimiter_target: false,
3988                                                                }
3989                                                            );
3990                                                        } else {
3991                                                            let is_current_target = this.folded_directory_drag_target
3992                                                                .as_ref()
3993                                                                .map_or(false, |target|
3994                                                                    target.entry_id == entry_id &&
3995                                                                    target.index == index &&
3996                                                                    !target.is_delimiter_target
3997                                                                );
3998                                                            if is_current_target {
3999                                                                this.folded_directory_drag_target = None;
4000                                                            }
4001                                                        }
4002                                                    },
4003                                                ))
4004                                                .on_drop(cx.listener(move |this, selections: &DraggedSelection, window,cx| {
4005                                                    this.hover_scroll_task.take();
4006                                                    this.folded_directory_drag_target = None;
4007                                                    if let Some(target_entry_id) = target_entry_id {
4008                                                        this.drag_onto(selections, target_entry_id, kind.is_file(), window, cx);
4009                                                    }
4010                                                }))
4011                                                .when(folded_directory_drag_target.map_or(false, |target|
4012                                                    target.entry_id == entry_id &&
4013                                                    target.index == index
4014                                                ), |this| {
4015                                                    this.bg(item_colors.drag_over)
4016                                                })
4017                                            })
4018                                            .child(
4019                                                Label::new(component)
4020                                                    .single_line()
4021                                                    .color(filename_text_color)
4022                                                    .when(
4023                                                        index == active_index
4024                                                            && (is_active || is_marked),
4025                                                        |this| this.underline(),
4026                                                    ),
4027                                            );
4028
4029                                        this = this.child(label);
4030                                    }
4031
4032                                    this
4033                                } else {
4034                                    this.child(
4035                                        Label::new(file_name)
4036                                            .single_line()
4037                                            .color(filename_text_color),
4038                                    )
4039                                }
4040                            })
4041                        }
4042                        .ml_1(),
4043                    )
4044                    .on_secondary_mouse_down(cx.listener(
4045                        move |this, event: &MouseDownEvent, window, cx| {
4046                            // Stop propagation to prevent the catch-all context menu for the project
4047                            // panel from being deployed.
4048                            cx.stop_propagation();
4049                            // Some context menu actions apply to all marked entries. If the user
4050                            // right-clicks on an entry that is not marked, they may not realize the
4051                            // action applies to multiple entries. To avoid inadvertent changes, all
4052                            // entries are unmarked.
4053                            if !this.marked_entries.contains(&selection) {
4054                                this.marked_entries.clear();
4055                            }
4056                            this.deploy_context_menu(event.position, entry_id, window, cx);
4057                        },
4058                    ))
4059                    .overflow_x(),
4060            )
4061    }
4062
4063    fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
4064        if !Self::should_show_scrollbar(cx)
4065            || !(self.show_scrollbar || self.vertical_scrollbar_state.is_dragging())
4066        {
4067            return None;
4068        }
4069        Some(
4070            div()
4071                .occlude()
4072                .id("project-panel-vertical-scroll")
4073                .on_mouse_move(cx.listener(|_, _, _, cx| {
4074                    cx.notify();
4075                    cx.stop_propagation()
4076                }))
4077                .on_hover(|_, _, cx| {
4078                    cx.stop_propagation();
4079                })
4080                .on_any_mouse_down(|_, _, cx| {
4081                    cx.stop_propagation();
4082                })
4083                .on_mouse_up(
4084                    MouseButton::Left,
4085                    cx.listener(|this, _, window, cx| {
4086                        if !this.vertical_scrollbar_state.is_dragging()
4087                            && !this.focus_handle.contains_focused(window, cx)
4088                        {
4089                            this.hide_scrollbar(window, cx);
4090                            cx.notify();
4091                        }
4092
4093                        cx.stop_propagation();
4094                    }),
4095                )
4096                .on_scroll_wheel(cx.listener(|_, _, _, cx| {
4097                    cx.notify();
4098                }))
4099                .h_full()
4100                .absolute()
4101                .right_1()
4102                .top_1()
4103                .bottom_1()
4104                .w(px(12.))
4105                .cursor_default()
4106                .children(Scrollbar::vertical(
4107                    // percentage as f32..end_offset as f32,
4108                    self.vertical_scrollbar_state.clone(),
4109                )),
4110        )
4111    }
4112
4113    fn render_horizontal_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
4114        if !Self::should_show_scrollbar(cx)
4115            || !(self.show_scrollbar || self.horizontal_scrollbar_state.is_dragging())
4116        {
4117            return None;
4118        }
4119
4120        let scroll_handle = self.scroll_handle.0.borrow();
4121        let longest_item_width = scroll_handle
4122            .last_item_size
4123            .filter(|size| size.contents.width > size.item.width)?
4124            .contents
4125            .width
4126            .0 as f64;
4127        if longest_item_width < scroll_handle.base_handle.bounds().size.width.0 as f64 {
4128            return None;
4129        }
4130
4131        Some(
4132            div()
4133                .occlude()
4134                .id("project-panel-horizontal-scroll")
4135                .on_mouse_move(cx.listener(|_, _, _, cx| {
4136                    cx.notify();
4137                    cx.stop_propagation()
4138                }))
4139                .on_hover(|_, _, cx| {
4140                    cx.stop_propagation();
4141                })
4142                .on_any_mouse_down(|_, _, cx| {
4143                    cx.stop_propagation();
4144                })
4145                .on_mouse_up(
4146                    MouseButton::Left,
4147                    cx.listener(|this, _, window, cx| {
4148                        if !this.horizontal_scrollbar_state.is_dragging()
4149                            && !this.focus_handle.contains_focused(window, cx)
4150                        {
4151                            this.hide_scrollbar(window, cx);
4152                            cx.notify();
4153                        }
4154
4155                        cx.stop_propagation();
4156                    }),
4157                )
4158                .on_scroll_wheel(cx.listener(|_, _, _, cx| {
4159                    cx.notify();
4160                }))
4161                .w_full()
4162                .absolute()
4163                .right_1()
4164                .left_1()
4165                .bottom_1()
4166                .h(px(12.))
4167                .cursor_default()
4168                .when(self.width.is_some(), |this| {
4169                    this.children(Scrollbar::horizontal(
4170                        self.horizontal_scrollbar_state.clone(),
4171                    ))
4172                }),
4173        )
4174    }
4175
4176    fn dispatch_context(&self, window: &Window, cx: &Context<Self>) -> KeyContext {
4177        let mut dispatch_context = KeyContext::new_with_defaults();
4178        dispatch_context.add("ProjectPanel");
4179        dispatch_context.add("menu");
4180
4181        let identifier = if self.filename_editor.focus_handle(cx).is_focused(window) {
4182            "editing"
4183        } else {
4184            "not_editing"
4185        };
4186
4187        dispatch_context.add(identifier);
4188        dispatch_context
4189    }
4190
4191    fn should_show_scrollbar(cx: &App) -> bool {
4192        let show = ProjectPanelSettings::get_global(cx)
4193            .scrollbar
4194            .show
4195            .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show);
4196        match show {
4197            ShowScrollbar::Auto => true,
4198            ShowScrollbar::System => true,
4199            ShowScrollbar::Always => true,
4200            ShowScrollbar::Never => false,
4201        }
4202    }
4203
4204    fn should_autohide_scrollbar(cx: &App) -> bool {
4205        let show = ProjectPanelSettings::get_global(cx)
4206            .scrollbar
4207            .show
4208            .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show);
4209        match show {
4210            ShowScrollbar::Auto => true,
4211            ShowScrollbar::System => cx
4212                .try_global::<ScrollbarAutoHide>()
4213                .map_or_else(|| cx.should_auto_hide_scrollbars(), |autohide| autohide.0),
4214            ShowScrollbar::Always => false,
4215            ShowScrollbar::Never => true,
4216        }
4217    }
4218
4219    fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4220        const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
4221        if !Self::should_autohide_scrollbar(cx) {
4222            return;
4223        }
4224        self.hide_scrollbar_task = Some(cx.spawn_in(window, |panel, mut cx| async move {
4225            cx.background_executor()
4226                .timer(SCROLLBAR_SHOW_INTERVAL)
4227                .await;
4228            panel
4229                .update(&mut cx, |panel, cx| {
4230                    panel.show_scrollbar = false;
4231                    cx.notify();
4232                })
4233                .log_err();
4234        }))
4235    }
4236
4237    fn reveal_entry(
4238        &mut self,
4239        project: Entity<Project>,
4240        entry_id: ProjectEntryId,
4241        skip_ignored: bool,
4242        cx: &mut Context<Self>,
4243    ) {
4244        if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) {
4245            let worktree = worktree.read(cx);
4246            if skip_ignored
4247                && worktree
4248                    .entry_for_id(entry_id)
4249                    .map_or(true, |entry| entry.is_ignored && !entry.is_always_included)
4250            {
4251                return;
4252            }
4253
4254            let worktree_id = worktree.id();
4255            self.expand_entry(worktree_id, entry_id, cx);
4256            self.update_visible_entries(Some((worktree_id, entry_id)), cx);
4257            self.marked_entries.clear();
4258            self.marked_entries.insert(SelectedEntry {
4259                worktree_id,
4260                entry_id,
4261            });
4262            self.autoscroll(cx);
4263            cx.notify();
4264        }
4265    }
4266
4267    fn find_active_indent_guide(
4268        &self,
4269        indent_guides: &[IndentGuideLayout],
4270        cx: &App,
4271    ) -> Option<usize> {
4272        let (worktree, entry) = self.selected_entry(cx)?;
4273
4274        // Find the parent entry of the indent guide, this will either be the
4275        // expanded folder we have selected, or the parent of the currently
4276        // selected file/collapsed directory
4277        let mut entry = entry;
4278        loop {
4279            let is_expanded_dir = entry.is_dir()
4280                && self
4281                    .expanded_dir_ids
4282                    .get(&worktree.id())
4283                    .map(|ids| ids.binary_search(&entry.id).is_ok())
4284                    .unwrap_or(false);
4285            if is_expanded_dir {
4286                break;
4287            }
4288            entry = worktree.entry_for_path(&entry.path.parent()?)?;
4289        }
4290
4291        let (active_indent_range, depth) = {
4292            let (worktree_ix, child_offset, ix) = self.index_for_entry(entry.id, worktree.id())?;
4293            let child_paths = &self.visible_entries[worktree_ix].1;
4294            let mut child_count = 0;
4295            let depth = entry.path.ancestors().count();
4296            while let Some(entry) = child_paths.get(child_offset + child_count + 1) {
4297                if entry.path.ancestors().count() <= depth {
4298                    break;
4299                }
4300                child_count += 1;
4301            }
4302
4303            let start = ix + 1;
4304            let end = start + child_count;
4305
4306            let (_, entries, paths) = &self.visible_entries[worktree_ix];
4307            let visible_worktree_entries =
4308                paths.get_or_init(|| entries.iter().map(|e| (e.path.clone())).collect());
4309
4310            // Calculate the actual depth of the entry, taking into account that directories can be auto-folded.
4311            let (depth, _) = Self::calculate_depth_and_difference(entry, visible_worktree_entries);
4312            (start..end, depth)
4313        };
4314
4315        let candidates = indent_guides
4316            .iter()
4317            .enumerate()
4318            .filter(|(_, indent_guide)| indent_guide.offset.x == depth);
4319
4320        for (i, indent) in candidates {
4321            // Find matches that are either an exact match, partially on screen, or inside the enclosing indent
4322            if active_indent_range.start <= indent.offset.y + indent.length
4323                && indent.offset.y <= active_indent_range.end
4324            {
4325                return Some(i);
4326            }
4327        }
4328        None
4329    }
4330}
4331
4332fn item_width_estimate(depth: usize, item_text_chars: usize, is_symlink: bool) -> usize {
4333    const ICON_SIZE_FACTOR: usize = 2;
4334    let mut item_width = depth * ICON_SIZE_FACTOR + item_text_chars;
4335    if is_symlink {
4336        item_width += ICON_SIZE_FACTOR;
4337    }
4338    item_width
4339}
4340
4341impl Render for ProjectPanel {
4342    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
4343        let has_worktree = !self.visible_entries.is_empty();
4344        let project = self.project.read(cx);
4345        let indent_size = ProjectPanelSettings::get_global(cx).indent_size;
4346        let show_indent_guides =
4347            ProjectPanelSettings::get_global(cx).indent_guides.show == ShowIndentGuides::Always;
4348        let is_local = project.is_local();
4349
4350        if has_worktree {
4351            let item_count = self
4352                .visible_entries
4353                .iter()
4354                .map(|(_, worktree_entries, _)| worktree_entries.len())
4355                .sum();
4356
4357            fn handle_drag_move_scroll<T: 'static>(
4358                this: &mut ProjectPanel,
4359                e: &DragMoveEvent<T>,
4360                window: &mut Window,
4361                cx: &mut Context<ProjectPanel>,
4362            ) {
4363                if !e.bounds.contains(&e.event.position) {
4364                    return;
4365                }
4366                this.hover_scroll_task.take();
4367                let panel_height = e.bounds.size.height;
4368                if panel_height <= px(0.) {
4369                    return;
4370                }
4371
4372                let event_offset = e.event.position.y - e.bounds.origin.y;
4373                // How far along in the project panel is our cursor? (0. is the top of a list, 1. is the bottom)
4374                let hovered_region_offset = event_offset / panel_height;
4375
4376                // We want the scrolling to be a bit faster when the cursor is closer to the edge of a list.
4377                // These pixels offsets were picked arbitrarily.
4378                let vertical_scroll_offset = if hovered_region_offset <= 0.05 {
4379                    8.
4380                } else if hovered_region_offset <= 0.15 {
4381                    5.
4382                } else if hovered_region_offset >= 0.95 {
4383                    -8.
4384                } else if hovered_region_offset >= 0.85 {
4385                    -5.
4386                } else {
4387                    return;
4388                };
4389                let adjustment = point(px(0.), px(vertical_scroll_offset));
4390                this.hover_scroll_task =
4391                    Some(cx.spawn_in(window, move |this, mut cx| async move {
4392                        loop {
4393                            let should_stop_scrolling = this
4394                                .update(&mut cx, |this, cx| {
4395                                    this.hover_scroll_task.as_ref()?;
4396                                    let handle = this.scroll_handle.0.borrow_mut();
4397                                    let offset = handle.base_handle.offset();
4398
4399                                    handle.base_handle.set_offset(offset + adjustment);
4400                                    cx.notify();
4401                                    Some(())
4402                                })
4403                                .ok()
4404                                .flatten()
4405                                .is_some();
4406                            if should_stop_scrolling {
4407                                return;
4408                            }
4409                            cx.background_executor()
4410                                .timer(Duration::from_millis(16))
4411                                .await;
4412                        }
4413                    }));
4414            }
4415            h_flex()
4416                .id("project-panel")
4417                .group("project-panel")
4418                .on_drag_move(cx.listener(handle_drag_move_scroll::<ExternalPaths>))
4419                .on_drag_move(cx.listener(handle_drag_move_scroll::<DraggedSelection>))
4420                .size_full()
4421                .relative()
4422                .on_hover(cx.listener(|this, hovered, window, cx| {
4423                    if *hovered {
4424                        this.show_scrollbar = true;
4425                        this.hide_scrollbar_task.take();
4426                        cx.notify();
4427                    } else if !this.focus_handle.contains_focused(window, cx) {
4428                        this.hide_scrollbar(window, cx);
4429                    }
4430                }))
4431                .on_click(cx.listener(|this, _event, _, cx| {
4432                    cx.stop_propagation();
4433                    this.selection = None;
4434                    this.marked_entries.clear();
4435                }))
4436                .key_context(self.dispatch_context(window, cx))
4437                .on_action(cx.listener(Self::select_next))
4438                .on_action(cx.listener(Self::select_previous))
4439                .on_action(cx.listener(Self::select_first))
4440                .on_action(cx.listener(Self::select_last))
4441                .on_action(cx.listener(Self::select_parent))
4442                .on_action(cx.listener(Self::select_next_git_entry))
4443                .on_action(cx.listener(Self::select_prev_git_entry))
4444                .on_action(cx.listener(Self::select_next_diagnostic))
4445                .on_action(cx.listener(Self::select_prev_diagnostic))
4446                .on_action(cx.listener(Self::select_next_directory))
4447                .on_action(cx.listener(Self::select_prev_directory))
4448                .on_action(cx.listener(Self::expand_selected_entry))
4449                .on_action(cx.listener(Self::collapse_selected_entry))
4450                .on_action(cx.listener(Self::collapse_all_entries))
4451                .on_action(cx.listener(Self::open))
4452                .on_action(cx.listener(Self::open_permanent))
4453                .on_action(cx.listener(Self::confirm))
4454                .on_action(cx.listener(Self::cancel))
4455                .on_action(cx.listener(Self::copy_path))
4456                .on_action(cx.listener(Self::copy_relative_path))
4457                .on_action(cx.listener(Self::new_search_in_directory))
4458                .on_action(cx.listener(Self::unfold_directory))
4459                .on_action(cx.listener(Self::fold_directory))
4460                .on_action(cx.listener(Self::remove_from_project))
4461                .when(!project.is_read_only(cx), |el| {
4462                    el.on_action(cx.listener(Self::new_file))
4463                        .on_action(cx.listener(Self::new_directory))
4464                        .on_action(cx.listener(Self::rename))
4465                        .on_action(cx.listener(Self::delete))
4466                        .on_action(cx.listener(Self::trash))
4467                        .on_action(cx.listener(Self::cut))
4468                        .on_action(cx.listener(Self::copy))
4469                        .on_action(cx.listener(Self::paste))
4470                        .on_action(cx.listener(Self::duplicate))
4471                        .on_click(cx.listener(|this, event: &gpui::ClickEvent, window, cx| {
4472                            if event.up.click_count > 1 {
4473                                if let Some(entry_id) = this.last_worktree_root_id {
4474                                    let project = this.project.read(cx);
4475
4476                                    let worktree_id = if let Some(worktree) =
4477                                        project.worktree_for_entry(entry_id, cx)
4478                                    {
4479                                        worktree.read(cx).id()
4480                                    } else {
4481                                        return;
4482                                    };
4483
4484                                    this.selection = Some(SelectedEntry {
4485                                        worktree_id,
4486                                        entry_id,
4487                                    });
4488
4489                                    this.new_file(&NewFile, window, cx);
4490                                }
4491                            }
4492                        }))
4493                })
4494                .when(project.is_local(), |el| {
4495                    el.on_action(cx.listener(Self::reveal_in_finder))
4496                        .on_action(cx.listener(Self::open_system))
4497                        .on_action(cx.listener(Self::open_in_terminal))
4498                })
4499                .when(project.is_via_ssh(), |el| {
4500                    el.on_action(cx.listener(Self::open_in_terminal))
4501                })
4502                .on_mouse_down(
4503                    MouseButton::Right,
4504                    cx.listener(move |this, event: &MouseDownEvent, window, cx| {
4505                        // When deploying the context menu anywhere below the last project entry,
4506                        // act as if the user clicked the root of the last worktree.
4507                        if let Some(entry_id) = this.last_worktree_root_id {
4508                            this.deploy_context_menu(event.position, entry_id, window, cx);
4509                        }
4510                    }),
4511                )
4512                .track_focus(&self.focus_handle(cx))
4513                .child(
4514                    uniform_list(cx.entity().clone(), "entries", item_count, {
4515                        |this, range, window, cx| {
4516                            let mut items = Vec::with_capacity(range.end - range.start);
4517                            this.for_each_visible_entry(
4518                                range,
4519                                window,
4520                                cx,
4521                                |id, details, window, cx| {
4522                                    items.push(this.render_entry(id, details, window, cx));
4523                                },
4524                            );
4525                            items
4526                        }
4527                    })
4528                    .when(show_indent_guides, |list| {
4529                        list.with_decoration(
4530                            ui::indent_guides(
4531                                cx.entity().clone(),
4532                                px(indent_size),
4533                                IndentGuideColors::panel(cx),
4534                                |this, range, window, cx| {
4535                                    let mut items =
4536                                        SmallVec::with_capacity(range.end - range.start);
4537                                    this.iter_visible_entries(
4538                                        range,
4539                                        window,
4540                                        cx,
4541                                        |entry, entries, _, _| {
4542                                            let (depth, _) = Self::calculate_depth_and_difference(
4543                                                entry, entries,
4544                                            );
4545                                            items.push(depth);
4546                                        },
4547                                    );
4548                                    items
4549                                },
4550                            )
4551                            .on_click(cx.listener(
4552                                |this, active_indent_guide: &IndentGuideLayout, window, cx| {
4553                                    if window.modifiers().secondary() {
4554                                        let ix = active_indent_guide.offset.y;
4555                                        let Some((target_entry, worktree)) = maybe!({
4556                                            let (worktree_id, entry) = this.entry_at_index(ix)?;
4557                                            let worktree = this
4558                                                .project
4559                                                .read(cx)
4560                                                .worktree_for_id(worktree_id, cx)?;
4561                                            let target_entry = worktree
4562                                                .read(cx)
4563                                                .entry_for_path(&entry.path.parent()?)?;
4564                                            Some((target_entry, worktree))
4565                                        }) else {
4566                                            return;
4567                                        };
4568
4569                                        this.collapse_entry(target_entry.clone(), worktree, cx);
4570                                    }
4571                                },
4572                            ))
4573                            .with_render_fn(
4574                                cx.entity().clone(),
4575                                move |this, params, _, cx| {
4576                                    const LEFT_OFFSET: f32 = 14.;
4577                                    const PADDING_Y: f32 = 4.;
4578                                    const HITBOX_OVERDRAW: f32 = 3.;
4579
4580                                    let active_indent_guide_index =
4581                                        this.find_active_indent_guide(&params.indent_guides, cx);
4582
4583                                    let indent_size = params.indent_size;
4584                                    let item_height = params.item_height;
4585
4586                                    params
4587                                        .indent_guides
4588                                        .into_iter()
4589                                        .enumerate()
4590                                        .map(|(idx, layout)| {
4591                                            let offset = if layout.continues_offscreen {
4592                                                px(0.)
4593                                            } else {
4594                                                px(PADDING_Y)
4595                                            };
4596                                            let bounds = Bounds::new(
4597                                                point(
4598                                                    px(layout.offset.x as f32) * indent_size
4599                                                        + px(LEFT_OFFSET),
4600                                                    px(layout.offset.y as f32) * item_height
4601                                                        + offset,
4602                                                ),
4603                                                size(
4604                                                    px(1.),
4605                                                    px(layout.length as f32) * item_height
4606                                                        - px(offset.0 * 2.),
4607                                                ),
4608                                            );
4609                                            ui::RenderedIndentGuide {
4610                                                bounds,
4611                                                layout,
4612                                                is_active: Some(idx) == active_indent_guide_index,
4613                                                hitbox: Some(Bounds::new(
4614                                                    point(
4615                                                        bounds.origin.x - px(HITBOX_OVERDRAW),
4616                                                        bounds.origin.y,
4617                                                    ),
4618                                                    size(
4619                                                        bounds.size.width
4620                                                            + px(2. * HITBOX_OVERDRAW),
4621                                                        bounds.size.height,
4622                                                    ),
4623                                                )),
4624                                            }
4625                                        })
4626                                        .collect()
4627                                },
4628                            ),
4629                        )
4630                    })
4631                    .size_full()
4632                    .with_sizing_behavior(ListSizingBehavior::Infer)
4633                    .with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained)
4634                    .with_width_from_item(self.max_width_item_index)
4635                    .track_scroll(self.scroll_handle.clone()),
4636                )
4637                .children(self.render_vertical_scrollbar(cx))
4638                .when_some(self.render_horizontal_scrollbar(cx), |this, scrollbar| {
4639                    this.pb_4().child(scrollbar)
4640                })
4641                .children(self.context_menu.as_ref().map(|(menu, position, _)| {
4642                    deferred(
4643                        anchored()
4644                            .position(*position)
4645                            .anchor(gpui::Corner::TopLeft)
4646                            .child(menu.clone()),
4647                    )
4648                    .with_priority(1)
4649                }))
4650        } else {
4651            v_flex()
4652                .id("empty-project_panel")
4653                .size_full()
4654                .p_4()
4655                .track_focus(&self.focus_handle(cx))
4656                .child(
4657                    Button::new("open_project", "Open a project")
4658                        .full_width()
4659                        .key_binding(KeyBinding::for_action(&workspace::Open, window, cx))
4660                        .on_click(cx.listener(|this, _, window, cx| {
4661                            this.workspace
4662                                .update(cx, |_, cx| {
4663                                    window.dispatch_action(Box::new(workspace::Open), cx)
4664                                })
4665                                .log_err();
4666                        })),
4667                )
4668                .when(is_local, |div| {
4669                    div.drag_over::<ExternalPaths>(|style, _, _, cx| {
4670                        style.bg(cx.theme().colors().drop_target_background)
4671                    })
4672                    .on_drop(cx.listener(
4673                        move |this, external_paths: &ExternalPaths, window, cx| {
4674                            this.last_external_paths_drag_over_entry = None;
4675                            this.marked_entries.clear();
4676                            this.hover_scroll_task.take();
4677                            if let Some(task) = this
4678                                .workspace
4679                                .update(cx, |workspace, cx| {
4680                                    workspace.open_workspace_for_paths(
4681                                        true,
4682                                        external_paths.paths().to_owned(),
4683                                        window,
4684                                        cx,
4685                                    )
4686                                })
4687                                .log_err()
4688                            {
4689                                task.detach_and_log_err(cx);
4690                            }
4691                            cx.stop_propagation();
4692                        },
4693                    ))
4694                })
4695        }
4696    }
4697}
4698
4699impl Render for DraggedProjectEntryView {
4700    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
4701        let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
4702        h_flex()
4703            .font(ui_font)
4704            .pl(self.click_offset.x + px(12.))
4705            .pt(self.click_offset.y + px(12.))
4706            .child(
4707                div()
4708                    .flex()
4709                    .gap_1()
4710                    .items_center()
4711                    .py_1()
4712                    .px_2()
4713                    .rounded_lg()
4714                    .bg(cx.theme().colors().background)
4715                    .map(|this| {
4716                        if self.selections.len() > 1 && self.selections.contains(&self.selection) {
4717                            this.child(Label::new(format!("{} entries", self.selections.len())))
4718                        } else {
4719                            this.child(if let Some(icon) = &self.details.icon {
4720                                div().child(Icon::from_path(icon.clone()))
4721                            } else {
4722                                div()
4723                            })
4724                            .child(Label::new(self.details.filename.clone()))
4725                        }
4726                    }),
4727            )
4728    }
4729}
4730
4731impl EventEmitter<Event> for ProjectPanel {}
4732
4733impl EventEmitter<PanelEvent> for ProjectPanel {}
4734
4735impl Panel for ProjectPanel {
4736    fn position(&self, _: &Window, cx: &App) -> DockPosition {
4737        match ProjectPanelSettings::get_global(cx).dock {
4738            ProjectPanelDockPosition::Left => DockPosition::Left,
4739            ProjectPanelDockPosition::Right => DockPosition::Right,
4740        }
4741    }
4742
4743    fn position_is_valid(&self, position: DockPosition) -> bool {
4744        matches!(position, DockPosition::Left | DockPosition::Right)
4745    }
4746
4747    fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
4748        settings::update_settings_file::<ProjectPanelSettings>(
4749            self.fs.clone(),
4750            cx,
4751            move |settings, _| {
4752                let dock = match position {
4753                    DockPosition::Left | DockPosition::Bottom => ProjectPanelDockPosition::Left,
4754                    DockPosition::Right => ProjectPanelDockPosition::Right,
4755                };
4756                settings.dock = Some(dock);
4757            },
4758        );
4759    }
4760
4761    fn size(&self, _: &Window, cx: &App) -> Pixels {
4762        self.width
4763            .unwrap_or_else(|| ProjectPanelSettings::get_global(cx).default_width)
4764    }
4765
4766    fn set_size(&mut self, size: Option<Pixels>, _: &mut Window, cx: &mut Context<Self>) {
4767        self.width = size;
4768        self.serialize(cx);
4769        cx.notify();
4770    }
4771
4772    fn icon(&self, _: &Window, cx: &App) -> Option<IconName> {
4773        ProjectPanelSettings::get_global(cx)
4774            .button
4775            .then_some(IconName::FileTree)
4776    }
4777
4778    fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
4779        Some("Project Panel")
4780    }
4781
4782    fn toggle_action(&self) -> Box<dyn Action> {
4783        Box::new(ToggleFocus)
4784    }
4785
4786    fn persistent_name() -> &'static str {
4787        "Project Panel"
4788    }
4789
4790    fn starts_open(&self, _: &Window, cx: &App) -> bool {
4791        let project = &self.project.read(cx);
4792        project.visible_worktrees(cx).any(|tree| {
4793            tree.read(cx)
4794                .root_entry()
4795                .map_or(false, |entry| entry.is_dir())
4796        })
4797    }
4798
4799    fn activation_priority(&self) -> u32 {
4800        0
4801    }
4802}
4803
4804impl Focusable for ProjectPanel {
4805    fn focus_handle(&self, _cx: &App) -> FocusHandle {
4806        self.focus_handle.clone()
4807    }
4808}
4809
4810impl ClipboardEntry {
4811    fn is_cut(&self) -> bool {
4812        matches!(self, Self::Cut { .. })
4813    }
4814
4815    fn items(&self) -> &BTreeSet<SelectedEntry> {
4816        match self {
4817            ClipboardEntry::Copied(entries) | ClipboardEntry::Cut(entries) => entries,
4818        }
4819    }
4820}
4821
4822#[cfg(test)]
4823mod tests {
4824    use super::*;
4825    use collections::HashSet;
4826    use gpui::{Empty, Entity, TestAppContext, VisualTestContext, WindowHandle};
4827    use pretty_assertions::assert_eq;
4828    use project::{FakeFs, WorktreeSettings};
4829    use serde_json::json;
4830    use settings::SettingsStore;
4831    use std::path::{Path, PathBuf};
4832    use util::{path, separator};
4833    use workspace::{
4834        item::{Item, ProjectItem},
4835        register_project_item, AppState,
4836    };
4837
4838    #[gpui::test]
4839    async fn test_visible_list(cx: &mut gpui::TestAppContext) {
4840        init_test(cx);
4841
4842        let fs = FakeFs::new(cx.executor().clone());
4843        fs.insert_tree(
4844            "/root1",
4845            json!({
4846                ".dockerignore": "",
4847                ".git": {
4848                    "HEAD": "",
4849                },
4850                "a": {
4851                    "0": { "q": "", "r": "", "s": "" },
4852                    "1": { "t": "", "u": "" },
4853                    "2": { "v": "", "w": "", "x": "", "y": "" },
4854                },
4855                "b": {
4856                    "3": { "Q": "" },
4857                    "4": { "R": "", "S": "", "T": "", "U": "" },
4858                },
4859                "C": {
4860                    "5": {},
4861                    "6": { "V": "", "W": "" },
4862                    "7": { "X": "" },
4863                    "8": { "Y": {}, "Z": "" }
4864                }
4865            }),
4866        )
4867        .await;
4868        fs.insert_tree(
4869            "/root2",
4870            json!({
4871                "d": {
4872                    "9": ""
4873                },
4874                "e": {}
4875            }),
4876        )
4877        .await;
4878
4879        let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
4880        let workspace =
4881            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4882        let cx = &mut VisualTestContext::from_window(*workspace, cx);
4883        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4884        assert_eq!(
4885            visible_entries_as_strings(&panel, 0..50, cx),
4886            &[
4887                "v root1",
4888                "    > .git",
4889                "    > a",
4890                "    > b",
4891                "    > C",
4892                "      .dockerignore",
4893                "v root2",
4894                "    > d",
4895                "    > e",
4896            ]
4897        );
4898
4899        toggle_expand_dir(&panel, "root1/b", cx);
4900        assert_eq!(
4901            visible_entries_as_strings(&panel, 0..50, cx),
4902            &[
4903                "v root1",
4904                "    > .git",
4905                "    > a",
4906                "    v b  <== selected",
4907                "        > 3",
4908                "        > 4",
4909                "    > C",
4910                "      .dockerignore",
4911                "v root2",
4912                "    > d",
4913                "    > e",
4914            ]
4915        );
4916
4917        assert_eq!(
4918            visible_entries_as_strings(&panel, 6..9, cx),
4919            &[
4920                //
4921                "    > C",
4922                "      .dockerignore",
4923                "v root2",
4924            ]
4925        );
4926    }
4927
4928    #[gpui::test]
4929    async fn test_opening_file(cx: &mut gpui::TestAppContext) {
4930        init_test_with_editor(cx);
4931
4932        let fs = FakeFs::new(cx.executor().clone());
4933        fs.insert_tree(
4934            path!("/src"),
4935            json!({
4936                "test": {
4937                    "first.rs": "// First Rust file",
4938                    "second.rs": "// Second Rust file",
4939                    "third.rs": "// Third Rust file",
4940                }
4941            }),
4942        )
4943        .await;
4944
4945        let project = Project::test(fs.clone(), [path!("/src").as_ref()], cx).await;
4946        let workspace =
4947            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4948        let cx = &mut VisualTestContext::from_window(*workspace, cx);
4949        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4950
4951        toggle_expand_dir(&panel, "src/test", cx);
4952        select_path(&panel, "src/test/first.rs", cx);
4953        panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
4954        cx.executor().run_until_parked();
4955        assert_eq!(
4956            visible_entries_as_strings(&panel, 0..10, cx),
4957            &[
4958                "v src",
4959                "    v test",
4960                "          first.rs  <== selected  <== marked",
4961                "          second.rs",
4962                "          third.rs"
4963            ]
4964        );
4965        ensure_single_file_is_opened(&workspace, "test/first.rs", cx);
4966
4967        select_path(&panel, "src/test/second.rs", cx);
4968        panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
4969        cx.executor().run_until_parked();
4970        assert_eq!(
4971            visible_entries_as_strings(&panel, 0..10, cx),
4972            &[
4973                "v src",
4974                "    v test",
4975                "          first.rs",
4976                "          second.rs  <== selected  <== marked",
4977                "          third.rs"
4978            ]
4979        );
4980        ensure_single_file_is_opened(&workspace, "test/second.rs", cx);
4981    }
4982
4983    #[gpui::test]
4984    async fn test_exclusions_in_visible_list(cx: &mut gpui::TestAppContext) {
4985        init_test(cx);
4986        cx.update(|cx| {
4987            cx.update_global::<SettingsStore, _>(|store, cx| {
4988                store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
4989                    worktree_settings.file_scan_exclusions =
4990                        Some(vec!["**/.git".to_string(), "**/4/**".to_string()]);
4991                });
4992            });
4993        });
4994
4995        let fs = FakeFs::new(cx.background_executor.clone());
4996        fs.insert_tree(
4997            "/root1",
4998            json!({
4999                ".dockerignore": "",
5000                ".git": {
5001                    "HEAD": "",
5002                },
5003                "a": {
5004                    "0": { "q": "", "r": "", "s": "" },
5005                    "1": { "t": "", "u": "" },
5006                    "2": { "v": "", "w": "", "x": "", "y": "" },
5007                },
5008                "b": {
5009                    "3": { "Q": "" },
5010                    "4": { "R": "", "S": "", "T": "", "U": "" },
5011                },
5012                "C": {
5013                    "5": {},
5014                    "6": { "V": "", "W": "" },
5015                    "7": { "X": "" },
5016                    "8": { "Y": {}, "Z": "" }
5017                }
5018            }),
5019        )
5020        .await;
5021        fs.insert_tree(
5022            "/root2",
5023            json!({
5024                "d": {
5025                    "4": ""
5026                },
5027                "e": {}
5028            }),
5029        )
5030        .await;
5031
5032        let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
5033        let workspace =
5034            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
5035        let cx = &mut VisualTestContext::from_window(*workspace, cx);
5036        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
5037        assert_eq!(
5038            visible_entries_as_strings(&panel, 0..50, cx),
5039            &[
5040                "v root1",
5041                "    > a",
5042                "    > b",
5043                "    > C",
5044                "      .dockerignore",
5045                "v root2",
5046                "    > d",
5047                "    > e",
5048            ]
5049        );
5050
5051        toggle_expand_dir(&panel, "root1/b", cx);
5052        assert_eq!(
5053            visible_entries_as_strings(&panel, 0..50, cx),
5054            &[
5055                "v root1",
5056                "    > a",
5057                "    v b  <== selected",
5058                "        > 3",
5059                "    > C",
5060                "      .dockerignore",
5061                "v root2",
5062                "    > d",
5063                "    > e",
5064            ]
5065        );
5066
5067        toggle_expand_dir(&panel, "root2/d", cx);
5068        assert_eq!(
5069            visible_entries_as_strings(&panel, 0..50, cx),
5070            &[
5071                "v root1",
5072                "    > a",
5073                "    v b",
5074                "        > 3",
5075                "    > C",
5076                "      .dockerignore",
5077                "v root2",
5078                "    v d  <== selected",
5079                "    > e",
5080            ]
5081        );
5082
5083        toggle_expand_dir(&panel, "root2/e", cx);
5084        assert_eq!(
5085            visible_entries_as_strings(&panel, 0..50, cx),
5086            &[
5087                "v root1",
5088                "    > a",
5089                "    v b",
5090                "        > 3",
5091                "    > C",
5092                "      .dockerignore",
5093                "v root2",
5094                "    v d",
5095                "    v e  <== selected",
5096            ]
5097        );
5098    }
5099
5100    #[gpui::test]
5101    async fn test_auto_collapse_dir_paths(cx: &mut gpui::TestAppContext) {
5102        init_test(cx);
5103
5104        let fs = FakeFs::new(cx.executor().clone());
5105        fs.insert_tree(
5106            path!("/root1"),
5107            json!({
5108                "dir_1": {
5109                    "nested_dir_1": {
5110                        "nested_dir_2": {
5111                            "nested_dir_3": {
5112                                "file_a.java": "// File contents",
5113                                "file_b.java": "// File contents",
5114                                "file_c.java": "// File contents",
5115                                "nested_dir_4": {
5116                                    "nested_dir_5": {
5117                                        "file_d.java": "// File contents",
5118                                    }
5119                                }
5120                            }
5121                        }
5122                    }
5123                }
5124            }),
5125        )
5126        .await;
5127        fs.insert_tree(
5128            path!("/root2"),
5129            json!({
5130                "dir_2": {
5131                    "file_1.java": "// File contents",
5132                }
5133            }),
5134        )
5135        .await;
5136
5137        let project = Project::test(
5138            fs.clone(),
5139            [path!("/root1").as_ref(), path!("/root2").as_ref()],
5140            cx,
5141        )
5142        .await;
5143        let workspace =
5144            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
5145        let cx = &mut VisualTestContext::from_window(*workspace, cx);
5146        cx.update(|_, cx| {
5147            let settings = *ProjectPanelSettings::get_global(cx);
5148            ProjectPanelSettings::override_global(
5149                ProjectPanelSettings {
5150                    auto_fold_dirs: true,
5151                    ..settings
5152                },
5153                cx,
5154            );
5155        });
5156        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
5157        assert_eq!(
5158            visible_entries_as_strings(&panel, 0..10, cx),
5159            &[
5160                separator!("v root1"),
5161                separator!("    > dir_1/nested_dir_1/nested_dir_2/nested_dir_3"),
5162                separator!("v root2"),
5163                separator!("    > dir_2"),
5164            ]
5165        );
5166
5167        toggle_expand_dir(
5168            &panel,
5169            "root1/dir_1/nested_dir_1/nested_dir_2/nested_dir_3",
5170            cx,
5171        );
5172        assert_eq!(
5173            visible_entries_as_strings(&panel, 0..10, cx),
5174            &[
5175                separator!("v root1"),
5176                separator!("    v dir_1/nested_dir_1/nested_dir_2/nested_dir_3  <== selected"),
5177                separator!("        > nested_dir_4/nested_dir_5"),
5178                separator!("          file_a.java"),
5179                separator!("          file_b.java"),
5180                separator!("          file_c.java"),
5181                separator!("v root2"),
5182                separator!("    > dir_2"),
5183            ]
5184        );
5185
5186        toggle_expand_dir(
5187            &panel,
5188            "root1/dir_1/nested_dir_1/nested_dir_2/nested_dir_3/nested_dir_4/nested_dir_5",
5189            cx,
5190        );
5191        assert_eq!(
5192            visible_entries_as_strings(&panel, 0..10, cx),
5193            &[
5194                separator!("v root1"),
5195                separator!("    v dir_1/nested_dir_1/nested_dir_2/nested_dir_3"),
5196                separator!("        v nested_dir_4/nested_dir_5  <== selected"),
5197                separator!("              file_d.java"),
5198                separator!("          file_a.java"),
5199                separator!("          file_b.java"),
5200                separator!("          file_c.java"),
5201                separator!("v root2"),
5202                separator!("    > dir_2"),
5203            ]
5204        );
5205        toggle_expand_dir(&panel, "root2/dir_2", cx);
5206        assert_eq!(
5207            visible_entries_as_strings(&panel, 0..10, cx),
5208            &[
5209                separator!("v root1"),
5210                separator!("    v dir_1/nested_dir_1/nested_dir_2/nested_dir_3"),
5211                separator!("        v nested_dir_4/nested_dir_5"),
5212                separator!("              file_d.java"),
5213                separator!("          file_a.java"),
5214                separator!("          file_b.java"),
5215                separator!("          file_c.java"),
5216                separator!("v root2"),
5217                separator!("    v dir_2  <== selected"),
5218                separator!("          file_1.java"),
5219            ]
5220        );
5221    }
5222
5223    #[gpui::test(iterations = 30)]
5224    async fn test_editing_files(cx: &mut gpui::TestAppContext) {
5225        init_test(cx);
5226
5227        let fs = FakeFs::new(cx.executor().clone());
5228        fs.insert_tree(
5229            "/root1",
5230            json!({
5231                ".dockerignore": "",
5232                ".git": {
5233                    "HEAD": "",
5234                },
5235                "a": {
5236                    "0": { "q": "", "r": "", "s": "" },
5237                    "1": { "t": "", "u": "" },
5238                    "2": { "v": "", "w": "", "x": "", "y": "" },
5239                },
5240                "b": {
5241                    "3": { "Q": "" },
5242                    "4": { "R": "", "S": "", "T": "", "U": "" },
5243                },
5244                "C": {
5245                    "5": {},
5246                    "6": { "V": "", "W": "" },
5247                    "7": { "X": "" },
5248                    "8": { "Y": {}, "Z": "" }
5249                }
5250            }),
5251        )
5252        .await;
5253        fs.insert_tree(
5254            "/root2",
5255            json!({
5256                "d": {
5257                    "9": ""
5258                },
5259                "e": {}
5260            }),
5261        )
5262        .await;
5263
5264        let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
5265        let workspace =
5266            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
5267        let cx = &mut VisualTestContext::from_window(*workspace, cx);
5268        let panel = workspace
5269            .update(cx, |workspace, window, cx| {
5270                let panel = ProjectPanel::new(workspace, window, cx);
5271                workspace.add_panel(panel.clone(), window, cx);
5272                panel
5273            })
5274            .unwrap();
5275
5276        select_path(&panel, "root1", cx);
5277        assert_eq!(
5278            visible_entries_as_strings(&panel, 0..10, cx),
5279            &[
5280                "v root1  <== selected",
5281                "    > .git",
5282                "    > a",
5283                "    > b",
5284                "    > C",
5285                "      .dockerignore",
5286                "v root2",
5287                "    > d",
5288                "    > e",
5289            ]
5290        );
5291
5292        // Add a file with the root folder selected. The filename editor is placed
5293        // before the first file in the root folder.
5294        panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
5295        panel.update_in(cx, |panel, window, cx| {
5296            assert!(panel.filename_editor.read(cx).is_focused(window));
5297        });
5298        assert_eq!(
5299            visible_entries_as_strings(&panel, 0..10, cx),
5300            &[
5301                "v root1",
5302                "    > .git",
5303                "    > a",
5304                "    > b",
5305                "    > C",
5306                "      [EDITOR: '']  <== selected",
5307                "      .dockerignore",
5308                "v root2",
5309                "    > d",
5310                "    > e",
5311            ]
5312        );
5313
5314        let confirm = panel.update_in(cx, |panel, window, cx| {
5315            panel.filename_editor.update(cx, |editor, cx| {
5316                editor.set_text("the-new-filename", window, cx)
5317            });
5318            panel.confirm_edit(window, cx).unwrap()
5319        });
5320        assert_eq!(
5321            visible_entries_as_strings(&panel, 0..10, cx),
5322            &[
5323                "v root1",
5324                "    > .git",
5325                "    > a",
5326                "    > b",
5327                "    > C",
5328                "      [PROCESSING: 'the-new-filename']  <== selected",
5329                "      .dockerignore",
5330                "v root2",
5331                "    > d",
5332                "    > e",
5333            ]
5334        );
5335
5336        confirm.await.unwrap();
5337        assert_eq!(
5338            visible_entries_as_strings(&panel, 0..10, cx),
5339            &[
5340                "v root1",
5341                "    > .git",
5342                "    > a",
5343                "    > b",
5344                "    > C",
5345                "      .dockerignore",
5346                "      the-new-filename  <== selected  <== marked",
5347                "v root2",
5348                "    > d",
5349                "    > e",
5350            ]
5351        );
5352
5353        select_path(&panel, "root1/b", cx);
5354        panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
5355        assert_eq!(
5356            visible_entries_as_strings(&panel, 0..10, cx),
5357            &[
5358                "v root1",
5359                "    > .git",
5360                "    > a",
5361                "    v b",
5362                "        > 3",
5363                "        > 4",
5364                "          [EDITOR: '']  <== selected",
5365                "    > C",
5366                "      .dockerignore",
5367                "      the-new-filename",
5368            ]
5369        );
5370
5371        panel
5372            .update_in(cx, |panel, window, cx| {
5373                panel.filename_editor.update(cx, |editor, cx| {
5374                    editor.set_text("another-filename.txt", window, cx)
5375                });
5376                panel.confirm_edit(window, cx).unwrap()
5377            })
5378            .await
5379            .unwrap();
5380        assert_eq!(
5381            visible_entries_as_strings(&panel, 0..10, cx),
5382            &[
5383                "v root1",
5384                "    > .git",
5385                "    > a",
5386                "    v b",
5387                "        > 3",
5388                "        > 4",
5389                "          another-filename.txt  <== selected  <== marked",
5390                "    > C",
5391                "      .dockerignore",
5392                "      the-new-filename",
5393            ]
5394        );
5395
5396        select_path(&panel, "root1/b/another-filename.txt", cx);
5397        panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
5398        assert_eq!(
5399            visible_entries_as_strings(&panel, 0..10, cx),
5400            &[
5401                "v root1",
5402                "    > .git",
5403                "    > a",
5404                "    v b",
5405                "        > 3",
5406                "        > 4",
5407                "          [EDITOR: 'another-filename.txt']  <== selected  <== marked",
5408                "    > C",
5409                "      .dockerignore",
5410                "      the-new-filename",
5411            ]
5412        );
5413
5414        let confirm = panel.update_in(cx, |panel, window, cx| {
5415            panel.filename_editor.update(cx, |editor, cx| {
5416                let file_name_selections = editor.selections.all::<usize>(cx);
5417                assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}");
5418                let file_name_selection = &file_name_selections[0];
5419                assert_eq!(file_name_selection.start, 0, "Should select the file name from the start");
5420                assert_eq!(file_name_selection.end, "another-filename".len(), "Should not select file extension");
5421
5422                editor.set_text("a-different-filename.tar.gz", window, cx)
5423            });
5424            panel.confirm_edit(window, cx).unwrap()
5425        });
5426        assert_eq!(
5427            visible_entries_as_strings(&panel, 0..10, cx),
5428            &[
5429                "v root1",
5430                "    > .git",
5431                "    > a",
5432                "    v b",
5433                "        > 3",
5434                "        > 4",
5435                "          [PROCESSING: 'a-different-filename.tar.gz']  <== selected  <== marked",
5436                "    > C",
5437                "      .dockerignore",
5438                "      the-new-filename",
5439            ]
5440        );
5441
5442        confirm.await.unwrap();
5443        assert_eq!(
5444            visible_entries_as_strings(&panel, 0..10, cx),
5445            &[
5446                "v root1",
5447                "    > .git",
5448                "    > a",
5449                "    v b",
5450                "        > 3",
5451                "        > 4",
5452                "          a-different-filename.tar.gz  <== selected",
5453                "    > C",
5454                "      .dockerignore",
5455                "      the-new-filename",
5456            ]
5457        );
5458
5459        panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
5460        assert_eq!(
5461            visible_entries_as_strings(&panel, 0..10, cx),
5462            &[
5463                "v root1",
5464                "    > .git",
5465                "    > a",
5466                "    v b",
5467                "        > 3",
5468                "        > 4",
5469                "          [EDITOR: 'a-different-filename.tar.gz']  <== selected",
5470                "    > C",
5471                "      .dockerignore",
5472                "      the-new-filename",
5473            ]
5474        );
5475
5476        panel.update_in(cx, |panel, window, cx| {
5477            panel.filename_editor.update(cx, |editor, cx| {
5478                let file_name_selections = editor.selections.all::<usize>(cx);
5479                assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}");
5480                let file_name_selection = &file_name_selections[0];
5481                assert_eq!(file_name_selection.start, 0, "Should select the file name from the start");
5482                assert_eq!(file_name_selection.end, "a-different-filename.tar".len(), "Should not select file extension, but still may select anything up to the last dot..");
5483
5484            });
5485            panel.cancel(&menu::Cancel, window, cx)
5486        });
5487
5488        panel.update_in(cx, |panel, window, cx| {
5489            panel.new_directory(&NewDirectory, window, cx)
5490        });
5491        assert_eq!(
5492            visible_entries_as_strings(&panel, 0..10, cx),
5493            &[
5494                "v root1",
5495                "    > .git",
5496                "    > a",
5497                "    v b",
5498                "        > 3",
5499                "        > 4",
5500                "        > [EDITOR: '']  <== selected",
5501                "          a-different-filename.tar.gz",
5502                "    > C",
5503                "      .dockerignore",
5504            ]
5505        );
5506
5507        let confirm = panel.update_in(cx, |panel, window, cx| {
5508            panel
5509                .filename_editor
5510                .update(cx, |editor, cx| editor.set_text("new-dir", window, cx));
5511            panel.confirm_edit(window, cx).unwrap()
5512        });
5513        panel.update_in(cx, |panel, window, cx| {
5514            panel.select_next(&Default::default(), window, cx)
5515        });
5516        assert_eq!(
5517            visible_entries_as_strings(&panel, 0..10, cx),
5518            &[
5519                "v root1",
5520                "    > .git",
5521                "    > a",
5522                "    v b",
5523                "        > 3",
5524                "        > 4",
5525                "        > [PROCESSING: 'new-dir']",
5526                "          a-different-filename.tar.gz  <== selected",
5527                "    > C",
5528                "      .dockerignore",
5529            ]
5530        );
5531
5532        confirm.await.unwrap();
5533        assert_eq!(
5534            visible_entries_as_strings(&panel, 0..10, cx),
5535            &[
5536                "v root1",
5537                "    > .git",
5538                "    > a",
5539                "    v b",
5540                "        > 3",
5541                "        > 4",
5542                "        > new-dir",
5543                "          a-different-filename.tar.gz  <== selected",
5544                "    > C",
5545                "      .dockerignore",
5546            ]
5547        );
5548
5549        panel.update_in(cx, |panel, window, cx| {
5550            panel.rename(&Default::default(), window, cx)
5551        });
5552        assert_eq!(
5553            visible_entries_as_strings(&panel, 0..10, cx),
5554            &[
5555                "v root1",
5556                "    > .git",
5557                "    > a",
5558                "    v b",
5559                "        > 3",
5560                "        > 4",
5561                "        > new-dir",
5562                "          [EDITOR: 'a-different-filename.tar.gz']  <== selected",
5563                "    > C",
5564                "      .dockerignore",
5565            ]
5566        );
5567
5568        // Dismiss the rename editor when it loses focus.
5569        workspace.update(cx, |_, window, _| window.blur()).unwrap();
5570        assert_eq!(
5571            visible_entries_as_strings(&panel, 0..10, cx),
5572            &[
5573                "v root1",
5574                "    > .git",
5575                "    > a",
5576                "    v b",
5577                "        > 3",
5578                "        > 4",
5579                "        > new-dir",
5580                "          a-different-filename.tar.gz  <== selected",
5581                "    > C",
5582                "      .dockerignore",
5583            ]
5584        );
5585    }
5586
5587    #[gpui::test(iterations = 10)]
5588    async fn test_adding_directories_via_file(cx: &mut gpui::TestAppContext) {
5589        init_test(cx);
5590
5591        let fs = FakeFs::new(cx.executor().clone());
5592        fs.insert_tree(
5593            "/root1",
5594            json!({
5595                ".dockerignore": "",
5596                ".git": {
5597                    "HEAD": "",
5598                },
5599                "a": {
5600                    "0": { "q": "", "r": "", "s": "" },
5601                    "1": { "t": "", "u": "" },
5602                    "2": { "v": "", "w": "", "x": "", "y": "" },
5603                },
5604                "b": {
5605                    "3": { "Q": "" },
5606                    "4": { "R": "", "S": "", "T": "", "U": "" },
5607                },
5608                "C": {
5609                    "5": {},
5610                    "6": { "V": "", "W": "" },
5611                    "7": { "X": "" },
5612                    "8": { "Y": {}, "Z": "" }
5613                }
5614            }),
5615        )
5616        .await;
5617        fs.insert_tree(
5618            "/root2",
5619            json!({
5620                "d": {
5621                    "9": ""
5622                },
5623                "e": {}
5624            }),
5625        )
5626        .await;
5627
5628        let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
5629        let workspace =
5630            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
5631        let cx = &mut VisualTestContext::from_window(*workspace, cx);
5632        let panel = workspace
5633            .update(cx, |workspace, window, cx| {
5634                let panel = ProjectPanel::new(workspace, window, cx);
5635                workspace.add_panel(panel.clone(), window, cx);
5636                panel
5637            })
5638            .unwrap();
5639
5640        select_path(&panel, "root1", cx);
5641        assert_eq!(
5642            visible_entries_as_strings(&panel, 0..10, cx),
5643            &[
5644                "v root1  <== selected",
5645                "    > .git",
5646                "    > a",
5647                "    > b",
5648                "    > C",
5649                "      .dockerignore",
5650                "v root2",
5651                "    > d",
5652                "    > e",
5653            ]
5654        );
5655
5656        // Add a file with the root folder selected. The filename editor is placed
5657        // before the first file in the root folder.
5658        panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
5659        panel.update_in(cx, |panel, window, cx| {
5660            assert!(panel.filename_editor.read(cx).is_focused(window));
5661        });
5662        assert_eq!(
5663            visible_entries_as_strings(&panel, 0..10, cx),
5664            &[
5665                "v root1",
5666                "    > .git",
5667                "    > a",
5668                "    > b",
5669                "    > C",
5670                "      [EDITOR: '']  <== selected",
5671                "      .dockerignore",
5672                "v root2",
5673                "    > d",
5674                "    > e",
5675            ]
5676        );
5677
5678        let confirm = panel.update_in(cx, |panel, window, cx| {
5679            panel.filename_editor.update(cx, |editor, cx| {
5680                editor.set_text("/bdir1/dir2/the-new-filename", window, cx)
5681            });
5682            panel.confirm_edit(window, cx).unwrap()
5683        });
5684
5685        assert_eq!(
5686            visible_entries_as_strings(&panel, 0..10, cx),
5687            &[
5688                "v root1",
5689                "    > .git",
5690                "    > a",
5691                "    > b",
5692                "    > C",
5693                "      [PROCESSING: '/bdir1/dir2/the-new-filename']  <== selected",
5694                "      .dockerignore",
5695                "v root2",
5696                "    > d",
5697                "    > e",
5698            ]
5699        );
5700
5701        confirm.await.unwrap();
5702        assert_eq!(
5703            visible_entries_as_strings(&panel, 0..13, cx),
5704            &[
5705                "v root1",
5706                "    > .git",
5707                "    > a",
5708                "    > b",
5709                "    v bdir1",
5710                "        v dir2",
5711                "              the-new-filename  <== selected  <== marked",
5712                "    > C",
5713                "      .dockerignore",
5714                "v root2",
5715                "    > d",
5716                "    > e",
5717            ]
5718        );
5719    }
5720
5721    #[gpui::test]
5722    async fn test_adding_directory_via_file(cx: &mut gpui::TestAppContext) {
5723        init_test(cx);
5724
5725        let fs = FakeFs::new(cx.executor().clone());
5726        fs.insert_tree(
5727            path!("/root1"),
5728            json!({
5729                ".dockerignore": "",
5730                ".git": {
5731                    "HEAD": "",
5732                },
5733            }),
5734        )
5735        .await;
5736
5737        let project = Project::test(fs.clone(), [path!("/root1").as_ref()], cx).await;
5738        let workspace =
5739            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
5740        let cx = &mut VisualTestContext::from_window(*workspace, cx);
5741        let panel = workspace
5742            .update(cx, |workspace, window, cx| {
5743                let panel = ProjectPanel::new(workspace, window, cx);
5744                workspace.add_panel(panel.clone(), window, cx);
5745                panel
5746            })
5747            .unwrap();
5748
5749        select_path(&panel, "root1", cx);
5750        assert_eq!(
5751            visible_entries_as_strings(&panel, 0..10, cx),
5752            &["v root1  <== selected", "    > .git", "      .dockerignore",]
5753        );
5754
5755        // Add a file with the root folder selected. The filename editor is placed
5756        // before the first file in the root folder.
5757        panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
5758        panel.update_in(cx, |panel, window, cx| {
5759            assert!(panel.filename_editor.read(cx).is_focused(window));
5760        });
5761        assert_eq!(
5762            visible_entries_as_strings(&panel, 0..10, cx),
5763            &[
5764                "v root1",
5765                "    > .git",
5766                "      [EDITOR: '']  <== selected",
5767                "      .dockerignore",
5768            ]
5769        );
5770
5771        let confirm = panel.update_in(cx, |panel, window, cx| {
5772            // If we want to create a subdirectory, there should be no prefix slash.
5773            panel
5774                .filename_editor
5775                .update(cx, |editor, cx| editor.set_text("new_dir/", window, cx));
5776            panel.confirm_edit(window, cx).unwrap()
5777        });
5778
5779        assert_eq!(
5780            visible_entries_as_strings(&panel, 0..10, cx),
5781            &[
5782                "v root1",
5783                "    > .git",
5784                "      [PROCESSING: 'new_dir/']  <== selected",
5785                "      .dockerignore",
5786            ]
5787        );
5788
5789        confirm.await.unwrap();
5790        assert_eq!(
5791            visible_entries_as_strings(&panel, 0..10, cx),
5792            &[
5793                "v root1",
5794                "    > .git",
5795                "    v new_dir  <== selected",
5796                "      .dockerignore",
5797            ]
5798        );
5799
5800        // Test filename with whitespace
5801        select_path(&panel, "root1", cx);
5802        panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
5803        let confirm = panel.update_in(cx, |panel, window, cx| {
5804            // If we want to create a subdirectory, there should be no prefix slash.
5805            panel
5806                .filename_editor
5807                .update(cx, |editor, cx| editor.set_text("new dir 2/", window, cx));
5808            panel.confirm_edit(window, cx).unwrap()
5809        });
5810        confirm.await.unwrap();
5811        assert_eq!(
5812            visible_entries_as_strings(&panel, 0..10, cx),
5813            &[
5814                "v root1",
5815                "    > .git",
5816                "    v new dir 2  <== selected",
5817                "    v new_dir",
5818                "      .dockerignore",
5819            ]
5820        );
5821
5822        // Test filename ends with "\"
5823        #[cfg(target_os = "windows")]
5824        {
5825            select_path(&panel, "root1", cx);
5826            panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
5827            let confirm = panel.update_in(cx, |panel, window, cx| {
5828                // If we want to create a subdirectory, there should be no prefix slash.
5829                panel
5830                    .filename_editor
5831                    .update(cx, |editor, cx| editor.set_text("new_dir_3\\", window, cx));
5832                panel.confirm_edit(window, cx).unwrap()
5833            });
5834            confirm.await.unwrap();
5835            assert_eq!(
5836                visible_entries_as_strings(&panel, 0..10, cx),
5837                &[
5838                    "v root1",
5839                    "    > .git",
5840                    "    v new dir 2",
5841                    "    v new_dir",
5842                    "    v new_dir_3  <== selected",
5843                    "      .dockerignore",
5844                ]
5845            );
5846        }
5847    }
5848
5849    #[gpui::test]
5850    async fn test_copy_paste(cx: &mut gpui::TestAppContext) {
5851        init_test(cx);
5852
5853        let fs = FakeFs::new(cx.executor().clone());
5854        fs.insert_tree(
5855            "/root1",
5856            json!({
5857                "one.two.txt": "",
5858                "one.txt": ""
5859            }),
5860        )
5861        .await;
5862
5863        let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
5864        let workspace =
5865            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
5866        let cx = &mut VisualTestContext::from_window(*workspace, cx);
5867        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
5868
5869        panel.update_in(cx, |panel, window, cx| {
5870            panel.select_next(&Default::default(), window, cx);
5871            panel.select_next(&Default::default(), window, cx);
5872        });
5873
5874        assert_eq!(
5875            visible_entries_as_strings(&panel, 0..50, cx),
5876            &[
5877                //
5878                "v root1",
5879                "      one.txt  <== selected",
5880                "      one.two.txt",
5881            ]
5882        );
5883
5884        // Regression test - file name is created correctly when
5885        // the copied file's name contains multiple dots.
5886        panel.update_in(cx, |panel, window, cx| {
5887            panel.copy(&Default::default(), window, cx);
5888            panel.paste(&Default::default(), window, cx);
5889        });
5890        cx.executor().run_until_parked();
5891
5892        assert_eq!(
5893            visible_entries_as_strings(&panel, 0..50, cx),
5894            &[
5895                //
5896                "v root1",
5897                "      one.txt",
5898                "      [EDITOR: 'one copy.txt']  <== selected  <== marked",
5899                "      one.two.txt",
5900            ]
5901        );
5902
5903        panel.update_in(cx, |panel, window, cx| {
5904            panel.filename_editor.update(cx, |editor, cx| {
5905                let file_name_selections = editor.selections.all::<usize>(cx);
5906                assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}");
5907                let file_name_selection = &file_name_selections[0];
5908                assert_eq!(file_name_selection.start, "one".len(), "Should select the file name disambiguation after the original file name");
5909                assert_eq!(file_name_selection.end, "one copy".len(), "Should select the file name disambiguation until the extension");
5910            });
5911            assert!(panel.confirm_edit(window, cx).is_none());
5912        });
5913
5914        panel.update_in(cx, |panel, window, cx| {
5915            panel.paste(&Default::default(), window, cx);
5916        });
5917        cx.executor().run_until_parked();
5918
5919        assert_eq!(
5920            visible_entries_as_strings(&panel, 0..50, cx),
5921            &[
5922                //
5923                "v root1",
5924                "      one.txt",
5925                "      one copy.txt",
5926                "      [EDITOR: 'one copy 1.txt']  <== selected  <== marked",
5927                "      one.two.txt",
5928            ]
5929        );
5930
5931        panel.update_in(cx, |panel, window, cx| {
5932            assert!(panel.confirm_edit(window, cx).is_none())
5933        });
5934    }
5935
5936    #[gpui::test]
5937    async fn test_cut_paste_between_different_worktrees(cx: &mut gpui::TestAppContext) {
5938        init_test(cx);
5939
5940        let fs = FakeFs::new(cx.executor().clone());
5941        fs.insert_tree(
5942            "/root1",
5943            json!({
5944                "one.txt": "",
5945                "two.txt": "",
5946                "three.txt": "",
5947                "a": {
5948                    "0": { "q": "", "r": "", "s": "" },
5949                    "1": { "t": "", "u": "" },
5950                    "2": { "v": "", "w": "", "x": "", "y": "" },
5951                },
5952            }),
5953        )
5954        .await;
5955
5956        fs.insert_tree(
5957            "/root2",
5958            json!({
5959                "one.txt": "",
5960                "two.txt": "",
5961                "four.txt": "",
5962                "b": {
5963                    "3": { "Q": "" },
5964                    "4": { "R": "", "S": "", "T": "", "U": "" },
5965                },
5966            }),
5967        )
5968        .await;
5969
5970        let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
5971        let workspace =
5972            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
5973        let cx = &mut VisualTestContext::from_window(*workspace, cx);
5974        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
5975
5976        select_path(&panel, "root1/three.txt", cx);
5977        panel.update_in(cx, |panel, window, cx| {
5978            panel.cut(&Default::default(), window, cx);
5979        });
5980
5981        select_path(&panel, "root2/one.txt", cx);
5982        panel.update_in(cx, |panel, window, cx| {
5983            panel.select_next(&Default::default(), window, cx);
5984            panel.paste(&Default::default(), window, cx);
5985        });
5986        cx.executor().run_until_parked();
5987        assert_eq!(
5988            visible_entries_as_strings(&panel, 0..50, cx),
5989            &[
5990                //
5991                "v root1",
5992                "    > a",
5993                "      one.txt",
5994                "      two.txt",
5995                "v root2",
5996                "    > b",
5997                "      four.txt",
5998                "      one.txt",
5999                "      three.txt  <== selected  <== marked",
6000                "      two.txt",
6001            ]
6002        );
6003
6004        select_path(&panel, "root1/a", cx);
6005        panel.update_in(cx, |panel, window, cx| {
6006            panel.cut(&Default::default(), window, cx);
6007        });
6008        select_path(&panel, "root2/two.txt", cx);
6009        panel.update_in(cx, |panel, window, cx| {
6010            panel.select_next(&Default::default(), window, cx);
6011            panel.paste(&Default::default(), window, cx);
6012        });
6013
6014        cx.executor().run_until_parked();
6015        assert_eq!(
6016            visible_entries_as_strings(&panel, 0..50, cx),
6017            &[
6018                //
6019                "v root1",
6020                "      one.txt",
6021                "      two.txt",
6022                "v root2",
6023                "    > a  <== selected",
6024                "    > b",
6025                "      four.txt",
6026                "      one.txt",
6027                "      three.txt  <== marked",
6028                "      two.txt",
6029            ]
6030        );
6031    }
6032
6033    #[gpui::test]
6034    async fn test_copy_paste_between_different_worktrees(cx: &mut gpui::TestAppContext) {
6035        init_test(cx);
6036
6037        let fs = FakeFs::new(cx.executor().clone());
6038        fs.insert_tree(
6039            "/root1",
6040            json!({
6041                "one.txt": "",
6042                "two.txt": "",
6043                "three.txt": "",
6044                "a": {
6045                    "0": { "q": "", "r": "", "s": "" },
6046                    "1": { "t": "", "u": "" },
6047                    "2": { "v": "", "w": "", "x": "", "y": "" },
6048                },
6049            }),
6050        )
6051        .await;
6052
6053        fs.insert_tree(
6054            "/root2",
6055            json!({
6056                "one.txt": "",
6057                "two.txt": "",
6058                "four.txt": "",
6059                "b": {
6060                    "3": { "Q": "" },
6061                    "4": { "R": "", "S": "", "T": "", "U": "" },
6062                },
6063            }),
6064        )
6065        .await;
6066
6067        let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
6068        let workspace =
6069            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6070        let cx = &mut VisualTestContext::from_window(*workspace, cx);
6071        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
6072
6073        select_path(&panel, "root1/three.txt", cx);
6074        panel.update_in(cx, |panel, window, cx| {
6075            panel.copy(&Default::default(), window, cx);
6076        });
6077
6078        select_path(&panel, "root2/one.txt", cx);
6079        panel.update_in(cx, |panel, window, cx| {
6080            panel.select_next(&Default::default(), window, cx);
6081            panel.paste(&Default::default(), window, cx);
6082        });
6083        cx.executor().run_until_parked();
6084        assert_eq!(
6085            visible_entries_as_strings(&panel, 0..50, cx),
6086            &[
6087                //
6088                "v root1",
6089                "    > a",
6090                "      one.txt",
6091                "      three.txt",
6092                "      two.txt",
6093                "v root2",
6094                "    > b",
6095                "      four.txt",
6096                "      one.txt",
6097                "      three.txt  <== selected  <== marked",
6098                "      two.txt",
6099            ]
6100        );
6101
6102        select_path(&panel, "root1/three.txt", cx);
6103        panel.update_in(cx, |panel, window, cx| {
6104            panel.copy(&Default::default(), window, cx);
6105        });
6106        select_path(&panel, "root2/two.txt", cx);
6107        panel.update_in(cx, |panel, window, cx| {
6108            panel.select_next(&Default::default(), window, cx);
6109            panel.paste(&Default::default(), window, cx);
6110        });
6111
6112        cx.executor().run_until_parked();
6113        assert_eq!(
6114            visible_entries_as_strings(&panel, 0..50, cx),
6115            &[
6116                //
6117                "v root1",
6118                "    > a",
6119                "      one.txt",
6120                "      three.txt",
6121                "      two.txt",
6122                "v root2",
6123                "    > b",
6124                "      four.txt",
6125                "      one.txt",
6126                "      three.txt",
6127                "      [EDITOR: 'three copy.txt']  <== selected  <== marked",
6128                "      two.txt",
6129            ]
6130        );
6131
6132        panel.update_in(cx, |panel, window, cx| {
6133            panel.cancel(&menu::Cancel {}, window, cx)
6134        });
6135        cx.executor().run_until_parked();
6136
6137        select_path(&panel, "root1/a", cx);
6138        panel.update_in(cx, |panel, window, cx| {
6139            panel.copy(&Default::default(), window, cx);
6140        });
6141        select_path(&panel, "root2/two.txt", cx);
6142        panel.update_in(cx, |panel, window, cx| {
6143            panel.select_next(&Default::default(), window, cx);
6144            panel.paste(&Default::default(), window, cx);
6145        });
6146
6147        cx.executor().run_until_parked();
6148        assert_eq!(
6149            visible_entries_as_strings(&panel, 0..50, cx),
6150            &[
6151                //
6152                "v root1",
6153                "    > a",
6154                "      one.txt",
6155                "      three.txt",
6156                "      two.txt",
6157                "v root2",
6158                "    > a  <== selected",
6159                "    > b",
6160                "      four.txt",
6161                "      one.txt",
6162                "      three.txt",
6163                "      three copy.txt",
6164                "      two.txt",
6165            ]
6166        );
6167    }
6168
6169    #[gpui::test]
6170    async fn test_copy_paste_directory(cx: &mut gpui::TestAppContext) {
6171        init_test(cx);
6172
6173        let fs = FakeFs::new(cx.executor().clone());
6174        fs.insert_tree(
6175            "/root",
6176            json!({
6177                "a": {
6178                    "one.txt": "",
6179                    "two.txt": "",
6180                    "inner_dir": {
6181                        "three.txt": "",
6182                        "four.txt": "",
6183                    }
6184                },
6185                "b": {}
6186            }),
6187        )
6188        .await;
6189
6190        let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
6191        let workspace =
6192            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6193        let cx = &mut VisualTestContext::from_window(*workspace, cx);
6194        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
6195
6196        select_path(&panel, "root/a", cx);
6197        panel.update_in(cx, |panel, window, cx| {
6198            panel.copy(&Default::default(), window, cx);
6199            panel.select_next(&Default::default(), window, cx);
6200            panel.paste(&Default::default(), window, cx);
6201        });
6202        cx.executor().run_until_parked();
6203
6204        let pasted_dir = find_project_entry(&panel, "root/b/a", cx);
6205        assert_ne!(pasted_dir, None, "Pasted directory should have an entry");
6206
6207        let pasted_dir_file = find_project_entry(&panel, "root/b/a/one.txt", cx);
6208        assert_ne!(
6209            pasted_dir_file, None,
6210            "Pasted directory file should have an entry"
6211        );
6212
6213        let pasted_dir_inner_dir = find_project_entry(&panel, "root/b/a/inner_dir", cx);
6214        assert_ne!(
6215            pasted_dir_inner_dir, None,
6216            "Directories inside pasted directory should have an entry"
6217        );
6218
6219        toggle_expand_dir(&panel, "root/b/a", cx);
6220        toggle_expand_dir(&panel, "root/b/a/inner_dir", cx);
6221
6222        assert_eq!(
6223            visible_entries_as_strings(&panel, 0..50, cx),
6224            &[
6225                //
6226                "v root",
6227                "    > a",
6228                "    v b",
6229                "        v a",
6230                "            v inner_dir  <== selected",
6231                "                  four.txt",
6232                "                  three.txt",
6233                "              one.txt",
6234                "              two.txt",
6235            ]
6236        );
6237
6238        select_path(&panel, "root", cx);
6239        panel.update_in(cx, |panel, window, cx| {
6240            panel.paste(&Default::default(), window, cx)
6241        });
6242        cx.executor().run_until_parked();
6243        assert_eq!(
6244            visible_entries_as_strings(&panel, 0..50, cx),
6245            &[
6246                //
6247                "v root",
6248                "    > a",
6249                "    > [EDITOR: 'a copy']  <== selected",
6250                "    v b",
6251                "        v a",
6252                "            v inner_dir",
6253                "                  four.txt",
6254                "                  three.txt",
6255                "              one.txt",
6256                "              two.txt"
6257            ]
6258        );
6259
6260        let confirm = panel.update_in(cx, |panel, window, cx| {
6261            panel
6262                .filename_editor
6263                .update(cx, |editor, cx| editor.set_text("c", window, cx));
6264            panel.confirm_edit(window, cx).unwrap()
6265        });
6266        assert_eq!(
6267            visible_entries_as_strings(&panel, 0..50, cx),
6268            &[
6269                //
6270                "v root",
6271                "    > a",
6272                "    > [PROCESSING: 'c']  <== selected",
6273                "    v b",
6274                "        v a",
6275                "            v inner_dir",
6276                "                  four.txt",
6277                "                  three.txt",
6278                "              one.txt",
6279                "              two.txt"
6280            ]
6281        );
6282
6283        confirm.await.unwrap();
6284
6285        panel.update_in(cx, |panel, window, cx| {
6286            panel.paste(&Default::default(), window, cx)
6287        });
6288        cx.executor().run_until_parked();
6289        assert_eq!(
6290            visible_entries_as_strings(&panel, 0..50, cx),
6291            &[
6292                //
6293                "v root",
6294                "    > a",
6295                "    v b",
6296                "        v a",
6297                "            v inner_dir",
6298                "                  four.txt",
6299                "                  three.txt",
6300                "              one.txt",
6301                "              two.txt",
6302                "    v c",
6303                "        > a  <== selected",
6304                "        > inner_dir",
6305                "          one.txt",
6306                "          two.txt",
6307            ]
6308        );
6309    }
6310
6311    #[gpui::test]
6312    async fn test_copy_paste_directory_with_sibling_file(cx: &mut gpui::TestAppContext) {
6313        init_test(cx);
6314
6315        let fs = FakeFs::new(cx.executor().clone());
6316        fs.insert_tree(
6317            "/test",
6318            json!({
6319                "dir1": {
6320                    "a.txt": "",
6321                    "b.txt": "",
6322                },
6323                "dir2": {},
6324                "c.txt": "",
6325                "d.txt": "",
6326            }),
6327        )
6328        .await;
6329
6330        let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
6331        let workspace =
6332            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6333        let cx = &mut VisualTestContext::from_window(*workspace, cx);
6334        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
6335
6336        toggle_expand_dir(&panel, "test/dir1", cx);
6337
6338        cx.simulate_modifiers_change(gpui::Modifiers {
6339            control: true,
6340            ..Default::default()
6341        });
6342
6343        select_path_with_mark(&panel, "test/dir1", cx);
6344        select_path_with_mark(&panel, "test/c.txt", cx);
6345
6346        assert_eq!(
6347            visible_entries_as_strings(&panel, 0..15, cx),
6348            &[
6349                "v test",
6350                "    v dir1  <== marked",
6351                "          a.txt",
6352                "          b.txt",
6353                "    > dir2",
6354                "      c.txt  <== selected  <== marked",
6355                "      d.txt",
6356            ],
6357            "Initial state before copying dir1 and c.txt"
6358        );
6359
6360        panel.update_in(cx, |panel, window, cx| {
6361            panel.copy(&Default::default(), window, cx);
6362        });
6363        select_path(&panel, "test/dir2", cx);
6364        panel.update_in(cx, |panel, window, cx| {
6365            panel.paste(&Default::default(), window, cx);
6366        });
6367        cx.executor().run_until_parked();
6368
6369        toggle_expand_dir(&panel, "test/dir2/dir1", cx);
6370
6371        assert_eq!(
6372            visible_entries_as_strings(&panel, 0..15, cx),
6373            &[
6374                "v test",
6375                "    v dir1  <== marked",
6376                "          a.txt",
6377                "          b.txt",
6378                "    v dir2",
6379                "        v dir1  <== selected",
6380                "              a.txt",
6381                "              b.txt",
6382                "          c.txt",
6383                "      c.txt  <== marked",
6384                "      d.txt",
6385            ],
6386            "Should copy dir1 as well as c.txt into dir2"
6387        );
6388
6389        // Disambiguating multiple files should not open the rename editor.
6390        select_path(&panel, "test/dir2", cx);
6391        panel.update_in(cx, |panel, window, cx| {
6392            panel.paste(&Default::default(), window, cx);
6393        });
6394        cx.executor().run_until_parked();
6395
6396        assert_eq!(
6397            visible_entries_as_strings(&panel, 0..15, cx),
6398            &[
6399                "v test",
6400                "    v dir1  <== marked",
6401                "          a.txt",
6402                "          b.txt",
6403                "    v dir2",
6404                "        v dir1",
6405                "              a.txt",
6406                "              b.txt",
6407                "        > dir1 copy  <== selected",
6408                "          c.txt",
6409                "          c copy.txt",
6410                "      c.txt  <== marked",
6411                "      d.txt",
6412            ],
6413            "Should copy dir1 as well as c.txt into dir2 and disambiguate them without opening the rename editor"
6414        );
6415    }
6416
6417    #[gpui::test]
6418    async fn test_copy_paste_nested_and_root_entries(cx: &mut gpui::TestAppContext) {
6419        init_test(cx);
6420
6421        let fs = FakeFs::new(cx.executor().clone());
6422        fs.insert_tree(
6423            "/test",
6424            json!({
6425                "dir1": {
6426                    "a.txt": "",
6427                    "b.txt": "",
6428                },
6429                "dir2": {},
6430                "c.txt": "",
6431                "d.txt": "",
6432            }),
6433        )
6434        .await;
6435
6436        let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
6437        let workspace =
6438            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6439        let cx = &mut VisualTestContext::from_window(*workspace, cx);
6440        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
6441
6442        toggle_expand_dir(&panel, "test/dir1", cx);
6443
6444        cx.simulate_modifiers_change(gpui::Modifiers {
6445            control: true,
6446            ..Default::default()
6447        });
6448
6449        select_path_with_mark(&panel, "test/dir1/a.txt", cx);
6450        select_path_with_mark(&panel, "test/dir1", cx);
6451        select_path_with_mark(&panel, "test/c.txt", cx);
6452
6453        assert_eq!(
6454            visible_entries_as_strings(&panel, 0..15, cx),
6455            &[
6456                "v test",
6457                "    v dir1  <== marked",
6458                "          a.txt  <== marked",
6459                "          b.txt",
6460                "    > dir2",
6461                "      c.txt  <== selected  <== marked",
6462                "      d.txt",
6463            ],
6464            "Initial state before copying a.txt, dir1 and c.txt"
6465        );
6466
6467        panel.update_in(cx, |panel, window, cx| {
6468            panel.copy(&Default::default(), window, cx);
6469        });
6470        select_path(&panel, "test/dir2", cx);
6471        panel.update_in(cx, |panel, window, cx| {
6472            panel.paste(&Default::default(), window, cx);
6473        });
6474        cx.executor().run_until_parked();
6475
6476        toggle_expand_dir(&panel, "test/dir2/dir1", cx);
6477
6478        assert_eq!(
6479            visible_entries_as_strings(&panel, 0..20, cx),
6480            &[
6481                "v test",
6482                "    v dir1  <== marked",
6483                "          a.txt  <== marked",
6484                "          b.txt",
6485                "    v dir2",
6486                "        v dir1  <== selected",
6487                "              a.txt",
6488                "              b.txt",
6489                "          c.txt",
6490                "      c.txt  <== marked",
6491                "      d.txt",
6492            ],
6493            "Should copy dir1 and c.txt into dir2. a.txt is already present in copied dir1."
6494        );
6495    }
6496
6497    #[gpui::test]
6498    async fn test_remove_opened_file(cx: &mut gpui::TestAppContext) {
6499        init_test_with_editor(cx);
6500
6501        let fs = FakeFs::new(cx.executor().clone());
6502        fs.insert_tree(
6503            path!("/src"),
6504            json!({
6505                "test": {
6506                    "first.rs": "// First Rust file",
6507                    "second.rs": "// Second Rust file",
6508                    "third.rs": "// Third Rust file",
6509                }
6510            }),
6511        )
6512        .await;
6513
6514        let project = Project::test(fs.clone(), [path!("/src").as_ref()], cx).await;
6515        let workspace =
6516            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6517        let cx = &mut VisualTestContext::from_window(*workspace, cx);
6518        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
6519
6520        toggle_expand_dir(&panel, "src/test", cx);
6521        select_path(&panel, "src/test/first.rs", cx);
6522        panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
6523        cx.executor().run_until_parked();
6524        assert_eq!(
6525            visible_entries_as_strings(&panel, 0..10, cx),
6526            &[
6527                "v src",
6528                "    v test",
6529                "          first.rs  <== selected  <== marked",
6530                "          second.rs",
6531                "          third.rs"
6532            ]
6533        );
6534        ensure_single_file_is_opened(&workspace, "test/first.rs", cx);
6535
6536        submit_deletion(&panel, cx);
6537        assert_eq!(
6538            visible_entries_as_strings(&panel, 0..10, cx),
6539            &[
6540                "v src",
6541                "    v test",
6542                "          second.rs  <== selected",
6543                "          third.rs"
6544            ],
6545            "Project panel should have no deleted file, no other file is selected in it"
6546        );
6547        ensure_no_open_items_and_panes(&workspace, cx);
6548
6549        panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
6550        cx.executor().run_until_parked();
6551        assert_eq!(
6552            visible_entries_as_strings(&panel, 0..10, cx),
6553            &[
6554                "v src",
6555                "    v test",
6556                "          second.rs  <== selected  <== marked",
6557                "          third.rs"
6558            ]
6559        );
6560        ensure_single_file_is_opened(&workspace, "test/second.rs", cx);
6561
6562        workspace
6563            .update(cx, |workspace, window, cx| {
6564                let active_items = workspace
6565                    .panes()
6566                    .iter()
6567                    .filter_map(|pane| pane.read(cx).active_item())
6568                    .collect::<Vec<_>>();
6569                assert_eq!(active_items.len(), 1);
6570                let open_editor = active_items
6571                    .into_iter()
6572                    .next()
6573                    .unwrap()
6574                    .downcast::<Editor>()
6575                    .expect("Open item should be an editor");
6576                open_editor.update(cx, |editor, cx| {
6577                    editor.set_text("Another text!", window, cx)
6578                });
6579            })
6580            .unwrap();
6581        submit_deletion_skipping_prompt(&panel, cx);
6582        assert_eq!(
6583            visible_entries_as_strings(&panel, 0..10, cx),
6584            &["v src", "    v test", "          third.rs  <== selected"],
6585            "Project panel should have no deleted file, with one last file remaining"
6586        );
6587        ensure_no_open_items_and_panes(&workspace, cx);
6588    }
6589
6590    #[gpui::test]
6591    async fn test_create_duplicate_items(cx: &mut gpui::TestAppContext) {
6592        init_test_with_editor(cx);
6593
6594        let fs = FakeFs::new(cx.executor().clone());
6595        fs.insert_tree(
6596            "/src",
6597            json!({
6598                "test": {
6599                    "first.rs": "// First Rust file",
6600                    "second.rs": "// Second Rust file",
6601                    "third.rs": "// Third Rust file",
6602                }
6603            }),
6604        )
6605        .await;
6606
6607        let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
6608        let workspace =
6609            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6610        let cx = &mut VisualTestContext::from_window(*workspace, cx);
6611        let panel = workspace
6612            .update(cx, |workspace, window, cx| {
6613                let panel = ProjectPanel::new(workspace, window, cx);
6614                workspace.add_panel(panel.clone(), window, cx);
6615                panel
6616            })
6617            .unwrap();
6618
6619        select_path(&panel, "src/", cx);
6620        panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
6621        cx.executor().run_until_parked();
6622        assert_eq!(
6623            visible_entries_as_strings(&panel, 0..10, cx),
6624            &[
6625                //
6626                "v src  <== selected",
6627                "    > test"
6628            ]
6629        );
6630        panel.update_in(cx, |panel, window, cx| {
6631            panel.new_directory(&NewDirectory, window, cx)
6632        });
6633        panel.update_in(cx, |panel, window, cx| {
6634            assert!(panel.filename_editor.read(cx).is_focused(window));
6635        });
6636        assert_eq!(
6637            visible_entries_as_strings(&panel, 0..10, cx),
6638            &[
6639                //
6640                "v src",
6641                "    > [EDITOR: '']  <== selected",
6642                "    > test"
6643            ]
6644        );
6645        panel.update_in(cx, |panel, window, cx| {
6646            panel
6647                .filename_editor
6648                .update(cx, |editor, cx| editor.set_text("test", window, cx));
6649            assert!(
6650                panel.confirm_edit(window, cx).is_none(),
6651                "Should not allow to confirm on conflicting new directory name"
6652            )
6653        });
6654        assert_eq!(
6655            visible_entries_as_strings(&panel, 0..10, cx),
6656            &[
6657                //
6658                "v src",
6659                "    > test"
6660            ],
6661            "File list should be unchanged after failed folder create confirmation"
6662        );
6663
6664        select_path(&panel, "src/test/", cx);
6665        panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
6666        cx.executor().run_until_parked();
6667        assert_eq!(
6668            visible_entries_as_strings(&panel, 0..10, cx),
6669            &[
6670                //
6671                "v src",
6672                "    > test  <== selected"
6673            ]
6674        );
6675        panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
6676        panel.update_in(cx, |panel, window, cx| {
6677            assert!(panel.filename_editor.read(cx).is_focused(window));
6678        });
6679        assert_eq!(
6680            visible_entries_as_strings(&panel, 0..10, cx),
6681            &[
6682                "v src",
6683                "    v test",
6684                "          [EDITOR: '']  <== selected",
6685                "          first.rs",
6686                "          second.rs",
6687                "          third.rs"
6688            ]
6689        );
6690        panel.update_in(cx, |panel, window, cx| {
6691            panel
6692                .filename_editor
6693                .update(cx, |editor, cx| editor.set_text("first.rs", window, cx));
6694            assert!(
6695                panel.confirm_edit(window, cx).is_none(),
6696                "Should not allow to confirm on conflicting new file name"
6697            )
6698        });
6699        assert_eq!(
6700            visible_entries_as_strings(&panel, 0..10, cx),
6701            &[
6702                "v src",
6703                "    v test",
6704                "          first.rs",
6705                "          second.rs",
6706                "          third.rs"
6707            ],
6708            "File list should be unchanged after failed file create confirmation"
6709        );
6710
6711        select_path(&panel, "src/test/first.rs", cx);
6712        panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
6713        cx.executor().run_until_parked();
6714        assert_eq!(
6715            visible_entries_as_strings(&panel, 0..10, cx),
6716            &[
6717                "v src",
6718                "    v test",
6719                "          first.rs  <== selected",
6720                "          second.rs",
6721                "          third.rs"
6722            ],
6723        );
6724        panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
6725        panel.update_in(cx, |panel, window, cx| {
6726            assert!(panel.filename_editor.read(cx).is_focused(window));
6727        });
6728        assert_eq!(
6729            visible_entries_as_strings(&panel, 0..10, cx),
6730            &[
6731                "v src",
6732                "    v test",
6733                "          [EDITOR: 'first.rs']  <== selected",
6734                "          second.rs",
6735                "          third.rs"
6736            ]
6737        );
6738        panel.update_in(cx, |panel, window, cx| {
6739            panel
6740                .filename_editor
6741                .update(cx, |editor, cx| editor.set_text("second.rs", window, cx));
6742            assert!(
6743                panel.confirm_edit(window, cx).is_none(),
6744                "Should not allow to confirm on conflicting file rename"
6745            )
6746        });
6747        assert_eq!(
6748            visible_entries_as_strings(&panel, 0..10, cx),
6749            &[
6750                "v src",
6751                "    v test",
6752                "          first.rs  <== selected",
6753                "          second.rs",
6754                "          third.rs"
6755            ],
6756            "File list should be unchanged after failed rename confirmation"
6757        );
6758    }
6759
6760    #[gpui::test]
6761    async fn test_select_git_entry(cx: &mut gpui::TestAppContext) {
6762        use git::status::{FileStatus, StatusCode, TrackedStatus};
6763        use std::path::Path;
6764
6765        init_test_with_editor(cx);
6766
6767        let fs = FakeFs::new(cx.executor().clone());
6768        fs.insert_tree(
6769            "/root",
6770            json!({
6771                "tree1": {
6772                    ".git": {},
6773                    "dir1": {
6774                        "modified1.txt": "",
6775                        "unmodified1.txt": "",
6776                        "modified2.txt": "",
6777                    },
6778                    "dir2": {
6779                        "modified3.txt": "",
6780                        "unmodified2.txt": "",
6781                    },
6782                    "modified4.txt": "",
6783                    "unmodified3.txt": "",
6784                },
6785                "tree2": {
6786                    ".git": {},
6787                    "dir3": {
6788                        "modified5.txt": "",
6789                        "unmodified4.txt": "",
6790                    },
6791                    "modified6.txt": "",
6792                    "unmodified5.txt": "",
6793                }
6794            }),
6795        )
6796        .await;
6797
6798        // Mark files as git modified
6799        let tree1_modified_files = [
6800            "dir1/modified1.txt",
6801            "dir1/modified2.txt",
6802            "modified4.txt",
6803            "dir2/modified3.txt",
6804        ];
6805
6806        let tree2_modified_files = ["dir3/modified5.txt", "modified6.txt"];
6807
6808        let root1_dot_git = Path::new("/root/tree1/.git");
6809        let root2_dot_git = Path::new("/root/tree2/.git");
6810        let set_value = FileStatus::Tracked(TrackedStatus {
6811            index_status: StatusCode::Modified,
6812            worktree_status: StatusCode::Modified,
6813        });
6814
6815        fs.with_git_state(&root1_dot_git, true, |git_repo_state| {
6816            for file_path in tree1_modified_files {
6817                git_repo_state.statuses.insert(file_path.into(), set_value);
6818            }
6819        });
6820
6821        fs.with_git_state(&root2_dot_git, true, |git_repo_state| {
6822            for file_path in tree2_modified_files {
6823                git_repo_state.statuses.insert(file_path.into(), set_value);
6824            }
6825        });
6826
6827        let project = Project::test(
6828            fs.clone(),
6829            ["/root/tree1".as_ref(), "/root/tree2".as_ref()],
6830            cx,
6831        )
6832        .await;
6833        let workspace =
6834            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6835        let cx = &mut VisualTestContext::from_window(*workspace, cx);
6836        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
6837
6838        // Check initial state
6839        assert_eq!(
6840            visible_entries_as_strings(&panel, 0..15, cx),
6841            &[
6842                "v tree1",
6843                "    > .git",
6844                "    > dir1",
6845                "    > dir2",
6846                "      modified4.txt",
6847                "      unmodified3.txt",
6848                "v tree2",
6849                "    > .git",
6850                "    > dir3",
6851                "      modified6.txt",
6852                "      unmodified5.txt"
6853            ],
6854        );
6855
6856        // Test selecting next modified entry
6857        panel.update_in(cx, |panel, window, cx| {
6858            panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
6859        });
6860
6861        assert_eq!(
6862            visible_entries_as_strings(&panel, 0..6, cx),
6863            &[
6864                "v tree1",
6865                "    > .git",
6866                "    v dir1",
6867                "          modified1.txt  <== selected",
6868                "          modified2.txt",
6869                "          unmodified1.txt",
6870            ],
6871        );
6872
6873        panel.update_in(cx, |panel, window, cx| {
6874            panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
6875        });
6876
6877        assert_eq!(
6878            visible_entries_as_strings(&panel, 0..6, cx),
6879            &[
6880                "v tree1",
6881                "    > .git",
6882                "    v dir1",
6883                "          modified1.txt",
6884                "          modified2.txt  <== selected",
6885                "          unmodified1.txt",
6886            ],
6887        );
6888
6889        panel.update_in(cx, |panel, window, cx| {
6890            panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
6891        });
6892
6893        assert_eq!(
6894            visible_entries_as_strings(&panel, 6..9, cx),
6895            &[
6896                "    v dir2",
6897                "          modified3.txt  <== selected",
6898                "          unmodified2.txt",
6899            ],
6900        );
6901
6902        panel.update_in(cx, |panel, window, cx| {
6903            panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
6904        });
6905
6906        assert_eq!(
6907            visible_entries_as_strings(&panel, 9..11, cx),
6908            &["      modified4.txt  <== selected", "      unmodified3.txt",],
6909        );
6910
6911        panel.update_in(cx, |panel, window, cx| {
6912            panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
6913        });
6914
6915        assert_eq!(
6916            visible_entries_as_strings(&panel, 13..16, cx),
6917            &[
6918                "    v dir3",
6919                "          modified5.txt  <== selected",
6920                "          unmodified4.txt",
6921            ],
6922        );
6923
6924        panel.update_in(cx, |panel, window, cx| {
6925            panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
6926        });
6927
6928        assert_eq!(
6929            visible_entries_as_strings(&panel, 16..18, cx),
6930            &["      modified6.txt  <== selected", "      unmodified5.txt",],
6931        );
6932
6933        // Wraps around to first modified file
6934        panel.update_in(cx, |panel, window, cx| {
6935            panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
6936        });
6937
6938        assert_eq!(
6939            visible_entries_as_strings(&panel, 0..18, cx),
6940            &[
6941                "v tree1",
6942                "    > .git",
6943                "    v dir1",
6944                "          modified1.txt  <== selected",
6945                "          modified2.txt",
6946                "          unmodified1.txt",
6947                "    v dir2",
6948                "          modified3.txt",
6949                "          unmodified2.txt",
6950                "      modified4.txt",
6951                "      unmodified3.txt",
6952                "v tree2",
6953                "    > .git",
6954                "    v dir3",
6955                "          modified5.txt",
6956                "          unmodified4.txt",
6957                "      modified6.txt",
6958                "      unmodified5.txt",
6959            ],
6960        );
6961
6962        // Wraps around again to last modified file
6963        panel.update_in(cx, |panel, window, cx| {
6964            panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
6965        });
6966
6967        assert_eq!(
6968            visible_entries_as_strings(&panel, 16..18, cx),
6969            &["      modified6.txt  <== selected", "      unmodified5.txt",],
6970        );
6971
6972        panel.update_in(cx, |panel, window, cx| {
6973            panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
6974        });
6975
6976        assert_eq!(
6977            visible_entries_as_strings(&panel, 13..16, cx),
6978            &[
6979                "    v dir3",
6980                "          modified5.txt  <== selected",
6981                "          unmodified4.txt",
6982            ],
6983        );
6984
6985        panel.update_in(cx, |panel, window, cx| {
6986            panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
6987        });
6988
6989        assert_eq!(
6990            visible_entries_as_strings(&panel, 9..11, cx),
6991            &["      modified4.txt  <== selected", "      unmodified3.txt",],
6992        );
6993
6994        panel.update_in(cx, |panel, window, cx| {
6995            panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
6996        });
6997
6998        assert_eq!(
6999            visible_entries_as_strings(&panel, 6..9, cx),
7000            &[
7001                "    v dir2",
7002                "          modified3.txt  <== selected",
7003                "          unmodified2.txt",
7004            ],
7005        );
7006
7007        panel.update_in(cx, |panel, window, cx| {
7008            panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
7009        });
7010
7011        assert_eq!(
7012            visible_entries_as_strings(&panel, 0..6, cx),
7013            &[
7014                "v tree1",
7015                "    > .git",
7016                "    v dir1",
7017                "          modified1.txt",
7018                "          modified2.txt  <== selected",
7019                "          unmodified1.txt",
7020            ],
7021        );
7022
7023        panel.update_in(cx, |panel, window, cx| {
7024            panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
7025        });
7026
7027        assert_eq!(
7028            visible_entries_as_strings(&panel, 0..6, cx),
7029            &[
7030                "v tree1",
7031                "    > .git",
7032                "    v dir1",
7033                "          modified1.txt  <== selected",
7034                "          modified2.txt",
7035                "          unmodified1.txt",
7036            ],
7037        );
7038    }
7039
7040    #[gpui::test]
7041    async fn test_select_directory(cx: &mut gpui::TestAppContext) {
7042        init_test_with_editor(cx);
7043
7044        let fs = FakeFs::new(cx.executor().clone());
7045        fs.insert_tree(
7046            "/project_root",
7047            json!({
7048                "dir_1": {
7049                    "nested_dir": {
7050                        "file_a.py": "# File contents",
7051                    }
7052                },
7053                "file_1.py": "# File contents",
7054                "dir_2": {
7055
7056                },
7057                "dir_3": {
7058
7059                },
7060                "file_2.py": "# File contents",
7061                "dir_4": {
7062
7063                },
7064            }),
7065        )
7066        .await;
7067
7068        let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
7069        let workspace =
7070            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7071        let cx = &mut VisualTestContext::from_window(*workspace, cx);
7072        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
7073
7074        panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
7075        cx.executor().run_until_parked();
7076        select_path(&panel, "project_root/dir_1", cx);
7077        cx.executor().run_until_parked();
7078        assert_eq!(
7079            visible_entries_as_strings(&panel, 0..10, cx),
7080            &[
7081                "v project_root",
7082                "    > dir_1  <== selected",
7083                "    > dir_2",
7084                "    > dir_3",
7085                "    > dir_4",
7086                "      file_1.py",
7087                "      file_2.py",
7088            ]
7089        );
7090        panel.update_in(cx, |panel, window, cx| {
7091            panel.select_prev_directory(&SelectPrevDirectory, window, cx)
7092        });
7093
7094        assert_eq!(
7095            visible_entries_as_strings(&panel, 0..10, cx),
7096            &[
7097                "v project_root  <== selected",
7098                "    > dir_1",
7099                "    > dir_2",
7100                "    > dir_3",
7101                "    > dir_4",
7102                "      file_1.py",
7103                "      file_2.py",
7104            ]
7105        );
7106
7107        panel.update_in(cx, |panel, window, cx| {
7108            panel.select_prev_directory(&SelectPrevDirectory, window, cx)
7109        });
7110
7111        assert_eq!(
7112            visible_entries_as_strings(&panel, 0..10, cx),
7113            &[
7114                "v project_root",
7115                "    > dir_1",
7116                "    > dir_2",
7117                "    > dir_3",
7118                "    > dir_4  <== selected",
7119                "      file_1.py",
7120                "      file_2.py",
7121            ]
7122        );
7123
7124        panel.update_in(cx, |panel, window, cx| {
7125            panel.select_next_directory(&SelectNextDirectory, window, cx)
7126        });
7127
7128        assert_eq!(
7129            visible_entries_as_strings(&panel, 0..10, cx),
7130            &[
7131                "v project_root  <== selected",
7132                "    > dir_1",
7133                "    > dir_2",
7134                "    > dir_3",
7135                "    > dir_4",
7136                "      file_1.py",
7137                "      file_2.py",
7138            ]
7139        );
7140    }
7141    #[gpui::test]
7142    async fn test_select_first_last(cx: &mut gpui::TestAppContext) {
7143        init_test_with_editor(cx);
7144
7145        let fs = FakeFs::new(cx.executor().clone());
7146        fs.insert_tree(
7147            "/project_root",
7148            json!({
7149                "dir_1": {
7150                    "nested_dir": {
7151                        "file_a.py": "# File contents",
7152                    }
7153                },
7154                "file_1.py": "# File contents",
7155                "file_2.py": "# File contents",
7156                "zdir_2": {
7157                    "nested_dir2": {
7158                        "file_b.py": "# File contents",
7159                    }
7160                },
7161            }),
7162        )
7163        .await;
7164
7165        let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
7166        let workspace =
7167            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7168        let cx = &mut VisualTestContext::from_window(*workspace, cx);
7169        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
7170
7171        assert_eq!(
7172            visible_entries_as_strings(&panel, 0..10, cx),
7173            &[
7174                "v project_root",
7175                "    > dir_1",
7176                "    > zdir_2",
7177                "      file_1.py",
7178                "      file_2.py",
7179            ]
7180        );
7181        panel.update_in(cx, |panel, window, cx| {
7182            panel.select_first(&SelectFirst, window, cx)
7183        });
7184
7185        assert_eq!(
7186            visible_entries_as_strings(&panel, 0..10, cx),
7187            &[
7188                "v project_root  <== selected",
7189                "    > dir_1",
7190                "    > zdir_2",
7191                "      file_1.py",
7192                "      file_2.py",
7193            ]
7194        );
7195
7196        panel.update_in(cx, |panel, window, cx| {
7197            panel.select_last(&SelectLast, window, cx)
7198        });
7199
7200        assert_eq!(
7201            visible_entries_as_strings(&panel, 0..10, cx),
7202            &[
7203                "v project_root",
7204                "    > dir_1",
7205                "    > zdir_2",
7206                "      file_1.py",
7207                "      file_2.py  <== selected",
7208            ]
7209        );
7210    }
7211
7212    #[gpui::test]
7213    async fn test_dir_toggle_collapse(cx: &mut gpui::TestAppContext) {
7214        init_test_with_editor(cx);
7215
7216        let fs = FakeFs::new(cx.executor().clone());
7217        fs.insert_tree(
7218            "/project_root",
7219            json!({
7220                "dir_1": {
7221                    "nested_dir": {
7222                        "file_a.py": "# File contents",
7223                    }
7224                },
7225                "file_1.py": "# File contents",
7226            }),
7227        )
7228        .await;
7229
7230        let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
7231        let workspace =
7232            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7233        let cx = &mut VisualTestContext::from_window(*workspace, cx);
7234        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
7235
7236        panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
7237        cx.executor().run_until_parked();
7238        select_path(&panel, "project_root/dir_1", cx);
7239        panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
7240        select_path(&panel, "project_root/dir_1/nested_dir", cx);
7241        panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
7242        panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
7243        cx.executor().run_until_parked();
7244        assert_eq!(
7245            visible_entries_as_strings(&panel, 0..10, cx),
7246            &[
7247                "v project_root",
7248                "    v dir_1",
7249                "        > nested_dir  <== selected",
7250                "      file_1.py",
7251            ]
7252        );
7253    }
7254
7255    #[gpui::test]
7256    async fn test_collapse_all_entries(cx: &mut gpui::TestAppContext) {
7257        init_test_with_editor(cx);
7258
7259        let fs = FakeFs::new(cx.executor().clone());
7260        fs.insert_tree(
7261            "/project_root",
7262            json!({
7263                "dir_1": {
7264                    "nested_dir": {
7265                        "file_a.py": "# File contents",
7266                        "file_b.py": "# File contents",
7267                        "file_c.py": "# File contents",
7268                    },
7269                    "file_1.py": "# File contents",
7270                    "file_2.py": "# File contents",
7271                    "file_3.py": "# File contents",
7272                },
7273                "dir_2": {
7274                    "file_1.py": "# File contents",
7275                    "file_2.py": "# File contents",
7276                    "file_3.py": "# File contents",
7277                }
7278            }),
7279        )
7280        .await;
7281
7282        let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
7283        let workspace =
7284            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7285        let cx = &mut VisualTestContext::from_window(*workspace, cx);
7286        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
7287
7288        panel.update_in(cx, |panel, window, cx| {
7289            panel.collapse_all_entries(&CollapseAllEntries, window, cx)
7290        });
7291        cx.executor().run_until_parked();
7292        assert_eq!(
7293            visible_entries_as_strings(&panel, 0..10, cx),
7294            &["v project_root", "    > dir_1", "    > dir_2",]
7295        );
7296
7297        // Open dir_1 and make sure nested_dir was collapsed when running collapse_all_entries
7298        toggle_expand_dir(&panel, "project_root/dir_1", cx);
7299        cx.executor().run_until_parked();
7300        assert_eq!(
7301            visible_entries_as_strings(&panel, 0..10, cx),
7302            &[
7303                "v project_root",
7304                "    v dir_1  <== selected",
7305                "        > nested_dir",
7306                "          file_1.py",
7307                "          file_2.py",
7308                "          file_3.py",
7309                "    > dir_2",
7310            ]
7311        );
7312    }
7313
7314    #[gpui::test]
7315    async fn test_new_file_move(cx: &mut gpui::TestAppContext) {
7316        init_test(cx);
7317
7318        let fs = FakeFs::new(cx.executor().clone());
7319        fs.as_fake().insert_tree(path!("/root"), json!({})).await;
7320        let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
7321        let workspace =
7322            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7323        let cx = &mut VisualTestContext::from_window(*workspace, cx);
7324        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
7325
7326        // Make a new buffer with no backing file
7327        workspace
7328            .update(cx, |workspace, window, cx| {
7329                Editor::new_file(workspace, &Default::default(), window, cx)
7330            })
7331            .unwrap();
7332
7333        cx.executor().run_until_parked();
7334
7335        // "Save as" the buffer, creating a new backing file for it
7336        let save_task = workspace
7337            .update(cx, |workspace, window, cx| {
7338                workspace.save_active_item(workspace::SaveIntent::Save, window, cx)
7339            })
7340            .unwrap();
7341
7342        cx.executor().run_until_parked();
7343        cx.simulate_new_path_selection(|_| Some(PathBuf::from(path!("/root/new"))));
7344        save_task.await.unwrap();
7345
7346        // Rename the file
7347        select_path(&panel, "root/new", cx);
7348        assert_eq!(
7349            visible_entries_as_strings(&panel, 0..10, cx),
7350            &["v root", "      new  <== selected  <== marked"]
7351        );
7352        panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
7353        panel.update_in(cx, |panel, window, cx| {
7354            panel
7355                .filename_editor
7356                .update(cx, |editor, cx| editor.set_text("newer", window, cx));
7357        });
7358        panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
7359
7360        cx.executor().run_until_parked();
7361        assert_eq!(
7362            visible_entries_as_strings(&panel, 0..10, cx),
7363            &["v root", "      newer  <== selected"]
7364        );
7365
7366        workspace
7367            .update(cx, |workspace, window, cx| {
7368                workspace.save_active_item(workspace::SaveIntent::Save, window, cx)
7369            })
7370            .unwrap()
7371            .await
7372            .unwrap();
7373
7374        cx.executor().run_until_parked();
7375        // assert that saving the file doesn't restore "new"
7376        assert_eq!(
7377            visible_entries_as_strings(&panel, 0..10, cx),
7378            &["v root", "      newer  <== selected"]
7379        );
7380    }
7381
7382    #[gpui::test]
7383    #[cfg_attr(target_os = "windows", ignore)]
7384    async fn test_rename_root_of_worktree(cx: &mut gpui::TestAppContext) {
7385        init_test_with_editor(cx);
7386
7387        let fs = FakeFs::new(cx.executor().clone());
7388        fs.insert_tree(
7389            "/root1",
7390            json!({
7391                "dir1": {
7392                    "file1.txt": "content 1",
7393                },
7394            }),
7395        )
7396        .await;
7397
7398        let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
7399        let workspace =
7400            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7401        let cx = &mut VisualTestContext::from_window(*workspace, cx);
7402        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
7403
7404        toggle_expand_dir(&panel, "root1/dir1", cx);
7405
7406        assert_eq!(
7407            visible_entries_as_strings(&panel, 0..20, cx),
7408            &["v root1", "    v dir1  <== selected", "          file1.txt",],
7409            "Initial state with worktrees"
7410        );
7411
7412        select_path(&panel, "root1", cx);
7413        assert_eq!(
7414            visible_entries_as_strings(&panel, 0..20, cx),
7415            &["v root1  <== selected", "    v dir1", "          file1.txt",],
7416        );
7417
7418        // Rename root1 to new_root1
7419        panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
7420
7421        assert_eq!(
7422            visible_entries_as_strings(&panel, 0..20, cx),
7423            &[
7424                "v [EDITOR: 'root1']  <== selected",
7425                "    v dir1",
7426                "          file1.txt",
7427            ],
7428        );
7429
7430        let confirm = panel.update_in(cx, |panel, window, cx| {
7431            panel
7432                .filename_editor
7433                .update(cx, |editor, cx| editor.set_text("new_root1", window, cx));
7434            panel.confirm_edit(window, cx).unwrap()
7435        });
7436        confirm.await.unwrap();
7437        assert_eq!(
7438            visible_entries_as_strings(&panel, 0..20, cx),
7439            &[
7440                "v new_root1  <== selected",
7441                "    v dir1",
7442                "          file1.txt",
7443            ],
7444            "Should update worktree name"
7445        );
7446
7447        // Ensure internal paths have been updated
7448        select_path(&panel, "new_root1/dir1/file1.txt", cx);
7449        assert_eq!(
7450            visible_entries_as_strings(&panel, 0..20, cx),
7451            &[
7452                "v new_root1",
7453                "    v dir1",
7454                "          file1.txt  <== selected",
7455            ],
7456            "Files in renamed worktree are selectable"
7457        );
7458    }
7459
7460    #[gpui::test]
7461    async fn test_multiple_marked_entries(cx: &mut gpui::TestAppContext) {
7462        init_test_with_editor(cx);
7463        let fs = FakeFs::new(cx.executor().clone());
7464        fs.insert_tree(
7465            "/project_root",
7466            json!({
7467                "dir_1": {
7468                    "nested_dir": {
7469                        "file_a.py": "# File contents",
7470                    }
7471                },
7472                "file_1.py": "# File contents",
7473            }),
7474        )
7475        .await;
7476
7477        let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
7478        let worktree_id =
7479            cx.update(|cx| project.read(cx).worktrees(cx).next().unwrap().read(cx).id());
7480        let workspace =
7481            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7482        let cx = &mut VisualTestContext::from_window(*workspace, cx);
7483        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
7484        cx.update(|window, cx| {
7485            panel.update(cx, |this, cx| {
7486                this.select_next(&Default::default(), window, cx);
7487                this.expand_selected_entry(&Default::default(), window, cx);
7488                this.expand_selected_entry(&Default::default(), window, cx);
7489                this.select_next(&Default::default(), window, cx);
7490                this.expand_selected_entry(&Default::default(), window, cx);
7491                this.select_next(&Default::default(), window, cx);
7492            })
7493        });
7494        assert_eq!(
7495            visible_entries_as_strings(&panel, 0..10, cx),
7496            &[
7497                "v project_root",
7498                "    v dir_1",
7499                "        v nested_dir",
7500                "              file_a.py  <== selected",
7501                "      file_1.py",
7502            ]
7503        );
7504        let modifiers_with_shift = gpui::Modifiers {
7505            shift: true,
7506            ..Default::default()
7507        };
7508        cx.simulate_modifiers_change(modifiers_with_shift);
7509        cx.update(|window, cx| {
7510            panel.update(cx, |this, cx| {
7511                this.select_next(&Default::default(), window, cx);
7512            })
7513        });
7514        assert_eq!(
7515            visible_entries_as_strings(&panel, 0..10, cx),
7516            &[
7517                "v project_root",
7518                "    v dir_1",
7519                "        v nested_dir",
7520                "              file_a.py",
7521                "      file_1.py  <== selected  <== marked",
7522            ]
7523        );
7524        cx.update(|window, cx| {
7525            panel.update(cx, |this, cx| {
7526                this.select_previous(&Default::default(), window, cx);
7527            })
7528        });
7529        assert_eq!(
7530            visible_entries_as_strings(&panel, 0..10, cx),
7531            &[
7532                "v project_root",
7533                "    v dir_1",
7534                "        v nested_dir",
7535                "              file_a.py  <== selected  <== marked",
7536                "      file_1.py  <== marked",
7537            ]
7538        );
7539        cx.update(|window, cx| {
7540            panel.update(cx, |this, cx| {
7541                let drag = DraggedSelection {
7542                    active_selection: this.selection.unwrap(),
7543                    marked_selections: Arc::new(this.marked_entries.clone()),
7544                };
7545                let target_entry = this
7546                    .project
7547                    .read(cx)
7548                    .entry_for_path(&(worktree_id, "").into(), cx)
7549                    .unwrap();
7550                this.drag_onto(&drag, target_entry.id, false, window, cx);
7551            });
7552        });
7553        cx.run_until_parked();
7554        assert_eq!(
7555            visible_entries_as_strings(&panel, 0..10, cx),
7556            &[
7557                "v project_root",
7558                "    v dir_1",
7559                "        v nested_dir",
7560                "      file_1.py  <== marked",
7561                "      file_a.py  <== selected  <== marked",
7562            ]
7563        );
7564        // ESC clears out all marks
7565        cx.update(|window, cx| {
7566            panel.update(cx, |this, cx| {
7567                this.cancel(&menu::Cancel, window, cx);
7568            })
7569        });
7570        assert_eq!(
7571            visible_entries_as_strings(&panel, 0..10, cx),
7572            &[
7573                "v project_root",
7574                "    v dir_1",
7575                "        v nested_dir",
7576                "      file_1.py",
7577                "      file_a.py  <== selected",
7578            ]
7579        );
7580        // ESC clears out all marks
7581        cx.update(|window, cx| {
7582            panel.update(cx, |this, cx| {
7583                this.select_previous(&SelectPrevious, window, cx);
7584                this.select_next(&SelectNext, window, cx);
7585            })
7586        });
7587        assert_eq!(
7588            visible_entries_as_strings(&panel, 0..10, cx),
7589            &[
7590                "v project_root",
7591                "    v dir_1",
7592                "        v nested_dir",
7593                "      file_1.py  <== marked",
7594                "      file_a.py  <== selected  <== marked",
7595            ]
7596        );
7597        cx.simulate_modifiers_change(Default::default());
7598        cx.update(|window, cx| {
7599            panel.update(cx, |this, cx| {
7600                this.cut(&Cut, window, cx);
7601                this.select_previous(&SelectPrevious, window, cx);
7602                this.select_previous(&SelectPrevious, window, cx);
7603
7604                this.paste(&Paste, window, cx);
7605                // this.expand_selected_entry(&ExpandSelectedEntry, cx);
7606            })
7607        });
7608        cx.run_until_parked();
7609        assert_eq!(
7610            visible_entries_as_strings(&panel, 0..10, cx),
7611            &[
7612                "v project_root",
7613                "    v dir_1",
7614                "        v nested_dir",
7615                "              file_1.py  <== marked",
7616                "              file_a.py  <== selected  <== marked",
7617            ]
7618        );
7619        cx.simulate_modifiers_change(modifiers_with_shift);
7620        cx.update(|window, cx| {
7621            panel.update(cx, |this, cx| {
7622                this.expand_selected_entry(&Default::default(), window, cx);
7623                this.select_next(&SelectNext, window, cx);
7624                this.select_next(&SelectNext, window, cx);
7625            })
7626        });
7627        submit_deletion(&panel, cx);
7628        assert_eq!(
7629            visible_entries_as_strings(&panel, 0..10, cx),
7630            &[
7631                "v project_root",
7632                "    v dir_1",
7633                "        v nested_dir  <== selected",
7634            ]
7635        );
7636    }
7637    #[gpui::test]
7638    async fn test_autoreveal_and_gitignored_files(cx: &mut gpui::TestAppContext) {
7639        init_test_with_editor(cx);
7640        cx.update(|cx| {
7641            cx.update_global::<SettingsStore, _>(|store, cx| {
7642                store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
7643                    worktree_settings.file_scan_exclusions = Some(Vec::new());
7644                });
7645                store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
7646                    project_panel_settings.auto_reveal_entries = Some(false)
7647                });
7648            })
7649        });
7650
7651        let fs = FakeFs::new(cx.background_executor.clone());
7652        fs.insert_tree(
7653            "/project_root",
7654            json!({
7655                ".git": {},
7656                ".gitignore": "**/gitignored_dir",
7657                "dir_1": {
7658                    "file_1.py": "# File 1_1 contents",
7659                    "file_2.py": "# File 1_2 contents",
7660                    "file_3.py": "# File 1_3 contents",
7661                    "gitignored_dir": {
7662                        "file_a.py": "# File contents",
7663                        "file_b.py": "# File contents",
7664                        "file_c.py": "# File contents",
7665                    },
7666                },
7667                "dir_2": {
7668                    "file_1.py": "# File 2_1 contents",
7669                    "file_2.py": "# File 2_2 contents",
7670                    "file_3.py": "# File 2_3 contents",
7671                }
7672            }),
7673        )
7674        .await;
7675
7676        let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
7677        let workspace =
7678            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7679        let cx = &mut VisualTestContext::from_window(*workspace, cx);
7680        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
7681
7682        assert_eq!(
7683            visible_entries_as_strings(&panel, 0..20, cx),
7684            &[
7685                "v project_root",
7686                "    > .git",
7687                "    > dir_1",
7688                "    > dir_2",
7689                "      .gitignore",
7690            ]
7691        );
7692
7693        let dir_1_file = find_project_entry(&panel, "project_root/dir_1/file_1.py", cx)
7694            .expect("dir 1 file is not ignored and should have an entry");
7695        let dir_2_file = find_project_entry(&panel, "project_root/dir_2/file_1.py", cx)
7696            .expect("dir 2 file is not ignored and should have an entry");
7697        let gitignored_dir_file =
7698            find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx);
7699        assert_eq!(
7700            gitignored_dir_file, None,
7701            "File in the gitignored dir should not have an entry before its dir is toggled"
7702        );
7703
7704        toggle_expand_dir(&panel, "project_root/dir_1", cx);
7705        toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
7706        cx.executor().run_until_parked();
7707        assert_eq!(
7708            visible_entries_as_strings(&panel, 0..20, cx),
7709            &[
7710                "v project_root",
7711                "    > .git",
7712                "    v dir_1",
7713                "        v gitignored_dir  <== selected",
7714                "              file_a.py",
7715                "              file_b.py",
7716                "              file_c.py",
7717                "          file_1.py",
7718                "          file_2.py",
7719                "          file_3.py",
7720                "    > dir_2",
7721                "      .gitignore",
7722            ],
7723            "Should show gitignored dir file list in the project panel"
7724        );
7725        let gitignored_dir_file =
7726            find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx)
7727                .expect("after gitignored dir got opened, a file entry should be present");
7728
7729        toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
7730        toggle_expand_dir(&panel, "project_root/dir_1", cx);
7731        assert_eq!(
7732            visible_entries_as_strings(&panel, 0..20, cx),
7733            &[
7734                "v project_root",
7735                "    > .git",
7736                "    > dir_1  <== selected",
7737                "    > dir_2",
7738                "      .gitignore",
7739            ],
7740            "Should hide all dir contents again and prepare for the auto reveal test"
7741        );
7742
7743        for file_entry in [dir_1_file, dir_2_file, gitignored_dir_file] {
7744            panel.update(cx, |panel, cx| {
7745                panel.project.update(cx, |_, cx| {
7746                    cx.emit(project::Event::ActiveEntryChanged(Some(file_entry)))
7747                })
7748            });
7749            cx.run_until_parked();
7750            assert_eq!(
7751                visible_entries_as_strings(&panel, 0..20, cx),
7752                &[
7753                    "v project_root",
7754                    "    > .git",
7755                    "    > dir_1  <== selected",
7756                    "    > dir_2",
7757                    "      .gitignore",
7758                ],
7759                "When no auto reveal is enabled, the selected entry should not be revealed in the project panel"
7760            );
7761        }
7762
7763        cx.update(|_, cx| {
7764            cx.update_global::<SettingsStore, _>(|store, cx| {
7765                store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
7766                    project_panel_settings.auto_reveal_entries = Some(true)
7767                });
7768            })
7769        });
7770
7771        panel.update(cx, |panel, cx| {
7772            panel.project.update(cx, |_, cx| {
7773                cx.emit(project::Event::ActiveEntryChanged(Some(dir_1_file)))
7774            })
7775        });
7776        cx.run_until_parked();
7777        assert_eq!(
7778            visible_entries_as_strings(&panel, 0..20, cx),
7779            &[
7780                "v project_root",
7781                "    > .git",
7782                "    v dir_1",
7783                "        > gitignored_dir",
7784                "          file_1.py  <== selected  <== marked",
7785                "          file_2.py",
7786                "          file_3.py",
7787                "    > dir_2",
7788                "      .gitignore",
7789            ],
7790            "When auto reveal is enabled, not ignored dir_1 entry should be revealed"
7791        );
7792
7793        panel.update(cx, |panel, cx| {
7794            panel.project.update(cx, |_, cx| {
7795                cx.emit(project::Event::ActiveEntryChanged(Some(dir_2_file)))
7796            })
7797        });
7798        cx.run_until_parked();
7799        assert_eq!(
7800            visible_entries_as_strings(&panel, 0..20, cx),
7801            &[
7802                "v project_root",
7803                "    > .git",
7804                "    v dir_1",
7805                "        > gitignored_dir",
7806                "          file_1.py",
7807                "          file_2.py",
7808                "          file_3.py",
7809                "    v dir_2",
7810                "          file_1.py  <== selected  <== marked",
7811                "          file_2.py",
7812                "          file_3.py",
7813                "      .gitignore",
7814            ],
7815            "When auto reveal is enabled, not ignored dir_2 entry should be revealed"
7816        );
7817
7818        panel.update(cx, |panel, cx| {
7819            panel.project.update(cx, |_, cx| {
7820                cx.emit(project::Event::ActiveEntryChanged(Some(
7821                    gitignored_dir_file,
7822                )))
7823            })
7824        });
7825        cx.run_until_parked();
7826        assert_eq!(
7827            visible_entries_as_strings(&panel, 0..20, cx),
7828            &[
7829                "v project_root",
7830                "    > .git",
7831                "    v dir_1",
7832                "        > gitignored_dir",
7833                "          file_1.py",
7834                "          file_2.py",
7835                "          file_3.py",
7836                "    v dir_2",
7837                "          file_1.py  <== selected  <== marked",
7838                "          file_2.py",
7839                "          file_3.py",
7840                "      .gitignore",
7841            ],
7842            "When auto reveal is enabled, a gitignored selected entry should not be revealed in the project panel"
7843        );
7844
7845        panel.update(cx, |panel, cx| {
7846            panel.project.update(cx, |_, cx| {
7847                cx.emit(project::Event::RevealInProjectPanel(gitignored_dir_file))
7848            })
7849        });
7850        cx.run_until_parked();
7851        assert_eq!(
7852            visible_entries_as_strings(&panel, 0..20, cx),
7853            &[
7854                "v project_root",
7855                "    > .git",
7856                "    v dir_1",
7857                "        v gitignored_dir",
7858                "              file_a.py  <== selected  <== marked",
7859                "              file_b.py",
7860                "              file_c.py",
7861                "          file_1.py",
7862                "          file_2.py",
7863                "          file_3.py",
7864                "    v dir_2",
7865                "          file_1.py",
7866                "          file_2.py",
7867                "          file_3.py",
7868                "      .gitignore",
7869            ],
7870            "When a gitignored entry is explicitly revealed, it should be shown in the project tree"
7871        );
7872    }
7873
7874    #[gpui::test]
7875    async fn test_gitignored_and_always_included(cx: &mut gpui::TestAppContext) {
7876        init_test_with_editor(cx);
7877        cx.update(|cx| {
7878            cx.update_global::<SettingsStore, _>(|store, cx| {
7879                store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
7880                    worktree_settings.file_scan_exclusions = Some(Vec::new());
7881                    worktree_settings.file_scan_inclusions =
7882                        Some(vec!["always_included_but_ignored_dir/*".to_string()]);
7883                });
7884                store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
7885                    project_panel_settings.auto_reveal_entries = Some(false)
7886                });
7887            })
7888        });
7889
7890        let fs = FakeFs::new(cx.background_executor.clone());
7891        fs.insert_tree(
7892            "/project_root",
7893            json!({
7894                ".git": {},
7895                ".gitignore": "**/gitignored_dir\n/always_included_but_ignored_dir",
7896                "dir_1": {
7897                    "file_1.py": "# File 1_1 contents",
7898                    "file_2.py": "# File 1_2 contents",
7899                    "file_3.py": "# File 1_3 contents",
7900                    "gitignored_dir": {
7901                        "file_a.py": "# File contents",
7902                        "file_b.py": "# File contents",
7903                        "file_c.py": "# File contents",
7904                    },
7905                },
7906                "dir_2": {
7907                    "file_1.py": "# File 2_1 contents",
7908                    "file_2.py": "# File 2_2 contents",
7909                    "file_3.py": "# File 2_3 contents",
7910                },
7911                "always_included_but_ignored_dir": {
7912                    "file_a.py": "# File contents",
7913                    "file_b.py": "# File contents",
7914                    "file_c.py": "# File contents",
7915                },
7916            }),
7917        )
7918        .await;
7919
7920        let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
7921        let workspace =
7922            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7923        let cx = &mut VisualTestContext::from_window(*workspace, cx);
7924        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
7925
7926        assert_eq!(
7927            visible_entries_as_strings(&panel, 0..20, cx),
7928            &[
7929                "v project_root",
7930                "    > .git",
7931                "    > always_included_but_ignored_dir",
7932                "    > dir_1",
7933                "    > dir_2",
7934                "      .gitignore",
7935            ]
7936        );
7937
7938        let gitignored_dir_file =
7939            find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx);
7940        let always_included_but_ignored_dir_file = find_project_entry(
7941            &panel,
7942            "project_root/always_included_but_ignored_dir/file_a.py",
7943            cx,
7944        )
7945        .expect("file that is .gitignored but set to always be included should have an entry");
7946        assert_eq!(
7947            gitignored_dir_file, None,
7948            "File in the gitignored dir should not have an entry unless its directory is toggled"
7949        );
7950
7951        toggle_expand_dir(&panel, "project_root/dir_1", cx);
7952        cx.run_until_parked();
7953        cx.update(|_, cx| {
7954            cx.update_global::<SettingsStore, _>(|store, cx| {
7955                store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
7956                    project_panel_settings.auto_reveal_entries = Some(true)
7957                });
7958            })
7959        });
7960
7961        panel.update(cx, |panel, cx| {
7962            panel.project.update(cx, |_, cx| {
7963                cx.emit(project::Event::ActiveEntryChanged(Some(
7964                    always_included_but_ignored_dir_file,
7965                )))
7966            })
7967        });
7968        cx.run_until_parked();
7969
7970        assert_eq!(
7971            visible_entries_as_strings(&panel, 0..20, cx),
7972            &[
7973                "v project_root",
7974                "    > .git",
7975                "    v always_included_but_ignored_dir",
7976                "          file_a.py  <== selected  <== marked",
7977                "          file_b.py",
7978                "          file_c.py",
7979                "    v dir_1",
7980                "        > gitignored_dir",
7981                "          file_1.py",
7982                "          file_2.py",
7983                "          file_3.py",
7984                "    > dir_2",
7985                "      .gitignore",
7986            ],
7987            "When auto reveal is enabled, a gitignored but always included selected entry should be revealed in the project panel"
7988        );
7989    }
7990
7991    #[gpui::test]
7992    async fn test_explicit_reveal(cx: &mut gpui::TestAppContext) {
7993        init_test_with_editor(cx);
7994        cx.update(|cx| {
7995            cx.update_global::<SettingsStore, _>(|store, cx| {
7996                store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
7997                    worktree_settings.file_scan_exclusions = Some(Vec::new());
7998                });
7999                store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
8000                    project_panel_settings.auto_reveal_entries = Some(false)
8001                });
8002            })
8003        });
8004
8005        let fs = FakeFs::new(cx.background_executor.clone());
8006        fs.insert_tree(
8007            "/project_root",
8008            json!({
8009                ".git": {},
8010                ".gitignore": "**/gitignored_dir",
8011                "dir_1": {
8012                    "file_1.py": "# File 1_1 contents",
8013                    "file_2.py": "# File 1_2 contents",
8014                    "file_3.py": "# File 1_3 contents",
8015                    "gitignored_dir": {
8016                        "file_a.py": "# File contents",
8017                        "file_b.py": "# File contents",
8018                        "file_c.py": "# File contents",
8019                    },
8020                },
8021                "dir_2": {
8022                    "file_1.py": "# File 2_1 contents",
8023                    "file_2.py": "# File 2_2 contents",
8024                    "file_3.py": "# File 2_3 contents",
8025                }
8026            }),
8027        )
8028        .await;
8029
8030        let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
8031        let workspace =
8032            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
8033        let cx = &mut VisualTestContext::from_window(*workspace, cx);
8034        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
8035
8036        assert_eq!(
8037            visible_entries_as_strings(&panel, 0..20, cx),
8038            &[
8039                "v project_root",
8040                "    > .git",
8041                "    > dir_1",
8042                "    > dir_2",
8043                "      .gitignore",
8044            ]
8045        );
8046
8047        let dir_1_file = find_project_entry(&panel, "project_root/dir_1/file_1.py", cx)
8048            .expect("dir 1 file is not ignored and should have an entry");
8049        let dir_2_file = find_project_entry(&panel, "project_root/dir_2/file_1.py", cx)
8050            .expect("dir 2 file is not ignored and should have an entry");
8051        let gitignored_dir_file =
8052            find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx);
8053        assert_eq!(
8054            gitignored_dir_file, None,
8055            "File in the gitignored dir should not have an entry before its dir is toggled"
8056        );
8057
8058        toggle_expand_dir(&panel, "project_root/dir_1", cx);
8059        toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
8060        cx.run_until_parked();
8061        assert_eq!(
8062            visible_entries_as_strings(&panel, 0..20, cx),
8063            &[
8064                "v project_root",
8065                "    > .git",
8066                "    v dir_1",
8067                "        v gitignored_dir  <== selected",
8068                "              file_a.py",
8069                "              file_b.py",
8070                "              file_c.py",
8071                "          file_1.py",
8072                "          file_2.py",
8073                "          file_3.py",
8074                "    > dir_2",
8075                "      .gitignore",
8076            ],
8077            "Should show gitignored dir file list in the project panel"
8078        );
8079        let gitignored_dir_file =
8080            find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx)
8081                .expect("after gitignored dir got opened, a file entry should be present");
8082
8083        toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
8084        toggle_expand_dir(&panel, "project_root/dir_1", cx);
8085        assert_eq!(
8086            visible_entries_as_strings(&panel, 0..20, cx),
8087            &[
8088                "v project_root",
8089                "    > .git",
8090                "    > dir_1  <== selected",
8091                "    > dir_2",
8092                "      .gitignore",
8093            ],
8094            "Should hide all dir contents again and prepare for the explicit reveal test"
8095        );
8096
8097        for file_entry in [dir_1_file, dir_2_file, gitignored_dir_file] {
8098            panel.update(cx, |panel, cx| {
8099                panel.project.update(cx, |_, cx| {
8100                    cx.emit(project::Event::ActiveEntryChanged(Some(file_entry)))
8101                })
8102            });
8103            cx.run_until_parked();
8104            assert_eq!(
8105                visible_entries_as_strings(&panel, 0..20, cx),
8106                &[
8107                    "v project_root",
8108                    "    > .git",
8109                    "    > dir_1  <== selected",
8110                    "    > dir_2",
8111                    "      .gitignore",
8112                ],
8113                "When no auto reveal is enabled, the selected entry should not be revealed in the project panel"
8114            );
8115        }
8116
8117        panel.update(cx, |panel, cx| {
8118            panel.project.update(cx, |_, cx| {
8119                cx.emit(project::Event::RevealInProjectPanel(dir_1_file))
8120            })
8121        });
8122        cx.run_until_parked();
8123        assert_eq!(
8124            visible_entries_as_strings(&panel, 0..20, cx),
8125            &[
8126                "v project_root",
8127                "    > .git",
8128                "    v dir_1",
8129                "        > gitignored_dir",
8130                "          file_1.py  <== selected  <== marked",
8131                "          file_2.py",
8132                "          file_3.py",
8133                "    > dir_2",
8134                "      .gitignore",
8135            ],
8136            "With no auto reveal, explicit reveal should show the dir_1 entry in the project panel"
8137        );
8138
8139        panel.update(cx, |panel, cx| {
8140            panel.project.update(cx, |_, cx| {
8141                cx.emit(project::Event::RevealInProjectPanel(dir_2_file))
8142            })
8143        });
8144        cx.run_until_parked();
8145        assert_eq!(
8146            visible_entries_as_strings(&panel, 0..20, cx),
8147            &[
8148                "v project_root",
8149                "    > .git",
8150                "    v dir_1",
8151                "        > gitignored_dir",
8152                "          file_1.py",
8153                "          file_2.py",
8154                "          file_3.py",
8155                "    v dir_2",
8156                "          file_1.py  <== selected  <== marked",
8157                "          file_2.py",
8158                "          file_3.py",
8159                "      .gitignore",
8160            ],
8161            "With no auto reveal, explicit reveal should show the dir_2 entry in the project panel"
8162        );
8163
8164        panel.update(cx, |panel, cx| {
8165            panel.project.update(cx, |_, cx| {
8166                cx.emit(project::Event::RevealInProjectPanel(gitignored_dir_file))
8167            })
8168        });
8169        cx.run_until_parked();
8170        assert_eq!(
8171            visible_entries_as_strings(&panel, 0..20, cx),
8172            &[
8173                "v project_root",
8174                "    > .git",
8175                "    v dir_1",
8176                "        v gitignored_dir",
8177                "              file_a.py  <== selected  <== marked",
8178                "              file_b.py",
8179                "              file_c.py",
8180                "          file_1.py",
8181                "          file_2.py",
8182                "          file_3.py",
8183                "    v dir_2",
8184                "          file_1.py",
8185                "          file_2.py",
8186                "          file_3.py",
8187                "      .gitignore",
8188            ],
8189            "With no auto reveal, explicit reveal should show the gitignored entry in the project panel"
8190        );
8191    }
8192
8193    #[gpui::test]
8194    async fn test_creating_excluded_entries(cx: &mut gpui::TestAppContext) {
8195        init_test(cx);
8196        cx.update(|cx| {
8197            cx.update_global::<SettingsStore, _>(|store, cx| {
8198                store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
8199                    project_settings.file_scan_exclusions =
8200                        Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]);
8201                });
8202            });
8203        });
8204
8205        cx.update(|cx| {
8206            register_project_item::<TestProjectItemView>(cx);
8207        });
8208
8209        let fs = FakeFs::new(cx.executor().clone());
8210        fs.insert_tree(
8211            "/root1",
8212            json!({
8213                ".dockerignore": "",
8214                ".git": {
8215                    "HEAD": "",
8216                },
8217            }),
8218        )
8219        .await;
8220
8221        let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
8222        let workspace =
8223            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
8224        let cx = &mut VisualTestContext::from_window(*workspace, cx);
8225        let panel = workspace
8226            .update(cx, |workspace, window, cx| {
8227                let panel = ProjectPanel::new(workspace, window, cx);
8228                workspace.add_panel(panel.clone(), window, cx);
8229                panel
8230            })
8231            .unwrap();
8232
8233        select_path(&panel, "root1", cx);
8234        assert_eq!(
8235            visible_entries_as_strings(&panel, 0..10, cx),
8236            &["v root1  <== selected", "      .dockerignore",]
8237        );
8238        workspace
8239            .update(cx, |workspace, _, cx| {
8240                assert!(
8241                    workspace.active_item(cx).is_none(),
8242                    "Should have no active items in the beginning"
8243                );
8244            })
8245            .unwrap();
8246
8247        let excluded_file_path = ".git/COMMIT_EDITMSG";
8248        let excluded_dir_path = "excluded_dir";
8249
8250        panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
8251        panel.update_in(cx, |panel, window, cx| {
8252            assert!(panel.filename_editor.read(cx).is_focused(window));
8253        });
8254        panel
8255            .update_in(cx, |panel, window, cx| {
8256                panel.filename_editor.update(cx, |editor, cx| {
8257                    editor.set_text(excluded_file_path, window, cx)
8258                });
8259                panel.confirm_edit(window, cx).unwrap()
8260            })
8261            .await
8262            .unwrap();
8263
8264        assert_eq!(
8265            visible_entries_as_strings(&panel, 0..13, cx),
8266            &["v root1", "      .dockerignore"],
8267            "Excluded dir should not be shown after opening a file in it"
8268        );
8269        panel.update_in(cx, |panel, window, cx| {
8270            assert!(
8271                !panel.filename_editor.read(cx).is_focused(window),
8272                "Should have closed the file name editor"
8273            );
8274        });
8275        workspace
8276            .update(cx, |workspace, _, cx| {
8277                let active_entry_path = workspace
8278                    .active_item(cx)
8279                    .expect("should have opened and activated the excluded item")
8280                    .act_as::<TestProjectItemView>(cx)
8281                    .expect(
8282                        "should have opened the corresponding project item for the excluded item",
8283                    )
8284                    .read(cx)
8285                    .path
8286                    .clone();
8287                assert_eq!(
8288                    active_entry_path.path.as_ref(),
8289                    Path::new(excluded_file_path),
8290                    "Should open the excluded file"
8291                );
8292
8293                assert!(
8294                    workspace.notification_ids().is_empty(),
8295                    "Should have no notifications after opening an excluded file"
8296                );
8297            })
8298            .unwrap();
8299        assert!(
8300            fs.is_file(Path::new("/root1/.git/COMMIT_EDITMSG")).await,
8301            "Should have created the excluded file"
8302        );
8303
8304        select_path(&panel, "root1", cx);
8305        panel.update_in(cx, |panel, window, cx| {
8306            panel.new_directory(&NewDirectory, window, cx)
8307        });
8308        panel.update_in(cx, |panel, window, cx| {
8309            assert!(panel.filename_editor.read(cx).is_focused(window));
8310        });
8311        panel
8312            .update_in(cx, |panel, window, cx| {
8313                panel.filename_editor.update(cx, |editor, cx| {
8314                    editor.set_text(excluded_file_path, window, cx)
8315                });
8316                panel.confirm_edit(window, cx).unwrap()
8317            })
8318            .await
8319            .unwrap();
8320
8321        assert_eq!(
8322            visible_entries_as_strings(&panel, 0..13, cx),
8323            &["v root1", "      .dockerignore"],
8324            "Should not change the project panel after trying to create an excluded directorya directory with the same name as the excluded file"
8325        );
8326        panel.update_in(cx, |panel, window, cx| {
8327            assert!(
8328                !panel.filename_editor.read(cx).is_focused(window),
8329                "Should have closed the file name editor"
8330            );
8331        });
8332        workspace
8333            .update(cx, |workspace, _, cx| {
8334                let notifications = workspace.notification_ids();
8335                assert_eq!(
8336                    notifications.len(),
8337                    1,
8338                    "Should receive one notification with the error message"
8339                );
8340                workspace.dismiss_notification(notifications.first().unwrap(), cx);
8341                assert!(workspace.notification_ids().is_empty());
8342            })
8343            .unwrap();
8344
8345        select_path(&panel, "root1", cx);
8346        panel.update_in(cx, |panel, window, cx| {
8347            panel.new_directory(&NewDirectory, window, cx)
8348        });
8349        panel.update_in(cx, |panel, window, cx| {
8350            assert!(panel.filename_editor.read(cx).is_focused(window));
8351        });
8352        panel
8353            .update_in(cx, |panel, window, cx| {
8354                panel.filename_editor.update(cx, |editor, cx| {
8355                    editor.set_text(excluded_dir_path, window, cx)
8356                });
8357                panel.confirm_edit(window, cx).unwrap()
8358            })
8359            .await
8360            .unwrap();
8361
8362        assert_eq!(
8363            visible_entries_as_strings(&panel, 0..13, cx),
8364            &["v root1", "      .dockerignore"],
8365            "Should not change the project panel after trying to create an excluded directory"
8366        );
8367        panel.update_in(cx, |panel, window, cx| {
8368            assert!(
8369                !panel.filename_editor.read(cx).is_focused(window),
8370                "Should have closed the file name editor"
8371            );
8372        });
8373        workspace
8374            .update(cx, |workspace, _, cx| {
8375                let notifications = workspace.notification_ids();
8376                assert_eq!(
8377                    notifications.len(),
8378                    1,
8379                    "Should receive one notification explaining that no directory is actually shown"
8380                );
8381                workspace.dismiss_notification(notifications.first().unwrap(), cx);
8382                assert!(workspace.notification_ids().is_empty());
8383            })
8384            .unwrap();
8385        assert!(
8386            fs.is_dir(Path::new("/root1/excluded_dir")).await,
8387            "Should have created the excluded directory"
8388        );
8389    }
8390
8391    #[gpui::test]
8392    async fn test_selection_restored_when_creation_cancelled(cx: &mut gpui::TestAppContext) {
8393        init_test_with_editor(cx);
8394
8395        let fs = FakeFs::new(cx.executor().clone());
8396        fs.insert_tree(
8397            "/src",
8398            json!({
8399                "test": {
8400                    "first.rs": "// First Rust file",
8401                    "second.rs": "// Second Rust file",
8402                    "third.rs": "// Third Rust file",
8403                }
8404            }),
8405        )
8406        .await;
8407
8408        let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
8409        let workspace =
8410            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
8411        let cx = &mut VisualTestContext::from_window(*workspace, cx);
8412        let panel = workspace
8413            .update(cx, |workspace, window, cx| {
8414                let panel = ProjectPanel::new(workspace, window, cx);
8415                workspace.add_panel(panel.clone(), window, cx);
8416                panel
8417            })
8418            .unwrap();
8419
8420        select_path(&panel, "src/", cx);
8421        panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
8422        cx.executor().run_until_parked();
8423        assert_eq!(
8424            visible_entries_as_strings(&panel, 0..10, cx),
8425            &[
8426                //
8427                "v src  <== selected",
8428                "    > test"
8429            ]
8430        );
8431        panel.update_in(cx, |panel, window, cx| {
8432            panel.new_directory(&NewDirectory, window, cx)
8433        });
8434        panel.update_in(cx, |panel, window, cx| {
8435            assert!(panel.filename_editor.read(cx).is_focused(window));
8436        });
8437        assert_eq!(
8438            visible_entries_as_strings(&panel, 0..10, cx),
8439            &[
8440                //
8441                "v src",
8442                "    > [EDITOR: '']  <== selected",
8443                "    > test"
8444            ]
8445        );
8446
8447        panel.update_in(cx, |panel, window, cx| {
8448            panel.cancel(&menu::Cancel, window, cx)
8449        });
8450        assert_eq!(
8451            visible_entries_as_strings(&panel, 0..10, cx),
8452            &[
8453                //
8454                "v src  <== selected",
8455                "    > test"
8456            ]
8457        );
8458    }
8459
8460    #[gpui::test]
8461    async fn test_basic_file_deletion_scenarios(cx: &mut gpui::TestAppContext) {
8462        init_test_with_editor(cx);
8463
8464        let fs = FakeFs::new(cx.executor().clone());
8465        fs.insert_tree(
8466            "/root",
8467            json!({
8468                "dir1": {
8469                    "subdir1": {},
8470                    "file1.txt": "",
8471                    "file2.txt": "",
8472                },
8473                "dir2": {
8474                    "subdir2": {},
8475                    "file3.txt": "",
8476                    "file4.txt": "",
8477                },
8478                "file5.txt": "",
8479                "file6.txt": "",
8480            }),
8481        )
8482        .await;
8483
8484        let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
8485        let workspace =
8486            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
8487        let cx = &mut VisualTestContext::from_window(*workspace, cx);
8488        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
8489
8490        toggle_expand_dir(&panel, "root/dir1", cx);
8491        toggle_expand_dir(&panel, "root/dir2", cx);
8492
8493        // Test Case 1: Delete middle file in directory
8494        select_path(&panel, "root/dir1/file1.txt", cx);
8495        assert_eq!(
8496            visible_entries_as_strings(&panel, 0..15, cx),
8497            &[
8498                "v root",
8499                "    v dir1",
8500                "        > subdir1",
8501                "          file1.txt  <== selected",
8502                "          file2.txt",
8503                "    v dir2",
8504                "        > subdir2",
8505                "          file3.txt",
8506                "          file4.txt",
8507                "      file5.txt",
8508                "      file6.txt",
8509            ],
8510            "Initial state before deleting middle file"
8511        );
8512
8513        submit_deletion(&panel, cx);
8514        assert_eq!(
8515            visible_entries_as_strings(&panel, 0..15, cx),
8516            &[
8517                "v root",
8518                "    v dir1",
8519                "        > subdir1",
8520                "          file2.txt  <== selected",
8521                "    v dir2",
8522                "        > subdir2",
8523                "          file3.txt",
8524                "          file4.txt",
8525                "      file5.txt",
8526                "      file6.txt",
8527            ],
8528            "Should select next file after deleting middle file"
8529        );
8530
8531        // Test Case 2: Delete last file in directory
8532        submit_deletion(&panel, cx);
8533        assert_eq!(
8534            visible_entries_as_strings(&panel, 0..15, cx),
8535            &[
8536                "v root",
8537                "    v dir1",
8538                "        > subdir1  <== selected",
8539                "    v dir2",
8540                "        > subdir2",
8541                "          file3.txt",
8542                "          file4.txt",
8543                "      file5.txt",
8544                "      file6.txt",
8545            ],
8546            "Should select next directory when last file is deleted"
8547        );
8548
8549        // Test Case 3: Delete root level file
8550        select_path(&panel, "root/file6.txt", cx);
8551        assert_eq!(
8552            visible_entries_as_strings(&panel, 0..15, cx),
8553            &[
8554                "v root",
8555                "    v dir1",
8556                "        > subdir1",
8557                "    v dir2",
8558                "        > subdir2",
8559                "          file3.txt",
8560                "          file4.txt",
8561                "      file5.txt",
8562                "      file6.txt  <== selected",
8563            ],
8564            "Initial state before deleting root level file"
8565        );
8566
8567        submit_deletion(&panel, cx);
8568        assert_eq!(
8569            visible_entries_as_strings(&panel, 0..15, cx),
8570            &[
8571                "v root",
8572                "    v dir1",
8573                "        > subdir1",
8574                "    v dir2",
8575                "        > subdir2",
8576                "          file3.txt",
8577                "          file4.txt",
8578                "      file5.txt  <== selected",
8579            ],
8580            "Should select prev entry at root level"
8581        );
8582    }
8583
8584    #[gpui::test]
8585    async fn test_complex_selection_scenarios(cx: &mut gpui::TestAppContext) {
8586        init_test_with_editor(cx);
8587
8588        let fs = FakeFs::new(cx.executor().clone());
8589        fs.insert_tree(
8590            "/root",
8591            json!({
8592                "dir1": {
8593                    "subdir1": {
8594                        "a.txt": "",
8595                        "b.txt": ""
8596                    },
8597                    "file1.txt": "",
8598                },
8599                "dir2": {
8600                    "subdir2": {
8601                        "c.txt": "",
8602                        "d.txt": ""
8603                    },
8604                    "file2.txt": "",
8605                },
8606                "file3.txt": "",
8607            }),
8608        )
8609        .await;
8610
8611        let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
8612        let workspace =
8613            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
8614        let cx = &mut VisualTestContext::from_window(*workspace, cx);
8615        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
8616
8617        toggle_expand_dir(&panel, "root/dir1", cx);
8618        toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
8619        toggle_expand_dir(&panel, "root/dir2", cx);
8620        toggle_expand_dir(&panel, "root/dir2/subdir2", cx);
8621
8622        // Test Case 1: Select and delete nested directory with parent
8623        cx.simulate_modifiers_change(gpui::Modifiers {
8624            control: true,
8625            ..Default::default()
8626        });
8627        select_path_with_mark(&panel, "root/dir1/subdir1", cx);
8628        select_path_with_mark(&panel, "root/dir1", cx);
8629
8630        assert_eq!(
8631            visible_entries_as_strings(&panel, 0..15, cx),
8632            &[
8633                "v root",
8634                "    v dir1  <== selected  <== marked",
8635                "        v subdir1  <== marked",
8636                "              a.txt",
8637                "              b.txt",
8638                "          file1.txt",
8639                "    v dir2",
8640                "        v subdir2",
8641                "              c.txt",
8642                "              d.txt",
8643                "          file2.txt",
8644                "      file3.txt",
8645            ],
8646            "Initial state before deleting nested directory with parent"
8647        );
8648
8649        submit_deletion(&panel, cx);
8650        assert_eq!(
8651            visible_entries_as_strings(&panel, 0..15, cx),
8652            &[
8653                "v root",
8654                "    v dir2  <== selected",
8655                "        v subdir2",
8656                "              c.txt",
8657                "              d.txt",
8658                "          file2.txt",
8659                "      file3.txt",
8660            ],
8661            "Should select next directory after deleting directory with parent"
8662        );
8663
8664        // Test Case 2: Select mixed files and directories across levels
8665        select_path_with_mark(&panel, "root/dir2/subdir2/c.txt", cx);
8666        select_path_with_mark(&panel, "root/dir2/file2.txt", cx);
8667        select_path_with_mark(&panel, "root/file3.txt", cx);
8668
8669        assert_eq!(
8670            visible_entries_as_strings(&panel, 0..15, cx),
8671            &[
8672                "v root",
8673                "    v dir2",
8674                "        v subdir2",
8675                "              c.txt  <== marked",
8676                "              d.txt",
8677                "          file2.txt  <== marked",
8678                "      file3.txt  <== selected  <== marked",
8679            ],
8680            "Initial state before deleting"
8681        );
8682
8683        submit_deletion(&panel, cx);
8684        assert_eq!(
8685            visible_entries_as_strings(&panel, 0..15, cx),
8686            &[
8687                "v root",
8688                "    v dir2  <== selected",
8689                "        v subdir2",
8690                "              d.txt",
8691            ],
8692            "Should select sibling directory"
8693        );
8694    }
8695
8696    #[gpui::test]
8697    async fn test_delete_all_files_and_directories(cx: &mut gpui::TestAppContext) {
8698        init_test_with_editor(cx);
8699
8700        let fs = FakeFs::new(cx.executor().clone());
8701        fs.insert_tree(
8702            "/root",
8703            json!({
8704                "dir1": {
8705                    "subdir1": {
8706                        "a.txt": "",
8707                        "b.txt": ""
8708                    },
8709                    "file1.txt": "",
8710                },
8711                "dir2": {
8712                    "subdir2": {
8713                        "c.txt": "",
8714                        "d.txt": ""
8715                    },
8716                    "file2.txt": "",
8717                },
8718                "file3.txt": "",
8719                "file4.txt": "",
8720            }),
8721        )
8722        .await;
8723
8724        let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
8725        let workspace =
8726            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
8727        let cx = &mut VisualTestContext::from_window(*workspace, cx);
8728        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
8729
8730        toggle_expand_dir(&panel, "root/dir1", cx);
8731        toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
8732        toggle_expand_dir(&panel, "root/dir2", cx);
8733        toggle_expand_dir(&panel, "root/dir2/subdir2", cx);
8734
8735        // Test Case 1: Select all root files and directories
8736        cx.simulate_modifiers_change(gpui::Modifiers {
8737            control: true,
8738            ..Default::default()
8739        });
8740        select_path_with_mark(&panel, "root/dir1", cx);
8741        select_path_with_mark(&panel, "root/dir2", cx);
8742        select_path_with_mark(&panel, "root/file3.txt", cx);
8743        select_path_with_mark(&panel, "root/file4.txt", cx);
8744        assert_eq!(
8745            visible_entries_as_strings(&panel, 0..20, cx),
8746            &[
8747                "v root",
8748                "    v dir1  <== marked",
8749                "        v subdir1",
8750                "              a.txt",
8751                "              b.txt",
8752                "          file1.txt",
8753                "    v dir2  <== marked",
8754                "        v subdir2",
8755                "              c.txt",
8756                "              d.txt",
8757                "          file2.txt",
8758                "      file3.txt  <== marked",
8759                "      file4.txt  <== selected  <== marked",
8760            ],
8761            "State before deleting all contents"
8762        );
8763
8764        submit_deletion(&panel, cx);
8765        assert_eq!(
8766            visible_entries_as_strings(&panel, 0..20, cx),
8767            &["v root  <== selected"],
8768            "Only empty root directory should remain after deleting all contents"
8769        );
8770    }
8771
8772    #[gpui::test]
8773    async fn test_nested_selection_deletion(cx: &mut gpui::TestAppContext) {
8774        init_test_with_editor(cx);
8775
8776        let fs = FakeFs::new(cx.executor().clone());
8777        fs.insert_tree(
8778            "/root",
8779            json!({
8780                "dir1": {
8781                    "subdir1": {
8782                        "file_a.txt": "content a",
8783                        "file_b.txt": "content b",
8784                    },
8785                    "subdir2": {
8786                        "file_c.txt": "content c",
8787                    },
8788                    "file1.txt": "content 1",
8789                },
8790                "dir2": {
8791                    "file2.txt": "content 2",
8792                },
8793            }),
8794        )
8795        .await;
8796
8797        let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
8798        let workspace =
8799            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
8800        let cx = &mut VisualTestContext::from_window(*workspace, cx);
8801        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
8802
8803        toggle_expand_dir(&panel, "root/dir1", cx);
8804        toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
8805        toggle_expand_dir(&panel, "root/dir2", cx);
8806        cx.simulate_modifiers_change(gpui::Modifiers {
8807            control: true,
8808            ..Default::default()
8809        });
8810
8811        // Test Case 1: Select parent directory, subdirectory, and a file inside the subdirectory
8812        select_path_with_mark(&panel, "root/dir1", cx);
8813        select_path_with_mark(&panel, "root/dir1/subdir1", cx);
8814        select_path_with_mark(&panel, "root/dir1/subdir1/file_a.txt", cx);
8815
8816        assert_eq!(
8817            visible_entries_as_strings(&panel, 0..20, cx),
8818            &[
8819                "v root",
8820                "    v dir1  <== marked",
8821                "        v subdir1  <== marked",
8822                "              file_a.txt  <== selected  <== marked",
8823                "              file_b.txt",
8824                "        > subdir2",
8825                "          file1.txt",
8826                "    v dir2",
8827                "          file2.txt",
8828            ],
8829            "State with parent dir, subdir, and file selected"
8830        );
8831        submit_deletion(&panel, cx);
8832        assert_eq!(
8833            visible_entries_as_strings(&panel, 0..20, cx),
8834            &["v root", "    v dir2  <== selected", "          file2.txt",],
8835            "Only dir2 should remain after deletion"
8836        );
8837    }
8838
8839    #[gpui::test]
8840    async fn test_multiple_worktrees_deletion(cx: &mut gpui::TestAppContext) {
8841        init_test_with_editor(cx);
8842
8843        let fs = FakeFs::new(cx.executor().clone());
8844        // First worktree
8845        fs.insert_tree(
8846            "/root1",
8847            json!({
8848                "dir1": {
8849                    "file1.txt": "content 1",
8850                    "file2.txt": "content 2",
8851                },
8852                "dir2": {
8853                    "file3.txt": "content 3",
8854                },
8855            }),
8856        )
8857        .await;
8858
8859        // Second worktree
8860        fs.insert_tree(
8861            "/root2",
8862            json!({
8863                "dir3": {
8864                    "file4.txt": "content 4",
8865                    "file5.txt": "content 5",
8866                },
8867                "file6.txt": "content 6",
8868            }),
8869        )
8870        .await;
8871
8872        let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
8873        let workspace =
8874            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
8875        let cx = &mut VisualTestContext::from_window(*workspace, cx);
8876        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
8877
8878        // Expand all directories for testing
8879        toggle_expand_dir(&panel, "root1/dir1", cx);
8880        toggle_expand_dir(&panel, "root1/dir2", cx);
8881        toggle_expand_dir(&panel, "root2/dir3", cx);
8882
8883        // Test Case 1: Delete files across different worktrees
8884        cx.simulate_modifiers_change(gpui::Modifiers {
8885            control: true,
8886            ..Default::default()
8887        });
8888        select_path_with_mark(&panel, "root1/dir1/file1.txt", cx);
8889        select_path_with_mark(&panel, "root2/dir3/file4.txt", cx);
8890
8891        assert_eq!(
8892            visible_entries_as_strings(&panel, 0..20, cx),
8893            &[
8894                "v root1",
8895                "    v dir1",
8896                "          file1.txt  <== marked",
8897                "          file2.txt",
8898                "    v dir2",
8899                "          file3.txt",
8900                "v root2",
8901                "    v dir3",
8902                "          file4.txt  <== selected  <== marked",
8903                "          file5.txt",
8904                "      file6.txt",
8905            ],
8906            "Initial state with files selected from different worktrees"
8907        );
8908
8909        submit_deletion(&panel, cx);
8910        assert_eq!(
8911            visible_entries_as_strings(&panel, 0..20, cx),
8912            &[
8913                "v root1",
8914                "    v dir1",
8915                "          file2.txt",
8916                "    v dir2",
8917                "          file3.txt",
8918                "v root2",
8919                "    v dir3",
8920                "          file5.txt  <== selected",
8921                "      file6.txt",
8922            ],
8923            "Should select next file in the last worktree after deletion"
8924        );
8925
8926        // Test Case 2: Delete directories from different worktrees
8927        select_path_with_mark(&panel, "root1/dir1", cx);
8928        select_path_with_mark(&panel, "root2/dir3", cx);
8929
8930        assert_eq!(
8931            visible_entries_as_strings(&panel, 0..20, cx),
8932            &[
8933                "v root1",
8934                "    v dir1  <== marked",
8935                "          file2.txt",
8936                "    v dir2",
8937                "          file3.txt",
8938                "v root2",
8939                "    v dir3  <== selected  <== marked",
8940                "          file5.txt",
8941                "      file6.txt",
8942            ],
8943            "State with directories marked from different worktrees"
8944        );
8945
8946        submit_deletion(&panel, cx);
8947        assert_eq!(
8948            visible_entries_as_strings(&panel, 0..20, cx),
8949            &[
8950                "v root1",
8951                "    v dir2",
8952                "          file3.txt",
8953                "v root2",
8954                "      file6.txt  <== selected",
8955            ],
8956            "Should select remaining file in last worktree after directory deletion"
8957        );
8958
8959        // Test Case 4: Delete all remaining files except roots
8960        select_path_with_mark(&panel, "root1/dir2/file3.txt", cx);
8961        select_path_with_mark(&panel, "root2/file6.txt", cx);
8962
8963        assert_eq!(
8964            visible_entries_as_strings(&panel, 0..20, cx),
8965            &[
8966                "v root1",
8967                "    v dir2",
8968                "          file3.txt  <== marked",
8969                "v root2",
8970                "      file6.txt  <== selected  <== marked",
8971            ],
8972            "State with all remaining files marked"
8973        );
8974
8975        submit_deletion(&panel, cx);
8976        assert_eq!(
8977            visible_entries_as_strings(&panel, 0..20, cx),
8978            &["v root1", "    v dir2", "v root2  <== selected"],
8979            "Second parent root should be selected after deleting"
8980        );
8981    }
8982
8983    #[gpui::test]
8984    async fn test_selection_vs_marked_entries_priority(cx: &mut gpui::TestAppContext) {
8985        init_test_with_editor(cx);
8986
8987        let fs = FakeFs::new(cx.executor().clone());
8988        fs.insert_tree(
8989            "/root",
8990            json!({
8991                "dir1": {
8992                    "file1.txt": "",
8993                    "file2.txt": "",
8994                    "file3.txt": "",
8995                },
8996                "dir2": {
8997                    "file4.txt": "",
8998                    "file5.txt": "",
8999                },
9000            }),
9001        )
9002        .await;
9003
9004        let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
9005        let workspace =
9006            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9007        let cx = &mut VisualTestContext::from_window(*workspace, cx);
9008        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
9009
9010        toggle_expand_dir(&panel, "root/dir1", cx);
9011        toggle_expand_dir(&panel, "root/dir2", cx);
9012
9013        cx.simulate_modifiers_change(gpui::Modifiers {
9014            control: true,
9015            ..Default::default()
9016        });
9017
9018        select_path_with_mark(&panel, "root/dir1/file2.txt", cx);
9019        select_path(&panel, "root/dir1/file1.txt", cx);
9020
9021        assert_eq!(
9022            visible_entries_as_strings(&panel, 0..15, cx),
9023            &[
9024                "v root",
9025                "    v dir1",
9026                "          file1.txt  <== selected",
9027                "          file2.txt  <== marked",
9028                "          file3.txt",
9029                "    v dir2",
9030                "          file4.txt",
9031                "          file5.txt",
9032            ],
9033            "Initial state with one marked entry and different selection"
9034        );
9035
9036        // Delete should operate on the selected entry (file1.txt)
9037        submit_deletion(&panel, cx);
9038        assert_eq!(
9039            visible_entries_as_strings(&panel, 0..15, cx),
9040            &[
9041                "v root",
9042                "    v dir1",
9043                "          file2.txt  <== selected  <== marked",
9044                "          file3.txt",
9045                "    v dir2",
9046                "          file4.txt",
9047                "          file5.txt",
9048            ],
9049            "Should delete selected file, not marked file"
9050        );
9051
9052        select_path_with_mark(&panel, "root/dir1/file3.txt", cx);
9053        select_path_with_mark(&panel, "root/dir2/file4.txt", cx);
9054        select_path(&panel, "root/dir2/file5.txt", cx);
9055
9056        assert_eq!(
9057            visible_entries_as_strings(&panel, 0..15, cx),
9058            &[
9059                "v root",
9060                "    v dir1",
9061                "          file2.txt  <== marked",
9062                "          file3.txt  <== marked",
9063                "    v dir2",
9064                "          file4.txt  <== marked",
9065                "          file5.txt  <== selected",
9066            ],
9067            "Initial state with multiple marked entries and different selection"
9068        );
9069
9070        // Delete should operate on all marked entries, ignoring the selection
9071        submit_deletion(&panel, cx);
9072        assert_eq!(
9073            visible_entries_as_strings(&panel, 0..15, cx),
9074            &[
9075                "v root",
9076                "    v dir1",
9077                "    v dir2",
9078                "          file5.txt  <== selected",
9079            ],
9080            "Should delete all marked files, leaving only the selected file"
9081        );
9082    }
9083
9084    #[gpui::test]
9085    async fn test_selection_fallback_to_next_highest_worktree(cx: &mut gpui::TestAppContext) {
9086        init_test_with_editor(cx);
9087
9088        let fs = FakeFs::new(cx.executor().clone());
9089        fs.insert_tree(
9090            "/root_b",
9091            json!({
9092                "dir1": {
9093                    "file1.txt": "content 1",
9094                    "file2.txt": "content 2",
9095                },
9096            }),
9097        )
9098        .await;
9099
9100        fs.insert_tree(
9101            "/root_c",
9102            json!({
9103                "dir2": {},
9104            }),
9105        )
9106        .await;
9107
9108        let project = Project::test(fs.clone(), ["/root_b".as_ref(), "/root_c".as_ref()], cx).await;
9109        let workspace =
9110            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9111        let cx = &mut VisualTestContext::from_window(*workspace, cx);
9112        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
9113
9114        toggle_expand_dir(&panel, "root_b/dir1", cx);
9115        toggle_expand_dir(&panel, "root_c/dir2", cx);
9116
9117        cx.simulate_modifiers_change(gpui::Modifiers {
9118            control: true,
9119            ..Default::default()
9120        });
9121        select_path_with_mark(&panel, "root_b/dir1/file1.txt", cx);
9122        select_path_with_mark(&panel, "root_b/dir1/file2.txt", cx);
9123
9124        assert_eq!(
9125            visible_entries_as_strings(&panel, 0..20, cx),
9126            &[
9127                "v root_b",
9128                "    v dir1",
9129                "          file1.txt  <== marked",
9130                "          file2.txt  <== selected  <== marked",
9131                "v root_c",
9132                "    v dir2",
9133            ],
9134            "Initial state with files marked in root_b"
9135        );
9136
9137        submit_deletion(&panel, cx);
9138        assert_eq!(
9139            visible_entries_as_strings(&panel, 0..20, cx),
9140            &[
9141                "v root_b",
9142                "    v dir1  <== selected",
9143                "v root_c",
9144                "    v dir2",
9145            ],
9146            "After deletion in root_b as it's last deletion, selection should be in root_b"
9147        );
9148
9149        select_path_with_mark(&panel, "root_c/dir2", cx);
9150
9151        submit_deletion(&panel, cx);
9152        assert_eq!(
9153            visible_entries_as_strings(&panel, 0..20, cx),
9154            &["v root_b", "    v dir1", "v root_c  <== selected",],
9155            "After deleting from root_c, it should remain in root_c"
9156        );
9157    }
9158
9159    fn toggle_expand_dir(
9160        panel: &Entity<ProjectPanel>,
9161        path: impl AsRef<Path>,
9162        cx: &mut VisualTestContext,
9163    ) {
9164        let path = path.as_ref();
9165        panel.update_in(cx, |panel, window, cx| {
9166            for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
9167                let worktree = worktree.read(cx);
9168                if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
9169                    let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
9170                    panel.toggle_expanded(entry_id, window, cx);
9171                    return;
9172                }
9173            }
9174            panic!("no worktree for path {:?}", path);
9175        });
9176    }
9177
9178    #[gpui::test]
9179    async fn test_expand_all_for_entry(cx: &mut gpui::TestAppContext) {
9180        init_test_with_editor(cx);
9181
9182        let fs = FakeFs::new(cx.executor().clone());
9183        fs.insert_tree(
9184            path!("/root"),
9185            json!({
9186                ".gitignore": "**/ignored_dir\n**/ignored_nested",
9187                "dir1": {
9188                    "empty1": {
9189                        "empty2": {
9190                            "empty3": {
9191                                "file.txt": ""
9192                            }
9193                        }
9194                    },
9195                    "subdir1": {
9196                        "file1.txt": "",
9197                        "file2.txt": "",
9198                        "ignored_nested": {
9199                            "ignored_file.txt": ""
9200                        }
9201                    },
9202                    "ignored_dir": {
9203                        "subdir": {
9204                            "deep_file.txt": ""
9205                        }
9206                    }
9207                }
9208            }),
9209        )
9210        .await;
9211
9212        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
9213        let workspace =
9214            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9215        let cx = &mut VisualTestContext::from_window(*workspace, cx);
9216
9217        // Test 1: When auto-fold is enabled
9218        cx.update(|_, cx| {
9219            let settings = *ProjectPanelSettings::get_global(cx);
9220            ProjectPanelSettings::override_global(
9221                ProjectPanelSettings {
9222                    auto_fold_dirs: true,
9223                    ..settings
9224                },
9225                cx,
9226            );
9227        });
9228
9229        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
9230
9231        assert_eq!(
9232            visible_entries_as_strings(&panel, 0..20, cx),
9233            &["v root", "    > dir1", "      .gitignore",],
9234            "Initial state should show collapsed root structure"
9235        );
9236
9237        toggle_expand_dir(&panel, "root/dir1", cx);
9238        assert_eq!(
9239            visible_entries_as_strings(&panel, 0..20, cx),
9240            &[
9241                separator!("v root"),
9242                separator!("    v dir1  <== selected"),
9243                separator!("        > empty1/empty2/empty3"),
9244                separator!("        > ignored_dir"),
9245                separator!("        > subdir1"),
9246                separator!("      .gitignore"),
9247            ],
9248            "Should show first level with auto-folded dirs and ignored dir visible"
9249        );
9250
9251        let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
9252        panel.update(cx, |panel, cx| {
9253            let project = panel.project.read(cx);
9254            let worktree = project.worktrees(cx).next().unwrap().read(cx);
9255            panel.expand_all_for_entry(worktree.id(), entry_id, cx);
9256            panel.update_visible_entries(None, cx);
9257        });
9258        cx.run_until_parked();
9259
9260        assert_eq!(
9261            visible_entries_as_strings(&panel, 0..20, cx),
9262            &[
9263                separator!("v root"),
9264                separator!("    v dir1  <== selected"),
9265                separator!("        v empty1"),
9266                separator!("            v empty2"),
9267                separator!("                v empty3"),
9268                separator!("                      file.txt"),
9269                separator!("        > ignored_dir"),
9270                separator!("        v subdir1"),
9271                separator!("            > ignored_nested"),
9272                separator!("              file1.txt"),
9273                separator!("              file2.txt"),
9274                separator!("      .gitignore"),
9275            ],
9276            "After expand_all with auto-fold: should not expand ignored_dir, should expand folded dirs, and should not expand ignored_nested"
9277        );
9278
9279        // Test 2: When auto-fold is disabled
9280        cx.update(|_, cx| {
9281            let settings = *ProjectPanelSettings::get_global(cx);
9282            ProjectPanelSettings::override_global(
9283                ProjectPanelSettings {
9284                    auto_fold_dirs: false,
9285                    ..settings
9286                },
9287                cx,
9288            );
9289        });
9290
9291        panel.update_in(cx, |panel, window, cx| {
9292            panel.collapse_all_entries(&CollapseAllEntries, window, cx);
9293        });
9294
9295        toggle_expand_dir(&panel, "root/dir1", cx);
9296        assert_eq!(
9297            visible_entries_as_strings(&panel, 0..20, cx),
9298            &[
9299                separator!("v root"),
9300                separator!("    v dir1  <== selected"),
9301                separator!("        > empty1"),
9302                separator!("        > ignored_dir"),
9303                separator!("        > subdir1"),
9304                separator!("      .gitignore"),
9305            ],
9306            "With auto-fold disabled: should show all directories separately"
9307        );
9308
9309        let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
9310        panel.update(cx, |panel, cx| {
9311            let project = panel.project.read(cx);
9312            let worktree = project.worktrees(cx).next().unwrap().read(cx);
9313            panel.expand_all_for_entry(worktree.id(), entry_id, cx);
9314            panel.update_visible_entries(None, cx);
9315        });
9316        cx.run_until_parked();
9317
9318        assert_eq!(
9319            visible_entries_as_strings(&panel, 0..20, cx),
9320            &[
9321                separator!("v root"),
9322                separator!("    v dir1  <== selected"),
9323                separator!("        v empty1"),
9324                separator!("            v empty2"),
9325                separator!("                v empty3"),
9326                separator!("                      file.txt"),
9327                separator!("        > ignored_dir"),
9328                separator!("        v subdir1"),
9329                separator!("            > ignored_nested"),
9330                separator!("              file1.txt"),
9331                separator!("              file2.txt"),
9332                separator!("      .gitignore"),
9333            ],
9334            "After expand_all without auto-fold: should expand all dirs normally, \
9335         expand ignored_dir itself but not its subdirs, and not expand ignored_nested"
9336        );
9337
9338        // Test 3: When explicitly called on ignored directory
9339        let ignored_dir_entry = find_project_entry(&panel, "root/dir1/ignored_dir", cx).unwrap();
9340        panel.update(cx, |panel, cx| {
9341            let project = panel.project.read(cx);
9342            let worktree = project.worktrees(cx).next().unwrap().read(cx);
9343            panel.expand_all_for_entry(worktree.id(), ignored_dir_entry, cx);
9344            panel.update_visible_entries(None, cx);
9345        });
9346        cx.run_until_parked();
9347
9348        assert_eq!(
9349            visible_entries_as_strings(&panel, 0..20, cx),
9350            &[
9351                separator!("v root"),
9352                separator!("    v dir1  <== selected"),
9353                separator!("        v empty1"),
9354                separator!("            v empty2"),
9355                separator!("                v empty3"),
9356                separator!("                      file.txt"),
9357                separator!("        v ignored_dir"),
9358                separator!("            v subdir"),
9359                separator!("                  deep_file.txt"),
9360                separator!("        v subdir1"),
9361                separator!("            > ignored_nested"),
9362                separator!("              file1.txt"),
9363                separator!("              file2.txt"),
9364                separator!("      .gitignore"),
9365            ],
9366            "After expand_all on ignored_dir: should expand all contents of the ignored directory"
9367        );
9368    }
9369
9370    #[gpui::test]
9371    async fn test_collapse_all_for_entry(cx: &mut gpui::TestAppContext) {
9372        init_test(cx);
9373
9374        let fs = FakeFs::new(cx.executor().clone());
9375        fs.insert_tree(
9376            path!("/root"),
9377            json!({
9378                "dir1": {
9379                    "subdir1": {
9380                        "nested1": {
9381                            "file1.txt": "",
9382                            "file2.txt": ""
9383                        },
9384                    },
9385                    "subdir2": {
9386                        "file4.txt": ""
9387                    }
9388                },
9389                "dir2": {
9390                    "single_file": {
9391                        "file5.txt": ""
9392                    }
9393                }
9394            }),
9395        )
9396        .await;
9397
9398        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
9399        let workspace =
9400            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9401        let cx = &mut VisualTestContext::from_window(*workspace, cx);
9402
9403        // Test 1: Basic collapsing
9404        {
9405            let panel = workspace.update(cx, ProjectPanel::new).unwrap();
9406
9407            toggle_expand_dir(&panel, "root/dir1", cx);
9408            toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
9409            toggle_expand_dir(&panel, "root/dir1/subdir1/nested1", cx);
9410            toggle_expand_dir(&panel, "root/dir1/subdir2", cx);
9411
9412            assert_eq!(
9413                visible_entries_as_strings(&panel, 0..20, cx),
9414                &[
9415                    separator!("v root"),
9416                    separator!("    v dir1"),
9417                    separator!("        v subdir1"),
9418                    separator!("            v nested1"),
9419                    separator!("                  file1.txt"),
9420                    separator!("                  file2.txt"),
9421                    separator!("        v subdir2  <== selected"),
9422                    separator!("              file4.txt"),
9423                    separator!("    > dir2"),
9424                ],
9425                "Initial state with everything expanded"
9426            );
9427
9428            let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
9429            panel.update(cx, |panel, cx| {
9430                let project = panel.project.read(cx);
9431                let worktree = project.worktrees(cx).next().unwrap().read(cx);
9432                panel.collapse_all_for_entry(worktree.id(), entry_id, cx);
9433                panel.update_visible_entries(None, cx);
9434            });
9435
9436            assert_eq!(
9437                visible_entries_as_strings(&panel, 0..20, cx),
9438                &["v root", "    > dir1", "    > dir2",],
9439                "All subdirs under dir1 should be collapsed"
9440            );
9441        }
9442
9443        // Test 2: With auto-fold enabled
9444        {
9445            cx.update(|_, cx| {
9446                let settings = *ProjectPanelSettings::get_global(cx);
9447                ProjectPanelSettings::override_global(
9448                    ProjectPanelSettings {
9449                        auto_fold_dirs: true,
9450                        ..settings
9451                    },
9452                    cx,
9453                );
9454            });
9455
9456            let panel = workspace.update(cx, ProjectPanel::new).unwrap();
9457
9458            toggle_expand_dir(&panel, "root/dir1", cx);
9459            toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
9460            toggle_expand_dir(&panel, "root/dir1/subdir1/nested1", cx);
9461
9462            assert_eq!(
9463                visible_entries_as_strings(&panel, 0..20, cx),
9464                &[
9465                    separator!("v root"),
9466                    separator!("    v dir1"),
9467                    separator!("        v subdir1/nested1  <== selected"),
9468                    separator!("              file1.txt"),
9469                    separator!("              file2.txt"),
9470                    separator!("        > subdir2"),
9471                    separator!("    > dir2/single_file"),
9472                ],
9473                "Initial state with some dirs expanded"
9474            );
9475
9476            let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
9477            panel.update(cx, |panel, cx| {
9478                let project = panel.project.read(cx);
9479                let worktree = project.worktrees(cx).next().unwrap().read(cx);
9480                panel.collapse_all_for_entry(worktree.id(), entry_id, cx);
9481            });
9482
9483            toggle_expand_dir(&panel, "root/dir1", cx);
9484
9485            assert_eq!(
9486                visible_entries_as_strings(&panel, 0..20, cx),
9487                &[
9488                    separator!("v root"),
9489                    separator!("    v dir1  <== selected"),
9490                    separator!("        > subdir1/nested1"),
9491                    separator!("        > subdir2"),
9492                    separator!("    > dir2/single_file"),
9493                ],
9494                "Subdirs should be collapsed and folded with auto-fold enabled"
9495            );
9496        }
9497
9498        // Test 3: With auto-fold disabled
9499        {
9500            cx.update(|_, cx| {
9501                let settings = *ProjectPanelSettings::get_global(cx);
9502                ProjectPanelSettings::override_global(
9503                    ProjectPanelSettings {
9504                        auto_fold_dirs: false,
9505                        ..settings
9506                    },
9507                    cx,
9508                );
9509            });
9510
9511            let panel = workspace.update(cx, ProjectPanel::new).unwrap();
9512
9513            toggle_expand_dir(&panel, "root/dir1", cx);
9514            toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
9515            toggle_expand_dir(&panel, "root/dir1/subdir1/nested1", cx);
9516
9517            assert_eq!(
9518                visible_entries_as_strings(&panel, 0..20, cx),
9519                &[
9520                    separator!("v root"),
9521                    separator!("    v dir1"),
9522                    separator!("        v subdir1"),
9523                    separator!("            v nested1  <== selected"),
9524                    separator!("                  file1.txt"),
9525                    separator!("                  file2.txt"),
9526                    separator!("        > subdir2"),
9527                    separator!("    > dir2"),
9528                ],
9529                "Initial state with some dirs expanded and auto-fold disabled"
9530            );
9531
9532            let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
9533            panel.update(cx, |panel, cx| {
9534                let project = panel.project.read(cx);
9535                let worktree = project.worktrees(cx).next().unwrap().read(cx);
9536                panel.collapse_all_for_entry(worktree.id(), entry_id, cx);
9537            });
9538
9539            toggle_expand_dir(&panel, "root/dir1", cx);
9540
9541            assert_eq!(
9542                visible_entries_as_strings(&panel, 0..20, cx),
9543                &[
9544                    separator!("v root"),
9545                    separator!("    v dir1  <== selected"),
9546                    separator!("        > subdir1"),
9547                    separator!("        > subdir2"),
9548                    separator!("    > dir2"),
9549                ],
9550                "Subdirs should be collapsed but not folded with auto-fold disabled"
9551            );
9552        }
9553    }
9554
9555    fn select_path(
9556        panel: &Entity<ProjectPanel>,
9557        path: impl AsRef<Path>,
9558        cx: &mut VisualTestContext,
9559    ) {
9560        let path = path.as_ref();
9561        panel.update(cx, |panel, cx| {
9562            for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
9563                let worktree = worktree.read(cx);
9564                if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
9565                    let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
9566                    panel.selection = Some(crate::SelectedEntry {
9567                        worktree_id: worktree.id(),
9568                        entry_id,
9569                    });
9570                    return;
9571                }
9572            }
9573            panic!("no worktree for path {:?}", path);
9574        });
9575    }
9576
9577    fn select_path_with_mark(
9578        panel: &Entity<ProjectPanel>,
9579        path: impl AsRef<Path>,
9580        cx: &mut VisualTestContext,
9581    ) {
9582        let path = path.as_ref();
9583        panel.update(cx, |panel, cx| {
9584            for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
9585                let worktree = worktree.read(cx);
9586                if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
9587                    let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
9588                    let entry = crate::SelectedEntry {
9589                        worktree_id: worktree.id(),
9590                        entry_id,
9591                    };
9592                    if !panel.marked_entries.contains(&entry) {
9593                        panel.marked_entries.insert(entry);
9594                    }
9595                    panel.selection = Some(entry);
9596                    return;
9597                }
9598            }
9599            panic!("no worktree for path {:?}", path);
9600        });
9601    }
9602
9603    fn find_project_entry(
9604        panel: &Entity<ProjectPanel>,
9605        path: impl AsRef<Path>,
9606        cx: &mut VisualTestContext,
9607    ) -> Option<ProjectEntryId> {
9608        let path = path.as_ref();
9609        panel.update(cx, |panel, cx| {
9610            for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
9611                let worktree = worktree.read(cx);
9612                if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
9613                    return worktree.entry_for_path(relative_path).map(|entry| entry.id);
9614                }
9615            }
9616            panic!("no worktree for path {path:?}");
9617        })
9618    }
9619
9620    fn visible_entries_as_strings(
9621        panel: &Entity<ProjectPanel>,
9622        range: Range<usize>,
9623        cx: &mut VisualTestContext,
9624    ) -> Vec<String> {
9625        let mut result = Vec::new();
9626        let mut project_entries = HashSet::default();
9627        let mut has_editor = false;
9628
9629        panel.update_in(cx, |panel, window, cx| {
9630            panel.for_each_visible_entry(range, window, cx, |project_entry, details, _, _| {
9631                if details.is_editing {
9632                    assert!(!has_editor, "duplicate editor entry");
9633                    has_editor = true;
9634                } else {
9635                    assert!(
9636                        project_entries.insert(project_entry),
9637                        "duplicate project entry {:?} {:?}",
9638                        project_entry,
9639                        details
9640                    );
9641                }
9642
9643                let indent = "    ".repeat(details.depth);
9644                let icon = if details.kind.is_dir() {
9645                    if details.is_expanded {
9646                        "v "
9647                    } else {
9648                        "> "
9649                    }
9650                } else {
9651                    "  "
9652                };
9653                let name = if details.is_editing {
9654                    format!("[EDITOR: '{}']", details.filename)
9655                } else if details.is_processing {
9656                    format!("[PROCESSING: '{}']", details.filename)
9657                } else {
9658                    details.filename.clone()
9659                };
9660                let selected = if details.is_selected {
9661                    "  <== selected"
9662                } else {
9663                    ""
9664                };
9665                let marked = if details.is_marked {
9666                    "  <== marked"
9667                } else {
9668                    ""
9669                };
9670
9671                result.push(format!("{indent}{icon}{name}{selected}{marked}"));
9672            });
9673        });
9674
9675        result
9676    }
9677
9678    fn init_test(cx: &mut TestAppContext) {
9679        cx.update(|cx| {
9680            let settings_store = SettingsStore::test(cx);
9681            cx.set_global(settings_store);
9682            init_settings(cx);
9683            theme::init(theme::LoadThemes::JustBase, cx);
9684            language::init(cx);
9685            editor::init_settings(cx);
9686            crate::init(cx);
9687            workspace::init_settings(cx);
9688            client::init_settings(cx);
9689            Project::init_settings(cx);
9690
9691            cx.update_global::<SettingsStore, _>(|store, cx| {
9692                store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
9693                    project_panel_settings.auto_fold_dirs = Some(false);
9694                });
9695                store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
9696                    worktree_settings.file_scan_exclusions = Some(Vec::new());
9697                });
9698            });
9699        });
9700    }
9701
9702    fn init_test_with_editor(cx: &mut TestAppContext) {
9703        cx.update(|cx| {
9704            let app_state = AppState::test(cx);
9705            theme::init(theme::LoadThemes::JustBase, cx);
9706            init_settings(cx);
9707            language::init(cx);
9708            editor::init(cx);
9709            crate::init(cx);
9710            workspace::init(app_state.clone(), cx);
9711            Project::init_settings(cx);
9712
9713            cx.update_global::<SettingsStore, _>(|store, cx| {
9714                store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
9715                    project_panel_settings.auto_fold_dirs = Some(false);
9716                });
9717                store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
9718                    worktree_settings.file_scan_exclusions = Some(Vec::new());
9719                });
9720            });
9721        });
9722    }
9723
9724    fn ensure_single_file_is_opened(
9725        window: &WindowHandle<Workspace>,
9726        expected_path: &str,
9727        cx: &mut TestAppContext,
9728    ) {
9729        window
9730            .update(cx, |workspace, _, cx| {
9731                let worktrees = workspace.worktrees(cx).collect::<Vec<_>>();
9732                assert_eq!(worktrees.len(), 1);
9733                let worktree_id = worktrees[0].read(cx).id();
9734
9735                let open_project_paths = workspace
9736                    .panes()
9737                    .iter()
9738                    .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
9739                    .collect::<Vec<_>>();
9740                assert_eq!(
9741                    open_project_paths,
9742                    vec![ProjectPath {
9743                        worktree_id,
9744                        path: Arc::from(Path::new(expected_path))
9745                    }],
9746                    "Should have opened file, selected in project panel"
9747                );
9748            })
9749            .unwrap();
9750    }
9751
9752    fn submit_deletion(panel: &Entity<ProjectPanel>, cx: &mut VisualTestContext) {
9753        assert!(
9754            !cx.has_pending_prompt(),
9755            "Should have no prompts before the deletion"
9756        );
9757        panel.update_in(cx, |panel, window, cx| {
9758            panel.delete(&Delete { skip_prompt: false }, window, cx)
9759        });
9760        assert!(
9761            cx.has_pending_prompt(),
9762            "Should have a prompt after the deletion"
9763        );
9764        cx.simulate_prompt_answer("Delete");
9765        assert!(
9766            !cx.has_pending_prompt(),
9767            "Should have no prompts after prompt was replied to"
9768        );
9769        cx.executor().run_until_parked();
9770    }
9771
9772    fn submit_deletion_skipping_prompt(panel: &Entity<ProjectPanel>, cx: &mut VisualTestContext) {
9773        assert!(
9774            !cx.has_pending_prompt(),
9775            "Should have no prompts before the deletion"
9776        );
9777        panel.update_in(cx, |panel, window, cx| {
9778            panel.delete(&Delete { skip_prompt: true }, window, cx)
9779        });
9780        assert!(!cx.has_pending_prompt(), "Should have received no prompts");
9781        cx.executor().run_until_parked();
9782    }
9783
9784    fn ensure_no_open_items_and_panes(
9785        workspace: &WindowHandle<Workspace>,
9786        cx: &mut VisualTestContext,
9787    ) {
9788        assert!(
9789            !cx.has_pending_prompt(),
9790            "Should have no prompts after deletion operation closes the file"
9791        );
9792        workspace
9793            .read_with(cx, |workspace, cx| {
9794                let open_project_paths = workspace
9795                    .panes()
9796                    .iter()
9797                    .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
9798                    .collect::<Vec<_>>();
9799                assert!(
9800                    open_project_paths.is_empty(),
9801                    "Deleted file's buffer should be closed, but got open files: {open_project_paths:?}"
9802                );
9803            })
9804            .unwrap();
9805    }
9806
9807    struct TestProjectItemView {
9808        focus_handle: FocusHandle,
9809        path: ProjectPath,
9810    }
9811
9812    struct TestProjectItem {
9813        path: ProjectPath,
9814    }
9815
9816    impl project::ProjectItem for TestProjectItem {
9817        fn try_open(
9818            _project: &Entity<Project>,
9819            path: &ProjectPath,
9820            cx: &mut App,
9821        ) -> Option<Task<gpui::Result<Entity<Self>>>> {
9822            let path = path.clone();
9823            Some(cx.spawn(|mut cx| async move { cx.new(|_| Self { path }) }))
9824        }
9825
9826        fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
9827            None
9828        }
9829
9830        fn project_path(&self, _: &App) -> Option<ProjectPath> {
9831            Some(self.path.clone())
9832        }
9833
9834        fn is_dirty(&self) -> bool {
9835            false
9836        }
9837    }
9838
9839    impl ProjectItem for TestProjectItemView {
9840        type Item = TestProjectItem;
9841
9842        fn for_project_item(
9843            _: Entity<Project>,
9844            project_item: Entity<Self::Item>,
9845            _: &mut Window,
9846            cx: &mut Context<Self>,
9847        ) -> Self
9848        where
9849            Self: Sized,
9850        {
9851            Self {
9852                path: project_item.update(cx, |project_item, _| project_item.path.clone()),
9853                focus_handle: cx.focus_handle(),
9854            }
9855        }
9856    }
9857
9858    impl Item for TestProjectItemView {
9859        type Event = ();
9860    }
9861
9862    impl EventEmitter<()> for TestProjectItemView {}
9863
9864    impl Focusable for TestProjectItemView {
9865        fn focus_handle(&self, _: &App) -> FocusHandle {
9866            self.focus_handle.clone()
9867        }
9868    }
9869
9870    impl Render for TestProjectItemView {
9871        fn render(&mut self, _window: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
9872            Empty
9873        }
9874    }
9875}