project_panel.rs

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