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