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