project_panel.rs

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