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