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