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