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