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