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