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