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