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