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