project_panel.rs

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