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