project_panel.rs

   1pub mod project_panel_settings;
   2mod undo;
   3mod utils;
   4
   5use anyhow::{Context as _, Result};
   6use client::{ErrorCode, ErrorExt};
   7use collections::{BTreeSet, HashMap, hash_map};
   8use command_palette_hooks::CommandPaletteFilter;
   9use editor::{
  10    Editor, EditorEvent, MultiBufferOffset,
  11    items::{
  12        entry_diagnostic_aware_icon_decoration_and_color,
  13        entry_diagnostic_aware_icon_name_and_color, entry_git_aware_label_color,
  14    },
  15};
  16use feature_flags::{FeatureFlagAppExt, ProjectPanelUndoRedoFeatureFlag};
  17use file_icons::FileIcons;
  18use git;
  19use git::status::GitSummary;
  20use git_ui;
  21use git_ui::file_diff_view::FileDiffView;
  22use gpui::{
  23    Action, AnyElement, App, AsyncWindowContext, Bounds, ClipboardEntry as GpuiClipboardEntry,
  24    ClipboardItem, Context, CursorStyle, DismissEvent, Div, DragMoveEvent, Entity, EventEmitter,
  25    ExternalPaths, FocusHandle, Focusable, FontWeight, Hsla, InteractiveElement, KeyContext,
  26    ListHorizontalSizingBehavior, ListSizingBehavior, Modifiers, ModifiersChangedEvent,
  27    MouseButton, MouseDownEvent, ParentElement, PathPromptOptions, Pixels, Point, PromptLevel,
  28    Render, ScrollStrategy, Stateful, Styled, Subscription, Task, UniformListScrollHandle,
  29    WeakEntity, Window, actions, anchored, deferred, div, hsla, linear_color_stop, linear_gradient,
  30    point, px, size, transparent_white, uniform_list,
  31};
  32use language::DiagnosticSeverity;
  33use menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrevious};
  34use notifications::status_toast::StatusToast;
  35use project::{
  36    Entry, EntryKind, Fs, GitEntry, GitEntryRef, GitTraversal, Project, ProjectEntryId,
  37    ProjectPath, Worktree, WorktreeId,
  38    git_store::{GitStoreEvent, RepositoryEvent, git_traversal::ChildEntriesGitIter},
  39    project_settings::GoToDiagnosticSeverityFilter,
  40};
  41use project_panel_settings::ProjectPanelSettings;
  42use rayon::slice::ParallelSliceMut;
  43use schemars::JsonSchema;
  44use serde::Deserialize;
  45use settings::{
  46    DockSide, ProjectPanelEntrySpacing, Settings, SettingsStore, ShowDiagnostics, ShowIndentGuides,
  47    update_settings_file,
  48};
  49use smallvec::SmallVec;
  50use std::{
  51    any::TypeId,
  52    cell::OnceCell,
  53    cmp,
  54    collections::HashSet,
  55    ops::Neg,
  56    ops::Range,
  57    path::{Path, PathBuf},
  58    sync::Arc,
  59    time::{Duration, Instant},
  60};
  61use theme_settings::ThemeSettings;
  62use ui::{
  63    Color, ContextMenu, ContextMenuEntry, DecoratedIcon, Divider, Icon, IconDecoration,
  64    IconDecorationKind, IndentGuideColors, IndentGuideLayout, Indicator, KeyBinding, Label,
  65    LabelSize, ListItem, ListItemSpacing, ScrollAxes, ScrollableHandle, Scrollbars,
  66    StickyCandidate, Tooltip, WithScrollbar, prelude::*, v_flex,
  67};
  68use util::{
  69    ResultExt, TakeUntilExt, TryFutureExt, maybe,
  70    paths::{PathStyle, compare_paths},
  71    rel_path::{RelPath, RelPathBuf},
  72};
  73use workspace::{
  74    DraggedSelection, OpenInTerminal, OpenMode, OpenOptions, OpenVisible, PreviewTabsSettings,
  75    SelectedEntry, SplitDirection, Workspace,
  76    dock::{DockPosition, Panel, PanelEvent},
  77    notifications::{DetachAndPromptErr, NotifyResultExt, NotifyTaskExt},
  78};
  79use worktree::CreatedEntry;
  80use zed_actions::{
  81    project_panel::{Toggle, ToggleFocus},
  82    workspace::OpenWithSystem,
  83};
  84
  85use crate::{
  86    project_panel_settings::ProjectPanelScrollbarProxy,
  87    undo::{Change, UndoManager},
  88};
  89
  90const PROJECT_PANEL_KEY: &str = "ProjectPanel";
  91const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX;
  92
  93struct VisibleEntriesForWorktree {
  94    worktree_id: WorktreeId,
  95    entries: Vec<GitEntry>,
  96    index: OnceCell<HashSet<Arc<RelPath>>>,
  97}
  98
  99struct State {
 100    last_worktree_root_id: Option<ProjectEntryId>,
 101    /// Maps from leaf project entry ID to the currently selected ancestor.
 102    /// Relevant only for auto-fold dirs, where a single project panel entry may actually consist of several
 103    /// project entries (and all non-leaf nodes are guaranteed to be directories).
 104    ancestors: HashMap<ProjectEntryId, FoldedAncestors>,
 105    visible_entries: Vec<VisibleEntriesForWorktree>,
 106    max_width_item_index: Option<usize>,
 107    edit_state: Option<EditState>,
 108    temporarily_unfolded_pending_state: Option<TemporaryUnfoldedPendingState>,
 109    unfolded_dir_ids: HashSet<ProjectEntryId>,
 110    expanded_dir_ids: HashMap<WorktreeId, Vec<ProjectEntryId>>,
 111}
 112
 113impl State {
 114    fn is_unfolded(&self, entry_id: &ProjectEntryId) -> bool {
 115        self.unfolded_dir_ids.contains(entry_id)
 116            || self.edit_state.as_ref().map_or(false, |edit_state| {
 117                edit_state.temporarily_unfolded == Some(*entry_id)
 118            })
 119    }
 120
 121    fn derive(old: &Self) -> Self {
 122        Self {
 123            last_worktree_root_id: None,
 124            ancestors: Default::default(),
 125            visible_entries: Default::default(),
 126            max_width_item_index: None,
 127            edit_state: old.edit_state.clone(),
 128            temporarily_unfolded_pending_state: None,
 129            unfolded_dir_ids: old.unfolded_dir_ids.clone(),
 130            expanded_dir_ids: old.expanded_dir_ids.clone(),
 131        }
 132    }
 133}
 134
 135pub struct ProjectPanel {
 136    project: Entity<Project>,
 137    fs: Arc<dyn Fs>,
 138    focus_handle: FocusHandle,
 139    scroll_handle: UniformListScrollHandle,
 140    // An update loop that keeps incrementing/decrementing scroll offset while there is a dragged entry that's
 141    // hovered over the start/end of a list.
 142    hover_scroll_task: Option<Task<()>>,
 143    rendered_entries_len: usize,
 144    folded_directory_drag_target: Option<FoldedDirectoryDragTarget>,
 145    drag_target_entry: Option<DragTarget>,
 146    marked_entries: Vec<SelectedEntry>,
 147    selection: Option<SelectedEntry>,
 148    context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
 149    filename_editor: Entity<Editor>,
 150    clipboard: Option<ClipboardEntry>,
 151    _dragged_entry_destination: Option<Arc<Path>>,
 152    workspace: WeakEntity<Workspace>,
 153    diagnostics: HashMap<(WorktreeId, Arc<RelPath>), DiagnosticSeverity>,
 154    diagnostic_counts: HashMap<(WorktreeId, Arc<RelPath>), DiagnosticCount>,
 155    diagnostic_summary_update: Task<()>,
 156    // We keep track of the mouse down state on entries so we don't flash the UI
 157    // in case a user clicks to open a file.
 158    mouse_down: bool,
 159    hover_expand_task: Option<Task<()>>,
 160    previous_drag_position: Option<Point<Pixels>>,
 161    sticky_items_count: usize,
 162    last_reported_update: Instant,
 163    update_visible_entries_task: UpdateVisibleEntriesTask,
 164    undo_manager: UndoManager,
 165    state: State,
 166}
 167
 168struct UpdateVisibleEntriesTask {
 169    _visible_entries_task: Task<()>,
 170    focus_filename_editor: bool,
 171    autoscroll: bool,
 172}
 173
 174#[derive(Debug)]
 175struct TemporaryUnfoldedPendingState {
 176    previously_focused_leaf_entry: SelectedEntry,
 177    temporarily_unfolded_active_entry_id: ProjectEntryId,
 178}
 179
 180impl Default for UpdateVisibleEntriesTask {
 181    fn default() -> Self {
 182        UpdateVisibleEntriesTask {
 183            _visible_entries_task: Task::ready(()),
 184            focus_filename_editor: Default::default(),
 185            autoscroll: Default::default(),
 186        }
 187    }
 188}
 189
 190enum DragTarget {
 191    /// Dragging on an entry
 192    Entry {
 193        /// The entry currently under the mouse cursor during a drag operation
 194        entry_id: ProjectEntryId,
 195        /// Highlight this entry along with all of its children
 196        highlight_entry_id: ProjectEntryId,
 197    },
 198    /// Dragging on background
 199    Background,
 200}
 201
 202#[derive(Copy, Clone, Debug)]
 203struct FoldedDirectoryDragTarget {
 204    entry_id: ProjectEntryId,
 205    index: usize,
 206    /// Whether we are dragging over the delimiter rather than the component itself.
 207    is_delimiter_target: bool,
 208}
 209
 210#[derive(Clone, Debug)]
 211enum ValidationState {
 212    None,
 213    Warning(String),
 214    Error(String),
 215}
 216
 217#[derive(Clone, Debug)]
 218struct EditState {
 219    worktree_id: WorktreeId,
 220    entry_id: ProjectEntryId,
 221    leaf_entry_id: Option<ProjectEntryId>,
 222    is_dir: bool,
 223    depth: usize,
 224    processing_filename: Option<Arc<RelPath>>,
 225    previously_focused: Option<SelectedEntry>,
 226    validation_state: ValidationState,
 227    temporarily_unfolded: Option<ProjectEntryId>,
 228}
 229
 230impl EditState {
 231    fn is_new_entry(&self) -> bool {
 232        self.leaf_entry_id.is_none()
 233    }
 234}
 235
 236#[derive(Clone, Debug)]
 237enum ClipboardEntry {
 238    Copied(BTreeSet<SelectedEntry>),
 239    Cut(BTreeSet<SelectedEntry>),
 240}
 241
 242#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)]
 243struct DiagnosticCount {
 244    error_count: usize,
 245    warning_count: usize,
 246}
 247
 248impl DiagnosticCount {
 249    fn capped_error_count(&self) -> String {
 250        Self::capped_count(self.error_count)
 251    }
 252
 253    fn capped_warning_count(&self) -> String {
 254        Self::capped_count(self.warning_count)
 255    }
 256
 257    fn capped_count(count: usize) -> String {
 258        if count > 99 {
 259            "99+".to_string()
 260        } else {
 261            count.to_string()
 262        }
 263    }
 264}
 265
 266#[derive(Debug, PartialEq, Eq, Clone)]
 267struct EntryDetails {
 268    filename: String,
 269    icon: Option<SharedString>,
 270    path: Arc<RelPath>,
 271    depth: usize,
 272    kind: EntryKind,
 273    is_ignored: bool,
 274    is_expanded: bool,
 275    is_selected: bool,
 276    is_marked: bool,
 277    is_editing: bool,
 278    is_processing: bool,
 279    is_cut: bool,
 280    sticky: Option<StickyDetails>,
 281    filename_text_color: Color,
 282    diagnostic_severity: Option<DiagnosticSeverity>,
 283    diagnostic_count: Option<DiagnosticCount>,
 284    git_status: GitSummary,
 285    is_private: bool,
 286    worktree_id: WorktreeId,
 287    canonical_path: Option<Arc<Path>>,
 288}
 289
 290#[derive(Debug, PartialEq, Eq, Clone)]
 291struct StickyDetails {
 292    sticky_index: usize,
 293}
 294
 295/// Permanently deletes the selected file or directory.
 296#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
 297#[action(namespace = project_panel)]
 298#[serde(deny_unknown_fields)]
 299struct Delete {
 300    #[serde(default)]
 301    pub skip_prompt: bool,
 302}
 303
 304/// Moves the selected file or directory to the system trash.
 305#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
 306#[action(namespace = project_panel)]
 307#[serde(deny_unknown_fields)]
 308struct Trash {
 309    #[serde(default)]
 310    pub skip_prompt: bool,
 311}
 312
 313/// Selects the next entry with diagnostics.
 314#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
 315#[action(namespace = project_panel)]
 316#[serde(deny_unknown_fields)]
 317struct SelectNextDiagnostic {
 318    #[serde(default)]
 319    pub severity: GoToDiagnosticSeverityFilter,
 320}
 321
 322/// Selects the previous entry with diagnostics.
 323#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
 324#[action(namespace = project_panel)]
 325#[serde(deny_unknown_fields)]
 326struct SelectPrevDiagnostic {
 327    #[serde(default)]
 328    pub severity: GoToDiagnosticSeverityFilter,
 329}
 330
 331actions!(
 332    project_panel,
 333    [
 334        /// Expands the selected entry in the project tree.
 335        ExpandSelectedEntry,
 336        /// Collapses the selected entry in the project tree.
 337        CollapseSelectedEntry,
 338        /// Collapses the selected entry and its children in the project tree.
 339        CollapseSelectedEntryAndChildren,
 340        /// Collapses all entries in the project tree.
 341        CollapseAllEntries,
 342        /// Creates a new directory.
 343        NewDirectory,
 344        /// Creates a new file.
 345        NewFile,
 346        /// Copies the selected file or directory.
 347        Copy,
 348        /// Duplicates the selected file or directory.
 349        Duplicate,
 350        /// Reveals the selected item in the system file manager.
 351        RevealInFileManager,
 352        /// Removes the selected folder from the project.
 353        RemoveFromProject,
 354        /// Cuts the selected file or directory.
 355        Cut,
 356        /// Pastes the previously cut or copied item.
 357        Paste,
 358        /// Downloads the selected remote file
 359        DownloadFromRemote,
 360        /// Renames the selected file or directory.
 361        Rename,
 362        /// Opens the selected file in the editor.
 363        Open,
 364        /// Opens the selected file in a permanent tab.
 365        OpenPermanent,
 366        /// Opens the selected file in a vertical split.
 367        OpenSplitVertical,
 368        /// Opens the selected file in a horizontal split.
 369        OpenSplitHorizontal,
 370        /// Toggles visibility of git-ignored files.
 371        ToggleHideGitIgnore,
 372        /// Toggles visibility of hidden files.
 373        ToggleHideHidden,
 374        /// Starts a new search in the selected directory.
 375        NewSearchInDirectory,
 376        /// Unfolds the selected directory.
 377        UnfoldDirectory,
 378        /// Folds the selected directory.
 379        FoldDirectory,
 380        /// Scroll half a page upwards
 381        ScrollUp,
 382        /// Scroll half a page downwards
 383        ScrollDown,
 384        /// Scroll until the cursor displays at the center
 385        ScrollCursorCenter,
 386        /// Scroll until the cursor displays at the top
 387        ScrollCursorTop,
 388        /// Scroll until the cursor displays at the bottom
 389        ScrollCursorBottom,
 390        /// Selects the parent directory.
 391        SelectParent,
 392        /// Selects the next entry with git changes.
 393        SelectNextGitEntry,
 394        /// Selects the previous entry with git changes.
 395        SelectPrevGitEntry,
 396        /// Selects the next directory.
 397        SelectNextDirectory,
 398        /// Selects the previous directory.
 399        SelectPrevDirectory,
 400        /// Opens a diff view to compare two marked files.
 401        CompareMarkedFiles,
 402        /// Undoes the last file operation.
 403        Undo,
 404        /// Redoes the last undone file operation.
 405        Redo,
 406    ]
 407);
 408
 409#[derive(Clone, Debug, Default)]
 410struct FoldedAncestors {
 411    current_ancestor_depth: usize,
 412    ancestors: Vec<ProjectEntryId>,
 413}
 414
 415impl FoldedAncestors {
 416    fn max_ancestor_depth(&self) -> usize {
 417        self.ancestors.len()
 418    }
 419
 420    /// Note: This returns None for last item in ancestors list
 421    fn active_ancestor(&self) -> Option<ProjectEntryId> {
 422        if self.current_ancestor_depth == 0 {
 423            return None;
 424        }
 425        self.ancestors.get(self.current_ancestor_depth).copied()
 426    }
 427
 428    fn active_index(&self) -> usize {
 429        self.max_ancestor_depth()
 430            .saturating_sub(1)
 431            .saturating_sub(self.current_ancestor_depth)
 432    }
 433
 434    fn set_active_index(&mut self, index: usize) -> bool {
 435        let new_depth = self
 436            .max_ancestor_depth()
 437            .saturating_sub(1)
 438            .saturating_sub(index);
 439        if self.current_ancestor_depth != new_depth {
 440            self.current_ancestor_depth = new_depth;
 441            true
 442        } else {
 443            false
 444        }
 445    }
 446
 447    fn active_component(&self, file_name: &str) -> Option<String> {
 448        Path::new(file_name)
 449            .components()
 450            .nth(self.active_index())
 451            .map(|comp| comp.as_os_str().to_string_lossy().into_owned())
 452    }
 453}
 454
 455pub fn init(cx: &mut App) {
 456    cx.observe_new(|workspace: &mut Workspace, _, _| {
 457        workspace.register_action(|workspace, _: &ToggleFocus, window, cx| {
 458            workspace.toggle_panel_focus::<ProjectPanel>(window, cx);
 459        });
 460        workspace.register_action(|workspace, _: &Toggle, window, cx| {
 461            if !workspace.toggle_panel_focus::<ProjectPanel>(window, cx) {
 462                workspace.close_panel::<ProjectPanel>(window, cx);
 463            }
 464        });
 465
 466        workspace.register_action(|workspace, _: &ToggleHideGitIgnore, _, cx| {
 467            let fs = workspace.app_state().fs.clone();
 468            update_settings_file(fs, cx, move |setting, _| {
 469                setting.project_panel.get_or_insert_default().hide_gitignore = Some(
 470                    !setting
 471                        .project_panel
 472                        .get_or_insert_default()
 473                        .hide_gitignore
 474                        .unwrap_or(false),
 475                );
 476            })
 477        });
 478
 479        workspace.register_action(|workspace, _: &ToggleHideHidden, _, cx| {
 480            let fs = workspace.app_state().fs.clone();
 481            update_settings_file(fs, cx, move |setting, _| {
 482                setting.project_panel.get_or_insert_default().hide_hidden = Some(
 483                    !setting
 484                        .project_panel
 485                        .get_or_insert_default()
 486                        .hide_hidden
 487                        .unwrap_or(false),
 488                );
 489            })
 490        });
 491
 492        workspace.register_action(|workspace, action: &CollapseAllEntries, window, cx| {
 493            if let Some(panel) = workspace.panel::<ProjectPanel>(cx) {
 494                panel.update(cx, |panel, cx| {
 495                    panel.collapse_all_entries(action, window, cx);
 496                });
 497            }
 498        });
 499
 500        workspace.register_action(|workspace, action: &Rename, window, cx| {
 501            workspace.open_panel::<ProjectPanel>(window, cx);
 502            if let Some(panel) = workspace.panel::<ProjectPanel>(cx) {
 503                panel.update(cx, |panel, cx| {
 504                    if let Some(first_marked) = panel.marked_entries.first() {
 505                        let first_marked = *first_marked;
 506                        panel.marked_entries.clear();
 507                        panel.selection = Some(first_marked);
 508                    }
 509                    panel.rename(action, window, cx);
 510                });
 511            }
 512        });
 513
 514        workspace.register_action(|workspace, action: &Duplicate, window, cx| {
 515            workspace.open_panel::<ProjectPanel>(window, cx);
 516            if let Some(panel) = workspace.panel::<ProjectPanel>(cx) {
 517                panel.update(cx, |panel, cx| {
 518                    panel.duplicate(action, window, cx);
 519                });
 520            }
 521        });
 522
 523        workspace.register_action(|workspace, action: &Delete, window, cx| {
 524            if let Some(panel) = workspace.panel::<ProjectPanel>(cx) {
 525                panel.update(cx, |panel, cx| panel.delete(action, window, cx));
 526            }
 527        });
 528
 529        workspace.register_action(|workspace, _: &git::FileHistory, window, cx| {
 530            // First try to get from project panel if it's focused
 531            if let Some(panel) = workspace.panel::<ProjectPanel>(cx) {
 532                let maybe_project_path = panel.read(cx).selection.and_then(|selection| {
 533                    let project = workspace.project().read(cx);
 534                    let worktree = project.worktree_for_id(selection.worktree_id, cx)?;
 535                    let entry = worktree.read(cx).entry_for_id(selection.entry_id)?;
 536                    if entry.is_file() {
 537                        Some(ProjectPath {
 538                            worktree_id: selection.worktree_id,
 539                            path: entry.path.clone(),
 540                        })
 541                    } else {
 542                        None
 543                    }
 544                });
 545
 546                if let Some(project_path) = maybe_project_path {
 547                    let project = workspace.project();
 548                    let git_store = project.read(cx).git_store();
 549                    if let Some((repo, repo_path)) = git_store
 550                        .read(cx)
 551                        .repository_and_path_for_project_path(&project_path, cx)
 552                    {
 553                        git_ui::file_history_view::FileHistoryView::open(
 554                            repo_path,
 555                            git_store.downgrade(),
 556                            repo.downgrade(),
 557                            workspace.weak_handle(),
 558                            window,
 559                            cx,
 560                        );
 561                        return;
 562                    }
 563                }
 564            }
 565
 566            // Fallback: try to get from active editor
 567            if let Some(active_item) = workspace.active_item(cx)
 568                && let Some(editor) = active_item.downcast::<Editor>()
 569                && let Some(buffer) = editor.read(cx).buffer().read(cx).as_singleton()
 570                && let Some(file) = buffer.read(cx).file()
 571            {
 572                let worktree_id = file.worktree_id(cx);
 573                let project_path = ProjectPath {
 574                    worktree_id,
 575                    path: file.path().clone(),
 576                };
 577                let project = workspace.project();
 578                let git_store = project.read(cx).git_store();
 579                if let Some((repo, repo_path)) = git_store
 580                    .read(cx)
 581                    .repository_and_path_for_project_path(&project_path, cx)
 582                {
 583                    git_ui::file_history_view::FileHistoryView::open(
 584                        repo_path,
 585                        git_store.downgrade(),
 586                        repo.downgrade(),
 587                        workspace.weak_handle(),
 588                        window,
 589                        cx,
 590                    );
 591                }
 592            }
 593        });
 594    })
 595    .detach();
 596}
 597
 598#[derive(Debug)]
 599pub enum Event {
 600    OpenedEntry {
 601        entry_id: ProjectEntryId,
 602        focus_opened_item: bool,
 603        allow_preview: bool,
 604    },
 605    SplitEntry {
 606        entry_id: ProjectEntryId,
 607        allow_preview: bool,
 608        split_direction: Option<SplitDirection>,
 609    },
 610    Focus,
 611}
 612
 613struct DraggedProjectEntryView {
 614    selection: SelectedEntry,
 615    icon: Option<SharedString>,
 616    filename: String,
 617    click_offset: Point<Pixels>,
 618    selections: Arc<[SelectedEntry]>,
 619}
 620
 621struct ItemColors {
 622    default: Hsla,
 623    hover: Hsla,
 624    drag_over: Hsla,
 625    marked: Hsla,
 626    focused: Hsla,
 627}
 628
 629fn get_item_color(is_sticky: bool, cx: &App) -> ItemColors {
 630    let colors = cx.theme().colors();
 631
 632    ItemColors {
 633        default: if is_sticky {
 634            colors.panel_overlay_background
 635        } else {
 636            colors.panel_background
 637        },
 638        hover: if is_sticky {
 639            colors.panel_overlay_hover
 640        } else {
 641            colors.element_hover
 642        },
 643        marked: colors.element_selected,
 644        focused: colors.panel_focused_border,
 645        drag_over: colors.drop_target_background,
 646    }
 647}
 648
 649impl ProjectPanel {
 650    fn new(
 651        workspace: &mut Workspace,
 652        window: &mut Window,
 653        cx: &mut Context<Workspace>,
 654    ) -> Entity<Self> {
 655        let project = workspace.project().clone();
 656        let git_store = project.read(cx).git_store().clone();
 657        let path_style = project.read(cx).path_style(cx);
 658        let project_panel = cx.new(|cx| {
 659            let focus_handle = cx.focus_handle();
 660            cx.on_focus(&focus_handle, window, Self::focus_in).detach();
 661
 662            cx.subscribe_in(
 663                &git_store,
 664                window,
 665                |this, _, event, window, cx| match event {
 666                    GitStoreEvent::RepositoryUpdated(_, RepositoryEvent::StatusesChanged, _)
 667                    | GitStoreEvent::RepositoryAdded
 668                    | GitStoreEvent::RepositoryRemoved(_) => {
 669                        this.update_visible_entries(None, false, false, window, cx);
 670                        cx.notify();
 671                    }
 672                    _ => {}
 673                },
 674            )
 675            .detach();
 676
 677            cx.subscribe_in(
 678                &project,
 679                window,
 680                |this, project, event, window, cx| match event {
 681                    project::Event::ActiveEntryChanged(Some(entry_id)) => {
 682                        if ProjectPanelSettings::get_global(cx).auto_reveal_entries {
 683                            this.reveal_entry(project.clone(), *entry_id, true, window, cx)
 684                                .ok();
 685                        }
 686                    }
 687                    project::Event::ActiveEntryChanged(None) => {
 688                        let is_active_item_file_diff_view = this
 689                            .workspace
 690                            .upgrade()
 691                            .and_then(|ws| ws.read(cx).active_item(cx))
 692                            .map(|item| {
 693                                item.act_as_type(TypeId::of::<FileDiffView>(), cx).is_some()
 694                            })
 695                            .unwrap_or(false);
 696                        if !is_active_item_file_diff_view {
 697                            this.marked_entries.clear();
 698                        }
 699                    }
 700                    project::Event::RevealInProjectPanel(entry_id) => {
 701                        if let Some(()) = this
 702                            .reveal_entry(project.clone(), *entry_id, false, window, cx)
 703                            .log_err()
 704                        {
 705                            cx.emit(PanelEvent::Activate);
 706                        }
 707                    }
 708                    project::Event::ActivateProjectPanel => {
 709                        cx.emit(PanelEvent::Activate);
 710                    }
 711                    project::Event::DiskBasedDiagnosticsFinished { .. }
 712                    | project::Event::DiagnosticsUpdated { .. } => {
 713                        if ProjectPanelSettings::get_global(cx).show_diagnostics
 714                            != ShowDiagnostics::Off
 715                        {
 716                            this.diagnostic_summary_update = cx.spawn(async move |this, cx| {
 717                                cx.background_executor()
 718                                    .timer(Duration::from_millis(30))
 719                                    .await;
 720                                this.update(cx, |this, cx| {
 721                                    this.update_diagnostics(cx);
 722                                    cx.notify();
 723                                })
 724                                .log_err();
 725                            });
 726                        }
 727                    }
 728                    project::Event::WorktreeRemoved(id) => {
 729                        this.state.expanded_dir_ids.remove(id);
 730                        this.update_visible_entries(None, false, false, window, cx);
 731                        cx.notify();
 732                    }
 733                    project::Event::WorktreeUpdatedEntries(_, _)
 734                    | project::Event::WorktreeAdded(_)
 735                    | project::Event::WorktreeOrderChanged => {
 736                        this.update_visible_entries(None, false, false, window, cx);
 737                        cx.notify();
 738                    }
 739                    project::Event::ExpandedAllForEntry(worktree_id, entry_id) => {
 740                        if let Some((worktree, expanded_dir_ids)) = project
 741                            .read(cx)
 742                            .worktree_for_id(*worktree_id, cx)
 743                            .zip(this.state.expanded_dir_ids.get_mut(worktree_id))
 744                        {
 745                            let worktree = worktree.read(cx);
 746
 747                            let Some(entry) = worktree.entry_for_id(*entry_id) else {
 748                                return;
 749                            };
 750                            let include_ignored_dirs = !entry.is_ignored;
 751
 752                            let mut dirs_to_expand = vec![*entry_id];
 753                            while let Some(current_id) = dirs_to_expand.pop() {
 754                                let Some(current_entry) = worktree.entry_for_id(current_id) else {
 755                                    continue;
 756                                };
 757                                for child in worktree.child_entries(&current_entry.path) {
 758                                    if !child.is_dir() || (include_ignored_dirs && child.is_ignored)
 759                                    {
 760                                        continue;
 761                                    }
 762
 763                                    dirs_to_expand.push(child.id);
 764
 765                                    if let Err(ix) = expanded_dir_ids.binary_search(&child.id) {
 766                                        expanded_dir_ids.insert(ix, child.id);
 767                                    }
 768                                    this.state.unfolded_dir_ids.insert(child.id);
 769                                }
 770                            }
 771                            this.update_visible_entries(None, false, false, window, cx);
 772                            cx.notify();
 773                        }
 774                    }
 775                    _ => {}
 776                },
 777            )
 778            .detach();
 779
 780            let trash_action = [TypeId::of::<Trash>()];
 781            let is_remote = project.read(cx).is_remote();
 782
 783            // Make sure the trash option is never displayed anywhere on remote
 784            // hosts since they may not support trashing. May want to dynamically
 785            // detect this in the future.
 786            if is_remote {
 787                CommandPaletteFilter::update_global(cx, |filter, _cx| {
 788                    filter.hide_action_types(&trash_action);
 789                });
 790            }
 791
 792            let filename_editor = cx.new(|cx| Editor::single_line(window, cx));
 793
 794            cx.subscribe_in(
 795                &filename_editor,
 796                window,
 797                |project_panel, _, editor_event, window, cx| match editor_event {
 798                    EditorEvent::BufferEdited => {
 799                        project_panel.populate_validation_error(cx);
 800                        project_panel.autoscroll(cx);
 801                    }
 802                    EditorEvent::SelectionsChanged { .. } => {
 803                        project_panel.autoscroll(cx);
 804                    }
 805                    EditorEvent::Blurred => {
 806                        if project_panel
 807                            .state
 808                            .edit_state
 809                            .as_ref()
 810                            .is_some_and(|state| state.processing_filename.is_none())
 811                        {
 812                            match project_panel.confirm_edit(false, window, cx) {
 813                                Some(task) => {
 814                                    task.detach_and_notify_err(
 815                                        project_panel.workspace.clone(),
 816                                        window,
 817                                        cx,
 818                                    );
 819                                }
 820                                None => {
 821                                    project_panel.discard_edit_state(window, cx);
 822                                }
 823                            }
 824                        }
 825                    }
 826                    _ => {}
 827                },
 828            )
 829            .detach();
 830
 831            cx.observe_global::<FileIcons>(|_, cx| {
 832                cx.notify();
 833            })
 834            .detach();
 835
 836            let mut project_panel_settings = *ProjectPanelSettings::get_global(cx);
 837            cx.observe_global_in::<SettingsStore>(window, move |this, window, cx| {
 838                let new_settings = *ProjectPanelSettings::get_global(cx);
 839                if project_panel_settings != new_settings {
 840                    if project_panel_settings.hide_gitignore != new_settings.hide_gitignore {
 841                        this.update_visible_entries(None, false, false, window, cx);
 842                    }
 843                    if project_panel_settings.hide_root != new_settings.hide_root {
 844                        this.update_visible_entries(None, false, false, window, cx);
 845                    }
 846                    if project_panel_settings.hide_hidden != new_settings.hide_hidden {
 847                        this.update_visible_entries(None, false, false, window, cx);
 848                    }
 849                    if project_panel_settings.sort_mode != new_settings.sort_mode {
 850                        this.update_visible_entries(None, false, false, window, cx);
 851                    }
 852                    if project_panel_settings.sort_order != new_settings.sort_order {
 853                        this.update_visible_entries(None, false, false, window, cx);
 854                    }
 855                    if project_panel_settings.sticky_scroll && !new_settings.sticky_scroll {
 856                        this.sticky_items_count = 0;
 857                    }
 858                    project_panel_settings = new_settings;
 859                    this.update_diagnostics(cx);
 860                    cx.notify();
 861                }
 862            })
 863            .detach();
 864
 865            let scroll_handle = UniformListScrollHandle::new();
 866            let weak_project_panel = cx.weak_entity();
 867            let mut this = Self {
 868                project: project.clone(),
 869                hover_scroll_task: None,
 870                fs: workspace.app_state().fs.clone(),
 871                focus_handle,
 872                rendered_entries_len: 0,
 873                folded_directory_drag_target: None,
 874                drag_target_entry: None,
 875                marked_entries: Default::default(),
 876                selection: None,
 877                context_menu: None,
 878                filename_editor,
 879                clipboard: None,
 880                _dragged_entry_destination: None,
 881                workspace: workspace.weak_handle(),
 882                diagnostics: Default::default(),
 883                diagnostic_counts: Default::default(),
 884                diagnostic_summary_update: Task::ready(()),
 885                scroll_handle,
 886                mouse_down: false,
 887                hover_expand_task: None,
 888                previous_drag_position: None,
 889                sticky_items_count: 0,
 890                last_reported_update: Instant::now(),
 891                state: State {
 892                    max_width_item_index: None,
 893                    edit_state: None,
 894                    temporarily_unfolded_pending_state: None,
 895                    last_worktree_root_id: Default::default(),
 896                    visible_entries: Default::default(),
 897                    ancestors: Default::default(),
 898                    expanded_dir_ids: Default::default(),
 899                    unfolded_dir_ids: Default::default(),
 900                },
 901                update_visible_entries_task: Default::default(),
 902                undo_manager: UndoManager::new(workspace.weak_handle(), weak_project_panel, &cx),
 903            };
 904            this.update_visible_entries(None, false, false, window, cx);
 905
 906            this
 907        });
 908
 909        cx.subscribe_in(&project_panel, window, {
 910            let project_panel = project_panel.downgrade();
 911            move |workspace, _, event, window, cx| match event {
 912                &Event::OpenedEntry {
 913                    entry_id,
 914                    focus_opened_item,
 915                    allow_preview,
 916                } => {
 917                    if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx)
 918                        && let Some(entry) = worktree.read(cx).entry_for_id(entry_id) {
 919                            let file_path = entry.path.clone();
 920                            let worktree_id = worktree.read(cx).id();
 921                            let entry_id = entry.id;
 922                            let is_via_ssh = project.read(cx).is_via_remote_server();
 923
 924                            workspace
 925                                .open_path_preview(
 926                                    ProjectPath {
 927                                        worktree_id,
 928                                        path: file_path.clone(),
 929                                    },
 930                                    None,
 931                                    focus_opened_item,
 932                                    allow_preview,
 933                                    true,
 934                                    window, cx,
 935                                )
 936                                .detach_and_prompt_err("Failed to open file", window, cx, move |e, _, _| {
 937                                    match e.error_code() {
 938                                        ErrorCode::Disconnected => if is_via_ssh {
 939                                            Some("Disconnected from SSH host".to_string())
 940                                        } else {
 941                                            Some("Disconnected from remote project".to_string())
 942                                        },
 943                                        ErrorCode::UnsharedItem => Some(format!(
 944                                            "{} is not shared by the host. This could be because it has been marked as `private`",
 945                                            file_path.display(path_style)
 946                                        )),
 947                                        // See note in worktree.rs where this error originates. Returning Some in this case prevents
 948                                        // the error popup from saying "Try Again", which is a red herring in this case
 949                                        ErrorCode::Internal if e.to_string().contains("File is too large to load") => Some(e.to_string()),
 950                                        _ => None,
 951                                    }
 952                                });
 953
 954                            if let Some(project_panel) = project_panel.upgrade() {
 955                                // Always select and mark the entry, regardless of whether it is opened or not.
 956                                project_panel.update(cx, |project_panel, _| {
 957                                    let entry = SelectedEntry { worktree_id, entry_id };
 958                                    project_panel.marked_entries.clear();
 959                                    project_panel.marked_entries.push(entry);
 960                                    project_panel.selection = Some(entry);
 961                                });
 962                                if !focus_opened_item {
 963                                    let focus_handle = project_panel.read(cx).focus_handle.clone();
 964                                    window.focus(&focus_handle, cx);
 965                                }
 966                            }
 967                        }
 968                }
 969                &Event::SplitEntry {
 970                    entry_id,
 971                    allow_preview,
 972                    split_direction,
 973                } => {
 974                    if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx)
 975                        && let Some(entry) = worktree.read(cx).entry_for_id(entry_id) {
 976                            workspace
 977                                .split_path_preview(
 978                                    ProjectPath {
 979                                        worktree_id: worktree.read(cx).id(),
 980                                        path: entry.path.clone(),
 981                                    },
 982                                    allow_preview,
 983                                    split_direction,
 984                                    window, cx,
 985                                )
 986                                .detach_and_log_err(cx);
 987                        }
 988                }
 989
 990                _ => {}
 991            }
 992        })
 993        .detach();
 994
 995        project_panel
 996    }
 997
 998    pub async fn load(
 999        workspace: WeakEntity<Workspace>,
1000        mut cx: AsyncWindowContext,
1001    ) -> Result<Entity<Self>> {
1002        workspace.update_in(&mut cx, |workspace, window, cx| {
1003            ProjectPanel::new(workspace, window, cx)
1004        })
1005    }
1006
1007    fn update_diagnostics(&mut self, cx: &mut Context<Self>) {
1008        let mut diagnostics: HashMap<(WorktreeId, Arc<RelPath>), DiagnosticSeverity> =
1009            Default::default();
1010        let show_diagnostics_setting = ProjectPanelSettings::get_global(cx).show_diagnostics;
1011
1012        if show_diagnostics_setting != ShowDiagnostics::Off {
1013            self.project
1014                .read(cx)
1015                .diagnostic_summaries(false, cx)
1016                .filter_map(|(path, _, diagnostic_summary)| {
1017                    if diagnostic_summary.error_count > 0 {
1018                        Some((path, DiagnosticSeverity::ERROR))
1019                    } else if show_diagnostics_setting == ShowDiagnostics::All
1020                        && diagnostic_summary.warning_count > 0
1021                    {
1022                        Some((path, DiagnosticSeverity::WARNING))
1023                    } else {
1024                        None
1025                    }
1026                })
1027                .for_each(|(project_path, diagnostic_severity)| {
1028                    let ancestors = project_path.path.ancestors().collect::<Vec<_>>();
1029                    for path in ancestors.into_iter().rev() {
1030                        Self::update_strongest_diagnostic_severity(
1031                            &mut diagnostics,
1032                            &project_path,
1033                            path.into(),
1034                            diagnostic_severity,
1035                        );
1036                    }
1037                });
1038        }
1039        self.diagnostics = diagnostics;
1040
1041        let diagnostic_badges = ProjectPanelSettings::get_global(cx).diagnostic_badges;
1042        self.diagnostic_counts =
1043            if diagnostic_badges && show_diagnostics_setting != ShowDiagnostics::Off {
1044                self.project.read(cx).diagnostic_summaries(false, cx).fold(
1045                    HashMap::default(),
1046                    |mut counts, (project_path, _, summary)| {
1047                        let entry = counts
1048                            .entry((project_path.worktree_id, project_path.path))
1049                            .or_default();
1050                        entry.error_count += summary.error_count;
1051                        if show_diagnostics_setting == ShowDiagnostics::All {
1052                            entry.warning_count += summary.warning_count;
1053                        }
1054                        counts
1055                    },
1056                )
1057            } else {
1058                Default::default()
1059            };
1060    }
1061
1062    fn update_strongest_diagnostic_severity(
1063        diagnostics: &mut HashMap<(WorktreeId, Arc<RelPath>), DiagnosticSeverity>,
1064        project_path: &ProjectPath,
1065        path_buffer: Arc<RelPath>,
1066        diagnostic_severity: DiagnosticSeverity,
1067    ) {
1068        diagnostics
1069            .entry((project_path.worktree_id, path_buffer))
1070            .and_modify(|strongest_diagnostic_severity| {
1071                *strongest_diagnostic_severity =
1072                    cmp::min(*strongest_diagnostic_severity, diagnostic_severity);
1073            })
1074            .or_insert(diagnostic_severity);
1075    }
1076
1077    fn focus_in(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1078        if !self.focus_handle.contains_focused(window, cx) {
1079            cx.emit(Event::Focus);
1080        }
1081    }
1082
1083    fn deploy_context_menu(
1084        &mut self,
1085        position: Point<Pixels>,
1086        entry_id: ProjectEntryId,
1087        window: &mut Window,
1088        cx: &mut Context<Self>,
1089    ) {
1090        let project = self.project.read(cx);
1091
1092        let worktree_id = if let Some(id) = project.worktree_id_for_entry(entry_id, cx) {
1093            id
1094        } else {
1095            return;
1096        };
1097
1098        self.selection = Some(SelectedEntry {
1099            worktree_id,
1100            entry_id,
1101        });
1102
1103        if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
1104            let auto_fold_dirs = ProjectPanelSettings::get_global(cx).auto_fold_dirs;
1105            let worktree = worktree.read(cx);
1106            let is_root = Some(entry) == worktree.root_entry();
1107            let is_dir = entry.is_dir();
1108            let is_foldable = auto_fold_dirs && self.is_foldable(entry, worktree);
1109            let is_unfoldable = auto_fold_dirs && self.is_unfoldable(entry, worktree);
1110            let is_read_only = project.is_read_only(cx);
1111            let is_remote = project.is_remote();
1112            let is_collab = project.is_via_collab();
1113            let is_local = project.is_local() || project.is_via_wsl_with_host_interop(cx);
1114
1115            let settings = ProjectPanelSettings::get_global(cx);
1116            let visible_worktrees_count = project.visible_worktrees(cx).count();
1117            let should_hide_rename = is_root
1118                && (cfg!(target_os = "windows")
1119                    || (settings.hide_root && visible_worktrees_count == 1));
1120            let should_show_compare = !is_dir && self.file_abs_paths_to_diff(cx).is_some();
1121
1122            let has_git_repo = !is_dir && {
1123                let project_path = project::ProjectPath {
1124                    worktree_id,
1125                    path: entry.path.clone(),
1126                };
1127                project
1128                    .git_store()
1129                    .read(cx)
1130                    .repository_and_path_for_project_path(&project_path, cx)
1131                    .is_some()
1132            };
1133
1134            let has_pasteable_content = self.has_pasteable_content(cx);
1135            let entity = cx.entity();
1136            let context_menu = ContextMenu::build(window, cx, |menu, _, cx| {
1137                menu.context(self.focus_handle.clone()).map(|menu| {
1138                    if is_read_only {
1139                        menu.when(is_dir, |menu| {
1140                            menu.action("Search Inside", Box::new(NewSearchInDirectory))
1141                        })
1142                    } else {
1143                        menu.action("New File", Box::new(NewFile))
1144                            .action("New Folder", Box::new(NewDirectory))
1145                            .separator()
1146                            .when(is_local, |menu| {
1147                                menu.action(
1148                                    ui::utils::reveal_in_file_manager_label(is_remote),
1149                                    Box::new(RevealInFileManager),
1150                                )
1151                            })
1152                            .when(is_local, |menu| {
1153                                menu.action("Open in Default App", Box::new(OpenWithSystem))
1154                            })
1155                            .action("Open in Terminal", Box::new(OpenInTerminal))
1156                            .when(is_dir, |menu| {
1157                                menu.separator()
1158                                    .action("Find in Folder…", Box::new(NewSearchInDirectory))
1159                            })
1160                            .when(is_unfoldable, |menu| {
1161                                menu.action("Unfold Directory", Box::new(UnfoldDirectory))
1162                            })
1163                            .when(is_foldable, |menu| {
1164                                menu.action("Fold Directory", Box::new(FoldDirectory))
1165                            })
1166                            .when(should_show_compare, |menu| {
1167                                menu.separator()
1168                                    .action("Compare Marked Files", Box::new(CompareMarkedFiles))
1169                            })
1170                            .separator()
1171                            .action("Cut", Box::new(Cut))
1172                            .action("Copy", Box::new(Copy))
1173                            .action("Duplicate", Box::new(Duplicate))
1174                            // TODO: Paste should always be visible, cbut disabled when clipboard is empty
1175                            .action_disabled_when(!has_pasteable_content, "Paste", Box::new(Paste))
1176                            .when(cx.has_flag::<ProjectPanelUndoRedoFeatureFlag>(), |menu| {
1177                                menu.action_disabled_when(
1178                                    !self.undo_manager.can_undo(),
1179                                    "Undo",
1180                                    Box::new(Undo),
1181                                )
1182                                .action_disabled_when(
1183                                    !self.undo_manager.can_redo(),
1184                                    "Redo",
1185                                    Box::new(Redo),
1186                                )
1187                            })
1188                            .when(is_remote, |menu| {
1189                                menu.separator()
1190                                    .action("Download...", Box::new(DownloadFromRemote))
1191                            })
1192                            .separator()
1193                            .action("Copy Path", Box::new(zed_actions::workspace::CopyPath))
1194                            .action(
1195                                "Copy Relative Path",
1196                                Box::new(zed_actions::workspace::CopyRelativePath),
1197                            )
1198                            .when(!is_dir && self.has_git_changes(entry_id), |menu| {
1199                                menu.separator().action(
1200                                    "Restore File",
1201                                    Box::new(git::RestoreFile { skip_prompt: false }),
1202                                )
1203                            })
1204                            .when(has_git_repo, |menu| {
1205                                menu.separator()
1206                                    .action("View File History", Box::new(git::FileHistory))
1207                            })
1208                            .when(!should_hide_rename, |menu| {
1209                                menu.separator().action("Rename", Box::new(Rename))
1210                            })
1211                            .when(!is_root && !is_remote, |menu| {
1212                                menu.action("Trash", Box::new(Trash { skip_prompt: false }))
1213                            })
1214                            .when(!is_root, |menu| {
1215                                menu.action("Delete", Box::new(Delete { skip_prompt: false }))
1216                            })
1217                            .when(!is_collab && is_root, |menu| {
1218                                menu.separator()
1219                                    .action(
1220                                        "Add Project to Workspace…",
1221                                        Box::new(workspace::AddFolderToProject),
1222                                    )
1223                                    .action("Remove from Workspace", Box::new(RemoveFromProject))
1224                            })
1225                            .when(is_dir && !is_root, |menu| {
1226                                menu.separator().action(
1227                                    "Collapse All",
1228                                    Box::new(CollapseSelectedEntryAndChildren),
1229                                )
1230                            })
1231                            .when(is_dir && is_root, |menu| {
1232                                let entity = entity.clone();
1233                                menu.separator().item(
1234                                    ContextMenuEntry::new("Collapse All").handler(
1235                                        move |window, cx| {
1236                                            entity.update(cx, |this, cx| {
1237                                                this.collapse_all_for_root(window, cx);
1238                                            });
1239                                        },
1240                                    ),
1241                                )
1242                            })
1243                    }
1244                })
1245            });
1246
1247            window.focus(&context_menu.focus_handle(cx), cx);
1248            let subscription = cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| {
1249                this.context_menu.take();
1250                cx.notify();
1251            });
1252            self.context_menu = Some((context_menu, position, subscription));
1253        }
1254
1255        cx.notify();
1256    }
1257
1258    fn has_git_changes(&self, entry_id: ProjectEntryId) -> bool {
1259        for visible in &self.state.visible_entries {
1260            if let Some(git_entry) = visible.entries.iter().find(|e| e.id == entry_id) {
1261                let total_modified =
1262                    git_entry.git_summary.index.modified + git_entry.git_summary.worktree.modified;
1263                let total_deleted =
1264                    git_entry.git_summary.index.deleted + git_entry.git_summary.worktree.deleted;
1265                return total_modified > 0 || total_deleted > 0;
1266            }
1267        }
1268        false
1269    }
1270
1271    fn is_unfoldable(&self, entry: &Entry, worktree: &Worktree) -> bool {
1272        if !entry.is_dir() || self.state.unfolded_dir_ids.contains(&entry.id) {
1273            return false;
1274        }
1275
1276        if let Some(parent_path) = entry.path.parent() {
1277            let snapshot = worktree.snapshot();
1278            let mut child_entries = snapshot.child_entries(parent_path);
1279            if let Some(child) = child_entries.next()
1280                && child_entries.next().is_none()
1281            {
1282                return child.kind.is_dir();
1283            }
1284        };
1285        false
1286    }
1287
1288    fn is_foldable(&self, entry: &Entry, worktree: &Worktree) -> bool {
1289        if entry.is_dir() {
1290            let snapshot = worktree.snapshot();
1291
1292            let mut child_entries = snapshot.child_entries(&entry.path);
1293            if let Some(child) = child_entries.next()
1294                && child_entries.next().is_none()
1295            {
1296                return child.kind.is_dir();
1297            }
1298        }
1299        false
1300    }
1301
1302    fn expand_selected_entry(
1303        &mut self,
1304        _: &ExpandSelectedEntry,
1305        window: &mut Window,
1306        cx: &mut Context<Self>,
1307    ) {
1308        if let Some((worktree, entry)) = self.selected_entry(cx) {
1309            if let Some(folded_ancestors) = self.state.ancestors.get_mut(&entry.id)
1310                && folded_ancestors.current_ancestor_depth > 0
1311            {
1312                folded_ancestors.current_ancestor_depth -= 1;
1313                cx.notify();
1314                return;
1315            }
1316            if entry.is_dir() {
1317                let worktree_id = worktree.id();
1318                let entry_id = entry.id;
1319                let expanded_dir_ids = if let Some(expanded_dir_ids) =
1320                    self.state.expanded_dir_ids.get_mut(&worktree_id)
1321                {
1322                    expanded_dir_ids
1323                } else {
1324                    return;
1325                };
1326
1327                match expanded_dir_ids.binary_search(&entry_id) {
1328                    Ok(_) => self.select_next(&SelectNext, window, cx),
1329                    Err(ix) => {
1330                        self.project.update(cx, |project, cx| {
1331                            project.expand_entry(worktree_id, entry_id, cx);
1332                        });
1333
1334                        expanded_dir_ids.insert(ix, entry_id);
1335                        self.update_visible_entries(None, false, false, window, cx);
1336                        cx.notify();
1337                    }
1338                }
1339            }
1340        }
1341    }
1342
1343    fn collapse_selected_entry(
1344        &mut self,
1345        _: &CollapseSelectedEntry,
1346        window: &mut Window,
1347        cx: &mut Context<Self>,
1348    ) {
1349        let Some((worktree, entry)) = self.selected_entry_handle(cx) else {
1350            return;
1351        };
1352        self.collapse_entry(entry.clone(), worktree, window, cx)
1353    }
1354
1355    fn collapse_entry(
1356        &mut self,
1357        entry: Entry,
1358        worktree: Entity<Worktree>,
1359        window: &mut Window,
1360        cx: &mut Context<Self>,
1361    ) {
1362        let worktree = worktree.read(cx);
1363        if let Some(folded_ancestors) = self.state.ancestors.get_mut(&entry.id)
1364            && folded_ancestors.current_ancestor_depth + 1 < folded_ancestors.max_ancestor_depth()
1365        {
1366            folded_ancestors.current_ancestor_depth += 1;
1367            cx.notify();
1368            return;
1369        }
1370        let worktree_id = worktree.id();
1371        let expanded_dir_ids =
1372            if let Some(expanded_dir_ids) = self.state.expanded_dir_ids.get_mut(&worktree_id) {
1373                expanded_dir_ids
1374            } else {
1375                return;
1376            };
1377
1378        let mut entry = &entry;
1379        loop {
1380            let entry_id = entry.id;
1381            match expanded_dir_ids.binary_search(&entry_id) {
1382                Ok(ix) => {
1383                    expanded_dir_ids.remove(ix);
1384                    self.update_visible_entries(
1385                        Some((worktree_id, entry_id)),
1386                        false,
1387                        false,
1388                        window,
1389                        cx,
1390                    );
1391                    cx.notify();
1392                    break;
1393                }
1394                Err(_) => {
1395                    if let Some(parent_entry) =
1396                        entry.path.parent().and_then(|p| worktree.entry_for_path(p))
1397                    {
1398                        entry = parent_entry;
1399                    } else {
1400                        break;
1401                    }
1402                }
1403            }
1404        }
1405    }
1406
1407    fn collapse_selected_entry_and_children(
1408        &mut self,
1409        _: &CollapseSelectedEntryAndChildren,
1410        window: &mut Window,
1411        cx: &mut Context<Self>,
1412    ) {
1413        if let Some((worktree, entry)) = self.selected_entry(cx) {
1414            let worktree_id = worktree.id();
1415            let entry_id = entry.id;
1416
1417            self.collapse_all_for_entry(worktree_id, entry_id, cx);
1418
1419            self.update_visible_entries(Some((worktree_id, entry_id)), false, false, window, cx);
1420            cx.notify();
1421        }
1422    }
1423
1424    /// Handles "Collapse All" from the context menu when a root directory is selected.
1425    /// With a single visible worktree, keeps the root expanded (matching CollapseAllEntries behavior).
1426    /// With multiple visible worktrees, collapses the root and all its children.
1427    fn collapse_all_for_root(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1428        let Some((worktree, entry)) = self.selected_entry(cx) else {
1429            return;
1430        };
1431
1432        let is_root = worktree.root_entry().map(|e| e.id) == Some(entry.id);
1433        if !is_root {
1434            return;
1435        }
1436
1437        let worktree_id = worktree.id();
1438        let root_id = entry.id;
1439
1440        if let Some(expanded_dir_ids) = self.state.expanded_dir_ids.get_mut(&worktree_id) {
1441            if self.project.read(cx).visible_worktrees(cx).count() == 1 {
1442                expanded_dir_ids.retain(|id| id == &root_id);
1443            } else {
1444                expanded_dir_ids.clear();
1445            }
1446        }
1447
1448        self.update_visible_entries(Some((worktree_id, root_id)), false, false, window, cx);
1449        cx.notify();
1450    }
1451
1452    fn collapse_all_entries(
1453        &mut self,
1454        _: &CollapseAllEntries,
1455        window: &mut Window,
1456        cx: &mut Context<Self>,
1457    ) {
1458        // By keeping entries for fully collapsed worktrees, we avoid expanding them within update_visible_entries
1459        // (which is it's default behavior when there's no entry for a worktree in expanded_dir_ids).
1460        let multiple_worktrees = self.project.read(cx).visible_worktrees(cx).count() > 1;
1461        let project = self.project.read(cx);
1462
1463        self.state
1464            .expanded_dir_ids
1465            .iter_mut()
1466            .for_each(|(worktree_id, expanded_entries)| {
1467                if multiple_worktrees {
1468                    *expanded_entries = Default::default();
1469                    return;
1470                }
1471
1472                let root_entry_id = project
1473                    .worktree_for_id(*worktree_id, cx)
1474                    .map(|worktree| worktree.read(cx).snapshot())
1475                    .and_then(|worktree_snapshot| {
1476                        worktree_snapshot.root_entry().map(|entry| entry.id)
1477                    });
1478
1479                match root_entry_id {
1480                    Some(id) => {
1481                        expanded_entries.retain(|entry_id| entry_id == &id);
1482                    }
1483                    None => *expanded_entries = Default::default(),
1484                };
1485            });
1486
1487        self.update_visible_entries(None, false, false, window, cx);
1488        cx.notify();
1489    }
1490
1491    fn toggle_expanded(
1492        &mut self,
1493        entry_id: ProjectEntryId,
1494        window: &mut Window,
1495        cx: &mut Context<Self>,
1496    ) {
1497        if let Some(worktree_id) = self.project.read(cx).worktree_id_for_entry(entry_id, cx)
1498            && let Some(expanded_dir_ids) = self.state.expanded_dir_ids.get_mut(&worktree_id)
1499        {
1500            self.project.update(cx, |project, cx| {
1501                match expanded_dir_ids.binary_search(&entry_id) {
1502                    Ok(ix) => {
1503                        expanded_dir_ids.remove(ix);
1504                    }
1505                    Err(ix) => {
1506                        project.expand_entry(worktree_id, entry_id, cx);
1507                        expanded_dir_ids.insert(ix, entry_id);
1508                    }
1509                }
1510            });
1511            self.update_visible_entries(Some((worktree_id, entry_id)), false, false, window, cx);
1512            window.focus(&self.focus_handle, cx);
1513            cx.notify();
1514        }
1515    }
1516
1517    fn toggle_expand_all(
1518        &mut self,
1519        entry_id: ProjectEntryId,
1520        window: &mut Window,
1521        cx: &mut Context<Self>,
1522    ) {
1523        if let Some(worktree_id) = self.project.read(cx).worktree_id_for_entry(entry_id, cx)
1524            && let Some(expanded_dir_ids) = self.state.expanded_dir_ids.get_mut(&worktree_id)
1525        {
1526            match expanded_dir_ids.binary_search(&entry_id) {
1527                Ok(_ix) => {
1528                    self.collapse_all_for_entry(worktree_id, entry_id, cx);
1529                }
1530                Err(_ix) => {
1531                    self.expand_all_for_entry(worktree_id, entry_id, cx);
1532                }
1533            }
1534            self.update_visible_entries(Some((worktree_id, entry_id)), false, false, window, cx);
1535            window.focus(&self.focus_handle, cx);
1536            cx.notify();
1537        }
1538    }
1539
1540    fn expand_all_for_entry(
1541        &mut self,
1542        worktree_id: WorktreeId,
1543        entry_id: ProjectEntryId,
1544        cx: &mut Context<Self>,
1545    ) {
1546        self.project.update(cx, |project, cx| {
1547            if let Some((worktree, expanded_dir_ids)) = project
1548                .worktree_for_id(worktree_id, cx)
1549                .zip(self.state.expanded_dir_ids.get_mut(&worktree_id))
1550            {
1551                if let Some(task) = project.expand_all_for_entry(worktree_id, entry_id, cx) {
1552                    task.detach();
1553                }
1554
1555                let worktree = worktree.read(cx);
1556
1557                if let Some(mut entry) = worktree.entry_for_id(entry_id) {
1558                    loop {
1559                        if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) {
1560                            expanded_dir_ids.insert(ix, entry.id);
1561                        }
1562
1563                        if let Some(parent_entry) =
1564                            entry.path.parent().and_then(|p| worktree.entry_for_path(p))
1565                        {
1566                            entry = parent_entry;
1567                        } else {
1568                            break;
1569                        }
1570                    }
1571                }
1572            }
1573        });
1574    }
1575
1576    fn collapse_all_for_entry(
1577        &mut self,
1578        worktree_id: WorktreeId,
1579        entry_id: ProjectEntryId,
1580        cx: &mut Context<Self>,
1581    ) {
1582        self.project.update(cx, |project, cx| {
1583            if let Some((worktree, expanded_dir_ids)) = project
1584                .worktree_for_id(worktree_id, cx)
1585                .zip(self.state.expanded_dir_ids.get_mut(&worktree_id))
1586            {
1587                let worktree = worktree.read(cx);
1588                let mut dirs_to_collapse = vec![entry_id];
1589                let auto_fold_enabled = ProjectPanelSettings::get_global(cx).auto_fold_dirs;
1590                while let Some(current_id) = dirs_to_collapse.pop() {
1591                    let Some(current_entry) = worktree.entry_for_id(current_id) else {
1592                        continue;
1593                    };
1594                    if let Ok(ix) = expanded_dir_ids.binary_search(&current_id) {
1595                        expanded_dir_ids.remove(ix);
1596                    }
1597                    if auto_fold_enabled {
1598                        self.state.unfolded_dir_ids.remove(&current_id);
1599                    }
1600                    for child in worktree.child_entries(&current_entry.path) {
1601                        if child.is_dir() {
1602                            dirs_to_collapse.push(child.id);
1603                        }
1604                    }
1605                }
1606            }
1607        });
1608    }
1609
1610    fn select_previous(&mut self, _: &SelectPrevious, window: &mut Window, cx: &mut Context<Self>) {
1611        if let Some(edit_state) = &self.state.edit_state
1612            && edit_state.processing_filename.is_none()
1613        {
1614            self.filename_editor.update(cx, |editor, cx| {
1615                editor.move_to_beginning_of_line(
1616                    &editor::actions::MoveToBeginningOfLine {
1617                        stop_at_soft_wraps: false,
1618                        stop_at_indent: false,
1619                    },
1620                    window,
1621                    cx,
1622                );
1623            });
1624            return;
1625        }
1626        if let Some(selection) = self.selection {
1627            let (mut worktree_ix, mut entry_ix, _) =
1628                self.index_for_selection(selection).unwrap_or_default();
1629            if entry_ix > 0 {
1630                entry_ix -= 1;
1631            } else if worktree_ix > 0 {
1632                worktree_ix -= 1;
1633                entry_ix = self.state.visible_entries[worktree_ix].entries.len() - 1;
1634            } else {
1635                return;
1636            }
1637
1638            let VisibleEntriesForWorktree {
1639                worktree_id,
1640                entries,
1641                ..
1642            } = &self.state.visible_entries[worktree_ix];
1643            let selection = SelectedEntry {
1644                worktree_id: *worktree_id,
1645                entry_id: entries[entry_ix].id,
1646            };
1647            self.selection = Some(selection);
1648            if window.modifiers().shift {
1649                self.marked_entries.push(selection);
1650            }
1651            self.autoscroll(cx);
1652            cx.notify();
1653        } else {
1654            self.select_first(&SelectFirst {}, window, cx);
1655        }
1656    }
1657
1658    fn confirm(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
1659        if let Some(task) = self.confirm_edit(true, window, cx) {
1660            task.detach_and_notify_err(self.workspace.clone(), window, cx);
1661        }
1662    }
1663
1664    fn open(&mut self, _: &Open, window: &mut Window, cx: &mut Context<Self>) {
1665        let preview_tabs_enabled =
1666            PreviewTabsSettings::get_global(cx).enable_preview_from_project_panel;
1667        self.open_internal(true, !preview_tabs_enabled, None, window, cx);
1668    }
1669
1670    fn open_permanent(&mut self, _: &OpenPermanent, window: &mut Window, cx: &mut Context<Self>) {
1671        self.open_internal(false, true, None, window, cx);
1672    }
1673
1674    fn open_split_vertical(
1675        &mut self,
1676        _: &OpenSplitVertical,
1677        window: &mut Window,
1678        cx: &mut Context<Self>,
1679    ) {
1680        self.open_internal(false, true, Some(SplitDirection::vertical(cx)), window, cx);
1681    }
1682
1683    fn open_split_horizontal(
1684        &mut self,
1685        _: &OpenSplitHorizontal,
1686        window: &mut Window,
1687        cx: &mut Context<Self>,
1688    ) {
1689        self.open_internal(
1690            false,
1691            true,
1692            Some(SplitDirection::horizontal(cx)),
1693            window,
1694            cx,
1695        );
1696    }
1697
1698    fn open_internal(
1699        &mut self,
1700        allow_preview: bool,
1701        focus_opened_item: bool,
1702        split_direction: Option<SplitDirection>,
1703        window: &mut Window,
1704        cx: &mut Context<Self>,
1705    ) {
1706        if let Some((_, entry)) = self.selected_entry(cx) {
1707            if entry.is_file() {
1708                if split_direction.is_some() {
1709                    self.split_entry(entry.id, allow_preview, split_direction, cx);
1710                } else {
1711                    self.open_entry(entry.id, focus_opened_item, allow_preview, cx);
1712                }
1713                cx.notify();
1714            } else {
1715                self.toggle_expanded(entry.id, window, cx);
1716            }
1717        }
1718    }
1719
1720    fn populate_validation_error(&mut self, cx: &mut Context<Self>) {
1721        let edit_state = match self.state.edit_state.as_mut() {
1722            Some(state) => state,
1723            None => return,
1724        };
1725        let filename = self.filename_editor.read(cx).text(cx);
1726        if !filename.is_empty() {
1727            if filename.is_empty() {
1728                edit_state.validation_state =
1729                    ValidationState::Error("File or directory name cannot be empty.".to_string());
1730                cx.notify();
1731                return;
1732            }
1733
1734            let trimmed_filename = filename.trim();
1735            if trimmed_filename != filename {
1736                edit_state.validation_state = ValidationState::Warning(
1737                    "File or directory name contains leading or trailing whitespace.".to_string(),
1738                );
1739                cx.notify();
1740                return;
1741            }
1742            let trimmed_filename = trimmed_filename.trim_start_matches('/');
1743
1744            let Ok(filename) = RelPath::unix(trimmed_filename) else {
1745                edit_state.validation_state = ValidationState::Warning(
1746                    "File or directory name contains leading or trailing whitespace.".to_string(),
1747                );
1748                cx.notify();
1749                return;
1750            };
1751
1752            if let Some(worktree) = self
1753                .project
1754                .read(cx)
1755                .worktree_for_id(edit_state.worktree_id, cx)
1756                && let Some(entry) = worktree.read(cx).entry_for_id(edit_state.entry_id)
1757            {
1758                let mut already_exists = false;
1759                if edit_state.is_new_entry() {
1760                    let new_path = entry.path.join(filename);
1761                    if worktree.read(cx).entry_for_path(&new_path).is_some() {
1762                        already_exists = true;
1763                    }
1764                } else {
1765                    let new_path = if let Some(parent) = entry.path.clone().parent() {
1766                        parent.join(&filename)
1767                    } else {
1768                        filename.into()
1769                    };
1770                    if let Some(existing) = worktree.read(cx).entry_for_path(&new_path)
1771                        && existing.id != entry.id
1772                    {
1773                        already_exists = true;
1774                    }
1775                };
1776                if already_exists {
1777                    edit_state.validation_state = ValidationState::Error(format!(
1778                        "File or directory '{}' already exists at location. Please choose a different name.",
1779                        filename.as_unix_str()
1780                    ));
1781                    cx.notify();
1782                    return;
1783                }
1784            }
1785        }
1786        edit_state.validation_state = ValidationState::None;
1787        cx.notify();
1788    }
1789
1790    fn confirm_edit(
1791        &mut self,
1792        refocus: bool,
1793        window: &mut Window,
1794        cx: &mut Context<Self>,
1795    ) -> Option<Task<Result<()>>> {
1796        let edit_state = self.state.edit_state.as_mut()?;
1797        let worktree_id = edit_state.worktree_id;
1798        let is_new_entry = edit_state.is_new_entry();
1799        let mut filename = self.filename_editor.read(cx).text(cx);
1800        let path_style = self.project.read(cx).path_style(cx);
1801        if path_style.is_windows() {
1802            // on windows, trailing dots are ignored in paths
1803            // this can cause project panel to create a new entry with a trailing dot
1804            // while the actual one without the dot gets populated by the file watcher
1805            while let Some(trimmed) = filename.strip_suffix('.') {
1806                filename = trimmed.to_string();
1807            }
1808        }
1809        if filename.trim().is_empty() {
1810            return None;
1811        }
1812
1813        let filename_indicates_dir = if path_style.is_windows() {
1814            filename.ends_with('/') || filename.ends_with('\\')
1815        } else {
1816            filename.ends_with('/')
1817        };
1818        let filename = if path_style.is_windows() {
1819            filename.trim_start_matches(&['/', '\\'])
1820        } else {
1821            filename.trim_start_matches('/')
1822        };
1823        let filename = RelPath::new(filename.as_ref(), path_style).ok()?.into_arc();
1824
1825        edit_state.is_dir =
1826            edit_state.is_dir || (edit_state.is_new_entry() && filename_indicates_dir);
1827        let is_dir = edit_state.is_dir;
1828        let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?;
1829        let entry = worktree.read(cx).entry_for_id(edit_state.entry_id)?.clone();
1830
1831        let edit_task;
1832        let edited_entry_id;
1833        let edited_entry;
1834        let new_project_path: ProjectPath;
1835        if is_new_entry {
1836            self.selection = Some(SelectedEntry {
1837                worktree_id,
1838                entry_id: NEW_ENTRY_ID,
1839            });
1840            let new_path = entry.path.join(&filename);
1841            if worktree.read(cx).entry_for_path(&new_path).is_some() {
1842                return None;
1843            }
1844
1845            edited_entry = None;
1846            edited_entry_id = NEW_ENTRY_ID;
1847            new_project_path = (worktree_id, new_path).into();
1848            edit_task = self.project.update(cx, |project, cx| {
1849                project.create_entry(new_project_path.clone(), is_dir, cx)
1850            });
1851        } else {
1852            let new_path = if let Some(parent) = entry.path.parent() {
1853                parent.join(&filename)
1854            } else {
1855                filename.clone()
1856            };
1857            if let Some(existing) = worktree.read(cx).entry_for_path(&new_path) {
1858                if existing.id == entry.id && refocus {
1859                    window.focus(&self.focus_handle, cx);
1860                }
1861                return None;
1862            }
1863            edited_entry_id = entry.id;
1864            edited_entry = Some(entry);
1865            new_project_path = (worktree_id, new_path).into();
1866            edit_task = self.project.update(cx, |project, cx| {
1867                project.rename_entry(edited_entry_id, new_project_path.clone(), cx)
1868            })
1869        };
1870
1871        if refocus {
1872            window.focus(&self.focus_handle, cx);
1873        }
1874        edit_state.processing_filename = Some(filename);
1875        cx.notify();
1876
1877        Some(cx.spawn_in(window, async move |project_panel, cx| {
1878            let new_entry = edit_task.await;
1879            project_panel.update(cx, |project_panel, cx| {
1880                project_panel.state.edit_state = None;
1881
1882                // Record the operation if the edit was applied
1883                if new_entry.is_ok() {
1884                    let operation = if let Some(old_entry) = edited_entry {
1885                        Change::Renamed((worktree_id, old_entry.path).into(), new_project_path)
1886                    } else {
1887                        Change::Created(new_project_path)
1888                    };
1889
1890                    project_panel.undo_manager.record([operation]).log_err();
1891                }
1892
1893                cx.notify();
1894            })?;
1895
1896            match new_entry {
1897                Err(e) => {
1898                    project_panel
1899                        .update_in(cx, |project_panel, window, cx| {
1900                            project_panel.marked_entries.clear();
1901                            project_panel.update_visible_entries(None, false, false, window, cx);
1902                        })
1903                        .ok();
1904                    Err(e)?;
1905                }
1906                Ok(CreatedEntry::Included(new_entry)) => {
1907                    project_panel.update_in(cx, |project_panel, window, cx| {
1908                        if let Some(selection) = &mut project_panel.selection
1909                            && selection.entry_id == edited_entry_id
1910                        {
1911                            selection.worktree_id = worktree_id;
1912                            selection.entry_id = new_entry.id;
1913                            project_panel.marked_entries.clear();
1914                            project_panel.expand_to_selection(cx);
1915                        }
1916                        project_panel.update_visible_entries(None, false, false, window, cx);
1917                        if is_new_entry && !is_dir {
1918                            let settings = ProjectPanelSettings::get_global(cx);
1919                            if settings.auto_open.should_open_on_create() {
1920                                project_panel.open_entry(new_entry.id, true, false, cx);
1921                            }
1922                        }
1923                        cx.notify();
1924                    })?;
1925                }
1926                Ok(CreatedEntry::Excluded { abs_path }) => {
1927                    if let Some(open_task) = project_panel
1928                        .update_in(cx, |project_panel, window, cx| {
1929                            project_panel.marked_entries.clear();
1930                            project_panel.update_visible_entries(None, false, false, window, cx);
1931
1932                            if is_dir {
1933                                project_panel.project.update(cx, |_, cx| {
1934                                    cx.emit(project::Event::Toast {
1935                                        notification_id: "excluded-directory".into(),
1936                                        message: format!(
1937                                            concat!(
1938                                                "Created an excluded directory at {:?}.\n",
1939                                                "Alter `file_scan_exclusions` in the settings ",
1940                                                "to show it in the panel"
1941                                            ),
1942                                            abs_path
1943                                        ),
1944                                        link: None,
1945                                    })
1946                                });
1947                                None
1948                            } else {
1949                                project_panel
1950                                    .workspace
1951                                    .update(cx, |workspace, cx| {
1952                                        workspace.open_abs_path(
1953                                            abs_path,
1954                                            OpenOptions {
1955                                                visible: Some(OpenVisible::All),
1956                                                ..Default::default()
1957                                            },
1958                                            window,
1959                                            cx,
1960                                        )
1961                                    })
1962                                    .ok()
1963                            }
1964                        })
1965                        .ok()
1966                        .flatten()
1967                    {
1968                        let _ = open_task.await?;
1969                    }
1970                }
1971            }
1972            Ok(())
1973        }))
1974    }
1975
1976    fn discard_edit_state(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1977        if let Some(edit_state) = self.state.edit_state.take() {
1978            self.state.temporarily_unfolded_pending_state = edit_state
1979                .temporarily_unfolded
1980                .and_then(|temporarily_unfolded_entry_id| {
1981                    let previously_focused_leaf_entry = edit_state.previously_focused?;
1982                    let folded_ancestors =
1983                        self.state.ancestors.get(&temporarily_unfolded_entry_id)?;
1984                    Some(TemporaryUnfoldedPendingState {
1985                        previously_focused_leaf_entry,
1986                        temporarily_unfolded_active_entry_id: folded_ancestors
1987                            .active_ancestor()
1988                            .unwrap_or(temporarily_unfolded_entry_id),
1989                    })
1990                });
1991            let previously_focused = edit_state
1992                .previously_focused
1993                .map(|entry| (entry.worktree_id, entry.entry_id));
1994            self.update_visible_entries(
1995                previously_focused,
1996                false,
1997                previously_focused.is_some(),
1998                window,
1999                cx,
2000            );
2001        }
2002    }
2003
2004    fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) {
2005        if cx.stop_active_drag(window) {
2006            self.drag_target_entry.take();
2007            self.hover_expand_task.take();
2008            return;
2009        }
2010        self.marked_entries.clear();
2011        cx.notify();
2012        self.discard_edit_state(window, cx);
2013        window.focus(&self.focus_handle, cx);
2014    }
2015
2016    fn open_entry(
2017        &mut self,
2018        entry_id: ProjectEntryId,
2019        focus_opened_item: bool,
2020        allow_preview: bool,
2021
2022        cx: &mut Context<Self>,
2023    ) {
2024        cx.emit(Event::OpenedEntry {
2025            entry_id,
2026            focus_opened_item,
2027            allow_preview,
2028        });
2029    }
2030
2031    fn split_entry(
2032        &mut self,
2033        entry_id: ProjectEntryId,
2034        allow_preview: bool,
2035        split_direction: Option<SplitDirection>,
2036
2037        cx: &mut Context<Self>,
2038    ) {
2039        cx.emit(Event::SplitEntry {
2040            entry_id,
2041            allow_preview,
2042            split_direction,
2043        });
2044    }
2045
2046    fn new_file(&mut self, _: &NewFile, window: &mut Window, cx: &mut Context<Self>) {
2047        self.add_entry(false, window, cx)
2048    }
2049
2050    fn new_directory(&mut self, _: &NewDirectory, window: &mut Window, cx: &mut Context<Self>) {
2051        self.add_entry(true, window, cx)
2052    }
2053
2054    fn add_entry(&mut self, is_dir: bool, window: &mut Window, cx: &mut Context<Self>) {
2055        let Some((worktree_id, entry_id)) = self
2056            .selection
2057            .map(|entry| (entry.worktree_id, entry.entry_id))
2058            .or_else(|| {
2059                let entry_id = self.state.last_worktree_root_id?;
2060                let worktree_id = self
2061                    .project
2062                    .read(cx)
2063                    .worktree_for_entry(entry_id, cx)?
2064                    .read(cx)
2065                    .id();
2066
2067                self.selection = Some(SelectedEntry {
2068                    worktree_id,
2069                    entry_id,
2070                });
2071
2072                Some((worktree_id, entry_id))
2073            })
2074        else {
2075            return;
2076        };
2077
2078        let directory_id;
2079        let new_entry_id = self.resolve_entry(entry_id);
2080        if let Some((worktree, expanded_dir_ids)) = self
2081            .project
2082            .read(cx)
2083            .worktree_for_id(worktree_id, cx)
2084            .zip(self.state.expanded_dir_ids.get_mut(&worktree_id))
2085        {
2086            let worktree = worktree.read(cx);
2087            if let Some(mut entry) = worktree.entry_for_id(new_entry_id) {
2088                loop {
2089                    if entry.is_dir() {
2090                        if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) {
2091                            expanded_dir_ids.insert(ix, entry.id);
2092                        }
2093                        directory_id = entry.id;
2094                        break;
2095                    } else {
2096                        if let Some(parent_path) = entry.path.parent()
2097                            && let Some(parent_entry) = worktree.entry_for_path(parent_path)
2098                        {
2099                            entry = parent_entry;
2100                            continue;
2101                        }
2102                        return;
2103                    }
2104                }
2105            } else {
2106                return;
2107            };
2108        } else {
2109            return;
2110        };
2111
2112        self.marked_entries.clear();
2113        self.state.edit_state = Some(EditState {
2114            worktree_id,
2115            entry_id: directory_id,
2116            leaf_entry_id: None,
2117            is_dir,
2118            processing_filename: None,
2119            previously_focused: self.selection,
2120            depth: 0,
2121            validation_state: ValidationState::None,
2122            temporarily_unfolded: (new_entry_id != entry_id).then_some(new_entry_id),
2123        });
2124        self.filename_editor.update(cx, |editor, cx| {
2125            editor.clear(window, cx);
2126        });
2127        self.update_visible_entries(Some((worktree_id, NEW_ENTRY_ID)), true, true, window, cx);
2128        cx.notify();
2129    }
2130
2131    fn unflatten_entry_id(&self, leaf_entry_id: ProjectEntryId) -> ProjectEntryId {
2132        if let Some(ancestors) = self.state.ancestors.get(&leaf_entry_id) {
2133            ancestors
2134                .ancestors
2135                .get(ancestors.current_ancestor_depth)
2136                .copied()
2137                .unwrap_or(leaf_entry_id)
2138        } else {
2139            leaf_entry_id
2140        }
2141    }
2142
2143    pub fn undo(&mut self, _: &Undo, _window: &mut Window, _cx: &mut Context<Self>) {
2144        self.undo_manager.undo().log_err();
2145    }
2146
2147    pub fn redo(&mut self, _: &Redo, _window: &mut Window, _cx: &mut Context<Self>) {
2148        self.undo_manager.redo().log_err();
2149    }
2150
2151    fn rename_impl(
2152        &mut self,
2153        selection: Option<Range<usize>>,
2154        window: &mut Window,
2155        cx: &mut Context<Self>,
2156    ) {
2157        if let Some(SelectedEntry {
2158            worktree_id,
2159            entry_id,
2160        }) = self.selection
2161            && let Some(worktree) = self.project.read(cx).worktree_for_id(worktree_id, cx)
2162        {
2163            let sub_entry_id = self.unflatten_entry_id(entry_id);
2164            if let Some(entry) = worktree.read(cx).entry_for_id(sub_entry_id) {
2165                #[cfg(target_os = "windows")]
2166                if Some(entry) == worktree.read(cx).root_entry() {
2167                    return;
2168                }
2169
2170                if Some(entry) == worktree.read(cx).root_entry() {
2171                    let settings = ProjectPanelSettings::get_global(cx);
2172                    let visible_worktrees_count =
2173                        self.project.read(cx).visible_worktrees(cx).count();
2174                    if settings.hide_root && visible_worktrees_count == 1 {
2175                        return;
2176                    }
2177                }
2178
2179                self.state.edit_state = Some(EditState {
2180                    worktree_id,
2181                    entry_id: sub_entry_id,
2182                    leaf_entry_id: Some(entry_id),
2183                    is_dir: entry.is_dir(),
2184                    processing_filename: None,
2185                    previously_focused: None,
2186                    depth: 0,
2187                    validation_state: ValidationState::None,
2188                    temporarily_unfolded: None,
2189                });
2190                let file_name = entry.path.file_name().unwrap_or_default().to_string();
2191                let selection = selection.unwrap_or_else(|| {
2192                    let file_stem = entry.path.file_stem().map(|s| s.to_string());
2193                    let selection_end =
2194                        file_stem.map_or(file_name.len(), |file_stem| file_stem.len());
2195                    0..selection_end
2196                });
2197                self.filename_editor.update(cx, |editor, cx| {
2198                    editor.set_text(file_name, window, cx);
2199                    editor.change_selections(Default::default(), window, cx, |s| {
2200                        s.select_ranges([
2201                            MultiBufferOffset(selection.start)..MultiBufferOffset(selection.end)
2202                        ])
2203                    });
2204                });
2205                self.update_visible_entries(None, true, true, window, cx);
2206                cx.notify();
2207            }
2208        }
2209    }
2210
2211    fn rename(&mut self, _: &Rename, window: &mut Window, cx: &mut Context<Self>) {
2212        self.rename_impl(None, window, cx);
2213    }
2214
2215    fn trash(&mut self, action: &Trash, window: &mut Window, cx: &mut Context<Self>) {
2216        self.remove(true, action.skip_prompt, window, cx);
2217    }
2218
2219    fn delete(&mut self, action: &Delete, window: &mut Window, cx: &mut Context<Self>) {
2220        self.remove(false, action.skip_prompt, window, cx);
2221    }
2222
2223    fn restore_file(
2224        &mut self,
2225        action: &git::RestoreFile,
2226        window: &mut Window,
2227        cx: &mut Context<Self>,
2228    ) {
2229        maybe!({
2230            let selection = self.selection?;
2231            let project = self.project.read(cx);
2232
2233            let (_worktree, entry) = self.selected_sub_entry(cx)?;
2234            if entry.is_dir() {
2235                return None;
2236            }
2237
2238            let project_path = project.path_for_entry(selection.entry_id, cx)?;
2239
2240            let git_store = project.git_store();
2241            let (repository, repo_path) = git_store
2242                .read(cx)
2243                .repository_and_path_for_project_path(&project_path, cx)?;
2244
2245            let snapshot = repository.read(cx).snapshot();
2246            let status = snapshot.status_for_path(&repo_path)?;
2247            if !status.status.is_modified() && !status.status.is_deleted() {
2248                return None;
2249            }
2250
2251            let file_name = entry.path.file_name()?.to_string();
2252
2253            let answer = if !action.skip_prompt {
2254                let prompt = format!("Discard changes to {}?", file_name);
2255                Some(window.prompt(PromptLevel::Info, &prompt, None, &["Restore", "Cancel"], cx))
2256            } else {
2257                None
2258            };
2259
2260            cx.spawn_in(window, async move |panel, cx| {
2261                if let Some(answer) = answer
2262                    && answer.await != Ok(0)
2263                {
2264                    return anyhow::Ok(());
2265                }
2266
2267                let task = panel.update(cx, |_panel, cx| {
2268                    repository.update(cx, |repo, cx| {
2269                        repo.checkout_files("HEAD", vec![repo_path], cx)
2270                    })
2271                })?;
2272
2273                if let Err(e) = task.await {
2274                    panel
2275                        .update(cx, |panel, cx| {
2276                            let message = format!("Failed to restore {}: {}", file_name, e);
2277                            let toast = StatusToast::new(message, cx, |this, _| {
2278                                this.icon(
2279                                    Icon::new(IconName::XCircle)
2280                                        .size(IconSize::Small)
2281                                        .color(Color::Error),
2282                                )
2283                                .dismiss_button(true)
2284                            });
2285                            panel
2286                                .workspace
2287                                .update(cx, |workspace, cx| {
2288                                    workspace.toggle_status_toast(toast, cx);
2289                                })
2290                                .ok();
2291                        })
2292                        .ok();
2293                }
2294
2295                panel
2296                    .update(cx, |panel, cx| {
2297                        panel.project.update(cx, |project, cx| {
2298                            if let Some(buffer_id) = project
2299                                .buffer_store()
2300                                .read(cx)
2301                                .buffer_id_for_project_path(&project_path)
2302                            {
2303                                if let Some(buffer) = project.buffer_for_id(*buffer_id, cx) {
2304                                    buffer.update(cx, |buffer, cx| {
2305                                        let _ = buffer.reload(cx);
2306                                    });
2307                                }
2308                            }
2309                        })
2310                    })
2311                    .ok();
2312
2313                anyhow::Ok(())
2314            })
2315            .detach_and_log_err(cx);
2316
2317            Some(())
2318        });
2319    }
2320
2321    fn remove(
2322        &mut self,
2323        trash: bool,
2324        skip_prompt: bool,
2325        window: &mut Window,
2326        cx: &mut Context<ProjectPanel>,
2327    ) {
2328        maybe!({
2329            let items_to_delete = self.disjoint_effective_entries(cx);
2330            if items_to_delete.is_empty() {
2331                return None;
2332            }
2333            let project = self.project.read(cx);
2334
2335            let mut dirty_buffers = 0;
2336            let file_paths = items_to_delete
2337                .iter()
2338                .filter_map(|selection| {
2339                    let project_path = project.path_for_entry(selection.entry_id, cx)?;
2340                    dirty_buffers +=
2341                        project.dirty_buffers(cx).any(|path| path == project_path) as usize;
2342
2343                    Some((
2344                        selection.entry_id,
2345                        selection.worktree_id,
2346                        project_path.path.file_name()?.to_string(),
2347                    ))
2348                })
2349                .collect::<Vec<_>>();
2350            if file_paths.is_empty() {
2351                return None;
2352            }
2353            let answer = if !skip_prompt {
2354                let operation = if trash { "Trash" } else { "Delete" };
2355                let message_start = if trash {
2356                    "Do you want to trash"
2357                } else {
2358                    "Are you sure you want to permanently delete"
2359                };
2360                let prompt = match file_paths.first() {
2361                    Some((_, _, path)) if file_paths.len() == 1 => {
2362                        let unsaved_warning = if dirty_buffers > 0 {
2363                            "\n\nIt has unsaved changes, which will be lost."
2364                        } else {
2365                            ""
2366                        };
2367
2368                        format!("{message_start} {path}?{unsaved_warning}")
2369                    }
2370                    _ => {
2371                        const CUTOFF_POINT: usize = 10;
2372                        let names = if file_paths.len() > CUTOFF_POINT {
2373                            let truncated_path_counts = file_paths.len() - CUTOFF_POINT;
2374                            let mut paths = file_paths
2375                                .iter()
2376                                .map(|(_, _, path)| path.clone())
2377                                .take(CUTOFF_POINT)
2378                                .collect::<Vec<_>>();
2379                            paths.truncate(CUTOFF_POINT);
2380                            if truncated_path_counts == 1 {
2381                                paths.push(".. 1 file not shown".into());
2382                            } else {
2383                                paths.push(format!(".. {} files not shown", truncated_path_counts));
2384                            }
2385                            paths
2386                        } else {
2387                            file_paths.iter().map(|(_, _, path)| path.clone()).collect()
2388                        };
2389                        let unsaved_warning = if dirty_buffers == 0 {
2390                            String::new()
2391                        } else if dirty_buffers == 1 {
2392                            "\n\n1 of these has unsaved changes, which will be lost.".to_string()
2393                        } else {
2394                            format!(
2395                                "\n\n{dirty_buffers} of these have unsaved changes, which will be lost."
2396                            )
2397                        };
2398
2399                        format!(
2400                            "{message_start} the following {} files?\n{}{unsaved_warning}",
2401                            file_paths.len(),
2402                            names.join("\n")
2403                        )
2404                    }
2405                };
2406                let detail = (!trash).then_some("This cannot be undone.");
2407                Some(window.prompt(
2408                    PromptLevel::Info,
2409                    &prompt,
2410                    detail,
2411                    &[operation, "Cancel"],
2412                    cx,
2413                ))
2414            } else {
2415                None
2416            };
2417            let next_selection = self.find_next_selection_after_deletion(items_to_delete, cx);
2418            cx.spawn_in(window, async move |panel, cx| {
2419                if let Some(answer) = answer
2420                    && answer.await != Ok(0)
2421                {
2422                    return anyhow::Ok(());
2423                }
2424
2425                let mut changes = Vec::new();
2426
2427                for (entry_id, worktree_id, _) in file_paths {
2428                    let trashed_entry = panel
2429                        .update(cx, |panel, cx| {
2430                            panel
2431                                .project
2432                                .update(cx, |project, cx| project.delete_entry(entry_id, trash, cx))
2433                                .context("no such entry")
2434                        })??
2435                        .await?;
2436
2437                    // Keep track of trashed change so that we can then record
2438                    // all of the changes at once, such that undoing and redoing
2439                    // restores or trashes all files in batch.
2440                    if trash && let Some(trashed_entry) = trashed_entry {
2441                        changes.push(Change::Trashed(worktree_id, trashed_entry));
2442                    }
2443                }
2444                panel.update_in(cx, |panel, window, cx| {
2445                    if trash {
2446                        panel.undo_manager.record(changes).log_err();
2447                    }
2448
2449                    if let Some(next_selection) = next_selection {
2450                        panel.update_visible_entries(
2451                            Some((next_selection.worktree_id, next_selection.entry_id)),
2452                            false,
2453                            true,
2454                            window,
2455                            cx,
2456                        );
2457                    } else {
2458                        panel.select_last(&SelectLast {}, window, cx);
2459                    }
2460                })?;
2461                Ok(())
2462            })
2463            .detach_and_log_err(cx);
2464            Some(())
2465        });
2466    }
2467
2468    fn find_next_selection_after_deletion(
2469        &self,
2470        sanitized_entries: BTreeSet<SelectedEntry>,
2471        cx: &mut Context<Self>,
2472    ) -> Option<SelectedEntry> {
2473        if sanitized_entries.is_empty() {
2474            return None;
2475        }
2476        let project = self.project.read(cx);
2477        let (worktree_id, worktree) = sanitized_entries
2478            .iter()
2479            .map(|entry| entry.worktree_id)
2480            .filter_map(|id| project.worktree_for_id(id, cx).map(|w| (id, w.read(cx))))
2481            .max_by(|(_, a), (_, b)| a.root_name().cmp(b.root_name()))?;
2482        let git_store = project.git_store().read(cx);
2483
2484        let marked_entries_in_worktree = sanitized_entries
2485            .iter()
2486            .filter(|e| e.worktree_id == worktree_id)
2487            .collect::<HashSet<_>>();
2488        let latest_entry = marked_entries_in_worktree
2489            .iter()
2490            .max_by(|a, b| {
2491                match (
2492                    worktree.entry_for_id(a.entry_id),
2493                    worktree.entry_for_id(b.entry_id),
2494                ) {
2495                    (Some(a), Some(b)) => compare_paths(
2496                        (a.path.as_std_path(), a.is_file()),
2497                        (b.path.as_std_path(), b.is_file()),
2498                    ),
2499                    _ => cmp::Ordering::Equal,
2500                }
2501            })
2502            .and_then(|e| worktree.entry_for_id(e.entry_id))?;
2503
2504        let parent_path = latest_entry.path.parent()?;
2505        let parent_entry = worktree.entry_for_path(parent_path)?;
2506
2507        // Remove all siblings that are being deleted except the last marked entry
2508        let repo_snapshots = git_store.repo_snapshots(cx);
2509        let worktree_snapshot = worktree.snapshot();
2510        let hide_gitignore = ProjectPanelSettings::get_global(cx).hide_gitignore;
2511        let mut siblings: Vec<_> =
2512            ChildEntriesGitIter::new(&repo_snapshots, &worktree_snapshot, parent_path)
2513                .filter(|sibling| {
2514                    (sibling.id == latest_entry.id)
2515                        || (!marked_entries_in_worktree.contains(&&SelectedEntry {
2516                            worktree_id,
2517                            entry_id: sibling.id,
2518                        }) && (!hide_gitignore || !sibling.is_ignored))
2519                })
2520                .map(|entry| entry.to_owned())
2521                .collect();
2522
2523        let sort_mode = ProjectPanelSettings::get_global(cx).sort_mode;
2524        let sort_order = ProjectPanelSettings::get_global(cx).sort_order;
2525        sort_worktree_entries(&mut siblings, sort_mode, sort_order);
2526        let sibling_entry_index = siblings
2527            .iter()
2528            .position(|sibling| sibling.id == latest_entry.id)?;
2529
2530        if let Some(next_sibling) = sibling_entry_index
2531            .checked_add(1)
2532            .and_then(|i| siblings.get(i))
2533        {
2534            return Some(SelectedEntry {
2535                worktree_id,
2536                entry_id: next_sibling.id,
2537            });
2538        }
2539        if let Some(prev_sibling) = sibling_entry_index
2540            .checked_sub(1)
2541            .and_then(|i| siblings.get(i))
2542        {
2543            return Some(SelectedEntry {
2544                worktree_id,
2545                entry_id: prev_sibling.id,
2546            });
2547        }
2548        // No neighbour sibling found, fall back to parent
2549        Some(SelectedEntry {
2550            worktree_id,
2551            entry_id: parent_entry.id,
2552        })
2553    }
2554
2555    fn unfold_directory(
2556        &mut self,
2557        _: &UnfoldDirectory,
2558        window: &mut Window,
2559        cx: &mut Context<Self>,
2560    ) {
2561        if let Some((worktree, entry)) = self.selected_entry(cx) {
2562            self.state.unfolded_dir_ids.insert(entry.id);
2563
2564            let snapshot = worktree.snapshot();
2565            let mut parent_path = entry.path.parent();
2566            while let Some(path) = parent_path {
2567                if let Some(parent_entry) = worktree.entry_for_path(path) {
2568                    let mut children_iter = snapshot.child_entries(path);
2569
2570                    if children_iter.by_ref().take(2).count() > 1 {
2571                        break;
2572                    }
2573
2574                    self.state.unfolded_dir_ids.insert(parent_entry.id);
2575                    parent_path = path.parent();
2576                } else {
2577                    break;
2578                }
2579            }
2580
2581            self.update_visible_entries(None, false, true, window, cx);
2582            cx.notify();
2583        }
2584    }
2585
2586    fn fold_directory(&mut self, _: &FoldDirectory, window: &mut Window, cx: &mut Context<Self>) {
2587        if let Some((worktree, entry)) = self.selected_entry(cx) {
2588            self.state.unfolded_dir_ids.remove(&entry.id);
2589
2590            let snapshot = worktree.snapshot();
2591            let mut path = &*entry.path;
2592            loop {
2593                let mut child_entries_iter = snapshot.child_entries(path);
2594                if let Some(child) = child_entries_iter.next() {
2595                    if child_entries_iter.next().is_none() && child.is_dir() {
2596                        self.state.unfolded_dir_ids.remove(&child.id);
2597                        path = &*child.path;
2598                    } else {
2599                        break;
2600                    }
2601                } else {
2602                    break;
2603                }
2604            }
2605
2606            self.update_visible_entries(None, false, true, window, cx);
2607            cx.notify();
2608        }
2609    }
2610
2611    fn scroll_up(&mut self, _: &ScrollUp, window: &mut Window, cx: &mut Context<Self>) {
2612        for _ in 0..self.rendered_entries_len / 2 {
2613            window.dispatch_action(SelectPrevious.boxed_clone(), cx);
2614        }
2615    }
2616
2617    fn scroll_down(&mut self, _: &ScrollDown, window: &mut Window, cx: &mut Context<Self>) {
2618        for _ in 0..self.rendered_entries_len / 2 {
2619            window.dispatch_action(SelectNext.boxed_clone(), cx);
2620        }
2621    }
2622
2623    fn scroll_cursor_center(
2624        &mut self,
2625        _: &ScrollCursorCenter,
2626        _: &mut Window,
2627        cx: &mut Context<Self>,
2628    ) {
2629        if let Some((_, _, index)) = self.selection.and_then(|s| self.index_for_selection(s)) {
2630            self.scroll_handle
2631                .scroll_to_item_strict(index, ScrollStrategy::Center);
2632            cx.notify();
2633        }
2634    }
2635
2636    fn scroll_cursor_top(&mut self, _: &ScrollCursorTop, _: &mut Window, cx: &mut Context<Self>) {
2637        if let Some((_, _, index)) = self.selection.and_then(|s| self.index_for_selection(s)) {
2638            self.scroll_handle
2639                .scroll_to_item_strict(index, ScrollStrategy::Top);
2640            cx.notify();
2641        }
2642    }
2643
2644    fn scroll_cursor_bottom(
2645        &mut self,
2646        _: &ScrollCursorBottom,
2647        _: &mut Window,
2648        cx: &mut Context<Self>,
2649    ) {
2650        if let Some((_, _, index)) = self.selection.and_then(|s| self.index_for_selection(s)) {
2651            self.scroll_handle
2652                .scroll_to_item_strict(index, ScrollStrategy::Bottom);
2653            cx.notify();
2654        }
2655    }
2656
2657    fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
2658        if let Some(edit_state) = &self.state.edit_state
2659            && edit_state.processing_filename.is_none()
2660        {
2661            self.filename_editor.update(cx, |editor, cx| {
2662                editor.move_to_end_of_line(
2663                    &editor::actions::MoveToEndOfLine {
2664                        stop_at_soft_wraps: false,
2665                    },
2666                    window,
2667                    cx,
2668                );
2669            });
2670            return;
2671        }
2672        if let Some(selection) = self.selection {
2673            let (mut worktree_ix, mut entry_ix, _) =
2674                self.index_for_selection(selection).unwrap_or_default();
2675            if let Some(worktree_entries) = self
2676                .state
2677                .visible_entries
2678                .get(worktree_ix)
2679                .map(|v| &v.entries)
2680            {
2681                if entry_ix + 1 < worktree_entries.len() {
2682                    entry_ix += 1;
2683                } else {
2684                    worktree_ix += 1;
2685                    entry_ix = 0;
2686                }
2687            }
2688
2689            if let Some(VisibleEntriesForWorktree {
2690                worktree_id,
2691                entries,
2692                ..
2693            }) = self.state.visible_entries.get(worktree_ix)
2694                && let Some(entry) = entries.get(entry_ix)
2695            {
2696                let selection = SelectedEntry {
2697                    worktree_id: *worktree_id,
2698                    entry_id: entry.id,
2699                };
2700                self.selection = Some(selection);
2701                if window.modifiers().shift {
2702                    self.marked_entries.push(selection);
2703                }
2704
2705                self.autoscroll(cx);
2706                cx.notify();
2707            }
2708        } else {
2709            self.select_first(&SelectFirst {}, window, cx);
2710        }
2711    }
2712
2713    fn select_prev_diagnostic(
2714        &mut self,
2715        action: &SelectPrevDiagnostic,
2716        window: &mut Window,
2717        cx: &mut Context<Self>,
2718    ) {
2719        let selection = self.find_entry(
2720            self.selection.as_ref(),
2721            true,
2722            &|entry: GitEntryRef, worktree_id: WorktreeId| {
2723                self.selection.is_none_or(|selection| {
2724                    if selection.worktree_id == worktree_id {
2725                        selection.entry_id != entry.id
2726                    } else {
2727                        true
2728                    }
2729                }) && entry.is_file()
2730                    && self
2731                        .diagnostics
2732                        .get(&(worktree_id, entry.path.clone()))
2733                        .is_some_and(|severity| action.severity.matches(*severity))
2734            },
2735            cx,
2736        );
2737
2738        if let Some(selection) = selection {
2739            self.selection = Some(selection);
2740            self.expand_entry(selection.worktree_id, selection.entry_id, cx);
2741            self.update_visible_entries(
2742                Some((selection.worktree_id, selection.entry_id)),
2743                false,
2744                true,
2745                window,
2746                cx,
2747            );
2748            cx.notify();
2749        }
2750    }
2751
2752    fn select_next_diagnostic(
2753        &mut self,
2754        action: &SelectNextDiagnostic,
2755        window: &mut Window,
2756        cx: &mut Context<Self>,
2757    ) {
2758        let selection = self.find_entry(
2759            self.selection.as_ref(),
2760            false,
2761            &|entry: GitEntryRef, worktree_id: WorktreeId| {
2762                self.selection.is_none_or(|selection| {
2763                    if selection.worktree_id == worktree_id {
2764                        selection.entry_id != entry.id
2765                    } else {
2766                        true
2767                    }
2768                }) && entry.is_file()
2769                    && self
2770                        .diagnostics
2771                        .get(&(worktree_id, entry.path.clone()))
2772                        .is_some_and(|severity| action.severity.matches(*severity))
2773            },
2774            cx,
2775        );
2776
2777        if let Some(selection) = selection {
2778            self.selection = Some(selection);
2779            self.expand_entry(selection.worktree_id, selection.entry_id, cx);
2780            self.update_visible_entries(
2781                Some((selection.worktree_id, selection.entry_id)),
2782                false,
2783                true,
2784                window,
2785                cx,
2786            );
2787            cx.notify();
2788        }
2789    }
2790
2791    fn select_prev_git_entry(
2792        &mut self,
2793        _: &SelectPrevGitEntry,
2794        window: &mut Window,
2795        cx: &mut Context<Self>,
2796    ) {
2797        let selection = self.find_entry(
2798            self.selection.as_ref(),
2799            true,
2800            &|entry: GitEntryRef, worktree_id: WorktreeId| {
2801                (self.selection.is_none()
2802                    || self.selection.is_some_and(|selection| {
2803                        if selection.worktree_id == worktree_id {
2804                            selection.entry_id != entry.id
2805                        } else {
2806                            true
2807                        }
2808                    }))
2809                    && entry.is_file()
2810                    && entry.git_summary.index.modified + entry.git_summary.worktree.modified > 0
2811            },
2812            cx,
2813        );
2814
2815        if let Some(selection) = selection {
2816            self.selection = Some(selection);
2817            self.expand_entry(selection.worktree_id, selection.entry_id, cx);
2818            self.update_visible_entries(
2819                Some((selection.worktree_id, selection.entry_id)),
2820                false,
2821                true,
2822                window,
2823                cx,
2824            );
2825            cx.notify();
2826        }
2827    }
2828
2829    fn select_prev_directory(
2830        &mut self,
2831        _: &SelectPrevDirectory,
2832        _: &mut Window,
2833        cx: &mut Context<Self>,
2834    ) {
2835        let selection = self.find_visible_entry(
2836            self.selection.as_ref(),
2837            true,
2838            &|entry: GitEntryRef, worktree_id: WorktreeId| {
2839                self.selection.is_none_or(|selection| {
2840                    if selection.worktree_id == worktree_id {
2841                        selection.entry_id != entry.id
2842                    } else {
2843                        true
2844                    }
2845                }) && entry.is_dir()
2846            },
2847            cx,
2848        );
2849
2850        if let Some(selection) = selection {
2851            self.selection = Some(selection);
2852            self.autoscroll(cx);
2853            cx.notify();
2854        }
2855    }
2856
2857    fn select_next_directory(
2858        &mut self,
2859        _: &SelectNextDirectory,
2860        _: &mut Window,
2861        cx: &mut Context<Self>,
2862    ) {
2863        let selection = self.find_visible_entry(
2864            self.selection.as_ref(),
2865            false,
2866            &|entry: GitEntryRef, worktree_id: WorktreeId| {
2867                self.selection.is_none_or(|selection| {
2868                    if selection.worktree_id == worktree_id {
2869                        selection.entry_id != entry.id
2870                    } else {
2871                        true
2872                    }
2873                }) && entry.is_dir()
2874            },
2875            cx,
2876        );
2877
2878        if let Some(selection) = selection {
2879            self.selection = Some(selection);
2880            self.autoscroll(cx);
2881            cx.notify();
2882        }
2883    }
2884
2885    fn select_next_git_entry(
2886        &mut self,
2887        _: &SelectNextGitEntry,
2888        window: &mut Window,
2889        cx: &mut Context<Self>,
2890    ) {
2891        let selection = self.find_entry(
2892            self.selection.as_ref(),
2893            false,
2894            &|entry: GitEntryRef, worktree_id: WorktreeId| {
2895                self.selection.is_none_or(|selection| {
2896                    if selection.worktree_id == worktree_id {
2897                        selection.entry_id != entry.id
2898                    } else {
2899                        true
2900                    }
2901                }) && entry.is_file()
2902                    && entry.git_summary.index.modified + entry.git_summary.worktree.modified > 0
2903            },
2904            cx,
2905        );
2906
2907        if let Some(selection) = selection {
2908            self.selection = Some(selection);
2909            self.expand_entry(selection.worktree_id, selection.entry_id, cx);
2910            self.update_visible_entries(
2911                Some((selection.worktree_id, selection.entry_id)),
2912                false,
2913                true,
2914                window,
2915                cx,
2916            );
2917            cx.notify();
2918        }
2919    }
2920
2921    fn select_parent(&mut self, _: &SelectParent, window: &mut Window, cx: &mut Context<Self>) {
2922        if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
2923            if let Some(parent) = entry.path.parent() {
2924                let worktree = worktree.read(cx);
2925                if let Some(parent_entry) = worktree.entry_for_path(parent) {
2926                    self.selection = Some(SelectedEntry {
2927                        worktree_id: worktree.id(),
2928                        entry_id: parent_entry.id,
2929                    });
2930                    self.autoscroll(cx);
2931                    cx.notify();
2932                }
2933            }
2934        } else {
2935            self.select_first(&SelectFirst {}, window, cx);
2936        }
2937    }
2938
2939    fn select_first(&mut self, _: &SelectFirst, window: &mut Window, cx: &mut Context<Self>) {
2940        if let Some(VisibleEntriesForWorktree {
2941            worktree_id,
2942            entries,
2943            ..
2944        }) = self.state.visible_entries.first()
2945            && let Some(entry) = entries.first()
2946        {
2947            let selection = SelectedEntry {
2948                worktree_id: *worktree_id,
2949                entry_id: entry.id,
2950            };
2951            self.selection = Some(selection);
2952            if window.modifiers().shift {
2953                self.marked_entries.push(selection);
2954            }
2955            self.autoscroll(cx);
2956            cx.notify();
2957        }
2958    }
2959
2960    fn select_last(&mut self, _: &SelectLast, _: &mut Window, cx: &mut Context<Self>) {
2961        if let Some(VisibleEntriesForWorktree {
2962            worktree_id,
2963            entries,
2964            ..
2965        }) = self.state.visible_entries.last()
2966        {
2967            let worktree = self.project.read(cx).worktree_for_id(*worktree_id, cx);
2968            if let (Some(worktree), Some(entry)) = (worktree, entries.last()) {
2969                let worktree = worktree.read(cx);
2970                if let Some(entry) = worktree.entry_for_id(entry.id) {
2971                    let selection = SelectedEntry {
2972                        worktree_id: *worktree_id,
2973                        entry_id: entry.id,
2974                    };
2975                    self.selection = Some(selection);
2976                    self.autoscroll(cx);
2977                    cx.notify();
2978                }
2979            }
2980        }
2981    }
2982
2983    fn autoscroll(&mut self, cx: &mut Context<Self>) {
2984        if let Some((_, _, index)) = self.selection.and_then(|s| self.index_for_selection(s)) {
2985            self.scroll_handle.scroll_to_item_with_offset(
2986                index,
2987                ScrollStrategy::Center,
2988                self.sticky_items_count,
2989            );
2990            cx.notify();
2991        }
2992    }
2993
2994    fn cut(&mut self, _: &Cut, _: &mut Window, cx: &mut Context<Self>) {
2995        let entries = self.disjoint_effective_entries(cx);
2996        if !entries.is_empty() {
2997            self.write_entries_to_system_clipboard(&entries, cx);
2998            self.clipboard = Some(ClipboardEntry::Cut(entries));
2999            cx.notify();
3000        }
3001    }
3002
3003    fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context<Self>) {
3004        let entries = self.disjoint_effective_entries(cx);
3005        if !entries.is_empty() {
3006            self.write_entries_to_system_clipboard(&entries, cx);
3007            self.clipboard = Some(ClipboardEntry::Copied(entries));
3008            cx.notify();
3009        }
3010    }
3011
3012    fn create_paste_path(
3013        &self,
3014        source: &SelectedEntry,
3015        (worktree, target_entry): (Entity<Worktree>, &Entry),
3016        cx: &App,
3017    ) -> Option<(Arc<RelPath>, Option<Range<usize>>)> {
3018        let mut new_path = target_entry.path.to_rel_path_buf();
3019        // If we're pasting into a file, or a directory into itself, go up one level.
3020        if target_entry.is_file() || (target_entry.is_dir() && target_entry.id == source.entry_id) {
3021            new_path.pop();
3022        }
3023
3024        let source_worktree = self
3025            .project
3026            .read(cx)
3027            .worktree_for_entry(source.entry_id, cx)?;
3028        let source_entry = source_worktree.read(cx).entry_for_id(source.entry_id)?;
3029
3030        let clipboard_entry_file_name = source_entry.path.file_name()?.to_string();
3031        new_path.push(RelPath::unix(&clipboard_entry_file_name).unwrap());
3032
3033        let (extension, file_name_without_extension) = if source_entry.is_file() {
3034            (
3035                new_path.extension().map(|s| s.to_string()),
3036                new_path.file_stem()?.to_string(),
3037            )
3038        } else {
3039            (None, clipboard_entry_file_name.clone())
3040        };
3041
3042        let file_name_len = file_name_without_extension.len();
3043        let mut disambiguation_range = None;
3044        let mut ix = 0;
3045        {
3046            let worktree = worktree.read(cx);
3047            while worktree.entry_for_path(&new_path).is_some() {
3048                new_path.pop();
3049
3050                let mut new_file_name = file_name_without_extension.to_string();
3051
3052                let disambiguation = " copy";
3053                let mut disambiguation_len = disambiguation.len();
3054
3055                new_file_name.push_str(disambiguation);
3056
3057                if ix > 0 {
3058                    let extra_disambiguation = format!(" {}", ix);
3059                    disambiguation_len += extra_disambiguation.len();
3060                    new_file_name.push_str(&extra_disambiguation);
3061                }
3062                if let Some(extension) = extension.as_ref() {
3063                    new_file_name.push_str(".");
3064                    new_file_name.push_str(extension);
3065                }
3066
3067                new_path.push(RelPath::unix(&new_file_name).unwrap());
3068
3069                disambiguation_range = Some(0..(file_name_len + disambiguation_len));
3070                ix += 1;
3071            }
3072        }
3073        Some((new_path.as_rel_path().into(), disambiguation_range))
3074    }
3075
3076    fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
3077        if let Some(external_paths) = self.external_paths_from_system_clipboard(cx) {
3078            let target_entry_id = self
3079                .selection
3080                .map(|s| s.entry_id)
3081                .or(self.state.last_worktree_root_id);
3082            if let Some(entry_id) = target_entry_id {
3083                self.drop_external_files(external_paths.paths(), entry_id, window, cx);
3084            }
3085            return;
3086        }
3087
3088        maybe!({
3089            let (worktree, entry) = self.selected_entry_handle(cx)?;
3090            let entry = entry.clone();
3091            let worktree_id = worktree.read(cx).id();
3092            let clipboard_entries = self
3093                .clipboard
3094                .as_ref()
3095                .filter(|clipboard| !clipboard.items().is_empty())?;
3096
3097            enum PasteTask {
3098                Rename {
3099                    task: Task<Result<CreatedEntry>>,
3100                    from: ProjectPath,
3101                    to: ProjectPath,
3102                },
3103                Copy {
3104                    task: Task<Result<Option<Entry>>>,
3105                    destination: ProjectPath,
3106                },
3107            }
3108
3109            let mut paste_tasks = Vec::new();
3110            let mut disambiguation_range = None;
3111            let clip_is_cut = clipboard_entries.is_cut();
3112            for clipboard_entry in clipboard_entries.items() {
3113                let (new_path, new_disambiguation_range) =
3114                    self.create_paste_path(clipboard_entry, self.selected_sub_entry(cx)?, cx)?;
3115                let clip_entry_id = clipboard_entry.entry_id;
3116                let destination: ProjectPath = (worktree_id, new_path).into();
3117                let task = if clipboard_entries.is_cut() {
3118                    let original_path = self.project.read(cx).path_for_entry(clip_entry_id, cx)?;
3119                    let task = self.project.update(cx, |project, cx| {
3120                        project.rename_entry(clip_entry_id, destination.clone(), cx)
3121                    });
3122                    PasteTask::Rename {
3123                        task,
3124                        from: original_path,
3125                        to: destination,
3126                    }
3127                } else {
3128                    let task = self.project.update(cx, |project, cx| {
3129                        project.copy_entry(clip_entry_id, destination.clone(), cx)
3130                    });
3131                    PasteTask::Copy { task, destination }
3132                };
3133                paste_tasks.push(task);
3134                disambiguation_range = new_disambiguation_range.or(disambiguation_range);
3135            }
3136
3137            let item_count = paste_tasks.len();
3138            let workspace = self.workspace.clone();
3139
3140            cx.spawn_in(window, async move |project_panel, mut cx| {
3141                let mut last_succeed = None;
3142                let mut changes = Vec::new();
3143
3144                for task in paste_tasks {
3145                    match task {
3146                        PasteTask::Rename { task, from, to } => {
3147                            if let Some(CreatedEntry::Included(entry)) = task
3148                                .await
3149                                .notify_workspace_async_err(workspace.clone(), &mut cx)
3150                            {
3151                                changes.push(Change::Renamed(from, to));
3152                                last_succeed = Some(entry);
3153                            }
3154                        }
3155                        PasteTask::Copy { task, destination } => {
3156                            if let Some(Some(entry)) = task
3157                                .await
3158                                .notify_workspace_async_err(workspace.clone(), &mut cx)
3159                            {
3160                                changes.push(Change::Created(destination));
3161                                last_succeed = Some(entry);
3162                            }
3163                        }
3164                    }
3165                }
3166
3167                project_panel
3168                    .update(cx, |this, _| {
3169                        this.undo_manager.record(changes).log_err();
3170                    })
3171                    .ok();
3172
3173                // update selection
3174                if let Some(entry) = last_succeed {
3175                    project_panel
3176                        .update_in(cx, |project_panel, window, cx| {
3177                            project_panel.selection = Some(SelectedEntry {
3178                                worktree_id,
3179                                entry_id: entry.id,
3180                            });
3181
3182                            if item_count == 1 {
3183                                // open entry if not dir, setting is enabled, and only focus if rename is not pending
3184                                if !entry.is_dir() {
3185                                    let settings = ProjectPanelSettings::get_global(cx);
3186                                    if settings.auto_open.should_open_on_paste() {
3187                                        project_panel.open_entry(
3188                                            entry.id,
3189                                            disambiguation_range.is_none(),
3190                                            false,
3191                                            cx,
3192                                        );
3193                                    }
3194                                }
3195
3196                                // if only one entry was pasted and it was disambiguated, open the rename editor
3197                                if disambiguation_range.is_some() {
3198                                    cx.defer_in(window, |this, window, cx| {
3199                                        this.rename_impl(disambiguation_range, window, cx);
3200                                    });
3201                                }
3202                            }
3203                        })
3204                        .ok();
3205                }
3206
3207                anyhow::Ok(())
3208            })
3209            .detach_and_log_err(cx);
3210
3211            if clip_is_cut {
3212                // Convert the clipboard cut entry to a copy entry after the first paste.
3213                self.clipboard = self.clipboard.take().map(ClipboardEntry::into_copy_entry);
3214            }
3215
3216            self.expand_entry(worktree_id, entry.id, cx);
3217            Some(())
3218        });
3219    }
3220
3221    fn download_from_remote(
3222        &mut self,
3223        _: &DownloadFromRemote,
3224        window: &mut Window,
3225        cx: &mut Context<Self>,
3226    ) {
3227        let entries = self.effective_entries();
3228        if entries.is_empty() {
3229            return;
3230        }
3231
3232        let project = self.project.read(cx);
3233
3234        // Collect file entries with their worktree_id, path, and relative path for destination
3235        // For directories, we collect all files under them recursively
3236        let mut files_to_download: Vec<(WorktreeId, Arc<RelPath>, PathBuf)> = Vec::new();
3237
3238        for selected in entries.iter() {
3239            let Some(worktree) = project.worktree_for_id(selected.worktree_id, cx) else {
3240                continue;
3241            };
3242            let worktree = worktree.read(cx);
3243            let Some(entry) = worktree.entry_for_id(selected.entry_id) else {
3244                continue;
3245            };
3246
3247            if entry.is_file() {
3248                // Single file: use just the filename
3249                let filename = entry
3250                    .path
3251                    .file_name()
3252                    .map(str::to_string)
3253                    .unwrap_or_default();
3254                files_to_download.push((
3255                    selected.worktree_id,
3256                    entry.path.clone(),
3257                    PathBuf::from(filename),
3258                ));
3259            } else if entry.is_dir() {
3260                // Directory: collect all files recursively, preserving relative paths
3261                let dir_name = entry
3262                    .path
3263                    .file_name()
3264                    .map(str::to_string)
3265                    .unwrap_or_default();
3266                let base_path = entry.path.clone();
3267
3268                // Use traverse_from_path to iterate all entries under this directory
3269                let mut traversal = worktree.traverse_from_path(true, true, true, &entry.path);
3270                while let Some(child_entry) = traversal.entry() {
3271                    // Stop when we're no longer under the directory
3272                    if !child_entry.path.starts_with(&base_path) {
3273                        break;
3274                    }
3275
3276                    if child_entry.is_file() {
3277                        // Calculate relative path from the directory root
3278                        let relative_path = child_entry
3279                            .path
3280                            .strip_prefix(&base_path)
3281                            .map(|p| PathBuf::from(dir_name.clone()).join(p.as_unix_str()))
3282                            .unwrap_or_else(|_| {
3283                                PathBuf::from(
3284                                    child_entry
3285                                        .path
3286                                        .file_name()
3287                                        .map(str::to_string)
3288                                        .unwrap_or_default(),
3289                                )
3290                            });
3291                        files_to_download.push((
3292                            selected.worktree_id,
3293                            child_entry.path.clone(),
3294                            relative_path,
3295                        ));
3296                    }
3297                    traversal.advance();
3298                }
3299            }
3300        }
3301
3302        if files_to_download.is_empty() {
3303            return;
3304        }
3305
3306        let total_files = files_to_download.len();
3307        let workspace = self.workspace.clone();
3308
3309        let destination_dir = cx.prompt_for_paths(PathPromptOptions {
3310            files: false,
3311            directories: true,
3312            multiple: false,
3313            prompt: Some("Download".into()),
3314        });
3315
3316        let fs = self.fs.clone();
3317        let notification_id =
3318            workspace::notifications::NotificationId::Named("download-progress".into());
3319        cx.spawn_in(window, async move |this, cx| {
3320            if let Ok(Ok(Some(mut paths))) = destination_dir.await {
3321                if let Some(dest_dir) = paths.pop() {
3322                    // Show initial toast
3323                    workspace
3324                        .update(cx, |workspace, cx| {
3325                            workspace.show_toast(
3326                                workspace::Toast::new(
3327                                    notification_id.clone(),
3328                                    format!("Downloading 0/{} files...", total_files),
3329                                ),
3330                                cx,
3331                            );
3332                        })
3333                        .ok();
3334
3335                    for (index, (worktree_id, entry_path, relative_path)) in
3336                        files_to_download.into_iter().enumerate()
3337                    {
3338                        // Update progress toast
3339                        workspace
3340                            .update(cx, |workspace, cx| {
3341                                workspace.show_toast(
3342                                    workspace::Toast::new(
3343                                        notification_id.clone(),
3344                                        format!(
3345                                            "Downloading {}/{} files...",
3346                                            index + 1,
3347                                            total_files
3348                                        ),
3349                                    ),
3350                                    cx,
3351                                );
3352                            })
3353                            .ok();
3354
3355                        let destination_path = dest_dir.join(&relative_path);
3356
3357                        // Create parent directories if needed
3358                        if let Some(parent) = destination_path.parent() {
3359                            if !parent.exists() {
3360                                fs.create_dir(parent).await.log_err();
3361                            }
3362                        }
3363
3364                        let download_task = this.update(cx, |this, cx| {
3365                            let project = this.project.clone();
3366                            project.update(cx, |project, cx| {
3367                                project.download_file(worktree_id, entry_path, destination_path, cx)
3368                            })
3369                        });
3370                        if let Ok(task) = download_task {
3371                            task.await.log_err();
3372                        }
3373                    }
3374
3375                    // Show completion toast
3376                    workspace
3377                        .update(cx, |workspace, cx| {
3378                            workspace.show_toast(
3379                                workspace::Toast::new(
3380                                    notification_id.clone(),
3381                                    format!("Downloaded {} files", total_files),
3382                                ),
3383                                cx,
3384                            );
3385                        })
3386                        .ok();
3387                }
3388            }
3389        })
3390        .detach();
3391    }
3392
3393    fn duplicate(&mut self, _: &Duplicate, window: &mut Window, cx: &mut Context<Self>) {
3394        self.copy(&Copy {}, window, cx);
3395        self.paste(&Paste {}, window, cx);
3396    }
3397
3398    fn copy_path(
3399        &mut self,
3400        _: &zed_actions::workspace::CopyPath,
3401        _: &mut Window,
3402        cx: &mut Context<Self>,
3403    ) {
3404        let abs_file_paths = {
3405            let project = self.project.read(cx);
3406            self.effective_entries()
3407                .into_iter()
3408                .filter_map(|entry| {
3409                    let entry_path = project.path_for_entry(entry.entry_id, cx)?.path;
3410                    Some(
3411                        project
3412                            .worktree_for_id(entry.worktree_id, cx)?
3413                            .read(cx)
3414                            .absolutize(&entry_path)
3415                            .to_string_lossy()
3416                            .to_string(),
3417                    )
3418                })
3419                .collect::<Vec<_>>()
3420        };
3421        if !abs_file_paths.is_empty() {
3422            cx.write_to_clipboard(ClipboardItem::new_string(abs_file_paths.join("\n")));
3423        }
3424    }
3425
3426    fn copy_relative_path(
3427        &mut self,
3428        _: &zed_actions::workspace::CopyRelativePath,
3429        _: &mut Window,
3430        cx: &mut Context<Self>,
3431    ) {
3432        let path_style = self.project.read(cx).path_style(cx);
3433        let file_paths = {
3434            let project = self.project.read(cx);
3435            self.effective_entries()
3436                .into_iter()
3437                .filter_map(|entry| {
3438                    Some(
3439                        project
3440                            .path_for_entry(entry.entry_id, cx)?
3441                            .path
3442                            .display(path_style)
3443                            .into_owned(),
3444                    )
3445                })
3446                .collect::<Vec<_>>()
3447        };
3448        if !file_paths.is_empty() {
3449            cx.write_to_clipboard(ClipboardItem::new_string(file_paths.join("\n")));
3450        }
3451    }
3452
3453    fn reveal_in_finder(
3454        &mut self,
3455        _: &RevealInFileManager,
3456        _: &mut Window,
3457        cx: &mut Context<Self>,
3458    ) {
3459        if let Some(path) = self.reveal_in_file_manager_path(cx) {
3460            self.project
3461                .update(cx, |project, cx| project.reveal_path(&path, cx));
3462        }
3463    }
3464
3465    fn remove_from_project(
3466        &mut self,
3467        _: &RemoveFromProject,
3468        _window: &mut Window,
3469        cx: &mut Context<Self>,
3470    ) {
3471        for entry in self.effective_entries().iter() {
3472            let worktree_id = entry.worktree_id;
3473            self.project
3474                .update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
3475        }
3476    }
3477
3478    fn file_abs_paths_to_diff(&self, cx: &Context<Self>) -> Option<(PathBuf, PathBuf)> {
3479        let mut selections_abs_path = self
3480            .marked_entries
3481            .iter()
3482            .filter_map(|entry| {
3483                let project = self.project.read(cx);
3484                let worktree = project.worktree_for_id(entry.worktree_id, cx)?;
3485                let entry = worktree.read(cx).entry_for_id(entry.entry_id)?;
3486                if !entry.is_file() {
3487                    return None;
3488                }
3489                Some(worktree.read(cx).absolutize(&entry.path))
3490            })
3491            .rev();
3492
3493        let last_path = selections_abs_path.next()?;
3494        let previous_to_last = selections_abs_path.next()?;
3495        Some((previous_to_last, last_path))
3496    }
3497
3498    fn compare_marked_files(
3499        &mut self,
3500        _: &CompareMarkedFiles,
3501        window: &mut Window,
3502        cx: &mut Context<Self>,
3503    ) {
3504        let selected_files = self.file_abs_paths_to_diff(cx);
3505        if let Some((file_path1, file_path2)) = selected_files {
3506            self.workspace
3507                .update(cx, |workspace, cx| {
3508                    FileDiffView::open(file_path1, file_path2, workspace.weak_handle(), window, cx)
3509                        .detach_and_log_err(cx);
3510                })
3511                .ok();
3512        }
3513    }
3514
3515    fn open_system(&mut self, _: &OpenWithSystem, _: &mut Window, cx: &mut Context<Self>) {
3516        if let Some((worktree, entry)) = self.selected_entry(cx) {
3517            let abs_path = worktree.absolutize(&entry.path);
3518            cx.open_with_system(&abs_path);
3519        }
3520    }
3521
3522    fn open_in_terminal(
3523        &mut self,
3524        _: &OpenInTerminal,
3525        window: &mut Window,
3526        cx: &mut Context<Self>,
3527    ) {
3528        if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
3529            let abs_path = match &entry.canonical_path {
3530                Some(canonical_path) => canonical_path.to_path_buf(),
3531                None => worktree.read(cx).absolutize(&entry.path),
3532            };
3533
3534            let working_directory = if entry.is_dir() {
3535                Some(abs_path)
3536            } else {
3537                abs_path.parent().map(|path| path.to_path_buf())
3538            };
3539            if let Some(working_directory) = working_directory {
3540                window.dispatch_action(
3541                    workspace::OpenTerminal {
3542                        working_directory,
3543                        local: false,
3544                    }
3545                    .boxed_clone(),
3546                    cx,
3547                )
3548            }
3549        }
3550    }
3551
3552    pub fn new_search_in_directory(
3553        &mut self,
3554        _: &NewSearchInDirectory,
3555        window: &mut Window,
3556        cx: &mut Context<Self>,
3557    ) {
3558        if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
3559            let dir_path = if entry.is_dir() {
3560                entry.path.clone()
3561            } else {
3562                // entry is a file, use its parent directory
3563                match entry.path.parent() {
3564                    Some(parent) => Arc::from(parent),
3565                    None => {
3566                        // File at root, open search with empty filter
3567                        self.workspace
3568                            .update(cx, |workspace, cx| {
3569                                search::ProjectSearchView::new_search_in_directory(
3570                                    workspace,
3571                                    RelPath::empty(),
3572                                    window,
3573                                    cx,
3574                                );
3575                            })
3576                            .ok();
3577                        return;
3578                    }
3579                }
3580            };
3581
3582            let include_root = self.project.read(cx).visible_worktrees(cx).count() > 1;
3583            let dir_path = if include_root {
3584                worktree.read(cx).root_name().join(&dir_path)
3585            } else {
3586                dir_path
3587            };
3588
3589            self.workspace
3590                .update(cx, |workspace, cx| {
3591                    search::ProjectSearchView::new_search_in_directory(
3592                        workspace, &dir_path, window, cx,
3593                    );
3594                })
3595                .ok();
3596        }
3597    }
3598
3599    fn move_entry(
3600        &mut self,
3601        entry_to_move: ProjectEntryId,
3602        destination: ProjectEntryId,
3603        destination_is_file: bool,
3604        cx: &mut Context<Self>,
3605    ) -> Option<Task<Result<CreatedEntry>>> {
3606        if self
3607            .project
3608            .read(cx)
3609            .entry_is_worktree_root(entry_to_move, cx)
3610        {
3611            self.move_worktree_root(entry_to_move, destination, cx);
3612            None
3613        } else {
3614            self.move_worktree_entry(entry_to_move, destination, destination_is_file, cx)
3615        }
3616    }
3617
3618    fn move_worktree_root(
3619        &mut self,
3620        entry_to_move: ProjectEntryId,
3621        destination: ProjectEntryId,
3622        cx: &mut Context<Self>,
3623    ) {
3624        self.project.update(cx, |project, cx| {
3625            let Some(worktree_to_move) = project.worktree_for_entry(entry_to_move, cx) else {
3626                return;
3627            };
3628            let Some(destination_worktree) = project.worktree_for_entry(destination, cx) else {
3629                return;
3630            };
3631
3632            let worktree_id = worktree_to_move.read(cx).id();
3633            let destination_id = destination_worktree.read(cx).id();
3634
3635            project
3636                .move_worktree(worktree_id, destination_id, cx)
3637                .log_err();
3638        });
3639    }
3640
3641    fn move_worktree_entry(
3642        &mut self,
3643        entry_to_move: ProjectEntryId,
3644        destination_entry: ProjectEntryId,
3645        destination_is_file: bool,
3646        cx: &mut Context<Self>,
3647    ) -> Option<Task<Result<CreatedEntry>>> {
3648        if entry_to_move == destination_entry {
3649            return None;
3650        }
3651
3652        let (destination_worktree, rename_task) = self.project.update(cx, |project, cx| {
3653            let Some(source_path) = project.path_for_entry(entry_to_move, cx) else {
3654                return (None, None);
3655            };
3656            let Some(destination_path) = project.path_for_entry(destination_entry, cx) else {
3657                return (None, None);
3658            };
3659            let destination_worktree_id = destination_path.worktree_id;
3660
3661            let destination_dir = if destination_is_file {
3662                destination_path.path.parent().unwrap_or(RelPath::empty())
3663            } else {
3664                destination_path.path.as_ref()
3665            };
3666
3667            let Some(source_name) = source_path.path.file_name() else {
3668                return (None, None);
3669            };
3670            let Ok(source_name) = RelPath::unix(source_name) else {
3671                return (None, None);
3672            };
3673
3674            let mut new_path = destination_dir.to_rel_path_buf();
3675            new_path.push(source_name);
3676            let rename_task = (new_path.as_rel_path() != source_path.path.as_ref()).then(|| {
3677                project.rename_entry(
3678                    entry_to_move,
3679                    (destination_worktree_id, new_path).into(),
3680                    cx,
3681                )
3682            });
3683
3684            (
3685                project.worktree_id_for_entry(destination_entry, cx),
3686                rename_task,
3687            )
3688        });
3689
3690        if let Some(destination_worktree) = destination_worktree {
3691            self.expand_entry(destination_worktree, destination_entry, cx);
3692        }
3693        rename_task
3694    }
3695
3696    fn index_for_selection(&self, selection: SelectedEntry) -> Option<(usize, usize, usize)> {
3697        self.index_for_entry(selection.entry_id, selection.worktree_id)
3698    }
3699
3700    fn disjoint_effective_entries(&self, cx: &App) -> BTreeSet<SelectedEntry> {
3701        self.disjoint_entries(self.effective_entries(), cx)
3702    }
3703
3704    fn disjoint_entries(
3705        &self,
3706        entries: BTreeSet<SelectedEntry>,
3707        cx: &App,
3708    ) -> BTreeSet<SelectedEntry> {
3709        let mut sanitized_entries = BTreeSet::new();
3710        if entries.is_empty() {
3711            return sanitized_entries;
3712        }
3713
3714        let project = self.project.read(cx);
3715        let entries_by_worktree: HashMap<WorktreeId, Vec<SelectedEntry>> = entries
3716            .into_iter()
3717            .filter(|entry| !project.entry_is_worktree_root(entry.entry_id, cx))
3718            .fold(HashMap::default(), |mut map, entry| {
3719                map.entry(entry.worktree_id).or_default().push(entry);
3720                map
3721            });
3722
3723        for (worktree_id, worktree_entries) in entries_by_worktree {
3724            if let Some(worktree) = project.worktree_for_id(worktree_id, cx) {
3725                let worktree = worktree.read(cx);
3726                let dir_paths = worktree_entries
3727                    .iter()
3728                    .filter_map(|entry| {
3729                        worktree.entry_for_id(entry.entry_id).and_then(|entry| {
3730                            if entry.is_dir() {
3731                                Some(entry.path.as_ref())
3732                            } else {
3733                                None
3734                            }
3735                        })
3736                    })
3737                    .collect::<BTreeSet<_>>();
3738
3739                sanitized_entries.extend(worktree_entries.into_iter().filter(|entry| {
3740                    let Some(entry_info) = worktree.entry_for_id(entry.entry_id) else {
3741                        return false;
3742                    };
3743                    let entry_path = entry_info.path.as_ref();
3744                    let inside_selected_dir = dir_paths.iter().any(|&dir_path| {
3745                        entry_path != dir_path && entry_path.starts_with(dir_path)
3746                    });
3747                    !inside_selected_dir
3748                }));
3749            }
3750        }
3751
3752        sanitized_entries
3753    }
3754
3755    fn effective_entries(&self) -> BTreeSet<SelectedEntry> {
3756        if let Some(selection) = self.selection {
3757            let selection = SelectedEntry {
3758                entry_id: self.resolve_entry(selection.entry_id),
3759                worktree_id: selection.worktree_id,
3760            };
3761
3762            // Default to using just the selected item when nothing is marked.
3763            if self.marked_entries.is_empty() {
3764                return BTreeSet::from([selection]);
3765            }
3766
3767            // Allow operating on the selected item even when something else is marked,
3768            // making it easier to perform one-off actions without clearing a mark.
3769            if self.marked_entries.len() == 1 && !self.marked_entries.contains(&selection) {
3770                return BTreeSet::from([selection]);
3771            }
3772        }
3773
3774        // Return only marked entries since we've already handled special cases where
3775        // only selection should take precedence. At this point, marked entries may or
3776        // may not include the current selection, which is intentional.
3777        self.marked_entries
3778            .iter()
3779            .map(|entry| SelectedEntry {
3780                entry_id: self.resolve_entry(entry.entry_id),
3781                worktree_id: entry.worktree_id,
3782            })
3783            .collect::<BTreeSet<_>>()
3784    }
3785
3786    /// Finds the currently selected subentry for a given leaf entry id. If a given entry
3787    /// has no ancestors, the project entry ID that's passed in is returned as-is.
3788    fn resolve_entry(&self, id: ProjectEntryId) -> ProjectEntryId {
3789        self.state
3790            .ancestors
3791            .get(&id)
3792            .and_then(|ancestors| ancestors.active_ancestor())
3793            .unwrap_or(id)
3794    }
3795
3796    pub fn selected_entry<'a>(&self, cx: &'a App) -> Option<(&'a Worktree, &'a project::Entry)> {
3797        let (worktree, entry) = self.selected_entry_handle(cx)?;
3798        Some((worktree.read(cx), entry))
3799    }
3800
3801    /// Compared to selected_entry, this function resolves to the currently
3802    /// selected subentry if dir auto-folding is enabled.
3803    fn selected_sub_entry<'a>(
3804        &self,
3805        cx: &'a App,
3806    ) -> Option<(Entity<Worktree>, &'a project::Entry)> {
3807        let (worktree, mut entry) = self.selected_entry_handle(cx)?;
3808
3809        let resolved_id = self.resolve_entry(entry.id);
3810        if resolved_id != entry.id {
3811            let worktree = worktree.read(cx);
3812            entry = worktree.entry_for_id(resolved_id)?;
3813        }
3814        Some((worktree, entry))
3815    }
3816
3817    fn reveal_in_file_manager_path(&self, cx: &App) -> Option<PathBuf> {
3818        if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
3819            return Some(worktree.read(cx).absolutize(&entry.path));
3820        }
3821
3822        let root_entry_id = self.state.last_worktree_root_id?;
3823        let project = self.project.read(cx);
3824        let worktree = project.worktree_for_entry(root_entry_id, cx)?;
3825        let worktree = worktree.read(cx);
3826        let root_entry = worktree.entry_for_id(root_entry_id)?;
3827        Some(worktree.absolutize(&root_entry.path))
3828    }
3829
3830    fn write_entries_to_system_clipboard(&self, entries: &BTreeSet<SelectedEntry>, cx: &mut App) {
3831        let project = self.project.read(cx);
3832        let paths: Vec<String> = entries
3833            .iter()
3834            .filter_map(|entry| {
3835                let worktree = project.worktree_for_id(entry.worktree_id, cx)?;
3836                let worktree = worktree.read(cx);
3837                let worktree_entry = worktree.entry_for_id(entry.entry_id)?;
3838                Some(
3839                    worktree
3840                        .abs_path()
3841                        .join(worktree_entry.path.as_std_path())
3842                        .to_string_lossy()
3843                        .to_string(),
3844                )
3845            })
3846            .collect();
3847        if !paths.is_empty() {
3848            cx.write_to_clipboard(ClipboardItem::new_string(paths.join("\n")));
3849        }
3850    }
3851
3852    fn external_paths_from_system_clipboard(&self, cx: &App) -> Option<ExternalPaths> {
3853        let clipboard_item = cx.read_from_clipboard()?;
3854        for entry in clipboard_item.entries() {
3855            if let GpuiClipboardEntry::ExternalPaths(paths) = entry {
3856                if !paths.paths().is_empty() {
3857                    return Some(paths.clone());
3858                }
3859            }
3860        }
3861        None
3862    }
3863
3864    fn has_pasteable_content(&self, cx: &App) -> bool {
3865        if self
3866            .clipboard
3867            .as_ref()
3868            .is_some_and(|c| !c.items().is_empty())
3869        {
3870            return true;
3871        }
3872        self.external_paths_from_system_clipboard(cx).is_some()
3873    }
3874
3875    fn selected_entry_handle<'a>(
3876        &self,
3877        cx: &'a App,
3878    ) -> Option<(Entity<Worktree>, &'a project::Entry)> {
3879        let selection = self.selection?;
3880        let project = self.project.read(cx);
3881        let worktree = project.worktree_for_id(selection.worktree_id, cx)?;
3882        let entry = worktree.read(cx).entry_for_id(selection.entry_id)?;
3883        Some((worktree, entry))
3884    }
3885
3886    fn expand_to_selection(&mut self, cx: &mut Context<Self>) -> Option<()> {
3887        let (worktree, entry) = self.selected_entry(cx)?;
3888        let expanded_dir_ids = self
3889            .state
3890            .expanded_dir_ids
3891            .entry(worktree.id())
3892            .or_default();
3893
3894        for path in entry.path.ancestors() {
3895            let Some(entry) = worktree.entry_for_path(path) else {
3896                continue;
3897            };
3898            if entry.is_dir()
3899                && let Err(idx) = expanded_dir_ids.binary_search(&entry.id)
3900            {
3901                expanded_dir_ids.insert(idx, entry.id);
3902            }
3903        }
3904
3905        Some(())
3906    }
3907
3908    fn create_new_git_entry(
3909        parent_entry: &Entry,
3910        git_summary: GitSummary,
3911        new_entry_kind: EntryKind,
3912    ) -> GitEntry {
3913        GitEntry {
3914            entry: Entry {
3915                id: NEW_ENTRY_ID,
3916                kind: new_entry_kind,
3917                path: parent_entry.path.join(RelPath::unix("\0").unwrap()),
3918                inode: 0,
3919                mtime: parent_entry.mtime,
3920                size: parent_entry.size,
3921                is_ignored: parent_entry.is_ignored,
3922                is_hidden: parent_entry.is_hidden,
3923                is_external: false,
3924                is_private: false,
3925                is_always_included: parent_entry.is_always_included,
3926                canonical_path: parent_entry.canonical_path.clone(),
3927                char_bag: parent_entry.char_bag,
3928                is_fifo: parent_entry.is_fifo,
3929            },
3930            git_summary,
3931        }
3932    }
3933
3934    fn update_visible_entries(
3935        &mut self,
3936        new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,
3937        focus_filename_editor: bool,
3938        autoscroll: bool,
3939        window: &mut Window,
3940        cx: &mut Context<Self>,
3941    ) {
3942        let now = Instant::now();
3943        let settings = ProjectPanelSettings::get_global(cx);
3944        let auto_collapse_dirs = settings.auto_fold_dirs;
3945        let hide_gitignore = settings.hide_gitignore;
3946        let sort_mode = settings.sort_mode;
3947        let sort_order = settings.sort_order;
3948        let project = self.project.read(cx);
3949        let repo_snapshots = project.git_store().read(cx).repo_snapshots(cx);
3950
3951        let old_ancestors = self.state.ancestors.clone();
3952        let temporary_unfolded_pending_state = self.state.temporarily_unfolded_pending_state.take();
3953        let mut new_state = State::derive(&self.state);
3954        new_state.last_worktree_root_id = project
3955            .visible_worktrees(cx)
3956            .next_back()
3957            .and_then(|worktree| worktree.read(cx).root_entry())
3958            .map(|entry| entry.id);
3959        let mut max_width_item = None;
3960
3961        let visible_worktrees: Vec<_> = project
3962            .visible_worktrees(cx)
3963            .map(|worktree| worktree.read(cx).snapshot())
3964            .collect();
3965        let hide_root = settings.hide_root && visible_worktrees.len() == 1;
3966        let hide_hidden = settings.hide_hidden;
3967
3968        let visible_entries_task = cx.spawn_in(window, async move |this, cx| {
3969            let new_state = cx
3970                .background_spawn(async move {
3971                    for worktree_snapshot in visible_worktrees {
3972                        let worktree_id = worktree_snapshot.id();
3973
3974                        let mut new_entry_parent_id = None;
3975                        let mut new_entry_kind = EntryKind::Dir;
3976                        if let Some(edit_state) = &new_state.edit_state
3977                            && edit_state.worktree_id == worktree_id
3978                            && edit_state.is_new_entry()
3979                        {
3980                            new_entry_parent_id = Some(edit_state.entry_id);
3981                            new_entry_kind = if edit_state.is_dir {
3982                                EntryKind::Dir
3983                            } else {
3984                                EntryKind::File
3985                            };
3986                        }
3987
3988                        let mut visible_worktree_entries = Vec::new();
3989                        let mut entry_iter =
3990                            GitTraversal::new(&repo_snapshots, worktree_snapshot.entries(true, 0));
3991                        let mut auto_folded_ancestors = vec![];
3992                        let worktree_abs_path = worktree_snapshot.abs_path();
3993                        while let Some(entry) = entry_iter.entry() {
3994                            if hide_root && Some(entry.entry) == worktree_snapshot.root_entry() {
3995                                if new_entry_parent_id == Some(entry.id) {
3996                                    visible_worktree_entries.push(Self::create_new_git_entry(
3997                                        entry.entry,
3998                                        entry.git_summary,
3999                                        new_entry_kind,
4000                                    ));
4001                                    new_entry_parent_id = None;
4002                                }
4003                                entry_iter.advance();
4004                                continue;
4005                            }
4006                            if auto_collapse_dirs && entry.kind.is_dir() {
4007                                auto_folded_ancestors.push(entry.id);
4008                                if !new_state.is_unfolded(&entry.id)
4009                                    && let Some(root_path) = worktree_snapshot.root_entry()
4010                                {
4011                                    let mut child_entries =
4012                                        worktree_snapshot.child_entries(&entry.path);
4013                                    if let Some(child) = child_entries.next()
4014                                        && entry.path != root_path.path
4015                                        && child_entries.next().is_none()
4016                                        && child.kind.is_dir()
4017                                    {
4018                                        entry_iter.advance();
4019
4020                                        continue;
4021                                    }
4022                                }
4023                                let depth = temporary_unfolded_pending_state
4024                                    .as_ref()
4025                                    .and_then(|state| {
4026                                        if state.previously_focused_leaf_entry.worktree_id
4027                                            == worktree_id
4028                                            && state.previously_focused_leaf_entry.entry_id
4029                                                == entry.id
4030                                        {
4031                                            auto_folded_ancestors.iter().rev().position(|id| {
4032                                                *id == state.temporarily_unfolded_active_entry_id
4033                                            })
4034                                        } else {
4035                                            None
4036                                        }
4037                                    })
4038                                    .unwrap_or_else(|| {
4039                                        old_ancestors
4040                                            .get(&entry.id)
4041                                            .map(|ancestor| ancestor.current_ancestor_depth)
4042                                            .unwrap_or_default()
4043                                    })
4044                                    .min(auto_folded_ancestors.len());
4045                                if let Some(edit_state) = &mut new_state.edit_state
4046                                    && edit_state.entry_id == entry.id
4047                                {
4048                                    edit_state.depth = depth;
4049                                }
4050                                let mut ancestors = std::mem::take(&mut auto_folded_ancestors);
4051                                if ancestors.len() > 1 {
4052                                    ancestors.reverse();
4053                                    new_state.ancestors.insert(
4054                                        entry.id,
4055                                        FoldedAncestors {
4056                                            current_ancestor_depth: depth,
4057                                            ancestors,
4058                                        },
4059                                    );
4060                                }
4061                            }
4062                            auto_folded_ancestors.clear();
4063                            if (!hide_gitignore || !entry.is_ignored)
4064                                && (!hide_hidden || !entry.is_hidden)
4065                            {
4066                                visible_worktree_entries.push(entry.to_owned());
4067                            }
4068                            let precedes_new_entry = if let Some(new_entry_id) = new_entry_parent_id
4069                            {
4070                                entry.id == new_entry_id || {
4071                                    new_state.ancestors.get(&entry.id).is_some_and(|entries| {
4072                                        entries.ancestors.contains(&new_entry_id)
4073                                    })
4074                                }
4075                            } else {
4076                                false
4077                            };
4078                            if precedes_new_entry
4079                                && (!hide_gitignore || !entry.is_ignored)
4080                                && (!hide_hidden || !entry.is_hidden)
4081                            {
4082                                visible_worktree_entries.push(Self::create_new_git_entry(
4083                                    entry.entry,
4084                                    entry.git_summary,
4085                                    new_entry_kind,
4086                                ));
4087                            }
4088
4089                            let (depth, chars) = if Some(entry.entry)
4090                                == worktree_snapshot.root_entry()
4091                            {
4092                                let Some(path_name) = worktree_abs_path.file_name() else {
4093                                    entry_iter.advance();
4094                                    continue;
4095                                };
4096                                let depth = 0;
4097                                (depth, path_name.to_string_lossy().chars().count())
4098                            } else if entry.is_file() {
4099                                let Some(path_name) = entry
4100                                    .path
4101                                    .file_name()
4102                                    .with_context(|| {
4103                                        format!("Non-root entry has no file name: {entry:?}")
4104                                    })
4105                                    .log_err()
4106                                else {
4107                                    continue;
4108                                };
4109                                let depth = entry.path.ancestors().count() - 1;
4110                                (depth, path_name.chars().count())
4111                            } else {
4112                                let path = new_state
4113                                    .ancestors
4114                                    .get(&entry.id)
4115                                    .and_then(|ancestors| {
4116                                        let outermost_ancestor = ancestors.ancestors.last()?;
4117                                        let root_folded_entry = worktree_snapshot
4118                                            .entry_for_id(*outermost_ancestor)?
4119                                            .path
4120                                            .as_ref();
4121                                        entry.path.strip_prefix(root_folded_entry).ok().and_then(
4122                                            |suffix| {
4123                                                Some(
4124                                                    RelPath::unix(root_folded_entry.file_name()?)
4125                                                        .unwrap()
4126                                                        .join(suffix),
4127                                                )
4128                                            },
4129                                        )
4130                                    })
4131                                    .or_else(|| {
4132                                        entry.path.file_name().map(|file_name| {
4133                                            RelPath::unix(file_name).unwrap().into()
4134                                        })
4135                                    })
4136                                    .unwrap_or_else(|| entry.path.clone());
4137                                let depth = path.components().count();
4138                                (depth, path.as_unix_str().chars().count())
4139                            };
4140                            let width_estimate =
4141                                item_width_estimate(depth, chars, entry.canonical_path.is_some());
4142
4143                            match max_width_item.as_mut() {
4144                                Some((id, worktree_id, width)) => {
4145                                    if *width < width_estimate {
4146                                        *id = entry.id;
4147                                        *worktree_id = worktree_snapshot.id();
4148                                        *width = width_estimate;
4149                                    }
4150                                }
4151                                None => {
4152                                    max_width_item =
4153                                        Some((entry.id, worktree_snapshot.id(), width_estimate))
4154                                }
4155                            }
4156
4157                            let expanded_dir_ids =
4158                                match new_state.expanded_dir_ids.entry(worktree_id) {
4159                                    hash_map::Entry::Occupied(e) => e.into_mut(),
4160                                    hash_map::Entry::Vacant(e) => {
4161                                        // The first time a worktree's root entry becomes available,
4162                                        // mark that root entry as expanded.
4163                                        if let Some(entry) = worktree_snapshot.root_entry() {
4164                                            e.insert(vec![entry.id]).as_slice()
4165                                        } else {
4166                                            &[]
4167                                        }
4168                                    }
4169                                };
4170
4171                            if expanded_dir_ids.binary_search(&entry.id).is_err()
4172                                && entry_iter.advance_to_sibling()
4173                            {
4174                                continue;
4175                            }
4176                            entry_iter.advance();
4177                        }
4178
4179                        par_sort_worktree_entries(
4180                            &mut visible_worktree_entries,
4181                            sort_mode,
4182                            sort_order,
4183                        );
4184                        new_state.visible_entries.push(VisibleEntriesForWorktree {
4185                            worktree_id,
4186                            entries: visible_worktree_entries,
4187                            index: OnceCell::new(),
4188                        })
4189                    }
4190                    if let Some((project_entry_id, worktree_id, _)) = max_width_item {
4191                        let mut visited_worktrees_length = 0;
4192                        let index = new_state
4193                            .visible_entries
4194                            .iter()
4195                            .find_map(|visible_entries| {
4196                                if worktree_id == visible_entries.worktree_id {
4197                                    visible_entries
4198                                        .entries
4199                                        .iter()
4200                                        .position(|entry| entry.id == project_entry_id)
4201                                } else {
4202                                    visited_worktrees_length += visible_entries.entries.len();
4203                                    None
4204                                }
4205                            });
4206                        if let Some(index) = index {
4207                            new_state.max_width_item_index = Some(visited_worktrees_length + index);
4208                        }
4209                    }
4210                    new_state
4211                })
4212                .await;
4213            this.update_in(cx, |this, window, cx| {
4214                this.state = new_state;
4215                if let Some((worktree_id, entry_id)) = new_selected_entry {
4216                    this.selection = Some(SelectedEntry {
4217                        worktree_id,
4218                        entry_id,
4219                    });
4220                }
4221                let elapsed = now.elapsed();
4222                if this.last_reported_update.elapsed() > Duration::from_secs(3600) {
4223                    telemetry::event!(
4224                        "Project Panel Updated",
4225                        elapsed_ms = elapsed.as_millis() as u64,
4226                        worktree_entries = this
4227                            .state
4228                            .visible_entries
4229                            .iter()
4230                            .map(|worktree| worktree.entries.len())
4231                            .sum::<usize>(),
4232                    )
4233                }
4234                if this.update_visible_entries_task.focus_filename_editor {
4235                    this.update_visible_entries_task.focus_filename_editor = false;
4236                    this.filename_editor.update(cx, |editor, cx| {
4237                        window.focus(&editor.focus_handle(cx), cx);
4238                    });
4239                }
4240                if this.update_visible_entries_task.autoscroll {
4241                    this.update_visible_entries_task.autoscroll = false;
4242                    this.autoscroll(cx);
4243                }
4244                cx.notify();
4245            })
4246            .ok();
4247        });
4248
4249        self.update_visible_entries_task = UpdateVisibleEntriesTask {
4250            _visible_entries_task: visible_entries_task,
4251            focus_filename_editor: focus_filename_editor
4252                || self.update_visible_entries_task.focus_filename_editor,
4253            autoscroll: autoscroll || self.update_visible_entries_task.autoscroll,
4254        };
4255    }
4256
4257    fn expand_entry(
4258        &mut self,
4259        worktree_id: WorktreeId,
4260        entry_id: ProjectEntryId,
4261        cx: &mut Context<Self>,
4262    ) {
4263        self.project.update(cx, |project, cx| {
4264            if let Some((worktree, expanded_dir_ids)) = project
4265                .worktree_for_id(worktree_id, cx)
4266                .zip(self.state.expanded_dir_ids.get_mut(&worktree_id))
4267            {
4268                project.expand_entry(worktree_id, entry_id, cx);
4269                let worktree = worktree.read(cx);
4270
4271                if let Some(mut entry) = worktree.entry_for_id(entry_id) {
4272                    loop {
4273                        if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) {
4274                            expanded_dir_ids.insert(ix, entry.id);
4275                        }
4276
4277                        if let Some(parent_entry) =
4278                            entry.path.parent().and_then(|p| worktree.entry_for_path(p))
4279                        {
4280                            entry = parent_entry;
4281                        } else {
4282                            break;
4283                        }
4284                    }
4285                }
4286            }
4287        });
4288    }
4289
4290    fn drop_external_files(
4291        &mut self,
4292        paths: &[PathBuf],
4293        entry_id: ProjectEntryId,
4294        window: &mut Window,
4295        cx: &mut Context<Self>,
4296    ) {
4297        let mut paths: Vec<Arc<Path>> = paths.iter().map(|path| Arc::from(path.clone())).collect();
4298
4299        let open_file_after_drop = paths.len() == 1 && paths[0].is_file();
4300
4301        let Some((target_directory, worktree, fs)) = maybe!({
4302            let project = self.project.read(cx);
4303            let fs = project.fs().clone();
4304            let worktree = project.worktree_for_entry(entry_id, cx)?;
4305            let entry = worktree.read(cx).entry_for_id(entry_id)?;
4306            let path = entry.path.clone();
4307            let target_directory = if entry.is_dir() {
4308                path
4309            } else {
4310                path.parent()?.into()
4311            };
4312            Some((target_directory, worktree, fs))
4313        }) else {
4314            return;
4315        };
4316
4317        let mut paths_to_replace = Vec::new();
4318        for path in &paths {
4319            if let Some(name) = path.file_name()
4320                && let Some(name) = name.to_str()
4321            {
4322                let target_path = target_directory.join(RelPath::unix(name).unwrap());
4323                if worktree.read(cx).entry_for_path(&target_path).is_some() {
4324                    paths_to_replace.push((name.to_string(), path.clone()));
4325                }
4326            }
4327        }
4328
4329        cx.spawn_in(window, async move |this, cx| {
4330            async move {
4331                for (filename, original_path) in &paths_to_replace {
4332                    let prompt_message = format!(
4333                        concat!(
4334                            "A file or folder with name {} ",
4335                            "already exists in the destination folder. ",
4336                            "Do you want to replace it?"
4337                        ),
4338                        filename
4339                    );
4340                    let answer = cx
4341                        .update(|window, cx| {
4342                            window.prompt(
4343                                PromptLevel::Info,
4344                                &prompt_message,
4345                                None,
4346                                &["Replace", "Cancel"],
4347                                cx,
4348                            )
4349                        })?
4350                        .await?;
4351
4352                    if answer == 1
4353                        && let Some(item_idx) = paths.iter().position(|p| p == original_path)
4354                    {
4355                        paths.remove(item_idx);
4356                    }
4357                }
4358
4359                if paths.is_empty() {
4360                    return Ok(());
4361                }
4362
4363                let (worktree_id, task) = worktree.update(cx, |worktree, cx| {
4364                    (
4365                        worktree.id(),
4366                        worktree.copy_external_entries(target_directory, paths, fs, cx),
4367                    )
4368                });
4369
4370                let opened_entries: Vec<_> = task
4371                    .await
4372                    .with_context(|| "failed to copy external paths")?;
4373                this.update_in(cx, |this, window, cx| {
4374                    let mut did_open = false;
4375                    if open_file_after_drop && !opened_entries.is_empty() {
4376                        let settings = ProjectPanelSettings::get_global(cx);
4377                        if settings.auto_open.should_open_on_drop() {
4378                            this.open_entry(opened_entries[0], true, false, cx);
4379                            did_open = true;
4380                        }
4381                    }
4382
4383                    if !did_open {
4384                        let new_selection = opened_entries
4385                            .last()
4386                            .map(|&entry_id| (worktree_id, entry_id));
4387                        for &entry_id in &opened_entries {
4388                            this.expand_entry(worktree_id, entry_id, cx);
4389                        }
4390                        this.marked_entries.clear();
4391                        this.update_visible_entries(new_selection, false, false, window, cx);
4392                    }
4393
4394                    let changes: Vec<Change> = opened_entries
4395                        .iter()
4396                        .filter_map(|entry_id| {
4397                            worktree.read(cx).entry_for_id(*entry_id).map(|entry| {
4398                                Change::Created(ProjectPath {
4399                                    worktree_id,
4400                                    path: entry.path.clone(),
4401                                })
4402                            })
4403                        })
4404                        .collect();
4405
4406                    this.undo_manager.record(changes).log_err();
4407                })
4408            }
4409            .log_err()
4410            .await
4411        })
4412        .detach();
4413    }
4414
4415    fn refresh_drag_cursor_style(
4416        &self,
4417        modifiers: &Modifiers,
4418        window: &mut Window,
4419        cx: &mut Context<Self>,
4420    ) {
4421        if let Some(existing_cursor) = cx.active_drag_cursor_style() {
4422            let new_cursor = if Self::is_copy_modifier_set(modifiers) {
4423                CursorStyle::DragCopy
4424            } else {
4425                CursorStyle::PointingHand
4426            };
4427            if existing_cursor != new_cursor {
4428                cx.set_active_drag_cursor_style(new_cursor, window);
4429            }
4430        }
4431    }
4432
4433    fn is_copy_modifier_set(modifiers: &Modifiers) -> bool {
4434        cfg!(target_os = "macos") && modifiers.alt
4435            || cfg!(not(target_os = "macos")) && modifiers.control
4436    }
4437
4438    fn drag_onto(
4439        &mut self,
4440        selections: &DraggedSelection,
4441        target_entry_id: ProjectEntryId,
4442        is_file: bool,
4443        window: &mut Window,
4444        cx: &mut Context<Self>,
4445    ) {
4446        let resolved_selections = selections
4447            .items()
4448            .map(|entry| SelectedEntry {
4449                entry_id: self.resolve_entry(entry.entry_id),
4450                worktree_id: entry.worktree_id,
4451            })
4452            .collect::<BTreeSet<SelectedEntry>>();
4453        let entries = self.disjoint_entries(resolved_selections, cx);
4454
4455        if Self::is_copy_modifier_set(&window.modifiers()) {
4456            let _ = maybe!({
4457                let project = self.project.read(cx);
4458                let target_worktree = project.worktree_for_entry(target_entry_id, cx)?;
4459                let worktree_id = target_worktree.read(cx).id();
4460                let target_entry = target_worktree
4461                    .read(cx)
4462                    .entry_for_id(target_entry_id)?
4463                    .clone();
4464
4465                let mut copy_tasks = Vec::new();
4466                let mut disambiguation_range = None;
4467                for selection in &entries {
4468                    let (new_path, new_disambiguation_range) = self.create_paste_path(
4469                        selection,
4470                        (target_worktree.clone(), &target_entry),
4471                        cx,
4472                    )?;
4473
4474                    let task = self.project.update(cx, |project, cx| {
4475                        project.copy_entry(selection.entry_id, (worktree_id, new_path).into(), cx)
4476                    });
4477                    copy_tasks.push(task);
4478                    disambiguation_range = new_disambiguation_range.or(disambiguation_range);
4479                }
4480
4481                let item_count = copy_tasks.len();
4482
4483                cx.spawn_in(window, async move |project_panel, cx| {
4484                    let mut last_succeed = None;
4485                    let mut changes = Vec::new();
4486                    for task in copy_tasks.into_iter() {
4487                        if let Some(Some(entry)) = task.await.log_err() {
4488                            last_succeed = Some(entry.id);
4489                            changes.push(Change::Created((worktree_id, entry.path).into()));
4490                        }
4491                    }
4492                    // update selection
4493                    if let Some(entry_id) = last_succeed {
4494                        project_panel.update_in(cx, |project_panel, window, cx| {
4495                            project_panel.selection = Some(SelectedEntry {
4496                                worktree_id,
4497                                entry_id,
4498                            });
4499                            // if only one entry was dragged and it was disambiguated, open the rename editor
4500                            if item_count == 1 && disambiguation_range.is_some() {
4501                                project_panel.rename_impl(disambiguation_range, window, cx);
4502                            }
4503
4504                            project_panel.undo_manager.record(changes)
4505                        })??;
4506                    }
4507
4508                    std::result::Result::Ok::<(), anyhow::Error>(())
4509                })
4510                .detach();
4511                Some(())
4512            });
4513        } else {
4514            let update_marks = !self.marked_entries.is_empty();
4515            let active_selection = selections.active_selection;
4516
4517            // For folded selections, track the leaf suffix relative to the resolved
4518            // entry so we can refresh it after the move completes.
4519            let (folded_selection_info, folded_selection_entries): (
4520                Vec<(ProjectEntryId, RelPathBuf)>,
4521                HashSet<SelectedEntry>,
4522            ) = {
4523                let project = self.project.read(cx);
4524                let mut info = Vec::new();
4525                let mut folded_entries = HashSet::default();
4526
4527                for selection in selections.items() {
4528                    let resolved_id = self.resolve_entry(selection.entry_id);
4529                    if resolved_id == selection.entry_id {
4530                        continue;
4531                    }
4532                    folded_entries.insert(*selection);
4533                    let Some(source_path) = project.path_for_entry(resolved_id, cx) else {
4534                        continue;
4535                    };
4536                    let Some(leaf_path) = project.path_for_entry(selection.entry_id, cx) else {
4537                        continue;
4538                    };
4539                    let Ok(suffix) = leaf_path.path.strip_prefix(source_path.path.as_ref()) else {
4540                        continue;
4541                    };
4542                    if suffix.as_unix_str().is_empty() {
4543                        continue;
4544                    }
4545
4546                    info.push((resolved_id, suffix.to_rel_path_buf()));
4547                }
4548                (info, folded_entries)
4549            };
4550
4551            // Capture old paths before moving so we can record undo operations.
4552            let old_paths: HashMap<ProjectEntryId, ProjectPath> = {
4553                let project = self.project.read(cx);
4554                entries
4555                    .iter()
4556                    .filter_map(|entry| {
4557                        let path = project.path_for_entry(entry.entry_id, cx)?;
4558                        Some((entry.entry_id, path))
4559                    })
4560                    .collect()
4561            };
4562            let destination_worktree_id = self
4563                .project
4564                .read(cx)
4565                .worktree_for_entry(target_entry_id, cx)
4566                .map(|wt| wt.read(cx).id());
4567
4568            // Collect move tasks paired with their source entry ID so we can correlate
4569            // results with folded selections that need refreshing.
4570            let mut move_tasks: Vec<(ProjectEntryId, Task<Result<CreatedEntry>>)> = Vec::new();
4571            for entry in entries {
4572                if let Some(task) = self.move_entry(entry.entry_id, target_entry_id, is_file, cx) {
4573                    move_tasks.push((entry.entry_id, task));
4574                }
4575            }
4576
4577            if move_tasks.is_empty() {
4578                return;
4579            }
4580
4581            let workspace = self.workspace.clone();
4582            if folded_selection_info.is_empty() {
4583                cx.spawn_in(window, async move |project_panel, mut cx| {
4584                    let mut changes = Vec::new();
4585                    for (entry_id, task) in move_tasks {
4586                        if let Some(CreatedEntry::Included(new_entry)) = task
4587                            .await
4588                            .notify_workspace_async_err(workspace.clone(), &mut cx)
4589                        {
4590                            if let (Some(old_path), Some(worktree_id)) =
4591                                (old_paths.get(&entry_id), destination_worktree_id)
4592                            {
4593                                changes.push(Change::Renamed(
4594                                    old_path.clone(),
4595                                    (worktree_id, new_entry.path).into(),
4596                                ));
4597                            }
4598                        }
4599                    }
4600                    project_panel
4601                        .update(cx, |this, _| {
4602                            this.undo_manager.record(changes).log_err();
4603                        })
4604                        .ok();
4605                })
4606                .detach();
4607            } else {
4608                cx.spawn_in(window, async move |project_panel, mut cx| {
4609                    // Await all move tasks and collect successful results
4610                    let mut move_results: Vec<(ProjectEntryId, Entry)> = Vec::new();
4611                    let mut operations = Vec::new();
4612                    for (entry_id, task) in move_tasks {
4613                        if let Some(CreatedEntry::Included(new_entry)) = task
4614                            .await
4615                            .notify_workspace_async_err(workspace.clone(), &mut cx)
4616                        {
4617                            if let (Some(old_path), Some(worktree_id)) =
4618                                (old_paths.get(&entry_id), destination_worktree_id)
4619                            {
4620                                operations.push(Change::Renamed(
4621                                    old_path.clone(),
4622                                    (worktree_id, new_entry.path.clone()).into(),
4623                                ));
4624                            }
4625                            move_results.push((entry_id, new_entry));
4626                        }
4627                    }
4628
4629                    if move_results.is_empty() {
4630                        return;
4631                    }
4632
4633                    project_panel
4634                        .update(cx, |this, _| {
4635                            this.undo_manager.record(operations).log_err();
4636                        })
4637                        .ok();
4638
4639                    // For folded selections, we need to refresh the leaf paths (with suffixes)
4640                    // because they may not be indexed yet after the parent directory was moved.
4641                    // First collect the paths to refresh, then refresh them.
4642                    let paths_to_refresh: Vec<(Entity<Worktree>, Arc<RelPath>)> = project_panel
4643                        .update(cx, |project_panel, cx| {
4644                            let project = project_panel.project.read(cx);
4645                            folded_selection_info
4646                                .iter()
4647                                .filter_map(|(resolved_id, suffix)| {
4648                                    let (_, new_entry) =
4649                                        move_results.iter().find(|(id, _)| id == resolved_id)?;
4650                                    let worktree = project.worktree_for_entry(new_entry.id, cx)?;
4651                                    let leaf_path = new_entry.path.join(suffix);
4652                                    Some((worktree, leaf_path))
4653                                })
4654                                .collect()
4655                        })
4656                        .ok()
4657                        .unwrap_or_default();
4658
4659                    let refresh_tasks: Vec<_> = paths_to_refresh
4660                        .into_iter()
4661                        .filter_map(|(worktree, leaf_path)| {
4662                            worktree.update(cx, |worktree, cx| {
4663                                worktree
4664                                    .as_local_mut()
4665                                    .map(|local| local.refresh_entry(leaf_path, None, cx))
4666                            })
4667                        })
4668                        .collect();
4669
4670                    for task in refresh_tasks {
4671                        task.await.log_err();
4672                    }
4673
4674                    if update_marks && !folded_selection_entries.is_empty() {
4675                        project_panel
4676                            .update(cx, |project_panel, cx| {
4677                                project_panel.marked_entries.retain(|entry| {
4678                                    !folded_selection_entries.contains(entry)
4679                                        || *entry == active_selection
4680                                });
4681                                cx.notify();
4682                            })
4683                            .ok();
4684                    }
4685                })
4686                .detach();
4687            }
4688        }
4689    }
4690
4691    fn index_for_entry(
4692        &self,
4693        entry_id: ProjectEntryId,
4694        worktree_id: WorktreeId,
4695    ) -> Option<(usize, usize, usize)> {
4696        let mut total_ix = 0;
4697        for (worktree_ix, visible) in self.state.visible_entries.iter().enumerate() {
4698            if worktree_id != visible.worktree_id {
4699                total_ix += visible.entries.len();
4700                continue;
4701            }
4702
4703            return visible
4704                .entries
4705                .iter()
4706                .enumerate()
4707                .find(|(_, entry)| entry.id == entry_id)
4708                .map(|(ix, _)| (worktree_ix, ix, total_ix + ix));
4709        }
4710        None
4711    }
4712
4713    fn entry_at_index(&self, index: usize) -> Option<(WorktreeId, GitEntryRef<'_>)> {
4714        let mut offset = 0;
4715        for worktree in &self.state.visible_entries {
4716            let current_len = worktree.entries.len();
4717            if index < offset + current_len {
4718                return worktree
4719                    .entries
4720                    .get(index - offset)
4721                    .map(|entry| (worktree.worktree_id, entry.to_ref()));
4722            }
4723            offset += current_len;
4724        }
4725        None
4726    }
4727
4728    fn iter_visible_entries(
4729        &self,
4730        range: Range<usize>,
4731        window: &mut Window,
4732        cx: &mut Context<ProjectPanel>,
4733        callback: &mut dyn FnMut(
4734            &Entry,
4735            usize,
4736            &HashSet<Arc<RelPath>>,
4737            &mut Window,
4738            &mut Context<ProjectPanel>,
4739        ),
4740    ) {
4741        let mut ix = 0;
4742        for visible in &self.state.visible_entries {
4743            if ix >= range.end {
4744                return;
4745            }
4746
4747            if ix + visible.entries.len() <= range.start {
4748                ix += visible.entries.len();
4749                continue;
4750            }
4751
4752            let end_ix = range.end.min(ix + visible.entries.len());
4753            let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
4754            let entries = visible
4755                .index
4756                .get_or_init(|| visible.entries.iter().map(|e| e.path.clone()).collect());
4757            let base_index = ix + entry_range.start;
4758            for (i, entry) in visible.entries[entry_range].iter().enumerate() {
4759                let global_index = base_index + i;
4760                callback(entry, global_index, entries, window, cx);
4761            }
4762            ix = end_ix;
4763        }
4764    }
4765
4766    fn for_each_visible_entry(
4767        &self,
4768        range: Range<usize>,
4769        window: &mut Window,
4770        cx: &mut Context<ProjectPanel>,
4771        callback: &mut dyn FnMut(
4772            ProjectEntryId,
4773            EntryDetails,
4774            &mut Window,
4775            &mut Context<ProjectPanel>,
4776        ),
4777    ) {
4778        let mut ix = 0;
4779        for visible in &self.state.visible_entries {
4780            if ix >= range.end {
4781                return;
4782            }
4783
4784            if ix + visible.entries.len() <= range.start {
4785                ix += visible.entries.len();
4786                continue;
4787            }
4788
4789            let end_ix = range.end.min(ix + visible.entries.len());
4790            let git_status_setting = {
4791                let settings = ProjectPanelSettings::get_global(cx);
4792                settings.git_status
4793            };
4794            if let Some(worktree) = self
4795                .project
4796                .read(cx)
4797                .worktree_for_id(visible.worktree_id, cx)
4798            {
4799                let snapshot = worktree.read(cx).snapshot();
4800                let root_name = snapshot.root_name();
4801
4802                let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
4803                let entries = visible
4804                    .index
4805                    .get_or_init(|| visible.entries.iter().map(|e| e.path.clone()).collect());
4806                for entry in visible.entries[entry_range].iter() {
4807                    let status = git_status_setting
4808                        .then_some(entry.git_summary)
4809                        .unwrap_or_default();
4810
4811                    let mut details = self.details_for_entry(
4812                        entry,
4813                        visible.worktree_id,
4814                        root_name,
4815                        entries,
4816                        status,
4817                        None,
4818                        window,
4819                        cx,
4820                    );
4821
4822                    if let Some(edit_state) = &self.state.edit_state {
4823                        let is_edited_entry = if edit_state.is_new_entry() {
4824                            entry.id == NEW_ENTRY_ID
4825                        } else {
4826                            entry.id == edit_state.entry_id
4827                                || self.state.ancestors.get(&entry.id).is_some_and(
4828                                    |auto_folded_dirs| {
4829                                        auto_folded_dirs.ancestors.contains(&edit_state.entry_id)
4830                                    },
4831                                )
4832                        };
4833
4834                        if is_edited_entry {
4835                            if let Some(processing_filename) = &edit_state.processing_filename {
4836                                details.is_processing = true;
4837                                if let Some(ancestors) = edit_state
4838                                    .leaf_entry_id
4839                                    .and_then(|entry| self.state.ancestors.get(&entry))
4840                                {
4841                                    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;
4842                                    let all_components = ancestors.ancestors.len();
4843
4844                                    let prefix_components = all_components - position;
4845                                    let suffix_components = position.checked_sub(1);
4846                                    let mut previous_components =
4847                                        Path::new(&details.filename).components();
4848                                    let mut new_path = previous_components
4849                                        .by_ref()
4850                                        .take(prefix_components)
4851                                        .collect::<PathBuf>();
4852                                    if let Some(last_component) =
4853                                        processing_filename.components().next_back()
4854                                    {
4855                                        new_path.push(last_component);
4856                                        previous_components.next();
4857                                    }
4858
4859                                    if suffix_components.is_some() {
4860                                        new_path.push(previous_components);
4861                                    }
4862                                    if let Some(str) = new_path.to_str() {
4863                                        details.filename.clear();
4864                                        details.filename.push_str(str);
4865                                    }
4866                                } else {
4867                                    details.filename.clear();
4868                                    details.filename.push_str(processing_filename.as_unix_str());
4869                                }
4870                            } else {
4871                                if edit_state.is_new_entry() {
4872                                    details.filename.clear();
4873                                }
4874                                details.is_editing = true;
4875                            }
4876                        }
4877                    }
4878
4879                    callback(entry.id, details, window, cx);
4880                }
4881            }
4882            ix = end_ix;
4883        }
4884    }
4885
4886    fn find_entry_in_worktree(
4887        &self,
4888        worktree_id: WorktreeId,
4889        reverse_search: bool,
4890        only_visible_entries: bool,
4891        predicate: &dyn Fn(GitEntryRef, WorktreeId) -> bool,
4892        cx: &mut Context<Self>,
4893    ) -> Option<GitEntry> {
4894        if only_visible_entries {
4895            let entries = self
4896                .state
4897                .visible_entries
4898                .iter()
4899                .find_map(|visible| {
4900                    if worktree_id == visible.worktree_id {
4901                        Some(&visible.entries)
4902                    } else {
4903                        None
4904                    }
4905                })?
4906                .clone();
4907
4908            return utils::ReversibleIterable::new(entries.iter(), reverse_search)
4909                .find(|ele| predicate(ele.to_ref(), worktree_id))
4910                .cloned();
4911        }
4912
4913        let repo_snapshots = self
4914            .project
4915            .read(cx)
4916            .git_store()
4917            .read(cx)
4918            .repo_snapshots(cx);
4919        let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?;
4920        worktree.read_with(cx, |tree, _| {
4921            utils::ReversibleIterable::new(
4922                GitTraversal::new(&repo_snapshots, tree.entries(true, 0usize)),
4923                reverse_search,
4924            )
4925            .find_single_ended(|ele| predicate(*ele, worktree_id))
4926            .map(|ele| ele.to_owned())
4927        })
4928    }
4929
4930    fn find_entry(
4931        &self,
4932        start: Option<&SelectedEntry>,
4933        reverse_search: bool,
4934        predicate: &dyn Fn(GitEntryRef, WorktreeId) -> bool,
4935        cx: &mut Context<Self>,
4936    ) -> Option<SelectedEntry> {
4937        let mut worktree_ids: Vec<_> = self
4938            .state
4939            .visible_entries
4940            .iter()
4941            .map(|worktree| worktree.worktree_id)
4942            .collect();
4943        let repo_snapshots = self
4944            .project
4945            .read(cx)
4946            .git_store()
4947            .read(cx)
4948            .repo_snapshots(cx);
4949
4950        let mut last_found: Option<SelectedEntry> = None;
4951
4952        if let Some(start) = start {
4953            let worktree = self
4954                .project
4955                .read(cx)
4956                .worktree_for_id(start.worktree_id, cx)?
4957                .read(cx);
4958
4959            let search = {
4960                let entry = worktree.entry_for_id(start.entry_id)?;
4961                let root_entry = worktree.root_entry()?;
4962                let tree_id = worktree.id();
4963
4964                let mut first_iter = GitTraversal::new(
4965                    &repo_snapshots,
4966                    worktree.traverse_from_path(true, true, true, entry.path.as_ref()),
4967                );
4968
4969                if reverse_search {
4970                    first_iter.next();
4971                }
4972
4973                let first = first_iter
4974                    .enumerate()
4975                    .take_until(|(count, entry)| entry.entry == root_entry && *count != 0usize)
4976                    .map(|(_, entry)| entry)
4977                    .find(|ele| predicate(*ele, tree_id))
4978                    .map(|ele| ele.to_owned());
4979
4980                let second_iter =
4981                    GitTraversal::new(&repo_snapshots, worktree.entries(true, 0usize));
4982
4983                let second = if reverse_search {
4984                    second_iter
4985                        .take_until(|ele| ele.id == start.entry_id)
4986                        .filter(|ele| predicate(*ele, tree_id))
4987                        .last()
4988                        .map(|ele| ele.to_owned())
4989                } else {
4990                    second_iter
4991                        .take_while(|ele| ele.id != start.entry_id)
4992                        .filter(|ele| predicate(*ele, tree_id))
4993                        .last()
4994                        .map(|ele| ele.to_owned())
4995                };
4996
4997                if reverse_search {
4998                    Some((second, first))
4999                } else {
5000                    Some((first, second))
5001                }
5002            };
5003
5004            if let Some((first, second)) = search {
5005                let first = first.map(|entry| SelectedEntry {
5006                    worktree_id: start.worktree_id,
5007                    entry_id: entry.id,
5008                });
5009
5010                let second = second.map(|entry| SelectedEntry {
5011                    worktree_id: start.worktree_id,
5012                    entry_id: entry.id,
5013                });
5014
5015                if first.is_some() {
5016                    return first;
5017                }
5018                last_found = second;
5019
5020                let idx = worktree_ids
5021                    .iter()
5022                    .enumerate()
5023                    .find(|(_, ele)| **ele == start.worktree_id)
5024                    .map(|(idx, _)| idx);
5025
5026                if let Some(idx) = idx {
5027                    worktree_ids.rotate_left(idx + 1usize);
5028                    worktree_ids.pop();
5029                }
5030            }
5031        }
5032
5033        for tree_id in worktree_ids.into_iter() {
5034            if let Some(found) =
5035                self.find_entry_in_worktree(tree_id, reverse_search, false, &predicate, cx)
5036            {
5037                return Some(SelectedEntry {
5038                    worktree_id: tree_id,
5039                    entry_id: found.id,
5040                });
5041            }
5042        }
5043
5044        last_found
5045    }
5046
5047    fn find_visible_entry(
5048        &self,
5049        start: Option<&SelectedEntry>,
5050        reverse_search: bool,
5051        predicate: &dyn Fn(GitEntryRef, WorktreeId) -> bool,
5052        cx: &mut Context<Self>,
5053    ) -> Option<SelectedEntry> {
5054        let mut worktree_ids: Vec<_> = self
5055            .state
5056            .visible_entries
5057            .iter()
5058            .map(|worktree| worktree.worktree_id)
5059            .collect();
5060
5061        let mut last_found: Option<SelectedEntry> = None;
5062
5063        if let Some(start) = start {
5064            let entries = self
5065                .state
5066                .visible_entries
5067                .iter()
5068                .find(|worktree| worktree.worktree_id == start.worktree_id)
5069                .map(|worktree| &worktree.entries)?;
5070
5071            let mut start_idx = entries
5072                .iter()
5073                .enumerate()
5074                .find(|(_, ele)| ele.id == start.entry_id)
5075                .map(|(idx, _)| idx)?;
5076
5077            if reverse_search {
5078                start_idx = start_idx.saturating_add(1usize);
5079            }
5080
5081            let (left, right) = entries.split_at_checked(start_idx)?;
5082
5083            let (first_iter, second_iter) = if reverse_search {
5084                (
5085                    utils::ReversibleIterable::new(left.iter(), reverse_search),
5086                    utils::ReversibleIterable::new(right.iter(), reverse_search),
5087                )
5088            } else {
5089                (
5090                    utils::ReversibleIterable::new(right.iter(), reverse_search),
5091                    utils::ReversibleIterable::new(left.iter(), reverse_search),
5092                )
5093            };
5094
5095            let first_search = first_iter.find(|ele| predicate(ele.to_ref(), start.worktree_id));
5096            let second_search = second_iter.find(|ele| predicate(ele.to_ref(), start.worktree_id));
5097
5098            if first_search.is_some() {
5099                return first_search.map(|entry| SelectedEntry {
5100                    worktree_id: start.worktree_id,
5101                    entry_id: entry.id,
5102                });
5103            }
5104
5105            last_found = second_search.map(|entry| SelectedEntry {
5106                worktree_id: start.worktree_id,
5107                entry_id: entry.id,
5108            });
5109
5110            let idx = worktree_ids
5111                .iter()
5112                .enumerate()
5113                .find(|(_, ele)| **ele == start.worktree_id)
5114                .map(|(idx, _)| idx);
5115
5116            if let Some(idx) = idx {
5117                worktree_ids.rotate_left(idx + 1usize);
5118                worktree_ids.pop();
5119            }
5120        }
5121
5122        for tree_id in worktree_ids.into_iter() {
5123            if let Some(found) =
5124                self.find_entry_in_worktree(tree_id, reverse_search, true, &predicate, cx)
5125            {
5126                return Some(SelectedEntry {
5127                    worktree_id: tree_id,
5128                    entry_id: found.id,
5129                });
5130            }
5131        }
5132
5133        last_found
5134    }
5135
5136    fn calculate_depth_and_difference(
5137        entry: &Entry,
5138        visible_worktree_entries: &HashSet<Arc<RelPath>>,
5139    ) -> (usize, usize) {
5140        let (depth, difference) = entry
5141            .path
5142            .ancestors()
5143            .skip(1) // Skip the entry itself
5144            .find_map(|ancestor| {
5145                if let Some(parent_entry) = visible_worktree_entries.get(ancestor) {
5146                    let entry_path_components_count = entry.path.components().count();
5147                    let parent_path_components_count = parent_entry.components().count();
5148                    let difference = entry_path_components_count - parent_path_components_count;
5149                    let depth = parent_entry
5150                        .ancestors()
5151                        .skip(1)
5152                        .filter(|ancestor| visible_worktree_entries.contains(*ancestor))
5153                        .count();
5154                    Some((depth + 1, difference))
5155                } else {
5156                    None
5157                }
5158            })
5159            .unwrap_or_else(|| (0, entry.path.components().count()));
5160
5161        (depth, difference)
5162    }
5163
5164    fn highlight_entry_for_external_drag(
5165        &self,
5166        target_entry: &Entry,
5167        target_worktree: &Worktree,
5168    ) -> Option<ProjectEntryId> {
5169        // Always highlight directory or parent directory if it's file
5170        if target_entry.is_dir() {
5171            Some(target_entry.id)
5172        } else {
5173            target_entry
5174                .path
5175                .parent()
5176                .and_then(|parent_path| target_worktree.entry_for_path(parent_path))
5177                .map(|parent_entry| parent_entry.id)
5178        }
5179    }
5180
5181    fn highlight_entry_for_selection_drag(
5182        &self,
5183        target_entry: &Entry,
5184        target_worktree: &Worktree,
5185        drag_state: &DraggedSelection,
5186        cx: &Context<Self>,
5187    ) -> Option<ProjectEntryId> {
5188        let target_parent_path = target_entry.path.parent();
5189
5190        // In case of single item drag, we do not highlight existing
5191        // directory which item belongs too
5192        if drag_state.items().count() == 1
5193            && drag_state.active_selection.worktree_id == target_worktree.id()
5194        {
5195            let active_entry_path = self
5196                .project
5197                .read(cx)
5198                .path_for_entry(drag_state.active_selection.entry_id, cx)?;
5199
5200            if let Some(active_parent_path) = active_entry_path.path.parent() {
5201                // Do not highlight active entry parent
5202                if active_parent_path == target_entry.path.as_ref() {
5203                    return None;
5204                }
5205
5206                // Do not highlight active entry sibling files
5207                if Some(active_parent_path) == target_parent_path && target_entry.is_file() {
5208                    return None;
5209                }
5210            }
5211        }
5212
5213        // Always highlight directory or parent directory if it's file
5214        if target_entry.is_dir() {
5215            Some(target_entry.id)
5216        } else {
5217            target_parent_path
5218                .and_then(|parent_path| target_worktree.entry_for_path(parent_path))
5219                .map(|parent_entry| parent_entry.id)
5220        }
5221    }
5222
5223    fn should_highlight_background_for_selection_drag(
5224        &self,
5225        drag_state: &DraggedSelection,
5226        last_root_id: ProjectEntryId,
5227        cx: &App,
5228    ) -> bool {
5229        // Always highlight for multiple entries
5230        if drag_state.items().count() > 1 {
5231            return true;
5232        }
5233
5234        // Since root will always have empty relative path
5235        if let Some(entry_path) = self
5236            .project
5237            .read(cx)
5238            .path_for_entry(drag_state.active_selection.entry_id, cx)
5239        {
5240            if let Some(parent_path) = entry_path.path.parent() {
5241                if !parent_path.is_empty() {
5242                    return true;
5243                }
5244            }
5245        }
5246
5247        // If parent is empty, check if different worktree
5248        if let Some(last_root_worktree_id) = self
5249            .project
5250            .read(cx)
5251            .worktree_id_for_entry(last_root_id, cx)
5252        {
5253            if drag_state.active_selection.worktree_id != last_root_worktree_id {
5254                return true;
5255            }
5256        }
5257
5258        false
5259    }
5260
5261    fn render_entry(
5262        &self,
5263        entry_id: ProjectEntryId,
5264        details: EntryDetails,
5265        window: &mut Window,
5266        cx: &mut Context<Self>,
5267    ) -> Stateful<Div> {
5268        const GROUP_NAME: &str = "project_entry";
5269
5270        let kind = details.kind;
5271        let is_sticky = details.sticky.is_some();
5272        let sticky_index = details.sticky.as_ref().map(|this| this.sticky_index);
5273        let settings = ProjectPanelSettings::get_global(cx);
5274        let show_editor = details.is_editing && !details.is_processing;
5275
5276        let selection = SelectedEntry {
5277            worktree_id: details.worktree_id,
5278            entry_id,
5279        };
5280
5281        let is_marked = self.marked_entries.contains(&selection);
5282        let is_active = self
5283            .selection
5284            .is_some_and(|selection| selection.entry_id == entry_id);
5285
5286        let file_name = details.filename.clone();
5287
5288        let mut icon = details.icon.clone();
5289        if settings.file_icons && show_editor && details.kind.is_file() {
5290            let filename = self.filename_editor.read(cx).text(cx);
5291            if filename.len() > 2 {
5292                icon = FileIcons::get_icon(Path::new(&filename), cx);
5293            }
5294        }
5295
5296        let filename_text_color = details.filename_text_color;
5297        let diagnostic_severity = details.diagnostic_severity;
5298        let diagnostic_count = details.diagnostic_count;
5299        let item_colors = get_item_color(is_sticky, cx);
5300
5301        let canonical_path = details
5302            .canonical_path
5303            .as_ref()
5304            .map(|f| f.to_string_lossy().into_owned());
5305        let path_style = self.project.read(cx).path_style(cx);
5306        let path = details.path.clone();
5307        let path_for_external_paths = path.clone();
5308        let path_for_dragged_selection = path.clone();
5309
5310        let depth = details.depth;
5311        let worktree_id = details.worktree_id;
5312        let dragged_selection = DraggedSelection {
5313            active_selection: SelectedEntry {
5314                worktree_id: selection.worktree_id,
5315                entry_id: selection.entry_id,
5316            },
5317            marked_selections: Arc::from(self.marked_entries.clone()),
5318        };
5319
5320        let bg_color = if is_marked {
5321            item_colors.marked
5322        } else {
5323            item_colors.default
5324        };
5325
5326        let bg_hover_color = if is_marked {
5327            item_colors.marked
5328        } else {
5329            item_colors.hover
5330        };
5331
5332        let validation_color_and_message = if show_editor {
5333            match self
5334                .state
5335                .edit_state
5336                .as_ref()
5337                .map_or(ValidationState::None, |e| e.validation_state.clone())
5338            {
5339                ValidationState::Error(msg) => Some((Color::Error.color(cx), msg)),
5340                ValidationState::Warning(msg) => Some((Color::Warning.color(cx), msg)),
5341                ValidationState::None => None,
5342            }
5343        } else {
5344            None
5345        };
5346
5347        let border_color =
5348            if !self.mouse_down && is_active && self.focus_handle.contains_focused(window, cx) {
5349                match validation_color_and_message {
5350                    Some((color, _)) => color,
5351                    None => item_colors.focused,
5352                }
5353            } else {
5354                bg_color
5355            };
5356
5357        let border_hover_color =
5358            if !self.mouse_down && is_active && self.focus_handle.contains_focused(window, cx) {
5359                match validation_color_and_message {
5360                    Some((color, _)) => color,
5361                    None => item_colors.focused,
5362                }
5363            } else {
5364                bg_hover_color
5365            };
5366
5367        let folded_directory_drag_target = self.folded_directory_drag_target;
5368        let is_highlighted = {
5369            if let Some(highlight_entry_id) =
5370                self.drag_target_entry
5371                    .as_ref()
5372                    .and_then(|drag_target| match drag_target {
5373                        DragTarget::Entry {
5374                            highlight_entry_id, ..
5375                        } => Some(*highlight_entry_id),
5376                        DragTarget::Background => self.state.last_worktree_root_id,
5377                    })
5378            {
5379                // Highlight if same entry or it's children
5380                if entry_id == highlight_entry_id {
5381                    true
5382                } else {
5383                    maybe!({
5384                        let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?;
5385                        let highlight_entry = worktree.read(cx).entry_for_id(highlight_entry_id)?;
5386                        Some(path.starts_with(&highlight_entry.path))
5387                    })
5388                    .unwrap_or(false)
5389                }
5390            } else {
5391                false
5392            }
5393        };
5394        let git_indicator = settings
5395            .git_status_indicator
5396            .then(|| git_status_indicator(details.git_status))
5397            .flatten();
5398
5399        let id: ElementId = if is_sticky {
5400            SharedString::from(format!("project_panel_sticky_item_{}", entry_id.to_usize())).into()
5401        } else {
5402            (entry_id.to_proto() as usize).into()
5403        };
5404
5405        div()
5406            .id(id.clone())
5407            .relative()
5408            .group(GROUP_NAME)
5409            .cursor_pointer()
5410            .rounded_none()
5411            .bg(bg_color)
5412            .border_1()
5413            .border_r_2()
5414            .border_color(border_color)
5415            .hover(|style| style.bg(bg_hover_color).border_color(border_hover_color))
5416            .when(is_sticky, |this| this.block_mouse_except_scroll())
5417            .when(!is_sticky, |this| {
5418                this.when(
5419                    is_highlighted && folded_directory_drag_target.is_none(),
5420                    |this| {
5421                        this.border_color(transparent_white())
5422                            .bg(item_colors.drag_over)
5423                    },
5424                )
5425                .when(settings.drag_and_drop, |this| {
5426                    this.on_drag_move::<ExternalPaths>(cx.listener(
5427                        move |this, event: &DragMoveEvent<ExternalPaths>, _, cx| {
5428                            let is_current_target =
5429                                this.drag_target_entry
5430                                    .as_ref()
5431                                    .and_then(|entry| match entry {
5432                                        DragTarget::Entry {
5433                                            entry_id: target_id,
5434                                            ..
5435                                        } => Some(*target_id),
5436                                        DragTarget::Background { .. } => None,
5437                                    })
5438                                    == Some(entry_id);
5439
5440                            if !event.bounds.contains(&event.event.position) {
5441                                // Entry responsible for setting drag target is also responsible to
5442                                // clear it up after drag is out of bounds
5443                                if is_current_target {
5444                                    this.drag_target_entry = None;
5445                                }
5446                                return;
5447                            }
5448
5449                            if is_current_target {
5450                                return;
5451                            }
5452
5453                            this.marked_entries.clear();
5454
5455                            let Some((entry_id, highlight_entry_id)) = maybe!({
5456                                let target_worktree = this
5457                                    .project
5458                                    .read(cx)
5459                                    .worktree_for_id(selection.worktree_id, cx)?
5460                                    .read(cx);
5461                                let target_entry =
5462                                    target_worktree.entry_for_path(&path_for_external_paths)?;
5463                                let highlight_entry_id = this.highlight_entry_for_external_drag(
5464                                    target_entry,
5465                                    target_worktree,
5466                                )?;
5467                                Some((target_entry.id, highlight_entry_id))
5468                            }) else {
5469                                return;
5470                            };
5471
5472                            this.drag_target_entry = Some(DragTarget::Entry {
5473                                entry_id,
5474                                highlight_entry_id,
5475                            });
5476                        },
5477                    ))
5478                    .on_drop(cx.listener(
5479                        move |this, external_paths: &ExternalPaths, window, cx| {
5480                            this.drag_target_entry = None;
5481                            this.hover_scroll_task.take();
5482                            this.drop_external_files(external_paths.paths(), entry_id, window, cx);
5483                            cx.stop_propagation();
5484                        },
5485                    ))
5486                    .on_drag_move::<DraggedSelection>(cx.listener(
5487                        move |this, event: &DragMoveEvent<DraggedSelection>, window, cx| {
5488                            let is_current_target =
5489                                this.drag_target_entry
5490                                    .as_ref()
5491                                    .and_then(|entry| match entry {
5492                                        DragTarget::Entry {
5493                                            entry_id: target_id,
5494                                            ..
5495                                        } => Some(*target_id),
5496                                        DragTarget::Background { .. } => None,
5497                                    })
5498                                    == Some(entry_id);
5499
5500                            if !event.bounds.contains(&event.event.position) {
5501                                // Entry responsible for setting drag target is also responsible to
5502                                // clear it up after drag is out of bounds
5503                                if is_current_target {
5504                                    this.drag_target_entry = None;
5505                                }
5506                                return;
5507                            }
5508
5509                            if is_current_target {
5510                                return;
5511                            }
5512
5513                            let drag_state = event.drag(cx);
5514
5515                            if drag_state.items().count() == 1 {
5516                                this.marked_entries.clear();
5517                                this.marked_entries.push(drag_state.active_selection);
5518                            }
5519
5520                            let Some((entry_id, highlight_entry_id)) = maybe!({
5521                                let target_worktree = this
5522                                    .project
5523                                    .read(cx)
5524                                    .worktree_for_id(selection.worktree_id, cx)?
5525                                    .read(cx);
5526                                let target_entry =
5527                                    target_worktree.entry_for_path(&path_for_dragged_selection)?;
5528                                let highlight_entry_id = this.highlight_entry_for_selection_drag(
5529                                    target_entry,
5530                                    target_worktree,
5531                                    drag_state,
5532                                    cx,
5533                                )?;
5534                                Some((target_entry.id, highlight_entry_id))
5535                            }) else {
5536                                return;
5537                            };
5538
5539                            this.drag_target_entry = Some(DragTarget::Entry {
5540                                entry_id,
5541                                highlight_entry_id,
5542                            });
5543
5544                            this.hover_expand_task.take();
5545
5546                            if !kind.is_dir()
5547                                || this
5548                                    .state
5549                                    .expanded_dir_ids
5550                                    .get(&details.worktree_id)
5551                                    .is_some_and(|ids| ids.binary_search(&entry_id).is_ok())
5552                            {
5553                                return;
5554                            }
5555
5556                            let bounds = event.bounds;
5557                            this.hover_expand_task =
5558                                Some(cx.spawn_in(window, async move |this, cx| {
5559                                    cx.background_executor()
5560                                        .timer(Duration::from_millis(500))
5561                                        .await;
5562                                    this.update_in(cx, |this, window, cx| {
5563                                        this.hover_expand_task.take();
5564                                        if this.drag_target_entry.as_ref().and_then(|entry| {
5565                                            match entry {
5566                                                DragTarget::Entry {
5567                                                    entry_id: target_id,
5568                                                    ..
5569                                                } => Some(*target_id),
5570                                                DragTarget::Background { .. } => None,
5571                                            }
5572                                        }) == Some(entry_id)
5573                                            && bounds.contains(&window.mouse_position())
5574                                        {
5575                                            this.expand_entry(worktree_id, entry_id, cx);
5576                                            this.update_visible_entries(
5577                                                Some((worktree_id, entry_id)),
5578                                                false,
5579                                                false,
5580                                                window,
5581                                                cx,
5582                                            );
5583                                            cx.notify();
5584                                        }
5585                                    })
5586                                    .ok();
5587                                }));
5588                        },
5589                    ))
5590                    .on_drag(dragged_selection, {
5591                        let active_component =
5592                            self.state.ancestors.get(&entry_id).and_then(|ancestors| {
5593                                ancestors.active_component(&details.filename)
5594                            });
5595                        move |selection, click_offset, _window, cx| {
5596                            let filename = active_component
5597                                .as_ref()
5598                                .unwrap_or_else(|| &details.filename);
5599                            cx.new(|_| DraggedProjectEntryView {
5600                                icon: details.icon.clone(),
5601                                filename: filename.clone(),
5602                                click_offset,
5603                                selection: selection.active_selection,
5604                                selections: selection.marked_selections.clone(),
5605                            })
5606                        }
5607                    })
5608                    .on_drop(cx.listener(
5609                        move |this, selections: &DraggedSelection, window, cx| {
5610                            this.drag_target_entry = None;
5611                            this.hover_scroll_task.take();
5612                            this.hover_expand_task.take();
5613                            if folded_directory_drag_target.is_some() {
5614                                return;
5615                            }
5616                            this.drag_onto(selections, entry_id, kind.is_file(), window, cx);
5617                        },
5618                    ))
5619                })
5620            })
5621            .on_mouse_down(
5622                MouseButton::Left,
5623                cx.listener(move |this, _, _, cx| {
5624                    this.mouse_down = true;
5625                    cx.propagate();
5626                }),
5627            )
5628            .on_click(
5629                cx.listener(move |project_panel, event: &gpui::ClickEvent, window, cx| {
5630                    if event.is_right_click() || event.first_focus() || show_editor {
5631                        return;
5632                    }
5633                    if event.standard_click() {
5634                        project_panel.mouse_down = false;
5635                    }
5636                    cx.stop_propagation();
5637
5638                    if let Some(selection) =
5639                        project_panel.selection.filter(|_| event.modifiers().shift)
5640                    {
5641                        let current_selection = project_panel.index_for_selection(selection);
5642                        let clicked_entry = SelectedEntry {
5643                            entry_id,
5644                            worktree_id,
5645                        };
5646                        let target_selection = project_panel.index_for_selection(clicked_entry);
5647                        if let Some(((_, _, source_index), (_, _, target_index))) =
5648                            current_selection.zip(target_selection)
5649                        {
5650                            let range_start = source_index.min(target_index);
5651                            let range_end = source_index.max(target_index) + 1;
5652                            let mut new_selections = Vec::new();
5653                            project_panel.for_each_visible_entry(
5654                                range_start..range_end,
5655                                window,
5656                                cx,
5657                                &mut |entry_id, details, _, _| {
5658                                    new_selections.push(SelectedEntry {
5659                                        entry_id,
5660                                        worktree_id: details.worktree_id,
5661                                    });
5662                                },
5663                            );
5664
5665                            for selection in &new_selections {
5666                                if !project_panel.marked_entries.contains(selection) {
5667                                    project_panel.marked_entries.push(*selection);
5668                                }
5669                            }
5670
5671                            project_panel.selection = Some(clicked_entry);
5672                            if !project_panel.marked_entries.contains(&clicked_entry) {
5673                                project_panel.marked_entries.push(clicked_entry);
5674                            }
5675                        }
5676                    } else if event.modifiers().secondary() {
5677                        if event.click_count() > 1 {
5678                            project_panel.split_entry(entry_id, false, None, cx);
5679                        } else {
5680                            project_panel.selection = Some(selection);
5681                            if let Some(position) = project_panel
5682                                .marked_entries
5683                                .iter()
5684                                .position(|e| *e == selection)
5685                            {
5686                                project_panel.marked_entries.remove(position);
5687                            } else {
5688                                project_panel.marked_entries.push(selection);
5689                            }
5690                        }
5691                    } else if kind.is_dir() {
5692                        project_panel.marked_entries.clear();
5693                        if is_sticky
5694                            && let Some((_, _, index)) =
5695                                project_panel.index_for_entry(entry_id, worktree_id)
5696                        {
5697                            project_panel
5698                                .scroll_handle
5699                                .scroll_to_item_strict_with_offset(
5700                                    index,
5701                                    ScrollStrategy::Top,
5702                                    sticky_index.unwrap_or(0),
5703                                );
5704                            cx.notify();
5705                            // move down by 1px so that clicked item
5706                            // don't count as sticky anymore
5707                            cx.on_next_frame(window, |_, window, cx| {
5708                                cx.on_next_frame(window, |this, _, cx| {
5709                                    let mut offset = this.scroll_handle.offset();
5710                                    offset.y += px(1.);
5711                                    this.scroll_handle.set_offset(offset);
5712                                    cx.notify();
5713                                });
5714                            });
5715                            return;
5716                        }
5717                        if event.modifiers().alt {
5718                            project_panel.toggle_expand_all(entry_id, window, cx);
5719                        } else {
5720                            project_panel.toggle_expanded(entry_id, window, cx);
5721                        }
5722                    } else {
5723                        let preview_tabs_enabled =
5724                            PreviewTabsSettings::get_global(cx).enable_preview_from_project_panel;
5725                        let click_count = event.click_count();
5726                        let focus_opened_item = click_count > 1;
5727                        let allow_preview = preview_tabs_enabled && click_count == 1;
5728                        project_panel.open_entry(entry_id, focus_opened_item, allow_preview, cx);
5729                    }
5730                }),
5731            )
5732            .child(
5733                ListItem::new(id)
5734                    .indent_level(depth)
5735                    .indent_step_size(px(settings.indent_size))
5736                    .spacing(match settings.entry_spacing {
5737                        ProjectPanelEntrySpacing::Comfortable => ListItemSpacing::Dense,
5738                        ProjectPanelEntrySpacing::Standard => ListItemSpacing::ExtraDense,
5739                    })
5740                    .selectable(false)
5741                    .when(
5742                        canonical_path.is_some()
5743                            || diagnostic_count.is_some()
5744                            || git_indicator.is_some(),
5745                        |this| {
5746                            let symlink_element = canonical_path.map(|path| {
5747                                div()
5748                                    .id("symlink_icon")
5749                                    .tooltip(move |_window, cx| {
5750                                        Tooltip::with_meta(
5751                                            path.to_string(),
5752                                            None,
5753                                            "Symbolic Link",
5754                                            cx,
5755                                        )
5756                                    })
5757                                    .child(
5758                                        Icon::new(IconName::ArrowUpRight)
5759                                            .size(IconSize::Indicator)
5760                                            .color(filename_text_color),
5761                                    )
5762                            });
5763                            this.end_slot::<AnyElement>(
5764                                h_flex()
5765                                    .gap_1()
5766                                    .flex_none()
5767                                    .pr_3()
5768                                    .when_some(diagnostic_count, |this, count| {
5769                                        this.when(count.error_count > 0, |this| {
5770                                            this.child(
5771                                                Label::new(count.capped_error_count())
5772                                                    .size(LabelSize::Small)
5773                                                    .color(Color::Error),
5774                                            )
5775                                        })
5776                                        .when(
5777                                            count.warning_count > 0,
5778                                            |this| {
5779                                                this.child(
5780                                                    Label::new(count.capped_warning_count())
5781                                                        .size(LabelSize::Small)
5782                                                        .color(Color::Warning),
5783                                                )
5784                                            },
5785                                        )
5786                                    })
5787                                    .when_some(git_indicator, |this, (label, color)| {
5788                                        let git_indicator = if kind.is_dir() {
5789                                            Indicator::dot()
5790                                                .color(Color::Custom(color.color(cx).opacity(0.5)))
5791                                                .into_any_element()
5792                                        } else {
5793                                            Label::new(label)
5794                                                .size(LabelSize::Small)
5795                                                .color(color)
5796                                                .into_any_element()
5797                                        };
5798
5799                                        this.child(git_indicator)
5800                                    })
5801                                    .when_some(symlink_element, |this, el| this.child(el))
5802                                    .into_any_element(),
5803                            )
5804                        },
5805                    )
5806                    .child(if let Some(icon) = &icon {
5807                        if let Some((_, decoration_color)) =
5808                            entry_diagnostic_aware_icon_decoration_and_color(diagnostic_severity)
5809                        {
5810                            let is_warning = diagnostic_severity
5811                                .map(|severity| matches!(severity, DiagnosticSeverity::WARNING))
5812                                .unwrap_or(false);
5813                            div().child(
5814                                DecoratedIcon::new(
5815                                    Icon::from_path(icon.clone()).color(Color::Muted),
5816                                    Some(
5817                                        IconDecoration::new(
5818                                            if kind.is_file() {
5819                                                if is_warning {
5820                                                    IconDecorationKind::Triangle
5821                                                } else {
5822                                                    IconDecorationKind::X
5823                                                }
5824                                            } else {
5825                                                IconDecorationKind::Dot
5826                                            },
5827                                            bg_color,
5828                                            cx,
5829                                        )
5830                                        .group_name(Some(GROUP_NAME.into()))
5831                                        .knockout_hover_color(bg_hover_color)
5832                                        .color(decoration_color.color(cx))
5833                                        .position(Point {
5834                                            x: px(-2.),
5835                                            y: px(-2.),
5836                                        }),
5837                                    ),
5838                                )
5839                                .into_any_element(),
5840                            )
5841                        } else {
5842                            h_flex().child(Icon::from_path(icon.to_string()).color(Color::Muted))
5843                        }
5844                    } else if let Some((icon_name, color)) =
5845                        entry_diagnostic_aware_icon_name_and_color(diagnostic_severity)
5846                    {
5847                        h_flex()
5848                            .size(IconSize::default().rems())
5849                            .child(Icon::new(icon_name).color(color).size(IconSize::Small))
5850                    } else {
5851                        h_flex()
5852                            .size(IconSize::default().rems())
5853                            .invisible()
5854                            .flex_none()
5855                    })
5856                    .child(if show_editor {
5857                        h_flex().h_6().w_full().child(self.filename_editor.clone())
5858                    } else {
5859                        h_flex()
5860                            .h_6()
5861                            .map(|this| match self.state.ancestors.get(&entry_id) {
5862                                Some(folded_ancestors) => {
5863                                    this.children(self.render_folder_elements(
5864                                        folded_ancestors,
5865                                        entry_id,
5866                                        file_name,
5867                                        path_style,
5868                                        is_sticky,
5869                                        kind.is_file(),
5870                                        is_active || is_marked,
5871                                        settings.drag_and_drop,
5872                                        settings.bold_folder_labels,
5873                                        item_colors.drag_over,
5874                                        folded_directory_drag_target,
5875                                        filename_text_color,
5876                                        cx,
5877                                    ))
5878                                }
5879
5880                                None => this.child(
5881                                    Label::new(file_name)
5882                                        .single_line()
5883                                        .color(filename_text_color)
5884                                        .when(
5885                                            settings.bold_folder_labels && kind.is_dir(),
5886                                            |this| this.weight(FontWeight::SEMIBOLD),
5887                                        )
5888                                        .into_any_element(),
5889                                ),
5890                            })
5891                    })
5892                    .on_secondary_mouse_down(cx.listener(
5893                        move |this, event: &MouseDownEvent, window, cx| {
5894                            // Stop propagation to prevent the catch-all context menu for the project
5895                            // panel from being deployed.
5896                            cx.stop_propagation();
5897                            // Some context menu actions apply to all marked entries. If the user
5898                            // right-clicks on an entry that is not marked, they may not realize the
5899                            // action applies to multiple entries. To avoid inadvertent changes, all
5900                            // entries are unmarked.
5901                            if !this.marked_entries.contains(&selection) {
5902                                this.marked_entries.clear();
5903                            }
5904                            this.deploy_context_menu(event.position, entry_id, window, cx);
5905                        },
5906                    ))
5907                    .overflow_x(),
5908            )
5909            .when_some(validation_color_and_message, |this, (color, message)| {
5910                this.relative().child(deferred(
5911                    div()
5912                        .occlude()
5913                        .absolute()
5914                        .top_full()
5915                        .left(px(-1.)) // Used px over rem so that it doesn't change with font size
5916                        .right(px(-0.5))
5917                        .py_1()
5918                        .px_2()
5919                        .border_1()
5920                        .border_color(color)
5921                        .bg(cx.theme().colors().background)
5922                        .child(
5923                            Label::new(message)
5924                                .color(Color::from(color))
5925                                .size(LabelSize::Small),
5926                        ),
5927                ))
5928            })
5929    }
5930
5931    fn render_folder_elements(
5932        &self,
5933        folded_ancestors: &FoldedAncestors,
5934        entry_id: ProjectEntryId,
5935        file_name: String,
5936        path_style: PathStyle,
5937        is_sticky: bool,
5938        is_file: bool,
5939        is_active_or_marked: bool,
5940        drag_and_drop_enabled: bool,
5941        bold_folder_labels: bool,
5942        drag_over_color: Hsla,
5943        folded_directory_drag_target: Option<FoldedDirectoryDragTarget>,
5944        filename_text_color: Color,
5945        cx: &Context<Self>,
5946    ) -> impl Iterator<Item = AnyElement> {
5947        let components = Path::new(&file_name)
5948            .components()
5949            .map(|comp| comp.as_os_str().to_string_lossy().into_owned())
5950            .collect::<Vec<_>>();
5951        let active_index = folded_ancestors.active_index();
5952        let components_len = components.len();
5953        let delimiter = SharedString::new(path_style.primary_separator());
5954
5955        let path_component_elements =
5956            components
5957                .into_iter()
5958                .enumerate()
5959                .map(move |(index, component)| {
5960                    div()
5961                        .id(SharedString::from(format!(
5962                            "project_panel_path_component_{}_{index}",
5963                            entry_id.to_usize()
5964                        )))
5965                        .when(index == 0, |this| this.ml_neg_0p5())
5966                        .px_0p5()
5967                        .rounded_xs()
5968                        .hover(|style| style.bg(cx.theme().colors().element_active))
5969                        .when(!is_sticky, |div| {
5970                            div.when(index != components_len - 1, |div| {
5971                                let target_entry_id = folded_ancestors
5972                                    .ancestors
5973                                    .get(components_len - 1 - index)
5974                                    .cloned();
5975                                div.when(drag_and_drop_enabled, |div| {
5976                                    div.on_drag_move(cx.listener(
5977                                        move |this,
5978                                              event: &DragMoveEvent<DraggedSelection>,
5979                                              _,
5980                                              _| {
5981                                            if event.bounds.contains(&event.event.position) {
5982                                                this.folded_directory_drag_target =
5983                                                    Some(FoldedDirectoryDragTarget {
5984                                                        entry_id,
5985                                                        index,
5986                                                        is_delimiter_target: false,
5987                                                    });
5988                                            } else {
5989                                                let is_current_target = this
5990                                                    .folded_directory_drag_target
5991                                                    .as_ref()
5992                                                    .is_some_and(|target| {
5993                                                        target.entry_id == entry_id
5994                                                            && target.index == index
5995                                                            && !target.is_delimiter_target
5996                                                    });
5997                                                if is_current_target {
5998                                                    this.folded_directory_drag_target = None;
5999                                                }
6000                                            }
6001                                        },
6002                                    ))
6003                                    .on_drop(cx.listener(
6004                                        move |this, selections: &DraggedSelection, window, cx| {
6005                                            this.hover_scroll_task.take();
6006                                            this.drag_target_entry = None;
6007                                            this.folded_directory_drag_target = None;
6008                                            if let Some(target_entry_id) = target_entry_id {
6009                                                this.drag_onto(
6010                                                    selections,
6011                                                    target_entry_id,
6012                                                    is_file,
6013                                                    window,
6014                                                    cx,
6015                                                );
6016                                            }
6017                                        },
6018                                    ))
6019                                    .when(
6020                                        folded_directory_drag_target.is_some_and(|target| {
6021                                            target.entry_id == entry_id && target.index == index
6022                                        }),
6023                                        |this| this.bg(drag_over_color),
6024                                    )
6025                                })
6026                            })
6027                        })
6028                        .on_mouse_down(
6029                            MouseButton::Left,
6030                            cx.listener(move |this, _, _, cx| {
6031                                if let Some(folds) = this.state.ancestors.get_mut(&entry_id) {
6032                                    if folds.set_active_index(index) {
6033                                        cx.notify();
6034                                    }
6035                                }
6036                            }),
6037                        )
6038                        .on_mouse_down(
6039                            MouseButton::Right,
6040                            cx.listener(move |this, _, _, cx| {
6041                                if let Some(folds) = this.state.ancestors.get_mut(&entry_id) {
6042                                    if folds.set_active_index(index) {
6043                                        cx.notify();
6044                                    }
6045                                }
6046                            }),
6047                        )
6048                        .child(
6049                            Label::new(component)
6050                                .single_line()
6051                                .color(filename_text_color)
6052                                .when(bold_folder_labels && !is_file, |this| {
6053                                    this.weight(FontWeight::SEMIBOLD)
6054                                })
6055                                .when(index == active_index && is_active_or_marked, |this| {
6056                                    this.underline()
6057                                }),
6058                        )
6059                        .into_any()
6060                });
6061
6062        let mut separator_index = 0;
6063        itertools::intersperse_with(path_component_elements, move || {
6064            separator_index += 1;
6065            self.render_entry_path_separator(
6066                entry_id,
6067                separator_index,
6068                components_len,
6069                is_sticky,
6070                is_file,
6071                drag_and_drop_enabled,
6072                filename_text_color,
6073                &delimiter,
6074                folded_ancestors,
6075                cx,
6076            )
6077            .into_any()
6078        })
6079    }
6080
6081    fn render_entry_path_separator(
6082        &self,
6083        entry_id: ProjectEntryId,
6084        index: usize,
6085        components_len: usize,
6086        is_sticky: bool,
6087        is_file: bool,
6088        drag_and_drop_enabled: bool,
6089        filename_text_color: Color,
6090        delimiter: &SharedString,
6091        folded_ancestors: &FoldedAncestors,
6092        cx: &Context<Self>,
6093    ) -> Div {
6094        let delimiter_target_index = index - 1;
6095        let target_entry_id = folded_ancestors
6096            .ancestors
6097            .get(components_len - 1 - delimiter_target_index)
6098            .cloned();
6099        div()
6100            .when(!is_sticky, |div| {
6101                div.when(drag_and_drop_enabled, |div| {
6102                    div.on_drop(cx.listener(
6103                        move |this, selections: &DraggedSelection, window, cx| {
6104                            this.hover_scroll_task.take();
6105                            this.drag_target_entry = None;
6106                            this.folded_directory_drag_target = None;
6107                            if let Some(target_entry_id) = target_entry_id {
6108                                this.drag_onto(selections, target_entry_id, is_file, window, cx);
6109                            }
6110                        },
6111                    ))
6112                    .on_drag_move(cx.listener(
6113                        move |this, event: &DragMoveEvent<DraggedSelection>, _, _| {
6114                            if event.bounds.contains(&event.event.position) {
6115                                this.folded_directory_drag_target =
6116                                    Some(FoldedDirectoryDragTarget {
6117                                        entry_id,
6118                                        index: delimiter_target_index,
6119                                        is_delimiter_target: true,
6120                                    });
6121                            } else {
6122                                let is_current_target =
6123                                    this.folded_directory_drag_target.is_some_and(|target| {
6124                                        target.entry_id == entry_id
6125                                            && target.index == delimiter_target_index
6126                                            && target.is_delimiter_target
6127                                    });
6128                                if is_current_target {
6129                                    this.folded_directory_drag_target = None;
6130                                }
6131                            }
6132                        },
6133                    ))
6134                })
6135            })
6136            .child(
6137                Label::new(delimiter.clone())
6138                    .single_line()
6139                    .color(filename_text_color),
6140            )
6141    }
6142
6143    fn details_for_entry(
6144        &self,
6145        entry: &Entry,
6146        worktree_id: WorktreeId,
6147        root_name: &RelPath,
6148        entries_paths: &HashSet<Arc<RelPath>>,
6149        git_status: GitSummary,
6150        sticky: Option<StickyDetails>,
6151        _window: &mut Window,
6152        cx: &mut Context<Self>,
6153    ) -> EntryDetails {
6154        let (show_file_icons, show_folder_icons) = {
6155            let settings = ProjectPanelSettings::get_global(cx);
6156            (settings.file_icons, settings.folder_icons)
6157        };
6158
6159        let expanded_entry_ids = self
6160            .state
6161            .expanded_dir_ids
6162            .get(&worktree_id)
6163            .map(Vec::as_slice)
6164            .unwrap_or(&[]);
6165        let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok();
6166
6167        let icon = match entry.kind {
6168            EntryKind::File => {
6169                if show_file_icons {
6170                    FileIcons::get_icon(entry.path.as_std_path(), cx)
6171                } else {
6172                    None
6173                }
6174            }
6175            _ => {
6176                if show_folder_icons {
6177                    FileIcons::get_folder_icon(is_expanded, entry.path.as_std_path(), cx)
6178                } else {
6179                    FileIcons::get_chevron_icon(is_expanded, cx)
6180                }
6181            }
6182        };
6183
6184        let path_style = self.project.read(cx).path_style(cx);
6185        let (depth, difference) =
6186            ProjectPanel::calculate_depth_and_difference(entry, entries_paths);
6187
6188        let filename = if difference > 1 {
6189            entry
6190                .path
6191                .last_n_components(difference)
6192                .map_or(String::new(), |suffix| {
6193                    suffix.display(path_style).to_string()
6194                })
6195        } else {
6196            entry
6197                .path
6198                .file_name()
6199                .map(|name| name.to_string())
6200                .unwrap_or_else(|| root_name.as_unix_str().to_string())
6201        };
6202
6203        let selection = SelectedEntry {
6204            worktree_id,
6205            entry_id: entry.id,
6206        };
6207        let is_marked = self.marked_entries.contains(&selection);
6208        let is_selected = self.selection == Some(selection);
6209
6210        let diagnostic_severity = self
6211            .diagnostics
6212            .get(&(worktree_id, entry.path.clone()))
6213            .cloned();
6214
6215        let diagnostic_count = self
6216            .diagnostic_counts
6217            .get(&(worktree_id, entry.path.clone()))
6218            .copied();
6219
6220        let filename_text_color =
6221            entry_git_aware_label_color(git_status, entry.is_ignored, is_marked);
6222
6223        let is_cut = self
6224            .clipboard
6225            .as_ref()
6226            .is_some_and(|e| e.is_cut() && e.items().contains(&selection));
6227
6228        EntryDetails {
6229            filename,
6230            icon,
6231            path: entry.path.clone(),
6232            depth,
6233            kind: entry.kind,
6234            is_ignored: entry.is_ignored,
6235            is_expanded,
6236            is_selected,
6237            is_marked,
6238            is_editing: false,
6239            is_processing: false,
6240            is_cut,
6241            sticky,
6242            filename_text_color,
6243            diagnostic_severity,
6244            diagnostic_count,
6245            git_status,
6246            is_private: entry.is_private,
6247            worktree_id,
6248            canonical_path: entry.canonical_path.clone(),
6249        }
6250    }
6251
6252    fn dispatch_context(&self, window: &Window, cx: &Context<Self>) -> KeyContext {
6253        let mut dispatch_context = KeyContext::new_with_defaults();
6254        dispatch_context.add("ProjectPanel");
6255        dispatch_context.add("menu");
6256
6257        let identifier = if self.filename_editor.focus_handle(cx).is_focused(window) {
6258            "editing"
6259        } else {
6260            "not_editing"
6261        };
6262
6263        dispatch_context.add(identifier);
6264        dispatch_context
6265    }
6266
6267    fn reveal_entry(
6268        &mut self,
6269        project: Entity<Project>,
6270        entry_id: ProjectEntryId,
6271        skip_ignored: bool,
6272        window: &mut Window,
6273        cx: &mut Context<Self>,
6274    ) -> Result<()> {
6275        let worktree = project
6276            .read(cx)
6277            .worktree_for_entry(entry_id, cx)
6278            .context("can't reveal a non-existent entry in the project panel")?;
6279        let worktree = worktree.read(cx);
6280        let worktree_id = worktree.id();
6281        let is_ignored = worktree
6282            .entry_for_id(entry_id)
6283            .is_none_or(|entry| entry.is_ignored && !entry.is_always_included);
6284        if skip_ignored && is_ignored {
6285            if self.index_for_entry(entry_id, worktree_id).is_none() {
6286                anyhow::bail!("can't reveal an ignored entry in the project panel");
6287            }
6288
6289            self.selection = Some(SelectedEntry {
6290                worktree_id,
6291                entry_id,
6292            });
6293            self.marked_entries.clear();
6294            self.marked_entries.push(SelectedEntry {
6295                worktree_id,
6296                entry_id,
6297            });
6298            self.autoscroll(cx);
6299            cx.notify();
6300            return Ok(());
6301        }
6302        let is_active_item_file_diff_view = self
6303            .workspace
6304            .upgrade()
6305            .and_then(|ws| ws.read(cx).active_item(cx))
6306            .map(|item| item.act_as_type(TypeId::of::<FileDiffView>(), cx).is_some())
6307            .unwrap_or(false);
6308        if is_active_item_file_diff_view {
6309            return Ok(());
6310        }
6311
6312        self.expand_entry(worktree_id, entry_id, cx);
6313        self.update_visible_entries(Some((worktree_id, entry_id)), false, true, window, cx);
6314        self.marked_entries.clear();
6315        self.marked_entries.push(SelectedEntry {
6316            worktree_id,
6317            entry_id,
6318        });
6319        cx.notify();
6320        Ok(())
6321    }
6322
6323    fn find_active_indent_guide(
6324        &self,
6325        indent_guides: &[IndentGuideLayout],
6326        cx: &App,
6327    ) -> Option<usize> {
6328        let (worktree, entry) = self.selected_entry(cx)?;
6329
6330        // Find the parent entry of the indent guide, this will either be the
6331        // expanded folder we have selected, or the parent of the currently
6332        // selected file/collapsed directory
6333        let mut entry = entry;
6334        loop {
6335            let is_expanded_dir = entry.is_dir()
6336                && self
6337                    .state
6338                    .expanded_dir_ids
6339                    .get(&worktree.id())
6340                    .map(|ids| ids.binary_search(&entry.id).is_ok())
6341                    .unwrap_or(false);
6342            if is_expanded_dir {
6343                break;
6344            }
6345            entry = worktree.entry_for_path(&entry.path.parent()?)?;
6346        }
6347
6348        let (active_indent_range, depth) = {
6349            let (worktree_ix, child_offset, ix) = self.index_for_entry(entry.id, worktree.id())?;
6350            let child_paths = &self.state.visible_entries[worktree_ix].entries;
6351            let mut child_count = 0;
6352            let depth = entry.path.ancestors().count();
6353            while let Some(entry) = child_paths.get(child_offset + child_count + 1) {
6354                if entry.path.ancestors().count() <= depth {
6355                    break;
6356                }
6357                child_count += 1;
6358            }
6359
6360            let start = ix + 1;
6361            let end = start + child_count;
6362
6363            let visible_worktree = &self.state.visible_entries[worktree_ix];
6364            let visible_worktree_entries = visible_worktree.index.get_or_init(|| {
6365                visible_worktree
6366                    .entries
6367                    .iter()
6368                    .map(|e| e.path.clone())
6369                    .collect()
6370            });
6371
6372            // Calculate the actual depth of the entry, taking into account that directories can be auto-folded.
6373            let (depth, _) = Self::calculate_depth_and_difference(entry, visible_worktree_entries);
6374            (start..end, depth)
6375        };
6376
6377        let candidates = indent_guides
6378            .iter()
6379            .enumerate()
6380            .filter(|(_, indent_guide)| indent_guide.offset.x == depth);
6381
6382        for (i, indent) in candidates {
6383            // Find matches that are either an exact match, partially on screen, or inside the enclosing indent
6384            if active_indent_range.start <= indent.offset.y + indent.length
6385                && indent.offset.y <= active_indent_range.end
6386            {
6387                return Some(i);
6388            }
6389        }
6390        None
6391    }
6392
6393    fn render_sticky_entries(
6394        &self,
6395        child: StickyProjectPanelCandidate,
6396        window: &mut Window,
6397        cx: &mut Context<Self>,
6398    ) -> SmallVec<[AnyElement; 8]> {
6399        let project = self.project.read(cx);
6400
6401        let Some((worktree_id, entry_ref)) = self.entry_at_index(child.index) else {
6402            return SmallVec::new();
6403        };
6404
6405        let Some(visible) = self
6406            .state
6407            .visible_entries
6408            .iter()
6409            .find(|worktree| worktree.worktree_id == worktree_id)
6410        else {
6411            return SmallVec::new();
6412        };
6413
6414        let Some(worktree) = project.worktree_for_id(worktree_id, cx) else {
6415            return SmallVec::new();
6416        };
6417        let worktree = worktree.read(cx).snapshot();
6418
6419        let paths = visible
6420            .index
6421            .get_or_init(|| visible.entries.iter().map(|e| e.path.clone()).collect());
6422
6423        let mut sticky_parents = Vec::new();
6424        let mut current_path = entry_ref.path.clone();
6425
6426        'outer: loop {
6427            if let Some(parent_path) = current_path.parent() {
6428                for ancestor_path in parent_path.ancestors() {
6429                    if paths.contains(ancestor_path)
6430                        && let Some(parent_entry) = worktree.entry_for_path(ancestor_path)
6431                    {
6432                        sticky_parents.push(parent_entry.clone());
6433                        current_path = parent_entry.path.clone();
6434                        continue 'outer;
6435                    }
6436                }
6437            }
6438            break 'outer;
6439        }
6440
6441        if sticky_parents.is_empty() {
6442            return SmallVec::new();
6443        }
6444
6445        sticky_parents.reverse();
6446
6447        let panel_settings = ProjectPanelSettings::get_global(cx);
6448        let git_status_enabled = panel_settings.git_status;
6449        let root_name = worktree.root_name();
6450
6451        let git_summaries_by_id = if git_status_enabled {
6452            visible
6453                .entries
6454                .iter()
6455                .map(|e| (e.id, e.git_summary))
6456                .collect::<HashMap<_, _>>()
6457        } else {
6458            Default::default()
6459        };
6460
6461        // already checked if non empty above
6462        let last_item_index = sticky_parents.len() - 1;
6463        sticky_parents
6464            .iter()
6465            .enumerate()
6466            .map(|(index, entry)| {
6467                let git_status = git_summaries_by_id
6468                    .get(&entry.id)
6469                    .copied()
6470                    .unwrap_or_default();
6471                let sticky_details = Some(StickyDetails {
6472                    sticky_index: index,
6473                });
6474                let details = self.details_for_entry(
6475                    entry,
6476                    worktree_id,
6477                    root_name,
6478                    paths,
6479                    git_status,
6480                    sticky_details,
6481                    window,
6482                    cx,
6483                );
6484                self.render_entry(entry.id, details, window, cx)
6485                    .when(index == last_item_index, |this| {
6486                        let shadow_color_top = hsla(0.0, 0.0, 0.0, 0.1);
6487                        let shadow_color_bottom = hsla(0.0, 0.0, 0.0, 0.);
6488                        let sticky_shadow = div()
6489                            .absolute()
6490                            .left_0()
6491                            .bottom_neg_1p5()
6492                            .h_1p5()
6493                            .w_full()
6494                            .bg(linear_gradient(
6495                                0.,
6496                                linear_color_stop(shadow_color_top, 1.),
6497                                linear_color_stop(shadow_color_bottom, 0.),
6498                            ));
6499                        this.child(sticky_shadow)
6500                    })
6501                    .into_any()
6502            })
6503            .collect()
6504    }
6505}
6506
6507#[derive(Clone)]
6508struct StickyProjectPanelCandidate {
6509    index: usize,
6510    depth: usize,
6511}
6512
6513impl StickyCandidate for StickyProjectPanelCandidate {
6514    fn depth(&self) -> usize {
6515        self.depth
6516    }
6517}
6518
6519fn item_width_estimate(depth: usize, item_text_chars: usize, is_symlink: bool) -> usize {
6520    const ICON_SIZE_FACTOR: usize = 2;
6521    let mut item_width = depth * ICON_SIZE_FACTOR + item_text_chars;
6522    if is_symlink {
6523        item_width += ICON_SIZE_FACTOR;
6524    }
6525    item_width
6526}
6527
6528impl Render for ProjectPanel {
6529    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
6530        let has_worktree = !self.state.visible_entries.is_empty();
6531        let project = self.project.read(cx);
6532        let panel_settings = ProjectPanelSettings::get_global(cx);
6533        let indent_size = panel_settings.indent_size;
6534        let show_indent_guides = panel_settings.indent_guides.show == ShowIndentGuides::Always;
6535        let horizontal_scroll = panel_settings.scrollbar.horizontal_scroll;
6536        let show_sticky_entries = {
6537            if panel_settings.sticky_scroll {
6538                let is_scrollable = self.scroll_handle.is_scrollable();
6539                let is_scrolled = self.scroll_handle.offset().y < px(0.);
6540                is_scrollable && is_scrolled
6541            } else {
6542                false
6543            }
6544        };
6545
6546        let is_local = project.is_local();
6547
6548        if has_worktree {
6549            let item_count = self
6550                .state
6551                .visible_entries
6552                .iter()
6553                .map(|worktree| worktree.entries.len())
6554                .sum();
6555
6556            fn handle_drag_move<T: 'static>(
6557                this: &mut ProjectPanel,
6558                e: &DragMoveEvent<T>,
6559                window: &mut Window,
6560                cx: &mut Context<ProjectPanel>,
6561            ) {
6562                if let Some(previous_position) = this.previous_drag_position {
6563                    // Refresh cursor only when an actual drag happens,
6564                    // because modifiers are not updated when the cursor is not moved.
6565                    if e.event.position != previous_position {
6566                        this.refresh_drag_cursor_style(&e.event.modifiers, window, cx);
6567                    }
6568                }
6569                this.previous_drag_position = Some(e.event.position);
6570
6571                if !e.bounds.contains(&e.event.position) {
6572                    this.drag_target_entry = None;
6573                    return;
6574                }
6575                this.hover_scroll_task.take();
6576                let panel_height = e.bounds.size.height;
6577                if panel_height <= px(0.) {
6578                    return;
6579                }
6580
6581                let event_offset = e.event.position.y - e.bounds.origin.y;
6582                // How far along in the project panel is our cursor? (0. is the top of a list, 1. is the bottom)
6583                let hovered_region_offset = event_offset / panel_height;
6584
6585                // We want the scrolling to be a bit faster when the cursor is closer to the edge of a list.
6586                // These pixels offsets were picked arbitrarily.
6587                let vertical_scroll_offset = if hovered_region_offset <= 0.05 {
6588                    8.
6589                } else if hovered_region_offset <= 0.15 {
6590                    5.
6591                } else if hovered_region_offset >= 0.95 {
6592                    -8.
6593                } else if hovered_region_offset >= 0.85 {
6594                    -5.
6595                } else {
6596                    return;
6597                };
6598                let adjustment = point(px(0.), px(vertical_scroll_offset));
6599                this.hover_scroll_task = Some(cx.spawn_in(window, async move |this, cx| {
6600                    loop {
6601                        let should_stop_scrolling = this
6602                            .update(cx, |this, cx| {
6603                                this.hover_scroll_task.as_ref()?;
6604                                let handle = this.scroll_handle.0.borrow_mut();
6605                                let offset = handle.base_handle.offset();
6606
6607                                handle.base_handle.set_offset(offset + adjustment);
6608                                cx.notify();
6609                                Some(())
6610                            })
6611                            .ok()
6612                            .flatten()
6613                            .is_some();
6614                        if should_stop_scrolling {
6615                            return;
6616                        }
6617                        cx.background_executor()
6618                            .timer(Duration::from_millis(16))
6619                            .await;
6620                    }
6621                }));
6622            }
6623            h_flex()
6624                .id("project-panel")
6625                .group("project-panel")
6626                .when(panel_settings.drag_and_drop, |this| {
6627                    this.on_drag_move(cx.listener(handle_drag_move::<ExternalPaths>))
6628                        .on_drag_move(cx.listener(handle_drag_move::<DraggedSelection>))
6629                })
6630                .size_full()
6631                .relative()
6632                .on_modifiers_changed(cx.listener(
6633                    |this, event: &ModifiersChangedEvent, window, cx| {
6634                        this.refresh_drag_cursor_style(&event.modifiers, window, cx);
6635                    },
6636                ))
6637                .key_context(self.dispatch_context(window, cx))
6638                .on_action(cx.listener(Self::scroll_up))
6639                .on_action(cx.listener(Self::scroll_down))
6640                .on_action(cx.listener(Self::scroll_cursor_center))
6641                .on_action(cx.listener(Self::scroll_cursor_top))
6642                .on_action(cx.listener(Self::scroll_cursor_bottom))
6643                .on_action(cx.listener(Self::select_next))
6644                .on_action(cx.listener(Self::select_previous))
6645                .on_action(cx.listener(Self::select_first))
6646                .on_action(cx.listener(Self::select_last))
6647                .on_action(cx.listener(Self::select_parent))
6648                .on_action(cx.listener(Self::select_next_git_entry))
6649                .on_action(cx.listener(Self::select_prev_git_entry))
6650                .on_action(cx.listener(Self::select_next_diagnostic))
6651                .on_action(cx.listener(Self::select_prev_diagnostic))
6652                .on_action(cx.listener(Self::select_next_directory))
6653                .on_action(cx.listener(Self::select_prev_directory))
6654                .on_action(cx.listener(Self::expand_selected_entry))
6655                .on_action(cx.listener(Self::collapse_selected_entry))
6656                .on_action(cx.listener(Self::collapse_all_entries))
6657                .on_action(cx.listener(Self::collapse_selected_entry_and_children))
6658                .on_action(cx.listener(Self::open))
6659                .on_action(cx.listener(Self::open_permanent))
6660                .on_action(cx.listener(Self::open_split_vertical))
6661                .on_action(cx.listener(Self::open_split_horizontal))
6662                .on_action(cx.listener(Self::confirm))
6663                .on_action(cx.listener(Self::cancel))
6664                .on_action(cx.listener(Self::copy_path))
6665                .on_action(cx.listener(Self::copy_relative_path))
6666                .on_action(cx.listener(Self::new_search_in_directory))
6667                .on_action(cx.listener(Self::unfold_directory))
6668                .on_action(cx.listener(Self::fold_directory))
6669                .on_action(cx.listener(Self::remove_from_project))
6670                .on_action(cx.listener(Self::compare_marked_files))
6671                .when(cx.has_flag::<ProjectPanelUndoRedoFeatureFlag>(), |el| {
6672                    el.on_action(cx.listener(Self::undo))
6673                        .on_action(cx.listener(Self::redo))
6674                })
6675                .when(!project.is_read_only(cx), |el| {
6676                    el.on_action(cx.listener(Self::new_file))
6677                        .on_action(cx.listener(Self::new_directory))
6678                        .on_action(cx.listener(Self::rename))
6679                        .on_action(cx.listener(Self::delete))
6680                        .on_action(cx.listener(Self::cut))
6681                        .on_action(cx.listener(Self::copy))
6682                        .on_action(cx.listener(Self::paste))
6683                        .on_action(cx.listener(Self::duplicate))
6684                        .on_action(cx.listener(Self::restore_file))
6685                        .when(!project.is_remote(), |el| {
6686                            el.on_action(cx.listener(Self::trash))
6687                        })
6688                })
6689                .when(
6690                    project.is_local() || project.is_via_wsl_with_host_interop(cx),
6691                    |el| {
6692                        el.on_action(cx.listener(Self::reveal_in_finder))
6693                            .on_action(cx.listener(Self::open_system))
6694                            .on_action(cx.listener(Self::open_in_terminal))
6695                    },
6696                )
6697                .when(project.is_via_remote_server(), |el| {
6698                    el.on_action(cx.listener(Self::open_in_terminal))
6699                        .on_action(cx.listener(Self::download_from_remote))
6700                })
6701                .track_focus(&self.focus_handle(cx))
6702                .child(
6703                    v_flex()
6704                        .child(
6705                            uniform_list("entries", item_count, {
6706                                cx.processor(|this, range: Range<usize>, window, cx| {
6707                                    this.rendered_entries_len = range.end - range.start;
6708                                    let mut items = Vec::with_capacity(this.rendered_entries_len);
6709                                    this.for_each_visible_entry(
6710                                        range,
6711                                        window,
6712                                        cx,
6713                                        &mut |id, details, window, cx| {
6714                                            items.push(this.render_entry(id, details, window, cx));
6715                                        },
6716                                    );
6717                                    items
6718                                })
6719                            })
6720                            .when(show_indent_guides, |list| {
6721                                list.with_decoration(
6722                                    ui::indent_guides(
6723                                        px(indent_size),
6724                                        IndentGuideColors::panel(cx),
6725                                    )
6726                                    .with_compute_indents_fn(
6727                                        cx.entity(),
6728                                        |this, range, window, cx| {
6729                                            let mut items =
6730                                                SmallVec::with_capacity(range.end - range.start);
6731                                            this.iter_visible_entries(
6732                                                range,
6733                                                window,
6734                                                cx,
6735                                                &mut |entry, _, entries, _, _| {
6736                                                    let (depth, _) =
6737                                                        Self::calculate_depth_and_difference(
6738                                                            entry, entries,
6739                                                        );
6740                                                    items.push(depth);
6741                                                },
6742                                            );
6743                                            items
6744                                        },
6745                                    )
6746                                    .on_click(cx.listener(
6747                                        |this,
6748                                         active_indent_guide: &IndentGuideLayout,
6749                                         window,
6750                                         cx| {
6751                                            if window.modifiers().secondary() {
6752                                                let ix = active_indent_guide.offset.y;
6753                                                let Some((target_entry, worktree)) = maybe!({
6754                                                    let (worktree_id, entry) =
6755                                                        this.entry_at_index(ix)?;
6756                                                    let worktree = this
6757                                                        .project
6758                                                        .read(cx)
6759                                                        .worktree_for_id(worktree_id, cx)?;
6760                                                    let target_entry = worktree
6761                                                        .read(cx)
6762                                                        .entry_for_path(&entry.path.parent()?)?;
6763                                                    Some((target_entry, worktree))
6764                                                }) else {
6765                                                    return;
6766                                                };
6767
6768                                                this.collapse_entry(
6769                                                    target_entry.clone(),
6770                                                    worktree,
6771                                                    window,
6772                                                    cx,
6773                                                );
6774                                            }
6775                                        },
6776                                    ))
6777                                    .with_render_fn(
6778                                        cx.entity(),
6779                                        move |this, params, _, cx| {
6780                                            const LEFT_OFFSET: Pixels = px(14.);
6781                                            const PADDING_Y: Pixels = px(4.);
6782                                            const HITBOX_OVERDRAW: Pixels = px(3.);
6783
6784                                            let active_indent_guide_index = this
6785                                                .find_active_indent_guide(
6786                                                    &params.indent_guides,
6787                                                    cx,
6788                                                );
6789
6790                                            let indent_size = params.indent_size;
6791                                            let item_height = params.item_height;
6792
6793                                            params
6794                                                .indent_guides
6795                                                .into_iter()
6796                                                .enumerate()
6797                                                .map(|(idx, layout)| {
6798                                                    let offset = if layout.continues_offscreen {
6799                                                        px(0.)
6800                                                    } else {
6801                                                        PADDING_Y
6802                                                    };
6803                                                    let bounds = Bounds::new(
6804                                                        point(
6805                                                            layout.offset.x * indent_size
6806                                                                + LEFT_OFFSET,
6807                                                            layout.offset.y * item_height + offset,
6808                                                        ),
6809                                                        size(
6810                                                            px(1.),
6811                                                            layout.length * item_height
6812                                                                - offset * 2.,
6813                                                        ),
6814                                                    );
6815                                                    ui::RenderedIndentGuide {
6816                                                        bounds,
6817                                                        layout,
6818                                                        is_active: Some(idx)
6819                                                            == active_indent_guide_index,
6820                                                        hitbox: Some(Bounds::new(
6821                                                            point(
6822                                                                bounds.origin.x - HITBOX_OVERDRAW,
6823                                                                bounds.origin.y,
6824                                                            ),
6825                                                            size(
6826                                                                bounds.size.width
6827                                                                    + HITBOX_OVERDRAW * 2.,
6828                                                                bounds.size.height,
6829                                                            ),
6830                                                        )),
6831                                                    }
6832                                                })
6833                                                .collect()
6834                                        },
6835                                    ),
6836                                )
6837                            })
6838                            .when(show_sticky_entries, |list| {
6839                                let sticky_items = ui::sticky_items(
6840                                    cx.entity(),
6841                                    |this, range, window, cx| {
6842                                        let mut items =
6843                                            SmallVec::with_capacity(range.end - range.start);
6844                                        this.iter_visible_entries(
6845                                            range,
6846                                            window,
6847                                            cx,
6848                                            &mut |entry, index, entries, _, _| {
6849                                                let (depth, _) =
6850                                                    Self::calculate_depth_and_difference(
6851                                                        entry, entries,
6852                                                    );
6853                                                let candidate =
6854                                                    StickyProjectPanelCandidate { index, depth };
6855                                                items.push(candidate);
6856                                            },
6857                                        );
6858                                        items
6859                                    },
6860                                    |this, marker_entry, window, cx| {
6861                                        let sticky_entries =
6862                                            this.render_sticky_entries(marker_entry, window, cx);
6863                                        this.sticky_items_count = sticky_entries.len();
6864                                        sticky_entries
6865                                    },
6866                                );
6867                                list.with_decoration(if show_indent_guides {
6868                                    sticky_items.with_decoration(
6869                                        ui::indent_guides(
6870                                            px(indent_size),
6871                                            IndentGuideColors::panel(cx),
6872                                        )
6873                                        .with_render_fn(
6874                                            cx.entity(),
6875                                            move |_, params, _, _| {
6876                                                const LEFT_OFFSET: Pixels = px(14.);
6877
6878                                                let indent_size = params.indent_size;
6879                                                let item_height = params.item_height;
6880
6881                                                params
6882                                                    .indent_guides
6883                                                    .into_iter()
6884                                                    .map(|layout| {
6885                                                        let bounds = Bounds::new(
6886                                                            point(
6887                                                                layout.offset.x * indent_size
6888                                                                    + LEFT_OFFSET,
6889                                                                layout.offset.y * item_height,
6890                                                            ),
6891                                                            size(
6892                                                                px(1.),
6893                                                                layout.length * item_height,
6894                                                            ),
6895                                                        );
6896                                                        ui::RenderedIndentGuide {
6897                                                            bounds,
6898                                                            layout,
6899                                                            is_active: false,
6900                                                            hitbox: None,
6901                                                        }
6902                                                    })
6903                                                    .collect()
6904                                            },
6905                                        ),
6906                                    )
6907                                } else {
6908                                    sticky_items
6909                                })
6910                            })
6911                            .with_sizing_behavior(ListSizingBehavior::Infer)
6912                            .with_horizontal_sizing_behavior(if horizontal_scroll {
6913                                ListHorizontalSizingBehavior::Unconstrained
6914                            } else {
6915                                ListHorizontalSizingBehavior::FitList
6916                            })
6917                            .when(horizontal_scroll, |list| {
6918                                list.with_width_from_item(self.state.max_width_item_index)
6919                            })
6920                            .track_scroll(&self.scroll_handle),
6921                        )
6922                        .child(
6923                            div()
6924                                .id("project-panel-blank-area")
6925                                .block_mouse_except_scroll()
6926                                .flex_grow()
6927                                .on_scroll_wheel({
6928                                    let scroll_handle = self.scroll_handle.clone();
6929                                    let entity_id = cx.entity().entity_id();
6930                                    move |event, window, cx| {
6931                                        let state = scroll_handle.0.borrow();
6932                                        let base_handle = &state.base_handle;
6933                                        let current_offset = base_handle.offset();
6934                                        let max_offset = base_handle.max_offset();
6935                                        let delta = event.delta.pixel_delta(window.line_height());
6936                                        let new_offset = (current_offset + delta)
6937                                            .clamp(&max_offset.neg(), &Point::default());
6938
6939                                        if new_offset != current_offset {
6940                                            base_handle.set_offset(new_offset);
6941                                            cx.notify(entity_id);
6942                                        }
6943                                    }
6944                                })
6945                                .when(
6946                                    self.drag_target_entry.as_ref().is_some_and(
6947                                        |entry| match entry {
6948                                            DragTarget::Background => true,
6949                                            DragTarget::Entry {
6950                                                highlight_entry_id, ..
6951                                            } => self.state.last_worktree_root_id.is_some_and(
6952                                                |root_id| *highlight_entry_id == root_id,
6953                                            ),
6954                                        },
6955                                    ),
6956                                    |div| div.bg(cx.theme().colors().drop_target_background),
6957                                )
6958                                .on_drag_move::<ExternalPaths>(cx.listener(
6959                                    move |this, event: &DragMoveEvent<ExternalPaths>, _, _| {
6960                                        let Some(_last_root_id) = this.state.last_worktree_root_id
6961                                        else {
6962                                            return;
6963                                        };
6964                                        if event.bounds.contains(&event.event.position) {
6965                                            this.drag_target_entry = Some(DragTarget::Background);
6966                                        } else {
6967                                            if this.drag_target_entry.as_ref().is_some_and(|e| {
6968                                                matches!(e, DragTarget::Background)
6969                                            }) {
6970                                                this.drag_target_entry = None;
6971                                            }
6972                                        }
6973                                    },
6974                                ))
6975                                .on_drag_move::<DraggedSelection>(cx.listener(
6976                                    move |this, event: &DragMoveEvent<DraggedSelection>, _, cx| {
6977                                        let Some(last_root_id) = this.state.last_worktree_root_id
6978                                        else {
6979                                            return;
6980                                        };
6981                                        if event.bounds.contains(&event.event.position) {
6982                                            let drag_state = event.drag(cx);
6983                                            if this.should_highlight_background_for_selection_drag(
6984                                                &drag_state,
6985                                                last_root_id,
6986                                                cx,
6987                                            ) {
6988                                                this.drag_target_entry =
6989                                                    Some(DragTarget::Background);
6990                                            }
6991                                        } else {
6992                                            if this.drag_target_entry.as_ref().is_some_and(|e| {
6993                                                matches!(e, DragTarget::Background)
6994                                            }) {
6995                                                this.drag_target_entry = None;
6996                                            }
6997                                        }
6998                                    },
6999                                ))
7000                                .on_drop(cx.listener(
7001                                    move |this, external_paths: &ExternalPaths, window, cx| {
7002                                        this.drag_target_entry = None;
7003                                        this.hover_scroll_task.take();
7004                                        if let Some(entry_id) = this.state.last_worktree_root_id {
7005                                            this.drop_external_files(
7006                                                external_paths.paths(),
7007                                                entry_id,
7008                                                window,
7009                                                cx,
7010                                            );
7011                                        }
7012                                        cx.stop_propagation();
7013                                    },
7014                                ))
7015                                .on_drop(cx.listener(
7016                                    move |this, selections: &DraggedSelection, window, cx| {
7017                                        this.drag_target_entry = None;
7018                                        this.hover_scroll_task.take();
7019                                        if let Some(entry_id) = this.state.last_worktree_root_id {
7020                                            this.drag_onto(selections, entry_id, false, window, cx);
7021                                        }
7022                                        cx.stop_propagation();
7023                                    },
7024                                ))
7025                                .on_click(cx.listener(|this, event, window, cx| {
7026                                    if matches!(event, gpui::ClickEvent::Keyboard(_)) {
7027                                        return;
7028                                    }
7029                                    cx.stop_propagation();
7030                                    this.selection = None;
7031                                    this.marked_entries.clear();
7032                                    this.focus_handle(cx).focus(window, cx);
7033                                }))
7034                                .on_mouse_down(
7035                                    MouseButton::Right,
7036                                    cx.listener(move |this, event: &MouseDownEvent, window, cx| {
7037                                        // When deploying the context menu anywhere below the last project entry,
7038                                        // act as if the user clicked the root of the last worktree.
7039                                        if let Some(entry_id) = this.state.last_worktree_root_id {
7040                                            this.deploy_context_menu(
7041                                                event.position,
7042                                                entry_id,
7043                                                window,
7044                                                cx,
7045                                            );
7046                                        }
7047                                    }),
7048                                )
7049                                .when(!project.is_read_only(cx), |el| {
7050                                    el.on_click(cx.listener(
7051                                        |this, event: &gpui::ClickEvent, window, cx| {
7052                                            if event.click_count() > 1
7053                                                && let Some(entry_id) =
7054                                                    this.state.last_worktree_root_id
7055                                            {
7056                                                let project = this.project.read(cx);
7057
7058                                                let worktree_id = if let Some(worktree) =
7059                                                    project.worktree_for_entry(entry_id, cx)
7060                                                {
7061                                                    worktree.read(cx).id()
7062                                                } else {
7063                                                    return;
7064                                                };
7065
7066                                                this.selection = Some(SelectedEntry {
7067                                                    worktree_id,
7068                                                    entry_id,
7069                                                });
7070
7071                                                this.new_file(&NewFile, window, cx);
7072                                            }
7073                                        },
7074                                    ))
7075                                }),
7076                        )
7077                        .size_full(),
7078                )
7079                .custom_scrollbars(
7080                    {
7081                        let mut scrollbars =
7082                            Scrollbars::for_settings::<ProjectPanelScrollbarProxy>()
7083                                .tracked_scroll_handle(&self.scroll_handle);
7084                        if horizontal_scroll {
7085                            scrollbars = scrollbars.with_track_along(
7086                                ScrollAxes::Horizontal,
7087                                cx.theme().colors().panel_background,
7088                            );
7089                        }
7090                        scrollbars.notify_content()
7091                    },
7092                    window,
7093                    cx,
7094                )
7095                .children(self.context_menu.as_ref().map(|(menu, position, _)| {
7096                    deferred(
7097                        anchored()
7098                            .position(*position)
7099                            .anchor(gpui::Corner::TopLeft)
7100                            .child(menu.clone()),
7101                    )
7102                    .with_priority(3)
7103                }))
7104        } else {
7105            let focus_handle = self.focus_handle(cx);
7106
7107            v_flex()
7108                .id("empty-project_panel")
7109                .p_4()
7110                .size_full()
7111                .items_center()
7112                .justify_center()
7113                .gap_1()
7114                .track_focus(&self.focus_handle(cx))
7115                .child(
7116                    Button::new("open_project", "Open Project")
7117                        .full_width()
7118                        .key_binding(KeyBinding::for_action_in(
7119                            &workspace::Open::default(),
7120                            &focus_handle,
7121                            cx,
7122                        ))
7123                        .on_click(cx.listener(|this, _, window, cx| {
7124                            this.workspace
7125                                .update(cx, |_, cx| {
7126                                    window.dispatch_action(
7127                                        workspace::Open::default().boxed_clone(),
7128                                        cx,
7129                                    );
7130                                })
7131                                .log_err();
7132                        })),
7133                )
7134                .child(
7135                    h_flex()
7136                        .w_1_2()
7137                        .gap_2()
7138                        .child(Divider::horizontal())
7139                        .child(Label::new("or").size(LabelSize::XSmall).color(Color::Muted))
7140                        .child(Divider::horizontal()),
7141                )
7142                .child(
7143                    Button::new("clone_repo", "Clone Repository")
7144                        .full_width()
7145                        .on_click(cx.listener(|this, _, window, cx| {
7146                            this.workspace
7147                                .update(cx, |_, cx| {
7148                                    window.dispatch_action(git::Clone.boxed_clone(), cx);
7149                                })
7150                                .log_err();
7151                        })),
7152                )
7153                .when(is_local, |div| {
7154                    div.when(panel_settings.drag_and_drop, |div| {
7155                        div.drag_over::<ExternalPaths>(|style, _, _, cx| {
7156                            style.bg(cx.theme().colors().drop_target_background)
7157                        })
7158                        .on_drop(cx.listener(
7159                            move |this, external_paths: &ExternalPaths, window, cx| {
7160                                this.drag_target_entry = None;
7161                                this.hover_scroll_task.take();
7162                                if let Some(task) = this
7163                                    .workspace
7164                                    .update(cx, |workspace, cx| {
7165                                        workspace.open_workspace_for_paths(
7166                                            OpenMode::Activate,
7167                                            external_paths.paths().to_owned(),
7168                                            window,
7169                                            cx,
7170                                        )
7171                                    })
7172                                    .log_err()
7173                                {
7174                                    task.detach_and_log_err(cx);
7175                                }
7176                                cx.stop_propagation();
7177                            },
7178                        ))
7179                    })
7180                })
7181        }
7182    }
7183}
7184
7185impl Render for DraggedProjectEntryView {
7186    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
7187        let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
7188        h_flex()
7189            .font(ui_font)
7190            .pl(self.click_offset.x + px(12.))
7191            .pt(self.click_offset.y + px(12.))
7192            .child(
7193                div()
7194                    .flex()
7195                    .gap_1()
7196                    .items_center()
7197                    .py_1()
7198                    .px_2()
7199                    .rounded_lg()
7200                    .bg(cx.theme().colors().background)
7201                    .map(|this| {
7202                        if self.selections.len() > 1 && self.selections.contains(&self.selection) {
7203                            this.child(Label::new(format!("{} entries", self.selections.len())))
7204                        } else {
7205                            this.child(if let Some(icon) = &self.icon {
7206                                div().child(Icon::from_path(icon.clone()))
7207                            } else {
7208                                div()
7209                            })
7210                            .child(Label::new(self.filename.clone()))
7211                        }
7212                    }),
7213            )
7214    }
7215}
7216
7217impl EventEmitter<Event> for ProjectPanel {}
7218
7219impl EventEmitter<PanelEvent> for ProjectPanel {}
7220
7221impl Panel for ProjectPanel {
7222    fn position(&self, _: &Window, cx: &App) -> DockPosition {
7223        match ProjectPanelSettings::get_global(cx).dock {
7224            DockSide::Left => DockPosition::Left,
7225            DockSide::Right => DockPosition::Right,
7226        }
7227    }
7228
7229    fn position_is_valid(&self, position: DockPosition) -> bool {
7230        matches!(position, DockPosition::Left | DockPosition::Right)
7231    }
7232
7233    fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
7234        settings::update_settings_file(self.fs.clone(), cx, move |settings, _| {
7235            let dock = match position {
7236                DockPosition::Left | DockPosition::Bottom => DockSide::Left,
7237                DockPosition::Right => DockSide::Right,
7238            };
7239            settings.project_panel.get_or_insert_default().dock = Some(dock);
7240        });
7241    }
7242
7243    fn default_size(&self, _: &Window, cx: &App) -> Pixels {
7244        ProjectPanelSettings::get_global(cx).default_width
7245    }
7246
7247    fn icon(&self, _: &Window, cx: &App) -> Option<IconName> {
7248        ProjectPanelSettings::get_global(cx)
7249            .button
7250            .then_some(IconName::FileTree)
7251    }
7252
7253    fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
7254        Some("Project Panel")
7255    }
7256
7257    fn toggle_action(&self) -> Box<dyn Action> {
7258        Box::new(ToggleFocus)
7259    }
7260
7261    fn persistent_name() -> &'static str {
7262        "Project Panel"
7263    }
7264
7265    fn panel_key() -> &'static str {
7266        PROJECT_PANEL_KEY
7267    }
7268
7269    fn starts_open(&self, _: &Window, cx: &App) -> bool {
7270        if !ProjectPanelSettings::get_global(cx).starts_open {
7271            return false;
7272        }
7273
7274        let project = &self.project.read(cx);
7275        project.visible_worktrees(cx).any(|tree| {
7276            tree.read(cx)
7277                .root_entry()
7278                .is_some_and(|entry| entry.is_dir())
7279        })
7280    }
7281
7282    fn activation_priority(&self) -> u32 {
7283        1
7284    }
7285}
7286
7287impl Focusable for ProjectPanel {
7288    fn focus_handle(&self, _cx: &App) -> FocusHandle {
7289        self.focus_handle.clone()
7290    }
7291}
7292
7293impl ClipboardEntry {
7294    fn is_cut(&self) -> bool {
7295        matches!(self, Self::Cut { .. })
7296    }
7297
7298    fn items(&self) -> &BTreeSet<SelectedEntry> {
7299        match self {
7300            ClipboardEntry::Copied(entries) | ClipboardEntry::Cut(entries) => entries,
7301        }
7302    }
7303
7304    fn into_copy_entry(self) -> Self {
7305        match self {
7306            ClipboardEntry::Copied(_) => self,
7307            ClipboardEntry::Cut(entries) => ClipboardEntry::Copied(entries),
7308        }
7309    }
7310}
7311
7312#[inline]
7313fn cmp_worktree_entries(
7314    a: &Entry,
7315    b: &Entry,
7316    mode: &settings::ProjectPanelSortMode,
7317    order: &settings::ProjectPanelSortOrder,
7318) -> cmp::Ordering {
7319    let a = (&*a.path, a.is_file());
7320    let b = (&*b.path, b.is_file());
7321    util::paths::compare_rel_paths_by(a, b, (*mode).into(), (*order).into())
7322}
7323
7324pub fn sort_worktree_entries(
7325    entries: &mut [impl AsRef<Entry>],
7326    mode: settings::ProjectPanelSortMode,
7327    order: settings::ProjectPanelSortOrder,
7328) {
7329    entries.sort_by(|lhs, rhs| cmp_worktree_entries(lhs.as_ref(), rhs.as_ref(), &mode, &order));
7330}
7331
7332pub fn par_sort_worktree_entries(
7333    entries: &mut Vec<GitEntry>,
7334    mode: settings::ProjectPanelSortMode,
7335    order: settings::ProjectPanelSortOrder,
7336) {
7337    entries.par_sort_by(|lhs, rhs| cmp_worktree_entries(lhs, rhs, &mode, &order));
7338}
7339
7340fn git_status_indicator(git_status: GitSummary) -> Option<(&'static str, Color)> {
7341    if git_status.conflict > 0 {
7342        return Some(("!", Color::Conflict));
7343    }
7344    if git_status.untracked > 0 {
7345        return Some(("U", Color::Created));
7346    }
7347    if git_status.worktree.deleted > 0 {
7348        return Some(("D", Color::Deleted));
7349    }
7350    if git_status.worktree.modified > 0 {
7351        return Some(("M", Color::Warning));
7352    }
7353    if git_status.index.deleted > 0 {
7354        return Some(("D", Color::Deleted));
7355    }
7356    if git_status.index.modified > 0 {
7357        return Some(("M", Color::Modified));
7358    }
7359    if git_status.index.added > 0 {
7360        return Some(("A", Color::Created));
7361    }
7362    None
7363}
7364
7365#[cfg(test)]
7366mod project_panel_tests;
7367mod tests;