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