project_panel.rs

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