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