project_panel.rs

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