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