project_panel.rs

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