project_panel.rs

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