project_panel.rs

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