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            self.expand_entry(worktree_id, entry.id, cx);
2347            Some(())
2348        });
2349    }
2350
2351    fn duplicate(&mut self, _: &Duplicate, window: &mut Window, cx: &mut Context<Self>) {
2352        self.copy(&Copy {}, window, cx);
2353        self.paste(&Paste {}, window, cx);
2354    }
2355
2356    fn copy_path(
2357        &mut self,
2358        _: &zed_actions::workspace::CopyPath,
2359        _: &mut Window,
2360        cx: &mut Context<Self>,
2361    ) {
2362        let abs_file_paths = {
2363            let project = self.project.read(cx);
2364            self.effective_entries()
2365                .into_iter()
2366                .filter_map(|entry| {
2367                    let entry_path = project.path_for_entry(entry.entry_id, cx)?.path;
2368                    Some(
2369                        project
2370                            .worktree_for_id(entry.worktree_id, cx)?
2371                            .read(cx)
2372                            .abs_path()
2373                            .join(entry_path)
2374                            .to_string_lossy()
2375                            .to_string(),
2376                    )
2377                })
2378                .collect::<Vec<_>>()
2379        };
2380        if !abs_file_paths.is_empty() {
2381            cx.write_to_clipboard(ClipboardItem::new_string(abs_file_paths.join("\n")));
2382        }
2383    }
2384
2385    fn copy_relative_path(
2386        &mut self,
2387        _: &zed_actions::workspace::CopyRelativePath,
2388        _: &mut Window,
2389        cx: &mut Context<Self>,
2390    ) {
2391        let file_paths = {
2392            let project = self.project.read(cx);
2393            self.effective_entries()
2394                .into_iter()
2395                .filter_map(|entry| {
2396                    Some(
2397                        project
2398                            .path_for_entry(entry.entry_id, cx)?
2399                            .path
2400                            .to_string_lossy()
2401                            .to_string(),
2402                    )
2403                })
2404                .collect::<Vec<_>>()
2405        };
2406        if !file_paths.is_empty() {
2407            cx.write_to_clipboard(ClipboardItem::new_string(file_paths.join("\n")));
2408        }
2409    }
2410
2411    fn reveal_in_finder(
2412        &mut self,
2413        _: &RevealInFileManager,
2414        _: &mut Window,
2415        cx: &mut Context<Self>,
2416    ) {
2417        if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
2418            cx.reveal_path(&worktree.read(cx).abs_path().join(&entry.path));
2419        }
2420    }
2421
2422    fn remove_from_project(
2423        &mut self,
2424        _: &RemoveFromProject,
2425        _window: &mut Window,
2426        cx: &mut Context<Self>,
2427    ) {
2428        for entry in self.effective_entries().iter() {
2429            let worktree_id = entry.worktree_id;
2430            self.project
2431                .update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
2432        }
2433    }
2434
2435    fn open_system(&mut self, _: &OpenWithSystem, _: &mut Window, cx: &mut Context<Self>) {
2436        if let Some((worktree, entry)) = self.selected_entry(cx) {
2437            let abs_path = worktree.abs_path().join(&entry.path);
2438            cx.open_with_system(&abs_path);
2439        }
2440    }
2441
2442    fn open_in_terminal(
2443        &mut self,
2444        _: &OpenInTerminal,
2445        window: &mut Window,
2446        cx: &mut Context<Self>,
2447    ) {
2448        if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
2449            let abs_path = match &entry.canonical_path {
2450                Some(canonical_path) => Some(canonical_path.to_path_buf()),
2451                None => worktree.read(cx).absolutize(&entry.path).ok(),
2452            };
2453
2454            let working_directory = if entry.is_dir() {
2455                abs_path
2456            } else {
2457                abs_path.and_then(|path| Some(path.parent()?.to_path_buf()))
2458            };
2459            if let Some(working_directory) = working_directory {
2460                window.dispatch_action(
2461                    workspace::OpenTerminal { working_directory }.boxed_clone(),
2462                    cx,
2463                )
2464            }
2465        }
2466    }
2467
2468    pub fn new_search_in_directory(
2469        &mut self,
2470        _: &NewSearchInDirectory,
2471        window: &mut Window,
2472        cx: &mut Context<Self>,
2473    ) {
2474        if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
2475            let dir_path = if entry.is_dir() {
2476                entry.path.clone()
2477            } else {
2478                // entry is a file, use its parent directory
2479                match entry.path.parent() {
2480                    Some(parent) => Arc::from(parent),
2481                    None => {
2482                        // File at root, open search with empty filter
2483                        self.workspace
2484                            .update(cx, |workspace, cx| {
2485                                search::ProjectSearchView::new_search_in_directory(
2486                                    workspace,
2487                                    Path::new(""),
2488                                    window,
2489                                    cx,
2490                                );
2491                            })
2492                            .ok();
2493                        return;
2494                    }
2495                }
2496            };
2497
2498            let include_root = self.project.read(cx).visible_worktrees(cx).count() > 1;
2499            let dir_path = if include_root {
2500                let mut full_path = PathBuf::from(worktree.read(cx).root_name());
2501                full_path.push(&dir_path);
2502                Arc::from(full_path)
2503            } else {
2504                dir_path
2505            };
2506
2507            self.workspace
2508                .update(cx, |workspace, cx| {
2509                    search::ProjectSearchView::new_search_in_directory(
2510                        workspace, &dir_path, window, cx,
2511                    );
2512                })
2513                .ok();
2514        }
2515    }
2516
2517    fn move_entry(
2518        &mut self,
2519        entry_to_move: ProjectEntryId,
2520        destination: ProjectEntryId,
2521        destination_is_file: bool,
2522        cx: &mut Context<Self>,
2523    ) {
2524        if self
2525            .project
2526            .read(cx)
2527            .entry_is_worktree_root(entry_to_move, cx)
2528        {
2529            self.move_worktree_root(entry_to_move, destination, cx)
2530        } else {
2531            self.move_worktree_entry(entry_to_move, destination, destination_is_file, cx)
2532        }
2533    }
2534
2535    fn move_worktree_root(
2536        &mut self,
2537        entry_to_move: ProjectEntryId,
2538        destination: ProjectEntryId,
2539        cx: &mut Context<Self>,
2540    ) {
2541        self.project.update(cx, |project, cx| {
2542            let Some(worktree_to_move) = project.worktree_for_entry(entry_to_move, cx) else {
2543                return;
2544            };
2545            let Some(destination_worktree) = project.worktree_for_entry(destination, cx) else {
2546                return;
2547            };
2548
2549            let worktree_id = worktree_to_move.read(cx).id();
2550            let destination_id = destination_worktree.read(cx).id();
2551
2552            project
2553                .move_worktree(worktree_id, destination_id, cx)
2554                .log_err();
2555        });
2556    }
2557
2558    fn move_worktree_entry(
2559        &mut self,
2560        entry_to_move: ProjectEntryId,
2561        destination: ProjectEntryId,
2562        destination_is_file: bool,
2563        cx: &mut Context<Self>,
2564    ) {
2565        if entry_to_move == destination {
2566            return;
2567        }
2568
2569        let destination_worktree = self.project.update(cx, |project, cx| {
2570            let entry_path = project.path_for_entry(entry_to_move, cx)?;
2571            let destination_entry_path = project.path_for_entry(destination, cx)?.path.clone();
2572
2573            let mut destination_path = destination_entry_path.as_ref();
2574            if destination_is_file {
2575                destination_path = destination_path.parent()?;
2576            }
2577
2578            let mut new_path = destination_path.to_path_buf();
2579            new_path.push(entry_path.path.file_name()?);
2580            if new_path != entry_path.path.as_ref() {
2581                let task = project.rename_entry(entry_to_move, new_path, cx);
2582                cx.foreground_executor().spawn(task).detach_and_log_err(cx);
2583            }
2584
2585            project.worktree_id_for_entry(destination, cx)
2586        });
2587
2588        if let Some(destination_worktree) = destination_worktree {
2589            self.expand_entry(destination_worktree, destination, cx);
2590        }
2591    }
2592
2593    fn index_for_selection(&self, selection: SelectedEntry) -> Option<(usize, usize, usize)> {
2594        let mut entry_index = 0;
2595        let mut visible_entries_index = 0;
2596        for (worktree_index, (worktree_id, worktree_entries, _)) in
2597            self.visible_entries.iter().enumerate()
2598        {
2599            if *worktree_id == selection.worktree_id {
2600                for entry in worktree_entries {
2601                    if entry.id == selection.entry_id {
2602                        return Some((worktree_index, entry_index, visible_entries_index));
2603                    } else {
2604                        visible_entries_index += 1;
2605                        entry_index += 1;
2606                    }
2607                }
2608                break;
2609            } else {
2610                visible_entries_index += worktree_entries.len();
2611            }
2612        }
2613        None
2614    }
2615
2616    fn disjoint_entries(&self, cx: &App) -> BTreeSet<SelectedEntry> {
2617        let marked_entries = self.effective_entries();
2618        let mut sanitized_entries = BTreeSet::new();
2619        if marked_entries.is_empty() {
2620            return sanitized_entries;
2621        }
2622
2623        let project = self.project.read(cx);
2624        let marked_entries_by_worktree: HashMap<WorktreeId, Vec<SelectedEntry>> = marked_entries
2625            .into_iter()
2626            .filter(|entry| !project.entry_is_worktree_root(entry.entry_id, cx))
2627            .fold(HashMap::default(), |mut map, entry| {
2628                map.entry(entry.worktree_id).or_default().push(entry);
2629                map
2630            });
2631
2632        for (worktree_id, marked_entries) in marked_entries_by_worktree {
2633            if let Some(worktree) = project.worktree_for_id(worktree_id, cx) {
2634                let worktree = worktree.read(cx);
2635                let marked_dir_paths = marked_entries
2636                    .iter()
2637                    .filter_map(|entry| {
2638                        worktree.entry_for_id(entry.entry_id).and_then(|entry| {
2639                            if entry.is_dir() {
2640                                Some(entry.path.as_ref())
2641                            } else {
2642                                None
2643                            }
2644                        })
2645                    })
2646                    .collect::<BTreeSet<_>>();
2647
2648                sanitized_entries.extend(marked_entries.into_iter().filter(|entry| {
2649                    let Some(entry_info) = worktree.entry_for_id(entry.entry_id) else {
2650                        return false;
2651                    };
2652                    let entry_path = entry_info.path.as_ref();
2653                    let inside_marked_dir = marked_dir_paths.iter().any(|&marked_dir_path| {
2654                        entry_path != marked_dir_path && entry_path.starts_with(marked_dir_path)
2655                    });
2656                    !inside_marked_dir
2657                }));
2658            }
2659        }
2660
2661        sanitized_entries
2662    }
2663
2664    fn effective_entries(&self) -> BTreeSet<SelectedEntry> {
2665        if let Some(selection) = self.selection {
2666            let selection = SelectedEntry {
2667                entry_id: self.resolve_entry(selection.entry_id),
2668                worktree_id: selection.worktree_id,
2669            };
2670
2671            // Default to using just the selected item when nothing is marked.
2672            if self.marked_entries.is_empty() {
2673                return BTreeSet::from([selection]);
2674            }
2675
2676            // Allow operating on the selected item even when something else is marked,
2677            // making it easier to perform one-off actions without clearing a mark.
2678            if self.marked_entries.len() == 1 && !self.marked_entries.contains(&selection) {
2679                return BTreeSet::from([selection]);
2680            }
2681        }
2682
2683        // Return only marked entries since we've already handled special cases where
2684        // only selection should take precedence. At this point, marked entries may or
2685        // may not include the current selection, which is intentional.
2686        self.marked_entries
2687            .iter()
2688            .map(|entry| SelectedEntry {
2689                entry_id: self.resolve_entry(entry.entry_id),
2690                worktree_id: entry.worktree_id,
2691            })
2692            .collect::<BTreeSet<_>>()
2693    }
2694
2695    /// Finds the currently selected subentry for a given leaf entry id. If a given entry
2696    /// has no ancestors, the project entry ID that's passed in is returned as-is.
2697    fn resolve_entry(&self, id: ProjectEntryId) -> ProjectEntryId {
2698        self.ancestors
2699            .get(&id)
2700            .and_then(|ancestors| {
2701                if ancestors.current_ancestor_depth == 0 {
2702                    return None;
2703                }
2704                ancestors.ancestors.get(ancestors.current_ancestor_depth)
2705            })
2706            .copied()
2707            .unwrap_or(id)
2708    }
2709
2710    pub fn selected_entry<'a>(&self, cx: &'a App) -> Option<(&'a Worktree, &'a project::Entry)> {
2711        let (worktree, entry) = self.selected_entry_handle(cx)?;
2712        Some((worktree.read(cx), entry))
2713    }
2714
2715    /// Compared to selected_entry, this function resolves to the currently
2716    /// selected subentry if dir auto-folding is enabled.
2717    fn selected_sub_entry<'a>(
2718        &self,
2719        cx: &'a App,
2720    ) -> Option<(Entity<Worktree>, &'a project::Entry)> {
2721        let (worktree, mut entry) = self.selected_entry_handle(cx)?;
2722
2723        let resolved_id = self.resolve_entry(entry.id);
2724        if resolved_id != entry.id {
2725            let worktree = worktree.read(cx);
2726            entry = worktree.entry_for_id(resolved_id)?;
2727        }
2728        Some((worktree, entry))
2729    }
2730    fn selected_entry_handle<'a>(
2731        &self,
2732        cx: &'a App,
2733    ) -> Option<(Entity<Worktree>, &'a project::Entry)> {
2734        let selection = self.selection?;
2735        let project = self.project.read(cx);
2736        let worktree = project.worktree_for_id(selection.worktree_id, cx)?;
2737        let entry = worktree.read(cx).entry_for_id(selection.entry_id)?;
2738        Some((worktree, entry))
2739    }
2740
2741    fn expand_to_selection(&mut self, cx: &mut Context<Self>) -> Option<()> {
2742        let (worktree, entry) = self.selected_entry(cx)?;
2743        let expanded_dir_ids = self.expanded_dir_ids.entry(worktree.id()).or_default();
2744
2745        for path in entry.path.ancestors() {
2746            let Some(entry) = worktree.entry_for_path(path) else {
2747                continue;
2748            };
2749            if entry.is_dir() {
2750                if let Err(idx) = expanded_dir_ids.binary_search(&entry.id) {
2751                    expanded_dir_ids.insert(idx, entry.id);
2752                }
2753            }
2754        }
2755
2756        Some(())
2757    }
2758
2759    fn update_visible_entries(
2760        &mut self,
2761        new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,
2762        cx: &mut Context<Self>,
2763    ) {
2764        let settings = ProjectPanelSettings::get_global(cx);
2765        let auto_collapse_dirs = settings.auto_fold_dirs;
2766        let hide_gitignore = settings.hide_gitignore;
2767        let project = self.project.read(cx);
2768        let repo_snapshots = project.git_store().read(cx).repo_snapshots(cx);
2769        self.last_worktree_root_id = project
2770            .visible_worktrees(cx)
2771            .next_back()
2772            .and_then(|worktree| worktree.read(cx).root_entry())
2773            .map(|entry| entry.id);
2774
2775        let old_ancestors = std::mem::take(&mut self.ancestors);
2776        self.visible_entries.clear();
2777        let mut max_width_item = None;
2778        for worktree in project.visible_worktrees(cx) {
2779            let worktree_snapshot = worktree.read(cx).snapshot();
2780            let worktree_id = worktree_snapshot.id();
2781
2782            let expanded_dir_ids = match self.expanded_dir_ids.entry(worktree_id) {
2783                hash_map::Entry::Occupied(e) => e.into_mut(),
2784                hash_map::Entry::Vacant(e) => {
2785                    // The first time a worktree's root entry becomes available,
2786                    // mark that root entry as expanded.
2787                    if let Some(entry) = worktree_snapshot.root_entry() {
2788                        e.insert(vec![entry.id]).as_slice()
2789                    } else {
2790                        &[]
2791                    }
2792                }
2793            };
2794
2795            let mut new_entry_parent_id = None;
2796            let mut new_entry_kind = EntryKind::Dir;
2797            if let Some(edit_state) = &self.edit_state {
2798                if edit_state.worktree_id == worktree_id && edit_state.is_new_entry() {
2799                    new_entry_parent_id = Some(edit_state.entry_id);
2800                    new_entry_kind = if edit_state.is_dir {
2801                        EntryKind::Dir
2802                    } else {
2803                        EntryKind::File
2804                    };
2805                }
2806            }
2807
2808            let mut visible_worktree_entries = Vec::new();
2809            let mut entry_iter =
2810                GitTraversal::new(&repo_snapshots, worktree_snapshot.entries(true, 0));
2811            let mut auto_folded_ancestors = vec![];
2812            while let Some(entry) = entry_iter.entry() {
2813                if auto_collapse_dirs && entry.kind.is_dir() {
2814                    auto_folded_ancestors.push(entry.id);
2815                    if !self.unfolded_dir_ids.contains(&entry.id) {
2816                        if let Some(root_path) = worktree_snapshot.root_entry() {
2817                            let mut child_entries = worktree_snapshot.child_entries(&entry.path);
2818                            if let Some(child) = child_entries.next() {
2819                                if entry.path != root_path.path
2820                                    && child_entries.next().is_none()
2821                                    && child.kind.is_dir()
2822                                {
2823                                    entry_iter.advance();
2824
2825                                    continue;
2826                                }
2827                            }
2828                        }
2829                    }
2830                    let depth = old_ancestors
2831                        .get(&entry.id)
2832                        .map(|ancestor| ancestor.current_ancestor_depth)
2833                        .unwrap_or_default()
2834                        .min(auto_folded_ancestors.len());
2835                    if let Some(edit_state) = &mut self.edit_state {
2836                        if edit_state.entry_id == entry.id {
2837                            edit_state.depth = depth;
2838                        }
2839                    }
2840                    let mut ancestors = std::mem::take(&mut auto_folded_ancestors);
2841                    if ancestors.len() > 1 {
2842                        ancestors.reverse();
2843                        self.ancestors.insert(
2844                            entry.id,
2845                            FoldedAncestors {
2846                                current_ancestor_depth: depth,
2847                                ancestors,
2848                            },
2849                        );
2850                    }
2851                }
2852                auto_folded_ancestors.clear();
2853                if !hide_gitignore || !entry.is_ignored {
2854                    visible_worktree_entries.push(entry.to_owned());
2855                }
2856                let precedes_new_entry = if let Some(new_entry_id) = new_entry_parent_id {
2857                    entry.id == new_entry_id || {
2858                        self.ancestors
2859                            .get(&entry.id)
2860                            .map_or(false, |entries| entries.ancestors.contains(&new_entry_id))
2861                    }
2862                } else {
2863                    false
2864                };
2865                if precedes_new_entry && (!hide_gitignore || !entry.is_ignored) {
2866                    visible_worktree_entries.push(GitEntry {
2867                        entry: Entry {
2868                            id: NEW_ENTRY_ID,
2869                            kind: new_entry_kind,
2870                            path: entry.path.join("\0").into(),
2871                            inode: 0,
2872                            mtime: entry.mtime,
2873                            size: entry.size,
2874                            is_ignored: entry.is_ignored,
2875                            is_external: false,
2876                            is_private: false,
2877                            is_always_included: entry.is_always_included,
2878                            canonical_path: entry.canonical_path.clone(),
2879                            char_bag: entry.char_bag,
2880                            is_fifo: entry.is_fifo,
2881                        },
2882                        git_summary: entry.git_summary,
2883                    });
2884                }
2885                let worktree_abs_path = worktree.read(cx).abs_path();
2886                let (depth, path) = if Some(entry.entry) == worktree.read(cx).root_entry() {
2887                    let Some(path_name) = worktree_abs_path.file_name() else {
2888                        continue;
2889                    };
2890                    let path = ArcCow::Borrowed(Path::new(path_name));
2891                    let depth = 0;
2892                    (depth, path)
2893                } else if entry.is_file() {
2894                    let Some(path_name) = entry
2895                        .path
2896                        .file_name()
2897                        .with_context(|| format!("Non-root entry has no file name: {entry:?}"))
2898                        .log_err()
2899                    else {
2900                        continue;
2901                    };
2902                    let path = ArcCow::Borrowed(Path::new(path_name));
2903                    let depth = entry.path.ancestors().count() - 1;
2904                    (depth, path)
2905                } else {
2906                    let path = self
2907                        .ancestors
2908                        .get(&entry.id)
2909                        .and_then(|ancestors| {
2910                            let outermost_ancestor = ancestors.ancestors.last()?;
2911                            let root_folded_entry = worktree
2912                                .read(cx)
2913                                .entry_for_id(*outermost_ancestor)?
2914                                .path
2915                                .as_ref();
2916                            entry
2917                                .path
2918                                .strip_prefix(root_folded_entry)
2919                                .ok()
2920                                .and_then(|suffix| {
2921                                    let full_path = Path::new(root_folded_entry.file_name()?);
2922                                    Some(ArcCow::Owned(Arc::<Path>::from(full_path.join(suffix))))
2923                                })
2924                        })
2925                        .or_else(|| entry.path.file_name().map(Path::new).map(ArcCow::Borrowed))
2926                        .unwrap_or_else(|| ArcCow::Owned(entry.path.clone()));
2927                    let depth = path.components().count();
2928                    (depth, path)
2929                };
2930                let width_estimate = item_width_estimate(
2931                    depth,
2932                    path.to_string_lossy().chars().count(),
2933                    entry.canonical_path.is_some(),
2934                );
2935
2936                match max_width_item.as_mut() {
2937                    Some((id, worktree_id, width)) => {
2938                        if *width < width_estimate {
2939                            *id = entry.id;
2940                            *worktree_id = worktree.read(cx).id();
2941                            *width = width_estimate;
2942                        }
2943                    }
2944                    None => {
2945                        max_width_item = Some((entry.id, worktree.read(cx).id(), width_estimate))
2946                    }
2947                }
2948
2949                if expanded_dir_ids.binary_search(&entry.id).is_err()
2950                    && entry_iter.advance_to_sibling()
2951                {
2952                    continue;
2953                }
2954                entry_iter.advance();
2955            }
2956
2957            project::sort_worktree_entries(&mut visible_worktree_entries);
2958
2959            self.visible_entries
2960                .push((worktree_id, visible_worktree_entries, OnceCell::new()));
2961        }
2962
2963        if let Some((project_entry_id, worktree_id, _)) = max_width_item {
2964            let mut visited_worktrees_length = 0;
2965            let index = self.visible_entries.iter().find_map(|(id, entries, _)| {
2966                if worktree_id == *id {
2967                    entries
2968                        .iter()
2969                        .position(|entry| entry.id == project_entry_id)
2970                } else {
2971                    visited_worktrees_length += entries.len();
2972                    None
2973                }
2974            });
2975            if let Some(index) = index {
2976                self.max_width_item_index = Some(visited_worktrees_length + index);
2977            }
2978        }
2979        if let Some((worktree_id, entry_id)) = new_selected_entry {
2980            self.selection = Some(SelectedEntry {
2981                worktree_id,
2982                entry_id,
2983            });
2984        }
2985    }
2986
2987    fn expand_entry(
2988        &mut self,
2989        worktree_id: WorktreeId,
2990        entry_id: ProjectEntryId,
2991        cx: &mut Context<Self>,
2992    ) {
2993        self.project.update(cx, |project, cx| {
2994            if let Some((worktree, expanded_dir_ids)) = project
2995                .worktree_for_id(worktree_id, cx)
2996                .zip(self.expanded_dir_ids.get_mut(&worktree_id))
2997            {
2998                project.expand_entry(worktree_id, entry_id, cx);
2999                let worktree = worktree.read(cx);
3000
3001                if let Some(mut entry) = worktree.entry_for_id(entry_id) {
3002                    loop {
3003                        if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) {
3004                            expanded_dir_ids.insert(ix, entry.id);
3005                        }
3006
3007                        if let Some(parent_entry) =
3008                            entry.path.parent().and_then(|p| worktree.entry_for_path(p))
3009                        {
3010                            entry = parent_entry;
3011                        } else {
3012                            break;
3013                        }
3014                    }
3015                }
3016            }
3017        });
3018    }
3019
3020    fn drop_external_files(
3021        &mut self,
3022        paths: &[PathBuf],
3023        entry_id: ProjectEntryId,
3024        window: &mut Window,
3025        cx: &mut Context<Self>,
3026    ) {
3027        let mut paths: Vec<Arc<Path>> = paths.iter().map(|path| Arc::from(path.clone())).collect();
3028
3029        let open_file_after_drop = paths.len() == 1 && paths[0].is_file();
3030
3031        let Some((target_directory, worktree, fs)) = maybe!({
3032            let project = self.project.read(cx);
3033            let fs = project.fs().clone();
3034            let worktree = project.worktree_for_entry(entry_id, cx)?;
3035            let entry = worktree.read(cx).entry_for_id(entry_id)?;
3036            let path = entry.path.clone();
3037            let target_directory = if entry.is_dir() {
3038                path.to_path_buf()
3039            } else {
3040                path.parent()?.to_path_buf()
3041            };
3042            Some((target_directory, worktree, fs))
3043        }) else {
3044            return;
3045        };
3046
3047        let mut paths_to_replace = Vec::new();
3048        for path in &paths {
3049            if let Some(name) = path.file_name() {
3050                let mut target_path = target_directory.clone();
3051                target_path.push(name);
3052                if target_path.exists() {
3053                    paths_to_replace.push((name.to_string_lossy().to_string(), path.clone()));
3054                }
3055            }
3056        }
3057
3058        cx.spawn_in(window, async move |this, cx| {
3059            async move {
3060                for (filename, original_path) in &paths_to_replace {
3061                    let answer = cx.update(|window, cx| {
3062                        window
3063                            .prompt(
3064                                PromptLevel::Info,
3065                                format!("A file or folder with name {filename} already exists in the destination folder. Do you want to replace it?").as_str(),
3066                                None,
3067                                &["Replace", "Cancel"],
3068                                cx,
3069                            )
3070                    })?.await?;
3071
3072                    if answer == 1 {
3073                        if let Some(item_idx) = paths.iter().position(|p| p == original_path) {
3074                            paths.remove(item_idx);
3075                        }
3076                    }
3077                }
3078
3079                if paths.is_empty() {
3080                    return Ok(());
3081                }
3082
3083                let task = worktree.update( cx, |worktree, cx| {
3084                    worktree.copy_external_entries(target_directory.into(), paths, fs, cx)
3085                })?;
3086
3087                let opened_entries = task.await.with_context(|| "failed to copy external paths")?;
3088                this.update(cx, |this, cx| {
3089                    if open_file_after_drop && !opened_entries.is_empty() {
3090                        this.open_entry(opened_entries[0], true, false, cx);
3091                    }
3092                })
3093            }
3094            .log_err().await
3095        })
3096        .detach();
3097    }
3098
3099    fn drag_onto(
3100        &mut self,
3101        selections: &DraggedSelection,
3102        target_entry_id: ProjectEntryId,
3103        is_file: bool,
3104        window: &mut Window,
3105        cx: &mut Context<Self>,
3106    ) {
3107        let should_copy = cfg!(target_os = "macos") && window.modifiers().alt
3108            || cfg!(not(target_os = "macos")) && window.modifiers().control;
3109        if should_copy {
3110            let _ = maybe!({
3111                let project = self.project.read(cx);
3112                let target_worktree = project.worktree_for_entry(target_entry_id, cx)?;
3113                let worktree_id = target_worktree.read(cx).id();
3114                let target_entry = target_worktree
3115                    .read(cx)
3116                    .entry_for_id(target_entry_id)?
3117                    .clone();
3118
3119                let mut copy_tasks = Vec::new();
3120                let mut disambiguation_range = None;
3121                for selection in selections.items() {
3122                    let (new_path, new_disambiguation_range) = self.create_paste_path(
3123                        selection,
3124                        (target_worktree.clone(), &target_entry),
3125                        cx,
3126                    )?;
3127
3128                    let task = self.project.update(cx, |project, cx| {
3129                        project.copy_entry(selection.entry_id, None, new_path, cx)
3130                    });
3131                    copy_tasks.push(task);
3132                    disambiguation_range = new_disambiguation_range.or(disambiguation_range);
3133                }
3134
3135                let item_count = copy_tasks.len();
3136
3137                cx.spawn_in(window, async move |project_panel, cx| {
3138                    let mut last_succeed = None;
3139                    for task in copy_tasks.into_iter() {
3140                        if let Some(Some(entry)) = task.await.log_err() {
3141                            last_succeed = Some(entry.id);
3142                        }
3143                    }
3144                    // update selection
3145                    if let Some(entry_id) = last_succeed {
3146                        project_panel
3147                            .update_in(cx, |project_panel, window, cx| {
3148                                project_panel.selection = Some(SelectedEntry {
3149                                    worktree_id,
3150                                    entry_id,
3151                                });
3152
3153                                // if only one entry was dragged and it was disambiguated, open the rename editor
3154                                if item_count == 1 && disambiguation_range.is_some() {
3155                                    project_panel.rename_impl(disambiguation_range, window, cx);
3156                                }
3157                            })
3158                            .ok();
3159                    }
3160                })
3161                .detach();
3162                Some(())
3163            });
3164        } else {
3165            for selection in selections.items() {
3166                self.move_entry(selection.entry_id, target_entry_id, is_file, cx);
3167            }
3168        }
3169    }
3170
3171    fn index_for_entry(
3172        &self,
3173        entry_id: ProjectEntryId,
3174        worktree_id: WorktreeId,
3175    ) -> Option<(usize, usize, usize)> {
3176        let mut worktree_ix = 0;
3177        let mut total_ix = 0;
3178        for (current_worktree_id, visible_worktree_entries, _) in &self.visible_entries {
3179            if worktree_id != *current_worktree_id {
3180                total_ix += visible_worktree_entries.len();
3181                worktree_ix += 1;
3182                continue;
3183            }
3184
3185            return visible_worktree_entries
3186                .iter()
3187                .enumerate()
3188                .find(|(_, entry)| entry.id == entry_id)
3189                .map(|(ix, _)| (worktree_ix, ix, total_ix + ix));
3190        }
3191        None
3192    }
3193
3194    fn entry_at_index(&self, index: usize) -> Option<(WorktreeId, GitEntryRef)> {
3195        let mut offset = 0;
3196        for (worktree_id, visible_worktree_entries, _) in &self.visible_entries {
3197            if visible_worktree_entries.len() > offset + index {
3198                return visible_worktree_entries
3199                    .get(index)
3200                    .map(|entry| (*worktree_id, entry.to_ref()));
3201            }
3202            offset += visible_worktree_entries.len();
3203        }
3204        None
3205    }
3206
3207    fn iter_visible_entries(
3208        &self,
3209        range: Range<usize>,
3210        window: &mut Window,
3211        cx: &mut Context<ProjectPanel>,
3212        mut callback: impl FnMut(&Entry, &HashSet<Arc<Path>>, &mut Window, &mut Context<ProjectPanel>),
3213    ) {
3214        let mut ix = 0;
3215        for (_, visible_worktree_entries, entries_paths) in &self.visible_entries {
3216            if ix >= range.end {
3217                return;
3218            }
3219
3220            if ix + visible_worktree_entries.len() <= range.start {
3221                ix += visible_worktree_entries.len();
3222                continue;
3223            }
3224
3225            let end_ix = range.end.min(ix + visible_worktree_entries.len());
3226            let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
3227            let entries = entries_paths.get_or_init(|| {
3228                visible_worktree_entries
3229                    .iter()
3230                    .map(|e| (e.path.clone()))
3231                    .collect()
3232            });
3233            for entry in visible_worktree_entries[entry_range].iter() {
3234                callback(&entry, entries, window, cx);
3235            }
3236            ix = end_ix;
3237        }
3238    }
3239
3240    fn for_each_visible_entry(
3241        &self,
3242        range: Range<usize>,
3243        window: &mut Window,
3244        cx: &mut Context<ProjectPanel>,
3245        mut callback: impl FnMut(ProjectEntryId, EntryDetails, &mut Window, &mut Context<ProjectPanel>),
3246    ) {
3247        let mut ix = 0;
3248        for (worktree_id, visible_worktree_entries, entries_paths) in &self.visible_entries {
3249            if ix >= range.end {
3250                return;
3251            }
3252
3253            if ix + visible_worktree_entries.len() <= range.start {
3254                ix += visible_worktree_entries.len();
3255                continue;
3256            }
3257
3258            let end_ix = range.end.min(ix + visible_worktree_entries.len());
3259            let (git_status_setting, show_file_icons, show_folder_icons) = {
3260                let settings = ProjectPanelSettings::get_global(cx);
3261                (
3262                    settings.git_status,
3263                    settings.file_icons,
3264                    settings.folder_icons,
3265                )
3266            };
3267            if let Some(worktree) = self.project.read(cx).worktree_for_id(*worktree_id, cx) {
3268                let snapshot = worktree.read(cx).snapshot();
3269                let root_name = OsStr::new(snapshot.root_name());
3270                let expanded_entry_ids = self
3271                    .expanded_dir_ids
3272                    .get(&snapshot.id())
3273                    .map(Vec::as_slice)
3274                    .unwrap_or(&[]);
3275
3276                let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
3277                let entries = entries_paths.get_or_init(|| {
3278                    visible_worktree_entries
3279                        .iter()
3280                        .map(|e| (e.path.clone()))
3281                        .collect()
3282                });
3283                for entry in visible_worktree_entries[entry_range].iter() {
3284                    let status = git_status_setting
3285                        .then_some(entry.git_summary)
3286                        .unwrap_or_default();
3287                    let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok();
3288                    let icon = match entry.kind {
3289                        EntryKind::File => {
3290                            if show_file_icons {
3291                                FileIcons::get_icon(&entry.path, cx)
3292                            } else {
3293                                None
3294                            }
3295                        }
3296                        _ => {
3297                            if show_folder_icons {
3298                                FileIcons::get_folder_icon(is_expanded, cx)
3299                            } else {
3300                                FileIcons::get_chevron_icon(is_expanded, cx)
3301                            }
3302                        }
3303                    };
3304
3305                    let (depth, difference) =
3306                        ProjectPanel::calculate_depth_and_difference(&entry, entries);
3307
3308                    let filename = match difference {
3309                        diff if diff > 1 => entry
3310                            .path
3311                            .iter()
3312                            .skip(entry.path.components().count() - diff)
3313                            .collect::<PathBuf>()
3314                            .to_str()
3315                            .unwrap_or_default()
3316                            .to_string(),
3317                        _ => entry
3318                            .path
3319                            .file_name()
3320                            .map(|name| name.to_string_lossy().into_owned())
3321                            .unwrap_or_else(|| root_name.to_string_lossy().to_string()),
3322                    };
3323                    let selection = SelectedEntry {
3324                        worktree_id: snapshot.id(),
3325                        entry_id: entry.id,
3326                    };
3327
3328                    let is_marked = self.marked_entries.contains(&selection);
3329
3330                    let diagnostic_severity = self
3331                        .diagnostics
3332                        .get(&(*worktree_id, entry.path.to_path_buf()))
3333                        .cloned();
3334
3335                    let filename_text_color =
3336                        entry_git_aware_label_color(status, entry.is_ignored, is_marked);
3337
3338                    let mut details = EntryDetails {
3339                        filename,
3340                        icon,
3341                        path: entry.path.clone(),
3342                        depth,
3343                        kind: entry.kind,
3344                        is_ignored: entry.is_ignored,
3345                        is_expanded,
3346                        is_selected: self.selection == Some(selection),
3347                        is_marked,
3348                        is_editing: false,
3349                        is_processing: false,
3350                        is_cut: self
3351                            .clipboard
3352                            .as_ref()
3353                            .map_or(false, |e| e.is_cut() && e.items().contains(&selection)),
3354                        filename_text_color,
3355                        diagnostic_severity,
3356                        git_status: status,
3357                        is_private: entry.is_private,
3358                        worktree_id: *worktree_id,
3359                        canonical_path: entry.canonical_path.clone(),
3360                    };
3361
3362                    if let Some(edit_state) = &self.edit_state {
3363                        let is_edited_entry = if edit_state.is_new_entry() {
3364                            entry.id == NEW_ENTRY_ID
3365                        } else {
3366                            entry.id == edit_state.entry_id
3367                                || self
3368                                    .ancestors
3369                                    .get(&entry.id)
3370                                    .is_some_and(|auto_folded_dirs| {
3371                                        auto_folded_dirs.ancestors.contains(&edit_state.entry_id)
3372                                    })
3373                        };
3374
3375                        if is_edited_entry {
3376                            if let Some(processing_filename) = &edit_state.processing_filename {
3377                                details.is_processing = true;
3378                                if let Some(ancestors) = edit_state
3379                                    .leaf_entry_id
3380                                    .and_then(|entry| self.ancestors.get(&entry))
3381                                {
3382                                    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;
3383                                    let all_components = ancestors.ancestors.len();
3384
3385                                    let prefix_components = all_components - position;
3386                                    let suffix_components = position.checked_sub(1);
3387                                    let mut previous_components =
3388                                        Path::new(&details.filename).components();
3389                                    let mut new_path = previous_components
3390                                        .by_ref()
3391                                        .take(prefix_components)
3392                                        .collect::<PathBuf>();
3393                                    if let Some(last_component) =
3394                                        Path::new(processing_filename).components().next_back()
3395                                    {
3396                                        new_path.push(last_component);
3397                                        previous_components.next();
3398                                    }
3399
3400                                    if let Some(_) = suffix_components {
3401                                        new_path.push(previous_components);
3402                                    }
3403                                    if let Some(str) = new_path.to_str() {
3404                                        details.filename.clear();
3405                                        details.filename.push_str(str);
3406                                    }
3407                                } else {
3408                                    details.filename.clear();
3409                                    details.filename.push_str(processing_filename);
3410                                }
3411                            } else {
3412                                if edit_state.is_new_entry() {
3413                                    details.filename.clear();
3414                                }
3415                                details.is_editing = true;
3416                            }
3417                        }
3418                    }
3419
3420                    callback(entry.id, details, window, cx);
3421                }
3422            }
3423            ix = end_ix;
3424        }
3425    }
3426
3427    fn find_entry_in_worktree(
3428        &self,
3429        worktree_id: WorktreeId,
3430        reverse_search: bool,
3431        only_visible_entries: bool,
3432        predicate: impl Fn(GitEntryRef, WorktreeId) -> bool,
3433        cx: &mut Context<Self>,
3434    ) -> Option<GitEntry> {
3435        if only_visible_entries {
3436            let entries = self
3437                .visible_entries
3438                .iter()
3439                .find_map(|(tree_id, entries, _)| {
3440                    if worktree_id == *tree_id {
3441                        Some(entries)
3442                    } else {
3443                        None
3444                    }
3445                })?
3446                .clone();
3447
3448            return utils::ReversibleIterable::new(entries.iter(), reverse_search)
3449                .find(|ele| predicate(ele.to_ref(), worktree_id))
3450                .cloned();
3451        }
3452
3453        let repo_snapshots = self
3454            .project
3455            .read(cx)
3456            .git_store()
3457            .read(cx)
3458            .repo_snapshots(cx);
3459        let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?;
3460        worktree.read_with(cx, |tree, _| {
3461            utils::ReversibleIterable::new(
3462                GitTraversal::new(&repo_snapshots, tree.entries(true, 0usize)),
3463                reverse_search,
3464            )
3465            .find_single_ended(|ele| predicate(*ele, worktree_id))
3466            .map(|ele| ele.to_owned())
3467        })
3468    }
3469
3470    fn find_entry(
3471        &self,
3472        start: Option<&SelectedEntry>,
3473        reverse_search: bool,
3474        predicate: impl Fn(GitEntryRef, WorktreeId) -> bool,
3475        cx: &mut Context<Self>,
3476    ) -> Option<SelectedEntry> {
3477        let mut worktree_ids: Vec<_> = self
3478            .visible_entries
3479            .iter()
3480            .map(|(worktree_id, _, _)| *worktree_id)
3481            .collect();
3482        let repo_snapshots = self
3483            .project
3484            .read(cx)
3485            .git_store()
3486            .read(cx)
3487            .repo_snapshots(cx);
3488
3489        let mut last_found: Option<SelectedEntry> = None;
3490
3491        if let Some(start) = start {
3492            let worktree = self
3493                .project
3494                .read(cx)
3495                .worktree_for_id(start.worktree_id, cx)?
3496                .read(cx);
3497
3498            let search = {
3499                let entry = worktree.entry_for_id(start.entry_id)?;
3500                let root_entry = worktree.root_entry()?;
3501                let tree_id = worktree.id();
3502
3503                let mut first_iter = GitTraversal::new(
3504                    &repo_snapshots,
3505                    worktree.traverse_from_path(true, true, true, entry.path.as_ref()),
3506                );
3507
3508                if reverse_search {
3509                    first_iter.next();
3510                }
3511
3512                let first = first_iter
3513                    .enumerate()
3514                    .take_until(|(count, entry)| entry.entry == root_entry && *count != 0usize)
3515                    .map(|(_, entry)| entry)
3516                    .find(|ele| predicate(*ele, tree_id))
3517                    .map(|ele| ele.to_owned());
3518
3519                let second_iter =
3520                    GitTraversal::new(&repo_snapshots, worktree.entries(true, 0usize));
3521
3522                let second = if reverse_search {
3523                    second_iter
3524                        .take_until(|ele| ele.id == start.entry_id)
3525                        .filter(|ele| predicate(*ele, tree_id))
3526                        .last()
3527                        .map(|ele| ele.to_owned())
3528                } else {
3529                    second_iter
3530                        .take_while(|ele| ele.id != start.entry_id)
3531                        .filter(|ele| predicate(*ele, tree_id))
3532                        .last()
3533                        .map(|ele| ele.to_owned())
3534                };
3535
3536                if reverse_search {
3537                    Some((second, first))
3538                } else {
3539                    Some((first, second))
3540                }
3541            };
3542
3543            if let Some((first, second)) = search {
3544                let first = first.map(|entry| SelectedEntry {
3545                    worktree_id: start.worktree_id,
3546                    entry_id: entry.id,
3547                });
3548
3549                let second = second.map(|entry| SelectedEntry {
3550                    worktree_id: start.worktree_id,
3551                    entry_id: entry.id,
3552                });
3553
3554                if first.is_some() {
3555                    return first;
3556                }
3557                last_found = second;
3558
3559                let idx = worktree_ids
3560                    .iter()
3561                    .enumerate()
3562                    .find(|(_, ele)| **ele == start.worktree_id)
3563                    .map(|(idx, _)| idx);
3564
3565                if let Some(idx) = idx {
3566                    worktree_ids.rotate_left(idx + 1usize);
3567                    worktree_ids.pop();
3568                }
3569            }
3570        }
3571
3572        for tree_id in worktree_ids.into_iter() {
3573            if let Some(found) =
3574                self.find_entry_in_worktree(tree_id, reverse_search, false, &predicate, cx)
3575            {
3576                return Some(SelectedEntry {
3577                    worktree_id: tree_id,
3578                    entry_id: found.id,
3579                });
3580            }
3581        }
3582
3583        last_found
3584    }
3585
3586    fn find_visible_entry(
3587        &self,
3588        start: Option<&SelectedEntry>,
3589        reverse_search: bool,
3590        predicate: impl Fn(GitEntryRef, WorktreeId) -> bool,
3591        cx: &mut Context<Self>,
3592    ) -> Option<SelectedEntry> {
3593        let mut worktree_ids: Vec<_> = self
3594            .visible_entries
3595            .iter()
3596            .map(|(worktree_id, _, _)| *worktree_id)
3597            .collect();
3598
3599        let mut last_found: Option<SelectedEntry> = None;
3600
3601        if let Some(start) = start {
3602            let entries = self
3603                .visible_entries
3604                .iter()
3605                .find(|(worktree_id, _, _)| *worktree_id == start.worktree_id)
3606                .map(|(_, entries, _)| entries)?;
3607
3608            let mut start_idx = entries
3609                .iter()
3610                .enumerate()
3611                .find(|(_, ele)| ele.id == start.entry_id)
3612                .map(|(idx, _)| idx)?;
3613
3614            if reverse_search {
3615                start_idx = start_idx.saturating_add(1usize);
3616            }
3617
3618            let (left, right) = entries.split_at_checked(start_idx)?;
3619
3620            let (first_iter, second_iter) = if reverse_search {
3621                (
3622                    utils::ReversibleIterable::new(left.iter(), reverse_search),
3623                    utils::ReversibleIterable::new(right.iter(), reverse_search),
3624                )
3625            } else {
3626                (
3627                    utils::ReversibleIterable::new(right.iter(), reverse_search),
3628                    utils::ReversibleIterable::new(left.iter(), reverse_search),
3629                )
3630            };
3631
3632            let first_search = first_iter.find(|ele| predicate(ele.to_ref(), start.worktree_id));
3633            let second_search = second_iter.find(|ele| predicate(ele.to_ref(), start.worktree_id));
3634
3635            if first_search.is_some() {
3636                return first_search.map(|entry| SelectedEntry {
3637                    worktree_id: start.worktree_id,
3638                    entry_id: entry.id,
3639                });
3640            }
3641
3642            last_found = second_search.map(|entry| SelectedEntry {
3643                worktree_id: start.worktree_id,
3644                entry_id: entry.id,
3645            });
3646
3647            let idx = worktree_ids
3648                .iter()
3649                .enumerate()
3650                .find(|(_, ele)| **ele == start.worktree_id)
3651                .map(|(idx, _)| idx);
3652
3653            if let Some(idx) = idx {
3654                worktree_ids.rotate_left(idx + 1usize);
3655                worktree_ids.pop();
3656            }
3657        }
3658
3659        for tree_id in worktree_ids.into_iter() {
3660            if let Some(found) =
3661                self.find_entry_in_worktree(tree_id, reverse_search, true, &predicate, cx)
3662            {
3663                return Some(SelectedEntry {
3664                    worktree_id: tree_id,
3665                    entry_id: found.id,
3666                });
3667            }
3668        }
3669
3670        last_found
3671    }
3672
3673    fn calculate_depth_and_difference(
3674        entry: &Entry,
3675        visible_worktree_entries: &HashSet<Arc<Path>>,
3676    ) -> (usize, usize) {
3677        let (depth, difference) = entry
3678            .path
3679            .ancestors()
3680            .skip(1) // Skip the entry itself
3681            .find_map(|ancestor| {
3682                if let Some(parent_entry) = visible_worktree_entries.get(ancestor) {
3683                    let entry_path_components_count = entry.path.components().count();
3684                    let parent_path_components_count = parent_entry.components().count();
3685                    let difference = entry_path_components_count - parent_path_components_count;
3686                    let depth = parent_entry
3687                        .ancestors()
3688                        .skip(1)
3689                        .filter(|ancestor| visible_worktree_entries.contains(*ancestor))
3690                        .count();
3691                    Some((depth + 1, difference))
3692                } else {
3693                    None
3694                }
3695            })
3696            .unwrap_or((0, 0));
3697
3698        (depth, difference)
3699    }
3700
3701    fn render_entry(
3702        &self,
3703        entry_id: ProjectEntryId,
3704        details: EntryDetails,
3705        window: &mut Window,
3706        cx: &mut Context<Self>,
3707    ) -> Stateful<Div> {
3708        const GROUP_NAME: &str = "project_entry";
3709
3710        let kind = details.kind;
3711        let settings = ProjectPanelSettings::get_global(cx);
3712        let show_editor = details.is_editing && !details.is_processing;
3713
3714        let selection = SelectedEntry {
3715            worktree_id: details.worktree_id,
3716            entry_id,
3717        };
3718
3719        let is_marked = self.marked_entries.contains(&selection);
3720        let is_active = self
3721            .selection
3722            .map_or(false, |selection| selection.entry_id == entry_id);
3723
3724        let file_name = details.filename.clone();
3725
3726        let mut icon = details.icon.clone();
3727        if settings.file_icons && show_editor && details.kind.is_file() {
3728            let filename = self.filename_editor.read(cx).text(cx);
3729            if filename.len() > 2 {
3730                icon = FileIcons::get_icon(Path::new(&filename), cx);
3731            }
3732        }
3733
3734        let filename_text_color = details.filename_text_color;
3735        let diagnostic_severity = details.diagnostic_severity;
3736        let item_colors = get_item_color(cx);
3737
3738        let canonical_path = details
3739            .canonical_path
3740            .as_ref()
3741            .map(|f| f.to_string_lossy().to_string());
3742        let path = details.path.clone();
3743
3744        let depth = details.depth;
3745        let worktree_id = details.worktree_id;
3746        let selections = Arc::new(self.marked_entries.clone());
3747
3748        let dragged_selection = DraggedSelection {
3749            active_selection: selection,
3750            marked_selections: selections,
3751        };
3752
3753        let bg_color = if is_marked {
3754            item_colors.marked
3755        } else {
3756            item_colors.default
3757        };
3758
3759        let bg_hover_color = if is_marked {
3760            item_colors.marked
3761        } else {
3762            item_colors.hover
3763        };
3764
3765        let validation_color_and_message = if show_editor {
3766            match self
3767                .edit_state
3768                .as_ref()
3769                .map_or(ValidationState::None, |e| e.validation_state.clone())
3770            {
3771                ValidationState::Error(msg) => Some((Color::Error.color(cx), msg.clone())),
3772                ValidationState::Warning(msg) => Some((Color::Warning.color(cx), msg.clone())),
3773                ValidationState::None => None,
3774            }
3775        } else {
3776            None
3777        };
3778
3779        let border_color =
3780            if !self.mouse_down && is_active && self.focus_handle.contains_focused(window, cx) {
3781                match validation_color_and_message {
3782                    Some((color, _)) => color,
3783                    None => item_colors.focused,
3784                }
3785            } else {
3786                bg_color
3787            };
3788
3789        let border_hover_color =
3790            if !self.mouse_down && is_active && self.focus_handle.contains_focused(window, cx) {
3791                match validation_color_and_message {
3792                    Some((color, _)) => color,
3793                    None => item_colors.focused,
3794                }
3795            } else {
3796                bg_hover_color
3797            };
3798
3799        let folded_directory_drag_target = self.folded_directory_drag_target;
3800
3801        div()
3802            .id(entry_id.to_proto() as usize)
3803            .group(GROUP_NAME)
3804            .cursor_pointer()
3805            .rounded_none()
3806            .bg(bg_color)
3807            .border_1()
3808            .border_r_2()
3809            .border_color(border_color)
3810            .hover(|style| style.bg(bg_hover_color).border_color(border_hover_color))
3811            .on_drag_move::<ExternalPaths>(cx.listener(
3812                move |this, event: &DragMoveEvent<ExternalPaths>, _, cx| {
3813                    if event.bounds.contains(&event.event.position) {
3814                        if this.last_external_paths_drag_over_entry == Some(entry_id) {
3815                            return;
3816                        }
3817                        this.last_external_paths_drag_over_entry = Some(entry_id);
3818                        this.marked_entries.clear();
3819
3820                        let Some((worktree, path, entry)) = maybe!({
3821                            let worktree = this
3822                                .project
3823                                .read(cx)
3824                                .worktree_for_id(selection.worktree_id, cx)?;
3825                            let worktree = worktree.read(cx);
3826                            let entry = worktree.entry_for_path(&path)?;
3827                            let path = if entry.is_dir() {
3828                                path.as_ref()
3829                            } else {
3830                                path.parent()?
3831                            };
3832                            Some((worktree, path, entry))
3833                        }) else {
3834                            return;
3835                        };
3836
3837                        this.marked_entries.insert(SelectedEntry {
3838                            entry_id: entry.id,
3839                            worktree_id: worktree.id(),
3840                        });
3841
3842                        for entry in worktree.child_entries(path) {
3843                            this.marked_entries.insert(SelectedEntry {
3844                                entry_id: entry.id,
3845                                worktree_id: worktree.id(),
3846                            });
3847                        }
3848
3849                        cx.notify();
3850                    }
3851                },
3852            ))
3853            .on_drop(cx.listener(
3854                move |this, external_paths: &ExternalPaths, window, cx| {
3855                    this.hover_scroll_task.take();
3856                    this.last_external_paths_drag_over_entry = None;
3857                    this.marked_entries.clear();
3858                    this.drop_external_files(external_paths.paths(), entry_id, window, cx);
3859                    cx.stop_propagation();
3860                },
3861            ))
3862            .on_drag_move::<DraggedSelection>(cx.listener(
3863                move |this, event: &DragMoveEvent<DraggedSelection>, window, cx| {
3864                    if event.bounds.contains(&event.event.position) {
3865                        if this.last_selection_drag_over_entry == Some(entry_id) {
3866                            return;
3867                        }
3868                        this.last_selection_drag_over_entry = Some(entry_id);
3869                        this.hover_expand_task.take();
3870
3871                        if !kind.is_dir()
3872                            || this
3873                                .expanded_dir_ids
3874                                .get(&details.worktree_id)
3875                                .map_or(false, |ids| ids.binary_search(&entry_id).is_ok())
3876                        {
3877                            return;
3878                        }
3879
3880                        let bounds = event.bounds;
3881                        this.hover_expand_task =
3882                            Some(cx.spawn_in(window, async move |this, cx| {
3883                                cx.background_executor()
3884                                    .timer(Duration::from_millis(500))
3885                                    .await;
3886                                this.update_in(cx, |this, window, cx| {
3887                                    this.hover_expand_task.take();
3888                                    if this.last_selection_drag_over_entry == Some(entry_id)
3889                                        && bounds.contains(&window.mouse_position())
3890                                    {
3891                                        this.expand_entry(worktree_id, entry_id, cx);
3892                                        this.update_visible_entries(
3893                                            Some((worktree_id, entry_id)),
3894                                            cx,
3895                                        );
3896                                        cx.notify();
3897                                    }
3898                                })
3899                                .ok();
3900                            }));
3901                    }
3902                },
3903            ))
3904            .on_drag(
3905                dragged_selection,
3906                move |selection, click_offset, _window, cx| {
3907                    cx.new(|_| DraggedProjectEntryView {
3908                        details: details.clone(),
3909                        click_offset,
3910                        selection: selection.active_selection,
3911                        selections: selection.marked_selections.clone(),
3912                    })
3913                },
3914            )
3915            .drag_over::<DraggedSelection>(move |style, _, _, _| {
3916                if  folded_directory_drag_target.is_some() {
3917                    return style;
3918                }
3919                style.bg(item_colors.drag_over)
3920            })
3921            .on_drop(
3922                cx.listener(move |this, selections: &DraggedSelection, window, cx| {
3923                    this.hover_scroll_task.take();
3924                    this.hover_expand_task.take();
3925                    if  folded_directory_drag_target.is_some() {
3926                        return;
3927                    }
3928                    this.drag_onto(selections, entry_id, kind.is_file(), window, cx);
3929                }),
3930            )
3931            .on_mouse_down(
3932                MouseButton::Left,
3933                cx.listener(move |this, _, _, cx| {
3934                    this.mouse_down = true;
3935                    cx.propagate();
3936                }),
3937            )
3938            .on_click(
3939                cx.listener(move |this, event: &gpui::ClickEvent, window, cx| {
3940                    if event.down.button == MouseButton::Right
3941                        || event.down.first_mouse
3942                        || show_editor
3943                    {
3944                        return;
3945                    }
3946                    if event.down.button == MouseButton::Left {
3947                        this.mouse_down = false;
3948                    }
3949                    cx.stop_propagation();
3950
3951                    if let Some(selection) = this.selection.filter(|_| event.modifiers().shift) {
3952                        let current_selection = this.index_for_selection(selection);
3953                        let clicked_entry = SelectedEntry {
3954                            entry_id,
3955                            worktree_id,
3956                        };
3957                        let target_selection = this.index_for_selection(clicked_entry);
3958                        if let Some(((_, _, source_index), (_, _, target_index))) =
3959                            current_selection.zip(target_selection)
3960                        {
3961                            let range_start = source_index.min(target_index);
3962                            let range_end = source_index.max(target_index) + 1; // Make the range inclusive.
3963                            let mut new_selections = BTreeSet::new();
3964                            this.for_each_visible_entry(
3965                                range_start..range_end,
3966                                window,
3967                                cx,
3968                                |entry_id, details, _, _| {
3969                                    new_selections.insert(SelectedEntry {
3970                                        entry_id,
3971                                        worktree_id: details.worktree_id,
3972                                    });
3973                                },
3974                            );
3975
3976                            this.marked_entries = this
3977                                .marked_entries
3978                                .union(&new_selections)
3979                                .cloned()
3980                                .collect();
3981
3982                            this.selection = Some(clicked_entry);
3983                            this.marked_entries.insert(clicked_entry);
3984                        }
3985                    } else if event.modifiers().secondary() {
3986                        if event.down.click_count > 1 {
3987                            this.split_entry(entry_id, cx);
3988                        } else {
3989                            this.selection = Some(selection);
3990                            if !this.marked_entries.insert(selection) {
3991                                this.marked_entries.remove(&selection);
3992                            }
3993                        }
3994                    } else if kind.is_dir() {
3995                        this.marked_entries.clear();
3996                        if event.modifiers().alt {
3997                            this.toggle_expand_all(entry_id, window, cx);
3998                        } else {
3999                            this.toggle_expanded(entry_id, window, cx);
4000                        }
4001                    } else {
4002                        let preview_tabs_enabled = PreviewTabsSettings::get_global(cx).enabled;
4003                        let click_count = event.up.click_count;
4004                        let focus_opened_item = !preview_tabs_enabled || click_count > 1;
4005                        let allow_preview = preview_tabs_enabled && click_count == 1;
4006                        this.open_entry(entry_id, focus_opened_item, allow_preview, cx);
4007                    }
4008                }),
4009            )
4010            .child(
4011                ListItem::new(entry_id.to_proto() as usize)
4012                    .indent_level(depth)
4013                    .indent_step_size(px(settings.indent_size))
4014                    .spacing(match settings.entry_spacing {
4015                        project_panel_settings::EntrySpacing::Comfortable => ListItemSpacing::Dense,
4016                        project_panel_settings::EntrySpacing::Standard => {
4017                            ListItemSpacing::ExtraDense
4018                        }
4019                    })
4020                    .selectable(false)
4021                    .when_some(canonical_path, |this, path| {
4022                        this.end_slot::<AnyElement>(
4023                            div()
4024                                .id("symlink_icon")
4025                                .pr_3()
4026                                .tooltip(move |window, cx| {
4027                                    Tooltip::with_meta(
4028                                        path.to_string(),
4029                                        None,
4030                                        "Symbolic Link",
4031                                        window,
4032                                        cx,
4033                                    )
4034                                })
4035                                .child(
4036                                    Icon::new(IconName::ArrowUpRight)
4037                                        .size(IconSize::Indicator)
4038                                        .color(filename_text_color),
4039                                )
4040                                .into_any_element(),
4041                        )
4042                    })
4043                    .child(if let Some(icon) = &icon {
4044                        if let Some((_, decoration_color)) =
4045                            entry_diagnostic_aware_icon_decoration_and_color(diagnostic_severity)
4046                        {
4047                            let is_warning = diagnostic_severity
4048                                .map(|severity| matches!(severity, DiagnosticSeverity::WARNING))
4049                                .unwrap_or(false);
4050                            div().child(
4051                                DecoratedIcon::new(
4052                                    Icon::from_path(icon.clone()).color(Color::Muted),
4053                                    Some(
4054                                        IconDecoration::new(
4055                                            if kind.is_file() {
4056                                                if is_warning {
4057                                                    IconDecorationKind::Triangle
4058                                                } else {
4059                                                    IconDecorationKind::X
4060                                                }
4061                                            } else {
4062                                                IconDecorationKind::Dot
4063                                            },
4064                                            bg_color,
4065                                            cx,
4066                                        )
4067                                        .group_name(Some(GROUP_NAME.into()))
4068                                        .knockout_hover_color(bg_hover_color)
4069                                        .color(decoration_color.color(cx))
4070                                        .position(Point {
4071                                            x: px(-2.),
4072                                            y: px(-2.),
4073                                        }),
4074                                    ),
4075                                )
4076                                .into_any_element(),
4077                            )
4078                        } else {
4079                            h_flex().child(Icon::from_path(icon.to_string()).color(Color::Muted))
4080                        }
4081                    } else {
4082                        if let Some((icon_name, color)) =
4083                            entry_diagnostic_aware_icon_name_and_color(diagnostic_severity)
4084                        {
4085                            h_flex()
4086                                .size(IconSize::default().rems())
4087                                .child(Icon::new(icon_name).color(color).size(IconSize::Small))
4088                        } else {
4089                            h_flex()
4090                                .size(IconSize::default().rems())
4091                                .invisible()
4092                                .flex_none()
4093                        }
4094                    })
4095                    .child(
4096                        if let (Some(editor), true) = (Some(&self.filename_editor), show_editor) {
4097                            h_flex().h_6().w_full().child(editor.clone())
4098                        } else {
4099                            h_flex().h_6().map(|mut this| {
4100                                if let Some(folded_ancestors) = self.ancestors.get(&entry_id) {
4101                                    let components = Path::new(&file_name)
4102                                        .components()
4103                                        .map(|comp| {
4104                                            let comp_str =
4105                                                comp.as_os_str().to_string_lossy().into_owned();
4106                                            comp_str
4107                                        })
4108                                        .collect::<Vec<_>>();
4109
4110                                    let components_len = components.len();
4111                                    let active_index = components_len
4112                                        - 1
4113                                        - folded_ancestors.current_ancestor_depth;
4114                                        const DELIMITER: SharedString =
4115                                        SharedString::new_static(std::path::MAIN_SEPARATOR_STR);
4116                                    for (index, component) in components.into_iter().enumerate() {
4117                                        if index != 0 {
4118                                                let delimiter_target_index = index - 1;
4119                                                let target_entry_id = folded_ancestors.ancestors.get(components_len - 1 - delimiter_target_index).cloned();
4120                                                this = this.child(
4121                                                    div()
4122                                                    .on_drop(cx.listener(move |this, selections: &DraggedSelection, window, cx| {
4123                                                        this.hover_scroll_task.take();
4124                                                        this.folded_directory_drag_target = None;
4125                                                        if let Some(target_entry_id) = target_entry_id {
4126                                                            this.drag_onto(selections, target_entry_id, kind.is_file(), window, cx);
4127                                                        }
4128                                                    }))
4129                                                    .on_drag_move(cx.listener(
4130                                                        move |this, event: &DragMoveEvent<DraggedSelection>, _, _| {
4131                                                            if event.bounds.contains(&event.event.position) {
4132                                                                this.folded_directory_drag_target = Some(
4133                                                                    FoldedDirectoryDragTarget {
4134                                                                        entry_id,
4135                                                                        index: delimiter_target_index,
4136                                                                        is_delimiter_target: true,
4137                                                                    }
4138                                                                );
4139                                                            } else {
4140                                                                let is_current_target = this.folded_directory_drag_target
4141                                                                    .map_or(false, |target|
4142                                                                        target.entry_id == entry_id &&
4143                                                                        target.index == delimiter_target_index &&
4144                                                                        target.is_delimiter_target
4145                                                                    );
4146                                                                if is_current_target {
4147                                                                    this.folded_directory_drag_target = None;
4148                                                                }
4149                                                            }
4150
4151                                                        },
4152                                                    ))
4153                                                    .child(
4154                                                        Label::new(DELIMITER.clone())
4155                                                            .single_line()
4156                                                            .color(filename_text_color)
4157                                                    )
4158                                                );
4159                                        }
4160                                        let id = SharedString::from(format!(
4161                                            "project_panel_path_component_{}_{index}",
4162                                            entry_id.to_usize()
4163                                        ));
4164                                        let label = div()
4165                                            .id(id)
4166                                            .on_click(cx.listener(move |this, _, _, cx| {
4167                                                if index != active_index {
4168                                                    if let Some(folds) =
4169                                                        this.ancestors.get_mut(&entry_id)
4170                                                    {
4171                                                        folds.current_ancestor_depth =
4172                                                            components_len - 1 - index;
4173                                                        cx.notify();
4174                                                    }
4175                                                }
4176                                            }))
4177                                            .when(index != components_len - 1, |div|{
4178                                                let target_entry_id = folded_ancestors.ancestors.get(components_len - 1 - index).cloned();
4179                                                div
4180                                                .on_drag_move(cx.listener(
4181                                                    move |this, event: &DragMoveEvent<DraggedSelection>, _, _| {
4182                                                    if event.bounds.contains(&event.event.position) {
4183                                                            this.folded_directory_drag_target = Some(
4184                                                                FoldedDirectoryDragTarget {
4185                                                                    entry_id,
4186                                                                    index,
4187                                                                    is_delimiter_target: false,
4188                                                                }
4189                                                            );
4190                                                        } else {
4191                                                            let is_current_target = this.folded_directory_drag_target
4192                                                                .as_ref()
4193                                                                .map_or(false, |target|
4194                                                                    target.entry_id == entry_id &&
4195                                                                    target.index == index &&
4196                                                                    !target.is_delimiter_target
4197                                                                );
4198                                                            if is_current_target {
4199                                                                this.folded_directory_drag_target = None;
4200                                                            }
4201                                                        }
4202                                                    },
4203                                                ))
4204                                                .on_drop(cx.listener(move |this, selections: &DraggedSelection, window,cx| {
4205                                                    this.hover_scroll_task.take();
4206                                                    this.folded_directory_drag_target = None;
4207                                                    if let Some(target_entry_id) = target_entry_id {
4208                                                        this.drag_onto(selections, target_entry_id, kind.is_file(), window, cx);
4209                                                    }
4210                                                }))
4211                                                .when(folded_directory_drag_target.map_or(false, |target|
4212                                                    target.entry_id == entry_id &&
4213                                                    target.index == index
4214                                                ), |this| {
4215                                                    this.bg(item_colors.drag_over)
4216                                                })
4217                                            })
4218                                            .child(
4219                                                Label::new(component)
4220                                                    .single_line()
4221                                                    .color(filename_text_color)
4222                                                    .when(
4223                                                        index == active_index
4224                                                            && (is_active || is_marked),
4225                                                        |this| this.underline(),
4226                                                    ),
4227                                            );
4228
4229                                        this = this.child(label);
4230                                    }
4231
4232                                    this
4233                                } else {
4234                                    this.child(
4235                                        Label::new(file_name)
4236                                            .single_line()
4237                                            .color(filename_text_color),
4238                                    )
4239                                }
4240                            })
4241                        }
4242                        .ml_1(),
4243                    )
4244                    .on_secondary_mouse_down(cx.listener(
4245                        move |this, event: &MouseDownEvent, window, cx| {
4246                            // Stop propagation to prevent the catch-all context menu for the project
4247                            // panel from being deployed.
4248                            cx.stop_propagation();
4249                            // Some context menu actions apply to all marked entries. If the user
4250                            // right-clicks on an entry that is not marked, they may not realize the
4251                            // action applies to multiple entries. To avoid inadvertent changes, all
4252                            // entries are unmarked.
4253                            if !this.marked_entries.contains(&selection) {
4254                                this.marked_entries.clear();
4255                            }
4256                            this.deploy_context_menu(event.position, entry_id, window, cx);
4257                        },
4258                    ))
4259                    .overflow_x(),
4260            )
4261            .when_some(
4262                validation_color_and_message,
4263                |this, (color, message)| {
4264                    this
4265                    .relative()
4266                    .child(
4267                        deferred(
4268                            div()
4269                            .occlude()
4270                            .absolute()
4271                            .top_full()
4272                            .left(px(-1.)) // Used px over rem so that it doesn't change with font size
4273                            .right(px(-0.5))
4274                            .py_1()
4275                            .px_2()
4276                            .border_1()
4277                            .border_color(color)
4278                            .bg(cx.theme().colors().background)
4279                            .child(
4280                                Label::new(message)
4281                                .color(Color::from(color))
4282                                .size(LabelSize::Small)
4283                            )
4284                        )
4285                    )
4286                }
4287            )
4288    }
4289
4290    fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
4291        if !Self::should_show_scrollbar(cx)
4292            || !(self.show_scrollbar || self.vertical_scrollbar_state.is_dragging())
4293        {
4294            return None;
4295        }
4296        Some(
4297            div()
4298                .occlude()
4299                .id("project-panel-vertical-scroll")
4300                .on_mouse_move(cx.listener(|_, _, _, cx| {
4301                    cx.notify();
4302                    cx.stop_propagation()
4303                }))
4304                .on_hover(|_, _, cx| {
4305                    cx.stop_propagation();
4306                })
4307                .on_any_mouse_down(|_, _, cx| {
4308                    cx.stop_propagation();
4309                })
4310                .on_mouse_up(
4311                    MouseButton::Left,
4312                    cx.listener(|this, _, window, cx| {
4313                        if !this.vertical_scrollbar_state.is_dragging()
4314                            && !this.focus_handle.contains_focused(window, cx)
4315                        {
4316                            this.hide_scrollbar(window, cx);
4317                            cx.notify();
4318                        }
4319
4320                        cx.stop_propagation();
4321                    }),
4322                )
4323                .on_scroll_wheel(cx.listener(|_, _, _, cx| {
4324                    cx.notify();
4325                }))
4326                .h_full()
4327                .absolute()
4328                .right_1()
4329                .top_1()
4330                .bottom_1()
4331                .w(px(12.))
4332                .cursor_default()
4333                .children(Scrollbar::vertical(
4334                    // percentage as f32..end_offset as f32,
4335                    self.vertical_scrollbar_state.clone(),
4336                )),
4337        )
4338    }
4339
4340    fn render_horizontal_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
4341        if !Self::should_show_scrollbar(cx)
4342            || !(self.show_scrollbar || self.horizontal_scrollbar_state.is_dragging())
4343        {
4344            return None;
4345        }
4346        Scrollbar::horizontal(self.horizontal_scrollbar_state.clone()).map(|scrollbar| {
4347            div()
4348                .occlude()
4349                .id("project-panel-horizontal-scroll")
4350                .on_mouse_move(cx.listener(|_, _, _, cx| {
4351                    cx.notify();
4352                    cx.stop_propagation()
4353                }))
4354                .on_hover(|_, _, cx| {
4355                    cx.stop_propagation();
4356                })
4357                .on_any_mouse_down(|_, _, cx| {
4358                    cx.stop_propagation();
4359                })
4360                .on_mouse_up(
4361                    MouseButton::Left,
4362                    cx.listener(|this, _, window, cx| {
4363                        if !this.horizontal_scrollbar_state.is_dragging()
4364                            && !this.focus_handle.contains_focused(window, cx)
4365                        {
4366                            this.hide_scrollbar(window, cx);
4367                            cx.notify();
4368                        }
4369
4370                        cx.stop_propagation();
4371                    }),
4372                )
4373                .on_scroll_wheel(cx.listener(|_, _, _, cx| {
4374                    cx.notify();
4375                }))
4376                .w_full()
4377                .absolute()
4378                .right_1()
4379                .left_1()
4380                .bottom_1()
4381                .h(px(12.))
4382                .cursor_default()
4383                .child(scrollbar)
4384        })
4385    }
4386
4387    fn dispatch_context(&self, window: &Window, cx: &Context<Self>) -> KeyContext {
4388        let mut dispatch_context = KeyContext::new_with_defaults();
4389        dispatch_context.add("ProjectPanel");
4390        dispatch_context.add("menu");
4391
4392        let identifier = if self.filename_editor.focus_handle(cx).is_focused(window) {
4393            "editing"
4394        } else {
4395            "not_editing"
4396        };
4397
4398        dispatch_context.add(identifier);
4399        dispatch_context
4400    }
4401
4402    fn should_show_scrollbar(cx: &App) -> bool {
4403        let show = ProjectPanelSettings::get_global(cx)
4404            .scrollbar
4405            .show
4406            .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show);
4407        match show {
4408            ShowScrollbar::Auto => true,
4409            ShowScrollbar::System => true,
4410            ShowScrollbar::Always => true,
4411            ShowScrollbar::Never => false,
4412        }
4413    }
4414
4415    fn should_autohide_scrollbar(cx: &App) -> bool {
4416        let show = ProjectPanelSettings::get_global(cx)
4417            .scrollbar
4418            .show
4419            .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show);
4420        match show {
4421            ShowScrollbar::Auto => true,
4422            ShowScrollbar::System => cx
4423                .try_global::<ScrollbarAutoHide>()
4424                .map_or_else(|| cx.should_auto_hide_scrollbars(), |autohide| autohide.0),
4425            ShowScrollbar::Always => false,
4426            ShowScrollbar::Never => true,
4427        }
4428    }
4429
4430    fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4431        const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
4432        if !Self::should_autohide_scrollbar(cx) {
4433            return;
4434        }
4435        self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |panel, cx| {
4436            cx.background_executor()
4437                .timer(SCROLLBAR_SHOW_INTERVAL)
4438                .await;
4439            panel
4440                .update(cx, |panel, cx| {
4441                    panel.show_scrollbar = false;
4442                    cx.notify();
4443                })
4444                .log_err();
4445        }))
4446    }
4447
4448    fn reveal_entry(
4449        &mut self,
4450        project: Entity<Project>,
4451        entry_id: ProjectEntryId,
4452        skip_ignored: bool,
4453        cx: &mut Context<Self>,
4454    ) -> Result<()> {
4455        let worktree = project
4456            .read(cx)
4457            .worktree_for_entry(entry_id, cx)
4458            .context("can't reveal a non-existent entry in the project panel")?;
4459        let worktree = worktree.read(cx);
4460        if skip_ignored
4461            && worktree
4462                .entry_for_id(entry_id)
4463                .map_or(true, |entry| entry.is_ignored && !entry.is_always_included)
4464        {
4465            anyhow::bail!("can't reveal an ignored entry in the project panel");
4466        }
4467
4468        let worktree_id = worktree.id();
4469        self.expand_entry(worktree_id, entry_id, cx);
4470        self.update_visible_entries(Some((worktree_id, entry_id)), cx);
4471        self.marked_entries.clear();
4472        self.marked_entries.insert(SelectedEntry {
4473            worktree_id,
4474            entry_id,
4475        });
4476        self.autoscroll(cx);
4477        cx.notify();
4478        Ok(())
4479    }
4480
4481    fn find_active_indent_guide(
4482        &self,
4483        indent_guides: &[IndentGuideLayout],
4484        cx: &App,
4485    ) -> Option<usize> {
4486        let (worktree, entry) = self.selected_entry(cx)?;
4487
4488        // Find the parent entry of the indent guide, this will either be the
4489        // expanded folder we have selected, or the parent of the currently
4490        // selected file/collapsed directory
4491        let mut entry = entry;
4492        loop {
4493            let is_expanded_dir = entry.is_dir()
4494                && self
4495                    .expanded_dir_ids
4496                    .get(&worktree.id())
4497                    .map(|ids| ids.binary_search(&entry.id).is_ok())
4498                    .unwrap_or(false);
4499            if is_expanded_dir {
4500                break;
4501            }
4502            entry = worktree.entry_for_path(&entry.path.parent()?)?;
4503        }
4504
4505        let (active_indent_range, depth) = {
4506            let (worktree_ix, child_offset, ix) = self.index_for_entry(entry.id, worktree.id())?;
4507            let child_paths = &self.visible_entries[worktree_ix].1;
4508            let mut child_count = 0;
4509            let depth = entry.path.ancestors().count();
4510            while let Some(entry) = child_paths.get(child_offset + child_count + 1) {
4511                if entry.path.ancestors().count() <= depth {
4512                    break;
4513                }
4514                child_count += 1;
4515            }
4516
4517            let start = ix + 1;
4518            let end = start + child_count;
4519
4520            let (_, entries, paths) = &self.visible_entries[worktree_ix];
4521            let visible_worktree_entries =
4522                paths.get_or_init(|| entries.iter().map(|e| (e.path.clone())).collect());
4523
4524            // Calculate the actual depth of the entry, taking into account that directories can be auto-folded.
4525            let (depth, _) = Self::calculate_depth_and_difference(entry, visible_worktree_entries);
4526            (start..end, depth)
4527        };
4528
4529        let candidates = indent_guides
4530            .iter()
4531            .enumerate()
4532            .filter(|(_, indent_guide)| indent_guide.offset.x == depth);
4533
4534        for (i, indent) in candidates {
4535            // Find matches that are either an exact match, partially on screen, or inside the enclosing indent
4536            if active_indent_range.start <= indent.offset.y + indent.length
4537                && indent.offset.y <= active_indent_range.end
4538            {
4539                return Some(i);
4540            }
4541        }
4542        None
4543    }
4544}
4545
4546fn item_width_estimate(depth: usize, item_text_chars: usize, is_symlink: bool) -> usize {
4547    const ICON_SIZE_FACTOR: usize = 2;
4548    let mut item_width = depth * ICON_SIZE_FACTOR + item_text_chars;
4549    if is_symlink {
4550        item_width += ICON_SIZE_FACTOR;
4551    }
4552    item_width
4553}
4554
4555impl Render for ProjectPanel {
4556    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
4557        let has_worktree = !self.visible_entries.is_empty();
4558        let project = self.project.read(cx);
4559        let indent_size = ProjectPanelSettings::get_global(cx).indent_size;
4560        let show_indent_guides =
4561            ProjectPanelSettings::get_global(cx).indent_guides.show == ShowIndentGuides::Always;
4562        let is_local = project.is_local();
4563
4564        if has_worktree {
4565            let item_count = self
4566                .visible_entries
4567                .iter()
4568                .map(|(_, worktree_entries, _)| worktree_entries.len())
4569                .sum();
4570
4571            fn handle_drag_move_scroll<T: 'static>(
4572                this: &mut ProjectPanel,
4573                e: &DragMoveEvent<T>,
4574                window: &mut Window,
4575                cx: &mut Context<ProjectPanel>,
4576            ) {
4577                if !e.bounds.contains(&e.event.position) {
4578                    return;
4579                }
4580                this.hover_scroll_task.take();
4581                let panel_height = e.bounds.size.height;
4582                if panel_height <= px(0.) {
4583                    return;
4584                }
4585
4586                let event_offset = e.event.position.y - e.bounds.origin.y;
4587                // How far along in the project panel is our cursor? (0. is the top of a list, 1. is the bottom)
4588                let hovered_region_offset = event_offset / panel_height;
4589
4590                // We want the scrolling to be a bit faster when the cursor is closer to the edge of a list.
4591                // These pixels offsets were picked arbitrarily.
4592                let vertical_scroll_offset = if hovered_region_offset <= 0.05 {
4593                    8.
4594                } else if hovered_region_offset <= 0.15 {
4595                    5.
4596                } else if hovered_region_offset >= 0.95 {
4597                    -8.
4598                } else if hovered_region_offset >= 0.85 {
4599                    -5.
4600                } else {
4601                    return;
4602                };
4603                let adjustment = point(px(0.), px(vertical_scroll_offset));
4604                this.hover_scroll_task = Some(cx.spawn_in(window, async move |this, cx| {
4605                    loop {
4606                        let should_stop_scrolling = this
4607                            .update(cx, |this, cx| {
4608                                this.hover_scroll_task.as_ref()?;
4609                                let handle = this.scroll_handle.0.borrow_mut();
4610                                let offset = handle.base_handle.offset();
4611
4612                                handle.base_handle.set_offset(offset + adjustment);
4613                                cx.notify();
4614                                Some(())
4615                            })
4616                            .ok()
4617                            .flatten()
4618                            .is_some();
4619                        if should_stop_scrolling {
4620                            return;
4621                        }
4622                        cx.background_executor()
4623                            .timer(Duration::from_millis(16))
4624                            .await;
4625                    }
4626                }));
4627            }
4628            h_flex()
4629                .id("project-panel")
4630                .group("project-panel")
4631                .on_drag_move(cx.listener(handle_drag_move_scroll::<ExternalPaths>))
4632                .on_drag_move(cx.listener(handle_drag_move_scroll::<DraggedSelection>))
4633                .size_full()
4634                .relative()
4635                .on_hover(cx.listener(|this, hovered, window, cx| {
4636                    if *hovered {
4637                        this.show_scrollbar = true;
4638                        this.hide_scrollbar_task.take();
4639                        cx.notify();
4640                    } else if !this.focus_handle.contains_focused(window, cx) {
4641                        this.hide_scrollbar(window, cx);
4642                    }
4643                }))
4644                .on_click(cx.listener(|this, _event, _, cx| {
4645                    cx.stop_propagation();
4646                    this.selection = None;
4647                    this.marked_entries.clear();
4648                }))
4649                .key_context(self.dispatch_context(window, cx))
4650                .on_action(cx.listener(Self::select_next))
4651                .on_action(cx.listener(Self::select_previous))
4652                .on_action(cx.listener(Self::select_first))
4653                .on_action(cx.listener(Self::select_last))
4654                .on_action(cx.listener(Self::select_parent))
4655                .on_action(cx.listener(Self::select_next_git_entry))
4656                .on_action(cx.listener(Self::select_prev_git_entry))
4657                .on_action(cx.listener(Self::select_next_diagnostic))
4658                .on_action(cx.listener(Self::select_prev_diagnostic))
4659                .on_action(cx.listener(Self::select_next_directory))
4660                .on_action(cx.listener(Self::select_prev_directory))
4661                .on_action(cx.listener(Self::expand_selected_entry))
4662                .on_action(cx.listener(Self::collapse_selected_entry))
4663                .on_action(cx.listener(Self::collapse_all_entries))
4664                .on_action(cx.listener(Self::open))
4665                .on_action(cx.listener(Self::open_permanent))
4666                .on_action(cx.listener(Self::confirm))
4667                .on_action(cx.listener(Self::cancel))
4668                .on_action(cx.listener(Self::copy_path))
4669                .on_action(cx.listener(Self::copy_relative_path))
4670                .on_action(cx.listener(Self::new_search_in_directory))
4671                .on_action(cx.listener(Self::unfold_directory))
4672                .on_action(cx.listener(Self::fold_directory))
4673                .on_action(cx.listener(Self::remove_from_project))
4674                .when(!project.is_read_only(cx), |el| {
4675                    el.on_action(cx.listener(Self::new_file))
4676                        .on_action(cx.listener(Self::new_directory))
4677                        .on_action(cx.listener(Self::rename))
4678                        .on_action(cx.listener(Self::delete))
4679                        .on_action(cx.listener(Self::trash))
4680                        .on_action(cx.listener(Self::cut))
4681                        .on_action(cx.listener(Self::copy))
4682                        .on_action(cx.listener(Self::paste))
4683                        .on_action(cx.listener(Self::duplicate))
4684                        .on_click(cx.listener(|this, event: &gpui::ClickEvent, window, cx| {
4685                            if event.up.click_count > 1 {
4686                                if let Some(entry_id) = this.last_worktree_root_id {
4687                                    let project = this.project.read(cx);
4688
4689                                    let worktree_id = if let Some(worktree) =
4690                                        project.worktree_for_entry(entry_id, cx)
4691                                    {
4692                                        worktree.read(cx).id()
4693                                    } else {
4694                                        return;
4695                                    };
4696
4697                                    this.selection = Some(SelectedEntry {
4698                                        worktree_id,
4699                                        entry_id,
4700                                    });
4701
4702                                    this.new_file(&NewFile, window, cx);
4703                                }
4704                            }
4705                        }))
4706                })
4707                .when(project.is_local(), |el| {
4708                    el.on_action(cx.listener(Self::reveal_in_finder))
4709                        .on_action(cx.listener(Self::open_system))
4710                        .on_action(cx.listener(Self::open_in_terminal))
4711                })
4712                .when(project.is_via_ssh(), |el| {
4713                    el.on_action(cx.listener(Self::open_in_terminal))
4714                })
4715                .on_mouse_down(
4716                    MouseButton::Right,
4717                    cx.listener(move |this, event: &MouseDownEvent, window, cx| {
4718                        // When deploying the context menu anywhere below the last project entry,
4719                        // act as if the user clicked the root of the last worktree.
4720                        if let Some(entry_id) = this.last_worktree_root_id {
4721                            this.deploy_context_menu(event.position, entry_id, window, cx);
4722                        }
4723                    }),
4724                )
4725                .track_focus(&self.focus_handle(cx))
4726                .child(
4727                    uniform_list(cx.entity().clone(), "entries", item_count, {
4728                        |this, range, window, cx| {
4729                            let mut items = Vec::with_capacity(range.end - range.start);
4730                            this.for_each_visible_entry(
4731                                range,
4732                                window,
4733                                cx,
4734                                |id, details, window, cx| {
4735                                    items.push(this.render_entry(id, details, window, cx));
4736                                },
4737                            );
4738                            items
4739                        }
4740                    })
4741                    .when(show_indent_guides, |list| {
4742                        list.with_decoration(
4743                            ui::indent_guides(
4744                                cx.entity().clone(),
4745                                px(indent_size),
4746                                IndentGuideColors::panel(cx),
4747                                |this, range, window, cx| {
4748                                    let mut items =
4749                                        SmallVec::with_capacity(range.end - range.start);
4750                                    this.iter_visible_entries(
4751                                        range,
4752                                        window,
4753                                        cx,
4754                                        |entry, entries, _, _| {
4755                                            let (depth, _) = Self::calculate_depth_and_difference(
4756                                                entry, entries,
4757                                            );
4758                                            items.push(depth);
4759                                        },
4760                                    );
4761                                    items
4762                                },
4763                            )
4764                            .on_click(cx.listener(
4765                                |this, active_indent_guide: &IndentGuideLayout, window, cx| {
4766                                    if window.modifiers().secondary() {
4767                                        let ix = active_indent_guide.offset.y;
4768                                        let Some((target_entry, worktree)) = maybe!({
4769                                            let (worktree_id, entry) = this.entry_at_index(ix)?;
4770                                            let worktree = this
4771                                                .project
4772                                                .read(cx)
4773                                                .worktree_for_id(worktree_id, cx)?;
4774                                            let target_entry = worktree
4775                                                .read(cx)
4776                                                .entry_for_path(&entry.path.parent()?)?;
4777                                            Some((target_entry, worktree))
4778                                        }) else {
4779                                            return;
4780                                        };
4781
4782                                        this.collapse_entry(target_entry.clone(), worktree, cx);
4783                                    }
4784                                },
4785                            ))
4786                            .with_render_fn(
4787                                cx.entity().clone(),
4788                                move |this, params, _, cx| {
4789                                    const LEFT_OFFSET: Pixels = px(14.);
4790                                    const PADDING_Y: Pixels = px(4.);
4791                                    const HITBOX_OVERDRAW: Pixels = px(3.);
4792
4793                                    let active_indent_guide_index =
4794                                        this.find_active_indent_guide(&params.indent_guides, cx);
4795
4796                                    let indent_size = params.indent_size;
4797                                    let item_height = params.item_height;
4798
4799                                    params
4800                                        .indent_guides
4801                                        .into_iter()
4802                                        .enumerate()
4803                                        .map(|(idx, layout)| {
4804                                            let offset = if layout.continues_offscreen {
4805                                                px(0.)
4806                                            } else {
4807                                                PADDING_Y
4808                                            };
4809                                            let bounds = Bounds::new(
4810                                                point(
4811                                                    layout.offset.x * indent_size + LEFT_OFFSET,
4812                                                    layout.offset.y * item_height + offset,
4813                                                ),
4814                                                size(
4815                                                    px(1.),
4816                                                    layout.length * item_height - offset * 2.,
4817                                                ),
4818                                            );
4819                                            ui::RenderedIndentGuide {
4820                                                bounds,
4821                                                layout,
4822                                                is_active: Some(idx) == active_indent_guide_index,
4823                                                hitbox: Some(Bounds::new(
4824                                                    point(
4825                                                        bounds.origin.x - HITBOX_OVERDRAW,
4826                                                        bounds.origin.y,
4827                                                    ),
4828                                                    size(
4829                                                        bounds.size.width + HITBOX_OVERDRAW * 2.,
4830                                                        bounds.size.height,
4831                                                    ),
4832                                                )),
4833                                            }
4834                                        })
4835                                        .collect()
4836                                },
4837                            ),
4838                        )
4839                    })
4840                    .size_full()
4841                    .with_sizing_behavior(ListSizingBehavior::Infer)
4842                    .with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained)
4843                    .with_width_from_item(self.max_width_item_index)
4844                    .track_scroll(self.scroll_handle.clone()),
4845                )
4846                .children(self.render_vertical_scrollbar(cx))
4847                .when_some(self.render_horizontal_scrollbar(cx), |this, scrollbar| {
4848                    this.pb_4().child(scrollbar)
4849                })
4850                .children(self.context_menu.as_ref().map(|(menu, position, _)| {
4851                    deferred(
4852                        anchored()
4853                            .position(*position)
4854                            .anchor(gpui::Corner::TopLeft)
4855                            .child(menu.clone()),
4856                    )
4857                    .with_priority(1)
4858                }))
4859        } else {
4860            v_flex()
4861                .id("empty-project_panel")
4862                .size_full()
4863                .p_4()
4864                .track_focus(&self.focus_handle(cx))
4865                .child(
4866                    Button::new("open_project", "Open a project")
4867                        .full_width()
4868                        .key_binding(KeyBinding::for_action_in(
4869                            &OpenRecent::default(),
4870                            &self.focus_handle,
4871                            window,
4872                            cx,
4873                        ))
4874                        .on_click(cx.listener(|this, _, window, cx| {
4875                            this.workspace
4876                                .update(cx, |_, cx| {
4877                                    window.dispatch_action(OpenRecent::default().boxed_clone(), cx);
4878                                })
4879                                .log_err();
4880                        })),
4881                )
4882                .when(is_local, |div| {
4883                    div.drag_over::<ExternalPaths>(|style, _, _, cx| {
4884                        style.bg(cx.theme().colors().drop_target_background)
4885                    })
4886                    .on_drop(cx.listener(
4887                        move |this, external_paths: &ExternalPaths, window, cx| {
4888                            this.last_external_paths_drag_over_entry = None;
4889                            this.marked_entries.clear();
4890                            this.hover_scroll_task.take();
4891                            if let Some(task) = this
4892                                .workspace
4893                                .update(cx, |workspace, cx| {
4894                                    workspace.open_workspace_for_paths(
4895                                        true,
4896                                        external_paths.paths().to_owned(),
4897                                        window,
4898                                        cx,
4899                                    )
4900                                })
4901                                .log_err()
4902                            {
4903                                task.detach_and_log_err(cx);
4904                            }
4905                            cx.stop_propagation();
4906                        },
4907                    ))
4908                })
4909        }
4910    }
4911}
4912
4913impl Render for DraggedProjectEntryView {
4914    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
4915        let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
4916        h_flex()
4917            .font(ui_font)
4918            .pl(self.click_offset.x + px(12.))
4919            .pt(self.click_offset.y + px(12.))
4920            .child(
4921                div()
4922                    .flex()
4923                    .gap_1()
4924                    .items_center()
4925                    .py_1()
4926                    .px_2()
4927                    .rounded_lg()
4928                    .bg(cx.theme().colors().background)
4929                    .map(|this| {
4930                        if self.selections.len() > 1 && self.selections.contains(&self.selection) {
4931                            this.child(Label::new(format!("{} entries", self.selections.len())))
4932                        } else {
4933                            this.child(if let Some(icon) = &self.details.icon {
4934                                div().child(Icon::from_path(icon.clone()))
4935                            } else {
4936                                div()
4937                            })
4938                            .child(Label::new(self.details.filename.clone()))
4939                        }
4940                    }),
4941            )
4942    }
4943}
4944
4945impl EventEmitter<Event> for ProjectPanel {}
4946
4947impl EventEmitter<PanelEvent> for ProjectPanel {}
4948
4949impl Panel for ProjectPanel {
4950    fn position(&self, _: &Window, cx: &App) -> DockPosition {
4951        match ProjectPanelSettings::get_global(cx).dock {
4952            ProjectPanelDockPosition::Left => DockPosition::Left,
4953            ProjectPanelDockPosition::Right => DockPosition::Right,
4954        }
4955    }
4956
4957    fn position_is_valid(&self, position: DockPosition) -> bool {
4958        matches!(position, DockPosition::Left | DockPosition::Right)
4959    }
4960
4961    fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
4962        settings::update_settings_file::<ProjectPanelSettings>(
4963            self.fs.clone(),
4964            cx,
4965            move |settings, _| {
4966                let dock = match position {
4967                    DockPosition::Left | DockPosition::Bottom => ProjectPanelDockPosition::Left,
4968                    DockPosition::Right => ProjectPanelDockPosition::Right,
4969                };
4970                settings.dock = Some(dock);
4971            },
4972        );
4973    }
4974
4975    fn size(&self, _: &Window, cx: &App) -> Pixels {
4976        self.width
4977            .unwrap_or_else(|| ProjectPanelSettings::get_global(cx).default_width)
4978    }
4979
4980    fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
4981        self.width = size;
4982        cx.notify();
4983        cx.defer_in(window, |this, _, cx| {
4984            this.serialize(cx);
4985        });
4986    }
4987
4988    fn icon(&self, _: &Window, cx: &App) -> Option<IconName> {
4989        ProjectPanelSettings::get_global(cx)
4990            .button
4991            .then_some(IconName::FileTree)
4992    }
4993
4994    fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
4995        Some("Project Panel")
4996    }
4997
4998    fn toggle_action(&self) -> Box<dyn Action> {
4999        Box::new(ToggleFocus)
5000    }
5001
5002    fn persistent_name() -> &'static str {
5003        "Project Panel"
5004    }
5005
5006    fn starts_open(&self, _: &Window, cx: &App) -> bool {
5007        let project = &self.project.read(cx);
5008        project.visible_worktrees(cx).any(|tree| {
5009            tree.read(cx)
5010                .root_entry()
5011                .map_or(false, |entry| entry.is_dir())
5012        })
5013    }
5014
5015    fn activation_priority(&self) -> u32 {
5016        0
5017    }
5018}
5019
5020impl Focusable for ProjectPanel {
5021    fn focus_handle(&self, _cx: &App) -> FocusHandle {
5022        self.focus_handle.clone()
5023    }
5024}
5025
5026impl ClipboardEntry {
5027    fn is_cut(&self) -> bool {
5028        matches!(self, Self::Cut { .. })
5029    }
5030
5031    fn items(&self) -> &BTreeSet<SelectedEntry> {
5032        match self {
5033            ClipboardEntry::Copied(entries) | ClipboardEntry::Cut(entries) => entries,
5034        }
5035    }
5036}
5037
5038#[cfg(test)]
5039mod project_panel_tests;