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