project_panel.rs

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