project_panel.rs

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