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