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 prompt = match file_paths.first() {
2375                    Some((_, path)) if file_paths.len() == 1 => {
2376                        let unsaved_warning = if dirty_buffers > 0 {
2377                            "\n\nIt has unsaved changes, which will be lost."
2378                        } else {
2379                            ""
2380                        };
2381
2382                        format!("{operation} {path}?{unsaved_warning}")
2383                    }
2384                    _ => {
2385                        const CUTOFF_POINT: usize = 10;
2386                        let names = if file_paths.len() > CUTOFF_POINT {
2387                            let truncated_path_counts = file_paths.len() - CUTOFF_POINT;
2388                            let mut paths = file_paths
2389                                .iter()
2390                                .map(|(_, path)| path.clone())
2391                                .take(CUTOFF_POINT)
2392                                .collect::<Vec<_>>();
2393                            paths.truncate(CUTOFF_POINT);
2394                            if truncated_path_counts == 1 {
2395                                paths.push(".. 1 file not shown".into());
2396                            } else {
2397                                paths.push(format!(".. {} files not shown", truncated_path_counts));
2398                            }
2399                            paths
2400                        } else {
2401                            file_paths.iter().map(|(_, path)| path.clone()).collect()
2402                        };
2403                        let unsaved_warning = if dirty_buffers == 0 {
2404                            String::new()
2405                        } else if dirty_buffers == 1 {
2406                            "\n\n1 of these has unsaved changes, which will be lost.".to_string()
2407                        } else {
2408                            format!(
2409                                "\n\n{dirty_buffers} of these have unsaved changes, which will be lost."
2410                            )
2411                        };
2412
2413                        format!(
2414                            "Do you want to {} the following {} files?\n{}{unsaved_warning}",
2415                            operation.to_lowercase(),
2416                            file_paths.len(),
2417                            names.join("\n")
2418                        )
2419                    }
2420                };
2421                Some(window.prompt(PromptLevel::Info, &prompt, None, &[operation, "Cancel"], cx))
2422            } else {
2423                None
2424            };
2425            let next_selection = self.find_next_selection_after_deletion(items_to_delete, cx);
2426            cx.spawn_in(window, async move |panel, cx| {
2427                if let Some(answer) = answer
2428                    && answer.await != Ok(0)
2429                {
2430                    return anyhow::Ok(());
2431                }
2432                for (entry_id, _) in file_paths {
2433                    panel
2434                        .update(cx, |panel, cx| {
2435                            panel
2436                                .project
2437                                .update(cx, |project, cx| project.delete_entry(entry_id, trash, cx))
2438                                .context("no such entry")
2439                        })??
2440                        .await?;
2441                }
2442                panel.update_in(cx, |panel, window, cx| {
2443                    if let Some(next_selection) = next_selection {
2444                        panel.update_visible_entries(
2445                            Some((next_selection.worktree_id, next_selection.entry_id)),
2446                            false,
2447                            true,
2448                            window,
2449                            cx,
2450                        );
2451                    } else {
2452                        panel.select_last(&SelectLast {}, window, cx);
2453                    }
2454                })?;
2455                Ok(())
2456            })
2457            .detach_and_log_err(cx);
2458            Some(())
2459        });
2460    }
2461
2462    fn find_next_selection_after_deletion(
2463        &self,
2464        sanitized_entries: BTreeSet<SelectedEntry>,
2465        cx: &mut Context<Self>,
2466    ) -> Option<SelectedEntry> {
2467        if sanitized_entries.is_empty() {
2468            return None;
2469        }
2470        let project = self.project.read(cx);
2471        let (worktree_id, worktree) = sanitized_entries
2472            .iter()
2473            .map(|entry| entry.worktree_id)
2474            .filter_map(|id| project.worktree_for_id(id, cx).map(|w| (id, w.read(cx))))
2475            .max_by(|(_, a), (_, b)| a.root_name().cmp(b.root_name()))?;
2476        let git_store = project.git_store().read(cx);
2477
2478        let marked_entries_in_worktree = sanitized_entries
2479            .iter()
2480            .filter(|e| e.worktree_id == worktree_id)
2481            .collect::<HashSet<_>>();
2482        let latest_entry = marked_entries_in_worktree
2483            .iter()
2484            .max_by(|a, b| {
2485                match (
2486                    worktree.entry_for_id(a.entry_id),
2487                    worktree.entry_for_id(b.entry_id),
2488                ) {
2489                    (Some(a), Some(b)) => compare_paths(
2490                        (a.path.as_std_path(), a.is_file()),
2491                        (b.path.as_std_path(), b.is_file()),
2492                    ),
2493                    _ => cmp::Ordering::Equal,
2494                }
2495            })
2496            .and_then(|e| worktree.entry_for_id(e.entry_id))?;
2497
2498        let parent_path = latest_entry.path.parent()?;
2499        let parent_entry = worktree.entry_for_path(parent_path)?;
2500
2501        // Remove all siblings that are being deleted except the last marked entry
2502        let repo_snapshots = git_store.repo_snapshots(cx);
2503        let worktree_snapshot = worktree.snapshot();
2504        let hide_gitignore = ProjectPanelSettings::get_global(cx).hide_gitignore;
2505        let mut siblings: Vec<_> =
2506            ChildEntriesGitIter::new(&repo_snapshots, &worktree_snapshot, parent_path)
2507                .filter(|sibling| {
2508                    (sibling.id == latest_entry.id)
2509                        || (!marked_entries_in_worktree.contains(&&SelectedEntry {
2510                            worktree_id,
2511                            entry_id: sibling.id,
2512                        }) && (!hide_gitignore || !sibling.is_ignored))
2513                })
2514                .map(|entry| entry.to_owned())
2515                .collect();
2516
2517        let mode = ProjectPanelSettings::get_global(cx).sort_mode;
2518        sort_worktree_entries_with_mode(&mut siblings, mode);
2519        let sibling_entry_index = siblings
2520            .iter()
2521            .position(|sibling| sibling.id == latest_entry.id)?;
2522
2523        if let Some(next_sibling) = sibling_entry_index
2524            .checked_add(1)
2525            .and_then(|i| siblings.get(i))
2526        {
2527            return Some(SelectedEntry {
2528                worktree_id,
2529                entry_id: next_sibling.id,
2530            });
2531        }
2532        if let Some(prev_sibling) = sibling_entry_index
2533            .checked_sub(1)
2534            .and_then(|i| siblings.get(i))
2535        {
2536            return Some(SelectedEntry {
2537                worktree_id,
2538                entry_id: prev_sibling.id,
2539            });
2540        }
2541        // No neighbour sibling found, fall back to parent
2542        Some(SelectedEntry {
2543            worktree_id,
2544            entry_id: parent_entry.id,
2545        })
2546    }
2547
2548    fn unfold_directory(
2549        &mut self,
2550        _: &UnfoldDirectory,
2551        window: &mut Window,
2552        cx: &mut Context<Self>,
2553    ) {
2554        if let Some((worktree, entry)) = self.selected_entry(cx) {
2555            self.state.unfolded_dir_ids.insert(entry.id);
2556
2557            let snapshot = worktree.snapshot();
2558            let mut parent_path = entry.path.parent();
2559            while let Some(path) = parent_path {
2560                if let Some(parent_entry) = worktree.entry_for_path(path) {
2561                    let mut children_iter = snapshot.child_entries(path);
2562
2563                    if children_iter.by_ref().take(2).count() > 1 {
2564                        break;
2565                    }
2566
2567                    self.state.unfolded_dir_ids.insert(parent_entry.id);
2568                    parent_path = path.parent();
2569                } else {
2570                    break;
2571                }
2572            }
2573
2574            self.update_visible_entries(None, false, true, window, cx);
2575            cx.notify();
2576        }
2577    }
2578
2579    fn fold_directory(&mut self, _: &FoldDirectory, window: &mut Window, cx: &mut Context<Self>) {
2580        if let Some((worktree, entry)) = self.selected_entry(cx) {
2581            self.state.unfolded_dir_ids.remove(&entry.id);
2582
2583            let snapshot = worktree.snapshot();
2584            let mut path = &*entry.path;
2585            loop {
2586                let mut child_entries_iter = snapshot.child_entries(path);
2587                if let Some(child) = child_entries_iter.next() {
2588                    if child_entries_iter.next().is_none() && child.is_dir() {
2589                        self.state.unfolded_dir_ids.remove(&child.id);
2590                        path = &*child.path;
2591                    } else {
2592                        break;
2593                    }
2594                } else {
2595                    break;
2596                }
2597            }
2598
2599            self.update_visible_entries(None, false, true, window, cx);
2600            cx.notify();
2601        }
2602    }
2603
2604    fn scroll_up(&mut self, _: &ScrollUp, window: &mut Window, cx: &mut Context<Self>) {
2605        for _ in 0..self.rendered_entries_len / 2 {
2606            window.dispatch_action(SelectPrevious.boxed_clone(), cx);
2607        }
2608    }
2609
2610    fn scroll_down(&mut self, _: &ScrollDown, window: &mut Window, cx: &mut Context<Self>) {
2611        for _ in 0..self.rendered_entries_len / 2 {
2612            window.dispatch_action(SelectNext.boxed_clone(), cx);
2613        }
2614    }
2615
2616    fn scroll_cursor_center(
2617        &mut self,
2618        _: &ScrollCursorCenter,
2619        _: &mut Window,
2620        cx: &mut Context<Self>,
2621    ) {
2622        if let Some((_, _, index)) = self.selection.and_then(|s| self.index_for_selection(s)) {
2623            self.scroll_handle
2624                .scroll_to_item_strict(index, ScrollStrategy::Center);
2625            cx.notify();
2626        }
2627    }
2628
2629    fn scroll_cursor_top(&mut self, _: &ScrollCursorTop, _: &mut Window, cx: &mut Context<Self>) {
2630        if let Some((_, _, index)) = self.selection.and_then(|s| self.index_for_selection(s)) {
2631            self.scroll_handle
2632                .scroll_to_item_strict(index, ScrollStrategy::Top);
2633            cx.notify();
2634        }
2635    }
2636
2637    fn scroll_cursor_bottom(
2638        &mut self,
2639        _: &ScrollCursorBottom,
2640        _: &mut Window,
2641        cx: &mut Context<Self>,
2642    ) {
2643        if let Some((_, _, index)) = self.selection.and_then(|s| self.index_for_selection(s)) {
2644            self.scroll_handle
2645                .scroll_to_item_strict(index, ScrollStrategy::Bottom);
2646            cx.notify();
2647        }
2648    }
2649
2650    fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
2651        if let Some(edit_state) = &self.state.edit_state
2652            && edit_state.processing_filename.is_none()
2653        {
2654            self.filename_editor.update(cx, |editor, cx| {
2655                editor.move_to_end_of_line(
2656                    &editor::actions::MoveToEndOfLine {
2657                        stop_at_soft_wraps: false,
2658                    },
2659                    window,
2660                    cx,
2661                );
2662            });
2663            return;
2664        }
2665        if let Some(selection) = self.selection {
2666            let (mut worktree_ix, mut entry_ix, _) =
2667                self.index_for_selection(selection).unwrap_or_default();
2668            if let Some(worktree_entries) = self
2669                .state
2670                .visible_entries
2671                .get(worktree_ix)
2672                .map(|v| &v.entries)
2673            {
2674                if entry_ix + 1 < worktree_entries.len() {
2675                    entry_ix += 1;
2676                } else {
2677                    worktree_ix += 1;
2678                    entry_ix = 0;
2679                }
2680            }
2681
2682            if let Some(VisibleEntriesForWorktree {
2683                worktree_id,
2684                entries,
2685                ..
2686            }) = self.state.visible_entries.get(worktree_ix)
2687                && let Some(entry) = entries.get(entry_ix)
2688            {
2689                let selection = SelectedEntry {
2690                    worktree_id: *worktree_id,
2691                    entry_id: entry.id,
2692                };
2693                self.selection = Some(selection);
2694                if window.modifiers().shift {
2695                    self.marked_entries.push(selection);
2696                }
2697
2698                self.autoscroll(cx);
2699                cx.notify();
2700            }
2701        } else {
2702            self.select_first(&SelectFirst {}, window, cx);
2703        }
2704    }
2705
2706    fn select_prev_diagnostic(
2707        &mut self,
2708        action: &SelectPrevDiagnostic,
2709        window: &mut Window,
2710        cx: &mut Context<Self>,
2711    ) {
2712        let selection = self.find_entry(
2713            self.selection.as_ref(),
2714            true,
2715            &|entry: GitEntryRef, worktree_id: WorktreeId| {
2716                self.selection.is_none_or(|selection| {
2717                    if selection.worktree_id == worktree_id {
2718                        selection.entry_id != entry.id
2719                    } else {
2720                        true
2721                    }
2722                }) && entry.is_file()
2723                    && self
2724                        .diagnostics
2725                        .get(&(worktree_id, entry.path.clone()))
2726                        .is_some_and(|severity| action.severity.matches(*severity))
2727            },
2728            cx,
2729        );
2730
2731        if let Some(selection) = selection {
2732            self.selection = Some(selection);
2733            self.expand_entry(selection.worktree_id, selection.entry_id, cx);
2734            self.update_visible_entries(
2735                Some((selection.worktree_id, selection.entry_id)),
2736                false,
2737                true,
2738                window,
2739                cx,
2740            );
2741            cx.notify();
2742        }
2743    }
2744
2745    fn select_next_diagnostic(
2746        &mut self,
2747        action: &SelectNextDiagnostic,
2748        window: &mut Window,
2749        cx: &mut Context<Self>,
2750    ) {
2751        let selection = self.find_entry(
2752            self.selection.as_ref(),
2753            false,
2754            &|entry: GitEntryRef, worktree_id: WorktreeId| {
2755                self.selection.is_none_or(|selection| {
2756                    if selection.worktree_id == worktree_id {
2757                        selection.entry_id != entry.id
2758                    } else {
2759                        true
2760                    }
2761                }) && entry.is_file()
2762                    && self
2763                        .diagnostics
2764                        .get(&(worktree_id, entry.path.clone()))
2765                        .is_some_and(|severity| action.severity.matches(*severity))
2766            },
2767            cx,
2768        );
2769
2770        if let Some(selection) = selection {
2771            self.selection = Some(selection);
2772            self.expand_entry(selection.worktree_id, selection.entry_id, cx);
2773            self.update_visible_entries(
2774                Some((selection.worktree_id, selection.entry_id)),
2775                false,
2776                true,
2777                window,
2778                cx,
2779            );
2780            cx.notify();
2781        }
2782    }
2783
2784    fn select_prev_git_entry(
2785        &mut self,
2786        _: &SelectPrevGitEntry,
2787        window: &mut Window,
2788        cx: &mut Context<Self>,
2789    ) {
2790        let selection = self.find_entry(
2791            self.selection.as_ref(),
2792            true,
2793            &|entry: GitEntryRef, worktree_id: WorktreeId| {
2794                (self.selection.is_none()
2795                    || self.selection.is_some_and(|selection| {
2796                        if selection.worktree_id == worktree_id {
2797                            selection.entry_id != entry.id
2798                        } else {
2799                            true
2800                        }
2801                    }))
2802                    && entry.is_file()
2803                    && entry.git_summary.index.modified + entry.git_summary.worktree.modified > 0
2804            },
2805            cx,
2806        );
2807
2808        if let Some(selection) = selection {
2809            self.selection = Some(selection);
2810            self.expand_entry(selection.worktree_id, selection.entry_id, cx);
2811            self.update_visible_entries(
2812                Some((selection.worktree_id, selection.entry_id)),
2813                false,
2814                true,
2815                window,
2816                cx,
2817            );
2818            cx.notify();
2819        }
2820    }
2821
2822    fn select_prev_directory(
2823        &mut self,
2824        _: &SelectPrevDirectory,
2825        _: &mut Window,
2826        cx: &mut Context<Self>,
2827    ) {
2828        let selection = self.find_visible_entry(
2829            self.selection.as_ref(),
2830            true,
2831            &|entry: GitEntryRef, worktree_id: WorktreeId| {
2832                self.selection.is_none_or(|selection| {
2833                    if selection.worktree_id == worktree_id {
2834                        selection.entry_id != entry.id
2835                    } else {
2836                        true
2837                    }
2838                }) && entry.is_dir()
2839            },
2840            cx,
2841        );
2842
2843        if let Some(selection) = selection {
2844            self.selection = Some(selection);
2845            self.autoscroll(cx);
2846            cx.notify();
2847        }
2848    }
2849
2850    fn select_next_directory(
2851        &mut self,
2852        _: &SelectNextDirectory,
2853        _: &mut Window,
2854        cx: &mut Context<Self>,
2855    ) {
2856        let selection = self.find_visible_entry(
2857            self.selection.as_ref(),
2858            false,
2859            &|entry: GitEntryRef, worktree_id: WorktreeId| {
2860                self.selection.is_none_or(|selection| {
2861                    if selection.worktree_id == worktree_id {
2862                        selection.entry_id != entry.id
2863                    } else {
2864                        true
2865                    }
2866                }) && entry.is_dir()
2867            },
2868            cx,
2869        );
2870
2871        if let Some(selection) = selection {
2872            self.selection = Some(selection);
2873            self.autoscroll(cx);
2874            cx.notify();
2875        }
2876    }
2877
2878    fn select_next_git_entry(
2879        &mut self,
2880        _: &SelectNextGitEntry,
2881        window: &mut Window,
2882        cx: &mut Context<Self>,
2883    ) {
2884        let selection = self.find_entry(
2885            self.selection.as_ref(),
2886            false,
2887            &|entry: GitEntryRef, worktree_id: WorktreeId| {
2888                self.selection.is_none_or(|selection| {
2889                    if selection.worktree_id == worktree_id {
2890                        selection.entry_id != entry.id
2891                    } else {
2892                        true
2893                    }
2894                }) && entry.is_file()
2895                    && entry.git_summary.index.modified + entry.git_summary.worktree.modified > 0
2896            },
2897            cx,
2898        );
2899
2900        if let Some(selection) = selection {
2901            self.selection = Some(selection);
2902            self.expand_entry(selection.worktree_id, selection.entry_id, cx);
2903            self.update_visible_entries(
2904                Some((selection.worktree_id, selection.entry_id)),
2905                false,
2906                true,
2907                window,
2908                cx,
2909            );
2910            cx.notify();
2911        }
2912    }
2913
2914    fn select_parent(&mut self, _: &SelectParent, window: &mut Window, cx: &mut Context<Self>) {
2915        if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
2916            if let Some(parent) = entry.path.parent() {
2917                let worktree = worktree.read(cx);
2918                if let Some(parent_entry) = worktree.entry_for_path(parent) {
2919                    self.selection = Some(SelectedEntry {
2920                        worktree_id: worktree.id(),
2921                        entry_id: parent_entry.id,
2922                    });
2923                    self.autoscroll(cx);
2924                    cx.notify();
2925                }
2926            }
2927        } else {
2928            self.select_first(&SelectFirst {}, window, cx);
2929        }
2930    }
2931
2932    fn select_first(&mut self, _: &SelectFirst, window: &mut Window, cx: &mut Context<Self>) {
2933        if let Some(VisibleEntriesForWorktree {
2934            worktree_id,
2935            entries,
2936            ..
2937        }) = self.state.visible_entries.first()
2938            && let Some(entry) = entries.first()
2939        {
2940            let selection = SelectedEntry {
2941                worktree_id: *worktree_id,
2942                entry_id: entry.id,
2943            };
2944            self.selection = Some(selection);
2945            if window.modifiers().shift {
2946                self.marked_entries.push(selection);
2947            }
2948            self.autoscroll(cx);
2949            cx.notify();
2950        }
2951    }
2952
2953    fn select_last(&mut self, _: &SelectLast, _: &mut Window, cx: &mut Context<Self>) {
2954        if let Some(VisibleEntriesForWorktree {
2955            worktree_id,
2956            entries,
2957            ..
2958        }) = self.state.visible_entries.last()
2959        {
2960            let worktree = self.project.read(cx).worktree_for_id(*worktree_id, cx);
2961            if let (Some(worktree), Some(entry)) = (worktree, entries.last()) {
2962                let worktree = worktree.read(cx);
2963                if let Some(entry) = worktree.entry_for_id(entry.id) {
2964                    let selection = SelectedEntry {
2965                        worktree_id: *worktree_id,
2966                        entry_id: entry.id,
2967                    };
2968                    self.selection = Some(selection);
2969                    self.autoscroll(cx);
2970                    cx.notify();
2971                }
2972            }
2973        }
2974    }
2975
2976    fn autoscroll(&mut self, cx: &mut Context<Self>) {
2977        if let Some((_, _, index)) = self.selection.and_then(|s| self.index_for_selection(s)) {
2978            self.scroll_handle.scroll_to_item_with_offset(
2979                index,
2980                ScrollStrategy::Center,
2981                self.sticky_items_count,
2982            );
2983            cx.notify();
2984        }
2985    }
2986
2987    fn cut(&mut self, _: &Cut, _: &mut Window, cx: &mut Context<Self>) {
2988        let entries = self.disjoint_effective_entries(cx);
2989        if !entries.is_empty() {
2990            self.clipboard = Some(ClipboardEntry::Cut(entries));
2991            cx.notify();
2992        }
2993    }
2994
2995    fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context<Self>) {
2996        let entries = self.disjoint_effective_entries(cx);
2997        if !entries.is_empty() {
2998            self.clipboard = Some(ClipboardEntry::Copied(entries));
2999            cx.notify();
3000        }
3001    }
3002
3003    fn create_paste_path(
3004        &self,
3005        source: &SelectedEntry,
3006        (worktree, target_entry): (Entity<Worktree>, &Entry),
3007        cx: &App,
3008    ) -> Option<(Arc<RelPath>, Option<Range<usize>>)> {
3009        let mut new_path = target_entry.path.to_rel_path_buf();
3010        // If we're pasting into a file, or a directory into itself, go up one level.
3011        if target_entry.is_file() || (target_entry.is_dir() && target_entry.id == source.entry_id) {
3012            new_path.pop();
3013        }
3014        let clipboard_entry_file_name = self
3015            .project
3016            .read(cx)
3017            .path_for_entry(source.entry_id, cx)?
3018            .path
3019            .file_name()?
3020            .to_string();
3021        new_path.push(RelPath::unix(&clipboard_entry_file_name).unwrap());
3022        let extension = new_path.extension().map(|s| s.to_string());
3023        let file_name_without_extension = new_path.file_stem()?.to_string();
3024        let file_name_len = file_name_without_extension.len();
3025        let mut disambiguation_range = None;
3026        let mut ix = 0;
3027        {
3028            let worktree = worktree.read(cx);
3029            while worktree.entry_for_path(&new_path).is_some() {
3030                new_path.pop();
3031
3032                let mut new_file_name = file_name_without_extension.to_string();
3033
3034                let disambiguation = " copy";
3035                let mut disambiguation_len = disambiguation.len();
3036
3037                new_file_name.push_str(disambiguation);
3038
3039                if ix > 0 {
3040                    let extra_disambiguation = format!(" {}", ix);
3041                    disambiguation_len += extra_disambiguation.len();
3042                    new_file_name.push_str(&extra_disambiguation);
3043                }
3044                if let Some(extension) = extension.as_ref() {
3045                    new_file_name.push_str(".");
3046                    new_file_name.push_str(extension);
3047                }
3048
3049                new_path.push(RelPath::unix(&new_file_name).unwrap());
3050
3051                disambiguation_range = Some(file_name_len..(file_name_len + disambiguation_len));
3052                ix += 1;
3053            }
3054        }
3055        Some((new_path.as_rel_path().into(), disambiguation_range))
3056    }
3057
3058    fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
3059        maybe!({
3060            let (worktree, entry) = self.selected_entry_handle(cx)?;
3061            let entry = entry.clone();
3062            let worktree_id = worktree.read(cx).id();
3063            let clipboard_entries = self
3064                .clipboard
3065                .as_ref()
3066                .filter(|clipboard| !clipboard.items().is_empty())?;
3067
3068            enum PasteTask {
3069                Rename(Task<Result<CreatedEntry>>),
3070                Copy(Task<Result<Option<Entry>>>),
3071            }
3072
3073            let mut paste_tasks = Vec::new();
3074            let mut disambiguation_range = None;
3075            let clip_is_cut = clipboard_entries.is_cut();
3076            for clipboard_entry in clipboard_entries.items() {
3077                let (new_path, new_disambiguation_range) =
3078                    self.create_paste_path(clipboard_entry, self.selected_sub_entry(cx)?, cx)?;
3079                let clip_entry_id = clipboard_entry.entry_id;
3080                let task = if clipboard_entries.is_cut() {
3081                    let task = self.project.update(cx, |project, cx| {
3082                        project.rename_entry(clip_entry_id, (worktree_id, new_path).into(), cx)
3083                    });
3084                    PasteTask::Rename(task)
3085                } else {
3086                    let task = self.project.update(cx, |project, cx| {
3087                        project.copy_entry(clip_entry_id, (worktree_id, new_path).into(), cx)
3088                    });
3089                    PasteTask::Copy(task)
3090                };
3091                paste_tasks.push(task);
3092                disambiguation_range = new_disambiguation_range.or(disambiguation_range);
3093            }
3094
3095            let item_count = paste_tasks.len();
3096            let workspace = self.workspace.clone();
3097
3098            cx.spawn_in(window, async move |project_panel, mut cx| {
3099                let mut last_succeed = None;
3100                for task in paste_tasks {
3101                    match task {
3102                        PasteTask::Rename(task) => {
3103                            if let Some(CreatedEntry::Included(entry)) = task
3104                                .await
3105                                .notify_workspace_async_err(workspace.clone(), &mut cx)
3106                            {
3107                                last_succeed = Some(entry);
3108                            }
3109                        }
3110                        PasteTask::Copy(task) => {
3111                            if let Some(Some(entry)) = task
3112                                .await
3113                                .notify_workspace_async_err(workspace.clone(), &mut cx)
3114                            {
3115                                last_succeed = Some(entry);
3116                            }
3117                        }
3118                    }
3119                }
3120                // update selection
3121                if let Some(entry) = last_succeed {
3122                    project_panel
3123                        .update_in(cx, |project_panel, window, cx| {
3124                            project_panel.selection = Some(SelectedEntry {
3125                                worktree_id,
3126                                entry_id: entry.id,
3127                            });
3128
3129                            if item_count == 1 {
3130                                // open entry if not dir, setting is enabled, and only focus if rename is not pending
3131                                if !entry.is_dir() {
3132                                    let settings = ProjectPanelSettings::get_global(cx);
3133                                    if settings.auto_open.should_open_on_paste() {
3134                                        project_panel.open_entry(
3135                                            entry.id,
3136                                            disambiguation_range.is_none(),
3137                                            false,
3138                                            cx,
3139                                        );
3140                                    }
3141                                }
3142
3143                                // if only one entry was pasted and it was disambiguated, open the rename editor
3144                                if disambiguation_range.is_some() {
3145                                    cx.defer_in(window, |this, window, cx| {
3146                                        this.rename_impl(disambiguation_range, window, cx);
3147                                    });
3148                                }
3149                            }
3150                        })
3151                        .ok();
3152                }
3153
3154                anyhow::Ok(())
3155            })
3156            .detach_and_log_err(cx);
3157
3158            if clip_is_cut {
3159                // Convert the clipboard cut entry to a copy entry after the first paste.
3160                self.clipboard = self.clipboard.take().map(ClipboardEntry::into_copy_entry);
3161            }
3162
3163            self.expand_entry(worktree_id, entry.id, cx);
3164            Some(())
3165        });
3166    }
3167
3168    fn download_from_remote(
3169        &mut self,
3170        _: &DownloadFromRemote,
3171        window: &mut Window,
3172        cx: &mut Context<Self>,
3173    ) {
3174        let entries = self.effective_entries();
3175        if entries.is_empty() {
3176            return;
3177        }
3178
3179        let project = self.project.read(cx);
3180
3181        // Collect file entries with their worktree_id, path, and relative path for destination
3182        // For directories, we collect all files under them recursively
3183        let mut files_to_download: Vec<(WorktreeId, Arc<RelPath>, PathBuf)> = Vec::new();
3184
3185        for selected in entries.iter() {
3186            let Some(worktree) = project.worktree_for_id(selected.worktree_id, cx) else {
3187                continue;
3188            };
3189            let worktree = worktree.read(cx);
3190            let Some(entry) = worktree.entry_for_id(selected.entry_id) else {
3191                continue;
3192            };
3193
3194            if entry.is_file() {
3195                // Single file: use just the filename
3196                let filename = entry
3197                    .path
3198                    .file_name()
3199                    .map(str::to_string)
3200                    .unwrap_or_default();
3201                files_to_download.push((
3202                    selected.worktree_id,
3203                    entry.path.clone(),
3204                    PathBuf::from(filename),
3205                ));
3206            } else if entry.is_dir() {
3207                // Directory: collect all files recursively, preserving relative paths
3208                let dir_name = entry
3209                    .path
3210                    .file_name()
3211                    .map(str::to_string)
3212                    .unwrap_or_default();
3213                let base_path = entry.path.clone();
3214
3215                // Use traverse_from_path to iterate all entries under this directory
3216                let mut traversal = worktree.traverse_from_path(true, true, true, &entry.path);
3217                while let Some(child_entry) = traversal.entry() {
3218                    // Stop when we're no longer under the directory
3219                    if !child_entry.path.starts_with(&base_path) {
3220                        break;
3221                    }
3222
3223                    if child_entry.is_file() {
3224                        // Calculate relative path from the directory root
3225                        let relative_path = child_entry
3226                            .path
3227                            .strip_prefix(&base_path)
3228                            .map(|p| PathBuf::from(dir_name.clone()).join(p.as_unix_str()))
3229                            .unwrap_or_else(|_| {
3230                                PathBuf::from(
3231                                    child_entry
3232                                        .path
3233                                        .file_name()
3234                                        .map(str::to_string)
3235                                        .unwrap_or_default(),
3236                                )
3237                            });
3238                        files_to_download.push((
3239                            selected.worktree_id,
3240                            child_entry.path.clone(),
3241                            relative_path,
3242                        ));
3243                    }
3244                    traversal.advance();
3245                }
3246            }
3247        }
3248
3249        if files_to_download.is_empty() {
3250            return;
3251        }
3252
3253        let total_files = files_to_download.len();
3254        let workspace = self.workspace.clone();
3255
3256        let destination_dir = cx.prompt_for_paths(PathPromptOptions {
3257            files: false,
3258            directories: true,
3259            multiple: false,
3260            prompt: Some("Download".into()),
3261        });
3262
3263        let fs = self.fs.clone();
3264        let notification_id =
3265            workspace::notifications::NotificationId::Named("download-progress".into());
3266        cx.spawn_in(window, async move |this, cx| {
3267            if let Ok(Ok(Some(mut paths))) = destination_dir.await {
3268                if let Some(dest_dir) = paths.pop() {
3269                    // Show initial toast
3270                    workspace
3271                        .update(cx, |workspace, cx| {
3272                            workspace.show_toast(
3273                                workspace::Toast::new(
3274                                    notification_id.clone(),
3275                                    format!("Downloading 0/{} files...", total_files),
3276                                ),
3277                                cx,
3278                            );
3279                        })
3280                        .ok();
3281
3282                    for (index, (worktree_id, entry_path, relative_path)) in
3283                        files_to_download.into_iter().enumerate()
3284                    {
3285                        // Update progress toast
3286                        workspace
3287                            .update(cx, |workspace, cx| {
3288                                workspace.show_toast(
3289                                    workspace::Toast::new(
3290                                        notification_id.clone(),
3291                                        format!(
3292                                            "Downloading {}/{} files...",
3293                                            index + 1,
3294                                            total_files
3295                                        ),
3296                                    ),
3297                                    cx,
3298                                );
3299                            })
3300                            .ok();
3301
3302                        let destination_path = dest_dir.join(&relative_path);
3303
3304                        // Create parent directories if needed
3305                        if let Some(parent) = destination_path.parent() {
3306                            if !parent.exists() {
3307                                fs.create_dir(parent).await.log_err();
3308                            }
3309                        }
3310
3311                        let download_task = this.update(cx, |this, cx| {
3312                            let project = this.project.clone();
3313                            project.update(cx, |project, cx| {
3314                                project.download_file(worktree_id, entry_path, destination_path, cx)
3315                            })
3316                        });
3317                        if let Ok(task) = download_task {
3318                            task.await.log_err();
3319                        }
3320                    }
3321
3322                    // Show completion toast
3323                    workspace
3324                        .update(cx, |workspace, cx| {
3325                            workspace.show_toast(
3326                                workspace::Toast::new(
3327                                    notification_id.clone(),
3328                                    format!("Downloaded {} files", total_files),
3329                                ),
3330                                cx,
3331                            );
3332                        })
3333                        .ok();
3334                }
3335            }
3336        })
3337        .detach();
3338    }
3339
3340    fn duplicate(&mut self, _: &Duplicate, window: &mut Window, cx: &mut Context<Self>) {
3341        self.copy(&Copy {}, window, cx);
3342        self.paste(&Paste {}, window, cx);
3343    }
3344
3345    fn copy_path(
3346        &mut self,
3347        _: &zed_actions::workspace::CopyPath,
3348        _: &mut Window,
3349        cx: &mut Context<Self>,
3350    ) {
3351        let abs_file_paths = {
3352            let project = self.project.read(cx);
3353            self.effective_entries()
3354                .into_iter()
3355                .filter_map(|entry| {
3356                    let entry_path = project.path_for_entry(entry.entry_id, cx)?.path;
3357                    Some(
3358                        project
3359                            .worktree_for_id(entry.worktree_id, cx)?
3360                            .read(cx)
3361                            .absolutize(&entry_path)
3362                            .to_string_lossy()
3363                            .to_string(),
3364                    )
3365                })
3366                .collect::<Vec<_>>()
3367        };
3368        if !abs_file_paths.is_empty() {
3369            cx.write_to_clipboard(ClipboardItem::new_string(abs_file_paths.join("\n")));
3370        }
3371    }
3372
3373    fn copy_relative_path(
3374        &mut self,
3375        _: &zed_actions::workspace::CopyRelativePath,
3376        _: &mut Window,
3377        cx: &mut Context<Self>,
3378    ) {
3379        let path_style = self.project.read(cx).path_style(cx);
3380        let file_paths = {
3381            let project = self.project.read(cx);
3382            self.effective_entries()
3383                .into_iter()
3384                .filter_map(|entry| {
3385                    Some(
3386                        project
3387                            .path_for_entry(entry.entry_id, cx)?
3388                            .path
3389                            .display(path_style)
3390                            .into_owned(),
3391                    )
3392                })
3393                .collect::<Vec<_>>()
3394        };
3395        if !file_paths.is_empty() {
3396            cx.write_to_clipboard(ClipboardItem::new_string(file_paths.join("\n")));
3397        }
3398    }
3399
3400    fn reveal_in_finder(
3401        &mut self,
3402        _: &RevealInFileManager,
3403        _: &mut Window,
3404        cx: &mut Context<Self>,
3405    ) {
3406        if let Some(path) = self.reveal_in_file_manager_path(cx) {
3407            self.project
3408                .update(cx, |project, cx| project.reveal_path(&path, cx));
3409        }
3410    }
3411
3412    fn remove_from_project(
3413        &mut self,
3414        _: &RemoveFromProject,
3415        _window: &mut Window,
3416        cx: &mut Context<Self>,
3417    ) {
3418        for entry in self.effective_entries().iter() {
3419            let worktree_id = entry.worktree_id;
3420            self.project
3421                .update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
3422        }
3423    }
3424
3425    fn file_abs_paths_to_diff(&self, cx: &Context<Self>) -> Option<(PathBuf, PathBuf)> {
3426        let mut selections_abs_path = self
3427            .marked_entries
3428            .iter()
3429            .filter_map(|entry| {
3430                let project = self.project.read(cx);
3431                let worktree = project.worktree_for_id(entry.worktree_id, cx)?;
3432                let entry = worktree.read(cx).entry_for_id(entry.entry_id)?;
3433                if !entry.is_file() {
3434                    return None;
3435                }
3436                Some(worktree.read(cx).absolutize(&entry.path))
3437            })
3438            .rev();
3439
3440        let last_path = selections_abs_path.next()?;
3441        let previous_to_last = selections_abs_path.next()?;
3442        Some((previous_to_last, last_path))
3443    }
3444
3445    fn compare_marked_files(
3446        &mut self,
3447        _: &CompareMarkedFiles,
3448        window: &mut Window,
3449        cx: &mut Context<Self>,
3450    ) {
3451        let selected_files = self.file_abs_paths_to_diff(cx);
3452        if let Some((file_path1, file_path2)) = selected_files {
3453            self.workspace
3454                .update(cx, |workspace, cx| {
3455                    FileDiffView::open(file_path1, file_path2, workspace.weak_handle(), window, cx)
3456                        .detach_and_log_err(cx);
3457                })
3458                .ok();
3459        }
3460    }
3461
3462    fn open_system(&mut self, _: &OpenWithSystem, _: &mut Window, cx: &mut Context<Self>) {
3463        if let Some((worktree, entry)) = self.selected_entry(cx) {
3464            let abs_path = worktree.absolutize(&entry.path);
3465            cx.open_with_system(&abs_path);
3466        }
3467    }
3468
3469    fn open_in_terminal(
3470        &mut self,
3471        _: &OpenInTerminal,
3472        window: &mut Window,
3473        cx: &mut Context<Self>,
3474    ) {
3475        if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
3476            let abs_path = match &entry.canonical_path {
3477                Some(canonical_path) => canonical_path.to_path_buf(),
3478                None => worktree.read(cx).absolutize(&entry.path),
3479            };
3480
3481            let working_directory = if entry.is_dir() {
3482                Some(abs_path)
3483            } else {
3484                abs_path.parent().map(|path| path.to_path_buf())
3485            };
3486            if let Some(working_directory) = working_directory {
3487                window.dispatch_action(
3488                    workspace::OpenTerminal {
3489                        working_directory,
3490                        local: false,
3491                    }
3492                    .boxed_clone(),
3493                    cx,
3494                )
3495            }
3496        }
3497    }
3498
3499    pub fn new_search_in_directory(
3500        &mut self,
3501        _: &NewSearchInDirectory,
3502        window: &mut Window,
3503        cx: &mut Context<Self>,
3504    ) {
3505        if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
3506            let dir_path = if entry.is_dir() {
3507                entry.path.clone()
3508            } else {
3509                // entry is a file, use its parent directory
3510                match entry.path.parent() {
3511                    Some(parent) => Arc::from(parent),
3512                    None => {
3513                        // File at root, open search with empty filter
3514                        self.workspace
3515                            .update(cx, |workspace, cx| {
3516                                search::ProjectSearchView::new_search_in_directory(
3517                                    workspace,
3518                                    RelPath::empty(),
3519                                    window,
3520                                    cx,
3521                                );
3522                            })
3523                            .ok();
3524                        return;
3525                    }
3526                }
3527            };
3528
3529            let include_root = self.project.read(cx).visible_worktrees(cx).count() > 1;
3530            let dir_path = if include_root {
3531                worktree.read(cx).root_name().join(&dir_path)
3532            } else {
3533                dir_path
3534            };
3535
3536            self.workspace
3537                .update(cx, |workspace, cx| {
3538                    search::ProjectSearchView::new_search_in_directory(
3539                        workspace, &dir_path, window, cx,
3540                    );
3541                })
3542                .ok();
3543        }
3544    }
3545
3546    fn move_entry(
3547        &mut self,
3548        entry_to_move: ProjectEntryId,
3549        destination: ProjectEntryId,
3550        destination_is_file: bool,
3551        cx: &mut Context<Self>,
3552    ) -> Option<Task<Result<CreatedEntry>>> {
3553        if self
3554            .project
3555            .read(cx)
3556            .entry_is_worktree_root(entry_to_move, cx)
3557        {
3558            self.move_worktree_root(entry_to_move, destination, cx);
3559            None
3560        } else {
3561            self.move_worktree_entry(entry_to_move, destination, destination_is_file, cx)
3562        }
3563    }
3564
3565    fn move_worktree_root(
3566        &mut self,
3567        entry_to_move: ProjectEntryId,
3568        destination: ProjectEntryId,
3569        cx: &mut Context<Self>,
3570    ) {
3571        self.project.update(cx, |project, cx| {
3572            let Some(worktree_to_move) = project.worktree_for_entry(entry_to_move, cx) else {
3573                return;
3574            };
3575            let Some(destination_worktree) = project.worktree_for_entry(destination, cx) else {
3576                return;
3577            };
3578
3579            let worktree_id = worktree_to_move.read(cx).id();
3580            let destination_id = destination_worktree.read(cx).id();
3581
3582            project
3583                .move_worktree(worktree_id, destination_id, cx)
3584                .log_err();
3585        });
3586    }
3587
3588    fn move_worktree_entry(
3589        &mut self,
3590        entry_to_move: ProjectEntryId,
3591        destination_entry: ProjectEntryId,
3592        destination_is_file: bool,
3593        cx: &mut Context<Self>,
3594    ) -> Option<Task<Result<CreatedEntry>>> {
3595        if entry_to_move == destination_entry {
3596            return None;
3597        }
3598
3599        let (destination_worktree, rename_task) = self.project.update(cx, |project, cx| {
3600            let Some(source_path) = project.path_for_entry(entry_to_move, cx) else {
3601                return (None, None);
3602            };
3603            let Some(destination_path) = project.path_for_entry(destination_entry, cx) else {
3604                return (None, None);
3605            };
3606            let destination_worktree_id = destination_path.worktree_id;
3607
3608            let destination_dir = if destination_is_file {
3609                destination_path.path.parent().unwrap_or(RelPath::empty())
3610            } else {
3611                destination_path.path.as_ref()
3612            };
3613
3614            let Some(source_name) = source_path.path.file_name() else {
3615                return (None, None);
3616            };
3617            let Ok(source_name) = RelPath::unix(source_name) else {
3618                return (None, None);
3619            };
3620
3621            let mut new_path = destination_dir.to_rel_path_buf();
3622            new_path.push(source_name);
3623            let rename_task = (new_path.as_rel_path() != source_path.path.as_ref()).then(|| {
3624                project.rename_entry(
3625                    entry_to_move,
3626                    (destination_worktree_id, new_path).into(),
3627                    cx,
3628                )
3629            });
3630
3631            (
3632                project.worktree_id_for_entry(destination_entry, cx),
3633                rename_task,
3634            )
3635        });
3636
3637        if let Some(destination_worktree) = destination_worktree {
3638            self.expand_entry(destination_worktree, destination_entry, cx);
3639        }
3640        rename_task
3641    }
3642
3643    fn index_for_selection(&self, selection: SelectedEntry) -> Option<(usize, usize, usize)> {
3644        self.index_for_entry(selection.entry_id, selection.worktree_id)
3645    }
3646
3647    fn disjoint_effective_entries(&self, cx: &App) -> BTreeSet<SelectedEntry> {
3648        self.disjoint_entries(self.effective_entries(), cx)
3649    }
3650
3651    fn disjoint_entries(
3652        &self,
3653        entries: BTreeSet<SelectedEntry>,
3654        cx: &App,
3655    ) -> BTreeSet<SelectedEntry> {
3656        let mut sanitized_entries = BTreeSet::new();
3657        if entries.is_empty() {
3658            return sanitized_entries;
3659        }
3660
3661        let project = self.project.read(cx);
3662        let entries_by_worktree: HashMap<WorktreeId, Vec<SelectedEntry>> = entries
3663            .into_iter()
3664            .filter(|entry| !project.entry_is_worktree_root(entry.entry_id, cx))
3665            .fold(HashMap::default(), |mut map, entry| {
3666                map.entry(entry.worktree_id).or_default().push(entry);
3667                map
3668            });
3669
3670        for (worktree_id, worktree_entries) in entries_by_worktree {
3671            if let Some(worktree) = project.worktree_for_id(worktree_id, cx) {
3672                let worktree = worktree.read(cx);
3673                let dir_paths = worktree_entries
3674                    .iter()
3675                    .filter_map(|entry| {
3676                        worktree.entry_for_id(entry.entry_id).and_then(|entry| {
3677                            if entry.is_dir() {
3678                                Some(entry.path.as_ref())
3679                            } else {
3680                                None
3681                            }
3682                        })
3683                    })
3684                    .collect::<BTreeSet<_>>();
3685
3686                sanitized_entries.extend(worktree_entries.into_iter().filter(|entry| {
3687                    let Some(entry_info) = worktree.entry_for_id(entry.entry_id) else {
3688                        return false;
3689                    };
3690                    let entry_path = entry_info.path.as_ref();
3691                    let inside_selected_dir = dir_paths.iter().any(|&dir_path| {
3692                        entry_path != dir_path && entry_path.starts_with(dir_path)
3693                    });
3694                    !inside_selected_dir
3695                }));
3696            }
3697        }
3698
3699        sanitized_entries
3700    }
3701
3702    fn effective_entries(&self) -> BTreeSet<SelectedEntry> {
3703        if let Some(selection) = self.selection {
3704            let selection = SelectedEntry {
3705                entry_id: self.resolve_entry(selection.entry_id),
3706                worktree_id: selection.worktree_id,
3707            };
3708
3709            // Default to using just the selected item when nothing is marked.
3710            if self.marked_entries.is_empty() {
3711                return BTreeSet::from([selection]);
3712            }
3713
3714            // Allow operating on the selected item even when something else is marked,
3715            // making it easier to perform one-off actions without clearing a mark.
3716            if self.marked_entries.len() == 1 && !self.marked_entries.contains(&selection) {
3717                return BTreeSet::from([selection]);
3718            }
3719        }
3720
3721        // Return only marked entries since we've already handled special cases where
3722        // only selection should take precedence. At this point, marked entries may or
3723        // may not include the current selection, which is intentional.
3724        self.marked_entries
3725            .iter()
3726            .map(|entry| SelectedEntry {
3727                entry_id: self.resolve_entry(entry.entry_id),
3728                worktree_id: entry.worktree_id,
3729            })
3730            .collect::<BTreeSet<_>>()
3731    }
3732
3733    /// Finds the currently selected subentry for a given leaf entry id. If a given entry
3734    /// has no ancestors, the project entry ID that's passed in is returned as-is.
3735    fn resolve_entry(&self, id: ProjectEntryId) -> ProjectEntryId {
3736        self.state
3737            .ancestors
3738            .get(&id)
3739            .and_then(|ancestors| ancestors.active_ancestor())
3740            .unwrap_or(id)
3741    }
3742
3743    pub fn selected_entry<'a>(&self, cx: &'a App) -> Option<(&'a Worktree, &'a project::Entry)> {
3744        let (worktree, entry) = self.selected_entry_handle(cx)?;
3745        Some((worktree.read(cx), entry))
3746    }
3747
3748    /// Compared to selected_entry, this function resolves to the currently
3749    /// selected subentry if dir auto-folding is enabled.
3750    fn selected_sub_entry<'a>(
3751        &self,
3752        cx: &'a App,
3753    ) -> Option<(Entity<Worktree>, &'a project::Entry)> {
3754        let (worktree, mut entry) = self.selected_entry_handle(cx)?;
3755
3756        let resolved_id = self.resolve_entry(entry.id);
3757        if resolved_id != entry.id {
3758            let worktree = worktree.read(cx);
3759            entry = worktree.entry_for_id(resolved_id)?;
3760        }
3761        Some((worktree, entry))
3762    }
3763
3764    fn reveal_in_file_manager_path(&self, cx: &App) -> Option<PathBuf> {
3765        if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
3766            return Some(worktree.read(cx).absolutize(&entry.path));
3767        }
3768
3769        let root_entry_id = self.state.last_worktree_root_id?;
3770        let project = self.project.read(cx);
3771        let worktree = project.worktree_for_entry(root_entry_id, cx)?;
3772        let worktree = worktree.read(cx);
3773        let root_entry = worktree.entry_for_id(root_entry_id)?;
3774        Some(worktree.absolutize(&root_entry.path))
3775    }
3776
3777    fn selected_entry_handle<'a>(
3778        &self,
3779        cx: &'a App,
3780    ) -> Option<(Entity<Worktree>, &'a project::Entry)> {
3781        let selection = self.selection?;
3782        let project = self.project.read(cx);
3783        let worktree = project.worktree_for_id(selection.worktree_id, cx)?;
3784        let entry = worktree.read(cx).entry_for_id(selection.entry_id)?;
3785        Some((worktree, entry))
3786    }
3787
3788    fn expand_to_selection(&mut self, cx: &mut Context<Self>) -> Option<()> {
3789        let (worktree, entry) = self.selected_entry(cx)?;
3790        let expanded_dir_ids = self
3791            .state
3792            .expanded_dir_ids
3793            .entry(worktree.id())
3794            .or_default();
3795
3796        for path in entry.path.ancestors() {
3797            let Some(entry) = worktree.entry_for_path(path) else {
3798                continue;
3799            };
3800            if entry.is_dir()
3801                && let Err(idx) = expanded_dir_ids.binary_search(&entry.id)
3802            {
3803                expanded_dir_ids.insert(idx, entry.id);
3804            }
3805        }
3806
3807        Some(())
3808    }
3809
3810    fn create_new_git_entry(
3811        parent_entry: &Entry,
3812        git_summary: GitSummary,
3813        new_entry_kind: EntryKind,
3814    ) -> GitEntry {
3815        GitEntry {
3816            entry: Entry {
3817                id: NEW_ENTRY_ID,
3818                kind: new_entry_kind,
3819                path: parent_entry.path.join(RelPath::unix("\0").unwrap()),
3820                inode: 0,
3821                mtime: parent_entry.mtime,
3822                size: parent_entry.size,
3823                is_ignored: parent_entry.is_ignored,
3824                is_hidden: parent_entry.is_hidden,
3825                is_external: false,
3826                is_private: false,
3827                is_always_included: parent_entry.is_always_included,
3828                canonical_path: parent_entry.canonical_path.clone(),
3829                char_bag: parent_entry.char_bag,
3830                is_fifo: parent_entry.is_fifo,
3831            },
3832            git_summary,
3833        }
3834    }
3835
3836    fn update_visible_entries(
3837        &mut self,
3838        new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,
3839        focus_filename_editor: bool,
3840        autoscroll: bool,
3841        window: &mut Window,
3842        cx: &mut Context<Self>,
3843    ) {
3844        let now = Instant::now();
3845        let settings = ProjectPanelSettings::get_global(cx);
3846        let auto_collapse_dirs = settings.auto_fold_dirs;
3847        let hide_gitignore = settings.hide_gitignore;
3848        let sort_mode = settings.sort_mode;
3849        let project = self.project.read(cx);
3850        let repo_snapshots = project.git_store().read(cx).repo_snapshots(cx);
3851
3852        let old_ancestors = self.state.ancestors.clone();
3853        let temporary_unfolded_pending_state = self.state.temporarily_unfolded_pending_state.take();
3854        let mut new_state = State::derive(&self.state);
3855        new_state.last_worktree_root_id = project
3856            .visible_worktrees(cx)
3857            .next_back()
3858            .and_then(|worktree| worktree.read(cx).root_entry())
3859            .map(|entry| entry.id);
3860        let mut max_width_item = None;
3861
3862        let visible_worktrees: Vec<_> = project
3863            .visible_worktrees(cx)
3864            .map(|worktree| worktree.read(cx).snapshot())
3865            .collect();
3866        let hide_root = settings.hide_root && visible_worktrees.len() == 1;
3867        let hide_hidden = settings.hide_hidden;
3868
3869        let visible_entries_task = cx.spawn_in(window, async move |this, cx| {
3870            let new_state = cx
3871                .background_spawn(async move {
3872                    for worktree_snapshot in visible_worktrees {
3873                        let worktree_id = worktree_snapshot.id();
3874
3875                        let mut new_entry_parent_id = None;
3876                        let mut new_entry_kind = EntryKind::Dir;
3877                        if let Some(edit_state) = &new_state.edit_state
3878                            && edit_state.worktree_id == worktree_id
3879                            && edit_state.is_new_entry()
3880                        {
3881                            new_entry_parent_id = Some(edit_state.entry_id);
3882                            new_entry_kind = if edit_state.is_dir {
3883                                EntryKind::Dir
3884                            } else {
3885                                EntryKind::File
3886                            };
3887                        }
3888
3889                        let mut visible_worktree_entries = Vec::new();
3890                        let mut entry_iter =
3891                            GitTraversal::new(&repo_snapshots, worktree_snapshot.entries(true, 0));
3892                        let mut auto_folded_ancestors = vec![];
3893                        let worktree_abs_path = worktree_snapshot.abs_path();
3894                        while let Some(entry) = entry_iter.entry() {
3895                            if hide_root && Some(entry.entry) == worktree_snapshot.root_entry() {
3896                                if new_entry_parent_id == Some(entry.id) {
3897                                    visible_worktree_entries.push(Self::create_new_git_entry(
3898                                        entry.entry,
3899                                        entry.git_summary,
3900                                        new_entry_kind,
3901                                    ));
3902                                    new_entry_parent_id = None;
3903                                }
3904                                entry_iter.advance();
3905                                continue;
3906                            }
3907                            if auto_collapse_dirs && entry.kind.is_dir() {
3908                                auto_folded_ancestors.push(entry.id);
3909                                if !new_state.is_unfolded(&entry.id)
3910                                    && let Some(root_path) = worktree_snapshot.root_entry()
3911                                {
3912                                    let mut child_entries =
3913                                        worktree_snapshot.child_entries(&entry.path);
3914                                    if let Some(child) = child_entries.next()
3915                                        && entry.path != root_path.path
3916                                        && child_entries.next().is_none()
3917                                        && child.kind.is_dir()
3918                                    {
3919                                        entry_iter.advance();
3920
3921                                        continue;
3922                                    }
3923                                }
3924                                let depth = temporary_unfolded_pending_state
3925                                    .as_ref()
3926                                    .and_then(|state| {
3927                                        if state.previously_focused_leaf_entry.worktree_id
3928                                            == worktree_id
3929                                            && state.previously_focused_leaf_entry.entry_id
3930                                                == entry.id
3931                                        {
3932                                            auto_folded_ancestors.iter().rev().position(|id| {
3933                                                *id == state.temporarily_unfolded_active_entry_id
3934                                            })
3935                                        } else {
3936                                            None
3937                                        }
3938                                    })
3939                                    .unwrap_or_else(|| {
3940                                        old_ancestors
3941                                            .get(&entry.id)
3942                                            .map(|ancestor| ancestor.current_ancestor_depth)
3943                                            .unwrap_or_default()
3944                                    })
3945                                    .min(auto_folded_ancestors.len());
3946                                if let Some(edit_state) = &mut new_state.edit_state
3947                                    && edit_state.entry_id == entry.id
3948                                {
3949                                    edit_state.depth = depth;
3950                                }
3951                                let mut ancestors = std::mem::take(&mut auto_folded_ancestors);
3952                                if ancestors.len() > 1 {
3953                                    ancestors.reverse();
3954                                    new_state.ancestors.insert(
3955                                        entry.id,
3956                                        FoldedAncestors {
3957                                            current_ancestor_depth: depth,
3958                                            ancestors,
3959                                        },
3960                                    );
3961                                }
3962                            }
3963                            auto_folded_ancestors.clear();
3964                            if (!hide_gitignore || !entry.is_ignored)
3965                                && (!hide_hidden || !entry.is_hidden)
3966                            {
3967                                visible_worktree_entries.push(entry.to_owned());
3968                            }
3969                            let precedes_new_entry = if let Some(new_entry_id) = new_entry_parent_id
3970                            {
3971                                entry.id == new_entry_id || {
3972                                    new_state.ancestors.get(&entry.id).is_some_and(|entries| {
3973                                        entries.ancestors.contains(&new_entry_id)
3974                                    })
3975                                }
3976                            } else {
3977                                false
3978                            };
3979                            if precedes_new_entry
3980                                && (!hide_gitignore || !entry.is_ignored)
3981                                && (!hide_hidden || !entry.is_hidden)
3982                            {
3983                                visible_worktree_entries.push(Self::create_new_git_entry(
3984                                    entry.entry,
3985                                    entry.git_summary,
3986                                    new_entry_kind,
3987                                ));
3988                            }
3989
3990                            let (depth, chars) = if Some(entry.entry)
3991                                == worktree_snapshot.root_entry()
3992                            {
3993                                let Some(path_name) = worktree_abs_path.file_name() else {
3994                                    entry_iter.advance();
3995                                    continue;
3996                                };
3997                                let depth = 0;
3998                                (depth, path_name.to_string_lossy().chars().count())
3999                            } else if entry.is_file() {
4000                                let Some(path_name) = entry
4001                                    .path
4002                                    .file_name()
4003                                    .with_context(|| {
4004                                        format!("Non-root entry has no file name: {entry:?}")
4005                                    })
4006                                    .log_err()
4007                                else {
4008                                    continue;
4009                                };
4010                                let depth = entry.path.ancestors().count() - 1;
4011                                (depth, path_name.chars().count())
4012                            } else {
4013                                let path = new_state
4014                                    .ancestors
4015                                    .get(&entry.id)
4016                                    .and_then(|ancestors| {
4017                                        let outermost_ancestor = ancestors.ancestors.last()?;
4018                                        let root_folded_entry = worktree_snapshot
4019                                            .entry_for_id(*outermost_ancestor)?
4020                                            .path
4021                                            .as_ref();
4022                                        entry.path.strip_prefix(root_folded_entry).ok().and_then(
4023                                            |suffix| {
4024                                                Some(
4025                                                    RelPath::unix(root_folded_entry.file_name()?)
4026                                                        .unwrap()
4027                                                        .join(suffix),
4028                                                )
4029                                            },
4030                                        )
4031                                    })
4032                                    .or_else(|| {
4033                                        entry.path.file_name().map(|file_name| {
4034                                            RelPath::unix(file_name).unwrap().into()
4035                                        })
4036                                    })
4037                                    .unwrap_or_else(|| entry.path.clone());
4038                                let depth = path.components().count();
4039                                (depth, path.as_unix_str().chars().count())
4040                            };
4041                            let width_estimate =
4042                                item_width_estimate(depth, chars, entry.canonical_path.is_some());
4043
4044                            match max_width_item.as_mut() {
4045                                Some((id, worktree_id, width)) => {
4046                                    if *width < width_estimate {
4047                                        *id = entry.id;
4048                                        *worktree_id = worktree_snapshot.id();
4049                                        *width = width_estimate;
4050                                    }
4051                                }
4052                                None => {
4053                                    max_width_item =
4054                                        Some((entry.id, worktree_snapshot.id(), width_estimate))
4055                                }
4056                            }
4057
4058                            let expanded_dir_ids =
4059                                match new_state.expanded_dir_ids.entry(worktree_id) {
4060                                    hash_map::Entry::Occupied(e) => e.into_mut(),
4061                                    hash_map::Entry::Vacant(e) => {
4062                                        // The first time a worktree's root entry becomes available,
4063                                        // mark that root entry as expanded.
4064                                        if let Some(entry) = worktree_snapshot.root_entry() {
4065                                            e.insert(vec![entry.id]).as_slice()
4066                                        } else {
4067                                            &[]
4068                                        }
4069                                    }
4070                                };
4071
4072                            if expanded_dir_ids.binary_search(&entry.id).is_err()
4073                                && entry_iter.advance_to_sibling()
4074                            {
4075                                continue;
4076                            }
4077                            entry_iter.advance();
4078                        }
4079
4080                        par_sort_worktree_entries_with_mode(
4081                            &mut visible_worktree_entries,
4082                            sort_mode,
4083                        );
4084                        new_state.visible_entries.push(VisibleEntriesForWorktree {
4085                            worktree_id,
4086                            entries: visible_worktree_entries,
4087                            index: OnceCell::new(),
4088                        })
4089                    }
4090                    if let Some((project_entry_id, worktree_id, _)) = max_width_item {
4091                        let mut visited_worktrees_length = 0;
4092                        let index = new_state
4093                            .visible_entries
4094                            .iter()
4095                            .find_map(|visible_entries| {
4096                                if worktree_id == visible_entries.worktree_id {
4097                                    visible_entries
4098                                        .entries
4099                                        .iter()
4100                                        .position(|entry| entry.id == project_entry_id)
4101                                } else {
4102                                    visited_worktrees_length += visible_entries.entries.len();
4103                                    None
4104                                }
4105                            });
4106                        if let Some(index) = index {
4107                            new_state.max_width_item_index = Some(visited_worktrees_length + index);
4108                        }
4109                    }
4110                    new_state
4111                })
4112                .await;
4113            this.update_in(cx, |this, window, cx| {
4114                this.state = new_state;
4115                if let Some((worktree_id, entry_id)) = new_selected_entry {
4116                    this.selection = Some(SelectedEntry {
4117                        worktree_id,
4118                        entry_id,
4119                    });
4120                }
4121                let elapsed = now.elapsed();
4122                if this.last_reported_update.elapsed() > Duration::from_secs(3600) {
4123                    telemetry::event!(
4124                        "Project Panel Updated",
4125                        elapsed_ms = elapsed.as_millis() as u64,
4126                        worktree_entries = this
4127                            .state
4128                            .visible_entries
4129                            .iter()
4130                            .map(|worktree| worktree.entries.len())
4131                            .sum::<usize>(),
4132                    )
4133                }
4134                if this.update_visible_entries_task.focus_filename_editor {
4135                    this.update_visible_entries_task.focus_filename_editor = false;
4136                    this.filename_editor.update(cx, |editor, cx| {
4137                        window.focus(&editor.focus_handle(cx), cx);
4138                    });
4139                }
4140                if this.update_visible_entries_task.autoscroll {
4141                    this.update_visible_entries_task.autoscroll = false;
4142                    this.autoscroll(cx);
4143                }
4144                cx.notify();
4145            })
4146            .ok();
4147        });
4148
4149        self.update_visible_entries_task = UpdateVisibleEntriesTask {
4150            _visible_entries_task: visible_entries_task,
4151            focus_filename_editor: focus_filename_editor
4152                || self.update_visible_entries_task.focus_filename_editor,
4153            autoscroll: autoscroll || self.update_visible_entries_task.autoscroll,
4154        };
4155    }
4156
4157    fn expand_entry(
4158        &mut self,
4159        worktree_id: WorktreeId,
4160        entry_id: ProjectEntryId,
4161        cx: &mut Context<Self>,
4162    ) {
4163        self.project.update(cx, |project, cx| {
4164            if let Some((worktree, expanded_dir_ids)) = project
4165                .worktree_for_id(worktree_id, cx)
4166                .zip(self.state.expanded_dir_ids.get_mut(&worktree_id))
4167            {
4168                project.expand_entry(worktree_id, entry_id, cx);
4169                let worktree = worktree.read(cx);
4170
4171                if let Some(mut entry) = worktree.entry_for_id(entry_id) {
4172                    loop {
4173                        if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) {
4174                            expanded_dir_ids.insert(ix, entry.id);
4175                        }
4176
4177                        if let Some(parent_entry) =
4178                            entry.path.parent().and_then(|p| worktree.entry_for_path(p))
4179                        {
4180                            entry = parent_entry;
4181                        } else {
4182                            break;
4183                        }
4184                    }
4185                }
4186            }
4187        });
4188    }
4189
4190    fn drop_external_files(
4191        &mut self,
4192        paths: &[PathBuf],
4193        entry_id: ProjectEntryId,
4194        window: &mut Window,
4195        cx: &mut Context<Self>,
4196    ) {
4197        let mut paths: Vec<Arc<Path>> = paths.iter().map(|path| Arc::from(path.clone())).collect();
4198
4199        let open_file_after_drop = paths.len() == 1 && paths[0].is_file();
4200
4201        let Some((target_directory, worktree, fs)) = maybe!({
4202            let project = self.project.read(cx);
4203            let fs = project.fs().clone();
4204            let worktree = project.worktree_for_entry(entry_id, cx)?;
4205            let entry = worktree.read(cx).entry_for_id(entry_id)?;
4206            let path = entry.path.clone();
4207            let target_directory = if entry.is_dir() {
4208                path
4209            } else {
4210                path.parent()?.into()
4211            };
4212            Some((target_directory, worktree, fs))
4213        }) else {
4214            return;
4215        };
4216
4217        let mut paths_to_replace = Vec::new();
4218        for path in &paths {
4219            if let Some(name) = path.file_name()
4220                && let Some(name) = name.to_str()
4221            {
4222                let target_path = target_directory.join(RelPath::unix(name).unwrap());
4223                if worktree.read(cx).entry_for_path(&target_path).is_some() {
4224                    paths_to_replace.push((name.to_string(), path.clone()));
4225                }
4226            }
4227        }
4228
4229        cx.spawn_in(window, async move |this, cx| {
4230            async move {
4231                for (filename, original_path) in &paths_to_replace {
4232                    let prompt_message = format!(
4233                        concat!(
4234                            "A file or folder with name {} ",
4235                            "already exists in the destination folder. ",
4236                            "Do you want to replace it?"
4237                        ),
4238                        filename
4239                    );
4240                    let answer = cx
4241                        .update(|window, cx| {
4242                            window.prompt(
4243                                PromptLevel::Info,
4244                                &prompt_message,
4245                                None,
4246                                &["Replace", "Cancel"],
4247                                cx,
4248                            )
4249                        })?
4250                        .await?;
4251
4252                    if answer == 1
4253                        && let Some(item_idx) = paths.iter().position(|p| p == original_path)
4254                    {
4255                        paths.remove(item_idx);
4256                    }
4257                }
4258
4259                if paths.is_empty() {
4260                    return Ok(());
4261                }
4262
4263                let task = worktree.update(cx, |worktree, cx| {
4264                    worktree.copy_external_entries(target_directory, paths, fs, cx)
4265                });
4266
4267                let opened_entries: Vec<_> = task
4268                    .await
4269                    .with_context(|| "failed to copy external paths")?;
4270                this.update(cx, |this, cx| {
4271                    if open_file_after_drop && !opened_entries.is_empty() {
4272                        let settings = ProjectPanelSettings::get_global(cx);
4273                        if settings.auto_open.should_open_on_drop() {
4274                            this.open_entry(opened_entries[0], true, false, cx);
4275                        }
4276                    }
4277                })
4278            }
4279            .log_err()
4280            .await
4281        })
4282        .detach();
4283    }
4284
4285    fn refresh_drag_cursor_style(
4286        &self,
4287        modifiers: &Modifiers,
4288        window: &mut Window,
4289        cx: &mut Context<Self>,
4290    ) {
4291        if let Some(existing_cursor) = cx.active_drag_cursor_style() {
4292            let new_cursor = if Self::is_copy_modifier_set(modifiers) {
4293                CursorStyle::DragCopy
4294            } else {
4295                CursorStyle::PointingHand
4296            };
4297            if existing_cursor != new_cursor {
4298                cx.set_active_drag_cursor_style(new_cursor, window);
4299            }
4300        }
4301    }
4302
4303    fn is_copy_modifier_set(modifiers: &Modifiers) -> bool {
4304        cfg!(target_os = "macos") && modifiers.alt
4305            || cfg!(not(target_os = "macos")) && modifiers.control
4306    }
4307
4308    fn drag_onto(
4309        &mut self,
4310        selections: &DraggedSelection,
4311        target_entry_id: ProjectEntryId,
4312        is_file: bool,
4313        window: &mut Window,
4314        cx: &mut Context<Self>,
4315    ) {
4316        let resolved_selections = selections
4317            .items()
4318            .map(|entry| SelectedEntry {
4319                entry_id: self.resolve_entry(entry.entry_id),
4320                worktree_id: entry.worktree_id,
4321            })
4322            .collect::<BTreeSet<SelectedEntry>>();
4323        let entries = self.disjoint_entries(resolved_selections, cx);
4324
4325        if Self::is_copy_modifier_set(&window.modifiers()) {
4326            let _ = maybe!({
4327                let project = self.project.read(cx);
4328                let target_worktree = project.worktree_for_entry(target_entry_id, cx)?;
4329                let worktree_id = target_worktree.read(cx).id();
4330                let target_entry = target_worktree
4331                    .read(cx)
4332                    .entry_for_id(target_entry_id)?
4333                    .clone();
4334
4335                let mut copy_tasks = Vec::new();
4336                let mut disambiguation_range = None;
4337                for selection in &entries {
4338                    let (new_path, new_disambiguation_range) = self.create_paste_path(
4339                        selection,
4340                        (target_worktree.clone(), &target_entry),
4341                        cx,
4342                    )?;
4343
4344                    let task = self.project.update(cx, |project, cx| {
4345                        project.copy_entry(selection.entry_id, (worktree_id, new_path).into(), cx)
4346                    });
4347                    copy_tasks.push(task);
4348                    disambiguation_range = new_disambiguation_range.or(disambiguation_range);
4349                }
4350
4351                let item_count = copy_tasks.len();
4352
4353                cx.spawn_in(window, async move |project_panel, cx| {
4354                    let mut last_succeed = None;
4355                    for task in copy_tasks.into_iter() {
4356                        if let Some(Some(entry)) = task.await.log_err() {
4357                            last_succeed = Some(entry.id);
4358                        }
4359                    }
4360                    // update selection
4361                    if let Some(entry_id) = last_succeed {
4362                        project_panel
4363                            .update_in(cx, |project_panel, window, cx| {
4364                                project_panel.selection = Some(SelectedEntry {
4365                                    worktree_id,
4366                                    entry_id,
4367                                });
4368
4369                                // if only one entry was dragged and it was disambiguated, open the rename editor
4370                                if item_count == 1 && disambiguation_range.is_some() {
4371                                    project_panel.rename_impl(disambiguation_range, window, cx);
4372                                }
4373                            })
4374                            .ok();
4375                    }
4376                })
4377                .detach();
4378                Some(())
4379            });
4380        } else {
4381            let update_marks = !self.marked_entries.is_empty();
4382            let active_selection = selections.active_selection;
4383
4384            // For folded selections, track the leaf suffix relative to the resolved
4385            // entry so we can refresh it after the move completes.
4386            let (folded_selection_info, folded_selection_entries): (
4387                Vec<(ProjectEntryId, RelPathBuf)>,
4388                HashSet<SelectedEntry>,
4389            ) = {
4390                let project = self.project.read(cx);
4391                let mut info = Vec::new();
4392                let mut folded_entries = HashSet::default();
4393
4394                for selection in selections.items() {
4395                    let resolved_id = self.resolve_entry(selection.entry_id);
4396                    if resolved_id == selection.entry_id {
4397                        continue;
4398                    }
4399                    folded_entries.insert(*selection);
4400                    let Some(source_path) = project.path_for_entry(resolved_id, cx) else {
4401                        continue;
4402                    };
4403                    let Some(leaf_path) = project.path_for_entry(selection.entry_id, cx) else {
4404                        continue;
4405                    };
4406                    let Ok(suffix) = leaf_path.path.strip_prefix(source_path.path.as_ref()) else {
4407                        continue;
4408                    };
4409                    if suffix.as_unix_str().is_empty() {
4410                        continue;
4411                    }
4412
4413                    info.push((resolved_id, suffix.to_rel_path_buf()));
4414                }
4415                (info, folded_entries)
4416            };
4417
4418            // Collect move tasks paired with their source entry ID so we can correlate
4419            // results with folded selections that need refreshing.
4420            let mut move_tasks: Vec<(ProjectEntryId, Task<Result<CreatedEntry>>)> = Vec::new();
4421            for entry in entries {
4422                if let Some(task) = self.move_entry(entry.entry_id, target_entry_id, is_file, cx) {
4423                    move_tasks.push((entry.entry_id, task));
4424                }
4425            }
4426
4427            if move_tasks.is_empty() {
4428                return;
4429            }
4430
4431            let workspace = self.workspace.clone();
4432            if folded_selection_info.is_empty() {
4433                for (_, task) in move_tasks {
4434                    let workspace = workspace.clone();
4435                    cx.spawn_in(window, async move |_, mut cx| {
4436                        task.await.notify_workspace_async_err(workspace, &mut cx);
4437                    })
4438                    .detach();
4439                }
4440            } else {
4441                cx.spawn_in(window, async move |project_panel, mut cx| {
4442                    // Await all move tasks and collect successful results
4443                    let mut move_results: Vec<(ProjectEntryId, Entry)> = Vec::new();
4444                    for (entry_id, task) in move_tasks {
4445                        if let Some(CreatedEntry::Included(new_entry)) = task
4446                            .await
4447                            .notify_workspace_async_err(workspace.clone(), &mut cx)
4448                        {
4449                            move_results.push((entry_id, new_entry));
4450                        }
4451                    }
4452
4453                    if move_results.is_empty() {
4454                        return;
4455                    }
4456
4457                    // For folded selections, we need to refresh the leaf paths (with suffixes)
4458                    // because they may not be indexed yet after the parent directory was moved.
4459                    // First collect the paths to refresh, then refresh them.
4460                    let paths_to_refresh: Vec<(Entity<Worktree>, Arc<RelPath>)> = project_panel
4461                        .update(cx, |project_panel, cx| {
4462                            let project = project_panel.project.read(cx);
4463                            folded_selection_info
4464                                .iter()
4465                                .filter_map(|(resolved_id, suffix)| {
4466                                    let (_, new_entry) =
4467                                        move_results.iter().find(|(id, _)| id == resolved_id)?;
4468                                    let worktree = project.worktree_for_entry(new_entry.id, cx)?;
4469                                    let leaf_path = new_entry.path.join(suffix);
4470                                    Some((worktree, leaf_path))
4471                                })
4472                                .collect()
4473                        })
4474                        .ok()
4475                        .unwrap_or_default();
4476
4477                    let refresh_tasks: Vec<_> = paths_to_refresh
4478                        .into_iter()
4479                        .filter_map(|(worktree, leaf_path)| {
4480                            worktree.update(cx, |worktree, cx| {
4481                                worktree
4482                                    .as_local_mut()
4483                                    .map(|local| local.refresh_entry(leaf_path, None, cx))
4484                            })
4485                        })
4486                        .collect();
4487
4488                    for task in refresh_tasks {
4489                        task.await.log_err();
4490                    }
4491
4492                    if update_marks && !folded_selection_entries.is_empty() {
4493                        project_panel
4494                            .update(cx, |project_panel, cx| {
4495                                project_panel.marked_entries.retain(|entry| {
4496                                    !folded_selection_entries.contains(entry)
4497                                        || *entry == active_selection
4498                                });
4499                                cx.notify();
4500                            })
4501                            .ok();
4502                    }
4503                })
4504                .detach();
4505            }
4506        }
4507    }
4508
4509    fn index_for_entry(
4510        &self,
4511        entry_id: ProjectEntryId,
4512        worktree_id: WorktreeId,
4513    ) -> Option<(usize, usize, usize)> {
4514        let mut total_ix = 0;
4515        for (worktree_ix, visible) in self.state.visible_entries.iter().enumerate() {
4516            if worktree_id != visible.worktree_id {
4517                total_ix += visible.entries.len();
4518                continue;
4519            }
4520
4521            return visible
4522                .entries
4523                .iter()
4524                .enumerate()
4525                .find(|(_, entry)| entry.id == entry_id)
4526                .map(|(ix, _)| (worktree_ix, ix, total_ix + ix));
4527        }
4528        None
4529    }
4530
4531    fn entry_at_index(&self, index: usize) -> Option<(WorktreeId, GitEntryRef<'_>)> {
4532        let mut offset = 0;
4533        for worktree in &self.state.visible_entries {
4534            let current_len = worktree.entries.len();
4535            if index < offset + current_len {
4536                return worktree
4537                    .entries
4538                    .get(index - offset)
4539                    .map(|entry| (worktree.worktree_id, entry.to_ref()));
4540            }
4541            offset += current_len;
4542        }
4543        None
4544    }
4545
4546    fn iter_visible_entries(
4547        &self,
4548        range: Range<usize>,
4549        window: &mut Window,
4550        cx: &mut Context<ProjectPanel>,
4551        callback: &mut dyn FnMut(
4552            &Entry,
4553            usize,
4554            &HashSet<Arc<RelPath>>,
4555            &mut Window,
4556            &mut Context<ProjectPanel>,
4557        ),
4558    ) {
4559        let mut ix = 0;
4560        for visible in &self.state.visible_entries {
4561            if ix >= range.end {
4562                return;
4563            }
4564
4565            if ix + visible.entries.len() <= range.start {
4566                ix += visible.entries.len();
4567                continue;
4568            }
4569
4570            let end_ix = range.end.min(ix + visible.entries.len());
4571            let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
4572            let entries = visible
4573                .index
4574                .get_or_init(|| visible.entries.iter().map(|e| e.path.clone()).collect());
4575            let base_index = ix + entry_range.start;
4576            for (i, entry) in visible.entries[entry_range].iter().enumerate() {
4577                let global_index = base_index + i;
4578                callback(entry, global_index, entries, window, cx);
4579            }
4580            ix = end_ix;
4581        }
4582    }
4583
4584    fn for_each_visible_entry(
4585        &self,
4586        range: Range<usize>,
4587        window: &mut Window,
4588        cx: &mut Context<ProjectPanel>,
4589        callback: &mut dyn FnMut(
4590            ProjectEntryId,
4591            EntryDetails,
4592            &mut Window,
4593            &mut Context<ProjectPanel>,
4594        ),
4595    ) {
4596        let mut ix = 0;
4597        for visible in &self.state.visible_entries {
4598            if ix >= range.end {
4599                return;
4600            }
4601
4602            if ix + visible.entries.len() <= range.start {
4603                ix += visible.entries.len();
4604                continue;
4605            }
4606
4607            let end_ix = range.end.min(ix + visible.entries.len());
4608            let git_status_setting = {
4609                let settings = ProjectPanelSettings::get_global(cx);
4610                settings.git_status
4611            };
4612            if let Some(worktree) = self
4613                .project
4614                .read(cx)
4615                .worktree_for_id(visible.worktree_id, cx)
4616            {
4617                let snapshot = worktree.read(cx).snapshot();
4618                let root_name = snapshot.root_name();
4619
4620                let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
4621                let entries = visible
4622                    .index
4623                    .get_or_init(|| visible.entries.iter().map(|e| e.path.clone()).collect());
4624                for entry in visible.entries[entry_range].iter() {
4625                    let status = git_status_setting
4626                        .then_some(entry.git_summary)
4627                        .unwrap_or_default();
4628
4629                    let mut details = self.details_for_entry(
4630                        entry,
4631                        visible.worktree_id,
4632                        root_name,
4633                        entries,
4634                        status,
4635                        None,
4636                        window,
4637                        cx,
4638                    );
4639
4640                    if let Some(edit_state) = &self.state.edit_state {
4641                        let is_edited_entry = if edit_state.is_new_entry() {
4642                            entry.id == NEW_ENTRY_ID
4643                        } else {
4644                            entry.id == edit_state.entry_id
4645                                || self.state.ancestors.get(&entry.id).is_some_and(
4646                                    |auto_folded_dirs| {
4647                                        auto_folded_dirs.ancestors.contains(&edit_state.entry_id)
4648                                    },
4649                                )
4650                        };
4651
4652                        if is_edited_entry {
4653                            if let Some(processing_filename) = &edit_state.processing_filename {
4654                                details.is_processing = true;
4655                                if let Some(ancestors) = edit_state
4656                                    .leaf_entry_id
4657                                    .and_then(|entry| self.state.ancestors.get(&entry))
4658                                {
4659                                    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;
4660                                    let all_components = ancestors.ancestors.len();
4661
4662                                    let prefix_components = all_components - position;
4663                                    let suffix_components = position.checked_sub(1);
4664                                    let mut previous_components =
4665                                        Path::new(&details.filename).components();
4666                                    let mut new_path = previous_components
4667                                        .by_ref()
4668                                        .take(prefix_components)
4669                                        .collect::<PathBuf>();
4670                                    if let Some(last_component) =
4671                                        processing_filename.components().next_back()
4672                                    {
4673                                        new_path.push(last_component);
4674                                        previous_components.next();
4675                                    }
4676
4677                                    if suffix_components.is_some() {
4678                                        new_path.push(previous_components);
4679                                    }
4680                                    if let Some(str) = new_path.to_str() {
4681                                        details.filename.clear();
4682                                        details.filename.push_str(str);
4683                                    }
4684                                } else {
4685                                    details.filename.clear();
4686                                    details.filename.push_str(processing_filename.as_unix_str());
4687                                }
4688                            } else {
4689                                if edit_state.is_new_entry() {
4690                                    details.filename.clear();
4691                                }
4692                                details.is_editing = true;
4693                            }
4694                        }
4695                    }
4696
4697                    callback(entry.id, details, window, cx);
4698                }
4699            }
4700            ix = end_ix;
4701        }
4702    }
4703
4704    fn find_entry_in_worktree(
4705        &self,
4706        worktree_id: WorktreeId,
4707        reverse_search: bool,
4708        only_visible_entries: bool,
4709        predicate: &dyn Fn(GitEntryRef, WorktreeId) -> bool,
4710        cx: &mut Context<Self>,
4711    ) -> Option<GitEntry> {
4712        if only_visible_entries {
4713            let entries = self
4714                .state
4715                .visible_entries
4716                .iter()
4717                .find_map(|visible| {
4718                    if worktree_id == visible.worktree_id {
4719                        Some(&visible.entries)
4720                    } else {
4721                        None
4722                    }
4723                })?
4724                .clone();
4725
4726            return utils::ReversibleIterable::new(entries.iter(), reverse_search)
4727                .find(|ele| predicate(ele.to_ref(), worktree_id))
4728                .cloned();
4729        }
4730
4731        let repo_snapshots = self
4732            .project
4733            .read(cx)
4734            .git_store()
4735            .read(cx)
4736            .repo_snapshots(cx);
4737        let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?;
4738        worktree.read_with(cx, |tree, _| {
4739            utils::ReversibleIterable::new(
4740                GitTraversal::new(&repo_snapshots, tree.entries(true, 0usize)),
4741                reverse_search,
4742            )
4743            .find_single_ended(|ele| predicate(*ele, worktree_id))
4744            .map(|ele| ele.to_owned())
4745        })
4746    }
4747
4748    fn find_entry(
4749        &self,
4750        start: Option<&SelectedEntry>,
4751        reverse_search: bool,
4752        predicate: &dyn Fn(GitEntryRef, WorktreeId) -> bool,
4753        cx: &mut Context<Self>,
4754    ) -> Option<SelectedEntry> {
4755        let mut worktree_ids: Vec<_> = self
4756            .state
4757            .visible_entries
4758            .iter()
4759            .map(|worktree| worktree.worktree_id)
4760            .collect();
4761        let repo_snapshots = self
4762            .project
4763            .read(cx)
4764            .git_store()
4765            .read(cx)
4766            .repo_snapshots(cx);
4767
4768        let mut last_found: Option<SelectedEntry> = None;
4769
4770        if let Some(start) = start {
4771            let worktree = self
4772                .project
4773                .read(cx)
4774                .worktree_for_id(start.worktree_id, cx)?
4775                .read(cx);
4776
4777            let search = {
4778                let entry = worktree.entry_for_id(start.entry_id)?;
4779                let root_entry = worktree.root_entry()?;
4780                let tree_id = worktree.id();
4781
4782                let mut first_iter = GitTraversal::new(
4783                    &repo_snapshots,
4784                    worktree.traverse_from_path(true, true, true, entry.path.as_ref()),
4785                );
4786
4787                if reverse_search {
4788                    first_iter.next();
4789                }
4790
4791                let first = first_iter
4792                    .enumerate()
4793                    .take_until(|(count, entry)| entry.entry == root_entry && *count != 0usize)
4794                    .map(|(_, entry)| entry)
4795                    .find(|ele| predicate(*ele, tree_id))
4796                    .map(|ele| ele.to_owned());
4797
4798                let second_iter =
4799                    GitTraversal::new(&repo_snapshots, worktree.entries(true, 0usize));
4800
4801                let second = if reverse_search {
4802                    second_iter
4803                        .take_until(|ele| ele.id == start.entry_id)
4804                        .filter(|ele| predicate(*ele, tree_id))
4805                        .last()
4806                        .map(|ele| ele.to_owned())
4807                } else {
4808                    second_iter
4809                        .take_while(|ele| ele.id != start.entry_id)
4810                        .filter(|ele| predicate(*ele, tree_id))
4811                        .last()
4812                        .map(|ele| ele.to_owned())
4813                };
4814
4815                if reverse_search {
4816                    Some((second, first))
4817                } else {
4818                    Some((first, second))
4819                }
4820            };
4821
4822            if let Some((first, second)) = search {
4823                let first = first.map(|entry| SelectedEntry {
4824                    worktree_id: start.worktree_id,
4825                    entry_id: entry.id,
4826                });
4827
4828                let second = second.map(|entry| SelectedEntry {
4829                    worktree_id: start.worktree_id,
4830                    entry_id: entry.id,
4831                });
4832
4833                if first.is_some() {
4834                    return first;
4835                }
4836                last_found = second;
4837
4838                let idx = worktree_ids
4839                    .iter()
4840                    .enumerate()
4841                    .find(|(_, ele)| **ele == start.worktree_id)
4842                    .map(|(idx, _)| idx);
4843
4844                if let Some(idx) = idx {
4845                    worktree_ids.rotate_left(idx + 1usize);
4846                    worktree_ids.pop();
4847                }
4848            }
4849        }
4850
4851        for tree_id in worktree_ids.into_iter() {
4852            if let Some(found) =
4853                self.find_entry_in_worktree(tree_id, reverse_search, false, &predicate, cx)
4854            {
4855                return Some(SelectedEntry {
4856                    worktree_id: tree_id,
4857                    entry_id: found.id,
4858                });
4859            }
4860        }
4861
4862        last_found
4863    }
4864
4865    fn find_visible_entry(
4866        &self,
4867        start: Option<&SelectedEntry>,
4868        reverse_search: bool,
4869        predicate: &dyn Fn(GitEntryRef, WorktreeId) -> bool,
4870        cx: &mut Context<Self>,
4871    ) -> Option<SelectedEntry> {
4872        let mut worktree_ids: Vec<_> = self
4873            .state
4874            .visible_entries
4875            .iter()
4876            .map(|worktree| worktree.worktree_id)
4877            .collect();
4878
4879        let mut last_found: Option<SelectedEntry> = None;
4880
4881        if let Some(start) = start {
4882            let entries = self
4883                .state
4884                .visible_entries
4885                .iter()
4886                .find(|worktree| worktree.worktree_id == start.worktree_id)
4887                .map(|worktree| &worktree.entries)?;
4888
4889            let mut start_idx = entries
4890                .iter()
4891                .enumerate()
4892                .find(|(_, ele)| ele.id == start.entry_id)
4893                .map(|(idx, _)| idx)?;
4894
4895            if reverse_search {
4896                start_idx = start_idx.saturating_add(1usize);
4897            }
4898
4899            let (left, right) = entries.split_at_checked(start_idx)?;
4900
4901            let (first_iter, second_iter) = if reverse_search {
4902                (
4903                    utils::ReversibleIterable::new(left.iter(), reverse_search),
4904                    utils::ReversibleIterable::new(right.iter(), reverse_search),
4905                )
4906            } else {
4907                (
4908                    utils::ReversibleIterable::new(right.iter(), reverse_search),
4909                    utils::ReversibleIterable::new(left.iter(), reverse_search),
4910                )
4911            };
4912
4913            let first_search = first_iter.find(|ele| predicate(ele.to_ref(), start.worktree_id));
4914            let second_search = second_iter.find(|ele| predicate(ele.to_ref(), start.worktree_id));
4915
4916            if first_search.is_some() {
4917                return first_search.map(|entry| SelectedEntry {
4918                    worktree_id: start.worktree_id,
4919                    entry_id: entry.id,
4920                });
4921            }
4922
4923            last_found = second_search.map(|entry| SelectedEntry {
4924                worktree_id: start.worktree_id,
4925                entry_id: entry.id,
4926            });
4927
4928            let idx = worktree_ids
4929                .iter()
4930                .enumerate()
4931                .find(|(_, ele)| **ele == start.worktree_id)
4932                .map(|(idx, _)| idx);
4933
4934            if let Some(idx) = idx {
4935                worktree_ids.rotate_left(idx + 1usize);
4936                worktree_ids.pop();
4937            }
4938        }
4939
4940        for tree_id in worktree_ids.into_iter() {
4941            if let Some(found) =
4942                self.find_entry_in_worktree(tree_id, reverse_search, true, &predicate, cx)
4943            {
4944                return Some(SelectedEntry {
4945                    worktree_id: tree_id,
4946                    entry_id: found.id,
4947                });
4948            }
4949        }
4950
4951        last_found
4952    }
4953
4954    fn calculate_depth_and_difference(
4955        entry: &Entry,
4956        visible_worktree_entries: &HashSet<Arc<RelPath>>,
4957    ) -> (usize, usize) {
4958        let (depth, difference) = entry
4959            .path
4960            .ancestors()
4961            .skip(1) // Skip the entry itself
4962            .find_map(|ancestor| {
4963                if let Some(parent_entry) = visible_worktree_entries.get(ancestor) {
4964                    let entry_path_components_count = entry.path.components().count();
4965                    let parent_path_components_count = parent_entry.components().count();
4966                    let difference = entry_path_components_count - parent_path_components_count;
4967                    let depth = parent_entry
4968                        .ancestors()
4969                        .skip(1)
4970                        .filter(|ancestor| visible_worktree_entries.contains(*ancestor))
4971                        .count();
4972                    Some((depth + 1, difference))
4973                } else {
4974                    None
4975                }
4976            })
4977            .unwrap_or_else(|| (0, entry.path.components().count()));
4978
4979        (depth, difference)
4980    }
4981
4982    fn highlight_entry_for_external_drag(
4983        &self,
4984        target_entry: &Entry,
4985        target_worktree: &Worktree,
4986    ) -> Option<ProjectEntryId> {
4987        // Always highlight directory or parent directory if it's file
4988        if target_entry.is_dir() {
4989            Some(target_entry.id)
4990        } else {
4991            target_entry
4992                .path
4993                .parent()
4994                .and_then(|parent_path| target_worktree.entry_for_path(parent_path))
4995                .map(|parent_entry| parent_entry.id)
4996        }
4997    }
4998
4999    fn highlight_entry_for_selection_drag(
5000        &self,
5001        target_entry: &Entry,
5002        target_worktree: &Worktree,
5003        drag_state: &DraggedSelection,
5004        cx: &Context<Self>,
5005    ) -> Option<ProjectEntryId> {
5006        let target_parent_path = target_entry.path.parent();
5007
5008        // In case of single item drag, we do not highlight existing
5009        // directory which item belongs too
5010        if drag_state.items().count() == 1
5011            && drag_state.active_selection.worktree_id == target_worktree.id()
5012        {
5013            let active_entry_path = self
5014                .project
5015                .read(cx)
5016                .path_for_entry(drag_state.active_selection.entry_id, cx)?;
5017
5018            if let Some(active_parent_path) = active_entry_path.path.parent() {
5019                // Do not highlight active entry parent
5020                if active_parent_path == target_entry.path.as_ref() {
5021                    return None;
5022                }
5023
5024                // Do not highlight active entry sibling files
5025                if Some(active_parent_path) == target_parent_path && target_entry.is_file() {
5026                    return None;
5027                }
5028            }
5029        }
5030
5031        // Always highlight directory or parent directory if it's file
5032        if target_entry.is_dir() {
5033            Some(target_entry.id)
5034        } else {
5035            target_parent_path
5036                .and_then(|parent_path| target_worktree.entry_for_path(parent_path))
5037                .map(|parent_entry| parent_entry.id)
5038        }
5039    }
5040
5041    fn should_highlight_background_for_selection_drag(
5042        &self,
5043        drag_state: &DraggedSelection,
5044        last_root_id: ProjectEntryId,
5045        cx: &App,
5046    ) -> bool {
5047        // Always highlight for multiple entries
5048        if drag_state.items().count() > 1 {
5049            return true;
5050        }
5051
5052        // Since root will always have empty relative path
5053        if let Some(entry_path) = self
5054            .project
5055            .read(cx)
5056            .path_for_entry(drag_state.active_selection.entry_id, cx)
5057        {
5058            if let Some(parent_path) = entry_path.path.parent() {
5059                if !parent_path.is_empty() {
5060                    return true;
5061                }
5062            }
5063        }
5064
5065        // If parent is empty, check if different worktree
5066        if let Some(last_root_worktree_id) = self
5067            .project
5068            .read(cx)
5069            .worktree_id_for_entry(last_root_id, cx)
5070        {
5071            if drag_state.active_selection.worktree_id != last_root_worktree_id {
5072                return true;
5073            }
5074        }
5075
5076        false
5077    }
5078
5079    fn render_entry(
5080        &self,
5081        entry_id: ProjectEntryId,
5082        details: EntryDetails,
5083        window: &mut Window,
5084        cx: &mut Context<Self>,
5085    ) -> Stateful<Div> {
5086        const GROUP_NAME: &str = "project_entry";
5087
5088        let kind = details.kind;
5089        let is_sticky = details.sticky.is_some();
5090        let sticky_index = details.sticky.as_ref().map(|this| this.sticky_index);
5091        let settings = ProjectPanelSettings::get_global(cx);
5092        let show_editor = details.is_editing && !details.is_processing;
5093
5094        let selection = SelectedEntry {
5095            worktree_id: details.worktree_id,
5096            entry_id,
5097        };
5098
5099        let is_marked = self.marked_entries.contains(&selection);
5100        let is_active = self
5101            .selection
5102            .is_some_and(|selection| selection.entry_id == entry_id);
5103
5104        let file_name = details.filename.clone();
5105
5106        let mut icon = details.icon.clone();
5107        if settings.file_icons && show_editor && details.kind.is_file() {
5108            let filename = self.filename_editor.read(cx).text(cx);
5109            if filename.len() > 2 {
5110                icon = FileIcons::get_icon(Path::new(&filename), cx);
5111            }
5112        }
5113
5114        let filename_text_color = details.filename_text_color;
5115        let diagnostic_severity = details.diagnostic_severity;
5116        let diagnostic_count = details.diagnostic_count;
5117        let item_colors = get_item_color(is_sticky, cx);
5118
5119        let canonical_path = details
5120            .canonical_path
5121            .as_ref()
5122            .map(|f| f.to_string_lossy().into_owned());
5123        let path_style = self.project.read(cx).path_style(cx);
5124        let path = details.path.clone();
5125        let path_for_external_paths = path.clone();
5126        let path_for_dragged_selection = path.clone();
5127
5128        let depth = details.depth;
5129        let worktree_id = details.worktree_id;
5130        let dragged_selection = DraggedSelection {
5131            active_selection: SelectedEntry {
5132                worktree_id: selection.worktree_id,
5133                entry_id: selection.entry_id,
5134            },
5135            marked_selections: Arc::from(self.marked_entries.clone()),
5136        };
5137
5138        let bg_color = if is_marked {
5139            item_colors.marked
5140        } else {
5141            item_colors.default
5142        };
5143
5144        let bg_hover_color = if is_marked {
5145            item_colors.marked
5146        } else {
5147            item_colors.hover
5148        };
5149
5150        let validation_color_and_message = if show_editor {
5151            match self
5152                .state
5153                .edit_state
5154                .as_ref()
5155                .map_or(ValidationState::None, |e| e.validation_state.clone())
5156            {
5157                ValidationState::Error(msg) => Some((Color::Error.color(cx), msg)),
5158                ValidationState::Warning(msg) => Some((Color::Warning.color(cx), msg)),
5159                ValidationState::None => None,
5160            }
5161        } else {
5162            None
5163        };
5164
5165        let border_color =
5166            if !self.mouse_down && is_active && self.focus_handle.contains_focused(window, cx) {
5167                match validation_color_and_message {
5168                    Some((color, _)) => color,
5169                    None => item_colors.focused,
5170                }
5171            } else {
5172                bg_color
5173            };
5174
5175        let border_hover_color =
5176            if !self.mouse_down && is_active && self.focus_handle.contains_focused(window, cx) {
5177                match validation_color_and_message {
5178                    Some((color, _)) => color,
5179                    None => item_colors.focused,
5180                }
5181            } else {
5182                bg_hover_color
5183            };
5184
5185        let folded_directory_drag_target = self.folded_directory_drag_target;
5186        let is_highlighted = {
5187            if let Some(highlight_entry_id) =
5188                self.drag_target_entry
5189                    .as_ref()
5190                    .and_then(|drag_target| match drag_target {
5191                        DragTarget::Entry {
5192                            highlight_entry_id, ..
5193                        } => Some(*highlight_entry_id),
5194                        DragTarget::Background => self.state.last_worktree_root_id,
5195                    })
5196            {
5197                // Highlight if same entry or it's children
5198                if entry_id == highlight_entry_id {
5199                    true
5200                } else {
5201                    maybe!({
5202                        let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?;
5203                        let highlight_entry = worktree.read(cx).entry_for_id(highlight_entry_id)?;
5204                        Some(path.starts_with(&highlight_entry.path))
5205                    })
5206                    .unwrap_or(false)
5207                }
5208            } else {
5209                false
5210            }
5211        };
5212
5213        let id: ElementId = if is_sticky {
5214            SharedString::from(format!("project_panel_sticky_item_{}", entry_id.to_usize())).into()
5215        } else {
5216            (entry_id.to_proto() as usize).into()
5217        };
5218
5219        div()
5220            .id(id.clone())
5221            .relative()
5222            .group(GROUP_NAME)
5223            .cursor_pointer()
5224            .rounded_none()
5225            .bg(bg_color)
5226            .border_1()
5227            .border_r_2()
5228            .border_color(border_color)
5229            .hover(|style| style.bg(bg_hover_color).border_color(border_hover_color))
5230            .when(is_sticky, |this| this.block_mouse_except_scroll())
5231            .when(!is_sticky, |this| {
5232                this.when(
5233                    is_highlighted && folded_directory_drag_target.is_none(),
5234                    |this| {
5235                        this.border_color(transparent_white())
5236                            .bg(item_colors.drag_over)
5237                    },
5238                )
5239                .when(settings.drag_and_drop, |this| {
5240                    this.on_drag_move::<ExternalPaths>(cx.listener(
5241                        move |this, event: &DragMoveEvent<ExternalPaths>, _, cx| {
5242                            let is_current_target =
5243                                this.drag_target_entry
5244                                    .as_ref()
5245                                    .and_then(|entry| match entry {
5246                                        DragTarget::Entry {
5247                                            entry_id: target_id,
5248                                            ..
5249                                        } => Some(*target_id),
5250                                        DragTarget::Background { .. } => None,
5251                                    })
5252                                    == Some(entry_id);
5253
5254                            if !event.bounds.contains(&event.event.position) {
5255                                // Entry responsible for setting drag target is also responsible to
5256                                // clear it up after drag is out of bounds
5257                                if is_current_target {
5258                                    this.drag_target_entry = None;
5259                                }
5260                                return;
5261                            }
5262
5263                            if is_current_target {
5264                                return;
5265                            }
5266
5267                            this.marked_entries.clear();
5268
5269                            let Some((entry_id, highlight_entry_id)) = maybe!({
5270                                let target_worktree = this
5271                                    .project
5272                                    .read(cx)
5273                                    .worktree_for_id(selection.worktree_id, cx)?
5274                                    .read(cx);
5275                                let target_entry =
5276                                    target_worktree.entry_for_path(&path_for_external_paths)?;
5277                                let highlight_entry_id = this.highlight_entry_for_external_drag(
5278                                    target_entry,
5279                                    target_worktree,
5280                                )?;
5281                                Some((target_entry.id, highlight_entry_id))
5282                            }) else {
5283                                return;
5284                            };
5285
5286                            this.drag_target_entry = Some(DragTarget::Entry {
5287                                entry_id,
5288                                highlight_entry_id,
5289                            });
5290                        },
5291                    ))
5292                    .on_drop(cx.listener(
5293                        move |this, external_paths: &ExternalPaths, window, cx| {
5294                            this.drag_target_entry = None;
5295                            this.hover_scroll_task.take();
5296                            this.drop_external_files(external_paths.paths(), entry_id, window, cx);
5297                            cx.stop_propagation();
5298                        },
5299                    ))
5300                    .on_drag_move::<DraggedSelection>(cx.listener(
5301                        move |this, event: &DragMoveEvent<DraggedSelection>, window, cx| {
5302                            let is_current_target =
5303                                this.drag_target_entry
5304                                    .as_ref()
5305                                    .and_then(|entry| match entry {
5306                                        DragTarget::Entry {
5307                                            entry_id: target_id,
5308                                            ..
5309                                        } => Some(*target_id),
5310                                        DragTarget::Background { .. } => None,
5311                                    })
5312                                    == Some(entry_id);
5313
5314                            if !event.bounds.contains(&event.event.position) {
5315                                // Entry responsible for setting drag target is also responsible to
5316                                // clear it up after drag is out of bounds
5317                                if is_current_target {
5318                                    this.drag_target_entry = None;
5319                                }
5320                                return;
5321                            }
5322
5323                            if is_current_target {
5324                                return;
5325                            }
5326
5327                            let drag_state = event.drag(cx);
5328
5329                            if drag_state.items().count() == 1 {
5330                                this.marked_entries.clear();
5331                                this.marked_entries.push(drag_state.active_selection);
5332                            }
5333
5334                            let Some((entry_id, highlight_entry_id)) = maybe!({
5335                                let target_worktree = this
5336                                    .project
5337                                    .read(cx)
5338                                    .worktree_for_id(selection.worktree_id, cx)?
5339                                    .read(cx);
5340                                let target_entry =
5341                                    target_worktree.entry_for_path(&path_for_dragged_selection)?;
5342                                let highlight_entry_id = this.highlight_entry_for_selection_drag(
5343                                    target_entry,
5344                                    target_worktree,
5345                                    drag_state,
5346                                    cx,
5347                                )?;
5348                                Some((target_entry.id, highlight_entry_id))
5349                            }) else {
5350                                return;
5351                            };
5352
5353                            this.drag_target_entry = Some(DragTarget::Entry {
5354                                entry_id,
5355                                highlight_entry_id,
5356                            });
5357
5358                            this.hover_expand_task.take();
5359
5360                            if !kind.is_dir()
5361                                || this
5362                                    .state
5363                                    .expanded_dir_ids
5364                                    .get(&details.worktree_id)
5365                                    .is_some_and(|ids| ids.binary_search(&entry_id).is_ok())
5366                            {
5367                                return;
5368                            }
5369
5370                            let bounds = event.bounds;
5371                            this.hover_expand_task =
5372                                Some(cx.spawn_in(window, async move |this, cx| {
5373                                    cx.background_executor()
5374                                        .timer(Duration::from_millis(500))
5375                                        .await;
5376                                    this.update_in(cx, |this, window, cx| {
5377                                        this.hover_expand_task.take();
5378                                        if this.drag_target_entry.as_ref().and_then(|entry| {
5379                                            match entry {
5380                                                DragTarget::Entry {
5381                                                    entry_id: target_id,
5382                                                    ..
5383                                                } => Some(*target_id),
5384                                                DragTarget::Background { .. } => None,
5385                                            }
5386                                        }) == Some(entry_id)
5387                                            && bounds.contains(&window.mouse_position())
5388                                        {
5389                                            this.expand_entry(worktree_id, entry_id, cx);
5390                                            this.update_visible_entries(
5391                                                Some((worktree_id, entry_id)),
5392                                                false,
5393                                                false,
5394                                                window,
5395                                                cx,
5396                                            );
5397                                            cx.notify();
5398                                        }
5399                                    })
5400                                    .ok();
5401                                }));
5402                        },
5403                    ))
5404                    .on_drag(dragged_selection, {
5405                        let active_component =
5406                            self.state.ancestors.get(&entry_id).and_then(|ancestors| {
5407                                ancestors.active_component(&details.filename)
5408                            });
5409                        move |selection, click_offset, _window, cx| {
5410                            let filename = active_component
5411                                .as_ref()
5412                                .unwrap_or_else(|| &details.filename);
5413                            cx.new(|_| DraggedProjectEntryView {
5414                                icon: details.icon.clone(),
5415                                filename: filename.clone(),
5416                                click_offset,
5417                                selection: selection.active_selection,
5418                                selections: selection.marked_selections.clone(),
5419                            })
5420                        }
5421                    })
5422                    .on_drop(cx.listener(
5423                        move |this, selections: &DraggedSelection, window, cx| {
5424                            this.drag_target_entry = None;
5425                            this.hover_scroll_task.take();
5426                            this.hover_expand_task.take();
5427                            if folded_directory_drag_target.is_some() {
5428                                return;
5429                            }
5430                            this.drag_onto(selections, entry_id, kind.is_file(), window, cx);
5431                        },
5432                    ))
5433                })
5434            })
5435            .on_mouse_down(
5436                MouseButton::Left,
5437                cx.listener(move |this, _, _, cx| {
5438                    this.mouse_down = true;
5439                    cx.propagate();
5440                }),
5441            )
5442            .on_click(
5443                cx.listener(move |project_panel, event: &gpui::ClickEvent, window, cx| {
5444                    if event.is_right_click() || event.first_focus() || show_editor {
5445                        return;
5446                    }
5447                    if event.standard_click() {
5448                        project_panel.mouse_down = false;
5449                    }
5450                    cx.stop_propagation();
5451
5452                    if let Some(selection) =
5453                        project_panel.selection.filter(|_| event.modifiers().shift)
5454                    {
5455                        let current_selection = project_panel.index_for_selection(selection);
5456                        let clicked_entry = SelectedEntry {
5457                            entry_id,
5458                            worktree_id,
5459                        };
5460                        let target_selection = project_panel.index_for_selection(clicked_entry);
5461                        if let Some(((_, _, source_index), (_, _, target_index))) =
5462                            current_selection.zip(target_selection)
5463                        {
5464                            let range_start = source_index.min(target_index);
5465                            let range_end = source_index.max(target_index) + 1;
5466                            let mut new_selections = Vec::new();
5467                            project_panel.for_each_visible_entry(
5468                                range_start..range_end,
5469                                window,
5470                                cx,
5471                                &mut |entry_id, details, _, _| {
5472                                    new_selections.push(SelectedEntry {
5473                                        entry_id,
5474                                        worktree_id: details.worktree_id,
5475                                    });
5476                                },
5477                            );
5478
5479                            for selection in &new_selections {
5480                                if !project_panel.marked_entries.contains(selection) {
5481                                    project_panel.marked_entries.push(*selection);
5482                                }
5483                            }
5484
5485                            project_panel.selection = Some(clicked_entry);
5486                            if !project_panel.marked_entries.contains(&clicked_entry) {
5487                                project_panel.marked_entries.push(clicked_entry);
5488                            }
5489                        }
5490                    } else if event.modifiers().secondary() {
5491                        if event.click_count() > 1 {
5492                            project_panel.split_entry(entry_id, false, None, cx);
5493                        } else {
5494                            project_panel.selection = Some(selection);
5495                            if let Some(position) = project_panel
5496                                .marked_entries
5497                                .iter()
5498                                .position(|e| *e == selection)
5499                            {
5500                                project_panel.marked_entries.remove(position);
5501                            } else {
5502                                project_panel.marked_entries.push(selection);
5503                            }
5504                        }
5505                    } else if kind.is_dir() {
5506                        project_panel.marked_entries.clear();
5507                        if is_sticky
5508                            && let Some((_, _, index)) =
5509                                project_panel.index_for_entry(entry_id, worktree_id)
5510                        {
5511                            project_panel
5512                                .scroll_handle
5513                                .scroll_to_item_strict_with_offset(
5514                                    index,
5515                                    ScrollStrategy::Top,
5516                                    sticky_index.unwrap_or(0),
5517                                );
5518                            cx.notify();
5519                            // move down by 1px so that clicked item
5520                            // don't count as sticky anymore
5521                            cx.on_next_frame(window, |_, window, cx| {
5522                                cx.on_next_frame(window, |this, _, cx| {
5523                                    let mut offset = this.scroll_handle.offset();
5524                                    offset.y += px(1.);
5525                                    this.scroll_handle.set_offset(offset);
5526                                    cx.notify();
5527                                });
5528                            });
5529                            return;
5530                        }
5531                        if event.modifiers().alt {
5532                            project_panel.toggle_expand_all(entry_id, window, cx);
5533                        } else {
5534                            project_panel.toggle_expanded(entry_id, window, cx);
5535                        }
5536                    } else {
5537                        let preview_tabs_enabled =
5538                            PreviewTabsSettings::get_global(cx).enable_preview_from_project_panel;
5539                        let click_count = event.click_count();
5540                        let focus_opened_item = click_count > 1;
5541                        let allow_preview = preview_tabs_enabled && click_count == 1;
5542                        project_panel.open_entry(entry_id, focus_opened_item, allow_preview, cx);
5543                    }
5544                }),
5545            )
5546            .child(
5547                ListItem::new(id)
5548                    .indent_level(depth)
5549                    .indent_step_size(px(settings.indent_size))
5550                    .spacing(match settings.entry_spacing {
5551                        ProjectPanelEntrySpacing::Comfortable => ListItemSpacing::Dense,
5552                        ProjectPanelEntrySpacing::Standard => ListItemSpacing::ExtraDense,
5553                    })
5554                    .selectable(false)
5555                    .when(
5556                        canonical_path.is_some() || diagnostic_count.is_some(),
5557                        |this| {
5558                            let symlink_element = canonical_path.map(|path| {
5559                                div()
5560                                    .id("symlink_icon")
5561                                    .tooltip(move |_window, cx| {
5562                                        Tooltip::with_meta(
5563                                            path.to_string(),
5564                                            None,
5565                                            "Symbolic Link",
5566                                            cx,
5567                                        )
5568                                    })
5569                                    .child(
5570                                        Icon::new(IconName::ArrowUpRight)
5571                                            .size(IconSize::Indicator)
5572                                            .color(filename_text_color),
5573                                    )
5574                            });
5575                            this.end_slot::<AnyElement>(
5576                                h_flex()
5577                                    .gap_1()
5578                                    .flex_none()
5579                                    .pr_3()
5580                                    .when_some(diagnostic_count, |this, count| {
5581                                        this.when(count.error_count > 0, |this| {
5582                                            this.child(
5583                                                Label::new(count.capped_error_count())
5584                                                    .size(LabelSize::Small)
5585                                                    .color(Color::Error),
5586                                            )
5587                                        })
5588                                        .when(
5589                                            count.warning_count > 0,
5590                                            |this| {
5591                                                this.child(
5592                                                    Label::new(count.capped_warning_count())
5593                                                        .size(LabelSize::Small)
5594                                                        .color(Color::Warning),
5595                                                )
5596                                            },
5597                                        )
5598                                    })
5599                                    .when_some(symlink_element, |this, el| this.child(el))
5600                                    .into_any_element(),
5601                            )
5602                        },
5603                    )
5604                    .child(if let Some(icon) = &icon {
5605                        if let Some((_, decoration_color)) =
5606                            entry_diagnostic_aware_icon_decoration_and_color(diagnostic_severity)
5607                        {
5608                            let is_warning = diagnostic_severity
5609                                .map(|severity| matches!(severity, DiagnosticSeverity::WARNING))
5610                                .unwrap_or(false);
5611                            div().child(
5612                                DecoratedIcon::new(
5613                                    Icon::from_path(icon.clone()).color(Color::Muted),
5614                                    Some(
5615                                        IconDecoration::new(
5616                                            if kind.is_file() {
5617                                                if is_warning {
5618                                                    IconDecorationKind::Triangle
5619                                                } else {
5620                                                    IconDecorationKind::X
5621                                                }
5622                                            } else {
5623                                                IconDecorationKind::Dot
5624                                            },
5625                                            bg_color,
5626                                            cx,
5627                                        )
5628                                        .group_name(Some(GROUP_NAME.into()))
5629                                        .knockout_hover_color(bg_hover_color)
5630                                        .color(decoration_color.color(cx))
5631                                        .position(Point {
5632                                            x: px(-2.),
5633                                            y: px(-2.),
5634                                        }),
5635                                    ),
5636                                )
5637                                .into_any_element(),
5638                            )
5639                        } else {
5640                            h_flex().child(Icon::from_path(icon.to_string()).color(Color::Muted))
5641                        }
5642                    } else if let Some((icon_name, color)) =
5643                        entry_diagnostic_aware_icon_name_and_color(diagnostic_severity)
5644                    {
5645                        h_flex()
5646                            .size(IconSize::default().rems())
5647                            .child(Icon::new(icon_name).color(color).size(IconSize::Small))
5648                    } else {
5649                        h_flex()
5650                            .size(IconSize::default().rems())
5651                            .invisible()
5652                            .flex_none()
5653                    })
5654                    .child(if show_editor {
5655                        h_flex().h_6().w_full().child(self.filename_editor.clone())
5656                    } else {
5657                        h_flex()
5658                            .h_6()
5659                            .map(|this| match self.state.ancestors.get(&entry_id) {
5660                                Some(folded_ancestors) => {
5661                                    this.children(self.render_folder_elements(
5662                                        folded_ancestors,
5663                                        entry_id,
5664                                        file_name,
5665                                        path_style,
5666                                        is_sticky,
5667                                        kind.is_file(),
5668                                        is_active || is_marked,
5669                                        settings.drag_and_drop,
5670                                        settings.bold_folder_labels,
5671                                        item_colors.drag_over,
5672                                        folded_directory_drag_target,
5673                                        filename_text_color,
5674                                        cx,
5675                                    ))
5676                                }
5677
5678                                None => this.child(
5679                                    Label::new(file_name)
5680                                        .single_line()
5681                                        .color(filename_text_color)
5682                                        .when(
5683                                            settings.bold_folder_labels && kind.is_dir(),
5684                                            |this| this.weight(FontWeight::SEMIBOLD),
5685                                        )
5686                                        .into_any_element(),
5687                                ),
5688                            })
5689                    })
5690                    .on_secondary_mouse_down(cx.listener(
5691                        move |this, event: &MouseDownEvent, window, cx| {
5692                            // Stop propagation to prevent the catch-all context menu for the project
5693                            // panel from being deployed.
5694                            cx.stop_propagation();
5695                            // Some context menu actions apply to all marked entries. If the user
5696                            // right-clicks on an entry that is not marked, they may not realize the
5697                            // action applies to multiple entries. To avoid inadvertent changes, all
5698                            // entries are unmarked.
5699                            if !this.marked_entries.contains(&selection) {
5700                                this.marked_entries.clear();
5701                            }
5702                            this.deploy_context_menu(event.position, entry_id, window, cx);
5703                        },
5704                    ))
5705                    .overflow_x(),
5706            )
5707            .when_some(validation_color_and_message, |this, (color, message)| {
5708                this.relative().child(deferred(
5709                    div()
5710                        .occlude()
5711                        .absolute()
5712                        .top_full()
5713                        .left(px(-1.)) // Used px over rem so that it doesn't change with font size
5714                        .right(px(-0.5))
5715                        .py_1()
5716                        .px_2()
5717                        .border_1()
5718                        .border_color(color)
5719                        .bg(cx.theme().colors().background)
5720                        .child(
5721                            Label::new(message)
5722                                .color(Color::from(color))
5723                                .size(LabelSize::Small),
5724                        ),
5725                ))
5726            })
5727    }
5728
5729    fn render_folder_elements(
5730        &self,
5731        folded_ancestors: &FoldedAncestors,
5732        entry_id: ProjectEntryId,
5733        file_name: String,
5734        path_style: PathStyle,
5735        is_sticky: bool,
5736        is_file: bool,
5737        is_active_or_marked: bool,
5738        drag_and_drop_enabled: bool,
5739        bold_folder_labels: bool,
5740        drag_over_color: Hsla,
5741        folded_directory_drag_target: Option<FoldedDirectoryDragTarget>,
5742        filename_text_color: Color,
5743        cx: &Context<Self>,
5744    ) -> impl Iterator<Item = AnyElement> {
5745        let components = Path::new(&file_name)
5746            .components()
5747            .map(|comp| comp.as_os_str().to_string_lossy().into_owned())
5748            .collect::<Vec<_>>();
5749        let active_index = folded_ancestors.active_index();
5750        let components_len = components.len();
5751        let delimiter = SharedString::new(path_style.primary_separator());
5752
5753        let path_component_elements =
5754            components
5755                .into_iter()
5756                .enumerate()
5757                .map(move |(index, component)| {
5758                    div()
5759                        .id(SharedString::from(format!(
5760                            "project_panel_path_component_{}_{index}",
5761                            entry_id.to_usize()
5762                        )))
5763                        .when(index == 0, |this| this.ml_neg_0p5())
5764                        .px_0p5()
5765                        .rounded_xs()
5766                        .hover(|style| style.bg(cx.theme().colors().element_active))
5767                        .when(!is_sticky, |div| {
5768                            div.when(index != components_len - 1, |div| {
5769                                let target_entry_id = folded_ancestors
5770                                    .ancestors
5771                                    .get(components_len - 1 - index)
5772                                    .cloned();
5773                                div.when(drag_and_drop_enabled, |div| {
5774                                    div.on_drag_move(cx.listener(
5775                                        move |this,
5776                                              event: &DragMoveEvent<DraggedSelection>,
5777                                              _,
5778                                              _| {
5779                                            if event.bounds.contains(&event.event.position) {
5780                                                this.folded_directory_drag_target =
5781                                                    Some(FoldedDirectoryDragTarget {
5782                                                        entry_id,
5783                                                        index,
5784                                                        is_delimiter_target: false,
5785                                                    });
5786                                            } else {
5787                                                let is_current_target = this
5788                                                    .folded_directory_drag_target
5789                                                    .as_ref()
5790                                                    .is_some_and(|target| {
5791                                                        target.entry_id == entry_id
5792                                                            && target.index == index
5793                                                            && !target.is_delimiter_target
5794                                                    });
5795                                                if is_current_target {
5796                                                    this.folded_directory_drag_target = None;
5797                                                }
5798                                            }
5799                                        },
5800                                    ))
5801                                    .on_drop(cx.listener(
5802                                        move |this, selections: &DraggedSelection, window, cx| {
5803                                            this.hover_scroll_task.take();
5804                                            this.drag_target_entry = None;
5805                                            this.folded_directory_drag_target = None;
5806                                            if let Some(target_entry_id) = target_entry_id {
5807                                                this.drag_onto(
5808                                                    selections,
5809                                                    target_entry_id,
5810                                                    is_file,
5811                                                    window,
5812                                                    cx,
5813                                                );
5814                                            }
5815                                        },
5816                                    ))
5817                                    .when(
5818                                        folded_directory_drag_target.is_some_and(|target| {
5819                                            target.entry_id == entry_id && target.index == index
5820                                        }),
5821                                        |this| this.bg(drag_over_color),
5822                                    )
5823                                })
5824                            })
5825                        })
5826                        .on_mouse_down(
5827                            MouseButton::Left,
5828                            cx.listener(move |this, _, _, cx| {
5829                                if let Some(folds) = this.state.ancestors.get_mut(&entry_id) {
5830                                    if folds.set_active_index(index) {
5831                                        cx.notify();
5832                                    }
5833                                }
5834                            }),
5835                        )
5836                        .on_mouse_down(
5837                            MouseButton::Right,
5838                            cx.listener(move |this, _, _, cx| {
5839                                if let Some(folds) = this.state.ancestors.get_mut(&entry_id) {
5840                                    if folds.set_active_index(index) {
5841                                        cx.notify();
5842                                    }
5843                                }
5844                            }),
5845                        )
5846                        .child(
5847                            Label::new(component)
5848                                .single_line()
5849                                .color(filename_text_color)
5850                                .when(bold_folder_labels && !is_file, |this| {
5851                                    this.weight(FontWeight::SEMIBOLD)
5852                                })
5853                                .when(index == active_index && is_active_or_marked, |this| {
5854                                    this.underline()
5855                                }),
5856                        )
5857                        .into_any()
5858                });
5859
5860        let mut separator_index = 0;
5861        itertools::intersperse_with(path_component_elements, move || {
5862            separator_index += 1;
5863            self.render_entry_path_separator(
5864                entry_id,
5865                separator_index,
5866                components_len,
5867                is_sticky,
5868                is_file,
5869                drag_and_drop_enabled,
5870                filename_text_color,
5871                &delimiter,
5872                folded_ancestors,
5873                cx,
5874            )
5875            .into_any()
5876        })
5877    }
5878
5879    fn render_entry_path_separator(
5880        &self,
5881        entry_id: ProjectEntryId,
5882        index: usize,
5883        components_len: usize,
5884        is_sticky: bool,
5885        is_file: bool,
5886        drag_and_drop_enabled: bool,
5887        filename_text_color: Color,
5888        delimiter: &SharedString,
5889        folded_ancestors: &FoldedAncestors,
5890        cx: &Context<Self>,
5891    ) -> Div {
5892        let delimiter_target_index = index - 1;
5893        let target_entry_id = folded_ancestors
5894            .ancestors
5895            .get(components_len - 1 - delimiter_target_index)
5896            .cloned();
5897        div()
5898            .when(!is_sticky, |div| {
5899                div.when(drag_and_drop_enabled, |div| {
5900                    div.on_drop(cx.listener(
5901                        move |this, selections: &DraggedSelection, window, cx| {
5902                            this.hover_scroll_task.take();
5903                            this.drag_target_entry = None;
5904                            this.folded_directory_drag_target = None;
5905                            if let Some(target_entry_id) = target_entry_id {
5906                                this.drag_onto(selections, target_entry_id, is_file, window, cx);
5907                            }
5908                        },
5909                    ))
5910                    .on_drag_move(cx.listener(
5911                        move |this, event: &DragMoveEvent<DraggedSelection>, _, _| {
5912                            if event.bounds.contains(&event.event.position) {
5913                                this.folded_directory_drag_target =
5914                                    Some(FoldedDirectoryDragTarget {
5915                                        entry_id,
5916                                        index: delimiter_target_index,
5917                                        is_delimiter_target: true,
5918                                    });
5919                            } else {
5920                                let is_current_target =
5921                                    this.folded_directory_drag_target.is_some_and(|target| {
5922                                        target.entry_id == entry_id
5923                                            && target.index == delimiter_target_index
5924                                            && target.is_delimiter_target
5925                                    });
5926                                if is_current_target {
5927                                    this.folded_directory_drag_target = None;
5928                                }
5929                            }
5930                        },
5931                    ))
5932                })
5933            })
5934            .child(
5935                Label::new(delimiter.clone())
5936                    .single_line()
5937                    .color(filename_text_color),
5938            )
5939    }
5940
5941    fn details_for_entry(
5942        &self,
5943        entry: &Entry,
5944        worktree_id: WorktreeId,
5945        root_name: &RelPath,
5946        entries_paths: &HashSet<Arc<RelPath>>,
5947        git_status: GitSummary,
5948        sticky: Option<StickyDetails>,
5949        _window: &mut Window,
5950        cx: &mut Context<Self>,
5951    ) -> EntryDetails {
5952        let (show_file_icons, show_folder_icons) = {
5953            let settings = ProjectPanelSettings::get_global(cx);
5954            (settings.file_icons, settings.folder_icons)
5955        };
5956
5957        let expanded_entry_ids = self
5958            .state
5959            .expanded_dir_ids
5960            .get(&worktree_id)
5961            .map(Vec::as_slice)
5962            .unwrap_or(&[]);
5963        let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok();
5964
5965        let icon = match entry.kind {
5966            EntryKind::File => {
5967                if show_file_icons {
5968                    FileIcons::get_icon(entry.path.as_std_path(), cx)
5969                } else {
5970                    None
5971                }
5972            }
5973            _ => {
5974                if show_folder_icons {
5975                    FileIcons::get_folder_icon(is_expanded, entry.path.as_std_path(), cx)
5976                } else {
5977                    FileIcons::get_chevron_icon(is_expanded, cx)
5978                }
5979            }
5980        };
5981
5982        let path_style = self.project.read(cx).path_style(cx);
5983        let (depth, difference) =
5984            ProjectPanel::calculate_depth_and_difference(entry, entries_paths);
5985
5986        let filename = if difference > 1 {
5987            entry
5988                .path
5989                .last_n_components(difference)
5990                .map_or(String::new(), |suffix| {
5991                    suffix.display(path_style).to_string()
5992                })
5993        } else {
5994            entry
5995                .path
5996                .file_name()
5997                .map(|name| name.to_string())
5998                .unwrap_or_else(|| root_name.as_unix_str().to_string())
5999        };
6000
6001        let selection = SelectedEntry {
6002            worktree_id,
6003            entry_id: entry.id,
6004        };
6005        let is_marked = self.marked_entries.contains(&selection);
6006        let is_selected = self.selection == Some(selection);
6007
6008        let diagnostic_severity = self
6009            .diagnostics
6010            .get(&(worktree_id, entry.path.clone()))
6011            .cloned();
6012
6013        let diagnostic_count = self
6014            .diagnostic_counts
6015            .get(&(worktree_id, entry.path.clone()))
6016            .copied();
6017
6018        let filename_text_color =
6019            entry_git_aware_label_color(git_status, entry.is_ignored, is_marked);
6020
6021        let is_cut = self
6022            .clipboard
6023            .as_ref()
6024            .is_some_and(|e| e.is_cut() && e.items().contains(&selection));
6025
6026        EntryDetails {
6027            filename,
6028            icon,
6029            path: entry.path.clone(),
6030            depth,
6031            kind: entry.kind,
6032            is_ignored: entry.is_ignored,
6033            is_expanded,
6034            is_selected,
6035            is_marked,
6036            is_editing: false,
6037            is_processing: false,
6038            is_cut,
6039            sticky,
6040            filename_text_color,
6041            diagnostic_severity,
6042            diagnostic_count,
6043            git_status,
6044            is_private: entry.is_private,
6045            worktree_id,
6046            canonical_path: entry.canonical_path.clone(),
6047        }
6048    }
6049
6050    fn dispatch_context(&self, window: &Window, cx: &Context<Self>) -> KeyContext {
6051        let mut dispatch_context = KeyContext::new_with_defaults();
6052        dispatch_context.add("ProjectPanel");
6053        dispatch_context.add("menu");
6054
6055        let identifier = if self.filename_editor.focus_handle(cx).is_focused(window) {
6056            "editing"
6057        } else {
6058            "not_editing"
6059        };
6060
6061        dispatch_context.add(identifier);
6062        dispatch_context
6063    }
6064
6065    fn reveal_entry(
6066        &mut self,
6067        project: Entity<Project>,
6068        entry_id: ProjectEntryId,
6069        skip_ignored: bool,
6070        window: &mut Window,
6071        cx: &mut Context<Self>,
6072    ) -> Result<()> {
6073        let worktree = project
6074            .read(cx)
6075            .worktree_for_entry(entry_id, cx)
6076            .context("can't reveal a non-existent entry in the project panel")?;
6077        let worktree = worktree.read(cx);
6078        let worktree_id = worktree.id();
6079        let is_ignored = worktree
6080            .entry_for_id(entry_id)
6081            .is_none_or(|entry| entry.is_ignored && !entry.is_always_included);
6082        if skip_ignored && is_ignored {
6083            if self.index_for_entry(entry_id, worktree_id).is_none() {
6084                anyhow::bail!("can't reveal an ignored entry in the project panel");
6085            }
6086
6087            self.selection = Some(SelectedEntry {
6088                worktree_id,
6089                entry_id,
6090            });
6091            self.marked_entries.clear();
6092            self.marked_entries.push(SelectedEntry {
6093                worktree_id,
6094                entry_id,
6095            });
6096            self.autoscroll(cx);
6097            cx.notify();
6098            return Ok(());
6099        }
6100        let is_active_item_file_diff_view = self
6101            .workspace
6102            .upgrade()
6103            .and_then(|ws| ws.read(cx).active_item(cx))
6104            .map(|item| item.act_as_type(TypeId::of::<FileDiffView>(), cx).is_some())
6105            .unwrap_or(false);
6106        if is_active_item_file_diff_view {
6107            return Ok(());
6108        }
6109
6110        self.expand_entry(worktree_id, entry_id, cx);
6111        self.update_visible_entries(Some((worktree_id, entry_id)), false, true, window, cx);
6112        self.marked_entries.clear();
6113        self.marked_entries.push(SelectedEntry {
6114            worktree_id,
6115            entry_id,
6116        });
6117        cx.notify();
6118        Ok(())
6119    }
6120
6121    fn find_active_indent_guide(
6122        &self,
6123        indent_guides: &[IndentGuideLayout],
6124        cx: &App,
6125    ) -> Option<usize> {
6126        let (worktree, entry) = self.selected_entry(cx)?;
6127
6128        // Find the parent entry of the indent guide, this will either be the
6129        // expanded folder we have selected, or the parent of the currently
6130        // selected file/collapsed directory
6131        let mut entry = entry;
6132        loop {
6133            let is_expanded_dir = entry.is_dir()
6134                && self
6135                    .state
6136                    .expanded_dir_ids
6137                    .get(&worktree.id())
6138                    .map(|ids| ids.binary_search(&entry.id).is_ok())
6139                    .unwrap_or(false);
6140            if is_expanded_dir {
6141                break;
6142            }
6143            entry = worktree.entry_for_path(&entry.path.parent()?)?;
6144        }
6145
6146        let (active_indent_range, depth) = {
6147            let (worktree_ix, child_offset, ix) = self.index_for_entry(entry.id, worktree.id())?;
6148            let child_paths = &self.state.visible_entries[worktree_ix].entries;
6149            let mut child_count = 0;
6150            let depth = entry.path.ancestors().count();
6151            while let Some(entry) = child_paths.get(child_offset + child_count + 1) {
6152                if entry.path.ancestors().count() <= depth {
6153                    break;
6154                }
6155                child_count += 1;
6156            }
6157
6158            let start = ix + 1;
6159            let end = start + child_count;
6160
6161            let visible_worktree = &self.state.visible_entries[worktree_ix];
6162            let visible_worktree_entries = visible_worktree.index.get_or_init(|| {
6163                visible_worktree
6164                    .entries
6165                    .iter()
6166                    .map(|e| e.path.clone())
6167                    .collect()
6168            });
6169
6170            // Calculate the actual depth of the entry, taking into account that directories can be auto-folded.
6171            let (depth, _) = Self::calculate_depth_and_difference(entry, visible_worktree_entries);
6172            (start..end, depth)
6173        };
6174
6175        let candidates = indent_guides
6176            .iter()
6177            .enumerate()
6178            .filter(|(_, indent_guide)| indent_guide.offset.x == depth);
6179
6180        for (i, indent) in candidates {
6181            // Find matches that are either an exact match, partially on screen, or inside the enclosing indent
6182            if active_indent_range.start <= indent.offset.y + indent.length
6183                && indent.offset.y <= active_indent_range.end
6184            {
6185                return Some(i);
6186            }
6187        }
6188        None
6189    }
6190
6191    fn render_sticky_entries(
6192        &self,
6193        child: StickyProjectPanelCandidate,
6194        window: &mut Window,
6195        cx: &mut Context<Self>,
6196    ) -> SmallVec<[AnyElement; 8]> {
6197        let project = self.project.read(cx);
6198
6199        let Some((worktree_id, entry_ref)) = self.entry_at_index(child.index) else {
6200            return SmallVec::new();
6201        };
6202
6203        let Some(visible) = self
6204            .state
6205            .visible_entries
6206            .iter()
6207            .find(|worktree| worktree.worktree_id == worktree_id)
6208        else {
6209            return SmallVec::new();
6210        };
6211
6212        let Some(worktree) = project.worktree_for_id(worktree_id, cx) else {
6213            return SmallVec::new();
6214        };
6215        let worktree = worktree.read(cx).snapshot();
6216
6217        let paths = visible
6218            .index
6219            .get_or_init(|| visible.entries.iter().map(|e| e.path.clone()).collect());
6220
6221        let mut sticky_parents = Vec::new();
6222        let mut current_path = entry_ref.path.clone();
6223
6224        'outer: loop {
6225            if let Some(parent_path) = current_path.parent() {
6226                for ancestor_path in parent_path.ancestors() {
6227                    if paths.contains(ancestor_path)
6228                        && let Some(parent_entry) = worktree.entry_for_path(ancestor_path)
6229                    {
6230                        sticky_parents.push(parent_entry.clone());
6231                        current_path = parent_entry.path.clone();
6232                        continue 'outer;
6233                    }
6234                }
6235            }
6236            break 'outer;
6237        }
6238
6239        if sticky_parents.is_empty() {
6240            return SmallVec::new();
6241        }
6242
6243        sticky_parents.reverse();
6244
6245        let panel_settings = ProjectPanelSettings::get_global(cx);
6246        let git_status_enabled = panel_settings.git_status;
6247        let root_name = worktree.root_name();
6248
6249        let git_summaries_by_id = if git_status_enabled {
6250            visible
6251                .entries
6252                .iter()
6253                .map(|e| (e.id, e.git_summary))
6254                .collect::<HashMap<_, _>>()
6255        } else {
6256            Default::default()
6257        };
6258
6259        // already checked if non empty above
6260        let last_item_index = sticky_parents.len() - 1;
6261        sticky_parents
6262            .iter()
6263            .enumerate()
6264            .map(|(index, entry)| {
6265                let git_status = git_summaries_by_id
6266                    .get(&entry.id)
6267                    .copied()
6268                    .unwrap_or_default();
6269                let sticky_details = Some(StickyDetails {
6270                    sticky_index: index,
6271                });
6272                let details = self.details_for_entry(
6273                    entry,
6274                    worktree_id,
6275                    root_name,
6276                    paths,
6277                    git_status,
6278                    sticky_details,
6279                    window,
6280                    cx,
6281                );
6282                self.render_entry(entry.id, details, window, cx)
6283                    .when(index == last_item_index, |this| {
6284                        let shadow_color_top = hsla(0.0, 0.0, 0.0, 0.1);
6285                        let shadow_color_bottom = hsla(0.0, 0.0, 0.0, 0.);
6286                        let sticky_shadow = div()
6287                            .absolute()
6288                            .left_0()
6289                            .bottom_neg_1p5()
6290                            .h_1p5()
6291                            .w_full()
6292                            .bg(linear_gradient(
6293                                0.,
6294                                linear_color_stop(shadow_color_top, 1.),
6295                                linear_color_stop(shadow_color_bottom, 0.),
6296                            ));
6297                        this.child(sticky_shadow)
6298                    })
6299                    .into_any()
6300            })
6301            .collect()
6302    }
6303}
6304
6305#[derive(Clone)]
6306struct StickyProjectPanelCandidate {
6307    index: usize,
6308    depth: usize,
6309}
6310
6311impl StickyCandidate for StickyProjectPanelCandidate {
6312    fn depth(&self) -> usize {
6313        self.depth
6314    }
6315}
6316
6317fn item_width_estimate(depth: usize, item_text_chars: usize, is_symlink: bool) -> usize {
6318    const ICON_SIZE_FACTOR: usize = 2;
6319    let mut item_width = depth * ICON_SIZE_FACTOR + item_text_chars;
6320    if is_symlink {
6321        item_width += ICON_SIZE_FACTOR;
6322    }
6323    item_width
6324}
6325
6326impl Render for ProjectPanel {
6327    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
6328        let has_worktree = !self.state.visible_entries.is_empty();
6329        let project = self.project.read(cx);
6330        let panel_settings = ProjectPanelSettings::get_global(cx);
6331        let indent_size = panel_settings.indent_size;
6332        let show_indent_guides = panel_settings.indent_guides.show == ShowIndentGuides::Always;
6333        let show_sticky_entries = {
6334            if panel_settings.sticky_scroll {
6335                let is_scrollable = self.scroll_handle.is_scrollable();
6336                let is_scrolled = self.scroll_handle.offset().y < px(0.);
6337                is_scrollable && is_scrolled
6338            } else {
6339                false
6340            }
6341        };
6342
6343        let is_local = project.is_local();
6344
6345        if has_worktree {
6346            let item_count = self
6347                .state
6348                .visible_entries
6349                .iter()
6350                .map(|worktree| worktree.entries.len())
6351                .sum();
6352
6353            fn handle_drag_move<T: 'static>(
6354                this: &mut ProjectPanel,
6355                e: &DragMoveEvent<T>,
6356                window: &mut Window,
6357                cx: &mut Context<ProjectPanel>,
6358            ) {
6359                if let Some(previous_position) = this.previous_drag_position {
6360                    // Refresh cursor only when an actual drag happens,
6361                    // because modifiers are not updated when the cursor is not moved.
6362                    if e.event.position != previous_position {
6363                        this.refresh_drag_cursor_style(&e.event.modifiers, window, cx);
6364                    }
6365                }
6366                this.previous_drag_position = Some(e.event.position);
6367
6368                if !e.bounds.contains(&e.event.position) {
6369                    this.drag_target_entry = None;
6370                    return;
6371                }
6372                this.hover_scroll_task.take();
6373                let panel_height = e.bounds.size.height;
6374                if panel_height <= px(0.) {
6375                    return;
6376                }
6377
6378                let event_offset = e.event.position.y - e.bounds.origin.y;
6379                // How far along in the project panel is our cursor? (0. is the top of a list, 1. is the bottom)
6380                let hovered_region_offset = event_offset / panel_height;
6381
6382                // We want the scrolling to be a bit faster when the cursor is closer to the edge of a list.
6383                // These pixels offsets were picked arbitrarily.
6384                let vertical_scroll_offset = if hovered_region_offset <= 0.05 {
6385                    8.
6386                } else if hovered_region_offset <= 0.15 {
6387                    5.
6388                } else if hovered_region_offset >= 0.95 {
6389                    -8.
6390                } else if hovered_region_offset >= 0.85 {
6391                    -5.
6392                } else {
6393                    return;
6394                };
6395                let adjustment = point(px(0.), px(vertical_scroll_offset));
6396                this.hover_scroll_task = Some(cx.spawn_in(window, async move |this, cx| {
6397                    loop {
6398                        let should_stop_scrolling = this
6399                            .update(cx, |this, cx| {
6400                                this.hover_scroll_task.as_ref()?;
6401                                let handle = this.scroll_handle.0.borrow_mut();
6402                                let offset = handle.base_handle.offset();
6403
6404                                handle.base_handle.set_offset(offset + adjustment);
6405                                cx.notify();
6406                                Some(())
6407                            })
6408                            .ok()
6409                            .flatten()
6410                            .is_some();
6411                        if should_stop_scrolling {
6412                            return;
6413                        }
6414                        cx.background_executor()
6415                            .timer(Duration::from_millis(16))
6416                            .await;
6417                    }
6418                }));
6419            }
6420            h_flex()
6421                .id("project-panel")
6422                .group("project-panel")
6423                .when(panel_settings.drag_and_drop, |this| {
6424                    this.on_drag_move(cx.listener(handle_drag_move::<ExternalPaths>))
6425                        .on_drag_move(cx.listener(handle_drag_move::<DraggedSelection>))
6426                })
6427                .size_full()
6428                .relative()
6429                .on_modifiers_changed(cx.listener(
6430                    |this, event: &ModifiersChangedEvent, window, cx| {
6431                        this.refresh_drag_cursor_style(&event.modifiers, window, cx);
6432                    },
6433                ))
6434                .key_context(self.dispatch_context(window, cx))
6435                .on_action(cx.listener(Self::scroll_up))
6436                .on_action(cx.listener(Self::scroll_down))
6437                .on_action(cx.listener(Self::scroll_cursor_center))
6438                .on_action(cx.listener(Self::scroll_cursor_top))
6439                .on_action(cx.listener(Self::scroll_cursor_bottom))
6440                .on_action(cx.listener(Self::select_next))
6441                .on_action(cx.listener(Self::select_previous))
6442                .on_action(cx.listener(Self::select_first))
6443                .on_action(cx.listener(Self::select_last))
6444                .on_action(cx.listener(Self::select_parent))
6445                .on_action(cx.listener(Self::select_next_git_entry))
6446                .on_action(cx.listener(Self::select_prev_git_entry))
6447                .on_action(cx.listener(Self::select_next_diagnostic))
6448                .on_action(cx.listener(Self::select_prev_diagnostic))
6449                .on_action(cx.listener(Self::select_next_directory))
6450                .on_action(cx.listener(Self::select_prev_directory))
6451                .on_action(cx.listener(Self::expand_selected_entry))
6452                .on_action(cx.listener(Self::collapse_selected_entry))
6453                .on_action(cx.listener(Self::collapse_all_entries))
6454                .on_action(cx.listener(Self::collapse_selected_entry_and_children))
6455                .on_action(cx.listener(Self::open))
6456                .on_action(cx.listener(Self::open_permanent))
6457                .on_action(cx.listener(Self::open_split_vertical))
6458                .on_action(cx.listener(Self::open_split_horizontal))
6459                .on_action(cx.listener(Self::confirm))
6460                .on_action(cx.listener(Self::cancel))
6461                .on_action(cx.listener(Self::copy_path))
6462                .on_action(cx.listener(Self::copy_relative_path))
6463                .on_action(cx.listener(Self::new_search_in_directory))
6464                .on_action(cx.listener(Self::unfold_directory))
6465                .on_action(cx.listener(Self::fold_directory))
6466                .on_action(cx.listener(Self::remove_from_project))
6467                .on_action(cx.listener(Self::compare_marked_files))
6468                .when(!project.is_read_only(cx), |el| {
6469                    el.on_action(cx.listener(Self::new_file))
6470                        .on_action(cx.listener(Self::new_directory))
6471                        .on_action(cx.listener(Self::rename))
6472                        .on_action(cx.listener(Self::delete))
6473                        .on_action(cx.listener(Self::cut))
6474                        .on_action(cx.listener(Self::copy))
6475                        .on_action(cx.listener(Self::paste))
6476                        .on_action(cx.listener(Self::duplicate))
6477                        .on_action(cx.listener(Self::restore_file))
6478                        .when(!project.is_remote(), |el| {
6479                            el.on_action(cx.listener(Self::trash))
6480                        })
6481                })
6482                .when(
6483                    project.is_local() || project.is_via_wsl_with_host_interop(cx),
6484                    |el| {
6485                        el.on_action(cx.listener(Self::reveal_in_finder))
6486                            .on_action(cx.listener(Self::open_system))
6487                            .on_action(cx.listener(Self::open_in_terminal))
6488                    },
6489                )
6490                .when(project.is_via_remote_server(), |el| {
6491                    el.on_action(cx.listener(Self::open_in_terminal))
6492                        .on_action(cx.listener(Self::download_from_remote))
6493                })
6494                .track_focus(&self.focus_handle(cx))
6495                .child(
6496                    v_flex()
6497                        .child(
6498                            uniform_list("entries", item_count, {
6499                                cx.processor(|this, range: Range<usize>, window, cx| {
6500                                    this.rendered_entries_len = range.end - range.start;
6501                                    let mut items = Vec::with_capacity(this.rendered_entries_len);
6502                                    this.for_each_visible_entry(
6503                                        range,
6504                                        window,
6505                                        cx,
6506                                        &mut |id, details, window, cx| {
6507                                            items.push(this.render_entry(id, details, window, cx));
6508                                        },
6509                                    );
6510                                    items
6511                                })
6512                            })
6513                            .when(show_indent_guides, |list| {
6514                                list.with_decoration(
6515                                    ui::indent_guides(
6516                                        px(indent_size),
6517                                        IndentGuideColors::panel(cx),
6518                                    )
6519                                    .with_compute_indents_fn(
6520                                        cx.entity(),
6521                                        |this, range, window, cx| {
6522                                            let mut items =
6523                                                SmallVec::with_capacity(range.end - range.start);
6524                                            this.iter_visible_entries(
6525                                                range,
6526                                                window,
6527                                                cx,
6528                                                &mut |entry, _, entries, _, _| {
6529                                                    let (depth, _) =
6530                                                        Self::calculate_depth_and_difference(
6531                                                            entry, entries,
6532                                                        );
6533                                                    items.push(depth);
6534                                                },
6535                                            );
6536                                            items
6537                                        },
6538                                    )
6539                                    .on_click(cx.listener(
6540                                        |this,
6541                                         active_indent_guide: &IndentGuideLayout,
6542                                         window,
6543                                         cx| {
6544                                            if window.modifiers().secondary() {
6545                                                let ix = active_indent_guide.offset.y;
6546                                                let Some((target_entry, worktree)) = maybe!({
6547                                                    let (worktree_id, entry) =
6548                                                        this.entry_at_index(ix)?;
6549                                                    let worktree = this
6550                                                        .project
6551                                                        .read(cx)
6552                                                        .worktree_for_id(worktree_id, cx)?;
6553                                                    let target_entry = worktree
6554                                                        .read(cx)
6555                                                        .entry_for_path(&entry.path.parent()?)?;
6556                                                    Some((target_entry, worktree))
6557                                                }) else {
6558                                                    return;
6559                                                };
6560
6561                                                this.collapse_entry(
6562                                                    target_entry.clone(),
6563                                                    worktree,
6564                                                    window,
6565                                                    cx,
6566                                                );
6567                                            }
6568                                        },
6569                                    ))
6570                                    .with_render_fn(
6571                                        cx.entity(),
6572                                        move |this, params, _, cx| {
6573                                            const LEFT_OFFSET: Pixels = px(14.);
6574                                            const PADDING_Y: Pixels = px(4.);
6575                                            const HITBOX_OVERDRAW: Pixels = px(3.);
6576
6577                                            let active_indent_guide_index = this
6578                                                .find_active_indent_guide(
6579                                                    &params.indent_guides,
6580                                                    cx,
6581                                                );
6582
6583                                            let indent_size = params.indent_size;
6584                                            let item_height = params.item_height;
6585
6586                                            params
6587                                                .indent_guides
6588                                                .into_iter()
6589                                                .enumerate()
6590                                                .map(|(idx, layout)| {
6591                                                    let offset = if layout.continues_offscreen {
6592                                                        px(0.)
6593                                                    } else {
6594                                                        PADDING_Y
6595                                                    };
6596                                                    let bounds = Bounds::new(
6597                                                        point(
6598                                                            layout.offset.x * indent_size
6599                                                                + LEFT_OFFSET,
6600                                                            layout.offset.y * item_height + offset,
6601                                                        ),
6602                                                        size(
6603                                                            px(1.),
6604                                                            layout.length * item_height
6605                                                                - offset * 2.,
6606                                                        ),
6607                                                    );
6608                                                    ui::RenderedIndentGuide {
6609                                                        bounds,
6610                                                        layout,
6611                                                        is_active: Some(idx)
6612                                                            == active_indent_guide_index,
6613                                                        hitbox: Some(Bounds::new(
6614                                                            point(
6615                                                                bounds.origin.x - HITBOX_OVERDRAW,
6616                                                                bounds.origin.y,
6617                                                            ),
6618                                                            size(
6619                                                                bounds.size.width
6620                                                                    + HITBOX_OVERDRAW * 2.,
6621                                                                bounds.size.height,
6622                                                            ),
6623                                                        )),
6624                                                    }
6625                                                })
6626                                                .collect()
6627                                        },
6628                                    ),
6629                                )
6630                            })
6631                            .when(show_sticky_entries, |list| {
6632                                let sticky_items = ui::sticky_items(
6633                                    cx.entity(),
6634                                    |this, range, window, cx| {
6635                                        let mut items =
6636                                            SmallVec::with_capacity(range.end - range.start);
6637                                        this.iter_visible_entries(
6638                                            range,
6639                                            window,
6640                                            cx,
6641                                            &mut |entry, index, entries, _, _| {
6642                                                let (depth, _) =
6643                                                    Self::calculate_depth_and_difference(
6644                                                        entry, entries,
6645                                                    );
6646                                                let candidate =
6647                                                    StickyProjectPanelCandidate { index, depth };
6648                                                items.push(candidate);
6649                                            },
6650                                        );
6651                                        items
6652                                    },
6653                                    |this, marker_entry, window, cx| {
6654                                        let sticky_entries =
6655                                            this.render_sticky_entries(marker_entry, window, cx);
6656                                        this.sticky_items_count = sticky_entries.len();
6657                                        sticky_entries
6658                                    },
6659                                );
6660                                list.with_decoration(if show_indent_guides {
6661                                    sticky_items.with_decoration(
6662                                        ui::indent_guides(
6663                                            px(indent_size),
6664                                            IndentGuideColors::panel(cx),
6665                                        )
6666                                        .with_render_fn(
6667                                            cx.entity(),
6668                                            move |_, params, _, _| {
6669                                                const LEFT_OFFSET: Pixels = px(14.);
6670
6671                                                let indent_size = params.indent_size;
6672                                                let item_height = params.item_height;
6673
6674                                                params
6675                                                    .indent_guides
6676                                                    .into_iter()
6677                                                    .map(|layout| {
6678                                                        let bounds = Bounds::new(
6679                                                            point(
6680                                                                layout.offset.x * indent_size
6681                                                                    + LEFT_OFFSET,
6682                                                                layout.offset.y * item_height,
6683                                                            ),
6684                                                            size(
6685                                                                px(1.),
6686                                                                layout.length * item_height,
6687                                                            ),
6688                                                        );
6689                                                        ui::RenderedIndentGuide {
6690                                                            bounds,
6691                                                            layout,
6692                                                            is_active: false,
6693                                                            hitbox: None,
6694                                                        }
6695                                                    })
6696                                                    .collect()
6697                                            },
6698                                        ),
6699                                    )
6700                                } else {
6701                                    sticky_items
6702                                })
6703                            })
6704                            .with_sizing_behavior(ListSizingBehavior::Infer)
6705                            .with_horizontal_sizing_behavior(
6706                                ListHorizontalSizingBehavior::Unconstrained,
6707                            )
6708                            .with_width_from_item(self.state.max_width_item_index)
6709                            .track_scroll(&self.scroll_handle),
6710                        )
6711                        .child(
6712                            div()
6713                                .id("project-panel-blank-area")
6714                                .block_mouse_except_scroll()
6715                                .flex_grow()
6716                                .on_scroll_wheel({
6717                                    let scroll_handle = self.scroll_handle.clone();
6718                                    let entity_id = cx.entity().entity_id();
6719                                    move |event, window, cx| {
6720                                        let state = scroll_handle.0.borrow();
6721                                        let base_handle = &state.base_handle;
6722                                        let current_offset = base_handle.offset();
6723                                        let max_offset = base_handle.max_offset();
6724                                        let delta = event.delta.pixel_delta(window.line_height());
6725                                        let new_offset = (current_offset + delta)
6726                                            .clamp(&max_offset.neg(), &Point::default());
6727
6728                                        if new_offset != current_offset {
6729                                            base_handle.set_offset(new_offset);
6730                                            cx.notify(entity_id);
6731                                        }
6732                                    }
6733                                })
6734                                .when(
6735                                    self.drag_target_entry.as_ref().is_some_and(
6736                                        |entry| match entry {
6737                                            DragTarget::Background => true,
6738                                            DragTarget::Entry {
6739                                                highlight_entry_id, ..
6740                                            } => self.state.last_worktree_root_id.is_some_and(
6741                                                |root_id| *highlight_entry_id == root_id,
6742                                            ),
6743                                        },
6744                                    ),
6745                                    |div| div.bg(cx.theme().colors().drop_target_background),
6746                                )
6747                                .on_drag_move::<ExternalPaths>(cx.listener(
6748                                    move |this, event: &DragMoveEvent<ExternalPaths>, _, _| {
6749                                        let Some(_last_root_id) = this.state.last_worktree_root_id
6750                                        else {
6751                                            return;
6752                                        };
6753                                        if event.bounds.contains(&event.event.position) {
6754                                            this.drag_target_entry = Some(DragTarget::Background);
6755                                        } else {
6756                                            if this.drag_target_entry.as_ref().is_some_and(|e| {
6757                                                matches!(e, DragTarget::Background)
6758                                            }) {
6759                                                this.drag_target_entry = None;
6760                                            }
6761                                        }
6762                                    },
6763                                ))
6764                                .on_drag_move::<DraggedSelection>(cx.listener(
6765                                    move |this, event: &DragMoveEvent<DraggedSelection>, _, cx| {
6766                                        let Some(last_root_id) = this.state.last_worktree_root_id
6767                                        else {
6768                                            return;
6769                                        };
6770                                        if event.bounds.contains(&event.event.position) {
6771                                            let drag_state = event.drag(cx);
6772                                            if this.should_highlight_background_for_selection_drag(
6773                                                &drag_state,
6774                                                last_root_id,
6775                                                cx,
6776                                            ) {
6777                                                this.drag_target_entry =
6778                                                    Some(DragTarget::Background);
6779                                            }
6780                                        } else {
6781                                            if this.drag_target_entry.as_ref().is_some_and(|e| {
6782                                                matches!(e, DragTarget::Background)
6783                                            }) {
6784                                                this.drag_target_entry = None;
6785                                            }
6786                                        }
6787                                    },
6788                                ))
6789                                .on_drop(cx.listener(
6790                                    move |this, external_paths: &ExternalPaths, window, cx| {
6791                                        this.drag_target_entry = None;
6792                                        this.hover_scroll_task.take();
6793                                        if let Some(entry_id) = this.state.last_worktree_root_id {
6794                                            this.drop_external_files(
6795                                                external_paths.paths(),
6796                                                entry_id,
6797                                                window,
6798                                                cx,
6799                                            );
6800                                        }
6801                                        cx.stop_propagation();
6802                                    },
6803                                ))
6804                                .on_drop(cx.listener(
6805                                    move |this, selections: &DraggedSelection, window, cx| {
6806                                        this.drag_target_entry = None;
6807                                        this.hover_scroll_task.take();
6808                                        if let Some(entry_id) = this.state.last_worktree_root_id {
6809                                            this.drag_onto(selections, entry_id, false, window, cx);
6810                                        }
6811                                        cx.stop_propagation();
6812                                    },
6813                                ))
6814                                .on_click(cx.listener(|this, event, window, cx| {
6815                                    if matches!(event, gpui::ClickEvent::Keyboard(_)) {
6816                                        return;
6817                                    }
6818                                    cx.stop_propagation();
6819                                    this.selection = None;
6820                                    this.marked_entries.clear();
6821                                    this.focus_handle(cx).focus(window, cx);
6822                                }))
6823                                .on_mouse_down(
6824                                    MouseButton::Right,
6825                                    cx.listener(move |this, event: &MouseDownEvent, window, cx| {
6826                                        // When deploying the context menu anywhere below the last project entry,
6827                                        // act as if the user clicked the root of the last worktree.
6828                                        if let Some(entry_id) = this.state.last_worktree_root_id {
6829                                            this.deploy_context_menu(
6830                                                event.position,
6831                                                entry_id,
6832                                                window,
6833                                                cx,
6834                                            );
6835                                        }
6836                                    }),
6837                                )
6838                                .when(!project.is_read_only(cx), |el| {
6839                                    el.on_click(cx.listener(
6840                                        |this, event: &gpui::ClickEvent, window, cx| {
6841                                            if event.click_count() > 1
6842                                                && let Some(entry_id) =
6843                                                    this.state.last_worktree_root_id
6844                                            {
6845                                                let project = this.project.read(cx);
6846
6847                                                let worktree_id = if let Some(worktree) =
6848                                                    project.worktree_for_entry(entry_id, cx)
6849                                                {
6850                                                    worktree.read(cx).id()
6851                                                } else {
6852                                                    return;
6853                                                };
6854
6855                                                this.selection = Some(SelectedEntry {
6856                                                    worktree_id,
6857                                                    entry_id,
6858                                                });
6859
6860                                                this.new_file(&NewFile, window, cx);
6861                                            }
6862                                        },
6863                                    ))
6864                                }),
6865                        )
6866                        .size_full(),
6867                )
6868                .custom_scrollbars(
6869                    Scrollbars::for_settings::<ProjectPanelSettings>()
6870                        .tracked_scroll_handle(&self.scroll_handle)
6871                        .with_track_along(
6872                            ScrollAxes::Horizontal,
6873                            cx.theme().colors().panel_background,
6874                        )
6875                        .notify_content(),
6876                    window,
6877                    cx,
6878                )
6879                .children(self.context_menu.as_ref().map(|(menu, position, _)| {
6880                    deferred(
6881                        anchored()
6882                            .position(*position)
6883                            .anchor(gpui::Corner::TopLeft)
6884                            .child(menu.clone()),
6885                    )
6886                    .with_priority(3)
6887                }))
6888        } else {
6889            let focus_handle = self.focus_handle(cx);
6890
6891            v_flex()
6892                .id("empty-project_panel")
6893                .p_4()
6894                .size_full()
6895                .items_center()
6896                .justify_center()
6897                .gap_1()
6898                .track_focus(&self.focus_handle(cx))
6899                .child(
6900                    Button::new("open_project", "Open Project")
6901                        .full_width()
6902                        .key_binding(KeyBinding::for_action_in(
6903                            &workspace::Open::default(),
6904                            &focus_handle,
6905                            cx,
6906                        ))
6907                        .on_click(cx.listener(|this, _, window, cx| {
6908                            this.workspace
6909                                .update(cx, |_, cx| {
6910                                    window.dispatch_action(
6911                                        workspace::Open::default().boxed_clone(),
6912                                        cx,
6913                                    );
6914                                })
6915                                .log_err();
6916                        })),
6917                )
6918                .child(
6919                    h_flex()
6920                        .w_1_2()
6921                        .gap_2()
6922                        .child(Divider::horizontal())
6923                        .child(Label::new("or").size(LabelSize::XSmall).color(Color::Muted))
6924                        .child(Divider::horizontal()),
6925                )
6926                .child(
6927                    Button::new("clone_repo", "Clone Repository")
6928                        .full_width()
6929                        .on_click(cx.listener(|this, _, window, cx| {
6930                            this.workspace
6931                                .update(cx, |_, cx| {
6932                                    window.dispatch_action(git::Clone.boxed_clone(), cx);
6933                                })
6934                                .log_err();
6935                        })),
6936                )
6937                .when(is_local, |div| {
6938                    div.when(panel_settings.drag_and_drop, |div| {
6939                        div.drag_over::<ExternalPaths>(|style, _, _, cx| {
6940                            style.bg(cx.theme().colors().drop_target_background)
6941                        })
6942                        .on_drop(cx.listener(
6943                            move |this, external_paths: &ExternalPaths, window, cx| {
6944                                this.drag_target_entry = None;
6945                                this.hover_scroll_task.take();
6946                                if let Some(task) = this
6947                                    .workspace
6948                                    .update(cx, |workspace, cx| {
6949                                        workspace.open_workspace_for_paths(
6950                                            true,
6951                                            external_paths.paths().to_owned(),
6952                                            window,
6953                                            cx,
6954                                        )
6955                                    })
6956                                    .log_err()
6957                                {
6958                                    task.detach_and_log_err(cx);
6959                                }
6960                                cx.stop_propagation();
6961                            },
6962                        ))
6963                    })
6964                })
6965        }
6966    }
6967}
6968
6969impl Render for DraggedProjectEntryView {
6970    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
6971        let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
6972        h_flex()
6973            .font(ui_font)
6974            .pl(self.click_offset.x + px(12.))
6975            .pt(self.click_offset.y + px(12.))
6976            .child(
6977                div()
6978                    .flex()
6979                    .gap_1()
6980                    .items_center()
6981                    .py_1()
6982                    .px_2()
6983                    .rounded_lg()
6984                    .bg(cx.theme().colors().background)
6985                    .map(|this| {
6986                        if self.selections.len() > 1 && self.selections.contains(&self.selection) {
6987                            this.child(Label::new(format!("{} entries", self.selections.len())))
6988                        } else {
6989                            this.child(if let Some(icon) = &self.icon {
6990                                div().child(Icon::from_path(icon.clone()))
6991                            } else {
6992                                div()
6993                            })
6994                            .child(Label::new(self.filename.clone()))
6995                        }
6996                    }),
6997            )
6998    }
6999}
7000
7001impl EventEmitter<Event> for ProjectPanel {}
7002
7003impl EventEmitter<PanelEvent> for ProjectPanel {}
7004
7005impl Panel for ProjectPanel {
7006    fn position(&self, _: &Window, cx: &App) -> DockPosition {
7007        match ProjectPanelSettings::get_global(cx).dock {
7008            DockSide::Left => DockPosition::Left,
7009            DockSide::Right => DockPosition::Right,
7010        }
7011    }
7012
7013    fn position_is_valid(&self, position: DockPosition) -> bool {
7014        matches!(position, DockPosition::Left | DockPosition::Right)
7015    }
7016
7017    fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
7018        settings::update_settings_file(self.fs.clone(), cx, move |settings, _| {
7019            let dock = match position {
7020                DockPosition::Left | DockPosition::Bottom => DockSide::Left,
7021                DockPosition::Right => DockSide::Right,
7022            };
7023            settings.project_panel.get_or_insert_default().dock = Some(dock);
7024        });
7025    }
7026
7027    fn size(&self, _: &Window, cx: &App) -> Pixels {
7028        self.width
7029            .unwrap_or_else(|| ProjectPanelSettings::get_global(cx).default_width)
7030    }
7031
7032    fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
7033        self.width = size;
7034        cx.notify();
7035        cx.defer_in(window, |this, _, cx| {
7036            this.serialize(cx);
7037        });
7038    }
7039
7040    fn icon(&self, _: &Window, cx: &App) -> Option<IconName> {
7041        ProjectPanelSettings::get_global(cx)
7042            .button
7043            .then_some(IconName::FileTree)
7044    }
7045
7046    fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
7047        Some("Project Panel")
7048    }
7049
7050    fn toggle_action(&self) -> Box<dyn Action> {
7051        Box::new(ToggleFocus)
7052    }
7053
7054    fn persistent_name() -> &'static str {
7055        "Project Panel"
7056    }
7057
7058    fn panel_key() -> &'static str {
7059        PROJECT_PANEL_KEY
7060    }
7061
7062    fn starts_open(&self, _: &Window, cx: &App) -> bool {
7063        if !ProjectPanelSettings::get_global(cx).starts_open {
7064            return false;
7065        }
7066
7067        let project = &self.project.read(cx);
7068        project.visible_worktrees(cx).any(|tree| {
7069            tree.read(cx)
7070                .root_entry()
7071                .is_some_and(|entry| entry.is_dir())
7072        })
7073    }
7074
7075    fn activation_priority(&self) -> u32 {
7076        0
7077    }
7078}
7079
7080impl Focusable for ProjectPanel {
7081    fn focus_handle(&self, _cx: &App) -> FocusHandle {
7082        self.focus_handle.clone()
7083    }
7084}
7085
7086impl ClipboardEntry {
7087    fn is_cut(&self) -> bool {
7088        matches!(self, Self::Cut { .. })
7089    }
7090
7091    fn items(&self) -> &BTreeSet<SelectedEntry> {
7092        match self {
7093            ClipboardEntry::Copied(entries) | ClipboardEntry::Cut(entries) => entries,
7094        }
7095    }
7096
7097    fn into_copy_entry(self) -> Self {
7098        match self {
7099            ClipboardEntry::Copied(_) => self,
7100            ClipboardEntry::Cut(entries) => ClipboardEntry::Copied(entries),
7101        }
7102    }
7103}
7104
7105#[inline]
7106fn cmp_directories_first(a: &Entry, b: &Entry) -> cmp::Ordering {
7107    util::paths::compare_rel_paths((&a.path, a.is_file()), (&b.path, b.is_file()))
7108}
7109
7110#[inline]
7111fn cmp_mixed(a: &Entry, b: &Entry) -> cmp::Ordering {
7112    util::paths::compare_rel_paths_mixed((&a.path, a.is_file()), (&b.path, b.is_file()))
7113}
7114
7115#[inline]
7116fn cmp_files_first(a: &Entry, b: &Entry) -> cmp::Ordering {
7117    util::paths::compare_rel_paths_files_first((&a.path, a.is_file()), (&b.path, b.is_file()))
7118}
7119
7120#[inline]
7121fn cmp_with_mode(a: &Entry, b: &Entry, mode: &settings::ProjectPanelSortMode) -> cmp::Ordering {
7122    match mode {
7123        settings::ProjectPanelSortMode::DirectoriesFirst => cmp_directories_first(a, b),
7124        settings::ProjectPanelSortMode::Mixed => cmp_mixed(a, b),
7125        settings::ProjectPanelSortMode::FilesFirst => cmp_files_first(a, b),
7126    }
7127}
7128
7129pub fn sort_worktree_entries_with_mode(
7130    entries: &mut [impl AsRef<Entry>],
7131    mode: settings::ProjectPanelSortMode,
7132) {
7133    entries.sort_by(|lhs, rhs| cmp_with_mode(lhs.as_ref(), rhs.as_ref(), &mode));
7134}
7135
7136pub fn par_sort_worktree_entries_with_mode(
7137    entries: &mut Vec<GitEntry>,
7138    mode: settings::ProjectPanelSortMode,
7139) {
7140    entries.par_sort_by(|lhs, rhs| cmp_with_mode(lhs, rhs, &mode));
7141}
7142
7143#[cfg(test)]
7144mod project_panel_tests;