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