project_panel.rs

   1pub mod project_panel_settings;
   2mod undo;
   3mod utils;
   4
   5use anyhow::{Context as _, Result};
   6use client::{ErrorCode, ErrorExt};
   7use collections::{BTreeSet, HashMap, hash_map};
   8use command_palette_hooks::CommandPaletteFilter;
   9use editor::{
  10    Editor, EditorEvent, MultiBufferOffset,
  11    items::{
  12        entry_diagnostic_aware_icon_decoration_and_color,
  13        entry_diagnostic_aware_icon_name_and_color, entry_git_aware_label_color,
  14    },
  15};
  16use feature_flags::{FeatureFlagAppExt, ProjectPanelUndoRedoFeatureFlag};
  17use file_icons::FileIcons;
  18use git;
  19use git::status::GitSummary;
  20use git_ui;
  21use git_ui::file_diff_view::FileDiffView;
  22use gpui::{
  23    Action, AnyElement, App, AsyncWindowContext, Bounds, ClipboardEntry as GpuiClipboardEntry,
  24    ClipboardItem, Context, CursorStyle, DismissEvent, Div, DragMoveEvent, Entity, EventEmitter,
  25    ExternalPaths, FocusHandle, Focusable, FontWeight, Hsla, InteractiveElement, KeyContext,
  26    ListHorizontalSizingBehavior, ListSizingBehavior, Modifiers, ModifiersChangedEvent,
  27    MouseButton, MouseDownEvent, ParentElement, PathPromptOptions, Pixels, Point, PromptLevel,
  28    Render, ScrollStrategy, Stateful, Styled, Subscription, Task, UniformListScrollHandle,
  29    WeakEntity, Window, actions, anchored, deferred, div, hsla, linear_color_stop, linear_gradient,
  30    point, px, size, transparent_white, uniform_list,
  31};
  32use language::DiagnosticSeverity;
  33use menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrevious};
  34use notifications::status_toast::{StatusToast, ToastIcon};
  35use project::{
  36    Entry, EntryKind, Fs, GitEntry, GitEntryRef, GitTraversal, Project, ProjectEntryId,
  37    ProjectPath, Worktree, WorktreeId,
  38    git_store::{GitStoreEvent, RepositoryEvent, git_traversal::ChildEntriesGitIter},
  39    project_settings::GoToDiagnosticSeverityFilter,
  40};
  41use project_panel_settings::ProjectPanelSettings;
  42use rayon::slice::ParallelSliceMut;
  43use schemars::JsonSchema;
  44use serde::Deserialize;
  45use settings::{
  46    DockSide, ProjectPanelEntrySpacing, Settings, SettingsStore, ShowDiagnostics, ShowIndentGuides,
  47    update_settings_file,
  48};
  49use smallvec::SmallVec;
  50use std::ops::Neg;
  51use std::{any::TypeId, time::Instant};
  52use std::{
  53    cell::OnceCell,
  54    cmp,
  55    collections::HashSet,
  56    ops::Range,
  57    path::{Path, PathBuf},
  58    sync::Arc,
  59    time::Duration,
  60};
  61use theme_settings::ThemeSettings;
  62use ui::{
  63    Color, ContextMenu, ContextMenuEntry, DecoratedIcon, Divider, Icon, IconDecoration,
  64    IconDecorationKind, IndentGuideColors, IndentGuideLayout, Indicator, KeyBinding, Label,
  65    LabelSize, ListItem, ListItemSpacing, ScrollAxes, ScrollableHandle, Scrollbars,
  66    StickyCandidate, Tooltip, WithScrollbar, prelude::*, v_flex,
  67};
  68use util::{
  69    ResultExt, TakeUntilExt, TryFutureExt, maybe,
  70    paths::{PathStyle, compare_paths},
  71    rel_path::{RelPath, RelPathBuf},
  72};
  73use workspace::{
  74    DraggedSelection, OpenInTerminal, OpenMode, OpenOptions, OpenVisible, PreviewTabsSettings,
  75    SelectedEntry, SplitDirection, Workspace,
  76    dock::{DockPosition, Panel, PanelEvent},
  77    notifications::{DetachAndPromptErr, NotifyResultExt, NotifyTaskExt},
  78};
  79use worktree::CreatedEntry;
  80use zed_actions::{
  81    project_panel::{Toggle, ToggleFocus},
  82    workspace::OpenWithSystem,
  83};
  84
  85use crate::{
  86    project_panel_settings::ProjectPanelScrollbarProxy,
  87    undo::{ProjectPanelOperation, UndoManager},
  88};
  89
  90const PROJECT_PANEL_KEY: &str = "ProjectPanel";
  91const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX;
  92
  93struct VisibleEntriesForWorktree {
  94    worktree_id: WorktreeId,
  95    entries: Vec<GitEntry>,
  96    index: OnceCell<HashSet<Arc<RelPath>>>,
  97}
  98
  99struct State {
 100    last_worktree_root_id: Option<ProjectEntryId>,
 101    /// Maps from leaf project entry ID to the currently selected ancestor.
 102    /// Relevant only for auto-fold dirs, where a single project panel entry may actually consist of several
 103    /// project entries (and all non-leaf nodes are guaranteed to be directories).
 104    ancestors: HashMap<ProjectEntryId, FoldedAncestors>,
 105    visible_entries: Vec<VisibleEntriesForWorktree>,
 106    max_width_item_index: Option<usize>,
 107    edit_state: Option<EditState>,
 108    temporarily_unfolded_pending_state: Option<TemporaryUnfoldedPendingState>,
 109    unfolded_dir_ids: HashSet<ProjectEntryId>,
 110    expanded_dir_ids: HashMap<WorktreeId, Vec<ProjectEntryId>>,
 111}
 112
 113impl State {
 114    fn is_unfolded(&self, entry_id: &ProjectEntryId) -> bool {
 115        self.unfolded_dir_ids.contains(entry_id)
 116            || self.edit_state.as_ref().map_or(false, |edit_state| {
 117                edit_state.temporarily_unfolded == Some(*entry_id)
 118            })
 119    }
 120
 121    fn derive(old: &Self) -> Self {
 122        Self {
 123            last_worktree_root_id: None,
 124            ancestors: Default::default(),
 125            visible_entries: Default::default(),
 126            max_width_item_index: None,
 127            edit_state: old.edit_state.clone(),
 128            temporarily_unfolded_pending_state: None,
 129            unfolded_dir_ids: old.unfolded_dir_ids.clone(),
 130            expanded_dir_ids: old.expanded_dir_ids.clone(),
 131        }
 132    }
 133}
 134
 135pub struct ProjectPanel {
 136    project: Entity<Project>,
 137    fs: Arc<dyn Fs>,
 138    focus_handle: FocusHandle,
 139    scroll_handle: UniformListScrollHandle,
 140    // An update loop that keeps incrementing/decrementing scroll offset while there is a dragged entry that's
 141    // hovered over the start/end of a list.
 142    hover_scroll_task: Option<Task<()>>,
 143    rendered_entries_len: usize,
 144    folded_directory_drag_target: Option<FoldedDirectoryDragTarget>,
 145    drag_target_entry: Option<DragTarget>,
 146    marked_entries: Vec<SelectedEntry>,
 147    selection: Option<SelectedEntry>,
 148    context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
 149    filename_editor: Entity<Editor>,
 150    clipboard: Option<ClipboardEntry>,
 151    _dragged_entry_destination: Option<Arc<Path>>,
 152    workspace: WeakEntity<Workspace>,
 153    diagnostics: HashMap<(WorktreeId, Arc<RelPath>), DiagnosticSeverity>,
 154    diagnostic_counts: HashMap<(WorktreeId, Arc<RelPath>), DiagnosticCount>,
 155    diagnostic_summary_update: Task<()>,
 156    // We keep track of the mouse down state on entries so we don't flash the UI
 157    // in case a user clicks to open a file.
 158    mouse_down: bool,
 159    hover_expand_task: Option<Task<()>>,
 160    previous_drag_position: Option<Point<Pixels>>,
 161    sticky_items_count: usize,
 162    last_reported_update: Instant,
 163    update_visible_entries_task: UpdateVisibleEntriesTask,
 164    undo_manager: UndoManager,
 165    state: State,
 166}
 167
 168struct UpdateVisibleEntriesTask {
 169    _visible_entries_task: Task<()>,
 170    focus_filename_editor: bool,
 171    autoscroll: bool,
 172}
 173
 174#[derive(Debug)]
 175struct TemporaryUnfoldedPendingState {
 176    previously_focused_leaf_entry: SelectedEntry,
 177    temporarily_unfolded_active_entry_id: ProjectEntryId,
 178}
 179
 180impl Default for UpdateVisibleEntriesTask {
 181    fn default() -> Self {
 182        UpdateVisibleEntriesTask {
 183            _visible_entries_task: Task::ready(()),
 184            focus_filename_editor: Default::default(),
 185            autoscroll: Default::default(),
 186        }
 187    }
 188}
 189
 190enum DragTarget {
 191    /// Dragging on an entry
 192    Entry {
 193        /// The entry currently under the mouse cursor during a drag operation
 194        entry_id: ProjectEntryId,
 195        /// Highlight this entry along with all of its children
 196        highlight_entry_id: ProjectEntryId,
 197    },
 198    /// Dragging on background
 199    Background,
 200}
 201
 202#[derive(Copy, Clone, Debug)]
 203struct FoldedDirectoryDragTarget {
 204    entry_id: ProjectEntryId,
 205    index: usize,
 206    /// Whether we are dragging over the delimiter rather than the component itself.
 207    is_delimiter_target: bool,
 208}
 209
 210#[derive(Clone, Debug)]
 211enum ValidationState {
 212    None,
 213    Warning(String),
 214    Error(String),
 215}
 216
 217#[derive(Clone, Debug)]
 218struct EditState {
 219    worktree_id: WorktreeId,
 220    entry_id: ProjectEntryId,
 221    leaf_entry_id: Option<ProjectEntryId>,
 222    is_dir: bool,
 223    depth: usize,
 224    processing_filename: Option<Arc<RelPath>>,
 225    previously_focused: Option<SelectedEntry>,
 226    validation_state: ValidationState,
 227    temporarily_unfolded: Option<ProjectEntryId>,
 228}
 229
 230impl EditState {
 231    fn is_new_entry(&self) -> bool {
 232        self.leaf_entry_id.is_none()
 233    }
 234}
 235
 236#[derive(Clone, Debug)]
 237enum ClipboardEntry {
 238    Copied(BTreeSet<SelectedEntry>),
 239    Cut(BTreeSet<SelectedEntry>),
 240}
 241
 242#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)]
 243struct DiagnosticCount {
 244    error_count: usize,
 245    warning_count: usize,
 246}
 247
 248impl DiagnosticCount {
 249    fn capped_error_count(&self) -> String {
 250        Self::capped_count(self.error_count)
 251    }
 252
 253    fn capped_warning_count(&self) -> String {
 254        Self::capped_count(self.warning_count)
 255    }
 256
 257    fn capped_count(count: usize) -> String {
 258        if count > 99 {
 259            "99+".to_string()
 260        } else {
 261            count.to_string()
 262        }
 263    }
 264}
 265
 266#[derive(Debug, PartialEq, Eq, Clone)]
 267struct EntryDetails {
 268    filename: String,
 269    icon: Option<SharedString>,
 270    path: Arc<RelPath>,
 271    depth: usize,
 272    kind: EntryKind,
 273    is_ignored: bool,
 274    is_expanded: bool,
 275    is_selected: bool,
 276    is_marked: bool,
 277    is_editing: bool,
 278    is_processing: bool,
 279    is_cut: bool,
 280    sticky: Option<StickyDetails>,
 281    filename_text_color: Color,
 282    diagnostic_severity: Option<DiagnosticSeverity>,
 283    diagnostic_count: Option<DiagnosticCount>,
 284    git_status: GitSummary,
 285    is_private: bool,
 286    worktree_id: WorktreeId,
 287    canonical_path: Option<Arc<Path>>,
 288}
 289
 290#[derive(Debug, PartialEq, Eq, Clone)]
 291struct StickyDetails {
 292    sticky_index: usize,
 293}
 294
 295/// Permanently deletes the selected file or directory.
 296#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
 297#[action(namespace = project_panel)]
 298#[serde(deny_unknown_fields)]
 299struct Delete {
 300    #[serde(default)]
 301    pub skip_prompt: bool,
 302}
 303
 304/// Moves the selected file or directory to the system trash.
 305#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
 306#[action(namespace = project_panel)]
 307#[serde(deny_unknown_fields)]
 308struct Trash {
 309    #[serde(default)]
 310    pub skip_prompt: bool,
 311}
 312
 313/// Selects the next entry with diagnostics.
 314#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
 315#[action(namespace = project_panel)]
 316#[serde(deny_unknown_fields)]
 317struct SelectNextDiagnostic {
 318    #[serde(default)]
 319    pub severity: GoToDiagnosticSeverityFilter,
 320}
 321
 322/// Selects the previous entry with diagnostics.
 323#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
 324#[action(namespace = project_panel)]
 325#[serde(deny_unknown_fields)]
 326struct SelectPrevDiagnostic {
 327    #[serde(default)]
 328    pub severity: GoToDiagnosticSeverityFilter,
 329}
 330
 331actions!(
 332    project_panel,
 333    [
 334        /// Expands the selected entry in the project tree.
 335        ExpandSelectedEntry,
 336        /// Collapses the selected entry in the project tree.
 337        CollapseSelectedEntry,
 338        /// Collapses the selected entry and its children in the project tree.
 339        CollapseSelectedEntryAndChildren,
 340        /// Collapses all entries in the project tree.
 341        CollapseAllEntries,
 342        /// Creates a new directory.
 343        NewDirectory,
 344        /// Creates a new file.
 345        NewFile,
 346        /// Copies the selected file or directory.
 347        Copy,
 348        /// Duplicates the selected file or directory.
 349        Duplicate,
 350        /// Reveals the selected item in the system file manager.
 351        RevealInFileManager,
 352        /// Removes the selected folder from the project.
 353        RemoveFromProject,
 354        /// Cuts the selected file or directory.
 355        Cut,
 356        /// Pastes the previously cut or copied item.
 357        Paste,
 358        /// Downloads the selected remote file
 359        DownloadFromRemote,
 360        /// Renames the selected file or directory.
 361        Rename,
 362        /// Opens the selected file in the editor.
 363        Open,
 364        /// Opens the selected file in a permanent tab.
 365        OpenPermanent,
 366        /// Opens the selected file in a vertical split.
 367        OpenSplitVertical,
 368        /// Opens the selected file in a horizontal split.
 369        OpenSplitHorizontal,
 370        /// Toggles visibility of git-ignored files.
 371        ToggleHideGitIgnore,
 372        /// Toggles visibility of hidden files.
 373        ToggleHideHidden,
 374        /// Starts a new search in the selected directory.
 375        NewSearchInDirectory,
 376        /// Unfolds the selected directory.
 377        UnfoldDirectory,
 378        /// Folds the selected directory.
 379        FoldDirectory,
 380        /// Scroll half a page upwards
 381        ScrollUp,
 382        /// Scroll half a page downwards
 383        ScrollDown,
 384        /// Scroll until the cursor displays at the center
 385        ScrollCursorCenter,
 386        /// Scroll until the cursor displays at the top
 387        ScrollCursorTop,
 388        /// Scroll until the cursor displays at the bottom
 389        ScrollCursorBottom,
 390        /// Selects the parent directory.
 391        SelectParent,
 392        /// Selects the next entry with git changes.
 393        SelectNextGitEntry,
 394        /// Selects the previous entry with git changes.
 395        SelectPrevGitEntry,
 396        /// Selects the next directory.
 397        SelectNextDirectory,
 398        /// Selects the previous directory.
 399        SelectPrevDirectory,
 400        /// Opens a diff view to compare two marked files.
 401        CompareMarkedFiles,
 402        /// Undoes the last file operation.
 403        Undo,
 404    ]
 405);
 406
 407#[derive(Clone, Debug, Default)]
 408struct FoldedAncestors {
 409    current_ancestor_depth: usize,
 410    ancestors: Vec<ProjectEntryId>,
 411}
 412
 413impl FoldedAncestors {
 414    fn max_ancestor_depth(&self) -> usize {
 415        self.ancestors.len()
 416    }
 417
 418    /// Note: This returns None for last item in ancestors list
 419    fn active_ancestor(&self) -> Option<ProjectEntryId> {
 420        if self.current_ancestor_depth == 0 {
 421            return None;
 422        }
 423        self.ancestors.get(self.current_ancestor_depth).copied()
 424    }
 425
 426    fn active_index(&self) -> usize {
 427        self.max_ancestor_depth()
 428            .saturating_sub(1)
 429            .saturating_sub(self.current_ancestor_depth)
 430    }
 431
 432    fn set_active_index(&mut self, index: usize) -> bool {
 433        let new_depth = self
 434            .max_ancestor_depth()
 435            .saturating_sub(1)
 436            .saturating_sub(index);
 437        if self.current_ancestor_depth != new_depth {
 438            self.current_ancestor_depth = new_depth;
 439            true
 440        } else {
 441            false
 442        }
 443    }
 444
 445    fn active_component(&self, file_name: &str) -> Option<String> {
 446        Path::new(file_name)
 447            .components()
 448            .nth(self.active_index())
 449            .map(|comp| comp.as_os_str().to_string_lossy().into_owned())
 450    }
 451}
 452
 453pub fn init(cx: &mut App) {
 454    cx.observe_new(|workspace: &mut Workspace, _, _| {
 455        workspace.register_action(|workspace, _: &ToggleFocus, window, cx| {
 456            workspace.toggle_panel_focus::<ProjectPanel>(window, cx);
 457        });
 458        workspace.register_action(|workspace, _: &Toggle, window, cx| {
 459            if !workspace.toggle_panel_focus::<ProjectPanel>(window, cx) {
 460                workspace.close_panel::<ProjectPanel>(window, cx);
 461            }
 462        });
 463
 464        workspace.register_action(|workspace, _: &ToggleHideGitIgnore, _, cx| {
 465            let fs = workspace.app_state().fs.clone();
 466            update_settings_file(fs, cx, move |setting, _| {
 467                setting.project_panel.get_or_insert_default().hide_gitignore = Some(
 468                    !setting
 469                        .project_panel
 470                        .get_or_insert_default()
 471                        .hide_gitignore
 472                        .unwrap_or(false),
 473                );
 474            })
 475        });
 476
 477        workspace.register_action(|workspace, _: &ToggleHideHidden, _, cx| {
 478            let fs = workspace.app_state().fs.clone();
 479            update_settings_file(fs, cx, move |setting, _| {
 480                setting.project_panel.get_or_insert_default().hide_hidden = Some(
 481                    !setting
 482                        .project_panel
 483                        .get_or_insert_default()
 484                        .hide_hidden
 485                        .unwrap_or(false),
 486                );
 487            })
 488        });
 489
 490        workspace.register_action(|workspace, action: &CollapseAllEntries, window, cx| {
 491            if let Some(panel) = workspace.panel::<ProjectPanel>(cx) {
 492                panel.update(cx, |panel, cx| {
 493                    panel.collapse_all_entries(action, window, cx);
 494                });
 495            }
 496        });
 497
 498        workspace.register_action(|workspace, action: &Rename, window, cx| {
 499            workspace.open_panel::<ProjectPanel>(window, cx);
 500            if let Some(panel) = workspace.panel::<ProjectPanel>(cx) {
 501                panel.update(cx, |panel, cx| {
 502                    if let Some(first_marked) = panel.marked_entries.first() {
 503                        let first_marked = *first_marked;
 504                        panel.marked_entries.clear();
 505                        panel.selection = Some(first_marked);
 506                    }
 507                    panel.rename(action, window, cx);
 508                });
 509            }
 510        });
 511
 512        workspace.register_action(|workspace, action: &Duplicate, window, cx| {
 513            workspace.open_panel::<ProjectPanel>(window, cx);
 514            if let Some(panel) = workspace.panel::<ProjectPanel>(cx) {
 515                panel.update(cx, |panel, cx| {
 516                    panel.duplicate(action, window, cx);
 517                });
 518            }
 519        });
 520
 521        workspace.register_action(|workspace, action: &Delete, window, cx| {
 522            if let Some(panel) = workspace.panel::<ProjectPanel>(cx) {
 523                panel.update(cx, |panel, cx| panel.delete(action, window, cx));
 524            }
 525        });
 526    })
 527    .detach();
 528}
 529
 530#[derive(Debug)]
 531pub enum Event {
 532    OpenedEntry {
 533        entry_id: ProjectEntryId,
 534        focus_opened_item: bool,
 535        allow_preview: bool,
 536    },
 537    SplitEntry {
 538        entry_id: ProjectEntryId,
 539        allow_preview: bool,
 540        split_direction: Option<SplitDirection>,
 541    },
 542    Focus,
 543}
 544
 545struct DraggedProjectEntryView {
 546    selection: SelectedEntry,
 547    icon: Option<SharedString>,
 548    filename: String,
 549    click_offset: Point<Pixels>,
 550    selections: Arc<[SelectedEntry]>,
 551}
 552
 553struct ItemColors {
 554    default: Hsla,
 555    hover: Hsla,
 556    drag_over: Hsla,
 557    marked: Hsla,
 558    focused: Hsla,
 559}
 560
 561fn get_item_color(is_sticky: bool, cx: &App) -> ItemColors {
 562    let colors = cx.theme().colors();
 563
 564    ItemColors {
 565        default: if is_sticky {
 566            colors.panel_overlay_background
 567        } else {
 568            colors.panel_background
 569        },
 570        hover: if is_sticky {
 571            colors.panel_overlay_hover
 572        } else {
 573            colors.element_hover
 574        },
 575        marked: colors.element_selected,
 576        focused: colors.panel_focused_border,
 577        drag_over: colors.drop_target_background,
 578    }
 579}
 580
 581impl ProjectPanel {
 582    fn new(
 583        workspace: &mut Workspace,
 584        window: &mut Window,
 585        cx: &mut Context<Workspace>,
 586    ) -> Entity<Self> {
 587        let project = workspace.project().clone();
 588        let git_store = project.read(cx).git_store().clone();
 589        let path_style = project.read(cx).path_style(cx);
 590        let project_panel = cx.new(|cx| {
 591            let focus_handle = cx.focus_handle();
 592            cx.on_focus(&focus_handle, window, Self::focus_in).detach();
 593
 594            cx.subscribe_in(
 595                &git_store,
 596                window,
 597                |this, _, event, window, cx| match event {
 598                    GitStoreEvent::RepositoryUpdated(_, RepositoryEvent::StatusesChanged, _)
 599                    | GitStoreEvent::RepositoryAdded
 600                    | GitStoreEvent::RepositoryRemoved(_) => {
 601                        this.update_visible_entries(None, false, false, window, cx);
 602                        cx.notify();
 603                    }
 604                    _ => {}
 605                },
 606            )
 607            .detach();
 608
 609            cx.subscribe_in(
 610                &project,
 611                window,
 612                |this, project, event, window, cx| match event {
 613                    project::Event::ActiveEntryChanged(Some(entry_id)) => {
 614                        if ProjectPanelSettings::get_global(cx).auto_reveal_entries {
 615                            this.reveal_entry(project.clone(), *entry_id, true, window, cx)
 616                                .ok();
 617                        }
 618                    }
 619                    project::Event::ActiveEntryChanged(None) => {
 620                        let is_active_item_file_diff_view = this
 621                            .workspace
 622                            .upgrade()
 623                            .and_then(|ws| ws.read(cx).active_item(cx))
 624                            .map(|item| {
 625                                item.act_as_type(TypeId::of::<FileDiffView>(), cx).is_some()
 626                            })
 627                            .unwrap_or(false);
 628                        if !is_active_item_file_diff_view {
 629                            this.marked_entries.clear();
 630                        }
 631                    }
 632                    project::Event::RevealInProjectPanel(entry_id) => {
 633                        if let Some(()) = this
 634                            .reveal_entry(project.clone(), *entry_id, false, window, cx)
 635                            .log_err()
 636                        {
 637                            cx.emit(PanelEvent::Activate);
 638                        }
 639                    }
 640                    project::Event::ActivateProjectPanel => {
 641                        cx.emit(PanelEvent::Activate);
 642                    }
 643                    project::Event::DiskBasedDiagnosticsFinished { .. }
 644                    | project::Event::DiagnosticsUpdated { .. } => {
 645                        if ProjectPanelSettings::get_global(cx).show_diagnostics
 646                            != ShowDiagnostics::Off
 647                        {
 648                            this.diagnostic_summary_update = cx.spawn(async move |this, cx| {
 649                                cx.background_executor()
 650                                    .timer(Duration::from_millis(30))
 651                                    .await;
 652                                this.update(cx, |this, cx| {
 653                                    this.update_diagnostics(cx);
 654                                    cx.notify();
 655                                })
 656                                .log_err();
 657                            });
 658                        }
 659                    }
 660                    project::Event::WorktreeRemoved(id) => {
 661                        this.state.expanded_dir_ids.remove(id);
 662                        this.update_visible_entries(None, false, false, window, cx);
 663                        cx.notify();
 664                    }
 665                    project::Event::WorktreeUpdatedEntries(_, _)
 666                    | project::Event::WorktreeAdded(_)
 667                    | project::Event::WorktreeOrderChanged => {
 668                        this.update_visible_entries(None, false, false, window, cx);
 669                        cx.notify();
 670                    }
 671                    project::Event::ExpandedAllForEntry(worktree_id, entry_id) => {
 672                        if let Some((worktree, expanded_dir_ids)) = project
 673                            .read(cx)
 674                            .worktree_for_id(*worktree_id, cx)
 675                            .zip(this.state.expanded_dir_ids.get_mut(worktree_id))
 676                        {
 677                            let worktree = worktree.read(cx);
 678
 679                            let Some(entry) = worktree.entry_for_id(*entry_id) else {
 680                                return;
 681                            };
 682                            let include_ignored_dirs = !entry.is_ignored;
 683
 684                            let mut dirs_to_expand = vec![*entry_id];
 685                            while let Some(current_id) = dirs_to_expand.pop() {
 686                                let Some(current_entry) = worktree.entry_for_id(current_id) else {
 687                                    continue;
 688                                };
 689                                for child in worktree.child_entries(&current_entry.path) {
 690                                    if !child.is_dir() || (include_ignored_dirs && child.is_ignored)
 691                                    {
 692                                        continue;
 693                                    }
 694
 695                                    dirs_to_expand.push(child.id);
 696
 697                                    if let Err(ix) = expanded_dir_ids.binary_search(&child.id) {
 698                                        expanded_dir_ids.insert(ix, child.id);
 699                                    }
 700                                    this.state.unfolded_dir_ids.insert(child.id);
 701                                }
 702                            }
 703                            this.update_visible_entries(None, false, false, window, cx);
 704                            cx.notify();
 705                        }
 706                    }
 707                    _ => {}
 708                },
 709            )
 710            .detach();
 711
 712            let trash_action = [TypeId::of::<Trash>()];
 713            let is_remote = project.read(cx).is_remote();
 714
 715            // Make sure the trash option is never displayed anywhere on remote
 716            // hosts since they may not support trashing. May want to dynamically
 717            // detect this in the future.
 718            if is_remote {
 719                CommandPaletteFilter::update_global(cx, |filter, _cx| {
 720                    filter.hide_action_types(&trash_action);
 721                });
 722            }
 723
 724            let filename_editor = cx.new(|cx| Editor::single_line(window, cx));
 725
 726            cx.subscribe_in(
 727                &filename_editor,
 728                window,
 729                |project_panel, _, editor_event, window, cx| match editor_event {
 730                    EditorEvent::BufferEdited => {
 731                        project_panel.populate_validation_error(cx);
 732                        project_panel.autoscroll(cx);
 733                    }
 734                    EditorEvent::SelectionsChanged { .. } => {
 735                        project_panel.autoscroll(cx);
 736                    }
 737                    EditorEvent::Blurred => {
 738                        if project_panel
 739                            .state
 740                            .edit_state
 741                            .as_ref()
 742                            .is_some_and(|state| state.processing_filename.is_none())
 743                        {
 744                            match project_panel.confirm_edit(false, window, cx) {
 745                                Some(task) => {
 746                                    task.detach_and_notify_err(
 747                                        project_panel.workspace.clone(),
 748                                        window,
 749                                        cx,
 750                                    );
 751                                }
 752                                None => {
 753                                    project_panel.discard_edit_state(window, cx);
 754                                }
 755                            }
 756                        }
 757                    }
 758                    _ => {}
 759                },
 760            )
 761            .detach();
 762
 763            cx.observe_global::<FileIcons>(|_, cx| {
 764                cx.notify();
 765            })
 766            .detach();
 767
 768            let mut project_panel_settings = *ProjectPanelSettings::get_global(cx);
 769            cx.observe_global_in::<SettingsStore>(window, move |this, window, cx| {
 770                let new_settings = *ProjectPanelSettings::get_global(cx);
 771                if project_panel_settings != new_settings {
 772                    if project_panel_settings.hide_gitignore != new_settings.hide_gitignore {
 773                        this.update_visible_entries(None, false, false, window, cx);
 774                    }
 775                    if project_panel_settings.hide_root != new_settings.hide_root {
 776                        this.update_visible_entries(None, false, false, window, cx);
 777                    }
 778                    if project_panel_settings.hide_hidden != new_settings.hide_hidden {
 779                        this.update_visible_entries(None, false, false, window, cx);
 780                    }
 781                    if project_panel_settings.sort_mode != new_settings.sort_mode {
 782                        this.update_visible_entries(None, false, false, window, cx);
 783                    }
 784                    if project_panel_settings.sticky_scroll && !new_settings.sticky_scroll {
 785                        this.sticky_items_count = 0;
 786                    }
 787                    project_panel_settings = new_settings;
 788                    this.update_diagnostics(cx);
 789                    cx.notify();
 790                }
 791            })
 792            .detach();
 793
 794            let scroll_handle = UniformListScrollHandle::new();
 795            let mut this = Self {
 796                project: project.clone(),
 797                hover_scroll_task: None,
 798                fs: workspace.app_state().fs.clone(),
 799                focus_handle,
 800                rendered_entries_len: 0,
 801                folded_directory_drag_target: None,
 802                drag_target_entry: None,
 803                marked_entries: Default::default(),
 804                selection: None,
 805                context_menu: None,
 806                filename_editor,
 807                clipboard: None,
 808                _dragged_entry_destination: None,
 809                workspace: workspace.weak_handle(),
 810                diagnostics: Default::default(),
 811                diagnostic_counts: Default::default(),
 812                diagnostic_summary_update: Task::ready(()),
 813                scroll_handle,
 814                mouse_down: false,
 815                hover_expand_task: None,
 816                previous_drag_position: None,
 817                sticky_items_count: 0,
 818                last_reported_update: Instant::now(),
 819                state: State {
 820                    max_width_item_index: None,
 821                    edit_state: None,
 822                    temporarily_unfolded_pending_state: None,
 823                    last_worktree_root_id: Default::default(),
 824                    visible_entries: Default::default(),
 825                    ancestors: Default::default(),
 826                    expanded_dir_ids: Default::default(),
 827                    unfolded_dir_ids: Default::default(),
 828                },
 829                update_visible_entries_task: Default::default(),
 830                undo_manager: UndoManager::new(workspace.weak_handle()),
 831            };
 832            this.update_visible_entries(None, false, false, window, cx);
 833
 834            this
 835        });
 836
 837        cx.subscribe_in(&project_panel, window, {
 838            let project_panel = project_panel.downgrade();
 839            move |workspace, _, event, window, cx| match event {
 840                &Event::OpenedEntry {
 841                    entry_id,
 842                    focus_opened_item,
 843                    allow_preview,
 844                } => {
 845                    if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx)
 846                        && let Some(entry) = worktree.read(cx).entry_for_id(entry_id) {
 847                            let file_path = entry.path.clone();
 848                            let worktree_id = worktree.read(cx).id();
 849                            let entry_id = entry.id;
 850                            let is_via_ssh = project.read(cx).is_via_remote_server();
 851
 852                            workspace
 853                                .open_path_preview(
 854                                    ProjectPath {
 855                                        worktree_id,
 856                                        path: file_path.clone(),
 857                                    },
 858                                    None,
 859                                    focus_opened_item,
 860                                    allow_preview,
 861                                    true,
 862                                    window, cx,
 863                                )
 864                                .detach_and_prompt_err("Failed to open file", window, cx, move |e, _, _| {
 865                                    match e.error_code() {
 866                                        ErrorCode::Disconnected => if is_via_ssh {
 867                                            Some("Disconnected from SSH host".to_string())
 868                                        } else {
 869                                            Some("Disconnected from remote project".to_string())
 870                                        },
 871                                        ErrorCode::UnsharedItem => Some(format!(
 872                                            "{} is not shared by the host. This could be because it has been marked as `private`",
 873                                            file_path.display(path_style)
 874                                        )),
 875                                        // See note in worktree.rs where this error originates. Returning Some in this case prevents
 876                                        // the error popup from saying "Try Again", which is a red herring in this case
 877                                        ErrorCode::Internal if e.to_string().contains("File is too large to load") => Some(e.to_string()),
 878                                        _ => None,
 879                                    }
 880                                });
 881
 882                            if let Some(project_panel) = project_panel.upgrade() {
 883                                // Always select and mark the entry, regardless of whether it is opened or not.
 884                                project_panel.update(cx, |project_panel, _| {
 885                                    let entry = SelectedEntry { worktree_id, entry_id };
 886                                    project_panel.marked_entries.clear();
 887                                    project_panel.marked_entries.push(entry);
 888                                    project_panel.selection = Some(entry);
 889                                });
 890                                if !focus_opened_item {
 891                                    let focus_handle = project_panel.read(cx).focus_handle.clone();
 892                                    window.focus(&focus_handle, cx);
 893                                }
 894                            }
 895                        }
 896                }
 897                &Event::SplitEntry {
 898                    entry_id,
 899                    allow_preview,
 900                    split_direction,
 901                } => {
 902                    if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx)
 903                        && let Some(entry) = worktree.read(cx).entry_for_id(entry_id) {
 904                            workspace
 905                                .split_path_preview(
 906                                    ProjectPath {
 907                                        worktree_id: worktree.read(cx).id(),
 908                                        path: entry.path.clone(),
 909                                    },
 910                                    allow_preview,
 911                                    split_direction,
 912                                    window, cx,
 913                                )
 914                                .detach_and_log_err(cx);
 915                        }
 916                }
 917
 918                _ => {}
 919            }
 920        })
 921        .detach();
 922
 923        project_panel
 924    }
 925
 926    pub async fn load(
 927        workspace: WeakEntity<Workspace>,
 928        mut cx: AsyncWindowContext,
 929    ) -> Result<Entity<Self>> {
 930        workspace.update_in(&mut cx, |workspace, window, cx| {
 931            ProjectPanel::new(workspace, window, cx)
 932        })
 933    }
 934
 935    fn update_diagnostics(&mut self, cx: &mut Context<Self>) {
 936        let mut diagnostics: HashMap<(WorktreeId, Arc<RelPath>), DiagnosticSeverity> =
 937            Default::default();
 938        let show_diagnostics_setting = ProjectPanelSettings::get_global(cx).show_diagnostics;
 939
 940        if show_diagnostics_setting != ShowDiagnostics::Off {
 941            self.project
 942                .read(cx)
 943                .diagnostic_summaries(false, cx)
 944                .filter_map(|(path, _, diagnostic_summary)| {
 945                    if diagnostic_summary.error_count > 0 {
 946                        Some((path, DiagnosticSeverity::ERROR))
 947                    } else if show_diagnostics_setting == ShowDiagnostics::All
 948                        && diagnostic_summary.warning_count > 0
 949                    {
 950                        Some((path, DiagnosticSeverity::WARNING))
 951                    } else {
 952                        None
 953                    }
 954                })
 955                .for_each(|(project_path, diagnostic_severity)| {
 956                    let ancestors = project_path.path.ancestors().collect::<Vec<_>>();
 957                    for path in ancestors.into_iter().rev() {
 958                        Self::update_strongest_diagnostic_severity(
 959                            &mut diagnostics,
 960                            &project_path,
 961                            path.into(),
 962                            diagnostic_severity,
 963                        );
 964                    }
 965                });
 966        }
 967        self.diagnostics = diagnostics;
 968
 969        let diagnostic_badges = ProjectPanelSettings::get_global(cx).diagnostic_badges;
 970        self.diagnostic_counts =
 971            if diagnostic_badges && show_diagnostics_setting != ShowDiagnostics::Off {
 972                self.project.read(cx).diagnostic_summaries(false, cx).fold(
 973                    HashMap::default(),
 974                    |mut counts, (project_path, _, summary)| {
 975                        let entry = counts
 976                            .entry((project_path.worktree_id, project_path.path))
 977                            .or_default();
 978                        entry.error_count += summary.error_count;
 979                        if show_diagnostics_setting == ShowDiagnostics::All {
 980                            entry.warning_count += summary.warning_count;
 981                        }
 982                        counts
 983                    },
 984                )
 985            } else {
 986                Default::default()
 987            };
 988    }
 989
 990    fn update_strongest_diagnostic_severity(
 991        diagnostics: &mut HashMap<(WorktreeId, Arc<RelPath>), DiagnosticSeverity>,
 992        project_path: &ProjectPath,
 993        path_buffer: Arc<RelPath>,
 994        diagnostic_severity: DiagnosticSeverity,
 995    ) {
 996        diagnostics
 997            .entry((project_path.worktree_id, path_buffer))
 998            .and_modify(|strongest_diagnostic_severity| {
 999                *strongest_diagnostic_severity =
1000                    cmp::min(*strongest_diagnostic_severity, diagnostic_severity);
1001            })
1002            .or_insert(diagnostic_severity);
1003    }
1004
1005    fn focus_in(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1006        if !self.focus_handle.contains_focused(window, cx) {
1007            cx.emit(Event::Focus);
1008        }
1009    }
1010
1011    fn deploy_context_menu(
1012        &mut self,
1013        position: Point<Pixels>,
1014        entry_id: ProjectEntryId,
1015        window: &mut Window,
1016        cx: &mut Context<Self>,
1017    ) {
1018        let project = self.project.read(cx);
1019
1020        let worktree_id = if let Some(id) = project.worktree_id_for_entry(entry_id, cx) {
1021            id
1022        } else {
1023            return;
1024        };
1025
1026        self.selection = Some(SelectedEntry {
1027            worktree_id,
1028            entry_id,
1029        });
1030
1031        if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
1032            let auto_fold_dirs = ProjectPanelSettings::get_global(cx).auto_fold_dirs;
1033            let worktree = worktree.read(cx);
1034            let is_root = Some(entry) == worktree.root_entry();
1035            let is_dir = entry.is_dir();
1036            let is_foldable = auto_fold_dirs && self.is_foldable(entry, worktree);
1037            let is_unfoldable = auto_fold_dirs && self.is_unfoldable(entry, worktree);
1038            let is_read_only = project.is_read_only(cx);
1039            let is_remote = project.is_remote();
1040            let is_collab = project.is_via_collab();
1041            let is_local = project.is_local() || project.is_via_wsl_with_host_interop(cx);
1042
1043            let settings = ProjectPanelSettings::get_global(cx);
1044            let visible_worktrees_count = project.visible_worktrees(cx).count();
1045            let should_hide_rename = is_root
1046                && (cfg!(target_os = "windows")
1047                    || (settings.hide_root && visible_worktrees_count == 1));
1048            let should_show_compare = !is_dir && self.file_abs_paths_to_diff(cx).is_some();
1049
1050            let has_file_history = !is_dir && {
1051                let project_path = project::ProjectPath {
1052                    worktree_id,
1053                    path: entry.path.clone(),
1054                };
1055                let git_store = project.git_store().read(cx);
1056                git_store
1057                    .repository_and_path_for_project_path(&project_path, cx)
1058                    .is_some()
1059                    && !git_store
1060                        .project_path_git_status(&project_path, cx)
1061                        .is_some_and(|status| status.is_created())
1062            };
1063
1064            let has_pasteable_content = self.has_pasteable_content(cx);
1065            let entity = cx.entity();
1066            let context_menu = ContextMenu::build(window, cx, |menu, _, cx| {
1067                menu.context(self.focus_handle.clone()).map(|menu| {
1068                    if is_read_only {
1069                        menu.when(is_dir, |menu| {
1070                            menu.action("Search Inside", Box::new(NewSearchInDirectory))
1071                        })
1072                    } else {
1073                        menu.action("New File", Box::new(NewFile))
1074                            .action("New Folder", Box::new(NewDirectory))
1075                            .separator()
1076                            .when(is_local, |menu| {
1077                                menu.action(
1078                                    ui::utils::reveal_in_file_manager_label(is_remote),
1079                                    Box::new(RevealInFileManager),
1080                                )
1081                            })
1082                            .when(is_local, |menu| {
1083                                menu.action("Open in Default App", Box::new(OpenWithSystem))
1084                            })
1085                            .action("Open in Terminal", Box::new(OpenInTerminal))
1086                            .when(is_dir, |menu| {
1087                                menu.separator()
1088                                    .action("Find in Folder…", Box::new(NewSearchInDirectory))
1089                            })
1090                            .when(is_unfoldable, |menu| {
1091                                menu.action("Unfold Directory", Box::new(UnfoldDirectory))
1092                            })
1093                            .when(is_foldable, |menu| {
1094                                menu.action("Fold Directory", Box::new(FoldDirectory))
1095                            })
1096                            .when(should_show_compare, |menu| {
1097                                menu.separator()
1098                                    .action("Compare Marked Files", Box::new(CompareMarkedFiles))
1099                            })
1100                            .separator()
1101                            .action("Cut", Box::new(Cut))
1102                            .action("Copy", Box::new(Copy))
1103                            .action("Duplicate", Box::new(Duplicate))
1104                            // TODO: Paste should always be visible, cbut disabled when clipboard is empty
1105                            .action_disabled_when(!has_pasteable_content, "Paste", Box::new(Paste))
1106                            .when(cx.has_flag::<ProjectPanelUndoRedoFeatureFlag>(), |menu| {
1107                                menu.action_disabled_when(
1108                                    !self.undo_manager.can_undo(),
1109                                    "Undo",
1110                                    Box::new(Undo),
1111                                )
1112                            })
1113                            .when(is_remote, |menu| {
1114                                menu.separator()
1115                                    .action("Download...", Box::new(DownloadFromRemote))
1116                            })
1117                            .separator()
1118                            .action("Copy Path", Box::new(zed_actions::workspace::CopyPath))
1119                            .action(
1120                                "Copy Relative Path",
1121                                Box::new(zed_actions::workspace::CopyRelativePath),
1122                            )
1123                            .when(!is_dir && self.has_git_changes(entry_id), |menu| {
1124                                menu.separator().action(
1125                                    "Restore File",
1126                                    Box::new(git::RestoreFile { skip_prompt: false }),
1127                                )
1128                            })
1129                            .when(has_file_history, |menu| {
1130                                menu.separator()
1131                                    .action("View File History", Box::new(git::FileHistory))
1132                            })
1133                            .when(!should_hide_rename, |menu| {
1134                                menu.separator().action("Rename", Box::new(Rename))
1135                            })
1136                            .when(!is_root && !is_remote, |menu| {
1137                                menu.action("Trash", Box::new(Trash { skip_prompt: false }))
1138                            })
1139                            .when(!is_root, |menu| {
1140                                menu.action("Delete", Box::new(Delete { skip_prompt: false }))
1141                            })
1142                            .when(!is_collab && is_root, |menu| {
1143                                menu.separator()
1144                                    .action(
1145                                        "Add Project to Workspace…",
1146                                        Box::new(workspace::AddFolderToProject),
1147                                    )
1148                                    .action("Remove from Workspace", Box::new(RemoveFromProject))
1149                            })
1150                            .when(is_dir && !is_root, |menu| {
1151                                menu.separator().action(
1152                                    "Collapse All",
1153                                    Box::new(CollapseSelectedEntryAndChildren),
1154                                )
1155                            })
1156                            .when(is_dir && is_root, |menu| {
1157                                let entity = entity.clone();
1158                                menu.separator().item(
1159                                    ContextMenuEntry::new("Collapse All").handler(
1160                                        move |window, cx| {
1161                                            entity.update(cx, |this, cx| {
1162                                                this.collapse_all_for_root(window, cx);
1163                                            });
1164                                        },
1165                                    ),
1166                                )
1167                            })
1168                    }
1169                })
1170            });
1171
1172            window.focus(&context_menu.focus_handle(cx), cx);
1173            let subscription = cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| {
1174                this.context_menu.take();
1175                cx.notify();
1176            });
1177            self.context_menu = Some((context_menu, position, subscription));
1178        }
1179
1180        cx.notify();
1181    }
1182
1183    fn has_git_changes(&self, entry_id: ProjectEntryId) -> bool {
1184        for visible in &self.state.visible_entries {
1185            if let Some(git_entry) = visible.entries.iter().find(|e| e.id == entry_id) {
1186                let total_modified =
1187                    git_entry.git_summary.index.modified + git_entry.git_summary.worktree.modified;
1188                let total_deleted =
1189                    git_entry.git_summary.index.deleted + git_entry.git_summary.worktree.deleted;
1190                return total_modified > 0 || total_deleted > 0;
1191            }
1192        }
1193        false
1194    }
1195
1196    fn is_unfoldable(&self, entry: &Entry, worktree: &Worktree) -> bool {
1197        if !entry.is_dir() || self.state.unfolded_dir_ids.contains(&entry.id) {
1198            return false;
1199        }
1200
1201        if let Some(parent_path) = entry.path.parent() {
1202            let snapshot = worktree.snapshot();
1203            let mut child_entries = snapshot.child_entries(parent_path);
1204            if let Some(child) = child_entries.next()
1205                && child_entries.next().is_none()
1206            {
1207                return child.kind.is_dir();
1208            }
1209        };
1210        false
1211    }
1212
1213    fn is_foldable(&self, entry: &Entry, worktree: &Worktree) -> bool {
1214        if entry.is_dir() {
1215            let snapshot = worktree.snapshot();
1216
1217            let mut child_entries = snapshot.child_entries(&entry.path);
1218            if let Some(child) = child_entries.next()
1219                && child_entries.next().is_none()
1220            {
1221                return child.kind.is_dir();
1222            }
1223        }
1224        false
1225    }
1226
1227    fn expand_selected_entry(
1228        &mut self,
1229        _: &ExpandSelectedEntry,
1230        window: &mut Window,
1231        cx: &mut Context<Self>,
1232    ) {
1233        if let Some((worktree, entry)) = self.selected_entry(cx) {
1234            if let Some(folded_ancestors) = self.state.ancestors.get_mut(&entry.id)
1235                && folded_ancestors.current_ancestor_depth > 0
1236            {
1237                folded_ancestors.current_ancestor_depth -= 1;
1238                cx.notify();
1239                return;
1240            }
1241            if entry.is_dir() {
1242                let worktree_id = worktree.id();
1243                let entry_id = entry.id;
1244                let expanded_dir_ids = if let Some(expanded_dir_ids) =
1245                    self.state.expanded_dir_ids.get_mut(&worktree_id)
1246                {
1247                    expanded_dir_ids
1248                } else {
1249                    return;
1250                };
1251
1252                match expanded_dir_ids.binary_search(&entry_id) {
1253                    Ok(_) => self.select_next(&SelectNext, window, cx),
1254                    Err(ix) => {
1255                        self.project.update(cx, |project, cx| {
1256                            project.expand_entry(worktree_id, entry_id, cx);
1257                        });
1258
1259                        expanded_dir_ids.insert(ix, entry_id);
1260                        self.update_visible_entries(None, false, false, window, cx);
1261                        cx.notify();
1262                    }
1263                }
1264            }
1265        }
1266    }
1267
1268    fn collapse_selected_entry(
1269        &mut self,
1270        _: &CollapseSelectedEntry,
1271        window: &mut Window,
1272        cx: &mut Context<Self>,
1273    ) {
1274        let Some((worktree, entry)) = self.selected_entry_handle(cx) else {
1275            return;
1276        };
1277        self.collapse_entry(entry.clone(), worktree, window, cx)
1278    }
1279
1280    fn collapse_entry(
1281        &mut self,
1282        entry: Entry,
1283        worktree: Entity<Worktree>,
1284        window: &mut Window,
1285        cx: &mut Context<Self>,
1286    ) {
1287        let worktree = worktree.read(cx);
1288        if let Some(folded_ancestors) = self.state.ancestors.get_mut(&entry.id)
1289            && folded_ancestors.current_ancestor_depth + 1 < folded_ancestors.max_ancestor_depth()
1290        {
1291            folded_ancestors.current_ancestor_depth += 1;
1292            cx.notify();
1293            return;
1294        }
1295        let worktree_id = worktree.id();
1296        let expanded_dir_ids =
1297            if let Some(expanded_dir_ids) = self.state.expanded_dir_ids.get_mut(&worktree_id) {
1298                expanded_dir_ids
1299            } else {
1300                return;
1301            };
1302
1303        let mut entry = &entry;
1304        loop {
1305            let entry_id = entry.id;
1306            match expanded_dir_ids.binary_search(&entry_id) {
1307                Ok(ix) => {
1308                    expanded_dir_ids.remove(ix);
1309                    self.update_visible_entries(
1310                        Some((worktree_id, entry_id)),
1311                        false,
1312                        false,
1313                        window,
1314                        cx,
1315                    );
1316                    cx.notify();
1317                    break;
1318                }
1319                Err(_) => {
1320                    if let Some(parent_entry) =
1321                        entry.path.parent().and_then(|p| worktree.entry_for_path(p))
1322                    {
1323                        entry = parent_entry;
1324                    } else {
1325                        break;
1326                    }
1327                }
1328            }
1329        }
1330    }
1331
1332    fn collapse_selected_entry_and_children(
1333        &mut self,
1334        _: &CollapseSelectedEntryAndChildren,
1335        window: &mut Window,
1336        cx: &mut Context<Self>,
1337    ) {
1338        if let Some((worktree, entry)) = self.selected_entry(cx) {
1339            let worktree_id = worktree.id();
1340            let entry_id = entry.id;
1341
1342            self.collapse_all_for_entry(worktree_id, entry_id, cx);
1343
1344            self.update_visible_entries(Some((worktree_id, entry_id)), false, false, window, cx);
1345            cx.notify();
1346        }
1347    }
1348
1349    /// Handles "Collapse All" from the context menu when a root directory is selected.
1350    /// With a single visible worktree, keeps the root expanded (matching CollapseAllEntries behavior).
1351    /// With multiple visible worktrees, collapses the root and all its children.
1352    fn collapse_all_for_root(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1353        let Some((worktree, entry)) = self.selected_entry(cx) else {
1354            return;
1355        };
1356
1357        let is_root = worktree.root_entry().map(|e| e.id) == Some(entry.id);
1358        if !is_root {
1359            return;
1360        }
1361
1362        let worktree_id = worktree.id();
1363        let root_id = entry.id;
1364
1365        if let Some(expanded_dir_ids) = self.state.expanded_dir_ids.get_mut(&worktree_id) {
1366            if self.project.read(cx).visible_worktrees(cx).count() == 1 {
1367                expanded_dir_ids.retain(|id| id == &root_id);
1368            } else {
1369                expanded_dir_ids.clear();
1370            }
1371        }
1372
1373        self.update_visible_entries(Some((worktree_id, root_id)), false, false, window, cx);
1374        cx.notify();
1375    }
1376
1377    fn collapse_all_entries(
1378        &mut self,
1379        _: &CollapseAllEntries,
1380        window: &mut Window,
1381        cx: &mut Context<Self>,
1382    ) {
1383        // By keeping entries for fully collapsed worktrees, we avoid expanding them within update_visible_entries
1384        // (which is it's default behavior when there's no entry for a worktree in expanded_dir_ids).
1385        let multiple_worktrees = self.project.read(cx).visible_worktrees(cx).count() > 1;
1386        let project = self.project.read(cx);
1387
1388        self.state
1389            .expanded_dir_ids
1390            .iter_mut()
1391            .for_each(|(worktree_id, expanded_entries)| {
1392                if multiple_worktrees {
1393                    *expanded_entries = Default::default();
1394                    return;
1395                }
1396
1397                let root_entry_id = project
1398                    .worktree_for_id(*worktree_id, cx)
1399                    .map(|worktree| worktree.read(cx).snapshot())
1400                    .and_then(|worktree_snapshot| {
1401                        worktree_snapshot.root_entry().map(|entry| entry.id)
1402                    });
1403
1404                match root_entry_id {
1405                    Some(id) => {
1406                        expanded_entries.retain(|entry_id| entry_id == &id);
1407                    }
1408                    None => *expanded_entries = Default::default(),
1409                };
1410            });
1411
1412        self.update_visible_entries(None, false, false, window, cx);
1413        cx.notify();
1414    }
1415
1416    fn toggle_expanded(
1417        &mut self,
1418        entry_id: ProjectEntryId,
1419        window: &mut Window,
1420        cx: &mut Context<Self>,
1421    ) {
1422        if let Some(worktree_id) = self.project.read(cx).worktree_id_for_entry(entry_id, cx)
1423            && let Some(expanded_dir_ids) = self.state.expanded_dir_ids.get_mut(&worktree_id)
1424        {
1425            self.project.update(cx, |project, cx| {
1426                match expanded_dir_ids.binary_search(&entry_id) {
1427                    Ok(ix) => {
1428                        expanded_dir_ids.remove(ix);
1429                    }
1430                    Err(ix) => {
1431                        project.expand_entry(worktree_id, entry_id, cx);
1432                        expanded_dir_ids.insert(ix, entry_id);
1433                    }
1434                }
1435            });
1436            self.update_visible_entries(Some((worktree_id, entry_id)), false, false, window, cx);
1437            window.focus(&self.focus_handle, cx);
1438            cx.notify();
1439        }
1440    }
1441
1442    fn toggle_expand_all(
1443        &mut self,
1444        entry_id: ProjectEntryId,
1445        window: &mut Window,
1446        cx: &mut Context<Self>,
1447    ) {
1448        if let Some(worktree_id) = self.project.read(cx).worktree_id_for_entry(entry_id, cx)
1449            && let Some(expanded_dir_ids) = self.state.expanded_dir_ids.get_mut(&worktree_id)
1450        {
1451            match expanded_dir_ids.binary_search(&entry_id) {
1452                Ok(_ix) => {
1453                    self.collapse_all_for_entry(worktree_id, entry_id, cx);
1454                }
1455                Err(_ix) => {
1456                    self.expand_all_for_entry(worktree_id, entry_id, cx);
1457                }
1458            }
1459            self.update_visible_entries(Some((worktree_id, entry_id)), false, false, window, cx);
1460            window.focus(&self.focus_handle, cx);
1461            cx.notify();
1462        }
1463    }
1464
1465    fn expand_all_for_entry(
1466        &mut self,
1467        worktree_id: WorktreeId,
1468        entry_id: ProjectEntryId,
1469        cx: &mut Context<Self>,
1470    ) {
1471        self.project.update(cx, |project, cx| {
1472            if let Some((worktree, expanded_dir_ids)) = project
1473                .worktree_for_id(worktree_id, cx)
1474                .zip(self.state.expanded_dir_ids.get_mut(&worktree_id))
1475            {
1476                if let Some(task) = project.expand_all_for_entry(worktree_id, entry_id, cx) {
1477                    task.detach();
1478                }
1479
1480                let worktree = worktree.read(cx);
1481
1482                if let Some(mut entry) = worktree.entry_for_id(entry_id) {
1483                    loop {
1484                        if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) {
1485                            expanded_dir_ids.insert(ix, entry.id);
1486                        }
1487
1488                        if let Some(parent_entry) =
1489                            entry.path.parent().and_then(|p| worktree.entry_for_path(p))
1490                        {
1491                            entry = parent_entry;
1492                        } else {
1493                            break;
1494                        }
1495                    }
1496                }
1497            }
1498        });
1499    }
1500
1501    fn collapse_all_for_entry(
1502        &mut self,
1503        worktree_id: WorktreeId,
1504        entry_id: ProjectEntryId,
1505        cx: &mut Context<Self>,
1506    ) {
1507        self.project.update(cx, |project, cx| {
1508            if let Some((worktree, expanded_dir_ids)) = project
1509                .worktree_for_id(worktree_id, cx)
1510                .zip(self.state.expanded_dir_ids.get_mut(&worktree_id))
1511            {
1512                let worktree = worktree.read(cx);
1513                let mut dirs_to_collapse = vec![entry_id];
1514                let auto_fold_enabled = ProjectPanelSettings::get_global(cx).auto_fold_dirs;
1515                while let Some(current_id) = dirs_to_collapse.pop() {
1516                    let Some(current_entry) = worktree.entry_for_id(current_id) else {
1517                        continue;
1518                    };
1519                    if let Ok(ix) = expanded_dir_ids.binary_search(&current_id) {
1520                        expanded_dir_ids.remove(ix);
1521                    }
1522                    if auto_fold_enabled {
1523                        self.state.unfolded_dir_ids.remove(&current_id);
1524                    }
1525                    for child in worktree.child_entries(&current_entry.path) {
1526                        if child.is_dir() {
1527                            dirs_to_collapse.push(child.id);
1528                        }
1529                    }
1530                }
1531            }
1532        });
1533    }
1534
1535    fn select_previous(&mut self, _: &SelectPrevious, window: &mut Window, cx: &mut Context<Self>) {
1536        if let Some(edit_state) = &self.state.edit_state
1537            && edit_state.processing_filename.is_none()
1538        {
1539            self.filename_editor.update(cx, |editor, cx| {
1540                editor.move_to_beginning_of_line(
1541                    &editor::actions::MoveToBeginningOfLine {
1542                        stop_at_soft_wraps: false,
1543                        stop_at_indent: false,
1544                    },
1545                    window,
1546                    cx,
1547                );
1548            });
1549            return;
1550        }
1551        if let Some(selection) = self.selection {
1552            let (mut worktree_ix, mut entry_ix, _) =
1553                self.index_for_selection(selection).unwrap_or_default();
1554            if entry_ix > 0 {
1555                entry_ix -= 1;
1556            } else if worktree_ix > 0 {
1557                worktree_ix -= 1;
1558                entry_ix = self.state.visible_entries[worktree_ix].entries.len() - 1;
1559            } else {
1560                return;
1561            }
1562
1563            let VisibleEntriesForWorktree {
1564                worktree_id,
1565                entries,
1566                ..
1567            } = &self.state.visible_entries[worktree_ix];
1568            let selection = SelectedEntry {
1569                worktree_id: *worktree_id,
1570                entry_id: entries[entry_ix].id,
1571            };
1572            self.selection = Some(selection);
1573            if window.modifiers().shift {
1574                self.marked_entries.push(selection);
1575            }
1576            self.autoscroll(cx);
1577            cx.notify();
1578        } else {
1579            self.select_first(&SelectFirst {}, window, cx);
1580        }
1581    }
1582
1583    fn confirm(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
1584        if let Some(task) = self.confirm_edit(true, window, cx) {
1585            task.detach_and_notify_err(self.workspace.clone(), window, cx);
1586        }
1587    }
1588
1589    fn open(&mut self, _: &Open, window: &mut Window, cx: &mut Context<Self>) {
1590        let preview_tabs_enabled =
1591            PreviewTabsSettings::get_global(cx).enable_preview_from_project_panel;
1592        self.open_internal(true, !preview_tabs_enabled, None, window, cx);
1593    }
1594
1595    fn open_permanent(&mut self, _: &OpenPermanent, window: &mut Window, cx: &mut Context<Self>) {
1596        self.open_internal(false, true, None, window, cx);
1597    }
1598
1599    fn open_split_vertical(
1600        &mut self,
1601        _: &OpenSplitVertical,
1602        window: &mut Window,
1603        cx: &mut Context<Self>,
1604    ) {
1605        self.open_internal(false, true, Some(SplitDirection::vertical(cx)), window, cx);
1606    }
1607
1608    fn open_split_horizontal(
1609        &mut self,
1610        _: &OpenSplitHorizontal,
1611        window: &mut Window,
1612        cx: &mut Context<Self>,
1613    ) {
1614        self.open_internal(
1615            false,
1616            true,
1617            Some(SplitDirection::horizontal(cx)),
1618            window,
1619            cx,
1620        );
1621    }
1622
1623    fn open_internal(
1624        &mut self,
1625        allow_preview: bool,
1626        focus_opened_item: bool,
1627        split_direction: Option<SplitDirection>,
1628        window: &mut Window,
1629        cx: &mut Context<Self>,
1630    ) {
1631        if let Some((_, entry)) = self.selected_entry(cx) {
1632            if entry.is_file() {
1633                if split_direction.is_some() {
1634                    self.split_entry(entry.id, allow_preview, split_direction, cx);
1635                } else {
1636                    self.open_entry(entry.id, focus_opened_item, allow_preview, cx);
1637                }
1638                cx.notify();
1639            } else {
1640                self.toggle_expanded(entry.id, window, cx);
1641            }
1642        }
1643    }
1644
1645    fn populate_validation_error(&mut self, cx: &mut Context<Self>) {
1646        let edit_state = match self.state.edit_state.as_mut() {
1647            Some(state) => state,
1648            None => return,
1649        };
1650        let filename = self.filename_editor.read(cx).text(cx);
1651        if !filename.is_empty() {
1652            if filename.is_empty() {
1653                edit_state.validation_state =
1654                    ValidationState::Error("File or directory name cannot be empty.".to_string());
1655                cx.notify();
1656                return;
1657            }
1658
1659            let trimmed_filename = filename.trim();
1660            if trimmed_filename != filename {
1661                edit_state.validation_state = ValidationState::Warning(
1662                    "File or directory name contains leading or trailing whitespace.".to_string(),
1663                );
1664                cx.notify();
1665                return;
1666            }
1667            let trimmed_filename = trimmed_filename.trim_start_matches('/');
1668
1669            let Ok(filename) = RelPath::unix(trimmed_filename) else {
1670                edit_state.validation_state = ValidationState::Warning(
1671                    "File or directory name contains leading or trailing whitespace.".to_string(),
1672                );
1673                cx.notify();
1674                return;
1675            };
1676
1677            if let Some(worktree) = self
1678                .project
1679                .read(cx)
1680                .worktree_for_id(edit_state.worktree_id, cx)
1681                && let Some(entry) = worktree.read(cx).entry_for_id(edit_state.entry_id)
1682            {
1683                let mut already_exists = false;
1684                if edit_state.is_new_entry() {
1685                    let new_path = entry.path.join(filename);
1686                    if worktree.read(cx).entry_for_path(&new_path).is_some() {
1687                        already_exists = true;
1688                    }
1689                } else {
1690                    let new_path = if let Some(parent) = entry.path.clone().parent() {
1691                        parent.join(&filename)
1692                    } else {
1693                        filename.into()
1694                    };
1695                    if let Some(existing) = worktree.read(cx).entry_for_path(&new_path)
1696                        && existing.id != entry.id
1697                    {
1698                        already_exists = true;
1699                    }
1700                };
1701                if already_exists {
1702                    edit_state.validation_state = ValidationState::Error(format!(
1703                        "File or directory '{}' already exists at location. Please choose a different name.",
1704                        filename.as_unix_str()
1705                    ));
1706                    cx.notify();
1707                    return;
1708                }
1709            }
1710        }
1711        edit_state.validation_state = ValidationState::None;
1712        cx.notify();
1713    }
1714
1715    fn confirm_edit(
1716        &mut self,
1717        refocus: bool,
1718        window: &mut Window,
1719        cx: &mut Context<Self>,
1720    ) -> Option<Task<Result<()>>> {
1721        let edit_state = self.state.edit_state.as_mut()?;
1722        let worktree_id = edit_state.worktree_id;
1723        let is_new_entry = edit_state.is_new_entry();
1724        let mut filename = self.filename_editor.read(cx).text(cx);
1725        let path_style = self.project.read(cx).path_style(cx);
1726        if path_style.is_windows() {
1727            // on windows, trailing dots are ignored in paths
1728            // this can cause project panel to create a new entry with a trailing dot
1729            // while the actual one without the dot gets populated by the file watcher
1730            while let Some(trimmed) = filename.strip_suffix('.') {
1731                filename = trimmed.to_string();
1732            }
1733        }
1734        if filename.trim().is_empty() {
1735            return None;
1736        }
1737
1738        let filename_indicates_dir = if path_style.is_windows() {
1739            filename.ends_with('/') || filename.ends_with('\\')
1740        } else {
1741            filename.ends_with('/')
1742        };
1743        let filename = if path_style.is_windows() {
1744            filename.trim_start_matches(&['/', '\\'])
1745        } else {
1746            filename.trim_start_matches('/')
1747        };
1748        let filename = RelPath::new(filename.as_ref(), path_style).ok()?.into_arc();
1749
1750        edit_state.is_dir =
1751            edit_state.is_dir || (edit_state.is_new_entry() && filename_indicates_dir);
1752        let is_dir = edit_state.is_dir;
1753        let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?;
1754        let entry = worktree.read(cx).entry_for_id(edit_state.entry_id)?.clone();
1755
1756        let edit_task;
1757        let edited_entry_id;
1758        let edited_entry;
1759        let new_project_path: ProjectPath;
1760        if is_new_entry {
1761            self.selection = Some(SelectedEntry {
1762                worktree_id,
1763                entry_id: NEW_ENTRY_ID,
1764            });
1765            let new_path = entry.path.join(&filename);
1766            if worktree.read(cx).entry_for_path(&new_path).is_some() {
1767                return None;
1768            }
1769
1770            edited_entry = None;
1771            edited_entry_id = NEW_ENTRY_ID;
1772            new_project_path = (worktree_id, new_path).into();
1773            edit_task = self.project.update(cx, |project, cx| {
1774                project.create_entry(new_project_path.clone(), is_dir, cx)
1775            });
1776        } else {
1777            let new_path = if let Some(parent) = entry.path.parent() {
1778                parent.join(&filename)
1779            } else {
1780                filename.clone()
1781            };
1782            if let Some(existing) = worktree.read(cx).entry_for_path(&new_path) {
1783                if existing.id == entry.id && refocus {
1784                    window.focus(&self.focus_handle, cx);
1785                }
1786                return None;
1787            }
1788            edited_entry_id = entry.id;
1789            edited_entry = Some(entry);
1790            new_project_path = (worktree_id, new_path).into();
1791            edit_task = self.project.update(cx, |project, cx| {
1792                project.rename_entry(edited_entry_id, new_project_path.clone(), cx)
1793            })
1794        };
1795
1796        if refocus {
1797            window.focus(&self.focus_handle, cx);
1798        }
1799        edit_state.processing_filename = Some(filename);
1800        cx.notify();
1801
1802        Some(cx.spawn_in(window, async move |project_panel, cx| {
1803            let new_entry = edit_task.await;
1804            project_panel.update(cx, |project_panel, cx| {
1805                project_panel.state.edit_state = None;
1806
1807                // Record the operation if the edit was applied
1808                if new_entry.is_ok() {
1809                    let operation = if let Some(old_entry) = edited_entry {
1810                        ProjectPanelOperation::Rename {
1811                            old_path: (worktree_id, old_entry.path).into(),
1812                            new_path: new_project_path,
1813                        }
1814                    } else {
1815                        ProjectPanelOperation::Create {
1816                            project_path: new_project_path,
1817                        }
1818                    };
1819                    project_panel.undo_manager.record(operation);
1820                }
1821
1822                cx.notify();
1823            })?;
1824
1825            match new_entry {
1826                Err(e) => {
1827                    project_panel
1828                        .update_in(cx, |project_panel, window, cx| {
1829                            project_panel.marked_entries.clear();
1830                            project_panel.update_visible_entries(None, false, false, window, cx);
1831                        })
1832                        .ok();
1833                    Err(e)?;
1834                }
1835                Ok(CreatedEntry::Included(new_entry)) => {
1836                    project_panel.update_in(cx, |project_panel, window, cx| {
1837                        if let Some(selection) = &mut project_panel.selection
1838                            && selection.entry_id == edited_entry_id
1839                        {
1840                            selection.worktree_id = worktree_id;
1841                            selection.entry_id = new_entry.id;
1842                            project_panel.marked_entries.clear();
1843                            project_panel.expand_to_selection(cx);
1844                        }
1845                        project_panel.update_visible_entries(None, false, false, window, cx);
1846                        if is_new_entry && !is_dir {
1847                            let settings = ProjectPanelSettings::get_global(cx);
1848                            if settings.auto_open.should_open_on_create() {
1849                                project_panel.open_entry(new_entry.id, true, false, cx);
1850                            }
1851                        }
1852                        cx.notify();
1853                    })?;
1854                }
1855                Ok(CreatedEntry::Excluded { abs_path }) => {
1856                    if let Some(open_task) = project_panel
1857                        .update_in(cx, |project_panel, window, cx| {
1858                            project_panel.marked_entries.clear();
1859                            project_panel.update_visible_entries(None, false, false, window, cx);
1860
1861                            if is_dir {
1862                                project_panel.project.update(cx, |_, cx| {
1863                                    cx.emit(project::Event::Toast {
1864                                        notification_id: "excluded-directory".into(),
1865                                        message: format!(
1866                                            concat!(
1867                                                "Created an excluded directory at {:?}.\n",
1868                                                "Alter `file_scan_exclusions` in the settings ",
1869                                                "to show it in the panel"
1870                                            ),
1871                                            abs_path
1872                                        ),
1873                                        link: None,
1874                                    })
1875                                });
1876                                None
1877                            } else {
1878                                project_panel
1879                                    .workspace
1880                                    .update(cx, |workspace, cx| {
1881                                        workspace.open_abs_path(
1882                                            abs_path,
1883                                            OpenOptions {
1884                                                visible: Some(OpenVisible::All),
1885                                                ..Default::default()
1886                                            },
1887                                            window,
1888                                            cx,
1889                                        )
1890                                    })
1891                                    .ok()
1892                            }
1893                        })
1894                        .ok()
1895                        .flatten()
1896                    {
1897                        let _ = open_task.await?;
1898                    }
1899                }
1900            }
1901            Ok(())
1902        }))
1903    }
1904
1905    fn discard_edit_state(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1906        if let Some(edit_state) = self.state.edit_state.take() {
1907            self.state.temporarily_unfolded_pending_state = edit_state
1908                .temporarily_unfolded
1909                .and_then(|temporarily_unfolded_entry_id| {
1910                    let previously_focused_leaf_entry = edit_state.previously_focused?;
1911                    let folded_ancestors =
1912                        self.state.ancestors.get(&temporarily_unfolded_entry_id)?;
1913                    Some(TemporaryUnfoldedPendingState {
1914                        previously_focused_leaf_entry,
1915                        temporarily_unfolded_active_entry_id: folded_ancestors
1916                            .active_ancestor()
1917                            .unwrap_or(temporarily_unfolded_entry_id),
1918                    })
1919                });
1920            let previously_focused = edit_state
1921                .previously_focused
1922                .map(|entry| (entry.worktree_id, entry.entry_id));
1923            self.update_visible_entries(
1924                previously_focused,
1925                false,
1926                previously_focused.is_some(),
1927                window,
1928                cx,
1929            );
1930        }
1931    }
1932
1933    fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) {
1934        if cx.stop_active_drag(window) {
1935            self.drag_target_entry.take();
1936            self.hover_expand_task.take();
1937            return;
1938        }
1939        self.marked_entries.clear();
1940        cx.notify();
1941        self.discard_edit_state(window, cx);
1942        window.focus(&self.focus_handle, cx);
1943    }
1944
1945    fn open_entry(
1946        &mut self,
1947        entry_id: ProjectEntryId,
1948        focus_opened_item: bool,
1949        allow_preview: bool,
1950
1951        cx: &mut Context<Self>,
1952    ) {
1953        cx.emit(Event::OpenedEntry {
1954            entry_id,
1955            focus_opened_item,
1956            allow_preview,
1957        });
1958    }
1959
1960    fn split_entry(
1961        &mut self,
1962        entry_id: ProjectEntryId,
1963        allow_preview: bool,
1964        split_direction: Option<SplitDirection>,
1965
1966        cx: &mut Context<Self>,
1967    ) {
1968        cx.emit(Event::SplitEntry {
1969            entry_id,
1970            allow_preview,
1971            split_direction,
1972        });
1973    }
1974
1975    fn new_file(&mut self, _: &NewFile, window: &mut Window, cx: &mut Context<Self>) {
1976        self.add_entry(false, window, cx)
1977    }
1978
1979    fn new_directory(&mut self, _: &NewDirectory, window: &mut Window, cx: &mut Context<Self>) {
1980        self.add_entry(true, window, cx)
1981    }
1982
1983    fn add_entry(&mut self, is_dir: bool, window: &mut Window, cx: &mut Context<Self>) {
1984        let Some((worktree_id, entry_id)) = self
1985            .selection
1986            .map(|entry| (entry.worktree_id, entry.entry_id))
1987            .or_else(|| {
1988                let entry_id = self.state.last_worktree_root_id?;
1989                let worktree_id = self
1990                    .project
1991                    .read(cx)
1992                    .worktree_for_entry(entry_id, cx)?
1993                    .read(cx)
1994                    .id();
1995
1996                self.selection = Some(SelectedEntry {
1997                    worktree_id,
1998                    entry_id,
1999                });
2000
2001                Some((worktree_id, entry_id))
2002            })
2003        else {
2004            return;
2005        };
2006
2007        let directory_id;
2008        let new_entry_id = self.resolve_entry(entry_id);
2009        if let Some((worktree, expanded_dir_ids)) = self
2010            .project
2011            .read(cx)
2012            .worktree_for_id(worktree_id, cx)
2013            .zip(self.state.expanded_dir_ids.get_mut(&worktree_id))
2014        {
2015            let worktree = worktree.read(cx);
2016            if let Some(mut entry) = worktree.entry_for_id(new_entry_id) {
2017                loop {
2018                    if entry.is_dir() {
2019                        if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) {
2020                            expanded_dir_ids.insert(ix, entry.id);
2021                        }
2022                        directory_id = entry.id;
2023                        break;
2024                    } else {
2025                        if let Some(parent_path) = entry.path.parent()
2026                            && let Some(parent_entry) = worktree.entry_for_path(parent_path)
2027                        {
2028                            entry = parent_entry;
2029                            continue;
2030                        }
2031                        return;
2032                    }
2033                }
2034            } else {
2035                return;
2036            };
2037        } else {
2038            return;
2039        };
2040
2041        self.marked_entries.clear();
2042        self.state.edit_state = Some(EditState {
2043            worktree_id,
2044            entry_id: directory_id,
2045            leaf_entry_id: None,
2046            is_dir,
2047            processing_filename: None,
2048            previously_focused: self.selection,
2049            depth: 0,
2050            validation_state: ValidationState::None,
2051            temporarily_unfolded: (new_entry_id != entry_id).then_some(new_entry_id),
2052        });
2053        self.filename_editor.update(cx, |editor, cx| {
2054            editor.clear(window, cx);
2055        });
2056        self.update_visible_entries(Some((worktree_id, NEW_ENTRY_ID)), true, true, window, cx);
2057        cx.notify();
2058    }
2059
2060    fn unflatten_entry_id(&self, leaf_entry_id: ProjectEntryId) -> ProjectEntryId {
2061        if let Some(ancestors) = self.state.ancestors.get(&leaf_entry_id) {
2062            ancestors
2063                .ancestors
2064                .get(ancestors.current_ancestor_depth)
2065                .copied()
2066                .unwrap_or(leaf_entry_id)
2067        } else {
2068            leaf_entry_id
2069        }
2070    }
2071
2072    pub fn undo(&mut self, _: &Undo, _window: &mut Window, cx: &mut Context<Self>) {
2073        self.undo_manager.undo(cx);
2074        cx.notify();
2075    }
2076
2077    fn rename_impl(
2078        &mut self,
2079        selection: Option<Range<usize>>,
2080        window: &mut Window,
2081        cx: &mut Context<Self>,
2082    ) {
2083        if let Some(SelectedEntry {
2084            worktree_id,
2085            entry_id,
2086        }) = self.selection
2087            && let Some(worktree) = self.project.read(cx).worktree_for_id(worktree_id, cx)
2088        {
2089            let sub_entry_id = self.unflatten_entry_id(entry_id);
2090            if let Some(entry) = worktree.read(cx).entry_for_id(sub_entry_id) {
2091                #[cfg(target_os = "windows")]
2092                if Some(entry) == worktree.read(cx).root_entry() {
2093                    return;
2094                }
2095
2096                if Some(entry) == worktree.read(cx).root_entry() {
2097                    let settings = ProjectPanelSettings::get_global(cx);
2098                    let visible_worktrees_count =
2099                        self.project.read(cx).visible_worktrees(cx).count();
2100                    if settings.hide_root && visible_worktrees_count == 1 {
2101                        return;
2102                    }
2103                }
2104
2105                self.state.edit_state = Some(EditState {
2106                    worktree_id,
2107                    entry_id: sub_entry_id,
2108                    leaf_entry_id: Some(entry_id),
2109                    is_dir: entry.is_dir(),
2110                    processing_filename: None,
2111                    previously_focused: None,
2112                    depth: 0,
2113                    validation_state: ValidationState::None,
2114                    temporarily_unfolded: None,
2115                });
2116                let file_name = entry.path.file_name().unwrap_or_default().to_string();
2117                let selection = selection.unwrap_or_else(|| {
2118                    let file_stem = entry.path.file_stem().map(|s| s.to_string());
2119                    let selection_end =
2120                        file_stem.map_or(file_name.len(), |file_stem| file_stem.len());
2121                    0..selection_end
2122                });
2123                self.filename_editor.update(cx, |editor, cx| {
2124                    editor.set_text(file_name, window, cx);
2125                    editor.change_selections(Default::default(), window, cx, |s| {
2126                        s.select_ranges([
2127                            MultiBufferOffset(selection.start)..MultiBufferOffset(selection.end)
2128                        ])
2129                    });
2130                });
2131                self.update_visible_entries(None, true, true, window, cx);
2132                cx.notify();
2133            }
2134        }
2135    }
2136
2137    fn rename(&mut self, _: &Rename, window: &mut Window, cx: &mut Context<Self>) {
2138        self.rename_impl(None, window, cx);
2139    }
2140
2141    fn trash(&mut self, action: &Trash, window: &mut Window, cx: &mut Context<Self>) {
2142        self.remove(true, action.skip_prompt, window, cx);
2143    }
2144
2145    fn delete(&mut self, action: &Delete, window: &mut Window, cx: &mut Context<Self>) {
2146        self.remove(false, action.skip_prompt, window, cx);
2147    }
2148
2149    fn restore_file(
2150        &mut self,
2151        action: &git::RestoreFile,
2152        window: &mut Window,
2153        cx: &mut Context<Self>,
2154    ) {
2155        maybe!({
2156            let selection = self.selection?;
2157            let project = self.project.read(cx);
2158
2159            let (_worktree, entry) = self.selected_sub_entry(cx)?;
2160            if entry.is_dir() {
2161                return None;
2162            }
2163
2164            let project_path = project.path_for_entry(selection.entry_id, cx)?;
2165
2166            let git_store = project.git_store();
2167            let (repository, repo_path) = git_store
2168                .read(cx)
2169                .repository_and_path_for_project_path(&project_path, cx)?;
2170
2171            let snapshot = repository.read(cx).snapshot();
2172            let status = snapshot.status_for_path(&repo_path)?;
2173            if !status.status.is_modified() && !status.status.is_deleted() {
2174                return None;
2175            }
2176
2177            let file_name = entry.path.file_name()?.to_string();
2178
2179            let answer = if !action.skip_prompt {
2180                let prompt = format!("Discard changes to {}?", file_name);
2181                Some(window.prompt(PromptLevel::Info, &prompt, None, &["Restore", "Cancel"], cx))
2182            } else {
2183                None
2184            };
2185
2186            cx.spawn_in(window, async move |panel, cx| {
2187                if let Some(answer) = answer
2188                    && answer.await != Ok(0)
2189                {
2190                    return anyhow::Ok(());
2191                }
2192
2193                let task = panel.update(cx, |_panel, cx| {
2194                    repository.update(cx, |repo, cx| {
2195                        repo.checkout_files("HEAD", vec![repo_path], cx)
2196                    })
2197                })?;
2198
2199                if let Err(e) = task.await {
2200                    panel
2201                        .update(cx, |panel, cx| {
2202                            let message = format!("Failed to restore {}: {}", file_name, e);
2203                            let toast = StatusToast::new(message, cx, |this, _| {
2204                                this.icon(ToastIcon::new(IconName::XCircle).color(Color::Error))
2205                                    .dismiss_button(true)
2206                            });
2207                            panel
2208                                .workspace
2209                                .update(cx, |workspace, cx| {
2210                                    workspace.toggle_status_toast(toast, cx);
2211                                })
2212                                .ok();
2213                        })
2214                        .ok();
2215                }
2216
2217                panel
2218                    .update(cx, |panel, cx| {
2219                        panel.project.update(cx, |project, cx| {
2220                            if let Some(buffer_id) = project
2221                                .buffer_store()
2222                                .read(cx)
2223                                .buffer_id_for_project_path(&project_path)
2224                            {
2225                                if let Some(buffer) = project.buffer_for_id(*buffer_id, cx) {
2226                                    buffer.update(cx, |buffer, cx| {
2227                                        let _ = buffer.reload(cx);
2228                                    });
2229                                }
2230                            }
2231                        })
2232                    })
2233                    .ok();
2234
2235                anyhow::Ok(())
2236            })
2237            .detach_and_log_err(cx);
2238
2239            Some(())
2240        });
2241    }
2242
2243    fn remove(
2244        &mut self,
2245        trash: bool,
2246        skip_prompt: bool,
2247        window: &mut Window,
2248        cx: &mut Context<ProjectPanel>,
2249    ) {
2250        maybe!({
2251            let items_to_delete = self.disjoint_effective_entries(cx);
2252            if items_to_delete.is_empty() {
2253                return None;
2254            }
2255            let project = self.project.read(cx);
2256
2257            let mut dirty_buffers = 0;
2258            let file_paths = items_to_delete
2259                .iter()
2260                .filter_map(|selection| {
2261                    let project_path = project.path_for_entry(selection.entry_id, cx)?;
2262                    dirty_buffers +=
2263                        project.dirty_buffers(cx).any(|path| path == project_path) as usize;
2264
2265                    Some((
2266                        selection.entry_id,
2267                        project_path.path.file_name()?.to_string(),
2268                    ))
2269                })
2270                .collect::<Vec<_>>();
2271            if file_paths.is_empty() {
2272                return None;
2273            }
2274            let answer = if !skip_prompt {
2275                let operation = if trash { "Trash" } else { "Delete" };
2276                let message_start = if trash {
2277                    "Do you want to trash"
2278                } else {
2279                    "Are you sure you want to permanently delete"
2280                };
2281                let prompt = match file_paths.first() {
2282                    Some((_, path)) if file_paths.len() == 1 => {
2283                        let unsaved_warning = if dirty_buffers > 0 {
2284                            "\n\nIt has unsaved changes, which will be lost."
2285                        } else {
2286                            ""
2287                        };
2288
2289                        format!("{message_start} {path}?{unsaved_warning}")
2290                    }
2291                    _ => {
2292                        const CUTOFF_POINT: usize = 10;
2293                        let names = if file_paths.len() > CUTOFF_POINT {
2294                            let truncated_path_counts = file_paths.len() - CUTOFF_POINT;
2295                            let mut paths = file_paths
2296                                .iter()
2297                                .map(|(_, path)| path.clone())
2298                                .take(CUTOFF_POINT)
2299                                .collect::<Vec<_>>();
2300                            paths.truncate(CUTOFF_POINT);
2301                            if truncated_path_counts == 1 {
2302                                paths.push(".. 1 file not shown".into());
2303                            } else {
2304                                paths.push(format!(".. {} files not shown", truncated_path_counts));
2305                            }
2306                            paths
2307                        } else {
2308                            file_paths.iter().map(|(_, path)| path.clone()).collect()
2309                        };
2310                        let unsaved_warning = if dirty_buffers == 0 {
2311                            String::new()
2312                        } else if dirty_buffers == 1 {
2313                            "\n\n1 of these has unsaved changes, which will be lost.".to_string()
2314                        } else {
2315                            format!(
2316                                "\n\n{dirty_buffers} of these have unsaved changes, which will be lost."
2317                            )
2318                        };
2319
2320                        format!(
2321                            "{message_start} the following {} files?\n{}{unsaved_warning}",
2322                            file_paths.len(),
2323                            names.join("\n")
2324                        )
2325                    }
2326                };
2327                let detail = (!trash).then_some("This cannot be undone.");
2328                Some(window.prompt(
2329                    PromptLevel::Info,
2330                    &prompt,
2331                    detail,
2332                    &[operation, "Cancel"],
2333                    cx,
2334                ))
2335            } else {
2336                None
2337            };
2338            let next_selection = self.find_next_selection_after_deletion(items_to_delete, cx);
2339            cx.spawn_in(window, async move |panel, cx| {
2340                if let Some(answer) = answer
2341                    && answer.await != Ok(0)
2342                {
2343                    return anyhow::Ok(());
2344                }
2345                for (entry_id, _) in file_paths {
2346                    panel
2347                        .update(cx, |panel, cx| {
2348                            panel
2349                                .project
2350                                .update(cx, |project, cx| project.delete_entry(entry_id, trash, cx))
2351                                .context("no such entry")
2352                        })??
2353                        .await?;
2354                }
2355                panel.update_in(cx, |panel, window, cx| {
2356                    if let Some(next_selection) = next_selection {
2357                        panel.update_visible_entries(
2358                            Some((next_selection.worktree_id, next_selection.entry_id)),
2359                            false,
2360                            true,
2361                            window,
2362                            cx,
2363                        );
2364                    } else {
2365                        panel.select_last(&SelectLast {}, window, cx);
2366                    }
2367                })?;
2368                Ok(())
2369            })
2370            .detach_and_log_err(cx);
2371            Some(())
2372        });
2373    }
2374
2375    fn find_next_selection_after_deletion(
2376        &self,
2377        sanitized_entries: BTreeSet<SelectedEntry>,
2378        cx: &mut Context<Self>,
2379    ) -> Option<SelectedEntry> {
2380        if sanitized_entries.is_empty() {
2381            return None;
2382        }
2383        let project = self.project.read(cx);
2384        let (worktree_id, worktree) = sanitized_entries
2385            .iter()
2386            .map(|entry| entry.worktree_id)
2387            .filter_map(|id| project.worktree_for_id(id, cx).map(|w| (id, w.read(cx))))
2388            .max_by(|(_, a), (_, b)| a.root_name().cmp(b.root_name()))?;
2389        let git_store = project.git_store().read(cx);
2390
2391        let marked_entries_in_worktree = sanitized_entries
2392            .iter()
2393            .filter(|e| e.worktree_id == worktree_id)
2394            .collect::<HashSet<_>>();
2395        let latest_entry = marked_entries_in_worktree
2396            .iter()
2397            .max_by(|a, b| {
2398                match (
2399                    worktree.entry_for_id(a.entry_id),
2400                    worktree.entry_for_id(b.entry_id),
2401                ) {
2402                    (Some(a), Some(b)) => compare_paths(
2403                        (a.path.as_std_path(), a.is_file()),
2404                        (b.path.as_std_path(), b.is_file()),
2405                    ),
2406                    _ => cmp::Ordering::Equal,
2407                }
2408            })
2409            .and_then(|e| worktree.entry_for_id(e.entry_id))?;
2410
2411        let parent_path = latest_entry.path.parent()?;
2412        let parent_entry = worktree.entry_for_path(parent_path)?;
2413
2414        // Remove all siblings that are being deleted except the last marked entry
2415        let repo_snapshots = git_store.repo_snapshots(cx);
2416        let worktree_snapshot = worktree.snapshot();
2417        let hide_gitignore = ProjectPanelSettings::get_global(cx).hide_gitignore;
2418        let mut siblings: Vec<_> =
2419            ChildEntriesGitIter::new(&repo_snapshots, &worktree_snapshot, parent_path)
2420                .filter(|sibling| {
2421                    (sibling.id == latest_entry.id)
2422                        || (!marked_entries_in_worktree.contains(&&SelectedEntry {
2423                            worktree_id,
2424                            entry_id: sibling.id,
2425                        }) && (!hide_gitignore || !sibling.is_ignored))
2426                })
2427                .map(|entry| entry.to_owned())
2428                .collect();
2429
2430        let mode = ProjectPanelSettings::get_global(cx).sort_mode;
2431        sort_worktree_entries_with_mode(&mut siblings, mode);
2432        let sibling_entry_index = siblings
2433            .iter()
2434            .position(|sibling| sibling.id == latest_entry.id)?;
2435
2436        if let Some(next_sibling) = sibling_entry_index
2437            .checked_add(1)
2438            .and_then(|i| siblings.get(i))
2439        {
2440            return Some(SelectedEntry {
2441                worktree_id,
2442                entry_id: next_sibling.id,
2443            });
2444        }
2445        if let Some(prev_sibling) = sibling_entry_index
2446            .checked_sub(1)
2447            .and_then(|i| siblings.get(i))
2448        {
2449            return Some(SelectedEntry {
2450                worktree_id,
2451                entry_id: prev_sibling.id,
2452            });
2453        }
2454        // No neighbour sibling found, fall back to parent
2455        Some(SelectedEntry {
2456            worktree_id,
2457            entry_id: parent_entry.id,
2458        })
2459    }
2460
2461    fn unfold_directory(
2462        &mut self,
2463        _: &UnfoldDirectory,
2464        window: &mut Window,
2465        cx: &mut Context<Self>,
2466    ) {
2467        if let Some((worktree, entry)) = self.selected_entry(cx) {
2468            self.state.unfolded_dir_ids.insert(entry.id);
2469
2470            let snapshot = worktree.snapshot();
2471            let mut parent_path = entry.path.parent();
2472            while let Some(path) = parent_path {
2473                if let Some(parent_entry) = worktree.entry_for_path(path) {
2474                    let mut children_iter = snapshot.child_entries(path);
2475
2476                    if children_iter.by_ref().take(2).count() > 1 {
2477                        break;
2478                    }
2479
2480                    self.state.unfolded_dir_ids.insert(parent_entry.id);
2481                    parent_path = path.parent();
2482                } else {
2483                    break;
2484                }
2485            }
2486
2487            self.update_visible_entries(None, false, true, window, cx);
2488            cx.notify();
2489        }
2490    }
2491
2492    fn fold_directory(&mut self, _: &FoldDirectory, window: &mut Window, cx: &mut Context<Self>) {
2493        if let Some((worktree, entry)) = self.selected_entry(cx) {
2494            self.state.unfolded_dir_ids.remove(&entry.id);
2495
2496            let snapshot = worktree.snapshot();
2497            let mut path = &*entry.path;
2498            loop {
2499                let mut child_entries_iter = snapshot.child_entries(path);
2500                if let Some(child) = child_entries_iter.next() {
2501                    if child_entries_iter.next().is_none() && child.is_dir() {
2502                        self.state.unfolded_dir_ids.remove(&child.id);
2503                        path = &*child.path;
2504                    } else {
2505                        break;
2506                    }
2507                } else {
2508                    break;
2509                }
2510            }
2511
2512            self.update_visible_entries(None, false, true, window, cx);
2513            cx.notify();
2514        }
2515    }
2516
2517    fn scroll_up(&mut self, _: &ScrollUp, window: &mut Window, cx: &mut Context<Self>) {
2518        for _ in 0..self.rendered_entries_len / 2 {
2519            window.dispatch_action(SelectPrevious.boxed_clone(), cx);
2520        }
2521    }
2522
2523    fn scroll_down(&mut self, _: &ScrollDown, window: &mut Window, cx: &mut Context<Self>) {
2524        for _ in 0..self.rendered_entries_len / 2 {
2525            window.dispatch_action(SelectNext.boxed_clone(), cx);
2526        }
2527    }
2528
2529    fn scroll_cursor_center(
2530        &mut self,
2531        _: &ScrollCursorCenter,
2532        _: &mut Window,
2533        cx: &mut Context<Self>,
2534    ) {
2535        if let Some((_, _, index)) = self.selection.and_then(|s| self.index_for_selection(s)) {
2536            self.scroll_handle
2537                .scroll_to_item_strict(index, ScrollStrategy::Center);
2538            cx.notify();
2539        }
2540    }
2541
2542    fn scroll_cursor_top(&mut self, _: &ScrollCursorTop, _: &mut Window, cx: &mut Context<Self>) {
2543        if let Some((_, _, index)) = self.selection.and_then(|s| self.index_for_selection(s)) {
2544            self.scroll_handle
2545                .scroll_to_item_strict(index, ScrollStrategy::Top);
2546            cx.notify();
2547        }
2548    }
2549
2550    fn scroll_cursor_bottom(
2551        &mut self,
2552        _: &ScrollCursorBottom,
2553        _: &mut Window,
2554        cx: &mut Context<Self>,
2555    ) {
2556        if let Some((_, _, index)) = self.selection.and_then(|s| self.index_for_selection(s)) {
2557            self.scroll_handle
2558                .scroll_to_item_strict(index, ScrollStrategy::Bottom);
2559            cx.notify();
2560        }
2561    }
2562
2563    fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
2564        if let Some(edit_state) = &self.state.edit_state
2565            && edit_state.processing_filename.is_none()
2566        {
2567            self.filename_editor.update(cx, |editor, cx| {
2568                editor.move_to_end_of_line(
2569                    &editor::actions::MoveToEndOfLine {
2570                        stop_at_soft_wraps: false,
2571                    },
2572                    window,
2573                    cx,
2574                );
2575            });
2576            return;
2577        }
2578        if let Some(selection) = self.selection {
2579            let (mut worktree_ix, mut entry_ix, _) =
2580                self.index_for_selection(selection).unwrap_or_default();
2581            if let Some(worktree_entries) = self
2582                .state
2583                .visible_entries
2584                .get(worktree_ix)
2585                .map(|v| &v.entries)
2586            {
2587                if entry_ix + 1 < worktree_entries.len() {
2588                    entry_ix += 1;
2589                } else {
2590                    worktree_ix += 1;
2591                    entry_ix = 0;
2592                }
2593            }
2594
2595            if let Some(VisibleEntriesForWorktree {
2596                worktree_id,
2597                entries,
2598                ..
2599            }) = self.state.visible_entries.get(worktree_ix)
2600                && let Some(entry) = entries.get(entry_ix)
2601            {
2602                let selection = SelectedEntry {
2603                    worktree_id: *worktree_id,
2604                    entry_id: entry.id,
2605                };
2606                self.selection = Some(selection);
2607                if window.modifiers().shift {
2608                    self.marked_entries.push(selection);
2609                }
2610
2611                self.autoscroll(cx);
2612                cx.notify();
2613            }
2614        } else {
2615            self.select_first(&SelectFirst {}, window, cx);
2616        }
2617    }
2618
2619    fn select_prev_diagnostic(
2620        &mut self,
2621        action: &SelectPrevDiagnostic,
2622        window: &mut Window,
2623        cx: &mut Context<Self>,
2624    ) {
2625        let selection = self.find_entry(
2626            self.selection.as_ref(),
2627            true,
2628            &|entry: GitEntryRef, worktree_id: WorktreeId| {
2629                self.selection.is_none_or(|selection| {
2630                    if selection.worktree_id == worktree_id {
2631                        selection.entry_id != entry.id
2632                    } else {
2633                        true
2634                    }
2635                }) && entry.is_file()
2636                    && self
2637                        .diagnostics
2638                        .get(&(worktree_id, entry.path.clone()))
2639                        .is_some_and(|severity| action.severity.matches(*severity))
2640            },
2641            cx,
2642        );
2643
2644        if let Some(selection) = selection {
2645            self.selection = Some(selection);
2646            self.expand_entry(selection.worktree_id, selection.entry_id, cx);
2647            self.update_visible_entries(
2648                Some((selection.worktree_id, selection.entry_id)),
2649                false,
2650                true,
2651                window,
2652                cx,
2653            );
2654            cx.notify();
2655        }
2656    }
2657
2658    fn select_next_diagnostic(
2659        &mut self,
2660        action: &SelectNextDiagnostic,
2661        window: &mut Window,
2662        cx: &mut Context<Self>,
2663    ) {
2664        let selection = self.find_entry(
2665            self.selection.as_ref(),
2666            false,
2667            &|entry: GitEntryRef, worktree_id: WorktreeId| {
2668                self.selection.is_none_or(|selection| {
2669                    if selection.worktree_id == worktree_id {
2670                        selection.entry_id != entry.id
2671                    } else {
2672                        true
2673                    }
2674                }) && entry.is_file()
2675                    && self
2676                        .diagnostics
2677                        .get(&(worktree_id, entry.path.clone()))
2678                        .is_some_and(|severity| action.severity.matches(*severity))
2679            },
2680            cx,
2681        );
2682
2683        if let Some(selection) = selection {
2684            self.selection = Some(selection);
2685            self.expand_entry(selection.worktree_id, selection.entry_id, cx);
2686            self.update_visible_entries(
2687                Some((selection.worktree_id, selection.entry_id)),
2688                false,
2689                true,
2690                window,
2691                cx,
2692            );
2693            cx.notify();
2694        }
2695    }
2696
2697    fn select_prev_git_entry(
2698        &mut self,
2699        _: &SelectPrevGitEntry,
2700        window: &mut Window,
2701        cx: &mut Context<Self>,
2702    ) {
2703        let selection = self.find_entry(
2704            self.selection.as_ref(),
2705            true,
2706            &|entry: GitEntryRef, worktree_id: WorktreeId| {
2707                (self.selection.is_none()
2708                    || self.selection.is_some_and(|selection| {
2709                        if selection.worktree_id == worktree_id {
2710                            selection.entry_id != entry.id
2711                        } else {
2712                            true
2713                        }
2714                    }))
2715                    && entry.is_file()
2716                    && entry.git_summary.index.modified + entry.git_summary.worktree.modified > 0
2717            },
2718            cx,
2719        );
2720
2721        if let Some(selection) = selection {
2722            self.selection = Some(selection);
2723            self.expand_entry(selection.worktree_id, selection.entry_id, cx);
2724            self.update_visible_entries(
2725                Some((selection.worktree_id, selection.entry_id)),
2726                false,
2727                true,
2728                window,
2729                cx,
2730            );
2731            cx.notify();
2732        }
2733    }
2734
2735    fn select_prev_directory(
2736        &mut self,
2737        _: &SelectPrevDirectory,
2738        _: &mut Window,
2739        cx: &mut Context<Self>,
2740    ) {
2741        let selection = self.find_visible_entry(
2742            self.selection.as_ref(),
2743            true,
2744            &|entry: GitEntryRef, worktree_id: WorktreeId| {
2745                self.selection.is_none_or(|selection| {
2746                    if selection.worktree_id == worktree_id {
2747                        selection.entry_id != entry.id
2748                    } else {
2749                        true
2750                    }
2751                }) && entry.is_dir()
2752            },
2753            cx,
2754        );
2755
2756        if let Some(selection) = selection {
2757            self.selection = Some(selection);
2758            self.autoscroll(cx);
2759            cx.notify();
2760        }
2761    }
2762
2763    fn select_next_directory(
2764        &mut self,
2765        _: &SelectNextDirectory,
2766        _: &mut Window,
2767        cx: &mut Context<Self>,
2768    ) {
2769        let selection = self.find_visible_entry(
2770            self.selection.as_ref(),
2771            false,
2772            &|entry: GitEntryRef, worktree_id: WorktreeId| {
2773                self.selection.is_none_or(|selection| {
2774                    if selection.worktree_id == worktree_id {
2775                        selection.entry_id != entry.id
2776                    } else {
2777                        true
2778                    }
2779                }) && entry.is_dir()
2780            },
2781            cx,
2782        );
2783
2784        if let Some(selection) = selection {
2785            self.selection = Some(selection);
2786            self.autoscroll(cx);
2787            cx.notify();
2788        }
2789    }
2790
2791    fn select_next_git_entry(
2792        &mut self,
2793        _: &SelectNextGitEntry,
2794        window: &mut Window,
2795        cx: &mut Context<Self>,
2796    ) {
2797        let selection = self.find_entry(
2798            self.selection.as_ref(),
2799            false,
2800            &|entry: GitEntryRef, worktree_id: WorktreeId| {
2801                self.selection.is_none_or(|selection| {
2802                    if selection.worktree_id == worktree_id {
2803                        selection.entry_id != entry.id
2804                    } else {
2805                        true
2806                    }
2807                }) && entry.is_file()
2808                    && entry.git_summary.index.modified + entry.git_summary.worktree.modified > 0
2809            },
2810            cx,
2811        );
2812
2813        if let Some(selection) = selection {
2814            self.selection = Some(selection);
2815            self.expand_entry(selection.worktree_id, selection.entry_id, cx);
2816            self.update_visible_entries(
2817                Some((selection.worktree_id, selection.entry_id)),
2818                false,
2819                true,
2820                window,
2821                cx,
2822            );
2823            cx.notify();
2824        }
2825    }
2826
2827    fn select_parent(&mut self, _: &SelectParent, window: &mut Window, cx: &mut Context<Self>) {
2828        if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
2829            if let Some(parent) = entry.path.parent() {
2830                let worktree = worktree.read(cx);
2831                if let Some(parent_entry) = worktree.entry_for_path(parent) {
2832                    self.selection = Some(SelectedEntry {
2833                        worktree_id: worktree.id(),
2834                        entry_id: parent_entry.id,
2835                    });
2836                    self.autoscroll(cx);
2837                    cx.notify();
2838                }
2839            }
2840        } else {
2841            self.select_first(&SelectFirst {}, window, cx);
2842        }
2843    }
2844
2845    fn select_first(&mut self, _: &SelectFirst, window: &mut Window, cx: &mut Context<Self>) {
2846        if let Some(VisibleEntriesForWorktree {
2847            worktree_id,
2848            entries,
2849            ..
2850        }) = self.state.visible_entries.first()
2851            && let Some(entry) = entries.first()
2852        {
2853            let selection = SelectedEntry {
2854                worktree_id: *worktree_id,
2855                entry_id: entry.id,
2856            };
2857            self.selection = Some(selection);
2858            if window.modifiers().shift {
2859                self.marked_entries.push(selection);
2860            }
2861            self.autoscroll(cx);
2862            cx.notify();
2863        }
2864    }
2865
2866    fn select_last(&mut self, _: &SelectLast, _: &mut Window, cx: &mut Context<Self>) {
2867        if let Some(VisibleEntriesForWorktree {
2868            worktree_id,
2869            entries,
2870            ..
2871        }) = self.state.visible_entries.last()
2872        {
2873            let worktree = self.project.read(cx).worktree_for_id(*worktree_id, cx);
2874            if let (Some(worktree), Some(entry)) = (worktree, entries.last()) {
2875                let worktree = worktree.read(cx);
2876                if let Some(entry) = worktree.entry_for_id(entry.id) {
2877                    let selection = SelectedEntry {
2878                        worktree_id: *worktree_id,
2879                        entry_id: entry.id,
2880                    };
2881                    self.selection = Some(selection);
2882                    self.autoscroll(cx);
2883                    cx.notify();
2884                }
2885            }
2886        }
2887    }
2888
2889    fn autoscroll(&mut self, cx: &mut Context<Self>) {
2890        if let Some((_, _, index)) = self.selection.and_then(|s| self.index_for_selection(s)) {
2891            self.scroll_handle.scroll_to_item_with_offset(
2892                index,
2893                ScrollStrategy::Center,
2894                self.sticky_items_count,
2895            );
2896            cx.notify();
2897        }
2898    }
2899
2900    fn cut(&mut self, _: &Cut, _: &mut Window, cx: &mut Context<Self>) {
2901        let entries = self.disjoint_effective_entries(cx);
2902        if !entries.is_empty() {
2903            self.write_entries_to_system_clipboard(&entries, cx);
2904            self.clipboard = Some(ClipboardEntry::Cut(entries));
2905            cx.notify();
2906        }
2907    }
2908
2909    fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context<Self>) {
2910        let entries = self.disjoint_effective_entries(cx);
2911        if !entries.is_empty() {
2912            self.write_entries_to_system_clipboard(&entries, cx);
2913            self.clipboard = Some(ClipboardEntry::Copied(entries));
2914            cx.notify();
2915        }
2916    }
2917
2918    fn create_paste_path(
2919        &self,
2920        source: &SelectedEntry,
2921        (worktree, target_entry): (Entity<Worktree>, &Entry),
2922        cx: &App,
2923    ) -> Option<(Arc<RelPath>, Option<Range<usize>>)> {
2924        let mut new_path = target_entry.path.to_rel_path_buf();
2925        // If we're pasting into a file, or a directory into itself, go up one level.
2926        if target_entry.is_file() || (target_entry.is_dir() && target_entry.id == source.entry_id) {
2927            new_path.pop();
2928        }
2929
2930        let source_worktree = self
2931            .project
2932            .read(cx)
2933            .worktree_for_entry(source.entry_id, cx)?;
2934        let source_entry = source_worktree.read(cx).entry_for_id(source.entry_id)?;
2935
2936        let clipboard_entry_file_name = source_entry.path.file_name()?.to_string();
2937        new_path.push(RelPath::unix(&clipboard_entry_file_name).unwrap());
2938
2939        let (extension, file_name_without_extension) = if source_entry.is_file() {
2940            (
2941                new_path.extension().map(|s| s.to_string()),
2942                new_path.file_stem()?.to_string(),
2943            )
2944        } else {
2945            (None, clipboard_entry_file_name.clone())
2946        };
2947
2948        let file_name_len = file_name_without_extension.len();
2949        let mut disambiguation_range = None;
2950        let mut ix = 0;
2951        {
2952            let worktree = worktree.read(cx);
2953            while worktree.entry_for_path(&new_path).is_some() {
2954                new_path.pop();
2955
2956                let mut new_file_name = file_name_without_extension.to_string();
2957
2958                let disambiguation = " copy";
2959                let mut disambiguation_len = disambiguation.len();
2960
2961                new_file_name.push_str(disambiguation);
2962
2963                if ix > 0 {
2964                    let extra_disambiguation = format!(" {}", ix);
2965                    disambiguation_len += extra_disambiguation.len();
2966                    new_file_name.push_str(&extra_disambiguation);
2967                }
2968                if let Some(extension) = extension.as_ref() {
2969                    new_file_name.push_str(".");
2970                    new_file_name.push_str(extension);
2971                }
2972
2973                new_path.push(RelPath::unix(&new_file_name).unwrap());
2974
2975                disambiguation_range = Some(file_name_len..(file_name_len + disambiguation_len));
2976                ix += 1;
2977            }
2978        }
2979        Some((new_path.as_rel_path().into(), disambiguation_range))
2980    }
2981
2982    fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
2983        if let Some(external_paths) = self.external_paths_from_system_clipboard(cx) {
2984            let target_entry_id = self
2985                .selection
2986                .map(|s| s.entry_id)
2987                .or(self.state.last_worktree_root_id);
2988            if let Some(entry_id) = target_entry_id {
2989                self.drop_external_files(external_paths.paths(), entry_id, window, cx);
2990            }
2991            return;
2992        }
2993
2994        maybe!({
2995            let (worktree, entry) = self.selected_entry_handle(cx)?;
2996            let entry = entry.clone();
2997            let worktree_id = worktree.read(cx).id();
2998            let clipboard_entries = self
2999                .clipboard
3000                .as_ref()
3001                .filter(|clipboard| !clipboard.items().is_empty())?;
3002
3003            enum PasteTask {
3004                Rename {
3005                    task: Task<Result<CreatedEntry>>,
3006                    old_path: ProjectPath,
3007                    new_path: ProjectPath,
3008                },
3009                Copy {
3010                    task: Task<Result<Option<Entry>>>,
3011                    destination: ProjectPath,
3012                },
3013            }
3014
3015            let mut paste_tasks = Vec::new();
3016            let mut disambiguation_range = None;
3017            let clip_is_cut = clipboard_entries.is_cut();
3018            for clipboard_entry in clipboard_entries.items() {
3019                let (new_path, new_disambiguation_range) =
3020                    self.create_paste_path(clipboard_entry, self.selected_sub_entry(cx)?, cx)?;
3021                let clip_entry_id = clipboard_entry.entry_id;
3022                let destination: ProjectPath = (worktree_id, new_path).into();
3023                let task = if clipboard_entries.is_cut() {
3024                    let old_path = self.project.read(cx).path_for_entry(clip_entry_id, cx)?;
3025                    let task = self.project.update(cx, |project, cx| {
3026                        project.rename_entry(clip_entry_id, destination.clone(), cx)
3027                    });
3028                    PasteTask::Rename {
3029                        task,
3030                        old_path,
3031                        new_path: destination,
3032                    }
3033                } else {
3034                    let task = self.project.update(cx, |project, cx| {
3035                        project.copy_entry(clip_entry_id, destination.clone(), cx)
3036                    });
3037                    PasteTask::Copy { task, destination }
3038                };
3039                paste_tasks.push(task);
3040                disambiguation_range = new_disambiguation_range.or(disambiguation_range);
3041            }
3042
3043            let item_count = paste_tasks.len();
3044            let workspace = self.workspace.clone();
3045
3046            cx.spawn_in(window, async move |project_panel, mut cx| {
3047                let mut last_succeed = None;
3048                let mut operations = Vec::new();
3049
3050                for task in paste_tasks {
3051                    match task {
3052                        PasteTask::Rename {
3053                            task,
3054                            old_path,
3055                            new_path,
3056                        } => {
3057                            if let Some(CreatedEntry::Included(entry)) = task
3058                                .await
3059                                .notify_workspace_async_err(workspace.clone(), &mut cx)
3060                            {
3061                                operations
3062                                    .push(ProjectPanelOperation::Rename { old_path, new_path });
3063                                last_succeed = Some(entry);
3064                            }
3065                        }
3066                        PasteTask::Copy { task, destination } => {
3067                            if let Some(Some(entry)) = task
3068                                .await
3069                                .notify_workspace_async_err(workspace.clone(), &mut cx)
3070                            {
3071                                operations.push(ProjectPanelOperation::Create {
3072                                    project_path: destination,
3073                                });
3074                                last_succeed = Some(entry);
3075                            }
3076                        }
3077                    }
3078                }
3079
3080                project_panel
3081                    .update(cx, |this, _| {
3082                        this.undo_manager.record_batch(operations);
3083                    })
3084                    .ok();
3085
3086                // update selection
3087                if let Some(entry) = last_succeed {
3088                    project_panel
3089                        .update_in(cx, |project_panel, window, cx| {
3090                            project_panel.selection = Some(SelectedEntry {
3091                                worktree_id,
3092                                entry_id: entry.id,
3093                            });
3094
3095                            if item_count == 1 {
3096                                // open entry if not dir, setting is enabled, and only focus if rename is not pending
3097                                if !entry.is_dir() {
3098                                    let settings = ProjectPanelSettings::get_global(cx);
3099                                    if settings.auto_open.should_open_on_paste() {
3100                                        project_panel.open_entry(
3101                                            entry.id,
3102                                            disambiguation_range.is_none(),
3103                                            false,
3104                                            cx,
3105                                        );
3106                                    }
3107                                }
3108
3109                                // if only one entry was pasted and it was disambiguated, open the rename editor
3110                                if disambiguation_range.is_some() {
3111                                    cx.defer_in(window, |this, window, cx| {
3112                                        this.rename_impl(disambiguation_range, window, cx);
3113                                    });
3114                                }
3115                            }
3116                        })
3117                        .ok();
3118                }
3119
3120                anyhow::Ok(())
3121            })
3122            .detach_and_log_err(cx);
3123
3124            if clip_is_cut {
3125                // Convert the clipboard cut entry to a copy entry after the first paste.
3126                self.clipboard = self.clipboard.take().map(ClipboardEntry::into_copy_entry);
3127            }
3128
3129            self.expand_entry(worktree_id, entry.id, cx);
3130            Some(())
3131        });
3132    }
3133
3134    fn download_from_remote(
3135        &mut self,
3136        _: &DownloadFromRemote,
3137        window: &mut Window,
3138        cx: &mut Context<Self>,
3139    ) {
3140        let entries = self.effective_entries();
3141        if entries.is_empty() {
3142            return;
3143        }
3144
3145        let project = self.project.read(cx);
3146
3147        // Collect file entries with their worktree_id, path, and relative path for destination
3148        // For directories, we collect all files under them recursively
3149        let mut files_to_download: Vec<(WorktreeId, Arc<RelPath>, PathBuf)> = Vec::new();
3150
3151        for selected in entries.iter() {
3152            let Some(worktree) = project.worktree_for_id(selected.worktree_id, cx) else {
3153                continue;
3154            };
3155            let worktree = worktree.read(cx);
3156            let Some(entry) = worktree.entry_for_id(selected.entry_id) else {
3157                continue;
3158            };
3159
3160            if entry.is_file() {
3161                // Single file: use just the filename
3162                let filename = entry
3163                    .path
3164                    .file_name()
3165                    .map(str::to_string)
3166                    .unwrap_or_default();
3167                files_to_download.push((
3168                    selected.worktree_id,
3169                    entry.path.clone(),
3170                    PathBuf::from(filename),
3171                ));
3172            } else if entry.is_dir() {
3173                // Directory: collect all files recursively, preserving relative paths
3174                let dir_name = entry
3175                    .path
3176                    .file_name()
3177                    .map(str::to_string)
3178                    .unwrap_or_default();
3179                let base_path = entry.path.clone();
3180
3181                // Use traverse_from_path to iterate all entries under this directory
3182                let mut traversal = worktree.traverse_from_path(true, true, true, &entry.path);
3183                while let Some(child_entry) = traversal.entry() {
3184                    // Stop when we're no longer under the directory
3185                    if !child_entry.path.starts_with(&base_path) {
3186                        break;
3187                    }
3188
3189                    if child_entry.is_file() {
3190                        // Calculate relative path from the directory root
3191                        let relative_path = child_entry
3192                            .path
3193                            .strip_prefix(&base_path)
3194                            .map(|p| PathBuf::from(dir_name.clone()).join(p.as_unix_str()))
3195                            .unwrap_or_else(|_| {
3196                                PathBuf::from(
3197                                    child_entry
3198                                        .path
3199                                        .file_name()
3200                                        .map(str::to_string)
3201                                        .unwrap_or_default(),
3202                                )
3203                            });
3204                        files_to_download.push((
3205                            selected.worktree_id,
3206                            child_entry.path.clone(),
3207                            relative_path,
3208                        ));
3209                    }
3210                    traversal.advance();
3211                }
3212            }
3213        }
3214
3215        if files_to_download.is_empty() {
3216            return;
3217        }
3218
3219        let total_files = files_to_download.len();
3220        let workspace = self.workspace.clone();
3221
3222        let destination_dir = cx.prompt_for_paths(PathPromptOptions {
3223            files: false,
3224            directories: true,
3225            multiple: false,
3226            prompt: Some("Download".into()),
3227        });
3228
3229        let fs = self.fs.clone();
3230        let notification_id =
3231            workspace::notifications::NotificationId::Named("download-progress".into());
3232        cx.spawn_in(window, async move |this, cx| {
3233            if let Ok(Ok(Some(mut paths))) = destination_dir.await {
3234                if let Some(dest_dir) = paths.pop() {
3235                    // Show initial toast
3236                    workspace
3237                        .update(cx, |workspace, cx| {
3238                            workspace.show_toast(
3239                                workspace::Toast::new(
3240                                    notification_id.clone(),
3241                                    format!("Downloading 0/{} files...", total_files),
3242                                ),
3243                                cx,
3244                            );
3245                        })
3246                        .ok();
3247
3248                    for (index, (worktree_id, entry_path, relative_path)) in
3249                        files_to_download.into_iter().enumerate()
3250                    {
3251                        // Update progress toast
3252                        workspace
3253                            .update(cx, |workspace, cx| {
3254                                workspace.show_toast(
3255                                    workspace::Toast::new(
3256                                        notification_id.clone(),
3257                                        format!(
3258                                            "Downloading {}/{} files...",
3259                                            index + 1,
3260                                            total_files
3261                                        ),
3262                                    ),
3263                                    cx,
3264                                );
3265                            })
3266                            .ok();
3267
3268                        let destination_path = dest_dir.join(&relative_path);
3269
3270                        // Create parent directories if needed
3271                        if let Some(parent) = destination_path.parent() {
3272                            if !parent.exists() {
3273                                fs.create_dir(parent).await.log_err();
3274                            }
3275                        }
3276
3277                        let download_task = this.update(cx, |this, cx| {
3278                            let project = this.project.clone();
3279                            project.update(cx, |project, cx| {
3280                                project.download_file(worktree_id, entry_path, destination_path, cx)
3281                            })
3282                        });
3283                        if let Ok(task) = download_task {
3284                            task.await.log_err();
3285                        }
3286                    }
3287
3288                    // Show completion toast
3289                    workspace
3290                        .update(cx, |workspace, cx| {
3291                            workspace.show_toast(
3292                                workspace::Toast::new(
3293                                    notification_id.clone(),
3294                                    format!("Downloaded {} files", total_files),
3295                                ),
3296                                cx,
3297                            );
3298                        })
3299                        .ok();
3300                }
3301            }
3302        })
3303        .detach();
3304    }
3305
3306    fn duplicate(&mut self, _: &Duplicate, window: &mut Window, cx: &mut Context<Self>) {
3307        self.copy(&Copy {}, window, cx);
3308        self.paste(&Paste {}, window, cx);
3309    }
3310
3311    fn copy_path(
3312        &mut self,
3313        _: &zed_actions::workspace::CopyPath,
3314        _: &mut Window,
3315        cx: &mut Context<Self>,
3316    ) {
3317        let abs_file_paths = {
3318            let project = self.project.read(cx);
3319            self.effective_entries()
3320                .into_iter()
3321                .filter_map(|entry| {
3322                    let entry_path = project.path_for_entry(entry.entry_id, cx)?.path;
3323                    Some(
3324                        project
3325                            .worktree_for_id(entry.worktree_id, cx)?
3326                            .read(cx)
3327                            .absolutize(&entry_path)
3328                            .to_string_lossy()
3329                            .to_string(),
3330                    )
3331                })
3332                .collect::<Vec<_>>()
3333        };
3334        if !abs_file_paths.is_empty() {
3335            cx.write_to_clipboard(ClipboardItem::new_string(abs_file_paths.join("\n")));
3336        }
3337    }
3338
3339    fn copy_relative_path(
3340        &mut self,
3341        _: &zed_actions::workspace::CopyRelativePath,
3342        _: &mut Window,
3343        cx: &mut Context<Self>,
3344    ) {
3345        let path_style = self.project.read(cx).path_style(cx);
3346        let file_paths = {
3347            let project = self.project.read(cx);
3348            self.effective_entries()
3349                .into_iter()
3350                .filter_map(|entry| {
3351                    Some(
3352                        project
3353                            .path_for_entry(entry.entry_id, cx)?
3354                            .path
3355                            .display(path_style)
3356                            .into_owned(),
3357                    )
3358                })
3359                .collect::<Vec<_>>()
3360        };
3361        if !file_paths.is_empty() {
3362            cx.write_to_clipboard(ClipboardItem::new_string(file_paths.join("\n")));
3363        }
3364    }
3365
3366    fn reveal_in_finder(
3367        &mut self,
3368        _: &RevealInFileManager,
3369        _: &mut Window,
3370        cx: &mut Context<Self>,
3371    ) {
3372        if let Some(path) = self.reveal_in_file_manager_path(cx) {
3373            self.project
3374                .update(cx, |project, cx| project.reveal_path(&path, cx));
3375        }
3376    }
3377
3378    fn remove_from_project(
3379        &mut self,
3380        _: &RemoveFromProject,
3381        _window: &mut Window,
3382        cx: &mut Context<Self>,
3383    ) {
3384        for entry in self.effective_entries().iter() {
3385            let worktree_id = entry.worktree_id;
3386            self.project
3387                .update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
3388        }
3389    }
3390
3391    fn file_abs_paths_to_diff(&self, cx: &Context<Self>) -> Option<(PathBuf, PathBuf)> {
3392        let mut selections_abs_path = self
3393            .marked_entries
3394            .iter()
3395            .filter_map(|entry| {
3396                let project = self.project.read(cx);
3397                let worktree = project.worktree_for_id(entry.worktree_id, cx)?;
3398                let entry = worktree.read(cx).entry_for_id(entry.entry_id)?;
3399                if !entry.is_file() {
3400                    return None;
3401                }
3402                Some(worktree.read(cx).absolutize(&entry.path))
3403            })
3404            .rev();
3405
3406        let last_path = selections_abs_path.next()?;
3407        let previous_to_last = selections_abs_path.next()?;
3408        Some((previous_to_last, last_path))
3409    }
3410
3411    fn compare_marked_files(
3412        &mut self,
3413        _: &CompareMarkedFiles,
3414        window: &mut Window,
3415        cx: &mut Context<Self>,
3416    ) {
3417        let selected_files = self.file_abs_paths_to_diff(cx);
3418        if let Some((file_path1, file_path2)) = selected_files {
3419            self.workspace
3420                .update(cx, |workspace, cx| {
3421                    FileDiffView::open(file_path1, file_path2, workspace.weak_handle(), window, cx)
3422                        .detach_and_log_err(cx);
3423                })
3424                .ok();
3425        }
3426    }
3427
3428    fn open_system(&mut self, _: &OpenWithSystem, _: &mut Window, cx: &mut Context<Self>) {
3429        if let Some((worktree, entry)) = self.selected_entry(cx) {
3430            let abs_path = worktree.absolutize(&entry.path);
3431            cx.open_with_system(&abs_path);
3432        }
3433    }
3434
3435    fn open_in_terminal(
3436        &mut self,
3437        _: &OpenInTerminal,
3438        window: &mut Window,
3439        cx: &mut Context<Self>,
3440    ) {
3441        if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
3442            let abs_path = match &entry.canonical_path {
3443                Some(canonical_path) => canonical_path.to_path_buf(),
3444                None => worktree.read(cx).absolutize(&entry.path),
3445            };
3446
3447            let working_directory = if entry.is_dir() {
3448                Some(abs_path)
3449            } else {
3450                abs_path.parent().map(|path| path.to_path_buf())
3451            };
3452            if let Some(working_directory) = working_directory {
3453                window.dispatch_action(
3454                    workspace::OpenTerminal {
3455                        working_directory,
3456                        local: false,
3457                    }
3458                    .boxed_clone(),
3459                    cx,
3460                )
3461            }
3462        }
3463    }
3464
3465    pub fn new_search_in_directory(
3466        &mut self,
3467        _: &NewSearchInDirectory,
3468        window: &mut Window,
3469        cx: &mut Context<Self>,
3470    ) {
3471        if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
3472            let dir_path = if entry.is_dir() {
3473                entry.path.clone()
3474            } else {
3475                // entry is a file, use its parent directory
3476                match entry.path.parent() {
3477                    Some(parent) => Arc::from(parent),
3478                    None => {
3479                        // File at root, open search with empty filter
3480                        self.workspace
3481                            .update(cx, |workspace, cx| {
3482                                search::ProjectSearchView::new_search_in_directory(
3483                                    workspace,
3484                                    RelPath::empty(),
3485                                    window,
3486                                    cx,
3487                                );
3488                            })
3489                            .ok();
3490                        return;
3491                    }
3492                }
3493            };
3494
3495            let include_root = self.project.read(cx).visible_worktrees(cx).count() > 1;
3496            let dir_path = if include_root {
3497                worktree.read(cx).root_name().join(&dir_path)
3498            } else {
3499                dir_path
3500            };
3501
3502            self.workspace
3503                .update(cx, |workspace, cx| {
3504                    search::ProjectSearchView::new_search_in_directory(
3505                        workspace, &dir_path, window, cx,
3506                    );
3507                })
3508                .ok();
3509        }
3510    }
3511
3512    fn move_entry(
3513        &mut self,
3514        entry_to_move: ProjectEntryId,
3515        destination: ProjectEntryId,
3516        destination_is_file: bool,
3517        cx: &mut Context<Self>,
3518    ) -> Option<Task<Result<CreatedEntry>>> {
3519        if self
3520            .project
3521            .read(cx)
3522            .entry_is_worktree_root(entry_to_move, cx)
3523        {
3524            self.move_worktree_root(entry_to_move, destination, cx);
3525            None
3526        } else {
3527            self.move_worktree_entry(entry_to_move, destination, destination_is_file, cx)
3528        }
3529    }
3530
3531    fn move_worktree_root(
3532        &mut self,
3533        entry_to_move: ProjectEntryId,
3534        destination: ProjectEntryId,
3535        cx: &mut Context<Self>,
3536    ) {
3537        self.project.update(cx, |project, cx| {
3538            let Some(worktree_to_move) = project.worktree_for_entry(entry_to_move, cx) else {
3539                return;
3540            };
3541            let Some(destination_worktree) = project.worktree_for_entry(destination, cx) else {
3542                return;
3543            };
3544
3545            let worktree_id = worktree_to_move.read(cx).id();
3546            let destination_id = destination_worktree.read(cx).id();
3547
3548            project
3549                .move_worktree(worktree_id, destination_id, cx)
3550                .log_err();
3551        });
3552    }
3553
3554    fn move_worktree_entry(
3555        &mut self,
3556        entry_to_move: ProjectEntryId,
3557        destination_entry: ProjectEntryId,
3558        destination_is_file: bool,
3559        cx: &mut Context<Self>,
3560    ) -> Option<Task<Result<CreatedEntry>>> {
3561        if entry_to_move == destination_entry {
3562            return None;
3563        }
3564
3565        let (destination_worktree, rename_task) = self.project.update(cx, |project, cx| {
3566            let Some(source_path) = project.path_for_entry(entry_to_move, cx) else {
3567                return (None, None);
3568            };
3569            let Some(destination_path) = project.path_for_entry(destination_entry, cx) else {
3570                return (None, None);
3571            };
3572            let destination_worktree_id = destination_path.worktree_id;
3573
3574            let destination_dir = if destination_is_file {
3575                destination_path.path.parent().unwrap_or(RelPath::empty())
3576            } else {
3577                destination_path.path.as_ref()
3578            };
3579
3580            let Some(source_name) = source_path.path.file_name() else {
3581                return (None, None);
3582            };
3583            let Ok(source_name) = RelPath::unix(source_name) else {
3584                return (None, None);
3585            };
3586
3587            let mut new_path = destination_dir.to_rel_path_buf();
3588            new_path.push(source_name);
3589            let rename_task = (new_path.as_rel_path() != source_path.path.as_ref()).then(|| {
3590                project.rename_entry(
3591                    entry_to_move,
3592                    (destination_worktree_id, new_path).into(),
3593                    cx,
3594                )
3595            });
3596
3597            (
3598                project.worktree_id_for_entry(destination_entry, cx),
3599                rename_task,
3600            )
3601        });
3602
3603        if let Some(destination_worktree) = destination_worktree {
3604            self.expand_entry(destination_worktree, destination_entry, cx);
3605        }
3606        rename_task
3607    }
3608
3609    fn index_for_selection(&self, selection: SelectedEntry) -> Option<(usize, usize, usize)> {
3610        self.index_for_entry(selection.entry_id, selection.worktree_id)
3611    }
3612
3613    fn disjoint_effective_entries(&self, cx: &App) -> BTreeSet<SelectedEntry> {
3614        self.disjoint_entries(self.effective_entries(), cx)
3615    }
3616
3617    fn disjoint_entries(
3618        &self,
3619        entries: BTreeSet<SelectedEntry>,
3620        cx: &App,
3621    ) -> BTreeSet<SelectedEntry> {
3622        let mut sanitized_entries = BTreeSet::new();
3623        if entries.is_empty() {
3624            return sanitized_entries;
3625        }
3626
3627        let project = self.project.read(cx);
3628        let entries_by_worktree: HashMap<WorktreeId, Vec<SelectedEntry>> = entries
3629            .into_iter()
3630            .filter(|entry| !project.entry_is_worktree_root(entry.entry_id, cx))
3631            .fold(HashMap::default(), |mut map, entry| {
3632                map.entry(entry.worktree_id).or_default().push(entry);
3633                map
3634            });
3635
3636        for (worktree_id, worktree_entries) in entries_by_worktree {
3637            if let Some(worktree) = project.worktree_for_id(worktree_id, cx) {
3638                let worktree = worktree.read(cx);
3639                let dir_paths = worktree_entries
3640                    .iter()
3641                    .filter_map(|entry| {
3642                        worktree.entry_for_id(entry.entry_id).and_then(|entry| {
3643                            if entry.is_dir() {
3644                                Some(entry.path.as_ref())
3645                            } else {
3646                                None
3647                            }
3648                        })
3649                    })
3650                    .collect::<BTreeSet<_>>();
3651
3652                sanitized_entries.extend(worktree_entries.into_iter().filter(|entry| {
3653                    let Some(entry_info) = worktree.entry_for_id(entry.entry_id) else {
3654                        return false;
3655                    };
3656                    let entry_path = entry_info.path.as_ref();
3657                    let inside_selected_dir = dir_paths.iter().any(|&dir_path| {
3658                        entry_path != dir_path && entry_path.starts_with(dir_path)
3659                    });
3660                    !inside_selected_dir
3661                }));
3662            }
3663        }
3664
3665        sanitized_entries
3666    }
3667
3668    fn effective_entries(&self) -> BTreeSet<SelectedEntry> {
3669        if let Some(selection) = self.selection {
3670            let selection = SelectedEntry {
3671                entry_id: self.resolve_entry(selection.entry_id),
3672                worktree_id: selection.worktree_id,
3673            };
3674
3675            // Default to using just the selected item when nothing is marked.
3676            if self.marked_entries.is_empty() {
3677                return BTreeSet::from([selection]);
3678            }
3679
3680            // Allow operating on the selected item even when something else is marked,
3681            // making it easier to perform one-off actions without clearing a mark.
3682            if self.marked_entries.len() == 1 && !self.marked_entries.contains(&selection) {
3683                return BTreeSet::from([selection]);
3684            }
3685        }
3686
3687        // Return only marked entries since we've already handled special cases where
3688        // only selection should take precedence. At this point, marked entries may or
3689        // may not include the current selection, which is intentional.
3690        self.marked_entries
3691            .iter()
3692            .map(|entry| SelectedEntry {
3693                entry_id: self.resolve_entry(entry.entry_id),
3694                worktree_id: entry.worktree_id,
3695            })
3696            .collect::<BTreeSet<_>>()
3697    }
3698
3699    /// Finds the currently selected subentry for a given leaf entry id. If a given entry
3700    /// has no ancestors, the project entry ID that's passed in is returned as-is.
3701    fn resolve_entry(&self, id: ProjectEntryId) -> ProjectEntryId {
3702        self.state
3703            .ancestors
3704            .get(&id)
3705            .and_then(|ancestors| ancestors.active_ancestor())
3706            .unwrap_or(id)
3707    }
3708
3709    pub fn selected_entry<'a>(&self, cx: &'a App) -> Option<(&'a Worktree, &'a project::Entry)> {
3710        let (worktree, entry) = self.selected_entry_handle(cx)?;
3711        Some((worktree.read(cx), entry))
3712    }
3713
3714    pub fn selected_file_project_path(&self, cx: &App) -> Option<ProjectPath> {
3715        let (worktree, entry) = self.selected_sub_entry(cx)?;
3716        Some(ProjectPath {
3717            worktree_id: worktree.read(cx).id(),
3718            path: entry.is_file().then(|| entry.path.clone())?,
3719        })
3720    }
3721
3722    /// Compared to selected_entry, this function resolves to the currently
3723    /// selected subentry if dir auto-folding is enabled.
3724    fn selected_sub_entry<'a>(
3725        &self,
3726        cx: &'a App,
3727    ) -> Option<(Entity<Worktree>, &'a project::Entry)> {
3728        let (worktree, mut entry) = self.selected_entry_handle(cx)?;
3729
3730        let resolved_id = self.resolve_entry(entry.id);
3731        if resolved_id != entry.id {
3732            let worktree = worktree.read(cx);
3733            entry = worktree.entry_for_id(resolved_id)?;
3734        }
3735        Some((worktree, entry))
3736    }
3737
3738    fn reveal_in_file_manager_path(&self, cx: &App) -> Option<PathBuf> {
3739        if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
3740            return Some(worktree.read(cx).absolutize(&entry.path));
3741        }
3742
3743        let root_entry_id = self.state.last_worktree_root_id?;
3744        let project = self.project.read(cx);
3745        let worktree = project.worktree_for_entry(root_entry_id, cx)?;
3746        let worktree = worktree.read(cx);
3747        let root_entry = worktree.entry_for_id(root_entry_id)?;
3748        Some(worktree.absolutize(&root_entry.path))
3749    }
3750
3751    fn write_entries_to_system_clipboard(&self, entries: &BTreeSet<SelectedEntry>, cx: &mut App) {
3752        let project = self.project.read(cx);
3753        let paths: Vec<String> = entries
3754            .iter()
3755            .filter_map(|entry| {
3756                let worktree = project.worktree_for_id(entry.worktree_id, cx)?;
3757                let worktree = worktree.read(cx);
3758                let worktree_entry = worktree.entry_for_id(entry.entry_id)?;
3759                Some(
3760                    worktree
3761                        .abs_path()
3762                        .join(worktree_entry.path.as_std_path())
3763                        .to_string_lossy()
3764                        .to_string(),
3765                )
3766            })
3767            .collect();
3768        if !paths.is_empty() {
3769            cx.write_to_clipboard(ClipboardItem::new_string(paths.join("\n")));
3770        }
3771    }
3772
3773    fn external_paths_from_system_clipboard(&self, cx: &App) -> Option<ExternalPaths> {
3774        let clipboard_item = cx.read_from_clipboard()?;
3775        for entry in clipboard_item.entries() {
3776            if let GpuiClipboardEntry::ExternalPaths(paths) = entry {
3777                if !paths.paths().is_empty() {
3778                    return Some(paths.clone());
3779                }
3780            }
3781        }
3782        None
3783    }
3784
3785    fn has_pasteable_content(&self, cx: &App) -> bool {
3786        if self
3787            .clipboard
3788            .as_ref()
3789            .is_some_and(|c| !c.items().is_empty())
3790        {
3791            return true;
3792        }
3793        self.external_paths_from_system_clipboard(cx).is_some()
3794    }
3795
3796    fn selected_entry_handle<'a>(
3797        &self,
3798        cx: &'a App,
3799    ) -> Option<(Entity<Worktree>, &'a project::Entry)> {
3800        let selection = self.selection?;
3801        let project = self.project.read(cx);
3802        let worktree = project.worktree_for_id(selection.worktree_id, cx)?;
3803        let entry = worktree.read(cx).entry_for_id(selection.entry_id)?;
3804        Some((worktree, entry))
3805    }
3806
3807    fn expand_to_selection(&mut self, cx: &mut Context<Self>) -> Option<()> {
3808        let (worktree, entry) = self.selected_entry(cx)?;
3809        let expanded_dir_ids = self
3810            .state
3811            .expanded_dir_ids
3812            .entry(worktree.id())
3813            .or_default();
3814
3815        for path in entry.path.ancestors() {
3816            let Some(entry) = worktree.entry_for_path(path) else {
3817                continue;
3818            };
3819            if entry.is_dir()
3820                && let Err(idx) = expanded_dir_ids.binary_search(&entry.id)
3821            {
3822                expanded_dir_ids.insert(idx, entry.id);
3823            }
3824        }
3825
3826        Some(())
3827    }
3828
3829    fn create_new_git_entry(
3830        parent_entry: &Entry,
3831        git_summary: GitSummary,
3832        new_entry_kind: EntryKind,
3833    ) -> GitEntry {
3834        GitEntry {
3835            entry: Entry {
3836                id: NEW_ENTRY_ID,
3837                kind: new_entry_kind,
3838                path: parent_entry.path.join(RelPath::unix("\0").unwrap()),
3839                inode: 0,
3840                mtime: parent_entry.mtime,
3841                size: parent_entry.size,
3842                is_ignored: parent_entry.is_ignored,
3843                is_hidden: parent_entry.is_hidden,
3844                is_external: false,
3845                is_private: false,
3846                is_always_included: parent_entry.is_always_included,
3847                canonical_path: parent_entry.canonical_path.clone(),
3848                char_bag: parent_entry.char_bag,
3849                is_fifo: parent_entry.is_fifo,
3850            },
3851            git_summary,
3852        }
3853    }
3854
3855    fn update_visible_entries(
3856        &mut self,
3857        new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,
3858        focus_filename_editor: bool,
3859        autoscroll: bool,
3860        window: &mut Window,
3861        cx: &mut Context<Self>,
3862    ) {
3863        let now = Instant::now();
3864        let settings = ProjectPanelSettings::get_global(cx);
3865        let auto_collapse_dirs = settings.auto_fold_dirs;
3866        let hide_gitignore = settings.hide_gitignore;
3867        let sort_mode = settings.sort_mode;
3868        let project = self.project.read(cx);
3869        let repo_snapshots = project.git_store().read(cx).repo_snapshots(cx);
3870
3871        let old_ancestors = self.state.ancestors.clone();
3872        let temporary_unfolded_pending_state = self.state.temporarily_unfolded_pending_state.take();
3873        let mut new_state = State::derive(&self.state);
3874        new_state.last_worktree_root_id = project
3875            .visible_worktrees(cx)
3876            .next_back()
3877            .and_then(|worktree| worktree.read(cx).root_entry())
3878            .map(|entry| entry.id);
3879        let mut max_width_item = None;
3880
3881        let visible_worktrees: Vec<_> = project
3882            .visible_worktrees(cx)
3883            .map(|worktree| worktree.read(cx).snapshot())
3884            .collect();
3885        let hide_root = settings.hide_root && visible_worktrees.len() == 1;
3886        let hide_hidden = settings.hide_hidden;
3887
3888        let visible_entries_task = cx.spawn_in(window, async move |this, cx| {
3889            let new_state = cx
3890                .background_spawn(async move {
3891                    for worktree_snapshot in visible_worktrees {
3892                        let worktree_id = worktree_snapshot.id();
3893
3894                        let mut new_entry_parent_id = None;
3895                        let mut new_entry_kind = EntryKind::Dir;
3896                        if let Some(edit_state) = &new_state.edit_state
3897                            && edit_state.worktree_id == worktree_id
3898                            && edit_state.is_new_entry()
3899                        {
3900                            new_entry_parent_id = Some(edit_state.entry_id);
3901                            new_entry_kind = if edit_state.is_dir {
3902                                EntryKind::Dir
3903                            } else {
3904                                EntryKind::File
3905                            };
3906                        }
3907
3908                        let mut visible_worktree_entries = Vec::new();
3909                        let mut entry_iter =
3910                            GitTraversal::new(&repo_snapshots, worktree_snapshot.entries(true, 0));
3911                        let mut auto_folded_ancestors = vec![];
3912                        let worktree_abs_path = worktree_snapshot.abs_path();
3913                        while let Some(entry) = entry_iter.entry() {
3914                            if hide_root && Some(entry.entry) == worktree_snapshot.root_entry() {
3915                                if new_entry_parent_id == Some(entry.id) {
3916                                    visible_worktree_entries.push(Self::create_new_git_entry(
3917                                        entry.entry,
3918                                        entry.git_summary,
3919                                        new_entry_kind,
3920                                    ));
3921                                    new_entry_parent_id = None;
3922                                }
3923                                entry_iter.advance();
3924                                continue;
3925                            }
3926                            if auto_collapse_dirs && entry.kind.is_dir() {
3927                                auto_folded_ancestors.push(entry.id);
3928                                if !new_state.is_unfolded(&entry.id)
3929                                    && let Some(root_path) = worktree_snapshot.root_entry()
3930                                {
3931                                    let mut child_entries =
3932                                        worktree_snapshot.child_entries(&entry.path);
3933                                    if let Some(child) = child_entries.next()
3934                                        && entry.path != root_path.path
3935                                        && child_entries.next().is_none()
3936                                        && child.kind.is_dir()
3937                                    {
3938                                        entry_iter.advance();
3939
3940                                        continue;
3941                                    }
3942                                }
3943                                let depth = temporary_unfolded_pending_state
3944                                    .as_ref()
3945                                    .and_then(|state| {
3946                                        if state.previously_focused_leaf_entry.worktree_id
3947                                            == worktree_id
3948                                            && state.previously_focused_leaf_entry.entry_id
3949                                                == entry.id
3950                                        {
3951                                            auto_folded_ancestors.iter().rev().position(|id| {
3952                                                *id == state.temporarily_unfolded_active_entry_id
3953                                            })
3954                                        } else {
3955                                            None
3956                                        }
3957                                    })
3958                                    .unwrap_or_else(|| {
3959                                        old_ancestors
3960                                            .get(&entry.id)
3961                                            .map(|ancestor| ancestor.current_ancestor_depth)
3962                                            .unwrap_or_default()
3963                                    })
3964                                    .min(auto_folded_ancestors.len());
3965                                if let Some(edit_state) = &mut new_state.edit_state
3966                                    && edit_state.entry_id == entry.id
3967                                {
3968                                    edit_state.depth = depth;
3969                                }
3970                                let mut ancestors = std::mem::take(&mut auto_folded_ancestors);
3971                                if ancestors.len() > 1 {
3972                                    ancestors.reverse();
3973                                    new_state.ancestors.insert(
3974                                        entry.id,
3975                                        FoldedAncestors {
3976                                            current_ancestor_depth: depth,
3977                                            ancestors,
3978                                        },
3979                                    );
3980                                }
3981                            }
3982                            auto_folded_ancestors.clear();
3983                            if (!hide_gitignore || !entry.is_ignored)
3984                                && (!hide_hidden || !entry.is_hidden)
3985                            {
3986                                visible_worktree_entries.push(entry.to_owned());
3987                            }
3988                            let precedes_new_entry = if let Some(new_entry_id) = new_entry_parent_id
3989                            {
3990                                entry.id == new_entry_id || {
3991                                    new_state.ancestors.get(&entry.id).is_some_and(|entries| {
3992                                        entries.ancestors.contains(&new_entry_id)
3993                                    })
3994                                }
3995                            } else {
3996                                false
3997                            };
3998                            if precedes_new_entry
3999                                && (!hide_gitignore || !entry.is_ignored)
4000                                && (!hide_hidden || !entry.is_hidden)
4001                            {
4002                                visible_worktree_entries.push(Self::create_new_git_entry(
4003                                    entry.entry,
4004                                    entry.git_summary,
4005                                    new_entry_kind,
4006                                ));
4007                            }
4008
4009                            let (depth, chars) = if Some(entry.entry)
4010                                == worktree_snapshot.root_entry()
4011                            {
4012                                let Some(path_name) = worktree_abs_path.file_name() else {
4013                                    entry_iter.advance();
4014                                    continue;
4015                                };
4016                                let depth = 0;
4017                                (depth, path_name.to_string_lossy().chars().count())
4018                            } else if entry.is_file() {
4019                                let Some(path_name) = entry
4020                                    .path
4021                                    .file_name()
4022                                    .with_context(|| {
4023                                        format!("Non-root entry has no file name: {entry:?}")
4024                                    })
4025                                    .log_err()
4026                                else {
4027                                    continue;
4028                                };
4029                                let depth = entry.path.ancestors().count() - 1;
4030                                (depth, path_name.chars().count())
4031                            } else {
4032                                let path = new_state
4033                                    .ancestors
4034                                    .get(&entry.id)
4035                                    .and_then(|ancestors| {
4036                                        let outermost_ancestor = ancestors.ancestors.last()?;
4037                                        let root_folded_entry = worktree_snapshot
4038                                            .entry_for_id(*outermost_ancestor)?
4039                                            .path
4040                                            .as_ref();
4041                                        entry.path.strip_prefix(root_folded_entry).ok().and_then(
4042                                            |suffix| {
4043                                                Some(
4044                                                    RelPath::unix(root_folded_entry.file_name()?)
4045                                                        .unwrap()
4046                                                        .join(suffix),
4047                                                )
4048                                            },
4049                                        )
4050                                    })
4051                                    .or_else(|| {
4052                                        entry.path.file_name().map(|file_name| {
4053                                            RelPath::unix(file_name).unwrap().into()
4054                                        })
4055                                    })
4056                                    .unwrap_or_else(|| entry.path.clone());
4057                                let depth = path.components().count();
4058                                (depth, path.as_unix_str().chars().count())
4059                            };
4060                            let width_estimate =
4061                                item_width_estimate(depth, chars, entry.canonical_path.is_some());
4062
4063                            match max_width_item.as_mut() {
4064                                Some((id, worktree_id, width)) => {
4065                                    if *width < width_estimate {
4066                                        *id = entry.id;
4067                                        *worktree_id = worktree_snapshot.id();
4068                                        *width = width_estimate;
4069                                    }
4070                                }
4071                                None => {
4072                                    max_width_item =
4073                                        Some((entry.id, worktree_snapshot.id(), width_estimate))
4074                                }
4075                            }
4076
4077                            let expanded_dir_ids =
4078                                match new_state.expanded_dir_ids.entry(worktree_id) {
4079                                    hash_map::Entry::Occupied(e) => e.into_mut(),
4080                                    hash_map::Entry::Vacant(e) => {
4081                                        // The first time a worktree's root entry becomes available,
4082                                        // mark that root entry as expanded.
4083                                        if let Some(entry) = worktree_snapshot.root_entry() {
4084                                            e.insert(vec![entry.id]).as_slice()
4085                                        } else {
4086                                            &[]
4087                                        }
4088                                    }
4089                                };
4090
4091                            if expanded_dir_ids.binary_search(&entry.id).is_err()
4092                                && entry_iter.advance_to_sibling()
4093                            {
4094                                continue;
4095                            }
4096                            entry_iter.advance();
4097                        }
4098
4099                        par_sort_worktree_entries_with_mode(
4100                            &mut visible_worktree_entries,
4101                            sort_mode,
4102                        );
4103                        new_state.visible_entries.push(VisibleEntriesForWorktree {
4104                            worktree_id,
4105                            entries: visible_worktree_entries,
4106                            index: OnceCell::new(),
4107                        })
4108                    }
4109                    if let Some((project_entry_id, worktree_id, _)) = max_width_item {
4110                        let mut visited_worktrees_length = 0;
4111                        let index = new_state
4112                            .visible_entries
4113                            .iter()
4114                            .find_map(|visible_entries| {
4115                                if worktree_id == visible_entries.worktree_id {
4116                                    visible_entries
4117                                        .entries
4118                                        .iter()
4119                                        .position(|entry| entry.id == project_entry_id)
4120                                } else {
4121                                    visited_worktrees_length += visible_entries.entries.len();
4122                                    None
4123                                }
4124                            });
4125                        if let Some(index) = index {
4126                            new_state.max_width_item_index = Some(visited_worktrees_length + index);
4127                        }
4128                    }
4129                    new_state
4130                })
4131                .await;
4132            this.update_in(cx, |this, window, cx| {
4133                this.state = new_state;
4134                if let Some((worktree_id, entry_id)) = new_selected_entry {
4135                    this.selection = Some(SelectedEntry {
4136                        worktree_id,
4137                        entry_id,
4138                    });
4139                }
4140                let elapsed = now.elapsed();
4141                if this.last_reported_update.elapsed() > Duration::from_secs(3600) {
4142                    telemetry::event!(
4143                        "Project Panel Updated",
4144                        elapsed_ms = elapsed.as_millis() as u64,
4145                        worktree_entries = this
4146                            .state
4147                            .visible_entries
4148                            .iter()
4149                            .map(|worktree| worktree.entries.len())
4150                            .sum::<usize>(),
4151                    )
4152                }
4153                if this.update_visible_entries_task.focus_filename_editor {
4154                    this.update_visible_entries_task.focus_filename_editor = false;
4155                    this.filename_editor.update(cx, |editor, cx| {
4156                        window.focus(&editor.focus_handle(cx), cx);
4157                    });
4158                }
4159                if this.update_visible_entries_task.autoscroll {
4160                    this.update_visible_entries_task.autoscroll = false;
4161                    this.autoscroll(cx);
4162                }
4163                cx.notify();
4164            })
4165            .ok();
4166        });
4167
4168        self.update_visible_entries_task = UpdateVisibleEntriesTask {
4169            _visible_entries_task: visible_entries_task,
4170            focus_filename_editor: focus_filename_editor
4171                || self.update_visible_entries_task.focus_filename_editor,
4172            autoscroll: autoscroll || self.update_visible_entries_task.autoscroll,
4173        };
4174    }
4175
4176    fn expand_entry(
4177        &mut self,
4178        worktree_id: WorktreeId,
4179        entry_id: ProjectEntryId,
4180        cx: &mut Context<Self>,
4181    ) {
4182        self.project.update(cx, |project, cx| {
4183            if let Some((worktree, expanded_dir_ids)) = project
4184                .worktree_for_id(worktree_id, cx)
4185                .zip(self.state.expanded_dir_ids.get_mut(&worktree_id))
4186            {
4187                project.expand_entry(worktree_id, entry_id, cx);
4188                let worktree = worktree.read(cx);
4189
4190                if let Some(mut entry) = worktree.entry_for_id(entry_id) {
4191                    loop {
4192                        if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) {
4193                            expanded_dir_ids.insert(ix, entry.id);
4194                        }
4195
4196                        if let Some(parent_entry) =
4197                            entry.path.parent().and_then(|p| worktree.entry_for_path(p))
4198                        {
4199                            entry = parent_entry;
4200                        } else {
4201                            break;
4202                        }
4203                    }
4204                }
4205            }
4206        });
4207    }
4208
4209    fn drop_external_files(
4210        &mut self,
4211        paths: &[PathBuf],
4212        entry_id: ProjectEntryId,
4213        window: &mut Window,
4214        cx: &mut Context<Self>,
4215    ) {
4216        let mut paths: Vec<Arc<Path>> = paths.iter().map(|path| Arc::from(path.clone())).collect();
4217
4218        let open_file_after_drop = paths.len() == 1 && paths[0].is_file();
4219
4220        let Some((target_directory, worktree, fs)) = maybe!({
4221            let project = self.project.read(cx);
4222            let fs = project.fs().clone();
4223            let worktree = project.worktree_for_entry(entry_id, cx)?;
4224            let entry = worktree.read(cx).entry_for_id(entry_id)?;
4225            let path = entry.path.clone();
4226            let target_directory = if entry.is_dir() {
4227                path
4228            } else {
4229                path.parent()?.into()
4230            };
4231            Some((target_directory, worktree, fs))
4232        }) else {
4233            return;
4234        };
4235
4236        let mut paths_to_replace = Vec::new();
4237        for path in &paths {
4238            if let Some(name) = path.file_name()
4239                && let Some(name) = name.to_str()
4240            {
4241                let target_path = target_directory.join(RelPath::unix(name).unwrap());
4242                if worktree.read(cx).entry_for_path(&target_path).is_some() {
4243                    paths_to_replace.push((name.to_string(), path.clone()));
4244                }
4245            }
4246        }
4247
4248        cx.spawn_in(window, async move |this, cx| {
4249            async move {
4250                for (filename, original_path) in &paths_to_replace {
4251                    let prompt_message = format!(
4252                        concat!(
4253                            "A file or folder with name {} ",
4254                            "already exists in the destination folder. ",
4255                            "Do you want to replace it?"
4256                        ),
4257                        filename
4258                    );
4259                    let answer = cx
4260                        .update(|window, cx| {
4261                            window.prompt(
4262                                PromptLevel::Info,
4263                                &prompt_message,
4264                                None,
4265                                &["Replace", "Cancel"],
4266                                cx,
4267                            )
4268                        })?
4269                        .await?;
4270
4271                    if answer == 1
4272                        && let Some(item_idx) = paths.iter().position(|p| p == original_path)
4273                    {
4274                        paths.remove(item_idx);
4275                    }
4276                }
4277
4278                if paths.is_empty() {
4279                    return Ok(());
4280                }
4281
4282                let (worktree_id, task) = worktree.update(cx, |worktree, cx| {
4283                    (
4284                        worktree.id(),
4285                        worktree.copy_external_entries(target_directory, paths, fs, cx),
4286                    )
4287                });
4288
4289                let opened_entries: Vec<_> = task
4290                    .await
4291                    .with_context(|| "failed to copy external paths")?;
4292                this.update_in(cx, |this, window, cx| {
4293                    let mut did_open = false;
4294                    if open_file_after_drop && !opened_entries.is_empty() {
4295                        let settings = ProjectPanelSettings::get_global(cx);
4296                        if settings.auto_open.should_open_on_drop() {
4297                            this.open_entry(opened_entries[0], true, false, cx);
4298                            did_open = true;
4299                        }
4300                    }
4301
4302                    if !did_open {
4303                        let new_selection = opened_entries
4304                            .last()
4305                            .map(|&entry_id| (worktree_id, entry_id));
4306                        for &entry_id in &opened_entries {
4307                            this.expand_entry(worktree_id, entry_id, cx);
4308                        }
4309                        this.marked_entries.clear();
4310                        this.update_visible_entries(new_selection, false, false, window, cx);
4311                    }
4312                })
4313            }
4314            .log_err()
4315            .await
4316        })
4317        .detach();
4318    }
4319
4320    fn refresh_drag_cursor_style(
4321        &self,
4322        modifiers: &Modifiers,
4323        window: &mut Window,
4324        cx: &mut Context<Self>,
4325    ) {
4326        if let Some(existing_cursor) = cx.active_drag_cursor_style() {
4327            let new_cursor = if Self::is_copy_modifier_set(modifiers) {
4328                CursorStyle::DragCopy
4329            } else {
4330                CursorStyle::PointingHand
4331            };
4332            if existing_cursor != new_cursor {
4333                cx.set_active_drag_cursor_style(new_cursor, window);
4334            }
4335        }
4336    }
4337
4338    fn is_copy_modifier_set(modifiers: &Modifiers) -> bool {
4339        cfg!(target_os = "macos") && modifiers.alt
4340            || cfg!(not(target_os = "macos")) && modifiers.control
4341    }
4342
4343    fn drag_onto(
4344        &mut self,
4345        selections: &DraggedSelection,
4346        target_entry_id: ProjectEntryId,
4347        is_file: bool,
4348        window: &mut Window,
4349        cx: &mut Context<Self>,
4350    ) {
4351        let resolved_selections = selections
4352            .items()
4353            .map(|entry| SelectedEntry {
4354                entry_id: self.resolve_entry(entry.entry_id),
4355                worktree_id: entry.worktree_id,
4356            })
4357            .collect::<BTreeSet<SelectedEntry>>();
4358        let entries = self.disjoint_entries(resolved_selections, cx);
4359
4360        if Self::is_copy_modifier_set(&window.modifiers()) {
4361            let _ = maybe!({
4362                let project = self.project.read(cx);
4363                let target_worktree = project.worktree_for_entry(target_entry_id, cx)?;
4364                let worktree_id = target_worktree.read(cx).id();
4365                let target_entry = target_worktree
4366                    .read(cx)
4367                    .entry_for_id(target_entry_id)?
4368                    .clone();
4369
4370                let mut copy_tasks = Vec::new();
4371                let mut disambiguation_range = None;
4372                for selection in &entries {
4373                    let (new_path, new_disambiguation_range) = self.create_paste_path(
4374                        selection,
4375                        (target_worktree.clone(), &target_entry),
4376                        cx,
4377                    )?;
4378
4379                    let task = self.project.update(cx, |project, cx| {
4380                        project.copy_entry(selection.entry_id, (worktree_id, new_path).into(), cx)
4381                    });
4382                    copy_tasks.push(task);
4383                    disambiguation_range = new_disambiguation_range.or(disambiguation_range);
4384                }
4385
4386                let item_count = copy_tasks.len();
4387
4388                cx.spawn_in(window, async move |project_panel, cx| {
4389                    let mut last_succeed = None;
4390                    let mut operations = Vec::new();
4391                    for task in copy_tasks.into_iter() {
4392                        if let Some(Some(entry)) = task.await.log_err() {
4393                            last_succeed = Some(entry.id);
4394                            operations.push(ProjectPanelOperation::Create {
4395                                project_path: (worktree_id, entry.path).into(),
4396                            });
4397                        }
4398                    }
4399                    // update selection
4400                    if let Some(entry_id) = last_succeed {
4401                        project_panel
4402                            .update_in(cx, |project_panel, window, cx| {
4403                                project_panel.selection = Some(SelectedEntry {
4404                                    worktree_id,
4405                                    entry_id,
4406                                });
4407
4408                                project_panel.undo_manager.record_batch(operations);
4409
4410                                // if only one entry was dragged and it was disambiguated, open the rename editor
4411                                if item_count == 1 && disambiguation_range.is_some() {
4412                                    project_panel.rename_impl(disambiguation_range, window, cx);
4413                                }
4414                            })
4415                            .ok();
4416                    }
4417                })
4418                .detach();
4419                Some(())
4420            });
4421        } else {
4422            let update_marks = !self.marked_entries.is_empty();
4423            let active_selection = selections.active_selection;
4424
4425            // For folded selections, track the leaf suffix relative to the resolved
4426            // entry so we can refresh it after the move completes.
4427            let (folded_selection_info, folded_selection_entries): (
4428                Vec<(ProjectEntryId, RelPathBuf)>,
4429                HashSet<SelectedEntry>,
4430            ) = {
4431                let project = self.project.read(cx);
4432                let mut info = Vec::new();
4433                let mut folded_entries = HashSet::default();
4434
4435                for selection in selections.items() {
4436                    let resolved_id = self.resolve_entry(selection.entry_id);
4437                    if resolved_id == selection.entry_id {
4438                        continue;
4439                    }
4440                    folded_entries.insert(*selection);
4441                    let Some(source_path) = project.path_for_entry(resolved_id, cx) else {
4442                        continue;
4443                    };
4444                    let Some(leaf_path) = project.path_for_entry(selection.entry_id, cx) else {
4445                        continue;
4446                    };
4447                    let Ok(suffix) = leaf_path.path.strip_prefix(source_path.path.as_ref()) else {
4448                        continue;
4449                    };
4450                    if suffix.as_unix_str().is_empty() {
4451                        continue;
4452                    }
4453
4454                    info.push((resolved_id, suffix.to_rel_path_buf()));
4455                }
4456                (info, folded_entries)
4457            };
4458
4459            // Capture old paths before moving so we can record undo operations.
4460            let old_paths: HashMap<ProjectEntryId, ProjectPath> = {
4461                let project = self.project.read(cx);
4462                entries
4463                    .iter()
4464                    .filter_map(|entry| {
4465                        let path = project.path_for_entry(entry.entry_id, cx)?;
4466                        Some((entry.entry_id, path))
4467                    })
4468                    .collect()
4469            };
4470            let destination_worktree_id = self
4471                .project
4472                .read(cx)
4473                .worktree_for_entry(target_entry_id, cx)
4474                .map(|wt| wt.read(cx).id());
4475
4476            // Collect move tasks paired with their source entry ID so we can correlate
4477            // results with folded selections that need refreshing.
4478            let mut move_tasks: Vec<(ProjectEntryId, Task<Result<CreatedEntry>>)> = Vec::new();
4479            for entry in entries {
4480                if let Some(task) = self.move_entry(entry.entry_id, target_entry_id, is_file, cx) {
4481                    move_tasks.push((entry.entry_id, task));
4482                }
4483            }
4484
4485            if move_tasks.is_empty() {
4486                return;
4487            }
4488
4489            let workspace = self.workspace.clone();
4490            if folded_selection_info.is_empty() {
4491                cx.spawn_in(window, async move |project_panel, mut cx| {
4492                    let mut operations = Vec::new();
4493                    for (entry_id, task) in move_tasks {
4494                        if let Some(CreatedEntry::Included(new_entry)) = task
4495                            .await
4496                            .notify_workspace_async_err(workspace.clone(), &mut cx)
4497                        {
4498                            if let (Some(old_path), Some(worktree_id)) =
4499                                (old_paths.get(&entry_id), destination_worktree_id)
4500                            {
4501                                operations.push(ProjectPanelOperation::Rename {
4502                                    old_path: old_path.clone(),
4503                                    new_path: (worktree_id, new_entry.path).into(),
4504                                });
4505                            }
4506                        }
4507                    }
4508                    project_panel
4509                        .update(cx, |this, _| {
4510                            this.undo_manager.record_batch(operations);
4511                        })
4512                        .ok();
4513                })
4514                .detach();
4515            } else {
4516                cx.spawn_in(window, async move |project_panel, mut cx| {
4517                    // Await all move tasks and collect successful results
4518                    let mut move_results: Vec<(ProjectEntryId, Entry)> = Vec::new();
4519                    let mut operations = Vec::new();
4520                    for (entry_id, task) in move_tasks {
4521                        if let Some(CreatedEntry::Included(new_entry)) = task
4522                            .await
4523                            .notify_workspace_async_err(workspace.clone(), &mut cx)
4524                        {
4525                            if let (Some(old_path), Some(worktree_id)) =
4526                                (old_paths.get(&entry_id), destination_worktree_id)
4527                            {
4528                                operations.push(ProjectPanelOperation::Rename {
4529                                    old_path: old_path.clone(),
4530                                    new_path: (worktree_id, new_entry.path.clone()).into(),
4531                                });
4532                            }
4533                            move_results.push((entry_id, new_entry));
4534                        }
4535                    }
4536
4537                    if move_results.is_empty() {
4538                        return;
4539                    }
4540
4541                    project_panel
4542                        .update(cx, |this, _| {
4543                            this.undo_manager.record_batch(operations);
4544                        })
4545                        .ok();
4546
4547                    // For folded selections, we need to refresh the leaf paths (with suffixes)
4548                    // because they may not be indexed yet after the parent directory was moved.
4549                    // First collect the paths to refresh, then refresh them.
4550                    let paths_to_refresh: Vec<(Entity<Worktree>, Arc<RelPath>)> = project_panel
4551                        .update(cx, |project_panel, cx| {
4552                            let project = project_panel.project.read(cx);
4553                            folded_selection_info
4554                                .iter()
4555                                .filter_map(|(resolved_id, suffix)| {
4556                                    let (_, new_entry) =
4557                                        move_results.iter().find(|(id, _)| id == resolved_id)?;
4558                                    let worktree = project.worktree_for_entry(new_entry.id, cx)?;
4559                                    let leaf_path = new_entry.path.join(suffix);
4560                                    Some((worktree, leaf_path))
4561                                })
4562                                .collect()
4563                        })
4564                        .ok()
4565                        .unwrap_or_default();
4566
4567                    let refresh_tasks: Vec<_> = paths_to_refresh
4568                        .into_iter()
4569                        .filter_map(|(worktree, leaf_path)| {
4570                            worktree.update(cx, |worktree, cx| {
4571                                worktree
4572                                    .as_local_mut()
4573                                    .map(|local| local.refresh_entry(leaf_path, None, cx))
4574                            })
4575                        })
4576                        .collect();
4577
4578                    for task in refresh_tasks {
4579                        task.await.log_err();
4580                    }
4581
4582                    if update_marks && !folded_selection_entries.is_empty() {
4583                        project_panel
4584                            .update(cx, |project_panel, cx| {
4585                                project_panel.marked_entries.retain(|entry| {
4586                                    !folded_selection_entries.contains(entry)
4587                                        || *entry == active_selection
4588                                });
4589                                cx.notify();
4590                            })
4591                            .ok();
4592                    }
4593                })
4594                .detach();
4595            }
4596        }
4597    }
4598
4599    fn index_for_entry(
4600        &self,
4601        entry_id: ProjectEntryId,
4602        worktree_id: WorktreeId,
4603    ) -> Option<(usize, usize, usize)> {
4604        let mut total_ix = 0;
4605        for (worktree_ix, visible) in self.state.visible_entries.iter().enumerate() {
4606            if worktree_id != visible.worktree_id {
4607                total_ix += visible.entries.len();
4608                continue;
4609            }
4610
4611            return visible
4612                .entries
4613                .iter()
4614                .enumerate()
4615                .find(|(_, entry)| entry.id == entry_id)
4616                .map(|(ix, _)| (worktree_ix, ix, total_ix + ix));
4617        }
4618        None
4619    }
4620
4621    fn entry_at_index(&self, index: usize) -> Option<(WorktreeId, GitEntryRef<'_>)> {
4622        let mut offset = 0;
4623        for worktree in &self.state.visible_entries {
4624            let current_len = worktree.entries.len();
4625            if index < offset + current_len {
4626                return worktree
4627                    .entries
4628                    .get(index - offset)
4629                    .map(|entry| (worktree.worktree_id, entry.to_ref()));
4630            }
4631            offset += current_len;
4632        }
4633        None
4634    }
4635
4636    fn iter_visible_entries(
4637        &self,
4638        range: Range<usize>,
4639        window: &mut Window,
4640        cx: &mut Context<ProjectPanel>,
4641        callback: &mut dyn FnMut(
4642            &Entry,
4643            usize,
4644            &HashSet<Arc<RelPath>>,
4645            &mut Window,
4646            &mut Context<ProjectPanel>,
4647        ),
4648    ) {
4649        let mut ix = 0;
4650        for visible in &self.state.visible_entries {
4651            if ix >= range.end {
4652                return;
4653            }
4654
4655            if ix + visible.entries.len() <= range.start {
4656                ix += visible.entries.len();
4657                continue;
4658            }
4659
4660            let end_ix = range.end.min(ix + visible.entries.len());
4661            let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
4662            let entries = visible
4663                .index
4664                .get_or_init(|| visible.entries.iter().map(|e| e.path.clone()).collect());
4665            let base_index = ix + entry_range.start;
4666            for (i, entry) in visible.entries[entry_range].iter().enumerate() {
4667                let global_index = base_index + i;
4668                callback(entry, global_index, entries, window, cx);
4669            }
4670            ix = end_ix;
4671        }
4672    }
4673
4674    fn for_each_visible_entry(
4675        &self,
4676        range: Range<usize>,
4677        window: &mut Window,
4678        cx: &mut Context<ProjectPanel>,
4679        callback: &mut dyn FnMut(
4680            ProjectEntryId,
4681            EntryDetails,
4682            &mut Window,
4683            &mut Context<ProjectPanel>,
4684        ),
4685    ) {
4686        let mut ix = 0;
4687        for visible in &self.state.visible_entries {
4688            if ix >= range.end {
4689                return;
4690            }
4691
4692            if ix + visible.entries.len() <= range.start {
4693                ix += visible.entries.len();
4694                continue;
4695            }
4696
4697            let end_ix = range.end.min(ix + visible.entries.len());
4698            let git_status_setting = {
4699                let settings = ProjectPanelSettings::get_global(cx);
4700                settings.git_status
4701            };
4702            if let Some(worktree) = self
4703                .project
4704                .read(cx)
4705                .worktree_for_id(visible.worktree_id, cx)
4706            {
4707                let snapshot = worktree.read(cx).snapshot();
4708                let root_name = snapshot.root_name();
4709
4710                let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
4711                let entries = visible
4712                    .index
4713                    .get_or_init(|| visible.entries.iter().map(|e| e.path.clone()).collect());
4714                for entry in visible.entries[entry_range].iter() {
4715                    let status = git_status_setting
4716                        .then_some(entry.git_summary)
4717                        .unwrap_or_default();
4718
4719                    let mut details = self.details_for_entry(
4720                        entry,
4721                        visible.worktree_id,
4722                        root_name,
4723                        entries,
4724                        status,
4725                        None,
4726                        window,
4727                        cx,
4728                    );
4729
4730                    if let Some(edit_state) = &self.state.edit_state {
4731                        let is_edited_entry = if edit_state.is_new_entry() {
4732                            entry.id == NEW_ENTRY_ID
4733                        } else {
4734                            entry.id == edit_state.entry_id
4735                                || self.state.ancestors.get(&entry.id).is_some_and(
4736                                    |auto_folded_dirs| {
4737                                        auto_folded_dirs.ancestors.contains(&edit_state.entry_id)
4738                                    },
4739                                )
4740                        };
4741
4742                        if is_edited_entry {
4743                            if let Some(processing_filename) = &edit_state.processing_filename {
4744                                details.is_processing = true;
4745                                if let Some(ancestors) = edit_state
4746                                    .leaf_entry_id
4747                                    .and_then(|entry| self.state.ancestors.get(&entry))
4748                                {
4749                                    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;
4750                                    let all_components = ancestors.ancestors.len();
4751
4752                                    let prefix_components = all_components - position;
4753                                    let suffix_components = position.checked_sub(1);
4754                                    let mut previous_components =
4755                                        Path::new(&details.filename).components();
4756                                    let mut new_path = previous_components
4757                                        .by_ref()
4758                                        .take(prefix_components)
4759                                        .collect::<PathBuf>();
4760                                    if let Some(last_component) =
4761                                        processing_filename.components().next_back()
4762                                    {
4763                                        new_path.push(last_component);
4764                                        previous_components.next();
4765                                    }
4766
4767                                    if suffix_components.is_some() {
4768                                        new_path.push(previous_components);
4769                                    }
4770                                    if let Some(str) = new_path.to_str() {
4771                                        details.filename.clear();
4772                                        details.filename.push_str(str);
4773                                    }
4774                                } else {
4775                                    details.filename.clear();
4776                                    details.filename.push_str(processing_filename.as_unix_str());
4777                                }
4778                            } else {
4779                                if edit_state.is_new_entry() {
4780                                    details.filename.clear();
4781                                }
4782                                details.is_editing = true;
4783                            }
4784                        }
4785                    }
4786
4787                    callback(entry.id, details, window, cx);
4788                }
4789            }
4790            ix = end_ix;
4791        }
4792    }
4793
4794    fn find_entry_in_worktree(
4795        &self,
4796        worktree_id: WorktreeId,
4797        reverse_search: bool,
4798        only_visible_entries: bool,
4799        predicate: &dyn Fn(GitEntryRef, WorktreeId) -> bool,
4800        cx: &mut Context<Self>,
4801    ) -> Option<GitEntry> {
4802        if only_visible_entries {
4803            let entries = self
4804                .state
4805                .visible_entries
4806                .iter()
4807                .find_map(|visible| {
4808                    if worktree_id == visible.worktree_id {
4809                        Some(&visible.entries)
4810                    } else {
4811                        None
4812                    }
4813                })?
4814                .clone();
4815
4816            return utils::ReversibleIterable::new(entries.iter(), reverse_search)
4817                .find(|ele| predicate(ele.to_ref(), worktree_id))
4818                .cloned();
4819        }
4820
4821        let repo_snapshots = self
4822            .project
4823            .read(cx)
4824            .git_store()
4825            .read(cx)
4826            .repo_snapshots(cx);
4827        let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?;
4828        worktree.read_with(cx, |tree, _| {
4829            utils::ReversibleIterable::new(
4830                GitTraversal::new(&repo_snapshots, tree.entries(true, 0usize)),
4831                reverse_search,
4832            )
4833            .find_single_ended(|ele| predicate(*ele, worktree_id))
4834            .map(|ele| ele.to_owned())
4835        })
4836    }
4837
4838    fn find_entry(
4839        &self,
4840        start: Option<&SelectedEntry>,
4841        reverse_search: bool,
4842        predicate: &dyn Fn(GitEntryRef, WorktreeId) -> bool,
4843        cx: &mut Context<Self>,
4844    ) -> Option<SelectedEntry> {
4845        let mut worktree_ids: Vec<_> = self
4846            .state
4847            .visible_entries
4848            .iter()
4849            .map(|worktree| worktree.worktree_id)
4850            .collect();
4851        let repo_snapshots = self
4852            .project
4853            .read(cx)
4854            .git_store()
4855            .read(cx)
4856            .repo_snapshots(cx);
4857
4858        let mut last_found: Option<SelectedEntry> = None;
4859
4860        if let Some(start) = start {
4861            let worktree = self
4862                .project
4863                .read(cx)
4864                .worktree_for_id(start.worktree_id, cx)?
4865                .read(cx);
4866
4867            let search = {
4868                let entry = worktree.entry_for_id(start.entry_id)?;
4869                let root_entry = worktree.root_entry()?;
4870                let tree_id = worktree.id();
4871
4872                let mut first_iter = GitTraversal::new(
4873                    &repo_snapshots,
4874                    worktree.traverse_from_path(true, true, true, entry.path.as_ref()),
4875                );
4876
4877                if reverse_search {
4878                    first_iter.next();
4879                }
4880
4881                let first = first_iter
4882                    .enumerate()
4883                    .take_until(|(count, entry)| entry.entry == root_entry && *count != 0usize)
4884                    .map(|(_, entry)| entry)
4885                    .find(|ele| predicate(*ele, tree_id))
4886                    .map(|ele| ele.to_owned());
4887
4888                let second_iter =
4889                    GitTraversal::new(&repo_snapshots, worktree.entries(true, 0usize));
4890
4891                let second = if reverse_search {
4892                    second_iter
4893                        .take_until(|ele| ele.id == start.entry_id)
4894                        .filter(|ele| predicate(*ele, tree_id))
4895                        .last()
4896                        .map(|ele| ele.to_owned())
4897                } else {
4898                    second_iter
4899                        .take_while(|ele| ele.id != start.entry_id)
4900                        .filter(|ele| predicate(*ele, tree_id))
4901                        .last()
4902                        .map(|ele| ele.to_owned())
4903                };
4904
4905                if reverse_search {
4906                    Some((second, first))
4907                } else {
4908                    Some((first, second))
4909                }
4910            };
4911
4912            if let Some((first, second)) = search {
4913                let first = first.map(|entry| SelectedEntry {
4914                    worktree_id: start.worktree_id,
4915                    entry_id: entry.id,
4916                });
4917
4918                let second = second.map(|entry| SelectedEntry {
4919                    worktree_id: start.worktree_id,
4920                    entry_id: entry.id,
4921                });
4922
4923                if first.is_some() {
4924                    return first;
4925                }
4926                last_found = second;
4927
4928                let idx = worktree_ids
4929                    .iter()
4930                    .enumerate()
4931                    .find(|(_, ele)| **ele == start.worktree_id)
4932                    .map(|(idx, _)| idx);
4933
4934                if let Some(idx) = idx {
4935                    worktree_ids.rotate_left(idx + 1usize);
4936                    worktree_ids.pop();
4937                }
4938            }
4939        }
4940
4941        for tree_id in worktree_ids.into_iter() {
4942            if let Some(found) =
4943                self.find_entry_in_worktree(tree_id, reverse_search, false, &predicate, cx)
4944            {
4945                return Some(SelectedEntry {
4946                    worktree_id: tree_id,
4947                    entry_id: found.id,
4948                });
4949            }
4950        }
4951
4952        last_found
4953    }
4954
4955    fn find_visible_entry(
4956        &self,
4957        start: Option<&SelectedEntry>,
4958        reverse_search: bool,
4959        predicate: &dyn Fn(GitEntryRef, WorktreeId) -> bool,
4960        cx: &mut Context<Self>,
4961    ) -> Option<SelectedEntry> {
4962        let mut worktree_ids: Vec<_> = self
4963            .state
4964            .visible_entries
4965            .iter()
4966            .map(|worktree| worktree.worktree_id)
4967            .collect();
4968
4969        let mut last_found: Option<SelectedEntry> = None;
4970
4971        if let Some(start) = start {
4972            let entries = self
4973                .state
4974                .visible_entries
4975                .iter()
4976                .find(|worktree| worktree.worktree_id == start.worktree_id)
4977                .map(|worktree| &worktree.entries)?;
4978
4979            let mut start_idx = entries
4980                .iter()
4981                .enumerate()
4982                .find(|(_, ele)| ele.id == start.entry_id)
4983                .map(|(idx, _)| idx)?;
4984
4985            if reverse_search {
4986                start_idx = start_idx.saturating_add(1usize);
4987            }
4988
4989            let (left, right) = entries.split_at_checked(start_idx)?;
4990
4991            let (first_iter, second_iter) = if reverse_search {
4992                (
4993                    utils::ReversibleIterable::new(left.iter(), reverse_search),
4994                    utils::ReversibleIterable::new(right.iter(), reverse_search),
4995                )
4996            } else {
4997                (
4998                    utils::ReversibleIterable::new(right.iter(), reverse_search),
4999                    utils::ReversibleIterable::new(left.iter(), reverse_search),
5000                )
5001            };
5002
5003            let first_search = first_iter.find(|ele| predicate(ele.to_ref(), start.worktree_id));
5004            let second_search = second_iter.find(|ele| predicate(ele.to_ref(), start.worktree_id));
5005
5006            if first_search.is_some() {
5007                return first_search.map(|entry| SelectedEntry {
5008                    worktree_id: start.worktree_id,
5009                    entry_id: entry.id,
5010                });
5011            }
5012
5013            last_found = second_search.map(|entry| SelectedEntry {
5014                worktree_id: start.worktree_id,
5015                entry_id: entry.id,
5016            });
5017
5018            let idx = worktree_ids
5019                .iter()
5020                .enumerate()
5021                .find(|(_, ele)| **ele == start.worktree_id)
5022                .map(|(idx, _)| idx);
5023
5024            if let Some(idx) = idx {
5025                worktree_ids.rotate_left(idx + 1usize);
5026                worktree_ids.pop();
5027            }
5028        }
5029
5030        for tree_id in worktree_ids.into_iter() {
5031            if let Some(found) =
5032                self.find_entry_in_worktree(tree_id, reverse_search, true, &predicate, cx)
5033            {
5034                return Some(SelectedEntry {
5035                    worktree_id: tree_id,
5036                    entry_id: found.id,
5037                });
5038            }
5039        }
5040
5041        last_found
5042    }
5043
5044    fn calculate_depth_and_difference(
5045        entry: &Entry,
5046        visible_worktree_entries: &HashSet<Arc<RelPath>>,
5047    ) -> (usize, usize) {
5048        let (depth, difference) = entry
5049            .path
5050            .ancestors()
5051            .skip(1) // Skip the entry itself
5052            .find_map(|ancestor| {
5053                if let Some(parent_entry) = visible_worktree_entries.get(ancestor) {
5054                    let entry_path_components_count = entry.path.components().count();
5055                    let parent_path_components_count = parent_entry.components().count();
5056                    let difference = entry_path_components_count - parent_path_components_count;
5057                    let depth = parent_entry
5058                        .ancestors()
5059                        .skip(1)
5060                        .filter(|ancestor| visible_worktree_entries.contains(*ancestor))
5061                        .count();
5062                    Some((depth + 1, difference))
5063                } else {
5064                    None
5065                }
5066            })
5067            .unwrap_or_else(|| (0, entry.path.components().count()));
5068
5069        (depth, difference)
5070    }
5071
5072    fn highlight_entry_for_external_drag(
5073        &self,
5074        target_entry: &Entry,
5075        target_worktree: &Worktree,
5076    ) -> Option<ProjectEntryId> {
5077        // Always highlight directory or parent directory if it's file
5078        if target_entry.is_dir() {
5079            Some(target_entry.id)
5080        } else {
5081            target_entry
5082                .path
5083                .parent()
5084                .and_then(|parent_path| target_worktree.entry_for_path(parent_path))
5085                .map(|parent_entry| parent_entry.id)
5086        }
5087    }
5088
5089    fn highlight_entry_for_selection_drag(
5090        &self,
5091        target_entry: &Entry,
5092        target_worktree: &Worktree,
5093        drag_state: &DraggedSelection,
5094        cx: &Context<Self>,
5095    ) -> Option<ProjectEntryId> {
5096        let target_parent_path = target_entry.path.parent();
5097
5098        // In case of single item drag, we do not highlight existing
5099        // directory which item belongs too
5100        if drag_state.items().count() == 1
5101            && drag_state.active_selection.worktree_id == target_worktree.id()
5102        {
5103            let active_entry_path = self
5104                .project
5105                .read(cx)
5106                .path_for_entry(drag_state.active_selection.entry_id, cx)?;
5107
5108            if let Some(active_parent_path) = active_entry_path.path.parent() {
5109                // Do not highlight active entry parent
5110                if active_parent_path == target_entry.path.as_ref() {
5111                    return None;
5112                }
5113
5114                // Do not highlight active entry sibling files
5115                if Some(active_parent_path) == target_parent_path && target_entry.is_file() {
5116                    return None;
5117                }
5118            }
5119        }
5120
5121        // Always highlight directory or parent directory if it's file
5122        if target_entry.is_dir() {
5123            Some(target_entry.id)
5124        } else {
5125            target_parent_path
5126                .and_then(|parent_path| target_worktree.entry_for_path(parent_path))
5127                .map(|parent_entry| parent_entry.id)
5128        }
5129    }
5130
5131    fn should_highlight_background_for_selection_drag(
5132        &self,
5133        drag_state: &DraggedSelection,
5134        last_root_id: ProjectEntryId,
5135        cx: &App,
5136    ) -> bool {
5137        // Always highlight for multiple entries
5138        if drag_state.items().count() > 1 {
5139            return true;
5140        }
5141
5142        // Since root will always have empty relative path
5143        if let Some(entry_path) = self
5144            .project
5145            .read(cx)
5146            .path_for_entry(drag_state.active_selection.entry_id, cx)
5147        {
5148            if let Some(parent_path) = entry_path.path.parent() {
5149                if !parent_path.is_empty() {
5150                    return true;
5151                }
5152            }
5153        }
5154
5155        // If parent is empty, check if different worktree
5156        if let Some(last_root_worktree_id) = self
5157            .project
5158            .read(cx)
5159            .worktree_id_for_entry(last_root_id, cx)
5160        {
5161            if drag_state.active_selection.worktree_id != last_root_worktree_id {
5162                return true;
5163            }
5164        }
5165
5166        false
5167    }
5168
5169    fn render_entry(
5170        &self,
5171        entry_id: ProjectEntryId,
5172        details: EntryDetails,
5173        window: &mut Window,
5174        cx: &mut Context<Self>,
5175    ) -> Stateful<Div> {
5176        const GROUP_NAME: &str = "project_entry";
5177
5178        let kind = details.kind;
5179        let is_sticky = details.sticky.is_some();
5180        let sticky_index = details.sticky.as_ref().map(|this| this.sticky_index);
5181        let settings = ProjectPanelSettings::get_global(cx);
5182        let show_editor = details.is_editing && !details.is_processing;
5183
5184        let selection = SelectedEntry {
5185            worktree_id: details.worktree_id,
5186            entry_id,
5187        };
5188
5189        let is_marked = self.marked_entries.contains(&selection);
5190        let is_active = self
5191            .selection
5192            .is_some_and(|selection| selection.entry_id == entry_id);
5193
5194        let file_name = details.filename.clone();
5195
5196        let mut icon = details.icon.clone();
5197        if settings.file_icons && show_editor && details.kind.is_file() {
5198            let filename = self.filename_editor.read(cx).text(cx);
5199            if filename.len() > 2 {
5200                icon = FileIcons::get_icon(Path::new(&filename), cx);
5201            }
5202        }
5203
5204        let filename_text_color = details.filename_text_color;
5205        let diagnostic_severity = details.diagnostic_severity;
5206        let diagnostic_count = details.diagnostic_count;
5207        let item_colors = get_item_color(is_sticky, cx);
5208
5209        let canonical_path = details
5210            .canonical_path
5211            .as_ref()
5212            .map(|f| f.to_string_lossy().into_owned());
5213        let path_style = self.project.read(cx).path_style(cx);
5214        let path = details.path.clone();
5215        let path_for_external_paths = path.clone();
5216        let path_for_dragged_selection = path.clone();
5217
5218        let depth = details.depth;
5219        let worktree_id = details.worktree_id;
5220        let dragged_selection = DraggedSelection {
5221            active_selection: SelectedEntry {
5222                worktree_id: selection.worktree_id,
5223                entry_id: selection.entry_id,
5224            },
5225            marked_selections: Arc::from(self.marked_entries.clone()),
5226        };
5227
5228        let bg_color = if is_marked {
5229            item_colors.marked
5230        } else {
5231            item_colors.default
5232        };
5233
5234        let bg_hover_color = if is_marked {
5235            item_colors.marked
5236        } else {
5237            item_colors.hover
5238        };
5239
5240        let validation_color_and_message = if show_editor {
5241            match self
5242                .state
5243                .edit_state
5244                .as_ref()
5245                .map_or(ValidationState::None, |e| e.validation_state.clone())
5246            {
5247                ValidationState::Error(msg) => Some((Color::Error.color(cx), msg)),
5248                ValidationState::Warning(msg) => Some((Color::Warning.color(cx), msg)),
5249                ValidationState::None => None,
5250            }
5251        } else {
5252            None
5253        };
5254
5255        let border_color =
5256            if !self.mouse_down && is_active && self.focus_handle.contains_focused(window, cx) {
5257                match validation_color_and_message {
5258                    Some((color, _)) => color,
5259                    None => item_colors.focused,
5260                }
5261            } else {
5262                bg_color
5263            };
5264
5265        let border_hover_color =
5266            if !self.mouse_down && is_active && self.focus_handle.contains_focused(window, cx) {
5267                match validation_color_and_message {
5268                    Some((color, _)) => color,
5269                    None => item_colors.focused,
5270                }
5271            } else {
5272                bg_hover_color
5273            };
5274
5275        let folded_directory_drag_target = self.folded_directory_drag_target;
5276        let is_highlighted = {
5277            if let Some(highlight_entry_id) =
5278                self.drag_target_entry
5279                    .as_ref()
5280                    .and_then(|drag_target| match drag_target {
5281                        DragTarget::Entry {
5282                            highlight_entry_id, ..
5283                        } => Some(*highlight_entry_id),
5284                        DragTarget::Background => self.state.last_worktree_root_id,
5285                    })
5286            {
5287                // Highlight if same entry or it's children
5288                if entry_id == highlight_entry_id {
5289                    true
5290                } else {
5291                    maybe!({
5292                        let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?;
5293                        let highlight_entry = worktree.read(cx).entry_for_id(highlight_entry_id)?;
5294                        Some(path.starts_with(&highlight_entry.path))
5295                    })
5296                    .unwrap_or(false)
5297                }
5298            } else {
5299                false
5300            }
5301        };
5302        let git_indicator = settings
5303            .git_status_indicator
5304            .then(|| git_status_indicator(details.git_status))
5305            .flatten();
5306
5307        let id: ElementId = if is_sticky {
5308            SharedString::from(format!("project_panel_sticky_item_{}", entry_id.to_usize())).into()
5309        } else {
5310            (entry_id.to_proto() as usize).into()
5311        };
5312
5313        div()
5314            .id(id.clone())
5315            .relative()
5316            .group(GROUP_NAME)
5317            .cursor_pointer()
5318            .rounded_none()
5319            .bg(bg_color)
5320            .border_1()
5321            .border_r_2()
5322            .border_color(border_color)
5323            .hover(|style| style.bg(bg_hover_color).border_color(border_hover_color))
5324            .when(is_sticky, |this| this.block_mouse_except_scroll())
5325            .when(!is_sticky, |this| {
5326                this.when(
5327                    is_highlighted && folded_directory_drag_target.is_none(),
5328                    |this| {
5329                        this.border_color(transparent_white())
5330                            .bg(item_colors.drag_over)
5331                    },
5332                )
5333                .when(settings.drag_and_drop, |this| {
5334                    this.on_drag_move::<ExternalPaths>(cx.listener(
5335                        move |this, event: &DragMoveEvent<ExternalPaths>, _, cx| {
5336                            let is_current_target =
5337                                this.drag_target_entry
5338                                    .as_ref()
5339                                    .and_then(|entry| match entry {
5340                                        DragTarget::Entry {
5341                                            entry_id: target_id,
5342                                            ..
5343                                        } => Some(*target_id),
5344                                        DragTarget::Background { .. } => None,
5345                                    })
5346                                    == Some(entry_id);
5347
5348                            if !event.bounds.contains(&event.event.position) {
5349                                // Entry responsible for setting drag target is also responsible to
5350                                // clear it up after drag is out of bounds
5351                                if is_current_target {
5352                                    this.drag_target_entry = None;
5353                                }
5354                                return;
5355                            }
5356
5357                            if is_current_target {
5358                                return;
5359                            }
5360
5361                            this.marked_entries.clear();
5362
5363                            let Some((entry_id, highlight_entry_id)) = maybe!({
5364                                let target_worktree = this
5365                                    .project
5366                                    .read(cx)
5367                                    .worktree_for_id(selection.worktree_id, cx)?
5368                                    .read(cx);
5369                                let target_entry =
5370                                    target_worktree.entry_for_path(&path_for_external_paths)?;
5371                                let highlight_entry_id = this.highlight_entry_for_external_drag(
5372                                    target_entry,
5373                                    target_worktree,
5374                                )?;
5375                                Some((target_entry.id, highlight_entry_id))
5376                            }) else {
5377                                return;
5378                            };
5379
5380                            this.drag_target_entry = Some(DragTarget::Entry {
5381                                entry_id,
5382                                highlight_entry_id,
5383                            });
5384                        },
5385                    ))
5386                    .on_drop(cx.listener(
5387                        move |this, external_paths: &ExternalPaths, window, cx| {
5388                            this.drag_target_entry = None;
5389                            this.hover_scroll_task.take();
5390                            this.drop_external_files(external_paths.paths(), entry_id, window, cx);
5391                            cx.stop_propagation();
5392                        },
5393                    ))
5394                    .on_drag_move::<DraggedSelection>(cx.listener(
5395                        move |this, event: &DragMoveEvent<DraggedSelection>, window, cx| {
5396                            let is_current_target =
5397                                this.drag_target_entry
5398                                    .as_ref()
5399                                    .and_then(|entry| match entry {
5400                                        DragTarget::Entry {
5401                                            entry_id: target_id,
5402                                            ..
5403                                        } => Some(*target_id),
5404                                        DragTarget::Background { .. } => None,
5405                                    })
5406                                    == Some(entry_id);
5407
5408                            if !event.bounds.contains(&event.event.position) {
5409                                // Entry responsible for setting drag target is also responsible to
5410                                // clear it up after drag is out of bounds
5411                                if is_current_target {
5412                                    this.drag_target_entry = None;
5413                                }
5414                                return;
5415                            }
5416
5417                            if is_current_target {
5418                                return;
5419                            }
5420
5421                            let drag_state = event.drag(cx);
5422
5423                            if drag_state.items().count() == 1 {
5424                                this.marked_entries.clear();
5425                                this.marked_entries.push(drag_state.active_selection);
5426                            }
5427
5428                            let Some((entry_id, highlight_entry_id)) = maybe!({
5429                                let target_worktree = this
5430                                    .project
5431                                    .read(cx)
5432                                    .worktree_for_id(selection.worktree_id, cx)?
5433                                    .read(cx);
5434                                let target_entry =
5435                                    target_worktree.entry_for_path(&path_for_dragged_selection)?;
5436                                let highlight_entry_id = this.highlight_entry_for_selection_drag(
5437                                    target_entry,
5438                                    target_worktree,
5439                                    drag_state,
5440                                    cx,
5441                                )?;
5442                                Some((target_entry.id, highlight_entry_id))
5443                            }) else {
5444                                return;
5445                            };
5446
5447                            this.drag_target_entry = Some(DragTarget::Entry {
5448                                entry_id,
5449                                highlight_entry_id,
5450                            });
5451
5452                            this.hover_expand_task.take();
5453
5454                            if !kind.is_dir()
5455                                || this
5456                                    .state
5457                                    .expanded_dir_ids
5458                                    .get(&details.worktree_id)
5459                                    .is_some_and(|ids| ids.binary_search(&entry_id).is_ok())
5460                            {
5461                                return;
5462                            }
5463
5464                            let bounds = event.bounds;
5465                            this.hover_expand_task =
5466                                Some(cx.spawn_in(window, async move |this, cx| {
5467                                    cx.background_executor()
5468                                        .timer(Duration::from_millis(500))
5469                                        .await;
5470                                    this.update_in(cx, |this, window, cx| {
5471                                        this.hover_expand_task.take();
5472                                        if this.drag_target_entry.as_ref().and_then(|entry| {
5473                                            match entry {
5474                                                DragTarget::Entry {
5475                                                    entry_id: target_id,
5476                                                    ..
5477                                                } => Some(*target_id),
5478                                                DragTarget::Background { .. } => None,
5479                                            }
5480                                        }) == Some(entry_id)
5481                                            && bounds.contains(&window.mouse_position())
5482                                        {
5483                                            this.expand_entry(worktree_id, entry_id, cx);
5484                                            this.update_visible_entries(
5485                                                Some((worktree_id, entry_id)),
5486                                                false,
5487                                                false,
5488                                                window,
5489                                                cx,
5490                                            );
5491                                            cx.notify();
5492                                        }
5493                                    })
5494                                    .ok();
5495                                }));
5496                        },
5497                    ))
5498                    .on_drag(dragged_selection, {
5499                        let active_component =
5500                            self.state.ancestors.get(&entry_id).and_then(|ancestors| {
5501                                ancestors.active_component(&details.filename)
5502                            });
5503                        move |selection, click_offset, _window, cx| {
5504                            let filename = active_component
5505                                .as_ref()
5506                                .unwrap_or_else(|| &details.filename);
5507                            cx.new(|_| DraggedProjectEntryView {
5508                                icon: details.icon.clone(),
5509                                filename: filename.clone(),
5510                                click_offset,
5511                                selection: selection.active_selection,
5512                                selections: selection.marked_selections.clone(),
5513                            })
5514                        }
5515                    })
5516                    .on_drop(cx.listener(
5517                        move |this, selections: &DraggedSelection, window, cx| {
5518                            this.drag_target_entry = None;
5519                            this.hover_scroll_task.take();
5520                            this.hover_expand_task.take();
5521                            if folded_directory_drag_target.is_some() {
5522                                return;
5523                            }
5524                            this.drag_onto(selections, entry_id, kind.is_file(), window, cx);
5525                        },
5526                    ))
5527                })
5528            })
5529            .on_mouse_down(
5530                MouseButton::Left,
5531                cx.listener(move |this, _, _, cx| {
5532                    this.mouse_down = true;
5533                    cx.propagate();
5534                }),
5535            )
5536            .on_click(
5537                cx.listener(move |project_panel, event: &gpui::ClickEvent, window, cx| {
5538                    if event.is_right_click() || event.first_focus() || show_editor {
5539                        return;
5540                    }
5541                    if event.standard_click() {
5542                        project_panel.mouse_down = false;
5543                    }
5544                    cx.stop_propagation();
5545
5546                    if let Some(selection) =
5547                        project_panel.selection.filter(|_| event.modifiers().shift)
5548                    {
5549                        let current_selection = project_panel.index_for_selection(selection);
5550                        let clicked_entry = SelectedEntry {
5551                            entry_id,
5552                            worktree_id,
5553                        };
5554                        let target_selection = project_panel.index_for_selection(clicked_entry);
5555                        if let Some(((_, _, source_index), (_, _, target_index))) =
5556                            current_selection.zip(target_selection)
5557                        {
5558                            let range_start = source_index.min(target_index);
5559                            let range_end = source_index.max(target_index) + 1;
5560                            let mut new_selections = Vec::new();
5561                            project_panel.for_each_visible_entry(
5562                                range_start..range_end,
5563                                window,
5564                                cx,
5565                                &mut |entry_id, details, _, _| {
5566                                    new_selections.push(SelectedEntry {
5567                                        entry_id,
5568                                        worktree_id: details.worktree_id,
5569                                    });
5570                                },
5571                            );
5572
5573                            for selection in &new_selections {
5574                                if !project_panel.marked_entries.contains(selection) {
5575                                    project_panel.marked_entries.push(*selection);
5576                                }
5577                            }
5578
5579                            project_panel.selection = Some(clicked_entry);
5580                            if !project_panel.marked_entries.contains(&clicked_entry) {
5581                                project_panel.marked_entries.push(clicked_entry);
5582                            }
5583                        }
5584                    } else if event.modifiers().secondary() {
5585                        if event.click_count() > 1 {
5586                            project_panel.split_entry(entry_id, false, None, cx);
5587                        } else {
5588                            project_panel.selection = Some(selection);
5589                            if let Some(position) = project_panel
5590                                .marked_entries
5591                                .iter()
5592                                .position(|e| *e == selection)
5593                            {
5594                                project_panel.marked_entries.remove(position);
5595                            } else {
5596                                project_panel.marked_entries.push(selection);
5597                            }
5598                        }
5599                    } else if kind.is_dir() {
5600                        project_panel.marked_entries.clear();
5601                        if is_sticky
5602                            && let Some((_, _, index)) =
5603                                project_panel.index_for_entry(entry_id, worktree_id)
5604                        {
5605                            project_panel
5606                                .scroll_handle
5607                                .scroll_to_item_strict_with_offset(
5608                                    index,
5609                                    ScrollStrategy::Top,
5610                                    sticky_index.unwrap_or(0),
5611                                );
5612                            cx.notify();
5613                            // move down by 1px so that clicked item
5614                            // don't count as sticky anymore
5615                            cx.on_next_frame(window, |_, window, cx| {
5616                                cx.on_next_frame(window, |this, _, cx| {
5617                                    let mut offset = this.scroll_handle.offset();
5618                                    offset.y += px(1.);
5619                                    this.scroll_handle.set_offset(offset);
5620                                    cx.notify();
5621                                });
5622                            });
5623                            return;
5624                        }
5625                        if event.modifiers().alt {
5626                            project_panel.toggle_expand_all(entry_id, window, cx);
5627                        } else {
5628                            project_panel.toggle_expanded(entry_id, window, cx);
5629                        }
5630                    } else {
5631                        let preview_tabs_enabled =
5632                            PreviewTabsSettings::get_global(cx).enable_preview_from_project_panel;
5633                        let click_count = event.click_count();
5634                        let focus_opened_item = click_count > 1;
5635                        let allow_preview = preview_tabs_enabled && click_count == 1;
5636                        project_panel.open_entry(entry_id, focus_opened_item, allow_preview, cx);
5637                    }
5638                }),
5639            )
5640            .child(
5641                ListItem::new(id)
5642                    .indent_level(depth)
5643                    .indent_step_size(px(settings.indent_size))
5644                    .spacing(match settings.entry_spacing {
5645                        ProjectPanelEntrySpacing::Comfortable => ListItemSpacing::Dense,
5646                        ProjectPanelEntrySpacing::Standard => ListItemSpacing::ExtraDense,
5647                    })
5648                    .selectable(false)
5649                    .when(
5650                        canonical_path.is_some()
5651                            || diagnostic_count.is_some()
5652                            || git_indicator.is_some(),
5653                        |this| {
5654                            let symlink_element = canonical_path.map(|path| {
5655                                div()
5656                                    .id("symlink_icon")
5657                                    .tooltip(move |_window, cx| {
5658                                        Tooltip::with_meta(
5659                                            path.to_string(),
5660                                            None,
5661                                            "Symbolic Link",
5662                                            cx,
5663                                        )
5664                                    })
5665                                    .child(
5666                                        Icon::new(IconName::ArrowUpRight)
5667                                            .size(IconSize::Indicator)
5668                                            .color(filename_text_color),
5669                                    )
5670                            });
5671                            this.end_slot::<AnyElement>(
5672                                h_flex()
5673                                    .gap_1()
5674                                    .flex_none()
5675                                    .pr_3()
5676                                    .when_some(diagnostic_count, |this, count| {
5677                                        this.when(count.error_count > 0, |this| {
5678                                            this.child(
5679                                                Label::new(count.capped_error_count())
5680                                                    .size(LabelSize::Small)
5681                                                    .color(Color::Error),
5682                                            )
5683                                        })
5684                                        .when(
5685                                            count.warning_count > 0,
5686                                            |this| {
5687                                                this.child(
5688                                                    Label::new(count.capped_warning_count())
5689                                                        .size(LabelSize::Small)
5690                                                        .color(Color::Warning),
5691                                                )
5692                                            },
5693                                        )
5694                                    })
5695                                    .when_some(git_indicator, |this, (label, color)| {
5696                                        let git_indicator = if kind.is_dir() {
5697                                            Indicator::dot()
5698                                                .color(Color::Custom(color.color(cx).opacity(0.5)))
5699                                                .into_any_element()
5700                                        } else {
5701                                            Label::new(label)
5702                                                .size(LabelSize::Small)
5703                                                .color(color)
5704                                                .into_any_element()
5705                                        };
5706
5707                                        this.child(git_indicator)
5708                                    })
5709                                    .when_some(symlink_element, |this, el| this.child(el))
5710                                    .into_any_element(),
5711                            )
5712                        },
5713                    )
5714                    .child(if let Some(icon) = &icon {
5715                        if let Some((_, decoration_color)) =
5716                            entry_diagnostic_aware_icon_decoration_and_color(diagnostic_severity)
5717                        {
5718                            let is_warning = diagnostic_severity
5719                                .map(|severity| matches!(severity, DiagnosticSeverity::WARNING))
5720                                .unwrap_or(false);
5721                            div().child(
5722                                DecoratedIcon::new(
5723                                    Icon::from_path(icon.clone()).color(Color::Muted),
5724                                    Some(
5725                                        IconDecoration::new(
5726                                            if kind.is_file() {
5727                                                if is_warning {
5728                                                    IconDecorationKind::Triangle
5729                                                } else {
5730                                                    IconDecorationKind::X
5731                                                }
5732                                            } else {
5733                                                IconDecorationKind::Dot
5734                                            },
5735                                            bg_color,
5736                                            cx,
5737                                        )
5738                                        .group_name(Some(GROUP_NAME.into()))
5739                                        .knockout_hover_color(bg_hover_color)
5740                                        .color(decoration_color.color(cx))
5741                                        .position(Point {
5742                                            x: px(-2.),
5743                                            y: px(-2.),
5744                                        }),
5745                                    ),
5746                                )
5747                                .into_any_element(),
5748                            )
5749                        } else {
5750                            h_flex().child(Icon::from_path(icon.to_string()).color(Color::Muted))
5751                        }
5752                    } else if let Some((icon_name, color)) =
5753                        entry_diagnostic_aware_icon_name_and_color(diagnostic_severity)
5754                    {
5755                        h_flex()
5756                            .size(IconSize::default().rems())
5757                            .child(Icon::new(icon_name).color(color).size(IconSize::Small))
5758                    } else {
5759                        h_flex()
5760                            .size(IconSize::default().rems())
5761                            .invisible()
5762                            .flex_none()
5763                    })
5764                    .child(if show_editor {
5765                        h_flex().h_6().w_full().child(self.filename_editor.clone())
5766                    } else {
5767                        h_flex()
5768                            .h_6()
5769                            .map(|this| match self.state.ancestors.get(&entry_id) {
5770                                Some(folded_ancestors) => {
5771                                    this.children(self.render_folder_elements(
5772                                        folded_ancestors,
5773                                        entry_id,
5774                                        file_name,
5775                                        path_style,
5776                                        is_sticky,
5777                                        kind.is_file(),
5778                                        is_active || is_marked,
5779                                        settings.drag_and_drop,
5780                                        settings.bold_folder_labels,
5781                                        item_colors.drag_over,
5782                                        folded_directory_drag_target,
5783                                        filename_text_color,
5784                                        cx,
5785                                    ))
5786                                }
5787
5788                                None => this.child(
5789                                    Label::new(file_name)
5790                                        .single_line()
5791                                        .color(filename_text_color)
5792                                        .when(
5793                                            settings.bold_folder_labels && kind.is_dir(),
5794                                            |this| this.weight(FontWeight::SEMIBOLD),
5795                                        )
5796                                        .into_any_element(),
5797                                ),
5798                            })
5799                    })
5800                    .on_secondary_mouse_down(cx.listener(
5801                        move |this, event: &MouseDownEvent, window, cx| {
5802                            // Stop propagation to prevent the catch-all context menu for the project
5803                            // panel from being deployed.
5804                            cx.stop_propagation();
5805                            // Some context menu actions apply to all marked entries. If the user
5806                            // right-clicks on an entry that is not marked, they may not realize the
5807                            // action applies to multiple entries. To avoid inadvertent changes, all
5808                            // entries are unmarked.
5809                            if !this.marked_entries.contains(&selection) {
5810                                this.marked_entries.clear();
5811                            }
5812                            this.deploy_context_menu(event.position, entry_id, window, cx);
5813                        },
5814                    ))
5815                    .overflow_x(),
5816            )
5817            .when_some(validation_color_and_message, |this, (color, message)| {
5818                this.relative().child(deferred(
5819                    div()
5820                        .occlude()
5821                        .absolute()
5822                        .top_full()
5823                        .left(px(-1.)) // Used px over rem so that it doesn't change with font size
5824                        .right(px(-0.5))
5825                        .py_1()
5826                        .px_2()
5827                        .border_1()
5828                        .border_color(color)
5829                        .bg(cx.theme().colors().background)
5830                        .child(
5831                            Label::new(message)
5832                                .color(Color::from(color))
5833                                .size(LabelSize::Small),
5834                        ),
5835                ))
5836            })
5837    }
5838
5839    fn render_folder_elements(
5840        &self,
5841        folded_ancestors: &FoldedAncestors,
5842        entry_id: ProjectEntryId,
5843        file_name: String,
5844        path_style: PathStyle,
5845        is_sticky: bool,
5846        is_file: bool,
5847        is_active_or_marked: bool,
5848        drag_and_drop_enabled: bool,
5849        bold_folder_labels: bool,
5850        drag_over_color: Hsla,
5851        folded_directory_drag_target: Option<FoldedDirectoryDragTarget>,
5852        filename_text_color: Color,
5853        cx: &Context<Self>,
5854    ) -> impl Iterator<Item = AnyElement> {
5855        let components = Path::new(&file_name)
5856            .components()
5857            .map(|comp| comp.as_os_str().to_string_lossy().into_owned())
5858            .collect::<Vec<_>>();
5859        let active_index = folded_ancestors.active_index();
5860        let components_len = components.len();
5861        let delimiter = SharedString::new(path_style.primary_separator());
5862
5863        let path_component_elements =
5864            components
5865                .into_iter()
5866                .enumerate()
5867                .map(move |(index, component)| {
5868                    div()
5869                        .id(SharedString::from(format!(
5870                            "project_panel_path_component_{}_{index}",
5871                            entry_id.to_usize()
5872                        )))
5873                        .when(index == 0, |this| this.ml_neg_0p5())
5874                        .px_0p5()
5875                        .rounded_xs()
5876                        .hover(|style| style.bg(cx.theme().colors().element_active))
5877                        .when(!is_sticky, |div| {
5878                            div.when(index != components_len - 1, |div| {
5879                                let target_entry_id = folded_ancestors
5880                                    .ancestors
5881                                    .get(components_len - 1 - index)
5882                                    .cloned();
5883                                div.when(drag_and_drop_enabled, |div| {
5884                                    div.on_drag_move(cx.listener(
5885                                        move |this,
5886                                              event: &DragMoveEvent<DraggedSelection>,
5887                                              _,
5888                                              _| {
5889                                            if event.bounds.contains(&event.event.position) {
5890                                                this.folded_directory_drag_target =
5891                                                    Some(FoldedDirectoryDragTarget {
5892                                                        entry_id,
5893                                                        index,
5894                                                        is_delimiter_target: false,
5895                                                    });
5896                                            } else {
5897                                                let is_current_target = this
5898                                                    .folded_directory_drag_target
5899                                                    .as_ref()
5900                                                    .is_some_and(|target| {
5901                                                        target.entry_id == entry_id
5902                                                            && target.index == index
5903                                                            && !target.is_delimiter_target
5904                                                    });
5905                                                if is_current_target {
5906                                                    this.folded_directory_drag_target = None;
5907                                                }
5908                                            }
5909                                        },
5910                                    ))
5911                                    .on_drop(cx.listener(
5912                                        move |this, selections: &DraggedSelection, window, cx| {
5913                                            this.hover_scroll_task.take();
5914                                            this.drag_target_entry = None;
5915                                            this.folded_directory_drag_target = None;
5916                                            if let Some(target_entry_id) = target_entry_id {
5917                                                this.drag_onto(
5918                                                    selections,
5919                                                    target_entry_id,
5920                                                    is_file,
5921                                                    window,
5922                                                    cx,
5923                                                );
5924                                            }
5925                                        },
5926                                    ))
5927                                    .when(
5928                                        folded_directory_drag_target.is_some_and(|target| {
5929                                            target.entry_id == entry_id && target.index == index
5930                                        }),
5931                                        |this| this.bg(drag_over_color),
5932                                    )
5933                                })
5934                            })
5935                        })
5936                        .on_mouse_down(
5937                            MouseButton::Left,
5938                            cx.listener(move |this, _, _, cx| {
5939                                if let Some(folds) = this.state.ancestors.get_mut(&entry_id) {
5940                                    if folds.set_active_index(index) {
5941                                        cx.notify();
5942                                    }
5943                                }
5944                            }),
5945                        )
5946                        .on_mouse_down(
5947                            MouseButton::Right,
5948                            cx.listener(move |this, _, _, cx| {
5949                                if let Some(folds) = this.state.ancestors.get_mut(&entry_id) {
5950                                    if folds.set_active_index(index) {
5951                                        cx.notify();
5952                                    }
5953                                }
5954                            }),
5955                        )
5956                        .child(
5957                            Label::new(component)
5958                                .single_line()
5959                                .color(filename_text_color)
5960                                .when(bold_folder_labels && !is_file, |this| {
5961                                    this.weight(FontWeight::SEMIBOLD)
5962                                })
5963                                .when(index == active_index && is_active_or_marked, |this| {
5964                                    this.underline()
5965                                }),
5966                        )
5967                        .into_any()
5968                });
5969
5970        let mut separator_index = 0;
5971        itertools::intersperse_with(path_component_elements, move || {
5972            separator_index += 1;
5973            self.render_entry_path_separator(
5974                entry_id,
5975                separator_index,
5976                components_len,
5977                is_sticky,
5978                is_file,
5979                drag_and_drop_enabled,
5980                filename_text_color,
5981                &delimiter,
5982                folded_ancestors,
5983                cx,
5984            )
5985            .into_any()
5986        })
5987    }
5988
5989    fn render_entry_path_separator(
5990        &self,
5991        entry_id: ProjectEntryId,
5992        index: usize,
5993        components_len: usize,
5994        is_sticky: bool,
5995        is_file: bool,
5996        drag_and_drop_enabled: bool,
5997        filename_text_color: Color,
5998        delimiter: &SharedString,
5999        folded_ancestors: &FoldedAncestors,
6000        cx: &Context<Self>,
6001    ) -> Div {
6002        let delimiter_target_index = index - 1;
6003        let target_entry_id = folded_ancestors
6004            .ancestors
6005            .get(components_len - 1 - delimiter_target_index)
6006            .cloned();
6007        div()
6008            .when(!is_sticky, |div| {
6009                div.when(drag_and_drop_enabled, |div| {
6010                    div.on_drop(cx.listener(
6011                        move |this, selections: &DraggedSelection, window, cx| {
6012                            this.hover_scroll_task.take();
6013                            this.drag_target_entry = None;
6014                            this.folded_directory_drag_target = None;
6015                            if let Some(target_entry_id) = target_entry_id {
6016                                this.drag_onto(selections, target_entry_id, is_file, window, cx);
6017                            }
6018                        },
6019                    ))
6020                    .on_drag_move(cx.listener(
6021                        move |this, event: &DragMoveEvent<DraggedSelection>, _, _| {
6022                            if event.bounds.contains(&event.event.position) {
6023                                this.folded_directory_drag_target =
6024                                    Some(FoldedDirectoryDragTarget {
6025                                        entry_id,
6026                                        index: delimiter_target_index,
6027                                        is_delimiter_target: true,
6028                                    });
6029                            } else {
6030                                let is_current_target =
6031                                    this.folded_directory_drag_target.is_some_and(|target| {
6032                                        target.entry_id == entry_id
6033                                            && target.index == delimiter_target_index
6034                                            && target.is_delimiter_target
6035                                    });
6036                                if is_current_target {
6037                                    this.folded_directory_drag_target = None;
6038                                }
6039                            }
6040                        },
6041                    ))
6042                })
6043            })
6044            .child(
6045                Label::new(delimiter.clone())
6046                    .single_line()
6047                    .color(filename_text_color),
6048            )
6049    }
6050
6051    fn details_for_entry(
6052        &self,
6053        entry: &Entry,
6054        worktree_id: WorktreeId,
6055        root_name: &RelPath,
6056        entries_paths: &HashSet<Arc<RelPath>>,
6057        git_status: GitSummary,
6058        sticky: Option<StickyDetails>,
6059        _window: &mut Window,
6060        cx: &mut Context<Self>,
6061    ) -> EntryDetails {
6062        let (show_file_icons, show_folder_icons) = {
6063            let settings = ProjectPanelSettings::get_global(cx);
6064            (settings.file_icons, settings.folder_icons)
6065        };
6066
6067        let expanded_entry_ids = self
6068            .state
6069            .expanded_dir_ids
6070            .get(&worktree_id)
6071            .map(Vec::as_slice)
6072            .unwrap_or(&[]);
6073        let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok();
6074
6075        let icon = match entry.kind {
6076            EntryKind::File => {
6077                if show_file_icons {
6078                    FileIcons::get_icon(entry.path.as_std_path(), cx)
6079                } else {
6080                    None
6081                }
6082            }
6083            _ => {
6084                if show_folder_icons {
6085                    FileIcons::get_folder_icon(is_expanded, entry.path.as_std_path(), cx)
6086                } else {
6087                    FileIcons::get_chevron_icon(is_expanded, cx)
6088                }
6089            }
6090        };
6091
6092        let path_style = self.project.read(cx).path_style(cx);
6093        let (depth, difference) =
6094            ProjectPanel::calculate_depth_and_difference(entry, entries_paths);
6095
6096        let filename = if difference > 1 {
6097            entry
6098                .path
6099                .last_n_components(difference)
6100                .map_or(String::new(), |suffix| {
6101                    suffix.display(path_style).to_string()
6102                })
6103        } else {
6104            entry
6105                .path
6106                .file_name()
6107                .map(|name| name.to_string())
6108                .unwrap_or_else(|| root_name.as_unix_str().to_string())
6109        };
6110
6111        let selection = SelectedEntry {
6112            worktree_id,
6113            entry_id: entry.id,
6114        };
6115        let is_marked = self.marked_entries.contains(&selection);
6116        let is_selected = self.selection == Some(selection);
6117
6118        let diagnostic_severity = self
6119            .diagnostics
6120            .get(&(worktree_id, entry.path.clone()))
6121            .cloned();
6122
6123        let diagnostic_count = self
6124            .diagnostic_counts
6125            .get(&(worktree_id, entry.path.clone()))
6126            .copied();
6127
6128        let filename_text_color =
6129            entry_git_aware_label_color(git_status, entry.is_ignored, is_marked);
6130
6131        let is_cut = self
6132            .clipboard
6133            .as_ref()
6134            .is_some_and(|e| e.is_cut() && e.items().contains(&selection));
6135
6136        EntryDetails {
6137            filename,
6138            icon,
6139            path: entry.path.clone(),
6140            depth,
6141            kind: entry.kind,
6142            is_ignored: entry.is_ignored,
6143            is_expanded,
6144            is_selected,
6145            is_marked,
6146            is_editing: false,
6147            is_processing: false,
6148            is_cut,
6149            sticky,
6150            filename_text_color,
6151            diagnostic_severity,
6152            diagnostic_count,
6153            git_status,
6154            is_private: entry.is_private,
6155            worktree_id,
6156            canonical_path: entry.canonical_path.clone(),
6157        }
6158    }
6159
6160    fn dispatch_context(&self, window: &Window, cx: &Context<Self>) -> KeyContext {
6161        let mut dispatch_context = KeyContext::new_with_defaults();
6162        dispatch_context.add("ProjectPanel");
6163        dispatch_context.add("menu");
6164
6165        let identifier = if self.filename_editor.focus_handle(cx).is_focused(window) {
6166            "editing"
6167        } else {
6168            "not_editing"
6169        };
6170
6171        dispatch_context.add(identifier);
6172        dispatch_context
6173    }
6174
6175    fn reveal_entry(
6176        &mut self,
6177        project: Entity<Project>,
6178        entry_id: ProjectEntryId,
6179        skip_ignored: bool,
6180        window: &mut Window,
6181        cx: &mut Context<Self>,
6182    ) -> Result<()> {
6183        let worktree = project
6184            .read(cx)
6185            .worktree_for_entry(entry_id, cx)
6186            .context("can't reveal a non-existent entry in the project panel")?;
6187        let worktree = worktree.read(cx);
6188        let worktree_id = worktree.id();
6189        let is_ignored = worktree
6190            .entry_for_id(entry_id)
6191            .is_none_or(|entry| entry.is_ignored && !entry.is_always_included);
6192        if skip_ignored && is_ignored {
6193            if self.index_for_entry(entry_id, worktree_id).is_none() {
6194                anyhow::bail!("can't reveal an ignored entry in the project panel");
6195            }
6196
6197            self.selection = Some(SelectedEntry {
6198                worktree_id,
6199                entry_id,
6200            });
6201            self.marked_entries.clear();
6202            self.marked_entries.push(SelectedEntry {
6203                worktree_id,
6204                entry_id,
6205            });
6206            self.autoscroll(cx);
6207            cx.notify();
6208            return Ok(());
6209        }
6210        let is_active_item_file_diff_view = self
6211            .workspace
6212            .upgrade()
6213            .and_then(|ws| ws.read(cx).active_item(cx))
6214            .map(|item| item.act_as_type(TypeId::of::<FileDiffView>(), cx).is_some())
6215            .unwrap_or(false);
6216        if is_active_item_file_diff_view {
6217            return Ok(());
6218        }
6219
6220        self.expand_entry(worktree_id, entry_id, cx);
6221        self.update_visible_entries(Some((worktree_id, entry_id)), false, true, window, cx);
6222        self.marked_entries.clear();
6223        self.marked_entries.push(SelectedEntry {
6224            worktree_id,
6225            entry_id,
6226        });
6227        cx.notify();
6228        Ok(())
6229    }
6230
6231    fn find_active_indent_guide(
6232        &self,
6233        indent_guides: &[IndentGuideLayout],
6234        cx: &App,
6235    ) -> Option<usize> {
6236        let (worktree, entry) = self.selected_entry(cx)?;
6237
6238        // Find the parent entry of the indent guide, this will either be the
6239        // expanded folder we have selected, or the parent of the currently
6240        // selected file/collapsed directory
6241        let mut entry = entry;
6242        loop {
6243            let is_expanded_dir = entry.is_dir()
6244                && self
6245                    .state
6246                    .expanded_dir_ids
6247                    .get(&worktree.id())
6248                    .map(|ids| ids.binary_search(&entry.id).is_ok())
6249                    .unwrap_or(false);
6250            if is_expanded_dir {
6251                break;
6252            }
6253            entry = worktree.entry_for_path(&entry.path.parent()?)?;
6254        }
6255
6256        let (active_indent_range, depth) = {
6257            let (worktree_ix, child_offset, ix) = self.index_for_entry(entry.id, worktree.id())?;
6258            let child_paths = &self.state.visible_entries[worktree_ix].entries;
6259            let mut child_count = 0;
6260            let depth = entry.path.ancestors().count();
6261            while let Some(entry) = child_paths.get(child_offset + child_count + 1) {
6262                if entry.path.ancestors().count() <= depth {
6263                    break;
6264                }
6265                child_count += 1;
6266            }
6267
6268            let start = ix + 1;
6269            let end = start + child_count;
6270
6271            let visible_worktree = &self.state.visible_entries[worktree_ix];
6272            let visible_worktree_entries = visible_worktree.index.get_or_init(|| {
6273                visible_worktree
6274                    .entries
6275                    .iter()
6276                    .map(|e| e.path.clone())
6277                    .collect()
6278            });
6279
6280            // Calculate the actual depth of the entry, taking into account that directories can be auto-folded.
6281            let (depth, _) = Self::calculate_depth_and_difference(entry, visible_worktree_entries);
6282            (start..end, depth)
6283        };
6284
6285        let candidates = indent_guides
6286            .iter()
6287            .enumerate()
6288            .filter(|(_, indent_guide)| indent_guide.offset.x == depth);
6289
6290        for (i, indent) in candidates {
6291            // Find matches that are either an exact match, partially on screen, or inside the enclosing indent
6292            if active_indent_range.start <= indent.offset.y + indent.length
6293                && indent.offset.y <= active_indent_range.end
6294            {
6295                return Some(i);
6296            }
6297        }
6298        None
6299    }
6300
6301    fn render_sticky_entries(
6302        &self,
6303        child: StickyProjectPanelCandidate,
6304        window: &mut Window,
6305        cx: &mut Context<Self>,
6306    ) -> SmallVec<[AnyElement; 8]> {
6307        let project = self.project.read(cx);
6308
6309        let Some((worktree_id, entry_ref)) = self.entry_at_index(child.index) else {
6310            return SmallVec::new();
6311        };
6312
6313        let Some(visible) = self
6314            .state
6315            .visible_entries
6316            .iter()
6317            .find(|worktree| worktree.worktree_id == worktree_id)
6318        else {
6319            return SmallVec::new();
6320        };
6321
6322        let Some(worktree) = project.worktree_for_id(worktree_id, cx) else {
6323            return SmallVec::new();
6324        };
6325        let worktree = worktree.read(cx).snapshot();
6326
6327        let paths = visible
6328            .index
6329            .get_or_init(|| visible.entries.iter().map(|e| e.path.clone()).collect());
6330
6331        let mut sticky_parents = Vec::new();
6332        let mut current_path = entry_ref.path.clone();
6333
6334        'outer: loop {
6335            if let Some(parent_path) = current_path.parent() {
6336                for ancestor_path in parent_path.ancestors() {
6337                    if paths.contains(ancestor_path)
6338                        && let Some(parent_entry) = worktree.entry_for_path(ancestor_path)
6339                    {
6340                        sticky_parents.push(parent_entry.clone());
6341                        current_path = parent_entry.path.clone();
6342                        continue 'outer;
6343                    }
6344                }
6345            }
6346            break 'outer;
6347        }
6348
6349        if sticky_parents.is_empty() {
6350            return SmallVec::new();
6351        }
6352
6353        sticky_parents.reverse();
6354
6355        let panel_settings = ProjectPanelSettings::get_global(cx);
6356        let git_status_enabled = panel_settings.git_status;
6357        let root_name = worktree.root_name();
6358
6359        let git_summaries_by_id = if git_status_enabled {
6360            visible
6361                .entries
6362                .iter()
6363                .map(|e| (e.id, e.git_summary))
6364                .collect::<HashMap<_, _>>()
6365        } else {
6366            Default::default()
6367        };
6368
6369        // already checked if non empty above
6370        let last_item_index = sticky_parents.len() - 1;
6371        sticky_parents
6372            .iter()
6373            .enumerate()
6374            .map(|(index, entry)| {
6375                let git_status = git_summaries_by_id
6376                    .get(&entry.id)
6377                    .copied()
6378                    .unwrap_or_default();
6379                let sticky_details = Some(StickyDetails {
6380                    sticky_index: index,
6381                });
6382                let details = self.details_for_entry(
6383                    entry,
6384                    worktree_id,
6385                    root_name,
6386                    paths,
6387                    git_status,
6388                    sticky_details,
6389                    window,
6390                    cx,
6391                );
6392                self.render_entry(entry.id, details, window, cx)
6393                    .when(index == last_item_index, |this| {
6394                        let shadow_color_top = hsla(0.0, 0.0, 0.0, 0.1);
6395                        let shadow_color_bottom = hsla(0.0, 0.0, 0.0, 0.);
6396                        let sticky_shadow = div()
6397                            .absolute()
6398                            .left_0()
6399                            .bottom_neg_1p5()
6400                            .h_1p5()
6401                            .w_full()
6402                            .bg(linear_gradient(
6403                                0.,
6404                                linear_color_stop(shadow_color_top, 1.),
6405                                linear_color_stop(shadow_color_bottom, 0.),
6406                            ));
6407                        this.child(sticky_shadow)
6408                    })
6409                    .into_any()
6410            })
6411            .collect()
6412    }
6413}
6414
6415#[derive(Clone)]
6416struct StickyProjectPanelCandidate {
6417    index: usize,
6418    depth: usize,
6419}
6420
6421impl StickyCandidate for StickyProjectPanelCandidate {
6422    fn depth(&self) -> usize {
6423        self.depth
6424    }
6425}
6426
6427fn item_width_estimate(depth: usize, item_text_chars: usize, is_symlink: bool) -> usize {
6428    const ICON_SIZE_FACTOR: usize = 2;
6429    let mut item_width = depth * ICON_SIZE_FACTOR + item_text_chars;
6430    if is_symlink {
6431        item_width += ICON_SIZE_FACTOR;
6432    }
6433    item_width
6434}
6435
6436impl Render for ProjectPanel {
6437    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
6438        let has_worktree = !self.state.visible_entries.is_empty();
6439        let project = self.project.read(cx);
6440        let panel_settings = ProjectPanelSettings::get_global(cx);
6441        let indent_size = panel_settings.indent_size;
6442        let show_indent_guides = panel_settings.indent_guides.show == ShowIndentGuides::Always;
6443        let horizontal_scroll = panel_settings.scrollbar.horizontal_scroll;
6444        let show_sticky_entries = {
6445            if panel_settings.sticky_scroll {
6446                let is_scrollable = self.scroll_handle.is_scrollable();
6447                let is_scrolled = self.scroll_handle.offset().y < px(0.);
6448                is_scrollable && is_scrolled
6449            } else {
6450                false
6451            }
6452        };
6453
6454        let is_local = project.is_local();
6455
6456        if has_worktree {
6457            let item_count = self
6458                .state
6459                .visible_entries
6460                .iter()
6461                .map(|worktree| worktree.entries.len())
6462                .sum();
6463
6464            fn handle_drag_move<T: 'static>(
6465                this: &mut ProjectPanel,
6466                e: &DragMoveEvent<T>,
6467                window: &mut Window,
6468                cx: &mut Context<ProjectPanel>,
6469            ) {
6470                if let Some(previous_position) = this.previous_drag_position {
6471                    // Refresh cursor only when an actual drag happens,
6472                    // because modifiers are not updated when the cursor is not moved.
6473                    if e.event.position != previous_position {
6474                        this.refresh_drag_cursor_style(&e.event.modifiers, window, cx);
6475                    }
6476                }
6477                this.previous_drag_position = Some(e.event.position);
6478
6479                if !e.bounds.contains(&e.event.position) {
6480                    this.drag_target_entry = None;
6481                    return;
6482                }
6483                this.hover_scroll_task.take();
6484                let panel_height = e.bounds.size.height;
6485                if panel_height <= px(0.) {
6486                    return;
6487                }
6488
6489                let event_offset = e.event.position.y - e.bounds.origin.y;
6490                // How far along in the project panel is our cursor? (0. is the top of a list, 1. is the bottom)
6491                let hovered_region_offset = event_offset / panel_height;
6492
6493                // We want the scrolling to be a bit faster when the cursor is closer to the edge of a list.
6494                // These pixels offsets were picked arbitrarily.
6495                let vertical_scroll_offset = if hovered_region_offset <= 0.05 {
6496                    8.
6497                } else if hovered_region_offset <= 0.15 {
6498                    5.
6499                } else if hovered_region_offset >= 0.95 {
6500                    -8.
6501                } else if hovered_region_offset >= 0.85 {
6502                    -5.
6503                } else {
6504                    return;
6505                };
6506                let adjustment = point(px(0.), px(vertical_scroll_offset));
6507                this.hover_scroll_task = Some(cx.spawn_in(window, async move |this, cx| {
6508                    loop {
6509                        let should_stop_scrolling = this
6510                            .update(cx, |this, cx| {
6511                                this.hover_scroll_task.as_ref()?;
6512                                let handle = this.scroll_handle.0.borrow_mut();
6513                                let offset = handle.base_handle.offset();
6514
6515                                handle.base_handle.set_offset(offset + adjustment);
6516                                cx.notify();
6517                                Some(())
6518                            })
6519                            .ok()
6520                            .flatten()
6521                            .is_some();
6522                        if should_stop_scrolling {
6523                            return;
6524                        }
6525                        cx.background_executor()
6526                            .timer(Duration::from_millis(16))
6527                            .await;
6528                    }
6529                }));
6530            }
6531            h_flex()
6532                .id("project-panel")
6533                .group("project-panel")
6534                .when(panel_settings.drag_and_drop, |this| {
6535                    this.on_drag_move(cx.listener(handle_drag_move::<ExternalPaths>))
6536                        .on_drag_move(cx.listener(handle_drag_move::<DraggedSelection>))
6537                })
6538                .size_full()
6539                .relative()
6540                .on_modifiers_changed(cx.listener(
6541                    |this, event: &ModifiersChangedEvent, window, cx| {
6542                        this.refresh_drag_cursor_style(&event.modifiers, window, cx);
6543                    },
6544                ))
6545                .key_context(self.dispatch_context(window, cx))
6546                .on_action(cx.listener(Self::scroll_up))
6547                .on_action(cx.listener(Self::scroll_down))
6548                .on_action(cx.listener(Self::scroll_cursor_center))
6549                .on_action(cx.listener(Self::scroll_cursor_top))
6550                .on_action(cx.listener(Self::scroll_cursor_bottom))
6551                .on_action(cx.listener(Self::select_next))
6552                .on_action(cx.listener(Self::select_previous))
6553                .on_action(cx.listener(Self::select_first))
6554                .on_action(cx.listener(Self::select_last))
6555                .on_action(cx.listener(Self::select_parent))
6556                .on_action(cx.listener(Self::select_next_git_entry))
6557                .on_action(cx.listener(Self::select_prev_git_entry))
6558                .on_action(cx.listener(Self::select_next_diagnostic))
6559                .on_action(cx.listener(Self::select_prev_diagnostic))
6560                .on_action(cx.listener(Self::select_next_directory))
6561                .on_action(cx.listener(Self::select_prev_directory))
6562                .on_action(cx.listener(Self::expand_selected_entry))
6563                .on_action(cx.listener(Self::collapse_selected_entry))
6564                .on_action(cx.listener(Self::collapse_all_entries))
6565                .on_action(cx.listener(Self::collapse_selected_entry_and_children))
6566                .on_action(cx.listener(Self::open))
6567                .on_action(cx.listener(Self::open_permanent))
6568                .on_action(cx.listener(Self::open_split_vertical))
6569                .on_action(cx.listener(Self::open_split_horizontal))
6570                .on_action(cx.listener(Self::confirm))
6571                .on_action(cx.listener(Self::cancel))
6572                .on_action(cx.listener(Self::copy_path))
6573                .on_action(cx.listener(Self::copy_relative_path))
6574                .on_action(cx.listener(Self::new_search_in_directory))
6575                .on_action(cx.listener(Self::unfold_directory))
6576                .on_action(cx.listener(Self::fold_directory))
6577                .on_action(cx.listener(Self::remove_from_project))
6578                .on_action(cx.listener(Self::compare_marked_files))
6579                .when(cx.has_flag::<ProjectPanelUndoRedoFeatureFlag>(), |el| {
6580                    el.on_action(cx.listener(Self::undo))
6581                })
6582                .when(!project.is_read_only(cx), |el| {
6583                    el.on_action(cx.listener(Self::new_file))
6584                        .on_action(cx.listener(Self::new_directory))
6585                        .on_action(cx.listener(Self::rename))
6586                        .on_action(cx.listener(Self::delete))
6587                        .on_action(cx.listener(Self::cut))
6588                        .on_action(cx.listener(Self::copy))
6589                        .on_action(cx.listener(Self::paste))
6590                        .on_action(cx.listener(Self::duplicate))
6591                        .on_action(cx.listener(Self::restore_file))
6592                        .when(!project.is_remote(), |el| {
6593                            el.on_action(cx.listener(Self::trash))
6594                        })
6595                })
6596                .when(
6597                    project.is_local() || project.is_via_wsl_with_host_interop(cx),
6598                    |el| {
6599                        el.on_action(cx.listener(Self::reveal_in_finder))
6600                            .on_action(cx.listener(Self::open_system))
6601                            .on_action(cx.listener(Self::open_in_terminal))
6602                    },
6603                )
6604                .when(project.is_via_remote_server(), |el| {
6605                    el.on_action(cx.listener(Self::open_in_terminal))
6606                        .on_action(cx.listener(Self::download_from_remote))
6607                })
6608                .track_focus(&self.focus_handle(cx))
6609                .child(
6610                    v_flex()
6611                        .child(
6612                            uniform_list("entries", item_count, {
6613                                cx.processor(|this, range: Range<usize>, window, cx| {
6614                                    this.rendered_entries_len = range.end - range.start;
6615                                    let mut items = Vec::with_capacity(this.rendered_entries_len);
6616                                    this.for_each_visible_entry(
6617                                        range,
6618                                        window,
6619                                        cx,
6620                                        &mut |id, details, window, cx| {
6621                                            items.push(this.render_entry(id, details, window, cx));
6622                                        },
6623                                    );
6624                                    items
6625                                })
6626                            })
6627                            .when(show_indent_guides, |list| {
6628                                list.with_decoration(
6629                                    ui::indent_guides(
6630                                        px(indent_size),
6631                                        IndentGuideColors::panel(cx),
6632                                    )
6633                                    .with_compute_indents_fn(
6634                                        cx.entity(),
6635                                        |this, range, window, cx| {
6636                                            let mut items =
6637                                                SmallVec::with_capacity(range.end - range.start);
6638                                            this.iter_visible_entries(
6639                                                range,
6640                                                window,
6641                                                cx,
6642                                                &mut |entry, _, entries, _, _| {
6643                                                    let (depth, _) =
6644                                                        Self::calculate_depth_and_difference(
6645                                                            entry, entries,
6646                                                        );
6647                                                    items.push(depth);
6648                                                },
6649                                            );
6650                                            items
6651                                        },
6652                                    )
6653                                    .on_click(cx.listener(
6654                                        |this,
6655                                         active_indent_guide: &IndentGuideLayout,
6656                                         window,
6657                                         cx| {
6658                                            if window.modifiers().secondary() {
6659                                                let ix = active_indent_guide.offset.y;
6660                                                let Some((target_entry, worktree)) = maybe!({
6661                                                    let (worktree_id, entry) =
6662                                                        this.entry_at_index(ix)?;
6663                                                    let worktree = this
6664                                                        .project
6665                                                        .read(cx)
6666                                                        .worktree_for_id(worktree_id, cx)?;
6667                                                    let target_entry = worktree
6668                                                        .read(cx)
6669                                                        .entry_for_path(&entry.path.parent()?)?;
6670                                                    Some((target_entry, worktree))
6671                                                }) else {
6672                                                    return;
6673                                                };
6674
6675                                                this.collapse_entry(
6676                                                    target_entry.clone(),
6677                                                    worktree,
6678                                                    window,
6679                                                    cx,
6680                                                );
6681                                            }
6682                                        },
6683                                    ))
6684                                    .with_render_fn(
6685                                        cx.entity(),
6686                                        move |this, params, _, cx| {
6687                                            const LEFT_OFFSET: Pixels = px(14.);
6688                                            const PADDING_Y: Pixels = px(4.);
6689                                            const HITBOX_OVERDRAW: Pixels = px(3.);
6690
6691                                            let active_indent_guide_index = this
6692                                                .find_active_indent_guide(
6693                                                    &params.indent_guides,
6694                                                    cx,
6695                                                );
6696
6697                                            let indent_size = params.indent_size;
6698                                            let item_height = params.item_height;
6699
6700                                            params
6701                                                .indent_guides
6702                                                .into_iter()
6703                                                .enumerate()
6704                                                .map(|(idx, layout)| {
6705                                                    let offset = if layout.continues_offscreen {
6706                                                        px(0.)
6707                                                    } else {
6708                                                        PADDING_Y
6709                                                    };
6710                                                    let bounds = Bounds::new(
6711                                                        point(
6712                                                            layout.offset.x * indent_size
6713                                                                + LEFT_OFFSET,
6714                                                            layout.offset.y * item_height + offset,
6715                                                        ),
6716                                                        size(
6717                                                            px(1.),
6718                                                            layout.length * item_height
6719                                                                - offset * 2.,
6720                                                        ),
6721                                                    );
6722                                                    ui::RenderedIndentGuide {
6723                                                        bounds,
6724                                                        layout,
6725                                                        is_active: Some(idx)
6726                                                            == active_indent_guide_index,
6727                                                        hitbox: Some(Bounds::new(
6728                                                            point(
6729                                                                bounds.origin.x - HITBOX_OVERDRAW,
6730                                                                bounds.origin.y,
6731                                                            ),
6732                                                            size(
6733                                                                bounds.size.width
6734                                                                    + HITBOX_OVERDRAW * 2.,
6735                                                                bounds.size.height,
6736                                                            ),
6737                                                        )),
6738                                                    }
6739                                                })
6740                                                .collect()
6741                                        },
6742                                    ),
6743                                )
6744                            })
6745                            .when(show_sticky_entries, |list| {
6746                                let sticky_items = ui::sticky_items(
6747                                    cx.entity(),
6748                                    |this, range, window, cx| {
6749                                        let mut items =
6750                                            SmallVec::with_capacity(range.end - range.start);
6751                                        this.iter_visible_entries(
6752                                            range,
6753                                            window,
6754                                            cx,
6755                                            &mut |entry, index, entries, _, _| {
6756                                                let (depth, _) =
6757                                                    Self::calculate_depth_and_difference(
6758                                                        entry, entries,
6759                                                    );
6760                                                let candidate =
6761                                                    StickyProjectPanelCandidate { index, depth };
6762                                                items.push(candidate);
6763                                            },
6764                                        );
6765                                        items
6766                                    },
6767                                    |this, marker_entry, window, cx| {
6768                                        let sticky_entries =
6769                                            this.render_sticky_entries(marker_entry, window, cx);
6770                                        this.sticky_items_count = sticky_entries.len();
6771                                        sticky_entries
6772                                    },
6773                                );
6774                                list.with_decoration(if show_indent_guides {
6775                                    sticky_items.with_decoration(
6776                                        ui::indent_guides(
6777                                            px(indent_size),
6778                                            IndentGuideColors::panel(cx),
6779                                        )
6780                                        .with_render_fn(
6781                                            cx.entity(),
6782                                            move |_, params, _, _| {
6783                                                const LEFT_OFFSET: Pixels = px(14.);
6784
6785                                                let indent_size = params.indent_size;
6786                                                let item_height = params.item_height;
6787
6788                                                params
6789                                                    .indent_guides
6790                                                    .into_iter()
6791                                                    .map(|layout| {
6792                                                        let bounds = Bounds::new(
6793                                                            point(
6794                                                                layout.offset.x * indent_size
6795                                                                    + LEFT_OFFSET,
6796                                                                layout.offset.y * item_height,
6797                                                            ),
6798                                                            size(
6799                                                                px(1.),
6800                                                                layout.length * item_height,
6801                                                            ),
6802                                                        );
6803                                                        ui::RenderedIndentGuide {
6804                                                            bounds,
6805                                                            layout,
6806                                                            is_active: false,
6807                                                            hitbox: None,
6808                                                        }
6809                                                    })
6810                                                    .collect()
6811                                            },
6812                                        ),
6813                                    )
6814                                } else {
6815                                    sticky_items
6816                                })
6817                            })
6818                            .with_sizing_behavior(ListSizingBehavior::Infer)
6819                            .with_horizontal_sizing_behavior(if horizontal_scroll {
6820                                ListHorizontalSizingBehavior::Unconstrained
6821                            } else {
6822                                ListHorizontalSizingBehavior::FitList
6823                            })
6824                            .when(horizontal_scroll, |list| {
6825                                list.with_width_from_item(self.state.max_width_item_index)
6826                            })
6827                            .track_scroll(&self.scroll_handle),
6828                        )
6829                        .child(
6830                            div()
6831                                .id("project-panel-blank-area")
6832                                .block_mouse_except_scroll()
6833                                .flex_grow()
6834                                .on_scroll_wheel({
6835                                    let scroll_handle = self.scroll_handle.clone();
6836                                    let entity_id = cx.entity().entity_id();
6837                                    move |event, window, cx| {
6838                                        let state = scroll_handle.0.borrow();
6839                                        let base_handle = &state.base_handle;
6840                                        let current_offset = base_handle.offset();
6841                                        let max_offset = base_handle.max_offset();
6842                                        let delta = event.delta.pixel_delta(window.line_height());
6843                                        let new_offset = (current_offset + delta)
6844                                            .clamp(&max_offset.neg(), &Point::default());
6845
6846                                        if new_offset != current_offset {
6847                                            base_handle.set_offset(new_offset);
6848                                            cx.notify(entity_id);
6849                                        }
6850                                    }
6851                                })
6852                                .when(
6853                                    self.drag_target_entry.as_ref().is_some_and(
6854                                        |entry| match entry {
6855                                            DragTarget::Background => true,
6856                                            DragTarget::Entry {
6857                                                highlight_entry_id, ..
6858                                            } => self.state.last_worktree_root_id.is_some_and(
6859                                                |root_id| *highlight_entry_id == root_id,
6860                                            ),
6861                                        },
6862                                    ),
6863                                    |div| div.bg(cx.theme().colors().drop_target_background),
6864                                )
6865                                .on_drag_move::<ExternalPaths>(cx.listener(
6866                                    move |this, event: &DragMoveEvent<ExternalPaths>, _, _| {
6867                                        let Some(_last_root_id) = this.state.last_worktree_root_id
6868                                        else {
6869                                            return;
6870                                        };
6871                                        if event.bounds.contains(&event.event.position) {
6872                                            this.drag_target_entry = Some(DragTarget::Background);
6873                                        } else {
6874                                            if this.drag_target_entry.as_ref().is_some_and(|e| {
6875                                                matches!(e, DragTarget::Background)
6876                                            }) {
6877                                                this.drag_target_entry = None;
6878                                            }
6879                                        }
6880                                    },
6881                                ))
6882                                .on_drag_move::<DraggedSelection>(cx.listener(
6883                                    move |this, event: &DragMoveEvent<DraggedSelection>, _, cx| {
6884                                        let Some(last_root_id) = this.state.last_worktree_root_id
6885                                        else {
6886                                            return;
6887                                        };
6888                                        if event.bounds.contains(&event.event.position) {
6889                                            let drag_state = event.drag(cx);
6890                                            if this.should_highlight_background_for_selection_drag(
6891                                                &drag_state,
6892                                                last_root_id,
6893                                                cx,
6894                                            ) {
6895                                                this.drag_target_entry =
6896                                                    Some(DragTarget::Background);
6897                                            }
6898                                        } else {
6899                                            if this.drag_target_entry.as_ref().is_some_and(|e| {
6900                                                matches!(e, DragTarget::Background)
6901                                            }) {
6902                                                this.drag_target_entry = None;
6903                                            }
6904                                        }
6905                                    },
6906                                ))
6907                                .on_drop(cx.listener(
6908                                    move |this, external_paths: &ExternalPaths, window, cx| {
6909                                        this.drag_target_entry = None;
6910                                        this.hover_scroll_task.take();
6911                                        if let Some(entry_id) = this.state.last_worktree_root_id {
6912                                            this.drop_external_files(
6913                                                external_paths.paths(),
6914                                                entry_id,
6915                                                window,
6916                                                cx,
6917                                            );
6918                                        }
6919                                        cx.stop_propagation();
6920                                    },
6921                                ))
6922                                .on_drop(cx.listener(
6923                                    move |this, selections: &DraggedSelection, window, cx| {
6924                                        this.drag_target_entry = None;
6925                                        this.hover_scroll_task.take();
6926                                        if let Some(entry_id) = this.state.last_worktree_root_id {
6927                                            this.drag_onto(selections, entry_id, false, window, cx);
6928                                        }
6929                                        cx.stop_propagation();
6930                                    },
6931                                ))
6932                                .on_click(cx.listener(|this, event, window, cx| {
6933                                    if matches!(event, gpui::ClickEvent::Keyboard(_)) {
6934                                        return;
6935                                    }
6936                                    cx.stop_propagation();
6937                                    this.selection = None;
6938                                    this.marked_entries.clear();
6939                                    this.focus_handle(cx).focus(window, cx);
6940                                }))
6941                                .on_mouse_down(
6942                                    MouseButton::Right,
6943                                    cx.listener(move |this, event: &MouseDownEvent, window, cx| {
6944                                        // When deploying the context menu anywhere below the last project entry,
6945                                        // act as if the user clicked the root of the last worktree.
6946                                        if let Some(entry_id) = this.state.last_worktree_root_id {
6947                                            this.deploy_context_menu(
6948                                                event.position,
6949                                                entry_id,
6950                                                window,
6951                                                cx,
6952                                            );
6953                                        }
6954                                    }),
6955                                )
6956                                .when(!project.is_read_only(cx), |el| {
6957                                    el.on_click(cx.listener(
6958                                        |this, event: &gpui::ClickEvent, window, cx| {
6959                                            if event.click_count() > 1
6960                                                && let Some(entry_id) =
6961                                                    this.state.last_worktree_root_id
6962                                            {
6963                                                let project = this.project.read(cx);
6964
6965                                                let worktree_id = if let Some(worktree) =
6966                                                    project.worktree_for_entry(entry_id, cx)
6967                                                {
6968                                                    worktree.read(cx).id()
6969                                                } else {
6970                                                    return;
6971                                                };
6972
6973                                                this.selection = Some(SelectedEntry {
6974                                                    worktree_id,
6975                                                    entry_id,
6976                                                });
6977
6978                                                this.new_file(&NewFile, window, cx);
6979                                            }
6980                                        },
6981                                    ))
6982                                }),
6983                        )
6984                        .size_full(),
6985                )
6986                .custom_scrollbars(
6987                    {
6988                        let mut scrollbars =
6989                            Scrollbars::for_settings::<ProjectPanelScrollbarProxy>()
6990                                .tracked_scroll_handle(&self.scroll_handle);
6991                        if horizontal_scroll {
6992                            scrollbars = scrollbars.with_track_along(
6993                                ScrollAxes::Horizontal,
6994                                cx.theme().colors().panel_background,
6995                            );
6996                        }
6997                        scrollbars.notify_content()
6998                    },
6999                    window,
7000                    cx,
7001                )
7002                .children(self.context_menu.as_ref().map(|(menu, position, _)| {
7003                    deferred(
7004                        anchored()
7005                            .position(*position)
7006                            .anchor(gpui::Corner::TopLeft)
7007                            .child(menu.clone()),
7008                    )
7009                    .with_priority(3)
7010                }))
7011        } else {
7012            let focus_handle = self.focus_handle(cx);
7013
7014            v_flex()
7015                .id("empty-project_panel")
7016                .p_4()
7017                .size_full()
7018                .items_center()
7019                .justify_center()
7020                .gap_1()
7021                .track_focus(&self.focus_handle(cx))
7022                .child(
7023                    Button::new("open_project", "Open Project")
7024                        .full_width()
7025                        .key_binding(KeyBinding::for_action_in(
7026                            &workspace::Open::default(),
7027                            &focus_handle,
7028                            cx,
7029                        ))
7030                        .on_click(cx.listener(|this, _, window, cx| {
7031                            this.workspace
7032                                .update(cx, |_, cx| {
7033                                    window.dispatch_action(
7034                                        workspace::Open::default().boxed_clone(),
7035                                        cx,
7036                                    );
7037                                })
7038                                .log_err();
7039                        })),
7040                )
7041                .child(
7042                    h_flex()
7043                        .w_1_2()
7044                        .gap_2()
7045                        .child(Divider::horizontal())
7046                        .child(Label::new("or").size(LabelSize::XSmall).color(Color::Muted))
7047                        .child(Divider::horizontal()),
7048                )
7049                .child(
7050                    Button::new("clone_repo", "Clone Repository")
7051                        .full_width()
7052                        .on_click(cx.listener(|this, _, window, cx| {
7053                            this.workspace
7054                                .update(cx, |_, cx| {
7055                                    window.dispatch_action(git::Clone.boxed_clone(), cx);
7056                                })
7057                                .log_err();
7058                        })),
7059                )
7060                .when(is_local, |div| {
7061                    div.when(panel_settings.drag_and_drop, |div| {
7062                        div.drag_over::<ExternalPaths>(|style, _, _, cx| {
7063                            style.bg(cx.theme().colors().drop_target_background)
7064                        })
7065                        .on_drop(cx.listener(
7066                            move |this, external_paths: &ExternalPaths, window, cx| {
7067                                this.drag_target_entry = None;
7068                                this.hover_scroll_task.take();
7069                                if let Some(task) = this
7070                                    .workspace
7071                                    .update(cx, |workspace, cx| {
7072                                        workspace.open_workspace_for_paths(
7073                                            OpenMode::Activate,
7074                                            external_paths.paths().to_owned(),
7075                                            window,
7076                                            cx,
7077                                        )
7078                                    })
7079                                    .log_err()
7080                                {
7081                                    task.detach_and_log_err(cx);
7082                                }
7083                                cx.stop_propagation();
7084                            },
7085                        ))
7086                    })
7087                })
7088        }
7089    }
7090}
7091
7092impl Render for DraggedProjectEntryView {
7093    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
7094        let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
7095        h_flex()
7096            .font(ui_font)
7097            .pl(self.click_offset.x + px(12.))
7098            .pt(self.click_offset.y + px(12.))
7099            .child(
7100                div()
7101                    .flex()
7102                    .gap_1()
7103                    .items_center()
7104                    .py_1()
7105                    .px_2()
7106                    .rounded_lg()
7107                    .bg(cx.theme().colors().background)
7108                    .map(|this| {
7109                        if self.selections.len() > 1 && self.selections.contains(&self.selection) {
7110                            this.child(Label::new(format!("{} entries", self.selections.len())))
7111                        } else {
7112                            this.child(if let Some(icon) = &self.icon {
7113                                div().child(Icon::from_path(icon.clone()))
7114                            } else {
7115                                div()
7116                            })
7117                            .child(Label::new(self.filename.clone()))
7118                        }
7119                    }),
7120            )
7121    }
7122}
7123
7124impl EventEmitter<Event> for ProjectPanel {}
7125
7126impl EventEmitter<PanelEvent> for ProjectPanel {}
7127
7128impl Panel for ProjectPanel {
7129    fn position(&self, _: &Window, cx: &App) -> DockPosition {
7130        match ProjectPanelSettings::get_global(cx).dock {
7131            DockSide::Left => DockPosition::Left,
7132            DockSide::Right => DockPosition::Right,
7133        }
7134    }
7135
7136    fn position_is_valid(&self, position: DockPosition) -> bool {
7137        matches!(position, DockPosition::Left | DockPosition::Right)
7138    }
7139
7140    fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
7141        settings::update_settings_file(self.fs.clone(), cx, move |settings, _| {
7142            let dock = match position {
7143                DockPosition::Left | DockPosition::Bottom => DockSide::Left,
7144                DockPosition::Right => DockSide::Right,
7145            };
7146            settings.project_panel.get_or_insert_default().dock = Some(dock);
7147        });
7148    }
7149
7150    fn default_size(&self, _: &Window, cx: &App) -> Pixels {
7151        ProjectPanelSettings::get_global(cx).default_width
7152    }
7153
7154    fn icon(&self, _: &Window, cx: &App) -> Option<IconName> {
7155        ProjectPanelSettings::get_global(cx)
7156            .button
7157            .then_some(IconName::FileTree)
7158    }
7159
7160    fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
7161        Some("Project Panel")
7162    }
7163
7164    fn toggle_action(&self) -> Box<dyn Action> {
7165        Box::new(ToggleFocus)
7166    }
7167
7168    fn persistent_name() -> &'static str {
7169        "Project Panel"
7170    }
7171
7172    fn panel_key() -> &'static str {
7173        PROJECT_PANEL_KEY
7174    }
7175
7176    fn starts_open(&self, _: &Window, cx: &App) -> bool {
7177        if !ProjectPanelSettings::get_global(cx).starts_open {
7178            return false;
7179        }
7180
7181        let project = &self.project.read(cx);
7182        project.visible_worktrees(cx).any(|tree| {
7183            tree.read(cx)
7184                .root_entry()
7185                .is_some_and(|entry| entry.is_dir())
7186        })
7187    }
7188
7189    fn activation_priority(&self) -> u32 {
7190        1
7191    }
7192}
7193
7194impl ProjectPanel {
7195    pub fn select_path_for_test(&mut self, project_path: ProjectPath, cx: &App) {
7196        let Some(worktree) = self
7197            .project
7198            .read(cx)
7199            .worktree_for_id(project_path.worktree_id, cx)
7200        else {
7201            return;
7202        };
7203        let Some(entry) = worktree.read(cx).entry_for_path(project_path.path.as_ref()) else {
7204            return;
7205        };
7206        self.selection = Some(SelectedEntry {
7207            worktree_id: project_path.worktree_id,
7208            entry_id: entry.id,
7209        });
7210    }
7211}
7212
7213impl Focusable for ProjectPanel {
7214    fn focus_handle(&self, _cx: &App) -> FocusHandle {
7215        self.focus_handle.clone()
7216    }
7217}
7218
7219impl ClipboardEntry {
7220    fn is_cut(&self) -> bool {
7221        matches!(self, Self::Cut { .. })
7222    }
7223
7224    fn items(&self) -> &BTreeSet<SelectedEntry> {
7225        match self {
7226            ClipboardEntry::Copied(entries) | ClipboardEntry::Cut(entries) => entries,
7227        }
7228    }
7229
7230    fn into_copy_entry(self) -> Self {
7231        match self {
7232            ClipboardEntry::Copied(_) => self,
7233            ClipboardEntry::Cut(entries) => ClipboardEntry::Copied(entries),
7234        }
7235    }
7236}
7237
7238#[inline]
7239fn cmp_directories_first(a: &Entry, b: &Entry) -> cmp::Ordering {
7240    util::paths::compare_rel_paths((&a.path, a.is_file()), (&b.path, b.is_file()))
7241}
7242
7243#[inline]
7244fn cmp_mixed(a: &Entry, b: &Entry) -> cmp::Ordering {
7245    util::paths::compare_rel_paths_mixed((&a.path, a.is_file()), (&b.path, b.is_file()))
7246}
7247
7248#[inline]
7249fn cmp_files_first(a: &Entry, b: &Entry) -> cmp::Ordering {
7250    util::paths::compare_rel_paths_files_first((&a.path, a.is_file()), (&b.path, b.is_file()))
7251}
7252
7253#[inline]
7254fn cmp_with_mode(a: &Entry, b: &Entry, mode: &settings::ProjectPanelSortMode) -> cmp::Ordering {
7255    match mode {
7256        settings::ProjectPanelSortMode::DirectoriesFirst => cmp_directories_first(a, b),
7257        settings::ProjectPanelSortMode::Mixed => cmp_mixed(a, b),
7258        settings::ProjectPanelSortMode::FilesFirst => cmp_files_first(a, b),
7259    }
7260}
7261
7262pub fn sort_worktree_entries_with_mode(
7263    entries: &mut [impl AsRef<Entry>],
7264    mode: settings::ProjectPanelSortMode,
7265) {
7266    entries.sort_by(|lhs, rhs| cmp_with_mode(lhs.as_ref(), rhs.as_ref(), &mode));
7267}
7268
7269pub fn par_sort_worktree_entries_with_mode(
7270    entries: &mut Vec<GitEntry>,
7271    mode: settings::ProjectPanelSortMode,
7272) {
7273    entries.par_sort_by(|lhs, rhs| cmp_with_mode(lhs, rhs, &mode));
7274}
7275
7276fn git_status_indicator(git_status: GitSummary) -> Option<(&'static str, Color)> {
7277    if git_status.conflict > 0 {
7278        return Some(("!", Color::Conflict));
7279    }
7280    if git_status.untracked > 0 {
7281        return Some(("U", Color::Created));
7282    }
7283    if git_status.worktree.deleted > 0 {
7284        return Some(("D", Color::Deleted));
7285    }
7286    if git_status.worktree.modified > 0 {
7287        return Some(("M", Color::Warning));
7288    }
7289    if git_status.index.deleted > 0 {
7290        return Some(("D", Color::Deleted));
7291    }
7292    if git_status.index.modified > 0 {
7293        return Some(("M", Color::Modified));
7294    }
7295    if git_status.index.added > 0 {
7296        return Some(("A", Color::Created));
7297    }
7298    None
7299}
7300
7301#[cfg(test)]
7302mod project_panel_tests;