project_panel.rs

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