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