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