project_panel.rs

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