project_panel.rs

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