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