project_panel.rs

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