project_panel.rs

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