project_panel.rs

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