project_panel.rs

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