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