project_panel.rs

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