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, KeyBinding, Label, LabelSize,
  65    ListItem, ListItemSpacing, ScrollAxes, ScrollableHandle, Scrollbars, StickyCandidate, Tooltip,
  66    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
5356        let id: ElementId = if is_sticky {
5357            SharedString::from(format!("project_panel_sticky_item_{}", entry_id.to_usize())).into()
5358        } else {
5359            (entry_id.to_proto() as usize).into()
5360        };
5361
5362        div()
5363            .id(id.clone())
5364            .relative()
5365            .group(GROUP_NAME)
5366            .cursor_pointer()
5367            .rounded_none()
5368            .bg(bg_color)
5369            .border_1()
5370            .border_r_2()
5371            .border_color(border_color)
5372            .hover(|style| style.bg(bg_hover_color).border_color(border_hover_color))
5373            .when(is_sticky, |this| this.block_mouse_except_scroll())
5374            .when(!is_sticky, |this| {
5375                this.when(
5376                    is_highlighted && folded_directory_drag_target.is_none(),
5377                    |this| {
5378                        this.border_color(transparent_white())
5379                            .bg(item_colors.drag_over)
5380                    },
5381                )
5382                .when(settings.drag_and_drop, |this| {
5383                    this.on_drag_move::<ExternalPaths>(cx.listener(
5384                        move |this, event: &DragMoveEvent<ExternalPaths>, _, cx| {
5385                            let is_current_target =
5386                                this.drag_target_entry
5387                                    .as_ref()
5388                                    .and_then(|entry| match entry {
5389                                        DragTarget::Entry {
5390                                            entry_id: target_id,
5391                                            ..
5392                                        } => Some(*target_id),
5393                                        DragTarget::Background { .. } => None,
5394                                    })
5395                                    == Some(entry_id);
5396
5397                            if !event.bounds.contains(&event.event.position) {
5398                                // Entry responsible for setting drag target is also responsible to
5399                                // clear it up after drag is out of bounds
5400                                if is_current_target {
5401                                    this.drag_target_entry = None;
5402                                }
5403                                return;
5404                            }
5405
5406                            if is_current_target {
5407                                return;
5408                            }
5409
5410                            this.marked_entries.clear();
5411
5412                            let Some((entry_id, highlight_entry_id)) = maybe!({
5413                                let target_worktree = this
5414                                    .project
5415                                    .read(cx)
5416                                    .worktree_for_id(selection.worktree_id, cx)?
5417                                    .read(cx);
5418                                let target_entry =
5419                                    target_worktree.entry_for_path(&path_for_external_paths)?;
5420                                let highlight_entry_id = this.highlight_entry_for_external_drag(
5421                                    target_entry,
5422                                    target_worktree,
5423                                )?;
5424                                Some((target_entry.id, highlight_entry_id))
5425                            }) else {
5426                                return;
5427                            };
5428
5429                            this.drag_target_entry = Some(DragTarget::Entry {
5430                                entry_id,
5431                                highlight_entry_id,
5432                            });
5433                        },
5434                    ))
5435                    .on_drop(cx.listener(
5436                        move |this, external_paths: &ExternalPaths, window, cx| {
5437                            this.drag_target_entry = None;
5438                            this.hover_scroll_task.take();
5439                            this.drop_external_files(external_paths.paths(), entry_id, window, cx);
5440                            cx.stop_propagation();
5441                        },
5442                    ))
5443                    .on_drag_move::<DraggedSelection>(cx.listener(
5444                        move |this, event: &DragMoveEvent<DraggedSelection>, window, cx| {
5445                            let is_current_target =
5446                                this.drag_target_entry
5447                                    .as_ref()
5448                                    .and_then(|entry| match entry {
5449                                        DragTarget::Entry {
5450                                            entry_id: target_id,
5451                                            ..
5452                                        } => Some(*target_id),
5453                                        DragTarget::Background { .. } => None,
5454                                    })
5455                                    == Some(entry_id);
5456
5457                            if !event.bounds.contains(&event.event.position) {
5458                                // Entry responsible for setting drag target is also responsible to
5459                                // clear it up after drag is out of bounds
5460                                if is_current_target {
5461                                    this.drag_target_entry = None;
5462                                }
5463                                return;
5464                            }
5465
5466                            if is_current_target {
5467                                return;
5468                            }
5469
5470                            let drag_state = event.drag(cx);
5471
5472                            if drag_state.items().count() == 1 {
5473                                this.marked_entries.clear();
5474                                this.marked_entries.push(drag_state.active_selection);
5475                            }
5476
5477                            let Some((entry_id, highlight_entry_id)) = maybe!({
5478                                let target_worktree = this
5479                                    .project
5480                                    .read(cx)
5481                                    .worktree_for_id(selection.worktree_id, cx)?
5482                                    .read(cx);
5483                                let target_entry =
5484                                    target_worktree.entry_for_path(&path_for_dragged_selection)?;
5485                                let highlight_entry_id = this.highlight_entry_for_selection_drag(
5486                                    target_entry,
5487                                    target_worktree,
5488                                    drag_state,
5489                                    cx,
5490                                )?;
5491                                Some((target_entry.id, highlight_entry_id))
5492                            }) else {
5493                                return;
5494                            };
5495
5496                            this.drag_target_entry = Some(DragTarget::Entry {
5497                                entry_id,
5498                                highlight_entry_id,
5499                            });
5500
5501                            this.hover_expand_task.take();
5502
5503                            if !kind.is_dir()
5504                                || this
5505                                    .state
5506                                    .expanded_dir_ids
5507                                    .get(&details.worktree_id)
5508                                    .is_some_and(|ids| ids.binary_search(&entry_id).is_ok())
5509                            {
5510                                return;
5511                            }
5512
5513                            let bounds = event.bounds;
5514                            this.hover_expand_task =
5515                                Some(cx.spawn_in(window, async move |this, cx| {
5516                                    cx.background_executor()
5517                                        .timer(Duration::from_millis(500))
5518                                        .await;
5519                                    this.update_in(cx, |this, window, cx| {
5520                                        this.hover_expand_task.take();
5521                                        if this.drag_target_entry.as_ref().and_then(|entry| {
5522                                            match entry {
5523                                                DragTarget::Entry {
5524                                                    entry_id: target_id,
5525                                                    ..
5526                                                } => Some(*target_id),
5527                                                DragTarget::Background { .. } => None,
5528                                            }
5529                                        }) == Some(entry_id)
5530                                            && bounds.contains(&window.mouse_position())
5531                                        {
5532                                            this.expand_entry(worktree_id, entry_id, cx);
5533                                            this.update_visible_entries(
5534                                                Some((worktree_id, entry_id)),
5535                                                false,
5536                                                false,
5537                                                window,
5538                                                cx,
5539                                            );
5540                                            cx.notify();
5541                                        }
5542                                    })
5543                                    .ok();
5544                                }));
5545                        },
5546                    ))
5547                    .on_drag(dragged_selection, {
5548                        let active_component =
5549                            self.state.ancestors.get(&entry_id).and_then(|ancestors| {
5550                                ancestors.active_component(&details.filename)
5551                            });
5552                        move |selection, click_offset, _window, cx| {
5553                            let filename = active_component
5554                                .as_ref()
5555                                .unwrap_or_else(|| &details.filename);
5556                            cx.new(|_| DraggedProjectEntryView {
5557                                icon: details.icon.clone(),
5558                                filename: filename.clone(),
5559                                click_offset,
5560                                selection: selection.active_selection,
5561                                selections: selection.marked_selections.clone(),
5562                            })
5563                        }
5564                    })
5565                    .on_drop(cx.listener(
5566                        move |this, selections: &DraggedSelection, window, cx| {
5567                            this.drag_target_entry = None;
5568                            this.hover_scroll_task.take();
5569                            this.hover_expand_task.take();
5570                            if folded_directory_drag_target.is_some() {
5571                                return;
5572                            }
5573                            this.drag_onto(selections, entry_id, kind.is_file(), window, cx);
5574                        },
5575                    ))
5576                })
5577            })
5578            .on_mouse_down(
5579                MouseButton::Left,
5580                cx.listener(move |this, _, _, cx| {
5581                    this.mouse_down = true;
5582                    cx.propagate();
5583                }),
5584            )
5585            .on_click(
5586                cx.listener(move |project_panel, event: &gpui::ClickEvent, window, cx| {
5587                    if event.is_right_click() || event.first_focus() || show_editor {
5588                        return;
5589                    }
5590                    if event.standard_click() {
5591                        project_panel.mouse_down = false;
5592                    }
5593                    cx.stop_propagation();
5594
5595                    if let Some(selection) =
5596                        project_panel.selection.filter(|_| event.modifiers().shift)
5597                    {
5598                        let current_selection = project_panel.index_for_selection(selection);
5599                        let clicked_entry = SelectedEntry {
5600                            entry_id,
5601                            worktree_id,
5602                        };
5603                        let target_selection = project_panel.index_for_selection(clicked_entry);
5604                        if let Some(((_, _, source_index), (_, _, target_index))) =
5605                            current_selection.zip(target_selection)
5606                        {
5607                            let range_start = source_index.min(target_index);
5608                            let range_end = source_index.max(target_index) + 1;
5609                            let mut new_selections = Vec::new();
5610                            project_panel.for_each_visible_entry(
5611                                range_start..range_end,
5612                                window,
5613                                cx,
5614                                &mut |entry_id, details, _, _| {
5615                                    new_selections.push(SelectedEntry {
5616                                        entry_id,
5617                                        worktree_id: details.worktree_id,
5618                                    });
5619                                },
5620                            );
5621
5622                            for selection in &new_selections {
5623                                if !project_panel.marked_entries.contains(selection) {
5624                                    project_panel.marked_entries.push(*selection);
5625                                }
5626                            }
5627
5628                            project_panel.selection = Some(clicked_entry);
5629                            if !project_panel.marked_entries.contains(&clicked_entry) {
5630                                project_panel.marked_entries.push(clicked_entry);
5631                            }
5632                        }
5633                    } else if event.modifiers().secondary() {
5634                        if event.click_count() > 1 {
5635                            project_panel.split_entry(entry_id, false, None, cx);
5636                        } else {
5637                            project_panel.selection = Some(selection);
5638                            if let Some(position) = project_panel
5639                                .marked_entries
5640                                .iter()
5641                                .position(|e| *e == selection)
5642                            {
5643                                project_panel.marked_entries.remove(position);
5644                            } else {
5645                                project_panel.marked_entries.push(selection);
5646                            }
5647                        }
5648                    } else if kind.is_dir() {
5649                        project_panel.marked_entries.clear();
5650                        if is_sticky
5651                            && let Some((_, _, index)) =
5652                                project_panel.index_for_entry(entry_id, worktree_id)
5653                        {
5654                            project_panel
5655                                .scroll_handle
5656                                .scroll_to_item_strict_with_offset(
5657                                    index,
5658                                    ScrollStrategy::Top,
5659                                    sticky_index.unwrap_or(0),
5660                                );
5661                            cx.notify();
5662                            // move down by 1px so that clicked item
5663                            // don't count as sticky anymore
5664                            cx.on_next_frame(window, |_, window, cx| {
5665                                cx.on_next_frame(window, |this, _, cx| {
5666                                    let mut offset = this.scroll_handle.offset();
5667                                    offset.y += px(1.);
5668                                    this.scroll_handle.set_offset(offset);
5669                                    cx.notify();
5670                                });
5671                            });
5672                            return;
5673                        }
5674                        if event.modifiers().alt {
5675                            project_panel.toggle_expand_all(entry_id, window, cx);
5676                        } else {
5677                            project_panel.toggle_expanded(entry_id, window, cx);
5678                        }
5679                    } else {
5680                        let preview_tabs_enabled =
5681                            PreviewTabsSettings::get_global(cx).enable_preview_from_project_panel;
5682                        let click_count = event.click_count();
5683                        let focus_opened_item = click_count > 1;
5684                        let allow_preview = preview_tabs_enabled && click_count == 1;
5685                        project_panel.open_entry(entry_id, focus_opened_item, allow_preview, cx);
5686                    }
5687                }),
5688            )
5689            .child(
5690                ListItem::new(id)
5691                    .indent_level(depth)
5692                    .indent_step_size(px(settings.indent_size))
5693                    .spacing(match settings.entry_spacing {
5694                        ProjectPanelEntrySpacing::Comfortable => ListItemSpacing::Dense,
5695                        ProjectPanelEntrySpacing::Standard => ListItemSpacing::ExtraDense,
5696                    })
5697                    .selectable(false)
5698                    .when(
5699                        canonical_path.is_some() || diagnostic_count.is_some(),
5700                        |this| {
5701                            let symlink_element = canonical_path.map(|path| {
5702                                div()
5703                                    .id("symlink_icon")
5704                                    .tooltip(move |_window, cx| {
5705                                        Tooltip::with_meta(
5706                                            path.to_string(),
5707                                            None,
5708                                            "Symbolic Link",
5709                                            cx,
5710                                        )
5711                                    })
5712                                    .child(
5713                                        Icon::new(IconName::ArrowUpRight)
5714                                            .size(IconSize::Indicator)
5715                                            .color(filename_text_color),
5716                                    )
5717                            });
5718                            this.end_slot::<AnyElement>(
5719                                h_flex()
5720                                    .gap_1()
5721                                    .flex_none()
5722                                    .pr_3()
5723                                    .when_some(diagnostic_count, |this, count| {
5724                                        this.when(count.error_count > 0, |this| {
5725                                            this.child(
5726                                                Label::new(count.capped_error_count())
5727                                                    .size(LabelSize::Small)
5728                                                    .color(Color::Error),
5729                                            )
5730                                        })
5731                                        .when(
5732                                            count.warning_count > 0,
5733                                            |this| {
5734                                                this.child(
5735                                                    Label::new(count.capped_warning_count())
5736                                                        .size(LabelSize::Small)
5737                                                        .color(Color::Warning),
5738                                                )
5739                                            },
5740                                        )
5741                                    })
5742                                    .when_some(symlink_element, |this, el| this.child(el))
5743                                    .into_any_element(),
5744                            )
5745                        },
5746                    )
5747                    .child(if let Some(icon) = &icon {
5748                        if let Some((_, decoration_color)) =
5749                            entry_diagnostic_aware_icon_decoration_and_color(diagnostic_severity)
5750                        {
5751                            let is_warning = diagnostic_severity
5752                                .map(|severity| matches!(severity, DiagnosticSeverity::WARNING))
5753                                .unwrap_or(false);
5754                            div().child(
5755                                DecoratedIcon::new(
5756                                    Icon::from_path(icon.clone()).color(Color::Muted),
5757                                    Some(
5758                                        IconDecoration::new(
5759                                            if kind.is_file() {
5760                                                if is_warning {
5761                                                    IconDecorationKind::Triangle
5762                                                } else {
5763                                                    IconDecorationKind::X
5764                                                }
5765                                            } else {
5766                                                IconDecorationKind::Dot
5767                                            },
5768                                            bg_color,
5769                                            cx,
5770                                        )
5771                                        .group_name(Some(GROUP_NAME.into()))
5772                                        .knockout_hover_color(bg_hover_color)
5773                                        .color(decoration_color.color(cx))
5774                                        .position(Point {
5775                                            x: px(-2.),
5776                                            y: px(-2.),
5777                                        }),
5778                                    ),
5779                                )
5780                                .into_any_element(),
5781                            )
5782                        } else {
5783                            h_flex().child(Icon::from_path(icon.to_string()).color(Color::Muted))
5784                        }
5785                    } else if let Some((icon_name, color)) =
5786                        entry_diagnostic_aware_icon_name_and_color(diagnostic_severity)
5787                    {
5788                        h_flex()
5789                            .size(IconSize::default().rems())
5790                            .child(Icon::new(icon_name).color(color).size(IconSize::Small))
5791                    } else {
5792                        h_flex()
5793                            .size(IconSize::default().rems())
5794                            .invisible()
5795                            .flex_none()
5796                    })
5797                    .child(if show_editor {
5798                        h_flex().h_6().w_full().child(self.filename_editor.clone())
5799                    } else {
5800                        h_flex()
5801                            .h_6()
5802                            .map(|this| match self.state.ancestors.get(&entry_id) {
5803                                Some(folded_ancestors) => {
5804                                    this.children(self.render_folder_elements(
5805                                        folded_ancestors,
5806                                        entry_id,
5807                                        file_name,
5808                                        path_style,
5809                                        is_sticky,
5810                                        kind.is_file(),
5811                                        is_active || is_marked,
5812                                        settings.drag_and_drop,
5813                                        settings.bold_folder_labels,
5814                                        item_colors.drag_over,
5815                                        folded_directory_drag_target,
5816                                        filename_text_color,
5817                                        cx,
5818                                    ))
5819                                }
5820
5821                                None => this.child(
5822                                    Label::new(file_name)
5823                                        .single_line()
5824                                        .color(filename_text_color)
5825                                        .when(
5826                                            settings.bold_folder_labels && kind.is_dir(),
5827                                            |this| this.weight(FontWeight::SEMIBOLD),
5828                                        )
5829                                        .into_any_element(),
5830                                ),
5831                            })
5832                    })
5833                    .on_secondary_mouse_down(cx.listener(
5834                        move |this, event: &MouseDownEvent, window, cx| {
5835                            // Stop propagation to prevent the catch-all context menu for the project
5836                            // panel from being deployed.
5837                            cx.stop_propagation();
5838                            // Some context menu actions apply to all marked entries. If the user
5839                            // right-clicks on an entry that is not marked, they may not realize the
5840                            // action applies to multiple entries. To avoid inadvertent changes, all
5841                            // entries are unmarked.
5842                            if !this.marked_entries.contains(&selection) {
5843                                this.marked_entries.clear();
5844                            }
5845                            this.deploy_context_menu(event.position, entry_id, window, cx);
5846                        },
5847                    ))
5848                    .overflow_x(),
5849            )
5850            .when_some(validation_color_and_message, |this, (color, message)| {
5851                this.relative().child(deferred(
5852                    div()
5853                        .occlude()
5854                        .absolute()
5855                        .top_full()
5856                        .left(px(-1.)) // Used px over rem so that it doesn't change with font size
5857                        .right(px(-0.5))
5858                        .py_1()
5859                        .px_2()
5860                        .border_1()
5861                        .border_color(color)
5862                        .bg(cx.theme().colors().background)
5863                        .child(
5864                            Label::new(message)
5865                                .color(Color::from(color))
5866                                .size(LabelSize::Small),
5867                        ),
5868                ))
5869            })
5870    }
5871
5872    fn render_folder_elements(
5873        &self,
5874        folded_ancestors: &FoldedAncestors,
5875        entry_id: ProjectEntryId,
5876        file_name: String,
5877        path_style: PathStyle,
5878        is_sticky: bool,
5879        is_file: bool,
5880        is_active_or_marked: bool,
5881        drag_and_drop_enabled: bool,
5882        bold_folder_labels: bool,
5883        drag_over_color: Hsla,
5884        folded_directory_drag_target: Option<FoldedDirectoryDragTarget>,
5885        filename_text_color: Color,
5886        cx: &Context<Self>,
5887    ) -> impl Iterator<Item = AnyElement> {
5888        let components = Path::new(&file_name)
5889            .components()
5890            .map(|comp| comp.as_os_str().to_string_lossy().into_owned())
5891            .collect::<Vec<_>>();
5892        let active_index = folded_ancestors.active_index();
5893        let components_len = components.len();
5894        let delimiter = SharedString::new(path_style.primary_separator());
5895
5896        let path_component_elements =
5897            components
5898                .into_iter()
5899                .enumerate()
5900                .map(move |(index, component)| {
5901                    div()
5902                        .id(SharedString::from(format!(
5903                            "project_panel_path_component_{}_{index}",
5904                            entry_id.to_usize()
5905                        )))
5906                        .when(index == 0, |this| this.ml_neg_0p5())
5907                        .px_0p5()
5908                        .rounded_xs()
5909                        .hover(|style| style.bg(cx.theme().colors().element_active))
5910                        .when(!is_sticky, |div| {
5911                            div.when(index != components_len - 1, |div| {
5912                                let target_entry_id = folded_ancestors
5913                                    .ancestors
5914                                    .get(components_len - 1 - index)
5915                                    .cloned();
5916                                div.when(drag_and_drop_enabled, |div| {
5917                                    div.on_drag_move(cx.listener(
5918                                        move |this,
5919                                              event: &DragMoveEvent<DraggedSelection>,
5920                                              _,
5921                                              _| {
5922                                            if event.bounds.contains(&event.event.position) {
5923                                                this.folded_directory_drag_target =
5924                                                    Some(FoldedDirectoryDragTarget {
5925                                                        entry_id,
5926                                                        index,
5927                                                        is_delimiter_target: false,
5928                                                    });
5929                                            } else {
5930                                                let is_current_target = this
5931                                                    .folded_directory_drag_target
5932                                                    .as_ref()
5933                                                    .is_some_and(|target| {
5934                                                        target.entry_id == entry_id
5935                                                            && target.index == index
5936                                                            && !target.is_delimiter_target
5937                                                    });
5938                                                if is_current_target {
5939                                                    this.folded_directory_drag_target = None;
5940                                                }
5941                                            }
5942                                        },
5943                                    ))
5944                                    .on_drop(cx.listener(
5945                                        move |this, selections: &DraggedSelection, window, cx| {
5946                                            this.hover_scroll_task.take();
5947                                            this.drag_target_entry = None;
5948                                            this.folded_directory_drag_target = None;
5949                                            if let Some(target_entry_id) = target_entry_id {
5950                                                this.drag_onto(
5951                                                    selections,
5952                                                    target_entry_id,
5953                                                    is_file,
5954                                                    window,
5955                                                    cx,
5956                                                );
5957                                            }
5958                                        },
5959                                    ))
5960                                    .when(
5961                                        folded_directory_drag_target.is_some_and(|target| {
5962                                            target.entry_id == entry_id && target.index == index
5963                                        }),
5964                                        |this| this.bg(drag_over_color),
5965                                    )
5966                                })
5967                            })
5968                        })
5969                        .on_mouse_down(
5970                            MouseButton::Left,
5971                            cx.listener(move |this, _, _, cx| {
5972                                if let Some(folds) = this.state.ancestors.get_mut(&entry_id) {
5973                                    if folds.set_active_index(index) {
5974                                        cx.notify();
5975                                    }
5976                                }
5977                            }),
5978                        )
5979                        .on_mouse_down(
5980                            MouseButton::Right,
5981                            cx.listener(move |this, _, _, cx| {
5982                                if let Some(folds) = this.state.ancestors.get_mut(&entry_id) {
5983                                    if folds.set_active_index(index) {
5984                                        cx.notify();
5985                                    }
5986                                }
5987                            }),
5988                        )
5989                        .child(
5990                            Label::new(component)
5991                                .single_line()
5992                                .color(filename_text_color)
5993                                .when(bold_folder_labels && !is_file, |this| {
5994                                    this.weight(FontWeight::SEMIBOLD)
5995                                })
5996                                .when(index == active_index && is_active_or_marked, |this| {
5997                                    this.underline()
5998                                }),
5999                        )
6000                        .into_any()
6001                });
6002
6003        let mut separator_index = 0;
6004        itertools::intersperse_with(path_component_elements, move || {
6005            separator_index += 1;
6006            self.render_entry_path_separator(
6007                entry_id,
6008                separator_index,
6009                components_len,
6010                is_sticky,
6011                is_file,
6012                drag_and_drop_enabled,
6013                filename_text_color,
6014                &delimiter,
6015                folded_ancestors,
6016                cx,
6017            )
6018            .into_any()
6019        })
6020    }
6021
6022    fn render_entry_path_separator(
6023        &self,
6024        entry_id: ProjectEntryId,
6025        index: usize,
6026        components_len: usize,
6027        is_sticky: bool,
6028        is_file: bool,
6029        drag_and_drop_enabled: bool,
6030        filename_text_color: Color,
6031        delimiter: &SharedString,
6032        folded_ancestors: &FoldedAncestors,
6033        cx: &Context<Self>,
6034    ) -> Div {
6035        let delimiter_target_index = index - 1;
6036        let target_entry_id = folded_ancestors
6037            .ancestors
6038            .get(components_len - 1 - delimiter_target_index)
6039            .cloned();
6040        div()
6041            .when(!is_sticky, |div| {
6042                div.when(drag_and_drop_enabled, |div| {
6043                    div.on_drop(cx.listener(
6044                        move |this, selections: &DraggedSelection, window, cx| {
6045                            this.hover_scroll_task.take();
6046                            this.drag_target_entry = None;
6047                            this.folded_directory_drag_target = None;
6048                            if let Some(target_entry_id) = target_entry_id {
6049                                this.drag_onto(selections, target_entry_id, is_file, window, cx);
6050                            }
6051                        },
6052                    ))
6053                    .on_drag_move(cx.listener(
6054                        move |this, event: &DragMoveEvent<DraggedSelection>, _, _| {
6055                            if event.bounds.contains(&event.event.position) {
6056                                this.folded_directory_drag_target =
6057                                    Some(FoldedDirectoryDragTarget {
6058                                        entry_id,
6059                                        index: delimiter_target_index,
6060                                        is_delimiter_target: true,
6061                                    });
6062                            } else {
6063                                let is_current_target =
6064                                    this.folded_directory_drag_target.is_some_and(|target| {
6065                                        target.entry_id == entry_id
6066                                            && target.index == delimiter_target_index
6067                                            && target.is_delimiter_target
6068                                    });
6069                                if is_current_target {
6070                                    this.folded_directory_drag_target = None;
6071                                }
6072                            }
6073                        },
6074                    ))
6075                })
6076            })
6077            .child(
6078                Label::new(delimiter.clone())
6079                    .single_line()
6080                    .color(filename_text_color),
6081            )
6082    }
6083
6084    fn details_for_entry(
6085        &self,
6086        entry: &Entry,
6087        worktree_id: WorktreeId,
6088        root_name: &RelPath,
6089        entries_paths: &HashSet<Arc<RelPath>>,
6090        git_status: GitSummary,
6091        sticky: Option<StickyDetails>,
6092        _window: &mut Window,
6093        cx: &mut Context<Self>,
6094    ) -> EntryDetails {
6095        let (show_file_icons, show_folder_icons) = {
6096            let settings = ProjectPanelSettings::get_global(cx);
6097            (settings.file_icons, settings.folder_icons)
6098        };
6099
6100        let expanded_entry_ids = self
6101            .state
6102            .expanded_dir_ids
6103            .get(&worktree_id)
6104            .map(Vec::as_slice)
6105            .unwrap_or(&[]);
6106        let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok();
6107
6108        let icon = match entry.kind {
6109            EntryKind::File => {
6110                if show_file_icons {
6111                    FileIcons::get_icon(entry.path.as_std_path(), cx)
6112                } else {
6113                    None
6114                }
6115            }
6116            _ => {
6117                if show_folder_icons {
6118                    FileIcons::get_folder_icon(is_expanded, entry.path.as_std_path(), cx)
6119                } else {
6120                    FileIcons::get_chevron_icon(is_expanded, cx)
6121                }
6122            }
6123        };
6124
6125        let path_style = self.project.read(cx).path_style(cx);
6126        let (depth, difference) =
6127            ProjectPanel::calculate_depth_and_difference(entry, entries_paths);
6128
6129        let filename = if difference > 1 {
6130            entry
6131                .path
6132                .last_n_components(difference)
6133                .map_or(String::new(), |suffix| {
6134                    suffix.display(path_style).to_string()
6135                })
6136        } else {
6137            entry
6138                .path
6139                .file_name()
6140                .map(|name| name.to_string())
6141                .unwrap_or_else(|| root_name.as_unix_str().to_string())
6142        };
6143
6144        let selection = SelectedEntry {
6145            worktree_id,
6146            entry_id: entry.id,
6147        };
6148        let is_marked = self.marked_entries.contains(&selection);
6149        let is_selected = self.selection == Some(selection);
6150
6151        let diagnostic_severity = self
6152            .diagnostics
6153            .get(&(worktree_id, entry.path.clone()))
6154            .cloned();
6155
6156        let diagnostic_count = self
6157            .diagnostic_counts
6158            .get(&(worktree_id, entry.path.clone()))
6159            .copied();
6160
6161        let filename_text_color =
6162            entry_git_aware_label_color(git_status, entry.is_ignored, is_marked);
6163
6164        let is_cut = self
6165            .clipboard
6166            .as_ref()
6167            .is_some_and(|e| e.is_cut() && e.items().contains(&selection));
6168
6169        EntryDetails {
6170            filename,
6171            icon,
6172            path: entry.path.clone(),
6173            depth,
6174            kind: entry.kind,
6175            is_ignored: entry.is_ignored,
6176            is_expanded,
6177            is_selected,
6178            is_marked,
6179            is_editing: false,
6180            is_processing: false,
6181            is_cut,
6182            sticky,
6183            filename_text_color,
6184            diagnostic_severity,
6185            diagnostic_count,
6186            git_status,
6187            is_private: entry.is_private,
6188            worktree_id,
6189            canonical_path: entry.canonical_path.clone(),
6190        }
6191    }
6192
6193    fn dispatch_context(&self, window: &Window, cx: &Context<Self>) -> KeyContext {
6194        let mut dispatch_context = KeyContext::new_with_defaults();
6195        dispatch_context.add("ProjectPanel");
6196        dispatch_context.add("menu");
6197
6198        let identifier = if self.filename_editor.focus_handle(cx).is_focused(window) {
6199            "editing"
6200        } else {
6201            "not_editing"
6202        };
6203
6204        dispatch_context.add(identifier);
6205        dispatch_context
6206    }
6207
6208    fn reveal_entry(
6209        &mut self,
6210        project: Entity<Project>,
6211        entry_id: ProjectEntryId,
6212        skip_ignored: bool,
6213        window: &mut Window,
6214        cx: &mut Context<Self>,
6215    ) -> Result<()> {
6216        let worktree = project
6217            .read(cx)
6218            .worktree_for_entry(entry_id, cx)
6219            .context("can't reveal a non-existent entry in the project panel")?;
6220        let worktree = worktree.read(cx);
6221        let worktree_id = worktree.id();
6222        let is_ignored = worktree
6223            .entry_for_id(entry_id)
6224            .is_none_or(|entry| entry.is_ignored && !entry.is_always_included);
6225        if skip_ignored && is_ignored {
6226            if self.index_for_entry(entry_id, worktree_id).is_none() {
6227                anyhow::bail!("can't reveal an ignored entry in the project panel");
6228            }
6229
6230            self.selection = Some(SelectedEntry {
6231                worktree_id,
6232                entry_id,
6233            });
6234            self.marked_entries.clear();
6235            self.marked_entries.push(SelectedEntry {
6236                worktree_id,
6237                entry_id,
6238            });
6239            self.autoscroll(cx);
6240            cx.notify();
6241            return Ok(());
6242        }
6243        let is_active_item_file_diff_view = self
6244            .workspace
6245            .upgrade()
6246            .and_then(|ws| ws.read(cx).active_item(cx))
6247            .map(|item| item.act_as_type(TypeId::of::<FileDiffView>(), cx).is_some())
6248            .unwrap_or(false);
6249        if is_active_item_file_diff_view {
6250            return Ok(());
6251        }
6252
6253        self.expand_entry(worktree_id, entry_id, cx);
6254        self.update_visible_entries(Some((worktree_id, entry_id)), false, true, window, cx);
6255        self.marked_entries.clear();
6256        self.marked_entries.push(SelectedEntry {
6257            worktree_id,
6258            entry_id,
6259        });
6260        cx.notify();
6261        Ok(())
6262    }
6263
6264    fn find_active_indent_guide(
6265        &self,
6266        indent_guides: &[IndentGuideLayout],
6267        cx: &App,
6268    ) -> Option<usize> {
6269        let (worktree, entry) = self.selected_entry(cx)?;
6270
6271        // Find the parent entry of the indent guide, this will either be the
6272        // expanded folder we have selected, or the parent of the currently
6273        // selected file/collapsed directory
6274        let mut entry = entry;
6275        loop {
6276            let is_expanded_dir = entry.is_dir()
6277                && self
6278                    .state
6279                    .expanded_dir_ids
6280                    .get(&worktree.id())
6281                    .map(|ids| ids.binary_search(&entry.id).is_ok())
6282                    .unwrap_or(false);
6283            if is_expanded_dir {
6284                break;
6285            }
6286            entry = worktree.entry_for_path(&entry.path.parent()?)?;
6287        }
6288
6289        let (active_indent_range, depth) = {
6290            let (worktree_ix, child_offset, ix) = self.index_for_entry(entry.id, worktree.id())?;
6291            let child_paths = &self.state.visible_entries[worktree_ix].entries;
6292            let mut child_count = 0;
6293            let depth = entry.path.ancestors().count();
6294            while let Some(entry) = child_paths.get(child_offset + child_count + 1) {
6295                if entry.path.ancestors().count() <= depth {
6296                    break;
6297                }
6298                child_count += 1;
6299            }
6300
6301            let start = ix + 1;
6302            let end = start + child_count;
6303
6304            let visible_worktree = &self.state.visible_entries[worktree_ix];
6305            let visible_worktree_entries = visible_worktree.index.get_or_init(|| {
6306                visible_worktree
6307                    .entries
6308                    .iter()
6309                    .map(|e| e.path.clone())
6310                    .collect()
6311            });
6312
6313            // Calculate the actual depth of the entry, taking into account that directories can be auto-folded.
6314            let (depth, _) = Self::calculate_depth_and_difference(entry, visible_worktree_entries);
6315            (start..end, depth)
6316        };
6317
6318        let candidates = indent_guides
6319            .iter()
6320            .enumerate()
6321            .filter(|(_, indent_guide)| indent_guide.offset.x == depth);
6322
6323        for (i, indent) in candidates {
6324            // Find matches that are either an exact match, partially on screen, or inside the enclosing indent
6325            if active_indent_range.start <= indent.offset.y + indent.length
6326                && indent.offset.y <= active_indent_range.end
6327            {
6328                return Some(i);
6329            }
6330        }
6331        None
6332    }
6333
6334    fn render_sticky_entries(
6335        &self,
6336        child: StickyProjectPanelCandidate,
6337        window: &mut Window,
6338        cx: &mut Context<Self>,
6339    ) -> SmallVec<[AnyElement; 8]> {
6340        let project = self.project.read(cx);
6341
6342        let Some((worktree_id, entry_ref)) = self.entry_at_index(child.index) else {
6343            return SmallVec::new();
6344        };
6345
6346        let Some(visible) = self
6347            .state
6348            .visible_entries
6349            .iter()
6350            .find(|worktree| worktree.worktree_id == worktree_id)
6351        else {
6352            return SmallVec::new();
6353        };
6354
6355        let Some(worktree) = project.worktree_for_id(worktree_id, cx) else {
6356            return SmallVec::new();
6357        };
6358        let worktree = worktree.read(cx).snapshot();
6359
6360        let paths = visible
6361            .index
6362            .get_or_init(|| visible.entries.iter().map(|e| e.path.clone()).collect());
6363
6364        let mut sticky_parents = Vec::new();
6365        let mut current_path = entry_ref.path.clone();
6366
6367        'outer: loop {
6368            if let Some(parent_path) = current_path.parent() {
6369                for ancestor_path in parent_path.ancestors() {
6370                    if paths.contains(ancestor_path)
6371                        && let Some(parent_entry) = worktree.entry_for_path(ancestor_path)
6372                    {
6373                        sticky_parents.push(parent_entry.clone());
6374                        current_path = parent_entry.path.clone();
6375                        continue 'outer;
6376                    }
6377                }
6378            }
6379            break 'outer;
6380        }
6381
6382        if sticky_parents.is_empty() {
6383            return SmallVec::new();
6384        }
6385
6386        sticky_parents.reverse();
6387
6388        let panel_settings = ProjectPanelSettings::get_global(cx);
6389        let git_status_enabled = panel_settings.git_status;
6390        let root_name = worktree.root_name();
6391
6392        let git_summaries_by_id = if git_status_enabled {
6393            visible
6394                .entries
6395                .iter()
6396                .map(|e| (e.id, e.git_summary))
6397                .collect::<HashMap<_, _>>()
6398        } else {
6399            Default::default()
6400        };
6401
6402        // already checked if non empty above
6403        let last_item_index = sticky_parents.len() - 1;
6404        sticky_parents
6405            .iter()
6406            .enumerate()
6407            .map(|(index, entry)| {
6408                let git_status = git_summaries_by_id
6409                    .get(&entry.id)
6410                    .copied()
6411                    .unwrap_or_default();
6412                let sticky_details = Some(StickyDetails {
6413                    sticky_index: index,
6414                });
6415                let details = self.details_for_entry(
6416                    entry,
6417                    worktree_id,
6418                    root_name,
6419                    paths,
6420                    git_status,
6421                    sticky_details,
6422                    window,
6423                    cx,
6424                );
6425                self.render_entry(entry.id, details, window, cx)
6426                    .when(index == last_item_index, |this| {
6427                        let shadow_color_top = hsla(0.0, 0.0, 0.0, 0.1);
6428                        let shadow_color_bottom = hsla(0.0, 0.0, 0.0, 0.);
6429                        let sticky_shadow = div()
6430                            .absolute()
6431                            .left_0()
6432                            .bottom_neg_1p5()
6433                            .h_1p5()
6434                            .w_full()
6435                            .bg(linear_gradient(
6436                                0.,
6437                                linear_color_stop(shadow_color_top, 1.),
6438                                linear_color_stop(shadow_color_bottom, 0.),
6439                            ));
6440                        this.child(sticky_shadow)
6441                    })
6442                    .into_any()
6443            })
6444            .collect()
6445    }
6446}
6447
6448#[derive(Clone)]
6449struct StickyProjectPanelCandidate {
6450    index: usize,
6451    depth: usize,
6452}
6453
6454impl StickyCandidate for StickyProjectPanelCandidate {
6455    fn depth(&self) -> usize {
6456        self.depth
6457    }
6458}
6459
6460fn item_width_estimate(depth: usize, item_text_chars: usize, is_symlink: bool) -> usize {
6461    const ICON_SIZE_FACTOR: usize = 2;
6462    let mut item_width = depth * ICON_SIZE_FACTOR + item_text_chars;
6463    if is_symlink {
6464        item_width += ICON_SIZE_FACTOR;
6465    }
6466    item_width
6467}
6468
6469impl Render for ProjectPanel {
6470    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
6471        let has_worktree = !self.state.visible_entries.is_empty();
6472        let project = self.project.read(cx);
6473        let panel_settings = ProjectPanelSettings::get_global(cx);
6474        let indent_size = panel_settings.indent_size;
6475        let show_indent_guides = panel_settings.indent_guides.show == ShowIndentGuides::Always;
6476        let horizontal_scroll = panel_settings.scrollbar.horizontal_scroll;
6477        let show_sticky_entries = {
6478            if panel_settings.sticky_scroll {
6479                let is_scrollable = self.scroll_handle.is_scrollable();
6480                let is_scrolled = self.scroll_handle.offset().y < px(0.);
6481                is_scrollable && is_scrolled
6482            } else {
6483                false
6484            }
6485        };
6486
6487        let is_local = project.is_local();
6488
6489        if has_worktree {
6490            let item_count = self
6491                .state
6492                .visible_entries
6493                .iter()
6494                .map(|worktree| worktree.entries.len())
6495                .sum();
6496
6497            fn handle_drag_move<T: 'static>(
6498                this: &mut ProjectPanel,
6499                e: &DragMoveEvent<T>,
6500                window: &mut Window,
6501                cx: &mut Context<ProjectPanel>,
6502            ) {
6503                if let Some(previous_position) = this.previous_drag_position {
6504                    // Refresh cursor only when an actual drag happens,
6505                    // because modifiers are not updated when the cursor is not moved.
6506                    if e.event.position != previous_position {
6507                        this.refresh_drag_cursor_style(&e.event.modifiers, window, cx);
6508                    }
6509                }
6510                this.previous_drag_position = Some(e.event.position);
6511
6512                if !e.bounds.contains(&e.event.position) {
6513                    this.drag_target_entry = None;
6514                    return;
6515                }
6516                this.hover_scroll_task.take();
6517                let panel_height = e.bounds.size.height;
6518                if panel_height <= px(0.) {
6519                    return;
6520                }
6521
6522                let event_offset = e.event.position.y - e.bounds.origin.y;
6523                // How far along in the project panel is our cursor? (0. is the top of a list, 1. is the bottom)
6524                let hovered_region_offset = event_offset / panel_height;
6525
6526                // We want the scrolling to be a bit faster when the cursor is closer to the edge of a list.
6527                // These pixels offsets were picked arbitrarily.
6528                let vertical_scroll_offset = if hovered_region_offset <= 0.05 {
6529                    8.
6530                } else if hovered_region_offset <= 0.15 {
6531                    5.
6532                } else if hovered_region_offset >= 0.95 {
6533                    -8.
6534                } else if hovered_region_offset >= 0.85 {
6535                    -5.
6536                } else {
6537                    return;
6538                };
6539                let adjustment = point(px(0.), px(vertical_scroll_offset));
6540                this.hover_scroll_task = Some(cx.spawn_in(window, async move |this, cx| {
6541                    loop {
6542                        let should_stop_scrolling = this
6543                            .update(cx, |this, cx| {
6544                                this.hover_scroll_task.as_ref()?;
6545                                let handle = this.scroll_handle.0.borrow_mut();
6546                                let offset = handle.base_handle.offset();
6547
6548                                handle.base_handle.set_offset(offset + adjustment);
6549                                cx.notify();
6550                                Some(())
6551                            })
6552                            .ok()
6553                            .flatten()
6554                            .is_some();
6555                        if should_stop_scrolling {
6556                            return;
6557                        }
6558                        cx.background_executor()
6559                            .timer(Duration::from_millis(16))
6560                            .await;
6561                    }
6562                }));
6563            }
6564            h_flex()
6565                .id("project-panel")
6566                .group("project-panel")
6567                .when(panel_settings.drag_and_drop, |this| {
6568                    this.on_drag_move(cx.listener(handle_drag_move::<ExternalPaths>))
6569                        .on_drag_move(cx.listener(handle_drag_move::<DraggedSelection>))
6570                })
6571                .size_full()
6572                .relative()
6573                .on_modifiers_changed(cx.listener(
6574                    |this, event: &ModifiersChangedEvent, window, cx| {
6575                        this.refresh_drag_cursor_style(&event.modifiers, window, cx);
6576                    },
6577                ))
6578                .key_context(self.dispatch_context(window, cx))
6579                .on_action(cx.listener(Self::scroll_up))
6580                .on_action(cx.listener(Self::scroll_down))
6581                .on_action(cx.listener(Self::scroll_cursor_center))
6582                .on_action(cx.listener(Self::scroll_cursor_top))
6583                .on_action(cx.listener(Self::scroll_cursor_bottom))
6584                .on_action(cx.listener(Self::select_next))
6585                .on_action(cx.listener(Self::select_previous))
6586                .on_action(cx.listener(Self::select_first))
6587                .on_action(cx.listener(Self::select_last))
6588                .on_action(cx.listener(Self::select_parent))
6589                .on_action(cx.listener(Self::select_next_git_entry))
6590                .on_action(cx.listener(Self::select_prev_git_entry))
6591                .on_action(cx.listener(Self::select_next_diagnostic))
6592                .on_action(cx.listener(Self::select_prev_diagnostic))
6593                .on_action(cx.listener(Self::select_next_directory))
6594                .on_action(cx.listener(Self::select_prev_directory))
6595                .on_action(cx.listener(Self::expand_selected_entry))
6596                .on_action(cx.listener(Self::collapse_selected_entry))
6597                .on_action(cx.listener(Self::collapse_all_entries))
6598                .on_action(cx.listener(Self::collapse_selected_entry_and_children))
6599                .on_action(cx.listener(Self::open))
6600                .on_action(cx.listener(Self::open_permanent))
6601                .on_action(cx.listener(Self::open_split_vertical))
6602                .on_action(cx.listener(Self::open_split_horizontal))
6603                .on_action(cx.listener(Self::confirm))
6604                .on_action(cx.listener(Self::cancel))
6605                .on_action(cx.listener(Self::copy_path))
6606                .on_action(cx.listener(Self::copy_relative_path))
6607                .on_action(cx.listener(Self::new_search_in_directory))
6608                .on_action(cx.listener(Self::unfold_directory))
6609                .on_action(cx.listener(Self::fold_directory))
6610                .on_action(cx.listener(Self::remove_from_project))
6611                .on_action(cx.listener(Self::compare_marked_files))
6612                .when(cx.has_flag::<ProjectPanelUndoRedoFeatureFlag>(), |el| {
6613                    el.on_action(cx.listener(Self::undo))
6614                })
6615                .when(!project.is_read_only(cx), |el| {
6616                    el.on_action(cx.listener(Self::new_file))
6617                        .on_action(cx.listener(Self::new_directory))
6618                        .on_action(cx.listener(Self::rename))
6619                        .on_action(cx.listener(Self::delete))
6620                        .on_action(cx.listener(Self::cut))
6621                        .on_action(cx.listener(Self::copy))
6622                        .on_action(cx.listener(Self::paste))
6623                        .on_action(cx.listener(Self::duplicate))
6624                        .on_action(cx.listener(Self::restore_file))
6625                        .when(!project.is_remote(), |el| {
6626                            el.on_action(cx.listener(Self::trash))
6627                        })
6628                })
6629                .when(
6630                    project.is_local() || project.is_via_wsl_with_host_interop(cx),
6631                    |el| {
6632                        el.on_action(cx.listener(Self::reveal_in_finder))
6633                            .on_action(cx.listener(Self::open_system))
6634                            .on_action(cx.listener(Self::open_in_terminal))
6635                    },
6636                )
6637                .when(project.is_via_remote_server(), |el| {
6638                    el.on_action(cx.listener(Self::open_in_terminal))
6639                        .on_action(cx.listener(Self::download_from_remote))
6640                })
6641                .track_focus(&self.focus_handle(cx))
6642                .child(
6643                    v_flex()
6644                        .child(
6645                            uniform_list("entries", item_count, {
6646                                cx.processor(|this, range: Range<usize>, window, cx| {
6647                                    this.rendered_entries_len = range.end - range.start;
6648                                    let mut items = Vec::with_capacity(this.rendered_entries_len);
6649                                    this.for_each_visible_entry(
6650                                        range,
6651                                        window,
6652                                        cx,
6653                                        &mut |id, details, window, cx| {
6654                                            items.push(this.render_entry(id, details, window, cx));
6655                                        },
6656                                    );
6657                                    items
6658                                })
6659                            })
6660                            .when(show_indent_guides, |list| {
6661                                list.with_decoration(
6662                                    ui::indent_guides(
6663                                        px(indent_size),
6664                                        IndentGuideColors::panel(cx),
6665                                    )
6666                                    .with_compute_indents_fn(
6667                                        cx.entity(),
6668                                        |this, range, window, cx| {
6669                                            let mut items =
6670                                                SmallVec::with_capacity(range.end - range.start);
6671                                            this.iter_visible_entries(
6672                                                range,
6673                                                window,
6674                                                cx,
6675                                                &mut |entry, _, entries, _, _| {
6676                                                    let (depth, _) =
6677                                                        Self::calculate_depth_and_difference(
6678                                                            entry, entries,
6679                                                        );
6680                                                    items.push(depth);
6681                                                },
6682                                            );
6683                                            items
6684                                        },
6685                                    )
6686                                    .on_click(cx.listener(
6687                                        |this,
6688                                         active_indent_guide: &IndentGuideLayout,
6689                                         window,
6690                                         cx| {
6691                                            if window.modifiers().secondary() {
6692                                                let ix = active_indent_guide.offset.y;
6693                                                let Some((target_entry, worktree)) = maybe!({
6694                                                    let (worktree_id, entry) =
6695                                                        this.entry_at_index(ix)?;
6696                                                    let worktree = this
6697                                                        .project
6698                                                        .read(cx)
6699                                                        .worktree_for_id(worktree_id, cx)?;
6700                                                    let target_entry = worktree
6701                                                        .read(cx)
6702                                                        .entry_for_path(&entry.path.parent()?)?;
6703                                                    Some((target_entry, worktree))
6704                                                }) else {
6705                                                    return;
6706                                                };
6707
6708                                                this.collapse_entry(
6709                                                    target_entry.clone(),
6710                                                    worktree,
6711                                                    window,
6712                                                    cx,
6713                                                );
6714                                            }
6715                                        },
6716                                    ))
6717                                    .with_render_fn(
6718                                        cx.entity(),
6719                                        move |this, params, _, cx| {
6720                                            const LEFT_OFFSET: Pixels = px(14.);
6721                                            const PADDING_Y: Pixels = px(4.);
6722                                            const HITBOX_OVERDRAW: Pixels = px(3.);
6723
6724                                            let active_indent_guide_index = this
6725                                                .find_active_indent_guide(
6726                                                    &params.indent_guides,
6727                                                    cx,
6728                                                );
6729
6730                                            let indent_size = params.indent_size;
6731                                            let item_height = params.item_height;
6732
6733                                            params
6734                                                .indent_guides
6735                                                .into_iter()
6736                                                .enumerate()
6737                                                .map(|(idx, layout)| {
6738                                                    let offset = if layout.continues_offscreen {
6739                                                        px(0.)
6740                                                    } else {
6741                                                        PADDING_Y
6742                                                    };
6743                                                    let bounds = Bounds::new(
6744                                                        point(
6745                                                            layout.offset.x * indent_size
6746                                                                + LEFT_OFFSET,
6747                                                            layout.offset.y * item_height + offset,
6748                                                        ),
6749                                                        size(
6750                                                            px(1.),
6751                                                            layout.length * item_height
6752                                                                - offset * 2.,
6753                                                        ),
6754                                                    );
6755                                                    ui::RenderedIndentGuide {
6756                                                        bounds,
6757                                                        layout,
6758                                                        is_active: Some(idx)
6759                                                            == active_indent_guide_index,
6760                                                        hitbox: Some(Bounds::new(
6761                                                            point(
6762                                                                bounds.origin.x - HITBOX_OVERDRAW,
6763                                                                bounds.origin.y,
6764                                                            ),
6765                                                            size(
6766                                                                bounds.size.width
6767                                                                    + HITBOX_OVERDRAW * 2.,
6768                                                                bounds.size.height,
6769                                                            ),
6770                                                        )),
6771                                                    }
6772                                                })
6773                                                .collect()
6774                                        },
6775                                    ),
6776                                )
6777                            })
6778                            .when(show_sticky_entries, |list| {
6779                                let sticky_items = ui::sticky_items(
6780                                    cx.entity(),
6781                                    |this, range, window, cx| {
6782                                        let mut items =
6783                                            SmallVec::with_capacity(range.end - range.start);
6784                                        this.iter_visible_entries(
6785                                            range,
6786                                            window,
6787                                            cx,
6788                                            &mut |entry, index, entries, _, _| {
6789                                                let (depth, _) =
6790                                                    Self::calculate_depth_and_difference(
6791                                                        entry, entries,
6792                                                    );
6793                                                let candidate =
6794                                                    StickyProjectPanelCandidate { index, depth };
6795                                                items.push(candidate);
6796                                            },
6797                                        );
6798                                        items
6799                                    },
6800                                    |this, marker_entry, window, cx| {
6801                                        let sticky_entries =
6802                                            this.render_sticky_entries(marker_entry, window, cx);
6803                                        this.sticky_items_count = sticky_entries.len();
6804                                        sticky_entries
6805                                    },
6806                                );
6807                                list.with_decoration(if show_indent_guides {
6808                                    sticky_items.with_decoration(
6809                                        ui::indent_guides(
6810                                            px(indent_size),
6811                                            IndentGuideColors::panel(cx),
6812                                        )
6813                                        .with_render_fn(
6814                                            cx.entity(),
6815                                            move |_, params, _, _| {
6816                                                const LEFT_OFFSET: Pixels = px(14.);
6817
6818                                                let indent_size = params.indent_size;
6819                                                let item_height = params.item_height;
6820
6821                                                params
6822                                                    .indent_guides
6823                                                    .into_iter()
6824                                                    .map(|layout| {
6825                                                        let bounds = Bounds::new(
6826                                                            point(
6827                                                                layout.offset.x * indent_size
6828                                                                    + LEFT_OFFSET,
6829                                                                layout.offset.y * item_height,
6830                                                            ),
6831                                                            size(
6832                                                                px(1.),
6833                                                                layout.length * item_height,
6834                                                            ),
6835                                                        );
6836                                                        ui::RenderedIndentGuide {
6837                                                            bounds,
6838                                                            layout,
6839                                                            is_active: false,
6840                                                            hitbox: None,
6841                                                        }
6842                                                    })
6843                                                    .collect()
6844                                            },
6845                                        ),
6846                                    )
6847                                } else {
6848                                    sticky_items
6849                                })
6850                            })
6851                            .with_sizing_behavior(ListSizingBehavior::Infer)
6852                            .with_horizontal_sizing_behavior(if horizontal_scroll {
6853                                ListHorizontalSizingBehavior::Unconstrained
6854                            } else {
6855                                ListHorizontalSizingBehavior::FitList
6856                            })
6857                            .when(horizontal_scroll, |list| {
6858                                list.with_width_from_item(self.state.max_width_item_index)
6859                            })
6860                            .track_scroll(&self.scroll_handle),
6861                        )
6862                        .child(
6863                            div()
6864                                .id("project-panel-blank-area")
6865                                .block_mouse_except_scroll()
6866                                .flex_grow()
6867                                .on_scroll_wheel({
6868                                    let scroll_handle = self.scroll_handle.clone();
6869                                    let entity_id = cx.entity().entity_id();
6870                                    move |event, window, cx| {
6871                                        let state = scroll_handle.0.borrow();
6872                                        let base_handle = &state.base_handle;
6873                                        let current_offset = base_handle.offset();
6874                                        let max_offset = base_handle.max_offset();
6875                                        let delta = event.delta.pixel_delta(window.line_height());
6876                                        let new_offset = (current_offset + delta)
6877                                            .clamp(&max_offset.neg(), &Point::default());
6878
6879                                        if new_offset != current_offset {
6880                                            base_handle.set_offset(new_offset);
6881                                            cx.notify(entity_id);
6882                                        }
6883                                    }
6884                                })
6885                                .when(
6886                                    self.drag_target_entry.as_ref().is_some_and(
6887                                        |entry| match entry {
6888                                            DragTarget::Background => true,
6889                                            DragTarget::Entry {
6890                                                highlight_entry_id, ..
6891                                            } => self.state.last_worktree_root_id.is_some_and(
6892                                                |root_id| *highlight_entry_id == root_id,
6893                                            ),
6894                                        },
6895                                    ),
6896                                    |div| div.bg(cx.theme().colors().drop_target_background),
6897                                )
6898                                .on_drag_move::<ExternalPaths>(cx.listener(
6899                                    move |this, event: &DragMoveEvent<ExternalPaths>, _, _| {
6900                                        let Some(_last_root_id) = this.state.last_worktree_root_id
6901                                        else {
6902                                            return;
6903                                        };
6904                                        if event.bounds.contains(&event.event.position) {
6905                                            this.drag_target_entry = Some(DragTarget::Background);
6906                                        } else {
6907                                            if this.drag_target_entry.as_ref().is_some_and(|e| {
6908                                                matches!(e, DragTarget::Background)
6909                                            }) {
6910                                                this.drag_target_entry = None;
6911                                            }
6912                                        }
6913                                    },
6914                                ))
6915                                .on_drag_move::<DraggedSelection>(cx.listener(
6916                                    move |this, event: &DragMoveEvent<DraggedSelection>, _, cx| {
6917                                        let Some(last_root_id) = this.state.last_worktree_root_id
6918                                        else {
6919                                            return;
6920                                        };
6921                                        if event.bounds.contains(&event.event.position) {
6922                                            let drag_state = event.drag(cx);
6923                                            if this.should_highlight_background_for_selection_drag(
6924                                                &drag_state,
6925                                                last_root_id,
6926                                                cx,
6927                                            ) {
6928                                                this.drag_target_entry =
6929                                                    Some(DragTarget::Background);
6930                                            }
6931                                        } else {
6932                                            if this.drag_target_entry.as_ref().is_some_and(|e| {
6933                                                matches!(e, DragTarget::Background)
6934                                            }) {
6935                                                this.drag_target_entry = None;
6936                                            }
6937                                        }
6938                                    },
6939                                ))
6940                                .on_drop(cx.listener(
6941                                    move |this, external_paths: &ExternalPaths, window, cx| {
6942                                        this.drag_target_entry = None;
6943                                        this.hover_scroll_task.take();
6944                                        if let Some(entry_id) = this.state.last_worktree_root_id {
6945                                            this.drop_external_files(
6946                                                external_paths.paths(),
6947                                                entry_id,
6948                                                window,
6949                                                cx,
6950                                            );
6951                                        }
6952                                        cx.stop_propagation();
6953                                    },
6954                                ))
6955                                .on_drop(cx.listener(
6956                                    move |this, selections: &DraggedSelection, window, cx| {
6957                                        this.drag_target_entry = None;
6958                                        this.hover_scroll_task.take();
6959                                        if let Some(entry_id) = this.state.last_worktree_root_id {
6960                                            this.drag_onto(selections, entry_id, false, window, cx);
6961                                        }
6962                                        cx.stop_propagation();
6963                                    },
6964                                ))
6965                                .on_click(cx.listener(|this, event, window, cx| {
6966                                    if matches!(event, gpui::ClickEvent::Keyboard(_)) {
6967                                        return;
6968                                    }
6969                                    cx.stop_propagation();
6970                                    this.selection = None;
6971                                    this.marked_entries.clear();
6972                                    this.focus_handle(cx).focus(window, cx);
6973                                }))
6974                                .on_mouse_down(
6975                                    MouseButton::Right,
6976                                    cx.listener(move |this, event: &MouseDownEvent, window, cx| {
6977                                        // When deploying the context menu anywhere below the last project entry,
6978                                        // act as if the user clicked the root of the last worktree.
6979                                        if let Some(entry_id) = this.state.last_worktree_root_id {
6980                                            this.deploy_context_menu(
6981                                                event.position,
6982                                                entry_id,
6983                                                window,
6984                                                cx,
6985                                            );
6986                                        }
6987                                    }),
6988                                )
6989                                .when(!project.is_read_only(cx), |el| {
6990                                    el.on_click(cx.listener(
6991                                        |this, event: &gpui::ClickEvent, window, cx| {
6992                                            if event.click_count() > 1
6993                                                && let Some(entry_id) =
6994                                                    this.state.last_worktree_root_id
6995                                            {
6996                                                let project = this.project.read(cx);
6997
6998                                                let worktree_id = if let Some(worktree) =
6999                                                    project.worktree_for_entry(entry_id, cx)
7000                                                {
7001                                                    worktree.read(cx).id()
7002                                                } else {
7003                                                    return;
7004                                                };
7005
7006                                                this.selection = Some(SelectedEntry {
7007                                                    worktree_id,
7008                                                    entry_id,
7009                                                });
7010
7011                                                this.new_file(&NewFile, window, cx);
7012                                            }
7013                                        },
7014                                    ))
7015                                }),
7016                        )
7017                        .size_full(),
7018                )
7019                .custom_scrollbars(
7020                    {
7021                        let mut scrollbars = Scrollbars::for_settings::<ProjectPanelSettings>()
7022                            .tracked_scroll_handle(&self.scroll_handle);
7023                        if horizontal_scroll {
7024                            scrollbars = scrollbars.with_track_along(
7025                                ScrollAxes::Horizontal,
7026                                cx.theme().colors().panel_background,
7027                            );
7028                        }
7029                        scrollbars.notify_content()
7030                    },
7031                    window,
7032                    cx,
7033                )
7034                .children(self.context_menu.as_ref().map(|(menu, position, _)| {
7035                    deferred(
7036                        anchored()
7037                            .position(*position)
7038                            .anchor(gpui::Corner::TopLeft)
7039                            .child(menu.clone()),
7040                    )
7041                    .with_priority(3)
7042                }))
7043        } else {
7044            let focus_handle = self.focus_handle(cx);
7045
7046            v_flex()
7047                .id("empty-project_panel")
7048                .p_4()
7049                .size_full()
7050                .items_center()
7051                .justify_center()
7052                .gap_1()
7053                .track_focus(&self.focus_handle(cx))
7054                .child(
7055                    Button::new("open_project", "Open Project")
7056                        .full_width()
7057                        .key_binding(KeyBinding::for_action_in(
7058                            &workspace::Open::default(),
7059                            &focus_handle,
7060                            cx,
7061                        ))
7062                        .on_click(cx.listener(|this, _, window, cx| {
7063                            this.workspace
7064                                .update(cx, |_, cx| {
7065                                    window.dispatch_action(
7066                                        workspace::Open::default().boxed_clone(),
7067                                        cx,
7068                                    );
7069                                })
7070                                .log_err();
7071                        })),
7072                )
7073                .child(
7074                    h_flex()
7075                        .w_1_2()
7076                        .gap_2()
7077                        .child(Divider::horizontal())
7078                        .child(Label::new("or").size(LabelSize::XSmall).color(Color::Muted))
7079                        .child(Divider::horizontal()),
7080                )
7081                .child(
7082                    Button::new("clone_repo", "Clone Repository")
7083                        .full_width()
7084                        .on_click(cx.listener(|this, _, window, cx| {
7085                            this.workspace
7086                                .update(cx, |_, cx| {
7087                                    window.dispatch_action(git::Clone.boxed_clone(), cx);
7088                                })
7089                                .log_err();
7090                        })),
7091                )
7092                .when(is_local, |div| {
7093                    div.when(panel_settings.drag_and_drop, |div| {
7094                        div.drag_over::<ExternalPaths>(|style, _, _, cx| {
7095                            style.bg(cx.theme().colors().drop_target_background)
7096                        })
7097                        .on_drop(cx.listener(
7098                            move |this, external_paths: &ExternalPaths, window, cx| {
7099                                this.drag_target_entry = None;
7100                                this.hover_scroll_task.take();
7101                                if let Some(task) = this
7102                                    .workspace
7103                                    .update(cx, |workspace, cx| {
7104                                        workspace.open_workspace_for_paths(
7105                                            true,
7106                                            external_paths.paths().to_owned(),
7107                                            window,
7108                                            cx,
7109                                        )
7110                                    })
7111                                    .log_err()
7112                                {
7113                                    task.detach_and_log_err(cx);
7114                                }
7115                                cx.stop_propagation();
7116                            },
7117                        ))
7118                    })
7119                })
7120        }
7121    }
7122}
7123
7124impl Render for DraggedProjectEntryView {
7125    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
7126        let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
7127        h_flex()
7128            .font(ui_font)
7129            .pl(self.click_offset.x + px(12.))
7130            .pt(self.click_offset.y + px(12.))
7131            .child(
7132                div()
7133                    .flex()
7134                    .gap_1()
7135                    .items_center()
7136                    .py_1()
7137                    .px_2()
7138                    .rounded_lg()
7139                    .bg(cx.theme().colors().background)
7140                    .map(|this| {
7141                        if self.selections.len() > 1 && self.selections.contains(&self.selection) {
7142                            this.child(Label::new(format!("{} entries", self.selections.len())))
7143                        } else {
7144                            this.child(if let Some(icon) = &self.icon {
7145                                div().child(Icon::from_path(icon.clone()))
7146                            } else {
7147                                div()
7148                            })
7149                            .child(Label::new(self.filename.clone()))
7150                        }
7151                    }),
7152            )
7153    }
7154}
7155
7156impl EventEmitter<Event> for ProjectPanel {}
7157
7158impl EventEmitter<PanelEvent> for ProjectPanel {}
7159
7160impl Panel for ProjectPanel {
7161    fn position(&self, _: &Window, cx: &App) -> DockPosition {
7162        match ProjectPanelSettings::get_global(cx).dock {
7163            DockSide::Left => DockPosition::Left,
7164            DockSide::Right => DockPosition::Right,
7165        }
7166    }
7167
7168    fn position_is_valid(&self, position: DockPosition) -> bool {
7169        matches!(position, DockPosition::Left | DockPosition::Right)
7170    }
7171
7172    fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
7173        settings::update_settings_file(self.fs.clone(), cx, move |settings, _| {
7174            let dock = match position {
7175                DockPosition::Left | DockPosition::Bottom => DockSide::Left,
7176                DockPosition::Right => DockSide::Right,
7177            };
7178            settings.project_panel.get_or_insert_default().dock = Some(dock);
7179        });
7180    }
7181
7182    fn default_size(&self, _: &Window, cx: &App) -> Pixels {
7183        ProjectPanelSettings::get_global(cx).default_width
7184    }
7185
7186    fn icon(&self, _: &Window, cx: &App) -> Option<IconName> {
7187        ProjectPanelSettings::get_global(cx)
7188            .button
7189            .then_some(IconName::FileTree)
7190    }
7191
7192    fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
7193        Some("Project Panel")
7194    }
7195
7196    fn toggle_action(&self) -> Box<dyn Action> {
7197        Box::new(ToggleFocus)
7198    }
7199
7200    fn persistent_name() -> &'static str {
7201        "Project Panel"
7202    }
7203
7204    fn panel_key() -> &'static str {
7205        PROJECT_PANEL_KEY
7206    }
7207
7208    fn starts_open(&self, _: &Window, cx: &App) -> bool {
7209        if !ProjectPanelSettings::get_global(cx).starts_open {
7210            return false;
7211        }
7212
7213        let project = &self.project.read(cx);
7214        project.visible_worktrees(cx).any(|tree| {
7215            tree.read(cx)
7216                .root_entry()
7217                .is_some_and(|entry| entry.is_dir())
7218        })
7219    }
7220
7221    fn activation_priority(&self) -> u32 {
7222        0
7223    }
7224}
7225
7226impl Focusable for ProjectPanel {
7227    fn focus_handle(&self, _cx: &App) -> FocusHandle {
7228        self.focus_handle.clone()
7229    }
7230}
7231
7232impl ClipboardEntry {
7233    fn is_cut(&self) -> bool {
7234        matches!(self, Self::Cut { .. })
7235    }
7236
7237    fn items(&self) -> &BTreeSet<SelectedEntry> {
7238        match self {
7239            ClipboardEntry::Copied(entries) | ClipboardEntry::Cut(entries) => entries,
7240        }
7241    }
7242
7243    fn into_copy_entry(self) -> Self {
7244        match self {
7245            ClipboardEntry::Copied(_) => self,
7246            ClipboardEntry::Cut(entries) => ClipboardEntry::Copied(entries),
7247        }
7248    }
7249}
7250
7251#[inline]
7252fn cmp_directories_first(a: &Entry, b: &Entry) -> cmp::Ordering {
7253    util::paths::compare_rel_paths((&a.path, a.is_file()), (&b.path, b.is_file()))
7254}
7255
7256#[inline]
7257fn cmp_mixed(a: &Entry, b: &Entry) -> cmp::Ordering {
7258    util::paths::compare_rel_paths_mixed((&a.path, a.is_file()), (&b.path, b.is_file()))
7259}
7260
7261#[inline]
7262fn cmp_files_first(a: &Entry, b: &Entry) -> cmp::Ordering {
7263    util::paths::compare_rel_paths_files_first((&a.path, a.is_file()), (&b.path, b.is_file()))
7264}
7265
7266#[inline]
7267fn cmp_with_mode(a: &Entry, b: &Entry, mode: &settings::ProjectPanelSortMode) -> cmp::Ordering {
7268    match mode {
7269        settings::ProjectPanelSortMode::DirectoriesFirst => cmp_directories_first(a, b),
7270        settings::ProjectPanelSortMode::Mixed => cmp_mixed(a, b),
7271        settings::ProjectPanelSortMode::FilesFirst => cmp_files_first(a, b),
7272    }
7273}
7274
7275pub fn sort_worktree_entries_with_mode(
7276    entries: &mut [impl AsRef<Entry>],
7277    mode: settings::ProjectPanelSortMode,
7278) {
7279    entries.sort_by(|lhs, rhs| cmp_with_mode(lhs.as_ref(), rhs.as_ref(), &mode));
7280}
7281
7282pub fn par_sort_worktree_entries_with_mode(
7283    entries: &mut Vec<GitEntry>,
7284    mode: settings::ProjectPanelSortMode,
7285) {
7286    entries.par_sort_by(|lhs, rhs| cmp_with_mode(lhs, rhs, &mode));
7287}
7288
7289#[cfg(test)]
7290mod project_panel_tests;