project_panel.rs

   1mod project_panel_settings;
   2mod utils;
   3
   4use anyhow::{Context as _, Result};
   5use client::{ErrorCode, ErrorExt};
   6use collections::{BTreeSet, HashMap, hash_map};
   7use command_palette_hooks::CommandPaletteFilter;
   8use db::kvp::KEY_VALUE_STORE;
   9use editor::{
  10    Editor, EditorEvent,
  11    items::{
  12        entry_diagnostic_aware_icon_decoration_and_color,
  13        entry_diagnostic_aware_icon_name_and_color, entry_git_aware_label_color,
  14    },
  15};
  16use file_icons::FileIcons;
  17use git::status::GitSummary;
  18use git_ui::file_diff_view::FileDiffView;
  19use gpui::{
  20    Action, AnyElement, App, AsyncWindowContext, Bounds, ClipboardItem, Context, CursorStyle,
  21    DismissEvent, Div, DragMoveEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable,
  22    Hsla, InteractiveElement, KeyContext, ListHorizontalSizingBehavior, ListSizingBehavior,
  23    Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, ParentElement, Pixels, Point,
  24    PromptLevel, Render, ScrollStrategy, Stateful, Styled, Subscription, Task,
  25    UniformListScrollHandle, WeakEntity, Window, actions, anchored, deferred, div, hsla,
  26    linear_color_stop, linear_gradient, point, px, size, transparent_white, uniform_list,
  27};
  28use language::DiagnosticSeverity;
  29use menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrevious};
  30use project::{
  31    Entry, EntryKind, Fs, GitEntry, GitEntryRef, GitTraversal, Project, ProjectEntryId,
  32    ProjectPath, Worktree, WorktreeId,
  33    git_store::{GitStoreEvent, RepositoryEvent, git_traversal::ChildEntriesGitIter},
  34    project_settings::GoToDiagnosticSeverityFilter,
  35};
  36use project_panel_settings::ProjectPanelSettings;
  37use rayon::slice::ParallelSliceMut;
  38use schemars::JsonSchema;
  39use serde::{Deserialize, Serialize};
  40use settings::{
  41    DockSide, ProjectPanelEntrySpacing, Settings, SettingsStore, ShowDiagnostics, ShowIndentGuides,
  42    update_settings_file,
  43};
  44use smallvec::SmallVec;
  45use std::{any::TypeId, time::Instant};
  46use std::{
  47    cell::OnceCell,
  48    cmp,
  49    collections::HashSet,
  50    ops::Range,
  51    path::{Path, PathBuf},
  52    sync::Arc,
  53    time::Duration,
  54};
  55use theme::ThemeSettings;
  56use ui::{
  57    Color, ContextMenu, DecoratedIcon, Divider, Icon, IconDecoration, IconDecorationKind,
  58    IndentGuideColors, IndentGuideLayout, KeyBinding, Label, LabelSize, ListItem, ListItemSpacing,
  59    ScrollAxes, ScrollableHandle, Scrollbars, StickyCandidate, Tooltip, WithScrollbar, prelude::*,
  60    v_flex,
  61};
  62use util::{ResultExt, TakeUntilExt, TryFutureExt, maybe, paths::compare_paths, rel_path::RelPath};
  63use workspace::{
  64    DraggedSelection, OpenInTerminal, OpenOptions, OpenVisible, PreviewTabsSettings, SelectedEntry,
  65    SplitDirection, Workspace,
  66    dock::{DockPosition, Panel, PanelEvent},
  67    notifications::{DetachAndPromptErr, NotifyResultExt, NotifyTaskExt},
  68};
  69use worktree::CreatedEntry;
  70use zed_actions::{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([selection])
1929                    });
1930                });
1931                self.update_visible_entries(None, true, true, window, cx);
1932                cx.notify();
1933            }
1934        }
1935    }
1936
1937    fn rename(&mut self, _: &Rename, window: &mut Window, cx: &mut Context<Self>) {
1938        self.rename_impl(None, window, cx);
1939    }
1940
1941    fn trash(&mut self, action: &Trash, window: &mut Window, cx: &mut Context<Self>) {
1942        self.remove(true, action.skip_prompt, window, cx);
1943    }
1944
1945    fn delete(&mut self, action: &Delete, window: &mut Window, cx: &mut Context<Self>) {
1946        self.remove(false, action.skip_prompt, window, cx);
1947    }
1948
1949    fn remove(
1950        &mut self,
1951        trash: bool,
1952        skip_prompt: bool,
1953        window: &mut Window,
1954        cx: &mut Context<ProjectPanel>,
1955    ) {
1956        maybe!({
1957            let items_to_delete = self.disjoint_entries(cx);
1958            if items_to_delete.is_empty() {
1959                return None;
1960            }
1961            let project = self.project.read(cx);
1962
1963            let mut dirty_buffers = 0;
1964            let file_paths = items_to_delete
1965                .iter()
1966                .filter_map(|selection| {
1967                    let project_path = project.path_for_entry(selection.entry_id, cx)?;
1968                    dirty_buffers +=
1969                        project.dirty_buffers(cx).any(|path| path == project_path) as usize;
1970                    Some((
1971                        selection.entry_id,
1972                        project_path.path.file_name()?.to_string(),
1973                    ))
1974                })
1975                .collect::<Vec<_>>();
1976            if file_paths.is_empty() {
1977                return None;
1978            }
1979            let answer = if !skip_prompt {
1980                let operation = if trash { "Trash" } else { "Delete" };
1981                let prompt = match file_paths.first() {
1982                    Some((_, path)) if file_paths.len() == 1 => {
1983                        let unsaved_warning = if dirty_buffers > 0 {
1984                            "\n\nIt has unsaved changes, which will be lost."
1985                        } else {
1986                            ""
1987                        };
1988
1989                        format!("{operation} {path}?{unsaved_warning}")
1990                    }
1991                    _ => {
1992                        const CUTOFF_POINT: usize = 10;
1993                        let names = if file_paths.len() > CUTOFF_POINT {
1994                            let truncated_path_counts = file_paths.len() - CUTOFF_POINT;
1995                            let mut paths = file_paths
1996                                .iter()
1997                                .map(|(_, path)| path.clone())
1998                                .take(CUTOFF_POINT)
1999                                .collect::<Vec<_>>();
2000                            paths.truncate(CUTOFF_POINT);
2001                            if truncated_path_counts == 1 {
2002                                paths.push(".. 1 file not shown".into());
2003                            } else {
2004                                paths.push(format!(".. {} files not shown", truncated_path_counts));
2005                            }
2006                            paths
2007                        } else {
2008                            file_paths.iter().map(|(_, path)| path.clone()).collect()
2009                        };
2010                        let unsaved_warning = if dirty_buffers == 0 {
2011                            String::new()
2012                        } else if dirty_buffers == 1 {
2013                            "\n\n1 of these has unsaved changes, which will be lost.".to_string()
2014                        } else {
2015                            format!(
2016                                "\n\n{dirty_buffers} of these have unsaved changes, which will be lost."
2017                            )
2018                        };
2019
2020                        format!(
2021                            "Do you want to {} the following {} files?\n{}{unsaved_warning}",
2022                            operation.to_lowercase(),
2023                            file_paths.len(),
2024                            names.join("\n")
2025                        )
2026                    }
2027                };
2028                Some(window.prompt(PromptLevel::Info, &prompt, None, &[operation, "Cancel"], cx))
2029            } else {
2030                None
2031            };
2032            let next_selection = self.find_next_selection_after_deletion(items_to_delete, cx);
2033            cx.spawn_in(window, async move |panel, cx| {
2034                if let Some(answer) = answer
2035                    && answer.await != Ok(0)
2036                {
2037                    return anyhow::Ok(());
2038                }
2039                for (entry_id, _) in file_paths {
2040                    panel
2041                        .update(cx, |panel, cx| {
2042                            panel
2043                                .project
2044                                .update(cx, |project, cx| project.delete_entry(entry_id, trash, cx))
2045                                .context("no such entry")
2046                        })??
2047                        .await?;
2048                }
2049                panel.update_in(cx, |panel, window, cx| {
2050                    if let Some(next_selection) = next_selection {
2051                        panel.update_visible_entries(
2052                            Some((next_selection.worktree_id, next_selection.entry_id)),
2053                            false,
2054                            true,
2055                            window,
2056                            cx,
2057                        );
2058                    } else {
2059                        panel.select_last(&SelectLast {}, window, cx);
2060                    }
2061                })?;
2062                Ok(())
2063            })
2064            .detach_and_log_err(cx);
2065            Some(())
2066        });
2067    }
2068
2069    fn find_next_selection_after_deletion(
2070        &self,
2071        sanitized_entries: BTreeSet<SelectedEntry>,
2072        cx: &mut Context<Self>,
2073    ) -> Option<SelectedEntry> {
2074        if sanitized_entries.is_empty() {
2075            return None;
2076        }
2077        let project = self.project.read(cx);
2078        let (worktree_id, worktree) = sanitized_entries
2079            .iter()
2080            .map(|entry| entry.worktree_id)
2081            .filter_map(|id| project.worktree_for_id(id, cx).map(|w| (id, w.read(cx))))
2082            .max_by(|(_, a), (_, b)| a.root_name().cmp(b.root_name()))?;
2083        let git_store = project.git_store().read(cx);
2084
2085        let marked_entries_in_worktree = sanitized_entries
2086            .iter()
2087            .filter(|e| e.worktree_id == worktree_id)
2088            .collect::<HashSet<_>>();
2089        let latest_entry = marked_entries_in_worktree
2090            .iter()
2091            .max_by(|a, b| {
2092                match (
2093                    worktree.entry_for_id(a.entry_id),
2094                    worktree.entry_for_id(b.entry_id),
2095                ) {
2096                    (Some(a), Some(b)) => compare_paths(
2097                        (a.path.as_std_path(), a.is_file()),
2098                        (b.path.as_std_path(), b.is_file()),
2099                    ),
2100                    _ => cmp::Ordering::Equal,
2101                }
2102            })
2103            .and_then(|e| worktree.entry_for_id(e.entry_id))?;
2104
2105        let parent_path = latest_entry.path.parent()?;
2106        let parent_entry = worktree.entry_for_path(parent_path)?;
2107
2108        // Remove all siblings that are being deleted except the last marked entry
2109        let repo_snapshots = git_store.repo_snapshots(cx);
2110        let worktree_snapshot = worktree.snapshot();
2111        let hide_gitignore = ProjectPanelSettings::get_global(cx).hide_gitignore;
2112        let mut siblings: Vec<_> =
2113            ChildEntriesGitIter::new(&repo_snapshots, &worktree_snapshot, parent_path)
2114                .filter(|sibling| {
2115                    (sibling.id == latest_entry.id)
2116                        || (!marked_entries_in_worktree.contains(&&SelectedEntry {
2117                            worktree_id,
2118                            entry_id: sibling.id,
2119                        }) && (!hide_gitignore || !sibling.is_ignored))
2120                })
2121                .map(|entry| entry.to_owned())
2122                .collect();
2123
2124        let mode = ProjectPanelSettings::get_global(cx).sort_mode;
2125        sort_worktree_entries_with_mode(&mut siblings, mode);
2126        let sibling_entry_index = siblings
2127            .iter()
2128            .position(|sibling| sibling.id == latest_entry.id)?;
2129
2130        if let Some(next_sibling) = sibling_entry_index
2131            .checked_add(1)
2132            .and_then(|i| siblings.get(i))
2133        {
2134            return Some(SelectedEntry {
2135                worktree_id,
2136                entry_id: next_sibling.id,
2137            });
2138        }
2139        if let Some(prev_sibling) = sibling_entry_index
2140            .checked_sub(1)
2141            .and_then(|i| siblings.get(i))
2142        {
2143            return Some(SelectedEntry {
2144                worktree_id,
2145                entry_id: prev_sibling.id,
2146            });
2147        }
2148        // No neighbour sibling found, fall back to parent
2149        Some(SelectedEntry {
2150            worktree_id,
2151            entry_id: parent_entry.id,
2152        })
2153    }
2154
2155    fn unfold_directory(
2156        &mut self,
2157        _: &UnfoldDirectory,
2158        window: &mut Window,
2159        cx: &mut Context<Self>,
2160    ) {
2161        if let Some((worktree, entry)) = self.selected_entry(cx) {
2162            self.state.unfolded_dir_ids.insert(entry.id);
2163
2164            let snapshot = worktree.snapshot();
2165            let mut parent_path = entry.path.parent();
2166            while let Some(path) = parent_path {
2167                if let Some(parent_entry) = worktree.entry_for_path(path) {
2168                    let mut children_iter = snapshot.child_entries(path);
2169
2170                    if children_iter.by_ref().take(2).count() > 1 {
2171                        break;
2172                    }
2173
2174                    self.state.unfolded_dir_ids.insert(parent_entry.id);
2175                    parent_path = path.parent();
2176                } else {
2177                    break;
2178                }
2179            }
2180
2181            self.update_visible_entries(None, false, true, window, cx);
2182            cx.notify();
2183        }
2184    }
2185
2186    fn fold_directory(&mut self, _: &FoldDirectory, window: &mut Window, cx: &mut Context<Self>) {
2187        if let Some((worktree, entry)) = self.selected_entry(cx) {
2188            self.state.unfolded_dir_ids.remove(&entry.id);
2189
2190            let snapshot = worktree.snapshot();
2191            let mut path = &*entry.path;
2192            loop {
2193                let mut child_entries_iter = snapshot.child_entries(path);
2194                if let Some(child) = child_entries_iter.next() {
2195                    if child_entries_iter.next().is_none() && child.is_dir() {
2196                        self.state.unfolded_dir_ids.remove(&child.id);
2197                        path = &*child.path;
2198                    } else {
2199                        break;
2200                    }
2201                } else {
2202                    break;
2203                }
2204            }
2205
2206            self.update_visible_entries(None, false, true, window, cx);
2207            cx.notify();
2208        }
2209    }
2210
2211    fn scroll_up(&mut self, _: &ScrollUp, window: &mut Window, cx: &mut Context<Self>) {
2212        for _ in 0..self.rendered_entries_len / 2 {
2213            window.dispatch_action(SelectPrevious.boxed_clone(), cx);
2214        }
2215    }
2216
2217    fn scroll_down(&mut self, _: &ScrollDown, window: &mut Window, cx: &mut Context<Self>) {
2218        for _ in 0..self.rendered_entries_len / 2 {
2219            window.dispatch_action(SelectNext.boxed_clone(), cx);
2220        }
2221    }
2222
2223    fn scroll_cursor_center(
2224        &mut self,
2225        _: &ScrollCursorCenter,
2226        _: &mut Window,
2227        cx: &mut Context<Self>,
2228    ) {
2229        if let Some((_, _, index)) = self
2230            .state
2231            .selection
2232            .and_then(|s| self.index_for_selection(s))
2233        {
2234            self.scroll_handle
2235                .scroll_to_item_strict(index, ScrollStrategy::Center);
2236            cx.notify();
2237        }
2238    }
2239
2240    fn scroll_cursor_top(&mut self, _: &ScrollCursorTop, _: &mut Window, cx: &mut Context<Self>) {
2241        if let Some((_, _, index)) = self
2242            .state
2243            .selection
2244            .and_then(|s| self.index_for_selection(s))
2245        {
2246            self.scroll_handle
2247                .scroll_to_item_strict(index, ScrollStrategy::Top);
2248            cx.notify();
2249        }
2250    }
2251
2252    fn scroll_cursor_bottom(
2253        &mut self,
2254        _: &ScrollCursorBottom,
2255        _: &mut Window,
2256        cx: &mut Context<Self>,
2257    ) {
2258        if let Some((_, _, index)) = self
2259            .state
2260            .selection
2261            .and_then(|s| self.index_for_selection(s))
2262        {
2263            self.scroll_handle
2264                .scroll_to_item_strict(index, ScrollStrategy::Bottom);
2265            cx.notify();
2266        }
2267    }
2268
2269    fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
2270        if let Some(edit_state) = &self.state.edit_state
2271            && edit_state.processing_filename.is_none()
2272        {
2273            self.filename_editor.update(cx, |editor, cx| {
2274                editor.move_to_end_of_line(
2275                    &editor::actions::MoveToEndOfLine {
2276                        stop_at_soft_wraps: false,
2277                    },
2278                    window,
2279                    cx,
2280                );
2281            });
2282            return;
2283        }
2284        if let Some(selection) = self.state.selection {
2285            let (mut worktree_ix, mut entry_ix, _) =
2286                self.index_for_selection(selection).unwrap_or_default();
2287            if let Some(worktree_entries) = self
2288                .state
2289                .visible_entries
2290                .get(worktree_ix)
2291                .map(|v| &v.entries)
2292            {
2293                if entry_ix + 1 < worktree_entries.len() {
2294                    entry_ix += 1;
2295                } else {
2296                    worktree_ix += 1;
2297                    entry_ix = 0;
2298                }
2299            }
2300
2301            if let Some(VisibleEntriesForWorktree {
2302                worktree_id,
2303                entries,
2304                ..
2305            }) = self.state.visible_entries.get(worktree_ix)
2306                && let Some(entry) = entries.get(entry_ix)
2307            {
2308                let selection = SelectedEntry {
2309                    worktree_id: *worktree_id,
2310                    entry_id: entry.id,
2311                };
2312                self.state.selection = Some(selection);
2313                if window.modifiers().shift {
2314                    self.marked_entries.push(selection);
2315                }
2316
2317                self.autoscroll(cx);
2318                cx.notify();
2319            }
2320        } else {
2321            self.select_first(&SelectFirst {}, window, cx);
2322        }
2323    }
2324
2325    fn select_prev_diagnostic(
2326        &mut self,
2327        action: &SelectPrevDiagnostic,
2328        window: &mut Window,
2329        cx: &mut Context<Self>,
2330    ) {
2331        let selection = self.find_entry(
2332            self.state.selection.as_ref(),
2333            true,
2334            |entry, worktree_id| {
2335                self.state.selection.is_none_or(|selection| {
2336                    if selection.worktree_id == worktree_id {
2337                        selection.entry_id != entry.id
2338                    } else {
2339                        true
2340                    }
2341                }) && entry.is_file()
2342                    && self
2343                        .diagnostics
2344                        .get(&(worktree_id, entry.path.clone()))
2345                        .is_some_and(|severity| action.severity.matches(*severity))
2346            },
2347            cx,
2348        );
2349
2350        if let Some(selection) = selection {
2351            self.state.selection = Some(selection);
2352            self.expand_entry(selection.worktree_id, selection.entry_id, cx);
2353            self.update_visible_entries(
2354                Some((selection.worktree_id, selection.entry_id)),
2355                false,
2356                true,
2357                window,
2358                cx,
2359            );
2360            cx.notify();
2361        }
2362    }
2363
2364    fn select_next_diagnostic(
2365        &mut self,
2366        action: &SelectNextDiagnostic,
2367        window: &mut Window,
2368        cx: &mut Context<Self>,
2369    ) {
2370        let selection = self.find_entry(
2371            self.state.selection.as_ref(),
2372            false,
2373            |entry, worktree_id| {
2374                self.state.selection.is_none_or(|selection| {
2375                    if selection.worktree_id == worktree_id {
2376                        selection.entry_id != entry.id
2377                    } else {
2378                        true
2379                    }
2380                }) && entry.is_file()
2381                    && self
2382                        .diagnostics
2383                        .get(&(worktree_id, entry.path.clone()))
2384                        .is_some_and(|severity| action.severity.matches(*severity))
2385            },
2386            cx,
2387        );
2388
2389        if let Some(selection) = selection {
2390            self.state.selection = Some(selection);
2391            self.expand_entry(selection.worktree_id, selection.entry_id, cx);
2392            self.update_visible_entries(
2393                Some((selection.worktree_id, selection.entry_id)),
2394                false,
2395                true,
2396                window,
2397                cx,
2398            );
2399            cx.notify();
2400        }
2401    }
2402
2403    fn select_prev_git_entry(
2404        &mut self,
2405        _: &SelectPrevGitEntry,
2406        window: &mut Window,
2407        cx: &mut Context<Self>,
2408    ) {
2409        let selection = self.find_entry(
2410            self.state.selection.as_ref(),
2411            true,
2412            |entry, worktree_id| {
2413                (self.state.selection.is_none()
2414                    || self.state.selection.is_some_and(|selection| {
2415                        if selection.worktree_id == worktree_id {
2416                            selection.entry_id != entry.id
2417                        } else {
2418                            true
2419                        }
2420                    }))
2421                    && entry.is_file()
2422                    && entry.git_summary.index.modified + entry.git_summary.worktree.modified > 0
2423            },
2424            cx,
2425        );
2426
2427        if let Some(selection) = selection {
2428            self.state.selection = Some(selection);
2429            self.expand_entry(selection.worktree_id, selection.entry_id, cx);
2430            self.update_visible_entries(
2431                Some((selection.worktree_id, selection.entry_id)),
2432                false,
2433                true,
2434                window,
2435                cx,
2436            );
2437            cx.notify();
2438        }
2439    }
2440
2441    fn select_prev_directory(
2442        &mut self,
2443        _: &SelectPrevDirectory,
2444        _: &mut Window,
2445        cx: &mut Context<Self>,
2446    ) {
2447        let selection = self.find_visible_entry(
2448            self.state.selection.as_ref(),
2449            true,
2450            |entry, worktree_id| {
2451                self.state.selection.is_none_or(|selection| {
2452                    if selection.worktree_id == worktree_id {
2453                        selection.entry_id != entry.id
2454                    } else {
2455                        true
2456                    }
2457                }) && entry.is_dir()
2458            },
2459            cx,
2460        );
2461
2462        if let Some(selection) = selection {
2463            self.state.selection = Some(selection);
2464            self.autoscroll(cx);
2465            cx.notify();
2466        }
2467    }
2468
2469    fn select_next_directory(
2470        &mut self,
2471        _: &SelectNextDirectory,
2472        _: &mut Window,
2473        cx: &mut Context<Self>,
2474    ) {
2475        let selection = self.find_visible_entry(
2476            self.state.selection.as_ref(),
2477            false,
2478            |entry, worktree_id| {
2479                self.state.selection.is_none_or(|selection| {
2480                    if selection.worktree_id == worktree_id {
2481                        selection.entry_id != entry.id
2482                    } else {
2483                        true
2484                    }
2485                }) && entry.is_dir()
2486            },
2487            cx,
2488        );
2489
2490        if let Some(selection) = selection {
2491            self.state.selection = Some(selection);
2492            self.autoscroll(cx);
2493            cx.notify();
2494        }
2495    }
2496
2497    fn select_next_git_entry(
2498        &mut self,
2499        _: &SelectNextGitEntry,
2500        window: &mut Window,
2501        cx: &mut Context<Self>,
2502    ) {
2503        let selection = self.find_entry(
2504            self.state.selection.as_ref(),
2505            false,
2506            |entry, worktree_id| {
2507                self.state.selection.is_none_or(|selection| {
2508                    if selection.worktree_id == worktree_id {
2509                        selection.entry_id != entry.id
2510                    } else {
2511                        true
2512                    }
2513                }) && entry.is_file()
2514                    && entry.git_summary.index.modified + entry.git_summary.worktree.modified > 0
2515            },
2516            cx,
2517        );
2518
2519        if let Some(selection) = selection {
2520            self.state.selection = Some(selection);
2521            self.expand_entry(selection.worktree_id, selection.entry_id, cx);
2522            self.update_visible_entries(
2523                Some((selection.worktree_id, selection.entry_id)),
2524                false,
2525                true,
2526                window,
2527                cx,
2528            );
2529            cx.notify();
2530        }
2531    }
2532
2533    fn select_parent(&mut self, _: &SelectParent, window: &mut Window, cx: &mut Context<Self>) {
2534        if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
2535            if let Some(parent) = entry.path.parent() {
2536                let worktree = worktree.read(cx);
2537                if let Some(parent_entry) = worktree.entry_for_path(parent) {
2538                    self.state.selection = Some(SelectedEntry {
2539                        worktree_id: worktree.id(),
2540                        entry_id: parent_entry.id,
2541                    });
2542                    self.autoscroll(cx);
2543                    cx.notify();
2544                }
2545            }
2546        } else {
2547            self.select_first(&SelectFirst {}, window, cx);
2548        }
2549    }
2550
2551    fn select_first(&mut self, _: &SelectFirst, window: &mut Window, cx: &mut Context<Self>) {
2552        if let Some(VisibleEntriesForWorktree {
2553            worktree_id,
2554            entries,
2555            ..
2556        }) = self.state.visible_entries.first()
2557            && let Some(entry) = entries.first()
2558        {
2559            let selection = SelectedEntry {
2560                worktree_id: *worktree_id,
2561                entry_id: entry.id,
2562            };
2563            self.state.selection = Some(selection);
2564            if window.modifiers().shift {
2565                self.marked_entries.push(selection);
2566            }
2567            self.autoscroll(cx);
2568            cx.notify();
2569        }
2570    }
2571
2572    fn select_last(&mut self, _: &SelectLast, _: &mut Window, cx: &mut Context<Self>) {
2573        if let Some(VisibleEntriesForWorktree {
2574            worktree_id,
2575            entries,
2576            ..
2577        }) = self.state.visible_entries.last()
2578        {
2579            let worktree = self.project.read(cx).worktree_for_id(*worktree_id, cx);
2580            if let (Some(worktree), Some(entry)) = (worktree, entries.last()) {
2581                let worktree = worktree.read(cx);
2582                if let Some(entry) = worktree.entry_for_id(entry.id) {
2583                    let selection = SelectedEntry {
2584                        worktree_id: *worktree_id,
2585                        entry_id: entry.id,
2586                    };
2587                    self.state.selection = Some(selection);
2588                    self.autoscroll(cx);
2589                    cx.notify();
2590                }
2591            }
2592        }
2593    }
2594
2595    fn autoscroll(&mut self, cx: &mut Context<Self>) {
2596        if let Some((_, _, index)) = self
2597            .state
2598            .selection
2599            .and_then(|s| self.index_for_selection(s))
2600        {
2601            self.scroll_handle.scroll_to_item_with_offset(
2602                index,
2603                ScrollStrategy::Center,
2604                self.sticky_items_count,
2605            );
2606            cx.notify();
2607        }
2608    }
2609
2610    fn cut(&mut self, _: &Cut, _: &mut Window, cx: &mut Context<Self>) {
2611        let entries = self.disjoint_entries(cx);
2612        if !entries.is_empty() {
2613            self.clipboard = Some(ClipboardEntry::Cut(entries));
2614            cx.notify();
2615        }
2616    }
2617
2618    fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context<Self>) {
2619        let entries = self.disjoint_entries(cx);
2620        if !entries.is_empty() {
2621            self.clipboard = Some(ClipboardEntry::Copied(entries));
2622            cx.notify();
2623        }
2624    }
2625
2626    fn create_paste_path(
2627        &self,
2628        source: &SelectedEntry,
2629        (worktree, target_entry): (Entity<Worktree>, &Entry),
2630        cx: &App,
2631    ) -> Option<(Arc<RelPath>, Option<Range<usize>>)> {
2632        let mut new_path = target_entry.path.to_rel_path_buf();
2633        // If we're pasting into a file, or a directory into itself, go up one level.
2634        if target_entry.is_file() || (target_entry.is_dir() && target_entry.id == source.entry_id) {
2635            new_path.pop();
2636        }
2637        let clipboard_entry_file_name = self
2638            .project
2639            .read(cx)
2640            .path_for_entry(source.entry_id, cx)?
2641            .path
2642            .file_name()?
2643            .to_string();
2644        new_path.push(RelPath::unix(&clipboard_entry_file_name).unwrap());
2645        let extension = new_path.extension().map(|s| s.to_string());
2646        let file_name_without_extension = new_path.file_stem()?.to_string();
2647        let file_name_len = file_name_without_extension.len();
2648        let mut disambiguation_range = None;
2649        let mut ix = 0;
2650        {
2651            let worktree = worktree.read(cx);
2652            while worktree.entry_for_path(&new_path).is_some() {
2653                new_path.pop();
2654
2655                let mut new_file_name = file_name_without_extension.to_string();
2656
2657                let disambiguation = " copy";
2658                let mut disambiguation_len = disambiguation.len();
2659
2660                new_file_name.push_str(disambiguation);
2661
2662                if ix > 0 {
2663                    let extra_disambiguation = format!(" {}", ix);
2664                    disambiguation_len += extra_disambiguation.len();
2665                    new_file_name.push_str(&extra_disambiguation);
2666                }
2667                if let Some(extension) = extension.as_ref() {
2668                    new_file_name.push_str(".");
2669                    new_file_name.push_str(extension);
2670                }
2671
2672                new_path.push(RelPath::unix(&new_file_name).unwrap());
2673
2674                disambiguation_range = Some(file_name_len..(file_name_len + disambiguation_len));
2675                ix += 1;
2676            }
2677        }
2678        Some((new_path.as_rel_path().into(), disambiguation_range))
2679    }
2680
2681    fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
2682        maybe!({
2683            let (worktree, entry) = self.selected_entry_handle(cx)?;
2684            let entry = entry.clone();
2685            let worktree_id = worktree.read(cx).id();
2686            let clipboard_entries = self
2687                .clipboard
2688                .as_ref()
2689                .filter(|clipboard| !clipboard.items().is_empty())?;
2690
2691            enum PasteTask {
2692                Rename(Task<Result<CreatedEntry>>),
2693                Copy(Task<Result<Option<Entry>>>),
2694            }
2695
2696            let mut paste_tasks = Vec::new();
2697            let mut disambiguation_range = None;
2698            let clip_is_cut = clipboard_entries.is_cut();
2699            for clipboard_entry in clipboard_entries.items() {
2700                let (new_path, new_disambiguation_range) =
2701                    self.create_paste_path(clipboard_entry, self.selected_sub_entry(cx)?, cx)?;
2702                let clip_entry_id = clipboard_entry.entry_id;
2703                let task = if clipboard_entries.is_cut() {
2704                    let task = self.project.update(cx, |project, cx| {
2705                        project.rename_entry(clip_entry_id, (worktree_id, new_path).into(), cx)
2706                    });
2707                    PasteTask::Rename(task)
2708                } else {
2709                    let task = self.project.update(cx, |project, cx| {
2710                        project.copy_entry(clip_entry_id, (worktree_id, new_path).into(), cx)
2711                    });
2712                    PasteTask::Copy(task)
2713                };
2714                paste_tasks.push(task);
2715                disambiguation_range = new_disambiguation_range.or(disambiguation_range);
2716            }
2717
2718            let item_count = paste_tasks.len();
2719
2720            cx.spawn_in(window, async move |project_panel, cx| {
2721                let mut last_succeed = None;
2722                for task in paste_tasks {
2723                    match task {
2724                        PasteTask::Rename(task) => {
2725                            if let Some(CreatedEntry::Included(entry)) =
2726                                task.await.notify_async_err(cx)
2727                            {
2728                                last_succeed = Some(entry);
2729                            }
2730                        }
2731                        PasteTask::Copy(task) => {
2732                            if let Some(Some(entry)) = task.await.notify_async_err(cx) {
2733                                last_succeed = Some(entry);
2734                            }
2735                        }
2736                    }
2737                }
2738                // update selection
2739                if let Some(entry) = last_succeed {
2740                    project_panel
2741                        .update_in(cx, |project_panel, window, cx| {
2742                            project_panel.state.selection = Some(SelectedEntry {
2743                                worktree_id,
2744                                entry_id: entry.id,
2745                            });
2746
2747                            if item_count == 1 {
2748                                // open entry if not dir, setting is enabled, and only focus if rename is not pending
2749                                if !entry.is_dir() {
2750                                    let settings = ProjectPanelSettings::get_global(cx);
2751                                    if settings.auto_open.should_open_on_paste() {
2752                                        project_panel.open_entry(
2753                                            entry.id,
2754                                            disambiguation_range.is_none(),
2755                                            false,
2756                                            cx,
2757                                        );
2758                                    }
2759                                }
2760
2761                                // if only one entry was pasted and it was disambiguated, open the rename editor
2762                                if disambiguation_range.is_some() {
2763                                    cx.defer_in(window, |this, window, cx| {
2764                                        this.rename_impl(disambiguation_range, window, cx);
2765                                    });
2766                                }
2767                            }
2768                        })
2769                        .ok();
2770                }
2771
2772                anyhow::Ok(())
2773            })
2774            .detach_and_log_err(cx);
2775
2776            if clip_is_cut {
2777                // Convert the clipboard cut entry to a copy entry after the first paste.
2778                self.clipboard = self.clipboard.take().map(ClipboardEntry::into_copy_entry);
2779            }
2780
2781            self.expand_entry(worktree_id, entry.id, cx);
2782            Some(())
2783        });
2784    }
2785
2786    fn duplicate(&mut self, _: &Duplicate, window: &mut Window, cx: &mut Context<Self>) {
2787        self.copy(&Copy {}, window, cx);
2788        self.paste(&Paste {}, window, cx);
2789    }
2790
2791    fn copy_path(
2792        &mut self,
2793        _: &zed_actions::workspace::CopyPath,
2794        _: &mut Window,
2795        cx: &mut Context<Self>,
2796    ) {
2797        let abs_file_paths = {
2798            let project = self.project.read(cx);
2799            self.effective_entries()
2800                .into_iter()
2801                .filter_map(|entry| {
2802                    let entry_path = project.path_for_entry(entry.entry_id, cx)?.path;
2803                    Some(
2804                        project
2805                            .worktree_for_id(entry.worktree_id, cx)?
2806                            .read(cx)
2807                            .absolutize(&entry_path)
2808                            .to_string_lossy()
2809                            .to_string(),
2810                    )
2811                })
2812                .collect::<Vec<_>>()
2813        };
2814        if !abs_file_paths.is_empty() {
2815            cx.write_to_clipboard(ClipboardItem::new_string(abs_file_paths.join("\n")));
2816        }
2817    }
2818
2819    fn copy_relative_path(
2820        &mut self,
2821        _: &zed_actions::workspace::CopyRelativePath,
2822        _: &mut Window,
2823        cx: &mut Context<Self>,
2824    ) {
2825        let path_style = self.project.read(cx).path_style(cx);
2826        let file_paths = {
2827            let project = self.project.read(cx);
2828            self.effective_entries()
2829                .into_iter()
2830                .filter_map(|entry| {
2831                    Some(
2832                        project
2833                            .path_for_entry(entry.entry_id, cx)?
2834                            .path
2835                            .display(path_style)
2836                            .into_owned(),
2837                    )
2838                })
2839                .collect::<Vec<_>>()
2840        };
2841        if !file_paths.is_empty() {
2842            cx.write_to_clipboard(ClipboardItem::new_string(file_paths.join("\n")));
2843        }
2844    }
2845
2846    fn reveal_in_finder(
2847        &mut self,
2848        _: &RevealInFileManager,
2849        _: &mut Window,
2850        cx: &mut Context<Self>,
2851    ) {
2852        if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
2853            cx.reveal_path(&worktree.read(cx).absolutize(&entry.path));
2854        }
2855    }
2856
2857    fn remove_from_project(
2858        &mut self,
2859        _: &RemoveFromProject,
2860        _window: &mut Window,
2861        cx: &mut Context<Self>,
2862    ) {
2863        for entry in self.effective_entries().iter() {
2864            let worktree_id = entry.worktree_id;
2865            self.project
2866                .update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
2867        }
2868    }
2869
2870    fn file_abs_paths_to_diff(&self, cx: &Context<Self>) -> Option<(PathBuf, PathBuf)> {
2871        let mut selections_abs_path = self
2872            .marked_entries
2873            .iter()
2874            .filter_map(|entry| {
2875                let project = self.project.read(cx);
2876                let worktree = project.worktree_for_id(entry.worktree_id, cx)?;
2877                let entry = worktree.read(cx).entry_for_id(entry.entry_id)?;
2878                if !entry.is_file() {
2879                    return None;
2880                }
2881                Some(worktree.read(cx).absolutize(&entry.path))
2882            })
2883            .rev();
2884
2885        let last_path = selections_abs_path.next()?;
2886        let previous_to_last = selections_abs_path.next()?;
2887        Some((previous_to_last, last_path))
2888    }
2889
2890    fn compare_marked_files(
2891        &mut self,
2892        _: &CompareMarkedFiles,
2893        window: &mut Window,
2894        cx: &mut Context<Self>,
2895    ) {
2896        let selected_files = self.file_abs_paths_to_diff(cx);
2897        if let Some((file_path1, file_path2)) = selected_files {
2898            self.workspace
2899                .update(cx, |workspace, cx| {
2900                    FileDiffView::open(file_path1, file_path2, workspace, window, cx)
2901                        .detach_and_log_err(cx);
2902                })
2903                .ok();
2904        }
2905    }
2906
2907    fn open_system(&mut self, _: &OpenWithSystem, _: &mut Window, cx: &mut Context<Self>) {
2908        if let Some((worktree, entry)) = self.selected_entry(cx) {
2909            let abs_path = worktree.absolutize(&entry.path);
2910            cx.open_with_system(&abs_path);
2911        }
2912    }
2913
2914    fn open_in_terminal(
2915        &mut self,
2916        _: &OpenInTerminal,
2917        window: &mut Window,
2918        cx: &mut Context<Self>,
2919    ) {
2920        if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
2921            let abs_path = match &entry.canonical_path {
2922                Some(canonical_path) => canonical_path.to_path_buf(),
2923                None => worktree.read(cx).absolutize(&entry.path),
2924            };
2925
2926            let working_directory = if entry.is_dir() {
2927                Some(abs_path)
2928            } else {
2929                abs_path.parent().map(|path| path.to_path_buf())
2930            };
2931            if let Some(working_directory) = working_directory {
2932                window.dispatch_action(
2933                    workspace::OpenTerminal { working_directory }.boxed_clone(),
2934                    cx,
2935                )
2936            }
2937        }
2938    }
2939
2940    pub fn new_search_in_directory(
2941        &mut self,
2942        _: &NewSearchInDirectory,
2943        window: &mut Window,
2944        cx: &mut Context<Self>,
2945    ) {
2946        if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
2947            let dir_path = if entry.is_dir() {
2948                entry.path.clone()
2949            } else {
2950                // entry is a file, use its parent directory
2951                match entry.path.parent() {
2952                    Some(parent) => Arc::from(parent),
2953                    None => {
2954                        // File at root, open search with empty filter
2955                        self.workspace
2956                            .update(cx, |workspace, cx| {
2957                                search::ProjectSearchView::new_search_in_directory(
2958                                    workspace,
2959                                    RelPath::empty(),
2960                                    window,
2961                                    cx,
2962                                );
2963                            })
2964                            .ok();
2965                        return;
2966                    }
2967                }
2968            };
2969
2970            let include_root = self.project.read(cx).visible_worktrees(cx).count() > 1;
2971            let dir_path = if include_root {
2972                worktree.read(cx).root_name().join(&dir_path)
2973            } else {
2974                dir_path
2975            };
2976
2977            self.workspace
2978                .update(cx, |workspace, cx| {
2979                    search::ProjectSearchView::new_search_in_directory(
2980                        workspace, &dir_path, window, cx,
2981                    );
2982                })
2983                .ok();
2984        }
2985    }
2986
2987    fn move_entry(
2988        &mut self,
2989        entry_to_move: ProjectEntryId,
2990        destination: ProjectEntryId,
2991        destination_is_file: bool,
2992        cx: &mut Context<Self>,
2993    ) {
2994        if self
2995            .project
2996            .read(cx)
2997            .entry_is_worktree_root(entry_to_move, cx)
2998        {
2999            self.move_worktree_root(entry_to_move, destination, cx)
3000        } else {
3001            self.move_worktree_entry(entry_to_move, destination, destination_is_file, cx)
3002        }
3003    }
3004
3005    fn move_worktree_root(
3006        &mut self,
3007        entry_to_move: ProjectEntryId,
3008        destination: ProjectEntryId,
3009        cx: &mut Context<Self>,
3010    ) {
3011        self.project.update(cx, |project, cx| {
3012            let Some(worktree_to_move) = project.worktree_for_entry(entry_to_move, cx) else {
3013                return;
3014            };
3015            let Some(destination_worktree) = project.worktree_for_entry(destination, cx) else {
3016                return;
3017            };
3018
3019            let worktree_id = worktree_to_move.read(cx).id();
3020            let destination_id = destination_worktree.read(cx).id();
3021
3022            project
3023                .move_worktree(worktree_id, destination_id, cx)
3024                .log_err();
3025        });
3026    }
3027
3028    fn move_worktree_entry(
3029        &mut self,
3030        entry_to_move: ProjectEntryId,
3031        destination_entry: ProjectEntryId,
3032        destination_is_file: bool,
3033        cx: &mut Context<Self>,
3034    ) {
3035        if entry_to_move == destination_entry {
3036            return;
3037        }
3038
3039        let destination_worktree = self.project.update(cx, |project, cx| {
3040            let source_path = project.path_for_entry(entry_to_move, cx)?;
3041            let destination_path = project.path_for_entry(destination_entry, cx)?;
3042            let destination_worktree_id = destination_path.worktree_id;
3043
3044            let mut destination_path = destination_path.path.as_ref();
3045            if destination_is_file {
3046                destination_path = destination_path.parent()?;
3047            }
3048
3049            let mut new_path = destination_path.to_rel_path_buf();
3050            new_path.push(RelPath::unix(source_path.path.file_name()?).unwrap());
3051            if new_path.as_rel_path() != source_path.path.as_ref() {
3052                let task = project.rename_entry(
3053                    entry_to_move,
3054                    (destination_worktree_id, new_path).into(),
3055                    cx,
3056                );
3057                cx.foreground_executor().spawn(task).detach_and_log_err(cx);
3058            }
3059
3060            project.worktree_id_for_entry(destination_entry, cx)
3061        });
3062
3063        if let Some(destination_worktree) = destination_worktree {
3064            self.expand_entry(destination_worktree, destination_entry, cx);
3065        }
3066    }
3067
3068    fn index_for_selection(&self, selection: SelectedEntry) -> Option<(usize, usize, usize)> {
3069        self.index_for_entry(selection.entry_id, selection.worktree_id)
3070    }
3071
3072    fn disjoint_entries(&self, cx: &App) -> BTreeSet<SelectedEntry> {
3073        let marked_entries = self.effective_entries();
3074        let mut sanitized_entries = BTreeSet::new();
3075        if marked_entries.is_empty() {
3076            return sanitized_entries;
3077        }
3078
3079        let project = self.project.read(cx);
3080        let marked_entries_by_worktree: HashMap<WorktreeId, Vec<SelectedEntry>> = marked_entries
3081            .into_iter()
3082            .filter(|entry| !project.entry_is_worktree_root(entry.entry_id, cx))
3083            .fold(HashMap::default(), |mut map, entry| {
3084                map.entry(entry.worktree_id).or_default().push(entry);
3085                map
3086            });
3087
3088        for (worktree_id, marked_entries) in marked_entries_by_worktree {
3089            if let Some(worktree) = project.worktree_for_id(worktree_id, cx) {
3090                let worktree = worktree.read(cx);
3091                let marked_dir_paths = marked_entries
3092                    .iter()
3093                    .filter_map(|entry| {
3094                        worktree.entry_for_id(entry.entry_id).and_then(|entry| {
3095                            if entry.is_dir() {
3096                                Some(entry.path.as_ref())
3097                            } else {
3098                                None
3099                            }
3100                        })
3101                    })
3102                    .collect::<BTreeSet<_>>();
3103
3104                sanitized_entries.extend(marked_entries.into_iter().filter(|entry| {
3105                    let Some(entry_info) = worktree.entry_for_id(entry.entry_id) else {
3106                        return false;
3107                    };
3108                    let entry_path = entry_info.path.as_ref();
3109                    let inside_marked_dir = marked_dir_paths.iter().any(|&marked_dir_path| {
3110                        entry_path != marked_dir_path && entry_path.starts_with(marked_dir_path)
3111                    });
3112                    !inside_marked_dir
3113                }));
3114            }
3115        }
3116
3117        sanitized_entries
3118    }
3119
3120    fn effective_entries(&self) -> BTreeSet<SelectedEntry> {
3121        if let Some(selection) = self.state.selection {
3122            let selection = SelectedEntry {
3123                entry_id: self.resolve_entry(selection.entry_id),
3124                worktree_id: selection.worktree_id,
3125            };
3126
3127            // Default to using just the selected item when nothing is marked.
3128            if self.marked_entries.is_empty() {
3129                return BTreeSet::from([selection]);
3130            }
3131
3132            // Allow operating on the selected item even when something else is marked,
3133            // making it easier to perform one-off actions without clearing a mark.
3134            if self.marked_entries.len() == 1 && !self.marked_entries.contains(&selection) {
3135                return BTreeSet::from([selection]);
3136            }
3137        }
3138
3139        // Return only marked entries since we've already handled special cases where
3140        // only selection should take precedence. At this point, marked entries may or
3141        // may not include the current selection, which is intentional.
3142        self.marked_entries
3143            .iter()
3144            .map(|entry| SelectedEntry {
3145                entry_id: self.resolve_entry(entry.entry_id),
3146                worktree_id: entry.worktree_id,
3147            })
3148            .collect::<BTreeSet<_>>()
3149    }
3150
3151    /// Finds the currently selected subentry for a given leaf entry id. If a given entry
3152    /// has no ancestors, the project entry ID that's passed in is returned as-is.
3153    fn resolve_entry(&self, id: ProjectEntryId) -> ProjectEntryId {
3154        self.state
3155            .ancestors
3156            .get(&id)
3157            .and_then(|ancestors| ancestors.active_ancestor())
3158            .unwrap_or(id)
3159    }
3160
3161    pub fn selected_entry<'a>(&self, cx: &'a App) -> Option<(&'a Worktree, &'a project::Entry)> {
3162        let (worktree, entry) = self.selected_entry_handle(cx)?;
3163        Some((worktree.read(cx), entry))
3164    }
3165
3166    /// Compared to selected_entry, this function resolves to the currently
3167    /// selected subentry if dir auto-folding is enabled.
3168    fn selected_sub_entry<'a>(
3169        &self,
3170        cx: &'a App,
3171    ) -> Option<(Entity<Worktree>, &'a project::Entry)> {
3172        let (worktree, mut entry) = self.selected_entry_handle(cx)?;
3173
3174        let resolved_id = self.resolve_entry(entry.id);
3175        if resolved_id != entry.id {
3176            let worktree = worktree.read(cx);
3177            entry = worktree.entry_for_id(resolved_id)?;
3178        }
3179        Some((worktree, entry))
3180    }
3181    fn selected_entry_handle<'a>(
3182        &self,
3183        cx: &'a App,
3184    ) -> Option<(Entity<Worktree>, &'a project::Entry)> {
3185        let selection = self.state.selection?;
3186        let project = self.project.read(cx);
3187        let worktree = project.worktree_for_id(selection.worktree_id, cx)?;
3188        let entry = worktree.read(cx).entry_for_id(selection.entry_id)?;
3189        Some((worktree, entry))
3190    }
3191
3192    fn expand_to_selection(&mut self, cx: &mut Context<Self>) -> Option<()> {
3193        let (worktree, entry) = self.selected_entry(cx)?;
3194        let expanded_dir_ids = self
3195            .state
3196            .expanded_dir_ids
3197            .entry(worktree.id())
3198            .or_default();
3199
3200        for path in entry.path.ancestors() {
3201            let Some(entry) = worktree.entry_for_path(path) else {
3202                continue;
3203            };
3204            if entry.is_dir()
3205                && let Err(idx) = expanded_dir_ids.binary_search(&entry.id)
3206            {
3207                expanded_dir_ids.insert(idx, entry.id);
3208            }
3209        }
3210
3211        Some(())
3212    }
3213
3214    fn create_new_git_entry(
3215        parent_entry: &Entry,
3216        git_summary: GitSummary,
3217        new_entry_kind: EntryKind,
3218    ) -> GitEntry {
3219        GitEntry {
3220            entry: Entry {
3221                id: NEW_ENTRY_ID,
3222                kind: new_entry_kind,
3223                path: parent_entry.path.join(RelPath::unix("\0").unwrap()),
3224                inode: 0,
3225                mtime: parent_entry.mtime,
3226                size: parent_entry.size,
3227                is_ignored: parent_entry.is_ignored,
3228                is_hidden: parent_entry.is_hidden,
3229                is_external: false,
3230                is_private: false,
3231                is_always_included: parent_entry.is_always_included,
3232                canonical_path: parent_entry.canonical_path.clone(),
3233                char_bag: parent_entry.char_bag,
3234                is_fifo: parent_entry.is_fifo,
3235            },
3236            git_summary,
3237        }
3238    }
3239
3240    fn update_visible_entries(
3241        &mut self,
3242        new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,
3243        focus_filename_editor: bool,
3244        autoscroll: bool,
3245        window: &mut Window,
3246        cx: &mut Context<Self>,
3247    ) {
3248        let now = Instant::now();
3249        let settings = ProjectPanelSettings::get_global(cx);
3250        let auto_collapse_dirs = settings.auto_fold_dirs;
3251        let hide_gitignore = settings.hide_gitignore;
3252        let sort_mode = settings.sort_mode;
3253        let project = self.project.read(cx);
3254        let repo_snapshots = project.git_store().read(cx).repo_snapshots(cx);
3255
3256        let old_ancestors = self.state.ancestors.clone();
3257        let mut new_state = State::derive(&self.state);
3258        new_state.last_worktree_root_id = project
3259            .visible_worktrees(cx)
3260            .next_back()
3261            .and_then(|worktree| worktree.read(cx).root_entry())
3262            .map(|entry| entry.id);
3263        let mut max_width_item = None;
3264
3265        let visible_worktrees: Vec<_> = project
3266            .visible_worktrees(cx)
3267            .map(|worktree| worktree.read(cx).snapshot())
3268            .collect();
3269        let hide_root = settings.hide_root && visible_worktrees.len() == 1;
3270        let hide_hidden = settings.hide_hidden;
3271
3272        let visible_entries_task = cx.spawn_in(window, async move |this, cx| {
3273            let new_state = cx
3274                .background_spawn(async move {
3275                    for worktree_snapshot in visible_worktrees {
3276                        let worktree_id = worktree_snapshot.id();
3277
3278                        let expanded_dir_ids = match new_state.expanded_dir_ids.entry(worktree_id) {
3279                            hash_map::Entry::Occupied(e) => e.into_mut(),
3280                            hash_map::Entry::Vacant(e) => {
3281                                // The first time a worktree's root entry becomes available,
3282                                // mark that root entry as expanded.
3283                                if let Some(entry) = worktree_snapshot.root_entry() {
3284                                    e.insert(vec![entry.id]).as_slice()
3285                                } else {
3286                                    &[]
3287                                }
3288                            }
3289                        };
3290
3291                        let mut new_entry_parent_id = None;
3292                        let mut new_entry_kind = EntryKind::Dir;
3293                        if let Some(edit_state) = &new_state.edit_state
3294                            && edit_state.worktree_id == worktree_id
3295                            && edit_state.is_new_entry()
3296                        {
3297                            new_entry_parent_id = Some(edit_state.entry_id);
3298                            new_entry_kind = if edit_state.is_dir {
3299                                EntryKind::Dir
3300                            } else {
3301                                EntryKind::File
3302                            };
3303                        }
3304
3305                        let mut visible_worktree_entries = Vec::new();
3306                        let mut entry_iter =
3307                            GitTraversal::new(&repo_snapshots, worktree_snapshot.entries(true, 0));
3308                        let mut auto_folded_ancestors = vec![];
3309                        let worktree_abs_path = worktree_snapshot.abs_path();
3310                        while let Some(entry) = entry_iter.entry() {
3311                            if hide_root && Some(entry.entry) == worktree_snapshot.root_entry() {
3312                                if new_entry_parent_id == Some(entry.id) {
3313                                    visible_worktree_entries.push(Self::create_new_git_entry(
3314                                        entry.entry,
3315                                        entry.git_summary,
3316                                        new_entry_kind,
3317                                    ));
3318                                    new_entry_parent_id = None;
3319                                }
3320                                entry_iter.advance();
3321                                continue;
3322                            }
3323                            if auto_collapse_dirs && entry.kind.is_dir() {
3324                                auto_folded_ancestors.push(entry.id);
3325                                if !new_state.unfolded_dir_ids.contains(&entry.id)
3326                                    && let Some(root_path) = worktree_snapshot.root_entry()
3327                                {
3328                                    let mut child_entries =
3329                                        worktree_snapshot.child_entries(&entry.path);
3330                                    if let Some(child) = child_entries.next()
3331                                        && entry.path != root_path.path
3332                                        && child_entries.next().is_none()
3333                                        && child.kind.is_dir()
3334                                    {
3335                                        entry_iter.advance();
3336
3337                                        continue;
3338                                    }
3339                                }
3340                                let depth = old_ancestors
3341                                    .get(&entry.id)
3342                                    .map(|ancestor| ancestor.current_ancestor_depth)
3343                                    .unwrap_or_default()
3344                                    .min(auto_folded_ancestors.len());
3345                                if let Some(edit_state) = &mut new_state.edit_state
3346                                    && edit_state.entry_id == entry.id
3347                                {
3348                                    edit_state.depth = depth;
3349                                }
3350                                let mut ancestors = std::mem::take(&mut auto_folded_ancestors);
3351                                if ancestors.len() > 1 {
3352                                    ancestors.reverse();
3353                                    new_state.ancestors.insert(
3354                                        entry.id,
3355                                        FoldedAncestors {
3356                                            current_ancestor_depth: depth,
3357                                            ancestors,
3358                                        },
3359                                    );
3360                                }
3361                            }
3362                            auto_folded_ancestors.clear();
3363                            if (!hide_gitignore || !entry.is_ignored)
3364                                && (!hide_hidden || !entry.is_hidden)
3365                            {
3366                                visible_worktree_entries.push(entry.to_owned());
3367                            }
3368                            let precedes_new_entry = if let Some(new_entry_id) = new_entry_parent_id
3369                            {
3370                                entry.id == new_entry_id || {
3371                                    new_state.ancestors.get(&entry.id).is_some_and(|entries| {
3372                                        entries.ancestors.contains(&new_entry_id)
3373                                    })
3374                                }
3375                            } else {
3376                                false
3377                            };
3378                            if precedes_new_entry
3379                                && (!hide_gitignore || !entry.is_ignored)
3380                                && (!hide_hidden || !entry.is_hidden)
3381                            {
3382                                visible_worktree_entries.push(Self::create_new_git_entry(
3383                                    entry.entry,
3384                                    entry.git_summary,
3385                                    new_entry_kind,
3386                                ));
3387                            }
3388
3389                            let (depth, chars) = if Some(entry.entry)
3390                                == worktree_snapshot.root_entry()
3391                            {
3392                                let Some(path_name) = worktree_abs_path.file_name() else {
3393                                    continue;
3394                                };
3395                                let depth = 0;
3396                                (depth, path_name.to_string_lossy().chars().count())
3397                            } else if entry.is_file() {
3398                                let Some(path_name) = entry
3399                                    .path
3400                                    .file_name()
3401                                    .with_context(|| {
3402                                        format!("Non-root entry has no file name: {entry:?}")
3403                                    })
3404                                    .log_err()
3405                                else {
3406                                    continue;
3407                                };
3408                                let depth = entry.path.ancestors().count() - 1;
3409                                (depth, path_name.chars().count())
3410                            } else {
3411                                let path = new_state
3412                                    .ancestors
3413                                    .get(&entry.id)
3414                                    .and_then(|ancestors| {
3415                                        let outermost_ancestor = ancestors.ancestors.last()?;
3416                                        let root_folded_entry = worktree_snapshot
3417                                            .entry_for_id(*outermost_ancestor)?
3418                                            .path
3419                                            .as_ref();
3420                                        entry.path.strip_prefix(root_folded_entry).ok().and_then(
3421                                            |suffix| {
3422                                                Some(
3423                                                    RelPath::unix(root_folded_entry.file_name()?)
3424                                                        .unwrap()
3425                                                        .join(suffix),
3426                                                )
3427                                            },
3428                                        )
3429                                    })
3430                                    .or_else(|| {
3431                                        entry.path.file_name().map(|file_name| {
3432                                            RelPath::unix(file_name).unwrap().into()
3433                                        })
3434                                    })
3435                                    .unwrap_or_else(|| entry.path.clone());
3436                                let depth = path.components().count();
3437                                (depth, path.as_unix_str().chars().count())
3438                            };
3439                            let width_estimate =
3440                                item_width_estimate(depth, chars, entry.canonical_path.is_some());
3441
3442                            match max_width_item.as_mut() {
3443                                Some((id, worktree_id, width)) => {
3444                                    if *width < width_estimate {
3445                                        *id = entry.id;
3446                                        *worktree_id = worktree_snapshot.id();
3447                                        *width = width_estimate;
3448                                    }
3449                                }
3450                                None => {
3451                                    max_width_item =
3452                                        Some((entry.id, worktree_snapshot.id(), width_estimate))
3453                                }
3454                            }
3455
3456                            if expanded_dir_ids.binary_search(&entry.id).is_err()
3457                                && entry_iter.advance_to_sibling()
3458                            {
3459                                continue;
3460                            }
3461                            entry_iter.advance();
3462                        }
3463
3464                        par_sort_worktree_entries_with_mode(
3465                            &mut visible_worktree_entries,
3466                            sort_mode,
3467                        );
3468                        new_state.visible_entries.push(VisibleEntriesForWorktree {
3469                            worktree_id,
3470                            entries: visible_worktree_entries,
3471                            index: OnceCell::new(),
3472                        })
3473                    }
3474                    if let Some((project_entry_id, worktree_id, _)) = max_width_item {
3475                        let mut visited_worktrees_length = 0;
3476                        let index = new_state
3477                            .visible_entries
3478                            .iter()
3479                            .find_map(|visible_entries| {
3480                                if worktree_id == visible_entries.worktree_id {
3481                                    visible_entries
3482                                        .entries
3483                                        .iter()
3484                                        .position(|entry| entry.id == project_entry_id)
3485                                } else {
3486                                    visited_worktrees_length += visible_entries.entries.len();
3487                                    None
3488                                }
3489                            });
3490                        if let Some(index) = index {
3491                            new_state.max_width_item_index = Some(visited_worktrees_length + index);
3492                        }
3493                    }
3494                    new_state
3495                })
3496                .await;
3497            this.update_in(cx, |this, window, cx| {
3498                let current_selection = this.state.selection;
3499                this.state = new_state;
3500                if let Some((worktree_id, entry_id)) = new_selected_entry {
3501                    this.state.selection = Some(SelectedEntry {
3502                        worktree_id,
3503                        entry_id,
3504                    });
3505                } else {
3506                    this.state.selection = current_selection;
3507                }
3508                let elapsed = now.elapsed();
3509                if this.last_reported_update.elapsed() > Duration::from_secs(3600) {
3510                    telemetry::event!(
3511                        "Project Panel Updated",
3512                        elapsed_ms = elapsed.as_millis() as u64,
3513                        worktree_entries = this
3514                            .state
3515                            .visible_entries
3516                            .iter()
3517                            .map(|worktree| worktree.entries.len())
3518                            .sum::<usize>(),
3519                    )
3520                }
3521                if this.update_visible_entries_task.focus_filename_editor {
3522                    this.update_visible_entries_task.focus_filename_editor = false;
3523                    this.filename_editor.update(cx, |editor, cx| {
3524                        window.focus(&editor.focus_handle(cx));
3525                    });
3526                }
3527                if this.update_visible_entries_task.autoscroll {
3528                    this.update_visible_entries_task.autoscroll = false;
3529                    this.autoscroll(cx);
3530                }
3531                cx.notify();
3532            })
3533            .ok();
3534        });
3535
3536        self.update_visible_entries_task = UpdateVisibleEntriesTask {
3537            _visible_entries_task: visible_entries_task,
3538            focus_filename_editor: focus_filename_editor
3539                || self.update_visible_entries_task.focus_filename_editor,
3540            autoscroll: autoscroll || self.update_visible_entries_task.autoscroll,
3541        };
3542    }
3543
3544    fn expand_entry(
3545        &mut self,
3546        worktree_id: WorktreeId,
3547        entry_id: ProjectEntryId,
3548        cx: &mut Context<Self>,
3549    ) {
3550        self.project.update(cx, |project, cx| {
3551            if let Some((worktree, expanded_dir_ids)) = project
3552                .worktree_for_id(worktree_id, cx)
3553                .zip(self.state.expanded_dir_ids.get_mut(&worktree_id))
3554            {
3555                project.expand_entry(worktree_id, entry_id, cx);
3556                let worktree = worktree.read(cx);
3557
3558                if let Some(mut entry) = worktree.entry_for_id(entry_id) {
3559                    loop {
3560                        if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) {
3561                            expanded_dir_ids.insert(ix, entry.id);
3562                        }
3563
3564                        if let Some(parent_entry) =
3565                            entry.path.parent().and_then(|p| worktree.entry_for_path(p))
3566                        {
3567                            entry = parent_entry;
3568                        } else {
3569                            break;
3570                        }
3571                    }
3572                }
3573            }
3574        });
3575    }
3576
3577    fn drop_external_files(
3578        &mut self,
3579        paths: &[PathBuf],
3580        entry_id: ProjectEntryId,
3581        window: &mut Window,
3582        cx: &mut Context<Self>,
3583    ) {
3584        let mut paths: Vec<Arc<Path>> = paths.iter().map(|path| Arc::from(path.clone())).collect();
3585
3586        let open_file_after_drop = paths.len() == 1 && paths[0].is_file();
3587
3588        let Some((target_directory, worktree, fs)) = maybe!({
3589            let project = self.project.read(cx);
3590            let fs = project.fs().clone();
3591            let worktree = project.worktree_for_entry(entry_id, cx)?;
3592            let entry = worktree.read(cx).entry_for_id(entry_id)?;
3593            let path = entry.path.clone();
3594            let target_directory = if entry.is_dir() {
3595                path
3596            } else {
3597                path.parent()?.into()
3598            };
3599            Some((target_directory, worktree, fs))
3600        }) else {
3601            return;
3602        };
3603
3604        let mut paths_to_replace = Vec::new();
3605        for path in &paths {
3606            if let Some(name) = path.file_name()
3607                && let Some(name) = name.to_str()
3608            {
3609                let target_path = target_directory.join(RelPath::unix(name).unwrap());
3610                if worktree.read(cx).entry_for_path(&target_path).is_some() {
3611                    paths_to_replace.push((name.to_string(), path.clone()));
3612                }
3613            }
3614        }
3615
3616        cx.spawn_in(window, async move |this, cx| {
3617            async move {
3618                for (filename, original_path) in &paths_to_replace {
3619                    let prompt_message = format!(
3620                        concat!(
3621                            "A file or folder with name {} ",
3622                            "already exists in the destination folder. ",
3623                            "Do you want to replace it?"
3624                        ),
3625                        filename
3626                    );
3627                    let answer = cx
3628                        .update(|window, cx| {
3629                            window.prompt(
3630                                PromptLevel::Info,
3631                                &prompt_message,
3632                                None,
3633                                &["Replace", "Cancel"],
3634                                cx,
3635                            )
3636                        })?
3637                        .await?;
3638
3639                    if answer == 1
3640                        && let Some(item_idx) = paths.iter().position(|p| p == original_path)
3641                    {
3642                        paths.remove(item_idx);
3643                    }
3644                }
3645
3646                if paths.is_empty() {
3647                    return Ok(());
3648                }
3649
3650                let task = worktree.update(cx, |worktree, cx| {
3651                    worktree.copy_external_entries(target_directory, paths, fs, cx)
3652                })?;
3653
3654                let opened_entries = task
3655                    .await
3656                    .with_context(|| "failed to copy external paths")?;
3657                this.update(cx, |this, cx| {
3658                    if open_file_after_drop && !opened_entries.is_empty() {
3659                        let settings = ProjectPanelSettings::get_global(cx);
3660                        if settings.auto_open.should_open_on_drop() {
3661                            this.open_entry(opened_entries[0], true, false, cx);
3662                        }
3663                    }
3664                })
3665            }
3666            .log_err()
3667            .await
3668        })
3669        .detach();
3670    }
3671
3672    fn refresh_drag_cursor_style(
3673        &self,
3674        modifiers: &Modifiers,
3675        window: &mut Window,
3676        cx: &mut Context<Self>,
3677    ) {
3678        if let Some(existing_cursor) = cx.active_drag_cursor_style() {
3679            let new_cursor = if Self::is_copy_modifier_set(modifiers) {
3680                CursorStyle::DragCopy
3681            } else {
3682                CursorStyle::PointingHand
3683            };
3684            if existing_cursor != new_cursor {
3685                cx.set_active_drag_cursor_style(new_cursor, window);
3686            }
3687        }
3688    }
3689
3690    fn is_copy_modifier_set(modifiers: &Modifiers) -> bool {
3691        cfg!(target_os = "macos") && modifiers.alt
3692            || cfg!(not(target_os = "macos")) && modifiers.control
3693    }
3694
3695    fn drag_onto(
3696        &mut self,
3697        selections: &DraggedSelection,
3698        target_entry_id: ProjectEntryId,
3699        is_file: bool,
3700        window: &mut Window,
3701        cx: &mut Context<Self>,
3702    ) {
3703        if Self::is_copy_modifier_set(&window.modifiers()) {
3704            let _ = maybe!({
3705                let project = self.project.read(cx);
3706                let target_worktree = project.worktree_for_entry(target_entry_id, cx)?;
3707                let worktree_id = target_worktree.read(cx).id();
3708                let target_entry = target_worktree
3709                    .read(cx)
3710                    .entry_for_id(target_entry_id)?
3711                    .clone();
3712
3713                let mut copy_tasks = Vec::new();
3714                let mut disambiguation_range = None;
3715                for selection in selections.items() {
3716                    let (new_path, new_disambiguation_range) = self.create_paste_path(
3717                        selection,
3718                        (target_worktree.clone(), &target_entry),
3719                        cx,
3720                    )?;
3721
3722                    let task = self.project.update(cx, |project, cx| {
3723                        project.copy_entry(selection.entry_id, (worktree_id, new_path).into(), cx)
3724                    });
3725                    copy_tasks.push(task);
3726                    disambiguation_range = new_disambiguation_range.or(disambiguation_range);
3727                }
3728
3729                let item_count = copy_tasks.len();
3730
3731                cx.spawn_in(window, async move |project_panel, cx| {
3732                    let mut last_succeed = None;
3733                    for task in copy_tasks.into_iter() {
3734                        if let Some(Some(entry)) = task.await.log_err() {
3735                            last_succeed = Some(entry.id);
3736                        }
3737                    }
3738                    // update selection
3739                    if let Some(entry_id) = last_succeed {
3740                        project_panel
3741                            .update_in(cx, |project_panel, window, cx| {
3742                                project_panel.state.selection = Some(SelectedEntry {
3743                                    worktree_id,
3744                                    entry_id,
3745                                });
3746
3747                                // if only one entry was dragged and it was disambiguated, open the rename editor
3748                                if item_count == 1 && disambiguation_range.is_some() {
3749                                    project_panel.rename_impl(disambiguation_range, window, cx);
3750                                }
3751                            })
3752                            .ok();
3753                    }
3754                })
3755                .detach();
3756                Some(())
3757            });
3758        } else {
3759            for selection in selections.items() {
3760                self.move_entry(selection.entry_id, target_entry_id, is_file, cx);
3761            }
3762        }
3763    }
3764
3765    fn index_for_entry(
3766        &self,
3767        entry_id: ProjectEntryId,
3768        worktree_id: WorktreeId,
3769    ) -> Option<(usize, usize, usize)> {
3770        let mut total_ix = 0;
3771        for (worktree_ix, visible) in self.state.visible_entries.iter().enumerate() {
3772            if worktree_id != visible.worktree_id {
3773                total_ix += visible.entries.len();
3774                continue;
3775            }
3776
3777            return visible
3778                .entries
3779                .iter()
3780                .enumerate()
3781                .find(|(_, entry)| entry.id == entry_id)
3782                .map(|(ix, _)| (worktree_ix, ix, total_ix + ix));
3783        }
3784        None
3785    }
3786
3787    fn entry_at_index(&self, index: usize) -> Option<(WorktreeId, GitEntryRef<'_>)> {
3788        let mut offset = 0;
3789        for worktree in &self.state.visible_entries {
3790            let current_len = worktree.entries.len();
3791            if index < offset + current_len {
3792                return worktree
3793                    .entries
3794                    .get(index - offset)
3795                    .map(|entry| (worktree.worktree_id, entry.to_ref()));
3796            }
3797            offset += current_len;
3798        }
3799        None
3800    }
3801
3802    fn iter_visible_entries(
3803        &self,
3804        range: Range<usize>,
3805        window: &mut Window,
3806        cx: &mut Context<ProjectPanel>,
3807        mut callback: impl FnMut(
3808            &Entry,
3809            usize,
3810            &HashSet<Arc<RelPath>>,
3811            &mut Window,
3812            &mut Context<ProjectPanel>,
3813        ),
3814    ) {
3815        let mut ix = 0;
3816        for visible in &self.state.visible_entries {
3817            if ix >= range.end {
3818                return;
3819            }
3820
3821            if ix + visible.entries.len() <= range.start {
3822                ix += visible.entries.len();
3823                continue;
3824            }
3825
3826            let end_ix = range.end.min(ix + visible.entries.len());
3827            let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
3828            let entries = visible
3829                .index
3830                .get_or_init(|| visible.entries.iter().map(|e| e.path.clone()).collect());
3831            let base_index = ix + entry_range.start;
3832            for (i, entry) in visible.entries[entry_range].iter().enumerate() {
3833                let global_index = base_index + i;
3834                callback(entry, global_index, entries, window, cx);
3835            }
3836            ix = end_ix;
3837        }
3838    }
3839
3840    fn for_each_visible_entry(
3841        &self,
3842        range: Range<usize>,
3843        window: &mut Window,
3844        cx: &mut Context<ProjectPanel>,
3845        mut callback: impl FnMut(ProjectEntryId, EntryDetails, &mut Window, &mut Context<ProjectPanel>),
3846    ) {
3847        let mut ix = 0;
3848        for visible in &self.state.visible_entries {
3849            if ix >= range.end {
3850                return;
3851            }
3852
3853            if ix + visible.entries.len() <= range.start {
3854                ix += visible.entries.len();
3855                continue;
3856            }
3857
3858            let end_ix = range.end.min(ix + visible.entries.len());
3859            let git_status_setting = {
3860                let settings = ProjectPanelSettings::get_global(cx);
3861                settings.git_status
3862            };
3863            if let Some(worktree) = self
3864                .project
3865                .read(cx)
3866                .worktree_for_id(visible.worktree_id, cx)
3867            {
3868                let snapshot = worktree.read(cx).snapshot();
3869                let root_name = snapshot.root_name();
3870
3871                let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
3872                let entries = visible
3873                    .index
3874                    .get_or_init(|| visible.entries.iter().map(|e| e.path.clone()).collect());
3875                for entry in visible.entries[entry_range].iter() {
3876                    let status = git_status_setting
3877                        .then_some(entry.git_summary)
3878                        .unwrap_or_default();
3879
3880                    let mut details = self.details_for_entry(
3881                        entry,
3882                        visible.worktree_id,
3883                        root_name,
3884                        entries,
3885                        status,
3886                        None,
3887                        window,
3888                        cx,
3889                    );
3890
3891                    if let Some(edit_state) = &self.state.edit_state {
3892                        let is_edited_entry = if edit_state.is_new_entry() {
3893                            entry.id == NEW_ENTRY_ID
3894                        } else {
3895                            entry.id == edit_state.entry_id
3896                                || self.state.ancestors.get(&entry.id).is_some_and(
3897                                    |auto_folded_dirs| {
3898                                        auto_folded_dirs.ancestors.contains(&edit_state.entry_id)
3899                                    },
3900                                )
3901                        };
3902
3903                        if is_edited_entry {
3904                            if let Some(processing_filename) = &edit_state.processing_filename {
3905                                details.is_processing = true;
3906                                if let Some(ancestors) = edit_state
3907                                    .leaf_entry_id
3908                                    .and_then(|entry| self.state.ancestors.get(&entry))
3909                                {
3910                                    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;
3911                                    let all_components = ancestors.ancestors.len();
3912
3913                                    let prefix_components = all_components - position;
3914                                    let suffix_components = position.checked_sub(1);
3915                                    let mut previous_components =
3916                                        Path::new(&details.filename).components();
3917                                    let mut new_path = previous_components
3918                                        .by_ref()
3919                                        .take(prefix_components)
3920                                        .collect::<PathBuf>();
3921                                    if let Some(last_component) =
3922                                        processing_filename.components().next_back()
3923                                    {
3924                                        new_path.push(last_component);
3925                                        previous_components.next();
3926                                    }
3927
3928                                    if suffix_components.is_some() {
3929                                        new_path.push(previous_components);
3930                                    }
3931                                    if let Some(str) = new_path.to_str() {
3932                                        details.filename.clear();
3933                                        details.filename.push_str(str);
3934                                    }
3935                                } else {
3936                                    details.filename.clear();
3937                                    details.filename.push_str(processing_filename.as_unix_str());
3938                                }
3939                            } else {
3940                                if edit_state.is_new_entry() {
3941                                    details.filename.clear();
3942                                }
3943                                details.is_editing = true;
3944                            }
3945                        }
3946                    }
3947
3948                    callback(entry.id, details, window, cx);
3949                }
3950            }
3951            ix = end_ix;
3952        }
3953    }
3954
3955    fn find_entry_in_worktree(
3956        &self,
3957        worktree_id: WorktreeId,
3958        reverse_search: bool,
3959        only_visible_entries: bool,
3960        predicate: impl Fn(GitEntryRef, WorktreeId) -> bool,
3961        cx: &mut Context<Self>,
3962    ) -> Option<GitEntry> {
3963        if only_visible_entries {
3964            let entries = self
3965                .state
3966                .visible_entries
3967                .iter()
3968                .find_map(|visible| {
3969                    if worktree_id == visible.worktree_id {
3970                        Some(&visible.entries)
3971                    } else {
3972                        None
3973                    }
3974                })?
3975                .clone();
3976
3977            return utils::ReversibleIterable::new(entries.iter(), reverse_search)
3978                .find(|ele| predicate(ele.to_ref(), worktree_id))
3979                .cloned();
3980        }
3981
3982        let repo_snapshots = self
3983            .project
3984            .read(cx)
3985            .git_store()
3986            .read(cx)
3987            .repo_snapshots(cx);
3988        let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?;
3989        worktree.read_with(cx, |tree, _| {
3990            utils::ReversibleIterable::new(
3991                GitTraversal::new(&repo_snapshots, tree.entries(true, 0usize)),
3992                reverse_search,
3993            )
3994            .find_single_ended(|ele| predicate(*ele, worktree_id))
3995            .map(|ele| ele.to_owned())
3996        })
3997    }
3998
3999    fn find_entry(
4000        &self,
4001        start: Option<&SelectedEntry>,
4002        reverse_search: bool,
4003        predicate: impl Fn(GitEntryRef, WorktreeId) -> bool,
4004        cx: &mut Context<Self>,
4005    ) -> Option<SelectedEntry> {
4006        let mut worktree_ids: Vec<_> = self
4007            .state
4008            .visible_entries
4009            .iter()
4010            .map(|worktree| worktree.worktree_id)
4011            .collect();
4012        let repo_snapshots = self
4013            .project
4014            .read(cx)
4015            .git_store()
4016            .read(cx)
4017            .repo_snapshots(cx);
4018
4019        let mut last_found: Option<SelectedEntry> = None;
4020
4021        if let Some(start) = start {
4022            let worktree = self
4023                .project
4024                .read(cx)
4025                .worktree_for_id(start.worktree_id, cx)?
4026                .read(cx);
4027
4028            let search = {
4029                let entry = worktree.entry_for_id(start.entry_id)?;
4030                let root_entry = worktree.root_entry()?;
4031                let tree_id = worktree.id();
4032
4033                let mut first_iter = GitTraversal::new(
4034                    &repo_snapshots,
4035                    worktree.traverse_from_path(true, true, true, entry.path.as_ref()),
4036                );
4037
4038                if reverse_search {
4039                    first_iter.next();
4040                }
4041
4042                let first = first_iter
4043                    .enumerate()
4044                    .take_until(|(count, entry)| entry.entry == root_entry && *count != 0usize)
4045                    .map(|(_, entry)| entry)
4046                    .find(|ele| predicate(*ele, tree_id))
4047                    .map(|ele| ele.to_owned());
4048
4049                let second_iter =
4050                    GitTraversal::new(&repo_snapshots, worktree.entries(true, 0usize));
4051
4052                let second = if reverse_search {
4053                    second_iter
4054                        .take_until(|ele| ele.id == start.entry_id)
4055                        .filter(|ele| predicate(*ele, tree_id))
4056                        .last()
4057                        .map(|ele| ele.to_owned())
4058                } else {
4059                    second_iter
4060                        .take_while(|ele| ele.id != start.entry_id)
4061                        .filter(|ele| predicate(*ele, tree_id))
4062                        .last()
4063                        .map(|ele| ele.to_owned())
4064                };
4065
4066                if reverse_search {
4067                    Some((second, first))
4068                } else {
4069                    Some((first, second))
4070                }
4071            };
4072
4073            if let Some((first, second)) = search {
4074                let first = first.map(|entry| SelectedEntry {
4075                    worktree_id: start.worktree_id,
4076                    entry_id: entry.id,
4077                });
4078
4079                let second = second.map(|entry| SelectedEntry {
4080                    worktree_id: start.worktree_id,
4081                    entry_id: entry.id,
4082                });
4083
4084                if first.is_some() {
4085                    return first;
4086                }
4087                last_found = second;
4088
4089                let idx = worktree_ids
4090                    .iter()
4091                    .enumerate()
4092                    .find(|(_, ele)| **ele == start.worktree_id)
4093                    .map(|(idx, _)| idx);
4094
4095                if let Some(idx) = idx {
4096                    worktree_ids.rotate_left(idx + 1usize);
4097                    worktree_ids.pop();
4098                }
4099            }
4100        }
4101
4102        for tree_id in worktree_ids.into_iter() {
4103            if let Some(found) =
4104                self.find_entry_in_worktree(tree_id, reverse_search, false, &predicate, cx)
4105            {
4106                return Some(SelectedEntry {
4107                    worktree_id: tree_id,
4108                    entry_id: found.id,
4109                });
4110            }
4111        }
4112
4113        last_found
4114    }
4115
4116    fn find_visible_entry(
4117        &self,
4118        start: Option<&SelectedEntry>,
4119        reverse_search: bool,
4120        predicate: impl Fn(GitEntryRef, WorktreeId) -> bool,
4121        cx: &mut Context<Self>,
4122    ) -> Option<SelectedEntry> {
4123        let mut worktree_ids: Vec<_> = self
4124            .state
4125            .visible_entries
4126            .iter()
4127            .map(|worktree| worktree.worktree_id)
4128            .collect();
4129
4130        let mut last_found: Option<SelectedEntry> = None;
4131
4132        if let Some(start) = start {
4133            let entries = self
4134                .state
4135                .visible_entries
4136                .iter()
4137                .find(|worktree| worktree.worktree_id == start.worktree_id)
4138                .map(|worktree| &worktree.entries)?;
4139
4140            let mut start_idx = entries
4141                .iter()
4142                .enumerate()
4143                .find(|(_, ele)| ele.id == start.entry_id)
4144                .map(|(idx, _)| idx)?;
4145
4146            if reverse_search {
4147                start_idx = start_idx.saturating_add(1usize);
4148            }
4149
4150            let (left, right) = entries.split_at_checked(start_idx)?;
4151
4152            let (first_iter, second_iter) = if reverse_search {
4153                (
4154                    utils::ReversibleIterable::new(left.iter(), reverse_search),
4155                    utils::ReversibleIterable::new(right.iter(), reverse_search),
4156                )
4157            } else {
4158                (
4159                    utils::ReversibleIterable::new(right.iter(), reverse_search),
4160                    utils::ReversibleIterable::new(left.iter(), reverse_search),
4161                )
4162            };
4163
4164            let first_search = first_iter.find(|ele| predicate(ele.to_ref(), start.worktree_id));
4165            let second_search = second_iter.find(|ele| predicate(ele.to_ref(), start.worktree_id));
4166
4167            if first_search.is_some() {
4168                return first_search.map(|entry| SelectedEntry {
4169                    worktree_id: start.worktree_id,
4170                    entry_id: entry.id,
4171                });
4172            }
4173
4174            last_found = second_search.map(|entry| SelectedEntry {
4175                worktree_id: start.worktree_id,
4176                entry_id: entry.id,
4177            });
4178
4179            let idx = worktree_ids
4180                .iter()
4181                .enumerate()
4182                .find(|(_, ele)| **ele == start.worktree_id)
4183                .map(|(idx, _)| idx);
4184
4185            if let Some(idx) = idx {
4186                worktree_ids.rotate_left(idx + 1usize);
4187                worktree_ids.pop();
4188            }
4189        }
4190
4191        for tree_id in worktree_ids.into_iter() {
4192            if let Some(found) =
4193                self.find_entry_in_worktree(tree_id, reverse_search, true, &predicate, cx)
4194            {
4195                return Some(SelectedEntry {
4196                    worktree_id: tree_id,
4197                    entry_id: found.id,
4198                });
4199            }
4200        }
4201
4202        last_found
4203    }
4204
4205    fn calculate_depth_and_difference(
4206        entry: &Entry,
4207        visible_worktree_entries: &HashSet<Arc<RelPath>>,
4208    ) -> (usize, usize) {
4209        let (depth, difference) = entry
4210            .path
4211            .ancestors()
4212            .skip(1) // Skip the entry itself
4213            .find_map(|ancestor| {
4214                if let Some(parent_entry) = visible_worktree_entries.get(ancestor) {
4215                    let entry_path_components_count = entry.path.components().count();
4216                    let parent_path_components_count = parent_entry.components().count();
4217                    let difference = entry_path_components_count - parent_path_components_count;
4218                    let depth = parent_entry
4219                        .ancestors()
4220                        .skip(1)
4221                        .filter(|ancestor| visible_worktree_entries.contains(*ancestor))
4222                        .count();
4223                    Some((depth + 1, difference))
4224                } else {
4225                    None
4226                }
4227            })
4228            .unwrap_or_else(|| (0, entry.path.components().count()));
4229
4230        (depth, difference)
4231    }
4232
4233    fn highlight_entry_for_external_drag(
4234        &self,
4235        target_entry: &Entry,
4236        target_worktree: &Worktree,
4237    ) -> Option<ProjectEntryId> {
4238        // Always highlight directory or parent directory if it's file
4239        if target_entry.is_dir() {
4240            Some(target_entry.id)
4241        } else {
4242            target_entry
4243                .path
4244                .parent()
4245                .and_then(|parent_path| target_worktree.entry_for_path(parent_path))
4246                .map(|parent_entry| parent_entry.id)
4247        }
4248    }
4249
4250    fn highlight_entry_for_selection_drag(
4251        &self,
4252        target_entry: &Entry,
4253        target_worktree: &Worktree,
4254        drag_state: &DraggedSelection,
4255        cx: &Context<Self>,
4256    ) -> Option<ProjectEntryId> {
4257        let target_parent_path = target_entry.path.parent();
4258
4259        // In case of single item drag, we do not highlight existing
4260        // directory which item belongs too
4261        if drag_state.items().count() == 1
4262            && drag_state.active_selection.worktree_id == target_worktree.id()
4263        {
4264            let active_entry_path = self
4265                .project
4266                .read(cx)
4267                .path_for_entry(drag_state.active_selection.entry_id, cx)?;
4268
4269            if let Some(active_parent_path) = active_entry_path.path.parent() {
4270                // Do not highlight active entry parent
4271                if active_parent_path == target_entry.path.as_ref() {
4272                    return None;
4273                }
4274
4275                // Do not highlight active entry sibling files
4276                if Some(active_parent_path) == target_parent_path && target_entry.is_file() {
4277                    return None;
4278                }
4279            }
4280        }
4281
4282        // Always highlight directory or parent directory if it's file
4283        if target_entry.is_dir() {
4284            Some(target_entry.id)
4285        } else {
4286            target_parent_path
4287                .and_then(|parent_path| target_worktree.entry_for_path(parent_path))
4288                .map(|parent_entry| parent_entry.id)
4289        }
4290    }
4291
4292    fn should_highlight_background_for_selection_drag(
4293        &self,
4294        drag_state: &DraggedSelection,
4295        last_root_id: ProjectEntryId,
4296        cx: &App,
4297    ) -> bool {
4298        // Always highlight for multiple entries
4299        if drag_state.items().count() > 1 {
4300            return true;
4301        }
4302
4303        // Since root will always have empty relative path
4304        if let Some(entry_path) = self
4305            .project
4306            .read(cx)
4307            .path_for_entry(drag_state.active_selection.entry_id, cx)
4308        {
4309            if let Some(parent_path) = entry_path.path.parent() {
4310                if !parent_path.is_empty() {
4311                    return true;
4312                }
4313            }
4314        }
4315
4316        // If parent is empty, check if different worktree
4317        if let Some(last_root_worktree_id) = self
4318            .project
4319            .read(cx)
4320            .worktree_id_for_entry(last_root_id, cx)
4321        {
4322            if drag_state.active_selection.worktree_id != last_root_worktree_id {
4323                return true;
4324            }
4325        }
4326
4327        false
4328    }
4329
4330    fn render_entry(
4331        &self,
4332        entry_id: ProjectEntryId,
4333        details: EntryDetails,
4334        window: &mut Window,
4335        cx: &mut Context<Self>,
4336    ) -> Stateful<Div> {
4337        const GROUP_NAME: &str = "project_entry";
4338
4339        let kind = details.kind;
4340        let is_sticky = details.sticky.is_some();
4341        let sticky_index = details.sticky.as_ref().map(|this| this.sticky_index);
4342        let settings = ProjectPanelSettings::get_global(cx);
4343        let show_editor = details.is_editing && !details.is_processing;
4344
4345        let selection = SelectedEntry {
4346            worktree_id: details.worktree_id,
4347            entry_id,
4348        };
4349
4350        let is_marked = self.marked_entries.contains(&selection);
4351        let is_active = self
4352            .state
4353            .selection
4354            .is_some_and(|selection| selection.entry_id == entry_id);
4355
4356        let file_name = details.filename.clone();
4357
4358        let mut icon = details.icon.clone();
4359        if settings.file_icons && show_editor && details.kind.is_file() {
4360            let filename = self.filename_editor.read(cx).text(cx);
4361            if filename.len() > 2 {
4362                icon = FileIcons::get_icon(Path::new(&filename), cx);
4363            }
4364        }
4365
4366        let filename_text_color = details.filename_text_color;
4367        let diagnostic_severity = details.diagnostic_severity;
4368        let item_colors = get_item_color(is_sticky, cx);
4369
4370        let canonical_path = details
4371            .canonical_path
4372            .as_ref()
4373            .map(|f| f.to_string_lossy().into_owned());
4374        let path_style = self.project.read(cx).path_style(cx);
4375        let path = details.path.clone();
4376        let path_for_external_paths = path.clone();
4377        let path_for_dragged_selection = path.clone();
4378
4379        let depth = details.depth;
4380        let worktree_id = details.worktree_id;
4381        let dragged_selection = DraggedSelection {
4382            active_selection: SelectedEntry {
4383                worktree_id: selection.worktree_id,
4384                entry_id: self.resolve_entry(selection.entry_id),
4385            },
4386            marked_selections: Arc::from(self.marked_entries.clone()),
4387        };
4388
4389        let bg_color = if is_marked {
4390            item_colors.marked
4391        } else {
4392            item_colors.default
4393        };
4394
4395        let bg_hover_color = if is_marked {
4396            item_colors.marked
4397        } else {
4398            item_colors.hover
4399        };
4400
4401        let validation_color_and_message = if show_editor {
4402            match self
4403                .state
4404                .edit_state
4405                .as_ref()
4406                .map_or(ValidationState::None, |e| e.validation_state.clone())
4407            {
4408                ValidationState::Error(msg) => Some((Color::Error.color(cx), msg)),
4409                ValidationState::Warning(msg) => Some((Color::Warning.color(cx), msg)),
4410                ValidationState::None => None,
4411            }
4412        } else {
4413            None
4414        };
4415
4416        let border_color =
4417            if !self.mouse_down && is_active && self.focus_handle.contains_focused(window, cx) {
4418                match validation_color_and_message {
4419                    Some((color, _)) => color,
4420                    None => item_colors.focused,
4421                }
4422            } else {
4423                bg_color
4424            };
4425
4426        let border_hover_color =
4427            if !self.mouse_down && is_active && self.focus_handle.contains_focused(window, cx) {
4428                match validation_color_and_message {
4429                    Some((color, _)) => color,
4430                    None => item_colors.focused,
4431                }
4432            } else {
4433                bg_hover_color
4434            };
4435
4436        let folded_directory_drag_target = self.folded_directory_drag_target;
4437        let is_highlighted = {
4438            if let Some(highlight_entry_id) =
4439                self.drag_target_entry
4440                    .as_ref()
4441                    .and_then(|drag_target| match drag_target {
4442                        DragTarget::Entry {
4443                            highlight_entry_id, ..
4444                        } => Some(*highlight_entry_id),
4445                        DragTarget::Background => self.state.last_worktree_root_id,
4446                    })
4447            {
4448                // Highlight if same entry or it's children
4449                if entry_id == highlight_entry_id {
4450                    true
4451                } else {
4452                    maybe!({
4453                        let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?;
4454                        let highlight_entry = worktree.read(cx).entry_for_id(highlight_entry_id)?;
4455                        Some(path.starts_with(&highlight_entry.path))
4456                    })
4457                    .unwrap_or(false)
4458                }
4459            } else {
4460                false
4461            }
4462        };
4463
4464        let id: ElementId = if is_sticky {
4465            SharedString::from(format!("project_panel_sticky_item_{}", entry_id.to_usize())).into()
4466        } else {
4467            (entry_id.to_proto() as usize).into()
4468        };
4469
4470        div()
4471            .id(id.clone())
4472            .relative()
4473            .group(GROUP_NAME)
4474            .cursor_pointer()
4475            .rounded_none()
4476            .bg(bg_color)
4477            .border_1()
4478            .border_r_2()
4479            .border_color(border_color)
4480            .hover(|style| style.bg(bg_hover_color).border_color(border_hover_color))
4481            .when(is_sticky, |this| {
4482                this.block_mouse_except_scroll()
4483            })
4484            .when(!is_sticky, |this| {
4485                this
4486                .when(is_highlighted && folded_directory_drag_target.is_none(), |this| this.border_color(transparent_white()).bg(item_colors.drag_over))
4487                .when(settings.drag_and_drop, |this| this
4488                .on_drag_move::<ExternalPaths>(cx.listener(
4489                    move |this, event: &DragMoveEvent<ExternalPaths>, _, cx| {
4490                        let is_current_target = this.drag_target_entry.as_ref()
4491                             .and_then(|entry| match entry {
4492                                 DragTarget::Entry { entry_id: target_id, .. } => Some(*target_id),
4493                                 DragTarget::Background { .. } => None,
4494                             }) == Some(entry_id);
4495
4496                        if !event.bounds.contains(&event.event.position) {
4497                            // Entry responsible for setting drag target is also responsible to
4498                            // clear it up after drag is out of bounds
4499                            if is_current_target {
4500                                this.drag_target_entry = None;
4501                            }
4502                            return;
4503                        }
4504
4505                        if is_current_target {
4506                            return;
4507                        }
4508
4509                        this.marked_entries.clear();
4510
4511                        let Some((entry_id, highlight_entry_id)) = maybe!({
4512                            let target_worktree = this.project.read(cx).worktree_for_id(selection.worktree_id, cx)?.read(cx);
4513                            let target_entry = target_worktree.entry_for_path(&path_for_external_paths)?;
4514                            let highlight_entry_id = this.highlight_entry_for_external_drag(target_entry, target_worktree)?;
4515                            Some((target_entry.id, highlight_entry_id))
4516                        }) else {
4517                            return;
4518                        };
4519
4520                        this.drag_target_entry = Some(DragTarget::Entry {
4521                            entry_id,
4522                            highlight_entry_id,
4523                        });
4524
4525                    },
4526                ))
4527                .on_drop(cx.listener(
4528                    move |this, external_paths: &ExternalPaths, window, cx| {
4529                        this.drag_target_entry = None;
4530                        this.hover_scroll_task.take();
4531                        this.drop_external_files(external_paths.paths(), entry_id, window, cx);
4532                        cx.stop_propagation();
4533                    },
4534                ))
4535                .on_drag_move::<DraggedSelection>(cx.listener(
4536                    move |this, event: &DragMoveEvent<DraggedSelection>, window, cx| {
4537                        let is_current_target = this.drag_target_entry.as_ref()
4538                             .and_then(|entry| match entry {
4539                                 DragTarget::Entry { entry_id: target_id, .. } => Some(*target_id),
4540                                 DragTarget::Background { .. } => None,
4541                             }) == Some(entry_id);
4542
4543                        if !event.bounds.contains(&event.event.position) {
4544                            // Entry responsible for setting drag target is also responsible to
4545                            // clear it up after drag is out of bounds
4546                            if is_current_target {
4547                                this.drag_target_entry = None;
4548                            }
4549                            return;
4550                        }
4551
4552                        if is_current_target {
4553                            return;
4554                        }
4555
4556                        let drag_state = event.drag(cx);
4557
4558                        if drag_state.items().count() == 1 {
4559                            this.marked_entries.clear();
4560                            this.marked_entries.push(drag_state.active_selection);
4561                        }
4562
4563                        let Some((entry_id, highlight_entry_id)) = maybe!({
4564                            let target_worktree = this.project.read(cx).worktree_for_id(selection.worktree_id, cx)?.read(cx);
4565                            let target_entry = target_worktree.entry_for_path(&path_for_dragged_selection)?;
4566                            let highlight_entry_id = this.highlight_entry_for_selection_drag(target_entry, target_worktree, drag_state, cx)?;
4567                            Some((target_entry.id, highlight_entry_id))
4568                        }) else {
4569                            return;
4570                        };
4571
4572                        this.drag_target_entry = Some(DragTarget::Entry {
4573                            entry_id,
4574                            highlight_entry_id,
4575                        });
4576
4577                        this.hover_expand_task.take();
4578
4579                        if !kind.is_dir()
4580                            || this
4581                                .state
4582                                .expanded_dir_ids
4583                                .get(&details.worktree_id)
4584                                .is_some_and(|ids| ids.binary_search(&entry_id).is_ok())
4585                        {
4586                            return;
4587                        }
4588
4589                        let bounds = event.bounds;
4590                        this.hover_expand_task =
4591                            Some(cx.spawn_in(window, async move |this, cx| {
4592                                cx.background_executor()
4593                                    .timer(Duration::from_millis(500))
4594                                    .await;
4595                                this.update_in(cx, |this, window, cx| {
4596                                    this.hover_expand_task.take();
4597                                    if this.drag_target_entry.as_ref().and_then(|entry| match entry {
4598                                        DragTarget::Entry { entry_id: target_id, .. } => Some(*target_id),
4599                                        DragTarget::Background { .. } => None,
4600                                    }) == Some(entry_id)
4601                                        && bounds.contains(&window.mouse_position())
4602                                    {
4603                                        this.expand_entry(worktree_id, entry_id, cx);
4604                                        this.update_visible_entries(
4605                                            Some((worktree_id, entry_id)),
4606                                            false,
4607                                            false,
4608                                            window,
4609                                            cx,
4610                                        );
4611                                        cx.notify();
4612                                    }
4613                                })
4614                                .ok();
4615                            }));
4616                    },
4617                ))
4618                .on_drag(
4619                    dragged_selection,
4620                    {
4621                        let active_component = self.state.ancestors.get(&entry_id).and_then(|ancestors| ancestors.active_component(&details.filename));
4622                        move |selection, click_offset, _window, cx| {
4623                            let filename = active_component.as_ref().unwrap_or_else(|| &details.filename);
4624                            cx.new(|_| DraggedProjectEntryView {
4625                                icon: details.icon.clone(),
4626                                filename: filename.clone(),
4627                                click_offset,
4628                                selection: selection.active_selection,
4629                                selections: selection.marked_selections.clone(),
4630                            })
4631                        }
4632                    }
4633                )
4634                .on_drop(
4635                    cx.listener(move |this, selections: &DraggedSelection, window, cx| {
4636                        this.drag_target_entry = None;
4637                        this.hover_scroll_task.take();
4638                        this.hover_expand_task.take();
4639                        if folded_directory_drag_target.is_some() {
4640                            return;
4641                        }
4642                        this.drag_onto(selections, entry_id, kind.is_file(), window, cx);
4643                    }),
4644                ))
4645            })
4646            .on_mouse_down(
4647                MouseButton::Left,
4648                cx.listener(move |this, _, _, cx| {
4649                    this.mouse_down = true;
4650                    cx.propagate();
4651                }),
4652            )
4653            .on_click(
4654                cx.listener(move |project_panel, event: &gpui::ClickEvent, window, cx| {
4655                    if event.is_right_click() || event.first_focus()
4656                        || show_editor
4657                    {
4658                        return;
4659                    }
4660                    if event.standard_click() {
4661                        project_panel.mouse_down = false;
4662                    }
4663                    cx.stop_propagation();
4664
4665                    if let Some(selection) = project_panel.state.selection.filter(|_| event.modifiers().shift) {
4666                        let current_selection = project_panel.index_for_selection(selection);
4667                        let clicked_entry = SelectedEntry {
4668                            entry_id,
4669                            worktree_id,
4670                        };
4671                        let target_selection = project_panel.index_for_selection(clicked_entry);
4672                        if let Some(((_, _, source_index), (_, _, target_index))) =
4673                            current_selection.zip(target_selection)
4674                        {
4675                            let range_start = source_index.min(target_index);
4676                            let range_end = source_index.max(target_index) + 1;
4677                            let mut new_selections = Vec::new();
4678                            project_panel.for_each_visible_entry(
4679                                range_start..range_end,
4680                                window,
4681                                cx,
4682                                |entry_id, details, _, _| {
4683                                    new_selections.push(SelectedEntry {
4684                                        entry_id,
4685                                        worktree_id: details.worktree_id,
4686                                    });
4687                                },
4688                            );
4689
4690                            for selection in &new_selections {
4691                                if !project_panel.marked_entries.contains(selection) {
4692                                    project_panel.marked_entries.push(*selection);
4693                                }
4694                            }
4695
4696                            project_panel.state.selection = Some(clicked_entry);
4697                            if !project_panel.marked_entries.contains(&clicked_entry) {
4698                                project_panel.marked_entries.push(clicked_entry);
4699                            }
4700                        }
4701                    } else if event.modifiers().secondary() {
4702                        if event.click_count() > 1 {
4703                            project_panel.split_entry(entry_id, false, None, cx);
4704                        } else {
4705                            project_panel.state.selection = Some(selection);
4706                            if let Some(position) = project_panel.marked_entries.iter().position(|e| *e == selection) {
4707                                project_panel.marked_entries.remove(position);
4708                            } else {
4709                                project_panel.marked_entries.push(selection);
4710                            }
4711                        }
4712                    } else if kind.is_dir() {
4713                        project_panel.marked_entries.clear();
4714                        if is_sticky
4715                            && let Some((_, _, index)) = project_panel.index_for_entry(entry_id, worktree_id) {
4716                                project_panel.scroll_handle.scroll_to_item_strict_with_offset(index, ScrollStrategy::Top, sticky_index.unwrap_or(0));
4717                                cx.notify();
4718                                // move down by 1px so that clicked item
4719                                // don't count as sticky anymore
4720                                cx.on_next_frame(window, |_, window, cx| {
4721                                    cx.on_next_frame(window, |this, _, cx| {
4722                                        let mut offset = this.scroll_handle.offset();
4723                                        offset.y += px(1.);
4724                                        this.scroll_handle.set_offset(offset);
4725                                        cx.notify();
4726                                    });
4727                                });
4728                                return;
4729                            }
4730                        if event.modifiers().alt {
4731                            project_panel.toggle_expand_all(entry_id, window, cx);
4732                        } else {
4733                            project_panel.toggle_expanded(entry_id, window, cx);
4734                        }
4735                    } else {
4736                        let preview_tabs_enabled = PreviewTabsSettings::get_global(cx).enabled;
4737                        let click_count = event.click_count();
4738                        let focus_opened_item = click_count > 1;
4739                        let allow_preview = preview_tabs_enabled && click_count == 1;
4740                        project_panel.open_entry(entry_id, focus_opened_item, allow_preview, cx);
4741                    }
4742                }),
4743            )
4744            .child(
4745                ListItem::new(id)
4746                    .indent_level(depth)
4747                    .indent_step_size(px(settings.indent_size))
4748                    .spacing(match settings.entry_spacing {
4749                        ProjectPanelEntrySpacing::Comfortable => ListItemSpacing::Dense,
4750                        ProjectPanelEntrySpacing::Standard => {
4751                            ListItemSpacing::ExtraDense
4752                        }
4753                    })
4754                    .selectable(false)
4755                    .when_some(canonical_path, |this, path| {
4756                        this.end_slot::<AnyElement>(
4757                            div()
4758                                .id("symlink_icon")
4759                                .pr_3()
4760                                .tooltip(move |_window, cx| {
4761                                    Tooltip::with_meta(
4762                                        path.to_string(),
4763                                        None,
4764                                        "Symbolic Link",
4765                                        cx,
4766                                    )
4767                                })
4768                                .child(
4769                                    Icon::new(IconName::ArrowUpRight)
4770                                        .size(IconSize::Indicator)
4771                                        .color(filename_text_color),
4772                                )
4773                                .into_any_element(),
4774                        )
4775                    })
4776                    .child(if let Some(icon) = &icon {
4777                        if let Some((_, decoration_color)) =
4778                            entry_diagnostic_aware_icon_decoration_and_color(diagnostic_severity)
4779                        {
4780                            let is_warning = diagnostic_severity
4781                                .map(|severity| matches!(severity, DiagnosticSeverity::WARNING))
4782                                .unwrap_or(false);
4783                            div().child(
4784                                DecoratedIcon::new(
4785                                    Icon::from_path(icon.clone()).color(Color::Muted),
4786                                    Some(
4787                                        IconDecoration::new(
4788                                            if kind.is_file() {
4789                                                if is_warning {
4790                                                    IconDecorationKind::Triangle
4791                                                } else {
4792                                                    IconDecorationKind::X
4793                                                }
4794                                            } else {
4795                                                IconDecorationKind::Dot
4796                                            },
4797                                            bg_color,
4798                                            cx,
4799                                        )
4800                                        .group_name(Some(GROUP_NAME.into()))
4801                                        .knockout_hover_color(bg_hover_color)
4802                                        .color(decoration_color.color(cx))
4803                                        .position(Point {
4804                                            x: px(-2.),
4805                                            y: px(-2.),
4806                                        }),
4807                                    ),
4808                                )
4809                                .into_any_element(),
4810                            )
4811                        } else {
4812                            h_flex().child(Icon::from_path(icon.to_string()).color(Color::Muted))
4813                        }
4814                    } else if let Some((icon_name, color)) =
4815                        entry_diagnostic_aware_icon_name_and_color(diagnostic_severity)
4816                    {
4817                        h_flex()
4818                            .size(IconSize::default().rems())
4819                            .child(Icon::new(icon_name).color(color).size(IconSize::Small))
4820                    } else {
4821                        h_flex()
4822                            .size(IconSize::default().rems())
4823                            .invisible()
4824                            .flex_none()
4825                    })
4826                    .child(
4827                        if let (Some(editor), true) = (Some(&self.filename_editor), show_editor) {
4828                            h_flex().h_6().w_full().child(editor.clone())
4829                        } else {
4830                            h_flex().h_6().map(|mut this| {
4831                                if let Some(folded_ancestors) = self.state.ancestors.get(&entry_id) {
4832                                    let components = Path::new(&file_name)
4833                                        .components()
4834                                        .map(|comp| comp.as_os_str().to_string_lossy().into_owned())
4835                                        .collect::<Vec<_>>();
4836                                    let active_index = folded_ancestors.active_index();
4837                                    let components_len = components.len();
4838                                    let delimiter = SharedString::new(path_style.separator());
4839                                    for (index, component) in components.iter().enumerate() {
4840                                        if index != 0 {
4841                                                let delimiter_target_index = index - 1;
4842                                                let target_entry_id = folded_ancestors.ancestors.get(components_len - 1 - delimiter_target_index).cloned();
4843                                                this = this.child(
4844                                                    div()
4845                                                    .when(!is_sticky, |div| {
4846                                                        div
4847                                                            .when(settings.drag_and_drop, |div| div
4848                                                            .on_drop(cx.listener(move |this, selections: &DraggedSelection, window, cx| {
4849                                                            this.hover_scroll_task.take();
4850                                                            this.drag_target_entry = None;
4851                                                            this.folded_directory_drag_target = None;
4852                                                            if let Some(target_entry_id) = target_entry_id {
4853                                                                this.drag_onto(selections, target_entry_id, kind.is_file(), window, cx);
4854                                                            }
4855                                                        }))
4856                                                        .on_drag_move(cx.listener(
4857                                                            move |this, event: &DragMoveEvent<DraggedSelection>, _, _| {
4858                                                                if event.bounds.contains(&event.event.position) {
4859                                                                    this.folded_directory_drag_target = Some(
4860                                                                        FoldedDirectoryDragTarget {
4861                                                                            entry_id,
4862                                                                            index: delimiter_target_index,
4863                                                                            is_delimiter_target: true,
4864                                                                        }
4865                                                                    );
4866                                                                } else {
4867                                                                    let is_current_target = this.folded_directory_drag_target
4868                                                                        .is_some_and(|target|
4869                                                                            target.entry_id == entry_id &&
4870                                                                            target.index == delimiter_target_index &&
4871                                                                            target.is_delimiter_target
4872                                                                        );
4873                                                                    if is_current_target {
4874                                                                        this.folded_directory_drag_target = None;
4875                                                                    }
4876                                                                }
4877
4878                                                            },
4879                                                        )))
4880                                                    })
4881                                                    .child(
4882                                                        Label::new(delimiter.clone())
4883                                                            .single_line()
4884                                                            .color(filename_text_color)
4885                                                    )
4886                                                );
4887                                        }
4888                                        let id = SharedString::from(format!(
4889                                            "project_panel_path_component_{}_{index}",
4890                                            entry_id.to_usize()
4891                                        ));
4892                                        let label = div()
4893                                            .id(id)
4894                                            .when(!is_sticky,| div| {
4895                                                div
4896                                                .when(index != components_len - 1, |div|{
4897                                                    let target_entry_id = folded_ancestors.ancestors.get(components_len - 1 - index).cloned();
4898                                                    div
4899                                                    .when(settings.drag_and_drop, |div| div
4900                                                    .on_drag_move(cx.listener(
4901                                                        move |this, event: &DragMoveEvent<DraggedSelection>, _, _| {
4902                                                        if event.bounds.contains(&event.event.position) {
4903                                                                this.folded_directory_drag_target = Some(
4904                                                                    FoldedDirectoryDragTarget {
4905                                                                        entry_id,
4906                                                                        index,
4907                                                                        is_delimiter_target: false,
4908                                                                    }
4909                                                                );
4910                                                            } else {
4911                                                                let is_current_target = this.folded_directory_drag_target
4912                                                                    .as_ref()
4913                                                                    .is_some_and(|target|
4914                                                                        target.entry_id == entry_id &&
4915                                                                        target.index == index &&
4916                                                                        !target.is_delimiter_target
4917                                                                    );
4918                                                                if is_current_target {
4919                                                                    this.folded_directory_drag_target = None;
4920                                                                }
4921                                                            }
4922                                                        },
4923                                                    ))
4924                                                    .on_drop(cx.listener(move |this, selections: &DraggedSelection, window,cx| {
4925                                                        this.hover_scroll_task.take();
4926                                                        this.drag_target_entry = None;
4927                                                        this.folded_directory_drag_target = None;
4928                                                        if let Some(target_entry_id) = target_entry_id {
4929                                                            this.drag_onto(selections, target_entry_id, kind.is_file(), window, cx);
4930                                                        }
4931                                                    }))
4932                                                    .when(folded_directory_drag_target.is_some_and(|target|
4933                                                        target.entry_id == entry_id &&
4934                                                        target.index == index
4935                                                    ), |this| {
4936                                                        this.bg(item_colors.drag_over)
4937                                                    }))
4938                                                })
4939                                            })
4940                                            .on_mouse_down(
4941                                                MouseButton::Left,
4942                                                cx.listener(move |this, _, _, cx| {
4943                                                    if index != active_index
4944                                                        && let Some(folds) =
4945                                                            this.state.ancestors.get_mut(&entry_id)
4946                                                        {
4947                                                            folds.current_ancestor_depth =
4948                                                                components_len - 1 - index;
4949                                                            cx.notify();
4950                                                        }
4951                                                }),
4952                                            )
4953                                            .child(
4954                                                Label::new(component)
4955                                                    .single_line()
4956                                                    .color(filename_text_color)
4957                                                    .when(
4958                                                        index == active_index
4959                                                            && (is_active || is_marked),
4960                                                        |this| this.underline(),
4961                                                    ),
4962                                            );
4963
4964                                        this = this.child(label);
4965                                    }
4966
4967                                    this
4968                                } else {
4969                                    this.child(
4970                                        Label::new(file_name)
4971                                            .single_line()
4972                                            .color(filename_text_color),
4973                                    )
4974                                }
4975                            })
4976                        },
4977                    )
4978                    .on_secondary_mouse_down(cx.listener(
4979                        move |this, event: &MouseDownEvent, window, cx| {
4980                            // Stop propagation to prevent the catch-all context menu for the project
4981                            // panel from being deployed.
4982                            cx.stop_propagation();
4983                            // Some context menu actions apply to all marked entries. If the user
4984                            // right-clicks on an entry that is not marked, they may not realize the
4985                            // action applies to multiple entries. To avoid inadvertent changes, all
4986                            // entries are unmarked.
4987                            if !this.marked_entries.contains(&selection) {
4988                                this.marked_entries.clear();
4989                            }
4990                            this.deploy_context_menu(event.position, entry_id, window, cx);
4991                        },
4992                    ))
4993                    .overflow_x(),
4994            )
4995            .when_some(
4996                validation_color_and_message,
4997                |this, (color, message)| {
4998                    this
4999                    .relative()
5000                    .child(
5001                        deferred(
5002                            div()
5003                            .occlude()
5004                            .absolute()
5005                            .top_full()
5006                            .left(px(-1.)) // Used px over rem so that it doesn't change with font size
5007                            .right(px(-0.5))
5008                            .py_1()
5009                            .px_2()
5010                            .border_1()
5011                            .border_color(color)
5012                            .bg(cx.theme().colors().background)
5013                            .child(
5014                                Label::new(message)
5015                                .color(Color::from(color))
5016                                .size(LabelSize::Small)
5017                            )
5018                        )
5019                    )
5020                }
5021            )
5022    }
5023
5024    fn details_for_entry(
5025        &self,
5026        entry: &Entry,
5027        worktree_id: WorktreeId,
5028        root_name: &RelPath,
5029        entries_paths: &HashSet<Arc<RelPath>>,
5030        git_status: GitSummary,
5031        sticky: Option<StickyDetails>,
5032        _window: &mut Window,
5033        cx: &mut Context<Self>,
5034    ) -> EntryDetails {
5035        let (show_file_icons, show_folder_icons) = {
5036            let settings = ProjectPanelSettings::get_global(cx);
5037            (settings.file_icons, settings.folder_icons)
5038        };
5039
5040        let expanded_entry_ids = self
5041            .state
5042            .expanded_dir_ids
5043            .get(&worktree_id)
5044            .map(Vec::as_slice)
5045            .unwrap_or(&[]);
5046        let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok();
5047
5048        let icon = match entry.kind {
5049            EntryKind::File => {
5050                if show_file_icons {
5051                    FileIcons::get_icon(entry.path.as_std_path(), cx)
5052                } else {
5053                    None
5054                }
5055            }
5056            _ => {
5057                if show_folder_icons {
5058                    FileIcons::get_folder_icon(is_expanded, entry.path.as_std_path(), cx)
5059                } else {
5060                    FileIcons::get_chevron_icon(is_expanded, cx)
5061                }
5062            }
5063        };
5064
5065        let path_style = self.project.read(cx).path_style(cx);
5066        let (depth, difference) =
5067            ProjectPanel::calculate_depth_and_difference(entry, entries_paths);
5068
5069        let filename = if difference > 1 {
5070            entry
5071                .path
5072                .last_n_components(difference)
5073                .map_or(String::new(), |suffix| {
5074                    suffix.display(path_style).to_string()
5075                })
5076        } else {
5077            entry
5078                .path
5079                .file_name()
5080                .map(|name| name.to_string())
5081                .unwrap_or_else(|| root_name.as_unix_str().to_string())
5082        };
5083
5084        let selection = SelectedEntry {
5085            worktree_id,
5086            entry_id: entry.id,
5087        };
5088        let is_marked = self.marked_entries.contains(&selection);
5089        let is_selected = self.state.selection == Some(selection);
5090
5091        let diagnostic_severity = self
5092            .diagnostics
5093            .get(&(worktree_id, entry.path.clone()))
5094            .cloned();
5095
5096        let filename_text_color =
5097            entry_git_aware_label_color(git_status, entry.is_ignored, is_marked);
5098
5099        let is_cut = self
5100            .clipboard
5101            .as_ref()
5102            .is_some_and(|e| e.is_cut() && e.items().contains(&selection));
5103
5104        EntryDetails {
5105            filename,
5106            icon,
5107            path: entry.path.clone(),
5108            depth,
5109            kind: entry.kind,
5110            is_ignored: entry.is_ignored,
5111            is_expanded,
5112            is_selected,
5113            is_marked,
5114            is_editing: false,
5115            is_processing: false,
5116            is_cut,
5117            sticky,
5118            filename_text_color,
5119            diagnostic_severity,
5120            git_status,
5121            is_private: entry.is_private,
5122            worktree_id,
5123            canonical_path: entry.canonical_path.clone(),
5124        }
5125    }
5126
5127    fn dispatch_context(&self, window: &Window, cx: &Context<Self>) -> KeyContext {
5128        let mut dispatch_context = KeyContext::new_with_defaults();
5129        dispatch_context.add("ProjectPanel");
5130        dispatch_context.add("menu");
5131
5132        let identifier = if self.filename_editor.focus_handle(cx).is_focused(window) {
5133            "editing"
5134        } else {
5135            "not_editing"
5136        };
5137
5138        dispatch_context.add(identifier);
5139        dispatch_context
5140    }
5141
5142    fn reveal_entry(
5143        &mut self,
5144        project: Entity<Project>,
5145        entry_id: ProjectEntryId,
5146        skip_ignored: bool,
5147        window: &mut Window,
5148        cx: &mut Context<Self>,
5149    ) -> Result<()> {
5150        let worktree = project
5151            .read(cx)
5152            .worktree_for_entry(entry_id, cx)
5153            .context("can't reveal a non-existent entry in the project panel")?;
5154        let worktree = worktree.read(cx);
5155        if skip_ignored
5156            && worktree
5157                .entry_for_id(entry_id)
5158                .is_none_or(|entry| entry.is_ignored && !entry.is_always_included)
5159        {
5160            anyhow::bail!("can't reveal an ignored entry in the project panel");
5161        }
5162        let is_active_item_file_diff_view = self
5163            .workspace
5164            .upgrade()
5165            .and_then(|ws| ws.read(cx).active_item(cx))
5166            .map(|item| item.act_as_type(TypeId::of::<FileDiffView>(), cx).is_some())
5167            .unwrap_or(false);
5168        if is_active_item_file_diff_view {
5169            return Ok(());
5170        }
5171
5172        let worktree_id = worktree.id();
5173        self.expand_entry(worktree_id, entry_id, cx);
5174        self.update_visible_entries(Some((worktree_id, entry_id)), false, true, window, cx);
5175        self.marked_entries.clear();
5176        self.marked_entries.push(SelectedEntry {
5177            worktree_id,
5178            entry_id,
5179        });
5180        cx.notify();
5181        Ok(())
5182    }
5183
5184    fn find_active_indent_guide(
5185        &self,
5186        indent_guides: &[IndentGuideLayout],
5187        cx: &App,
5188    ) -> Option<usize> {
5189        let (worktree, entry) = self.selected_entry(cx)?;
5190
5191        // Find the parent entry of the indent guide, this will either be the
5192        // expanded folder we have selected, or the parent of the currently
5193        // selected file/collapsed directory
5194        let mut entry = entry;
5195        loop {
5196            let is_expanded_dir = entry.is_dir()
5197                && self
5198                    .state
5199                    .expanded_dir_ids
5200                    .get(&worktree.id())
5201                    .map(|ids| ids.binary_search(&entry.id).is_ok())
5202                    .unwrap_or(false);
5203            if is_expanded_dir {
5204                break;
5205            }
5206            entry = worktree.entry_for_path(&entry.path.parent()?)?;
5207        }
5208
5209        let (active_indent_range, depth) = {
5210            let (worktree_ix, child_offset, ix) = self.index_for_entry(entry.id, worktree.id())?;
5211            let child_paths = &self.state.visible_entries[worktree_ix].entries;
5212            let mut child_count = 0;
5213            let depth = entry.path.ancestors().count();
5214            while let Some(entry) = child_paths.get(child_offset + child_count + 1) {
5215                if entry.path.ancestors().count() <= depth {
5216                    break;
5217                }
5218                child_count += 1;
5219            }
5220
5221            let start = ix + 1;
5222            let end = start + child_count;
5223
5224            let visible_worktree = &self.state.visible_entries[worktree_ix];
5225            let visible_worktree_entries = visible_worktree.index.get_or_init(|| {
5226                visible_worktree
5227                    .entries
5228                    .iter()
5229                    .map(|e| e.path.clone())
5230                    .collect()
5231            });
5232
5233            // Calculate the actual depth of the entry, taking into account that directories can be auto-folded.
5234            let (depth, _) = Self::calculate_depth_and_difference(entry, visible_worktree_entries);
5235            (start..end, depth)
5236        };
5237
5238        let candidates = indent_guides
5239            .iter()
5240            .enumerate()
5241            .filter(|(_, indent_guide)| indent_guide.offset.x == depth);
5242
5243        for (i, indent) in candidates {
5244            // Find matches that are either an exact match, partially on screen, or inside the enclosing indent
5245            if active_indent_range.start <= indent.offset.y + indent.length
5246                && indent.offset.y <= active_indent_range.end
5247            {
5248                return Some(i);
5249            }
5250        }
5251        None
5252    }
5253
5254    fn render_sticky_entries(
5255        &self,
5256        child: StickyProjectPanelCandidate,
5257        window: &mut Window,
5258        cx: &mut Context<Self>,
5259    ) -> SmallVec<[AnyElement; 8]> {
5260        let project = self.project.read(cx);
5261
5262        let Some((worktree_id, entry_ref)) = self.entry_at_index(child.index) else {
5263            return SmallVec::new();
5264        };
5265
5266        let Some(visible) = self
5267            .state
5268            .visible_entries
5269            .iter()
5270            .find(|worktree| worktree.worktree_id == worktree_id)
5271        else {
5272            return SmallVec::new();
5273        };
5274
5275        let Some(worktree) = project.worktree_for_id(worktree_id, cx) else {
5276            return SmallVec::new();
5277        };
5278        let worktree = worktree.read(cx).snapshot();
5279
5280        let paths = visible
5281            .index
5282            .get_or_init(|| visible.entries.iter().map(|e| e.path.clone()).collect());
5283
5284        let mut sticky_parents = Vec::new();
5285        let mut current_path = entry_ref.path.clone();
5286
5287        'outer: loop {
5288            if let Some(parent_path) = current_path.parent() {
5289                for ancestor_path in parent_path.ancestors() {
5290                    if paths.contains(ancestor_path)
5291                        && let Some(parent_entry) = worktree.entry_for_path(ancestor_path)
5292                    {
5293                        sticky_parents.push(parent_entry.clone());
5294                        current_path = parent_entry.path.clone();
5295                        continue 'outer;
5296                    }
5297                }
5298            }
5299            break 'outer;
5300        }
5301
5302        if sticky_parents.is_empty() {
5303            return SmallVec::new();
5304        }
5305
5306        sticky_parents.reverse();
5307
5308        let panel_settings = ProjectPanelSettings::get_global(cx);
5309        let git_status_enabled = panel_settings.git_status;
5310        let root_name = worktree.root_name();
5311
5312        let git_summaries_by_id = if git_status_enabled {
5313            visible
5314                .entries
5315                .iter()
5316                .map(|e| (e.id, e.git_summary))
5317                .collect::<HashMap<_, _>>()
5318        } else {
5319            Default::default()
5320        };
5321
5322        // already checked if non empty above
5323        let last_item_index = sticky_parents.len() - 1;
5324        sticky_parents
5325            .iter()
5326            .enumerate()
5327            .map(|(index, entry)| {
5328                let git_status = git_summaries_by_id
5329                    .get(&entry.id)
5330                    .copied()
5331                    .unwrap_or_default();
5332                let sticky_details = Some(StickyDetails {
5333                    sticky_index: index,
5334                });
5335                let details = self.details_for_entry(
5336                    entry,
5337                    worktree_id,
5338                    root_name,
5339                    paths,
5340                    git_status,
5341                    sticky_details,
5342                    window,
5343                    cx,
5344                );
5345                self.render_entry(entry.id, details, window, cx)
5346                    .when(index == last_item_index, |this| {
5347                        let shadow_color_top = hsla(0.0, 0.0, 0.0, 0.1);
5348                        let shadow_color_bottom = hsla(0.0, 0.0, 0.0, 0.);
5349                        let sticky_shadow = div()
5350                            .absolute()
5351                            .left_0()
5352                            .bottom_neg_1p5()
5353                            .h_1p5()
5354                            .w_full()
5355                            .bg(linear_gradient(
5356                                0.,
5357                                linear_color_stop(shadow_color_top, 1.),
5358                                linear_color_stop(shadow_color_bottom, 0.),
5359                            ));
5360                        this.child(sticky_shadow)
5361                    })
5362                    .into_any()
5363            })
5364            .collect()
5365    }
5366}
5367
5368#[derive(Clone)]
5369struct StickyProjectPanelCandidate {
5370    index: usize,
5371    depth: usize,
5372}
5373
5374impl StickyCandidate for StickyProjectPanelCandidate {
5375    fn depth(&self) -> usize {
5376        self.depth
5377    }
5378}
5379
5380fn item_width_estimate(depth: usize, item_text_chars: usize, is_symlink: bool) -> usize {
5381    const ICON_SIZE_FACTOR: usize = 2;
5382    let mut item_width = depth * ICON_SIZE_FACTOR + item_text_chars;
5383    if is_symlink {
5384        item_width += ICON_SIZE_FACTOR;
5385    }
5386    item_width
5387}
5388
5389impl Render for ProjectPanel {
5390    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
5391        let has_worktree = !self.state.visible_entries.is_empty();
5392        let project = self.project.read(cx);
5393        let panel_settings = ProjectPanelSettings::get_global(cx);
5394        let indent_size = panel_settings.indent_size;
5395        let show_indent_guides = panel_settings.indent_guides.show == ShowIndentGuides::Always;
5396        let show_sticky_entries = {
5397            if panel_settings.sticky_scroll {
5398                let is_scrollable = self.scroll_handle.is_scrollable();
5399                let is_scrolled = self.scroll_handle.offset().y < px(0.);
5400                is_scrollable && is_scrolled
5401            } else {
5402                false
5403            }
5404        };
5405
5406        let is_local = project.is_local();
5407
5408        if has_worktree {
5409            let item_count = self
5410                .state
5411                .visible_entries
5412                .iter()
5413                .map(|worktree| worktree.entries.len())
5414                .sum();
5415
5416            fn handle_drag_move<T: 'static>(
5417                this: &mut ProjectPanel,
5418                e: &DragMoveEvent<T>,
5419                window: &mut Window,
5420                cx: &mut Context<ProjectPanel>,
5421            ) {
5422                if let Some(previous_position) = this.previous_drag_position {
5423                    // Refresh cursor only when an actual drag happens,
5424                    // because modifiers are not updated when the cursor is not moved.
5425                    if e.event.position != previous_position {
5426                        this.refresh_drag_cursor_style(&e.event.modifiers, window, cx);
5427                    }
5428                }
5429                this.previous_drag_position = Some(e.event.position);
5430
5431                if !e.bounds.contains(&e.event.position) {
5432                    this.drag_target_entry = None;
5433                    return;
5434                }
5435                this.hover_scroll_task.take();
5436                let panel_height = e.bounds.size.height;
5437                if panel_height <= px(0.) {
5438                    return;
5439                }
5440
5441                let event_offset = e.event.position.y - e.bounds.origin.y;
5442                // How far along in the project panel is our cursor? (0. is the top of a list, 1. is the bottom)
5443                let hovered_region_offset = event_offset / panel_height;
5444
5445                // We want the scrolling to be a bit faster when the cursor is closer to the edge of a list.
5446                // These pixels offsets were picked arbitrarily.
5447                let vertical_scroll_offset = if hovered_region_offset <= 0.05 {
5448                    8.
5449                } else if hovered_region_offset <= 0.15 {
5450                    5.
5451                } else if hovered_region_offset >= 0.95 {
5452                    -8.
5453                } else if hovered_region_offset >= 0.85 {
5454                    -5.
5455                } else {
5456                    return;
5457                };
5458                let adjustment = point(px(0.), px(vertical_scroll_offset));
5459                this.hover_scroll_task = Some(cx.spawn_in(window, async move |this, cx| {
5460                    loop {
5461                        let should_stop_scrolling = this
5462                            .update(cx, |this, cx| {
5463                                this.hover_scroll_task.as_ref()?;
5464                                let handle = this.scroll_handle.0.borrow_mut();
5465                                let offset = handle.base_handle.offset();
5466
5467                                handle.base_handle.set_offset(offset + adjustment);
5468                                cx.notify();
5469                                Some(())
5470                            })
5471                            .ok()
5472                            .flatten()
5473                            .is_some();
5474                        if should_stop_scrolling {
5475                            return;
5476                        }
5477                        cx.background_executor()
5478                            .timer(Duration::from_millis(16))
5479                            .await;
5480                    }
5481                }));
5482            }
5483            h_flex()
5484                .id("project-panel")
5485                .group("project-panel")
5486                .when(panel_settings.drag_and_drop, |this| {
5487                    this.on_drag_move(cx.listener(handle_drag_move::<ExternalPaths>))
5488                        .on_drag_move(cx.listener(handle_drag_move::<DraggedSelection>))
5489                })
5490                .size_full()
5491                .relative()
5492                .on_modifiers_changed(cx.listener(
5493                    |this, event: &ModifiersChangedEvent, window, cx| {
5494                        this.refresh_drag_cursor_style(&event.modifiers, window, cx);
5495                    },
5496                ))
5497                .key_context(self.dispatch_context(window, cx))
5498                .on_action(cx.listener(Self::scroll_up))
5499                .on_action(cx.listener(Self::scroll_down))
5500                .on_action(cx.listener(Self::scroll_cursor_center))
5501                .on_action(cx.listener(Self::scroll_cursor_top))
5502                .on_action(cx.listener(Self::scroll_cursor_bottom))
5503                .on_action(cx.listener(Self::select_next))
5504                .on_action(cx.listener(Self::select_previous))
5505                .on_action(cx.listener(Self::select_first))
5506                .on_action(cx.listener(Self::select_last))
5507                .on_action(cx.listener(Self::select_parent))
5508                .on_action(cx.listener(Self::select_next_git_entry))
5509                .on_action(cx.listener(Self::select_prev_git_entry))
5510                .on_action(cx.listener(Self::select_next_diagnostic))
5511                .on_action(cx.listener(Self::select_prev_diagnostic))
5512                .on_action(cx.listener(Self::select_next_directory))
5513                .on_action(cx.listener(Self::select_prev_directory))
5514                .on_action(cx.listener(Self::expand_selected_entry))
5515                .on_action(cx.listener(Self::collapse_selected_entry))
5516                .on_action(cx.listener(Self::collapse_all_entries))
5517                .on_action(cx.listener(Self::open))
5518                .on_action(cx.listener(Self::open_permanent))
5519                .on_action(cx.listener(Self::open_split_vertical))
5520                .on_action(cx.listener(Self::open_split_horizontal))
5521                .on_action(cx.listener(Self::confirm))
5522                .on_action(cx.listener(Self::cancel))
5523                .on_action(cx.listener(Self::copy_path))
5524                .on_action(cx.listener(Self::copy_relative_path))
5525                .on_action(cx.listener(Self::new_search_in_directory))
5526                .on_action(cx.listener(Self::unfold_directory))
5527                .on_action(cx.listener(Self::fold_directory))
5528                .on_action(cx.listener(Self::remove_from_project))
5529                .on_action(cx.listener(Self::compare_marked_files))
5530                .when(!project.is_read_only(cx), |el| {
5531                    el.on_action(cx.listener(Self::new_file))
5532                        .on_action(cx.listener(Self::new_directory))
5533                        .on_action(cx.listener(Self::rename))
5534                        .on_action(cx.listener(Self::delete))
5535                        .on_action(cx.listener(Self::cut))
5536                        .on_action(cx.listener(Self::copy))
5537                        .on_action(cx.listener(Self::paste))
5538                        .on_action(cx.listener(Self::duplicate))
5539                        .when(!project.is_remote(), |el| {
5540                            el.on_action(cx.listener(Self::trash))
5541                        })
5542                })
5543                .when(project.is_local(), |el| {
5544                    el.on_action(cx.listener(Self::reveal_in_finder))
5545                        .on_action(cx.listener(Self::open_system))
5546                        .on_action(cx.listener(Self::open_in_terminal))
5547                })
5548                .when(project.is_via_remote_server(), |el| {
5549                    el.on_action(cx.listener(Self::open_in_terminal))
5550                })
5551                .track_focus(&self.focus_handle(cx))
5552                .child(
5553                    v_flex()
5554                        .child(
5555                            uniform_list("entries", item_count, {
5556                                cx.processor(|this, range: Range<usize>, window, cx| {
5557                                    this.rendered_entries_len = range.end - range.start;
5558                                    let mut items = Vec::with_capacity(this.rendered_entries_len);
5559                                    this.for_each_visible_entry(
5560                                        range,
5561                                        window,
5562                                        cx,
5563                                        |id, details, window, cx| {
5564                                            items.push(this.render_entry(id, details, window, cx));
5565                                        },
5566                                    );
5567                                    items
5568                                })
5569                            })
5570                            .when(show_indent_guides, |list| {
5571                                list.with_decoration(
5572                                    ui::indent_guides(
5573                                        px(indent_size),
5574                                        IndentGuideColors::panel(cx),
5575                                    )
5576                                    .with_compute_indents_fn(
5577                                        cx.entity(),
5578                                        |this, range, window, cx| {
5579                                            let mut items =
5580                                                SmallVec::with_capacity(range.end - range.start);
5581                                            this.iter_visible_entries(
5582                                                range,
5583                                                window,
5584                                                cx,
5585                                                |entry, _, entries, _, _| {
5586                                                    let (depth, _) =
5587                                                        Self::calculate_depth_and_difference(
5588                                                            entry, entries,
5589                                                        );
5590                                                    items.push(depth);
5591                                                },
5592                                            );
5593                                            items
5594                                        },
5595                                    )
5596                                    .on_click(cx.listener(
5597                                        |this,
5598                                         active_indent_guide: &IndentGuideLayout,
5599                                         window,
5600                                         cx| {
5601                                            if window.modifiers().secondary() {
5602                                                let ix = active_indent_guide.offset.y;
5603                                                let Some((target_entry, worktree)) = maybe!({
5604                                                    let (worktree_id, entry) =
5605                                                        this.entry_at_index(ix)?;
5606                                                    let worktree = this
5607                                                        .project
5608                                                        .read(cx)
5609                                                        .worktree_for_id(worktree_id, cx)?;
5610                                                    let target_entry = worktree
5611                                                        .read(cx)
5612                                                        .entry_for_path(&entry.path.parent()?)?;
5613                                                    Some((target_entry, worktree))
5614                                                }) else {
5615                                                    return;
5616                                                };
5617
5618                                                this.collapse_entry(
5619                                                    target_entry.clone(),
5620                                                    worktree,
5621                                                    window,
5622                                                    cx,
5623                                                );
5624                                            }
5625                                        },
5626                                    ))
5627                                    .with_render_fn(
5628                                        cx.entity(),
5629                                        move |this, params, _, cx| {
5630                                            const LEFT_OFFSET: Pixels = px(14.);
5631                                            const PADDING_Y: Pixels = px(4.);
5632                                            const HITBOX_OVERDRAW: Pixels = px(3.);
5633
5634                                            let active_indent_guide_index = this
5635                                                .find_active_indent_guide(
5636                                                    &params.indent_guides,
5637                                                    cx,
5638                                                );
5639
5640                                            let indent_size = params.indent_size;
5641                                            let item_height = params.item_height;
5642
5643                                            params
5644                                                .indent_guides
5645                                                .into_iter()
5646                                                .enumerate()
5647                                                .map(|(idx, layout)| {
5648                                                    let offset = if layout.continues_offscreen {
5649                                                        px(0.)
5650                                                    } else {
5651                                                        PADDING_Y
5652                                                    };
5653                                                    let bounds = Bounds::new(
5654                                                        point(
5655                                                            layout.offset.x * indent_size
5656                                                                + LEFT_OFFSET,
5657                                                            layout.offset.y * item_height + offset,
5658                                                        ),
5659                                                        size(
5660                                                            px(1.),
5661                                                            layout.length * item_height
5662                                                                - offset * 2.,
5663                                                        ),
5664                                                    );
5665                                                    ui::RenderedIndentGuide {
5666                                                        bounds,
5667                                                        layout,
5668                                                        is_active: Some(idx)
5669                                                            == active_indent_guide_index,
5670                                                        hitbox: Some(Bounds::new(
5671                                                            point(
5672                                                                bounds.origin.x - HITBOX_OVERDRAW,
5673                                                                bounds.origin.y,
5674                                                            ),
5675                                                            size(
5676                                                                bounds.size.width
5677                                                                    + HITBOX_OVERDRAW * 2.,
5678                                                                bounds.size.height,
5679                                                            ),
5680                                                        )),
5681                                                    }
5682                                                })
5683                                                .collect()
5684                                        },
5685                                    ),
5686                                )
5687                            })
5688                            .when(show_sticky_entries, |list| {
5689                                let sticky_items = ui::sticky_items(
5690                                    cx.entity(),
5691                                    |this, range, window, cx| {
5692                                        let mut items =
5693                                            SmallVec::with_capacity(range.end - range.start);
5694                                        this.iter_visible_entries(
5695                                            range,
5696                                            window,
5697                                            cx,
5698                                            |entry, index, entries, _, _| {
5699                                                let (depth, _) =
5700                                                    Self::calculate_depth_and_difference(
5701                                                        entry, entries,
5702                                                    );
5703                                                let candidate =
5704                                                    StickyProjectPanelCandidate { index, depth };
5705                                                items.push(candidate);
5706                                            },
5707                                        );
5708                                        items
5709                                    },
5710                                    |this, marker_entry, window, cx| {
5711                                        let sticky_entries =
5712                                            this.render_sticky_entries(marker_entry, window, cx);
5713                                        this.sticky_items_count = sticky_entries.len();
5714                                        sticky_entries
5715                                    },
5716                                );
5717                                list.with_decoration(if show_indent_guides {
5718                                    sticky_items.with_decoration(
5719                                        ui::indent_guides(
5720                                            px(indent_size),
5721                                            IndentGuideColors::panel(cx),
5722                                        )
5723                                        .with_render_fn(
5724                                            cx.entity(),
5725                                            move |_, params, _, _| {
5726                                                const LEFT_OFFSET: Pixels = px(14.);
5727
5728                                                let indent_size = params.indent_size;
5729                                                let item_height = params.item_height;
5730
5731                                                params
5732                                                    .indent_guides
5733                                                    .into_iter()
5734                                                    .map(|layout| {
5735                                                        let bounds = Bounds::new(
5736                                                            point(
5737                                                                layout.offset.x * indent_size
5738                                                                    + LEFT_OFFSET,
5739                                                                layout.offset.y * item_height,
5740                                                            ),
5741                                                            size(
5742                                                                px(1.),
5743                                                                layout.length * item_height,
5744                                                            ),
5745                                                        );
5746                                                        ui::RenderedIndentGuide {
5747                                                            bounds,
5748                                                            layout,
5749                                                            is_active: false,
5750                                                            hitbox: None,
5751                                                        }
5752                                                    })
5753                                                    .collect()
5754                                            },
5755                                        ),
5756                                    )
5757                                } else {
5758                                    sticky_items
5759                                })
5760                            })
5761                            .with_sizing_behavior(ListSizingBehavior::Infer)
5762                            .with_horizontal_sizing_behavior(
5763                                ListHorizontalSizingBehavior::Unconstrained,
5764                            )
5765                            .with_width_from_item(self.state.max_width_item_index)
5766                            .track_scroll(self.scroll_handle.clone()),
5767                        )
5768                        .child(
5769                            div()
5770                                .id("project-panel-blank-area")
5771                                .block_mouse_except_scroll()
5772                                .flex_grow()
5773                                .when(
5774                                    self.drag_target_entry.as_ref().is_some_and(
5775                                        |entry| match entry {
5776                                            DragTarget::Background => true,
5777                                            DragTarget::Entry {
5778                                                highlight_entry_id, ..
5779                                            } => self.state.last_worktree_root_id.is_some_and(
5780                                                |root_id| *highlight_entry_id == root_id,
5781                                            ),
5782                                        },
5783                                    ),
5784                                    |div| div.bg(cx.theme().colors().drop_target_background),
5785                                )
5786                                .on_drag_move::<ExternalPaths>(cx.listener(
5787                                    move |this, event: &DragMoveEvent<ExternalPaths>, _, _| {
5788                                        let Some(_last_root_id) = this.state.last_worktree_root_id
5789                                        else {
5790                                            return;
5791                                        };
5792                                        if event.bounds.contains(&event.event.position) {
5793                                            this.drag_target_entry = Some(DragTarget::Background);
5794                                        } else {
5795                                            if this.drag_target_entry.as_ref().is_some_and(|e| {
5796                                                matches!(e, DragTarget::Background)
5797                                            }) {
5798                                                this.drag_target_entry = None;
5799                                            }
5800                                        }
5801                                    },
5802                                ))
5803                                .on_drag_move::<DraggedSelection>(cx.listener(
5804                                    move |this, event: &DragMoveEvent<DraggedSelection>, _, cx| {
5805                                        let Some(last_root_id) = this.state.last_worktree_root_id
5806                                        else {
5807                                            return;
5808                                        };
5809                                        if event.bounds.contains(&event.event.position) {
5810                                            let drag_state = event.drag(cx);
5811                                            if this.should_highlight_background_for_selection_drag(
5812                                                &drag_state,
5813                                                last_root_id,
5814                                                cx,
5815                                            ) {
5816                                                this.drag_target_entry =
5817                                                    Some(DragTarget::Background);
5818                                            }
5819                                        } else {
5820                                            if this.drag_target_entry.as_ref().is_some_and(|e| {
5821                                                matches!(e, DragTarget::Background)
5822                                            }) {
5823                                                this.drag_target_entry = None;
5824                                            }
5825                                        }
5826                                    },
5827                                ))
5828                                .on_drop(cx.listener(
5829                                    move |this, external_paths: &ExternalPaths, window, cx| {
5830                                        this.drag_target_entry = None;
5831                                        this.hover_scroll_task.take();
5832                                        if let Some(entry_id) = this.state.last_worktree_root_id {
5833                                            this.drop_external_files(
5834                                                external_paths.paths(),
5835                                                entry_id,
5836                                                window,
5837                                                cx,
5838                                            );
5839                                        }
5840                                        cx.stop_propagation();
5841                                    },
5842                                ))
5843                                .on_drop(cx.listener(
5844                                    move |this, selections: &DraggedSelection, window, cx| {
5845                                        this.drag_target_entry = None;
5846                                        this.hover_scroll_task.take();
5847                                        if let Some(entry_id) = this.state.last_worktree_root_id {
5848                                            this.drag_onto(selections, entry_id, false, window, cx);
5849                                        }
5850                                        cx.stop_propagation();
5851                                    },
5852                                ))
5853                                .on_click(cx.listener(|this, event, window, cx| {
5854                                    if matches!(event, gpui::ClickEvent::Keyboard(_)) {
5855                                        return;
5856                                    }
5857                                    cx.stop_propagation();
5858                                    this.state.selection = None;
5859                                    this.marked_entries.clear();
5860                                    this.focus_handle(cx).focus(window);
5861                                }))
5862                                .on_mouse_down(
5863                                    MouseButton::Right,
5864                                    cx.listener(move |this, event: &MouseDownEvent, window, cx| {
5865                                        // When deploying the context menu anywhere below the last project entry,
5866                                        // act as if the user clicked the root of the last worktree.
5867                                        if let Some(entry_id) = this.state.last_worktree_root_id {
5868                                            this.deploy_context_menu(
5869                                                event.position,
5870                                                entry_id,
5871                                                window,
5872                                                cx,
5873                                            );
5874                                        }
5875                                    }),
5876                                )
5877                                .when(!project.is_read_only(cx), |el| {
5878                                    el.on_click(cx.listener(
5879                                        |this, event: &gpui::ClickEvent, window, cx| {
5880                                            if event.click_count() > 1
5881                                                && let Some(entry_id) =
5882                                                    this.state.last_worktree_root_id
5883                                            {
5884                                                let project = this.project.read(cx);
5885
5886                                                let worktree_id = if let Some(worktree) =
5887                                                    project.worktree_for_entry(entry_id, cx)
5888                                                {
5889                                                    worktree.read(cx).id()
5890                                                } else {
5891                                                    return;
5892                                                };
5893
5894                                                this.state.selection = Some(SelectedEntry {
5895                                                    worktree_id,
5896                                                    entry_id,
5897                                                });
5898
5899                                                this.new_file(&NewFile, window, cx);
5900                                            }
5901                                        },
5902                                    ))
5903                                }),
5904                        )
5905                        .size_full(),
5906                )
5907                .custom_scrollbars(
5908                    Scrollbars::for_settings::<ProjectPanelSettings>()
5909                        .tracked_scroll_handle(self.scroll_handle.clone())
5910                        .with_track_along(
5911                            ScrollAxes::Horizontal,
5912                            cx.theme().colors().panel_background,
5913                        )
5914                        .notify_content(),
5915                    window,
5916                    cx,
5917                )
5918                .children(self.context_menu.as_ref().map(|(menu, position, _)| {
5919                    deferred(
5920                        anchored()
5921                            .position(*position)
5922                            .anchor(gpui::Corner::TopLeft)
5923                            .child(menu.clone()),
5924                    )
5925                    .with_priority(3)
5926                }))
5927        } else {
5928            let focus_handle = self.focus_handle(cx);
5929
5930            v_flex()
5931                .id("empty-project_panel")
5932                .p_4()
5933                .size_full()
5934                .items_center()
5935                .justify_center()
5936                .gap_1()
5937                .track_focus(&self.focus_handle(cx))
5938                .child(
5939                    Button::new("open_project", "Open Project")
5940                        .full_width()
5941                        .key_binding(KeyBinding::for_action_in(
5942                            &workspace::Open,
5943                            &focus_handle,
5944                            cx,
5945                        ))
5946                        .on_click(cx.listener(|this, _, window, cx| {
5947                            this.workspace
5948                                .update(cx, |_, cx| {
5949                                    window.dispatch_action(workspace::Open.boxed_clone(), cx);
5950                                })
5951                                .log_err();
5952                        })),
5953                )
5954                .child(
5955                    h_flex()
5956                        .w_1_2()
5957                        .gap_2()
5958                        .child(Divider::horizontal())
5959                        .child(Label::new("or").size(LabelSize::XSmall).color(Color::Muted))
5960                        .child(Divider::horizontal()),
5961                )
5962                .child(
5963                    Button::new("clone_repo", "Clone Repository")
5964                        .full_width()
5965                        .on_click(cx.listener(|this, _, window, cx| {
5966                            this.workspace
5967                                .update(cx, |_, cx| {
5968                                    window.dispatch_action(git::Clone.boxed_clone(), cx);
5969                                })
5970                                .log_err();
5971                        })),
5972                )
5973                .when(is_local, |div| {
5974                    div.when(panel_settings.drag_and_drop, |div| {
5975                        div.drag_over::<ExternalPaths>(|style, _, _, cx| {
5976                            style.bg(cx.theme().colors().drop_target_background)
5977                        })
5978                        .on_drop(cx.listener(
5979                            move |this, external_paths: &ExternalPaths, window, cx| {
5980                                this.drag_target_entry = None;
5981                                this.hover_scroll_task.take();
5982                                if let Some(task) = this
5983                                    .workspace
5984                                    .update(cx, |workspace, cx| {
5985                                        workspace.open_workspace_for_paths(
5986                                            true,
5987                                            external_paths.paths().to_owned(),
5988                                            window,
5989                                            cx,
5990                                        )
5991                                    })
5992                                    .log_err()
5993                                {
5994                                    task.detach_and_log_err(cx);
5995                                }
5996                                cx.stop_propagation();
5997                            },
5998                        ))
5999                    })
6000                })
6001        }
6002    }
6003}
6004
6005impl Render for DraggedProjectEntryView {
6006    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
6007        let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
6008        h_flex()
6009            .font(ui_font)
6010            .pl(self.click_offset.x + px(12.))
6011            .pt(self.click_offset.y + px(12.))
6012            .child(
6013                div()
6014                    .flex()
6015                    .gap_1()
6016                    .items_center()
6017                    .py_1()
6018                    .px_2()
6019                    .rounded_lg()
6020                    .bg(cx.theme().colors().background)
6021                    .map(|this| {
6022                        if self.selections.len() > 1 && self.selections.contains(&self.selection) {
6023                            this.child(Label::new(format!("{} entries", self.selections.len())))
6024                        } else {
6025                            this.child(if let Some(icon) = &self.icon {
6026                                div().child(Icon::from_path(icon.clone()))
6027                            } else {
6028                                div()
6029                            })
6030                            .child(Label::new(self.filename.clone()))
6031                        }
6032                    }),
6033            )
6034    }
6035}
6036
6037impl EventEmitter<Event> for ProjectPanel {}
6038
6039impl EventEmitter<PanelEvent> for ProjectPanel {}
6040
6041impl Panel for ProjectPanel {
6042    fn position(&self, _: &Window, cx: &App) -> DockPosition {
6043        match ProjectPanelSettings::get_global(cx).dock {
6044            DockSide::Left => DockPosition::Left,
6045            DockSide::Right => DockPosition::Right,
6046        }
6047    }
6048
6049    fn position_is_valid(&self, position: DockPosition) -> bool {
6050        matches!(position, DockPosition::Left | DockPosition::Right)
6051    }
6052
6053    fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
6054        settings::update_settings_file(self.fs.clone(), cx, move |settings, _| {
6055            let dock = match position {
6056                DockPosition::Left | DockPosition::Bottom => DockSide::Left,
6057                DockPosition::Right => DockSide::Right,
6058            };
6059            settings.project_panel.get_or_insert_default().dock = Some(dock);
6060        });
6061    }
6062
6063    fn size(&self, _: &Window, cx: &App) -> Pixels {
6064        self.width
6065            .unwrap_or_else(|| ProjectPanelSettings::get_global(cx).default_width)
6066    }
6067
6068    fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
6069        self.width = size;
6070        cx.notify();
6071        cx.defer_in(window, |this, _, cx| {
6072            this.serialize(cx);
6073        });
6074    }
6075
6076    fn icon(&self, _: &Window, cx: &App) -> Option<IconName> {
6077        ProjectPanelSettings::get_global(cx)
6078            .button
6079            .then_some(IconName::FileTree)
6080    }
6081
6082    fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
6083        Some("Project Panel")
6084    }
6085
6086    fn toggle_action(&self) -> Box<dyn Action> {
6087        Box::new(ToggleFocus)
6088    }
6089
6090    fn persistent_name() -> &'static str {
6091        "Project Panel"
6092    }
6093
6094    fn panel_key() -> &'static str {
6095        PROJECT_PANEL_KEY
6096    }
6097
6098    fn starts_open(&self, _: &Window, cx: &App) -> bool {
6099        if !ProjectPanelSettings::get_global(cx).starts_open {
6100            return false;
6101        }
6102
6103        let project = &self.project.read(cx);
6104        project.visible_worktrees(cx).any(|tree| {
6105            tree.read(cx)
6106                .root_entry()
6107                .is_some_and(|entry| entry.is_dir())
6108        })
6109    }
6110
6111    fn activation_priority(&self) -> u32 {
6112        0
6113    }
6114}
6115
6116impl Focusable for ProjectPanel {
6117    fn focus_handle(&self, _cx: &App) -> FocusHandle {
6118        self.focus_handle.clone()
6119    }
6120}
6121
6122impl ClipboardEntry {
6123    fn is_cut(&self) -> bool {
6124        matches!(self, Self::Cut { .. })
6125    }
6126
6127    fn items(&self) -> &BTreeSet<SelectedEntry> {
6128        match self {
6129            ClipboardEntry::Copied(entries) | ClipboardEntry::Cut(entries) => entries,
6130        }
6131    }
6132
6133    fn into_copy_entry(self) -> Self {
6134        match self {
6135            ClipboardEntry::Copied(_) => self,
6136            ClipboardEntry::Cut(entries) => ClipboardEntry::Copied(entries),
6137        }
6138    }
6139}
6140
6141#[inline]
6142fn cmp_directories_first(a: &Entry, b: &Entry) -> cmp::Ordering {
6143    util::paths::compare_rel_paths((&a.path, a.is_file()), (&b.path, b.is_file()))
6144}
6145
6146#[inline]
6147fn cmp_mixed(a: &Entry, b: &Entry) -> cmp::Ordering {
6148    util::paths::compare_rel_paths_mixed((&a.path, a.is_file()), (&b.path, b.is_file()))
6149}
6150
6151#[inline]
6152fn cmp_files_first(a: &Entry, b: &Entry) -> cmp::Ordering {
6153    util::paths::compare_rel_paths_files_first((&a.path, a.is_file()), (&b.path, b.is_file()))
6154}
6155
6156#[inline]
6157fn cmp_with_mode(a: &Entry, b: &Entry, mode: &settings::ProjectPanelSortMode) -> cmp::Ordering {
6158    match mode {
6159        settings::ProjectPanelSortMode::DirectoriesFirst => cmp_directories_first(a, b),
6160        settings::ProjectPanelSortMode::Mixed => cmp_mixed(a, b),
6161        settings::ProjectPanelSortMode::FilesFirst => cmp_files_first(a, b),
6162    }
6163}
6164
6165pub fn sort_worktree_entries_with_mode(
6166    entries: &mut [impl AsRef<Entry>],
6167    mode: settings::ProjectPanelSortMode,
6168) {
6169    entries.sort_by(|lhs, rhs| cmp_with_mode(lhs.as_ref(), rhs.as_ref(), &mode));
6170}
6171
6172pub fn par_sort_worktree_entries_with_mode(
6173    entries: &mut Vec<GitEntry>,
6174    mode: settings::ProjectPanelSortMode,
6175) {
6176    entries.par_sort_by(|lhs, rhs| cmp_with_mode(lhs, rhs, &mode));
6177}
6178
6179#[cfg(test)]
6180mod project_panel_tests;