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        self.index_for_entry(selection.entry_id, selection.worktree_id)
2727    }
2728
2729    fn disjoint_entries(&self, cx: &App) -> BTreeSet<SelectedEntry> {
2730        let marked_entries = self.effective_entries();
2731        let mut sanitized_entries = BTreeSet::new();
2732        if marked_entries.is_empty() {
2733            return sanitized_entries;
2734        }
2735
2736        let project = self.project.read(cx);
2737        let marked_entries_by_worktree: HashMap<WorktreeId, Vec<SelectedEntry>> = marked_entries
2738            .into_iter()
2739            .filter(|entry| !project.entry_is_worktree_root(entry.entry_id, cx))
2740            .fold(HashMap::default(), |mut map, entry| {
2741                map.entry(entry.worktree_id).or_default().push(entry);
2742                map
2743            });
2744
2745        for (worktree_id, marked_entries) in marked_entries_by_worktree {
2746            if let Some(worktree) = project.worktree_for_id(worktree_id, cx) {
2747                let worktree = worktree.read(cx);
2748                let marked_dir_paths = marked_entries
2749                    .iter()
2750                    .filter_map(|entry| {
2751                        worktree.entry_for_id(entry.entry_id).and_then(|entry| {
2752                            if entry.is_dir() {
2753                                Some(entry.path.as_ref())
2754                            } else {
2755                                None
2756                            }
2757                        })
2758                    })
2759                    .collect::<BTreeSet<_>>();
2760
2761                sanitized_entries.extend(marked_entries.into_iter().filter(|entry| {
2762                    let Some(entry_info) = worktree.entry_for_id(entry.entry_id) else {
2763                        return false;
2764                    };
2765                    let entry_path = entry_info.path.as_ref();
2766                    let inside_marked_dir = marked_dir_paths.iter().any(|&marked_dir_path| {
2767                        entry_path != marked_dir_path && entry_path.starts_with(marked_dir_path)
2768                    });
2769                    !inside_marked_dir
2770                }));
2771            }
2772        }
2773
2774        sanitized_entries
2775    }
2776
2777    fn effective_entries(&self) -> BTreeSet<SelectedEntry> {
2778        if let Some(selection) = self.selection {
2779            let selection = SelectedEntry {
2780                entry_id: self.resolve_entry(selection.entry_id),
2781                worktree_id: selection.worktree_id,
2782            };
2783
2784            // Default to using just the selected item when nothing is marked.
2785            if self.marked_entries.is_empty() {
2786                return BTreeSet::from([selection]);
2787            }
2788
2789            // Allow operating on the selected item even when something else is marked,
2790            // making it easier to perform one-off actions without clearing a mark.
2791            if self.marked_entries.len() == 1 && !self.marked_entries.contains(&selection) {
2792                return BTreeSet::from([selection]);
2793            }
2794        }
2795
2796        // Return only marked entries since we've already handled special cases where
2797        // only selection should take precedence. At this point, marked entries may or
2798        // may not include the current selection, which is intentional.
2799        self.marked_entries
2800            .iter()
2801            .map(|entry| SelectedEntry {
2802                entry_id: self.resolve_entry(entry.entry_id),
2803                worktree_id: entry.worktree_id,
2804            })
2805            .collect::<BTreeSet<_>>()
2806    }
2807
2808    /// Finds the currently selected subentry for a given leaf entry id. If a given entry
2809    /// has no ancestors, the project entry ID that's passed in is returned as-is.
2810    fn resolve_entry(&self, id: ProjectEntryId) -> ProjectEntryId {
2811        self.ancestors
2812            .get(&id)
2813            .and_then(|ancestors| {
2814                if ancestors.current_ancestor_depth == 0 {
2815                    return None;
2816                }
2817                ancestors.ancestors.get(ancestors.current_ancestor_depth)
2818            })
2819            .copied()
2820            .unwrap_or(id)
2821    }
2822
2823    pub fn selected_entry<'a>(&self, cx: &'a App) -> Option<(&'a Worktree, &'a project::Entry)> {
2824        let (worktree, entry) = self.selected_entry_handle(cx)?;
2825        Some((worktree.read(cx), entry))
2826    }
2827
2828    /// Compared to selected_entry, this function resolves to the currently
2829    /// selected subentry if dir auto-folding is enabled.
2830    fn selected_sub_entry<'a>(
2831        &self,
2832        cx: &'a App,
2833    ) -> Option<(Entity<Worktree>, &'a project::Entry)> {
2834        let (worktree, mut entry) = self.selected_entry_handle(cx)?;
2835
2836        let resolved_id = self.resolve_entry(entry.id);
2837        if resolved_id != entry.id {
2838            let worktree = worktree.read(cx);
2839            entry = worktree.entry_for_id(resolved_id)?;
2840        }
2841        Some((worktree, entry))
2842    }
2843    fn selected_entry_handle<'a>(
2844        &self,
2845        cx: &'a App,
2846    ) -> Option<(Entity<Worktree>, &'a project::Entry)> {
2847        let selection = self.selection?;
2848        let project = self.project.read(cx);
2849        let worktree = project.worktree_for_id(selection.worktree_id, cx)?;
2850        let entry = worktree.read(cx).entry_for_id(selection.entry_id)?;
2851        Some((worktree, entry))
2852    }
2853
2854    fn expand_to_selection(&mut self, cx: &mut Context<Self>) -> Option<()> {
2855        let (worktree, entry) = self.selected_entry(cx)?;
2856        let expanded_dir_ids = self.expanded_dir_ids.entry(worktree.id()).or_default();
2857
2858        for path in entry.path.ancestors() {
2859            let Some(entry) = worktree.entry_for_path(path) else {
2860                continue;
2861            };
2862            if entry.is_dir() {
2863                if let Err(idx) = expanded_dir_ids.binary_search(&entry.id) {
2864                    expanded_dir_ids.insert(idx, entry.id);
2865                }
2866            }
2867        }
2868
2869        Some(())
2870    }
2871
2872    fn create_new_git_entry(
2873        parent_entry: &Entry,
2874        git_summary: GitSummary,
2875        new_entry_kind: EntryKind,
2876    ) -> GitEntry {
2877        GitEntry {
2878            entry: Entry {
2879                id: NEW_ENTRY_ID,
2880                kind: new_entry_kind,
2881                path: parent_entry.path.join("\0").into(),
2882                inode: 0,
2883                mtime: parent_entry.mtime,
2884                size: parent_entry.size,
2885                is_ignored: parent_entry.is_ignored,
2886                is_external: false,
2887                is_private: false,
2888                is_always_included: parent_entry.is_always_included,
2889                canonical_path: parent_entry.canonical_path.clone(),
2890                char_bag: parent_entry.char_bag,
2891                is_fifo: parent_entry.is_fifo,
2892            },
2893            git_summary,
2894        }
2895    }
2896
2897    fn update_visible_entries(
2898        &mut self,
2899        new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,
2900        cx: &mut Context<Self>,
2901    ) {
2902        let settings = ProjectPanelSettings::get_global(cx);
2903        let auto_collapse_dirs = settings.auto_fold_dirs;
2904        let hide_gitignore = settings.hide_gitignore;
2905        let project = self.project.read(cx);
2906        let repo_snapshots = project.git_store().read(cx).repo_snapshots(cx);
2907        self.last_worktree_root_id = project
2908            .visible_worktrees(cx)
2909            .next_back()
2910            .and_then(|worktree| worktree.read(cx).root_entry())
2911            .map(|entry| entry.id);
2912
2913        let old_ancestors = std::mem::take(&mut self.ancestors);
2914        self.visible_entries.clear();
2915        let mut max_width_item = None;
2916
2917        let visible_worktrees: Vec<_> = project.visible_worktrees(cx).collect();
2918        let hide_root = settings.hide_root && visible_worktrees.len() == 1;
2919        for worktree in visible_worktrees {
2920            let worktree_snapshot = worktree.read(cx).snapshot();
2921            let worktree_id = worktree_snapshot.id();
2922
2923            let expanded_dir_ids = match self.expanded_dir_ids.entry(worktree_id) {
2924                hash_map::Entry::Occupied(e) => e.into_mut(),
2925                hash_map::Entry::Vacant(e) => {
2926                    // The first time a worktree's root entry becomes available,
2927                    // mark that root entry as expanded.
2928                    if let Some(entry) = worktree_snapshot.root_entry() {
2929                        e.insert(vec![entry.id]).as_slice()
2930                    } else {
2931                        &[]
2932                    }
2933                }
2934            };
2935
2936            let mut new_entry_parent_id = None;
2937            let mut new_entry_kind = EntryKind::Dir;
2938            if let Some(edit_state) = &self.edit_state {
2939                if edit_state.worktree_id == worktree_id && edit_state.is_new_entry() {
2940                    new_entry_parent_id = Some(edit_state.entry_id);
2941                    new_entry_kind = if edit_state.is_dir {
2942                        EntryKind::Dir
2943                    } else {
2944                        EntryKind::File
2945                    };
2946                }
2947            }
2948
2949            let mut visible_worktree_entries = Vec::new();
2950            let mut entry_iter =
2951                GitTraversal::new(&repo_snapshots, worktree_snapshot.entries(true, 0));
2952            let mut auto_folded_ancestors = vec![];
2953            while let Some(entry) = entry_iter.entry() {
2954                if hide_root && Some(entry.entry) == worktree.read(cx).root_entry() {
2955                    if new_entry_parent_id == Some(entry.id) {
2956                        visible_worktree_entries.push(Self::create_new_git_entry(
2957                            &entry.entry,
2958                            entry.git_summary,
2959                            new_entry_kind,
2960                        ));
2961                        new_entry_parent_id = None;
2962                    }
2963                    entry_iter.advance();
2964                    continue;
2965                }
2966                if auto_collapse_dirs && entry.kind.is_dir() {
2967                    auto_folded_ancestors.push(entry.id);
2968                    if !self.unfolded_dir_ids.contains(&entry.id) {
2969                        if let Some(root_path) = worktree_snapshot.root_entry() {
2970                            let mut child_entries = worktree_snapshot.child_entries(&entry.path);
2971                            if let Some(child) = child_entries.next() {
2972                                if entry.path != root_path.path
2973                                    && child_entries.next().is_none()
2974                                    && child.kind.is_dir()
2975                                {
2976                                    entry_iter.advance();
2977
2978                                    continue;
2979                                }
2980                            }
2981                        }
2982                    }
2983                    let depth = old_ancestors
2984                        .get(&entry.id)
2985                        .map(|ancestor| ancestor.current_ancestor_depth)
2986                        .unwrap_or_default()
2987                        .min(auto_folded_ancestors.len());
2988                    if let Some(edit_state) = &mut self.edit_state {
2989                        if edit_state.entry_id == entry.id {
2990                            edit_state.depth = depth;
2991                        }
2992                    }
2993                    let mut ancestors = std::mem::take(&mut auto_folded_ancestors);
2994                    if ancestors.len() > 1 {
2995                        ancestors.reverse();
2996                        self.ancestors.insert(
2997                            entry.id,
2998                            FoldedAncestors {
2999                                current_ancestor_depth: depth,
3000                                ancestors,
3001                            },
3002                        );
3003                    }
3004                }
3005                auto_folded_ancestors.clear();
3006                if !hide_gitignore || !entry.is_ignored {
3007                    visible_worktree_entries.push(entry.to_owned());
3008                }
3009                let precedes_new_entry = if let Some(new_entry_id) = new_entry_parent_id {
3010                    entry.id == new_entry_id || {
3011                        self.ancestors
3012                            .get(&entry.id)
3013                            .map_or(false, |entries| entries.ancestors.contains(&new_entry_id))
3014                    }
3015                } else {
3016                    false
3017                };
3018                if precedes_new_entry && (!hide_gitignore || !entry.is_ignored) {
3019                    visible_worktree_entries.push(Self::create_new_git_entry(
3020                        &entry.entry,
3021                        entry.git_summary,
3022                        new_entry_kind,
3023                    ));
3024                }
3025                let worktree_abs_path = worktree.read(cx).abs_path();
3026                let (depth, path) = if Some(entry.entry) == worktree.read(cx).root_entry() {
3027                    let Some(path_name) = worktree_abs_path.file_name() else {
3028                        continue;
3029                    };
3030                    let path = ArcCow::Borrowed(Path::new(path_name));
3031                    let depth = 0;
3032                    (depth, path)
3033                } else if entry.is_file() {
3034                    let Some(path_name) = entry
3035                        .path
3036                        .file_name()
3037                        .with_context(|| format!("Non-root entry has no file name: {entry:?}"))
3038                        .log_err()
3039                    else {
3040                        continue;
3041                    };
3042                    let path = ArcCow::Borrowed(Path::new(path_name));
3043                    let depth = entry.path.ancestors().count() - 1;
3044                    (depth, path)
3045                } else {
3046                    let path = self
3047                        .ancestors
3048                        .get(&entry.id)
3049                        .and_then(|ancestors| {
3050                            let outermost_ancestor = ancestors.ancestors.last()?;
3051                            let root_folded_entry = worktree
3052                                .read(cx)
3053                                .entry_for_id(*outermost_ancestor)?
3054                                .path
3055                                .as_ref();
3056                            entry
3057                                .path
3058                                .strip_prefix(root_folded_entry)
3059                                .ok()
3060                                .and_then(|suffix| {
3061                                    let full_path = Path::new(root_folded_entry.file_name()?);
3062                                    Some(ArcCow::Owned(Arc::<Path>::from(full_path.join(suffix))))
3063                                })
3064                        })
3065                        .or_else(|| entry.path.file_name().map(Path::new).map(ArcCow::Borrowed))
3066                        .unwrap_or_else(|| ArcCow::Owned(entry.path.clone()));
3067                    let depth = path.components().count();
3068                    (depth, path)
3069                };
3070                let width_estimate = item_width_estimate(
3071                    depth,
3072                    path.to_string_lossy().chars().count(),
3073                    entry.canonical_path.is_some(),
3074                );
3075
3076                match max_width_item.as_mut() {
3077                    Some((id, worktree_id, width)) => {
3078                        if *width < width_estimate {
3079                            *id = entry.id;
3080                            *worktree_id = worktree.read(cx).id();
3081                            *width = width_estimate;
3082                        }
3083                    }
3084                    None => {
3085                        max_width_item = Some((entry.id, worktree.read(cx).id(), width_estimate))
3086                    }
3087                }
3088
3089                if expanded_dir_ids.binary_search(&entry.id).is_err()
3090                    && entry_iter.advance_to_sibling()
3091                {
3092                    continue;
3093                }
3094                entry_iter.advance();
3095            }
3096
3097            project::sort_worktree_entries(&mut visible_worktree_entries);
3098
3099            self.visible_entries
3100                .push((worktree_id, visible_worktree_entries, OnceCell::new()));
3101        }
3102
3103        if let Some((project_entry_id, worktree_id, _)) = max_width_item {
3104            let mut visited_worktrees_length = 0;
3105            let index = self.visible_entries.iter().find_map(|(id, entries, _)| {
3106                if worktree_id == *id {
3107                    entries
3108                        .iter()
3109                        .position(|entry| entry.id == project_entry_id)
3110                } else {
3111                    visited_worktrees_length += entries.len();
3112                    None
3113                }
3114            });
3115            if let Some(index) = index {
3116                self.max_width_item_index = Some(visited_worktrees_length + index);
3117            }
3118        }
3119        if let Some((worktree_id, entry_id)) = new_selected_entry {
3120            self.selection = Some(SelectedEntry {
3121                worktree_id,
3122                entry_id,
3123            });
3124        }
3125    }
3126
3127    fn expand_entry(
3128        &mut self,
3129        worktree_id: WorktreeId,
3130        entry_id: ProjectEntryId,
3131        cx: &mut Context<Self>,
3132    ) {
3133        self.project.update(cx, |project, cx| {
3134            if let Some((worktree, expanded_dir_ids)) = project
3135                .worktree_for_id(worktree_id, cx)
3136                .zip(self.expanded_dir_ids.get_mut(&worktree_id))
3137            {
3138                project.expand_entry(worktree_id, entry_id, cx);
3139                let worktree = worktree.read(cx);
3140
3141                if let Some(mut entry) = worktree.entry_for_id(entry_id) {
3142                    loop {
3143                        if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) {
3144                            expanded_dir_ids.insert(ix, entry.id);
3145                        }
3146
3147                        if let Some(parent_entry) =
3148                            entry.path.parent().and_then(|p| worktree.entry_for_path(p))
3149                        {
3150                            entry = parent_entry;
3151                        } else {
3152                            break;
3153                        }
3154                    }
3155                }
3156            }
3157        });
3158    }
3159
3160    fn drop_external_files(
3161        &mut self,
3162        paths: &[PathBuf],
3163        entry_id: ProjectEntryId,
3164        window: &mut Window,
3165        cx: &mut Context<Self>,
3166    ) {
3167        let mut paths: Vec<Arc<Path>> = paths.iter().map(|path| Arc::from(path.clone())).collect();
3168
3169        let open_file_after_drop = paths.len() == 1 && paths[0].is_file();
3170
3171        let Some((target_directory, worktree, fs)) = maybe!({
3172            let project = self.project.read(cx);
3173            let fs = project.fs().clone();
3174            let worktree = project.worktree_for_entry(entry_id, cx)?;
3175            let entry = worktree.read(cx).entry_for_id(entry_id)?;
3176            let path = entry.path.clone();
3177            let target_directory = if entry.is_dir() {
3178                path.to_path_buf()
3179            } else {
3180                path.parent()?.to_path_buf()
3181            };
3182            Some((target_directory, worktree, fs))
3183        }) else {
3184            return;
3185        };
3186
3187        let mut paths_to_replace = Vec::new();
3188        for path in &paths {
3189            if let Some(name) = path.file_name() {
3190                let mut target_path = target_directory.clone();
3191                target_path.push(name);
3192                if target_path.exists() {
3193                    paths_to_replace.push((name.to_string_lossy().to_string(), path.clone()));
3194                }
3195            }
3196        }
3197
3198        cx.spawn_in(window, async move |this, cx| {
3199            async move {
3200                for (filename, original_path) in &paths_to_replace {
3201                    let answer = cx.update(|window, cx| {
3202                        window
3203                            .prompt(
3204                                PromptLevel::Info,
3205                                format!("A file or folder with name {filename} already exists in the destination folder. Do you want to replace it?").as_str(),
3206                                None,
3207                                &["Replace", "Cancel"],
3208                                cx,
3209                            )
3210                    })?.await?;
3211
3212                    if answer == 1 {
3213                        if let Some(item_idx) = paths.iter().position(|p| p == original_path) {
3214                            paths.remove(item_idx);
3215                        }
3216                    }
3217                }
3218
3219                if paths.is_empty() {
3220                    return Ok(());
3221                }
3222
3223                let task = worktree.update( cx, |worktree, cx| {
3224                    worktree.copy_external_entries(target_directory.into(), paths, fs, cx)
3225                })?;
3226
3227                let opened_entries = task.await.with_context(|| "failed to copy external paths")?;
3228                this.update(cx, |this, cx| {
3229                    if open_file_after_drop && !opened_entries.is_empty() {
3230                        this.open_entry(opened_entries[0], true, false, cx);
3231                    }
3232                })
3233            }
3234            .log_err().await
3235        })
3236        .detach();
3237    }
3238
3239    fn refresh_drag_cursor_style(
3240        &self,
3241        modifiers: &Modifiers,
3242        window: &mut Window,
3243        cx: &mut Context<Self>,
3244    ) {
3245        if let Some(existing_cursor) = cx.active_drag_cursor_style() {
3246            let new_cursor = if Self::is_copy_modifier_set(modifiers) {
3247                CursorStyle::DragCopy
3248            } else {
3249                CursorStyle::PointingHand
3250            };
3251            if existing_cursor != new_cursor {
3252                cx.set_active_drag_cursor_style(new_cursor, window);
3253            }
3254        }
3255    }
3256
3257    fn is_copy_modifier_set(modifiers: &Modifiers) -> bool {
3258        cfg!(target_os = "macos") && modifiers.alt
3259            || cfg!(not(target_os = "macos")) && modifiers.control
3260    }
3261
3262    fn drag_onto(
3263        &mut self,
3264        selections: &DraggedSelection,
3265        target_entry_id: ProjectEntryId,
3266        is_file: bool,
3267        window: &mut Window,
3268        cx: &mut Context<Self>,
3269    ) {
3270        if Self::is_copy_modifier_set(&window.modifiers()) {
3271            let _ = maybe!({
3272                let project = self.project.read(cx);
3273                let target_worktree = project.worktree_for_entry(target_entry_id, cx)?;
3274                let worktree_id = target_worktree.read(cx).id();
3275                let target_entry = target_worktree
3276                    .read(cx)
3277                    .entry_for_id(target_entry_id)?
3278                    .clone();
3279
3280                let mut copy_tasks = Vec::new();
3281                let mut disambiguation_range = None;
3282                for selection in selections.items() {
3283                    let (new_path, new_disambiguation_range) = self.create_paste_path(
3284                        selection,
3285                        (target_worktree.clone(), &target_entry),
3286                        cx,
3287                    )?;
3288
3289                    let task = self.project.update(cx, |project, cx| {
3290                        project.copy_entry(selection.entry_id, None, new_path, cx)
3291                    });
3292                    copy_tasks.push(task);
3293                    disambiguation_range = new_disambiguation_range.or(disambiguation_range);
3294                }
3295
3296                let item_count = copy_tasks.len();
3297
3298                cx.spawn_in(window, async move |project_panel, cx| {
3299                    let mut last_succeed = None;
3300                    for task in copy_tasks.into_iter() {
3301                        if let Some(Some(entry)) = task.await.log_err() {
3302                            last_succeed = Some(entry.id);
3303                        }
3304                    }
3305                    // update selection
3306                    if let Some(entry_id) = last_succeed {
3307                        project_panel
3308                            .update_in(cx, |project_panel, window, cx| {
3309                                project_panel.selection = Some(SelectedEntry {
3310                                    worktree_id,
3311                                    entry_id,
3312                                });
3313
3314                                // if only one entry was dragged and it was disambiguated, open the rename editor
3315                                if item_count == 1 && disambiguation_range.is_some() {
3316                                    project_panel.rename_impl(disambiguation_range, window, cx);
3317                                }
3318                            })
3319                            .ok();
3320                    }
3321                })
3322                .detach();
3323                Some(())
3324            });
3325        } else {
3326            for selection in selections.items() {
3327                self.move_entry(selection.entry_id, target_entry_id, is_file, cx);
3328            }
3329        }
3330    }
3331
3332    fn index_for_entry(
3333        &self,
3334        entry_id: ProjectEntryId,
3335        worktree_id: WorktreeId,
3336    ) -> Option<(usize, usize, usize)> {
3337        let mut total_ix = 0;
3338        for (worktree_ix, (current_worktree_id, visible_worktree_entries, _)) in
3339            self.visible_entries.iter().enumerate()
3340        {
3341            if worktree_id != *current_worktree_id {
3342                total_ix += visible_worktree_entries.len();
3343                continue;
3344            }
3345
3346            return visible_worktree_entries
3347                .iter()
3348                .enumerate()
3349                .find(|(_, entry)| entry.id == entry_id)
3350                .map(|(ix, _)| (worktree_ix, ix, total_ix + ix));
3351        }
3352        None
3353    }
3354
3355    fn entry_at_index(&self, index: usize) -> Option<(WorktreeId, GitEntryRef<'_>)> {
3356        let mut offset = 0;
3357        for (worktree_id, visible_worktree_entries, _) in &self.visible_entries {
3358            let current_len = visible_worktree_entries.len();
3359            if index < offset + current_len {
3360                return visible_worktree_entries
3361                    .get(index - offset)
3362                    .map(|entry| (*worktree_id, entry.to_ref()));
3363            }
3364            offset += current_len;
3365        }
3366        None
3367    }
3368
3369    fn iter_visible_entries(
3370        &self,
3371        range: Range<usize>,
3372        window: &mut Window,
3373        cx: &mut Context<ProjectPanel>,
3374        mut callback: impl FnMut(
3375            &Entry,
3376            usize,
3377            &HashSet<Arc<Path>>,
3378            &mut Window,
3379            &mut Context<ProjectPanel>,
3380        ),
3381    ) {
3382        let mut ix = 0;
3383        for (_, visible_worktree_entries, entries_paths) in &self.visible_entries {
3384            if ix >= range.end {
3385                return;
3386            }
3387
3388            if ix + visible_worktree_entries.len() <= range.start {
3389                ix += visible_worktree_entries.len();
3390                continue;
3391            }
3392
3393            let end_ix = range.end.min(ix + visible_worktree_entries.len());
3394            let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
3395            let entries = entries_paths.get_or_init(|| {
3396                visible_worktree_entries
3397                    .iter()
3398                    .map(|e| (e.path.clone()))
3399                    .collect()
3400            });
3401            let base_index = ix + entry_range.start;
3402            for (i, entry) in visible_worktree_entries[entry_range].iter().enumerate() {
3403                let global_index = base_index + i;
3404                callback(&entry, global_index, entries, window, cx);
3405            }
3406            ix = end_ix;
3407        }
3408    }
3409
3410    fn for_each_visible_entry(
3411        &self,
3412        range: Range<usize>,
3413        window: &mut Window,
3414        cx: &mut Context<ProjectPanel>,
3415        mut callback: impl FnMut(ProjectEntryId, EntryDetails, &mut Window, &mut Context<ProjectPanel>),
3416    ) {
3417        let mut ix = 0;
3418        for (worktree_id, visible_worktree_entries, entries_paths) in &self.visible_entries {
3419            if ix >= range.end {
3420                return;
3421            }
3422
3423            if ix + visible_worktree_entries.len() <= range.start {
3424                ix += visible_worktree_entries.len();
3425                continue;
3426            }
3427
3428            let end_ix = range.end.min(ix + visible_worktree_entries.len());
3429            let git_status_setting = {
3430                let settings = ProjectPanelSettings::get_global(cx);
3431                settings.git_status
3432            };
3433            if let Some(worktree) = self.project.read(cx).worktree_for_id(*worktree_id, cx) {
3434                let snapshot = worktree.read(cx).snapshot();
3435                let root_name = OsStr::new(snapshot.root_name());
3436
3437                let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
3438                let entries = entries_paths.get_or_init(|| {
3439                    visible_worktree_entries
3440                        .iter()
3441                        .map(|e| (e.path.clone()))
3442                        .collect()
3443                });
3444                for entry in visible_worktree_entries[entry_range].iter() {
3445                    let status = git_status_setting
3446                        .then_some(entry.git_summary)
3447                        .unwrap_or_default();
3448
3449                    let mut details = self.details_for_entry(
3450                        entry,
3451                        *worktree_id,
3452                        root_name,
3453                        entries,
3454                        status,
3455                        None,
3456                        window,
3457                        cx,
3458                    );
3459
3460                    if let Some(edit_state) = &self.edit_state {
3461                        let is_edited_entry = if edit_state.is_new_entry() {
3462                            entry.id == NEW_ENTRY_ID
3463                        } else {
3464                            entry.id == edit_state.entry_id
3465                                || self
3466                                    .ancestors
3467                                    .get(&entry.id)
3468                                    .is_some_and(|auto_folded_dirs| {
3469                                        auto_folded_dirs.ancestors.contains(&edit_state.entry_id)
3470                                    })
3471                        };
3472
3473                        if is_edited_entry {
3474                            if let Some(processing_filename) = &edit_state.processing_filename {
3475                                details.is_processing = true;
3476                                if let Some(ancestors) = edit_state
3477                                    .leaf_entry_id
3478                                    .and_then(|entry| self.ancestors.get(&entry))
3479                                {
3480                                    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;
3481                                    let all_components = ancestors.ancestors.len();
3482
3483                                    let prefix_components = all_components - position;
3484                                    let suffix_components = position.checked_sub(1);
3485                                    let mut previous_components =
3486                                        Path::new(&details.filename).components();
3487                                    let mut new_path = previous_components
3488                                        .by_ref()
3489                                        .take(prefix_components)
3490                                        .collect::<PathBuf>();
3491                                    if let Some(last_component) =
3492                                        Path::new(processing_filename).components().next_back()
3493                                    {
3494                                        new_path.push(last_component);
3495                                        previous_components.next();
3496                                    }
3497
3498                                    if let Some(_) = suffix_components {
3499                                        new_path.push(previous_components);
3500                                    }
3501                                    if let Some(str) = new_path.to_str() {
3502                                        details.filename.clear();
3503                                        details.filename.push_str(str);
3504                                    }
3505                                } else {
3506                                    details.filename.clear();
3507                                    details.filename.push_str(processing_filename);
3508                                }
3509                            } else {
3510                                if edit_state.is_new_entry() {
3511                                    details.filename.clear();
3512                                }
3513                                details.is_editing = true;
3514                            }
3515                        }
3516                    }
3517
3518                    callback(entry.id, details, window, cx);
3519                }
3520            }
3521            ix = end_ix;
3522        }
3523    }
3524
3525    fn find_entry_in_worktree(
3526        &self,
3527        worktree_id: WorktreeId,
3528        reverse_search: bool,
3529        only_visible_entries: bool,
3530        predicate: impl Fn(GitEntryRef, WorktreeId) -> bool,
3531        cx: &mut Context<Self>,
3532    ) -> Option<GitEntry> {
3533        if only_visible_entries {
3534            let entries = self
3535                .visible_entries
3536                .iter()
3537                .find_map(|(tree_id, entries, _)| {
3538                    if worktree_id == *tree_id {
3539                        Some(entries)
3540                    } else {
3541                        None
3542                    }
3543                })?
3544                .clone();
3545
3546            return utils::ReversibleIterable::new(entries.iter(), reverse_search)
3547                .find(|ele| predicate(ele.to_ref(), worktree_id))
3548                .cloned();
3549        }
3550
3551        let repo_snapshots = self
3552            .project
3553            .read(cx)
3554            .git_store()
3555            .read(cx)
3556            .repo_snapshots(cx);
3557        let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?;
3558        worktree.read_with(cx, |tree, _| {
3559            utils::ReversibleIterable::new(
3560                GitTraversal::new(&repo_snapshots, tree.entries(true, 0usize)),
3561                reverse_search,
3562            )
3563            .find_single_ended(|ele| predicate(*ele, worktree_id))
3564            .map(|ele| ele.to_owned())
3565        })
3566    }
3567
3568    fn find_entry(
3569        &self,
3570        start: Option<&SelectedEntry>,
3571        reverse_search: bool,
3572        predicate: impl Fn(GitEntryRef, WorktreeId) -> bool,
3573        cx: &mut Context<Self>,
3574    ) -> Option<SelectedEntry> {
3575        let mut worktree_ids: Vec<_> = self
3576            .visible_entries
3577            .iter()
3578            .map(|(worktree_id, _, _)| *worktree_id)
3579            .collect();
3580        let repo_snapshots = self
3581            .project
3582            .read(cx)
3583            .git_store()
3584            .read(cx)
3585            .repo_snapshots(cx);
3586
3587        let mut last_found: Option<SelectedEntry> = None;
3588
3589        if let Some(start) = start {
3590            let worktree = self
3591                .project
3592                .read(cx)
3593                .worktree_for_id(start.worktree_id, cx)?
3594                .read(cx);
3595
3596            let search = {
3597                let entry = worktree.entry_for_id(start.entry_id)?;
3598                let root_entry = worktree.root_entry()?;
3599                let tree_id = worktree.id();
3600
3601                let mut first_iter = GitTraversal::new(
3602                    &repo_snapshots,
3603                    worktree.traverse_from_path(true, true, true, entry.path.as_ref()),
3604                );
3605
3606                if reverse_search {
3607                    first_iter.next();
3608                }
3609
3610                let first = first_iter
3611                    .enumerate()
3612                    .take_until(|(count, entry)| entry.entry == root_entry && *count != 0usize)
3613                    .map(|(_, entry)| entry)
3614                    .find(|ele| predicate(*ele, tree_id))
3615                    .map(|ele| ele.to_owned());
3616
3617                let second_iter =
3618                    GitTraversal::new(&repo_snapshots, worktree.entries(true, 0usize));
3619
3620                let second = if reverse_search {
3621                    second_iter
3622                        .take_until(|ele| ele.id == start.entry_id)
3623                        .filter(|ele| predicate(*ele, tree_id))
3624                        .last()
3625                        .map(|ele| ele.to_owned())
3626                } else {
3627                    second_iter
3628                        .take_while(|ele| ele.id != start.entry_id)
3629                        .filter(|ele| predicate(*ele, tree_id))
3630                        .last()
3631                        .map(|ele| ele.to_owned())
3632                };
3633
3634                if reverse_search {
3635                    Some((second, first))
3636                } else {
3637                    Some((first, second))
3638                }
3639            };
3640
3641            if let Some((first, second)) = search {
3642                let first = first.map(|entry| SelectedEntry {
3643                    worktree_id: start.worktree_id,
3644                    entry_id: entry.id,
3645                });
3646
3647                let second = second.map(|entry| SelectedEntry {
3648                    worktree_id: start.worktree_id,
3649                    entry_id: entry.id,
3650                });
3651
3652                if first.is_some() {
3653                    return first;
3654                }
3655                last_found = second;
3656
3657                let idx = worktree_ids
3658                    .iter()
3659                    .enumerate()
3660                    .find(|(_, ele)| **ele == start.worktree_id)
3661                    .map(|(idx, _)| idx);
3662
3663                if let Some(idx) = idx {
3664                    worktree_ids.rotate_left(idx + 1usize);
3665                    worktree_ids.pop();
3666                }
3667            }
3668        }
3669
3670        for tree_id in worktree_ids.into_iter() {
3671            if let Some(found) =
3672                self.find_entry_in_worktree(tree_id, reverse_search, false, &predicate, cx)
3673            {
3674                return Some(SelectedEntry {
3675                    worktree_id: tree_id,
3676                    entry_id: found.id,
3677                });
3678            }
3679        }
3680
3681        last_found
3682    }
3683
3684    fn find_visible_entry(
3685        &self,
3686        start: Option<&SelectedEntry>,
3687        reverse_search: bool,
3688        predicate: impl Fn(GitEntryRef, WorktreeId) -> bool,
3689        cx: &mut Context<Self>,
3690    ) -> Option<SelectedEntry> {
3691        let mut worktree_ids: Vec<_> = self
3692            .visible_entries
3693            .iter()
3694            .map(|(worktree_id, _, _)| *worktree_id)
3695            .collect();
3696
3697        let mut last_found: Option<SelectedEntry> = None;
3698
3699        if let Some(start) = start {
3700            let entries = self
3701                .visible_entries
3702                .iter()
3703                .find(|(worktree_id, _, _)| *worktree_id == start.worktree_id)
3704                .map(|(_, entries, _)| entries)?;
3705
3706            let mut start_idx = entries
3707                .iter()
3708                .enumerate()
3709                .find(|(_, ele)| ele.id == start.entry_id)
3710                .map(|(idx, _)| idx)?;
3711
3712            if reverse_search {
3713                start_idx = start_idx.saturating_add(1usize);
3714            }
3715
3716            let (left, right) = entries.split_at_checked(start_idx)?;
3717
3718            let (first_iter, second_iter) = if reverse_search {
3719                (
3720                    utils::ReversibleIterable::new(left.iter(), reverse_search),
3721                    utils::ReversibleIterable::new(right.iter(), reverse_search),
3722                )
3723            } else {
3724                (
3725                    utils::ReversibleIterable::new(right.iter(), reverse_search),
3726                    utils::ReversibleIterable::new(left.iter(), reverse_search),
3727                )
3728            };
3729
3730            let first_search = first_iter.find(|ele| predicate(ele.to_ref(), start.worktree_id));
3731            let second_search = second_iter.find(|ele| predicate(ele.to_ref(), start.worktree_id));
3732
3733            if first_search.is_some() {
3734                return first_search.map(|entry| SelectedEntry {
3735                    worktree_id: start.worktree_id,
3736                    entry_id: entry.id,
3737                });
3738            }
3739
3740            last_found = second_search.map(|entry| SelectedEntry {
3741                worktree_id: start.worktree_id,
3742                entry_id: entry.id,
3743            });
3744
3745            let idx = worktree_ids
3746                .iter()
3747                .enumerate()
3748                .find(|(_, ele)| **ele == start.worktree_id)
3749                .map(|(idx, _)| idx);
3750
3751            if let Some(idx) = idx {
3752                worktree_ids.rotate_left(idx + 1usize);
3753                worktree_ids.pop();
3754            }
3755        }
3756
3757        for tree_id in worktree_ids.into_iter() {
3758            if let Some(found) =
3759                self.find_entry_in_worktree(tree_id, reverse_search, true, &predicate, cx)
3760            {
3761                return Some(SelectedEntry {
3762                    worktree_id: tree_id,
3763                    entry_id: found.id,
3764                });
3765            }
3766        }
3767
3768        last_found
3769    }
3770
3771    fn calculate_depth_and_difference(
3772        entry: &Entry,
3773        visible_worktree_entries: &HashSet<Arc<Path>>,
3774    ) -> (usize, usize) {
3775        let (depth, difference) = entry
3776            .path
3777            .ancestors()
3778            .skip(1) // Skip the entry itself
3779            .find_map(|ancestor| {
3780                if let Some(parent_entry) = visible_worktree_entries.get(ancestor) {
3781                    let entry_path_components_count = entry.path.components().count();
3782                    let parent_path_components_count = parent_entry.components().count();
3783                    let difference = entry_path_components_count - parent_path_components_count;
3784                    let depth = parent_entry
3785                        .ancestors()
3786                        .skip(1)
3787                        .filter(|ancestor| visible_worktree_entries.contains(*ancestor))
3788                        .count();
3789                    Some((depth + 1, difference))
3790                } else {
3791                    None
3792                }
3793            })
3794            .unwrap_or_else(|| (0, entry.path.components().count()));
3795
3796        (depth, difference)
3797    }
3798
3799    fn highlight_entry_for_external_drag(
3800        &self,
3801        target_entry: &Entry,
3802        target_worktree: &Worktree,
3803    ) -> Option<ProjectEntryId> {
3804        // Always highlight directory or parent directory if it's file
3805        if target_entry.is_dir() {
3806            Some(target_entry.id)
3807        } else if let Some(parent_entry) = target_entry
3808            .path
3809            .parent()
3810            .and_then(|parent_path| target_worktree.entry_for_path(parent_path))
3811        {
3812            Some(parent_entry.id)
3813        } else {
3814            None
3815        }
3816    }
3817
3818    fn highlight_entry_for_selection_drag(
3819        &self,
3820        target_entry: &Entry,
3821        target_worktree: &Worktree,
3822        drag_state: &DraggedSelection,
3823        cx: &Context<Self>,
3824    ) -> Option<ProjectEntryId> {
3825        let target_parent_path = target_entry.path.parent();
3826
3827        // In case of single item drag, we do not highlight existing
3828        // directory which item belongs too
3829        if drag_state.items().count() == 1 {
3830            let active_entry_path = self
3831                .project
3832                .read(cx)
3833                .path_for_entry(drag_state.active_selection.entry_id, cx)?;
3834
3835            if let Some(active_parent_path) = active_entry_path.path.parent() {
3836                // Do not highlight active entry parent
3837                if active_parent_path == target_entry.path.as_ref() {
3838                    return None;
3839                }
3840
3841                // Do not highlight active entry sibling files
3842                if Some(active_parent_path) == target_parent_path && target_entry.is_file() {
3843                    return None;
3844                }
3845            }
3846        }
3847
3848        // Always highlight directory or parent directory if it's file
3849        if target_entry.is_dir() {
3850            Some(target_entry.id)
3851        } else if let Some(parent_entry) =
3852            target_parent_path.and_then(|parent_path| target_worktree.entry_for_path(parent_path))
3853        {
3854            Some(parent_entry.id)
3855        } else {
3856            None
3857        }
3858    }
3859
3860    fn render_entry(
3861        &self,
3862        entry_id: ProjectEntryId,
3863        details: EntryDetails,
3864        window: &mut Window,
3865        cx: &mut Context<Self>,
3866    ) -> Stateful<Div> {
3867        const GROUP_NAME: &str = "project_entry";
3868
3869        let kind = details.kind;
3870        let is_sticky = details.sticky.is_some();
3871        let sticky_index = details.sticky.as_ref().map(|this| this.sticky_index);
3872        let settings = ProjectPanelSettings::get_global(cx);
3873        let show_editor = details.is_editing && !details.is_processing;
3874
3875        let selection = SelectedEntry {
3876            worktree_id: details.worktree_id,
3877            entry_id,
3878        };
3879
3880        let is_marked = self.marked_entries.contains(&selection);
3881        let is_active = self
3882            .selection
3883            .map_or(false, |selection| selection.entry_id == entry_id);
3884
3885        let file_name = details.filename.clone();
3886
3887        let mut icon = details.icon.clone();
3888        if settings.file_icons && show_editor && details.kind.is_file() {
3889            let filename = self.filename_editor.read(cx).text(cx);
3890            if filename.len() > 2 {
3891                icon = FileIcons::get_icon(Path::new(&filename), cx);
3892            }
3893        }
3894
3895        let filename_text_color = details.filename_text_color;
3896        let diagnostic_severity = details.diagnostic_severity;
3897        let item_colors = get_item_color(is_sticky, cx);
3898
3899        let canonical_path = details
3900            .canonical_path
3901            .as_ref()
3902            .map(|f| f.to_string_lossy().to_string());
3903        let path = details.path.clone();
3904        let path_for_external_paths = path.clone();
3905        let path_for_dragged_selection = path.clone();
3906
3907        let depth = details.depth;
3908        let worktree_id = details.worktree_id;
3909        let selections = Arc::new(self.marked_entries.clone());
3910
3911        let dragged_selection = DraggedSelection {
3912            active_selection: selection,
3913            marked_selections: selections,
3914        };
3915
3916        let bg_color = if is_marked {
3917            item_colors.marked
3918        } else {
3919            item_colors.default
3920        };
3921
3922        let bg_hover_color = if is_marked {
3923            item_colors.marked
3924        } else {
3925            item_colors.hover
3926        };
3927
3928        let validation_color_and_message = if show_editor {
3929            match self
3930                .edit_state
3931                .as_ref()
3932                .map_or(ValidationState::None, |e| e.validation_state.clone())
3933            {
3934                ValidationState::Error(msg) => Some((Color::Error.color(cx), msg.clone())),
3935                ValidationState::Warning(msg) => Some((Color::Warning.color(cx), msg.clone())),
3936                ValidationState::None => None,
3937            }
3938        } else {
3939            None
3940        };
3941
3942        let border_color =
3943            if !self.mouse_down && is_active && self.focus_handle.contains_focused(window, cx) {
3944                match validation_color_and_message {
3945                    Some((color, _)) => color,
3946                    None => item_colors.focused,
3947                }
3948            } else {
3949                bg_color
3950            };
3951
3952        let border_hover_color =
3953            if !self.mouse_down && is_active && self.focus_handle.contains_focused(window, cx) {
3954                match validation_color_and_message {
3955                    Some((color, _)) => color,
3956                    None => item_colors.focused,
3957                }
3958            } else {
3959                bg_hover_color
3960            };
3961
3962        let folded_directory_drag_target = self.folded_directory_drag_target;
3963        let is_highlighted = {
3964            if let Some(highlight_entry_id) = self
3965                .drag_target_entry
3966                .as_ref()
3967                .and_then(|drag_target| drag_target.highlight_entry_id)
3968            {
3969                // Highlight if same entry or it's children
3970                if entry_id == highlight_entry_id {
3971                    true
3972                } else {
3973                    maybe!({
3974                        let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?;
3975                        let highlight_entry = worktree.read(cx).entry_for_id(highlight_entry_id)?;
3976                        Some(path.starts_with(&highlight_entry.path))
3977                    })
3978                    .unwrap_or(false)
3979                }
3980            } else {
3981                false
3982            }
3983        };
3984
3985        let id: ElementId = if is_sticky {
3986            SharedString::from(format!("project_panel_sticky_item_{}", entry_id.to_usize())).into()
3987        } else {
3988            (entry_id.to_proto() as usize).into()
3989        };
3990
3991        div()
3992            .id(id.clone())
3993            .relative()
3994            .group(GROUP_NAME)
3995            .cursor_pointer()
3996            .rounded_none()
3997            .bg(bg_color)
3998            .border_1()
3999            .border_r_2()
4000            .border_color(border_color)
4001            .hover(|style| style.bg(bg_hover_color).border_color(border_hover_color))
4002            .when(is_sticky, |this| {
4003                this.block_mouse_except_scroll()
4004            })
4005            .when(!is_sticky, |this| {
4006                this
4007                .when(is_highlighted && folded_directory_drag_target.is_none(), |this| this.border_color(transparent_white()).bg(item_colors.drag_over))
4008                .on_drag_move::<ExternalPaths>(cx.listener(
4009                    move |this, event: &DragMoveEvent<ExternalPaths>, _, cx| {
4010                        let is_current_target = this.drag_target_entry.as_ref()
4011                             .map(|entry| entry.entry_id) == Some(entry_id);
4012
4013                        if !event.bounds.contains(&event.event.position) {
4014                            // Entry responsible for setting drag target is also responsible to
4015                            // clear it up after drag is out of bounds
4016                            if is_current_target {
4017                                this.drag_target_entry = None;
4018                            }
4019                            return;
4020                        }
4021
4022                        if is_current_target {
4023                            return;
4024                        }
4025
4026                        let Some((entry_id, highlight_entry_id)) = maybe!({
4027                            let target_worktree = this.project.read(cx).worktree_for_id(selection.worktree_id, cx)?.read(cx);
4028                            let target_entry = target_worktree.entry_for_path(&path_for_external_paths)?;
4029                            let highlight_entry_id = this.highlight_entry_for_external_drag(target_entry, target_worktree);
4030                            Some((target_entry.id, highlight_entry_id))
4031                        }) else {
4032                            return;
4033                        };
4034
4035                        this.drag_target_entry = Some(DragTargetEntry {
4036                            entry_id,
4037                            highlight_entry_id,
4038                        });
4039                        this.marked_entries.clear();
4040                    },
4041                ))
4042                .on_drop(cx.listener(
4043                    move |this, external_paths: &ExternalPaths, window, cx| {
4044                        this.drag_target_entry = None;
4045                        this.hover_scroll_task.take();
4046                        this.drop_external_files(external_paths.paths(), entry_id, window, cx);
4047                        cx.stop_propagation();
4048                    },
4049                ))
4050                .on_drag_move::<DraggedSelection>(cx.listener(
4051                    move |this, event: &DragMoveEvent<DraggedSelection>, window, cx| {
4052                        let is_current_target = this.drag_target_entry.as_ref()
4053                             .map(|entry| entry.entry_id) == Some(entry_id);
4054
4055                        if !event.bounds.contains(&event.event.position) {
4056                            // Entry responsible for setting drag target is also responsible to
4057                            // clear it up after drag is out of bounds
4058                            if is_current_target {
4059                                this.drag_target_entry = None;
4060                            }
4061                            return;
4062                        }
4063
4064                        if is_current_target {
4065                            return;
4066                        }
4067
4068                        let drag_state = event.drag(cx);
4069                        let Some((entry_id, highlight_entry_id)) = maybe!({
4070                            let target_worktree = this.project.read(cx).worktree_for_id(selection.worktree_id, cx)?.read(cx);
4071                            let target_entry = target_worktree.entry_for_path(&path_for_dragged_selection)?;
4072                            let highlight_entry_id = this.highlight_entry_for_selection_drag(target_entry, target_worktree, drag_state, cx);
4073                            Some((target_entry.id, highlight_entry_id))
4074                        }) else {
4075                            return;
4076                        };
4077
4078                        this.drag_target_entry = Some(DragTargetEntry {
4079                            entry_id,
4080                            highlight_entry_id,
4081                        });
4082                        if drag_state.items().count() == 1 {
4083                            this.marked_entries.clear();
4084                            this.marked_entries.insert(drag_state.active_selection);
4085                        }
4086                        this.hover_expand_task.take();
4087
4088                        if !kind.is_dir()
4089                            || this
4090                                .expanded_dir_ids
4091                                .get(&details.worktree_id)
4092                                .map_or(false, |ids| ids.binary_search(&entry_id).is_ok())
4093                        {
4094                            return;
4095                        }
4096
4097                        let bounds = event.bounds;
4098                        this.hover_expand_task =
4099                            Some(cx.spawn_in(window, async move |this, cx| {
4100                                cx.background_executor()
4101                                    .timer(Duration::from_millis(500))
4102                                    .await;
4103                                this.update_in(cx, |this, window, cx| {
4104                                    this.hover_expand_task.take();
4105                                    if this.drag_target_entry.as_ref().map(|entry| entry.entry_id) == Some(entry_id)
4106                                        && bounds.contains(&window.mouse_position())
4107                                    {
4108                                        this.expand_entry(worktree_id, entry_id, cx);
4109                                        this.update_visible_entries(
4110                                            Some((worktree_id, entry_id)),
4111                                            cx,
4112                                        );
4113                                        cx.notify();
4114                                    }
4115                                })
4116                                .ok();
4117                            }));
4118                    },
4119                ))
4120                .on_drag(
4121                    dragged_selection,
4122                    move |selection, click_offset, _window, cx| {
4123                        cx.new(|_| DraggedProjectEntryView {
4124                            details: details.clone(),
4125                            click_offset,
4126                            selection: selection.active_selection,
4127                            selections: selection.marked_selections.clone(),
4128                        })
4129                    },
4130                )
4131                .on_drop(
4132                    cx.listener(move |this, selections: &DraggedSelection, window, cx| {
4133                        this.drag_target_entry = None;
4134                        this.hover_scroll_task.take();
4135                        this.hover_expand_task.take();
4136                        if  folded_directory_drag_target.is_some() {
4137                            return;
4138                        }
4139                        this.drag_onto(selections, entry_id, kind.is_file(), window, cx);
4140                    }),
4141                )
4142            })
4143            .on_mouse_down(
4144                MouseButton::Left,
4145                cx.listener(move |this, _, _, cx| {
4146                    this.mouse_down = true;
4147                    cx.propagate();
4148                }),
4149            )
4150            .on_click(
4151                cx.listener(move |this, event: &gpui::ClickEvent, window, cx| {
4152                    if event.down.button == MouseButton::Right
4153                        || event.down.first_mouse
4154                        || show_editor
4155                    {
4156                        return;
4157                    }
4158                    if event.down.button == MouseButton::Left {
4159                        this.mouse_down = false;
4160                    }
4161                    cx.stop_propagation();
4162
4163                    if let Some(selection) = this.selection.filter(|_| event.modifiers().shift) {
4164                        let current_selection = this.index_for_selection(selection);
4165                        let clicked_entry = SelectedEntry {
4166                            entry_id,
4167                            worktree_id,
4168                        };
4169                        let target_selection = this.index_for_selection(clicked_entry);
4170                        if let Some(((_, _, source_index), (_, _, target_index))) =
4171                            current_selection.zip(target_selection)
4172                        {
4173                            let range_start = source_index.min(target_index);
4174                            let range_end = source_index.max(target_index) + 1;
4175                            let mut new_selections = BTreeSet::new();
4176                            this.for_each_visible_entry(
4177                                range_start..range_end,
4178                                window,
4179                                cx,
4180                                |entry_id, details, _, _| {
4181                                    new_selections.insert(SelectedEntry {
4182                                        entry_id,
4183                                        worktree_id: details.worktree_id,
4184                                    });
4185                                },
4186                            );
4187
4188                            this.marked_entries = this
4189                                .marked_entries
4190                                .union(&new_selections)
4191                                .cloned()
4192                                .collect();
4193
4194                            this.selection = Some(clicked_entry);
4195                            this.marked_entries.insert(clicked_entry);
4196                        }
4197                    } else if event.modifiers().secondary() {
4198                        if event.down.click_count > 1 {
4199                            this.split_entry(entry_id, cx);
4200                        } else {
4201                            this.selection = Some(selection);
4202                            if !this.marked_entries.insert(selection) {
4203                                this.marked_entries.remove(&selection);
4204                            }
4205                        }
4206                    } else if kind.is_dir() {
4207                        this.marked_entries.clear();
4208                        if is_sticky {
4209                            if let Some((_, _, index)) = this.index_for_entry(entry_id, worktree_id) {
4210                                this.scroll_handle.scroll_to_item_with_offset(index, ScrollStrategy::Top, sticky_index.unwrap_or(0));
4211                                cx.notify();
4212                                // move down by 1px so that clicked item
4213                                // don't count as sticky anymore
4214                                cx.on_next_frame(window, |_, window, cx| {
4215                                    cx.on_next_frame(window, |this, _, cx| {
4216                                        let mut offset = this.scroll_handle.offset();
4217                                        offset.y += px(1.);
4218                                        this.scroll_handle.set_offset(offset);
4219                                        cx.notify();
4220                                    });
4221                                });
4222                                return;
4223                            }
4224                        }
4225                        if event.modifiers().alt {
4226                            this.toggle_expand_all(entry_id, window, cx);
4227                        } else {
4228                            this.toggle_expanded(entry_id, window, cx);
4229                        }
4230                    } else {
4231                        let preview_tabs_enabled = PreviewTabsSettings::get_global(cx).enabled;
4232                        let click_count = event.up.click_count;
4233                        let focus_opened_item = !preview_tabs_enabled || click_count > 1;
4234                        let allow_preview = preview_tabs_enabled && click_count == 1;
4235                        this.open_entry(entry_id, focus_opened_item, allow_preview, cx);
4236                    }
4237                }),
4238            )
4239            .child(
4240                ListItem::new(id)
4241                    .indent_level(depth)
4242                    .indent_step_size(px(settings.indent_size))
4243                    .spacing(match settings.entry_spacing {
4244                        project_panel_settings::EntrySpacing::Comfortable => ListItemSpacing::Dense,
4245                        project_panel_settings::EntrySpacing::Standard => {
4246                            ListItemSpacing::ExtraDense
4247                        }
4248                    })
4249                    .selectable(false)
4250                    .when_some(canonical_path, |this, path| {
4251                        this.end_slot::<AnyElement>(
4252                            div()
4253                                .id("symlink_icon")
4254                                .pr_3()
4255                                .tooltip(move |window, cx| {
4256                                    Tooltip::with_meta(
4257                                        path.to_string(),
4258                                        None,
4259                                        "Symbolic Link",
4260                                        window,
4261                                        cx,
4262                                    )
4263                                })
4264                                .child(
4265                                    Icon::new(IconName::ArrowUpRight)
4266                                        .size(IconSize::Indicator)
4267                                        .color(filename_text_color),
4268                                )
4269                                .into_any_element(),
4270                        )
4271                    })
4272                    .child(if let Some(icon) = &icon {
4273                        if let Some((_, decoration_color)) =
4274                            entry_diagnostic_aware_icon_decoration_and_color(diagnostic_severity)
4275                        {
4276                            let is_warning = diagnostic_severity
4277                                .map(|severity| matches!(severity, DiagnosticSeverity::WARNING))
4278                                .unwrap_or(false);
4279                            div().child(
4280                                DecoratedIcon::new(
4281                                    Icon::from_path(icon.clone()).color(Color::Muted),
4282                                    Some(
4283                                        IconDecoration::new(
4284                                            if kind.is_file() {
4285                                                if is_warning {
4286                                                    IconDecorationKind::Triangle
4287                                                } else {
4288                                                    IconDecorationKind::X
4289                                                }
4290                                            } else {
4291                                                IconDecorationKind::Dot
4292                                            },
4293                                            bg_color,
4294                                            cx,
4295                                        )
4296                                        .group_name(Some(GROUP_NAME.into()))
4297                                        .knockout_hover_color(bg_hover_color)
4298                                        .color(decoration_color.color(cx))
4299                                        .position(Point {
4300                                            x: px(-2.),
4301                                            y: px(-2.),
4302                                        }),
4303                                    ),
4304                                )
4305                                .into_any_element(),
4306                            )
4307                        } else {
4308                            h_flex().child(Icon::from_path(icon.to_string()).color(Color::Muted))
4309                        }
4310                    } else {
4311                        if let Some((icon_name, color)) =
4312                            entry_diagnostic_aware_icon_name_and_color(diagnostic_severity)
4313                        {
4314                            h_flex()
4315                                .size(IconSize::default().rems())
4316                                .child(Icon::new(icon_name).color(color).size(IconSize::Small))
4317                        } else {
4318                            h_flex()
4319                                .size(IconSize::default().rems())
4320                                .invisible()
4321                                .flex_none()
4322                        }
4323                    })
4324                    .child(
4325                        if let (Some(editor), true) = (Some(&self.filename_editor), show_editor) {
4326                            h_flex().h_6().w_full().child(editor.clone())
4327                        } else {
4328                            h_flex().h_6().map(|mut this| {
4329                                if let Some(folded_ancestors) = self.ancestors.get(&entry_id) {
4330                                    let components = Path::new(&file_name)
4331                                        .components()
4332                                        .map(|comp| {
4333                                            let comp_str =
4334                                                comp.as_os_str().to_string_lossy().into_owned();
4335                                            comp_str
4336                                        })
4337                                        .collect::<Vec<_>>();
4338
4339                                    let components_len = components.len();
4340                                    // TODO this can underflow
4341                                    let active_index = components_len
4342                                        - 1
4343                                        - folded_ancestors.current_ancestor_depth;
4344                                        const DELIMITER: SharedString =
4345                                        SharedString::new_static(std::path::MAIN_SEPARATOR_STR);
4346                                    for (index, component) in components.into_iter().enumerate() {
4347                                        if index != 0 {
4348                                                let delimiter_target_index = index - 1;
4349                                                let target_entry_id = folded_ancestors.ancestors.get(components_len - 1 - delimiter_target_index).cloned();
4350                                                this = this.child(
4351                                                    div()
4352                                                    .when(!is_sticky, |div| {
4353                                                        div
4354                                                            .on_drop(cx.listener(move |this, selections: &DraggedSelection, window, cx| {
4355                                                            this.hover_scroll_task.take();
4356                                                            this.drag_target_entry = None;
4357                                                            this.folded_directory_drag_target = None;
4358                                                            if let Some(target_entry_id) = target_entry_id {
4359                                                                this.drag_onto(selections, target_entry_id, kind.is_file(), window, cx);
4360                                                            }
4361                                                        }))
4362                                                        .on_drag_move(cx.listener(
4363                                                            move |this, event: &DragMoveEvent<DraggedSelection>, _, _| {
4364                                                                if event.bounds.contains(&event.event.position) {
4365                                                                    this.folded_directory_drag_target = Some(
4366                                                                        FoldedDirectoryDragTarget {
4367                                                                            entry_id,
4368                                                                            index: delimiter_target_index,
4369                                                                            is_delimiter_target: true,
4370                                                                        }
4371                                                                    );
4372                                                                } else {
4373                                                                    let is_current_target = this.folded_directory_drag_target
4374                                                                        .map_or(false, |target|
4375                                                                            target.entry_id == entry_id &&
4376                                                                            target.index == delimiter_target_index &&
4377                                                                            target.is_delimiter_target
4378                                                                        );
4379                                                                    if is_current_target {
4380                                                                        this.folded_directory_drag_target = None;
4381                                                                    }
4382                                                                }
4383
4384                                                            },
4385                                                        ))
4386                                                    })
4387                                                    .child(
4388                                                        Label::new(DELIMITER.clone())
4389                                                            .single_line()
4390                                                            .color(filename_text_color)
4391                                                    )
4392                                                );
4393                                        }
4394                                        let id = SharedString::from(format!(
4395                                            "project_panel_path_component_{}_{index}",
4396                                            entry_id.to_usize()
4397                                        ));
4398                                        let label = div()
4399                                            .id(id)
4400                                            .when(!is_sticky,| div| {
4401                                                div
4402                                                .when(index != components_len - 1, |div|{
4403                                                    let target_entry_id = folded_ancestors.ancestors.get(components_len - 1 - index).cloned();
4404                                                    div
4405                                                    .on_drag_move(cx.listener(
4406                                                        move |this, event: &DragMoveEvent<DraggedSelection>, _, _| {
4407                                                        if event.bounds.contains(&event.event.position) {
4408                                                                this.folded_directory_drag_target = Some(
4409                                                                    FoldedDirectoryDragTarget {
4410                                                                        entry_id,
4411                                                                        index,
4412                                                                        is_delimiter_target: false,
4413                                                                    }
4414                                                                );
4415                                                            } else {
4416                                                                let is_current_target = this.folded_directory_drag_target
4417                                                                    .as_ref()
4418                                                                    .map_or(false, |target|
4419                                                                        target.entry_id == entry_id &&
4420                                                                        target.index == index &&
4421                                                                        !target.is_delimiter_target
4422                                                                    );
4423                                                                if is_current_target {
4424                                                                    this.folded_directory_drag_target = None;
4425                                                                }
4426                                                            }
4427                                                        },
4428                                                    ))
4429                                                    .on_drop(cx.listener(move |this, selections: &DraggedSelection, window,cx| {
4430                                                        this.hover_scroll_task.take();
4431                                                        this.drag_target_entry = None;
4432                                                        this.folded_directory_drag_target = None;
4433                                                        if let Some(target_entry_id) = target_entry_id {
4434                                                            this.drag_onto(selections, target_entry_id, kind.is_file(), window, cx);
4435                                                        }
4436                                                    }))
4437                                                    .when(folded_directory_drag_target.map_or(false, |target|
4438                                                        target.entry_id == entry_id &&
4439                                                        target.index == index
4440                                                    ), |this| {
4441                                                        this.bg(item_colors.drag_over)
4442                                                    })
4443                                                })
4444                                            })
4445                                            .on_click(cx.listener(move |this, _, _, cx| {
4446                                                if index != active_index {
4447                                                    if let Some(folds) =
4448                                                        this.ancestors.get_mut(&entry_id)
4449                                                    {
4450                                                        folds.current_ancestor_depth =
4451                                                            components_len - 1 - index;
4452                                                        cx.notify();
4453                                                    }
4454                                                }
4455                                            }))
4456                                            .child(
4457                                                Label::new(component)
4458                                                    .single_line()
4459                                                    .color(filename_text_color)
4460                                                    .when(
4461                                                        index == active_index
4462                                                            && (is_active || is_marked),
4463                                                        |this| this.underline(),
4464                                                    ),
4465                                            );
4466
4467                                        this = this.child(label);
4468                                    }
4469
4470                                    this
4471                                } else {
4472                                    this.child(
4473                                        Label::new(file_name)
4474                                            .single_line()
4475                                            .color(filename_text_color),
4476                                    )
4477                                }
4478                            })
4479                        },
4480                    )
4481                    .on_secondary_mouse_down(cx.listener(
4482                        move |this, event: &MouseDownEvent, window, cx| {
4483                            // Stop propagation to prevent the catch-all context menu for the project
4484                            // panel from being deployed.
4485                            cx.stop_propagation();
4486                            // Some context menu actions apply to all marked entries. If the user
4487                            // right-clicks on an entry that is not marked, they may not realize the
4488                            // action applies to multiple entries. To avoid inadvertent changes, all
4489                            // entries are unmarked.
4490                            if !this.marked_entries.contains(&selection) {
4491                                this.marked_entries.clear();
4492                            }
4493                            this.deploy_context_menu(event.position, entry_id, window, cx);
4494                        },
4495                    ))
4496                    .overflow_x(),
4497            )
4498            .when_some(
4499                validation_color_and_message,
4500                |this, (color, message)| {
4501                    this
4502                    .relative()
4503                    .child(
4504                        deferred(
4505                            div()
4506                            .occlude()
4507                            .absolute()
4508                            .top_full()
4509                            .left(px(-1.)) // Used px over rem so that it doesn't change with font size
4510                            .right(px(-0.5))
4511                            .py_1()
4512                            .px_2()
4513                            .border_1()
4514                            .border_color(color)
4515                            .bg(cx.theme().colors().background)
4516                            .child(
4517                                Label::new(message)
4518                                .color(Color::from(color))
4519                                .size(LabelSize::Small)
4520                            )
4521                        )
4522                    )
4523                }
4524            )
4525    }
4526
4527    fn details_for_entry(
4528        &self,
4529        entry: &Entry,
4530        worktree_id: WorktreeId,
4531        root_name: &OsStr,
4532        entries_paths: &HashSet<Arc<Path>>,
4533        git_status: GitSummary,
4534        sticky: Option<StickyDetails>,
4535        _window: &mut Window,
4536        cx: &mut Context<Self>,
4537    ) -> EntryDetails {
4538        let (show_file_icons, show_folder_icons) = {
4539            let settings = ProjectPanelSettings::get_global(cx);
4540            (settings.file_icons, settings.folder_icons)
4541        };
4542
4543        let expanded_entry_ids = self
4544            .expanded_dir_ids
4545            .get(&worktree_id)
4546            .map(Vec::as_slice)
4547            .unwrap_or(&[]);
4548        let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok();
4549
4550        let icon = match entry.kind {
4551            EntryKind::File => {
4552                if show_file_icons {
4553                    FileIcons::get_icon(&entry.path, cx)
4554                } else {
4555                    None
4556                }
4557            }
4558            _ => {
4559                if show_folder_icons {
4560                    FileIcons::get_folder_icon(is_expanded, cx)
4561                } else {
4562                    FileIcons::get_chevron_icon(is_expanded, cx)
4563                }
4564            }
4565        };
4566
4567        let (depth, difference) =
4568            ProjectPanel::calculate_depth_and_difference(&entry, entries_paths);
4569
4570        let filename = match difference {
4571            diff if diff > 1 => entry
4572                .path
4573                .iter()
4574                .skip(entry.path.components().count() - diff)
4575                .collect::<PathBuf>()
4576                .to_str()
4577                .unwrap_or_default()
4578                .to_string(),
4579            _ => entry
4580                .path
4581                .file_name()
4582                .map(|name| name.to_string_lossy().into_owned())
4583                .unwrap_or_else(|| root_name.to_string_lossy().to_string()),
4584        };
4585
4586        let selection = SelectedEntry {
4587            worktree_id,
4588            entry_id: entry.id,
4589        };
4590        let is_marked = self.marked_entries.contains(&selection);
4591        let is_selected = self.selection == Some(selection);
4592
4593        let diagnostic_severity = self
4594            .diagnostics
4595            .get(&(worktree_id, entry.path.to_path_buf()))
4596            .cloned();
4597
4598        let filename_text_color =
4599            entry_git_aware_label_color(git_status, entry.is_ignored, is_marked);
4600
4601        let is_cut = self
4602            .clipboard
4603            .as_ref()
4604            .map_or(false, |e| e.is_cut() && e.items().contains(&selection));
4605
4606        EntryDetails {
4607            filename,
4608            icon,
4609            path: entry.path.clone(),
4610            depth,
4611            kind: entry.kind,
4612            is_ignored: entry.is_ignored,
4613            is_expanded,
4614            is_selected,
4615            is_marked,
4616            is_editing: false,
4617            is_processing: false,
4618            is_cut,
4619            sticky,
4620            filename_text_color,
4621            diagnostic_severity,
4622            git_status,
4623            is_private: entry.is_private,
4624            worktree_id,
4625            canonical_path: entry.canonical_path.clone(),
4626        }
4627    }
4628
4629    fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
4630        if !Self::should_show_scrollbar(cx)
4631            || !(self.show_scrollbar || self.vertical_scrollbar_state.is_dragging())
4632        {
4633            return None;
4634        }
4635        Some(
4636            div()
4637                .occlude()
4638                .id("project-panel-vertical-scroll")
4639                .on_mouse_move(cx.listener(|_, _, _, cx| {
4640                    cx.notify();
4641                    cx.stop_propagation()
4642                }))
4643                .on_hover(|_, _, cx| {
4644                    cx.stop_propagation();
4645                })
4646                .on_any_mouse_down(|_, _, cx| {
4647                    cx.stop_propagation();
4648                })
4649                .on_mouse_up(
4650                    MouseButton::Left,
4651                    cx.listener(|this, _, window, cx| {
4652                        if !this.vertical_scrollbar_state.is_dragging()
4653                            && !this.focus_handle.contains_focused(window, cx)
4654                        {
4655                            this.hide_scrollbar(window, cx);
4656                            cx.notify();
4657                        }
4658
4659                        cx.stop_propagation();
4660                    }),
4661                )
4662                .on_scroll_wheel(cx.listener(|_, _, _, cx| {
4663                    cx.notify();
4664                }))
4665                .h_full()
4666                .absolute()
4667                .right_1()
4668                .top_1()
4669                .bottom_1()
4670                .w(px(12.))
4671                .cursor_default()
4672                .children(Scrollbar::vertical(
4673                    // percentage as f32..end_offset as f32,
4674                    self.vertical_scrollbar_state.clone(),
4675                )),
4676        )
4677    }
4678
4679    fn render_horizontal_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
4680        if !Self::should_show_scrollbar(cx)
4681            || !(self.show_scrollbar || self.horizontal_scrollbar_state.is_dragging())
4682        {
4683            return None;
4684        }
4685        Scrollbar::horizontal(self.horizontal_scrollbar_state.clone()).map(|scrollbar| {
4686            div()
4687                .occlude()
4688                .id("project-panel-horizontal-scroll")
4689                .on_mouse_move(cx.listener(|_, _, _, cx| {
4690                    cx.notify();
4691                    cx.stop_propagation()
4692                }))
4693                .on_hover(|_, _, cx| {
4694                    cx.stop_propagation();
4695                })
4696                .on_any_mouse_down(|_, _, cx| {
4697                    cx.stop_propagation();
4698                })
4699                .on_mouse_up(
4700                    MouseButton::Left,
4701                    cx.listener(|this, _, window, cx| {
4702                        if !this.horizontal_scrollbar_state.is_dragging()
4703                            && !this.focus_handle.contains_focused(window, cx)
4704                        {
4705                            this.hide_scrollbar(window, cx);
4706                            cx.notify();
4707                        }
4708
4709                        cx.stop_propagation();
4710                    }),
4711                )
4712                .on_scroll_wheel(cx.listener(|_, _, _, cx| {
4713                    cx.notify();
4714                }))
4715                .w_full()
4716                .absolute()
4717                .right_1()
4718                .left_1()
4719                .bottom_1()
4720                .h(px(12.))
4721                .cursor_default()
4722                .child(scrollbar)
4723        })
4724    }
4725
4726    fn dispatch_context(&self, window: &Window, cx: &Context<Self>) -> KeyContext {
4727        let mut dispatch_context = KeyContext::new_with_defaults();
4728        dispatch_context.add("ProjectPanel");
4729        dispatch_context.add("menu");
4730
4731        let identifier = if self.filename_editor.focus_handle(cx).is_focused(window) {
4732            "editing"
4733        } else {
4734            "not_editing"
4735        };
4736
4737        dispatch_context.add(identifier);
4738        dispatch_context
4739    }
4740
4741    fn should_show_scrollbar(cx: &App) -> bool {
4742        let show = ProjectPanelSettings::get_global(cx)
4743            .scrollbar
4744            .show
4745            .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show);
4746        match show {
4747            ShowScrollbar::Auto => true,
4748            ShowScrollbar::System => true,
4749            ShowScrollbar::Always => true,
4750            ShowScrollbar::Never => false,
4751        }
4752    }
4753
4754    fn should_autohide_scrollbar(cx: &App) -> bool {
4755        let show = ProjectPanelSettings::get_global(cx)
4756            .scrollbar
4757            .show
4758            .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show);
4759        match show {
4760            ShowScrollbar::Auto => true,
4761            ShowScrollbar::System => cx
4762                .try_global::<ScrollbarAutoHide>()
4763                .map_or_else(|| cx.should_auto_hide_scrollbars(), |autohide| autohide.0),
4764            ShowScrollbar::Always => false,
4765            ShowScrollbar::Never => true,
4766        }
4767    }
4768
4769    fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4770        const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
4771        if !Self::should_autohide_scrollbar(cx) {
4772            return;
4773        }
4774        self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |panel, cx| {
4775            cx.background_executor()
4776                .timer(SCROLLBAR_SHOW_INTERVAL)
4777                .await;
4778            panel
4779                .update(cx, |panel, cx| {
4780                    panel.show_scrollbar = false;
4781                    cx.notify();
4782                })
4783                .log_err();
4784        }))
4785    }
4786
4787    fn reveal_entry(
4788        &mut self,
4789        project: Entity<Project>,
4790        entry_id: ProjectEntryId,
4791        skip_ignored: bool,
4792        cx: &mut Context<Self>,
4793    ) -> Result<()> {
4794        let worktree = project
4795            .read(cx)
4796            .worktree_for_entry(entry_id, cx)
4797            .context("can't reveal a non-existent entry in the project panel")?;
4798        let worktree = worktree.read(cx);
4799        if skip_ignored
4800            && worktree
4801                .entry_for_id(entry_id)
4802                .map_or(true, |entry| entry.is_ignored && !entry.is_always_included)
4803        {
4804            anyhow::bail!("can't reveal an ignored entry in the project panel");
4805        }
4806
4807        let worktree_id = worktree.id();
4808        self.expand_entry(worktree_id, entry_id, cx);
4809        self.update_visible_entries(Some((worktree_id, entry_id)), cx);
4810        self.marked_entries.clear();
4811        self.marked_entries.insert(SelectedEntry {
4812            worktree_id,
4813            entry_id,
4814        });
4815        self.autoscroll(cx);
4816        cx.notify();
4817        Ok(())
4818    }
4819
4820    fn find_active_indent_guide(
4821        &self,
4822        indent_guides: &[IndentGuideLayout],
4823        cx: &App,
4824    ) -> Option<usize> {
4825        let (worktree, entry) = self.selected_entry(cx)?;
4826
4827        // Find the parent entry of the indent guide, this will either be the
4828        // expanded folder we have selected, or the parent of the currently
4829        // selected file/collapsed directory
4830        let mut entry = entry;
4831        loop {
4832            let is_expanded_dir = entry.is_dir()
4833                && self
4834                    .expanded_dir_ids
4835                    .get(&worktree.id())
4836                    .map(|ids| ids.binary_search(&entry.id).is_ok())
4837                    .unwrap_or(false);
4838            if is_expanded_dir {
4839                break;
4840            }
4841            entry = worktree.entry_for_path(&entry.path.parent()?)?;
4842        }
4843
4844        let (active_indent_range, depth) = {
4845            let (worktree_ix, child_offset, ix) = self.index_for_entry(entry.id, worktree.id())?;
4846            let child_paths = &self.visible_entries[worktree_ix].1;
4847            let mut child_count = 0;
4848            let depth = entry.path.ancestors().count();
4849            while let Some(entry) = child_paths.get(child_offset + child_count + 1) {
4850                if entry.path.ancestors().count() <= depth {
4851                    break;
4852                }
4853                child_count += 1;
4854            }
4855
4856            let start = ix + 1;
4857            let end = start + child_count;
4858
4859            let (_, entries, paths) = &self.visible_entries[worktree_ix];
4860            let visible_worktree_entries =
4861                paths.get_or_init(|| entries.iter().map(|e| (e.path.clone())).collect());
4862
4863            // Calculate the actual depth of the entry, taking into account that directories can be auto-folded.
4864            let (depth, _) = Self::calculate_depth_and_difference(entry, visible_worktree_entries);
4865            (start..end, depth)
4866        };
4867
4868        let candidates = indent_guides
4869            .iter()
4870            .enumerate()
4871            .filter(|(_, indent_guide)| indent_guide.offset.x == depth);
4872
4873        for (i, indent) in candidates {
4874            // Find matches that are either an exact match, partially on screen, or inside the enclosing indent
4875            if active_indent_range.start <= indent.offset.y + indent.length
4876                && indent.offset.y <= active_indent_range.end
4877            {
4878                return Some(i);
4879            }
4880        }
4881        None
4882    }
4883
4884    fn render_sticky_entries(
4885        &self,
4886        child: StickyProjectPanelCandidate,
4887        window: &mut Window,
4888        cx: &mut Context<Self>,
4889    ) -> SmallVec<[AnyElement; 8]> {
4890        let project = self.project.read(cx);
4891
4892        let Some((worktree_id, entry_ref)) = self.entry_at_index(child.index) else {
4893            return SmallVec::new();
4894        };
4895
4896        let Some((_, visible_worktree_entries, entries_paths)) = self
4897            .visible_entries
4898            .iter()
4899            .find(|(id, _, _)| *id == worktree_id)
4900        else {
4901            return SmallVec::new();
4902        };
4903
4904        let Some(worktree) = project.worktree_for_id(worktree_id, cx) else {
4905            return SmallVec::new();
4906        };
4907        let worktree = worktree.read(cx).snapshot();
4908
4909        let paths = entries_paths.get_or_init(|| {
4910            visible_worktree_entries
4911                .iter()
4912                .map(|e| e.path.clone())
4913                .collect()
4914        });
4915
4916        let mut sticky_parents = Vec::new();
4917        let mut current_path = entry_ref.path.clone();
4918
4919        'outer: loop {
4920            if let Some(parent_path) = current_path.parent() {
4921                for ancestor_path in parent_path.ancestors() {
4922                    if paths.contains(ancestor_path) {
4923                        if let Some(parent_entry) = worktree.entry_for_path(ancestor_path) {
4924                            sticky_parents.push(parent_entry.clone());
4925                            current_path = parent_entry.path.clone();
4926                            continue 'outer;
4927                        }
4928                    }
4929                }
4930            }
4931            break 'outer;
4932        }
4933
4934        if sticky_parents.is_empty() {
4935            return SmallVec::new();
4936        }
4937
4938        sticky_parents.reverse();
4939
4940        let git_status_enabled = ProjectPanelSettings::get_global(cx).git_status;
4941        let root_name = OsStr::new(worktree.root_name());
4942
4943        let git_summaries_by_id = if git_status_enabled {
4944            visible_worktree_entries
4945                .iter()
4946                .map(|e| (e.id, e.git_summary))
4947                .collect::<HashMap<_, _>>()
4948        } else {
4949            Default::default()
4950        };
4951
4952        // already checked if non empty above
4953        let last_item_index = sticky_parents.len() - 1;
4954        sticky_parents
4955            .iter()
4956            .enumerate()
4957            .map(|(index, entry)| {
4958                let git_status = git_summaries_by_id
4959                    .get(&entry.id)
4960                    .copied()
4961                    .unwrap_or_default();
4962                let sticky_details = Some(StickyDetails {
4963                    sticky_index: index,
4964                });
4965                let details = self.details_for_entry(
4966                    entry,
4967                    worktree_id,
4968                    root_name,
4969                    paths,
4970                    git_status,
4971                    sticky_details,
4972                    window,
4973                    cx,
4974                );
4975                self.render_entry(entry.id, details, window, cx)
4976                    .when(index == last_item_index, |this| {
4977                        let shadow_color_top = hsla(0.0, 0.0, 0.0, 0.1);
4978                        let shadow_color_bottom = hsla(0.0, 0.0, 0.0, 0.);
4979                        let sticky_shadow = div()
4980                            .absolute()
4981                            .left_0()
4982                            .bottom_neg_1p5()
4983                            .h_1p5()
4984                            .w_full()
4985                            .bg(linear_gradient(
4986                                0.,
4987                                linear_color_stop(shadow_color_top, 1.),
4988                                linear_color_stop(shadow_color_bottom, 0.),
4989                            ));
4990                        this.child(sticky_shadow)
4991                    })
4992                    .into_any()
4993            })
4994            .collect()
4995    }
4996}
4997
4998#[derive(Clone)]
4999struct StickyProjectPanelCandidate {
5000    index: usize,
5001    depth: usize,
5002}
5003
5004impl StickyCandidate for StickyProjectPanelCandidate {
5005    fn depth(&self) -> usize {
5006        self.depth
5007    }
5008}
5009
5010fn item_width_estimate(depth: usize, item_text_chars: usize, is_symlink: bool) -> usize {
5011    const ICON_SIZE_FACTOR: usize = 2;
5012    let mut item_width = depth * ICON_SIZE_FACTOR + item_text_chars;
5013    if is_symlink {
5014        item_width += ICON_SIZE_FACTOR;
5015    }
5016    item_width
5017}
5018
5019impl Render for ProjectPanel {
5020    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
5021        let has_worktree = !self.visible_entries.is_empty();
5022        let project = self.project.read(cx);
5023        let indent_size = ProjectPanelSettings::get_global(cx).indent_size;
5024        let show_indent_guides =
5025            ProjectPanelSettings::get_global(cx).indent_guides.show == ShowIndentGuides::Always;
5026        let show_sticky_entries = {
5027            if ProjectPanelSettings::get_global(cx).sticky_scroll {
5028                let is_scrollable = self.scroll_handle.is_scrollable();
5029                let is_scrolled = self.scroll_handle.offset().y < px(0.);
5030                is_scrollable && is_scrolled
5031            } else {
5032                false
5033            }
5034        };
5035
5036        let is_local = project.is_local();
5037
5038        if has_worktree {
5039            let item_count = self
5040                .visible_entries
5041                .iter()
5042                .map(|(_, worktree_entries, _)| worktree_entries.len())
5043                .sum();
5044
5045            fn handle_drag_move<T: 'static>(
5046                this: &mut ProjectPanel,
5047                e: &DragMoveEvent<T>,
5048                window: &mut Window,
5049                cx: &mut Context<ProjectPanel>,
5050            ) {
5051                if let Some(previous_position) = this.previous_drag_position {
5052                    // Refresh cursor only when an actual drag happens,
5053                    // because modifiers are not updated when the cursor is not moved.
5054                    if e.event.position != previous_position {
5055                        this.refresh_drag_cursor_style(&e.event.modifiers, window, cx);
5056                    }
5057                }
5058                this.previous_drag_position = Some(e.event.position);
5059
5060                if !e.bounds.contains(&e.event.position) {
5061                    this.drag_target_entry = None;
5062                    return;
5063                }
5064                this.hover_scroll_task.take();
5065                let panel_height = e.bounds.size.height;
5066                if panel_height <= px(0.) {
5067                    return;
5068                }
5069
5070                let event_offset = e.event.position.y - e.bounds.origin.y;
5071                // How far along in the project panel is our cursor? (0. is the top of a list, 1. is the bottom)
5072                let hovered_region_offset = event_offset / panel_height;
5073
5074                // We want the scrolling to be a bit faster when the cursor is closer to the edge of a list.
5075                // These pixels offsets were picked arbitrarily.
5076                let vertical_scroll_offset = if hovered_region_offset <= 0.05 {
5077                    8.
5078                } else if hovered_region_offset <= 0.15 {
5079                    5.
5080                } else if hovered_region_offset >= 0.95 {
5081                    -8.
5082                } else if hovered_region_offset >= 0.85 {
5083                    -5.
5084                } else {
5085                    return;
5086                };
5087                let adjustment = point(px(0.), px(vertical_scroll_offset));
5088                this.hover_scroll_task = Some(cx.spawn_in(window, async move |this, cx| {
5089                    loop {
5090                        let should_stop_scrolling = this
5091                            .update(cx, |this, cx| {
5092                                this.hover_scroll_task.as_ref()?;
5093                                let handle = this.scroll_handle.0.borrow_mut();
5094                                let offset = handle.base_handle.offset();
5095
5096                                handle.base_handle.set_offset(offset + adjustment);
5097                                cx.notify();
5098                                Some(())
5099                            })
5100                            .ok()
5101                            .flatten()
5102                            .is_some();
5103                        if should_stop_scrolling {
5104                            return;
5105                        }
5106                        cx.background_executor()
5107                            .timer(Duration::from_millis(16))
5108                            .await;
5109                    }
5110                }));
5111            }
5112            h_flex()
5113                .id("project-panel")
5114                .group("project-panel")
5115                .on_drag_move(cx.listener(handle_drag_move::<ExternalPaths>))
5116                .on_drag_move(cx.listener(handle_drag_move::<DraggedSelection>))
5117                .size_full()
5118                .relative()
5119                .on_modifiers_changed(cx.listener(
5120                    |this, event: &ModifiersChangedEvent, window, cx| {
5121                        this.refresh_drag_cursor_style(&event.modifiers, window, cx);
5122                    },
5123                ))
5124                .on_hover(cx.listener(|this, hovered, window, cx| {
5125                    if *hovered {
5126                        this.show_scrollbar = true;
5127                        this.hide_scrollbar_task.take();
5128                        cx.notify();
5129                    } else if !this.focus_handle.contains_focused(window, cx) {
5130                        this.hide_scrollbar(window, cx);
5131                    }
5132                }))
5133                .on_click(cx.listener(|this, _event, _, cx| {
5134                    cx.stop_propagation();
5135                    this.selection = None;
5136                    this.marked_entries.clear();
5137                }))
5138                .key_context(self.dispatch_context(window, cx))
5139                .on_action(cx.listener(Self::select_next))
5140                .on_action(cx.listener(Self::select_previous))
5141                .on_action(cx.listener(Self::select_first))
5142                .on_action(cx.listener(Self::select_last))
5143                .on_action(cx.listener(Self::select_parent))
5144                .on_action(cx.listener(Self::select_next_git_entry))
5145                .on_action(cx.listener(Self::select_prev_git_entry))
5146                .on_action(cx.listener(Self::select_next_diagnostic))
5147                .on_action(cx.listener(Self::select_prev_diagnostic))
5148                .on_action(cx.listener(Self::select_next_directory))
5149                .on_action(cx.listener(Self::select_prev_directory))
5150                .on_action(cx.listener(Self::expand_selected_entry))
5151                .on_action(cx.listener(Self::collapse_selected_entry))
5152                .on_action(cx.listener(Self::collapse_all_entries))
5153                .on_action(cx.listener(Self::open))
5154                .on_action(cx.listener(Self::open_permanent))
5155                .on_action(cx.listener(Self::confirm))
5156                .on_action(cx.listener(Self::cancel))
5157                .on_action(cx.listener(Self::copy_path))
5158                .on_action(cx.listener(Self::copy_relative_path))
5159                .on_action(cx.listener(Self::new_search_in_directory))
5160                .on_action(cx.listener(Self::unfold_directory))
5161                .on_action(cx.listener(Self::fold_directory))
5162                .on_action(cx.listener(Self::remove_from_project))
5163                .when(!project.is_read_only(cx), |el| {
5164                    el.on_action(cx.listener(Self::new_file))
5165                        .on_action(cx.listener(Self::new_directory))
5166                        .on_action(cx.listener(Self::rename))
5167                        .on_action(cx.listener(Self::delete))
5168                        .on_action(cx.listener(Self::trash))
5169                        .on_action(cx.listener(Self::cut))
5170                        .on_action(cx.listener(Self::copy))
5171                        .on_action(cx.listener(Self::paste))
5172                        .on_action(cx.listener(Self::duplicate))
5173                        .on_click(cx.listener(|this, event: &gpui::ClickEvent, window, cx| {
5174                            if event.up.click_count > 1 {
5175                                if let Some(entry_id) = this.last_worktree_root_id {
5176                                    let project = this.project.read(cx);
5177
5178                                    let worktree_id = if let Some(worktree) =
5179                                        project.worktree_for_entry(entry_id, cx)
5180                                    {
5181                                        worktree.read(cx).id()
5182                                    } else {
5183                                        return;
5184                                    };
5185
5186                                    this.selection = Some(SelectedEntry {
5187                                        worktree_id,
5188                                        entry_id,
5189                                    });
5190
5191                                    this.new_file(&NewFile, window, cx);
5192                                }
5193                            }
5194                        }))
5195                })
5196                .when(project.is_local(), |el| {
5197                    el.on_action(cx.listener(Self::reveal_in_finder))
5198                        .on_action(cx.listener(Self::open_system))
5199                        .on_action(cx.listener(Self::open_in_terminal))
5200                })
5201                .when(project.is_via_ssh(), |el| {
5202                    el.on_action(cx.listener(Self::open_in_terminal))
5203                })
5204                .on_mouse_down(
5205                    MouseButton::Right,
5206                    cx.listener(move |this, event: &MouseDownEvent, window, cx| {
5207                        // When deploying the context menu anywhere below the last project entry,
5208                        // act as if the user clicked the root of the last worktree.
5209                        if let Some(entry_id) = this.last_worktree_root_id {
5210                            this.deploy_context_menu(event.position, entry_id, window, cx);
5211                        }
5212                    }),
5213                )
5214                .track_focus(&self.focus_handle(cx))
5215                .child(
5216                    uniform_list("entries", item_count, {
5217                        cx.processor(|this, range: Range<usize>, window, cx| {
5218                            let mut items = Vec::with_capacity(range.end - range.start);
5219                            this.for_each_visible_entry(
5220                                range,
5221                                window,
5222                                cx,
5223                                |id, details, window, cx| {
5224                                    items.push(this.render_entry(id, details, window, cx));
5225                                },
5226                            );
5227                            items
5228                        })
5229                    })
5230                    .when(show_indent_guides, |list| {
5231                        list.with_decoration(
5232                            ui::indent_guides(px(indent_size), IndentGuideColors::panel(cx))
5233                                .with_compute_indents_fn(
5234                                    cx.entity().clone(),
5235                                    |this, range, window, cx| {
5236                                        let mut items =
5237                                            SmallVec::with_capacity(range.end - range.start);
5238                                        this.iter_visible_entries(
5239                                            range,
5240                                            window,
5241                                            cx,
5242                                            |entry, _, entries, _, _| {
5243                                                let (depth, _) =
5244                                                    Self::calculate_depth_and_difference(
5245                                                        entry, entries,
5246                                                    );
5247                                                items.push(depth);
5248                                            },
5249                                        );
5250                                        items
5251                                    },
5252                                )
5253                                .on_click(cx.listener(
5254                                    |this, active_indent_guide: &IndentGuideLayout, window, cx| {
5255                                        if window.modifiers().secondary() {
5256                                            let ix = active_indent_guide.offset.y;
5257                                            let Some((target_entry, worktree)) = maybe!({
5258                                                let (worktree_id, entry) =
5259                                                    this.entry_at_index(ix)?;
5260                                                let worktree = this
5261                                                    .project
5262                                                    .read(cx)
5263                                                    .worktree_for_id(worktree_id, cx)?;
5264                                                let target_entry = worktree
5265                                                    .read(cx)
5266                                                    .entry_for_path(&entry.path.parent()?)?;
5267                                                Some((target_entry, worktree))
5268                                            }) else {
5269                                                return;
5270                                            };
5271
5272                                            this.collapse_entry(target_entry.clone(), worktree, cx);
5273                                        }
5274                                    },
5275                                ))
5276                                .with_render_fn(cx.entity().clone(), move |this, params, _, cx| {
5277                                    const LEFT_OFFSET: Pixels = px(14.);
5278                                    const PADDING_Y: Pixels = px(4.);
5279                                    const HITBOX_OVERDRAW: Pixels = px(3.);
5280
5281                                    let active_indent_guide_index =
5282                                        this.find_active_indent_guide(&params.indent_guides, cx);
5283
5284                                    let indent_size = params.indent_size;
5285                                    let item_height = params.item_height;
5286
5287                                    params
5288                                        .indent_guides
5289                                        .into_iter()
5290                                        .enumerate()
5291                                        .map(|(idx, layout)| {
5292                                            let offset = if layout.continues_offscreen {
5293                                                px(0.)
5294                                            } else {
5295                                                PADDING_Y
5296                                            };
5297                                            let bounds = Bounds::new(
5298                                                point(
5299                                                    layout.offset.x * indent_size + LEFT_OFFSET,
5300                                                    layout.offset.y * item_height + offset,
5301                                                ),
5302                                                size(
5303                                                    px(1.),
5304                                                    layout.length * item_height - offset * 2.,
5305                                                ),
5306                                            );
5307                                            ui::RenderedIndentGuide {
5308                                                bounds,
5309                                                layout,
5310                                                is_active: Some(idx) == active_indent_guide_index,
5311                                                hitbox: Some(Bounds::new(
5312                                                    point(
5313                                                        bounds.origin.x - HITBOX_OVERDRAW,
5314                                                        bounds.origin.y,
5315                                                    ),
5316                                                    size(
5317                                                        bounds.size.width + HITBOX_OVERDRAW * 2.,
5318                                                        bounds.size.height,
5319                                                    ),
5320                                                )),
5321                                            }
5322                                        })
5323                                        .collect()
5324                                }),
5325                        )
5326                    })
5327                    .when(show_sticky_entries, |list| {
5328                        let sticky_items = ui::sticky_items(
5329                            cx.entity().clone(),
5330                            |this, range, window, cx| {
5331                                let mut items = SmallVec::with_capacity(range.end - range.start);
5332                                this.iter_visible_entries(
5333                                    range,
5334                                    window,
5335                                    cx,
5336                                    |entry, index, entries, _, _| {
5337                                        let (depth, _) =
5338                                            Self::calculate_depth_and_difference(entry, entries);
5339                                        let candidate =
5340                                            StickyProjectPanelCandidate { index, depth };
5341                                        items.push(candidate);
5342                                    },
5343                                );
5344                                items
5345                            },
5346                            |this, marker_entry, window, cx| {
5347                                this.render_sticky_entries(marker_entry, window, cx)
5348                            },
5349                        );
5350                        list.with_decoration(if show_indent_guides {
5351                            sticky_items.with_decoration(
5352                                ui::indent_guides(px(indent_size), IndentGuideColors::panel(cx))
5353                                    .with_render_fn(cx.entity().clone(), move |_, params, _, _| {
5354                                        const LEFT_OFFSET: Pixels = px(14.);
5355
5356                                        let indent_size = params.indent_size;
5357                                        let item_height = params.item_height;
5358
5359                                        params
5360                                            .indent_guides
5361                                            .into_iter()
5362                                            .map(|layout| {
5363                                                let bounds = Bounds::new(
5364                                                    point(
5365                                                        layout.offset.x * indent_size + LEFT_OFFSET,
5366                                                        layout.offset.y * item_height,
5367                                                    ),
5368                                                    size(px(1.), layout.length * item_height),
5369                                                );
5370                                                ui::RenderedIndentGuide {
5371                                                    bounds,
5372                                                    layout,
5373                                                    is_active: false,
5374                                                    hitbox: None,
5375                                                }
5376                                            })
5377                                            .collect()
5378                                    }),
5379                            )
5380                        } else {
5381                            sticky_items
5382                        })
5383                    })
5384                    .size_full()
5385                    .with_sizing_behavior(ListSizingBehavior::Infer)
5386                    .with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained)
5387                    .with_width_from_item(self.max_width_item_index)
5388                    .track_scroll(self.scroll_handle.clone()),
5389                )
5390                .children(self.render_vertical_scrollbar(cx))
5391                .when_some(self.render_horizontal_scrollbar(cx), |this, scrollbar| {
5392                    this.pb_4().child(scrollbar)
5393                })
5394                .children(self.context_menu.as_ref().map(|(menu, position, _)| {
5395                    deferred(
5396                        anchored()
5397                            .position(*position)
5398                            .anchor(gpui::Corner::TopLeft)
5399                            .child(menu.clone()),
5400                    )
5401                    .with_priority(3)
5402                }))
5403        } else {
5404            v_flex()
5405                .id("empty-project_panel")
5406                .size_full()
5407                .p_4()
5408                .track_focus(&self.focus_handle(cx))
5409                .child(
5410                    Button::new("open_project", "Open a project")
5411                        .full_width()
5412                        .key_binding(KeyBinding::for_action_in(
5413                            &OpenRecent::default(),
5414                            &self.focus_handle,
5415                            window,
5416                            cx,
5417                        ))
5418                        .on_click(cx.listener(|this, _, window, cx| {
5419                            this.workspace
5420                                .update(cx, |_, cx| {
5421                                    window.dispatch_action(OpenRecent::default().boxed_clone(), cx);
5422                                })
5423                                .log_err();
5424                        })),
5425                )
5426                .when(is_local, |div| {
5427                    div.drag_over::<ExternalPaths>(|style, _, _, cx| {
5428                        style.bg(cx.theme().colors().drop_target_background)
5429                    })
5430                    .on_drop(cx.listener(
5431                        move |this, external_paths: &ExternalPaths, window, cx| {
5432                            this.drag_target_entry = None;
5433                            this.hover_scroll_task.take();
5434                            if let Some(task) = this
5435                                .workspace
5436                                .update(cx, |workspace, cx| {
5437                                    workspace.open_workspace_for_paths(
5438                                        true,
5439                                        external_paths.paths().to_owned(),
5440                                        window,
5441                                        cx,
5442                                    )
5443                                })
5444                                .log_err()
5445                            {
5446                                task.detach_and_log_err(cx);
5447                            }
5448                            cx.stop_propagation();
5449                        },
5450                    ))
5451                })
5452        }
5453    }
5454}
5455
5456impl Render for DraggedProjectEntryView {
5457    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
5458        let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
5459        h_flex()
5460            .font(ui_font)
5461            .pl(self.click_offset.x + px(12.))
5462            .pt(self.click_offset.y + px(12.))
5463            .child(
5464                div()
5465                    .flex()
5466                    .gap_1()
5467                    .items_center()
5468                    .py_1()
5469                    .px_2()
5470                    .rounded_lg()
5471                    .bg(cx.theme().colors().background)
5472                    .map(|this| {
5473                        if self.selections.len() > 1 && self.selections.contains(&self.selection) {
5474                            this.child(Label::new(format!("{} entries", self.selections.len())))
5475                        } else {
5476                            this.child(if let Some(icon) = &self.details.icon {
5477                                div().child(Icon::from_path(icon.clone()))
5478                            } else {
5479                                div()
5480                            })
5481                            .child(Label::new(self.details.filename.clone()))
5482                        }
5483                    }),
5484            )
5485    }
5486}
5487
5488impl EventEmitter<Event> for ProjectPanel {}
5489
5490impl EventEmitter<PanelEvent> for ProjectPanel {}
5491
5492impl Panel for ProjectPanel {
5493    fn position(&self, _: &Window, cx: &App) -> DockPosition {
5494        match ProjectPanelSettings::get_global(cx).dock {
5495            ProjectPanelDockPosition::Left => DockPosition::Left,
5496            ProjectPanelDockPosition::Right => DockPosition::Right,
5497        }
5498    }
5499
5500    fn position_is_valid(&self, position: DockPosition) -> bool {
5501        matches!(position, DockPosition::Left | DockPosition::Right)
5502    }
5503
5504    fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
5505        settings::update_settings_file::<ProjectPanelSettings>(
5506            self.fs.clone(),
5507            cx,
5508            move |settings, _| {
5509                let dock = match position {
5510                    DockPosition::Left | DockPosition::Bottom => ProjectPanelDockPosition::Left,
5511                    DockPosition::Right => ProjectPanelDockPosition::Right,
5512                };
5513                settings.dock = Some(dock);
5514            },
5515        );
5516    }
5517
5518    fn size(&self, _: &Window, cx: &App) -> Pixels {
5519        self.width
5520            .unwrap_or_else(|| ProjectPanelSettings::get_global(cx).default_width)
5521    }
5522
5523    fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
5524        self.width = size;
5525        cx.notify();
5526        cx.defer_in(window, |this, _, cx| {
5527            this.serialize(cx);
5528        });
5529    }
5530
5531    fn icon(&self, _: &Window, cx: &App) -> Option<IconName> {
5532        ProjectPanelSettings::get_global(cx)
5533            .button
5534            .then_some(IconName::FileTree)
5535    }
5536
5537    fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
5538        Some("Project Panel")
5539    }
5540
5541    fn toggle_action(&self) -> Box<dyn Action> {
5542        Box::new(ToggleFocus)
5543    }
5544
5545    fn persistent_name() -> &'static str {
5546        "Project Panel"
5547    }
5548
5549    fn starts_open(&self, _: &Window, cx: &App) -> bool {
5550        let project = &self.project.read(cx);
5551        project.visible_worktrees(cx).any(|tree| {
5552            tree.read(cx)
5553                .root_entry()
5554                .map_or(false, |entry| entry.is_dir())
5555        })
5556    }
5557
5558    fn activation_priority(&self) -> u32 {
5559        0
5560    }
5561}
5562
5563impl Focusable for ProjectPanel {
5564    fn focus_handle(&self, _cx: &App) -> FocusHandle {
5565        self.focus_handle.clone()
5566    }
5567}
5568
5569impl ClipboardEntry {
5570    fn is_cut(&self) -> bool {
5571        matches!(self, Self::Cut { .. })
5572    }
5573
5574    fn items(&self) -> &BTreeSet<SelectedEntry> {
5575        match self {
5576            ClipboardEntry::Copied(entries) | ClipboardEntry::Cut(entries) => entries,
5577        }
5578    }
5579
5580    fn to_copy_entry(self) -> Self {
5581        match self {
5582            ClipboardEntry::Copied(_) => self,
5583            ClipboardEntry::Cut(entries) => ClipboardEntry::Copied(entries),
5584        }
5585    }
5586}
5587
5588#[cfg(test)]
5589mod project_panel_tests;