project_panel.rs

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