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