project_panel.rs

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