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