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!(
1497                                "\n\n{dirty_buffers} of these have unsaved changes, which will be lost."
1498                            )
1499                        };
1500
1501                        format!(
1502                            "Do you want to {} the following {} files?\n{}{unsaved_warning}",
1503                            operation.to_lowercase(),
1504                            file_paths.len(),
1505                            names.join("\n")
1506                        )
1507                    }
1508                };
1509                Some(window.prompt(PromptLevel::Info, &prompt, None, &[operation, "Cancel"], cx))
1510            } else {
1511                None
1512            };
1513            let next_selection = self.find_next_selection_after_deletion(items_to_delete, cx);
1514            cx.spawn_in(window, async move |panel, cx| {
1515                if let Some(answer) = answer {
1516                    if answer.await != Ok(0) {
1517                        return anyhow::Ok(());
1518                    }
1519                }
1520                for (entry_id, _) in file_paths {
1521                    panel
1522                        .update(cx, |panel, cx| {
1523                            panel
1524                                .project
1525                                .update(cx, |project, cx| project.delete_entry(entry_id, trash, cx))
1526                                .context("no such entry")
1527                        })??
1528                        .await?;
1529                }
1530                panel.update_in(cx, |panel, window, cx| {
1531                    if let Some(next_selection) = next_selection {
1532                        panel.selection = Some(next_selection);
1533                        panel.autoscroll(cx);
1534                    } else {
1535                        panel.select_last(&SelectLast {}, window, cx);
1536                    }
1537                })?;
1538                Ok(())
1539            })
1540            .detach_and_log_err(cx);
1541            Some(())
1542        });
1543    }
1544
1545    fn find_next_selection_after_deletion(
1546        &self,
1547        sanitized_entries: BTreeSet<SelectedEntry>,
1548        cx: &mut Context<Self>,
1549    ) -> Option<SelectedEntry> {
1550        if sanitized_entries.is_empty() {
1551            return None;
1552        }
1553        let project = self.project.read(cx);
1554        let (worktree_id, worktree) = sanitized_entries
1555            .iter()
1556            .map(|entry| entry.worktree_id)
1557            .filter_map(|id| project.worktree_for_id(id, cx).map(|w| (id, w.read(cx))))
1558            .max_by(|(_, a), (_, b)| a.root_name().cmp(b.root_name()))?;
1559        let git_store = project.git_store().read(cx);
1560
1561        let marked_entries_in_worktree = sanitized_entries
1562            .iter()
1563            .filter(|e| e.worktree_id == worktree_id)
1564            .collect::<HashSet<_>>();
1565        let latest_entry = marked_entries_in_worktree
1566            .iter()
1567            .max_by(|a, b| {
1568                match (
1569                    worktree.entry_for_id(a.entry_id),
1570                    worktree.entry_for_id(b.entry_id),
1571                ) {
1572                    (Some(a), Some(b)) => {
1573                        compare_paths((&a.path, a.is_file()), (&b.path, b.is_file()))
1574                    }
1575                    _ => cmp::Ordering::Equal,
1576                }
1577            })
1578            .and_then(|e| worktree.entry_for_id(e.entry_id))?;
1579
1580        let parent_path = latest_entry.path.parent()?;
1581        let parent_entry = worktree.entry_for_path(parent_path)?;
1582
1583        // Remove all siblings that are being deleted except the last marked entry
1584        let repo_snapshots = git_store.repo_snapshots(cx);
1585        let worktree_snapshot = worktree.snapshot();
1586        let hide_gitignore = ProjectPanelSettings::get_global(cx).hide_gitignore;
1587        let mut siblings: Vec<_> =
1588            ChildEntriesGitIter::new(&repo_snapshots, &worktree_snapshot, parent_path)
1589                .filter(|sibling| {
1590                    (sibling.id == latest_entry.id)
1591                        || (!marked_entries_in_worktree.contains(&&SelectedEntry {
1592                            worktree_id,
1593                            entry_id: sibling.id,
1594                        }) && (!hide_gitignore || !sibling.is_ignored))
1595                })
1596                .map(|entry| entry.to_owned())
1597                .collect();
1598
1599        project::sort_worktree_entries(&mut siblings);
1600        let sibling_entry_index = siblings
1601            .iter()
1602            .position(|sibling| sibling.id == latest_entry.id)?;
1603
1604        if let Some(next_sibling) = sibling_entry_index
1605            .checked_add(1)
1606            .and_then(|i| siblings.get(i))
1607        {
1608            return Some(SelectedEntry {
1609                worktree_id,
1610                entry_id: next_sibling.id,
1611            });
1612        }
1613        if let Some(prev_sibling) = sibling_entry_index
1614            .checked_sub(1)
1615            .and_then(|i| siblings.get(i))
1616        {
1617            return Some(SelectedEntry {
1618                worktree_id,
1619                entry_id: prev_sibling.id,
1620            });
1621        }
1622        // No neighbour sibling found, fall back to parent
1623        Some(SelectedEntry {
1624            worktree_id,
1625            entry_id: parent_entry.id,
1626        })
1627    }
1628
1629    fn unfold_directory(&mut self, _: &UnfoldDirectory, _: &mut Window, cx: &mut Context<Self>) {
1630        if let Some((worktree, entry)) = self.selected_entry(cx) {
1631            self.unfolded_dir_ids.insert(entry.id);
1632
1633            let snapshot = worktree.snapshot();
1634            let mut parent_path = entry.path.parent();
1635            while let Some(path) = parent_path {
1636                if let Some(parent_entry) = worktree.entry_for_path(path) {
1637                    let mut children_iter = snapshot.child_entries(path);
1638
1639                    if children_iter.by_ref().take(2).count() > 1 {
1640                        break;
1641                    }
1642
1643                    self.unfolded_dir_ids.insert(parent_entry.id);
1644                    parent_path = path.parent();
1645                } else {
1646                    break;
1647                }
1648            }
1649
1650            self.update_visible_entries(None, cx);
1651            self.autoscroll(cx);
1652            cx.notify();
1653        }
1654    }
1655
1656    fn fold_directory(&mut self, _: &FoldDirectory, _: &mut Window, cx: &mut Context<Self>) {
1657        if let Some((worktree, entry)) = self.selected_entry(cx) {
1658            self.unfolded_dir_ids.remove(&entry.id);
1659
1660            let snapshot = worktree.snapshot();
1661            let mut path = &*entry.path;
1662            loop {
1663                let mut child_entries_iter = snapshot.child_entries(path);
1664                if let Some(child) = child_entries_iter.next() {
1665                    if child_entries_iter.next().is_none() && child.is_dir() {
1666                        self.unfolded_dir_ids.remove(&child.id);
1667                        path = &*child.path;
1668                    } else {
1669                        break;
1670                    }
1671                } else {
1672                    break;
1673                }
1674            }
1675
1676            self.update_visible_entries(None, cx);
1677            self.autoscroll(cx);
1678            cx.notify();
1679        }
1680    }
1681
1682    fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
1683        if let Some(edit_state) = &self.edit_state {
1684            if edit_state.processing_filename.is_none() {
1685                self.filename_editor.update(cx, |editor, cx| {
1686                    editor.move_to_end_of_line(
1687                        &editor::actions::MoveToEndOfLine {
1688                            stop_at_soft_wraps: false,
1689                        },
1690                        window,
1691                        cx,
1692                    );
1693                });
1694                return;
1695            }
1696        }
1697        if let Some(selection) = self.selection {
1698            let (mut worktree_ix, mut entry_ix, _) =
1699                self.index_for_selection(selection).unwrap_or_default();
1700            if let Some((_, worktree_entries, _)) = self.visible_entries.get(worktree_ix) {
1701                if entry_ix + 1 < worktree_entries.len() {
1702                    entry_ix += 1;
1703                } else {
1704                    worktree_ix += 1;
1705                    entry_ix = 0;
1706                }
1707            }
1708
1709            if let Some((worktree_id, worktree_entries, _)) = self.visible_entries.get(worktree_ix)
1710            {
1711                if let Some(entry) = worktree_entries.get(entry_ix) {
1712                    let selection = SelectedEntry {
1713                        worktree_id: *worktree_id,
1714                        entry_id: entry.id,
1715                    };
1716                    self.selection = Some(selection);
1717                    if window.modifiers().shift {
1718                        self.marked_entries.insert(selection);
1719                    }
1720
1721                    self.autoscroll(cx);
1722                    cx.notify();
1723                }
1724            }
1725        } else {
1726            self.select_first(&SelectFirst {}, window, cx);
1727        }
1728    }
1729
1730    fn select_prev_diagnostic(
1731        &mut self,
1732        _: &SelectPrevDiagnostic,
1733        _: &mut Window,
1734        cx: &mut Context<Self>,
1735    ) {
1736        let selection = self.find_entry(
1737            self.selection.as_ref(),
1738            true,
1739            |entry, worktree_id| {
1740                (self.selection.is_none()
1741                    || self.selection.is_some_and(|selection| {
1742                        if selection.worktree_id == worktree_id {
1743                            selection.entry_id != entry.id
1744                        } else {
1745                            true
1746                        }
1747                    }))
1748                    && entry.is_file()
1749                    && self
1750                        .diagnostics
1751                        .contains_key(&(worktree_id, entry.path.to_path_buf()))
1752            },
1753            cx,
1754        );
1755
1756        if let Some(selection) = selection {
1757            self.selection = Some(selection);
1758            self.expand_entry(selection.worktree_id, selection.entry_id, cx);
1759            self.update_visible_entries(Some((selection.worktree_id, selection.entry_id)), cx);
1760            self.autoscroll(cx);
1761            cx.notify();
1762        }
1763    }
1764
1765    fn select_next_diagnostic(
1766        &mut self,
1767        _: &SelectNextDiagnostic,
1768        _: &mut Window,
1769        cx: &mut Context<Self>,
1770    ) {
1771        let selection = self.find_entry(
1772            self.selection.as_ref(),
1773            false,
1774            |entry, worktree_id| {
1775                (self.selection.is_none()
1776                    || self.selection.is_some_and(|selection| {
1777                        if selection.worktree_id == worktree_id {
1778                            selection.entry_id != entry.id
1779                        } else {
1780                            true
1781                        }
1782                    }))
1783                    && entry.is_file()
1784                    && self
1785                        .diagnostics
1786                        .contains_key(&(worktree_id, entry.path.to_path_buf()))
1787            },
1788            cx,
1789        );
1790
1791        if let Some(selection) = selection {
1792            self.selection = Some(selection);
1793            self.expand_entry(selection.worktree_id, selection.entry_id, cx);
1794            self.update_visible_entries(Some((selection.worktree_id, selection.entry_id)), cx);
1795            self.autoscroll(cx);
1796            cx.notify();
1797        }
1798    }
1799
1800    fn select_prev_git_entry(
1801        &mut self,
1802        _: &SelectPrevGitEntry,
1803        _: &mut Window,
1804        cx: &mut Context<Self>,
1805    ) {
1806        let selection = self.find_entry(
1807            self.selection.as_ref(),
1808            true,
1809            |entry, worktree_id| {
1810                (self.selection.is_none()
1811                    || self.selection.is_some_and(|selection| {
1812                        if selection.worktree_id == worktree_id {
1813                            selection.entry_id != entry.id
1814                        } else {
1815                            true
1816                        }
1817                    }))
1818                    && entry.is_file()
1819                    && entry.git_summary.index.modified + entry.git_summary.worktree.modified > 0
1820            },
1821            cx,
1822        );
1823
1824        if let Some(selection) = selection {
1825            self.selection = Some(selection);
1826            self.expand_entry(selection.worktree_id, selection.entry_id, cx);
1827            self.update_visible_entries(Some((selection.worktree_id, selection.entry_id)), cx);
1828            self.autoscroll(cx);
1829            cx.notify();
1830        }
1831    }
1832
1833    fn select_prev_directory(
1834        &mut self,
1835        _: &SelectPrevDirectory,
1836        _: &mut Window,
1837        cx: &mut Context<Self>,
1838    ) {
1839        let selection = self.find_visible_entry(
1840            self.selection.as_ref(),
1841            true,
1842            |entry, worktree_id| {
1843                (self.selection.is_none()
1844                    || self.selection.is_some_and(|selection| {
1845                        if selection.worktree_id == worktree_id {
1846                            selection.entry_id != entry.id
1847                        } else {
1848                            true
1849                        }
1850                    }))
1851                    && entry.is_dir()
1852            },
1853            cx,
1854        );
1855
1856        if let Some(selection) = selection {
1857            self.selection = Some(selection);
1858            self.autoscroll(cx);
1859            cx.notify();
1860        }
1861    }
1862
1863    fn select_next_directory(
1864        &mut self,
1865        _: &SelectNextDirectory,
1866        _: &mut Window,
1867        cx: &mut Context<Self>,
1868    ) {
1869        let selection = self.find_visible_entry(
1870            self.selection.as_ref(),
1871            false,
1872            |entry, worktree_id| {
1873                (self.selection.is_none()
1874                    || self.selection.is_some_and(|selection| {
1875                        if selection.worktree_id == worktree_id {
1876                            selection.entry_id != entry.id
1877                        } else {
1878                            true
1879                        }
1880                    }))
1881                    && entry.is_dir()
1882            },
1883            cx,
1884        );
1885
1886        if let Some(selection) = selection {
1887            self.selection = Some(selection);
1888            self.autoscroll(cx);
1889            cx.notify();
1890        }
1891    }
1892
1893    fn select_next_git_entry(
1894        &mut self,
1895        _: &SelectNextGitEntry,
1896        _: &mut Window,
1897        cx: &mut Context<Self>,
1898    ) {
1899        let selection = self.find_entry(
1900            self.selection.as_ref(),
1901            false,
1902            |entry, worktree_id| {
1903                (self.selection.is_none()
1904                    || self.selection.is_some_and(|selection| {
1905                        if selection.worktree_id == worktree_id {
1906                            selection.entry_id != entry.id
1907                        } else {
1908                            true
1909                        }
1910                    }))
1911                    && entry.is_file()
1912                    && entry.git_summary.index.modified + entry.git_summary.worktree.modified > 0
1913            },
1914            cx,
1915        );
1916
1917        if let Some(selection) = selection {
1918            self.selection = Some(selection);
1919            self.expand_entry(selection.worktree_id, selection.entry_id, cx);
1920            self.update_visible_entries(Some((selection.worktree_id, selection.entry_id)), cx);
1921            self.autoscroll(cx);
1922            cx.notify();
1923        }
1924    }
1925
1926    fn select_parent(&mut self, _: &SelectParent, window: &mut Window, cx: &mut Context<Self>) {
1927        if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
1928            if let Some(parent) = entry.path.parent() {
1929                let worktree = worktree.read(cx);
1930                if let Some(parent_entry) = worktree.entry_for_path(parent) {
1931                    self.selection = Some(SelectedEntry {
1932                        worktree_id: worktree.id(),
1933                        entry_id: parent_entry.id,
1934                    });
1935                    self.autoscroll(cx);
1936                    cx.notify();
1937                }
1938            }
1939        } else {
1940            self.select_first(&SelectFirst {}, window, cx);
1941        }
1942    }
1943
1944    fn select_first(&mut self, _: &SelectFirst, window: &mut Window, cx: &mut Context<Self>) {
1945        let worktree = self
1946            .visible_entries
1947            .first()
1948            .and_then(|(worktree_id, _, _)| {
1949                self.project.read(cx).worktree_for_id(*worktree_id, cx)
1950            });
1951        if let Some(worktree) = worktree {
1952            let worktree = worktree.read(cx);
1953            let worktree_id = worktree.id();
1954            if let Some(root_entry) = worktree.root_entry() {
1955                let selection = SelectedEntry {
1956                    worktree_id,
1957                    entry_id: root_entry.id,
1958                };
1959                self.selection = Some(selection);
1960                if window.modifiers().shift {
1961                    self.marked_entries.insert(selection);
1962                }
1963                self.autoscroll(cx);
1964                cx.notify();
1965            }
1966        }
1967    }
1968
1969    fn select_last(&mut self, _: &SelectLast, _: &mut Window, cx: &mut Context<Self>) {
1970        if let Some((worktree_id, visible_worktree_entries, _)) = self.visible_entries.last() {
1971            let worktree = self.project.read(cx).worktree_for_id(*worktree_id, cx);
1972            if let (Some(worktree), Some(entry)) = (worktree, visible_worktree_entries.last()) {
1973                let worktree = worktree.read(cx);
1974                if let Some(entry) = worktree.entry_for_id(entry.id) {
1975                    let selection = SelectedEntry {
1976                        worktree_id: *worktree_id,
1977                        entry_id: entry.id,
1978                    };
1979                    self.selection = Some(selection);
1980                    self.autoscroll(cx);
1981                    cx.notify();
1982                }
1983            }
1984        }
1985    }
1986
1987    fn autoscroll(&mut self, cx: &mut Context<Self>) {
1988        if let Some((_, _, index)) = self.selection.and_then(|s| self.index_for_selection(s)) {
1989            self.scroll_handle
1990                .scroll_to_item(index, ScrollStrategy::Center);
1991            cx.notify();
1992        }
1993    }
1994
1995    fn cut(&mut self, _: &Cut, _: &mut Window, cx: &mut Context<Self>) {
1996        let entries = self.disjoint_entries(cx);
1997        if !entries.is_empty() {
1998            self.clipboard = Some(ClipboardEntry::Cut(entries));
1999            cx.notify();
2000        }
2001    }
2002
2003    fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context<Self>) {
2004        let entries = self.disjoint_entries(cx);
2005        if !entries.is_empty() {
2006            self.clipboard = Some(ClipboardEntry::Copied(entries));
2007            cx.notify();
2008        }
2009    }
2010
2011    fn create_paste_path(
2012        &self,
2013        source: &SelectedEntry,
2014        (worktree, target_entry): (Entity<Worktree>, &Entry),
2015        cx: &App,
2016    ) -> Option<(PathBuf, Option<Range<usize>>)> {
2017        let mut new_path = target_entry.path.to_path_buf();
2018        // If we're pasting into a file, or a directory into itself, go up one level.
2019        if target_entry.is_file() || (target_entry.is_dir() && target_entry.id == source.entry_id) {
2020            new_path.pop();
2021        }
2022        let clipboard_entry_file_name = self
2023            .project
2024            .read(cx)
2025            .path_for_entry(source.entry_id, cx)?
2026            .path
2027            .file_name()?
2028            .to_os_string();
2029        new_path.push(&clipboard_entry_file_name);
2030        let extension = new_path.extension().map(|e| e.to_os_string());
2031        let file_name_without_extension = Path::new(&clipboard_entry_file_name).file_stem()?;
2032        let file_name_len = file_name_without_extension.to_string_lossy().len();
2033        let mut disambiguation_range = None;
2034        let mut ix = 0;
2035        {
2036            let worktree = worktree.read(cx);
2037            while worktree.entry_for_path(&new_path).is_some() {
2038                new_path.pop();
2039
2040                let mut new_file_name = file_name_without_extension.to_os_string();
2041
2042                let disambiguation = " copy";
2043                let mut disambiguation_len = disambiguation.len();
2044
2045                new_file_name.push(disambiguation);
2046
2047                if ix > 0 {
2048                    let extra_disambiguation = format!(" {}", ix);
2049                    disambiguation_len += extra_disambiguation.len();
2050
2051                    new_file_name.push(extra_disambiguation);
2052                }
2053                if let Some(extension) = extension.as_ref() {
2054                    new_file_name.push(".");
2055                    new_file_name.push(extension);
2056                }
2057
2058                new_path.push(new_file_name);
2059                disambiguation_range = Some(file_name_len..(file_name_len + disambiguation_len));
2060                ix += 1;
2061            }
2062        }
2063        Some((new_path, disambiguation_range))
2064    }
2065
2066    fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
2067        maybe!({
2068            let (worktree, entry) = self.selected_entry_handle(cx)?;
2069            let entry = entry.clone();
2070            let worktree_id = worktree.read(cx).id();
2071            let clipboard_entries = self
2072                .clipboard
2073                .as_ref()
2074                .filter(|clipboard| !clipboard.items().is_empty())?;
2075            enum PasteTask {
2076                Rename(Task<Result<CreatedEntry>>),
2077                Copy(Task<Result<Option<Entry>>>),
2078            }
2079            let mut paste_entry_tasks: IndexMap<(ProjectEntryId, bool), PasteTask> =
2080                IndexMap::default();
2081            let mut disambiguation_range = None;
2082            let clip_is_cut = clipboard_entries.is_cut();
2083            for clipboard_entry in clipboard_entries.items() {
2084                let (new_path, new_disambiguation_range) =
2085                    self.create_paste_path(clipboard_entry, self.selected_sub_entry(cx)?, cx)?;
2086                let clip_entry_id = clipboard_entry.entry_id;
2087                let is_same_worktree = clipboard_entry.worktree_id == worktree_id;
2088                let relative_worktree_source_path = if !is_same_worktree {
2089                    let target_base_path = worktree.read(cx).abs_path();
2090                    let clipboard_project_path =
2091                        self.project.read(cx).path_for_entry(clip_entry_id, cx)?;
2092                    let clipboard_abs_path = self
2093                        .project
2094                        .read(cx)
2095                        .absolute_path(&clipboard_project_path, cx)?;
2096                    Some(relativize_path(
2097                        &target_base_path,
2098                        clipboard_abs_path.as_path(),
2099                    ))
2100                } else {
2101                    None
2102                };
2103                let task = if clip_is_cut && is_same_worktree {
2104                    let task = self.project.update(cx, |project, cx| {
2105                        project.rename_entry(clip_entry_id, new_path, cx)
2106                    });
2107                    PasteTask::Rename(task)
2108                } else {
2109                    let entry_id = if is_same_worktree {
2110                        clip_entry_id
2111                    } else {
2112                        entry.id
2113                    };
2114                    let task = self.project.update(cx, |project, cx| {
2115                        project.copy_entry(entry_id, relative_worktree_source_path, new_path, cx)
2116                    });
2117                    PasteTask::Copy(task)
2118                };
2119                let needs_delete = !is_same_worktree && clip_is_cut;
2120                paste_entry_tasks.insert((clip_entry_id, needs_delete), task);
2121                disambiguation_range = new_disambiguation_range.or(disambiguation_range);
2122            }
2123
2124            let item_count = paste_entry_tasks.len();
2125
2126            cx.spawn_in(window, async move |project_panel, cx| {
2127                let mut last_succeed = None;
2128                let mut need_delete_ids = Vec::new();
2129                for ((entry_id, need_delete), task) in paste_entry_tasks.into_iter() {
2130                    match task {
2131                        PasteTask::Rename(task) => {
2132                            if let Some(CreatedEntry::Included(entry)) = task.await.log_err() {
2133                                last_succeed = Some(entry);
2134                            }
2135                        }
2136                        PasteTask::Copy(task) => {
2137                            if let Some(Some(entry)) = task.await.log_err() {
2138                                last_succeed = Some(entry);
2139                                if need_delete {
2140                                    need_delete_ids.push(entry_id);
2141                                }
2142                            }
2143                        }
2144                    }
2145                }
2146                // remove entry for cut in difference worktree
2147                for entry_id in need_delete_ids {
2148                    project_panel
2149                        .update(cx, |project_panel, cx| {
2150                            project_panel
2151                                .project
2152                                .update(cx, |project, cx| project.delete_entry(entry_id, true, cx))
2153                                .ok_or_else(|| anyhow!("no such entry"))
2154                        })??
2155                        .await?;
2156                }
2157                // update selection
2158                if let Some(entry) = last_succeed {
2159                    project_panel
2160                        .update_in(cx, |project_panel, window, cx| {
2161                            project_panel.selection = Some(SelectedEntry {
2162                                worktree_id,
2163                                entry_id: entry.id,
2164                            });
2165
2166                            if item_count == 1 {
2167                                // open entry if not dir, and only focus if rename is not pending
2168                                if !entry.is_dir() {
2169                                    project_panel.open_entry(
2170                                        entry.id,
2171                                        disambiguation_range.is_none(),
2172                                        false,
2173                                        cx,
2174                                    );
2175                                }
2176
2177                                // if only one entry was pasted and it was disambiguated, open the rename editor
2178                                if disambiguation_range.is_some() {
2179                                    cx.defer_in(window, |this, window, cx| {
2180                                        this.rename_impl(disambiguation_range, window, cx);
2181                                    });
2182                                }
2183                            }
2184                        })
2185                        .ok();
2186                }
2187
2188                anyhow::Ok(())
2189            })
2190            .detach_and_log_err(cx);
2191
2192            self.expand_entry(worktree_id, entry.id, cx);
2193            Some(())
2194        });
2195    }
2196
2197    fn duplicate(&mut self, _: &Duplicate, window: &mut Window, cx: &mut Context<Self>) {
2198        self.copy(&Copy {}, window, cx);
2199        self.paste(&Paste {}, window, cx);
2200    }
2201
2202    fn copy_path(
2203        &mut self,
2204        _: &zed_actions::workspace::CopyPath,
2205        _: &mut Window,
2206        cx: &mut Context<Self>,
2207    ) {
2208        let abs_file_paths = {
2209            let project = self.project.read(cx);
2210            self.effective_entries()
2211                .into_iter()
2212                .filter_map(|entry| {
2213                    let entry_path = project.path_for_entry(entry.entry_id, cx)?.path;
2214                    Some(
2215                        project
2216                            .worktree_for_id(entry.worktree_id, cx)?
2217                            .read(cx)
2218                            .abs_path()
2219                            .join(entry_path)
2220                            .to_string_lossy()
2221                            .to_string(),
2222                    )
2223                })
2224                .collect::<Vec<_>>()
2225        };
2226        if !abs_file_paths.is_empty() {
2227            cx.write_to_clipboard(ClipboardItem::new_string(abs_file_paths.join("\n")));
2228        }
2229    }
2230
2231    fn copy_relative_path(
2232        &mut self,
2233        _: &zed_actions::workspace::CopyRelativePath,
2234        _: &mut Window,
2235        cx: &mut Context<Self>,
2236    ) {
2237        let file_paths = {
2238            let project = self.project.read(cx);
2239            self.effective_entries()
2240                .into_iter()
2241                .filter_map(|entry| {
2242                    Some(
2243                        project
2244                            .path_for_entry(entry.entry_id, cx)?
2245                            .path
2246                            .to_string_lossy()
2247                            .to_string(),
2248                    )
2249                })
2250                .collect::<Vec<_>>()
2251        };
2252        if !file_paths.is_empty() {
2253            cx.write_to_clipboard(ClipboardItem::new_string(file_paths.join("\n")));
2254        }
2255    }
2256
2257    fn reveal_in_finder(
2258        &mut self,
2259        _: &RevealInFileManager,
2260        _: &mut Window,
2261        cx: &mut Context<Self>,
2262    ) {
2263        if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
2264            cx.reveal_path(&worktree.read(cx).abs_path().join(&entry.path));
2265        }
2266    }
2267
2268    fn remove_from_project(
2269        &mut self,
2270        _: &RemoveFromProject,
2271        _window: &mut Window,
2272        cx: &mut Context<Self>,
2273    ) {
2274        for entry in self.effective_entries().iter() {
2275            let worktree_id = entry.worktree_id;
2276            self.project
2277                .update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
2278        }
2279    }
2280
2281    fn open_system(&mut self, _: &OpenWithSystem, _: &mut Window, cx: &mut Context<Self>) {
2282        if let Some((worktree, entry)) = self.selected_entry(cx) {
2283            let abs_path = worktree.abs_path().join(&entry.path);
2284            cx.open_with_system(&abs_path);
2285        }
2286    }
2287
2288    fn open_in_terminal(
2289        &mut self,
2290        _: &OpenInTerminal,
2291        window: &mut Window,
2292        cx: &mut Context<Self>,
2293    ) {
2294        if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
2295            let abs_path = match &entry.canonical_path {
2296                Some(canonical_path) => Some(canonical_path.to_path_buf()),
2297                None => worktree.read(cx).absolutize(&entry.path).ok(),
2298            };
2299
2300            let working_directory = if entry.is_dir() {
2301                abs_path
2302            } else {
2303                abs_path.and_then(|path| Some(path.parent()?.to_path_buf()))
2304            };
2305            if let Some(working_directory) = working_directory {
2306                window.dispatch_action(
2307                    workspace::OpenTerminal { working_directory }.boxed_clone(),
2308                    cx,
2309                )
2310            }
2311        }
2312    }
2313
2314    pub fn new_search_in_directory(
2315        &mut self,
2316        _: &NewSearchInDirectory,
2317        window: &mut Window,
2318        cx: &mut Context<Self>,
2319    ) {
2320        if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
2321            let dir_path = if entry.is_dir() {
2322                entry.path.clone()
2323            } else {
2324                // entry is a file, use its parent directory
2325                match entry.path.parent() {
2326                    Some(parent) => Arc::from(parent),
2327                    None => {
2328                        // File at root, open search with empty filter
2329                        self.workspace
2330                            .update(cx, |workspace, cx| {
2331                                search::ProjectSearchView::new_search_in_directory(
2332                                    workspace,
2333                                    Path::new(""),
2334                                    window,
2335                                    cx,
2336                                );
2337                            })
2338                            .ok();
2339                        return;
2340                    }
2341                }
2342            };
2343
2344            let include_root = self.project.read(cx).visible_worktrees(cx).count() > 1;
2345            let dir_path = if include_root {
2346                let mut full_path = PathBuf::from(worktree.read(cx).root_name());
2347                full_path.push(&dir_path);
2348                Arc::from(full_path)
2349            } else {
2350                dir_path
2351            };
2352
2353            self.workspace
2354                .update(cx, |workspace, cx| {
2355                    search::ProjectSearchView::new_search_in_directory(
2356                        workspace, &dir_path, window, cx,
2357                    );
2358                })
2359                .ok();
2360        }
2361    }
2362
2363    fn move_entry(
2364        &mut self,
2365        entry_to_move: ProjectEntryId,
2366        destination: ProjectEntryId,
2367        destination_is_file: bool,
2368        cx: &mut Context<Self>,
2369    ) {
2370        if self
2371            .project
2372            .read(cx)
2373            .entry_is_worktree_root(entry_to_move, cx)
2374        {
2375            self.move_worktree_root(entry_to_move, destination, cx)
2376        } else {
2377            self.move_worktree_entry(entry_to_move, destination, destination_is_file, cx)
2378        }
2379    }
2380
2381    fn move_worktree_root(
2382        &mut self,
2383        entry_to_move: ProjectEntryId,
2384        destination: ProjectEntryId,
2385        cx: &mut Context<Self>,
2386    ) {
2387        self.project.update(cx, |project, cx| {
2388            let Some(worktree_to_move) = project.worktree_for_entry(entry_to_move, cx) else {
2389                return;
2390            };
2391            let Some(destination_worktree) = project.worktree_for_entry(destination, cx) else {
2392                return;
2393            };
2394
2395            let worktree_id = worktree_to_move.read(cx).id();
2396            let destination_id = destination_worktree.read(cx).id();
2397
2398            project
2399                .move_worktree(worktree_id, destination_id, cx)
2400                .log_err();
2401        });
2402    }
2403
2404    fn move_worktree_entry(
2405        &mut self,
2406        entry_to_move: ProjectEntryId,
2407        destination: ProjectEntryId,
2408        destination_is_file: bool,
2409        cx: &mut Context<Self>,
2410    ) {
2411        if entry_to_move == destination {
2412            return;
2413        }
2414
2415        let destination_worktree = self.project.update(cx, |project, cx| {
2416            let entry_path = project.path_for_entry(entry_to_move, cx)?;
2417            let destination_entry_path = project.path_for_entry(destination, cx)?.path.clone();
2418
2419            let mut destination_path = destination_entry_path.as_ref();
2420            if destination_is_file {
2421                destination_path = destination_path.parent()?;
2422            }
2423
2424            let mut new_path = destination_path.to_path_buf();
2425            new_path.push(entry_path.path.file_name()?);
2426            if new_path != entry_path.path.as_ref() {
2427                let task = project.rename_entry(entry_to_move, new_path, cx);
2428                cx.foreground_executor().spawn(task).detach_and_log_err(cx);
2429            }
2430
2431            project.worktree_id_for_entry(destination, cx)
2432        });
2433
2434        if let Some(destination_worktree) = destination_worktree {
2435            self.expand_entry(destination_worktree, destination, cx);
2436        }
2437    }
2438
2439    fn index_for_selection(&self, selection: SelectedEntry) -> Option<(usize, usize, usize)> {
2440        let mut entry_index = 0;
2441        let mut visible_entries_index = 0;
2442        for (worktree_index, (worktree_id, worktree_entries, _)) in
2443            self.visible_entries.iter().enumerate()
2444        {
2445            if *worktree_id == selection.worktree_id {
2446                for entry in worktree_entries {
2447                    if entry.id == selection.entry_id {
2448                        return Some((worktree_index, entry_index, visible_entries_index));
2449                    } else {
2450                        visible_entries_index += 1;
2451                        entry_index += 1;
2452                    }
2453                }
2454                break;
2455            } else {
2456                visible_entries_index += worktree_entries.len();
2457            }
2458        }
2459        None
2460    }
2461
2462    fn disjoint_entries(&self, cx: &App) -> BTreeSet<SelectedEntry> {
2463        let marked_entries = self.effective_entries();
2464        let mut sanitized_entries = BTreeSet::new();
2465        if marked_entries.is_empty() {
2466            return sanitized_entries;
2467        }
2468
2469        let project = self.project.read(cx);
2470        let marked_entries_by_worktree: HashMap<WorktreeId, Vec<SelectedEntry>> = marked_entries
2471            .into_iter()
2472            .filter(|entry| !project.entry_is_worktree_root(entry.entry_id, cx))
2473            .fold(HashMap::default(), |mut map, entry| {
2474                map.entry(entry.worktree_id).or_default().push(entry);
2475                map
2476            });
2477
2478        for (worktree_id, marked_entries) in marked_entries_by_worktree {
2479            if let Some(worktree) = project.worktree_for_id(worktree_id, cx) {
2480                let worktree = worktree.read(cx);
2481                let marked_dir_paths = marked_entries
2482                    .iter()
2483                    .filter_map(|entry| {
2484                        worktree.entry_for_id(entry.entry_id).and_then(|entry| {
2485                            if entry.is_dir() {
2486                                Some(entry.path.as_ref())
2487                            } else {
2488                                None
2489                            }
2490                        })
2491                    })
2492                    .collect::<BTreeSet<_>>();
2493
2494                sanitized_entries.extend(marked_entries.into_iter().filter(|entry| {
2495                    let Some(entry_info) = worktree.entry_for_id(entry.entry_id) else {
2496                        return false;
2497                    };
2498                    let entry_path = entry_info.path.as_ref();
2499                    let inside_marked_dir = marked_dir_paths.iter().any(|&marked_dir_path| {
2500                        entry_path != marked_dir_path && entry_path.starts_with(marked_dir_path)
2501                    });
2502                    !inside_marked_dir
2503                }));
2504            }
2505        }
2506
2507        sanitized_entries
2508    }
2509
2510    fn effective_entries(&self) -> BTreeSet<SelectedEntry> {
2511        if let Some(selection) = self.selection {
2512            let selection = SelectedEntry {
2513                entry_id: self.resolve_entry(selection.entry_id),
2514                worktree_id: selection.worktree_id,
2515            };
2516
2517            // Default to using just the selected item when nothing is marked.
2518            if self.marked_entries.is_empty() {
2519                return BTreeSet::from([selection]);
2520            }
2521
2522            // Allow operating on the selected item even when something else is marked,
2523            // making it easier to perform one-off actions without clearing a mark.
2524            if self.marked_entries.len() == 1 && !self.marked_entries.contains(&selection) {
2525                return BTreeSet::from([selection]);
2526            }
2527        }
2528
2529        // Return only marked entries since we've already handled special cases where
2530        // only selection should take precedence. At this point, marked entries may or
2531        // may not include the current selection, which is intentional.
2532        self.marked_entries
2533            .iter()
2534            .map(|entry| SelectedEntry {
2535                entry_id: self.resolve_entry(entry.entry_id),
2536                worktree_id: entry.worktree_id,
2537            })
2538            .collect::<BTreeSet<_>>()
2539    }
2540
2541    /// Finds the currently selected subentry for a given leaf entry id. If a given entry
2542    /// has no ancestors, the project entry ID that's passed in is returned as-is.
2543    fn resolve_entry(&self, id: ProjectEntryId) -> ProjectEntryId {
2544        self.ancestors
2545            .get(&id)
2546            .and_then(|ancestors| {
2547                if ancestors.current_ancestor_depth == 0 {
2548                    return None;
2549                }
2550                ancestors.ancestors.get(ancestors.current_ancestor_depth)
2551            })
2552            .copied()
2553            .unwrap_or(id)
2554    }
2555
2556    pub fn selected_entry<'a>(&self, cx: &'a App) -> Option<(&'a Worktree, &'a project::Entry)> {
2557        let (worktree, entry) = self.selected_entry_handle(cx)?;
2558        Some((worktree.read(cx), entry))
2559    }
2560
2561    /// Compared to selected_entry, this function resolves to the currently
2562    /// selected subentry if dir auto-folding is enabled.
2563    fn selected_sub_entry<'a>(
2564        &self,
2565        cx: &'a App,
2566    ) -> Option<(Entity<Worktree>, &'a project::Entry)> {
2567        let (worktree, mut entry) = self.selected_entry_handle(cx)?;
2568
2569        let resolved_id = self.resolve_entry(entry.id);
2570        if resolved_id != entry.id {
2571            let worktree = worktree.read(cx);
2572            entry = worktree.entry_for_id(resolved_id)?;
2573        }
2574        Some((worktree, entry))
2575    }
2576    fn selected_entry_handle<'a>(
2577        &self,
2578        cx: &'a App,
2579    ) -> Option<(Entity<Worktree>, &'a project::Entry)> {
2580        let selection = self.selection?;
2581        let project = self.project.read(cx);
2582        let worktree = project.worktree_for_id(selection.worktree_id, cx)?;
2583        let entry = worktree.read(cx).entry_for_id(selection.entry_id)?;
2584        Some((worktree, entry))
2585    }
2586
2587    fn expand_to_selection(&mut self, cx: &mut Context<Self>) -> Option<()> {
2588        let (worktree, entry) = self.selected_entry(cx)?;
2589        let expanded_dir_ids = self.expanded_dir_ids.entry(worktree.id()).or_default();
2590
2591        for path in entry.path.ancestors() {
2592            let Some(entry) = worktree.entry_for_path(path) else {
2593                continue;
2594            };
2595            if entry.is_dir() {
2596                if let Err(idx) = expanded_dir_ids.binary_search(&entry.id) {
2597                    expanded_dir_ids.insert(idx, entry.id);
2598                }
2599            }
2600        }
2601
2602        Some(())
2603    }
2604
2605    fn update_visible_entries(
2606        &mut self,
2607        new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,
2608        cx: &mut Context<Self>,
2609    ) {
2610        let settings = ProjectPanelSettings::get_global(cx);
2611        let auto_collapse_dirs = settings.auto_fold_dirs;
2612        let hide_gitignore = settings.hide_gitignore;
2613        let project = self.project.read(cx);
2614        let repo_snapshots = project.git_store().read(cx).repo_snapshots(cx);
2615        self.last_worktree_root_id = project
2616            .visible_worktrees(cx)
2617            .next_back()
2618            .and_then(|worktree| worktree.read(cx).root_entry())
2619            .map(|entry| entry.id);
2620
2621        let old_ancestors = std::mem::take(&mut self.ancestors);
2622        self.visible_entries.clear();
2623        let mut max_width_item = None;
2624        for worktree in project.visible_worktrees(cx) {
2625            let worktree_snapshot = worktree.read(cx).snapshot();
2626            let worktree_id = worktree_snapshot.id();
2627
2628            let expanded_dir_ids = match self.expanded_dir_ids.entry(worktree_id) {
2629                hash_map::Entry::Occupied(e) => e.into_mut(),
2630                hash_map::Entry::Vacant(e) => {
2631                    // The first time a worktree's root entry becomes available,
2632                    // mark that root entry as expanded.
2633                    if let Some(entry) = worktree_snapshot.root_entry() {
2634                        e.insert(vec![entry.id]).as_slice()
2635                    } else {
2636                        &[]
2637                    }
2638                }
2639            };
2640
2641            let mut new_entry_parent_id = None;
2642            let mut new_entry_kind = EntryKind::Dir;
2643            if let Some(edit_state) = &self.edit_state {
2644                if edit_state.worktree_id == worktree_id && edit_state.is_new_entry() {
2645                    new_entry_parent_id = Some(edit_state.entry_id);
2646                    new_entry_kind = if edit_state.is_dir {
2647                        EntryKind::Dir
2648                    } else {
2649                        EntryKind::File
2650                    };
2651                }
2652            }
2653
2654            let mut visible_worktree_entries = Vec::new();
2655            let mut entry_iter =
2656                GitTraversal::new(&repo_snapshots, worktree_snapshot.entries(true, 0));
2657            let mut auto_folded_ancestors = vec![];
2658            while let Some(entry) = entry_iter.entry() {
2659                if auto_collapse_dirs && entry.kind.is_dir() {
2660                    auto_folded_ancestors.push(entry.id);
2661                    if !self.unfolded_dir_ids.contains(&entry.id) {
2662                        if let Some(root_path) = worktree_snapshot.root_entry() {
2663                            let mut child_entries = worktree_snapshot.child_entries(&entry.path);
2664                            if let Some(child) = child_entries.next() {
2665                                if entry.path != root_path.path
2666                                    && child_entries.next().is_none()
2667                                    && child.kind.is_dir()
2668                                {
2669                                    entry_iter.advance();
2670
2671                                    continue;
2672                                }
2673                            }
2674                        }
2675                    }
2676                    let depth = old_ancestors
2677                        .get(&entry.id)
2678                        .map(|ancestor| ancestor.current_ancestor_depth)
2679                        .unwrap_or_default()
2680                        .min(auto_folded_ancestors.len());
2681                    if let Some(edit_state) = &mut self.edit_state {
2682                        if edit_state.entry_id == entry.id {
2683                            edit_state.depth = depth;
2684                        }
2685                    }
2686                    let mut ancestors = std::mem::take(&mut auto_folded_ancestors);
2687                    if ancestors.len() > 1 {
2688                        ancestors.reverse();
2689                        self.ancestors.insert(
2690                            entry.id,
2691                            FoldedAncestors {
2692                                current_ancestor_depth: depth,
2693                                ancestors,
2694                            },
2695                        );
2696                    }
2697                }
2698                auto_folded_ancestors.clear();
2699                if !hide_gitignore || !entry.is_ignored {
2700                    visible_worktree_entries.push(entry.to_owned());
2701                }
2702                let precedes_new_entry = if let Some(new_entry_id) = new_entry_parent_id {
2703                    entry.id == new_entry_id || {
2704                        self.ancestors.get(&entry.id).map_or(false, |entries| {
2705                            entries
2706                                .ancestors
2707                                .iter()
2708                                .any(|entry_id| *entry_id == new_entry_id)
2709                        })
2710                    }
2711                } else {
2712                    false
2713                };
2714                if precedes_new_entry && (!hide_gitignore || !entry.is_ignored) {
2715                    visible_worktree_entries.push(GitEntry {
2716                        entry: Entry {
2717                            id: NEW_ENTRY_ID,
2718                            kind: new_entry_kind,
2719                            path: entry.path.join("\0").into(),
2720                            inode: 0,
2721                            mtime: entry.mtime,
2722                            size: entry.size,
2723                            is_ignored: entry.is_ignored,
2724                            is_external: false,
2725                            is_private: false,
2726                            is_always_included: entry.is_always_included,
2727                            canonical_path: entry.canonical_path.clone(),
2728                            char_bag: entry.char_bag,
2729                            is_fifo: entry.is_fifo,
2730                        },
2731                        git_summary: entry.git_summary,
2732                    });
2733                }
2734                let worktree_abs_path = worktree.read(cx).abs_path();
2735                let (depth, path) = if Some(entry.entry) == worktree.read(cx).root_entry() {
2736                    let Some(path_name) = worktree_abs_path
2737                        .file_name()
2738                        .with_context(|| {
2739                            format!("Worktree abs path has no file name, root entry: {entry:?}")
2740                        })
2741                        .log_err()
2742                    else {
2743                        continue;
2744                    };
2745                    let path = ArcCow::Borrowed(Path::new(path_name));
2746                    let depth = 0;
2747                    (depth, path)
2748                } else if entry.is_file() {
2749                    let Some(path_name) = entry
2750                        .path
2751                        .file_name()
2752                        .with_context(|| format!("Non-root entry has no file name: {entry:?}"))
2753                        .log_err()
2754                    else {
2755                        continue;
2756                    };
2757                    let path = ArcCow::Borrowed(Path::new(path_name));
2758                    let depth = entry.path.ancestors().count() - 1;
2759                    (depth, path)
2760                } else {
2761                    let path = self
2762                        .ancestors
2763                        .get(&entry.id)
2764                        .and_then(|ancestors| {
2765                            let outermost_ancestor = ancestors.ancestors.last()?;
2766                            let root_folded_entry = worktree
2767                                .read(cx)
2768                                .entry_for_id(*outermost_ancestor)?
2769                                .path
2770                                .as_ref();
2771                            entry
2772                                .path
2773                                .strip_prefix(root_folded_entry)
2774                                .ok()
2775                                .and_then(|suffix| {
2776                                    let full_path = Path::new(root_folded_entry.file_name()?);
2777                                    Some(ArcCow::Owned(Arc::<Path>::from(full_path.join(suffix))))
2778                                })
2779                        })
2780                        .or_else(|| entry.path.file_name().map(Path::new).map(ArcCow::Borrowed))
2781                        .unwrap_or_else(|| ArcCow::Owned(entry.path.clone()));
2782                    let depth = path.components().count();
2783                    (depth, path)
2784                };
2785                let width_estimate = item_width_estimate(
2786                    depth,
2787                    path.to_string_lossy().chars().count(),
2788                    entry.canonical_path.is_some(),
2789                );
2790
2791                match max_width_item.as_mut() {
2792                    Some((id, worktree_id, width)) => {
2793                        if *width < width_estimate {
2794                            *id = entry.id;
2795                            *worktree_id = worktree.read(cx).id();
2796                            *width = width_estimate;
2797                        }
2798                    }
2799                    None => {
2800                        max_width_item = Some((entry.id, worktree.read(cx).id(), width_estimate))
2801                    }
2802                }
2803
2804                if expanded_dir_ids.binary_search(&entry.id).is_err()
2805                    && entry_iter.advance_to_sibling()
2806                {
2807                    continue;
2808                }
2809                entry_iter.advance();
2810            }
2811
2812            project::sort_worktree_entries(&mut visible_worktree_entries);
2813
2814            self.visible_entries
2815                .push((worktree_id, visible_worktree_entries, OnceCell::new()));
2816        }
2817
2818        if let Some((project_entry_id, worktree_id, _)) = max_width_item {
2819            let mut visited_worktrees_length = 0;
2820            let index = self.visible_entries.iter().find_map(|(id, entries, _)| {
2821                if worktree_id == *id {
2822                    entries
2823                        .iter()
2824                        .position(|entry| entry.id == project_entry_id)
2825                } else {
2826                    visited_worktrees_length += entries.len();
2827                    None
2828                }
2829            });
2830            if let Some(index) = index {
2831                self.max_width_item_index = Some(visited_worktrees_length + index);
2832            }
2833        }
2834        if let Some((worktree_id, entry_id)) = new_selected_entry {
2835            self.selection = Some(SelectedEntry {
2836                worktree_id,
2837                entry_id,
2838            });
2839        }
2840    }
2841
2842    fn expand_entry(
2843        &mut self,
2844        worktree_id: WorktreeId,
2845        entry_id: ProjectEntryId,
2846        cx: &mut Context<Self>,
2847    ) {
2848        self.project.update(cx, |project, cx| {
2849            if let Some((worktree, expanded_dir_ids)) = project
2850                .worktree_for_id(worktree_id, cx)
2851                .zip(self.expanded_dir_ids.get_mut(&worktree_id))
2852            {
2853                project.expand_entry(worktree_id, entry_id, cx);
2854                let worktree = worktree.read(cx);
2855
2856                if let Some(mut entry) = worktree.entry_for_id(entry_id) {
2857                    loop {
2858                        if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) {
2859                            expanded_dir_ids.insert(ix, entry.id);
2860                        }
2861
2862                        if let Some(parent_entry) =
2863                            entry.path.parent().and_then(|p| worktree.entry_for_path(p))
2864                        {
2865                            entry = parent_entry;
2866                        } else {
2867                            break;
2868                        }
2869                    }
2870                }
2871            }
2872        });
2873    }
2874
2875    fn drop_external_files(
2876        &mut self,
2877        paths: &[PathBuf],
2878        entry_id: ProjectEntryId,
2879        window: &mut Window,
2880        cx: &mut Context<Self>,
2881    ) {
2882        let mut paths: Vec<Arc<Path>> = paths.iter().map(|path| Arc::from(path.clone())).collect();
2883
2884        let open_file_after_drop = paths.len() == 1 && paths[0].is_file();
2885
2886        let Some((target_directory, worktree)) = maybe!({
2887            let worktree = self.project.read(cx).worktree_for_entry(entry_id, cx)?;
2888            let entry = worktree.read(cx).entry_for_id(entry_id)?;
2889            let path = worktree.read(cx).absolutize(&entry.path).ok()?;
2890            let target_directory = if path.is_dir() {
2891                path
2892            } else {
2893                path.parent()?.to_path_buf()
2894            };
2895            Some((target_directory, worktree))
2896        }) else {
2897            return;
2898        };
2899
2900        let mut paths_to_replace = Vec::new();
2901        for path in &paths {
2902            if let Some(name) = path.file_name() {
2903                let mut target_path = target_directory.clone();
2904                target_path.push(name);
2905                if target_path.exists() {
2906                    paths_to_replace.push((name.to_string_lossy().to_string(), path.clone()));
2907                }
2908            }
2909        }
2910
2911        cx.spawn_in(window, async move |this, cx| {
2912            async move {
2913                for (filename, original_path) in &paths_to_replace {
2914                    let answer = cx.update(|window, cx| {
2915                        window
2916                            .prompt(
2917                                PromptLevel::Info,
2918                                format!("A file or folder with name {filename} already exists in the destination folder. Do you want to replace it?").as_str(),
2919                                None,
2920                                &["Replace", "Cancel"],
2921                                cx,
2922                            )
2923                    })?.await?;
2924
2925                    if answer == 1 {
2926                        if let Some(item_idx) = paths.iter().position(|p| p == original_path) {
2927                            paths.remove(item_idx);
2928                        }
2929                    }
2930                }
2931
2932                if paths.is_empty() {
2933                    return Ok(());
2934                }
2935
2936                let task = worktree.update( cx, |worktree, cx| {
2937                    worktree.copy_external_entries(target_directory, paths, true, cx)
2938                })?;
2939
2940                let opened_entries = task.await?;
2941                this.update(cx, |this, cx| {
2942                    if open_file_after_drop && !opened_entries.is_empty() {
2943                        this.open_entry(opened_entries[0], true, false, cx);
2944                    }
2945                })
2946            }
2947            .log_err().await
2948        })
2949        .detach();
2950    }
2951
2952    fn drag_onto(
2953        &mut self,
2954        selections: &DraggedSelection,
2955        target_entry_id: ProjectEntryId,
2956        is_file: bool,
2957        window: &mut Window,
2958        cx: &mut Context<Self>,
2959    ) {
2960        let should_copy = window.modifiers().alt;
2961        if should_copy {
2962            let _ = maybe!({
2963                let project = self.project.read(cx);
2964                let target_worktree = project.worktree_for_entry(target_entry_id, cx)?;
2965                let worktree_id = target_worktree.read(cx).id();
2966                let target_entry = target_worktree
2967                    .read(cx)
2968                    .entry_for_id(target_entry_id)?
2969                    .clone();
2970
2971                let mut copy_tasks = Vec::new();
2972                let mut disambiguation_range = None;
2973                for selection in selections.items() {
2974                    let (new_path, new_disambiguation_range) = self.create_paste_path(
2975                        selection,
2976                        (target_worktree.clone(), &target_entry),
2977                        cx,
2978                    )?;
2979
2980                    let task = self.project.update(cx, |project, cx| {
2981                        project.copy_entry(selection.entry_id, None, new_path, cx)
2982                    });
2983                    copy_tasks.push(task);
2984                    disambiguation_range = new_disambiguation_range.or(disambiguation_range);
2985                }
2986
2987                let item_count = copy_tasks.len();
2988
2989                cx.spawn_in(window, async move |project_panel, cx| {
2990                    let mut last_succeed = None;
2991                    for task in copy_tasks.into_iter() {
2992                        if let Some(Some(entry)) = task.await.log_err() {
2993                            last_succeed = Some(entry.id);
2994                        }
2995                    }
2996                    // update selection
2997                    if let Some(entry_id) = last_succeed {
2998                        project_panel
2999                            .update_in(cx, |project_panel, window, cx| {
3000                                project_panel.selection = Some(SelectedEntry {
3001                                    worktree_id,
3002                                    entry_id,
3003                                });
3004
3005                                // if only one entry was dragged and it was disambiguated, open the rename editor
3006                                if item_count == 1 && disambiguation_range.is_some() {
3007                                    project_panel.rename_impl(disambiguation_range, window, cx);
3008                                }
3009                            })
3010                            .ok();
3011                    }
3012                })
3013                .detach();
3014                Some(())
3015            });
3016        } else {
3017            for selection in selections.items() {
3018                self.move_entry(selection.entry_id, target_entry_id, is_file, cx);
3019            }
3020        }
3021    }
3022
3023    fn index_for_entry(
3024        &self,
3025        entry_id: ProjectEntryId,
3026        worktree_id: WorktreeId,
3027    ) -> Option<(usize, usize, usize)> {
3028        let mut worktree_ix = 0;
3029        let mut total_ix = 0;
3030        for (current_worktree_id, visible_worktree_entries, _) in &self.visible_entries {
3031            if worktree_id != *current_worktree_id {
3032                total_ix += visible_worktree_entries.len();
3033                worktree_ix += 1;
3034                continue;
3035            }
3036
3037            return visible_worktree_entries
3038                .iter()
3039                .enumerate()
3040                .find(|(_, entry)| entry.id == entry_id)
3041                .map(|(ix, _)| (worktree_ix, ix, total_ix + ix));
3042        }
3043        None
3044    }
3045
3046    fn entry_at_index(&self, index: usize) -> Option<(WorktreeId, GitEntryRef)> {
3047        let mut offset = 0;
3048        for (worktree_id, visible_worktree_entries, _) in &self.visible_entries {
3049            if visible_worktree_entries.len() > offset + index {
3050                return visible_worktree_entries
3051                    .get(index)
3052                    .map(|entry| (*worktree_id, entry.to_ref()));
3053            }
3054            offset += visible_worktree_entries.len();
3055        }
3056        None
3057    }
3058
3059    fn iter_visible_entries(
3060        &self,
3061        range: Range<usize>,
3062        window: &mut Window,
3063        cx: &mut Context<ProjectPanel>,
3064        mut callback: impl FnMut(&Entry, &HashSet<Arc<Path>>, &mut Window, &mut Context<ProjectPanel>),
3065    ) {
3066        let mut ix = 0;
3067        for (_, visible_worktree_entries, entries_paths) in &self.visible_entries {
3068            if ix >= range.end {
3069                return;
3070            }
3071
3072            if ix + visible_worktree_entries.len() <= range.start {
3073                ix += visible_worktree_entries.len();
3074                continue;
3075            }
3076
3077            let end_ix = range.end.min(ix + visible_worktree_entries.len());
3078            let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
3079            let entries = entries_paths.get_or_init(|| {
3080                visible_worktree_entries
3081                    .iter()
3082                    .map(|e| (e.path.clone()))
3083                    .collect()
3084            });
3085            for entry in visible_worktree_entries[entry_range].iter() {
3086                callback(&entry, entries, window, cx);
3087            }
3088            ix = end_ix;
3089        }
3090    }
3091
3092    fn for_each_visible_entry(
3093        &self,
3094        range: Range<usize>,
3095        window: &mut Window,
3096        cx: &mut Context<ProjectPanel>,
3097        mut callback: impl FnMut(ProjectEntryId, EntryDetails, &mut Window, &mut Context<ProjectPanel>),
3098    ) {
3099        let mut ix = 0;
3100        for (worktree_id, visible_worktree_entries, entries_paths) in &self.visible_entries {
3101            if ix >= range.end {
3102                return;
3103            }
3104
3105            if ix + visible_worktree_entries.len() <= range.start {
3106                ix += visible_worktree_entries.len();
3107                continue;
3108            }
3109
3110            let end_ix = range.end.min(ix + visible_worktree_entries.len());
3111            let (git_status_setting, show_file_icons, show_folder_icons) = {
3112                let settings = ProjectPanelSettings::get_global(cx);
3113                (
3114                    settings.git_status,
3115                    settings.file_icons,
3116                    settings.folder_icons,
3117                )
3118            };
3119            if let Some(worktree) = self.project.read(cx).worktree_for_id(*worktree_id, cx) {
3120                let snapshot = worktree.read(cx).snapshot();
3121                let root_name = OsStr::new(snapshot.root_name());
3122                let expanded_entry_ids = self
3123                    .expanded_dir_ids
3124                    .get(&snapshot.id())
3125                    .map(Vec::as_slice)
3126                    .unwrap_or(&[]);
3127
3128                let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
3129                let entries = entries_paths.get_or_init(|| {
3130                    visible_worktree_entries
3131                        .iter()
3132                        .map(|e| (e.path.clone()))
3133                        .collect()
3134                });
3135                for entry in visible_worktree_entries[entry_range].iter() {
3136                    let status = git_status_setting
3137                        .then_some(entry.git_summary)
3138                        .unwrap_or_default();
3139                    let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok();
3140                    let icon = match entry.kind {
3141                        EntryKind::File => {
3142                            if show_file_icons {
3143                                FileIcons::get_icon(&entry.path, cx)
3144                            } else {
3145                                None
3146                            }
3147                        }
3148                        _ => {
3149                            if show_folder_icons {
3150                                FileIcons::get_folder_icon(is_expanded, cx)
3151                            } else {
3152                                FileIcons::get_chevron_icon(is_expanded, cx)
3153                            }
3154                        }
3155                    };
3156
3157                    let (depth, difference) =
3158                        ProjectPanel::calculate_depth_and_difference(&entry, entries);
3159
3160                    let filename = match difference {
3161                        diff if diff > 1 => entry
3162                            .path
3163                            .iter()
3164                            .skip(entry.path.components().count() - diff)
3165                            .collect::<PathBuf>()
3166                            .to_str()
3167                            .unwrap_or_default()
3168                            .to_string(),
3169                        _ => entry
3170                            .path
3171                            .file_name()
3172                            .map(|name| name.to_string_lossy().into_owned())
3173                            .unwrap_or_else(|| root_name.to_string_lossy().to_string()),
3174                    };
3175                    let selection = SelectedEntry {
3176                        worktree_id: snapshot.id(),
3177                        entry_id: entry.id,
3178                    };
3179
3180                    let is_marked = self.marked_entries.contains(&selection);
3181
3182                    let diagnostic_severity = self
3183                        .diagnostics
3184                        .get(&(*worktree_id, entry.path.to_path_buf()))
3185                        .cloned();
3186
3187                    let filename_text_color =
3188                        entry_git_aware_label_color(status, entry.is_ignored, is_marked);
3189
3190                    let mut details = EntryDetails {
3191                        filename,
3192                        icon,
3193                        path: entry.path.clone(),
3194                        depth,
3195                        kind: entry.kind,
3196                        is_ignored: entry.is_ignored,
3197                        is_expanded,
3198                        is_selected: self.selection == Some(selection),
3199                        is_marked,
3200                        is_editing: false,
3201                        is_processing: false,
3202                        is_cut: self
3203                            .clipboard
3204                            .as_ref()
3205                            .map_or(false, |e| e.is_cut() && e.items().contains(&selection)),
3206                        filename_text_color,
3207                        diagnostic_severity,
3208                        git_status: status,
3209                        is_private: entry.is_private,
3210                        worktree_id: *worktree_id,
3211                        canonical_path: entry.canonical_path.clone(),
3212                    };
3213
3214                    if let Some(edit_state) = &self.edit_state {
3215                        let is_edited_entry = if edit_state.is_new_entry() {
3216                            entry.id == NEW_ENTRY_ID
3217                        } else {
3218                            entry.id == edit_state.entry_id
3219                                || self
3220                                    .ancestors
3221                                    .get(&entry.id)
3222                                    .is_some_and(|auto_folded_dirs| {
3223                                        auto_folded_dirs
3224                                            .ancestors
3225                                            .iter()
3226                                            .any(|entry_id| *entry_id == edit_state.entry_id)
3227                                    })
3228                        };
3229
3230                        if is_edited_entry {
3231                            if let Some(processing_filename) = &edit_state.processing_filename {
3232                                details.is_processing = true;
3233                                if let Some(ancestors) = edit_state
3234                                    .leaf_entry_id
3235                                    .and_then(|entry| self.ancestors.get(&entry))
3236                                {
3237                                    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;
3238                                    let all_components = ancestors.ancestors.len();
3239
3240                                    let prefix_components = all_components - position;
3241                                    let suffix_components = position.checked_sub(1);
3242                                    let mut previous_components =
3243                                        Path::new(&details.filename).components();
3244                                    let mut new_path = previous_components
3245                                        .by_ref()
3246                                        .take(prefix_components)
3247                                        .collect::<PathBuf>();
3248                                    if let Some(last_component) =
3249                                        Path::new(processing_filename).components().last()
3250                                    {
3251                                        new_path.push(last_component);
3252                                        previous_components.next();
3253                                    }
3254
3255                                    if let Some(_) = suffix_components {
3256                                        new_path.push(previous_components);
3257                                    }
3258                                    if let Some(str) = new_path.to_str() {
3259                                        details.filename.clear();
3260                                        details.filename.push_str(str);
3261                                    }
3262                                } else {
3263                                    details.filename.clear();
3264                                    details.filename.push_str(processing_filename);
3265                                }
3266                            } else {
3267                                if edit_state.is_new_entry() {
3268                                    details.filename.clear();
3269                                }
3270                                details.is_editing = true;
3271                            }
3272                        }
3273                    }
3274
3275                    callback(entry.id, details, window, cx);
3276                }
3277            }
3278            ix = end_ix;
3279        }
3280    }
3281
3282    fn find_entry_in_worktree(
3283        &self,
3284        worktree_id: WorktreeId,
3285        reverse_search: bool,
3286        only_visible_entries: bool,
3287        predicate: impl Fn(GitEntryRef, WorktreeId) -> bool,
3288        cx: &mut Context<Self>,
3289    ) -> Option<GitEntry> {
3290        if only_visible_entries {
3291            let entries = self
3292                .visible_entries
3293                .iter()
3294                .find_map(|(tree_id, entries, _)| {
3295                    if worktree_id == *tree_id {
3296                        Some(entries)
3297                    } else {
3298                        None
3299                    }
3300                })?
3301                .clone();
3302
3303            return utils::ReversibleIterable::new(entries.iter(), reverse_search)
3304                .find(|ele| predicate(ele.to_ref(), worktree_id))
3305                .cloned();
3306        }
3307
3308        let repo_snapshots = self
3309            .project
3310            .read(cx)
3311            .git_store()
3312            .read(cx)
3313            .repo_snapshots(cx);
3314        let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?;
3315        worktree.update(cx, |tree, _| {
3316            utils::ReversibleIterable::new(
3317                GitTraversal::new(&repo_snapshots, tree.entries(true, 0usize)),
3318                reverse_search,
3319            )
3320            .find_single_ended(|ele| predicate(*ele, worktree_id))
3321            .map(|ele| ele.to_owned())
3322        })
3323    }
3324
3325    fn find_entry(
3326        &self,
3327        start: Option<&SelectedEntry>,
3328        reverse_search: bool,
3329        predicate: impl Fn(GitEntryRef, WorktreeId) -> bool,
3330        cx: &mut Context<Self>,
3331    ) -> Option<SelectedEntry> {
3332        let mut worktree_ids: Vec<_> = self
3333            .visible_entries
3334            .iter()
3335            .map(|(worktree_id, _, _)| *worktree_id)
3336            .collect();
3337        let repo_snapshots = self
3338            .project
3339            .read(cx)
3340            .git_store()
3341            .read(cx)
3342            .repo_snapshots(cx);
3343
3344        let mut last_found: Option<SelectedEntry> = None;
3345
3346        if let Some(start) = start {
3347            let worktree = self
3348                .project
3349                .read(cx)
3350                .worktree_for_id(start.worktree_id, cx)?;
3351
3352            let search = worktree.update(cx, |tree, _| {
3353                let entry = tree.entry_for_id(start.entry_id)?;
3354                let root_entry = tree.root_entry()?;
3355                let tree_id = tree.id();
3356
3357                let mut first_iter = GitTraversal::new(
3358                    &repo_snapshots,
3359                    tree.traverse_from_path(true, true, true, entry.path.as_ref()),
3360                );
3361
3362                if reverse_search {
3363                    first_iter.next();
3364                }
3365
3366                let first = first_iter
3367                    .enumerate()
3368                    .take_until(|(count, entry)| entry.entry == root_entry && *count != 0usize)
3369                    .map(|(_, entry)| entry)
3370                    .find(|ele| predicate(*ele, tree_id))
3371                    .map(|ele| ele.to_owned());
3372
3373                let second_iter = GitTraversal::new(&repo_snapshots, tree.entries(true, 0usize));
3374
3375                let second = if reverse_search {
3376                    second_iter
3377                        .take_until(|ele| ele.id == start.entry_id)
3378                        .filter(|ele| predicate(*ele, tree_id))
3379                        .last()
3380                        .map(|ele| ele.to_owned())
3381                } else {
3382                    second_iter
3383                        .take_while(|ele| ele.id != start.entry_id)
3384                        .filter(|ele| predicate(*ele, tree_id))
3385                        .last()
3386                        .map(|ele| ele.to_owned())
3387                };
3388
3389                if reverse_search {
3390                    Some((second, first))
3391                } else {
3392                    Some((first, second))
3393                }
3394            });
3395
3396            if let Some((first, second)) = search {
3397                let first = first.map(|entry| SelectedEntry {
3398                    worktree_id: start.worktree_id,
3399                    entry_id: entry.id,
3400                });
3401
3402                let second = second.map(|entry| SelectedEntry {
3403                    worktree_id: start.worktree_id,
3404                    entry_id: entry.id,
3405                });
3406
3407                if first.is_some() {
3408                    return first;
3409                }
3410                last_found = second;
3411
3412                let idx = worktree_ids
3413                    .iter()
3414                    .enumerate()
3415                    .find(|(_, ele)| **ele == start.worktree_id)
3416                    .map(|(idx, _)| idx);
3417
3418                if let Some(idx) = idx {
3419                    worktree_ids.rotate_left(idx + 1usize);
3420                    worktree_ids.pop();
3421                }
3422            }
3423        }
3424
3425        for tree_id in worktree_ids.into_iter() {
3426            if let Some(found) =
3427                self.find_entry_in_worktree(tree_id, reverse_search, false, &predicate, cx)
3428            {
3429                return Some(SelectedEntry {
3430                    worktree_id: tree_id,
3431                    entry_id: found.id,
3432                });
3433            }
3434        }
3435
3436        last_found
3437    }
3438
3439    fn find_visible_entry(
3440        &self,
3441        start: Option<&SelectedEntry>,
3442        reverse_search: bool,
3443        predicate: impl Fn(GitEntryRef, WorktreeId) -> bool,
3444        cx: &mut Context<Self>,
3445    ) -> Option<SelectedEntry> {
3446        let mut worktree_ids: Vec<_> = self
3447            .visible_entries
3448            .iter()
3449            .map(|(worktree_id, _, _)| *worktree_id)
3450            .collect();
3451
3452        let mut last_found: Option<SelectedEntry> = None;
3453
3454        if let Some(start) = start {
3455            let entries = self
3456                .visible_entries
3457                .iter()
3458                .find(|(worktree_id, _, _)| *worktree_id == start.worktree_id)
3459                .map(|(_, entries, _)| entries)?;
3460
3461            let mut start_idx = entries
3462                .iter()
3463                .enumerate()
3464                .find(|(_, ele)| ele.id == start.entry_id)
3465                .map(|(idx, _)| idx)?;
3466
3467            if reverse_search {
3468                start_idx = start_idx.saturating_add(1usize);
3469            }
3470
3471            let (left, right) = entries.split_at_checked(start_idx)?;
3472
3473            let (first_iter, second_iter) = if reverse_search {
3474                (
3475                    utils::ReversibleIterable::new(left.iter(), reverse_search),
3476                    utils::ReversibleIterable::new(right.iter(), reverse_search),
3477                )
3478            } else {
3479                (
3480                    utils::ReversibleIterable::new(right.iter(), reverse_search),
3481                    utils::ReversibleIterable::new(left.iter(), reverse_search),
3482                )
3483            };
3484
3485            let first_search = first_iter.find(|ele| predicate(ele.to_ref(), start.worktree_id));
3486            let second_search = second_iter.find(|ele| predicate(ele.to_ref(), start.worktree_id));
3487
3488            if first_search.is_some() {
3489                return first_search.map(|entry| SelectedEntry {
3490                    worktree_id: start.worktree_id,
3491                    entry_id: entry.id,
3492                });
3493            }
3494
3495            last_found = second_search.map(|entry| SelectedEntry {
3496                worktree_id: start.worktree_id,
3497                entry_id: entry.id,
3498            });
3499
3500            let idx = worktree_ids
3501                .iter()
3502                .enumerate()
3503                .find(|(_, ele)| **ele == start.worktree_id)
3504                .map(|(idx, _)| idx);
3505
3506            if let Some(idx) = idx {
3507                worktree_ids.rotate_left(idx + 1usize);
3508                worktree_ids.pop();
3509            }
3510        }
3511
3512        for tree_id in worktree_ids.into_iter() {
3513            if let Some(found) =
3514                self.find_entry_in_worktree(tree_id, reverse_search, true, &predicate, cx)
3515            {
3516                return Some(SelectedEntry {
3517                    worktree_id: tree_id,
3518                    entry_id: found.id,
3519                });
3520            }
3521        }
3522
3523        last_found
3524    }
3525
3526    fn calculate_depth_and_difference(
3527        entry: &Entry,
3528        visible_worktree_entries: &HashSet<Arc<Path>>,
3529    ) -> (usize, usize) {
3530        let (depth, difference) = entry
3531            .path
3532            .ancestors()
3533            .skip(1) // Skip the entry itself
3534            .find_map(|ancestor| {
3535                if let Some(parent_entry) = visible_worktree_entries.get(ancestor) {
3536                    let entry_path_components_count = entry.path.components().count();
3537                    let parent_path_components_count = parent_entry.components().count();
3538                    let difference = entry_path_components_count - parent_path_components_count;
3539                    let depth = parent_entry
3540                        .ancestors()
3541                        .skip(1)
3542                        .filter(|ancestor| visible_worktree_entries.contains(*ancestor))
3543                        .count();
3544                    Some((depth + 1, difference))
3545                } else {
3546                    None
3547                }
3548            })
3549            .unwrap_or((0, 0));
3550
3551        (depth, difference)
3552    }
3553
3554    fn render_entry(
3555        &self,
3556        entry_id: ProjectEntryId,
3557        details: EntryDetails,
3558        window: &mut Window,
3559        cx: &mut Context<Self>,
3560    ) -> Stateful<Div> {
3561        const GROUP_NAME: &str = "project_entry";
3562
3563        let kind = details.kind;
3564        let settings = ProjectPanelSettings::get_global(cx);
3565        let show_editor = details.is_editing && !details.is_processing;
3566
3567        let selection = SelectedEntry {
3568            worktree_id: details.worktree_id,
3569            entry_id,
3570        };
3571
3572        let is_marked = self.marked_entries.contains(&selection);
3573        let is_active = self
3574            .selection
3575            .map_or(false, |selection| selection.entry_id == entry_id);
3576
3577        let file_name = details.filename.clone();
3578
3579        let mut icon = details.icon.clone();
3580        if settings.file_icons && show_editor && details.kind.is_file() {
3581            let filename = self.filename_editor.read(cx).text(cx);
3582            if filename.len() > 2 {
3583                icon = FileIcons::get_icon(Path::new(&filename), cx);
3584            }
3585        }
3586
3587        let filename_text_color = details.filename_text_color;
3588        let diagnostic_severity = details.diagnostic_severity;
3589        let item_colors = get_item_color(cx);
3590
3591        let canonical_path = details
3592            .canonical_path
3593            .as_ref()
3594            .map(|f| f.to_string_lossy().to_string());
3595        let path = details.path.clone();
3596
3597        let depth = details.depth;
3598        let worktree_id = details.worktree_id;
3599        let selections = Arc::new(self.marked_entries.clone());
3600        let is_local = self.project.read(cx).is_local();
3601
3602        let dragged_selection = DraggedSelection {
3603            active_selection: selection,
3604            marked_selections: selections,
3605        };
3606
3607        let bg_color = if is_marked {
3608            item_colors.marked
3609        } else {
3610            item_colors.default
3611        };
3612
3613        let bg_hover_color = if is_marked {
3614            item_colors.marked
3615        } else {
3616            item_colors.hover
3617        };
3618
3619        let border_color =
3620            if !self.mouse_down && is_active && self.focus_handle.contains_focused(window, cx) {
3621                item_colors.focused
3622            } else {
3623                bg_color
3624            };
3625
3626        let border_hover_color =
3627            if !self.mouse_down && is_active && self.focus_handle.contains_focused(window, cx) {
3628                item_colors.focused
3629            } else {
3630                bg_hover_color
3631            };
3632
3633        let folded_directory_drag_target = self.folded_directory_drag_target;
3634
3635        div()
3636            .id(entry_id.to_proto() as usize)
3637            .group(GROUP_NAME)
3638            .cursor_pointer()
3639            .rounded_none()
3640            .bg(bg_color)
3641            .border_1()
3642            .border_r_2()
3643            .border_color(border_color)
3644            .hover(|style| style.bg(bg_hover_color).border_color(border_hover_color))
3645            .when(is_local, |div| {
3646                div.on_drag_move::<ExternalPaths>(cx.listener(
3647                    move |this, event: &DragMoveEvent<ExternalPaths>, _, cx| {
3648                        if event.bounds.contains(&event.event.position) {
3649                            if this.last_external_paths_drag_over_entry == Some(entry_id) {
3650                                return;
3651                            }
3652                            this.last_external_paths_drag_over_entry = Some(entry_id);
3653                            this.marked_entries.clear();
3654
3655                            let Some((worktree, path, entry)) = maybe!({
3656                                let worktree = this
3657                                    .project
3658                                    .read(cx)
3659                                    .worktree_for_id(selection.worktree_id, cx)?;
3660                                let worktree = worktree.read(cx);
3661                                let abs_path = worktree.absolutize(&path).log_err()?;
3662                                let path = if abs_path.is_dir() {
3663                                    path.as_ref()
3664                                } else {
3665                                    path.parent()?
3666                                };
3667                                let entry = worktree.entry_for_path(path)?;
3668                                Some((worktree, path, entry))
3669                            }) else {
3670                                return;
3671                            };
3672
3673                            this.marked_entries.insert(SelectedEntry {
3674                                entry_id: entry.id,
3675                                worktree_id: worktree.id(),
3676                            });
3677
3678                            for entry in worktree.child_entries(path) {
3679                                this.marked_entries.insert(SelectedEntry {
3680                                    entry_id: entry.id,
3681                                    worktree_id: worktree.id(),
3682                                });
3683                            }
3684
3685                            cx.notify();
3686                        }
3687                    },
3688                ))
3689                .on_drop(cx.listener(
3690                    move |this, external_paths: &ExternalPaths, window, cx| {
3691                        this.hover_scroll_task.take();
3692                        this.last_external_paths_drag_over_entry = None;
3693                        this.marked_entries.clear();
3694                        this.drop_external_files(external_paths.paths(), entry_id, window, cx);
3695                        cx.stop_propagation();
3696                    },
3697                ))
3698            })
3699            .on_drag_move::<DraggedSelection>(cx.listener(
3700                move |this, event: &DragMoveEvent<DraggedSelection>, window, cx| {
3701                    if event.bounds.contains(&event.event.position) {
3702                        if this.last_selection_drag_over_entry == Some(entry_id) {
3703                            return;
3704                        }
3705                        this.last_selection_drag_over_entry = Some(entry_id);
3706                        this.hover_expand_task.take();
3707
3708                        if !kind.is_dir()
3709                            || this
3710                                .expanded_dir_ids
3711                                .get(&details.worktree_id)
3712                                .map_or(false, |ids| ids.binary_search(&entry_id).is_ok())
3713                        {
3714                            return;
3715                        }
3716
3717                        let bounds = event.bounds;
3718                        this.hover_expand_task =
3719                            Some(cx.spawn_in(window, async move |this, cx| {
3720                                cx.background_executor()
3721                                    .timer(Duration::from_millis(500))
3722                                    .await;
3723                                this.update_in(cx, |this, window, cx| {
3724                                    this.hover_expand_task.take();
3725                                    if this.last_selection_drag_over_entry == Some(entry_id)
3726                                        && bounds.contains(&window.mouse_position())
3727                                    {
3728                                        this.expand_entry(worktree_id, entry_id, cx);
3729                                        this.update_visible_entries(
3730                                            Some((worktree_id, entry_id)),
3731                                            cx,
3732                                        );
3733                                        cx.notify();
3734                                    }
3735                                })
3736                                .ok();
3737                            }));
3738                    }
3739                },
3740            ))
3741            .on_drag(
3742                dragged_selection,
3743                move |selection, click_offset, _window, cx| {
3744                    cx.new(|_| DraggedProjectEntryView {
3745                        details: details.clone(),
3746                        click_offset,
3747                        selection: selection.active_selection,
3748                        selections: selection.marked_selections.clone(),
3749                    })
3750                },
3751            )
3752            .drag_over::<DraggedSelection>(move |style, _, _, _| {
3753                if  folded_directory_drag_target.is_some() {
3754                    return style;
3755                }
3756                style.bg(item_colors.drag_over)
3757            })
3758            .on_drop(
3759                cx.listener(move |this, selections: &DraggedSelection, window, cx| {
3760                    this.hover_scroll_task.take();
3761                    this.hover_expand_task.take();
3762                    if  folded_directory_drag_target.is_some() {
3763                        return;
3764                    }
3765                    this.drag_onto(selections, entry_id, kind.is_file(), window, cx);
3766                }),
3767            )
3768            .on_mouse_down(
3769                MouseButton::Left,
3770                cx.listener(move |this, _, _, cx| {
3771                    this.mouse_down = true;
3772                    cx.propagate();
3773                }),
3774            )
3775            .on_click(
3776                cx.listener(move |this, event: &gpui::ClickEvent, window, cx| {
3777                    if event.down.button == MouseButton::Right
3778                        || event.down.first_mouse
3779                        || show_editor
3780                    {
3781                        return;
3782                    }
3783                    if event.down.button == MouseButton::Left {
3784                        this.mouse_down = false;
3785                    }
3786                    cx.stop_propagation();
3787
3788                    if let Some(selection) = this.selection.filter(|_| event.modifiers().shift) {
3789                        let current_selection = this.index_for_selection(selection);
3790                        let clicked_entry = SelectedEntry {
3791                            entry_id,
3792                            worktree_id,
3793                        };
3794                        let target_selection = this.index_for_selection(clicked_entry);
3795                        if let Some(((_, _, source_index), (_, _, target_index))) =
3796                            current_selection.zip(target_selection)
3797                        {
3798                            let range_start = source_index.min(target_index);
3799                            let range_end = source_index.max(target_index) + 1; // Make the range inclusive.
3800                            let mut new_selections = BTreeSet::new();
3801                            this.for_each_visible_entry(
3802                                range_start..range_end,
3803                                window,
3804                                cx,
3805                                |entry_id, details, _, _| {
3806                                    new_selections.insert(SelectedEntry {
3807                                        entry_id,
3808                                        worktree_id: details.worktree_id,
3809                                    });
3810                                },
3811                            );
3812
3813                            this.marked_entries = this
3814                                .marked_entries
3815                                .union(&new_selections)
3816                                .cloned()
3817                                .collect();
3818
3819                            this.selection = Some(clicked_entry);
3820                            this.marked_entries.insert(clicked_entry);
3821                        }
3822                    } else if event.modifiers().secondary() {
3823                        if event.down.click_count > 1 {
3824                            this.split_entry(entry_id, cx);
3825                        } else {
3826                            this.selection = Some(selection);
3827                            if !this.marked_entries.insert(selection) {
3828                                this.marked_entries.remove(&selection);
3829                            }
3830                        }
3831                    } else if kind.is_dir() {
3832                        this.marked_entries.clear();
3833                        if event.modifiers().alt {
3834                            this.toggle_expand_all(entry_id, window, cx);
3835                        } else {
3836                            this.toggle_expanded(entry_id, window, cx);
3837                        }
3838                    } else {
3839                        let preview_tabs_enabled = PreviewTabsSettings::get_global(cx).enabled;
3840                        let click_count = event.up.click_count;
3841                        let focus_opened_item = !preview_tabs_enabled || click_count > 1;
3842                        let allow_preview = preview_tabs_enabled && click_count == 1;
3843                        this.open_entry(entry_id, focus_opened_item, allow_preview, cx);
3844                    }
3845                }),
3846            )
3847            .child(
3848                ListItem::new(entry_id.to_proto() as usize)
3849                    .indent_level(depth)
3850                    .indent_step_size(px(settings.indent_size))
3851                    .spacing(match settings.entry_spacing {
3852                        project_panel_settings::EntrySpacing::Comfortable => ListItemSpacing::Dense,
3853                        project_panel_settings::EntrySpacing::Standard => {
3854                            ListItemSpacing::ExtraDense
3855                        }
3856                    })
3857                    .selectable(false)
3858                    .when_some(canonical_path, |this, path| {
3859                        this.end_slot::<AnyElement>(
3860                            div()
3861                                .id("symlink_icon")
3862                                .pr_3()
3863                                .tooltip(move |window, cx| {
3864                                    Tooltip::with_meta(
3865                                        path.to_string(),
3866                                        None,
3867                                        "Symbolic Link",
3868                                        window,
3869                                        cx,
3870                                    )
3871                                })
3872                                .child(
3873                                    Icon::new(IconName::ArrowUpRight)
3874                                        .size(IconSize::Indicator)
3875                                        .color(filename_text_color),
3876                                )
3877                                .into_any_element(),
3878                        )
3879                    })
3880                    .child(if let Some(icon) = &icon {
3881                        if let Some((_, decoration_color)) =
3882                            entry_diagnostic_aware_icon_decoration_and_color(diagnostic_severity)
3883                        {
3884                            let is_warning = diagnostic_severity
3885                                .map(|severity| matches!(severity, DiagnosticSeverity::WARNING))
3886                                .unwrap_or(false);
3887                            div().child(
3888                                DecoratedIcon::new(
3889                                    Icon::from_path(icon.clone()).color(Color::Muted),
3890                                    Some(
3891                                        IconDecoration::new(
3892                                            if kind.is_file() {
3893                                                if is_warning {
3894                                                    IconDecorationKind::Triangle
3895                                                } else {
3896                                                    IconDecorationKind::X
3897                                                }
3898                                            } else {
3899                                                IconDecorationKind::Dot
3900                                            },
3901                                            bg_color,
3902                                            cx,
3903                                        )
3904                                        .group_name(Some(GROUP_NAME.into()))
3905                                        .knockout_hover_color(bg_hover_color)
3906                                        .color(decoration_color.color(cx))
3907                                        .position(Point {
3908                                            x: px(-2.),
3909                                            y: px(-2.),
3910                                        }),
3911                                    ),
3912                                )
3913                                .into_any_element(),
3914                            )
3915                        } else {
3916                            h_flex().child(Icon::from_path(icon.to_string()).color(Color::Muted))
3917                        }
3918                    } else {
3919                        if let Some((icon_name, color)) =
3920                            entry_diagnostic_aware_icon_name_and_color(diagnostic_severity)
3921                        {
3922                            h_flex()
3923                                .size(IconSize::default().rems())
3924                                .child(Icon::new(icon_name).color(color).size(IconSize::Small))
3925                        } else {
3926                            h_flex()
3927                                .size(IconSize::default().rems())
3928                                .invisible()
3929                                .flex_none()
3930                        }
3931                    })
3932                    .child(
3933                        if let (Some(editor), true) = (Some(&self.filename_editor), show_editor) {
3934                            h_flex().h_6().w_full().child(editor.clone())
3935                        } else {
3936                            h_flex().h_6().map(|mut this| {
3937                                if let Some(folded_ancestors) = self.ancestors.get(&entry_id) {
3938                                    let components = Path::new(&file_name)
3939                                        .components()
3940                                        .map(|comp| {
3941                                            let comp_str =
3942                                                comp.as_os_str().to_string_lossy().into_owned();
3943                                            comp_str
3944                                        })
3945                                        .collect::<Vec<_>>();
3946
3947                                    let components_len = components.len();
3948                                    let active_index = components_len
3949                                        - 1
3950                                        - folded_ancestors.current_ancestor_depth;
3951                                        const DELIMITER: SharedString =
3952                                        SharedString::new_static(std::path::MAIN_SEPARATOR_STR);
3953                                    for (index, component) in components.into_iter().enumerate() {
3954                                        if index != 0 {
3955                                                let delimiter_target_index = index - 1;
3956                                                let target_entry_id = folded_ancestors.ancestors.get(components_len - 1 - delimiter_target_index).cloned();
3957                                                this = this.child(
3958                                                    div()
3959                                                    .on_drop(cx.listener(move |this, selections: &DraggedSelection, window, cx| {
3960                                                        this.hover_scroll_task.take();
3961                                                        this.folded_directory_drag_target = None;
3962                                                        if let Some(target_entry_id) = target_entry_id {
3963                                                            this.drag_onto(selections, target_entry_id, kind.is_file(), window, cx);
3964                                                        }
3965                                                    }))
3966                                                    .on_drag_move(cx.listener(
3967                                                        move |this, event: &DragMoveEvent<DraggedSelection>, _, _| {
3968                                                            if event.bounds.contains(&event.event.position) {
3969                                                                this.folded_directory_drag_target = Some(
3970                                                                    FoldedDirectoryDragTarget {
3971                                                                        entry_id,
3972                                                                        index: delimiter_target_index,
3973                                                                        is_delimiter_target: true,
3974                                                                    }
3975                                                                );
3976                                                            } else {
3977                                                                let is_current_target = this.folded_directory_drag_target
3978                                                                    .map_or(false, |target|
3979                                                                        target.entry_id == entry_id &&
3980                                                                        target.index == delimiter_target_index &&
3981                                                                        target.is_delimiter_target
3982                                                                    );
3983                                                                if is_current_target {
3984                                                                    this.folded_directory_drag_target = None;
3985                                                                }
3986                                                            }
3987
3988                                                        },
3989                                                    ))
3990                                                    .child(
3991                                                        Label::new(DELIMITER.clone())
3992                                                            .single_line()
3993                                                            .color(filename_text_color)
3994                                                    )
3995                                                );
3996                                        }
3997                                        let id = SharedString::from(format!(
3998                                            "project_panel_path_component_{}_{index}",
3999                                            entry_id.to_usize()
4000                                        ));
4001                                        let label = div()
4002                                            .id(id)
4003                                            .on_click(cx.listener(move |this, _, _, cx| {
4004                                                if index != active_index {
4005                                                    if let Some(folds) =
4006                                                        this.ancestors.get_mut(&entry_id)
4007                                                    {
4008                                                        folds.current_ancestor_depth =
4009                                                            components_len - 1 - index;
4010                                                        cx.notify();
4011                                                    }
4012                                                }
4013                                            }))
4014                                            .when(index != components_len - 1, |div|{
4015                                                let target_entry_id = folded_ancestors.ancestors.get(components_len - 1 - index).cloned();
4016                                                div
4017                                                .on_drag_move(cx.listener(
4018                                                    move |this, event: &DragMoveEvent<DraggedSelection>, _, _| {
4019                                                    if event.bounds.contains(&event.event.position) {
4020                                                            this.folded_directory_drag_target = Some(
4021                                                                FoldedDirectoryDragTarget {
4022                                                                    entry_id,
4023                                                                    index,
4024                                                                    is_delimiter_target: false,
4025                                                                }
4026                                                            );
4027                                                        } else {
4028                                                            let is_current_target = this.folded_directory_drag_target
4029                                                                .as_ref()
4030                                                                .map_or(false, |target|
4031                                                                    target.entry_id == entry_id &&
4032                                                                    target.index == index &&
4033                                                                    !target.is_delimiter_target
4034                                                                );
4035                                                            if is_current_target {
4036                                                                this.folded_directory_drag_target = None;
4037                                                            }
4038                                                        }
4039                                                    },
4040                                                ))
4041                                                .on_drop(cx.listener(move |this, selections: &DraggedSelection, window,cx| {
4042                                                    this.hover_scroll_task.take();
4043                                                    this.folded_directory_drag_target = None;
4044                                                    if let Some(target_entry_id) = target_entry_id {
4045                                                        this.drag_onto(selections, target_entry_id, kind.is_file(), window, cx);
4046                                                    }
4047                                                }))
4048                                                .when(folded_directory_drag_target.map_or(false, |target|
4049                                                    target.entry_id == entry_id &&
4050                                                    target.index == index
4051                                                ), |this| {
4052                                                    this.bg(item_colors.drag_over)
4053                                                })
4054                                            })
4055                                            .child(
4056                                                Label::new(component)
4057                                                    .single_line()
4058                                                    .color(filename_text_color)
4059                                                    .when(
4060                                                        index == active_index
4061                                                            && (is_active || is_marked),
4062                                                        |this| this.underline(),
4063                                                    ),
4064                                            );
4065
4066                                        this = this.child(label);
4067                                    }
4068
4069                                    this
4070                                } else {
4071                                    this.child(
4072                                        Label::new(file_name)
4073                                            .single_line()
4074                                            .color(filename_text_color),
4075                                    )
4076                                }
4077                            })
4078                        }
4079                        .ml_1(),
4080                    )
4081                    .on_secondary_mouse_down(cx.listener(
4082                        move |this, event: &MouseDownEvent, window, cx| {
4083                            // Stop propagation to prevent the catch-all context menu for the project
4084                            // panel from being deployed.
4085                            cx.stop_propagation();
4086                            // Some context menu actions apply to all marked entries. If the user
4087                            // right-clicks on an entry that is not marked, they may not realize the
4088                            // action applies to multiple entries. To avoid inadvertent changes, all
4089                            // entries are unmarked.
4090                            if !this.marked_entries.contains(&selection) {
4091                                this.marked_entries.clear();
4092                            }
4093                            this.deploy_context_menu(event.position, entry_id, window, cx);
4094                        },
4095                    ))
4096                    .overflow_x(),
4097            )
4098    }
4099
4100    fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
4101        if !Self::should_show_scrollbar(cx)
4102            || !(self.show_scrollbar || self.vertical_scrollbar_state.is_dragging())
4103        {
4104            return None;
4105        }
4106        Some(
4107            div()
4108                .occlude()
4109                .id("project-panel-vertical-scroll")
4110                .on_mouse_move(cx.listener(|_, _, _, cx| {
4111                    cx.notify();
4112                    cx.stop_propagation()
4113                }))
4114                .on_hover(|_, _, cx| {
4115                    cx.stop_propagation();
4116                })
4117                .on_any_mouse_down(|_, _, cx| {
4118                    cx.stop_propagation();
4119                })
4120                .on_mouse_up(
4121                    MouseButton::Left,
4122                    cx.listener(|this, _, window, cx| {
4123                        if !this.vertical_scrollbar_state.is_dragging()
4124                            && !this.focus_handle.contains_focused(window, cx)
4125                        {
4126                            this.hide_scrollbar(window, cx);
4127                            cx.notify();
4128                        }
4129
4130                        cx.stop_propagation();
4131                    }),
4132                )
4133                .on_scroll_wheel(cx.listener(|_, _, _, cx| {
4134                    cx.notify();
4135                }))
4136                .h_full()
4137                .absolute()
4138                .right_1()
4139                .top_1()
4140                .bottom_1()
4141                .w(px(12.))
4142                .cursor_default()
4143                .children(Scrollbar::vertical(
4144                    // percentage as f32..end_offset as f32,
4145                    self.vertical_scrollbar_state.clone(),
4146                )),
4147        )
4148    }
4149
4150    fn render_horizontal_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
4151        if !Self::should_show_scrollbar(cx)
4152            || !(self.show_scrollbar || self.horizontal_scrollbar_state.is_dragging())
4153        {
4154            return None;
4155        }
4156
4157        let scroll_handle = self.scroll_handle.0.borrow();
4158        let longest_item_width = scroll_handle
4159            .last_item_size
4160            .filter(|size| size.contents.width > size.item.width)?
4161            .contents
4162            .width
4163            .0 as f64;
4164        if longest_item_width < scroll_handle.base_handle.bounds().size.width.0 as f64 {
4165            return None;
4166        }
4167
4168        Some(
4169            div()
4170                .occlude()
4171                .id("project-panel-horizontal-scroll")
4172                .on_mouse_move(cx.listener(|_, _, _, cx| {
4173                    cx.notify();
4174                    cx.stop_propagation()
4175                }))
4176                .on_hover(|_, _, cx| {
4177                    cx.stop_propagation();
4178                })
4179                .on_any_mouse_down(|_, _, cx| {
4180                    cx.stop_propagation();
4181                })
4182                .on_mouse_up(
4183                    MouseButton::Left,
4184                    cx.listener(|this, _, window, cx| {
4185                        if !this.horizontal_scrollbar_state.is_dragging()
4186                            && !this.focus_handle.contains_focused(window, cx)
4187                        {
4188                            this.hide_scrollbar(window, cx);
4189                            cx.notify();
4190                        }
4191
4192                        cx.stop_propagation();
4193                    }),
4194                )
4195                .on_scroll_wheel(cx.listener(|_, _, _, cx| {
4196                    cx.notify();
4197                }))
4198                .w_full()
4199                .absolute()
4200                .right_1()
4201                .left_1()
4202                .bottom_1()
4203                .h(px(12.))
4204                .cursor_default()
4205                .when(self.width.is_some(), |this| {
4206                    this.children(Scrollbar::horizontal(
4207                        self.horizontal_scrollbar_state.clone(),
4208                    ))
4209                }),
4210        )
4211    }
4212
4213    fn dispatch_context(&self, window: &Window, cx: &Context<Self>) -> KeyContext {
4214        let mut dispatch_context = KeyContext::new_with_defaults();
4215        dispatch_context.add("ProjectPanel");
4216        dispatch_context.add("menu");
4217
4218        let identifier = if self.filename_editor.focus_handle(cx).is_focused(window) {
4219            "editing"
4220        } else {
4221            "not_editing"
4222        };
4223
4224        dispatch_context.add(identifier);
4225        dispatch_context
4226    }
4227
4228    fn should_show_scrollbar(cx: &App) -> bool {
4229        let show = ProjectPanelSettings::get_global(cx)
4230            .scrollbar
4231            .show
4232            .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show);
4233        match show {
4234            ShowScrollbar::Auto => true,
4235            ShowScrollbar::System => true,
4236            ShowScrollbar::Always => true,
4237            ShowScrollbar::Never => false,
4238        }
4239    }
4240
4241    fn should_autohide_scrollbar(cx: &App) -> bool {
4242        let show = ProjectPanelSettings::get_global(cx)
4243            .scrollbar
4244            .show
4245            .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show);
4246        match show {
4247            ShowScrollbar::Auto => true,
4248            ShowScrollbar::System => cx
4249                .try_global::<ScrollbarAutoHide>()
4250                .map_or_else(|| cx.should_auto_hide_scrollbars(), |autohide| autohide.0),
4251            ShowScrollbar::Always => false,
4252            ShowScrollbar::Never => true,
4253        }
4254    }
4255
4256    fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4257        const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
4258        if !Self::should_autohide_scrollbar(cx) {
4259            return;
4260        }
4261        self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |panel, cx| {
4262            cx.background_executor()
4263                .timer(SCROLLBAR_SHOW_INTERVAL)
4264                .await;
4265            panel
4266                .update(cx, |panel, cx| {
4267                    panel.show_scrollbar = false;
4268                    cx.notify();
4269                })
4270                .log_err();
4271        }))
4272    }
4273
4274    fn reveal_entry(
4275        &mut self,
4276        project: Entity<Project>,
4277        entry_id: ProjectEntryId,
4278        skip_ignored: bool,
4279        cx: &mut Context<Self>,
4280    ) {
4281        if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) {
4282            let worktree = worktree.read(cx);
4283            if skip_ignored
4284                && worktree
4285                    .entry_for_id(entry_id)
4286                    .map_or(true, |entry| entry.is_ignored && !entry.is_always_included)
4287            {
4288                return;
4289            }
4290
4291            let worktree_id = worktree.id();
4292            self.expand_entry(worktree_id, entry_id, cx);
4293            self.update_visible_entries(Some((worktree_id, entry_id)), cx);
4294            self.marked_entries.clear();
4295            self.marked_entries.insert(SelectedEntry {
4296                worktree_id,
4297                entry_id,
4298            });
4299            self.autoscroll(cx);
4300            cx.notify();
4301        }
4302    }
4303
4304    fn find_active_indent_guide(
4305        &self,
4306        indent_guides: &[IndentGuideLayout],
4307        cx: &App,
4308    ) -> Option<usize> {
4309        let (worktree, entry) = self.selected_entry(cx)?;
4310
4311        // Find the parent entry of the indent guide, this will either be the
4312        // expanded folder we have selected, or the parent of the currently
4313        // selected file/collapsed directory
4314        let mut entry = entry;
4315        loop {
4316            let is_expanded_dir = entry.is_dir()
4317                && self
4318                    .expanded_dir_ids
4319                    .get(&worktree.id())
4320                    .map(|ids| ids.binary_search(&entry.id).is_ok())
4321                    .unwrap_or(false);
4322            if is_expanded_dir {
4323                break;
4324            }
4325            entry = worktree.entry_for_path(&entry.path.parent()?)?;
4326        }
4327
4328        let (active_indent_range, depth) = {
4329            let (worktree_ix, child_offset, ix) = self.index_for_entry(entry.id, worktree.id())?;
4330            let child_paths = &self.visible_entries[worktree_ix].1;
4331            let mut child_count = 0;
4332            let depth = entry.path.ancestors().count();
4333            while let Some(entry) = child_paths.get(child_offset + child_count + 1) {
4334                if entry.path.ancestors().count() <= depth {
4335                    break;
4336                }
4337                child_count += 1;
4338            }
4339
4340            let start = ix + 1;
4341            let end = start + child_count;
4342
4343            let (_, entries, paths) = &self.visible_entries[worktree_ix];
4344            let visible_worktree_entries =
4345                paths.get_or_init(|| entries.iter().map(|e| (e.path.clone())).collect());
4346
4347            // Calculate the actual depth of the entry, taking into account that directories can be auto-folded.
4348            let (depth, _) = Self::calculate_depth_and_difference(entry, visible_worktree_entries);
4349            (start..end, depth)
4350        };
4351
4352        let candidates = indent_guides
4353            .iter()
4354            .enumerate()
4355            .filter(|(_, indent_guide)| indent_guide.offset.x == depth);
4356
4357        for (i, indent) in candidates {
4358            // Find matches that are either an exact match, partially on screen, or inside the enclosing indent
4359            if active_indent_range.start <= indent.offset.y + indent.length
4360                && indent.offset.y <= active_indent_range.end
4361            {
4362                return Some(i);
4363            }
4364        }
4365        None
4366    }
4367}
4368
4369fn item_width_estimate(depth: usize, item_text_chars: usize, is_symlink: bool) -> usize {
4370    const ICON_SIZE_FACTOR: usize = 2;
4371    let mut item_width = depth * ICON_SIZE_FACTOR + item_text_chars;
4372    if is_symlink {
4373        item_width += ICON_SIZE_FACTOR;
4374    }
4375    item_width
4376}
4377
4378impl Render for ProjectPanel {
4379    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
4380        let has_worktree = !self.visible_entries.is_empty();
4381        let project = self.project.read(cx);
4382        let indent_size = ProjectPanelSettings::get_global(cx).indent_size;
4383        let show_indent_guides =
4384            ProjectPanelSettings::get_global(cx).indent_guides.show == ShowIndentGuides::Always;
4385        let is_local = project.is_local();
4386
4387        if has_worktree {
4388            let item_count = self
4389                .visible_entries
4390                .iter()
4391                .map(|(_, worktree_entries, _)| worktree_entries.len())
4392                .sum();
4393
4394            fn handle_drag_move_scroll<T: 'static>(
4395                this: &mut ProjectPanel,
4396                e: &DragMoveEvent<T>,
4397                window: &mut Window,
4398                cx: &mut Context<ProjectPanel>,
4399            ) {
4400                if !e.bounds.contains(&e.event.position) {
4401                    return;
4402                }
4403                this.hover_scroll_task.take();
4404                let panel_height = e.bounds.size.height;
4405                if panel_height <= px(0.) {
4406                    return;
4407                }
4408
4409                let event_offset = e.event.position.y - e.bounds.origin.y;
4410                // How far along in the project panel is our cursor? (0. is the top of a list, 1. is the bottom)
4411                let hovered_region_offset = event_offset / panel_height;
4412
4413                // We want the scrolling to be a bit faster when the cursor is closer to the edge of a list.
4414                // These pixels offsets were picked arbitrarily.
4415                let vertical_scroll_offset = if hovered_region_offset <= 0.05 {
4416                    8.
4417                } else if hovered_region_offset <= 0.15 {
4418                    5.
4419                } else if hovered_region_offset >= 0.95 {
4420                    -8.
4421                } else if hovered_region_offset >= 0.85 {
4422                    -5.
4423                } else {
4424                    return;
4425                };
4426                let adjustment = point(px(0.), px(vertical_scroll_offset));
4427                this.hover_scroll_task = Some(cx.spawn_in(window, async move |this, cx| loop {
4428                    let should_stop_scrolling = this
4429                        .update(cx, |this, cx| {
4430                            this.hover_scroll_task.as_ref()?;
4431                            let handle = this.scroll_handle.0.borrow_mut();
4432                            let offset = handle.base_handle.offset();
4433
4434                            handle.base_handle.set_offset(offset + adjustment);
4435                            cx.notify();
4436                            Some(())
4437                        })
4438                        .ok()
4439                        .flatten()
4440                        .is_some();
4441                    if should_stop_scrolling {
4442                        return;
4443                    }
4444                    cx.background_executor()
4445                        .timer(Duration::from_millis(16))
4446                        .await;
4447                }));
4448            }
4449            h_flex()
4450                .id("project-panel")
4451                .group("project-panel")
4452                .on_drag_move(cx.listener(handle_drag_move_scroll::<ExternalPaths>))
4453                .on_drag_move(cx.listener(handle_drag_move_scroll::<DraggedSelection>))
4454                .size_full()
4455                .relative()
4456                .on_hover(cx.listener(|this, hovered, window, cx| {
4457                    if *hovered {
4458                        this.show_scrollbar = true;
4459                        this.hide_scrollbar_task.take();
4460                        cx.notify();
4461                    } else if !this.focus_handle.contains_focused(window, cx) {
4462                        this.hide_scrollbar(window, cx);
4463                    }
4464                }))
4465                .on_click(cx.listener(|this, _event, _, cx| {
4466                    cx.stop_propagation();
4467                    this.selection = None;
4468                    this.marked_entries.clear();
4469                }))
4470                .key_context(self.dispatch_context(window, cx))
4471                .on_action(cx.listener(Self::select_next))
4472                .on_action(cx.listener(Self::select_previous))
4473                .on_action(cx.listener(Self::select_first))
4474                .on_action(cx.listener(Self::select_last))
4475                .on_action(cx.listener(Self::select_parent))
4476                .on_action(cx.listener(Self::select_next_git_entry))
4477                .on_action(cx.listener(Self::select_prev_git_entry))
4478                .on_action(cx.listener(Self::select_next_diagnostic))
4479                .on_action(cx.listener(Self::select_prev_diagnostic))
4480                .on_action(cx.listener(Self::select_next_directory))
4481                .on_action(cx.listener(Self::select_prev_directory))
4482                .on_action(cx.listener(Self::expand_selected_entry))
4483                .on_action(cx.listener(Self::collapse_selected_entry))
4484                .on_action(cx.listener(Self::collapse_all_entries))
4485                .on_action(cx.listener(Self::open))
4486                .on_action(cx.listener(Self::open_permanent))
4487                .on_action(cx.listener(Self::confirm))
4488                .on_action(cx.listener(Self::cancel))
4489                .on_action(cx.listener(Self::copy_path))
4490                .on_action(cx.listener(Self::copy_relative_path))
4491                .on_action(cx.listener(Self::new_search_in_directory))
4492                .on_action(cx.listener(Self::unfold_directory))
4493                .on_action(cx.listener(Self::fold_directory))
4494                .on_action(cx.listener(Self::remove_from_project))
4495                .when(!project.is_read_only(cx), |el| {
4496                    el.on_action(cx.listener(Self::new_file))
4497                        .on_action(cx.listener(Self::new_directory))
4498                        .on_action(cx.listener(Self::rename))
4499                        .on_action(cx.listener(Self::delete))
4500                        .on_action(cx.listener(Self::trash))
4501                        .on_action(cx.listener(Self::cut))
4502                        .on_action(cx.listener(Self::copy))
4503                        .on_action(cx.listener(Self::paste))
4504                        .on_action(cx.listener(Self::duplicate))
4505                        .on_click(cx.listener(|this, event: &gpui::ClickEvent, window, cx| {
4506                            if event.up.click_count > 1 {
4507                                if let Some(entry_id) = this.last_worktree_root_id {
4508                                    let project = this.project.read(cx);
4509
4510                                    let worktree_id = if let Some(worktree) =
4511                                        project.worktree_for_entry(entry_id, cx)
4512                                    {
4513                                        worktree.read(cx).id()
4514                                    } else {
4515                                        return;
4516                                    };
4517
4518                                    this.selection = Some(SelectedEntry {
4519                                        worktree_id,
4520                                        entry_id,
4521                                    });
4522
4523                                    this.new_file(&NewFile, window, cx);
4524                                }
4525                            }
4526                        }))
4527                })
4528                .when(project.is_local(), |el| {
4529                    el.on_action(cx.listener(Self::reveal_in_finder))
4530                        .on_action(cx.listener(Self::open_system))
4531                        .on_action(cx.listener(Self::open_in_terminal))
4532                })
4533                .when(project.is_via_ssh(), |el| {
4534                    el.on_action(cx.listener(Self::open_in_terminal))
4535                })
4536                .on_mouse_down(
4537                    MouseButton::Right,
4538                    cx.listener(move |this, event: &MouseDownEvent, window, cx| {
4539                        // When deploying the context menu anywhere below the last project entry,
4540                        // act as if the user clicked the root of the last worktree.
4541                        if let Some(entry_id) = this.last_worktree_root_id {
4542                            this.deploy_context_menu(event.position, entry_id, window, cx);
4543                        }
4544                    }),
4545                )
4546                .track_focus(&self.focus_handle(cx))
4547                .child(
4548                    uniform_list(cx.entity().clone(), "entries", item_count, {
4549                        |this, range, window, cx| {
4550                            let mut items = Vec::with_capacity(range.end - range.start);
4551                            this.for_each_visible_entry(
4552                                range,
4553                                window,
4554                                cx,
4555                                |id, details, window, cx| {
4556                                    items.push(this.render_entry(id, details, window, cx));
4557                                },
4558                            );
4559                            items
4560                        }
4561                    })
4562                    .when(show_indent_guides, |list| {
4563                        list.with_decoration(
4564                            ui::indent_guides(
4565                                cx.entity().clone(),
4566                                px(indent_size),
4567                                IndentGuideColors::panel(cx),
4568                                |this, range, window, cx| {
4569                                    let mut items =
4570                                        SmallVec::with_capacity(range.end - range.start);
4571                                    this.iter_visible_entries(
4572                                        range,
4573                                        window,
4574                                        cx,
4575                                        |entry, entries, _, _| {
4576                                            let (depth, _) = Self::calculate_depth_and_difference(
4577                                                entry, entries,
4578                                            );
4579                                            items.push(depth);
4580                                        },
4581                                    );
4582                                    items
4583                                },
4584                            )
4585                            .on_click(cx.listener(
4586                                |this, active_indent_guide: &IndentGuideLayout, window, cx| {
4587                                    if window.modifiers().secondary() {
4588                                        let ix = active_indent_guide.offset.y;
4589                                        let Some((target_entry, worktree)) = maybe!({
4590                                            let (worktree_id, entry) = this.entry_at_index(ix)?;
4591                                            let worktree = this
4592                                                .project
4593                                                .read(cx)
4594                                                .worktree_for_id(worktree_id, cx)?;
4595                                            let target_entry = worktree
4596                                                .read(cx)
4597                                                .entry_for_path(&entry.path.parent()?)?;
4598                                            Some((target_entry, worktree))
4599                                        }) else {
4600                                            return;
4601                                        };
4602
4603                                        this.collapse_entry(target_entry.clone(), worktree, cx);
4604                                    }
4605                                },
4606                            ))
4607                            .with_render_fn(
4608                                cx.entity().clone(),
4609                                move |this, params, _, cx| {
4610                                    const LEFT_OFFSET: Pixels = px(14.);
4611                                    const PADDING_Y: Pixels = px(4.);
4612                                    const HITBOX_OVERDRAW: Pixels = px(3.);
4613
4614                                    let active_indent_guide_index =
4615                                        this.find_active_indent_guide(&params.indent_guides, cx);
4616
4617                                    let indent_size = params.indent_size;
4618                                    let item_height = params.item_height;
4619
4620                                    params
4621                                        .indent_guides
4622                                        .into_iter()
4623                                        .enumerate()
4624                                        .map(|(idx, layout)| {
4625                                            let offset = if layout.continues_offscreen {
4626                                                px(0.)
4627                                            } else {
4628                                                PADDING_Y
4629                                            };
4630                                            let bounds = Bounds::new(
4631                                                point(
4632                                                    layout.offset.x * indent_size + LEFT_OFFSET,
4633                                                    layout.offset.y * item_height + offset,
4634                                                ),
4635                                                size(
4636                                                    px(1.),
4637                                                    layout.length * item_height - offset * 2.,
4638                                                ),
4639                                            );
4640                                            ui::RenderedIndentGuide {
4641                                                bounds,
4642                                                layout,
4643                                                is_active: Some(idx) == active_indent_guide_index,
4644                                                hitbox: Some(Bounds::new(
4645                                                    point(
4646                                                        bounds.origin.x - HITBOX_OVERDRAW,
4647                                                        bounds.origin.y,
4648                                                    ),
4649                                                    size(
4650                                                        bounds.size.width + HITBOX_OVERDRAW * 2.,
4651                                                        bounds.size.height,
4652                                                    ),
4653                                                )),
4654                                            }
4655                                        })
4656                                        .collect()
4657                                },
4658                            ),
4659                        )
4660                    })
4661                    .size_full()
4662                    .with_sizing_behavior(ListSizingBehavior::Infer)
4663                    .with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained)
4664                    .with_width_from_item(self.max_width_item_index)
4665                    .track_scroll(self.scroll_handle.clone()),
4666                )
4667                .children(self.render_vertical_scrollbar(cx))
4668                .when_some(self.render_horizontal_scrollbar(cx), |this, scrollbar| {
4669                    this.pb_4().child(scrollbar)
4670                })
4671                .children(self.context_menu.as_ref().map(|(menu, position, _)| {
4672                    deferred(
4673                        anchored()
4674                            .position(*position)
4675                            .anchor(gpui::Corner::TopLeft)
4676                            .child(menu.clone()),
4677                    )
4678                    .with_priority(1)
4679                }))
4680        } else {
4681            v_flex()
4682                .id("empty-project_panel")
4683                .size_full()
4684                .p_4()
4685                .track_focus(&self.focus_handle(cx))
4686                .child(
4687                    Button::new("open_project", "Open a project")
4688                        .full_width()
4689                        .key_binding(KeyBinding::for_action(&workspace::Open, window, cx))
4690                        .on_click(cx.listener(|this, _, window, cx| {
4691                            this.workspace
4692                                .update(cx, |_, cx| {
4693                                    window.dispatch_action(Box::new(workspace::Open), cx)
4694                                })
4695                                .log_err();
4696                        })),
4697                )
4698                .when(is_local, |div| {
4699                    div.drag_over::<ExternalPaths>(|style, _, _, cx| {
4700                        style.bg(cx.theme().colors().drop_target_background)
4701                    })
4702                    .on_drop(cx.listener(
4703                        move |this, external_paths: &ExternalPaths, window, cx| {
4704                            this.last_external_paths_drag_over_entry = None;
4705                            this.marked_entries.clear();
4706                            this.hover_scroll_task.take();
4707                            if let Some(task) = this
4708                                .workspace
4709                                .update(cx, |workspace, cx| {
4710                                    workspace.open_workspace_for_paths(
4711                                        true,
4712                                        external_paths.paths().to_owned(),
4713                                        window,
4714                                        cx,
4715                                    )
4716                                })
4717                                .log_err()
4718                            {
4719                                task.detach_and_log_err(cx);
4720                            }
4721                            cx.stop_propagation();
4722                        },
4723                    ))
4724                })
4725        }
4726    }
4727}
4728
4729impl Render for DraggedProjectEntryView {
4730    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
4731        let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
4732        h_flex()
4733            .font(ui_font)
4734            .pl(self.click_offset.x + px(12.))
4735            .pt(self.click_offset.y + px(12.))
4736            .child(
4737                div()
4738                    .flex()
4739                    .gap_1()
4740                    .items_center()
4741                    .py_1()
4742                    .px_2()
4743                    .rounded_lg()
4744                    .bg(cx.theme().colors().background)
4745                    .map(|this| {
4746                        if self.selections.len() > 1 && self.selections.contains(&self.selection) {
4747                            this.child(Label::new(format!("{} entries", self.selections.len())))
4748                        } else {
4749                            this.child(if let Some(icon) = &self.details.icon {
4750                                div().child(Icon::from_path(icon.clone()))
4751                            } else {
4752                                div()
4753                            })
4754                            .child(Label::new(self.details.filename.clone()))
4755                        }
4756                    }),
4757            )
4758    }
4759}
4760
4761impl EventEmitter<Event> for ProjectPanel {}
4762
4763impl EventEmitter<PanelEvent> for ProjectPanel {}
4764
4765impl Panel for ProjectPanel {
4766    fn position(&self, _: &Window, cx: &App) -> DockPosition {
4767        match ProjectPanelSettings::get_global(cx).dock {
4768            ProjectPanelDockPosition::Left => DockPosition::Left,
4769            ProjectPanelDockPosition::Right => DockPosition::Right,
4770        }
4771    }
4772
4773    fn position_is_valid(&self, position: DockPosition) -> bool {
4774        matches!(position, DockPosition::Left | DockPosition::Right)
4775    }
4776
4777    fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
4778        settings::update_settings_file::<ProjectPanelSettings>(
4779            self.fs.clone(),
4780            cx,
4781            move |settings, _| {
4782                let dock = match position {
4783                    DockPosition::Left | DockPosition::Bottom => ProjectPanelDockPosition::Left,
4784                    DockPosition::Right => ProjectPanelDockPosition::Right,
4785                };
4786                settings.dock = Some(dock);
4787            },
4788        );
4789    }
4790
4791    fn size(&self, _: &Window, cx: &App) -> Pixels {
4792        self.width
4793            .unwrap_or_else(|| ProjectPanelSettings::get_global(cx).default_width)
4794    }
4795
4796    fn set_size(&mut self, size: Option<Pixels>, _: &mut Window, cx: &mut Context<Self>) {
4797        self.width = size;
4798        self.serialize(cx);
4799        cx.notify();
4800    }
4801
4802    fn icon(&self, _: &Window, cx: &App) -> Option<IconName> {
4803        ProjectPanelSettings::get_global(cx)
4804            .button
4805            .then_some(IconName::FileTree)
4806    }
4807
4808    fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
4809        Some("Project Panel")
4810    }
4811
4812    fn toggle_action(&self) -> Box<dyn Action> {
4813        Box::new(ToggleFocus)
4814    }
4815
4816    fn persistent_name() -> &'static str {
4817        "Project Panel"
4818    }
4819
4820    fn starts_open(&self, _: &Window, cx: &App) -> bool {
4821        let project = &self.project.read(cx);
4822        project.visible_worktrees(cx).any(|tree| {
4823            tree.read(cx)
4824                .root_entry()
4825                .map_or(false, |entry| entry.is_dir())
4826        })
4827    }
4828
4829    fn activation_priority(&self) -> u32 {
4830        0
4831    }
4832}
4833
4834impl Focusable for ProjectPanel {
4835    fn focus_handle(&self, _cx: &App) -> FocusHandle {
4836        self.focus_handle.clone()
4837    }
4838}
4839
4840impl ClipboardEntry {
4841    fn is_cut(&self) -> bool {
4842        matches!(self, Self::Cut { .. })
4843    }
4844
4845    fn items(&self) -> &BTreeSet<SelectedEntry> {
4846        match self {
4847            ClipboardEntry::Copied(entries) | ClipboardEntry::Cut(entries) => entries,
4848        }
4849    }
4850}
4851
4852#[cfg(test)]
4853mod project_panel_tests;