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