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                                        link: None,
1838                                    })
1839                                });
1840                                None
1841                            } else {
1842                                project_panel
1843                                    .workspace
1844                                    .update(cx, |workspace, cx| {
1845                                        workspace.open_abs_path(
1846                                            abs_path,
1847                                            OpenOptions {
1848                                                visible: Some(OpenVisible::All),
1849                                                ..Default::default()
1850                                            },
1851                                            window,
1852                                            cx,
1853                                        )
1854                                    })
1855                                    .ok()
1856                            }
1857                        })
1858                        .ok()
1859                        .flatten()
1860                    {
1861                        let _ = open_task.await?;
1862                    }
1863                }
1864            }
1865            Ok(())
1866        }))
1867    }
1868
1869    fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) {
1870        if cx.stop_active_drag(window) {
1871            self.drag_target_entry.take();
1872            self.hover_expand_task.take();
1873            return;
1874        }
1875
1876        let previous_edit_state = self.state.edit_state.take();
1877        self.update_visible_entries(None, false, false, window, cx);
1878        self.marked_entries.clear();
1879
1880        if let Some(previously_focused) =
1881            previous_edit_state.and_then(|edit_state| edit_state.previously_focused)
1882        {
1883            self.state.selection = Some(previously_focused);
1884            self.autoscroll(cx);
1885        }
1886
1887        window.focus(&self.focus_handle, cx);
1888        cx.notify();
1889    }
1890
1891    fn open_entry(
1892        &mut self,
1893        entry_id: ProjectEntryId,
1894        focus_opened_item: bool,
1895        allow_preview: bool,
1896
1897        cx: &mut Context<Self>,
1898    ) {
1899        cx.emit(Event::OpenedEntry {
1900            entry_id,
1901            focus_opened_item,
1902            allow_preview,
1903        });
1904    }
1905
1906    fn split_entry(
1907        &mut self,
1908        entry_id: ProjectEntryId,
1909        allow_preview: bool,
1910        split_direction: Option<SplitDirection>,
1911
1912        cx: &mut Context<Self>,
1913    ) {
1914        cx.emit(Event::SplitEntry {
1915            entry_id,
1916            allow_preview,
1917            split_direction,
1918        });
1919    }
1920
1921    fn new_file(&mut self, _: &NewFile, window: &mut Window, cx: &mut Context<Self>) {
1922        self.add_entry(false, window, cx)
1923    }
1924
1925    fn new_directory(&mut self, _: &NewDirectory, window: &mut Window, cx: &mut Context<Self>) {
1926        self.add_entry(true, window, cx)
1927    }
1928
1929    fn add_entry(&mut self, is_dir: bool, window: &mut Window, cx: &mut Context<Self>) {
1930        let Some((worktree_id, entry_id)) = self
1931            .state
1932            .selection
1933            .map(|entry| (entry.worktree_id, entry.entry_id))
1934            .or_else(|| {
1935                let entry_id = self.state.last_worktree_root_id?;
1936                let worktree_id = self
1937                    .project
1938                    .read(cx)
1939                    .worktree_for_entry(entry_id, cx)?
1940                    .read(cx)
1941                    .id();
1942
1943                self.state.selection = Some(SelectedEntry {
1944                    worktree_id,
1945                    entry_id,
1946                });
1947
1948                Some((worktree_id, entry_id))
1949            })
1950        else {
1951            return;
1952        };
1953
1954        let directory_id;
1955        let new_entry_id = self.resolve_entry(entry_id);
1956        if let Some((worktree, expanded_dir_ids)) = self
1957            .project
1958            .read(cx)
1959            .worktree_for_id(worktree_id, cx)
1960            .zip(self.state.expanded_dir_ids.get_mut(&worktree_id))
1961        {
1962            let worktree = worktree.read(cx);
1963            if let Some(mut entry) = worktree.entry_for_id(new_entry_id) {
1964                loop {
1965                    if entry.is_dir() {
1966                        if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) {
1967                            expanded_dir_ids.insert(ix, entry.id);
1968                        }
1969                        directory_id = entry.id;
1970                        break;
1971                    } else {
1972                        if let Some(parent_path) = entry.path.parent()
1973                            && let Some(parent_entry) = worktree.entry_for_path(parent_path)
1974                        {
1975                            entry = parent_entry;
1976                            continue;
1977                        }
1978                        return;
1979                    }
1980                }
1981            } else {
1982                return;
1983            };
1984        } else {
1985            return;
1986        };
1987
1988        self.marked_entries.clear();
1989        self.state.edit_state = Some(EditState {
1990            worktree_id,
1991            entry_id: directory_id,
1992            leaf_entry_id: None,
1993            is_dir,
1994            processing_filename: None,
1995            previously_focused: self.state.selection,
1996            depth: 0,
1997            validation_state: ValidationState::None,
1998        });
1999        self.filename_editor.update(cx, |editor, cx| {
2000            editor.clear(window, cx);
2001        });
2002        self.update_visible_entries(Some((worktree_id, NEW_ENTRY_ID)), true, true, window, cx);
2003        cx.notify();
2004    }
2005
2006    fn unflatten_entry_id(&self, leaf_entry_id: ProjectEntryId) -> ProjectEntryId {
2007        if let Some(ancestors) = self.state.ancestors.get(&leaf_entry_id) {
2008            ancestors
2009                .ancestors
2010                .get(ancestors.current_ancestor_depth)
2011                .copied()
2012                .unwrap_or(leaf_entry_id)
2013        } else {
2014            leaf_entry_id
2015        }
2016    }
2017
2018    fn rename_impl(
2019        &mut self,
2020        selection: Option<Range<usize>>,
2021        window: &mut Window,
2022        cx: &mut Context<Self>,
2023    ) {
2024        if let Some(SelectedEntry {
2025            worktree_id,
2026            entry_id,
2027        }) = self.state.selection
2028            && let Some(worktree) = self.project.read(cx).worktree_for_id(worktree_id, cx)
2029        {
2030            let sub_entry_id = self.unflatten_entry_id(entry_id);
2031            if let Some(entry) = worktree.read(cx).entry_for_id(sub_entry_id) {
2032                #[cfg(target_os = "windows")]
2033                if Some(entry) == worktree.read(cx).root_entry() {
2034                    return;
2035                }
2036
2037                if Some(entry) == worktree.read(cx).root_entry() {
2038                    let settings = ProjectPanelSettings::get_global(cx);
2039                    let visible_worktrees_count =
2040                        self.project.read(cx).visible_worktrees(cx).count();
2041                    if settings.hide_root && visible_worktrees_count == 1 {
2042                        return;
2043                    }
2044                }
2045
2046                self.state.edit_state = Some(EditState {
2047                    worktree_id,
2048                    entry_id: sub_entry_id,
2049                    leaf_entry_id: Some(entry_id),
2050                    is_dir: entry.is_dir(),
2051                    processing_filename: None,
2052                    previously_focused: None,
2053                    depth: 0,
2054                    validation_state: ValidationState::None,
2055                });
2056                let file_name = entry.path.file_name().unwrap_or_default().to_string();
2057                let selection = selection.unwrap_or_else(|| {
2058                    let file_stem = entry.path.file_stem().map(|s| s.to_string());
2059                    let selection_end =
2060                        file_stem.map_or(file_name.len(), |file_stem| file_stem.len());
2061                    0..selection_end
2062                });
2063                self.filename_editor.update(cx, |editor, cx| {
2064                    editor.set_text(file_name, window, cx);
2065                    editor.change_selections(Default::default(), window, cx, |s| {
2066                        s.select_ranges([
2067                            MultiBufferOffset(selection.start)..MultiBufferOffset(selection.end)
2068                        ])
2069                    });
2070                });
2071                self.update_visible_entries(None, true, true, window, cx);
2072                cx.notify();
2073            }
2074        }
2075    }
2076
2077    fn rename(&mut self, _: &Rename, window: &mut Window, cx: &mut Context<Self>) {
2078        self.rename_impl(None, window, cx);
2079    }
2080
2081    fn trash(&mut self, action: &Trash, window: &mut Window, cx: &mut Context<Self>) {
2082        self.remove(true, action.skip_prompt, window, cx);
2083    }
2084
2085    fn delete(&mut self, action: &Delete, window: &mut Window, cx: &mut Context<Self>) {
2086        self.remove(false, action.skip_prompt, window, cx);
2087    }
2088
2089    fn restore_file(
2090        &mut self,
2091        action: &git::RestoreFile,
2092        window: &mut Window,
2093        cx: &mut Context<Self>,
2094    ) {
2095        maybe!({
2096            let selection = self.state.selection?;
2097            let project = self.project.read(cx);
2098
2099            let (_worktree, entry) = self.selected_sub_entry(cx)?;
2100            if entry.is_dir() {
2101                return None;
2102            }
2103
2104            let project_path = project.path_for_entry(selection.entry_id, cx)?;
2105
2106            let git_store = project.git_store();
2107            let (repository, repo_path) = git_store
2108                .read(cx)
2109                .repository_and_path_for_project_path(&project_path, cx)?;
2110
2111            let snapshot = repository.read(cx).snapshot();
2112            let status = snapshot.status_for_path(&repo_path)?;
2113            if !status.status.is_modified() && !status.status.is_deleted() {
2114                return None;
2115            }
2116
2117            let file_name = entry.path.file_name()?.to_string();
2118
2119            let answer = if !action.skip_prompt {
2120                let prompt = format!("Discard changes to {}?", file_name);
2121                Some(window.prompt(PromptLevel::Info, &prompt, None, &["Restore", "Cancel"], cx))
2122            } else {
2123                None
2124            };
2125
2126            cx.spawn_in(window, async move |panel, cx| {
2127                if let Some(answer) = answer
2128                    && answer.await != Ok(0)
2129                {
2130                    return anyhow::Ok(());
2131                }
2132
2133                let task = panel.update(cx, |_panel, cx| {
2134                    repository.update(cx, |repo, cx| {
2135                        repo.checkout_files("HEAD", vec![repo_path], cx)
2136                    })
2137                })?;
2138
2139                if let Err(e) = task.await {
2140                    panel
2141                        .update(cx, |panel, cx| {
2142                            let message = format!("Failed to restore {}: {}", file_name, e);
2143                            let toast = StatusToast::new(message, cx, |this, _| {
2144                                this.icon(ToastIcon::new(IconName::XCircle).color(Color::Error))
2145                                    .dismiss_button(true)
2146                            });
2147                            panel
2148                                .workspace
2149                                .update(cx, |workspace, cx| {
2150                                    workspace.toggle_status_toast(toast, cx);
2151                                })
2152                                .ok();
2153                        })
2154                        .ok();
2155                }
2156
2157                panel
2158                    .update(cx, |panel, cx| {
2159                        panel.project.update(cx, |project, cx| {
2160                            if let Some(buffer_id) = project
2161                                .buffer_store()
2162                                .read(cx)
2163                                .buffer_id_for_project_path(&project_path)
2164                            {
2165                                if let Some(buffer) = project.buffer_for_id(*buffer_id, cx) {
2166                                    buffer.update(cx, |buffer, cx| {
2167                                        let _ = buffer.reload(cx);
2168                                    });
2169                                }
2170                            }
2171                        })
2172                    })
2173                    .ok();
2174
2175                anyhow::Ok(())
2176            })
2177            .detach_and_log_err(cx);
2178
2179            Some(())
2180        });
2181    }
2182
2183    fn remove(
2184        &mut self,
2185        trash: bool,
2186        skip_prompt: bool,
2187        window: &mut Window,
2188        cx: &mut Context<ProjectPanel>,
2189    ) {
2190        maybe!({
2191            let items_to_delete = self.disjoint_effective_entries(cx);
2192            if items_to_delete.is_empty() {
2193                return None;
2194            }
2195            let project = self.project.read(cx);
2196
2197            let mut dirty_buffers = 0;
2198            let file_paths = items_to_delete
2199                .iter()
2200                .filter_map(|selection| {
2201                    let project_path = project.path_for_entry(selection.entry_id, cx)?;
2202                    dirty_buffers +=
2203                        project.dirty_buffers(cx).any(|path| path == project_path) as usize;
2204                    Some((
2205                        selection.entry_id,
2206                        project_path.path.file_name()?.to_string(),
2207                    ))
2208                })
2209                .collect::<Vec<_>>();
2210            if file_paths.is_empty() {
2211                return None;
2212            }
2213            let answer = if !skip_prompt {
2214                let operation = if trash { "Trash" } else { "Delete" };
2215                let prompt = match file_paths.first() {
2216                    Some((_, path)) if file_paths.len() == 1 => {
2217                        let unsaved_warning = if dirty_buffers > 0 {
2218                            "\n\nIt has unsaved changes, which will be lost."
2219                        } else {
2220                            ""
2221                        };
2222
2223                        format!("{operation} {path}?{unsaved_warning}")
2224                    }
2225                    _ => {
2226                        const CUTOFF_POINT: usize = 10;
2227                        let names = if file_paths.len() > CUTOFF_POINT {
2228                            let truncated_path_counts = file_paths.len() - CUTOFF_POINT;
2229                            let mut paths = file_paths
2230                                .iter()
2231                                .map(|(_, path)| path.clone())
2232                                .take(CUTOFF_POINT)
2233                                .collect::<Vec<_>>();
2234                            paths.truncate(CUTOFF_POINT);
2235                            if truncated_path_counts == 1 {
2236                                paths.push(".. 1 file not shown".into());
2237                            } else {
2238                                paths.push(format!(".. {} files not shown", truncated_path_counts));
2239                            }
2240                            paths
2241                        } else {
2242                            file_paths.iter().map(|(_, path)| path.clone()).collect()
2243                        };
2244                        let unsaved_warning = if dirty_buffers == 0 {
2245                            String::new()
2246                        } else if dirty_buffers == 1 {
2247                            "\n\n1 of these has unsaved changes, which will be lost.".to_string()
2248                        } else {
2249                            format!(
2250                                "\n\n{dirty_buffers} of these have unsaved changes, which will be lost."
2251                            )
2252                        };
2253
2254                        format!(
2255                            "Do you want to {} the following {} files?\n{}{unsaved_warning}",
2256                            operation.to_lowercase(),
2257                            file_paths.len(),
2258                            names.join("\n")
2259                        )
2260                    }
2261                };
2262                Some(window.prompt(PromptLevel::Info, &prompt, None, &[operation, "Cancel"], cx))
2263            } else {
2264                None
2265            };
2266            let next_selection = self.find_next_selection_after_deletion(items_to_delete, cx);
2267            cx.spawn_in(window, async move |panel, cx| {
2268                if let Some(answer) = answer
2269                    && answer.await != Ok(0)
2270                {
2271                    return anyhow::Ok(());
2272                }
2273                for (entry_id, _) in file_paths {
2274                    panel
2275                        .update(cx, |panel, cx| {
2276                            panel
2277                                .project
2278                                .update(cx, |project, cx| project.delete_entry(entry_id, trash, cx))
2279                                .context("no such entry")
2280                        })??
2281                        .await?;
2282                }
2283                panel.update_in(cx, |panel, window, cx| {
2284                    if let Some(next_selection) = next_selection {
2285                        panel.update_visible_entries(
2286                            Some((next_selection.worktree_id, next_selection.entry_id)),
2287                            false,
2288                            true,
2289                            window,
2290                            cx,
2291                        );
2292                    } else {
2293                        panel.select_last(&SelectLast {}, window, cx);
2294                    }
2295                })?;
2296                Ok(())
2297            })
2298            .detach_and_log_err(cx);
2299            Some(())
2300        });
2301    }
2302
2303    fn find_next_selection_after_deletion(
2304        &self,
2305        sanitized_entries: BTreeSet<SelectedEntry>,
2306        cx: &mut Context<Self>,
2307    ) -> Option<SelectedEntry> {
2308        if sanitized_entries.is_empty() {
2309            return None;
2310        }
2311        let project = self.project.read(cx);
2312        let (worktree_id, worktree) = sanitized_entries
2313            .iter()
2314            .map(|entry| entry.worktree_id)
2315            .filter_map(|id| project.worktree_for_id(id, cx).map(|w| (id, w.read(cx))))
2316            .max_by(|(_, a), (_, b)| a.root_name().cmp(b.root_name()))?;
2317        let git_store = project.git_store().read(cx);
2318
2319        let marked_entries_in_worktree = sanitized_entries
2320            .iter()
2321            .filter(|e| e.worktree_id == worktree_id)
2322            .collect::<HashSet<_>>();
2323        let latest_entry = marked_entries_in_worktree
2324            .iter()
2325            .max_by(|a, b| {
2326                match (
2327                    worktree.entry_for_id(a.entry_id),
2328                    worktree.entry_for_id(b.entry_id),
2329                ) {
2330                    (Some(a), Some(b)) => compare_paths(
2331                        (a.path.as_std_path(), a.is_file()),
2332                        (b.path.as_std_path(), b.is_file()),
2333                    ),
2334                    _ => cmp::Ordering::Equal,
2335                }
2336            })
2337            .and_then(|e| worktree.entry_for_id(e.entry_id))?;
2338
2339        let parent_path = latest_entry.path.parent()?;
2340        let parent_entry = worktree.entry_for_path(parent_path)?;
2341
2342        // Remove all siblings that are being deleted except the last marked entry
2343        let repo_snapshots = git_store.repo_snapshots(cx);
2344        let worktree_snapshot = worktree.snapshot();
2345        let hide_gitignore = ProjectPanelSettings::get_global(cx).hide_gitignore;
2346        let mut siblings: Vec<_> =
2347            ChildEntriesGitIter::new(&repo_snapshots, &worktree_snapshot, parent_path)
2348                .filter(|sibling| {
2349                    (sibling.id == latest_entry.id)
2350                        || (!marked_entries_in_worktree.contains(&&SelectedEntry {
2351                            worktree_id,
2352                            entry_id: sibling.id,
2353                        }) && (!hide_gitignore || !sibling.is_ignored))
2354                })
2355                .map(|entry| entry.to_owned())
2356                .collect();
2357
2358        let mode = ProjectPanelSettings::get_global(cx).sort_mode;
2359        sort_worktree_entries_with_mode(&mut siblings, mode);
2360        let sibling_entry_index = siblings
2361            .iter()
2362            .position(|sibling| sibling.id == latest_entry.id)?;
2363
2364        if let Some(next_sibling) = sibling_entry_index
2365            .checked_add(1)
2366            .and_then(|i| siblings.get(i))
2367        {
2368            return Some(SelectedEntry {
2369                worktree_id,
2370                entry_id: next_sibling.id,
2371            });
2372        }
2373        if let Some(prev_sibling) = sibling_entry_index
2374            .checked_sub(1)
2375            .and_then(|i| siblings.get(i))
2376        {
2377            return Some(SelectedEntry {
2378                worktree_id,
2379                entry_id: prev_sibling.id,
2380            });
2381        }
2382        // No neighbour sibling found, fall back to parent
2383        Some(SelectedEntry {
2384            worktree_id,
2385            entry_id: parent_entry.id,
2386        })
2387    }
2388
2389    fn unfold_directory(
2390        &mut self,
2391        _: &UnfoldDirectory,
2392        window: &mut Window,
2393        cx: &mut Context<Self>,
2394    ) {
2395        if let Some((worktree, entry)) = self.selected_entry(cx) {
2396            self.state.unfolded_dir_ids.insert(entry.id);
2397
2398            let snapshot = worktree.snapshot();
2399            let mut parent_path = entry.path.parent();
2400            while let Some(path) = parent_path {
2401                if let Some(parent_entry) = worktree.entry_for_path(path) {
2402                    let mut children_iter = snapshot.child_entries(path);
2403
2404                    if children_iter.by_ref().take(2).count() > 1 {
2405                        break;
2406                    }
2407
2408                    self.state.unfolded_dir_ids.insert(parent_entry.id);
2409                    parent_path = path.parent();
2410                } else {
2411                    break;
2412                }
2413            }
2414
2415            self.update_visible_entries(None, false, true, window, cx);
2416            cx.notify();
2417        }
2418    }
2419
2420    fn fold_directory(&mut self, _: &FoldDirectory, window: &mut Window, cx: &mut Context<Self>) {
2421        if let Some((worktree, entry)) = self.selected_entry(cx) {
2422            self.state.unfolded_dir_ids.remove(&entry.id);
2423
2424            let snapshot = worktree.snapshot();
2425            let mut path = &*entry.path;
2426            loop {
2427                let mut child_entries_iter = snapshot.child_entries(path);
2428                if let Some(child) = child_entries_iter.next() {
2429                    if child_entries_iter.next().is_none() && child.is_dir() {
2430                        self.state.unfolded_dir_ids.remove(&child.id);
2431                        path = &*child.path;
2432                    } else {
2433                        break;
2434                    }
2435                } else {
2436                    break;
2437                }
2438            }
2439
2440            self.update_visible_entries(None, false, true, window, cx);
2441            cx.notify();
2442        }
2443    }
2444
2445    fn scroll_up(&mut self, _: &ScrollUp, window: &mut Window, cx: &mut Context<Self>) {
2446        for _ in 0..self.rendered_entries_len / 2 {
2447            window.dispatch_action(SelectPrevious.boxed_clone(), cx);
2448        }
2449    }
2450
2451    fn scroll_down(&mut self, _: &ScrollDown, window: &mut Window, cx: &mut Context<Self>) {
2452        for _ in 0..self.rendered_entries_len / 2 {
2453            window.dispatch_action(SelectNext.boxed_clone(), cx);
2454        }
2455    }
2456
2457    fn scroll_cursor_center(
2458        &mut self,
2459        _: &ScrollCursorCenter,
2460        _: &mut Window,
2461        cx: &mut Context<Self>,
2462    ) {
2463        if let Some((_, _, index)) = self
2464            .state
2465            .selection
2466            .and_then(|s| self.index_for_selection(s))
2467        {
2468            self.scroll_handle
2469                .scroll_to_item_strict(index, ScrollStrategy::Center);
2470            cx.notify();
2471        }
2472    }
2473
2474    fn scroll_cursor_top(&mut self, _: &ScrollCursorTop, _: &mut Window, cx: &mut Context<Self>) {
2475        if let Some((_, _, index)) = self
2476            .state
2477            .selection
2478            .and_then(|s| self.index_for_selection(s))
2479        {
2480            self.scroll_handle
2481                .scroll_to_item_strict(index, ScrollStrategy::Top);
2482            cx.notify();
2483        }
2484    }
2485
2486    fn scroll_cursor_bottom(
2487        &mut self,
2488        _: &ScrollCursorBottom,
2489        _: &mut Window,
2490        cx: &mut Context<Self>,
2491    ) {
2492        if let Some((_, _, index)) = self
2493            .state
2494            .selection
2495            .and_then(|s| self.index_for_selection(s))
2496        {
2497            self.scroll_handle
2498                .scroll_to_item_strict(index, ScrollStrategy::Bottom);
2499            cx.notify();
2500        }
2501    }
2502
2503    fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
2504        if let Some(edit_state) = &self.state.edit_state
2505            && edit_state.processing_filename.is_none()
2506        {
2507            self.filename_editor.update(cx, |editor, cx| {
2508                editor.move_to_end_of_line(
2509                    &editor::actions::MoveToEndOfLine {
2510                        stop_at_soft_wraps: false,
2511                    },
2512                    window,
2513                    cx,
2514                );
2515            });
2516            return;
2517        }
2518        if let Some(selection) = self.state.selection {
2519            let (mut worktree_ix, mut entry_ix, _) =
2520                self.index_for_selection(selection).unwrap_or_default();
2521            if let Some(worktree_entries) = self
2522                .state
2523                .visible_entries
2524                .get(worktree_ix)
2525                .map(|v| &v.entries)
2526            {
2527                if entry_ix + 1 < worktree_entries.len() {
2528                    entry_ix += 1;
2529                } else {
2530                    worktree_ix += 1;
2531                    entry_ix = 0;
2532                }
2533            }
2534
2535            if let Some(VisibleEntriesForWorktree {
2536                worktree_id,
2537                entries,
2538                ..
2539            }) = self.state.visible_entries.get(worktree_ix)
2540                && let Some(entry) = entries.get(entry_ix)
2541            {
2542                let selection = SelectedEntry {
2543                    worktree_id: *worktree_id,
2544                    entry_id: entry.id,
2545                };
2546                self.state.selection = Some(selection);
2547                if window.modifiers().shift {
2548                    self.marked_entries.push(selection);
2549                }
2550
2551                self.autoscroll(cx);
2552                cx.notify();
2553            }
2554        } else {
2555            self.select_first(&SelectFirst {}, window, cx);
2556        }
2557    }
2558
2559    fn select_prev_diagnostic(
2560        &mut self,
2561        action: &SelectPrevDiagnostic,
2562        window: &mut Window,
2563        cx: &mut Context<Self>,
2564    ) {
2565        let selection = self.find_entry(
2566            self.state.selection.as_ref(),
2567            true,
2568            |entry, worktree_id| {
2569                self.state.selection.is_none_or(|selection| {
2570                    if selection.worktree_id == worktree_id {
2571                        selection.entry_id != entry.id
2572                    } else {
2573                        true
2574                    }
2575                }) && entry.is_file()
2576                    && self
2577                        .diagnostics
2578                        .get(&(worktree_id, entry.path.clone()))
2579                        .is_some_and(|severity| action.severity.matches(*severity))
2580            },
2581            cx,
2582        );
2583
2584        if let Some(selection) = selection {
2585            self.state.selection = Some(selection);
2586            self.expand_entry(selection.worktree_id, selection.entry_id, cx);
2587            self.update_visible_entries(
2588                Some((selection.worktree_id, selection.entry_id)),
2589                false,
2590                true,
2591                window,
2592                cx,
2593            );
2594            cx.notify();
2595        }
2596    }
2597
2598    fn select_next_diagnostic(
2599        &mut self,
2600        action: &SelectNextDiagnostic,
2601        window: &mut Window,
2602        cx: &mut Context<Self>,
2603    ) {
2604        let selection = self.find_entry(
2605            self.state.selection.as_ref(),
2606            false,
2607            |entry, worktree_id| {
2608                self.state.selection.is_none_or(|selection| {
2609                    if selection.worktree_id == worktree_id {
2610                        selection.entry_id != entry.id
2611                    } else {
2612                        true
2613                    }
2614                }) && entry.is_file()
2615                    && self
2616                        .diagnostics
2617                        .get(&(worktree_id, entry.path.clone()))
2618                        .is_some_and(|severity| action.severity.matches(*severity))
2619            },
2620            cx,
2621        );
2622
2623        if let Some(selection) = selection {
2624            self.state.selection = Some(selection);
2625            self.expand_entry(selection.worktree_id, selection.entry_id, cx);
2626            self.update_visible_entries(
2627                Some((selection.worktree_id, selection.entry_id)),
2628                false,
2629                true,
2630                window,
2631                cx,
2632            );
2633            cx.notify();
2634        }
2635    }
2636
2637    fn select_prev_git_entry(
2638        &mut self,
2639        _: &SelectPrevGitEntry,
2640        window: &mut Window,
2641        cx: &mut Context<Self>,
2642    ) {
2643        let selection = self.find_entry(
2644            self.state.selection.as_ref(),
2645            true,
2646            |entry, worktree_id| {
2647                (self.state.selection.is_none()
2648                    || self.state.selection.is_some_and(|selection| {
2649                        if selection.worktree_id == worktree_id {
2650                            selection.entry_id != entry.id
2651                        } else {
2652                            true
2653                        }
2654                    }))
2655                    && entry.is_file()
2656                    && entry.git_summary.index.modified + entry.git_summary.worktree.modified > 0
2657            },
2658            cx,
2659        );
2660
2661        if let Some(selection) = selection {
2662            self.state.selection = Some(selection);
2663            self.expand_entry(selection.worktree_id, selection.entry_id, cx);
2664            self.update_visible_entries(
2665                Some((selection.worktree_id, selection.entry_id)),
2666                false,
2667                true,
2668                window,
2669                cx,
2670            );
2671            cx.notify();
2672        }
2673    }
2674
2675    fn select_prev_directory(
2676        &mut self,
2677        _: &SelectPrevDirectory,
2678        _: &mut Window,
2679        cx: &mut Context<Self>,
2680    ) {
2681        let selection = self.find_visible_entry(
2682            self.state.selection.as_ref(),
2683            true,
2684            |entry, worktree_id| {
2685                self.state.selection.is_none_or(|selection| {
2686                    if selection.worktree_id == worktree_id {
2687                        selection.entry_id != entry.id
2688                    } else {
2689                        true
2690                    }
2691                }) && entry.is_dir()
2692            },
2693            cx,
2694        );
2695
2696        if let Some(selection) = selection {
2697            self.state.selection = Some(selection);
2698            self.autoscroll(cx);
2699            cx.notify();
2700        }
2701    }
2702
2703    fn select_next_directory(
2704        &mut self,
2705        _: &SelectNextDirectory,
2706        _: &mut Window,
2707        cx: &mut Context<Self>,
2708    ) {
2709        let selection = self.find_visible_entry(
2710            self.state.selection.as_ref(),
2711            false,
2712            |entry, worktree_id| {
2713                self.state.selection.is_none_or(|selection| {
2714                    if selection.worktree_id == worktree_id {
2715                        selection.entry_id != entry.id
2716                    } else {
2717                        true
2718                    }
2719                }) && entry.is_dir()
2720            },
2721            cx,
2722        );
2723
2724        if let Some(selection) = selection {
2725            self.state.selection = Some(selection);
2726            self.autoscroll(cx);
2727            cx.notify();
2728        }
2729    }
2730
2731    fn select_next_git_entry(
2732        &mut self,
2733        _: &SelectNextGitEntry,
2734        window: &mut Window,
2735        cx: &mut Context<Self>,
2736    ) {
2737        let selection = self.find_entry(
2738            self.state.selection.as_ref(),
2739            false,
2740            |entry, worktree_id| {
2741                self.state.selection.is_none_or(|selection| {
2742                    if selection.worktree_id == worktree_id {
2743                        selection.entry_id != entry.id
2744                    } else {
2745                        true
2746                    }
2747                }) && entry.is_file()
2748                    && entry.git_summary.index.modified + entry.git_summary.worktree.modified > 0
2749            },
2750            cx,
2751        );
2752
2753        if let Some(selection) = selection {
2754            self.state.selection = Some(selection);
2755            self.expand_entry(selection.worktree_id, selection.entry_id, cx);
2756            self.update_visible_entries(
2757                Some((selection.worktree_id, selection.entry_id)),
2758                false,
2759                true,
2760                window,
2761                cx,
2762            );
2763            cx.notify();
2764        }
2765    }
2766
2767    fn select_parent(&mut self, _: &SelectParent, window: &mut Window, cx: &mut Context<Self>) {
2768        if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
2769            if let Some(parent) = entry.path.parent() {
2770                let worktree = worktree.read(cx);
2771                if let Some(parent_entry) = worktree.entry_for_path(parent) {
2772                    self.state.selection = Some(SelectedEntry {
2773                        worktree_id: worktree.id(),
2774                        entry_id: parent_entry.id,
2775                    });
2776                    self.autoscroll(cx);
2777                    cx.notify();
2778                }
2779            }
2780        } else {
2781            self.select_first(&SelectFirst {}, window, cx);
2782        }
2783    }
2784
2785    fn select_first(&mut self, _: &SelectFirst, window: &mut Window, cx: &mut Context<Self>) {
2786        if let Some(VisibleEntriesForWorktree {
2787            worktree_id,
2788            entries,
2789            ..
2790        }) = self.state.visible_entries.first()
2791            && let Some(entry) = entries.first()
2792        {
2793            let selection = SelectedEntry {
2794                worktree_id: *worktree_id,
2795                entry_id: entry.id,
2796            };
2797            self.state.selection = Some(selection);
2798            if window.modifiers().shift {
2799                self.marked_entries.push(selection);
2800            }
2801            self.autoscroll(cx);
2802            cx.notify();
2803        }
2804    }
2805
2806    fn select_last(&mut self, _: &SelectLast, _: &mut Window, cx: &mut Context<Self>) {
2807        if let Some(VisibleEntriesForWorktree {
2808            worktree_id,
2809            entries,
2810            ..
2811        }) = self.state.visible_entries.last()
2812        {
2813            let worktree = self.project.read(cx).worktree_for_id(*worktree_id, cx);
2814            if let (Some(worktree), Some(entry)) = (worktree, entries.last()) {
2815                let worktree = worktree.read(cx);
2816                if let Some(entry) = worktree.entry_for_id(entry.id) {
2817                    let selection = SelectedEntry {
2818                        worktree_id: *worktree_id,
2819                        entry_id: entry.id,
2820                    };
2821                    self.state.selection = Some(selection);
2822                    self.autoscroll(cx);
2823                    cx.notify();
2824                }
2825            }
2826        }
2827    }
2828
2829    fn autoscroll(&mut self, cx: &mut Context<Self>) {
2830        if let Some((_, _, index)) = self
2831            .state
2832            .selection
2833            .and_then(|s| self.index_for_selection(s))
2834        {
2835            self.scroll_handle.scroll_to_item_with_offset(
2836                index,
2837                ScrollStrategy::Center,
2838                self.sticky_items_count,
2839            );
2840            cx.notify();
2841        }
2842    }
2843
2844    fn cut(&mut self, _: &Cut, _: &mut Window, cx: &mut Context<Self>) {
2845        let entries = self.disjoint_effective_entries(cx);
2846        if !entries.is_empty() {
2847            self.clipboard = Some(ClipboardEntry::Cut(entries));
2848            cx.notify();
2849        }
2850    }
2851
2852    fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context<Self>) {
2853        let entries = self.disjoint_effective_entries(cx);
2854        if !entries.is_empty() {
2855            self.clipboard = Some(ClipboardEntry::Copied(entries));
2856            cx.notify();
2857        }
2858    }
2859
2860    fn create_paste_path(
2861        &self,
2862        source: &SelectedEntry,
2863        (worktree, target_entry): (Entity<Worktree>, &Entry),
2864        cx: &App,
2865    ) -> Option<(Arc<RelPath>, Option<Range<usize>>)> {
2866        let mut new_path = target_entry.path.to_rel_path_buf();
2867        // If we're pasting into a file, or a directory into itself, go up one level.
2868        if target_entry.is_file() || (target_entry.is_dir() && target_entry.id == source.entry_id) {
2869            new_path.pop();
2870        }
2871        let clipboard_entry_file_name = self
2872            .project
2873            .read(cx)
2874            .path_for_entry(source.entry_id, cx)?
2875            .path
2876            .file_name()?
2877            .to_string();
2878        new_path.push(RelPath::unix(&clipboard_entry_file_name).unwrap());
2879        let extension = new_path.extension().map(|s| s.to_string());
2880        let file_name_without_extension = new_path.file_stem()?.to_string();
2881        let file_name_len = file_name_without_extension.len();
2882        let mut disambiguation_range = None;
2883        let mut ix = 0;
2884        {
2885            let worktree = worktree.read(cx);
2886            while worktree.entry_for_path(&new_path).is_some() {
2887                new_path.pop();
2888
2889                let mut new_file_name = file_name_without_extension.to_string();
2890
2891                let disambiguation = " copy";
2892                let mut disambiguation_len = disambiguation.len();
2893
2894                new_file_name.push_str(disambiguation);
2895
2896                if ix > 0 {
2897                    let extra_disambiguation = format!(" {}", ix);
2898                    disambiguation_len += extra_disambiguation.len();
2899                    new_file_name.push_str(&extra_disambiguation);
2900                }
2901                if let Some(extension) = extension.as_ref() {
2902                    new_file_name.push_str(".");
2903                    new_file_name.push_str(extension);
2904                }
2905
2906                new_path.push(RelPath::unix(&new_file_name).unwrap());
2907
2908                disambiguation_range = Some(file_name_len..(file_name_len + disambiguation_len));
2909                ix += 1;
2910            }
2911        }
2912        Some((new_path.as_rel_path().into(), disambiguation_range))
2913    }
2914
2915    fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
2916        maybe!({
2917            let (worktree, entry) = self.selected_entry_handle(cx)?;
2918            let entry = entry.clone();
2919            let worktree_id = worktree.read(cx).id();
2920            let clipboard_entries = self
2921                .clipboard
2922                .as_ref()
2923                .filter(|clipboard| !clipboard.items().is_empty())?;
2924
2925            enum PasteTask {
2926                Rename(Task<Result<CreatedEntry>>),
2927                Copy(Task<Result<Option<Entry>>>),
2928            }
2929
2930            let mut paste_tasks = Vec::new();
2931            let mut disambiguation_range = None;
2932            let clip_is_cut = clipboard_entries.is_cut();
2933            for clipboard_entry in clipboard_entries.items() {
2934                let (new_path, new_disambiguation_range) =
2935                    self.create_paste_path(clipboard_entry, self.selected_sub_entry(cx)?, cx)?;
2936                let clip_entry_id = clipboard_entry.entry_id;
2937                let task = if clipboard_entries.is_cut() {
2938                    let task = self.project.update(cx, |project, cx| {
2939                        project.rename_entry(clip_entry_id, (worktree_id, new_path).into(), cx)
2940                    });
2941                    PasteTask::Rename(task)
2942                } else {
2943                    let task = self.project.update(cx, |project, cx| {
2944                        project.copy_entry(clip_entry_id, (worktree_id, new_path).into(), cx)
2945                    });
2946                    PasteTask::Copy(task)
2947                };
2948                paste_tasks.push(task);
2949                disambiguation_range = new_disambiguation_range.or(disambiguation_range);
2950            }
2951
2952            let item_count = paste_tasks.len();
2953
2954            cx.spawn_in(window, async move |project_panel, cx| {
2955                let mut last_succeed = None;
2956                for task in paste_tasks {
2957                    match task {
2958                        PasteTask::Rename(task) => {
2959                            if let Some(CreatedEntry::Included(entry)) =
2960                                task.await.notify_async_err(cx)
2961                            {
2962                                last_succeed = Some(entry);
2963                            }
2964                        }
2965                        PasteTask::Copy(task) => {
2966                            if let Some(Some(entry)) = task.await.notify_async_err(cx) {
2967                                last_succeed = Some(entry);
2968                            }
2969                        }
2970                    }
2971                }
2972                // update selection
2973                if let Some(entry) = last_succeed {
2974                    project_panel
2975                        .update_in(cx, |project_panel, window, cx| {
2976                            project_panel.state.selection = Some(SelectedEntry {
2977                                worktree_id,
2978                                entry_id: entry.id,
2979                            });
2980
2981                            if item_count == 1 {
2982                                // open entry if not dir, setting is enabled, and only focus if rename is not pending
2983                                if !entry.is_dir() {
2984                                    let settings = ProjectPanelSettings::get_global(cx);
2985                                    if settings.auto_open.should_open_on_paste() {
2986                                        project_panel.open_entry(
2987                                            entry.id,
2988                                            disambiguation_range.is_none(),
2989                                            false,
2990                                            cx,
2991                                        );
2992                                    }
2993                                }
2994
2995                                // if only one entry was pasted and it was disambiguated, open the rename editor
2996                                if disambiguation_range.is_some() {
2997                                    cx.defer_in(window, |this, window, cx| {
2998                                        this.rename_impl(disambiguation_range, window, cx);
2999                                    });
3000                                }
3001                            }
3002                        })
3003                        .ok();
3004                }
3005
3006                anyhow::Ok(())
3007            })
3008            .detach_and_log_err(cx);
3009
3010            if clip_is_cut {
3011                // Convert the clipboard cut entry to a copy entry after the first paste.
3012                self.clipboard = self.clipboard.take().map(ClipboardEntry::into_copy_entry);
3013            }
3014
3015            self.expand_entry(worktree_id, entry.id, cx);
3016            Some(())
3017        });
3018    }
3019
3020    fn duplicate(&mut self, _: &Duplicate, window: &mut Window, cx: &mut Context<Self>) {
3021        self.copy(&Copy {}, window, cx);
3022        self.paste(&Paste {}, window, cx);
3023    }
3024
3025    fn copy_path(
3026        &mut self,
3027        _: &zed_actions::workspace::CopyPath,
3028        _: &mut Window,
3029        cx: &mut Context<Self>,
3030    ) {
3031        let abs_file_paths = {
3032            let project = self.project.read(cx);
3033            self.effective_entries()
3034                .into_iter()
3035                .filter_map(|entry| {
3036                    let entry_path = project.path_for_entry(entry.entry_id, cx)?.path;
3037                    Some(
3038                        project
3039                            .worktree_for_id(entry.worktree_id, cx)?
3040                            .read(cx)
3041                            .absolutize(&entry_path)
3042                            .to_string_lossy()
3043                            .to_string(),
3044                    )
3045                })
3046                .collect::<Vec<_>>()
3047        };
3048        if !abs_file_paths.is_empty() {
3049            cx.write_to_clipboard(ClipboardItem::new_string(abs_file_paths.join("\n")));
3050        }
3051    }
3052
3053    fn copy_relative_path(
3054        &mut self,
3055        _: &zed_actions::workspace::CopyRelativePath,
3056        _: &mut Window,
3057        cx: &mut Context<Self>,
3058    ) {
3059        let path_style = self.project.read(cx).path_style(cx);
3060        let file_paths = {
3061            let project = self.project.read(cx);
3062            self.effective_entries()
3063                .into_iter()
3064                .filter_map(|entry| {
3065                    Some(
3066                        project
3067                            .path_for_entry(entry.entry_id, cx)?
3068                            .path
3069                            .display(path_style)
3070                            .into_owned(),
3071                    )
3072                })
3073                .collect::<Vec<_>>()
3074        };
3075        if !file_paths.is_empty() {
3076            cx.write_to_clipboard(ClipboardItem::new_string(file_paths.join("\n")));
3077        }
3078    }
3079
3080    fn reveal_in_finder(
3081        &mut self,
3082        _: &RevealInFileManager,
3083        _: &mut Window,
3084        cx: &mut Context<Self>,
3085    ) {
3086        if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
3087            let path = worktree.read(cx).absolutize(&entry.path);
3088            self.project
3089                .update(cx, |project, cx| project.reveal_path(&path, cx));
3090        }
3091    }
3092
3093    fn remove_from_project(
3094        &mut self,
3095        _: &RemoveFromProject,
3096        _window: &mut Window,
3097        cx: &mut Context<Self>,
3098    ) {
3099        for entry in self.effective_entries().iter() {
3100            let worktree_id = entry.worktree_id;
3101            self.project
3102                .update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
3103        }
3104    }
3105
3106    fn file_abs_paths_to_diff(&self, cx: &Context<Self>) -> Option<(PathBuf, PathBuf)> {
3107        let mut selections_abs_path = self
3108            .marked_entries
3109            .iter()
3110            .filter_map(|entry| {
3111                let project = self.project.read(cx);
3112                let worktree = project.worktree_for_id(entry.worktree_id, cx)?;
3113                let entry = worktree.read(cx).entry_for_id(entry.entry_id)?;
3114                if !entry.is_file() {
3115                    return None;
3116                }
3117                Some(worktree.read(cx).absolutize(&entry.path))
3118            })
3119            .rev();
3120
3121        let last_path = selections_abs_path.next()?;
3122        let previous_to_last = selections_abs_path.next()?;
3123        Some((previous_to_last, last_path))
3124    }
3125
3126    fn compare_marked_files(
3127        &mut self,
3128        _: &CompareMarkedFiles,
3129        window: &mut Window,
3130        cx: &mut Context<Self>,
3131    ) {
3132        let selected_files = self.file_abs_paths_to_diff(cx);
3133        if let Some((file_path1, file_path2)) = selected_files {
3134            self.workspace
3135                .update(cx, |workspace, cx| {
3136                    FileDiffView::open(file_path1, file_path2, workspace, window, cx)
3137                        .detach_and_log_err(cx);
3138                })
3139                .ok();
3140        }
3141    }
3142
3143    fn open_system(&mut self, _: &OpenWithSystem, _: &mut Window, cx: &mut Context<Self>) {
3144        if let Some((worktree, entry)) = self.selected_entry(cx) {
3145            let abs_path = worktree.absolutize(&entry.path);
3146            cx.open_with_system(&abs_path);
3147        }
3148    }
3149
3150    fn open_in_terminal(
3151        &mut self,
3152        _: &OpenInTerminal,
3153        window: &mut Window,
3154        cx: &mut Context<Self>,
3155    ) {
3156        if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
3157            let abs_path = match &entry.canonical_path {
3158                Some(canonical_path) => canonical_path.to_path_buf(),
3159                None => worktree.read(cx).absolutize(&entry.path),
3160            };
3161
3162            let working_directory = if entry.is_dir() {
3163                Some(abs_path)
3164            } else {
3165                abs_path.parent().map(|path| path.to_path_buf())
3166            };
3167            if let Some(working_directory) = working_directory {
3168                window.dispatch_action(
3169                    workspace::OpenTerminal {
3170                        working_directory,
3171                        local: false,
3172                    }
3173                    .boxed_clone(),
3174                    cx,
3175                )
3176            }
3177        }
3178    }
3179
3180    pub fn new_search_in_directory(
3181        &mut self,
3182        _: &NewSearchInDirectory,
3183        window: &mut Window,
3184        cx: &mut Context<Self>,
3185    ) {
3186        if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
3187            let dir_path = if entry.is_dir() {
3188                entry.path.clone()
3189            } else {
3190                // entry is a file, use its parent directory
3191                match entry.path.parent() {
3192                    Some(parent) => Arc::from(parent),
3193                    None => {
3194                        // File at root, open search with empty filter
3195                        self.workspace
3196                            .update(cx, |workspace, cx| {
3197                                search::ProjectSearchView::new_search_in_directory(
3198                                    workspace,
3199                                    RelPath::empty(),
3200                                    window,
3201                                    cx,
3202                                );
3203                            })
3204                            .ok();
3205                        return;
3206                    }
3207                }
3208            };
3209
3210            let include_root = self.project.read(cx).visible_worktrees(cx).count() > 1;
3211            let dir_path = if include_root {
3212                worktree.read(cx).root_name().join(&dir_path)
3213            } else {
3214                dir_path
3215            };
3216
3217            self.workspace
3218                .update(cx, |workspace, cx| {
3219                    search::ProjectSearchView::new_search_in_directory(
3220                        workspace, &dir_path, window, cx,
3221                    );
3222                })
3223                .ok();
3224        }
3225    }
3226
3227    fn move_entry(
3228        &mut self,
3229        entry_to_move: ProjectEntryId,
3230        destination: ProjectEntryId,
3231        destination_is_file: bool,
3232        cx: &mut Context<Self>,
3233    ) -> Option<Task<Result<CreatedEntry>>> {
3234        if self
3235            .project
3236            .read(cx)
3237            .entry_is_worktree_root(entry_to_move, cx)
3238        {
3239            self.move_worktree_root(entry_to_move, destination, cx);
3240            None
3241        } else {
3242            self.move_worktree_entry(entry_to_move, destination, destination_is_file, cx)
3243        }
3244    }
3245
3246    fn move_worktree_root(
3247        &mut self,
3248        entry_to_move: ProjectEntryId,
3249        destination: ProjectEntryId,
3250        cx: &mut Context<Self>,
3251    ) {
3252        self.project.update(cx, |project, cx| {
3253            let Some(worktree_to_move) = project.worktree_for_entry(entry_to_move, cx) else {
3254                return;
3255            };
3256            let Some(destination_worktree) = project.worktree_for_entry(destination, cx) else {
3257                return;
3258            };
3259
3260            let worktree_id = worktree_to_move.read(cx).id();
3261            let destination_id = destination_worktree.read(cx).id();
3262
3263            project
3264                .move_worktree(worktree_id, destination_id, cx)
3265                .log_err();
3266        });
3267    }
3268
3269    fn move_worktree_entry(
3270        &mut self,
3271        entry_to_move: ProjectEntryId,
3272        destination_entry: ProjectEntryId,
3273        destination_is_file: bool,
3274        cx: &mut Context<Self>,
3275    ) -> Option<Task<Result<CreatedEntry>>> {
3276        if entry_to_move == destination_entry {
3277            return None;
3278        }
3279
3280        let (destination_worktree, rename_task) = self.project.update(cx, |project, cx| {
3281            let Some(source_path) = project.path_for_entry(entry_to_move, cx) else {
3282                return (None, None);
3283            };
3284            let Some(destination_path) = project.path_for_entry(destination_entry, cx) else {
3285                return (None, None);
3286            };
3287            let destination_worktree_id = destination_path.worktree_id;
3288
3289            let destination_dir = if destination_is_file {
3290                destination_path.path.parent().unwrap_or(RelPath::empty())
3291            } else {
3292                destination_path.path.as_ref()
3293            };
3294
3295            let Some(source_name) = source_path.path.file_name() else {
3296                return (None, None);
3297            };
3298            let Ok(source_name) = RelPath::unix(source_name) else {
3299                return (None, None);
3300            };
3301
3302            let mut new_path = destination_dir.to_rel_path_buf();
3303            new_path.push(source_name);
3304            let rename_task = (new_path.as_rel_path() != source_path.path.as_ref()).then(|| {
3305                project.rename_entry(
3306                    entry_to_move,
3307                    (destination_worktree_id, new_path).into(),
3308                    cx,
3309                )
3310            });
3311
3312            (
3313                project.worktree_id_for_entry(destination_entry, cx),
3314                rename_task,
3315            )
3316        });
3317
3318        if let Some(destination_worktree) = destination_worktree {
3319            self.expand_entry(destination_worktree, destination_entry, cx);
3320        }
3321        rename_task
3322    }
3323
3324    fn index_for_selection(&self, selection: SelectedEntry) -> Option<(usize, usize, usize)> {
3325        self.index_for_entry(selection.entry_id, selection.worktree_id)
3326    }
3327
3328    fn disjoint_effective_entries(&self, cx: &App) -> BTreeSet<SelectedEntry> {
3329        self.disjoint_entries(self.effective_entries(), cx)
3330    }
3331
3332    fn disjoint_entries(
3333        &self,
3334        entries: BTreeSet<SelectedEntry>,
3335        cx: &App,
3336    ) -> BTreeSet<SelectedEntry> {
3337        let mut sanitized_entries = BTreeSet::new();
3338        if entries.is_empty() {
3339            return sanitized_entries;
3340        }
3341
3342        let project = self.project.read(cx);
3343        let entries_by_worktree: HashMap<WorktreeId, Vec<SelectedEntry>> = entries
3344            .into_iter()
3345            .filter(|entry| !project.entry_is_worktree_root(entry.entry_id, cx))
3346            .fold(HashMap::default(), |mut map, entry| {
3347                map.entry(entry.worktree_id).or_default().push(entry);
3348                map
3349            });
3350
3351        for (worktree_id, worktree_entries) in entries_by_worktree {
3352            if let Some(worktree) = project.worktree_for_id(worktree_id, cx) {
3353                let worktree = worktree.read(cx);
3354                let dir_paths = worktree_entries
3355                    .iter()
3356                    .filter_map(|entry| {
3357                        worktree.entry_for_id(entry.entry_id).and_then(|entry| {
3358                            if entry.is_dir() {
3359                                Some(entry.path.as_ref())
3360                            } else {
3361                                None
3362                            }
3363                        })
3364                    })
3365                    .collect::<BTreeSet<_>>();
3366
3367                sanitized_entries.extend(worktree_entries.into_iter().filter(|entry| {
3368                    let Some(entry_info) = worktree.entry_for_id(entry.entry_id) else {
3369                        return false;
3370                    };
3371                    let entry_path = entry_info.path.as_ref();
3372                    let inside_selected_dir = dir_paths.iter().any(|&dir_path| {
3373                        entry_path != dir_path && entry_path.starts_with(dir_path)
3374                    });
3375                    !inside_selected_dir
3376                }));
3377            }
3378        }
3379
3380        sanitized_entries
3381    }
3382
3383    fn effective_entries(&self) -> BTreeSet<SelectedEntry> {
3384        if let Some(selection) = self.state.selection {
3385            let selection = SelectedEntry {
3386                entry_id: self.resolve_entry(selection.entry_id),
3387                worktree_id: selection.worktree_id,
3388            };
3389
3390            // Default to using just the selected item when nothing is marked.
3391            if self.marked_entries.is_empty() {
3392                return BTreeSet::from([selection]);
3393            }
3394
3395            // Allow operating on the selected item even when something else is marked,
3396            // making it easier to perform one-off actions without clearing a mark.
3397            if self.marked_entries.len() == 1 && !self.marked_entries.contains(&selection) {
3398                return BTreeSet::from([selection]);
3399            }
3400        }
3401
3402        // Return only marked entries since we've already handled special cases where
3403        // only selection should take precedence. At this point, marked entries may or
3404        // may not include the current selection, which is intentional.
3405        self.marked_entries
3406            .iter()
3407            .map(|entry| SelectedEntry {
3408                entry_id: self.resolve_entry(entry.entry_id),
3409                worktree_id: entry.worktree_id,
3410            })
3411            .collect::<BTreeSet<_>>()
3412    }
3413
3414    /// Finds the currently selected subentry for a given leaf entry id. If a given entry
3415    /// has no ancestors, the project entry ID that's passed in is returned as-is.
3416    fn resolve_entry(&self, id: ProjectEntryId) -> ProjectEntryId {
3417        self.state
3418            .ancestors
3419            .get(&id)
3420            .and_then(|ancestors| ancestors.active_ancestor())
3421            .unwrap_or(id)
3422    }
3423
3424    pub fn selected_entry<'a>(&self, cx: &'a App) -> Option<(&'a Worktree, &'a project::Entry)> {
3425        let (worktree, entry) = self.selected_entry_handle(cx)?;
3426        Some((worktree.read(cx), entry))
3427    }
3428
3429    /// Compared to selected_entry, this function resolves to the currently
3430    /// selected subentry if dir auto-folding is enabled.
3431    fn selected_sub_entry<'a>(
3432        &self,
3433        cx: &'a App,
3434    ) -> Option<(Entity<Worktree>, &'a project::Entry)> {
3435        let (worktree, mut entry) = self.selected_entry_handle(cx)?;
3436
3437        let resolved_id = self.resolve_entry(entry.id);
3438        if resolved_id != entry.id {
3439            let worktree = worktree.read(cx);
3440            entry = worktree.entry_for_id(resolved_id)?;
3441        }
3442        Some((worktree, entry))
3443    }
3444    fn selected_entry_handle<'a>(
3445        &self,
3446        cx: &'a App,
3447    ) -> Option<(Entity<Worktree>, &'a project::Entry)> {
3448        let selection = self.state.selection?;
3449        let project = self.project.read(cx);
3450        let worktree = project.worktree_for_id(selection.worktree_id, cx)?;
3451        let entry = worktree.read(cx).entry_for_id(selection.entry_id)?;
3452        Some((worktree, entry))
3453    }
3454
3455    fn expand_to_selection(&mut self, cx: &mut Context<Self>) -> Option<()> {
3456        let (worktree, entry) = self.selected_entry(cx)?;
3457        let expanded_dir_ids = self
3458            .state
3459            .expanded_dir_ids
3460            .entry(worktree.id())
3461            .or_default();
3462
3463        for path in entry.path.ancestors() {
3464            let Some(entry) = worktree.entry_for_path(path) else {
3465                continue;
3466            };
3467            if entry.is_dir()
3468                && let Err(idx) = expanded_dir_ids.binary_search(&entry.id)
3469            {
3470                expanded_dir_ids.insert(idx, entry.id);
3471            }
3472        }
3473
3474        Some(())
3475    }
3476
3477    fn create_new_git_entry(
3478        parent_entry: &Entry,
3479        git_summary: GitSummary,
3480        new_entry_kind: EntryKind,
3481    ) -> GitEntry {
3482        GitEntry {
3483            entry: Entry {
3484                id: NEW_ENTRY_ID,
3485                kind: new_entry_kind,
3486                path: parent_entry.path.join(RelPath::unix("\0").unwrap()),
3487                inode: 0,
3488                mtime: parent_entry.mtime,
3489                size: parent_entry.size,
3490                is_ignored: parent_entry.is_ignored,
3491                is_hidden: parent_entry.is_hidden,
3492                is_external: false,
3493                is_private: false,
3494                is_always_included: parent_entry.is_always_included,
3495                canonical_path: parent_entry.canonical_path.clone(),
3496                char_bag: parent_entry.char_bag,
3497                is_fifo: parent_entry.is_fifo,
3498            },
3499            git_summary,
3500        }
3501    }
3502
3503    fn update_visible_entries(
3504        &mut self,
3505        new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,
3506        focus_filename_editor: bool,
3507        autoscroll: bool,
3508        window: &mut Window,
3509        cx: &mut Context<Self>,
3510    ) {
3511        let now = Instant::now();
3512        let settings = ProjectPanelSettings::get_global(cx);
3513        let auto_collapse_dirs = settings.auto_fold_dirs;
3514        let hide_gitignore = settings.hide_gitignore;
3515        let sort_mode = settings.sort_mode;
3516        let project = self.project.read(cx);
3517        let repo_snapshots = project.git_store().read(cx).repo_snapshots(cx);
3518
3519        let old_ancestors = self.state.ancestors.clone();
3520        let mut new_state = State::derive(&self.state);
3521        new_state.last_worktree_root_id = project
3522            .visible_worktrees(cx)
3523            .next_back()
3524            .and_then(|worktree| worktree.read(cx).root_entry())
3525            .map(|entry| entry.id);
3526        let mut max_width_item = None;
3527
3528        let visible_worktrees: Vec<_> = project
3529            .visible_worktrees(cx)
3530            .map(|worktree| worktree.read(cx).snapshot())
3531            .collect();
3532        let hide_root = settings.hide_root && visible_worktrees.len() == 1;
3533        let hide_hidden = settings.hide_hidden;
3534
3535        let visible_entries_task = cx.spawn_in(window, async move |this, cx| {
3536            let new_state = cx
3537                .background_spawn(async move {
3538                    for worktree_snapshot in visible_worktrees {
3539                        let worktree_id = worktree_snapshot.id();
3540
3541                        let expanded_dir_ids = match new_state.expanded_dir_ids.entry(worktree_id) {
3542                            hash_map::Entry::Occupied(e) => e.into_mut(),
3543                            hash_map::Entry::Vacant(e) => {
3544                                // The first time a worktree's root entry becomes available,
3545                                // mark that root entry as expanded.
3546                                if let Some(entry) = worktree_snapshot.root_entry() {
3547                                    e.insert(vec![entry.id]).as_slice()
3548                                } else {
3549                                    &[]
3550                                }
3551                            }
3552                        };
3553
3554                        let mut new_entry_parent_id = None;
3555                        let mut new_entry_kind = EntryKind::Dir;
3556                        if let Some(edit_state) = &new_state.edit_state
3557                            && edit_state.worktree_id == worktree_id
3558                            && edit_state.is_new_entry()
3559                        {
3560                            new_entry_parent_id = Some(edit_state.entry_id);
3561                            new_entry_kind = if edit_state.is_dir {
3562                                EntryKind::Dir
3563                            } else {
3564                                EntryKind::File
3565                            };
3566                        }
3567
3568                        let mut visible_worktree_entries = Vec::new();
3569                        let mut entry_iter =
3570                            GitTraversal::new(&repo_snapshots, worktree_snapshot.entries(true, 0));
3571                        let mut auto_folded_ancestors = vec![];
3572                        let worktree_abs_path = worktree_snapshot.abs_path();
3573                        while let Some(entry) = entry_iter.entry() {
3574                            if hide_root && Some(entry.entry) == worktree_snapshot.root_entry() {
3575                                if new_entry_parent_id == Some(entry.id) {
3576                                    visible_worktree_entries.push(Self::create_new_git_entry(
3577                                        entry.entry,
3578                                        entry.git_summary,
3579                                        new_entry_kind,
3580                                    ));
3581                                    new_entry_parent_id = None;
3582                                }
3583                                entry_iter.advance();
3584                                continue;
3585                            }
3586                            if auto_collapse_dirs && entry.kind.is_dir() {
3587                                auto_folded_ancestors.push(entry.id);
3588                                if !new_state.unfolded_dir_ids.contains(&entry.id)
3589                                    && let Some(root_path) = worktree_snapshot.root_entry()
3590                                {
3591                                    let mut child_entries =
3592                                        worktree_snapshot.child_entries(&entry.path);
3593                                    if let Some(child) = child_entries.next()
3594                                        && entry.path != root_path.path
3595                                        && child_entries.next().is_none()
3596                                        && child.kind.is_dir()
3597                                    {
3598                                        entry_iter.advance();
3599
3600                                        continue;
3601                                    }
3602                                }
3603                                let depth = old_ancestors
3604                                    .get(&entry.id)
3605                                    .map(|ancestor| ancestor.current_ancestor_depth)
3606                                    .unwrap_or_default()
3607                                    .min(auto_folded_ancestors.len());
3608                                if let Some(edit_state) = &mut new_state.edit_state
3609                                    && edit_state.entry_id == entry.id
3610                                {
3611                                    edit_state.depth = depth;
3612                                }
3613                                let mut ancestors = std::mem::take(&mut auto_folded_ancestors);
3614                                if ancestors.len() > 1 {
3615                                    ancestors.reverse();
3616                                    new_state.ancestors.insert(
3617                                        entry.id,
3618                                        FoldedAncestors {
3619                                            current_ancestor_depth: depth,
3620                                            ancestors,
3621                                        },
3622                                    );
3623                                }
3624                            }
3625                            auto_folded_ancestors.clear();
3626                            if (!hide_gitignore || !entry.is_ignored)
3627                                && (!hide_hidden || !entry.is_hidden)
3628                            {
3629                                visible_worktree_entries.push(entry.to_owned());
3630                            }
3631                            let precedes_new_entry = if let Some(new_entry_id) = new_entry_parent_id
3632                            {
3633                                entry.id == new_entry_id || {
3634                                    new_state.ancestors.get(&entry.id).is_some_and(|entries| {
3635                                        entries.ancestors.contains(&new_entry_id)
3636                                    })
3637                                }
3638                            } else {
3639                                false
3640                            };
3641                            if precedes_new_entry
3642                                && (!hide_gitignore || !entry.is_ignored)
3643                                && (!hide_hidden || !entry.is_hidden)
3644                            {
3645                                visible_worktree_entries.push(Self::create_new_git_entry(
3646                                    entry.entry,
3647                                    entry.git_summary,
3648                                    new_entry_kind,
3649                                ));
3650                            }
3651
3652                            let (depth, chars) = if Some(entry.entry)
3653                                == worktree_snapshot.root_entry()
3654                            {
3655                                let Some(path_name) = worktree_abs_path.file_name() else {
3656                                    entry_iter.advance();
3657                                    continue;
3658                                };
3659                                let depth = 0;
3660                                (depth, path_name.to_string_lossy().chars().count())
3661                            } else if entry.is_file() {
3662                                let Some(path_name) = entry
3663                                    .path
3664                                    .file_name()
3665                                    .with_context(|| {
3666                                        format!("Non-root entry has no file name: {entry:?}")
3667                                    })
3668                                    .log_err()
3669                                else {
3670                                    continue;
3671                                };
3672                                let depth = entry.path.ancestors().count() - 1;
3673                                (depth, path_name.chars().count())
3674                            } else {
3675                                let path = new_state
3676                                    .ancestors
3677                                    .get(&entry.id)
3678                                    .and_then(|ancestors| {
3679                                        let outermost_ancestor = ancestors.ancestors.last()?;
3680                                        let root_folded_entry = worktree_snapshot
3681                                            .entry_for_id(*outermost_ancestor)?
3682                                            .path
3683                                            .as_ref();
3684                                        entry.path.strip_prefix(root_folded_entry).ok().and_then(
3685                                            |suffix| {
3686                                                Some(
3687                                                    RelPath::unix(root_folded_entry.file_name()?)
3688                                                        .unwrap()
3689                                                        .join(suffix),
3690                                                )
3691                                            },
3692                                        )
3693                                    })
3694                                    .or_else(|| {
3695                                        entry.path.file_name().map(|file_name| {
3696                                            RelPath::unix(file_name).unwrap().into()
3697                                        })
3698                                    })
3699                                    .unwrap_or_else(|| entry.path.clone());
3700                                let depth = path.components().count();
3701                                (depth, path.as_unix_str().chars().count())
3702                            };
3703                            let width_estimate =
3704                                item_width_estimate(depth, chars, entry.canonical_path.is_some());
3705
3706                            match max_width_item.as_mut() {
3707                                Some((id, worktree_id, width)) => {
3708                                    if *width < width_estimate {
3709                                        *id = entry.id;
3710                                        *worktree_id = worktree_snapshot.id();
3711                                        *width = width_estimate;
3712                                    }
3713                                }
3714                                None => {
3715                                    max_width_item =
3716                                        Some((entry.id, worktree_snapshot.id(), width_estimate))
3717                                }
3718                            }
3719
3720                            if expanded_dir_ids.binary_search(&entry.id).is_err()
3721                                && entry_iter.advance_to_sibling()
3722                            {
3723                                continue;
3724                            }
3725                            entry_iter.advance();
3726                        }
3727
3728                        par_sort_worktree_entries_with_mode(
3729                            &mut visible_worktree_entries,
3730                            sort_mode,
3731                        );
3732                        new_state.visible_entries.push(VisibleEntriesForWorktree {
3733                            worktree_id,
3734                            entries: visible_worktree_entries,
3735                            index: OnceCell::new(),
3736                        })
3737                    }
3738                    if let Some((project_entry_id, worktree_id, _)) = max_width_item {
3739                        let mut visited_worktrees_length = 0;
3740                        let index = new_state
3741                            .visible_entries
3742                            .iter()
3743                            .find_map(|visible_entries| {
3744                                if worktree_id == visible_entries.worktree_id {
3745                                    visible_entries
3746                                        .entries
3747                                        .iter()
3748                                        .position(|entry| entry.id == project_entry_id)
3749                                } else {
3750                                    visited_worktrees_length += visible_entries.entries.len();
3751                                    None
3752                                }
3753                            });
3754                        if let Some(index) = index {
3755                            new_state.max_width_item_index = Some(visited_worktrees_length + index);
3756                        }
3757                    }
3758                    new_state
3759                })
3760                .await;
3761            this.update_in(cx, |this, window, cx| {
3762                let current_selection = this.state.selection;
3763                this.state = new_state;
3764                if let Some((worktree_id, entry_id)) = new_selected_entry {
3765                    this.state.selection = Some(SelectedEntry {
3766                        worktree_id,
3767                        entry_id,
3768                    });
3769                } else {
3770                    this.state.selection = current_selection;
3771                }
3772                let elapsed = now.elapsed();
3773                if this.last_reported_update.elapsed() > Duration::from_secs(3600) {
3774                    telemetry::event!(
3775                        "Project Panel Updated",
3776                        elapsed_ms = elapsed.as_millis() as u64,
3777                        worktree_entries = this
3778                            .state
3779                            .visible_entries
3780                            .iter()
3781                            .map(|worktree| worktree.entries.len())
3782                            .sum::<usize>(),
3783                    )
3784                }
3785                if this.update_visible_entries_task.focus_filename_editor {
3786                    this.update_visible_entries_task.focus_filename_editor = false;
3787                    this.filename_editor.update(cx, |editor, cx| {
3788                        window.focus(&editor.focus_handle(cx), cx);
3789                    });
3790                }
3791                if this.update_visible_entries_task.autoscroll {
3792                    this.update_visible_entries_task.autoscroll = false;
3793                    this.autoscroll(cx);
3794                }
3795                cx.notify();
3796            })
3797            .ok();
3798        });
3799
3800        self.update_visible_entries_task = UpdateVisibleEntriesTask {
3801            _visible_entries_task: visible_entries_task,
3802            focus_filename_editor: focus_filename_editor
3803                || self.update_visible_entries_task.focus_filename_editor,
3804            autoscroll: autoscroll || self.update_visible_entries_task.autoscroll,
3805        };
3806    }
3807
3808    fn expand_entry(
3809        &mut self,
3810        worktree_id: WorktreeId,
3811        entry_id: ProjectEntryId,
3812        cx: &mut Context<Self>,
3813    ) {
3814        self.project.update(cx, |project, cx| {
3815            if let Some((worktree, expanded_dir_ids)) = project
3816                .worktree_for_id(worktree_id, cx)
3817                .zip(self.state.expanded_dir_ids.get_mut(&worktree_id))
3818            {
3819                project.expand_entry(worktree_id, entry_id, cx);
3820                let worktree = worktree.read(cx);
3821
3822                if let Some(mut entry) = worktree.entry_for_id(entry_id) {
3823                    loop {
3824                        if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) {
3825                            expanded_dir_ids.insert(ix, entry.id);
3826                        }
3827
3828                        if let Some(parent_entry) =
3829                            entry.path.parent().and_then(|p| worktree.entry_for_path(p))
3830                        {
3831                            entry = parent_entry;
3832                        } else {
3833                            break;
3834                        }
3835                    }
3836                }
3837            }
3838        });
3839    }
3840
3841    fn drop_external_files(
3842        &mut self,
3843        paths: &[PathBuf],
3844        entry_id: ProjectEntryId,
3845        window: &mut Window,
3846        cx: &mut Context<Self>,
3847    ) {
3848        let mut paths: Vec<Arc<Path>> = paths.iter().map(|path| Arc::from(path.clone())).collect();
3849
3850        let open_file_after_drop = paths.len() == 1 && paths[0].is_file();
3851
3852        let Some((target_directory, worktree, fs)) = maybe!({
3853            let project = self.project.read(cx);
3854            let fs = project.fs().clone();
3855            let worktree = project.worktree_for_entry(entry_id, cx)?;
3856            let entry = worktree.read(cx).entry_for_id(entry_id)?;
3857            let path = entry.path.clone();
3858            let target_directory = if entry.is_dir() {
3859                path
3860            } else {
3861                path.parent()?.into()
3862            };
3863            Some((target_directory, worktree, fs))
3864        }) else {
3865            return;
3866        };
3867
3868        let mut paths_to_replace = Vec::new();
3869        for path in &paths {
3870            if let Some(name) = path.file_name()
3871                && let Some(name) = name.to_str()
3872            {
3873                let target_path = target_directory.join(RelPath::unix(name).unwrap());
3874                if worktree.read(cx).entry_for_path(&target_path).is_some() {
3875                    paths_to_replace.push((name.to_string(), path.clone()));
3876                }
3877            }
3878        }
3879
3880        cx.spawn_in(window, async move |this, cx| {
3881            async move {
3882                for (filename, original_path) in &paths_to_replace {
3883                    let prompt_message = format!(
3884                        concat!(
3885                            "A file or folder with name {} ",
3886                            "already exists in the destination folder. ",
3887                            "Do you want to replace it?"
3888                        ),
3889                        filename
3890                    );
3891                    let answer = cx
3892                        .update(|window, cx| {
3893                            window.prompt(
3894                                PromptLevel::Info,
3895                                &prompt_message,
3896                                None,
3897                                &["Replace", "Cancel"],
3898                                cx,
3899                            )
3900                        })?
3901                        .await?;
3902
3903                    if answer == 1
3904                        && let Some(item_idx) = paths.iter().position(|p| p == original_path)
3905                    {
3906                        paths.remove(item_idx);
3907                    }
3908                }
3909
3910                if paths.is_empty() {
3911                    return Ok(());
3912                }
3913
3914                let task = worktree.update(cx, |worktree, cx| {
3915                    worktree.copy_external_entries(target_directory, paths, fs, cx)
3916                });
3917
3918                let opened_entries: Vec<_> = task
3919                    .await
3920                    .with_context(|| "failed to copy external paths")?;
3921                this.update(cx, |this, cx| {
3922                    if open_file_after_drop && !opened_entries.is_empty() {
3923                        let settings = ProjectPanelSettings::get_global(cx);
3924                        if settings.auto_open.should_open_on_drop() {
3925                            this.open_entry(opened_entries[0], true, false, cx);
3926                        }
3927                    }
3928                })
3929            }
3930            .log_err()
3931            .await
3932        })
3933        .detach();
3934    }
3935
3936    fn refresh_drag_cursor_style(
3937        &self,
3938        modifiers: &Modifiers,
3939        window: &mut Window,
3940        cx: &mut Context<Self>,
3941    ) {
3942        if let Some(existing_cursor) = cx.active_drag_cursor_style() {
3943            let new_cursor = if Self::is_copy_modifier_set(modifiers) {
3944                CursorStyle::DragCopy
3945            } else {
3946                CursorStyle::PointingHand
3947            };
3948            if existing_cursor != new_cursor {
3949                cx.set_active_drag_cursor_style(new_cursor, window);
3950            }
3951        }
3952    }
3953
3954    fn is_copy_modifier_set(modifiers: &Modifiers) -> bool {
3955        cfg!(target_os = "macos") && modifiers.alt
3956            || cfg!(not(target_os = "macos")) && modifiers.control
3957    }
3958
3959    fn drag_onto(
3960        &mut self,
3961        selections: &DraggedSelection,
3962        target_entry_id: ProjectEntryId,
3963        is_file: bool,
3964        window: &mut Window,
3965        cx: &mut Context<Self>,
3966    ) {
3967        let resolved_selections = selections
3968            .items()
3969            .map(|entry| SelectedEntry {
3970                entry_id: self.resolve_entry(entry.entry_id),
3971                worktree_id: entry.worktree_id,
3972            })
3973            .collect::<BTreeSet<SelectedEntry>>();
3974        let entries = self.disjoint_entries(resolved_selections, cx);
3975
3976        if Self::is_copy_modifier_set(&window.modifiers()) {
3977            let _ = maybe!({
3978                let project = self.project.read(cx);
3979                let target_worktree = project.worktree_for_entry(target_entry_id, cx)?;
3980                let worktree_id = target_worktree.read(cx).id();
3981                let target_entry = target_worktree
3982                    .read(cx)
3983                    .entry_for_id(target_entry_id)?
3984                    .clone();
3985
3986                let mut copy_tasks = Vec::new();
3987                let mut disambiguation_range = None;
3988                for selection in &entries {
3989                    let (new_path, new_disambiguation_range) = self.create_paste_path(
3990                        selection,
3991                        (target_worktree.clone(), &target_entry),
3992                        cx,
3993                    )?;
3994
3995                    let task = self.project.update(cx, |project, cx| {
3996                        project.copy_entry(selection.entry_id, (worktree_id, new_path).into(), cx)
3997                    });
3998                    copy_tasks.push(task);
3999                    disambiguation_range = new_disambiguation_range.or(disambiguation_range);
4000                }
4001
4002                let item_count = copy_tasks.len();
4003
4004                cx.spawn_in(window, async move |project_panel, cx| {
4005                    let mut last_succeed = None;
4006                    for task in copy_tasks.into_iter() {
4007                        if let Some(Some(entry)) = task.await.log_err() {
4008                            last_succeed = Some(entry.id);
4009                        }
4010                    }
4011                    // update selection
4012                    if let Some(entry_id) = last_succeed {
4013                        project_panel
4014                            .update_in(cx, |project_panel, window, cx| {
4015                                project_panel.state.selection = Some(SelectedEntry {
4016                                    worktree_id,
4017                                    entry_id,
4018                                });
4019
4020                                // if only one entry was dragged and it was disambiguated, open the rename editor
4021                                if item_count == 1 && disambiguation_range.is_some() {
4022                                    project_panel.rename_impl(disambiguation_range, window, cx);
4023                                }
4024                            })
4025                            .ok();
4026                    }
4027                })
4028                .detach();
4029                Some(())
4030            });
4031        } else {
4032            let update_marks = !self.marked_entries.is_empty();
4033            let active_selection = selections.active_selection;
4034
4035            // For folded selections, track the leaf suffix relative to the resolved
4036            // entry so we can refresh it after the move completes.
4037            let (folded_selection_info, folded_selection_entries): (
4038                Vec<(ProjectEntryId, RelPathBuf)>,
4039                HashSet<SelectedEntry>,
4040            ) = {
4041                let project = self.project.read(cx);
4042                let mut info = Vec::new();
4043                let mut folded_entries = HashSet::default();
4044
4045                for selection in selections.items() {
4046                    let resolved_id = self.resolve_entry(selection.entry_id);
4047                    if resolved_id == selection.entry_id {
4048                        continue;
4049                    }
4050                    folded_entries.insert(*selection);
4051                    let Some(source_path) = project.path_for_entry(resolved_id, cx) else {
4052                        continue;
4053                    };
4054                    let Some(leaf_path) = project.path_for_entry(selection.entry_id, cx) else {
4055                        continue;
4056                    };
4057                    let Ok(suffix) = leaf_path.path.strip_prefix(source_path.path.as_ref()) else {
4058                        continue;
4059                    };
4060                    if suffix.as_unix_str().is_empty() {
4061                        continue;
4062                    }
4063
4064                    info.push((resolved_id, suffix.to_rel_path_buf()));
4065                }
4066                (info, folded_entries)
4067            };
4068
4069            // Collect move tasks paired with their source entry ID so we can correlate
4070            // results with folded selections that need refreshing.
4071            let mut move_tasks: Vec<(ProjectEntryId, Task<Result<CreatedEntry>>)> = Vec::new();
4072            for entry in entries {
4073                if let Some(task) = self.move_entry(entry.entry_id, target_entry_id, is_file, cx) {
4074                    move_tasks.push((entry.entry_id, task));
4075                }
4076            }
4077
4078            if move_tasks.is_empty() {
4079                return;
4080            }
4081
4082            if folded_selection_info.is_empty() {
4083                for (_, task) in move_tasks {
4084                    task.detach_and_log_err(cx);
4085                }
4086            } else {
4087                cx.spawn_in(window, async move |project_panel, cx| {
4088                    // Await all move tasks and collect successful results
4089                    let mut move_results: Vec<(ProjectEntryId, Entry)> = Vec::new();
4090                    for (entry_id, task) in move_tasks {
4091                        if let Some(CreatedEntry::Included(new_entry)) = task.await.log_err() {
4092                            move_results.push((entry_id, new_entry));
4093                        }
4094                    }
4095
4096                    if move_results.is_empty() {
4097                        return;
4098                    }
4099
4100                    // For folded selections, we need to refresh the leaf paths (with suffixes)
4101                    // because they may not be indexed yet after the parent directory was moved.
4102                    // First collect the paths to refresh, then refresh them.
4103                    let paths_to_refresh: Vec<(Entity<Worktree>, Arc<RelPath>)> = project_panel
4104                        .update(cx, |project_panel, cx| {
4105                            let project = project_panel.project.read(cx);
4106                            folded_selection_info
4107                                .iter()
4108                                .filter_map(|(resolved_id, suffix)| {
4109                                    let (_, new_entry) =
4110                                        move_results.iter().find(|(id, _)| id == resolved_id)?;
4111                                    let worktree = project.worktree_for_entry(new_entry.id, cx)?;
4112                                    let leaf_path = new_entry.path.join(suffix);
4113                                    Some((worktree, leaf_path))
4114                                })
4115                                .collect()
4116                        })
4117                        .ok()
4118                        .unwrap_or_default();
4119
4120                    let refresh_tasks: Vec<_> = paths_to_refresh
4121                        .into_iter()
4122                        .filter_map(|(worktree, leaf_path)| {
4123                            worktree.update(cx, |worktree, cx| {
4124                                worktree
4125                                    .as_local_mut()
4126                                    .map(|local| local.refresh_entry(leaf_path, None, cx))
4127                            })
4128                        })
4129                        .collect();
4130
4131                    for task in refresh_tasks {
4132                        task.await.log_err();
4133                    }
4134
4135                    if update_marks && !folded_selection_entries.is_empty() {
4136                        project_panel
4137                            .update(cx, |project_panel, cx| {
4138                                project_panel.marked_entries.retain(|entry| {
4139                                    !folded_selection_entries.contains(entry)
4140                                        || *entry == active_selection
4141                                });
4142                                cx.notify();
4143                            })
4144                            .ok();
4145                    }
4146                })
4147                .detach();
4148            }
4149        }
4150    }
4151
4152    fn index_for_entry(
4153        &self,
4154        entry_id: ProjectEntryId,
4155        worktree_id: WorktreeId,
4156    ) -> Option<(usize, usize, usize)> {
4157        let mut total_ix = 0;
4158        for (worktree_ix, visible) in self.state.visible_entries.iter().enumerate() {
4159            if worktree_id != visible.worktree_id {
4160                total_ix += visible.entries.len();
4161                continue;
4162            }
4163
4164            return visible
4165                .entries
4166                .iter()
4167                .enumerate()
4168                .find(|(_, entry)| entry.id == entry_id)
4169                .map(|(ix, _)| (worktree_ix, ix, total_ix + ix));
4170        }
4171        None
4172    }
4173
4174    fn entry_at_index(&self, index: usize) -> Option<(WorktreeId, GitEntryRef<'_>)> {
4175        let mut offset = 0;
4176        for worktree in &self.state.visible_entries {
4177            let current_len = worktree.entries.len();
4178            if index < offset + current_len {
4179                return worktree
4180                    .entries
4181                    .get(index - offset)
4182                    .map(|entry| (worktree.worktree_id, entry.to_ref()));
4183            }
4184            offset += current_len;
4185        }
4186        None
4187    }
4188
4189    fn iter_visible_entries(
4190        &self,
4191        range: Range<usize>,
4192        window: &mut Window,
4193        cx: &mut Context<ProjectPanel>,
4194        mut callback: impl FnMut(
4195            &Entry,
4196            usize,
4197            &HashSet<Arc<RelPath>>,
4198            &mut Window,
4199            &mut Context<ProjectPanel>,
4200        ),
4201    ) {
4202        let mut ix = 0;
4203        for visible in &self.state.visible_entries {
4204            if ix >= range.end {
4205                return;
4206            }
4207
4208            if ix + visible.entries.len() <= range.start {
4209                ix += visible.entries.len();
4210                continue;
4211            }
4212
4213            let end_ix = range.end.min(ix + visible.entries.len());
4214            let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
4215            let entries = visible
4216                .index
4217                .get_or_init(|| visible.entries.iter().map(|e| e.path.clone()).collect());
4218            let base_index = ix + entry_range.start;
4219            for (i, entry) in visible.entries[entry_range].iter().enumerate() {
4220                let global_index = base_index + i;
4221                callback(entry, global_index, entries, window, cx);
4222            }
4223            ix = end_ix;
4224        }
4225    }
4226
4227    fn for_each_visible_entry(
4228        &self,
4229        range: Range<usize>,
4230        window: &mut Window,
4231        cx: &mut Context<ProjectPanel>,
4232        mut callback: impl FnMut(ProjectEntryId, EntryDetails, &mut Window, &mut Context<ProjectPanel>),
4233    ) {
4234        let mut ix = 0;
4235        for visible in &self.state.visible_entries {
4236            if ix >= range.end {
4237                return;
4238            }
4239
4240            if ix + visible.entries.len() <= range.start {
4241                ix += visible.entries.len();
4242                continue;
4243            }
4244
4245            let end_ix = range.end.min(ix + visible.entries.len());
4246            let git_status_setting = {
4247                let settings = ProjectPanelSettings::get_global(cx);
4248                settings.git_status
4249            };
4250            if let Some(worktree) = self
4251                .project
4252                .read(cx)
4253                .worktree_for_id(visible.worktree_id, cx)
4254            {
4255                let snapshot = worktree.read(cx).snapshot();
4256                let root_name = snapshot.root_name();
4257
4258                let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
4259                let entries = visible
4260                    .index
4261                    .get_or_init(|| visible.entries.iter().map(|e| e.path.clone()).collect());
4262                for entry in visible.entries[entry_range].iter() {
4263                    let status = git_status_setting
4264                        .then_some(entry.git_summary)
4265                        .unwrap_or_default();
4266
4267                    let mut details = self.details_for_entry(
4268                        entry,
4269                        visible.worktree_id,
4270                        root_name,
4271                        entries,
4272                        status,
4273                        None,
4274                        window,
4275                        cx,
4276                    );
4277
4278                    if let Some(edit_state) = &self.state.edit_state {
4279                        let is_edited_entry = if edit_state.is_new_entry() {
4280                            entry.id == NEW_ENTRY_ID
4281                        } else {
4282                            entry.id == edit_state.entry_id
4283                                || self.state.ancestors.get(&entry.id).is_some_and(
4284                                    |auto_folded_dirs| {
4285                                        auto_folded_dirs.ancestors.contains(&edit_state.entry_id)
4286                                    },
4287                                )
4288                        };
4289
4290                        if is_edited_entry {
4291                            if let Some(processing_filename) = &edit_state.processing_filename {
4292                                details.is_processing = true;
4293                                if let Some(ancestors) = edit_state
4294                                    .leaf_entry_id
4295                                    .and_then(|entry| self.state.ancestors.get(&entry))
4296                                {
4297                                    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;
4298                                    let all_components = ancestors.ancestors.len();
4299
4300                                    let prefix_components = all_components - position;
4301                                    let suffix_components = position.checked_sub(1);
4302                                    let mut previous_components =
4303                                        Path::new(&details.filename).components();
4304                                    let mut new_path = previous_components
4305                                        .by_ref()
4306                                        .take(prefix_components)
4307                                        .collect::<PathBuf>();
4308                                    if let Some(last_component) =
4309                                        processing_filename.components().next_back()
4310                                    {
4311                                        new_path.push(last_component);
4312                                        previous_components.next();
4313                                    }
4314
4315                                    if suffix_components.is_some() {
4316                                        new_path.push(previous_components);
4317                                    }
4318                                    if let Some(str) = new_path.to_str() {
4319                                        details.filename.clear();
4320                                        details.filename.push_str(str);
4321                                    }
4322                                } else {
4323                                    details.filename.clear();
4324                                    details.filename.push_str(processing_filename.as_unix_str());
4325                                }
4326                            } else {
4327                                if edit_state.is_new_entry() {
4328                                    details.filename.clear();
4329                                }
4330                                details.is_editing = true;
4331                            }
4332                        }
4333                    }
4334
4335                    callback(entry.id, details, window, cx);
4336                }
4337            }
4338            ix = end_ix;
4339        }
4340    }
4341
4342    fn find_entry_in_worktree(
4343        &self,
4344        worktree_id: WorktreeId,
4345        reverse_search: bool,
4346        only_visible_entries: bool,
4347        predicate: impl Fn(GitEntryRef, WorktreeId) -> bool,
4348        cx: &mut Context<Self>,
4349    ) -> Option<GitEntry> {
4350        if only_visible_entries {
4351            let entries = self
4352                .state
4353                .visible_entries
4354                .iter()
4355                .find_map(|visible| {
4356                    if worktree_id == visible.worktree_id {
4357                        Some(&visible.entries)
4358                    } else {
4359                        None
4360                    }
4361                })?
4362                .clone();
4363
4364            return utils::ReversibleIterable::new(entries.iter(), reverse_search)
4365                .find(|ele| predicate(ele.to_ref(), worktree_id))
4366                .cloned();
4367        }
4368
4369        let repo_snapshots = self
4370            .project
4371            .read(cx)
4372            .git_store()
4373            .read(cx)
4374            .repo_snapshots(cx);
4375        let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?;
4376        worktree.read_with(cx, |tree, _| {
4377            utils::ReversibleIterable::new(
4378                GitTraversal::new(&repo_snapshots, tree.entries(true, 0usize)),
4379                reverse_search,
4380            )
4381            .find_single_ended(|ele| predicate(*ele, worktree_id))
4382            .map(|ele| ele.to_owned())
4383        })
4384    }
4385
4386    fn find_entry(
4387        &self,
4388        start: Option<&SelectedEntry>,
4389        reverse_search: bool,
4390        predicate: impl Fn(GitEntryRef, WorktreeId) -> bool,
4391        cx: &mut Context<Self>,
4392    ) -> Option<SelectedEntry> {
4393        let mut worktree_ids: Vec<_> = self
4394            .state
4395            .visible_entries
4396            .iter()
4397            .map(|worktree| worktree.worktree_id)
4398            .collect();
4399        let repo_snapshots = self
4400            .project
4401            .read(cx)
4402            .git_store()
4403            .read(cx)
4404            .repo_snapshots(cx);
4405
4406        let mut last_found: Option<SelectedEntry> = None;
4407
4408        if let Some(start) = start {
4409            let worktree = self
4410                .project
4411                .read(cx)
4412                .worktree_for_id(start.worktree_id, cx)?
4413                .read(cx);
4414
4415            let search = {
4416                let entry = worktree.entry_for_id(start.entry_id)?;
4417                let root_entry = worktree.root_entry()?;
4418                let tree_id = worktree.id();
4419
4420                let mut first_iter = GitTraversal::new(
4421                    &repo_snapshots,
4422                    worktree.traverse_from_path(true, true, true, entry.path.as_ref()),
4423                );
4424
4425                if reverse_search {
4426                    first_iter.next();
4427                }
4428
4429                let first = first_iter
4430                    .enumerate()
4431                    .take_until(|(count, entry)| entry.entry == root_entry && *count != 0usize)
4432                    .map(|(_, entry)| entry)
4433                    .find(|ele| predicate(*ele, tree_id))
4434                    .map(|ele| ele.to_owned());
4435
4436                let second_iter =
4437                    GitTraversal::new(&repo_snapshots, worktree.entries(true, 0usize));
4438
4439                let second = if reverse_search {
4440                    second_iter
4441                        .take_until(|ele| ele.id == start.entry_id)
4442                        .filter(|ele| predicate(*ele, tree_id))
4443                        .last()
4444                        .map(|ele| ele.to_owned())
4445                } else {
4446                    second_iter
4447                        .take_while(|ele| ele.id != start.entry_id)
4448                        .filter(|ele| predicate(*ele, tree_id))
4449                        .last()
4450                        .map(|ele| ele.to_owned())
4451                };
4452
4453                if reverse_search {
4454                    Some((second, first))
4455                } else {
4456                    Some((first, second))
4457                }
4458            };
4459
4460            if let Some((first, second)) = search {
4461                let first = first.map(|entry| SelectedEntry {
4462                    worktree_id: start.worktree_id,
4463                    entry_id: entry.id,
4464                });
4465
4466                let second = second.map(|entry| SelectedEntry {
4467                    worktree_id: start.worktree_id,
4468                    entry_id: entry.id,
4469                });
4470
4471                if first.is_some() {
4472                    return first;
4473                }
4474                last_found = second;
4475
4476                let idx = worktree_ids
4477                    .iter()
4478                    .enumerate()
4479                    .find(|(_, ele)| **ele == start.worktree_id)
4480                    .map(|(idx, _)| idx);
4481
4482                if let Some(idx) = idx {
4483                    worktree_ids.rotate_left(idx + 1usize);
4484                    worktree_ids.pop();
4485                }
4486            }
4487        }
4488
4489        for tree_id in worktree_ids.into_iter() {
4490            if let Some(found) =
4491                self.find_entry_in_worktree(tree_id, reverse_search, false, &predicate, cx)
4492            {
4493                return Some(SelectedEntry {
4494                    worktree_id: tree_id,
4495                    entry_id: found.id,
4496                });
4497            }
4498        }
4499
4500        last_found
4501    }
4502
4503    fn find_visible_entry(
4504        &self,
4505        start: Option<&SelectedEntry>,
4506        reverse_search: bool,
4507        predicate: impl Fn(GitEntryRef, WorktreeId) -> bool,
4508        cx: &mut Context<Self>,
4509    ) -> Option<SelectedEntry> {
4510        let mut worktree_ids: Vec<_> = self
4511            .state
4512            .visible_entries
4513            .iter()
4514            .map(|worktree| worktree.worktree_id)
4515            .collect();
4516
4517        let mut last_found: Option<SelectedEntry> = None;
4518
4519        if let Some(start) = start {
4520            let entries = self
4521                .state
4522                .visible_entries
4523                .iter()
4524                .find(|worktree| worktree.worktree_id == start.worktree_id)
4525                .map(|worktree| &worktree.entries)?;
4526
4527            let mut start_idx = entries
4528                .iter()
4529                .enumerate()
4530                .find(|(_, ele)| ele.id == start.entry_id)
4531                .map(|(idx, _)| idx)?;
4532
4533            if reverse_search {
4534                start_idx = start_idx.saturating_add(1usize);
4535            }
4536
4537            let (left, right) = entries.split_at_checked(start_idx)?;
4538
4539            let (first_iter, second_iter) = if reverse_search {
4540                (
4541                    utils::ReversibleIterable::new(left.iter(), reverse_search),
4542                    utils::ReversibleIterable::new(right.iter(), reverse_search),
4543                )
4544            } else {
4545                (
4546                    utils::ReversibleIterable::new(right.iter(), reverse_search),
4547                    utils::ReversibleIterable::new(left.iter(), reverse_search),
4548                )
4549            };
4550
4551            let first_search = first_iter.find(|ele| predicate(ele.to_ref(), start.worktree_id));
4552            let second_search = second_iter.find(|ele| predicate(ele.to_ref(), start.worktree_id));
4553
4554            if first_search.is_some() {
4555                return first_search.map(|entry| SelectedEntry {
4556                    worktree_id: start.worktree_id,
4557                    entry_id: entry.id,
4558                });
4559            }
4560
4561            last_found = second_search.map(|entry| SelectedEntry {
4562                worktree_id: start.worktree_id,
4563                entry_id: entry.id,
4564            });
4565
4566            let idx = worktree_ids
4567                .iter()
4568                .enumerate()
4569                .find(|(_, ele)| **ele == start.worktree_id)
4570                .map(|(idx, _)| idx);
4571
4572            if let Some(idx) = idx {
4573                worktree_ids.rotate_left(idx + 1usize);
4574                worktree_ids.pop();
4575            }
4576        }
4577
4578        for tree_id in worktree_ids.into_iter() {
4579            if let Some(found) =
4580                self.find_entry_in_worktree(tree_id, reverse_search, true, &predicate, cx)
4581            {
4582                return Some(SelectedEntry {
4583                    worktree_id: tree_id,
4584                    entry_id: found.id,
4585                });
4586            }
4587        }
4588
4589        last_found
4590    }
4591
4592    fn calculate_depth_and_difference(
4593        entry: &Entry,
4594        visible_worktree_entries: &HashSet<Arc<RelPath>>,
4595    ) -> (usize, usize) {
4596        let (depth, difference) = entry
4597            .path
4598            .ancestors()
4599            .skip(1) // Skip the entry itself
4600            .find_map(|ancestor| {
4601                if let Some(parent_entry) = visible_worktree_entries.get(ancestor) {
4602                    let entry_path_components_count = entry.path.components().count();
4603                    let parent_path_components_count = parent_entry.components().count();
4604                    let difference = entry_path_components_count - parent_path_components_count;
4605                    let depth = parent_entry
4606                        .ancestors()
4607                        .skip(1)
4608                        .filter(|ancestor| visible_worktree_entries.contains(*ancestor))
4609                        .count();
4610                    Some((depth + 1, difference))
4611                } else {
4612                    None
4613                }
4614            })
4615            .unwrap_or_else(|| (0, entry.path.components().count()));
4616
4617        (depth, difference)
4618    }
4619
4620    fn highlight_entry_for_external_drag(
4621        &self,
4622        target_entry: &Entry,
4623        target_worktree: &Worktree,
4624    ) -> Option<ProjectEntryId> {
4625        // Always highlight directory or parent directory if it's file
4626        if target_entry.is_dir() {
4627            Some(target_entry.id)
4628        } else {
4629            target_entry
4630                .path
4631                .parent()
4632                .and_then(|parent_path| target_worktree.entry_for_path(parent_path))
4633                .map(|parent_entry| parent_entry.id)
4634        }
4635    }
4636
4637    fn highlight_entry_for_selection_drag(
4638        &self,
4639        target_entry: &Entry,
4640        target_worktree: &Worktree,
4641        drag_state: &DraggedSelection,
4642        cx: &Context<Self>,
4643    ) -> Option<ProjectEntryId> {
4644        let target_parent_path = target_entry.path.parent();
4645
4646        // In case of single item drag, we do not highlight existing
4647        // directory which item belongs too
4648        if drag_state.items().count() == 1
4649            && drag_state.active_selection.worktree_id == target_worktree.id()
4650        {
4651            let active_entry_path = self
4652                .project
4653                .read(cx)
4654                .path_for_entry(drag_state.active_selection.entry_id, cx)?;
4655
4656            if let Some(active_parent_path) = active_entry_path.path.parent() {
4657                // Do not highlight active entry parent
4658                if active_parent_path == target_entry.path.as_ref() {
4659                    return None;
4660                }
4661
4662                // Do not highlight active entry sibling files
4663                if Some(active_parent_path) == target_parent_path && target_entry.is_file() {
4664                    return None;
4665                }
4666            }
4667        }
4668
4669        // Always highlight directory or parent directory if it's file
4670        if target_entry.is_dir() {
4671            Some(target_entry.id)
4672        } else {
4673            target_parent_path
4674                .and_then(|parent_path| target_worktree.entry_for_path(parent_path))
4675                .map(|parent_entry| parent_entry.id)
4676        }
4677    }
4678
4679    fn should_highlight_background_for_selection_drag(
4680        &self,
4681        drag_state: &DraggedSelection,
4682        last_root_id: ProjectEntryId,
4683        cx: &App,
4684    ) -> bool {
4685        // Always highlight for multiple entries
4686        if drag_state.items().count() > 1 {
4687            return true;
4688        }
4689
4690        // Since root will always have empty relative path
4691        if let Some(entry_path) = self
4692            .project
4693            .read(cx)
4694            .path_for_entry(drag_state.active_selection.entry_id, cx)
4695        {
4696            if let Some(parent_path) = entry_path.path.parent() {
4697                if !parent_path.is_empty() {
4698                    return true;
4699                }
4700            }
4701        }
4702
4703        // If parent is empty, check if different worktree
4704        if let Some(last_root_worktree_id) = self
4705            .project
4706            .read(cx)
4707            .worktree_id_for_entry(last_root_id, cx)
4708        {
4709            if drag_state.active_selection.worktree_id != last_root_worktree_id {
4710                return true;
4711            }
4712        }
4713
4714        false
4715    }
4716
4717    fn render_entry(
4718        &self,
4719        entry_id: ProjectEntryId,
4720        details: EntryDetails,
4721        window: &mut Window,
4722        cx: &mut Context<Self>,
4723    ) -> Stateful<Div> {
4724        const GROUP_NAME: &str = "project_entry";
4725
4726        let kind = details.kind;
4727        let is_sticky = details.sticky.is_some();
4728        let sticky_index = details.sticky.as_ref().map(|this| this.sticky_index);
4729        let settings = ProjectPanelSettings::get_global(cx);
4730        let show_editor = details.is_editing && !details.is_processing;
4731
4732        let selection = SelectedEntry {
4733            worktree_id: details.worktree_id,
4734            entry_id,
4735        };
4736
4737        let is_marked = self.marked_entries.contains(&selection);
4738        let is_active = self
4739            .state
4740            .selection
4741            .is_some_and(|selection| selection.entry_id == entry_id);
4742
4743        let file_name = details.filename.clone();
4744
4745        let mut icon = details.icon.clone();
4746        if settings.file_icons && show_editor && details.kind.is_file() {
4747            let filename = self.filename_editor.read(cx).text(cx);
4748            if filename.len() > 2 {
4749                icon = FileIcons::get_icon(Path::new(&filename), cx);
4750            }
4751        }
4752
4753        let filename_text_color = details.filename_text_color;
4754        let diagnostic_severity = details.diagnostic_severity;
4755        let item_colors = get_item_color(is_sticky, cx);
4756
4757        let canonical_path = details
4758            .canonical_path
4759            .as_ref()
4760            .map(|f| f.to_string_lossy().into_owned());
4761        let path_style = self.project.read(cx).path_style(cx);
4762        let path = details.path.clone();
4763        let path_for_external_paths = path.clone();
4764        let path_for_dragged_selection = path.clone();
4765
4766        let depth = details.depth;
4767        let worktree_id = details.worktree_id;
4768        let dragged_selection = DraggedSelection {
4769            active_selection: SelectedEntry {
4770                worktree_id: selection.worktree_id,
4771                entry_id: selection.entry_id,
4772            },
4773            marked_selections: Arc::from(self.marked_entries.clone()),
4774        };
4775
4776        let bg_color = if is_marked {
4777            item_colors.marked
4778        } else {
4779            item_colors.default
4780        };
4781
4782        let bg_hover_color = if is_marked {
4783            item_colors.marked
4784        } else {
4785            item_colors.hover
4786        };
4787
4788        let validation_color_and_message = if show_editor {
4789            match self
4790                .state
4791                .edit_state
4792                .as_ref()
4793                .map_or(ValidationState::None, |e| e.validation_state.clone())
4794            {
4795                ValidationState::Error(msg) => Some((Color::Error.color(cx), msg)),
4796                ValidationState::Warning(msg) => Some((Color::Warning.color(cx), msg)),
4797                ValidationState::None => None,
4798            }
4799        } else {
4800            None
4801        };
4802
4803        let border_color =
4804            if !self.mouse_down && is_active && self.focus_handle.contains_focused(window, cx) {
4805                match validation_color_and_message {
4806                    Some((color, _)) => color,
4807                    None => item_colors.focused,
4808                }
4809            } else {
4810                bg_color
4811            };
4812
4813        let border_hover_color =
4814            if !self.mouse_down && is_active && self.focus_handle.contains_focused(window, cx) {
4815                match validation_color_and_message {
4816                    Some((color, _)) => color,
4817                    None => item_colors.focused,
4818                }
4819            } else {
4820                bg_hover_color
4821            };
4822
4823        let folded_directory_drag_target = self.folded_directory_drag_target;
4824        let is_highlighted = {
4825            if let Some(highlight_entry_id) =
4826                self.drag_target_entry
4827                    .as_ref()
4828                    .and_then(|drag_target| match drag_target {
4829                        DragTarget::Entry {
4830                            highlight_entry_id, ..
4831                        } => Some(*highlight_entry_id),
4832                        DragTarget::Background => self.state.last_worktree_root_id,
4833                    })
4834            {
4835                // Highlight if same entry or it's children
4836                if entry_id == highlight_entry_id {
4837                    true
4838                } else {
4839                    maybe!({
4840                        let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?;
4841                        let highlight_entry = worktree.read(cx).entry_for_id(highlight_entry_id)?;
4842                        Some(path.starts_with(&highlight_entry.path))
4843                    })
4844                    .unwrap_or(false)
4845                }
4846            } else {
4847                false
4848            }
4849        };
4850
4851        let id: ElementId = if is_sticky {
4852            SharedString::from(format!("project_panel_sticky_item_{}", entry_id.to_usize())).into()
4853        } else {
4854            (entry_id.to_proto() as usize).into()
4855        };
4856
4857        div()
4858            .id(id.clone())
4859            .relative()
4860            .group(GROUP_NAME)
4861            .cursor_pointer()
4862            .rounded_none()
4863            .bg(bg_color)
4864            .border_1()
4865            .border_r_2()
4866            .border_color(border_color)
4867            .hover(|style| style.bg(bg_hover_color).border_color(border_hover_color))
4868            .when(is_sticky, |this| this.block_mouse_except_scroll())
4869            .when(!is_sticky, |this| {
4870                this.when(
4871                    is_highlighted && folded_directory_drag_target.is_none(),
4872                    |this| {
4873                        this.border_color(transparent_white())
4874                            .bg(item_colors.drag_over)
4875                    },
4876                )
4877                .when(settings.drag_and_drop, |this| {
4878                    this.on_drag_move::<ExternalPaths>(cx.listener(
4879                        move |this, event: &DragMoveEvent<ExternalPaths>, _, cx| {
4880                            let is_current_target =
4881                                this.drag_target_entry
4882                                    .as_ref()
4883                                    .and_then(|entry| match entry {
4884                                        DragTarget::Entry {
4885                                            entry_id: target_id,
4886                                            ..
4887                                        } => Some(*target_id),
4888                                        DragTarget::Background { .. } => None,
4889                                    })
4890                                    == Some(entry_id);
4891
4892                            if !event.bounds.contains(&event.event.position) {
4893                                // Entry responsible for setting drag target is also responsible to
4894                                // clear it up after drag is out of bounds
4895                                if is_current_target {
4896                                    this.drag_target_entry = None;
4897                                }
4898                                return;
4899                            }
4900
4901                            if is_current_target {
4902                                return;
4903                            }
4904
4905                            this.marked_entries.clear();
4906
4907                            let Some((entry_id, highlight_entry_id)) = maybe!({
4908                                let target_worktree = this
4909                                    .project
4910                                    .read(cx)
4911                                    .worktree_for_id(selection.worktree_id, cx)?
4912                                    .read(cx);
4913                                let target_entry =
4914                                    target_worktree.entry_for_path(&path_for_external_paths)?;
4915                                let highlight_entry_id = this.highlight_entry_for_external_drag(
4916                                    target_entry,
4917                                    target_worktree,
4918                                )?;
4919                                Some((target_entry.id, highlight_entry_id))
4920                            }) else {
4921                                return;
4922                            };
4923
4924                            this.drag_target_entry = Some(DragTarget::Entry {
4925                                entry_id,
4926                                highlight_entry_id,
4927                            });
4928                        },
4929                    ))
4930                    .on_drop(cx.listener(
4931                        move |this, external_paths: &ExternalPaths, window, cx| {
4932                            this.drag_target_entry = None;
4933                            this.hover_scroll_task.take();
4934                            this.drop_external_files(external_paths.paths(), entry_id, window, cx);
4935                            cx.stop_propagation();
4936                        },
4937                    ))
4938                    .on_drag_move::<DraggedSelection>(cx.listener(
4939                        move |this, event: &DragMoveEvent<DraggedSelection>, window, cx| {
4940                            let is_current_target =
4941                                this.drag_target_entry
4942                                    .as_ref()
4943                                    .and_then(|entry| match entry {
4944                                        DragTarget::Entry {
4945                                            entry_id: target_id,
4946                                            ..
4947                                        } => Some(*target_id),
4948                                        DragTarget::Background { .. } => None,
4949                                    })
4950                                    == Some(entry_id);
4951
4952                            if !event.bounds.contains(&event.event.position) {
4953                                // Entry responsible for setting drag target is also responsible to
4954                                // clear it up after drag is out of bounds
4955                                if is_current_target {
4956                                    this.drag_target_entry = None;
4957                                }
4958                                return;
4959                            }
4960
4961                            if is_current_target {
4962                                return;
4963                            }
4964
4965                            let drag_state = event.drag(cx);
4966
4967                            if drag_state.items().count() == 1 {
4968                                this.marked_entries.clear();
4969                                this.marked_entries.push(drag_state.active_selection);
4970                            }
4971
4972                            let Some((entry_id, highlight_entry_id)) = maybe!({
4973                                let target_worktree = this
4974                                    .project
4975                                    .read(cx)
4976                                    .worktree_for_id(selection.worktree_id, cx)?
4977                                    .read(cx);
4978                                let target_entry =
4979                                    target_worktree.entry_for_path(&path_for_dragged_selection)?;
4980                                let highlight_entry_id = this.highlight_entry_for_selection_drag(
4981                                    target_entry,
4982                                    target_worktree,
4983                                    drag_state,
4984                                    cx,
4985                                )?;
4986                                Some((target_entry.id, highlight_entry_id))
4987                            }) else {
4988                                return;
4989                            };
4990
4991                            this.drag_target_entry = Some(DragTarget::Entry {
4992                                entry_id,
4993                                highlight_entry_id,
4994                            });
4995
4996                            this.hover_expand_task.take();
4997
4998                            if !kind.is_dir()
4999                                || this
5000                                    .state
5001                                    .expanded_dir_ids
5002                                    .get(&details.worktree_id)
5003                                    .is_some_and(|ids| ids.binary_search(&entry_id).is_ok())
5004                            {
5005                                return;
5006                            }
5007
5008                            let bounds = event.bounds;
5009                            this.hover_expand_task =
5010                                Some(cx.spawn_in(window, async move |this, cx| {
5011                                    cx.background_executor()
5012                                        .timer(Duration::from_millis(500))
5013                                        .await;
5014                                    this.update_in(cx, |this, window, cx| {
5015                                        this.hover_expand_task.take();
5016                                        if this.drag_target_entry.as_ref().and_then(|entry| {
5017                                            match entry {
5018                                                DragTarget::Entry {
5019                                                    entry_id: target_id,
5020                                                    ..
5021                                                } => Some(*target_id),
5022                                                DragTarget::Background { .. } => None,
5023                                            }
5024                                        }) == Some(entry_id)
5025                                            && bounds.contains(&window.mouse_position())
5026                                        {
5027                                            this.expand_entry(worktree_id, entry_id, cx);
5028                                            this.update_visible_entries(
5029                                                Some((worktree_id, entry_id)),
5030                                                false,
5031                                                false,
5032                                                window,
5033                                                cx,
5034                                            );
5035                                            cx.notify();
5036                                        }
5037                                    })
5038                                    .ok();
5039                                }));
5040                        },
5041                    ))
5042                    .on_drag(dragged_selection, {
5043                        let active_component =
5044                            self.state.ancestors.get(&entry_id).and_then(|ancestors| {
5045                                ancestors.active_component(&details.filename)
5046                            });
5047                        move |selection, click_offset, _window, cx| {
5048                            let filename = active_component
5049                                .as_ref()
5050                                .unwrap_or_else(|| &details.filename);
5051                            cx.new(|_| DraggedProjectEntryView {
5052                                icon: details.icon.clone(),
5053                                filename: filename.clone(),
5054                                click_offset,
5055                                selection: selection.active_selection,
5056                                selections: selection.marked_selections.clone(),
5057                            })
5058                        }
5059                    })
5060                    .on_drop(cx.listener(
5061                        move |this, selections: &DraggedSelection, window, cx| {
5062                            this.drag_target_entry = None;
5063                            this.hover_scroll_task.take();
5064                            this.hover_expand_task.take();
5065                            if folded_directory_drag_target.is_some() {
5066                                return;
5067                            }
5068                            this.drag_onto(selections, entry_id, kind.is_file(), window, cx);
5069                        },
5070                    ))
5071                })
5072            })
5073            .on_mouse_down(
5074                MouseButton::Left,
5075                cx.listener(move |this, _, _, cx| {
5076                    this.mouse_down = true;
5077                    cx.propagate();
5078                }),
5079            )
5080            .on_click(
5081                cx.listener(move |project_panel, event: &gpui::ClickEvent, window, cx| {
5082                    if event.is_right_click() || event.first_focus() || show_editor {
5083                        return;
5084                    }
5085                    if event.standard_click() {
5086                        project_panel.mouse_down = false;
5087                    }
5088                    cx.stop_propagation();
5089
5090                    if let Some(selection) = project_panel
5091                        .state
5092                        .selection
5093                        .filter(|_| event.modifiers().shift)
5094                    {
5095                        let current_selection = project_panel.index_for_selection(selection);
5096                        let clicked_entry = SelectedEntry {
5097                            entry_id,
5098                            worktree_id,
5099                        };
5100                        let target_selection = project_panel.index_for_selection(clicked_entry);
5101                        if let Some(((_, _, source_index), (_, _, target_index))) =
5102                            current_selection.zip(target_selection)
5103                        {
5104                            let range_start = source_index.min(target_index);
5105                            let range_end = source_index.max(target_index) + 1;
5106                            let mut new_selections = Vec::new();
5107                            project_panel.for_each_visible_entry(
5108                                range_start..range_end,
5109                                window,
5110                                cx,
5111                                |entry_id, details, _, _| {
5112                                    new_selections.push(SelectedEntry {
5113                                        entry_id,
5114                                        worktree_id: details.worktree_id,
5115                                    });
5116                                },
5117                            );
5118
5119                            for selection in &new_selections {
5120                                if !project_panel.marked_entries.contains(selection) {
5121                                    project_panel.marked_entries.push(*selection);
5122                                }
5123                            }
5124
5125                            project_panel.state.selection = Some(clicked_entry);
5126                            if !project_panel.marked_entries.contains(&clicked_entry) {
5127                                project_panel.marked_entries.push(clicked_entry);
5128                            }
5129                        }
5130                    } else if event.modifiers().secondary() {
5131                        if event.click_count() > 1 {
5132                            project_panel.split_entry(entry_id, false, None, cx);
5133                        } else {
5134                            project_panel.state.selection = Some(selection);
5135                            if let Some(position) = project_panel
5136                                .marked_entries
5137                                .iter()
5138                                .position(|e| *e == selection)
5139                            {
5140                                project_panel.marked_entries.remove(position);
5141                            } else {
5142                                project_panel.marked_entries.push(selection);
5143                            }
5144                        }
5145                    } else if kind.is_dir() {
5146                        project_panel.marked_entries.clear();
5147                        if is_sticky
5148                            && let Some((_, _, index)) =
5149                                project_panel.index_for_entry(entry_id, worktree_id)
5150                        {
5151                            project_panel
5152                                .scroll_handle
5153                                .scroll_to_item_strict_with_offset(
5154                                    index,
5155                                    ScrollStrategy::Top,
5156                                    sticky_index.unwrap_or(0),
5157                                );
5158                            cx.notify();
5159                            // move down by 1px so that clicked item
5160                            // don't count as sticky anymore
5161                            cx.on_next_frame(window, |_, window, cx| {
5162                                cx.on_next_frame(window, |this, _, cx| {
5163                                    let mut offset = this.scroll_handle.offset();
5164                                    offset.y += px(1.);
5165                                    this.scroll_handle.set_offset(offset);
5166                                    cx.notify();
5167                                });
5168                            });
5169                            return;
5170                        }
5171                        if event.modifiers().alt {
5172                            project_panel.toggle_expand_all(entry_id, window, cx);
5173                        } else {
5174                            project_panel.toggle_expanded(entry_id, window, cx);
5175                        }
5176                    } else {
5177                        let preview_tabs_enabled =
5178                            PreviewTabsSettings::get_global(cx).enable_preview_from_project_panel;
5179                        let click_count = event.click_count();
5180                        let focus_opened_item = click_count > 1;
5181                        let allow_preview = preview_tabs_enabled && click_count == 1;
5182                        project_panel.open_entry(entry_id, focus_opened_item, allow_preview, cx);
5183                    }
5184                }),
5185            )
5186            .child(
5187                ListItem::new(id)
5188                    .indent_level(depth)
5189                    .indent_step_size(px(settings.indent_size))
5190                    .spacing(match settings.entry_spacing {
5191                        ProjectPanelEntrySpacing::Comfortable => ListItemSpacing::Dense,
5192                        ProjectPanelEntrySpacing::Standard => ListItemSpacing::ExtraDense,
5193                    })
5194                    .selectable(false)
5195                    .when_some(canonical_path, |this, path| {
5196                        this.end_slot::<AnyElement>(
5197                            div()
5198                                .id("symlink_icon")
5199                                .pr_3()
5200                                .tooltip(move |_window, cx| {
5201                                    Tooltip::with_meta(path.to_string(), None, "Symbolic Link", cx)
5202                                })
5203                                .child(
5204                                    Icon::new(IconName::ArrowUpRight)
5205                                        .size(IconSize::Indicator)
5206                                        .color(filename_text_color),
5207                                )
5208                                .into_any_element(),
5209                        )
5210                    })
5211                    .child(if let Some(icon) = &icon {
5212                        if let Some((_, decoration_color)) =
5213                            entry_diagnostic_aware_icon_decoration_and_color(diagnostic_severity)
5214                        {
5215                            let is_warning = diagnostic_severity
5216                                .map(|severity| matches!(severity, DiagnosticSeverity::WARNING))
5217                                .unwrap_or(false);
5218                            div().child(
5219                                DecoratedIcon::new(
5220                                    Icon::from_path(icon.clone()).color(Color::Muted),
5221                                    Some(
5222                                        IconDecoration::new(
5223                                            if kind.is_file() {
5224                                                if is_warning {
5225                                                    IconDecorationKind::Triangle
5226                                                } else {
5227                                                    IconDecorationKind::X
5228                                                }
5229                                            } else {
5230                                                IconDecorationKind::Dot
5231                                            },
5232                                            bg_color,
5233                                            cx,
5234                                        )
5235                                        .group_name(Some(GROUP_NAME.into()))
5236                                        .knockout_hover_color(bg_hover_color)
5237                                        .color(decoration_color.color(cx))
5238                                        .position(Point {
5239                                            x: px(-2.),
5240                                            y: px(-2.),
5241                                        }),
5242                                    ),
5243                                )
5244                                .into_any_element(),
5245                            )
5246                        } else {
5247                            h_flex().child(Icon::from_path(icon.to_string()).color(Color::Muted))
5248                        }
5249                    } else if let Some((icon_name, color)) =
5250                        entry_diagnostic_aware_icon_name_and_color(diagnostic_severity)
5251                    {
5252                        h_flex()
5253                            .size(IconSize::default().rems())
5254                            .child(Icon::new(icon_name).color(color).size(IconSize::Small))
5255                    } else {
5256                        h_flex()
5257                            .size(IconSize::default().rems())
5258                            .invisible()
5259                            .flex_none()
5260                    })
5261                    .child(if show_editor {
5262                        h_flex().h_6().w_full().child(self.filename_editor.clone())
5263                    } else {
5264                        h_flex()
5265                            .h_6()
5266                            .map(|this| match self.state.ancestors.get(&entry_id) {
5267                                Some(folded_ancestors) => {
5268                                    this.children(self.render_folder_elements(
5269                                        folded_ancestors,
5270                                        entry_id,
5271                                        file_name,
5272                                        path_style,
5273                                        is_sticky,
5274                                        kind.is_file(),
5275                                        is_active || is_marked,
5276                                        settings.drag_and_drop,
5277                                        item_colors.drag_over,
5278                                        folded_directory_drag_target,
5279                                        filename_text_color,
5280                                        cx,
5281                                    ))
5282                                }
5283
5284                                None => this.child(
5285                                    Label::new(file_name)
5286                                        .single_line()
5287                                        .color(filename_text_color)
5288                                        .into_any_element(),
5289                                ),
5290                            })
5291                    })
5292                    .on_secondary_mouse_down(cx.listener(
5293                        move |this, event: &MouseDownEvent, window, cx| {
5294                            // Stop propagation to prevent the catch-all context menu for the project
5295                            // panel from being deployed.
5296                            cx.stop_propagation();
5297                            // Some context menu actions apply to all marked entries. If the user
5298                            // right-clicks on an entry that is not marked, they may not realize the
5299                            // action applies to multiple entries. To avoid inadvertent changes, all
5300                            // entries are unmarked.
5301                            if !this.marked_entries.contains(&selection) {
5302                                this.marked_entries.clear();
5303                            }
5304                            this.deploy_context_menu(event.position, entry_id, window, cx);
5305                        },
5306                    ))
5307                    .overflow_x(),
5308            )
5309            .when_some(validation_color_and_message, |this, (color, message)| {
5310                this.relative().child(deferred(
5311                    div()
5312                        .occlude()
5313                        .absolute()
5314                        .top_full()
5315                        .left(px(-1.)) // Used px over rem so that it doesn't change with font size
5316                        .right(px(-0.5))
5317                        .py_1()
5318                        .px_2()
5319                        .border_1()
5320                        .border_color(color)
5321                        .bg(cx.theme().colors().background)
5322                        .child(
5323                            Label::new(message)
5324                                .color(Color::from(color))
5325                                .size(LabelSize::Small),
5326                        ),
5327                ))
5328            })
5329    }
5330
5331    fn render_folder_elements(
5332        &self,
5333        folded_ancestors: &FoldedAncestors,
5334        entry_id: ProjectEntryId,
5335        file_name: String,
5336        path_style: PathStyle,
5337        is_sticky: bool,
5338        is_file: bool,
5339        is_active_or_marked: bool,
5340        drag_and_drop_enabled: bool,
5341        drag_over_color: Hsla,
5342        folded_directory_drag_target: Option<FoldedDirectoryDragTarget>,
5343        filename_text_color: Color,
5344        cx: &Context<Self>,
5345    ) -> impl Iterator<Item = AnyElement> {
5346        let components = Path::new(&file_name)
5347            .components()
5348            .map(|comp| comp.as_os_str().to_string_lossy().into_owned())
5349            .collect::<Vec<_>>();
5350        let active_index = folded_ancestors.active_index();
5351        let components_len = components.len();
5352        let delimiter = SharedString::new(path_style.primary_separator());
5353
5354        let path_component_elements =
5355            components
5356                .into_iter()
5357                .enumerate()
5358                .map(move |(index, component)| {
5359                    div()
5360                        .id(SharedString::from(format!(
5361                            "project_panel_path_component_{}_{index}",
5362                            entry_id.to_usize()
5363                        )))
5364                        .when(index == 0, |this| this.ml_neg_0p5())
5365                        .px_0p5()
5366                        .rounded_xs()
5367                        .hover(|style| style.bg(cx.theme().colors().element_active))
5368                        .when(!is_sticky, |div| {
5369                            div.when(index != components_len - 1, |div| {
5370                                let target_entry_id = folded_ancestors
5371                                    .ancestors
5372                                    .get(components_len - 1 - index)
5373                                    .cloned();
5374                                div.when(drag_and_drop_enabled, |div| {
5375                                    div.on_drag_move(cx.listener(
5376                                        move |this,
5377                                              event: &DragMoveEvent<DraggedSelection>,
5378                                              _,
5379                                              _| {
5380                                            if event.bounds.contains(&event.event.position) {
5381                                                this.folded_directory_drag_target =
5382                                                    Some(FoldedDirectoryDragTarget {
5383                                                        entry_id,
5384                                                        index,
5385                                                        is_delimiter_target: false,
5386                                                    });
5387                                            } else {
5388                                                let is_current_target = this
5389                                                    .folded_directory_drag_target
5390                                                    .as_ref()
5391                                                    .is_some_and(|target| {
5392                                                        target.entry_id == entry_id
5393                                                            && target.index == index
5394                                                            && !target.is_delimiter_target
5395                                                    });
5396                                                if is_current_target {
5397                                                    this.folded_directory_drag_target = None;
5398                                                }
5399                                            }
5400                                        },
5401                                    ))
5402                                    .on_drop(cx.listener(
5403                                        move |this, selections: &DraggedSelection, window, cx| {
5404                                            this.hover_scroll_task.take();
5405                                            this.drag_target_entry = None;
5406                                            this.folded_directory_drag_target = None;
5407                                            if let Some(target_entry_id) = target_entry_id {
5408                                                this.drag_onto(
5409                                                    selections,
5410                                                    target_entry_id,
5411                                                    is_file,
5412                                                    window,
5413                                                    cx,
5414                                                );
5415                                            }
5416                                        },
5417                                    ))
5418                                    .when(
5419                                        folded_directory_drag_target.is_some_and(|target| {
5420                                            target.entry_id == entry_id && target.index == index
5421                                        }),
5422                                        |this| this.bg(drag_over_color),
5423                                    )
5424                                })
5425                            })
5426                        })
5427                        .on_mouse_down(
5428                            MouseButton::Left,
5429                            cx.listener(move |this, _, _, cx| {
5430                                if let Some(folds) = this.state.ancestors.get_mut(&entry_id) {
5431                                    if folds.set_active_index(index) {
5432                                        cx.notify();
5433                                    }
5434                                }
5435                            }),
5436                        )
5437                        .on_mouse_down(
5438                            MouseButton::Right,
5439                            cx.listener(move |this, _, _, cx| {
5440                                if let Some(folds) = this.state.ancestors.get_mut(&entry_id) {
5441                                    if folds.set_active_index(index) {
5442                                        cx.notify();
5443                                    }
5444                                }
5445                            }),
5446                        )
5447                        .child(
5448                            Label::new(component)
5449                                .single_line()
5450                                .color(filename_text_color)
5451                                .when(index == active_index && is_active_or_marked, |this| {
5452                                    this.underline()
5453                                }),
5454                        )
5455                        .into_any()
5456                });
5457
5458        let mut separator_index = 0;
5459        itertools::intersperse_with(path_component_elements, move || {
5460            separator_index += 1;
5461            self.render_entry_path_separator(
5462                entry_id,
5463                separator_index,
5464                components_len,
5465                is_sticky,
5466                is_file,
5467                drag_and_drop_enabled,
5468                filename_text_color,
5469                &delimiter,
5470                folded_ancestors,
5471                cx,
5472            )
5473            .into_any()
5474        })
5475    }
5476
5477    fn render_entry_path_separator(
5478        &self,
5479        entry_id: ProjectEntryId,
5480        index: usize,
5481        components_len: usize,
5482        is_sticky: bool,
5483        is_file: bool,
5484        drag_and_drop_enabled: bool,
5485        filename_text_color: Color,
5486        delimiter: &SharedString,
5487        folded_ancestors: &FoldedAncestors,
5488        cx: &Context<Self>,
5489    ) -> Div {
5490        let delimiter_target_index = index - 1;
5491        let target_entry_id = folded_ancestors
5492            .ancestors
5493            .get(components_len - 1 - delimiter_target_index)
5494            .cloned();
5495        div()
5496            .when(!is_sticky, |div| {
5497                div.when(drag_and_drop_enabled, |div| {
5498                    div.on_drop(cx.listener(
5499                        move |this, selections: &DraggedSelection, window, cx| {
5500                            this.hover_scroll_task.take();
5501                            this.drag_target_entry = None;
5502                            this.folded_directory_drag_target = None;
5503                            if let Some(target_entry_id) = target_entry_id {
5504                                this.drag_onto(selections, target_entry_id, is_file, window, cx);
5505                            }
5506                        },
5507                    ))
5508                    .on_drag_move(cx.listener(
5509                        move |this, event: &DragMoveEvent<DraggedSelection>, _, _| {
5510                            if event.bounds.contains(&event.event.position) {
5511                                this.folded_directory_drag_target =
5512                                    Some(FoldedDirectoryDragTarget {
5513                                        entry_id,
5514                                        index: delimiter_target_index,
5515                                        is_delimiter_target: true,
5516                                    });
5517                            } else {
5518                                let is_current_target =
5519                                    this.folded_directory_drag_target.is_some_and(|target| {
5520                                        target.entry_id == entry_id
5521                                            && target.index == delimiter_target_index
5522                                            && target.is_delimiter_target
5523                                    });
5524                                if is_current_target {
5525                                    this.folded_directory_drag_target = None;
5526                                }
5527                            }
5528                        },
5529                    ))
5530                })
5531            })
5532            .child(
5533                Label::new(delimiter.clone())
5534                    .single_line()
5535                    .color(filename_text_color),
5536            )
5537    }
5538
5539    fn details_for_entry(
5540        &self,
5541        entry: &Entry,
5542        worktree_id: WorktreeId,
5543        root_name: &RelPath,
5544        entries_paths: &HashSet<Arc<RelPath>>,
5545        git_status: GitSummary,
5546        sticky: Option<StickyDetails>,
5547        _window: &mut Window,
5548        cx: &mut Context<Self>,
5549    ) -> EntryDetails {
5550        let (show_file_icons, show_folder_icons) = {
5551            let settings = ProjectPanelSettings::get_global(cx);
5552            (settings.file_icons, settings.folder_icons)
5553        };
5554
5555        let expanded_entry_ids = self
5556            .state
5557            .expanded_dir_ids
5558            .get(&worktree_id)
5559            .map(Vec::as_slice)
5560            .unwrap_or(&[]);
5561        let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok();
5562
5563        let icon = match entry.kind {
5564            EntryKind::File => {
5565                if show_file_icons {
5566                    FileIcons::get_icon(entry.path.as_std_path(), cx)
5567                } else {
5568                    None
5569                }
5570            }
5571            _ => {
5572                if show_folder_icons {
5573                    FileIcons::get_folder_icon(is_expanded, entry.path.as_std_path(), cx)
5574                } else {
5575                    FileIcons::get_chevron_icon(is_expanded, cx)
5576                }
5577            }
5578        };
5579
5580        let path_style = self.project.read(cx).path_style(cx);
5581        let (depth, difference) =
5582            ProjectPanel::calculate_depth_and_difference(entry, entries_paths);
5583
5584        let filename = if difference > 1 {
5585            entry
5586                .path
5587                .last_n_components(difference)
5588                .map_or(String::new(), |suffix| {
5589                    suffix.display(path_style).to_string()
5590                })
5591        } else {
5592            entry
5593                .path
5594                .file_name()
5595                .map(|name| name.to_string())
5596                .unwrap_or_else(|| root_name.as_unix_str().to_string())
5597        };
5598
5599        let selection = SelectedEntry {
5600            worktree_id,
5601            entry_id: entry.id,
5602        };
5603        let is_marked = self.marked_entries.contains(&selection);
5604        let is_selected = self.state.selection == Some(selection);
5605
5606        let diagnostic_severity = self
5607            .diagnostics
5608            .get(&(worktree_id, entry.path.clone()))
5609            .cloned();
5610
5611        let filename_text_color =
5612            entry_git_aware_label_color(git_status, entry.is_ignored, is_marked);
5613
5614        let is_cut = self
5615            .clipboard
5616            .as_ref()
5617            .is_some_and(|e| e.is_cut() && e.items().contains(&selection));
5618
5619        EntryDetails {
5620            filename,
5621            icon,
5622            path: entry.path.clone(),
5623            depth,
5624            kind: entry.kind,
5625            is_ignored: entry.is_ignored,
5626            is_expanded,
5627            is_selected,
5628            is_marked,
5629            is_editing: false,
5630            is_processing: false,
5631            is_cut,
5632            sticky,
5633            filename_text_color,
5634            diagnostic_severity,
5635            git_status,
5636            is_private: entry.is_private,
5637            worktree_id,
5638            canonical_path: entry.canonical_path.clone(),
5639        }
5640    }
5641
5642    fn dispatch_context(&self, window: &Window, cx: &Context<Self>) -> KeyContext {
5643        let mut dispatch_context = KeyContext::new_with_defaults();
5644        dispatch_context.add("ProjectPanel");
5645        dispatch_context.add("menu");
5646
5647        let identifier = if self.filename_editor.focus_handle(cx).is_focused(window) {
5648            "editing"
5649        } else {
5650            "not_editing"
5651        };
5652
5653        dispatch_context.add(identifier);
5654        dispatch_context
5655    }
5656
5657    fn reveal_entry(
5658        &mut self,
5659        project: Entity<Project>,
5660        entry_id: ProjectEntryId,
5661        skip_ignored: bool,
5662        window: &mut Window,
5663        cx: &mut Context<Self>,
5664    ) -> Result<()> {
5665        let worktree = project
5666            .read(cx)
5667            .worktree_for_entry(entry_id, cx)
5668            .context("can't reveal a non-existent entry in the project panel")?;
5669        let worktree = worktree.read(cx);
5670        if skip_ignored
5671            && worktree
5672                .entry_for_id(entry_id)
5673                .is_none_or(|entry| entry.is_ignored && !entry.is_always_included)
5674        {
5675            anyhow::bail!("can't reveal an ignored entry in the project panel");
5676        }
5677        let is_active_item_file_diff_view = self
5678            .workspace
5679            .upgrade()
5680            .and_then(|ws| ws.read(cx).active_item(cx))
5681            .map(|item| item.act_as_type(TypeId::of::<FileDiffView>(), cx).is_some())
5682            .unwrap_or(false);
5683        if is_active_item_file_diff_view {
5684            return Ok(());
5685        }
5686
5687        let worktree_id = worktree.id();
5688        self.expand_entry(worktree_id, entry_id, cx);
5689        self.update_visible_entries(Some((worktree_id, entry_id)), false, true, window, cx);
5690        self.marked_entries.clear();
5691        self.marked_entries.push(SelectedEntry {
5692            worktree_id,
5693            entry_id,
5694        });
5695        cx.notify();
5696        Ok(())
5697    }
5698
5699    fn find_active_indent_guide(
5700        &self,
5701        indent_guides: &[IndentGuideLayout],
5702        cx: &App,
5703    ) -> Option<usize> {
5704        let (worktree, entry) = self.selected_entry(cx)?;
5705
5706        // Find the parent entry of the indent guide, this will either be the
5707        // expanded folder we have selected, or the parent of the currently
5708        // selected file/collapsed directory
5709        let mut entry = entry;
5710        loop {
5711            let is_expanded_dir = entry.is_dir()
5712                && self
5713                    .state
5714                    .expanded_dir_ids
5715                    .get(&worktree.id())
5716                    .map(|ids| ids.binary_search(&entry.id).is_ok())
5717                    .unwrap_or(false);
5718            if is_expanded_dir {
5719                break;
5720            }
5721            entry = worktree.entry_for_path(&entry.path.parent()?)?;
5722        }
5723
5724        let (active_indent_range, depth) = {
5725            let (worktree_ix, child_offset, ix) = self.index_for_entry(entry.id, worktree.id())?;
5726            let child_paths = &self.state.visible_entries[worktree_ix].entries;
5727            let mut child_count = 0;
5728            let depth = entry.path.ancestors().count();
5729            while let Some(entry) = child_paths.get(child_offset + child_count + 1) {
5730                if entry.path.ancestors().count() <= depth {
5731                    break;
5732                }
5733                child_count += 1;
5734            }
5735
5736            let start = ix + 1;
5737            let end = start + child_count;
5738
5739            let visible_worktree = &self.state.visible_entries[worktree_ix];
5740            let visible_worktree_entries = visible_worktree.index.get_or_init(|| {
5741                visible_worktree
5742                    .entries
5743                    .iter()
5744                    .map(|e| e.path.clone())
5745                    .collect()
5746            });
5747
5748            // Calculate the actual depth of the entry, taking into account that directories can be auto-folded.
5749            let (depth, _) = Self::calculate_depth_and_difference(entry, visible_worktree_entries);
5750            (start..end, depth)
5751        };
5752
5753        let candidates = indent_guides
5754            .iter()
5755            .enumerate()
5756            .filter(|(_, indent_guide)| indent_guide.offset.x == depth);
5757
5758        for (i, indent) in candidates {
5759            // Find matches that are either an exact match, partially on screen, or inside the enclosing indent
5760            if active_indent_range.start <= indent.offset.y + indent.length
5761                && indent.offset.y <= active_indent_range.end
5762            {
5763                return Some(i);
5764            }
5765        }
5766        None
5767    }
5768
5769    fn render_sticky_entries(
5770        &self,
5771        child: StickyProjectPanelCandidate,
5772        window: &mut Window,
5773        cx: &mut Context<Self>,
5774    ) -> SmallVec<[AnyElement; 8]> {
5775        let project = self.project.read(cx);
5776
5777        let Some((worktree_id, entry_ref)) = self.entry_at_index(child.index) else {
5778            return SmallVec::new();
5779        };
5780
5781        let Some(visible) = self
5782            .state
5783            .visible_entries
5784            .iter()
5785            .find(|worktree| worktree.worktree_id == worktree_id)
5786        else {
5787            return SmallVec::new();
5788        };
5789
5790        let Some(worktree) = project.worktree_for_id(worktree_id, cx) else {
5791            return SmallVec::new();
5792        };
5793        let worktree = worktree.read(cx).snapshot();
5794
5795        let paths = visible
5796            .index
5797            .get_or_init(|| visible.entries.iter().map(|e| e.path.clone()).collect());
5798
5799        let mut sticky_parents = Vec::new();
5800        let mut current_path = entry_ref.path.clone();
5801
5802        'outer: loop {
5803            if let Some(parent_path) = current_path.parent() {
5804                for ancestor_path in parent_path.ancestors() {
5805                    if paths.contains(ancestor_path)
5806                        && let Some(parent_entry) = worktree.entry_for_path(ancestor_path)
5807                    {
5808                        sticky_parents.push(parent_entry.clone());
5809                        current_path = parent_entry.path.clone();
5810                        continue 'outer;
5811                    }
5812                }
5813            }
5814            break 'outer;
5815        }
5816
5817        if sticky_parents.is_empty() {
5818            return SmallVec::new();
5819        }
5820
5821        sticky_parents.reverse();
5822
5823        let panel_settings = ProjectPanelSettings::get_global(cx);
5824        let git_status_enabled = panel_settings.git_status;
5825        let root_name = worktree.root_name();
5826
5827        let git_summaries_by_id = if git_status_enabled {
5828            visible
5829                .entries
5830                .iter()
5831                .map(|e| (e.id, e.git_summary))
5832                .collect::<HashMap<_, _>>()
5833        } else {
5834            Default::default()
5835        };
5836
5837        // already checked if non empty above
5838        let last_item_index = sticky_parents.len() - 1;
5839        sticky_parents
5840            .iter()
5841            .enumerate()
5842            .map(|(index, entry)| {
5843                let git_status = git_summaries_by_id
5844                    .get(&entry.id)
5845                    .copied()
5846                    .unwrap_or_default();
5847                let sticky_details = Some(StickyDetails {
5848                    sticky_index: index,
5849                });
5850                let details = self.details_for_entry(
5851                    entry,
5852                    worktree_id,
5853                    root_name,
5854                    paths,
5855                    git_status,
5856                    sticky_details,
5857                    window,
5858                    cx,
5859                );
5860                self.render_entry(entry.id, details, window, cx)
5861                    .when(index == last_item_index, |this| {
5862                        let shadow_color_top = hsla(0.0, 0.0, 0.0, 0.1);
5863                        let shadow_color_bottom = hsla(0.0, 0.0, 0.0, 0.);
5864                        let sticky_shadow = div()
5865                            .absolute()
5866                            .left_0()
5867                            .bottom_neg_1p5()
5868                            .h_1p5()
5869                            .w_full()
5870                            .bg(linear_gradient(
5871                                0.,
5872                                linear_color_stop(shadow_color_top, 1.),
5873                                linear_color_stop(shadow_color_bottom, 0.),
5874                            ));
5875                        this.child(sticky_shadow)
5876                    })
5877                    .into_any()
5878            })
5879            .collect()
5880    }
5881}
5882
5883#[derive(Clone)]
5884struct StickyProjectPanelCandidate {
5885    index: usize,
5886    depth: usize,
5887}
5888
5889impl StickyCandidate for StickyProjectPanelCandidate {
5890    fn depth(&self) -> usize {
5891        self.depth
5892    }
5893}
5894
5895fn item_width_estimate(depth: usize, item_text_chars: usize, is_symlink: bool) -> usize {
5896    const ICON_SIZE_FACTOR: usize = 2;
5897    let mut item_width = depth * ICON_SIZE_FACTOR + item_text_chars;
5898    if is_symlink {
5899        item_width += ICON_SIZE_FACTOR;
5900    }
5901    item_width
5902}
5903
5904impl Render for ProjectPanel {
5905    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
5906        let has_worktree = !self.state.visible_entries.is_empty();
5907        let project = self.project.read(cx);
5908        let panel_settings = ProjectPanelSettings::get_global(cx);
5909        let indent_size = panel_settings.indent_size;
5910        let show_indent_guides = panel_settings.indent_guides.show == ShowIndentGuides::Always;
5911        let show_sticky_entries = {
5912            if panel_settings.sticky_scroll {
5913                let is_scrollable = self.scroll_handle.is_scrollable();
5914                let is_scrolled = self.scroll_handle.offset().y < px(0.);
5915                is_scrollable && is_scrolled
5916            } else {
5917                false
5918            }
5919        };
5920
5921        let is_local = project.is_local();
5922
5923        if has_worktree {
5924            let item_count = self
5925                .state
5926                .visible_entries
5927                .iter()
5928                .map(|worktree| worktree.entries.len())
5929                .sum();
5930
5931            fn handle_drag_move<T: 'static>(
5932                this: &mut ProjectPanel,
5933                e: &DragMoveEvent<T>,
5934                window: &mut Window,
5935                cx: &mut Context<ProjectPanel>,
5936            ) {
5937                if let Some(previous_position) = this.previous_drag_position {
5938                    // Refresh cursor only when an actual drag happens,
5939                    // because modifiers are not updated when the cursor is not moved.
5940                    if e.event.position != previous_position {
5941                        this.refresh_drag_cursor_style(&e.event.modifiers, window, cx);
5942                    }
5943                }
5944                this.previous_drag_position = Some(e.event.position);
5945
5946                if !e.bounds.contains(&e.event.position) {
5947                    this.drag_target_entry = None;
5948                    return;
5949                }
5950                this.hover_scroll_task.take();
5951                let panel_height = e.bounds.size.height;
5952                if panel_height <= px(0.) {
5953                    return;
5954                }
5955
5956                let event_offset = e.event.position.y - e.bounds.origin.y;
5957                // How far along in the project panel is our cursor? (0. is the top of a list, 1. is the bottom)
5958                let hovered_region_offset = event_offset / panel_height;
5959
5960                // We want the scrolling to be a bit faster when the cursor is closer to the edge of a list.
5961                // These pixels offsets were picked arbitrarily.
5962                let vertical_scroll_offset = if hovered_region_offset <= 0.05 {
5963                    8.
5964                } else if hovered_region_offset <= 0.15 {
5965                    5.
5966                } else if hovered_region_offset >= 0.95 {
5967                    -8.
5968                } else if hovered_region_offset >= 0.85 {
5969                    -5.
5970                } else {
5971                    return;
5972                };
5973                let adjustment = point(px(0.), px(vertical_scroll_offset));
5974                this.hover_scroll_task = Some(cx.spawn_in(window, async move |this, cx| {
5975                    loop {
5976                        let should_stop_scrolling = this
5977                            .update(cx, |this, cx| {
5978                                this.hover_scroll_task.as_ref()?;
5979                                let handle = this.scroll_handle.0.borrow_mut();
5980                                let offset = handle.base_handle.offset();
5981
5982                                handle.base_handle.set_offset(offset + adjustment);
5983                                cx.notify();
5984                                Some(())
5985                            })
5986                            .ok()
5987                            .flatten()
5988                            .is_some();
5989                        if should_stop_scrolling {
5990                            return;
5991                        }
5992                        cx.background_executor()
5993                            .timer(Duration::from_millis(16))
5994                            .await;
5995                    }
5996                }));
5997            }
5998            h_flex()
5999                .id("project-panel")
6000                .group("project-panel")
6001                .when(panel_settings.drag_and_drop, |this| {
6002                    this.on_drag_move(cx.listener(handle_drag_move::<ExternalPaths>))
6003                        .on_drag_move(cx.listener(handle_drag_move::<DraggedSelection>))
6004                })
6005                .size_full()
6006                .relative()
6007                .on_modifiers_changed(cx.listener(
6008                    |this, event: &ModifiersChangedEvent, window, cx| {
6009                        this.refresh_drag_cursor_style(&event.modifiers, window, cx);
6010                    },
6011                ))
6012                .key_context(self.dispatch_context(window, cx))
6013                .on_action(cx.listener(Self::scroll_up))
6014                .on_action(cx.listener(Self::scroll_down))
6015                .on_action(cx.listener(Self::scroll_cursor_center))
6016                .on_action(cx.listener(Self::scroll_cursor_top))
6017                .on_action(cx.listener(Self::scroll_cursor_bottom))
6018                .on_action(cx.listener(Self::select_next))
6019                .on_action(cx.listener(Self::select_previous))
6020                .on_action(cx.listener(Self::select_first))
6021                .on_action(cx.listener(Self::select_last))
6022                .on_action(cx.listener(Self::select_parent))
6023                .on_action(cx.listener(Self::select_next_git_entry))
6024                .on_action(cx.listener(Self::select_prev_git_entry))
6025                .on_action(cx.listener(Self::select_next_diagnostic))
6026                .on_action(cx.listener(Self::select_prev_diagnostic))
6027                .on_action(cx.listener(Self::select_next_directory))
6028                .on_action(cx.listener(Self::select_prev_directory))
6029                .on_action(cx.listener(Self::expand_selected_entry))
6030                .on_action(cx.listener(Self::collapse_selected_entry))
6031                .on_action(cx.listener(Self::collapse_all_entries))
6032                .on_action(cx.listener(Self::open))
6033                .on_action(cx.listener(Self::open_permanent))
6034                .on_action(cx.listener(Self::open_split_vertical))
6035                .on_action(cx.listener(Self::open_split_horizontal))
6036                .on_action(cx.listener(Self::confirm))
6037                .on_action(cx.listener(Self::cancel))
6038                .on_action(cx.listener(Self::copy_path))
6039                .on_action(cx.listener(Self::copy_relative_path))
6040                .on_action(cx.listener(Self::new_search_in_directory))
6041                .on_action(cx.listener(Self::unfold_directory))
6042                .on_action(cx.listener(Self::fold_directory))
6043                .on_action(cx.listener(Self::remove_from_project))
6044                .on_action(cx.listener(Self::compare_marked_files))
6045                .when(!project.is_read_only(cx), |el| {
6046                    el.on_action(cx.listener(Self::new_file))
6047                        .on_action(cx.listener(Self::new_directory))
6048                        .on_action(cx.listener(Self::rename))
6049                        .on_action(cx.listener(Self::delete))
6050                        .on_action(cx.listener(Self::cut))
6051                        .on_action(cx.listener(Self::copy))
6052                        .on_action(cx.listener(Self::paste))
6053                        .on_action(cx.listener(Self::duplicate))
6054                        .on_action(cx.listener(Self::restore_file))
6055                        .when(!project.is_remote(), |el| {
6056                            el.on_action(cx.listener(Self::trash))
6057                        })
6058                })
6059                .when(project.is_local(), |el| {
6060                    el.on_action(cx.listener(Self::reveal_in_finder))
6061                        .on_action(cx.listener(Self::open_system))
6062                        .on_action(cx.listener(Self::open_in_terminal))
6063                })
6064                .when(project.is_via_remote_server(), |el| {
6065                    el.on_action(cx.listener(Self::open_in_terminal))
6066                })
6067                .track_focus(&self.focus_handle(cx))
6068                .child(
6069                    v_flex()
6070                        .child(
6071                            uniform_list("entries", item_count, {
6072                                cx.processor(|this, range: Range<usize>, window, cx| {
6073                                    this.rendered_entries_len = range.end - range.start;
6074                                    let mut items = Vec::with_capacity(this.rendered_entries_len);
6075                                    this.for_each_visible_entry(
6076                                        range,
6077                                        window,
6078                                        cx,
6079                                        |id, details, window, cx| {
6080                                            items.push(this.render_entry(id, details, window, cx));
6081                                        },
6082                                    );
6083                                    items
6084                                })
6085                            })
6086                            .when(show_indent_guides, |list| {
6087                                list.with_decoration(
6088                                    ui::indent_guides(
6089                                        px(indent_size),
6090                                        IndentGuideColors::panel(cx),
6091                                    )
6092                                    .with_compute_indents_fn(
6093                                        cx.entity(),
6094                                        |this, range, window, cx| {
6095                                            let mut items =
6096                                                SmallVec::with_capacity(range.end - range.start);
6097                                            this.iter_visible_entries(
6098                                                range,
6099                                                window,
6100                                                cx,
6101                                                |entry, _, entries, _, _| {
6102                                                    let (depth, _) =
6103                                                        Self::calculate_depth_and_difference(
6104                                                            entry, entries,
6105                                                        );
6106                                                    items.push(depth);
6107                                                },
6108                                            );
6109                                            items
6110                                        },
6111                                    )
6112                                    .on_click(cx.listener(
6113                                        |this,
6114                                         active_indent_guide: &IndentGuideLayout,
6115                                         window,
6116                                         cx| {
6117                                            if window.modifiers().secondary() {
6118                                                let ix = active_indent_guide.offset.y;
6119                                                let Some((target_entry, worktree)) = maybe!({
6120                                                    let (worktree_id, entry) =
6121                                                        this.entry_at_index(ix)?;
6122                                                    let worktree = this
6123                                                        .project
6124                                                        .read(cx)
6125                                                        .worktree_for_id(worktree_id, cx)?;
6126                                                    let target_entry = worktree
6127                                                        .read(cx)
6128                                                        .entry_for_path(&entry.path.parent()?)?;
6129                                                    Some((target_entry, worktree))
6130                                                }) else {
6131                                                    return;
6132                                                };
6133
6134                                                this.collapse_entry(
6135                                                    target_entry.clone(),
6136                                                    worktree,
6137                                                    window,
6138                                                    cx,
6139                                                );
6140                                            }
6141                                        },
6142                                    ))
6143                                    .with_render_fn(
6144                                        cx.entity(),
6145                                        move |this, params, _, cx| {
6146                                            const LEFT_OFFSET: Pixels = px(14.);
6147                                            const PADDING_Y: Pixels = px(4.);
6148                                            const HITBOX_OVERDRAW: Pixels = px(3.);
6149
6150                                            let active_indent_guide_index = this
6151                                                .find_active_indent_guide(
6152                                                    &params.indent_guides,
6153                                                    cx,
6154                                                );
6155
6156                                            let indent_size = params.indent_size;
6157                                            let item_height = params.item_height;
6158
6159                                            params
6160                                                .indent_guides
6161                                                .into_iter()
6162                                                .enumerate()
6163                                                .map(|(idx, layout)| {
6164                                                    let offset = if layout.continues_offscreen {
6165                                                        px(0.)
6166                                                    } else {
6167                                                        PADDING_Y
6168                                                    };
6169                                                    let bounds = Bounds::new(
6170                                                        point(
6171                                                            layout.offset.x * indent_size
6172                                                                + LEFT_OFFSET,
6173                                                            layout.offset.y * item_height + offset,
6174                                                        ),
6175                                                        size(
6176                                                            px(1.),
6177                                                            layout.length * item_height
6178                                                                - offset * 2.,
6179                                                        ),
6180                                                    );
6181                                                    ui::RenderedIndentGuide {
6182                                                        bounds,
6183                                                        layout,
6184                                                        is_active: Some(idx)
6185                                                            == active_indent_guide_index,
6186                                                        hitbox: Some(Bounds::new(
6187                                                            point(
6188                                                                bounds.origin.x - HITBOX_OVERDRAW,
6189                                                                bounds.origin.y,
6190                                                            ),
6191                                                            size(
6192                                                                bounds.size.width
6193                                                                    + HITBOX_OVERDRAW * 2.,
6194                                                                bounds.size.height,
6195                                                            ),
6196                                                        )),
6197                                                    }
6198                                                })
6199                                                .collect()
6200                                        },
6201                                    ),
6202                                )
6203                            })
6204                            .when(show_sticky_entries, |list| {
6205                                let sticky_items = ui::sticky_items(
6206                                    cx.entity(),
6207                                    |this, range, window, cx| {
6208                                        let mut items =
6209                                            SmallVec::with_capacity(range.end - range.start);
6210                                        this.iter_visible_entries(
6211                                            range,
6212                                            window,
6213                                            cx,
6214                                            |entry, index, entries, _, _| {
6215                                                let (depth, _) =
6216                                                    Self::calculate_depth_and_difference(
6217                                                        entry, entries,
6218                                                    );
6219                                                let candidate =
6220                                                    StickyProjectPanelCandidate { index, depth };
6221                                                items.push(candidate);
6222                                            },
6223                                        );
6224                                        items
6225                                    },
6226                                    |this, marker_entry, window, cx| {
6227                                        let sticky_entries =
6228                                            this.render_sticky_entries(marker_entry, window, cx);
6229                                        this.sticky_items_count = sticky_entries.len();
6230                                        sticky_entries
6231                                    },
6232                                );
6233                                list.with_decoration(if show_indent_guides {
6234                                    sticky_items.with_decoration(
6235                                        ui::indent_guides(
6236                                            px(indent_size),
6237                                            IndentGuideColors::panel(cx),
6238                                        )
6239                                        .with_render_fn(
6240                                            cx.entity(),
6241                                            move |_, params, _, _| {
6242                                                const LEFT_OFFSET: Pixels = px(14.);
6243
6244                                                let indent_size = params.indent_size;
6245                                                let item_height = params.item_height;
6246
6247                                                params
6248                                                    .indent_guides
6249                                                    .into_iter()
6250                                                    .map(|layout| {
6251                                                        let bounds = Bounds::new(
6252                                                            point(
6253                                                                layout.offset.x * indent_size
6254                                                                    + LEFT_OFFSET,
6255                                                                layout.offset.y * item_height,
6256                                                            ),
6257                                                            size(
6258                                                                px(1.),
6259                                                                layout.length * item_height,
6260                                                            ),
6261                                                        );
6262                                                        ui::RenderedIndentGuide {
6263                                                            bounds,
6264                                                            layout,
6265                                                            is_active: false,
6266                                                            hitbox: None,
6267                                                        }
6268                                                    })
6269                                                    .collect()
6270                                            },
6271                                        ),
6272                                    )
6273                                } else {
6274                                    sticky_items
6275                                })
6276                            })
6277                            .with_sizing_behavior(ListSizingBehavior::Infer)
6278                            .with_horizontal_sizing_behavior(
6279                                ListHorizontalSizingBehavior::Unconstrained,
6280                            )
6281                            .with_width_from_item(self.state.max_width_item_index)
6282                            .track_scroll(&self.scroll_handle),
6283                        )
6284                        .child(
6285                            div()
6286                                .id("project-panel-blank-area")
6287                                .block_mouse_except_scroll()
6288                                .flex_grow()
6289                                .when(
6290                                    self.drag_target_entry.as_ref().is_some_and(
6291                                        |entry| match entry {
6292                                            DragTarget::Background => true,
6293                                            DragTarget::Entry {
6294                                                highlight_entry_id, ..
6295                                            } => self.state.last_worktree_root_id.is_some_and(
6296                                                |root_id| *highlight_entry_id == root_id,
6297                                            ),
6298                                        },
6299                                    ),
6300                                    |div| div.bg(cx.theme().colors().drop_target_background),
6301                                )
6302                                .on_drag_move::<ExternalPaths>(cx.listener(
6303                                    move |this, event: &DragMoveEvent<ExternalPaths>, _, _| {
6304                                        let Some(_last_root_id) = this.state.last_worktree_root_id
6305                                        else {
6306                                            return;
6307                                        };
6308                                        if event.bounds.contains(&event.event.position) {
6309                                            this.drag_target_entry = Some(DragTarget::Background);
6310                                        } else {
6311                                            if this.drag_target_entry.as_ref().is_some_and(|e| {
6312                                                matches!(e, DragTarget::Background)
6313                                            }) {
6314                                                this.drag_target_entry = None;
6315                                            }
6316                                        }
6317                                    },
6318                                ))
6319                                .on_drag_move::<DraggedSelection>(cx.listener(
6320                                    move |this, event: &DragMoveEvent<DraggedSelection>, _, cx| {
6321                                        let Some(last_root_id) = this.state.last_worktree_root_id
6322                                        else {
6323                                            return;
6324                                        };
6325                                        if event.bounds.contains(&event.event.position) {
6326                                            let drag_state = event.drag(cx);
6327                                            if this.should_highlight_background_for_selection_drag(
6328                                                &drag_state,
6329                                                last_root_id,
6330                                                cx,
6331                                            ) {
6332                                                this.drag_target_entry =
6333                                                    Some(DragTarget::Background);
6334                                            }
6335                                        } else {
6336                                            if this.drag_target_entry.as_ref().is_some_and(|e| {
6337                                                matches!(e, DragTarget::Background)
6338                                            }) {
6339                                                this.drag_target_entry = None;
6340                                            }
6341                                        }
6342                                    },
6343                                ))
6344                                .on_drop(cx.listener(
6345                                    move |this, external_paths: &ExternalPaths, window, cx| {
6346                                        this.drag_target_entry = None;
6347                                        this.hover_scroll_task.take();
6348                                        if let Some(entry_id) = this.state.last_worktree_root_id {
6349                                            this.drop_external_files(
6350                                                external_paths.paths(),
6351                                                entry_id,
6352                                                window,
6353                                                cx,
6354                                            );
6355                                        }
6356                                        cx.stop_propagation();
6357                                    },
6358                                ))
6359                                .on_drop(cx.listener(
6360                                    move |this, selections: &DraggedSelection, window, cx| {
6361                                        this.drag_target_entry = None;
6362                                        this.hover_scroll_task.take();
6363                                        if let Some(entry_id) = this.state.last_worktree_root_id {
6364                                            this.drag_onto(selections, entry_id, false, window, cx);
6365                                        }
6366                                        cx.stop_propagation();
6367                                    },
6368                                ))
6369                                .on_click(cx.listener(|this, event, window, cx| {
6370                                    if matches!(event, gpui::ClickEvent::Keyboard(_)) {
6371                                        return;
6372                                    }
6373                                    cx.stop_propagation();
6374                                    this.state.selection = None;
6375                                    this.marked_entries.clear();
6376                                    this.focus_handle(cx).focus(window, cx);
6377                                }))
6378                                .on_mouse_down(
6379                                    MouseButton::Right,
6380                                    cx.listener(move |this, event: &MouseDownEvent, window, cx| {
6381                                        // When deploying the context menu anywhere below the last project entry,
6382                                        // act as if the user clicked the root of the last worktree.
6383                                        if let Some(entry_id) = this.state.last_worktree_root_id {
6384                                            this.deploy_context_menu(
6385                                                event.position,
6386                                                entry_id,
6387                                                window,
6388                                                cx,
6389                                            );
6390                                        }
6391                                    }),
6392                                )
6393                                .when(!project.is_read_only(cx), |el| {
6394                                    el.on_click(cx.listener(
6395                                        |this, event: &gpui::ClickEvent, window, cx| {
6396                                            if event.click_count() > 1
6397                                                && let Some(entry_id) =
6398                                                    this.state.last_worktree_root_id
6399                                            {
6400                                                let project = this.project.read(cx);
6401
6402                                                let worktree_id = if let Some(worktree) =
6403                                                    project.worktree_for_entry(entry_id, cx)
6404                                                {
6405                                                    worktree.read(cx).id()
6406                                                } else {
6407                                                    return;
6408                                                };
6409
6410                                                this.state.selection = Some(SelectedEntry {
6411                                                    worktree_id,
6412                                                    entry_id,
6413                                                });
6414
6415                                                this.new_file(&NewFile, window, cx);
6416                                            }
6417                                        },
6418                                    ))
6419                                }),
6420                        )
6421                        .size_full(),
6422                )
6423                .custom_scrollbars(
6424                    Scrollbars::for_settings::<ProjectPanelSettings>()
6425                        .tracked_scroll_handle(&self.scroll_handle)
6426                        .with_track_along(
6427                            ScrollAxes::Horizontal,
6428                            cx.theme().colors().panel_background,
6429                        )
6430                        .notify_content(),
6431                    window,
6432                    cx,
6433                )
6434                .children(self.context_menu.as_ref().map(|(menu, position, _)| {
6435                    deferred(
6436                        anchored()
6437                            .position(*position)
6438                            .anchor(gpui::Corner::TopLeft)
6439                            .child(menu.clone()),
6440                    )
6441                    .with_priority(3)
6442                }))
6443        } else {
6444            let focus_handle = self.focus_handle(cx);
6445
6446            v_flex()
6447                .id("empty-project_panel")
6448                .p_4()
6449                .size_full()
6450                .items_center()
6451                .justify_center()
6452                .gap_1()
6453                .track_focus(&self.focus_handle(cx))
6454                .child(
6455                    Button::new("open_project", "Open Project")
6456                        .full_width()
6457                        .key_binding(KeyBinding::for_action_in(
6458                            &workspace::Open,
6459                            &focus_handle,
6460                            cx,
6461                        ))
6462                        .on_click(cx.listener(|this, _, window, cx| {
6463                            this.workspace
6464                                .update(cx, |_, cx| {
6465                                    window.dispatch_action(workspace::Open.boxed_clone(), cx);
6466                                })
6467                                .log_err();
6468                        })),
6469                )
6470                .child(
6471                    h_flex()
6472                        .w_1_2()
6473                        .gap_2()
6474                        .child(Divider::horizontal())
6475                        .child(Label::new("or").size(LabelSize::XSmall).color(Color::Muted))
6476                        .child(Divider::horizontal()),
6477                )
6478                .child(
6479                    Button::new("clone_repo", "Clone Repository")
6480                        .full_width()
6481                        .on_click(cx.listener(|this, _, window, cx| {
6482                            this.workspace
6483                                .update(cx, |_, cx| {
6484                                    window.dispatch_action(git::Clone.boxed_clone(), cx);
6485                                })
6486                                .log_err();
6487                        })),
6488                )
6489                .when(is_local, |div| {
6490                    div.when(panel_settings.drag_and_drop, |div| {
6491                        div.drag_over::<ExternalPaths>(|style, _, _, cx| {
6492                            style.bg(cx.theme().colors().drop_target_background)
6493                        })
6494                        .on_drop(cx.listener(
6495                            move |this, external_paths: &ExternalPaths, window, cx| {
6496                                this.drag_target_entry = None;
6497                                this.hover_scroll_task.take();
6498                                if let Some(task) = this
6499                                    .workspace
6500                                    .update(cx, |workspace, cx| {
6501                                        workspace.open_workspace_for_paths(
6502                                            true,
6503                                            external_paths.paths().to_owned(),
6504                                            window,
6505                                            cx,
6506                                        )
6507                                    })
6508                                    .log_err()
6509                                {
6510                                    task.detach_and_log_err(cx);
6511                                }
6512                                cx.stop_propagation();
6513                            },
6514                        ))
6515                    })
6516                })
6517        }
6518    }
6519}
6520
6521impl Render for DraggedProjectEntryView {
6522    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
6523        let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
6524        h_flex()
6525            .font(ui_font)
6526            .pl(self.click_offset.x + px(12.))
6527            .pt(self.click_offset.y + px(12.))
6528            .child(
6529                div()
6530                    .flex()
6531                    .gap_1()
6532                    .items_center()
6533                    .py_1()
6534                    .px_2()
6535                    .rounded_lg()
6536                    .bg(cx.theme().colors().background)
6537                    .map(|this| {
6538                        if self.selections.len() > 1 && self.selections.contains(&self.selection) {
6539                            this.child(Label::new(format!("{} entries", self.selections.len())))
6540                        } else {
6541                            this.child(if let Some(icon) = &self.icon {
6542                                div().child(Icon::from_path(icon.clone()))
6543                            } else {
6544                                div()
6545                            })
6546                            .child(Label::new(self.filename.clone()))
6547                        }
6548                    }),
6549            )
6550    }
6551}
6552
6553impl EventEmitter<Event> for ProjectPanel {}
6554
6555impl EventEmitter<PanelEvent> for ProjectPanel {}
6556
6557impl Panel for ProjectPanel {
6558    fn position(&self, _: &Window, cx: &App) -> DockPosition {
6559        match ProjectPanelSettings::get_global(cx).dock {
6560            DockSide::Left => DockPosition::Left,
6561            DockSide::Right => DockPosition::Right,
6562        }
6563    }
6564
6565    fn position_is_valid(&self, position: DockPosition) -> bool {
6566        matches!(position, DockPosition::Left | DockPosition::Right)
6567    }
6568
6569    fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
6570        settings::update_settings_file(self.fs.clone(), cx, move |settings, _| {
6571            let dock = match position {
6572                DockPosition::Left | DockPosition::Bottom => DockSide::Left,
6573                DockPosition::Right => DockSide::Right,
6574            };
6575            settings.project_panel.get_or_insert_default().dock = Some(dock);
6576        });
6577    }
6578
6579    fn size(&self, _: &Window, cx: &App) -> Pixels {
6580        self.width
6581            .unwrap_or_else(|| ProjectPanelSettings::get_global(cx).default_width)
6582    }
6583
6584    fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
6585        self.width = size;
6586        cx.notify();
6587        cx.defer_in(window, |this, _, cx| {
6588            this.serialize(cx);
6589        });
6590    }
6591
6592    fn icon(&self, _: &Window, cx: &App) -> Option<IconName> {
6593        ProjectPanelSettings::get_global(cx)
6594            .button
6595            .then_some(IconName::FileTree)
6596    }
6597
6598    fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
6599        Some("Project Panel")
6600    }
6601
6602    fn toggle_action(&self) -> Box<dyn Action> {
6603        Box::new(ToggleFocus)
6604    }
6605
6606    fn persistent_name() -> &'static str {
6607        "Project Panel"
6608    }
6609
6610    fn panel_key() -> &'static str {
6611        PROJECT_PANEL_KEY
6612    }
6613
6614    fn starts_open(&self, _: &Window, cx: &App) -> bool {
6615        if !ProjectPanelSettings::get_global(cx).starts_open {
6616            return false;
6617        }
6618
6619        let project = &self.project.read(cx);
6620        project.visible_worktrees(cx).any(|tree| {
6621            tree.read(cx)
6622                .root_entry()
6623                .is_some_and(|entry| entry.is_dir())
6624        })
6625    }
6626
6627    fn activation_priority(&self) -> u32 {
6628        0
6629    }
6630}
6631
6632impl Focusable for ProjectPanel {
6633    fn focus_handle(&self, _cx: &App) -> FocusHandle {
6634        self.focus_handle.clone()
6635    }
6636}
6637
6638impl ClipboardEntry {
6639    fn is_cut(&self) -> bool {
6640        matches!(self, Self::Cut { .. })
6641    }
6642
6643    fn items(&self) -> &BTreeSet<SelectedEntry> {
6644        match self {
6645            ClipboardEntry::Copied(entries) | ClipboardEntry::Cut(entries) => entries,
6646        }
6647    }
6648
6649    fn into_copy_entry(self) -> Self {
6650        match self {
6651            ClipboardEntry::Copied(_) => self,
6652            ClipboardEntry::Cut(entries) => ClipboardEntry::Copied(entries),
6653        }
6654    }
6655}
6656
6657#[inline]
6658fn cmp_directories_first(a: &Entry, b: &Entry) -> cmp::Ordering {
6659    util::paths::compare_rel_paths((&a.path, a.is_file()), (&b.path, b.is_file()))
6660}
6661
6662#[inline]
6663fn cmp_mixed(a: &Entry, b: &Entry) -> cmp::Ordering {
6664    util::paths::compare_rel_paths_mixed((&a.path, a.is_file()), (&b.path, b.is_file()))
6665}
6666
6667#[inline]
6668fn cmp_files_first(a: &Entry, b: &Entry) -> cmp::Ordering {
6669    util::paths::compare_rel_paths_files_first((&a.path, a.is_file()), (&b.path, b.is_file()))
6670}
6671
6672#[inline]
6673fn cmp_with_mode(a: &Entry, b: &Entry, mode: &settings::ProjectPanelSortMode) -> cmp::Ordering {
6674    match mode {
6675        settings::ProjectPanelSortMode::DirectoriesFirst => cmp_directories_first(a, b),
6676        settings::ProjectPanelSortMode::Mixed => cmp_mixed(a, b),
6677        settings::ProjectPanelSortMode::FilesFirst => cmp_files_first(a, b),
6678    }
6679}
6680
6681pub fn sort_worktree_entries_with_mode(
6682    entries: &mut [impl AsRef<Entry>],
6683    mode: settings::ProjectPanelSortMode,
6684) {
6685    entries.sort_by(|lhs, rhs| cmp_with_mode(lhs.as_ref(), rhs.as_ref(), &mode));
6686}
6687
6688pub fn par_sort_worktree_entries_with_mode(
6689    entries: &mut Vec<GitEntry>,
6690    mode: settings::ProjectPanelSortMode,
6691) {
6692    entries.par_sort_by(|lhs, rhs| cmp_with_mode(lhs, rhs, &mode));
6693}
6694
6695#[cfg(test)]
6696mod project_panel_tests;