project_panel.rs

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