project_panel.rs

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