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