project_panel.rs

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