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
2858                            .get(&entry.id)
2859                            .map_or(false, |entries| entries.ancestors.contains(&new_entry_id))
2860                    }
2861                } else {
2862                    false
2863                };
2864                if precedes_new_entry && (!hide_gitignore || !entry.is_ignored) {
2865                    visible_worktree_entries.push(GitEntry {
2866                        entry: Entry {
2867                            id: NEW_ENTRY_ID,
2868                            kind: new_entry_kind,
2869                            path: entry.path.join("\0").into(),
2870                            inode: 0,
2871                            mtime: entry.mtime,
2872                            size: entry.size,
2873                            is_ignored: entry.is_ignored,
2874                            is_external: false,
2875                            is_private: false,
2876                            is_always_included: entry.is_always_included,
2877                            canonical_path: entry.canonical_path.clone(),
2878                            char_bag: entry.char_bag,
2879                            is_fifo: entry.is_fifo,
2880                        },
2881                        git_summary: entry.git_summary,
2882                    });
2883                }
2884                let worktree_abs_path = worktree.read(cx).abs_path();
2885                let (depth, path) = if Some(entry.entry) == worktree.read(cx).root_entry() {
2886                    let Some(path_name) = worktree_abs_path.file_name() else {
2887                        continue;
2888                    };
2889                    let path = ArcCow::Borrowed(Path::new(path_name));
2890                    let depth = 0;
2891                    (depth, path)
2892                } else if entry.is_file() {
2893                    let Some(path_name) = entry
2894                        .path
2895                        .file_name()
2896                        .with_context(|| format!("Non-root entry has no file name: {entry:?}"))
2897                        .log_err()
2898                    else {
2899                        continue;
2900                    };
2901                    let path = ArcCow::Borrowed(Path::new(path_name));
2902                    let depth = entry.path.ancestors().count() - 1;
2903                    (depth, path)
2904                } else {
2905                    let path = self
2906                        .ancestors
2907                        .get(&entry.id)
2908                        .and_then(|ancestors| {
2909                            let outermost_ancestor = ancestors.ancestors.last()?;
2910                            let root_folded_entry = worktree
2911                                .read(cx)
2912                                .entry_for_id(*outermost_ancestor)?
2913                                .path
2914                                .as_ref();
2915                            entry
2916                                .path
2917                                .strip_prefix(root_folded_entry)
2918                                .ok()
2919                                .and_then(|suffix| {
2920                                    let full_path = Path::new(root_folded_entry.file_name()?);
2921                                    Some(ArcCow::Owned(Arc::<Path>::from(full_path.join(suffix))))
2922                                })
2923                        })
2924                        .or_else(|| entry.path.file_name().map(Path::new).map(ArcCow::Borrowed))
2925                        .unwrap_or_else(|| ArcCow::Owned(entry.path.clone()));
2926                    let depth = path.components().count();
2927                    (depth, path)
2928                };
2929                let width_estimate = item_width_estimate(
2930                    depth,
2931                    path.to_string_lossy().chars().count(),
2932                    entry.canonical_path.is_some(),
2933                );
2934
2935                match max_width_item.as_mut() {
2936                    Some((id, worktree_id, width)) => {
2937                        if *width < width_estimate {
2938                            *id = entry.id;
2939                            *worktree_id = worktree.read(cx).id();
2940                            *width = width_estimate;
2941                        }
2942                    }
2943                    None => {
2944                        max_width_item = Some((entry.id, worktree.read(cx).id(), width_estimate))
2945                    }
2946                }
2947
2948                if expanded_dir_ids.binary_search(&entry.id).is_err()
2949                    && entry_iter.advance_to_sibling()
2950                {
2951                    continue;
2952                }
2953                entry_iter.advance();
2954            }
2955
2956            project::sort_worktree_entries(&mut visible_worktree_entries);
2957
2958            self.visible_entries
2959                .push((worktree_id, visible_worktree_entries, OnceCell::new()));
2960        }
2961
2962        if let Some((project_entry_id, worktree_id, _)) = max_width_item {
2963            let mut visited_worktrees_length = 0;
2964            let index = self.visible_entries.iter().find_map(|(id, entries, _)| {
2965                if worktree_id == *id {
2966                    entries
2967                        .iter()
2968                        .position(|entry| entry.id == project_entry_id)
2969                } else {
2970                    visited_worktrees_length += entries.len();
2971                    None
2972                }
2973            });
2974            if let Some(index) = index {
2975                self.max_width_item_index = Some(visited_worktrees_length + index);
2976            }
2977        }
2978        if let Some((worktree_id, entry_id)) = new_selected_entry {
2979            self.selection = Some(SelectedEntry {
2980                worktree_id,
2981                entry_id,
2982            });
2983        }
2984    }
2985
2986    fn expand_entry(
2987        &mut self,
2988        worktree_id: WorktreeId,
2989        entry_id: ProjectEntryId,
2990        cx: &mut Context<Self>,
2991    ) {
2992        self.project.update(cx, |project, cx| {
2993            if let Some((worktree, expanded_dir_ids)) = project
2994                .worktree_for_id(worktree_id, cx)
2995                .zip(self.expanded_dir_ids.get_mut(&worktree_id))
2996            {
2997                project.expand_entry(worktree_id, entry_id, cx);
2998                let worktree = worktree.read(cx);
2999
3000                if let Some(mut entry) = worktree.entry_for_id(entry_id) {
3001                    loop {
3002                        if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) {
3003                            expanded_dir_ids.insert(ix, entry.id);
3004                        }
3005
3006                        if let Some(parent_entry) =
3007                            entry.path.parent().and_then(|p| worktree.entry_for_path(p))
3008                        {
3009                            entry = parent_entry;
3010                        } else {
3011                            break;
3012                        }
3013                    }
3014                }
3015            }
3016        });
3017    }
3018
3019    fn drop_external_files(
3020        &mut self,
3021        paths: &[PathBuf],
3022        entry_id: ProjectEntryId,
3023        window: &mut Window,
3024        cx: &mut Context<Self>,
3025    ) {
3026        let mut paths: Vec<Arc<Path>> = paths.iter().map(|path| Arc::from(path.clone())).collect();
3027
3028        let open_file_after_drop = paths.len() == 1 && paths[0].is_file();
3029
3030        let Some((target_directory, worktree, fs)) = maybe!({
3031            let project = self.project.read(cx);
3032            let fs = project.fs().clone();
3033            let worktree = project.worktree_for_entry(entry_id, cx)?;
3034            let entry = worktree.read(cx).entry_for_id(entry_id)?;
3035            let path = entry.path.clone();
3036            let target_directory = if entry.is_dir() {
3037                path.to_path_buf()
3038            } else {
3039                path.parent()?.to_path_buf()
3040            };
3041            Some((target_directory, worktree, fs))
3042        }) else {
3043            return;
3044        };
3045
3046        let mut paths_to_replace = Vec::new();
3047        for path in &paths {
3048            if let Some(name) = path.file_name() {
3049                let mut target_path = target_directory.clone();
3050                target_path.push(name);
3051                if target_path.exists() {
3052                    paths_to_replace.push((name.to_string_lossy().to_string(), path.clone()));
3053                }
3054            }
3055        }
3056
3057        cx.spawn_in(window, async move |this, cx| {
3058            async move {
3059                for (filename, original_path) in &paths_to_replace {
3060                    let answer = cx.update(|window, cx| {
3061                        window
3062                            .prompt(
3063                                PromptLevel::Info,
3064                                format!("A file or folder with name {filename} already exists in the destination folder. Do you want to replace it?").as_str(),
3065                                None,
3066                                &["Replace", "Cancel"],
3067                                cx,
3068                            )
3069                    })?.await?;
3070
3071                    if answer == 1 {
3072                        if let Some(item_idx) = paths.iter().position(|p| p == original_path) {
3073                            paths.remove(item_idx);
3074                        }
3075                    }
3076                }
3077
3078                if paths.is_empty() {
3079                    return Ok(());
3080                }
3081
3082                let task = worktree.update( cx, |worktree, cx| {
3083                    worktree.copy_external_entries(target_directory.into(), paths, fs, cx)
3084                })?;
3085
3086                let opened_entries = task.await.with_context(|| "failed to copy external paths")?;
3087                this.update(cx, |this, cx| {
3088                    if open_file_after_drop && !opened_entries.is_empty() {
3089                        this.open_entry(opened_entries[0], true, false, cx);
3090                    }
3091                })
3092            }
3093            .log_err().await
3094        })
3095        .detach();
3096    }
3097
3098    fn drag_onto(
3099        &mut self,
3100        selections: &DraggedSelection,
3101        target_entry_id: ProjectEntryId,
3102        is_file: bool,
3103        window: &mut Window,
3104        cx: &mut Context<Self>,
3105    ) {
3106        let should_copy = window.modifiers().alt;
3107        if should_copy {
3108            let _ = maybe!({
3109                let project = self.project.read(cx);
3110                let target_worktree = project.worktree_for_entry(target_entry_id, cx)?;
3111                let worktree_id = target_worktree.read(cx).id();
3112                let target_entry = target_worktree
3113                    .read(cx)
3114                    .entry_for_id(target_entry_id)?
3115                    .clone();
3116
3117                let mut copy_tasks = Vec::new();
3118                let mut disambiguation_range = None;
3119                for selection in selections.items() {
3120                    let (new_path, new_disambiguation_range) = self.create_paste_path(
3121                        selection,
3122                        (target_worktree.clone(), &target_entry),
3123                        cx,
3124                    )?;
3125
3126                    let task = self.project.update(cx, |project, cx| {
3127                        project.copy_entry(selection.entry_id, None, new_path, cx)
3128                    });
3129                    copy_tasks.push(task);
3130                    disambiguation_range = new_disambiguation_range.or(disambiguation_range);
3131                }
3132
3133                let item_count = copy_tasks.len();
3134
3135                cx.spawn_in(window, async move |project_panel, cx| {
3136                    let mut last_succeed = None;
3137                    for task in copy_tasks.into_iter() {
3138                        if let Some(Some(entry)) = task.await.log_err() {
3139                            last_succeed = Some(entry.id);
3140                        }
3141                    }
3142                    // update selection
3143                    if let Some(entry_id) = last_succeed {
3144                        project_panel
3145                            .update_in(cx, |project_panel, window, cx| {
3146                                project_panel.selection = Some(SelectedEntry {
3147                                    worktree_id,
3148                                    entry_id,
3149                                });
3150
3151                                // if only one entry was dragged and it was disambiguated, open the rename editor
3152                                if item_count == 1 && disambiguation_range.is_some() {
3153                                    project_panel.rename_impl(disambiguation_range, window, cx);
3154                                }
3155                            })
3156                            .ok();
3157                    }
3158                })
3159                .detach();
3160                Some(())
3161            });
3162        } else {
3163            for selection in selections.items() {
3164                self.move_entry(selection.entry_id, target_entry_id, is_file, cx);
3165            }
3166        }
3167    }
3168
3169    fn index_for_entry(
3170        &self,
3171        entry_id: ProjectEntryId,
3172        worktree_id: WorktreeId,
3173    ) -> Option<(usize, usize, usize)> {
3174        let mut worktree_ix = 0;
3175        let mut total_ix = 0;
3176        for (current_worktree_id, visible_worktree_entries, _) in &self.visible_entries {
3177            if worktree_id != *current_worktree_id {
3178                total_ix += visible_worktree_entries.len();
3179                worktree_ix += 1;
3180                continue;
3181            }
3182
3183            return visible_worktree_entries
3184                .iter()
3185                .enumerate()
3186                .find(|(_, entry)| entry.id == entry_id)
3187                .map(|(ix, _)| (worktree_ix, ix, total_ix + ix));
3188        }
3189        None
3190    }
3191
3192    fn entry_at_index(&self, index: usize) -> Option<(WorktreeId, GitEntryRef)> {
3193        let mut offset = 0;
3194        for (worktree_id, visible_worktree_entries, _) in &self.visible_entries {
3195            if visible_worktree_entries.len() > offset + index {
3196                return visible_worktree_entries
3197                    .get(index)
3198                    .map(|entry| (*worktree_id, entry.to_ref()));
3199            }
3200            offset += visible_worktree_entries.len();
3201        }
3202        None
3203    }
3204
3205    fn iter_visible_entries(
3206        &self,
3207        range: Range<usize>,
3208        window: &mut Window,
3209        cx: &mut Context<ProjectPanel>,
3210        mut callback: impl FnMut(&Entry, &HashSet<Arc<Path>>, &mut Window, &mut Context<ProjectPanel>),
3211    ) {
3212        let mut ix = 0;
3213        for (_, visible_worktree_entries, entries_paths) in &self.visible_entries {
3214            if ix >= range.end {
3215                return;
3216            }
3217
3218            if ix + visible_worktree_entries.len() <= range.start {
3219                ix += visible_worktree_entries.len();
3220                continue;
3221            }
3222
3223            let end_ix = range.end.min(ix + visible_worktree_entries.len());
3224            let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
3225            let entries = entries_paths.get_or_init(|| {
3226                visible_worktree_entries
3227                    .iter()
3228                    .map(|e| (e.path.clone()))
3229                    .collect()
3230            });
3231            for entry in visible_worktree_entries[entry_range].iter() {
3232                callback(&entry, entries, window, cx);
3233            }
3234            ix = end_ix;
3235        }
3236    }
3237
3238    fn for_each_visible_entry(
3239        &self,
3240        range: Range<usize>,
3241        window: &mut Window,
3242        cx: &mut Context<ProjectPanel>,
3243        mut callback: impl FnMut(ProjectEntryId, EntryDetails, &mut Window, &mut Context<ProjectPanel>),
3244    ) {
3245        let mut ix = 0;
3246        for (worktree_id, visible_worktree_entries, entries_paths) in &self.visible_entries {
3247            if ix >= range.end {
3248                return;
3249            }
3250
3251            if ix + visible_worktree_entries.len() <= range.start {
3252                ix += visible_worktree_entries.len();
3253                continue;
3254            }
3255
3256            let end_ix = range.end.min(ix + visible_worktree_entries.len());
3257            let (git_status_setting, show_file_icons, show_folder_icons) = {
3258                let settings = ProjectPanelSettings::get_global(cx);
3259                (
3260                    settings.git_status,
3261                    settings.file_icons,
3262                    settings.folder_icons,
3263                )
3264            };
3265            if let Some(worktree) = self.project.read(cx).worktree_for_id(*worktree_id, cx) {
3266                let snapshot = worktree.read(cx).snapshot();
3267                let root_name = OsStr::new(snapshot.root_name());
3268                let expanded_entry_ids = self
3269                    .expanded_dir_ids
3270                    .get(&snapshot.id())
3271                    .map(Vec::as_slice)
3272                    .unwrap_or(&[]);
3273
3274                let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
3275                let entries = entries_paths.get_or_init(|| {
3276                    visible_worktree_entries
3277                        .iter()
3278                        .map(|e| (e.path.clone()))
3279                        .collect()
3280                });
3281                for entry in visible_worktree_entries[entry_range].iter() {
3282                    let status = git_status_setting
3283                        .then_some(entry.git_summary)
3284                        .unwrap_or_default();
3285                    let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok();
3286                    let icon = match entry.kind {
3287                        EntryKind::File => {
3288                            if show_file_icons {
3289                                FileIcons::get_icon(&entry.path, cx)
3290                            } else {
3291                                None
3292                            }
3293                        }
3294                        _ => {
3295                            if show_folder_icons {
3296                                FileIcons::get_folder_icon(is_expanded, cx)
3297                            } else {
3298                                FileIcons::get_chevron_icon(is_expanded, cx)
3299                            }
3300                        }
3301                    };
3302
3303                    let (depth, difference) =
3304                        ProjectPanel::calculate_depth_and_difference(&entry, entries);
3305
3306                    let filename = match difference {
3307                        diff if diff > 1 => entry
3308                            .path
3309                            .iter()
3310                            .skip(entry.path.components().count() - diff)
3311                            .collect::<PathBuf>()
3312                            .to_str()
3313                            .unwrap_or_default()
3314                            .to_string(),
3315                        _ => entry
3316                            .path
3317                            .file_name()
3318                            .map(|name| name.to_string_lossy().into_owned())
3319                            .unwrap_or_else(|| root_name.to_string_lossy().to_string()),
3320                    };
3321                    let selection = SelectedEntry {
3322                        worktree_id: snapshot.id(),
3323                        entry_id: entry.id,
3324                    };
3325
3326                    let is_marked = self.marked_entries.contains(&selection);
3327
3328                    let diagnostic_severity = self
3329                        .diagnostics
3330                        .get(&(*worktree_id, entry.path.to_path_buf()))
3331                        .cloned();
3332
3333                    let filename_text_color =
3334                        entry_git_aware_label_color(status, entry.is_ignored, is_marked);
3335
3336                    let mut details = EntryDetails {
3337                        filename,
3338                        icon,
3339                        path: entry.path.clone(),
3340                        depth,
3341                        kind: entry.kind,
3342                        is_ignored: entry.is_ignored,
3343                        is_expanded,
3344                        is_selected: self.selection == Some(selection),
3345                        is_marked,
3346                        is_editing: false,
3347                        is_processing: false,
3348                        is_cut: self
3349                            .clipboard
3350                            .as_ref()
3351                            .map_or(false, |e| e.is_cut() && e.items().contains(&selection)),
3352                        filename_text_color,
3353                        diagnostic_severity,
3354                        git_status: status,
3355                        is_private: entry.is_private,
3356                        worktree_id: *worktree_id,
3357                        canonical_path: entry.canonical_path.clone(),
3358                    };
3359
3360                    if let Some(edit_state) = &self.edit_state {
3361                        let is_edited_entry = if edit_state.is_new_entry() {
3362                            entry.id == NEW_ENTRY_ID
3363                        } else {
3364                            entry.id == edit_state.entry_id
3365                                || self
3366                                    .ancestors
3367                                    .get(&entry.id)
3368                                    .is_some_and(|auto_folded_dirs| {
3369                                        auto_folded_dirs.ancestors.contains(&edit_state.entry_id)
3370                                    })
3371                        };
3372
3373                        if is_edited_entry {
3374                            if let Some(processing_filename) = &edit_state.processing_filename {
3375                                details.is_processing = true;
3376                                if let Some(ancestors) = edit_state
3377                                    .leaf_entry_id
3378                                    .and_then(|entry| self.ancestors.get(&entry))
3379                                {
3380                                    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;
3381                                    let all_components = ancestors.ancestors.len();
3382
3383                                    let prefix_components = all_components - position;
3384                                    let suffix_components = position.checked_sub(1);
3385                                    let mut previous_components =
3386                                        Path::new(&details.filename).components();
3387                                    let mut new_path = previous_components
3388                                        .by_ref()
3389                                        .take(prefix_components)
3390                                        .collect::<PathBuf>();
3391                                    if let Some(last_component) =
3392                                        Path::new(processing_filename).components().next_back()
3393                                    {
3394                                        new_path.push(last_component);
3395                                        previous_components.next();
3396                                    }
3397
3398                                    if let Some(_) = suffix_components {
3399                                        new_path.push(previous_components);
3400                                    }
3401                                    if let Some(str) = new_path.to_str() {
3402                                        details.filename.clear();
3403                                        details.filename.push_str(str);
3404                                    }
3405                                } else {
3406                                    details.filename.clear();
3407                                    details.filename.push_str(processing_filename);
3408                                }
3409                            } else {
3410                                if edit_state.is_new_entry() {
3411                                    details.filename.clear();
3412                                }
3413                                details.is_editing = true;
3414                            }
3415                        }
3416                    }
3417
3418                    callback(entry.id, details, window, cx);
3419                }
3420            }
3421            ix = end_ix;
3422        }
3423    }
3424
3425    fn find_entry_in_worktree(
3426        &self,
3427        worktree_id: WorktreeId,
3428        reverse_search: bool,
3429        only_visible_entries: bool,
3430        predicate: impl Fn(GitEntryRef, WorktreeId) -> bool,
3431        cx: &mut Context<Self>,
3432    ) -> Option<GitEntry> {
3433        if only_visible_entries {
3434            let entries = self
3435                .visible_entries
3436                .iter()
3437                .find_map(|(tree_id, entries, _)| {
3438                    if worktree_id == *tree_id {
3439                        Some(entries)
3440                    } else {
3441                        None
3442                    }
3443                })?
3444                .clone();
3445
3446            return utils::ReversibleIterable::new(entries.iter(), reverse_search)
3447                .find(|ele| predicate(ele.to_ref(), worktree_id))
3448                .cloned();
3449        }
3450
3451        let repo_snapshots = self
3452            .project
3453            .read(cx)
3454            .git_store()
3455            .read(cx)
3456            .repo_snapshots(cx);
3457        let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?;
3458        worktree.update(cx, |tree, _| {
3459            utils::ReversibleIterable::new(
3460                GitTraversal::new(&repo_snapshots, tree.entries(true, 0usize)),
3461                reverse_search,
3462            )
3463            .find_single_ended(|ele| predicate(*ele, worktree_id))
3464            .map(|ele| ele.to_owned())
3465        })
3466    }
3467
3468    fn find_entry(
3469        &self,
3470        start: Option<&SelectedEntry>,
3471        reverse_search: bool,
3472        predicate: impl Fn(GitEntryRef, WorktreeId) -> bool,
3473        cx: &mut Context<Self>,
3474    ) -> Option<SelectedEntry> {
3475        let mut worktree_ids: Vec<_> = self
3476            .visible_entries
3477            .iter()
3478            .map(|(worktree_id, _, _)| *worktree_id)
3479            .collect();
3480        let repo_snapshots = self
3481            .project
3482            .read(cx)
3483            .git_store()
3484            .read(cx)
3485            .repo_snapshots(cx);
3486
3487        let mut last_found: Option<SelectedEntry> = None;
3488
3489        if let Some(start) = start {
3490            let worktree = self
3491                .project
3492                .read(cx)
3493                .worktree_for_id(start.worktree_id, cx)?;
3494
3495            let search = worktree.update(cx, |tree, _| {
3496                let entry = tree.entry_for_id(start.entry_id)?;
3497                let root_entry = tree.root_entry()?;
3498                let tree_id = tree.id();
3499
3500                let mut first_iter = GitTraversal::new(
3501                    &repo_snapshots,
3502                    tree.traverse_from_path(true, true, true, entry.path.as_ref()),
3503                );
3504
3505                if reverse_search {
3506                    first_iter.next();
3507                }
3508
3509                let first = first_iter
3510                    .enumerate()
3511                    .take_until(|(count, entry)| entry.entry == root_entry && *count != 0usize)
3512                    .map(|(_, entry)| entry)
3513                    .find(|ele| predicate(*ele, tree_id))
3514                    .map(|ele| ele.to_owned());
3515
3516                let second_iter = GitTraversal::new(&repo_snapshots, tree.entries(true, 0usize));
3517
3518                let second = if reverse_search {
3519                    second_iter
3520                        .take_until(|ele| ele.id == start.entry_id)
3521                        .filter(|ele| predicate(*ele, tree_id))
3522                        .last()
3523                        .map(|ele| ele.to_owned())
3524                } else {
3525                    second_iter
3526                        .take_while(|ele| ele.id != start.entry_id)
3527                        .filter(|ele| predicate(*ele, tree_id))
3528                        .last()
3529                        .map(|ele| ele.to_owned())
3530                };
3531
3532                if reverse_search {
3533                    Some((second, first))
3534                } else {
3535                    Some((first, second))
3536                }
3537            });
3538
3539            if let Some((first, second)) = search {
3540                let first = first.map(|entry| SelectedEntry {
3541                    worktree_id: start.worktree_id,
3542                    entry_id: entry.id,
3543                });
3544
3545                let second = second.map(|entry| SelectedEntry {
3546                    worktree_id: start.worktree_id,
3547                    entry_id: entry.id,
3548                });
3549
3550                if first.is_some() {
3551                    return first;
3552                }
3553                last_found = second;
3554
3555                let idx = worktree_ids
3556                    .iter()
3557                    .enumerate()
3558                    .find(|(_, ele)| **ele == start.worktree_id)
3559                    .map(|(idx, _)| idx);
3560
3561                if let Some(idx) = idx {
3562                    worktree_ids.rotate_left(idx + 1usize);
3563                    worktree_ids.pop();
3564                }
3565            }
3566        }
3567
3568        for tree_id in worktree_ids.into_iter() {
3569            if let Some(found) =
3570                self.find_entry_in_worktree(tree_id, reverse_search, false, &predicate, cx)
3571            {
3572                return Some(SelectedEntry {
3573                    worktree_id: tree_id,
3574                    entry_id: found.id,
3575                });
3576            }
3577        }
3578
3579        last_found
3580    }
3581
3582    fn find_visible_entry(
3583        &self,
3584        start: Option<&SelectedEntry>,
3585        reverse_search: bool,
3586        predicate: impl Fn(GitEntryRef, WorktreeId) -> bool,
3587        cx: &mut Context<Self>,
3588    ) -> Option<SelectedEntry> {
3589        let mut worktree_ids: Vec<_> = self
3590            .visible_entries
3591            .iter()
3592            .map(|(worktree_id, _, _)| *worktree_id)
3593            .collect();
3594
3595        let mut last_found: Option<SelectedEntry> = None;
3596
3597        if let Some(start) = start {
3598            let entries = self
3599                .visible_entries
3600                .iter()
3601                .find(|(worktree_id, _, _)| *worktree_id == start.worktree_id)
3602                .map(|(_, entries, _)| entries)?;
3603
3604            let mut start_idx = entries
3605                .iter()
3606                .enumerate()
3607                .find(|(_, ele)| ele.id == start.entry_id)
3608                .map(|(idx, _)| idx)?;
3609
3610            if reverse_search {
3611                start_idx = start_idx.saturating_add(1usize);
3612            }
3613
3614            let (left, right) = entries.split_at_checked(start_idx)?;
3615
3616            let (first_iter, second_iter) = if reverse_search {
3617                (
3618                    utils::ReversibleIterable::new(left.iter(), reverse_search),
3619                    utils::ReversibleIterable::new(right.iter(), reverse_search),
3620                )
3621            } else {
3622                (
3623                    utils::ReversibleIterable::new(right.iter(), reverse_search),
3624                    utils::ReversibleIterable::new(left.iter(), reverse_search),
3625                )
3626            };
3627
3628            let first_search = first_iter.find(|ele| predicate(ele.to_ref(), start.worktree_id));
3629            let second_search = second_iter.find(|ele| predicate(ele.to_ref(), start.worktree_id));
3630
3631            if first_search.is_some() {
3632                return first_search.map(|entry| SelectedEntry {
3633                    worktree_id: start.worktree_id,
3634                    entry_id: entry.id,
3635                });
3636            }
3637
3638            last_found = second_search.map(|entry| SelectedEntry {
3639                worktree_id: start.worktree_id,
3640                entry_id: entry.id,
3641            });
3642
3643            let idx = worktree_ids
3644                .iter()
3645                .enumerate()
3646                .find(|(_, ele)| **ele == start.worktree_id)
3647                .map(|(idx, _)| idx);
3648
3649            if let Some(idx) = idx {
3650                worktree_ids.rotate_left(idx + 1usize);
3651                worktree_ids.pop();
3652            }
3653        }
3654
3655        for tree_id in worktree_ids.into_iter() {
3656            if let Some(found) =
3657                self.find_entry_in_worktree(tree_id, reverse_search, true, &predicate, cx)
3658            {
3659                return Some(SelectedEntry {
3660                    worktree_id: tree_id,
3661                    entry_id: found.id,
3662                });
3663            }
3664        }
3665
3666        last_found
3667    }
3668
3669    fn calculate_depth_and_difference(
3670        entry: &Entry,
3671        visible_worktree_entries: &HashSet<Arc<Path>>,
3672    ) -> (usize, usize) {
3673        let (depth, difference) = entry
3674            .path
3675            .ancestors()
3676            .skip(1) // Skip the entry itself
3677            .find_map(|ancestor| {
3678                if let Some(parent_entry) = visible_worktree_entries.get(ancestor) {
3679                    let entry_path_components_count = entry.path.components().count();
3680                    let parent_path_components_count = parent_entry.components().count();
3681                    let difference = entry_path_components_count - parent_path_components_count;
3682                    let depth = parent_entry
3683                        .ancestors()
3684                        .skip(1)
3685                        .filter(|ancestor| visible_worktree_entries.contains(*ancestor))
3686                        .count();
3687                    Some((depth + 1, difference))
3688                } else {
3689                    None
3690                }
3691            })
3692            .unwrap_or((0, 0));
3693
3694        (depth, difference)
3695    }
3696
3697    fn render_entry(
3698        &self,
3699        entry_id: ProjectEntryId,
3700        details: EntryDetails,
3701        window: &mut Window,
3702        cx: &mut Context<Self>,
3703    ) -> Stateful<Div> {
3704        const GROUP_NAME: &str = "project_entry";
3705
3706        let kind = details.kind;
3707        let settings = ProjectPanelSettings::get_global(cx);
3708        let show_editor = details.is_editing && !details.is_processing;
3709
3710        let selection = SelectedEntry {
3711            worktree_id: details.worktree_id,
3712            entry_id,
3713        };
3714
3715        let is_marked = self.marked_entries.contains(&selection);
3716        let is_active = self
3717            .selection
3718            .map_or(false, |selection| selection.entry_id == entry_id);
3719
3720        let file_name = details.filename.clone();
3721
3722        let mut icon = details.icon.clone();
3723        if settings.file_icons && show_editor && details.kind.is_file() {
3724            let filename = self.filename_editor.read(cx).text(cx);
3725            if filename.len() > 2 {
3726                icon = FileIcons::get_icon(Path::new(&filename), cx);
3727            }
3728        }
3729
3730        let filename_text_color = details.filename_text_color;
3731        let diagnostic_severity = details.diagnostic_severity;
3732        let item_colors = get_item_color(cx);
3733
3734        let canonical_path = details
3735            .canonical_path
3736            .as_ref()
3737            .map(|f| f.to_string_lossy().to_string());
3738        let path = details.path.clone();
3739
3740        let depth = details.depth;
3741        let worktree_id = details.worktree_id;
3742        let selections = Arc::new(self.marked_entries.clone());
3743
3744        let dragged_selection = DraggedSelection {
3745            active_selection: selection,
3746            marked_selections: selections,
3747        };
3748
3749        let bg_color = if is_marked {
3750            item_colors.marked
3751        } else {
3752            item_colors.default
3753        };
3754
3755        let bg_hover_color = if is_marked {
3756            item_colors.marked
3757        } else {
3758            item_colors.hover
3759        };
3760
3761        let validation_color_and_message = if show_editor {
3762            match self
3763                .edit_state
3764                .as_ref()
3765                .map_or(ValidationState::None, |e| e.validation_state.clone())
3766            {
3767                ValidationState::Error(msg) => Some((Color::Error.color(cx), msg.clone())),
3768                ValidationState::Warning(msg) => Some((Color::Warning.color(cx), msg.clone())),
3769                ValidationState::None => None,
3770            }
3771        } else {
3772            None
3773        };
3774
3775        let border_color =
3776            if !self.mouse_down && is_active && self.focus_handle.contains_focused(window, cx) {
3777                match validation_color_and_message {
3778                    Some((color, _)) => color,
3779                    None => item_colors.focused,
3780                }
3781            } else {
3782                bg_color
3783            };
3784
3785        let border_hover_color =
3786            if !self.mouse_down && is_active && self.focus_handle.contains_focused(window, cx) {
3787                match validation_color_and_message {
3788                    Some((color, _)) => color,
3789                    None => item_colors.focused,
3790                }
3791            } else {
3792                bg_hover_color
3793            };
3794
3795        let folded_directory_drag_target = self.folded_directory_drag_target;
3796
3797        div()
3798            .id(entry_id.to_proto() as usize)
3799            .group(GROUP_NAME)
3800            .cursor_pointer()
3801            .rounded_none()
3802            .bg(bg_color)
3803            .border_1()
3804            .border_r_2()
3805            .border_color(border_color)
3806            .hover(|style| style.bg(bg_hover_color).border_color(border_hover_color))
3807            .on_drag_move::<ExternalPaths>(cx.listener(
3808                move |this, event: &DragMoveEvent<ExternalPaths>, _, cx| {
3809                    if event.bounds.contains(&event.event.position) {
3810                        if this.last_external_paths_drag_over_entry == Some(entry_id) {
3811                            return;
3812                        }
3813                        this.last_external_paths_drag_over_entry = Some(entry_id);
3814                        this.marked_entries.clear();
3815
3816                        let Some((worktree, path, entry)) = maybe!({
3817                            let worktree = this
3818                                .project
3819                                .read(cx)
3820                                .worktree_for_id(selection.worktree_id, cx)?;
3821                            let worktree = worktree.read(cx);
3822                            let entry = worktree.entry_for_path(&path)?;
3823                            let path = if entry.is_dir() {
3824                                path.as_ref()
3825                            } else {
3826                                path.parent()?
3827                            };
3828                            Some((worktree, path, entry))
3829                        }) else {
3830                            return;
3831                        };
3832
3833                        this.marked_entries.insert(SelectedEntry {
3834                            entry_id: entry.id,
3835                            worktree_id: worktree.id(),
3836                        });
3837
3838                        for entry in worktree.child_entries(path) {
3839                            this.marked_entries.insert(SelectedEntry {
3840                                entry_id: entry.id,
3841                                worktree_id: worktree.id(),
3842                            });
3843                        }
3844
3845                        cx.notify();
3846                    }
3847                },
3848            ))
3849            .on_drop(cx.listener(
3850                move |this, external_paths: &ExternalPaths, window, cx| {
3851                    this.hover_scroll_task.take();
3852                    this.last_external_paths_drag_over_entry = None;
3853                    this.marked_entries.clear();
3854                    this.drop_external_files(external_paths.paths(), entry_id, window, cx);
3855                    cx.stop_propagation();
3856                },
3857            ))
3858            .on_drag_move::<DraggedSelection>(cx.listener(
3859                move |this, event: &DragMoveEvent<DraggedSelection>, window, cx| {
3860                    if event.bounds.contains(&event.event.position) {
3861                        if this.last_selection_drag_over_entry == Some(entry_id) {
3862                            return;
3863                        }
3864                        this.last_selection_drag_over_entry = Some(entry_id);
3865                        this.hover_expand_task.take();
3866
3867                        if !kind.is_dir()
3868                            || this
3869                                .expanded_dir_ids
3870                                .get(&details.worktree_id)
3871                                .map_or(false, |ids| ids.binary_search(&entry_id).is_ok())
3872                        {
3873                            return;
3874                        }
3875
3876                        let bounds = event.bounds;
3877                        this.hover_expand_task =
3878                            Some(cx.spawn_in(window, async move |this, cx| {
3879                                cx.background_executor()
3880                                    .timer(Duration::from_millis(500))
3881                                    .await;
3882                                this.update_in(cx, |this, window, cx| {
3883                                    this.hover_expand_task.take();
3884                                    if this.last_selection_drag_over_entry == Some(entry_id)
3885                                        && bounds.contains(&window.mouse_position())
3886                                    {
3887                                        this.expand_entry(worktree_id, entry_id, cx);
3888                                        this.update_visible_entries(
3889                                            Some((worktree_id, entry_id)),
3890                                            cx,
3891                                        );
3892                                        cx.notify();
3893                                    }
3894                                })
3895                                .ok();
3896                            }));
3897                    }
3898                },
3899            ))
3900            .on_drag(
3901                dragged_selection,
3902                move |selection, click_offset, _window, cx| {
3903                    cx.new(|_| DraggedProjectEntryView {
3904                        details: details.clone(),
3905                        click_offset,
3906                        selection: selection.active_selection,
3907                        selections: selection.marked_selections.clone(),
3908                    })
3909                },
3910            )
3911            .drag_over::<DraggedSelection>(move |style, _, _, _| {
3912                if  folded_directory_drag_target.is_some() {
3913                    return style;
3914                }
3915                style.bg(item_colors.drag_over)
3916            })
3917            .on_drop(
3918                cx.listener(move |this, selections: &DraggedSelection, window, cx| {
3919                    this.hover_scroll_task.take();
3920                    this.hover_expand_task.take();
3921                    if  folded_directory_drag_target.is_some() {
3922                        return;
3923                    }
3924                    this.drag_onto(selections, entry_id, kind.is_file(), window, cx);
3925                }),
3926            )
3927            .on_mouse_down(
3928                MouseButton::Left,
3929                cx.listener(move |this, _, _, cx| {
3930                    this.mouse_down = true;
3931                    cx.propagate();
3932                }),
3933            )
3934            .on_click(
3935                cx.listener(move |this, event: &gpui::ClickEvent, window, cx| {
3936                    if event.down.button == MouseButton::Right
3937                        || event.down.first_mouse
3938                        || show_editor
3939                    {
3940                        return;
3941                    }
3942                    if event.down.button == MouseButton::Left {
3943                        this.mouse_down = false;
3944                    }
3945                    cx.stop_propagation();
3946
3947                    if let Some(selection) = this.selection.filter(|_| event.modifiers().shift) {
3948                        let current_selection = this.index_for_selection(selection);
3949                        let clicked_entry = SelectedEntry {
3950                            entry_id,
3951                            worktree_id,
3952                        };
3953                        let target_selection = this.index_for_selection(clicked_entry);
3954                        if let Some(((_, _, source_index), (_, _, target_index))) =
3955                            current_selection.zip(target_selection)
3956                        {
3957                            let range_start = source_index.min(target_index);
3958                            let range_end = source_index.max(target_index) + 1; // Make the range inclusive.
3959                            let mut new_selections = BTreeSet::new();
3960                            this.for_each_visible_entry(
3961                                range_start..range_end,
3962                                window,
3963                                cx,
3964                                |entry_id, details, _, _| {
3965                                    new_selections.insert(SelectedEntry {
3966                                        entry_id,
3967                                        worktree_id: details.worktree_id,
3968                                    });
3969                                },
3970                            );
3971
3972                            this.marked_entries = this
3973                                .marked_entries
3974                                .union(&new_selections)
3975                                .cloned()
3976                                .collect();
3977
3978                            this.selection = Some(clicked_entry);
3979                            this.marked_entries.insert(clicked_entry);
3980                        }
3981                    } else if event.modifiers().secondary() {
3982                        if event.down.click_count > 1 {
3983                            this.split_entry(entry_id, cx);
3984                        } else {
3985                            this.selection = Some(selection);
3986                            if !this.marked_entries.insert(selection) {
3987                                this.marked_entries.remove(&selection);
3988                            }
3989                        }
3990                    } else if kind.is_dir() {
3991                        this.marked_entries.clear();
3992                        if event.modifiers().alt {
3993                            this.toggle_expand_all(entry_id, window, cx);
3994                        } else {
3995                            this.toggle_expanded(entry_id, window, cx);
3996                        }
3997                    } else {
3998                        let preview_tabs_enabled = PreviewTabsSettings::get_global(cx).enabled;
3999                        let click_count = event.up.click_count;
4000                        let focus_opened_item = !preview_tabs_enabled || click_count > 1;
4001                        let allow_preview = preview_tabs_enabled && click_count == 1;
4002                        this.open_entry(entry_id, focus_opened_item, allow_preview, cx);
4003                    }
4004                }),
4005            )
4006            .child(
4007                ListItem::new(entry_id.to_proto() as usize)
4008                    .indent_level(depth)
4009                    .indent_step_size(px(settings.indent_size))
4010                    .spacing(match settings.entry_spacing {
4011                        project_panel_settings::EntrySpacing::Comfortable => ListItemSpacing::Dense,
4012                        project_panel_settings::EntrySpacing::Standard => {
4013                            ListItemSpacing::ExtraDense
4014                        }
4015                    })
4016                    .selectable(false)
4017                    .when_some(canonical_path, |this, path| {
4018                        this.end_slot::<AnyElement>(
4019                            div()
4020                                .id("symlink_icon")
4021                                .pr_3()
4022                                .tooltip(move |window, cx| {
4023                                    Tooltip::with_meta(
4024                                        path.to_string(),
4025                                        None,
4026                                        "Symbolic Link",
4027                                        window,
4028                                        cx,
4029                                    )
4030                                })
4031                                .child(
4032                                    Icon::new(IconName::ArrowUpRight)
4033                                        .size(IconSize::Indicator)
4034                                        .color(filename_text_color),
4035                                )
4036                                .into_any_element(),
4037                        )
4038                    })
4039                    .child(if let Some(icon) = &icon {
4040                        if let Some((_, decoration_color)) =
4041                            entry_diagnostic_aware_icon_decoration_and_color(diagnostic_severity)
4042                        {
4043                            let is_warning = diagnostic_severity
4044                                .map(|severity| matches!(severity, DiagnosticSeverity::WARNING))
4045                                .unwrap_or(false);
4046                            div().child(
4047                                DecoratedIcon::new(
4048                                    Icon::from_path(icon.clone()).color(Color::Muted),
4049                                    Some(
4050                                        IconDecoration::new(
4051                                            if kind.is_file() {
4052                                                if is_warning {
4053                                                    IconDecorationKind::Triangle
4054                                                } else {
4055                                                    IconDecorationKind::X
4056                                                }
4057                                            } else {
4058                                                IconDecorationKind::Dot
4059                                            },
4060                                            bg_color,
4061                                            cx,
4062                                        )
4063                                        .group_name(Some(GROUP_NAME.into()))
4064                                        .knockout_hover_color(bg_hover_color)
4065                                        .color(decoration_color.color(cx))
4066                                        .position(Point {
4067                                            x: px(-2.),
4068                                            y: px(-2.),
4069                                        }),
4070                                    ),
4071                                )
4072                                .into_any_element(),
4073                            )
4074                        } else {
4075                            h_flex().child(Icon::from_path(icon.to_string()).color(Color::Muted))
4076                        }
4077                    } else {
4078                        if let Some((icon_name, color)) =
4079                            entry_diagnostic_aware_icon_name_and_color(diagnostic_severity)
4080                        {
4081                            h_flex()
4082                                .size(IconSize::default().rems())
4083                                .child(Icon::new(icon_name).color(color).size(IconSize::Small))
4084                        } else {
4085                            h_flex()
4086                                .size(IconSize::default().rems())
4087                                .invisible()
4088                                .flex_none()
4089                        }
4090                    })
4091                    .child(
4092                        if let (Some(editor), true) = (Some(&self.filename_editor), show_editor) {
4093                            h_flex().h_6().w_full().child(editor.clone())
4094                        } else {
4095                            h_flex().h_6().map(|mut this| {
4096                                if let Some(folded_ancestors) = self.ancestors.get(&entry_id) {
4097                                    let components = Path::new(&file_name)
4098                                        .components()
4099                                        .map(|comp| {
4100                                            let comp_str =
4101                                                comp.as_os_str().to_string_lossy().into_owned();
4102                                            comp_str
4103                                        })
4104                                        .collect::<Vec<_>>();
4105
4106                                    let components_len = components.len();
4107                                    let active_index = components_len
4108                                        - 1
4109                                        - folded_ancestors.current_ancestor_depth;
4110                                        const DELIMITER: SharedString =
4111                                        SharedString::new_static(std::path::MAIN_SEPARATOR_STR);
4112                                    for (index, component) in components.into_iter().enumerate() {
4113                                        if index != 0 {
4114                                                let delimiter_target_index = index - 1;
4115                                                let target_entry_id = folded_ancestors.ancestors.get(components_len - 1 - delimiter_target_index).cloned();
4116                                                this = this.child(
4117                                                    div()
4118                                                    .on_drop(cx.listener(move |this, selections: &DraggedSelection, window, cx| {
4119                                                        this.hover_scroll_task.take();
4120                                                        this.folded_directory_drag_target = None;
4121                                                        if let Some(target_entry_id) = target_entry_id {
4122                                                            this.drag_onto(selections, target_entry_id, kind.is_file(), window, cx);
4123                                                        }
4124                                                    }))
4125                                                    .on_drag_move(cx.listener(
4126                                                        move |this, event: &DragMoveEvent<DraggedSelection>, _, _| {
4127                                                            if event.bounds.contains(&event.event.position) {
4128                                                                this.folded_directory_drag_target = Some(
4129                                                                    FoldedDirectoryDragTarget {
4130                                                                        entry_id,
4131                                                                        index: delimiter_target_index,
4132                                                                        is_delimiter_target: true,
4133                                                                    }
4134                                                                );
4135                                                            } else {
4136                                                                let is_current_target = this.folded_directory_drag_target
4137                                                                    .map_or(false, |target|
4138                                                                        target.entry_id == entry_id &&
4139                                                                        target.index == delimiter_target_index &&
4140                                                                        target.is_delimiter_target
4141                                                                    );
4142                                                                if is_current_target {
4143                                                                    this.folded_directory_drag_target = None;
4144                                                                }
4145                                                            }
4146
4147                                                        },
4148                                                    ))
4149                                                    .child(
4150                                                        Label::new(DELIMITER.clone())
4151                                                            .single_line()
4152                                                            .color(filename_text_color)
4153                                                    )
4154                                                );
4155                                        }
4156                                        let id = SharedString::from(format!(
4157                                            "project_panel_path_component_{}_{index}",
4158                                            entry_id.to_usize()
4159                                        ));
4160                                        let label = div()
4161                                            .id(id)
4162                                            .on_click(cx.listener(move |this, _, _, cx| {
4163                                                if index != active_index {
4164                                                    if let Some(folds) =
4165                                                        this.ancestors.get_mut(&entry_id)
4166                                                    {
4167                                                        folds.current_ancestor_depth =
4168                                                            components_len - 1 - index;
4169                                                        cx.notify();
4170                                                    }
4171                                                }
4172                                            }))
4173                                            .when(index != components_len - 1, |div|{
4174                                                let target_entry_id = folded_ancestors.ancestors.get(components_len - 1 - index).cloned();
4175                                                div
4176                                                .on_drag_move(cx.listener(
4177                                                    move |this, event: &DragMoveEvent<DraggedSelection>, _, _| {
4178                                                    if event.bounds.contains(&event.event.position) {
4179                                                            this.folded_directory_drag_target = Some(
4180                                                                FoldedDirectoryDragTarget {
4181                                                                    entry_id,
4182                                                                    index,
4183                                                                    is_delimiter_target: false,
4184                                                                }
4185                                                            );
4186                                                        } else {
4187                                                            let is_current_target = this.folded_directory_drag_target
4188                                                                .as_ref()
4189                                                                .map_or(false, |target|
4190                                                                    target.entry_id == entry_id &&
4191                                                                    target.index == index &&
4192                                                                    !target.is_delimiter_target
4193                                                                );
4194                                                            if is_current_target {
4195                                                                this.folded_directory_drag_target = None;
4196                                                            }
4197                                                        }
4198                                                    },
4199                                                ))
4200                                                .on_drop(cx.listener(move |this, selections: &DraggedSelection, window,cx| {
4201                                                    this.hover_scroll_task.take();
4202                                                    this.folded_directory_drag_target = None;
4203                                                    if let Some(target_entry_id) = target_entry_id {
4204                                                        this.drag_onto(selections, target_entry_id, kind.is_file(), window, cx);
4205                                                    }
4206                                                }))
4207                                                .when(folded_directory_drag_target.map_or(false, |target|
4208                                                    target.entry_id == entry_id &&
4209                                                    target.index == index
4210                                                ), |this| {
4211                                                    this.bg(item_colors.drag_over)
4212                                                })
4213                                            })
4214                                            .child(
4215                                                Label::new(component)
4216                                                    .single_line()
4217                                                    .color(filename_text_color)
4218                                                    .when(
4219                                                        index == active_index
4220                                                            && (is_active || is_marked),
4221                                                        |this| this.underline(),
4222                                                    ),
4223                                            );
4224
4225                                        this = this.child(label);
4226                                    }
4227
4228                                    this
4229                                } else {
4230                                    this.child(
4231                                        Label::new(file_name)
4232                                            .single_line()
4233                                            .color(filename_text_color),
4234                                    )
4235                                }
4236                            })
4237                        }
4238                        .ml_1(),
4239                    )
4240                    .on_secondary_mouse_down(cx.listener(
4241                        move |this, event: &MouseDownEvent, window, cx| {
4242                            // Stop propagation to prevent the catch-all context menu for the project
4243                            // panel from being deployed.
4244                            cx.stop_propagation();
4245                            // Some context menu actions apply to all marked entries. If the user
4246                            // right-clicks on an entry that is not marked, they may not realize the
4247                            // action applies to multiple entries. To avoid inadvertent changes, all
4248                            // entries are unmarked.
4249                            if !this.marked_entries.contains(&selection) {
4250                                this.marked_entries.clear();
4251                            }
4252                            this.deploy_context_menu(event.position, entry_id, window, cx);
4253                        },
4254                    ))
4255                    .overflow_x(),
4256            )
4257            .when_some(
4258                validation_color_and_message,
4259                |this, (color, message)| {
4260                    this
4261                    .relative()
4262                    .child(
4263                        deferred(
4264                            div()
4265                            .occlude()
4266                            .absolute()
4267                            .top_full()
4268                            .left(px(-1.)) // Used px over rem so that it doesn't change with font size
4269                            .right(px(-0.5))
4270                            .py_1()
4271                            .px_2()
4272                            .border_1()
4273                            .border_color(color)
4274                            .bg(cx.theme().colors().background)
4275                            .child(
4276                                Label::new(message)
4277                                .color(Color::from(color))
4278                                .size(LabelSize::Small)
4279                            )
4280                        )
4281                    )
4282                }
4283            )
4284    }
4285
4286    fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
4287        if !Self::should_show_scrollbar(cx)
4288            || !(self.show_scrollbar || self.vertical_scrollbar_state.is_dragging())
4289        {
4290            return None;
4291        }
4292        Some(
4293            div()
4294                .occlude()
4295                .id("project-panel-vertical-scroll")
4296                .on_mouse_move(cx.listener(|_, _, _, cx| {
4297                    cx.notify();
4298                    cx.stop_propagation()
4299                }))
4300                .on_hover(|_, _, cx| {
4301                    cx.stop_propagation();
4302                })
4303                .on_any_mouse_down(|_, _, cx| {
4304                    cx.stop_propagation();
4305                })
4306                .on_mouse_up(
4307                    MouseButton::Left,
4308                    cx.listener(|this, _, window, cx| {
4309                        if !this.vertical_scrollbar_state.is_dragging()
4310                            && !this.focus_handle.contains_focused(window, cx)
4311                        {
4312                            this.hide_scrollbar(window, cx);
4313                            cx.notify();
4314                        }
4315
4316                        cx.stop_propagation();
4317                    }),
4318                )
4319                .on_scroll_wheel(cx.listener(|_, _, _, cx| {
4320                    cx.notify();
4321                }))
4322                .h_full()
4323                .absolute()
4324                .right_1()
4325                .top_1()
4326                .bottom_1()
4327                .w(px(12.))
4328                .cursor_default()
4329                .children(Scrollbar::vertical(
4330                    // percentage as f32..end_offset as f32,
4331                    self.vertical_scrollbar_state.clone(),
4332                )),
4333        )
4334    }
4335
4336    fn render_horizontal_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
4337        if !Self::should_show_scrollbar(cx)
4338            || !(self.show_scrollbar || self.horizontal_scrollbar_state.is_dragging())
4339        {
4340            return None;
4341        }
4342
4343        let scroll_handle = self.scroll_handle.0.borrow();
4344        let longest_item_width = scroll_handle
4345            .last_item_size
4346            .filter(|size| size.contents.width > size.item.width)?
4347            .contents
4348            .width
4349            .0 as f64;
4350        if longest_item_width < scroll_handle.base_handle.bounds().size.width.0 as f64 {
4351            return None;
4352        }
4353
4354        Some(
4355            div()
4356                .occlude()
4357                .id("project-panel-horizontal-scroll")
4358                .on_mouse_move(cx.listener(|_, _, _, cx| {
4359                    cx.notify();
4360                    cx.stop_propagation()
4361                }))
4362                .on_hover(|_, _, cx| {
4363                    cx.stop_propagation();
4364                })
4365                .on_any_mouse_down(|_, _, cx| {
4366                    cx.stop_propagation();
4367                })
4368                .on_mouse_up(
4369                    MouseButton::Left,
4370                    cx.listener(|this, _, window, cx| {
4371                        if !this.horizontal_scrollbar_state.is_dragging()
4372                            && !this.focus_handle.contains_focused(window, cx)
4373                        {
4374                            this.hide_scrollbar(window, cx);
4375                            cx.notify();
4376                        }
4377
4378                        cx.stop_propagation();
4379                    }),
4380                )
4381                .on_scroll_wheel(cx.listener(|_, _, _, cx| {
4382                    cx.notify();
4383                }))
4384                .w_full()
4385                .absolute()
4386                .right_1()
4387                .left_1()
4388                .bottom_1()
4389                .h(px(12.))
4390                .cursor_default()
4391                .when(self.width.is_some(), |this| {
4392                    this.children(Scrollbar::horizontal(
4393                        self.horizontal_scrollbar_state.clone(),
4394                    ))
4395                }),
4396        )
4397    }
4398
4399    fn dispatch_context(&self, window: &Window, cx: &Context<Self>) -> KeyContext {
4400        let mut dispatch_context = KeyContext::new_with_defaults();
4401        dispatch_context.add("ProjectPanel");
4402        dispatch_context.add("menu");
4403
4404        let identifier = if self.filename_editor.focus_handle(cx).is_focused(window) {
4405            "editing"
4406        } else {
4407            "not_editing"
4408        };
4409
4410        dispatch_context.add(identifier);
4411        dispatch_context
4412    }
4413
4414    fn should_show_scrollbar(cx: &App) -> bool {
4415        let show = ProjectPanelSettings::get_global(cx)
4416            .scrollbar
4417            .show
4418            .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show);
4419        match show {
4420            ShowScrollbar::Auto => true,
4421            ShowScrollbar::System => true,
4422            ShowScrollbar::Always => true,
4423            ShowScrollbar::Never => false,
4424        }
4425    }
4426
4427    fn should_autohide_scrollbar(cx: &App) -> bool {
4428        let show = ProjectPanelSettings::get_global(cx)
4429            .scrollbar
4430            .show
4431            .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show);
4432        match show {
4433            ShowScrollbar::Auto => true,
4434            ShowScrollbar::System => cx
4435                .try_global::<ScrollbarAutoHide>()
4436                .map_or_else(|| cx.should_auto_hide_scrollbars(), |autohide| autohide.0),
4437            ShowScrollbar::Always => false,
4438            ShowScrollbar::Never => true,
4439        }
4440    }
4441
4442    fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4443        const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
4444        if !Self::should_autohide_scrollbar(cx) {
4445            return;
4446        }
4447        self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |panel, cx| {
4448            cx.background_executor()
4449                .timer(SCROLLBAR_SHOW_INTERVAL)
4450                .await;
4451            panel
4452                .update(cx, |panel, cx| {
4453                    panel.show_scrollbar = false;
4454                    cx.notify();
4455                })
4456                .log_err();
4457        }))
4458    }
4459
4460    fn reveal_entry(
4461        &mut self,
4462        project: Entity<Project>,
4463        entry_id: ProjectEntryId,
4464        skip_ignored: bool,
4465        cx: &mut Context<Self>,
4466    ) -> Result<()> {
4467        if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) {
4468            let worktree = worktree.read(cx);
4469            if skip_ignored
4470                && worktree
4471                    .entry_for_id(entry_id)
4472                    .map_or(true, |entry| entry.is_ignored && !entry.is_always_included)
4473            {
4474                return Err(anyhow!(
4475                    "can't reveal an ignored entry in the project panel"
4476                ));
4477            }
4478
4479            let worktree_id = worktree.id();
4480            self.expand_entry(worktree_id, entry_id, cx);
4481            self.update_visible_entries(Some((worktree_id, entry_id)), cx);
4482            self.marked_entries.clear();
4483            self.marked_entries.insert(SelectedEntry {
4484                worktree_id,
4485                entry_id,
4486            });
4487            self.autoscroll(cx);
4488            cx.notify();
4489            Ok(())
4490        } else {
4491            Err(anyhow!(
4492                "can't reveal a non-existent entry in the project panel"
4493            ))
4494        }
4495    }
4496
4497    fn find_active_indent_guide(
4498        &self,
4499        indent_guides: &[IndentGuideLayout],
4500        cx: &App,
4501    ) -> Option<usize> {
4502        let (worktree, entry) = self.selected_entry(cx)?;
4503
4504        // Find the parent entry of the indent guide, this will either be the
4505        // expanded folder we have selected, or the parent of the currently
4506        // selected file/collapsed directory
4507        let mut entry = entry;
4508        loop {
4509            let is_expanded_dir = entry.is_dir()
4510                && self
4511                    .expanded_dir_ids
4512                    .get(&worktree.id())
4513                    .map(|ids| ids.binary_search(&entry.id).is_ok())
4514                    .unwrap_or(false);
4515            if is_expanded_dir {
4516                break;
4517            }
4518            entry = worktree.entry_for_path(&entry.path.parent()?)?;
4519        }
4520
4521        let (active_indent_range, depth) = {
4522            let (worktree_ix, child_offset, ix) = self.index_for_entry(entry.id, worktree.id())?;
4523            let child_paths = &self.visible_entries[worktree_ix].1;
4524            let mut child_count = 0;
4525            let depth = entry.path.ancestors().count();
4526            while let Some(entry) = child_paths.get(child_offset + child_count + 1) {
4527                if entry.path.ancestors().count() <= depth {
4528                    break;
4529                }
4530                child_count += 1;
4531            }
4532
4533            let start = ix + 1;
4534            let end = start + child_count;
4535
4536            let (_, entries, paths) = &self.visible_entries[worktree_ix];
4537            let visible_worktree_entries =
4538                paths.get_or_init(|| entries.iter().map(|e| (e.path.clone())).collect());
4539
4540            // Calculate the actual depth of the entry, taking into account that directories can be auto-folded.
4541            let (depth, _) = Self::calculate_depth_and_difference(entry, visible_worktree_entries);
4542            (start..end, depth)
4543        };
4544
4545        let candidates = indent_guides
4546            .iter()
4547            .enumerate()
4548            .filter(|(_, indent_guide)| indent_guide.offset.x == depth);
4549
4550        for (i, indent) in candidates {
4551            // Find matches that are either an exact match, partially on screen, or inside the enclosing indent
4552            if active_indent_range.start <= indent.offset.y + indent.length
4553                && indent.offset.y <= active_indent_range.end
4554            {
4555                return Some(i);
4556            }
4557        }
4558        None
4559    }
4560}
4561
4562fn item_width_estimate(depth: usize, item_text_chars: usize, is_symlink: bool) -> usize {
4563    const ICON_SIZE_FACTOR: usize = 2;
4564    let mut item_width = depth * ICON_SIZE_FACTOR + item_text_chars;
4565    if is_symlink {
4566        item_width += ICON_SIZE_FACTOR;
4567    }
4568    item_width
4569}
4570
4571impl Render for ProjectPanel {
4572    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
4573        let has_worktree = !self.visible_entries.is_empty();
4574        let project = self.project.read(cx);
4575        let indent_size = ProjectPanelSettings::get_global(cx).indent_size;
4576        let show_indent_guides =
4577            ProjectPanelSettings::get_global(cx).indent_guides.show == ShowIndentGuides::Always;
4578        let is_local = project.is_local();
4579
4580        if has_worktree {
4581            let item_count = self
4582                .visible_entries
4583                .iter()
4584                .map(|(_, worktree_entries, _)| worktree_entries.len())
4585                .sum();
4586
4587            fn handle_drag_move_scroll<T: 'static>(
4588                this: &mut ProjectPanel,
4589                e: &DragMoveEvent<T>,
4590                window: &mut Window,
4591                cx: &mut Context<ProjectPanel>,
4592            ) {
4593                if !e.bounds.contains(&e.event.position) {
4594                    return;
4595                }
4596                this.hover_scroll_task.take();
4597                let panel_height = e.bounds.size.height;
4598                if panel_height <= px(0.) {
4599                    return;
4600                }
4601
4602                let event_offset = e.event.position.y - e.bounds.origin.y;
4603                // How far along in the project panel is our cursor? (0. is the top of a list, 1. is the bottom)
4604                let hovered_region_offset = event_offset / panel_height;
4605
4606                // We want the scrolling to be a bit faster when the cursor is closer to the edge of a list.
4607                // These pixels offsets were picked arbitrarily.
4608                let vertical_scroll_offset = if hovered_region_offset <= 0.05 {
4609                    8.
4610                } else if hovered_region_offset <= 0.15 {
4611                    5.
4612                } else if hovered_region_offset >= 0.95 {
4613                    -8.
4614                } else if hovered_region_offset >= 0.85 {
4615                    -5.
4616                } else {
4617                    return;
4618                };
4619                let adjustment = point(px(0.), px(vertical_scroll_offset));
4620                this.hover_scroll_task = Some(cx.spawn_in(window, async move |this, cx| {
4621                    loop {
4622                        let should_stop_scrolling = this
4623                            .update(cx, |this, cx| {
4624                                this.hover_scroll_task.as_ref()?;
4625                                let handle = this.scroll_handle.0.borrow_mut();
4626                                let offset = handle.base_handle.offset();
4627
4628                                handle.base_handle.set_offset(offset + adjustment);
4629                                cx.notify();
4630                                Some(())
4631                            })
4632                            .ok()
4633                            .flatten()
4634                            .is_some();
4635                        if should_stop_scrolling {
4636                            return;
4637                        }
4638                        cx.background_executor()
4639                            .timer(Duration::from_millis(16))
4640                            .await;
4641                    }
4642                }));
4643            }
4644            h_flex()
4645                .id("project-panel")
4646                .group("project-panel")
4647                .on_drag_move(cx.listener(handle_drag_move_scroll::<ExternalPaths>))
4648                .on_drag_move(cx.listener(handle_drag_move_scroll::<DraggedSelection>))
4649                .size_full()
4650                .relative()
4651                .on_hover(cx.listener(|this, hovered, window, cx| {
4652                    if *hovered {
4653                        this.show_scrollbar = true;
4654                        this.hide_scrollbar_task.take();
4655                        cx.notify();
4656                    } else if !this.focus_handle.contains_focused(window, cx) {
4657                        this.hide_scrollbar(window, cx);
4658                    }
4659                }))
4660                .on_click(cx.listener(|this, _event, _, cx| {
4661                    cx.stop_propagation();
4662                    this.selection = None;
4663                    this.marked_entries.clear();
4664                }))
4665                .key_context(self.dispatch_context(window, cx))
4666                .on_action(cx.listener(Self::select_next))
4667                .on_action(cx.listener(Self::select_previous))
4668                .on_action(cx.listener(Self::select_first))
4669                .on_action(cx.listener(Self::select_last))
4670                .on_action(cx.listener(Self::select_parent))
4671                .on_action(cx.listener(Self::select_next_git_entry))
4672                .on_action(cx.listener(Self::select_prev_git_entry))
4673                .on_action(cx.listener(Self::select_next_diagnostic))
4674                .on_action(cx.listener(Self::select_prev_diagnostic))
4675                .on_action(cx.listener(Self::select_next_directory))
4676                .on_action(cx.listener(Self::select_prev_directory))
4677                .on_action(cx.listener(Self::expand_selected_entry))
4678                .on_action(cx.listener(Self::collapse_selected_entry))
4679                .on_action(cx.listener(Self::collapse_all_entries))
4680                .on_action(cx.listener(Self::open))
4681                .on_action(cx.listener(Self::open_permanent))
4682                .on_action(cx.listener(Self::confirm))
4683                .on_action(cx.listener(Self::cancel))
4684                .on_action(cx.listener(Self::copy_path))
4685                .on_action(cx.listener(Self::copy_relative_path))
4686                .on_action(cx.listener(Self::new_search_in_directory))
4687                .on_action(cx.listener(Self::unfold_directory))
4688                .on_action(cx.listener(Self::fold_directory))
4689                .on_action(cx.listener(Self::remove_from_project))
4690                .when(!project.is_read_only(cx), |el| {
4691                    el.on_action(cx.listener(Self::new_file))
4692                        .on_action(cx.listener(Self::new_directory))
4693                        .on_action(cx.listener(Self::rename))
4694                        .on_action(cx.listener(Self::delete))
4695                        .on_action(cx.listener(Self::trash))
4696                        .on_action(cx.listener(Self::cut))
4697                        .on_action(cx.listener(Self::copy))
4698                        .on_action(cx.listener(Self::paste))
4699                        .on_action(cx.listener(Self::duplicate))
4700                        .on_click(cx.listener(|this, event: &gpui::ClickEvent, window, cx| {
4701                            if event.up.click_count > 1 {
4702                                if let Some(entry_id) = this.last_worktree_root_id {
4703                                    let project = this.project.read(cx);
4704
4705                                    let worktree_id = if let Some(worktree) =
4706                                        project.worktree_for_entry(entry_id, cx)
4707                                    {
4708                                        worktree.read(cx).id()
4709                                    } else {
4710                                        return;
4711                                    };
4712
4713                                    this.selection = Some(SelectedEntry {
4714                                        worktree_id,
4715                                        entry_id,
4716                                    });
4717
4718                                    this.new_file(&NewFile, window, cx);
4719                                }
4720                            }
4721                        }))
4722                })
4723                .when(project.is_local(), |el| {
4724                    el.on_action(cx.listener(Self::reveal_in_finder))
4725                        .on_action(cx.listener(Self::open_system))
4726                        .on_action(cx.listener(Self::open_in_terminal))
4727                })
4728                .when(project.is_via_ssh(), |el| {
4729                    el.on_action(cx.listener(Self::open_in_terminal))
4730                })
4731                .on_mouse_down(
4732                    MouseButton::Right,
4733                    cx.listener(move |this, event: &MouseDownEvent, window, cx| {
4734                        // When deploying the context menu anywhere below the last project entry,
4735                        // act as if the user clicked the root of the last worktree.
4736                        if let Some(entry_id) = this.last_worktree_root_id {
4737                            this.deploy_context_menu(event.position, entry_id, window, cx);
4738                        }
4739                    }),
4740                )
4741                .track_focus(&self.focus_handle(cx))
4742                .child(
4743                    uniform_list(cx.entity().clone(), "entries", item_count, {
4744                        |this, range, window, cx| {
4745                            let mut items = Vec::with_capacity(range.end - range.start);
4746                            this.for_each_visible_entry(
4747                                range,
4748                                window,
4749                                cx,
4750                                |id, details, window, cx| {
4751                                    items.push(this.render_entry(id, details, window, cx));
4752                                },
4753                            );
4754                            items
4755                        }
4756                    })
4757                    .when(show_indent_guides, |list| {
4758                        list.with_decoration(
4759                            ui::indent_guides(
4760                                cx.entity().clone(),
4761                                px(indent_size),
4762                                IndentGuideColors::panel(cx),
4763                                |this, range, window, cx| {
4764                                    let mut items =
4765                                        SmallVec::with_capacity(range.end - range.start);
4766                                    this.iter_visible_entries(
4767                                        range,
4768                                        window,
4769                                        cx,
4770                                        |entry, entries, _, _| {
4771                                            let (depth, _) = Self::calculate_depth_and_difference(
4772                                                entry, entries,
4773                                            );
4774                                            items.push(depth);
4775                                        },
4776                                    );
4777                                    items
4778                                },
4779                            )
4780                            .on_click(cx.listener(
4781                                |this, active_indent_guide: &IndentGuideLayout, window, cx| {
4782                                    if window.modifiers().secondary() {
4783                                        let ix = active_indent_guide.offset.y;
4784                                        let Some((target_entry, worktree)) = maybe!({
4785                                            let (worktree_id, entry) = this.entry_at_index(ix)?;
4786                                            let worktree = this
4787                                                .project
4788                                                .read(cx)
4789                                                .worktree_for_id(worktree_id, cx)?;
4790                                            let target_entry = worktree
4791                                                .read(cx)
4792                                                .entry_for_path(&entry.path.parent()?)?;
4793                                            Some((target_entry, worktree))
4794                                        }) else {
4795                                            return;
4796                                        };
4797
4798                                        this.collapse_entry(target_entry.clone(), worktree, cx);
4799                                    }
4800                                },
4801                            ))
4802                            .with_render_fn(
4803                                cx.entity().clone(),
4804                                move |this, params, _, cx| {
4805                                    const LEFT_OFFSET: Pixels = px(14.);
4806                                    const PADDING_Y: Pixels = px(4.);
4807                                    const HITBOX_OVERDRAW: Pixels = px(3.);
4808
4809                                    let active_indent_guide_index =
4810                                        this.find_active_indent_guide(&params.indent_guides, cx);
4811
4812                                    let indent_size = params.indent_size;
4813                                    let item_height = params.item_height;
4814
4815                                    params
4816                                        .indent_guides
4817                                        .into_iter()
4818                                        .enumerate()
4819                                        .map(|(idx, layout)| {
4820                                            let offset = if layout.continues_offscreen {
4821                                                px(0.)
4822                                            } else {
4823                                                PADDING_Y
4824                                            };
4825                                            let bounds = Bounds::new(
4826                                                point(
4827                                                    layout.offset.x * indent_size + LEFT_OFFSET,
4828                                                    layout.offset.y * item_height + offset,
4829                                                ),
4830                                                size(
4831                                                    px(1.),
4832                                                    layout.length * item_height - offset * 2.,
4833                                                ),
4834                                            );
4835                                            ui::RenderedIndentGuide {
4836                                                bounds,
4837                                                layout,
4838                                                is_active: Some(idx) == active_indent_guide_index,
4839                                                hitbox: Some(Bounds::new(
4840                                                    point(
4841                                                        bounds.origin.x - HITBOX_OVERDRAW,
4842                                                        bounds.origin.y,
4843                                                    ),
4844                                                    size(
4845                                                        bounds.size.width + HITBOX_OVERDRAW * 2.,
4846                                                        bounds.size.height,
4847                                                    ),
4848                                                )),
4849                                            }
4850                                        })
4851                                        .collect()
4852                                },
4853                            ),
4854                        )
4855                    })
4856                    .size_full()
4857                    .with_sizing_behavior(ListSizingBehavior::Infer)
4858                    .with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained)
4859                    .with_width_from_item(self.max_width_item_index)
4860                    .track_scroll(self.scroll_handle.clone()),
4861                )
4862                .children(self.render_vertical_scrollbar(cx))
4863                .when_some(self.render_horizontal_scrollbar(cx), |this, scrollbar| {
4864                    this.pb_4().child(scrollbar)
4865                })
4866                .children(self.context_menu.as_ref().map(|(menu, position, _)| {
4867                    deferred(
4868                        anchored()
4869                            .position(*position)
4870                            .anchor(gpui::Corner::TopLeft)
4871                            .child(menu.clone()),
4872                    )
4873                    .with_priority(1)
4874                }))
4875        } else {
4876            v_flex()
4877                .id("empty-project_panel")
4878                .size_full()
4879                .p_4()
4880                .track_focus(&self.focus_handle(cx))
4881                .child(
4882                    Button::new("open_project", "Open a project")
4883                        .full_width()
4884                        .key_binding(KeyBinding::for_action(&workspace::Open, window, cx))
4885                        .on_click(cx.listener(|this, _, window, cx| {
4886                            this.workspace
4887                                .update(cx, |_, cx| {
4888                                    window.dispatch_action(Box::new(workspace::Open), cx)
4889                                })
4890                                .log_err();
4891                        })),
4892                )
4893                .when(is_local, |div| {
4894                    div.drag_over::<ExternalPaths>(|style, _, _, cx| {
4895                        style.bg(cx.theme().colors().drop_target_background)
4896                    })
4897                    .on_drop(cx.listener(
4898                        move |this, external_paths: &ExternalPaths, window, cx| {
4899                            this.last_external_paths_drag_over_entry = None;
4900                            this.marked_entries.clear();
4901                            this.hover_scroll_task.take();
4902                            if let Some(task) = this
4903                                .workspace
4904                                .update(cx, |workspace, cx| {
4905                                    workspace.open_workspace_for_paths(
4906                                        true,
4907                                        external_paths.paths().to_owned(),
4908                                        window,
4909                                        cx,
4910                                    )
4911                                })
4912                                .log_err()
4913                            {
4914                                task.detach_and_log_err(cx);
4915                            }
4916                            cx.stop_propagation();
4917                        },
4918                    ))
4919                })
4920        }
4921    }
4922}
4923
4924impl Render for DraggedProjectEntryView {
4925    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
4926        let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
4927        h_flex()
4928            .font(ui_font)
4929            .pl(self.click_offset.x + px(12.))
4930            .pt(self.click_offset.y + px(12.))
4931            .child(
4932                div()
4933                    .flex()
4934                    .gap_1()
4935                    .items_center()
4936                    .py_1()
4937                    .px_2()
4938                    .rounded_lg()
4939                    .bg(cx.theme().colors().background)
4940                    .map(|this| {
4941                        if self.selections.len() > 1 && self.selections.contains(&self.selection) {
4942                            this.child(Label::new(format!("{} entries", self.selections.len())))
4943                        } else {
4944                            this.child(if let Some(icon) = &self.details.icon {
4945                                div().child(Icon::from_path(icon.clone()))
4946                            } else {
4947                                div()
4948                            })
4949                            .child(Label::new(self.details.filename.clone()))
4950                        }
4951                    }),
4952            )
4953    }
4954}
4955
4956impl EventEmitter<Event> for ProjectPanel {}
4957
4958impl EventEmitter<PanelEvent> for ProjectPanel {}
4959
4960impl Panel for ProjectPanel {
4961    fn position(&self, _: &Window, cx: &App) -> DockPosition {
4962        match ProjectPanelSettings::get_global(cx).dock {
4963            ProjectPanelDockPosition::Left => DockPosition::Left,
4964            ProjectPanelDockPosition::Right => DockPosition::Right,
4965        }
4966    }
4967
4968    fn position_is_valid(&self, position: DockPosition) -> bool {
4969        matches!(position, DockPosition::Left | DockPosition::Right)
4970    }
4971
4972    fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
4973        settings::update_settings_file::<ProjectPanelSettings>(
4974            self.fs.clone(),
4975            cx,
4976            move |settings, _| {
4977                let dock = match position {
4978                    DockPosition::Left | DockPosition::Bottom => ProjectPanelDockPosition::Left,
4979                    DockPosition::Right => ProjectPanelDockPosition::Right,
4980                };
4981                settings.dock = Some(dock);
4982            },
4983        );
4984    }
4985
4986    fn size(&self, _: &Window, cx: &App) -> Pixels {
4987        self.width
4988            .unwrap_or_else(|| ProjectPanelSettings::get_global(cx).default_width)
4989    }
4990
4991    fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
4992        self.width = size;
4993        cx.notify();
4994        cx.defer_in(window, |this, _, cx| {
4995            this.serialize(cx);
4996        });
4997    }
4998
4999    fn icon(&self, _: &Window, cx: &App) -> Option<IconName> {
5000        ProjectPanelSettings::get_global(cx)
5001            .button
5002            .then_some(IconName::FileTree)
5003    }
5004
5005    fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
5006        Some("Project Panel")
5007    }
5008
5009    fn toggle_action(&self) -> Box<dyn Action> {
5010        Box::new(ToggleFocus)
5011    }
5012
5013    fn persistent_name() -> &'static str {
5014        "Project Panel"
5015    }
5016
5017    fn starts_open(&self, _: &Window, cx: &App) -> bool {
5018        let project = &self.project.read(cx);
5019        project.visible_worktrees(cx).any(|tree| {
5020            tree.read(cx)
5021                .root_entry()
5022                .map_or(false, |entry| entry.is_dir())
5023        })
5024    }
5025
5026    fn activation_priority(&self) -> u32 {
5027        0
5028    }
5029}
5030
5031impl Focusable for ProjectPanel {
5032    fn focus_handle(&self, _cx: &App) -> FocusHandle {
5033        self.focus_handle.clone()
5034    }
5035}
5036
5037impl ClipboardEntry {
5038    fn is_cut(&self) -> bool {
5039        matches!(self, Self::Cut { .. })
5040    }
5041
5042    fn items(&self) -> &BTreeSet<SelectedEntry> {
5043        match self {
5044            ClipboardEntry::Copied(entries) | ClipboardEntry::Cut(entries) => entries,
5045        }
5046    }
5047}
5048
5049#[cfg(test)]
5050mod project_panel_tests;