project_panel.rs

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