project_panel.rs

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