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