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