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