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