project_panel.rs

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